@gantfdt/fp-babel-plugin-profiler
v0.2.4
Published
Build-time babel plugin that wraps React component exports with @gantfdt/fp-runtime-react fpProfile Profiler boundaries
Maintainers
Keywords
Readme
@gantfdt/fp-babel-plugin-profiler
构建期 babel 插件:自动给识别出的 React 组件 export 包上 fpProfile Profiler 边界(运行时 HOC 在 @gantfdt/fp-runtime-react)。规格见 docs/PROFILER_AUTOWRAP.md(本文档只是使用说明,不复制设计)。
// 变换前(宿主源码)
function SearchList(props) { ... }
export default SearchList;
export default () => { ... }
// 变换后(import 自动注入并去重;importPath 缺省时为插件 require.resolve 出的绝对路径)
import { fpProfile as __fpProfile } from '/abs/path/node_modules/@gantfdt/fp-runtime-react/dist/index.js';
function SearchList(props) { ... }
export default __fpProfile(SearchList, { id: 'src/pages/search/index.tsx:SearchList', name: 'SearchList' });
export default __fpProfile(() => { ... }, { id: 'src/pages/search/index.tsx:default', name: 'search' });直接的 export default function(具名/匿名)和 export function 不转换:拆成求值期包装会破坏函数声明导出的提升语义,循环依赖下可抛 ReferenceError。小写命名在 default 引用、函数表达式等路径也不进入组件候选,防止把多参数普通工具函数误包成 React 组件;export specifier 同时检查本地名和导出名。此处宁可少采,也不改变业务模块语义。
用法
经 @gantfdt/fp-umi-plugin 注入(fpPerf.autoProfile),或直接使用:
// babel 配置
plugins: [
[require.resolve('@gantfdt/fp-babel-plugin-profiler'), {
cwd: process.cwd(), // componentId 相对路径与 include/exclude 的基准目录
include: ['src/pages/**'], // minimatch 风格 glob,相对 cwd;默认 ['src/**']
exclude: ['**/*.test.*'], // 默认 ['**/*.test.*', '**/*.spec.*', '**/node_modules/**']
// importPath: '...', // 注入 import 的 specifier;缺省 require.resolve('@gantfdt/fp-runtime-react')
manifest: { outFile: 'dist/fp-components.json' }, // 存在时注册一次 process exit hook 自动写出
}],
]Options
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| cwd | string | process.cwd() | 相对路径基准;文件不在 cwd 内则跳过 |
| include | string[] | ['src/**'] | picomatch glob,相对 cwd 的 POSIX 路径 |
| exclude | string[] | ['**/*.test.*', '**/*.spec.*', '**/node_modules/**'] | 同上 |
| importPath | string | 插件内 require.resolve('@gantfdt/fp-runtime-react') 的绝对路径 | pnpm 严格结构下宿主不能裸引包名(设计文档 D4) |
| manifest.outFile | string | 无 | 存在时注册一次 process.on('exit') 自动 flush |
| manifest.deferMerge | boolean | false | umi 场景(umi-plugin 注入时置位):本进程 exit 钩子只落 worker sidecar;umi-plugin 在 onBuildComplete 临时合并、在主进程 exit 最终合并产出 fp-components.json |
manifest(fp-components.json)
进程内聚合本进程本次实际变换文件的产出。落盘走 sidecar 汇合(2026-10-23 起):exit 钩子把切片序列化为 $TMPDIR/fp-profiler-manifest-<sha1(outFile) 前 12 位>/worker-<pid>.json(先写临时文件再 rename 保证原子性),权威合并方读全部 sidecar 做 union(components 按 file+\0+id、skipped 按 file+\0+reason 去重,config 先到先得,排序字典序确定性)后落盘并清理 worker sidecar。独立单进程场景 flushManifest(outFile) 原地完成「落 sidecar → 合并 → 清理」全流程,产物与旧版逐字段一致:
{
"build": "20260923-1130",
"components": [
{ "id": "src/pages/search/index.tsx:default", "name": "OrderList", "file": "src/pages/search/index.tsx", "line": 42, "exportKind": "default", "wrapped": true }
],
"skipped": [{ "file": "src/utils/tree.ts", "reason": "no-jsx-return" }]
}跨构建快照(2026-10-25 起,babel-loader 磁盘缓存语义):缓存命中的文件本次构建不经插件(零 sidecar),其包装记录由快照承接——<cwd>/node_modules/.cache/fp-profiler/manifest-<sha12(outFile)>.json(cwd 不可写时回退到 sidecar 目录内 snapshot.json),内容 { configHash, components, skipped, capturedAt }。configHash = sha1(JSON({cwd, include, exclude})) 前 16 位:与本次生效配置不匹配即丢弃(options 变 → babel-loader 缓存 key 变 → 全量重转 → 本次 sidecar 已是全集)。快照条目按 fs.existsSync 修剪已删文件(只剪快照条目)。快照是跨构建资产:clearManifestSidecars 只清 worker-*.json、保留快照。本包导出 VERSION 供 umi-plugin 注入 babel options 排雷(插件升版本 → 缓存 key 变 → 自动全量重转)。零匹配告警三态不变:无 sidecar 且无快照 → 告警;无 sidecar 有快照承接(暖缓存正常路径)→ 静默;有 sidecar 但汇合 0 匹配 → 告警。umi 的 onBuildComplete 是 provisional 合并,以 deferWarnings: true 禁止两类告警及告警 latch;此时无 sidecar/manifest 是合法中间状态。最终覆盖告警由主进程 exit 兜底合并判定,且同进程 babel writer 的 exit 监听器先于 umi 合并监听器执行。
skipped reason 词汇表:
| reason | 含义 |
|---|---|
| no-jsx-return | 候选函数静态分析不到 JSX 返回 |
| hook | useXxx 命名 |
| lowercase-identifier | 小写开头命名(default 引用/函数表达式、named export、export specifier 的本地名或导出名) |
| hoisted-function-export | 直接的 default/named 函数声明导出;当前没有保持导出提升语义的包装变换,保守不采 |
| uncertain-form | 条件/动态/计算属性等静态不可判形态 |
| not-a-component | 导出体显然不是组件(字面量、非组件 class 等) |
| imported-reference | 默认导出的是裸 import 引用(组件会在其自身模块被包) |
| re-export-form | export ... from 再导出 |
| react-lazy | React.lazy 本体(被 lazy 的组件在其自身模块内按规则处理) |
| no-candidates | 文件内没有任何 export 候选 |
Node API
const plugin = require('@gantfdt/fp-babel-plugin-profiler');
plugin.VERSION; // 插件自身版本(读 package.json),umi-plugin 注入 babel options 排雷用
plugin.flushManifest(outFile); // 独立场景:落本进程 sidecar → 汇合(含快照承接)→ 写出并返回 manifest 对象(worker sidecar 已清理)
plugin.flushManifest(outFile, { snapshot: false }); // 完全跳过快照读写(宿主 dry-run 等只读场景)
plugin.resetManifest(); // 清空进程内聚合(测试隔离用)
// 合并:读全部 worker sidecar ∪ 跨构建快照(ctx 的 configHash 匹配才承接)→ union → 落盘 → 更新快照 → 清理;
// ctx = { cwd, include, exclude }(默认展开后的生效配置);无 sidecar 且无快照返回 null 不写盘。
plugin.mergeAndFlushManifest(outFile, ctx); // 默认按 F3 三态立即告警,供最终合并/直接调用
plugin.mergeAndFlushManifest(outFile, ctx, { deferWarnings: true }); // provisional:仅合并,不调用 noCoverage/zeroMatch,也不设置告警 latch
plugin.clearManifestSidecars(outFile); // 只清 worker sidecar(构建起点防被杀构建残留污染),保留跨构建快照
plugin.getManifestSidecarDir(outFile); // sidecar 聚合目录路径($TMPDIR/fp-profiler-manifest-<hash>)ESM import plugin from ... 亦可(module.exports.default = module.exports)。
幂等与失败模式
- 检测到
__fpProfile(...)包装引用即跳过;同一文件跑两遍产物逐字节一致。 - 任何识别不确定的形态跳过并记 skipped——漏包只是「该组件没数据」,不会改变组件行为。
- 运行时 fail-open 由
fpProfile保证(window.__fp缺失时返回原组件),不在本包职责内。
