译:如何创建 NPM 包

原文:https://www.totaltypescript.com/how-to-create-an-npm-package
作者:Matt Pocock
译者:ChatGPT 4 Turbo

编者注: https://x.com/mattpocockuk/status/1827963903382298718 这里有太长不看版。

在这个指南中,我们将逐步讲解发布包到 npm 需要做的每一个步骤。

这不是一个最简指南。我们将从一个空目录开始,设置一个完全适用于生产环境的包。这将包括:

如果你想看到成品,请查看这个 演示仓库

0. 视频

如果你更喜欢视频内容,我已经创建了一段这个指南的视频讲解:

注:原文有视频讲解。

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": "一个 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 格式。每次发布新版本时,你应该增加这个数字。
  • descriptionkeywords 是你的包的简短描述。它们在 npm 注册表的搜索中被列出。
  • homepage 是你包的主页 URL。GitHub 仓库是一个好的默认值,或者如果你有的话,也可以是文档站点。
  • bugs 是人们可以报告你的包的问题的 URL。
  • author 是你!你可以选择性地添加你的电子邮件和网站。如果你有多个贡献者,你可以以同样的格式指定一个 contributors 数组。
  • repository 是你包的仓库 URL。这在 npm 注册表上为你的 GitHub 仓库创建了一个链接。
  • files 是一个数组,包含安装你的包时应该包括的文件。在这种情况下,我们包含了 dist 文件夹。README.mdpackage.jsonLICENSE 默认包含在内。
  • 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: TypeScript

在本节中,我们将安装 TypeScript,设置 tsconfig.json,创建一个源文件,创建一个索引文件,设置一个 build 脚本,运行我们的构建,将 dist 添加到 .gitignore,设置一个 ci 脚本,并为 DOM 配置我们的 tsconfig.json

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 中运行(即需要访问 documentwindowlocalStorage 等),跳过这一步。

如果你的代码不需要访问 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

这将会创建一个包含你编译后的 JavaScript 代码的 dist 文件夹。

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 脚本

在你的 package.json 中的 ci 脚本里添加 check-format 脚本:

{
  "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            │ 💀 解析失败          │
├───────────────────┼──────────────────────┤
node16 (来自 CJS) │ 💀 解析失败          │
├───────────────────┼──────────────────────┤
node16 (来自 ESM) │ 💀 解析失败          │
├───────────────────┼──────────────────────┤
│ 打包器             │ 💀 解析失败          │
└───────────────────┴──────────────────────┘

这表明没有任何版本的 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 (从 CJS)   │ ⚠️ ESM(仅限动态导入)        │
├───────────────────┼──────────────────────────────┤
node16 (从 ESM)   │ 🟢 (ESM)                     │
├───────────────────┼──────────────────────────────┤
│ 打包工具           │ 🟢                           │
└───────────────────┴──────────────────────────────┘

这告诉我们,我们的包与运行 ESM 的系统兼容。使用 CJS(经常在遗留系统中)的人将需要使用动态导入来导入它。

如果我们愿意,我们可以稍后修复这个问题。

5.6: 添加到我们的 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 脚本

在你的 package.json 中,将 lint 脚本添加到你的 ci 脚本:

{
  "scripts": {
    "ci": "npm run build && npm run check-format && npm run check-exports && npm run lint"
  }
}

现在,TypeScript 错误将作为我们 CI 流程的一部分。

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 { hello } from "./utils.js";
import { test, expect } from "vitest";

test("hello", () => {
  expect(hello("world")).toBe("Hello, world!");
});

这是一个简单的测试,用来检查 hello 函数是否返回正确的值。

7.3: 设置 test 脚本

在你的 package.json 中添加一个 test 脚本,内容如下:

{
  "scripts": {
    "test": "vitest run"
  }
}

vitest run 将运行你项目中的所有测试一次,不会进行监听。

7.4: 运行 test 脚本

运行以下命令以运行你的测试:

npm run test

你应该会看到以下输出:

✓ src/utils.test.ts (1)
   ✓ hello

 Test Files  1 passed (1)
      Tests  1 passed (1)

这表明你的测试成功通过了。

7.5: 设置 dev 脚本

一个常见的工作流程是在开发时以监听模式运行你的测试。在你的 package.json 中添加一个 dev 脚本,内容如下:

{
  "scripts": {
    "dev": "vitest"
  }
}

这将以监听模式运行你的测试。

7.6: 添加到我们的 CI 脚本

test 脚本添加到你的 ci 脚本中,位于你的 package.json 中:

{
  "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 指定何时运行工作流。在这种情况下,它在 pull requests 和推送到 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 发布设置为公开,设置 committrue,设置一个 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
}

这将在版本化后将变更集提交到你的仓库。

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 脚本分开时很有用,以防用户在没有运行 local-release 的情况下误运行 npm publish。感谢 Jordan Harband 的建议!

9.7: 添加一个变更集

运行以下命令添加一个变更集:

npx changeset

这将打开一个交互式提示,你可以在其中添加一个变更集。变更集是一种将变更分组在一起并给它们一个版本号的方式。

将此次发布标记为一个 patch 发布,并给它一个描述,如 “初始发布”。

这将在 .changeset 文件夹中创建一个带有变更集的新文件。

9.8: 提交你的更改

将更改提交到你的仓库:

git add .
git commit -m "Prepare for initial release"

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 代码编译为 JavaScript
  • vitest,它运行你的测试
  • GitHub Actions,它运行你的 CI 流程
  • Changesets,它对你的包进行版本控制和发布

进一步阅读,我建议设置 Changesets GitHub actionPR 机器人 来自动推荐贡献者在他们的 PR 中添加变更集。它们都非常出色。

如果你还有任何问题,请告诉我!