2024 年如何创建一个NPM包

原文信息: 查看原文查看原文

How To Create An NPM Package

- Matt Pocock

在本指南中,我们将经历发布一个包到npm所需的每一个步骤。

这不是一个极简指南。我们将从一个空目录开始设置一个完全生产就绪的包。这将包括:

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

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格式。每次发布新版本时,你都应该增加这个数字。
  • 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(无扩展名)的文件,包含你的许可证文本。

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中运行(即需要访问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.jsonscripts对象中:

{
  "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.jsonci脚本中:

{
  "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.jsonformat脚本中:

{
  "scripts": {
    "format": "prettier --write ."
  }
}

这将使用Prettier格式化你项目中的所有文件。

4.4:运行format脚本

运行以下命令格式化你项目中的所有文件:

npm run format

你可能会发现一些文件发生了变化。提交它们:

git add .
git commit -m "使用Prettier格式化代码"

4.5:设置一个check-format脚本

将以下内容添加到你的package.jsoncheck-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.jsoncheck-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.jsonlint脚本中:

{
  "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.jsontest脚本中:

{
  "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.jsondev脚本中:

{
  "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.jsonlocal-release脚本中:

{
  "scripts": {
    "local-release": "changeset version && changeset publish"
  }
}

这个脚本将运行你的CI流程,然后发布你的包到npm。当你想从本地机器发布新版本的包时,将运行此命令。

9.6 在prepublishOnly中运行CI

将以下内容添加到你的package.jsonprepublishOnly脚本中:

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

对于进一步阅读,我建议设置Changesets GitHub actionPR bot,以自动建议贡献者在他们的PR中添加Changesets。它们都是极好的工具。

分享于 2024-08-30

访问量 146

预览图片