返回博客

批量重命名 npm scope:锚定斜杠的 sed 和 grep 会系统性漏掉分隔符变体

给一个组件库做 npm scope 迁移,从 @old 改到 @newscope。一行 sed,全仓库替换,typecheck 绿、测试全过、包发布成功。grep 验收也干净——看着大功告成。

结果两周后,消费方照文档配 .npmrc 装包直接 404;CI 里一个覆盖率脚本悄悄返回 0,没有任何报错,只是数字不对。两个问题追溯到同一个根因:替换和验收都锚定了同一个分隔符,而 token 在其他地方以不同分隔符出现

现象

替换命令长这样:

grep -rl '@old/' --include='*.ts' --include='*.tsx' --include='*.json' . \
  | xargs sed -i '' 's#@old/#@newscope/#g'

验收命令:

grep -rn '@old/' --include='*.ts' --include='*.tsx' --include='*.json' .
# → 0 结果,以为搞定了 ✅

两个地方出了问题:

  1. .npmrc 里的 scope→registry 映射用的是冒号@old:registry=https://npm.pkg.github.com。sed 搜的是 @old/,完全不匹配,原封不动留下来。消费方按文档复制这段配置,pnpm add @newscope/... 去错误的 registry 找包,404。

  2. 脚本里有一个正则字面量,斜杠被转义/from\s+["']@old\/ui["']/。sed 搜的是 @old/,实际文本是 @old\/ui(中间多了一个反斜杠),一样不匹配。rename 后这个正则再也匹配不到任何 import,覆盖率脚本静默返回 0,没有异常,只是数字从 199/231 变成了 0/231。

根因

坑:替换和验收都锚定了同一个分隔符,构成系统性盲区

sed s#@old/#...#grep '@old/' 共享同一个正则模式 @old/(token + 尾部斜杠)。这个模式对以下形式的 token 全部不可见:

  • @old:registry=(冒号形式,.npmrc)
  • @old\/ui(转义斜杠形式,正则字面量)
  • @old(裸 token,文档/注释/yml)
  • --include 过滤掉的文件类型(.yml、.npmrc、dotfiles)

转义斜杠那个最危险:工具不报错,只是静默返回错误的数字,你不盯那个数字根本不会发现。

正解

替换用裸 token,验收也用裸 token,覆盖所有文件类型。

验收命令改成:

# 匹配 @old 后跟任何非字母数字字符(含行尾),排除新名字和 vendor 目录
grep -rnE '@old([:/]|\\\\|[^a-zA-Z0-9]|$)' . 2>/dev/null \
  | grep -v node_modules | grep -v '\.git/' | grep -v '@newscope'

替换阶段,除原有 *.ts/*.tsx/*.json 之外,显式追加这几类:

# .npmrc —— 冒号形式
sed -i '' 's#@old:registry=#@newscope:registry=#g' .npmrc
 
# 正则字面量 —— 转义斜杠形式
grep -rl '@old\\/' . | grep -v node_modules \
  | xargs sed -i '' 's#@old\\/#@newscope\\/#g'
 
# CI / workflow yml
grep -rl '@old' --include='*.yml' --include='*.yaml' . \
  | xargs sed -i '' 's#@old#@newscope#g'
正解:验收 grep 要搜裸 token,并覆盖所有可能的文件类型

验收阶段只允许两类残留:(1)mock 数据里的 email/handle,(2)故意保留的历史注释。正则里的 @old.npmrc 里的 @old:、yml 里的 @old 都必须被替换掉。

验收通过后,跑一次 pnpm install --frozen-lockfile 和依赖 import 扫描脚本,确认计数非零且符合预期。

一句话外卖

批量 rename 时,替换和验收用的 pattern 一样,就等于你看不见的部分也永远不会被发现——用裸 token + 所有文件类型做最终验收,才算真的干净。