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.

Вы используете dnt? Дайте нам знать в Twitter или Discord.