
dnt — самый простой способ опубликовать гибридный модуль npm для ESM и CommonJS.
(Первоначально опубликовано на deno.com/blog.)
Хотя браузеры и JavaScript прошли долгий путь, написание и публикация модулей JavaScript по-прежнему болезненны. Для максимального внедрения ваш модуль должен поддерживать CommonJS и ESM, JavaScript с объявлениями TypeScript и работать в Deno, Node.js и веб-браузерах. Для этого многие прибегают к сложным конвейерам выпуска или сопровождению двух копий кода с немного отличающимся синтаксисом модулей.
Что, если бы вы могли написать свой модуль один раз с помощью современных инструментов, таких как TypeScript, и преобразовать его для поддержки всех вариантов использования?
dnt — Преобразование Deno в Node
dnt — это инструмент сборки, который преобразует модули Deno в пакеты, совместимые с Node.js/npm. Мало того, трансформированный пакет:
- поддерживает как CommonJS, так и ESM,
- может работать в Node.js, Deno, браузерах,
- запускает тесты как в CommonJS, так и в ESM,
- поддерживает TypeScript и JavaScript
Как это работает? На высоком уровне:
- Преобразует код Deno в код TypeScript, совместимый с Node.js.
- Переписывает спецификаторы расширенных модулей Deno на совместимые с разрешением модуля Node.js.
- Внедряет прокладки для любых обнаруженных API-интерфейсов пространств имен Deno, а также других глобальных переменных, которые можно настроить.
- Переписывает удаленный импорт из Skypack или esm.sh как импорт без спецификаторов и добавляет их в package.json в качестве зависимостей.
- Другие удаленные импорты загружаются и включаются в пакет.
- Type проверяет преобразованный код TypeScript с помощью tsc.
- Записывает пакет в виде набора файлов объявления типов ESM, CommonJS и TypeScript вместе с package.json.
- Запускает окончательный вывод в Node.js через средство запуска тестов, которое поддерживает
Deno.test()API.
Вы можете разрабатывать и тестировать весь свой код в Deno и TypeScript. Когда придет время публикации, вы можете использовать dnt для экспорта в формат, совместимый с Node.js/npm.
Давайте рассмотрим пример с моим модулем is-42. (Вы также можете просмотреть окончательный исходный код здесь.)
Пишите, трансформируйте, публикуйте
Мы создали простой и вполне реальный модуль, который проверяет, является ли переменная числом 42. Основная логика будет в mod.ts:
// mod.ts
export function is42(num: number): boolean {
return num === 42;
}
Мы напишем несколько тестов в mod_test.ts:
// mod_test.ts
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import { is42 } from "./mod.ts";
Deno.test("42 should return true", () => {
assertEquals(true, is42(42));
});
Deno.test("1 should return false", () => {
assertEquals(false, is42(1));
});
Мы можем запускать тесты без дополнительной настройки с помощью deno test:
$ deno test Check file:///Users/andyjiang/Developer/deno/is-42/mod_test.ts running 2 tests from ./mod_test.ts 42 should return true ... ok (13ms) 1 should return false ... ok (7ms) ok | 2 passed | 0 failed (142ms)
Наконец, давайте также добавим файлы LICENSE и README.md в корень каталога, потому что это настоящий модуль:

