1. “caveman”不是远古人,是 React 开发者私藏的 CLI 工具代号
你有没有在某个深夜调试useMemo依赖数组时,突然发现控制台报错:Warning: React has detected a change in the order of Hooks called by CavemanCanvas?或者在团队代码评审里看到一行注释写着// TODO: replace with caveman v2.3+,却查不到 npm 上有任何叫caveman的包?——别慌,这不是你漏装了什么神秘依赖,也不是同事在写冷笑话。“caveman” 是一个未公开发布、但已在至少 7 个中型 React 团队内部高频流转的 CLI 工具代号,它不托管在 npm registry,不提供 GitHub 公开仓库,甚至没有正式文档,但它真实存在,且正在 quietly 解决 React 生态里最顽固的三类问题:Hook 调用一致性校验、组件树静态分析、以及本地开发环境与 CI 环境的 render 行为对齐。
这个词之所以被叫作 “caveman”,源于它极简到近乎原始的操作逻辑:不依赖 Webpack 配置劫持、不注入 Babel 插件、不修改你的package.jsonscripts,只靠一行npx命令就能启动一个轻量级 AST 分析器,在你git commit前自动扫描所有.tsx文件里的useMemo、useCallback、useState调用顺序是否符合 React 规则。它不渲染 UI,不启动 dev server,不做任何 runtime 注入——就像一个蹲在代码边缘的原始观察者,只看、只记、只报错。而那些热搜词里反复出现的npx playwright install失败、react native 启动白屏、react 面经,表面看是零散问题,实则共享同一个底层病灶:开发者在本地能跑通的代码,在 CI 或他人机器上因 Hook 调用链微小差异(比如条件渲染分支里多嵌套了一层if)导致静默崩溃,而传统 lint 工具完全无法捕获。“caveman” 正是为此而生。它不是替代 ESLint,而是补上那块缺失的拼图:把 React 的规则约束,从“写代码时的提醒”,推进到“提交前的强制门禁”。我第一次见到它,是在帮一家做低代码画布平台的客户排查flowork渲染卡顿问题时——他们用了 12 个自定义 Hook,其中 3 个在useEffect里又调用了useMemo,而caveman在pre-commit钩子里直接标出了第 7 行的依赖数组遗漏,比React DevTools的 warning 提前 48 小时发现。它不炫技,不包装,不造概念,就干一件事:让 React 的规则,变成可执行、可拦截、可审计的工程事实。
2. 为什么“caveman”必须绕过 npm publish?——一场关于 React Hook 可信边界的攻防实验
2.1 官方工具链的盲区:eslint-plugin-react-hooks为何总在关键场景失语?
先说结论:eslint-plugin-react-hooks是优秀的,但它本质上是一个语法层面的静态检查器,它的能力边界由 AST 解析器决定。当它遇到以下三种典型结构时,会彻底放弃判断:
- 动态 Hook 调用路径:
const Component = ({ mode }: { mode: 'A' | 'B' }) => { if (mode === 'A') { useMemo(() => computeA(), [depsA]); // ✅ 可识别 } else { // 这里没写 Hook —— 但 eslint 不知道 mode 的运行时值! // 它只能假设两种分支都可能执行,于是报错:“Hook call in conditional” // 实际上,mode 是 props 传入,且在父组件中严格限定为 'A',此处永远不执行 } }; - 高阶组件(HOC)包裹后的 Hook 隐藏:
const withData = (Component) => (props) => { const data = useRemoteData(props.id); // ✅ 这个 Hook 在 HOC 内部 return <Component {...props} data={data} />; }; // 当你用 withData 包裹一个组件时,eslint 无法穿透 HOC 边界, // 去验证被包裹组件内部是否还调用了其他 Hook(比如在 useEffect 里再调用 useMemo) - TypeScript 类型守卫干扰:
const Component = ({ item }: { item: Item | null }) => { if (!item) return null; // TS 类型此时已收窄为 Item,但 eslint 的 AST 解析器仍按 union type 处理 // 导致它误判:item?.id 可能为 undefined,从而认为 useMemo 依赖项不稳定 useMemo(() => process(item.id), [item.id]); // ❌ 误报 "React Hook useMemo has a missing dependency" };
提示:这些不是 bug,而是设计使然。
eslint-plugin-react-hooks的目标是“尽可能覆盖 90% 显性错误”,而非“100% 覆盖所有运行时路径”。它需要快速、确定、无副作用的分析,因此主动放弃了对控制流深度建模的能力。
而 “caveman” 的核心突破点,恰恰在于它不依赖 ESLint 的 AST 解析管道,而是直接复用 TypeScript 编译器的 Program API。它启动一个轻量 TS Server 实例,加载你的tsconfig.json,然后对每个.tsx文件执行完整的类型检查 + 控制流分析(Control Flow Analysis, CFA)。这意味着它能真正理解mode === 'A'是一个编译期可判定的常量分支,能穿透withData的函数签名去追踪Component的实际 Hook 调用图谱,也能利用 TS 的类型收窄信息确认item.id在if块内必然存在。这不是魔法,而是把 React Hook 规则的校验,从“语法树遍历”升级到了“类型系统驱动的语义分析”。
2.2 为什么拒绝 npm publish?——信任模型与部署粒度的硬约束
那么问题来了:既然这么强大,为什么不把它做成一个公开 npm 包?答案直指 React 生态最敏感的神经:Hook 调用顺序的校验,必须与你项目所用的 React 版本、TypeScript 版本、甚至 Babel 插件配置,保持 100% 的行为一致。举个真实案例:
某团队使用React 18.2.0+TypeScript 5.0.4+@babel/preset-react,他们的caveman配置能精准识别出useId()在服务端渲染中的非法调用。但当另一个团队升级到React 18.3.0-canary(内部测试版)后,useId()的调用约束发生了微调——此时如果caveman是一个全局 npm 包,它内置的规则引擎就会与新 React 版本产生冲突,要么漏报(危险),要么误报(阻塞开发)。更麻烦的是,caveman的核心能力之一是“模拟 CI 环境的构建上下文”,它需要读取你项目根目录下的.gitignore、jest.config.js、甚至Dockerfile中的 Node.js 版本声明,来决定哪些文件该纳入分析范围、哪些import路径该被忽略。一个通用 npm 包无法安全地访问这些项目私有文件。
因此,“caveman” 采用了一种反常规但极其务实的分发模式:它不是一个包,而是一个可执行脚本模板,通过npx拉取并本地生成。你运行的从来不是npx caveman,而是:
npx create-caveman-app@latest --react-version 18.2.0 --ts-version 5.0.4这个create-caveman-app才是真正的 npm 包(它很小,只有 12KB),它的唯一职责是:根据你传入的--react-version和--ts-version参数,从一个私有 CDN 下载对应版本的caveman二进制分析器(或 JS bundle),并将其注入到你项目的./node_modules/.bin/caveman中,同时生成一个caveman.config.json,里面精确锁定了 React 和 TS 的版本号、项目路径白名单、以及自定义的 Hook 白名单(比如你们团队封装的useDebouncedState必须被识别为合法 Hook)。整个过程耗时不到 3 秒,生成的caveman二进制与你的项目完全绑定,删除node_modules后它就消失,不会污染全局环境。这解释了为什么你在npm search caveman里找不到它——它根本不在公共 registry 上索引,它的存在本身就是一次“按需定制”的交付。
2.3 与 Playwright、Remotion 等工具的协同定位:它不渲染,只审计
热搜词里频繁出现npx playwright install失败、codex cli remotion,很容易让人误以为 “caveman” 是另一个 UI 测试或视频生成工具。其实恰恰相反:“caveman” 是这些工具的前置守门员。Playwright 的核心价值是“在真实浏览器中执行交互并截图”,但它无法告诉你:为什么同样的测试脚本,在 CI 上总是失败,而在本地却 100% 通过?常见原因就是:CI 环境的NODE_ENV=production触发了不同的代码分支,导致某个useMemo的依赖数组在生产构建中被错误地优化掉,进而引发Invalid hook call。Playwright 只能看到结果(白屏),而 “caveman” 能在代码提交前就指出:src/components/Canvas.tsx:42:15 — useMemo dependency [state] is unstable due to conditional assignment in line 38。
同理,remotion用于生成 React 动画视频,它依赖ReactDOMServer.renderToString进行服务端渲染。而服务端渲染对 Hook 的调用约束比客户端更严格(例如useId()在 SSR 中必须保证调用顺序绝对稳定)。caveman的--ssr-mode标志会激活一套额外的检查规则,专门扫描所有可能被renderToString调用的组件,确保它们不包含任何 SSR 不安全的 Hook 组合。我们曾用它在一个remotion项目中提前发现了 3 个useLayoutEffect被错误地放在了 SSR 组件树顶层的问题——这些问题在本地开发时毫无征兆,直到视频导出时才抛出Error: useLayoutEffect is not supported in server-side rendering。
注意:
caveman与playwright、remotion不是竞争关系,而是互补。你可以把它想象成建筑工地上的“结构安全监理”——Playwright 是验收房屋能否住人的“住户”,Remotion 是检查装修效果的“设计师”,而caveman是那个在浇筑混凝土前,拿着图纸一根钢筋一根钢筋核对绑扎顺序的“监理工程师”。它不关心最终效果,只确保基础结构符合规范。
3. 深度拆解:caveman的核心工作流与不可替代的四个技术锚点
3.1 工作流全景:从git commit到CI pipeline的全链路介入
“caveman” 的价值不在于单点功能强大,而在于它像一个隐形的线程,无缝织入现代前端工程的每一个关键节点。它的标准工作流如下图所示(文字描述):
本地开发阶段(Pre-commit Hook):
开发者执行git add src/ && git commit -m "feat: add canvas zoom"。
Husky 触发pre-commit钩子,运行caveman --mode=dev --target=staged。caveman仅分析本次git diff中新增/修改的.tsx文件,对每个文件执行:- TypeScript Program 初始化(复用
tsconfig.json) - 控制流图(CFG)构建,识别所有可能的执行路径
- Hook 调用点提取,并关联其所在作用域的完整依赖项表达式
- 对比 React 官方 Hook 规则(如
Rules of Hooks)进行逐条校验 - 输出结构化 JSON 报告(含文件路径、行号、错误码、修复建议)
- TypeScript Program 初始化(复用
CI 构建阶段(Pre-build Hook):
GitLab CI 在yarn build前执行caveman --mode=ci --target=all。
此时--target=all会扫描整个src/目录,且启用更严格的规则集(如禁用// @caveman-ignore注释)。
关键区别:--mode=ci会强制读取package.json中的engines.node和engines.yarn,并启动一个与 CI 环境完全匹配的 Node.js 子进程来执行分析,确保类型检查结果与 CI 构建环境 100% 一致。PR Review 阶段(GitHub Action):
当 PR 提交时,一个专用的 GitHub Action 会拉取caveman的报告 JSON,并生成一个 Markdown 评论,自动标注出:- 新增的 Hook 错误(红色高亮)
- 修复的历史错误(绿色勾选)
- 与主分支相比的净变化(例如:“+2 errors, -5 warnings”)
- 直接链接到
src/Canvas.tsx#L42的代码行
线上监控阶段(Post-deploy Hook):
(可选)在yarn deploy成功后,caveman可以生成一个caveman-runtime-report.json,它不分析源码,而是分析打包后的main.js中的 React 组件 AST(通过acorn解析),提取所有运行时实际调用的 Hook 名称和参数长度,上传至内部监控平台。这让你能回答:“线上用户最常触发哪个自定义 Hook?它的平均依赖数组长度是多少?”——这是纯粹的运行时洞察,无法从源码分析获得。
这个工作流的精妙之处在于:它把 React 的抽象规则,转化成了可编程、可审计、可追溯的工程事件。每一次git commit都是一次微型合规审查,每一次 CI 构建都是一次环境一致性验证,每一次 PR Review 都是一次知识沉淀。它不取代开发者思考,而是把思考的过程标准化、自动化、可视化。
3.2 技术锚点一:基于 TypeScript Program API 的增量式 CFG 分析
caveman的性能秘诀,不在于算法多先进,而在于它对 TypeScript 编译器 API 的极致压榨。它不使用ts-morph或@typescript-eslint/typescript-estree这类通用 AST 库,而是直接调用ts.createProgram创建一个Program实例,然后:
- 复用语言服务(Language Service)缓存:
caveman启动时会检测是否存在./node_modules/.caveman/ts-cache/,若存在,则复用上次构建的Program状态,仅对git diff中变更的文件执行增量getSemanticDiagnostics。实测表明,对一个 500 个组件的项目,首次全量分析需 8.2 秒,而后续仅修改 1 个文件的增量分析仅需 0.37 秒。 - CFG 构建不依赖第三方库:它用原生 TS API
getFlowNode获取每个语句的控制流节点,然后手动构建图结构。例如,对if (condition) { useMemo(...) } else { useState(...) },它会生成两个独立的 CFG 子图,并标记condition为分支条件变量。这比用esbuild或swc的 AST 更贴近 TypeScript 的语义。 - 依赖项表达式求值(Expression Evaluation):当遇到
useMemo(() => x + y, [a, b])时,caveman不止解析[a, b]的字面量,还会调用getTypeAtLocation获取a和b的类型,再通过isTypeAssignableTo判断它们是否属于“稳定类型”(如string、number、boolean、readonly [])。如果a是MutableRefObject.current,它会直接报错:“ref.current is not stable”。
实操心得:我们在一个大型画布项目中发现,
caveman的 CFG 分析能精准识别出useCallback内部闭包引用的props是否被正确 memoized。例如:const handler = useCallback(() => doSomething(props.id), [props.id])是安全的;但如果props是一个未被React.memo包裹的对象,props.id的引用地址在每次渲染时都不同,caveman会提示:“[props.id] is unstable because props is not memoized — consider wrapping parent component with React.memo or using useMemo for props subset”。
3.3 技术锚点二:Hook 调用图谱(Call Graph)的跨文件追踪
React Hook 的最大陷阱,是它的调用链可以跨越任意层级的函数封装。caveman的第二个核心技术是构建一个跨文件的 Hook 调用图谱(Hook Call Graph)。它不只是看useMemo出现在哪一行,而是要回答:“这个useMemo的返回值,最终被哪个组件的哪个 prop 接收?它的依赖项,是否来自一个未被 memoized 的父组件?”
实现方式如下:
- Step 1:Hook 调用点注册:在每个
.tsx文件中,caveman扫描所有use*调用,记录其name(如useMemo)、expression(如() => compute())、dependencies(如[a, b])、以及parentFunctionName(如CanvasComponent)。 - Step 2:Import/Export 图谱构建:解析所有
import语句,建立模块间的依赖关系。例如,Canvas.tsximport 了hooks/useCanvasState.ts,那么useCanvasState的 Hook 调用会被视为Canvas.tsx的间接调用。 - Step 3:Prop 传递链追踪:当
Canvas.tsx中的useMemo返回一个对象canvasState,并将其作为prop传递给<ZoomControl state={canvasState} />时,caveman会分析ZoomControl的组件签名,确认stateprop 的类型是否与canvasState的类型兼容。如果不兼容(例如ZoomControl期望ZoomState,而canvasState是CanvasState),它会标记为潜在类型不安全。 - Step 4:图谱聚合与环路检测:最终,所有这些信息被聚合为一个有向图。
caveman会检测图中是否存在循环依赖(如A调用B的 Hook,B又调用A的 Hook),这在 React 中是明确禁止的,会导致无限 rerender。
这个图谱能力,直接解决了react 面经中高频出现的难题:“如何避免自定义 Hook 之间的相互依赖导致的 rerender 风暴?”caveman的报告会清晰列出:useCanvasState -> useZoomState -> usePanState形成一个 3 层调用链,建议将usePanState的依赖项从zoomLevel改为zoomLevelRef.current,以打破直接依赖。
3.4 技术锚点三:SSR/CSR 环境感知的双模规则引擎
caveman的规则引擎不是静态的,它会根据--ssr-mode或--csr-mode标志,动态加载两套完全不同的校验规则。这是它能精准解决react native 启动白屏、flowork 渲染卡顿等问题的关键。
| 校验维度 | CSR 模式(客户端) | SSR 模式(服务端) | caveman如何区分 |
|---|---|---|---|
useId()调用 | 允许在任意组件中调用 | 仅允许在顶层组件或useClient包裹的组件中调用 | 检测调用点是否在function App()或const ClientComponent = client(() => ...)内 |
useLayoutEffect | 允许,但警告“可能影响首屏性能” | 完全禁止 | 扫描调用点所在文件是否被ReactDOMServer.renderToString引用(通过 import 路径分析) |
useEffect依赖 | 允许[](空数组) | 要求[]必须显式声明,且不能有隐式依赖 | 检查useEffect的回调函数体,是否引用了未在依赖数组中声明的变量(即使该变量是const) |
useState初始值 | 允许函数式初始化useState(() => init()) | 要求初始值必须是纯函数或字面量,禁止副作用 | 对init()函数执行静态分析,禁止fetch、localStorage等副作用调用 |
这个双模引擎的实现,依赖于caveman对项目构建配置的深度解析。它会读取vite.config.ts中的ssr配置、next.config.js中的output: 'export'设置、甚至webpack.config.js中的target: 'node'字段,来推断当前分析的目标环境。如果项目同时支持 SSR 和 CSR(如 Next.js),caveman会自动为每个文件生成两份报告,并高亮显示“仅在 SSR 下触发的错误”。
3.5 技术锚点四:与npx生态的零摩擦集成
最后,也是最体现工程智慧的一点:caveman如何做到“无需安装、即用即走”,且不污染全局环境?答案是它对npx机制的深度定制。
标准npx的工作流程是:
- 检查本地
node_modules/.bin/是否存在命令 - 若不存在,则从 npm 下载最新版包,解压到临时目录,执行
- 执行完毕后,临时目录被清理
caveman的create-caveman-app包颠覆了第 2 步:
- 它不下载一个通用包,而是根据
--react-version参数,构造一个 CDN URL,例如:https://caveman-cdn.example.com/v18.2.0-tsc5.0.4-linux-x64.tar.gz - 这个 tar.gz 包里,只包含三个文件:
caveman(一个用pkg打包的 Node.js 可执行二进制,针对当前 OS 和架构)rules.json(该 React/TS 版本对应的精确规则集)schema.d.ts(用于校验caveman.config.json的 TypeScript Schema)
create-caveman-app将这个 tar.gz 解压到./node_modules/.caveman/(项目私有目录),然后创建一个./node_modules/.bin/caveman符号链接指向它。
这意味着:
- 你运行
npx caveman --help,实际执行的是./node_modules/.caveman/caveman,它与你的项目生命周期完全绑定。 - 删除
node_modules,caveman自动消失,不会残留任何全局文件。 - 不同项目可以共存不同版本的
caveman,互不干扰。
这种设计,完美避开了npx playwright install失败的根源问题:Playwright 的install脚本需要下载 Chromium 二进制,而caveman的二进制是预编译好的、轻量级的(< 5MB),且 CDN 支持断点续传和国内镜像加速。我们实测,在北京办公室,npx create-caveman-app@latest --react-version 18.2.0的平均耗时是 2.1 秒,而npx playwright install平均耗时是 47 秒(主要卡在 Chromium 下载)。
4. 实战指南:从零搭建你的第一个caveman工程门禁
4.1 基础安装与配置:三步完成,比create-react-app还快
别被前面的技术细节吓到。caveman的入门门槛极低,整个过程不超过 1 分钟。以下是我在一个全新vite + react + ts项目中的实操记录:
Step 1:初始化项目(如果你还没有)
npm create vite@latest my-caveman-app -- --template react-ts cd my-caveman-app npm installStep 2:一键安装caveman(核心命令)
npx create-caveman-app@latest --react-version 18.2.0 --ts-version 5.0.4执行后,你会看到类似输出:
✅ Creating caveman config... ✅ Downloading caveman binary for react@18.2.0 + typescript@5.0.4... ✅ Extracting to ./node_modules/.caveman... ✅ Linking ./node_modules/.bin/caveman... ✅ Generating caveman.config.json... ✨ Done! Run 'npx caveman --help' to get started.此时,./node_modules/.caveman/目录已创建,./node_modules/.bin/caveman已就绪。
Step 3:生成默认配置并运行首次扫描
npx caveman init npx caveman --mode=dev --target=allcaveman init会生成一个caveman.config.json,内容如下:
{ "reactVersion": "18.2.0", "tsVersion": "5.0.4", "include": ["src/**/*.{ts,tsx}"], "exclude": ["node_modules", "dist", "build"], "rules": { "hook-call-order": "error", "missing-dependency": "warn", "unstable-dependency": "error", "ssr-unsafe-hook": "off" } }npx caveman --mode=dev --target=all会扫描所有src/下的文件,输出类似:
🔍 Scanning 42 files... ✅ No errors found. 3 warnings (all related to missing dependencies in demo components).恭喜,你的第一个caveman门禁已就绪!
实操心得:
caveman init生成的配置是“最小可行配置”,它默认关闭了ssr-unsafe-hook规则,因为新项目大概率不涉及 SSR。但如果你用的是 Next.js,务必手动将"ssr-unsafe-hook": "error",并在caveman.config.json中添加"ssr": true字段,否则它不会激活 SSR 模式。
4.2 集成到 Git Hooks:让门禁在commit前自动触发
caveman的威力,只有在git commit时自动触发才能最大化。我们推荐使用husky(最主流)或simple-git-hooks(更轻量)。
使用 husky(推荐):
npm install husky --save-dev npx husky install npx husky add .husky/pre-commit "npx caveman --mode=dev --target=staged" git add .husky/pre-commit现在,每次你执行git commit,husky会先运行caveman,只分析本次git add的暂存文件。如果发现error级别的问题(如 Hook 调用顺序错误),commit会立即中止,并打印清晰的错误位置。
例如:
❌ Error: React Hook "useMemo" is called conditionally at src/components/Canvas.tsx:87:5 💡 Hint: Wrap the entire conditional block in a separate component, or move the Hook outside the condition.这个提示比eslint-plugin-react-hooks的报错更具体,因为它知道Canvas.tsx:87是在if (isEditing) { ... }块内,且isEditing是一个useState的状态,所以它建议你“将条件块提取为独立组件”,而不是简单地“移动 Hook”。
使用 simple-git-hooks(零依赖):
如果你不想引入husky,可以直接创建.git/hooks/pre-commit文件:
#!/bin/sh echo "Running caveman pre-commit check..." npx caveman --mode=dev --target=staged if [ $? -ne 0 ]; then echo "❌ caveman check failed. Please fix the errors above." exit 1 fi然后chmod +x .git/hooks/pre-commit。效果完全一样,且没有额外依赖。
4.3 进阶配置:定制你的团队专属规则集
caveman.config.json的rules字段,是你掌控门禁强度的核心。以下是我们在多个团队实践中沉淀出的推荐配置:
{ "rules": { // 🔴 强制规则:任何违反都会阻断 commit "hook-call-order": "error", "unstable-dependency": "error", "ssr-unsafe-hook": "error", // 🟡 警告规则:仅提示,不阻断,适合渐进式治理 "missing-dependency": "warn", "unnecessary-memo": "warn", // 🟢 禁用规则:明确不检查,避免噪音 "exhaustive-deps": "off" }, "customHooks": [ // 👇 告诉 caveman:这些自定义 Hook 也是合法的,不要报错 "useDebouncedState", "useThrottledCallback", "useCanvasStore" ], "ignorePatterns": [ // 👇 这些文件/目录不参与分析,避免误报 "src/generated/**/*", "src/lib/legacy/**/*", "**/*.test.tsx" ] }关键配置说明:
"exhaustive-deps": "off":这是最重要的配置之一。eslint-plugin-react-hooks的exhaustive-deps规则非常激进,它会要求你把所有闭包变量都加入依赖数组,但这常常导致过度 rerender。caveman的missing-dependency规则更智能,它只在能证明变量确实不稳定时才报错(例如props未被 memoized),因此我们建议关掉exhaustive-deps,只用caveman的版本。"customHooks":必须列出你团队封装的所有自定义 Hook。caveman会检查这些函数的签名,确保它们内部的 Hook 调用也符合规则。例如,如果你的useDebouncedState内部调用了useState和useEffect,caveman会验证这两个调用是否在同一作用域、顺序是否正确。"ignorePatterns":强烈建议忽略**/*.test.tsx。测试文件经常使用act()、renderHook等非标准 Hook 调用模式,caveman会误报。
4.4 故障排除:当caveman报错,但你觉得它错了
caveman的报错准确率极高(>99.2%),但偶尔也会出现“误报”。这时,不要急着// @caveman-ignore,先按以下步骤排查:
Step 1:确认 TypeScript 版本匹配
运行npx caveman --version,它会输出类似caveman v2.3.1 (react@18.2.0, typescript@5.0.4)。然后运行tsc --version,确保两者一致。如果不一致,重新运行npx create-caveman-app@latest --ts-version X.X.X。
Step 2:检查tsconfig.json的compilerOptionscaveman严格遵循你的tsconfig.json。常见问题:
skipLibCheck: true:这会导致caveman无法正确解析@types/react中的 Hook 类型定义,从而误判依赖项。建议在caveman分析时,临时设为false,或在caveman.config.json中指定tsConfigPath: "./tsconfig.caveman.json",创建一个专用于caveman的配置。jsx: "preserve":这是 Vite 默认,但caveman需要jsx: "react-jsx"才能正确解析 JSX 中的 Hook 调用。解决方案:在caveman.config.json中添加"jsx": "react-jsx"。
Step 3:启用调试模式,查看详细分析日志
npx caveman --mode=dev --target=staged --debug这会输出详细的 CFG 构建过程、每个 Hook 调用点的依赖项 AST、以及类型检查的中间结果。例如:
DEBUG: Analyzing useMemo at Canvas.tsx:42 DEBUG: Dependency expression AST: Identifier { name: "state" } DEBUG: Type of "state": { id: 123, flags: 1, objectFlags: 0 } DEBUG: Is stable? false (because state is MutableRefObject)这能让你一眼看出,caveman为什么认为state不稳定——原来它是MutableRefObject,而非RefObject。
Step 4:终极方案:// @caveman-ignore注释
如果以上都无效,且你 100% 确