#!/usr/bin/env node /** * Report React Compiler bailouts for the app's hottest components. * * The compiler silently skips any function it cannot prove safe (see the * comment in vite.config.ts), which for a long time included App, * DocumentCanvas, and the visual markdown editor — the exact components whose * re-render cost dominates typing latency. This script runs the same Babel * plugin the build uses and prints every CompileError with its location, so * bailouts are visible instead of silent. `react-compiler-guard.test.ts` * wires it into the test suite to keep the hot files at zero bailouts. * * Usage: node scripts/react-compiler-report.mjs [files...] */ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { isMain, projectRoot } from "./lib/util.mjs"; const REPO = projectRoot; // @babel/core is a transitive dep (of @vitejs/plugin-react) under pnpm, so // resolve it from the package that declares it — via the real path, since the // hoisted entry is a symlink and module lookup walks the physical tree. const require = createRequire(import.meta.url); const pluginReactRequire = createRequire( path.join(fs.realpathSync(path.join(REPO, "node_modules/@vitejs/plugin-react")), "package.json"), ); const { transformFileSync } = pluginReactRequire("@babel/core"); const reactCompiler = require("babel-plugin-react-compiler"); const linguiMacro = require("@lingui/babel-plugin-lingui-macro"); const GUARDED_FILES = [ "src/App.tsx", "src/canvas/document-canvas.tsx", "src/editor/markdown/engine/lattice-visual-editor.tsx", "src/pdf/pdf-viewer.tsx", ]; /** * @param {string} relative repo-relative file path * @returns {{ fn: string; line: number; reason: string }[]} */ function compileErrors(relative) { const events = []; transformFileSync(path.join(REPO, relative), { configFile: false, babelrc: false, // Same shape as @vitejs/plugin-react: the compiler runs over the raw TSX // AST (parser-level typescript/jsx support, no TS-stripping preset). parserOpts: { plugins: ["typescript", "jsx"] }, // As in vite.config.ts, Lingui macros expand before the compiler runs: it // bails out on a raw `t` tagged template with interpolations, which the // build never shows it. plugins: [linguiMacro, [reactCompiler, { target: "19", // As in vite.config.ts and vitest.config.ts; see // scripts/react-compiler-suppressions.ts. eslintSuppressionRules: ["react-hooks-js/exhaustive-deps", "react-hooks-js/rules-of-hooks"], logger: { logEvent: (_filename, event) => events.push(event) }, }]], code: false, ast: false, }); return events .filter((event) => event.kind === "CompileError") .map((event) => ({ fn: event.fnName ?? "(anonymous)", line: event.fnLoc?.start?.line ?? 0, at: event.detail?.loc?.start?.line ?? event.detail?.options?.loc?.start?.line ?? event.detail?.details?.[0]?.loc?.start?.line ?? 0, reason: String( event.detail?.reason ?? event.detail?.options?.reason ?? event.detail?.description ?? "unknown", ), })); } if (isMain(import.meta.url)) { const files = process.argv.slice(2).length ? process.argv.slice(2) : GUARDED_FILES; let total = 0; for (const file of files) { const errors = compileErrors(file); total += errors.length; console.log(`${file}: ${errors.length} bailout(s)`); for (const error of errors) { console.log(` L${error.line} ${error.fn} (at L${error.at}): ${error.reason}`); } } process.exitCode = total > 0 ? 1 : 0; }