技术一直在变化,我们的流程和做法也需要跟上这些变化。因此,虽然npm已经有12年的历史了,但围绕 npm 包创建的做法应该更现代。
在这节课中,我们使用现代最佳实践(截至2022年)一步一步地创建一个npm包。首先学习如何创建一个npm包,这样你就可以熟悉构建和发布一个包到 npm 注册表。
然后,再学习如何通过建立测试框架、持续集成和部署管道、安全检查以及发布的自动语义版本管理,来制作一个更健壮、可用于生产的npm包。
简单的npm包示例
我们先通过一个简单的例子来熟悉创建和发布npm包的过程。
创建项目
创建一个 GitHub 仓库: https://github.com/new
克隆本地的 repo。例如:git clone https://github.com/snyk-labs/simple-npm-package.git
打开你的终端,进入到克隆的项目文件夹。例如:cd simple-npm-package
运行npm init -y 来创建 package.json 文件。注意:如果克隆了示例仓库,就不需要做这一步。
在package.json 取一个名称,对应 name 字段
为该包编写你的代码
创建 npm 账户
为了能够让我们的 npm 包供他人使用,需要一个npm账户。
- 通过 https://www.npmjs.com/signup 注册
- 为了提高安全性,请在您的npm账户上启用2FA:https://docs.npmjs.com/configuring-two-factor-authentication
- 使用 npm login 命令在终端中用你的 npm账户登录,并按照屏幕上的指示操作。
> npm login
npm notice Log in on https://registry.npmjs.org/
Username: clarkio
Password:
Email: (this IS public) <email address>
npm notice Please use the one-time password (OTP) from your authenticator application
Enter one-time password from our authenticator app: <OTP>
Logged in as clarkio on https://registry.npmjs.org/.
如何发布 npm 包
一旦你有了一个npm项目和一个npm账户,你就可以把你的npm包发布到公开的官方npmjs注册表上,让其他人可以使用。以下是你要遵循的步骤,在执行之前检查将发布的内容,然后运行实际的发布过程。
在终端,运行npx npm-packlist 来查看将被包含在发布版本的软件包中的内容。
这可以确保我们没有遗漏任何源代码文件,这些文件是软件包正常运行所需要的。这也是一个好的做法,以确保我们不会意外地将敏感信息泄露给公众,如带有数据库凭证或API密钥的本地配置文件。
> npx npm-packlist
LICENSE
index.js
package.json
README.md
在终端,运行npm publish --dry-run,看看实际运行命令时将会做什么。
> npm publish --dry-run
npm notice
npm notice 📦@clarkio/simple-npm-package@0.0.1
npm notice === Tarball Contents ===
npm notice 1.1kB LICENSE
npm notice 1.2kB README.md
npm notice 95B index.js
npm notice 690B package.json
npm notice === Tarball Details===
npm notice name: @clarkio/simple-npm-package
npm notice version: 0.0.1
npm notice filename:@clarkio/simple-npm-package-0.0.1.tgz
npm notice package size:1.7 kB
npm notice unpacked size: 3.1 kB
npm notice shasum:40ede3ed630fa8857c0c9b8d4c81664374aa811c
npm notice integrity:sha512-QZCyWZTspkcUXL... ]L60ZKBOOBRLTg==
npm notice total files:4
npm notice
+ @clarkio/simple-npm-package@0.0.1
在终端,运行npm publish --access=public 来发布软件包到npm。
注意:**--access=public**对于作用哉内的包(@clarkio/modern-npm-package)是需要的,因为它们默认是私有的。如果它不是作用哉内的,并且在你的 package.json 中没有将private 字段设置为 true,它也将是公开的。
> npm publish --access=public
npm notice
npm notice 📦@clarkio/simple-npm-package@0.0.1
npm notice === Tarball Contents ===
npm notice 1.1kB LICENSE
npm notice 1.2kB README.md
npm notice 95B index.js
npm notice 690B package.json
npm notice === Tarball Details===
npm notice name: @clarkio/simple-npm-package
npm notice version: 0.0.1
npm notice filename:@clarkio/simple-npm-package-0.0.1.tgz
npm notice package size:2.1 kB
npm notice unpacked size: 4.1 kB
npm notice shasum:6f335d6254ebb77a5a24ee729650052a69994594
npm notice integrity:sha512-VZ1K1eMFOKeJW[...]7ZjKFVAxLcpdQ==
npm notice total files:4
npm notice
This operation requires a one-time password.
Enter OTP: <OTP>
+ @clarkio/simple-npm-package@0.0.1
现在,我们已经完成了构建和部署自己的npm包。接下来,我们来看一下如何制作一个更强大的包,为生产环境做好准备,并得到更广泛的使用。
生产就绪的npm包
虽然前面的例子的包可以在生产中使用,但它涉及到人工成本来保持其长期的维护。使用工具和自动化以及适当的测试和安全检查将有助于最大限度地减少保持软件包顺利运行的总工作量。让我们深入了解一下这其中的内容。
- 构建CommonJS(CJS)和ECMAScript(ESM)模块
- 设置和编写单元测试
- 实施安全检查
- 实现版本管理和发布的自动化
构建 CommonJS(CJS)和ECMAScript(ESM)模块
虽然ECMAScript模块格式现在在Node.js的12+版本中被原生支持,但它还没有被社区广泛采用。为了面向未来并支持这两种格式,我们来看下使用 TypeScript怎么来配置。
首先,创建一个基本的 TypeScript 配置文件 tsconfig.base.json。这是通用的编译设置,无论你的目标是哪种模块格式,都可以使用。
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"checkJs": true,
"allowJs": true,
"declaration": true,
"declarationMap": true,
"allowSyntheticDefaultImports": true
},
"files": ["../src/index.ts"]
}
然后为 CommonJS 格式创建一个TypeScript配置文件,命名为tsconfig.cjs.json。
- lib 属性向TypeScript指出它应该参考哪些类型。
- target 属性向TypeScript指出要编译的项目代码的JavaScript版本。
- module 属性向 TypeScript 指出在编译的项目代码时应该使用哪种JavaScript模块格式。
- moduleResolution 属性帮助 TypeScript 弄清 "import"语句应该如何被提及。
- outDir 和 declarationDir 属性向TypeScript指出了将编译的代码和定义其中使用的类型的结果放在哪里。
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES6", "DOM"],
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "../lib/cjs",
"declarationDir": "../lib/cjs/types"
}
}
之后,为 ECMAScript 格式创建一个TypeScript配置文件,命名为tsconfig.esm.json。这里的属性与你在 CommonJS 配置中看到的相同,但现在针对现代ECMAScript模块格式作为其输出。
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "NodeNext",
"outDir": "../lib/esm",
"declarationDir": "../lib/esm/types"
}
}
更新 package.json 文件,增加一个 files 字段,指向lib文件夹,里面有 TypeScript为你构建软件包的结果。
更新 package.json 文件中的 exports 字段,以定义如何根据使用的模块加载器(CJS vs. ESM)查找源文件。
"exports": {
".": {
"import": {
"types": "./lib/esm/types/index.d.ts",
"default": "./lib/esm/index.mjs"
},
"require": {
"types": "./lib/cjs/types/index.d.ts",
"default": "./lib/cjs/index.js"
}
}
},
更新 package.json 文件的 main和 types 字段,以指向软件包的CJS版本。这将作为一个默认的、后备的选项。
“types": "./lib/cjs/types/index.d.ts",
"main": "./lib/cjs/index.js",
在 package.json 文件中添加一个 files 字段,以表明当 npm 打包你的代码进行发布时,应该包括哪些文件。
"files": [
"lib/**/*"
],
通过 package.json 中的 scripts 字段创建命令,使用 tsc 并编译包的 CJS 和 ESM 格式,并生成 lib 文件。
clean 命令是用来删除过去构建的输出,并从一个干净的地方开始。
build:esm命令末尾的 mv lib/esm/index.js lib/esm/index.mjs 重命名了文件扩展名,这样Node.js模块加载器就知道它是一个ESM模块。
prepack命令是npm在打包npm包准备发布到注册表之前使用的。
"clean": "rm -rf ./lib",
"build": "npm run clean && npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p ./configs/tsconfig.esm.json && mv lib/esm/index.js lib/esm/index.mjs",
"build:cjs": "tsc -p ./configs/tsconfig.cjs.json",
"prepack": "npm run build"
现在可以在终端运行 npm run build,让TypeScript构建你的项目,为使用和发布做准备
这就是使用 TypeScript 构建 npm 包所需要做的所有设置,它同时支持 CommonJS 和ECMAScript模块格式。
设置和添加测试
为了对代码的行为和结果有信心,我们需要有一个测试过程。测试迫使在第一次创建代码时,在happy-path 之外,以不同的方式思考代码的功能。举个例子,可以想办法打破一个函数,使它抛出一个错误或产生一个非预期的结果。这样做将使你的应用程序更有弹性和可持续性,并确保在添加更多内容时不会出现问题。
单元测试
要确保库以我们想要的方式运行,需要针对代码编写测试。我们需要一些工具来帮助设置我们项目来运行单元测试并显示结果。
这些工具有 Mocha.js、Chai.js和 ts-node。Mocha.js 是一个测试运行器,Chai.js是一个断言库,帮助确定你是否从你的代码中得到你所期望的结果,而 ts-node 帮助我们在TypeScript项目中使用这些工具。按照下面的步骤,为 npm包设置和运行测试。
- 在终端中使用以下命令安装开发者的依赖:npm i -D mocha @type/mocha chai @types/chai ts-node
- 在项目的根目录下创建一个新文件.mocharc.json,内容如下:
{
"extension": ["ts"],
"spec": "./**/*.spec.ts",
"require": "ts-node/register"
}
在项目的根目录下创建一个tests 文件夹。
在index.spec.ts 文件中写单元测试来测试 index.ts 中的代码。
在package.json 文件的 scripts 部分添加一个 test 属性,给它一个 mocha 的值。
"scripts": {
"clean": "rm -rf ./lib",
"build": "npm run clean && npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p ./configs/tsconfig.esm.json && mv lib/esm/index.js lib/esm/index.mjs",
"build:cjs": "tsc -p ./configs/tsconfig.cjs.json",
"prepack": "npm run build",
"test": "mocha"
},
最后,在终端运行npm test
bc@mbp-snyk modern-npm-package % npm test
> @clarkio/modern-npm-package@0.0.0-development test
> mocha
NPM Package
✔️ should be an object
✔️ should have a helloworld property
Hello World Function
✔️ should be a function
✔️ should return the hello world message
4 passing (22ms)
管道中的测试
按照下面的步骤,创建一个测试工作流,作为项目管道的一部分。
为仓库创建一个新的GitHub Action :https://github.com/<your-account-or-organization>/<your-repo-name>/actions/new
将工作流程重命名为test.yml
在工作流程文件中插入以下Snyk动作脚本:
name: Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
这个YAML脚本检查出你的最新代码,安装其依赖性,并运行 npm test命令来执行测试。它对node-version字段中列出的每一个Node.js版本都会这样做,所以可以确保代码在每次运行时都能按预期工作。
现在已经完成了对项目的设置,以便对npm包的代码进行运行和评估测试。然而,你可能在想 "我如何在另一个项目中使用我的npm包进行测试?" 让我们来看看。
包测试
包上传完成后,除了单元测试外,我们还要测试在另一个项目引入我们包使用的情况,看看是否像我们所期望那样。这里有五种可以测试的方法:
- 通过 npm pack 输出安装
- 通过相对路径安装
- 通过npm链接安装
- 通过注册表安装(如npmjs.com的npm公共注册表)。
- 使用Verdaccio(一个开源的npm私有npm注册项目)来运行端到端的软件包发布和安装步骤,作为你CI的一部分。
npm pack
这种方法将利用npm pack命令将 npm 包打包并压缩成一个文件(<package-name>.tgz)。然后你可以到你想使用该包的项目中,通过这个文件安装它。这样做的步骤如下。
- 终端运行npm pack。注意它产生的.tgz文件和它的位置。
- 改变目录到你想使用npm 包的项目目录。例如:cd /path/to/project
- 运行npm install /path/to/package.tgz
- 然后就可以在项目中使用该包来测试东西了
npm link
利用 npm link 命令来安装本地包:
- 在当前包目录中,在终端运行npm link
- 改变目录到你想使用npm包的项目目录。例如:cd /path/to/project
- 在项目中运行npm link <name-of-your-package>
这样在项目中就可以使用我们的包。
相对路径
这种类似于npm link。
- 在终端运行npm install /path/to/your/package
与 npm link 的方法类似,这允许我们在项目中快速测试包的功能,但不会给你完整的类似生产的体验。这是因为它指向完整的软件包源代码目录,而不是你在npm注册表中找到的软件包的构建版本。
npm registry
这种方法利用了npm包的公共(或你自己)注册表。它涉及到发布的包,并像你通常对任何其他npm包那样进行安装。
- 使用本文前面概述的步骤,通过npm publish 命令发布npm包
- 改变目录到想使用npm包的项目目录。例如:cd /path/to/project
- 在项目目录中运行npm install <name-of-your-package>
实施安全检查
就像你不希望在自己的项目中出现安全漏洞一样,你也不希望在其他人的项目中引入漏洞。构建一个预计会在许多其他项目中使用的npm包,这就增加了确保事情安全的责任。你需要有安全检查,以帮助监测、提醒和提供帮助来减少漏洞。这就是像Snyk这样的工具可以简化完成这些需求所需的工作的地方。
对于这个例子中的npm包,你使用GitHub作为你的源码控制管理工具,所以利用它的GitHub Actions功能将Snyk整合到工作流程中。Snyk 有一个GitHub Actions参考项目,可以帮助启动这方面的工作,并为你的项目可能使用的其他编程语言和工具提供例子。
- Snyk是免费的,这里可以进行注册。
- 在GitHub上将你的Snyk API令牌添加为仓库秘密:https://github.com/<your-account-or-organization>/<your-repo-name>/settings/secrets/actions/new
- 仓库创建一个新的GitHub Action:https://github.com/<your-account-or-organization>/<your-repo-name>/actions/new
- 将workflow 重命名为snyk.yml
- 在 workflow 文件中插入以下Snyk Action 脚本:
name: Snyk Security Check
on: [push,pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@main
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- 提交你的修改。
- 验证Action 成功运行:https://github.com/<your-account-or-organization>/<your-repo-name>/actions
有了这个设置,任何时候任何人推送到你的版本库或针对它打开一个拉动请求,都会进行安全检查,以确保它不会在软件包中引入任何漏洞。如果发现了问题,行动将失败,并提醒你发现的安全问题的细节。接下来,你将围绕版本管理和发布你的npm包进行自动化处理。
关于目前的设置,需要注意的一点是,它只利用了Snyk开源(SCA)产品,而不是Snyk代码(SAST)。Snyk Code是我们的代码安全产品,你需要首先通过你的Snyk账户启用它(免费),然后在这里添加到你的工作流程脚本中,以充分利用它。
实现版本管理和发布的自动化
每当在主分支中合并变化时,我们不想每次都手动更新npm包的版本并发布它。相反,会想让这个过程自动发生。如果你还记得本篇文章前面那个简单的npm包的例子,用以下命令来更新npm包的版本,然后发布它。
npm version <major|minor|patch>
npm publish
什么是语义版本管理?
语义版本管理规定,版本要用三个占位符进行编号。第一个是主要版本,第二个是次要版本,而最后一个是补丁版本。
Semantic Release的工具可以与 GitHub Actions 整合来帮助我们自动修改版本并发布。实现这一过程自动化的关键是,你在向项目提交变更时使用所谓的常规提交。这使得自动化能够相应地更新一切,并知道如何为你准备项目的下一个版本。
1.运行:npm i -D semantic-release
2.npx semantic-release-cli setup
3.按照终端的提示,提供所需的令牌
- 需要一个来自 GitHub 的个人访问令牌。要创建一个,请到 https://github.com///settings/secrets/actions/new</your-name-or-github-organization>
- 在创建此令牌时,请使用以下作用域
4.还需要一个来自npm的自动化类型的访问令牌,只在CI环境中使用,这样它就能绕过你的账户的2FA。要创建一个,请到https://www.npmjs.com/settings//tokens。请确保选择 "Automation"类型,因为这将用于CI/CD工作流程中。
bc@mbp-snyk modern-npm-package % npx semantic-release-cli setup
? What is your npm registry? https://registry.npmjs.org/
? What is vour nom username? clarkio
? What is your pm password? [hidden]
? What is your NPM two-factor authentication code? <2FA code>
Provide a GitHub Personal Access Token (create a token at https://github.com/settings/tokens/new?scopes=repo
<token>
? What CI are you using? Github Actions
bc@mbp-snyk modern-npm-package %
5.将npm令牌作为仓库秘密添加到GitHub仓库中:https://github.com/<your-name-or-organization/<your-repository>/settings/secrets/actions/new。将秘密的名称设置为NPM_TOKEN,其值是你在前面步骤中检索到的
6.回到项目中,进入package.json文件,像下面这样添加一个release键。如果你的版本库的主分支仍然叫master而不是main,那么就相应地更新上述分支的值。
"release": {
"branches": ["main"]
}
7.在package.json 文件中也添加一个publishConfig键。
"publishConfig": {
"access": "public"
}
8.通过使用semantic-release npm脚本进行模拟运行来测试一切。采用以下命令,并将NPM_TOKEN=和GH_TOKEN=值设置为使用您各自的令牌值。然后在你的终端中复制并运行完整的命令,看看一切是否运行正常。你会看到进程被记录在终端的输出中。如果出现任何问题,它们会在这里显示出来,并提供解决这些问题的细节。
9.在确认试运行成功后,可以为GitHub仓库设置一个新的GitHub动作来为你处理发布过程。转到你在GitHub上的仓库,点击 "Actions"。
10.点击新建工作流程选项。
11.将工作流程重命名为release.yml。
12.在新的工作流程文件中加入以下YAML脚本。这个脚本主要是说,一旦Snyk安全检查工作成功完成,就运行发布工作。发布作业会检查代码,设置Node.js环境,安装你的依赖项,然后使用你的GitHub和npm令牌运行语义发布。
name: Release
on:
workflow_run:
workflows: ['Snyk Security Check', 'Tests']
branches: [main]
types:
- completed
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 'lts/*'
- name: Install dependencies
run: npm ci
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
13.提交你的本地修改并推送到你的GitHub仓库
- 可以通过在终端运行命令git commit -am '<your commit message>',然后git push来实现。
- 也可以在VS Code中通过其版本控制功能做到这一点。
14.在所有这些设置完成后,现在可以使用传统的提交方式将修改推送到你的主分支(或通过合并拉动请求),然后发布工作流就会运行(当然是在Snyk安全检查之后)。你可以在modern-npm-package版本库工作流程的例子中看到这种情况。
总结
我们总结一下在本文中学到的一切。首先,熟悉了设置、创建和部署一个简单的npm包。这对于熟悉首次发布自己的npm包来说是很好的。然而,如果想制作一个供生产使用的npm包,这样做是相当费力的,也是不可持续的。
为了完成制作一个可用于生产的包,随后学会了如何为CommonJS(CJS)和ECMAScript(ESM)模块格式进行构建,设置和编写单元测试,实现安全检查,并自动进行版本管理和发布。有了这些知识,现在已经准备好制作更多属于你自己的npm包了,这些包很容易被社区或你的公司所使用。
来源:https://snyk.io/blog/best-practices-create-modern-npm-package/