项目文件夹

文件
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

167 行
5.5 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// Errors when a function hosting a tap resource (useResource / useResources /
// useClientLookup) bails the React Compiler, leaving its inline element/array
// argument un-memoized. Opt out an intentional bail with a
// `react-compiler-bail-ok` comment inside the function.
import { transformAsync, parseAsync, traverse } from "@babel/core";
import { readFileSync, globSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { createRequire } from "node:module";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
// Absolute path so the plugin loads regardless of cwd.
const reactCompilerPlugin = createRequire(import.meta.url).resolve(
"babel-plugin-react-compiler",
);
const HOSTING_HOOKS = new Set([
"useResource",
"useResources",
"useClientLookup",
]);
// Quick text prefilter so we only parse/compile files that host resources.
const HOSTING_CALL = /\b(useResources?|useClientLookup)\(/;
const OPT_OUT = "react-compiler-bail-ok";
const parserPlugins = (rel) =>
rel.endsWith(".tsx") ? ["typescript", "jsx"] : ["typescript"];
// Only flag args built in place. A param/prop passthrough (`useResource(element)`,
// `useResource(props.host)`) is memoized by the caller, not this function.
const isConstructedHere = (arg, path) => {
if (!arg) return false;
// Walk a member expression down to its root object identifier.
let root = arg;
while (
root.type === "MemberExpression" ||
root.type === "OptionalMemberExpression"
) {
root = root.object;
}
if (root.type === "Identifier" && root !== arg) {
// `x.y` passthrough — constructed here only if `x` is a local (non-param).
const binding = path.scope.getBinding(root.name);
return binding != null && binding.kind !== "param";
}
if (arg.type === "Identifier") {
const binding = path.scope.getBinding(arg.name);
return binding != null && binding.kind !== "param";
}
// Object/array literal, factory call, ternary, etc. — built in place.
return true;
};
const files = globSync("packages/*/src/**/*.{ts,tsx}", { cwd: repoRoot })
// tap defines these hooks and is never React-Compiled; skip it and non-source.
.filter(
(f) =>
!f.startsWith("packages/tap/") &&
!f.includes("/__tests__/") &&
!/\.(test|bench)\.[tj]sx?$/.test(f) &&
!f.endsWith(".d.ts"),
)
.filter((f) => HOSTING_CALL.test(readFileSync(join(repoRoot, f), "utf8")));
const violations = [];
const allowed = [];
for (const rel of files) {
const code = readFileSync(join(repoRoot, rel), "utf8");
const lines = code.split("\n");
// Collect resource-hosting calls whose element/array is constructed in place.
const ast = await parseAsync(code, {
filename: join(repoRoot, rel),
babelrc: false,
configFile: false,
parserOpts: { plugins: parserPlugins(rel) },
});
const hostingCalls = []; // { line }
traverse(ast, {
CallExpression(path) {
const callee = path.node.callee;
if (callee.type !== "Identifier" || !HOSTING_HOOKS.has(callee.name))
return;
if (!isConstructedHere(path.node.arguments[0], path)) return;
hostingCalls.push({ line: path.node.loc.start.line });
},
});
if (hostingCalls.length === 0) continue;
// Compile and collect every bailed function range.
const bails = new Map(); // `start-end` -> {start, end, reason}
await transformAsync(code, {
filename: join(repoRoot, rel),
babelrc: false,
configFile: false,
parserOpts: { plugins: parserPlugins(rel) },
plugins: [
[
reactCompilerPlugin,
{
logger: {
logEvent(_filename, event) {
if (event.kind !== "CompileError") return;
const start = event.fnLoc?.start?.line;
const end = event.fnLoc?.end?.line;
if (start == null || end == null) return;
const reason = (
event.detail?.description ||
event.detail?.reason ||
"unknown"
).split("\n")[0];
bails.set(`${start}-${end}`, { start, end, reason });
},
},
},
],
],
});
for (const { line } of hostingCalls) {
const range = [...bails.values()].find(
(b) => line >= b.start && line <= b.end,
);
if (!range) continue;
const optedOut = lines
.slice(range.start - 1, range.end)
.some((l) => l.includes(OPT_OUT));
const record = { rel, line, range, call: lines[line - 1].trim() };
(optedOut ? allowed : violations).push(record);
}
}
if (allowed.length) {
console.log(`\n${allowed.length} opted-out resource-hosting bail(s):`);
for (const a of allowed) {
console.log(` ${a.rel}:${a.line} (${a.range.reason})`);
}
}
if (violations.length) {
console.error(
`\n❌ ${violations.length} resource-hosting function(s) bail out of the React Compiler,\n` +
` so their element/array argument is NOT memoized:\n`,
);
for (const v of violations) {
console.error(` ${v.rel}:${v.line}`);
console.error(` call: ${v.call}`);
console.error(
` why: compiler skips its function (lines ${v.range.start}-${v.range.end}): ${v.range.reason}`,
);
}
console.error(
`\n Fix the compiler bail (preferred), or, if the bail is intentional and\n` +
` unavoidable, add a \`${OPT_OUT}\` comment inside the offending function.\n`,
);
process.exit(1);
}
console.log(
`\n✅ All ${files.length} resource-hosting file(s) compile cleanly — every` +
` useResource/useResources/useClientLookup argument is memoized.`,
);