产品思维:有时候最好的机会就藏在我们「身边的小需求」里 —— 傅行之
原文地址:www.totaltypescript.com/how-to-crea… , 作者:Matt Pocock
TIPS: 文章是npm系列相关的第一篇文章,由:上面的译文(主要)+小编补充, 译文部分采用意译,而非直译,有不少不准确的地方,欢迎在评论区中指出!
在本文中,我们将从一个空目录开始,详细介绍如何发布一个可应用于生产环境的npm包,相关内容如下:
- Git: 版本控制;
- TypeScript : 编写类型安全的代码;
- Prettier: 格式化代码;
- @arethetypeswrong/cli: 检查输出;
- tsup: 编译TypeScript代码,输出CJS 和 ESM 模块;
- Vitest: 测试
- Github Actions: 运行CI流程
- Changesets: 发版时的版本控制
可以查看已完成的demo。
OS环境:Mac M1
1. Git
这一部分,我们将创建一个git 仓库,设置 .gitignore
,创建一个初始的提交并将代码提交到GitHub上。
1.1 初始化,运行下面的命令:
mkdir stage-npm-template;cd $_
# 初始化git
git init
1.2 设置 .gitignore
在项目的根目录下,创建.gitignore
文件并添加下面的内容:
echo 'node_modules' > .gitignore
1.3 运行下面的命令,创建一个初始的提交记录:
git add .
git commit -m "feat: initial commit"
1.4 在GitHub创建一个新的仓库
使用GitHub CLI创建一个新的仓库,命令如下:
gh repo create stage-npm-package-template --source=. --public
1.5 使用下面的命令推送到GitHub上
git push --set-upstream origin main
package.json
在这一节中,我们将创建一个package.json文件,并介绍和使用其中一些字段域。
2.1 创建一个package.json文件:
- 使用npm/pnpm命令
npm init -y
内容如下:
{
"name": "stage-npm-package-template",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}
- 手动方式
{
"name": "stage-npm-package-template",
"version": "0.1.0",
"description": "A demo package for Total TypeScript",
"keywords": ["demo", "typescript"],
"homepage": "https://github.com/fushenguang/stage-npm-package-template",
"bugs": {
"url": "https://github.com/fushenguang/stage-npm-package-template/issues"
},
"author": "JiaFu <fujia.site@gmail.com> (https://fujia.site)",
"repository": {
"type": "git",
"url": "git+https://github.com/fushenguang/stage-npm-package-template.git"
},
"files": ["dist"],
"type": "module"
}
字段说明:
- name: npm包名,在npm上必须是唯一的。好一点的名字很难取,也可以创建一个组织(如:@fujia/stage),这样更便于管理发布的包;
- version: 包的版本,应该遵循「版本语义化」,每次发新版时,都应该增加相应的版本号;
- description 和 keywords:包的简短描述,关键字是在npm平台上搜索包时起作用的;
- homepage: 包的首页或使用文档;
- bugs: 反馈包的bugs的地址;
- author: 包的开发者,如果有多个贡献者,可以使用数组;
- repository:npm包的仓库地址,注意:这个地址会展示在npm的包首页上,如果不填写,使用者会倾向认为这是一个私有包;
- files: 文件数组,当用户下载你的包时包含的文件列表,在这里,我们是包含了
dist
文件夹,注意:README.md, package.json 和 LICENSE 文件是默认包含的; - type: 设置包的类型,它表示你的包是使用 ECMAScript 模块类型还是 CommonJS的模块类型。
2.2 添加 license 字段
在package.json中添加 license字段,如何选择?这里选择MIT,一般来说,如果没有商业化的需求的话,就选择该协议,如:vuejs/core。
2.3 创建 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 文件
该文件用于描述npm的包是什么?如何使用?等等。
注意:该文件非常重要, 它会展示在GitHub 仓库和npm 包的首页。一个完善的、格式良好的README文档是用户选择相信并使用该开源包的重要因素之一。
为什么期望用户采用你的开源库?见「编者语 【1】」
3. TypeScript
本小节的内容如下:
- 安装TypeScript
- 设置tsconfig.json
- 创建源文件
- 创建index文件
- 配置package.json的脚本
- 其它
3.1 安装TypeScript
运行下面的命令:
npm install --save-dev typescript
这里添加 --save-dev
参数表示将依赖安装到「开发依赖中」,它仅在开发环境中有效,在生产环境中,开发依赖是不会被安装到你的应用项目中的。
3.2 配置 tsconfig.json
创建tsconfig.json文件,并添加下面的配置
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
/* If transpiling with TypeScript: */
"module": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
/* AND if you're building for a library: */
"declaration": true,
/* AND if you're building for a library in a monorepo: */
"declarationMap": true
}
}
上面配置项的详细解释,可以参考TSConfig Cheat Sheet。
3.3 配置DOM的使用
如果代码运行在DOM环境下(如:需要访问 document, window或localStorage等)跳过这一步。
如果代码不需要访问DOM API's,添加下面的内容:
{
"compilerOptions": {
// ...other options
"lib": ["es2022"]
}
}
这会阻止DOM的类型被加载到代码库。如果不确定,也先跳过。
3.4 在src目录下,创建一个utils.ts的源文件,内容如下:
export const add = (a: number, b: number) => a + b;
3.5 创建一个index.ts文件,内容如下:
export { add } from "./utils.js";
这里的.js扩展看上去很奇怪,查看这篇文章了解更多。
3.6 设置build脚本
在package.json添加scripts对象字段,内容如下:
{
"scripts": {
"build": "tsc"
}
}
运行它会将TypeScript代码编译成JavaScript代码。
3.7 运行build命令
npm run build
这会创建一个dist目录包含编译生成的JavaScript代码。
3.8 将dist添加到 .gitignore文件中,避免编译的代码被提交到git 仓库,没有必要。
3.9 设置ci脚本
在package.json文件中添加ci脚本,如下:
{
"scripts": {
"ci": "npm run build"
}
}
这提供了一种快捷方式,在CI上运行所有需要的操作。
4. Prettier
本节内容包括:
- 安装prettier
- 设置.prettierrc
- 设置format脚本
- 其它
Prettier是一个代码格式化器,会自动格式化代码保证一致性的风格,使得代码更易于阅读和维护。
4.1 安装prettier
npm install --save-dev prettier
4.2 设置 .prettierrc
创建一个 .prettierrc 文件,内容如下:
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
更多的配置项,可以查看文档。
4.3 设置format脚本
在package.json文件中添加format脚本,如下:
{
"scripts": {
"format": "prettier --write ."
}
}
这会使用Prettier格式化项目中的所有文件。
4.4 可以运行format脚本试下。
npm run format
4.5 设置check-format脚本,内容如下:
{
"scripts": {
"check-format": "prettier --check ."
}
}
这会检查项目中所有的文件是否正确地被格式化。
4.5 添加CI脚本
在package.json文件中将check-format脚本添加到ci脚本中。
{
"scripts": {
"ci": "npm run build && npm run check-format"
}
}
这样在CI流程中,check-format的脚本会作为CI脚本的一部分被运行。
5. export, main和@arethetypeswrong/cli
本小节内容如下:
- 安装@arethetypeswrong/cli;
- 设置check-exports脚本;
- 设置main字段;
@arethetypeswrong/cli是一个检查发行包是否正确地导出内容的工具。这非常重要,因为这一步很容易发生错误, 会给正在使用我们发行包的项目带来问题。
5.1 安装@arethetypeswrong/cli
npm install --save-dev @arethetypeswrong/cli
5.2 设置check-exports脚本
在package.json中添加下面的内容:
{
"scripts": {
"check-exports": "attw --pack ."
}
}
这会检查包导出的所有文件是否正确。
5.3 运行check-exports 脚本
命令:
npm run check-exports
输出:
┌───────────────────┬──────────────────────┐
│ │ "stage-npm-package-template" │
├───────────────────┼──────────────────────┤
│ node10 │ 💀 Resolution failed │
├───────────────────┼──────────────────────┤
│ node16 (from CJS) │ 💀 Resolution failed │
├───────────────────┼──────────────────────┤
│ node16 (from ESM) │ 💀 Resolution failed │
├───────────────────┼──────────────────────┤
│ bundler │ 💀 Resolution failed │
└───────────────────┴──────────────────────┘
上面信息说明:没有node 版本,没有bundler可使用。
下面我们修复它们。
5.4 设置main
在package.json中添加main字段:
{
"main": "dist/index.js"
}
main字段的作用:让Node在哪里找到包的入口文件。
5.5 再次运行check-exports命令,一切都OK了。
5.6 添加check-exports到ci脚本:
{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports"
}
}
6. 使用tsup发布
如果想要同时发布CJS和ESM代码,可以使用tsup,该工具是构建在esbuild上的,它能将TypeScript代码编译成多种形式的JavaScript代码。
如果仅需要ES模块,个人建议是不要配置tsup,一种很常见的场景是:发布UI组件库。这会使得发布相关配置变得更加简单,同时能避免「双重发布(dual publishing,指的是同时支持cjs和esm模块)」的很多陷阱, 参考Dual Package Hazard。
如果需要,我们就继续下面的配置。
6.1 安装tsup
npm install --save-dev tsup
6.2 创建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 modules);
- dts: 表示是否生成声明文件;
- outDir: 编译后代码的存放目录,自动生成;
- clean:编译前是否清空输出目录的文件。
6.3 修改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
。
这里建议在exports字段下添加'./package.json'。 这是因为有些工具需要更快的访问到package.json文件。
6.5 再次运行check-exports脚本,发现一切OK。
6.6 将TypeScript转为linter
我们不再运行tsc来编译代码。事实上,tsup 不会检查代码的错误 — 它仅仅是编译代码。
这意味着如果代码中存在TypeScript错误,ci脚本是不会抛出异常的。
如何修复它?
6.6.1 在tsconfig.json中添加noEmit字段:
{
"compilerOptions": {
// ...other options
"noEmit": true
}
}
6.6.2 从tsconfig.json移除未使用的字段:
- outDir
- rootDir
- sourceMap
- declaration
- declarationMap
这些字段在新的"linting"配置中不再需要。
6.6.3 将module设置为"Preserve"
可选,现在可以将tsconfig.json中的module字段设置为"Preserve":
{
"compilerOptions": {
// ...other options
"module": "Preserve"
}
}
这意味着不再需要使用.js扩展来引入文件。这意味着index.ts可以如下引入(说明一下,这句话和示例仅提供参考,因为TS的版本一直在升级,各个字段的意义和影响也在改变):
export * from "./utils";
6.6.4 在package.json中添加lint脚本
{
"scripts": {
"lint": "tsc"
}
}
这样TypeScript就会作为一个linter运行了。
6.6.5 将lint添加到ci脚本中:
{
"scripts": {
"ci": "npm run build && npm run check-format && npm run check-exports && npm run lint"
}
}
现在,TypeScript的类型错误校验也加入了CI流程中。
7. 使用Vitest进行测试
本小节内容:
- 安装vitest;
- 创建测试单例;
- 设置test脚本;
- 其它
7.1 安装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);
})
7.3 在package.json中设置test脚本
{
"scripts": {
"test": "vitest run"
}
}
vitest run
命令会运行一次项目中所有的测试,不会监听。
7.4 运行test脚本
命令:
# 小技巧: 使用npm 运行 test/dev/start 命令可以省去run
npm run test
输出:
> stage-npm-package-template@1.0.0 test
> vitest run
RUN v2.0.5 /Users/sunny/Desktop/Practice/DevOps/stage-npm-package-template
✓ src/utils.test.ts (1)
✓ add
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 10:56:50
Duration 242ms (transform 25ms, setup 0ms, collect 22ms, tests 1ms, environment 0ms, prepare 64ms)
上面表示所有测试用例通过了。
7.5 设置dev脚本
在开发过程中,一种常见的工作流是使用监听模式运行测试命令。在package.json添加dev脚本:
{
"scripts": {
"dev": "vitest"
}
}
这会在「监听模式」下运行测试。
7.6 在package.json中将test添加到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工作流,它会在每次提交和PR(pull request)时自动运行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 : 指定什么时候运行工作流。在这里,当开发者PR和推送代码到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 测试工作流
将代码库中的修改提交到仓库中(main分支),在GitHub项目中Actions栏目下查看运行的工作流。
9 使用Changesets发布
本节内容:
- 安装@changesets/cli,初始化Changesets,公开changeset 发布;
- 设置commit为true;
- 设置local-release脚本,添加changeset,提交修改;
- 运行local-release脚本,发布到npm上。
「Changesets」是一个易用的帮助开发者管理版本和发布npm包的工具,推荐使用。
9.1 安装@changesets/cli
npm install --save-dev @changesets/cli
9.2 使用下面的命令初始化Changesets
npx changeset init
该命令会在项目根目录下创建一个.changeset文件夹,它包含一个config.json文件,这也是changesets(变更集存放的地方)。
9.3 将changeset的版本设为public
在.changeset/config.json文件中,将access字段设为public:
{
"access": "public"
}
如果不修改该字段,changesets是无法将包发布到npm上的。
9.4 在.changeset/config.json文件中,设置commit为true:
// .changeset/config.json
{
"commit": true
}
这样,版本变更后将会把变更提交到仓库中。
9.5 在package.json中设置local-release脚本,内容如下:
{
"scripts": {
"local-release": "changeset version && changeset publish"
}
}
当你想要从本地机器上发布一个npm包新版时,就可以执行该命令,它会运行CI流程然后将包发布到npm上。
9.6 在prepublishOnly中运行CI命令,内容如下:
// package.json
{
"scripts": {
"prepublishOnly": "npm run ci"
}
}
在发布包到npm之前会自动运行CI命令。
这对与local-release脚本分离非常有用,以防用户意外运行npm publish而没有运行local-release命令。
9.7 运行下面的命令添加一个changeset:
npx changeset
该命令会在终端显示一个交互提示,你可以添加一个变更集(changeset)。Changesets是一种将修改分组的方式并提供一个版本号。
执行后会在.changeset文件夹下创建一个新的文件。
9.8 提交改变
git add .
git commit -m "feat: prepare for initial release"
9.9 运行local-release命令:
npm run local-release
该命令会运行CI流程,升级npm版本然后发布到npm。
它将在代码库中创建一个CHANGELOG.md文件,详细说明此版本中的更改。每次发布时都会更新。
9.10 查看发布的包:
查看地址:www.npmjs.com/package/sta…
可以看到已经发布成功了。
10. nrm(可选读)
当我们需要经常切换npm registry时,建议使用它。
10.1 全局安装:
npm install -g nrm
10.2 查看npm镜像并使用
nrm ls
nrm use taobao
11. husky(可选)
为什么需要husky?
- 在将代码提交到仓库时,我们需要保证代码风格一致且没有错误,可以使用husky在提交代码时执行一些操作如:风格检查、TypeScript类型检查和单元测试等;
- 配合其它工具,在使用git提交变更信息时,为了保证信息更加可读且风格统一,需要使用husky工具在每次提交时都进行校验,符合规范才能提交。
11.1 安装husky
npm install --save-dev husky
11.2 使用husky init命令初始化(推荐)
npx husky init
11.3 修改.husky/pre-commit的内容:
npm run lint && npm run format
提交下面的信息验证下,可以看到已经生效了:
git add .
git commit -m 'Keep calm and commit'
11.4 安装@commitlint/{cli,config-conventional}
npm install --save-dev @commitlint/{cli,config-conventional}
11.5 添加配置文件
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
11.6 添加Hook(钩子),配置commit-msg
注意:目前「pre-commit」钩子不再支持
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
11.7 验证下
git add .
git commit -m 'add husky'
# 输出如下
> stage-npm-package-template@1.1.4 lint
> tsc
> stage-npm-package-template@1.1.4 format
> prettier --write ./src
src/index.ts 26ms (unchanged)
src/utils.test.ts 5ms (unchanged)
src/utils.ts 3ms (unchanged)
⧗ input: add husky
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warnings
ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
husky - commit-msg script failed (code 1)
可以看到校验是生效了的。
11.8 使用prompt(可选,commit message的规范建议是牢记的,某种程度上这是开发约定,始终遵循即可)
安装:
npm install --save-dev @commitlint/prompt-cli
# package.json 添加脚本
{
"scripts": {
"commit": "commit"
}
}
小结
现在,我们来总结下做出的配置,如下:
- 使用最新配置的TypeScript项目;
- Prettier,用来格式化代码和检查是否正确地格式化;
- @arethetypeswrong/cli,用来检查包导出是否正确;
- tsup,将TypeScript代码编译成JavaScript;
- vitest,单元测试;
- GitHub Actions,运行CI流程;
- Changesets,版本管理和发布npm包;
更深入的阅读,建议设置Changesets GitHub 操作和PR 机器人,可以自动建议贡献者将变更集添加到他们的 PR 中。他们都非常实用。
编者语
- 为什么期望用户采用你的开源库?
一句话说明:可构建开源软件正常的开发周期 —— 提供需求,促进持续迭代优化,测试验证;更多人使用,又会产生更多的需求,从而保证了软件的开发始终处于正常的流程中。
可以类比思考下公司内真实的业务开发流程就可以发现,如果使用的人过少,在需求端出现了问题,你要迭代下去,就要花费精力去「找」需求,此时的难度会急剧加大。如果软件开发的一个正常流程都无法走通,最后很有可能就沦为“自娱自乐”了。
- 在Mac M1/M2下,安装 GitHub cli 的命令:
arch -arm64 brew install gh
附录 Ⅰ
- GitHub CLI: cli.github.com/
- Git 使用规范流程: www.ruanyifeng.com/blog/2015/0…
- Commit message 和 Change log 编写指南: www.ruanyifeng.com/blog/2016/0…
- Git 工作流程: www.ruanyifeng.com/blog/2015/1…
- about-organization-scopes-and-packages:docs.npmjs.com/about-organ…
- semantic versioning:semver.org/
- 选择开源协议:choosealicense.com/licenses/
- 如何选择开源许可证?:www.ruanyifeng.com/blog/2011/0…
- tsconfig-cheat-sheet: www.totaltypescript.com/tsconfig-ch…
- tsconfig: aka.ms/tsconfig
- prettier options: prettier.io/docs/en/opt…
- package.json所有字段说明:docs.npmjs.com/cli/v10/con…
- dual-package-hazard: github.com/GeoffreyBoo…
- Changesets Github Action: github.com/changesets/…
- PR bot: github.com/changesets/…
- nrm: www.npmjs.com/package/nrm
- husky: github.com/typicode/hu…