本地构建全绿、CI 爆红——barrel 入口提交了但组件源码没提交
组件库正在多个会话并行扩展,每个 session 分到不同组件,各自 commit、互不干扰。本地 tsc 和 next build 全绿,PR 提上去 CI 直接报:
error TS2307: Cannot find module './chrome' or its corresponding type declarations.
路径明明存在,本地怎么 build 都没问题,CI 就是找不到。
现象
- 本地全套 typecheck + build 绿灯,CI 同一份代码报
TS2307 Cannot find module - 错误指向的是
index.ts里的 re-export:export * from "./chrome" - 在本地文件系统上,
packages/ui/src/chrome/目录真实存在、文件完整 git status输出里,该目录标记为??(untracked)
根因
并行 session 里,组件的「接线」和「实体」被分别提交:
| 已提交(committed) | 未提交(?? untracked) |
|---|---|
| index.ts 的 export * from "./chrome" | packages/ui/src/chrome/ 目录及全部源文件 |
| manifest.ts 里的 slug 注册 | |
| registry.tsx 里的 import + map 条目 | |
脏工作区(dirty working tree)下,./chrome 在磁盘上真实存在,tsc 能找到它,构建通过。但 HEAD 本身是内部不一致的:barrel 导出了一个 git 根本不知道的目录。只要做 clean checkout(CI、新 clone、git reset --hard、新 git worktree),目录消失,TS2307 就出现。
ls 或 test -d 只能告诉你目录在磁盘上存不存在,无法判断它有没有进 git。精准提交(pathspec commit)是正确纪律,但也意味着「谁提交了接线,谁也要提交源码」——若两步由不同 session 各自完成,很容易出现接线已落 HEAD、源码仍在某人工作区飘着的局面。源码只要那个工作区不存在(删目录、reset、切分支),就永远消失了。
排查
第一步:审计 HEAD 自洽性——用 git ls-files --error-unmatch 而非 ls,问 git 而不是磁盘:
for d in $(git show HEAD:packages/ui/src/index.ts \
| grep -oE '"\./[a-z-]+"' | tr -d '"./'); do
git ls-files --error-unmatch "packages/ui/src/$d" >/dev/null 2>&1 \
|| echo "❌ HEAD 有 export,但源码未追踪: $d"
done读的是 HEAD:index.ts(committed 版本),不是脏工作区里的文件。
第二步:确认孤儿目录完整,别把半成品提交进去:
ls packages/ui/src/chrome/ # 期望看到完整的 tsx/types/index 文件集
git ls-files packages/ui/src/chrome/ | wc -l # 期望输出 0(完全 untracked)正解
把孤儿源码目录提交进 HEAD。但如果此刻另一个 session 还在跑、有 staged 文件,直接 git add -A 会把别人的变更扫进来。用 temp-index plumbing,完全不碰真实 index 和工作区:
export GIT_INDEX_FILE=/tmp/orphan.idx; rm -f "$GIT_INDEX_FILE"
git read-tree HEAD # 从 HEAD 初始化临时 index
git add packages/ui/src/chrome \
packages/ui/src/tablet \
packages/ui/src/watch # 只加孤儿目录
TREE=$(git write-tree)
NEW=$(git commit-tree "$TREE" -p HEAD -m "fix: commit orphan component sources to make HEAD self-consistent")
git update-ref refs/heads/master "$NEW" "$(git rev-parse HEAD)"
unset GIT_INDEX_FILE
git reset -q # 把真实 index 同步到新 HEAD,工作区不动提交后验证:
for d in chrome tablet watch; do
git ls-files --error-unmatch packages/ui/src/$d >/dev/null 2>&1 \
&& echo "✅ tracked: $d" || echo "❌ still untracked: $d"
done要做最终确认,在新 git worktree 里跑 typecheck——当前工作区是脏的,永远通过,不算数。
最根本的预防手段:不允许「先提交 barrel/manifest 接线,再提交源码」这种顺序。一个组件的源码目录和它在 index.ts/manifest/registry 里的注册,必须在同一个 commit 里一起落。并行 session 各自负责自己的组件时,明确约定:谁做接线谁提源码,不分离。
一句话外卖
构建能不能通,问 git,不问磁盘——
git ls-files --error-unmatch才是 HEAD 自洽性的唯一裁判。