跳至主要内容
Deno 2 终于来了 🎉️
了解更多
Publish a module to npm that supports ESM and CJS with dnt.

dnt — 最简单的发布支持 ESM 和 CommonJS 的混合 npm 模块的方式

虽然浏览器和 JavaScript 已经取得了长足的进步,但编写和发布 JavaScript 模块仍然很痛苦。为了最大限度地提高采用率,您的模块应该支持 CommonJS 和 ESM、带有 TypeScript 声明的 JavaScript,并在 Deno、Node.js 和 Web 浏览器中运行。为了实现这一点,许多人求助于复杂的发布管道维护两份具有略微不同模块语法的代码

如果您可以只写一次模块,使用像 TypeScript 这样的现代工具并将其转换为支持所有用例,那会怎么样呢?

dnt — Deno 到 Node 转换

dnt 是一款构建工具,可将 Deno 模块转换为 Node.js/npm 兼容的包。不仅如此,转换后的包

  • 支持 CommonJS 和 ESM,
  • 可以在 Node.js、Deno、浏览器中运行,
  • 在 CommonJS 和 ESM 中运行测试,
  • 支持 TypeScript 和 JavaScript。

它是如何工作的?概览

  • 将 Deno 代码转换为 Node.js 兼容的 TypeScript 代码
    • 将 Deno 的扩展模块标识符改写为 Node.js 模块解析兼容的标识符。
    • 为检测到的任何 Deno 命名空间 API 注入 shims,以及可以配置的其他全局变量。
    • 将来自 Skypack 或 esm.sh 的远程导入改写为裸规范导入,并将它们添加到 package.json 中作为依赖项。
    • 其他远程导入将被下载并包含在包中。
  • 使用 tsc 对转换后的 TypeScript 代码进行类型检查。
  • 将包写入一组 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)

最后,让我们还在目录根目录中添加 LICENSEREADME.md 文件,因为它是一个真正的模块

a screenshot of the directory

就是这样!

让我们通过创建构建脚本 build_npm.ts 将其转换为 npm 包

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() 选项中,我们设置了入口文件、输出目录、shims 以及构建 package.json 文件所需的所有上下文。

postBuild() 函数中,我们包括文件系统操作来相应地复制 LICENSEREADME.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),以及两种格式的测试。

screenshot of our new npm subdirectory

不仅为 CJS 和 ESM 生成了测试,它们还使用 Deno 和 Node 运行,因此您可以确信您的代码在这两种运行时中都能运行。

现在,发布您的 CommonJS/ESM 兼容的 npm 包就像这样简单

$ npm publish /npm

查看 npm 上发布的包.

通过 dnt 将您的模块转换为支持 CommonJS 和 ES 模块,维护您的模块变得更加容易,因为您的代码库更小。

使用 GitHub Actions 自动化

为了更方便地发布每次发布的标记,我们可以使用GitHub Actionsdnt。请注意,以下是一个非常简化的版本,但应该可以帮助您入门。

创建一个 .github/workflows/action.yml 目录和文件,该文件将在每次发布新版本时执行以下步骤

  • 检出仓库
  • 解析发布版本
  • 设置 Deno
  • 使用发布版本号运行 build_npm.ts 脚本
  • 使用npm 身份验证令牌 设置 Node 和 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 }}

请注意,您需要从 npmjs.com 创建一个经典令牌(类型为自动化 并将其保存为GitHub Actions 密钥,名为 NPM_AUTH_TOKEN

现在,每次发布新标记的版本时,它都会触发此操作并将您的模块发布到 npm。

有关将 GitHub Actions 与 dnt 结合使用的更多详细信息,请查看文档

下一步是什么?

编写软件应该是有成效、简单和有趣的。它不应包括管理复杂的构建管道或错综复杂的代码库以支持最广泛的用户群。

虽然我们认为 ESM 是未来,但我们认识到许多 npm 模块仍然使用 CommonJS。模块作者不幸地承担了需要同时支持 CommonJS 和 ESM 的重担。因此,我们喜欢让创建和发布软件变得更简单的抽象,例如 dnt

您是否正在使用 dnt?请在TwitterDiscord 上告诉我们。