import { readFileSync } from "node:fs"; import { expect, it } from "vitest"; // The third contract test, after `nodeEngine` and `pythonEngine`, and for the same reason: a version // that is spelled in two places drifts, and the drift is silent until something downstream breaks. // // Here the two places are `package.json`'s React pin and `reactPlugin.ts`'s `target`, which tells // babel-plugin-react-compiler which React runtime to emit memoisation against. The rule was first // written as a comment — "`target` follows the installed React major and must move with it" — and // the house rule this codebase was built on says a rule stated in a comment must also be enforced // in code. So it is enforced here. // // The compiler does validate the option itself (an unknown target throws at build time), which // bounds the damage: the failure mode is not a corrupt bundle but a STALE target — React moves to a // new major and the compiler keeps emitting for the old one, with no lane saying so, because tsc // never sees Babel's output. // // To move React: bump `react`, `react-dom` and `@types/react` in package.json, then follow this // test's failures. const readFrontend = (rel: string) => readFileSync(new URL(`../../${rel}`, import.meta.url), "utf8"); const pkg = JSON.parse(readFrontend("package.json")) as { dependencies: Record; devDependencies: Record; }; /** The major in a version range, whether it is exact (`19.3.0`) or a caret (`^19.3.0`). */ function major(range: string, where: string): number { const m = /^\D*(\d+)\./.exec(range); expect(m, `could not read a major from ${where}: ${range}`).not.toBeNull(); return Number(m![1]); } const pinned = major(pkg.dependencies["react"]!, "react"); it("pins a plausible React major", () => { expect(pinned).toBeGreaterThanOrEqual(19); }); it("keeps react-dom on the same major as react", () => { expect(major(pkg.dependencies["react-dom"]!, "react-dom")).toBe(pinned); }); it("keeps the types on the same major as the runtime", () => { // Types a major ahead of the runtime make tsc green over APIs the shipped React does not have. expect(major(pkg.devDependencies["@types/react"]!, "@types/react")).toBe(pinned); expect(major(pkg.devDependencies["@types/react-dom"]!, "@types/react-dom")).toBe(pinned); }); it("compiles for the React that is actually installed", () => { const src = readFrontend("reactPlugin.ts"); const m = /target:\s*"(\d+)"/.exec(src); // A rename or a moved line lands here. Re-point the test — the contract did not go away with it. expect(m, 'no `target: ""` in reactPlugin.ts').not.toBeNull(); expect( Number(m![1]), `reactPlugin.ts compiles for React ${m![1]} while package.json pins ${pinned}. The React ` + `Compiler emits against the runtime named by \`target\`; move it with the pin.`, ).toBe(pinned); }); it("keeps react-i18next on a line that supports the pinned React", () => { // The 14 line predates React 19: its read `element.ref` (removed in 19) and its // `withTranslation` types were built on the GLOBAL JSX namespace that @types/react 19 no longer // declares — both latent only because this app happens to use neither export. 15 is the first // line that handles both, so the floor is a real contract, not a tidiness preference. expect(major(pkg.dependencies["react-i18next"]!, "react-i18next")).toBeGreaterThanOrEqual(15); });