跳到主要内容
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 以及其他可配置的全局变量注入 shim。
    • 将来自 Skypack 或 esm.sh 的远程导入重写为裸标识符导入,并将它们添加到 package.json 中作为依赖项。
    • 其他远程导入将被下载并包含在包中。
  • 使用 tsc 对转换后的 TypeScript 代码进行类型检查。
  • 将包输出为一组 ESM、CommonJS 和 TypeScript 类型声明文件以及一个 package.json 文件。
  • 通过支持 Deno.test() API 的测试运行器在 Node.js 中运行最终输出。

你可以使用 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() 选项中,我们设置了入口文件、输出目录、shim 以及构建 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 创建一个经典令牌(类型为 Automation,并将其作为名为 NPM_AUTH_TOKENGitHub Actions 密钥保存。

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

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

接下来是什么?

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

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

你正在使用 dnt 吗? 请在 TwitterDiscord 上告诉我们。