发布 TypeScript 包

本文专门介绍有关 TypeScript 发布的内容。 发布是指通过 npm(或其他包管理器)分发为软件包; 这不是关于编译应用程序/服务器以在生产环境中运行(例如 PWA 和/或端点服务器)。

一些需要注意的重要事项

  • 发布软件包中的所有内容都适用于此处。

    • main 这样的字段在*发布的*内容上运行,因此当 TypeScript 源代码被转译为 JavaScript 时,JavaScript 是发布的内容,并且 main 将指向带有 JavaScript 文件扩展名的 JavaScript 文件(例如 main.ts"main": "main.js")。

    • scripts.test 这样的字段在源代码上运行,因此它们将使用源代码的文件扩展名(例如 "test": "node --test './src/**/*.test.ts')。

  • Node 通过称为“类型剥离”的过程运行 TypeScript 代码,其中 node(通过 Amaro)删除特定于 TypeScript 的语法,留下纯 JavaScript(node 已经理解)。 从 node 版本 23.6.0 开始,默认启用此行为。

    • Node剥离 node_modules 中的类型,因为它可能导致官方 TypeScript 编译器 (tsc) 和 VS Code 的部分出现严重的性能问题,因此 TypeScript 维护者希望阻止人们发布原始 TypeScript,至少目前是这样。
  • 在 node 中使用特定于 TypeScript 的功能(例如 enum)仍然需要一个标志(--experimental-transform-types)。无论如何,通常有更好的替代方法。

    • 为了确保不存在特定于 TypeScript 的功能(以便您的代码可以在 node 中直接运行),请在 TypeScript 版本 5.8+ 中设置 erasableSyntaxOnly 配置选项。
  • 使用 dependabot 使您的依赖项保持最新,包括 github 操作中的依赖项。 这是一个非常容易设置和忘记的配置。

  • .nvmrc 来自 nvm,这是一个用于 node 的多版本管理器。 它允许您指定项目通常应使用的 node 版本。

存储库的目录概述可能如下所示

example-ts-pkg/
├ .github/
│ ├ workflows/
│ │ ├ ci.yml
│ │ └ publish.yml
│ └ dependabot.yml
├ src/
│ ├ foo.fixture.js
│ ├ main.ts
│ ├ main.test.ts
│ ├ some-util.ts
│ └ some-util.test.ts
├ LICENSE
├ package.json
├ README.md
└ tsconfig.json

其发布的软件包的目录概述可能如下所示

example-ts-pkg/
├ LICENSE
├ main.d.ts
├ main.d.ts.map
├ main.js
├ package.json
├ README.md
├ some-util.d.ts
├ some-util.d.ts.map
└ some-util.js

关于目录组织的说明:放置测试有一些常见的做法。 最小知识原则说要将它们并置(将它们放在实现的旁边)。 有时,这在同一目录中,或者在抽屉中(如 __test__)(也与实现相邻,“文件并置但分离”)。 或者,有些人选择创建一个与 src/ 同级的 test/(“'src' 和 'test' 完全分离”),可以使用镜像结构或“杂物抽屉”。

如何处理您的类型

像对待测试一样对待类型

类型的目的是警告实现将不起作用

const foo = 'a';
const bar: number = 1 + foo;
//    ^^^ Type 'string' is not assignable to type 'number'.

TypeScript 已警告说,以上代码的行为将不如预期,就像单元测试警告代码的行为不如预期一样。 它们是互补的,并验证不同的内容——您应该两者都具备。

您的编辑器(例如 VS Code)可能具有对 TypeScript 的内置支持,可以在您工作时显示错误。 如果没有,和/或您错过了这些错误,CI 将为您提供支持。

以下 GitHub Action 设置了一个 CI 任务,以自动检查(并要求)类型通过对 main 分支的 PR 的检查。

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: Tests

on:
  pull_request:
    branches: ['*']

jobs:
  check-types:
    # Separate these from tests because
    # they are platform and node-version independent
    # and need be run only once.

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'
      - name: npm clean install
        run: npm ci
      # You may want to run a lint check here too
      - run: node --run types:check

  get-matrix:
    # Automatically pick active LTS versions
    runs-on: ubuntu-latest
    outputs:
      latest: ${{ steps.set-matrix.outputs.requireds }}
    steps:
      - uses: ljharb/actions/node/matrix@main
        id: set-matrix
        with:
          versionsAsRoot: true
          type: majors
          preset: '>= 22' # glob is not backported below 22.x

  test:
    needs: [get-matrix]
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        node-version: ${{ fromJson(needs.get-matrix.outputs.latest) }}
        os:
          - macos-latest
          - ubuntu-latest
          - windows-latest

    steps:
      - uses: actions/checkout@v4
      - name: Use node ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - name: npm clean install
        run: npm ci
      - run: node --run test

请注意,测试文件可能应用了不同的 tsconfig.json(因此它们在上述示例中被排除在外)。

生成类型声明

类型声明(.d.ts 及其朋友)以 sidecar 文件的形式提供类型信息,允许执行代码为纯 JavaScript,同时仍然具有类型。

由于这些是根据源代码生成的,因此可以将它们构建为发布过程的一部分,而无需将它们检入到您的存储库中。

以下面的示例为例,其中类型声明在发布到 npm 注册表之前生成。

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

# This is mostly boilerplate.

name: Publish to npm
on:
  push:
    tags:
      - '**@*'

jobs:
  build:
    runs-on: ubuntu-latest

    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci

      # - name: Publish to npm
      #   run: … npm publish …

您将需要发布一个编译后的软件包,以支持所有 Node.js LTS 版本,因为您不知道消费者将运行哪个版本; 本文中的 tsconfig 支持 node 18.x 及更高版本。

npm publish 将自动运行 prepack 在之前npm 还将在 npm pack --dry-run 之前自动运行 prepack(因此您可以轻松查看您发布的软件包将是什么样子,而无需实际发布它)。注意node --run这样做。您不能将 node --run 用于此步骤,因此该警告不适用于此处,但它可能适用于其他步骤。

实际发布到 npm 的步骤将包含在另一篇文章中(除了本文的范围之外,还有一些优点和缺点)。

分解一下

生成类型声明是确定性的:您每次都会从相同的输入获得相同的输出。 因此,无需将这些提交到 git。

npm publish 获取命令运行时适用的所有内容; 因此,立即生成类型声明意味着这些类型声明可用并将被提取。

默认情况下,npm publish 获取(几乎)所有内容(请参阅 软件包中包含的文件)。为了保持您发布的软件包尽可能小(请参阅有关 node_modules 的“宇宙中最重的对象”模因),您需要从打包中排除某些文件(如测试和测试固定装置)。 将这些添加到 .npmignore 中指定的选择退出列表; 确保已列出 !*.d.ts 异常,否则生成的类型声明将不会发布! 或者,您可以使用 package.json "files" 来创建选择加入(如果意外遗漏文件,您的软件包可能会对下游用户造成损坏,因此这不是一个安全的选项)。