返回博客

本地构建全绿、CI 爆红——barrel 入口提交了但组件源码没提交

组件库正在多个会话并行扩展,每个 session 分到不同组件,各自 commit、互不干扰。本地 tscnext 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.tsexport * 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 就出现。

坑:脏工作区掩盖了 HEAD 不一致

lstest -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——当前工作区是脏的,永远通过,不算数。

正解:接线和源码必须同一个 commit 原子落地

最根本的预防手段:不允许「先提交 barrel/manifest 接线,再提交源码」这种顺序。一个组件的源码目录和它在 index.ts/manifest/registry 里的注册,必须在同一个 commit 里一起落。并行 session 各自负责自己的组件时,明确约定:谁做接线谁提源码,不分离。

一句话外卖

构建能不能通,问 git,不问磁盘——git ls-files --error-unmatch 才是 HEAD 自洽性的唯一裁判。