发布 TypeScript 包
🌐 Publishing a TypeScript package
本文涵盖了关于 TypeScript 发布的相关内容。发布是指通过 npm(或其他包管理器)作为包进行分发;这不是关于将应用/服务器编译以在生产环境中运行(例如 PWA 和/或终端服务器)的内容。
🌐 This article covers items regarding TypeScript publishing specifically. Publishing means distributed as a package via npm (or other package manager); this is not about compiling an app / server to be run in production (such as a PWA and/or endpoint server).
需要注意的一些重要事项:
🌐 Some important things to note:
- 这里适用发布软件包中的所有内容。
- 像
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 版本。
存储库的目录概览如下所示:
🌐 A directory overview of a repository would look something like:
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
其已发布包的目录概览如下所示:
🌐 And a directory overview of its published package would look something like:
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' 完全分离”),可以采用镜像结构或“杂物抽屉”的方式。
🌐 A note about directory organisation: There are a few common practices for placing tests. Principle of least knowledge says to co-locate them (put them adjacent to implementation). Sometimes, that's in the same directory, or within a drawer like a __test__ (also adjacent to the implementation, "Files co-located but segregated"). Alternatively, some opt to create a test/ sibling to src/ ("'src' and 'test' fully segregated"), either with a mirrored structure or a "junk drawer".
如何处理你的类型
🌐 What to do with your types
把类型当作测试
🌐 Treat types like a test
类型的目的是警告实现不起作用:
🌐 The purpose of types is to warn an implementation will not work:
const = 'a';
const bar: number = 1 + ;TypeScript 已经警告说,上面的代码不会按预期运行,就像单元测试会警告代码未按预期运行一样。它们是互补的,验证不同的内容 - 你应该两者都使用。
🌐 TypeScript has warned that the above code will not behave as intended, just like a unit test warns that code does not behave as intended. They are complementary and verify different things—you should have both.
你的编辑器(例如 VS Code)很可能内置对 TypeScript 的支持,在你编码时会显示错误。如果没有,或者你遗漏了这些错误,CI 会帮你检查。
🌐 Your editor (e.g. VS Code) likely has built-in support for TypeScript, displaying errors as you work. If not, and/or you missed those, CI will have your back.
下面的 GitHub Action 设置了一个 CI 任务,用于自动检查(并要求)类型在合并到 main 分支的 PR 中通过检查。
🌐 The following GitHub Action sets up a CI task to automatically check (and require) types pass inspection for a PR into the main branch.
# 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(这就是为什么它们在上面的示例中被排除的原因)。
🌐 Note that test files may well have a different tsconfig.json applied (hence why they are excluded in the above sample).
生成类型声明
🌐 Generate type declarations
类型声明(.d.ts 等)作为附属文件提供类型信息,使得执行代码可以是原生 JavaScript,同时仍然拥有类型。
🌐 Type declarations (.d.ts and friends) provide type information as a sidecar file, allowing the execution code to be vanilla JavaScript whilst still having types.
由于这些是基于源代码生成的,因此可以将它们作为发布过程的一部分进行构建,而不需要签入存储库。
🌐 Since these are generated based on source code, they can be built as part of your publication process and do not need to be checked into your repository.
以下示例为类型声明生成于发布到 npm 注册表之前。
🌐 Take the following example, where the type declarations are generated just before publishing to the npm registry.
# 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 及更高版本。
🌐 You'll want to publish a package compiled to support all Node.js LTS versions since you don't know which version the consumer will be running; the tsconfigs in this article support node 18.x and later.
npm publish 会自动在发布前运行 prepack。npm 也会在运行 npm pack --dry-run 之前自动运行 prepack(这样你可以轻松查看发布的包的内容,而无需实际发布)。注意,node --run 不会这样做。你不能在这一步使用 node --run,所以这里不适用这个警告,但在其他步骤中可能会适用。
实际发布到 npm 的步骤将包含在单独的文章中(有几个优点和缺点超出了本文的范围)。
🌐 The steps to actually publish to npm will be included in a separate article (there are several pros and cons beyond the scope of this article).
分解这个内容
🌐 Breaking this down
生成类型声明是确定性的:相同的输入每次都会得到相同的输出。因此没有必要将这些提交到 git。
🌐 Generating type declarations is deterministic: you'll get the same output from the same input, every time. So there is no need to commit these to git.
npm publish 会在执行命令时获取所有适用且可用的内容;因此,在此之前立即生成类型声明意味着这些声明是可用的,并且会被正确采纳。
默认情况下,npm publish 会抓取(几乎)所有内容(参见 包中包含的文件)。为了保持发布包的最小化(参见关于 node_modules 的“宇宙中最重的对象”梗),你需要从打包中排除某些文件(例如测试和测试夹具)。将这些文件添加到 .npmignore 中指定的排除列表中;确保列出 !*.d.ts 异常,否则生成的类型声明将无法发布!或者,你也可以使用 package.json 的 "files" 字段来创建一个选项加入列表(如果不小心遗漏了某个文件,可能会导致下游用户的包出现问题,因此这是一个不太安全的选项)。
🌐 By default, npm publish grabs (almost) everything (see Files included in package). In order to keep your published package minimal (see the "Heaviest Objects in the Universe" meme about node_modules), you want to exclude certain files (like tests and test fixtures) from packaging. Add these to the opt-out list specified in .npmignore; ensure the !*.d.ts exception is listed, or the generated type declartions will not be published! Alternatively, you can use package.json "files" to create an opt-in (if a mistake is made accidentally omitting a file, your package may be broken for downstream users, so this is a less safe option).