Chapter 10

从脚本到发布

package.json 的 scripts、pre/post 钩子、生命周期事件、如何给 monorepo 发版——这是把 pnpm 工程串成真生产的最后一公里。

scripts 是什么

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint .",
    "test": "vitest",
    "typecheck": "tsc --noEmit"
  }
}
pnpm run dev        # 跑 scripts.dev
pnpm dev            # 简写(非保留字的 script 名)
pnpm test           # 特殊:test/start/stop 可省 run

pre / post 钩子

{
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "tsc",
    "postbuild": "echo 'build done'"
  }
}

pnpm build 会依次跑 prebuild → build → postbuild。

pnpm 默认不跑用户自定义的 pre/post
出于性能,pnpm 从某个版本起不自动跑 prefoo/postfoo——只有内置的 preinstall/postinstall 等 npm 规范钩子还跑。要启用自定义钩子,在 .npmrcenable-pre-post-scripts=true

npm 生命周期钩子(pnpm 自动跑的)

钩子触发时机
preinstallinstall 前(常用来检查 node 版本)
postinstall包装完后;编译原生模块、下载资源常用
prepare发包前 + 本地 install 后,常跑 husky init
prepacknpm pack / publish 前
prepublishOnlypnpm publish 前(典型:先 build 再发)

onlyBuiltDependencies(安全)

# pnpm-workspace.yaml
onlyBuiltDependencies:
  - esbuild
  - sharp
  - @prisma/client

v9 起,pnpm 默认不跑第三方包的 postinstall 脚本——供应链攻击门被堵死一道。只有白名单里的能跑。第一次 install 会提示「某些包想跑脚本,要不要允许」。

env 变量

pnpm run build
# 自动设置的变量:
# npm_package_name=my-app
# npm_package_version=1.0.0
# npm_lifecycle_event=build
# npm_execpath=/path/to/pnpm
# PATH=./node_modules/.bin:$PATH

scripts 里可以直接写 "prebuild": "echo $npm_package_version"

把参数传给 script

pnpm run test -- --watch
# -- 之后的参数原样传给 vitest

pnpm test --watch
# v9 起也支持(不需要 --)

pnpm 发布到 npm

# 单个包
pnpm publish

# 整个 monorepo(遍历所有非 private 的)
pnpm publish -r

# 干跑看效果,不真发
pnpm publish -r --dry-run

# 指定 tag
pnpm publish --tag beta
# npm install mypkg@beta 安装这个 tag

Changesets 发版流程

Step 1:安装

pnpm add -Dw @changesets/cli
pnpm changeset init
# 生成 .changeset/ 目录和 config.json
// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

Step 2:记录改动

pnpm changeset
# 交互式:
# 1. 选择哪些包被影响
# 2. 选择版本类型(patch/minor/major)
# 3. 写变更说明
# 生成 .changeset/abc-xxx-yyy.md
<!-- .changeset/abc-xxx-yyy.md -->
---
"@myorg/ui": minor
"@myorg/utils": patch
---

Add new Button variant. Fix utils.formatDate edge case.

Step 3:进入 PR 评审

.changeset/*.md 文件进 git,PR 里能看到影响哪些包、升什么版本——Code review 有依据。

Step 4:merge 后升版

pnpm changeset version
# 1. 读所有 .changeset/*.md
# 2. 更新对应包的 package.json version
# 3. 更新 CHANGELOG.md
# 4. 更新 pnpm-lock.yaml
# 5. 删掉已消费的 .changeset/*.md

Step 5:发包

pnpm publish -r --access public
# workspace 协议自动展开为版本号
# 只发 version 比 npm 上新的包

GitHub Actions 自动发版

# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      id-token: write      # npm provenance
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
          registry-url: 'https://registry.npmjs.org'

      - run: pnpm install --frozen-lockfile
      - run: pnpm -r build

      - uses: changesets/action@v1
        with:
          publish: pnpm changeset publish
          version: pnpm changeset version
          title: 'chore: version packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          NPM_CONFIG_PROVENANCE: 'true'
changesets/action 的魔法

npm provenance

# .npmrc
provenance=true

npm 2023 起支持 provenance:发布时附带签名,证明「这个 tarball 是从这个 git commit 在 GitHub Actions 构建的」。npm 网页会打「已验证」标——供应链信任的基石。要求 id-token: write 权限。

生产落地 checklist

锁 pnpm 版本
package.jsonpackageManager: "pnpm@9.12.0";CI 启用 Corepack。
lockfile 进 git
pnpm-lock.yaml 是可复现的唯一保证。
.npmrc 进 git
registry、auto-install-peers、engine-strict 这些共享——token 走 ${VAR} 引用,不入库。
CI 用 --frozen-lockfile
防止偷偷升版本。
onlyBuiltDependencies 白名单
新项目开始就列好必需的 native 包,新增要 PR 讨论。
Catalog 统一版本
核心依赖(React/TS/Vite)放 catalog,消除漂移。
Changesets + CI 自动发版
不要手工 pnpm publish——所有 release 走 GitHub Actions + provenance。
patches 目录进 git
任何第三方改动都走 pnpm patch,别手改 node_modules。
Turborepo 缓存
多包大仓必配,远程缓存让同事共享 CI 产物。

常用命令速查

命令用途
pnpm iinstall,按 lockfile 装
pnpm i --frozen-lockfileCI 用,严格不升版本
pnpm add <pkg>加依赖
pnpm -F <pkg> add <dep>给指定包加依赖
pnpm add -Dw <pkg>加到根 workspace devDep
pnpm -r build所有包跑 build,按拓扑
pnpm -F "...[origin/main]" test改动影响到的包测试
pnpm why <pkg>解释为什么装了
pnpm outdated -r看所有可升版本
pnpm dlx <pkg>临时运行一个包
pnpm patch <pkg@ver>给第三方包打补丁
pnpm deploy <path>实体化子包到独立目录
pnpm store prune清 store 中未引用的文件
pnpm changeset publish按 changesets 记录发版

后续学习路线

本章小结