在本指南中,我们将经历发布一个包到npm所需的每一个步骤。
这不是一个极简指南。我们将从一个空目录开始设置一个完全生产就绪的包。这将包括:
- Git 用于版本控制
- TypeScript 用于编写我们的代码并保持类型安全
- Prettier 用于格式化我们的代码
- @arethetypeswrong/cli 用于检查我们的导出
- tsup 用于将我们的TypeScript代码编译成CJS和ESM
- Vitest 用于运行我们的测试
- GitHub Actions 用于运行我们的CI流程
- Changesets 用于版本控制和发布我们的包
如果你想看到成品,请查看这个演示仓库。
1. Git
在本节中,我们将创建一个新的git仓库,设置一个.gitignore
,创建初始提交,创建GitHub上的新仓库,并将我们的代码推送到GitHub。
1.1:初始化仓库
运行以下命令以初始化一个新的git仓库:
git init
1.2:设置.gitignore
在项目的根目录创建一个.gitignore
文件,并添加以下内容:
node_modules
1.3:创建初始提交
运行以下命令以创建初始提交:
git add .
git commit -m "Initial commit"
1.4:在GitHub上创建新仓库
使用GitHub CLI,运行以下命令创建一个新的仓库。在这个例子中,我选择了tt-package-demo
这个名字:
gh repo create tt-package-demo --source=. --public
1.5:推送到GitHub
运行以下命令将你的代码推送到GitHub:
git push --set-upstream origin main
2: package.json
在本节中,我们将创建一个package.json
文件,添加一个license
字段,创建一个LICENSE
文件,并添加一个README.md
文件。
2.1:创建一个package.json
文件
创建一个包含这些值的package.json
文件:
{
"name": "tt-package-demo",
"version": "1.0.0",
"description": "A demo package for Total TypeScript",
"keywords": ["demo", "typescript"],
"homepage": "https://github.com/mattpocock/tt-package-demo",
"bugs": {
"url": "https://github.com/mattpocock/tt-package-demo/issues"
},
"author": "Matt Pocock <team@totaltypescript.com> (https://totaltypescript.com)",
"repository": {
"type": "git",
"url": "git+https://github.com/mattpocock/tt-package-demo.git"
},
"files": ["dist"],
"type": "module"
}
name
是人们将用来安装你的包的名称。它在npm上必须是唯一的。你可以免费创建组织作用域(例如@total-typescript/demo
),这可以帮助它变得独一无二。version
是你的包的版本。它应该遵循语义化版本控制:0.0.1
格式。每次发布新版本时,你都应该增加这个数字。description
和keywords
是你的包的简短描述。它们在npm注册表的搜索中列出。homepage
是你的包主页的URL。GitHub仓库是一个很好的默认选项,或者如果你有一个文档站点的话。bugs
是人们可以报告你的包问题的地方的URL。author
是你!你可以选择性地添加你的电子邮件和网站。如果你有多个贡献者,你可以使用相同的格式将它们指定为contributors
数组。repository
是你的包的仓库的URL。这在npm注册表上创建了一个指向你的GitHub仓库的链接。files
是当人们安装你的包时应包括的文件数组。在这种情况下,我们包括了dist
文件夹。README.md
,package.json
和LICENSE
默认被包括在内。type
设置为module
表示你的包使用ECMAScript模块,而不是CommonJS模块。
2.2:添加license
字段
在package.json
中添加一个license
字段。在这里选择一个许可证。我选择了MIT。
{
"license": "MIT"
}
2.3:添加一个LICENSE
文件
创建一个名为LICENSE
(无扩展名)的文件,包含你的许可证文本。
2.4:添加一个README.md
文件
创建一个README.md
文件,对你的包进行描述。这里有一个例子:
**tt-package-demo**
Total TypeScript的演示包。
当人们在npm注册表上查看你的包时,这将被显示。
3: TypeScript
在本节中,我们将安装TypeScript,设置一个tsconfig.json
,创建一个源文件,创建一个索引文件,设置一个build
脚本,运行我们的构建,将dist
添加到.gitignore
,设置一个ci
脚本,以及为我们的tsconfig.json
配置DOM。
3.1:安装TypeScript
运行以下命令安装TypeScript:
npm install --save-dev typescript
我们添加--save-dev
将TypeScript作为开发依赖项安装。这意味着当人们安装你的包时,它不会被包含在内。
3.2:设置一个tsconfig.json
创建一个包含以下值的tsconfig.json
:
{
"compilerOptions": {
/* 基础选项:*/
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
/* 严格性 */
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
/* 如果使用TypeScript进行转译:*/
"module": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
/* 并且如果你正在为库构建:*/
"declaration": true,
/* 并且如果你正在为monorepo中的库构建:*/
"declarationMap": true
}
}
这些选项在我的TSConfig备忘单中有详细解释。
3.3:为你的tsconfig.json
配置DOM
如果你的代码在DOM中运行(即需要访问document
、window
或localStorage
等),则跳过这一步。
如果你的代码不需要访问DOM API,将以下内容添加到你的tsconfig.json
:
{
"compilerOptions": {
// ...其他选项
"lib": ["es2022"]
}
}
这将防止DOM类型声明在你的代码中可用。
如果你不确定,跳过这一步。
3.4:创建一个源文件
创建一个包含以下内容的src/utils.ts
文件:
export const add = (a: number, b: number) => a + b;
3.5:创建一个索引文件
创建一个包含以下内容的src/index.ts
文件:
export { add } from "./utils.js";
.js
扩展名看起来可能有些奇怪。这篇文章解释了更多。
3.6:设置一个build
脚本
将以下内容添加到你的package.json
的scripts
对象中:
{
"scripts": {
"build": "tsc"
}
这将编译你的TypeScript代码为JavaScript。
3.7:运行你的构建
运行以下命令编译你的TypeScript代码:
npm run build
这将在dist
文件夹中创建你编译后的JavaScript代码。
3.8:将dist
添加到.gitignore
将dist
文件夹添加到你的.gitignore
文件中:
dist
这将防止你编译后的代码被包含在你的git仓库中。
3.9:设置一个ci
脚本
将以下内容添加到你的package.json
的ci
脚本中:
{
"scripts": {
"ci": "npm run build"
}
}
这为我们在CI上运行所有必需的操作提供了一个快捷方式。
4: Prettier
在本节中,我们将安装Prettier,设置一个.prettierrc
,设置一个format
脚本,运行format
脚本,设置一个check-format
脚本,将check-format
脚本添加到我们的CI
脚本中,并运行CI
脚本。
Prettier是一个代码格式化工具,它会自动将你的代码格式化为一致的风格。这使你的代码更容易阅读和维护。
4.1:安装Prettier
运行以下命令安装Prettier:
npm install --save-dev prettier
4.2:设置一个.prettierrc
创建一个包含以下内容的.prettierrc
文件:
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
你可以向此文件添加更多选项以自定义Prettier的行为。你可以在这里找到完整的选项列表。
4.3:设置一个format
脚本
将以下内容添加到你的package.json
的format
脚本中:
{
"scripts": {
"format": "prettier --write ."
}
}
这将使用Prettier格式化你项目中的所有文件。
4.4:运行format
脚本
运行以下命令格式化你项目中的所有文件:
npm run format
你可能会发现一些文件发生了变化。提交它们:
git add .
git commit -m "使用Prettier格式化代码"
4.5:设置一个check-format
脚本
将以下内容添加到你的package.json
的check-format
脚本中:
{
"scripts": {
"check-format": "prettier --check ."
}
}
这将检查你项目中的所有文件是否已正确格式化。
4.6:添加到我们的CI
脚本
将check-format
脚本添加到你的package.json
中的ci
脚本:
{
"scripts": {
"ci": "npm run build && npm run check-format"
}
}
这将作为你CI流程的一部分运行check-format
脚本。
5: exports
, main
和 @arethetypeswrong/cli
在本节中,我们将安装@arethetypeswrong/cli
,设置一个check-exports
脚本,运行check-exports
脚本,设置一个main
字段,再次运行check-exports
脚本,设置一个ci
脚本,并运行ci
脚本。
@arethetypeswrong/cli
是一个检查你的包导出是否正确的工具。这很重要,因为这些地方容易出错,可能会给使用你的包的人带来问题。
5.1:安装@arethetypeswrong/cli
运行以下命令安装@arethetypeswrong/cli
:
npm install --save-dev @arethetypeswrong/cli
5.2:设置一个check-exports
脚本
将以下内容添加到你的package.json
的check-exports
脚本中:
{
"scripts": {
"check-exports": "attw --pack ."
}
}
这将检查你的包的所有导出是否正确。
5.3:运行check-exports
脚本
运行以下命令检查你的包的所有导出是否正确:
npm run check-exports
你应该会注意到各种错误:
"tt-package-demo" | |
---|---|
node10 | 💀 Resolution failed |
node16 (from CJS) | 💀 Resolution failed |
node16 (from ESM) | 💀 Resolution failed |
bundler | 💀 Resolution failed |
这表明没有任何版本的Node或任何打包器可以使用我们的包。
让我们来解决这个问题。
5.4:设置main
在package.json
中添加一个main
字段,包含以下内容:
{
"main": "dist/index.js"
}
这告诉Node在哪里找到你包的入口点。
5.5:再次尝试check-exports
运行以下命令检查你的包的所有导出是否正确:
npm run check-exports
你应该会注意到只有一个警告:
"tt-package-demo" | |
---|---|
node10 | 🟢 |
node16 (from CJS) | ⚠️ ESM (仅限动态导入) |
node16 (from ESM) | 🟢 (ESM) |
bundler | 🟢 |
这告诉我们,我们的包与运行ESM的系统兼容。使用CJS的人(通常是在旧系统中)将需要使用动态导入来导入它。
5.6 修复CJS警告
如果你不想支持CJS(我建议这样做),将check-exports
脚本更改为:
{
"scripts": {
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm"
}
}
现在,运行check-exports
将显示一切正常:
"tt-package-demo" | |
---|---|
node10 | 🟢 |
node16 (from CJS) | 🟢 (ESM) |
node16 (from ESM) | 🟢 (ESM) |
bundler | 🟢 |
如果你想同时发布CJS和ESM,跳过这一步。
5.7:添加到我们的CI
脚本
将check-exports
脚本添加到你的package.json
中的ci
脚本:
{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports"
}
}
6: 使用tsup
双重发布
如果你想同时发布CJS和ESM代码,你可以使用tsup
。这是基于esbuild
构建的工具,它可以将你的TypeScript代码编译成两种格式。
我个人的建议是跳过这一步,只发布ES模块。这将显著简化你的设置,并避免双重发布带来的许多陷阱,比如双重包风险。
但如果你想这样做,那就继续。
6.1:安装tsup
运行以下命令安装tsup
:
npm install --save-dev tsup
6.2:创建一个tsup.config.ts
文件
创建一个包含以下内容的tsup.config.ts
文件:
import { defineConfig } from "tsup";
export default defineConfig({
entryPoints: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
outDir: "dist",
clean: true,
});
entryPoints
是你包的入口点数组。在这种情况下,我们使用src/index.ts
。format
是输出格式的数组。我们使用cjs
(CommonJS)和esm
(ECMAScript模块)。dts
是一个布尔值,告诉tsup
生成声明文件。outDir
是编译代码的输出目录。clean
告诉tsup
在构建之前清理输出目录。
#### 6.3:更改build
脚本
将package.json
中的build
脚本更改为以下内容:
{
"scripts": {
"build": "tsup"
}
}
我们现在将运行tsup
来编译我们的代码,而不是tsc
。
6.4:添加一个exports
字段
在package.json
中添加一个exports
字段,包含以下内容:
{
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/index.js",
"default": "./dist/index.cjs"
}
}
}
exports
字段告诉使用你包的程序如何找到你包的CJS和ESM版本。在这种情况下,我们将使用import
的人指向dist/index.js
,将使用require
的人指向dist/index.cjs
。
还建议将./package.json
添加到exports
字段。这是因为某些工具需要轻松访问你的package.json
文件。
6.5:再次尝试check-exports
运行以下命令检查你的包的所有导出是否正确:
npm run check-exports
现在,一切都是绿色的:
"tt-package-demo" | |
---|---|
node10 | 🟢 |
node16 (from CJS) | 🟢 (CJS) |
node16 (from ESM) | 🟢 (ESM) |
bundler | 🟢 |
6.6:将TypeScript变成一个linter
我们不再运行tsc
来编译我们的代码。而且tsup
实际上并不检查我们的代码是否有错误 - 它只是将其转换为JavaScript。
这意味着如果我们的代码中有TypeScript错误,我们的ci
脚本也不会出错。哎呀。
让我们来解决这个问题。
6.6.1:在tsconfig.json
中添加noEmit
在tsconfig.json
中添加一个noEmit
字段:
{
"compilerOptions": {
// ...其他选项
"noEmit": true
}
}
6.6.2:从tsconfig.json
中移除未使用的字段
从你的tsconfig.json
中移除以下字段:
outDir
rootDir
sourceMap
declaration
declarationMap
它们在我们的新'linting'设置中不再需要。
6.6.3:将module
更改为Preserve
可选地,你现在可以将tsconfig.json
中的module
更改为Preserve
:
{
"compilerOptions": {
// ...其他选项
"module": "Preserve"
}
}
这意味着你将不再需要使用.js
扩展名来导入你的文件。这意味着index.ts
可以这样写:
export * from "./utils";
6.6.4:添加一个lint
脚本
将以下内容添加到你的package.json
的lint
脚本中:
{
"scripts": {
"lint": "tsc"
}
}
这将运行TypeScript作为一个linter。
6.6.5:将lint
添加到你的ci
脚本
将lint
脚本添加到你的package.json
中的ci
脚本:
{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports && npm run lint"
}
}
现在,我们的CI流程中将包括TypeScript错误。
7: 使用Vitest进行测试
在本节中,我们将安装vitest
,创建一个测试,设置一个test
脚本,运行test
脚本,设置一个dev
脚本,并将test
脚本添加到我们的CI
脚本。
vitest
是一个用于ESM和TypeScript的现代测试运行器。它就像Jest,但更好。
7.1:安装vitest
运行以下命令安装vitest
:
npm install --save-dev vitest
7.2:创建一个测试
创建一个包含以下内容的src/utils.test.ts
文件:
import { add } from "./utils.js";
import { test, expect } from "vitest";
test("add", () => {
expect(add(1, 2)).toBe(3);
});
这是一个简单的测试,检查add
函数是否返回正确的值。
7.3:设置test
脚本
将以下内容添加到你的package.json
的test
脚本中:
{
"scripts": {
"test": "vitest run"
}
}
vitest run
会一次性运行你项目中的所有测试,不处于监听模式。
7.4:运行test
脚本
运行以下命令运行你的测试:
npm run test
你应该看到以下输出:
✓ src/utils.test.ts (1)
✓ hello
测试文件 1 个通过(1)
测试 1 个通过(1)
这表明你的测试已成功通过。
7.5:设置dev
脚本
一种常见的工作流程是在开发时以监听模式运行测试。将以下内容添加到你的package.json
的dev
脚本中:
{
"scripts": {
"dev": "vitest"
}
}
这将以监听模式运行你的测试。
7.6:添加到我们的CI
脚本
将test
脚本添加到你的package.json
中的ci
脚本:
{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports && npm run lint && npm run test"
}
}
8. 使用GitHub Actions设置我们的CI
在本节中,我们将创建一个GitHub Actions工作流程,每次提交和拉取请求时都运行我们的CI流程。
这是确保我们的包始终处于工作状态的关键步骤。
8.1:创建我们的工作流程
创建一个包含以下内容的.github/workflows/ci.yml
文件:
name: CI
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm install
- name: Run CI
run: npm run 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选项卡。你应该看到你的工作流程正在运行。
这将在每次提交和每次向仓库提出的PR时给出警告。
9. 使用Changesets发布
在本节中,我们将安装@changesets/cli
,初始化Changesets,使Changeset发布公开,将commit
设置为true
,设置一个local-release
脚本,添加一个Changeset,提交你的更改,运行local-release
脚本,最后在npm上看到你的包。
Changesets是一个帮助你对包进行版本控制和发布的工具。这是一个令人难以置信的工具,我向任何在npm上发布包的人推荐。
9.1:安装@changesets/cli
运行以下命令初始化Changesets:
npm install --save-dev @changesets/cli
9.2:初始化Changesets
运行以下命令初始化Changesets:
npx changeset init
这将在你的项目中创建一个.changeset
文件夹,包含一个config.json
文件。这也是你的Changesets所在的地方。
9.3:使Changeset发布公开
在.changeset/config.json
中,将access
字段更改为public
:
// .changeset/config.json
{
"access": "public"
}
如果不更改这个字段,changesets
不会将你的包发布到npm。
9.4:将commit
设置为true
:
在.changeset/config.json
中,将commit
字段更改为true
:
// .changeset/config.json
{
"commit": true
}
这将在版本控制后将Changeset提交到你的仓库。
9.5:设置一个local-release
脚本
将以下内容添加到你的package.json
的local-release
脚本中:
{
"scripts": {
"local-release": "changeset version && changeset publish"
}
}
这个脚本将运行你的CI流程,然后发布你的包到npm。当你想从本地机器发布新版本的包时,将运行此命令。
9.6 在prepublishOnly
中运行CI
将以下内容添加到你的package.json
的prepublishOnly
脚本中:
{
"scripts": {
"prepublishOnly": "npm run ci"
}
}
这将在发布你的包到npm之前自动运行你的CI流程。
这有助于将local-release
脚本分开,以防用户意外运行npm publish
而没有运行local-release
。感谢Jordan Harband的建议!
9.7:添加一个Changeset
运行以下命令添加一个Changeset:
npx changeset
这将打开一个交互式提示,你可以在其中添加一个Changeset。Changesets是将更改分组并给它们一个版本号的一种方式。
将此发布标记为patch
发布,并给出一个像“初始发布”这样的描述。
这将在.changeset
文件夹中创建一个新文件,包含Changeset。
9.8:提交你的更改
将你的更改提交到你的仓库:
git add .
git commit -m "准备初始发布"
9.9:运行local-release
脚本
运行以下命令发布你的包:
npm run local-release
这将运行你的CI流程,对你的包进行版本控制,并将其发布到npm。
它将在你的仓库中创建一个CHANGELOG.md
文件,详细说明此版本中的更改。每次发布时都会更新。
9.10:在npm上看到你的包
访问:
http://npmjs.com/package/<你的包名称>
你应该在那里看到你的包!你做到了!你已经发布到npm了!
总结
你现在拥有一个完全设置好的包。你已经设置了:
- 一个带有最新设置的TypeScript项目
- Prettier,它可以格式化你的代码并检查它是否正确格式化
@arethetypeswrong/cli
,它检查你的包导出是否正确tsup
,它将你的TypeScript代码编译成JavaScriptvitest
,它运行你的测试- GitHub Actions,它运行你的CI流程
- Changesets,它对你的包进行版本控制和发布
对于进一步阅读,我建议设置Changesets GitHub action和PR bot,以自动建议贡献者在他们的PR中添加Changesets。它们都是极好的工具。