react-i18next was still on ^14.1.2. The E27 S1 plan named it as "the one peer that actually needs moving — React 19 support is the 15 line", and then it did not move. The 14 line's <Trans> reads `element.ref`, removed in React 19, and its withTranslation types are built on the GLOBAL JSX namespace @types/react 19 no longer declares — both latent only because this app imports neither. 15.7.4 fixes both at the source (`c.props.ref ?? c.ref`, `React.JSX.LibraryManagedAttributes`). `reactEngine.contract.test.ts` joins its Node and Python twins and holds what was only a comment: the compiler's `target` against the installed React major, react and react-dom together, the types against the runtime, and react-i18next's floor. A stale target is not a corrupt bundle — the plugin validates the option — but it is a silent one, since tsc never sees Babel's output. pythonEngine gains the seventh spelling nobody held: `requirements.lock`'s own regeneration recipe names a python image, and wheels resolve against THAT interpreter, so a recipe left behind yields a lock that does not match the image installing it. The type lane now covers `alembic` too, for the reason the ruff lane was widened to it — migrations run on every container start, so one that raises crash-loops prod. It came back at zero new errors, so the baseline is untouched. `tests/` is deliberately still out, and the config says why. vitest.config's docblock claimed the React transform is "a no-op for the plain .ts node tests". It is not: the compiler rewrites src/lib's hooks in that lane too, which is the point of sharing one factory with the build — and worth knowing before ruling the compiler out of a divergence. Tests: useCardPager had none, in either the hook or its two callers, though its page clamp moved to render in S2 — the transformation where a non-convergent condition is a crash rather than a glitch. Four cases now, including termination on an empty list. Combobox's and Avatar's close/failed-URL cases are relabelled CHARACTERIZATION: both pass against the pre-sweep code, and the tests beside them are the ones that discriminate.
70 lines
3.4 KiB
TypeScript
70 lines
3.4 KiB
TypeScript
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. E27 S1 wrote the rule
|
|
// 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<string, string>;
|
|
devDependencies: Record<string, string>;
|
|
};
|
|
|
|
/** 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: "<major>"` 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 <Trans> 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);
|
|
});
|