偶尔会团队开发一些 Node.js 工具库之类的小玩意,这篇文章就简单介绍一下我经常会用到的工具和一些实践,想到哪写到哪。

创建项目

GitHub 上有一些模板仓库,例如 egoist/ts-lib-starterantfu/starter-ts,你可以点击 Use this template,GitHub 就会自动帮你创建一个仓库。 如果你想要参考的仓库不是一个 template 仓库,又或者你这是一个公司内部的项目,需要你手动创建 git,那么你也可以使用 degit 这个工具。 例如我想使用 egoist/ts-lib-starter 来创建一个本地项目:

~ degit https://github.com/egoist/ts-lib-starter my-lib
~ cd my-lib
~ git init

包管理器

我所有个人新项目都使用 pnpm 作为包管理器,快速,节约硬盘空间。

请在项目根目录创建一个 .npmrc 文件,里面写好你的 npm registry,不要依赖全局的配置。

# .npmrc
registry = <private npm registry or public mirror>

Typescript

不管多么简单的库,我都是用 ts。本地调试的时候,可以使用 tsx 这个包,它让你执行 ts 文件就像执行一个普通的 js 文件一样。一个简单的 tsx index.ts 就能运行。不需要 tsc 编译,也比 ts-node 要快。

关于 tsconfig.json

tsconfig 的配置我想说两个东西。target 和 moduleResolution。

  • target:Node.js v12 已经 end of life 了,因此建议将项目 Node 版本至少升级到 14。Node 14 版本已经支持了大部分 ES2020 的特性了,参考 node.green。因此我们也可以将 target 设置为 ES2020 或者更低的版本。2023 年了,不要再写 target: "es5" 了。
  • moduleResolution:建议设置为 Node16 或者 NodeNext,这样 ts 会支持 package.json 里的 exports 特性,否则的话你的 VS Code 可能会给你报错,具体参考 Announcing TypeScript 4.7

附上我个人使用的 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "ESNext",
    "lib": [
      "ESNext"
    ],
    "jsx": "preserve",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "moduleResolution": "Node16",
    "allowJs": true,
    "strict": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noImplicitAny": true,
    "sourceMap": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true
  }
}

打包

打包工具使用 tsup,简单易用,基于 esbuild,快速,支持 watch。 一行简单的 tsup src/index.ts --format cjs,esm 就能生成支持 CommonJS 和 ES Module 的产物。--dts 可以生成类型。 可以参考 文档这篇 blog。 需要注意的是 tsup 是一个 bundler,因此它可能会将你的依赖一起打进去。如果你不想将某个依赖打进去可以使用 --external <package>dependenciespeerDependencies 默认是 external 的,所以正确的区分你的依赖是 dev 还是 prod 很重要。

package.json

请在你的 package.json 里补充上你的 publishConfig.registry,避免你的一个内部包发布到外网。

// package.json
{
  "publishConfig": {
    "registry": "<private npm registry>",
    "access": "public"
  },
  "files": [
    "dist",
    "CHANGELOG.md",
    "README.md"
  ],
}

如果你的包是一个带有 scope 的包,还得在 publishConfig 里补充上 "access": "public",否则会被当作是 private 的无法发布。 声明 files 字段,不要把一些无关的文件给带上,导致增加 package 的体积。

关于 package entry points

Node 16 已经支持了识别 package.json 下的 exports 字段。大部分情况下抄以下的配置就行了

// package.json
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
     ".": {
      "types": "./dist/index.d.ts",
      "require": "./dist/index.cjs",
      "import": "./dist/index.mjs",
      "default": "./dist/index.cjs"
    }
  }
}

mainmoduletypes 作为 fallback,为了兼容不支持 exports 字段的环境。

建议熟读 Node.js 文档, 也可以参考 打包JavaScript库的现代化指南

发布工作流

使用 changesets

在你 commit 之前,运行下 pnpm changeset 生成 changeset 文件。到要发布之前运行

pnpm changeset version && git commit -am 'bump version' && git push

最终 publish 就行了。