compilerCoverage.contract.test.ts used to check two files. It now runs the real compiler over all of src (minus tests) and holds the exact set it skips today, file -> function -> category: 93 functions. A new skip or a category change is red, and so is a listed skip that has gone, so the list only shrinks. Channels and DataTable must also show up as compiled, so a harness that compiled nothing cannot pass. Measured against the audit's scan: no function this sprint touched started to skip. PlexPersonView and useOverlayOrder now compile. reactPlugin.ts claimed the compiler's rules were linted, which was false while react-hooks/refs was off. That comment and the refs block in eslint.config.js now say what holds the line and why refs is still off: 245 real hits, pinned by the ratchet, burned down from S3.
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { parseSync, transformSync, type types as t } from "@babel/core";
|
|
import reactCompiler from "babel-plugin-react-compiler";
|
|
|
|
// Runs the real React Compiler — the options reactPlugin.ts gives it — over the shipped source and
|
|
// reports every function it SKIPS, keyed `file → function → category`. The compiler skips without a
|
|
// word (`panicThreshold` is "none" and the build has no logger), so this is the only place a skip is
|
|
// visible at all. Used by compilerCoverage.contract.test.ts.
|
|
|
|
const SRC = new URL("../", import.meta.url);
|
|
|
|
/** Every file the production build compiles: src, minus tests and test kits. */
|
|
export function shippedSources(): string[] {
|
|
return (readdirSync(SRC, { recursive: true }) as string[])
|
|
.map((p) => p.split("\\").join("/"))
|
|
.filter(
|
|
(p) => /\.(ts|tsx)$/.test(p) && !/\.(test|testkit)\.tsx?$/.test(p) && !p.endsWith(".d.ts"),
|
|
)
|
|
.sort();
|
|
}
|
|
|
|
type Node = t.Node & { [k: string]: unknown };
|
|
const isFn = (n: Node) =>
|
|
n.type === "FunctionDeclaration" ||
|
|
n.type === "FunctionExpression" ||
|
|
n.type === "ArrowFunctionExpression";
|
|
|
|
/** The name a reader would look a function up by: its own, the variable it is assigned to (through
|
|
* a `memo(…)` / `forwardRef(…)` wrapper), or `default` for an anonymous default export. */
|
|
function functionNames(ast: t.File): Map<string, string> {
|
|
const names = new Map<string, string>();
|
|
const walk = (node: Node, parents: Node[]) => {
|
|
if (isFn(node) && node.loc) {
|
|
let name = (node as t.FunctionDeclaration).id?.name ?? null;
|
|
for (let i = parents.length - 1; !name && i >= 0; i--) {
|
|
const p = parents[i]!;
|
|
if (p.type === "VariableDeclarator" && p.id.type === "Identifier") name = p.id.name;
|
|
else if (p.type === "ExportDefaultDeclaration") name = "default";
|
|
else if (p.type !== "CallExpression" && p.type !== "TSAsExpression") break;
|
|
}
|
|
names.set(
|
|
`${node.loc.start.line}:${node.loc.start.column}`,
|
|
name ?? `<line ${node.loc.start.line}>`,
|
|
);
|
|
}
|
|
for (const key of Object.keys(node)) {
|
|
if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
|
|
const v = node[key];
|
|
if (Array.isArray(v)) {
|
|
for (const c of v)
|
|
if (c && typeof c === "object" && "type" in c) walk(c as Node, [...parents, node]);
|
|
} else if (v && typeof v === "object" && "type" in v) walk(v as Node, [...parents, node]);
|
|
}
|
|
};
|
|
walk(ast as unknown as Node, []);
|
|
return names;
|
|
}
|
|
|
|
type Detail = {
|
|
category?: string;
|
|
reason?: string;
|
|
options?: { category?: string; reason?: string };
|
|
};
|
|
|
|
export interface CompileReport {
|
|
/** file → function → the compiler's category for the skip (Refs, Todo, Immutability, …). */
|
|
skipped: Record<string, Record<string, string>>;
|
|
/** file → the functions it did compile. */
|
|
compiled: Record<string, string[]>;
|
|
}
|
|
|
|
export function compileReport(files: string[]): CompileReport {
|
|
const skipped: CompileReport["skipped"] = {};
|
|
const compiled: CompileReport["compiled"] = {};
|
|
for (const file of files) {
|
|
const source = readFileSync(new URL(file, SRC), "utf8");
|
|
const opts = { filename: file, babelrc: false, configFile: false } as const;
|
|
const ast = parseSync(source, { ...opts, parserOpts: { plugins: ["jsx", "typescript"] } });
|
|
if (!ast) throw new Error(`could not parse ${file}`);
|
|
const names = functionNames(ast);
|
|
const nameAt = (loc: t.SourceLocation | null) =>
|
|
loc
|
|
? (names.get(`${loc.start.line}:${loc.start.column}`) ?? `<line ${loc.start.line}>`)
|
|
: "<unknown>";
|
|
transformSync(source, {
|
|
...opts,
|
|
parserOpts: { plugins: ["jsx", "typescript"] },
|
|
plugins: [
|
|
[
|
|
reactCompiler,
|
|
{
|
|
target: "19",
|
|
logger: {
|
|
logEvent: (
|
|
_f: string | null,
|
|
e: {
|
|
kind: string;
|
|
fnLoc?: t.SourceLocation | null;
|
|
detail?: Detail;
|
|
reason?: string;
|
|
},
|
|
) => {
|
|
if (e.kind === "CompileSuccess") {
|
|
(compiled[file] ??= []).push(nameAt(e.fnLoc ?? null));
|
|
return;
|
|
}
|
|
let category: string;
|
|
if (e.kind === "CompileError")
|
|
category = e.detail?.options?.category ?? e.detail?.category ?? "Unknown";
|
|
else if (e.kind === "CompileSkip") category = `Skip: ${e.reason}`;
|
|
else if (e.kind === "PipelineError") category = "PipelineError";
|
|
else return;
|
|
const fns = (skipped[file] ??= {});
|
|
// The first reason is the one that stopped it; later ones are the same function.
|
|
fns[nameAt(e.fnLoc ?? null)] ??= category;
|
|
},
|
|
},
|
|
},
|
|
],
|
|
],
|
|
});
|
|
}
|
|
return { skipped, compiled };
|
|
}
|