今天我们将从一个空目录开始,一步步地介绍如何将一个包发布到npm,发布一个完全生产可用的包。这将包括:
-
使用Git进行版本控制 -
使用TypeScript编写代码并保持类型安全 -
使用Prettier格式化代码 -
使用 @arethetypeswrong/cli
检查我们的导出 -
使用tsup将TypeScript代码编译为CJS和ESM -
使用Vitest运行我们的测试 -
使用GitHub Actions运行我们的CI流程 -
使用Changesets进行版本控制和发布我们的包
.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
在这一部分,我们将创建一个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/yourgithub/tt-package-demo",
"bugs": {
"url": "https://github.com/yourgithub/tt-package-demo/issues"
},
"author": "Matt Pocock <team@totaltypescript.com> (https://totaltypescript.com)",
"repository": {
"type": "git",
"url": "git+https://github.com/yourgithub/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
(无扩展名)的文件,包含你的许可证文本。对于MIT,这是:
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
将[year]
和[fullname]
占位符更改为当前年份和你的姓名。
2.4:添加README.md
文件
创建一个README.md
文件,描述你的包。这里是一个例子:
**tt-package-demo**
Total TypeScript的一个演示包。
这将在人们在npm注册表查看你的包时显示。
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:为DOM配置你的tsconfig.json
如果你的代码在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
的scripts
对象中,设置ci
脚本:
{
"scripts": {
"ci": "npm run build"
}
}
这为我们在CI上运行所有必需的操作提供了一个快捷方式。
4.1: 安装Prettier
运行以下命令安装Prettier:
npm install --save-dev prettier
4.2: 设置.prettierrc
创建一个.prettierrc
文件,包含以下内容:
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
你可以向此文件添加更多选项以自定义Prettier的行为。你可以在这里找到完整的选项列表https://prettier.io/docs/en/options.html。
4.3: 设置format
脚本
在package.json
中添加一个format
脚本,包含以下内容:
{
"scripts": {
"format": "prettier --write ."
}
}
这将使用Prettier格式化项目中的所有文件。
4.4: 运行format
脚本
运行以下命令格式化项目中的所有文件:
npm run format
你可能会注意到一些文件发生了变化。提交它们:
git add .
git commit -m "Format code with 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 (dynamic import only) │
├───────────────────┼──────────────────────────────┤
│ 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"
}
}
如果你想同时发布CJS和ESM代码,你可以使用tsup
。这是基于esbuild
构建的工具,它可以将你的TypeScript代码编译成两种格式。
我个人的建议是跳过这一步,只发布ES模块。这会显著简化你的设置,并避免许多同时发布CJS和ESM的陷阱,比如双包危险。
但如果你想这样做,请继续。
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.1: 安装vitest
运行以下命令安装vitest
:
npm install --save-dev vitest
7.2: 创建测试
创建一个名为src/utils.test.ts
的文件,包含以下内容:
import { add } from "./utils";
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)
✓ add
测试文件 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.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.1: 安装@changesets/cli
运行以下命令初始化Changesets:
npm install --save-dev @changesets/cli
9.2: 初始化Changesets
运行以下命令初始化Changesets:
npx changeset init
这将在你的项目中创建一个.changeset
文件夹,包含一个config.json
文件。这也是你的changeset将存放的地方。
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: 设置本地发布脚本(续)
{
"scripts": {
"local-release": "changeset version && changeset publish"
}
}
这个脚本将运行CI流程,然后发布你的包到npm。这是当你想从本地机器发布新版本的包时运行的命令。
9.6: 在prepublishOnly
中运行CI(续)
{
"scripts": {
"prepublishOnly": "npm run ci"
}
}
这将确保在将包发布到npm之前自动运行CI流程。
9.7: 添加一个更改集
运行以下命令添加一个更改集:
npx changeset
这将打开一个交互式提示,你可以在这里添加一个更改集。更改集是一种将更改分组并赋予它们版本号的方式。
将此版本标记为patch
,并添加一个描述,如“初始发布”。
这将在.changeset
文件夹中创建一个新的更改集文件。
9.8: 提交你的更改
将更改提交到你的仓库:
git add .
git commit -m "Prepare for initial release"
9.9: 运行本地发布脚本
运行以下命令发布你的包:
npm run local-release
这将运行CI流程,对你的包进行版本控制,并将其发布到npm。
它还将在你的仓库中创建一个CHANGELOG.md
文件,详细记录此版本中的更改。每次你发布时,这都会更新。
9.10: 在npm上查看你的包
访问以下网址查看你的包:
http://npmjs.com/package/<your-package-name>
你应该能够在那里看到你的包!你做到了!你已经成功地发布到了npm!
总结
现在,你已经完全设置好了你的包。你已经设置了:
-
一个带有最新设置的TypeScript项目 -
Prettier,既可以格式化你的代码,也可以检查它是否正确格式化 -
@arethetypeswrong/cli
,检查你的包的导出是否正确 -
tsup
,将你的TypeScript代码编译成JavaScript -
vitest
,运行你的测试 -
GitHub Actions,运行你的CI流程 -
Changesets,帮助你的包进行版本控制和发布
github还支持设置Changesets GitHub操作和PR机器人,可以更好的实现自动化。
本文由 mdnice 多平台发布