Вот и все!
Давайте преобразуем это в пакет npm, создав скрипт сборки build_npm.ts:
import { build, emptyDir } from "https://deno.land/x/[email protected]/mod.ts";
await emptyDir("./npm");
await build({
entryPoints: ["./mod.ts"],
outDir: "./npm",
shims: {
deno: true,
},
package: {
name: "is-42",
version: Deno.args[0],
description:
"Boolean function that returns whether or not parameter is the number 42",
license: "MIT",
repository: {
type: "git",
url: "git+https://github.com/lambtron/is-42.git",
},
bugs: {
url: "https://github.com/lambtron/is-42/issues",
},
},
postBuild() {
Deno.copyFileSync("LICENSE", "npm/LICENSE");
Deno.copyFileSync("README.md", "npm/README.md");
},
});
Этот скрипт создает новую папку npm в качестве выходного каталога, куда он выводит весь пакет npm из вашего модуля.
В опциях build() мы устанавливаем входной файл, выходной каталог, прокладки и весь контекст, необходимый для построения файла package.json.
В функцию postBuild() мы включаем операции файловой системы для копирования наших файлов LICENSE и README.md соответственно.
Запустим скрипт build_npm.ts с версией в качестве параметра:
$ deno run -A build_npm.ts 0.0.1 [dnt] Transforming... [dnt] Running npm install... added 6 packages, and audited 7 packages in 2s found 0 vulnerabilities [dnt] Building project... [dnt] Type checking ESM... [dnt] Emitting ESM package... [dnt] Emitting script package... [dnt] Running post build action... [dnt] Running tests... > test > node test_runner.js Running tests in ./script/mod_test.js... test 42 should return true ... ok test 1 should return false ... ok Running tests in ./esm/mod_test.js... test 42 should return true ... ok test 1 should return false ... ok [dnt] Complete!
Если вы следуете этому примеру, в вашем каталоге должен быть новый подкаталог npm, в котором находится ваш преобразованный пакет npm (поддерживающий CJS и ESM), а также тесты в обоих форматах.

Тесты создаются не только для CJS и ESM, они также выполняются с использованием как Deno, так и Node, поэтому вы можете быть уверены, что ваш код работает в обеих средах выполнения.
Теперь опубликовать совместимый с CommonJS/ESM пакет npm так же просто, как:
$ npm publish /npm
Проверьте опубликованный пакет на npm.
С dnt преобразованием вашего модуля для поддержки модулей CommonJS и ES обслуживание вашего модуля станет проще, так как ваша кодовая база меньше.
Автоматизируйте с помощью GitHub Actions
Чтобы упростить публикацию каждый раз, когда мы отмечаем выпуск, мы можем использовать GitHub Actions с dnt. Обратите внимание, что ниже приведена чрезвычайно упрощенная версия, но она должна помочь вам начать работу в правильном направлении.
Создайте каталог и файл .github/workflows/action.yml, которые будут выполнять следующие шаги каждый раз, когда новый выпуск помечается и публикуется:
- проверить репо
- разобрать релизную версию
- настройка Дено
- запустить скрипт
build_npm.tsс номером версии выпуска - настроить Node и npm с токеном авторизации npm
- опубликовать с
npm publish npm/
name: Publish to registry
on:
release:
types: [published]
jobs:
publish_to_npm:
name: Publish to npm
runs-on: ubuntu-latest
steps:
- name: Checkout is-42
uses: actions/checkout@v3
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Build npm package
run: deno run -A build_npm.ts $RELEASE_VERSION
- name: Setup Node/npm
uses: actions/setup-node@v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
scope: '@lambtron'
- name: Publish to npm
run: npm publish npm/ --access=public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
Обратите внимание, что вам нужно будет создать Классический токен (введите Automation) на сайте npmjs.com и сохранить его как секрет GitHub Actions как NPM_AUTH_TOKEN.
Теперь каждый раз, когда вы публикуете новый выпуск с тегами, он будет запускать это действие и публиковать ваш модуль в npm.
Чтобы узнать больше об использовании GitHub Actions с dnt, ознакомьтесь с документацией.
Что дальше?
Написание программного обеспечения должно быть продуктивным, простым и увлекательным. Он также не должен включать в себя управление сложными конвейерами сборки или сложными базами кода для поддержки самой широкой базы пользователей.
И хотя мы верим, что ESM — это будущее, мы понимаем, что многие модули npm все еще используют CommonJS. К сожалению, авторы модулей вынуждены поддерживать как CommonJS, так и ESM. Поэтому нам нравятся абстракции, упрощающие создание и публикацию программного обеспечения, такие как dnt.