The desktop window opened blank white on a fresh install: React threw "Minified React error #527" before the first paint, from the `vendor-react-<hash>.js` chunk. `apps/desktop` pins react and react-dom to the same exact version, but `vite.config.ts` aliased both to a hardcoded `../../node_modules/<pkg>` — straight into the monorepo root, where npm is free to hoist a different react. `@streamdown/math` is a root dependency whose react peer is `^18.0.0 || ^19.0.0` and which declares no react-dom peer, so npm hoists the newest react (19.2.8) to the root while react-dom stays at the version hoisted from the workspaces (19.2.7). react-dom's own peer is `react: ^19.2.7`, which 19.2.8 satisfies, so the install reports success and nothing warns. The bundle then shipped react 19.2.8 with react-dom 19.2.7 and React refused to run. `npm ci` masks this because the lockfile pins the root react to 19.2.7, which is why CI is green. The recurrence engine is `_run_npm_install_deterministic()`: when `npm ci` fails it falls back to `npm install --no-save`, which re-resolves the whole tree and never records the result — so the split comes back on the next update and leaves no trace. Fix the resolution rather than the hoist. The aliases now resolve both packages from the desktop workspace itself, where npm guarantees the declared versions are reachable (it nests a copy under the workspace exactly when the hoisted one differs), so the pair can only ever match. Pinning react at the npm layer instead was rejected: every manifest-level pin tried (root dependency, root `overrides`, a scoped override on `@streamdown/math`) breaks a fresh install with `ERESOLVE ... peer ink-text-input@"6.0.0" from @hermes/ink@0.0.1`. Two guards keep it from silently returning: - `assert-root-install.mjs` (the existing preflight of `build`, `dev:renderer` and `preview`) now fails the build when the resolved react and react-dom versions differ, so a split surfaces as an actionable error instead of a white window. - A `tests-js` contract test asserts every workspace pins the two to the same exact version, and that the desktop bundler no longer points at a hardcoded `node_modules` path. Verified on a synthetic split tree (root react 19.2.8 / react-dom 19.2.7, workspace react 19.2.7): the old aliases resolve 19.2.8 + 19.2.7, the new ones resolve 19.2.7 + 19.2.7, and the preflight exits 1 when the workspace itself resolves the mismatch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
36 lines
1.4 KiB
JavaScript
36 lines
1.4 KiB
JavaScript
import { accessSync, readFileSync } from "fs"
|
|
import { createRequire } from "module"
|
|
import { resolve, join } from "path"
|
|
|
|
const app = resolve(import.meta.dirname, "..")
|
|
const root = resolve(app, "..", "..")
|
|
|
|
try {
|
|
accessSync(join(root, "node_modules", "vite", "package.json"))
|
|
} catch {
|
|
console.error(`Run from repo root: cd ${root} && npm ci`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// `vite.config.ts` aliases react/react-dom to whatever this workspace resolves,
|
|
// and React refuses to run when the two come from different installed copies
|
|
// ("Minified React error #527" — it throws before the first paint, so the app
|
|
// window stays blank). npm stays silent about the split because the hoisted
|
|
// react still satisfies react-dom's caret peer range. Fail the build loudly
|
|
// instead of shipping a white screen.
|
|
const requireFromApp = createRequire(join(app, "package.json"))
|
|
const installedVersion = (pkg) =>
|
|
JSON.parse(readFileSync(requireFromApp.resolve(`${pkg}/package.json`), "utf8")).version
|
|
|
|
const react = installedVersion("react")
|
|
const reactDom = installedVersion("react-dom")
|
|
|
|
if (react !== reactDom) {
|
|
console.error(
|
|
`react@${react} / react-dom@${reactDom} version mismatch — React would fail ` +
|
|
`with error #527 and render a blank window. Pin both to the same version ` +
|
|
`in ${join(app, "package.json")}, then reinstall: cd ${root} && npm ci`
|
|
)
|
|
process.exit(1)
|
|
}
|