在本指南中,我们将逐步介绍将一个包发布到 npm 所需的每一步。
这不是一个简略的指南。我们将从一个空目录开始,设置一个完全生产就绪的包。这将包括:
- 使用 Git 进行版本控制
- 使用 TypeScript 编写代码并确保其类型安全
- 使用 Prettier 格式化代码
- 使用 @arethetypeswrong/cli 检查导出内容
- 使用 tsup 将 TypeScript 代码编译为 CJS 和 ESM
- 使用 Vitest 运行测试
- 使用 GitHub Actions 运行 CI 流程
- 使用 Changesets 进行版本控制和发布包
如果你想查看最终产品,可以查看这个演示仓库。
1. Git
在这一部分,我们将创建一个新的 git 仓库,设置 .gitignore 文件,创建初始提交,在 GitHub 上创建一个新的仓库,并将代码推送到 GitHub。
1.1: 初始化仓库
运行以下命令来初始化一个新的 git 仓库:
1 | git init |
1.2: 设置 .gitignore 文件
在项目的根目录中创建一个 .gitignore 文件,并添加以下内容:
1 | node_modules |
1.3: 创建初始提交
运行以下命令来创建初始提交:
1 | git add . |
1.4: 在 GitHub 上创建一个新的仓库
使用 GitHub CLI 运行以下命令来创建一个新的仓库。在此示例中,我选择了名称 tt-package-demo
:
1 | gh repo create tt-package-demo --source=. --public |
1.5: 推送到 GitHub
运行以下命令将代码推送到 GitHub:
1 | git push --set-upstream origin main |
2: package.json
在这一部分,我们将创建一个 package.json 文件,添加 license 字段,创建 LICENSE 文件,并添加 README.md 文件。
2.1: 创建 package.json 文件
创建一个 package.json 文件,并设置如下值:
1 | { |
name
是人们安装你的包时使用的名称,必须在 npm 上唯一。你可以创建免费的组织范围(如@total-typescript/demo
),这有助于使其唯一。version
是包的版本,应遵循语义化版本控制:0.0.1 格式。每次发布新版本时,都应增加此数字。description
和keywords
是你包的简短描述,它们会显示在 npm 注册表的搜索结果中。homepage
是你包主页的 URL,GitHub 仓库是一个很好的默认选项,或者如果你有文档站点,也可以使用。bugs
是人们报告你包问题的 URL。author
是你!你可以选择性地添加你的电子邮件和网站。如果有多个贡献者,你可以将它们指定为一个具有相同格式的贡献者数组。repository
是你包仓库的 URL,这将在 npm 注册表中创建一个指向你的 GitHub 仓库的链接。files
是安装你的包时应包含的文件数组。在这种情况下,我们包括了dist
文件夹。README.md
、package.json
和LICENSE
是默认包含的。type
设置为module
以表明你的包使用的是 ECMAScript 模块,而不是 CommonJS 模块。
2.2: 添加 license 字段
在你的 package.json 中添加一个 license 字段。在这里选择一个许可证。我选择了 MIT。
1 | { |
2.3: 添加 LICENSE 文件
创建一个名为 LICENSE(无扩展名)的文件,内容为你的许可证文本。对于 MIT 许可证,内容如下:
1 | MIT License |
将 [year]
和 [fullname]
占位符替换为当前年份和你的名字。
2.4: 添加 README.md 文件
创建一个 README.md 文件,描述你的包。以下是一个示例:
1 | **tt-package-demo** |
这将在人们查看你的包时显示在 npm 注册表中。
3: TypeScript
在这一部分,我们将安装 TypeScript,设置 tsconfig.json,创建源文件,创建索引文件,设置构建脚本,运行构建,将 dist 添加到 .gitignore,设置 ci 脚本,并为 DOM 配置 tsconfig.json。
3.1: 安装 TypeScript
运行以下命令安装 TypeScript:
1 | npm install --save-dev typescript |
我们添加 --save-dev
来将 TypeScript 作为开发依赖项安装。这意味着它不会在其他人安装你的包时被包含进去。
3.2: 设置 tsconfig.json
创建一个 tsconfig.json
文件,并使用以下配置:
1 | { |
这些选项的详细解释可以在我的 TSConfig 速查表中找到。
3.3: 为 DOM 配置你的 tsconfig.json
如果你的代码在 DOM 中运行(即需要访问 document
、window
或 localStorage
等),跳过此步骤。
如果你的代码不需要访问 DOM API,请在 tsconfig.json
中添加以下内容:
1 | { |
这将防止 DOM 类型在你的代码中可用。
如果不确定,可以跳过此步骤。
3.4: 创建源文件
创建一个 src/utils.ts
文件,并添加以下内容:
1 | export const add = (a: number, b: number) => a + b; |
3.5: 创建索引文件
创建一个 src/index.ts
文件,并添加以下内容:
1 | export { add } from "./utils.js"; |
.js
扩展名可能看起来有些奇怪。这里有一篇文章解释了更多内容。
3.6: 设置构建脚本
在你的 package.json
中添加一个 scripts
对象,并设置如下内容:
1 | { |
这将把你的 TypeScript 代码编译为 JavaScript。
3.7: 运行构建
运行以下命令来编译你的 TypeScript 代码:
1 | npm run build |
这将创建一个 dist
文件夹,里面包含已编译的 JavaScript 代码。
3.8: 将 dist
添加到 .gitignore
将 dist
文件夹添加到你的 .gitignore
文件中:
1 | dist |
这将防止已编译的代码被包含在你的 git 仓库中。
3.9: 设置 CI 脚本
在你的 package.json
中添加一个 ci
脚本,并设置如下内容:
1 | { |
这为我们提供了一个在 CI 中运行所有必要操作的快捷方式。
4: 使用 Prettier
在本节中,我们将安装 Prettier,配置 .prettierrc
文件,设置格式化脚本,运行格式化脚本,设置检查格式脚本,将检查格式脚本添加到 CI 脚本中,并运行 CI 脚本。
Prettier 是一个代码格式化工具,它会自动将你的代码格式化为一致的风格,使代码更易读和维护。
4.1: 安装 Prettier
运行以下命令来安装 Prettier:
1 | npm install --save-dev prettier |
4.2: 配置 .prettierrc
创建一个 .prettierrc
文件,内容如下:
1 | { |
你可以在这个文件中添加更多选项来定制 Prettier 的行为。你可以在这里找到完整的选项列表。
4.3: 设置格式化脚本
在 package.json
中添加一个格式化脚本,内容如下:
1 | { |
这将使用 Prettier 格式化项目中的所有文件。
4.4: 运行格式化脚本
运行以下命令格式化项目中的所有文件:
1 | npm run format |
你可能会注意到一些文件发生了变化。将它们提交:
1 | git add . |
4.5: 设置检查格式脚本
在 package.json
中添加一个检查格式脚本,内容如下:
1 | { |
这将检查项目中的所有文件是否已正确格式化。
4.6: 将检查格式脚本添加到 CI 脚本中
将检查格式脚本添加到 package.json
中的 CI 脚本中:
1 | { |
这将使检查格式脚本成为 CI 过程的一部分。
5: 导出、main 字段和 @arethetypeswrong/cli
在本节中,我们将安装 @arethetypeswrong/cli
,设置检查导出脚本,运行检查导出脚本,设置 main
字段,再次运行检查导出脚本,设置 CI 脚本,并运行 CI 脚本。
@arethetypeswrong/cli
是一个工具,用于检查你的包的导出是否正确。这很重要,因为导出错误可能会导致使用你包的人遇到问题。
5.1: 安装 @arethetypeswrong/cli
运行以下命令安装 @arethetypeswrong/cli
:
1 | npm install --save-dev @arethetypeswrong/cli |
5.2: 设置检查导出脚本
在 package.json
中添加一个检查导出脚本,内容如下:
1 | { |
这将检查你的包的所有导出是否正确。
5.3: 运行检查导出脚本
运行以下命令检查你的包的所有导出是否正确:
1 | npm run check-exports |
你可能会看到各种错误提示:
1 | ┌───────────────────┬──────────────────────┐ |
这表明没有一个 Node 版本或打包器可以使用我们的包。
让我们来修复它。
5.4: 设置 main
字段
在 package.json
中添加 main
字段,内容如下:
1 | { |
这告诉 Node 哪里可以找到包的入口点。
5.5: 再次运行检查导出脚本
运行以下命令再次检查你的包的所有导出是否正确:
1 | npm run check-exports |
你现在应该只会看到一个警告:
1 | ┌───────────────────┬──────────────────────────────┐ |
这表明我们的包与运行 ESM 的系统兼容。使用 CJS 的人(通常在旧系统中)需要使用动态导入来导入它。
5.6: 修复 CJS 警告
如果你不想支持 CJS(我推荐这样做),请将检查导出脚本更改为:
1 | { |
现在,运行 check-exports
将显示一切都是绿色的:
1 | ┌───────────────────┬───────────────────┐ |
如果你更愿意同时发布 CJS 和 ESM,可以跳过这一步。
5.7: 将检查导出脚本添加到 CI 脚本中
将检查导出脚本添加到 package.json
中的 CI 脚本中:
1 | { |
6: 使用 tsup 进行双发布
如果你想同时发布 CJS 和 ESM 代码,可以使用 tsup。这是一个基于 esbuild 构建的工具,能够将 TypeScript 代码编译为这两种格式。
我个人建议跳过这一步,只发布 ES 模块。这样可以显著简化你的设置,并避免双发布带来的一些问题,比如双包隐患(Dual Package Hazard)。
但如果你确实需要这么做,请继续。
6.1: 安装 tsup
运行以下命令安装 tsup:
1 | npm install --save-dev tsup |
6.2: 创建 tsup.config.ts
文件
创建一个名为 tsup.config.ts
的文件,内容如下:
1 | import { defineConfig } from "tsup"; |
entryPoints
是一个数组,指定了你的包的入口点。在这个例子中,入口点是src/index.ts
。format
是输出格式的数组,我们使用cjs
(CommonJS) 和esm
(ECMAScript 模块)。dts
是一个布尔值,告诉 tsup 是否生成声明文件。outDir
是编译后代码的输出目录。clean
告诉 tsup 在构建前清理输出目录。
6.3: 更改构建脚本
将 package.json
中的构建脚本更改为以下内容:
1 | { |
现在我们将使用 tsup 编译代码,而不是 tsc。
6.4: 添加 exports
字段
在 package.json
中添加 exports
字段,内容如下:
1 | { |
exports
字段告诉使用你包的程序如何找到 CJS 和 ESM 版本。在此例中,我们指向 dist/index.js
给使用 import
的用户,指向 dist/index.cjs
给使用 require
的用户。
同时,建议将 ./package.json
添加到 exports
字段中,因为某些工具需要方便地访问你的 package.json
文件。
6.5: 再次运行 check-exports
运行以下命令检查包的所有导出是否正确:
1 | npm run check-exports |
现在,一切都显示为绿色:
1 | ┌───────────────────┬───────────────────┐ |
6.6: 将 TypeScript 用作代码检查工具
我们不再运行 tsc 来编译代码,而 tsup 也不检查代码错误,只是将其转为 JavaScript。
这意味着如果代码中有 TypeScript 错误,CI 脚本不会报错。让我们来修复这个问题。
6.6.1: 在 tsconfig.json
中添加 noEmit
在 tsconfig.json
中添加 noEmit
字段:
1 | { |
6.6.2: 移除 tsconfig.json
中的未使用字段
删除以下字段:
outDir
rootDir
sourceMap
declaration
declarationMap
在新的“代码检查”设置中,这些字段已不再需要。
6.6.3: 将模块模式更改为 Preserve
你可以选择将模块模式更改为 Preserve
:
1 | { |
这意味着你不再需要为导入文件添加 .js
扩展名。现在,index.ts
文件可以这样写:
1 | export * from "./utils"; |
6.6.4: 添加 lint
脚本
在 package.json
中添加 lint
脚本:
1 | { |
这个命令将运行 TypeScript 作为代码检查工具。
6.6.5: 在 CI 脚本中添加 lint
将 lint
脚本添加到 CI 脚本中:
1 | { |
现在,我们的 CI 过程中将检查 TypeScript 错误。
7: 使用 Vitest 进行测试
在本节中,我们将安装 vitest、创建测试、设置测试脚本、运行测试脚本、设置开发脚本,并将测试脚本添加到 CI 脚本中。
vitest 是一个适用于 ESM 和 TypeScript 的现代测试运行器。它类似于 Jest,但功能更强大。
7.1: 安装 vitest
运行以下命令安装 vitest:
1 | npm install --save-dev vitest |
7.2: 创建测试
创建一个 src/utils.test.ts
文件,内容如下:
1 | import { add } from "./utils.js"; |
这是一个简单的测试,用来检查 add
函数是否返回正确的值。
7.3: 设置测试脚本
在 package.json
中添加测试脚本:
1 | { |
vitest run
会一次性运行项目中的所有测试,而不会进行监视。
7.4: 运行测试脚本
运行以下命令来执行测试:
1 | npm run test |
你应该会看到以下输出:
1 | ✓ src/utils.test.ts (1) |
这表示你的测试成功通过了。
7.5: 设置开发脚本
在开发过程中,常见的工作流程是以监视模式运行测试。在 package.json
中添加开发脚本:
1 | { |
这将以监视模式运行你的测试。
7.6: 添加到 CI 脚本中
将测试脚本添加到 CI 脚本中:
1 | { |
8: 使用 GitHub Actions 设置 CI
在本节中,我们将创建一个 GitHub Actions 工作流,确保在每次提交和拉取请求时运行 CI 流程。这是确保包始终处于工作状态的关键步骤。
8.1: 创建工作流
创建一个 .github/workflows/ci.yml
文件,内容如下:
1 | name: CI |
这个文件是 GitHub 用来运行 CI 流程的指令。
name
是工作流的名称。on
指定了工作流的运行时机。在这里,它会在拉取请求和推送到main
分支时运行。concurrency
防止多个实例同时运行,并使用cancel-in-progress
取消任何正在进行的运行。jobs
是一组需要运行的任务。在这里,我们有一个名为ci
的任务。actions/checkout@v4
会将代码从仓库中检出。actions/setup-node@v4
设置 Node.js 和 npm 环境。npm install
安装项目的依赖项。npm run ci
运行项目的 CI 脚本。
如果 CI 过程的任何部分失败,工作流将失败,GitHub 会通过在提交旁边显示红色叉号来通知我们。
8.2: 测试工作流
将更改推送到 GitHub,然后检查仓库的 Actions 选项卡。你应该会看到工作流正在运行。
这将在每次提交和每次拉取请求时为我们提供警告。
9. 使用 Changesets 发布
在本节中,我们将安装 @changesets/cli
,初始化 Changesets,将 changeset 发布设置为公开,将 commit 设置为 true,设置本地发布脚本,添加一个 changeset,提交更改,运行本地发布脚本,最后在 npm 上查看你的包。
Changesets 是一个帮助你进行版本控制和发布包的工具。我强烈推荐任何将包发布到 npm 的人使用它。
9.1: 安装 @changesets/cli
运行以下命令来安装 Changesets:
1 | npm install --save-dev @changesets/cli |
9.2: 初始化 Changesets
运行以下命令来初始化 Changesets:
1 | npx changeset init |
这将会在你的项目中创建一个 .changeset
文件夹,里面包含一个 config.json
文件。这也是你存储 changesets 的地方。
9.3: 将 changeset 发布设置为公开
在 .changeset/config.json
中,将 access
字段设置为 public
:
1 | // .changeset/config.json |
如果不更改此字段,changesets 将不会将你的包发布到 npm。
9.4: 将 commit 设置为 true
在 .changeset/config.json
中,将 commit
字段设置为 true
:
1 | // .changeset/config.json |
这将会在版本控制之后将 changeset 提交到你的仓库中。
9.5: 设置本地发布脚本
在你的 package.json
中添加一个 local-release
脚本,内容如下:
1 | { |
这个脚本将运行你的 CI 过程,然后将你的包发布到 npm。当你想要在本地机器上发布一个新版本的包时,这将是你运行的命令。
9.6: 在 prepublishOnly
中运行 CI
在你的 package.json
中添加一个 prepublishOnly
脚本,内容如下:
1 | { |
这将在发布你的包到 npm 之前自动运行你的 CI 过程。
这对于区分 local-release
脚本非常有用,以防用户意外运行 npm publish
而不是 local-release
。感谢 Jordan Harband 的建议!
9.7: 添加一个 changeset
运行以下命令添加一个 changeset:
1 | npx changeset |
这将打开一个交互式提示,允许你添加一个 changeset。Changesets 是将变更分组并为它们分配版本号的一种方式。
将此版本标记为补丁版本,并为其添加一个类似 “初始发布” 的描述。
这将在 .changeset
文件夹中创建一个新的 changeset 文件。
9.8: 提交你的更改
将你的更改提交到仓库中:
1 | git add . |
9.9: 运行本地发布脚本
运行以下命令发布你的包:
1 | npm run local-release |
这将运行你的 CI 过程,对你的包进行版本控制并将其发布到 npm。
它还将在你的仓库中创建一个 CHANGELOG.md
文件,详细说明此版本的更改。每次发布时,此文件将会更新。
9.10: 在 npm 上查看你的包
访问以下网址:
1 | http://npmjs.com/package/<your package name> |
你应该可以在上面看到你的包!恭喜!你已经成功发布到 npm!
总结
现在你已经完全设置好你的包。你已经完成了以下工作:
- 使用最新设置创建了一个 TypeScript 项目
- 配置了 Prettier,格式化代码并检查代码是否格式化正确
- 使用
@arethetypeswrong/cli
检查包的导出是否正确 - 使用 tsup 将 TypeScript 代码编译为 JavaScript
- 使用 vitest 运行测试
- 设置了 GitHub Actions 运行 CI 过程
- 使用 Changesets 进行版本控制并发布包
原文:https://www.totaltypescript.com/how-to-create-an-npm-package