我的 package.json 中的一些有趣的脚本

378 阅读4分钟

持续更新中…… 第一次更新 2025-5-23

1. 📝 package.json 的“巧妙”注释

常用于分组或者注释某一条比较难以阅读命令

比如分组:

{
  "scripts": {
    "========== Linting ==========": "",
    "lint": "biome lint && tsc --noEmit",
    "lint:fix": "biome check --write --unsafe",
    "prepare": "husky",
    "lint-staged": "biome check --staged --fix",
    
    "========== Testing ==========": "",
    "test": "vitest run --typecheck",
    "test:cov": "vitest run --typecheck --coverage",
    "ci": "npm run test:cov",
    
    "========== Publishing ==========": "",
    "pub:patch": "npm version patch",
    "pub:minor": "npm version minor",
    "pub:major": "npm version major",
    
    "preversion": "nvm use 22 && git pull && git push origin HEAD && pnpm i && npm run build-test-lint-concurrently",
    "postversion": "npm publish && git push origin HEAD && git push --tags",

    "build-test-lint-concurrently": "npm run build && concurrently -n 🧪,🔍 -p name \"npm run test:cov\" \"npm run lint\"",
    "publishd": "npm publish --dry-run",
  }
}

或解释某条 script:

{
  "scripts": {
     "bun-test-staged": "bun test $(git status -s | awk '{print $2}')",
     "// 失败自动 watch": "",
     "test:staged": "bun bun-test-staged || bun bun-test-staged --watch",
    }

2. 🧪 仅运行修改新增的测试文件

{
  "scripts": {
    "test:staged": "bun test $(git status -s | awk '{print $2}')",
  }
}

为什么不用 git diff --name-only? 因为新增文件不会输出。

如果你只想测试某次修改或新增的单测文件,执行 bun test:staged 即可,这在本地测试阶段非常有用。比如存在如下修改:

modified:   package.json
modified:   src/openApp.edit-last-one-multi-modal.test.tsx
modified:   src/openApp.edit.test.tsx
modified:   tests/formatHTML.ts
Added:      demo # git diff 不会列出新增的文件,只会列出文件夹

将只会执行 test 结尾的两个文件 demo 文件夹内的 test 文件。

bun 很聪明,一股脑将文件给他,自己会过滤出符合测试模式的文件,无需 grep 一大堆正则表达式,而且虽然 git status 不会列出新增的文件,只会列出文件夹,但是 bun 也能自动过滤 😎!

能否更完善点?如果测试文件没有改动但是对应的源码动了,也执行那就更好了。

更完善点

比如修改了 foo.ts,foo.test.ts 也自动执行,有个难点,若仅按照文件名规则 *.test.ts[x] 会有遗漏,还是需要检查引用关系且是 test 后缀的文件。但这样速度就很慢了,需要结合 oxc 或 biome 的快速 parser。

为什么不用 --watch,初始化会执行所有测试

那就过于复杂了,能否一行脚本搞定。

假设我们的单测和源码在一个目录:

❯ tree src/NoData 
NoData
├── demo
│   ├── basic.less    
│   ├── basic.test.tsx // <-
│   └── basic.tsx     
├── index.less        
├── index.md
├── index.test.tsx     // <-
└── index.tsx

当 NoData 内任意文件修改都能执行对应的单测。

更进一步,仅执行新增或修改的单测以及源文件修改涉及的单测 & 失败自动重试以及 watch:

    "// 仅执行新增或修改的单测以及源文件修改涉及的单测 & 失败自动重试以及 watch": "",
    "test:staged": "bun test:staged:core || bun test:staged:core --watch",
    "test:staged:core": "bun test $(git status -s | awk '{print $2}' | xargs -I {} dirname {} | grep -v '^\\.' | sort -u)",

假设修改如下:

❯ git status -s 
 M .nvmrc
 M .vscode/settings.json
 M README.md
 M package.json
 M src/NoData/demo/basic.tsx
 M src/NoData/index.md
 M src/NoData/index.tsx
?? src/NoData/demo/basic.less
?? src/NoData/index.less

上面虽然没有改动单测文件,但是源文件有变化也需要执行测试。其实我们只需要“喂给” bun test 改动的文件夹即可。即 src/NoData。

❯ git status -s | awk '{print $2}' | xargs -I {} dirname {} | grep -v '^\.' | sort -u
src/NoData
src/NoData/demo

虽然多输出了一个目录但是 bun 足够“聪明”,已经能满足我们的诉求!

    "// 仅执行新增或修改的单测以及源文件修改涉及的单测 & 失败自动重试以及 watch": "",
    "test:staged": "bun test:staged:core || bun test:staged:core --watch",
    "test:staged:core": "bun test $(git status -s | awk '{print $2}' | xargs -I {} dirname {} | grep -v '^\\.' | sort -u)",

3. 🚚 本地一行命令自动发布 npm 包

{
  "scripts": {
    "========== Publishing ==========": "",
    "pub:patch": "npm version patch",
    "pub:minor": "npm version minor",
    "pub:major": "npm version major",
    "preversion": "nvm use 22 && git pull && git push origin HEAD && pnpm i && npm run build",
    "postversion": "npm publish && git push origin HEAD && git push --tags",
    "prebuild": "concurrently -n 🧪,🔍 -p name \"npm run test:cov\" \"npm run lint\"",
    "publishd": "npm publish --dry-run",
  }
}

详见 0 依赖 1 行命令发布 NPM 项目

4. 📦 指定特定包管理器

比如某个老项目必须使用 yarn 安装:

"preinstall": "npx only-allow yarn",

上述钩子会在安装前通过 only-allow 校验包管理器是否是 yarn。

若尝试后发现会拖慢包安装的速度,可改成 pnpxbunx

5. 🕵️‍♂️ npm run ... 之前检查 Node.js 版本号

虽然我们已经通过脚本在切换项目目录的时候自动切换版本号,但是若存在多个项目,A 项目仍然可能被其他项目“意外”切换版本号,所以仍然需要在每次运行的时候再次确认下版本号。

假设某个项目必须 Node.js v22 (LTS) 以上版本:

{
  "scripts": {
    "check-node-version": "node -e \"const version = process.versions.node.split('.')[0]; console.log('\\n', version >= 22 ? '✅ ' + require('chalk').green('Node.js 版本号正确') : (process.exitCode = 1, '❌ ' + require('chalk').bgRed('需 >= v22')), '\\n')\"",
    "predev": "check-node-version"
  }
}

不限于 dev 之前检查,predev 改成 pre{target} 即可。

上述一行代码展开后:

const version = process.versions.node.split('.')[0]; 

console.log(
  '\n',
  version >= 22 ? 
    '✅ ' + require('chalk').green('Node.js 版本号正确') : 
    (process.exitCode = 1, '❌ ' + require('chalk').bgRed('需 >= v22')),
  '\n'
)

关于为什么使用 process.exitCode 而非“粗暴”的 process.exit(),详见 process.exit([code]),总结来说就是因为太粗暴,而前者更“优雅”。还有一种优雅方式是 throw Error,针对版本号范围检测可以使用 RangeError

image.png