发布 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 22.18.0 及更高版本中默认启用。
- Node **不会**剥离
node_modules中的类型,因为这可能对官方 TypeScript 编译器(tsc)和 VS Code 的部分功能造成严重的性能问题,所以 TypeScript 维护者希望阻止人们发布原始的 TypeScript,至少目前是这样。
- Node **不会**剥离
-
在 Node 中使用 TypeScript 特定的功能(如
enum)仍然需要一个标志(--experimental-transform-types)。无论如何,对于这些功能通常有更好的替代方案。- 为确保不包含 TypeScript 特定的功能(这样你的代码就可以直接在 Node 中运行),请在 TypeScript 5.8 及更高版本中设置
erasableSyntaxOnly配置选项。
- 为确保不包含 TypeScript 特定的功能(这样你的代码就可以直接在 Node 中运行),请在 TypeScript 5.8 及更高版本中设置
-
使用 Dependabot 来保持你的依赖项最新,包括 GitHub Actions 中的依赖。这是一个非常容易“设置后即忘”的配置。
-
.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 = 'a';
const bar: number = 1 + ;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 及相关文件)以伴随文件的形式提供类型信息,允许执行代码是原生 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" 字段来创建一个包含列表(如果意外遗漏了一个文件,你的包可能会对下游用户造成破坏,所以这是一个不太安全的选择)。