File size: 6,510 Bytes
c61f7ed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | // Build-time guard: refuse to start a build the installed tree cannot finish.
//
// The desktop workspace's dependencies are hoisted to the repo-root
// `node_modules`, so a root install that only covers *part* of the workspace
// graph leaves this app importable-looking but unbuildable. The guard exists to
// turn that into one actionable line ("run npm ci from the repo root") instead
// of a failure deep inside vite.
//
// It runs from `prebuild`, ahead of `npm run clean`, so a tree that cannot
// build is rejected before the build starts deleting its own outputs. `build`
// re-runs it for anyone invoking the build steps directly; the check is pure
// filesystem lookups, so paying for it twice costs nothing.
import { existsSync, readFileSync } from "fs"
import { createRequire } from "module"
import { resolve, join, dirname } from "path"
import { isMain } from "./utils.mjs"
// Packages the build *consumes*, as opposed to merely declares. Each one is
// load-bearing for a distinct build step, and each one has been observed
// missing from a partial root install:
//
// vite β bundles the renderer (`vite build`).
// katex β `src/styles.css` imports `katex/dist/katex.min.css`, so
// the CSS transform fails before a single chunk is emitted.
// electron β the runtime electron-builder packages; without it `pack`
// cannot produce an unpacked app at all.
// electron-builder β the packager `npm run builder` shells out to.
//
// Checking only `vite` (the original guard) passes a tree missing any of the
// others, which is how an incomplete install reached `vite build` and died on
// an unresolved `katex/dist/katex.min.css` with no hint that the install β not
// the source β was at fault (#86443).
//
// These four are the documented floor β always checked, even when the app's
// package.json cannot be read. The full class is wider: EVERY non-optional
// package the workspace manifest declares is something the build may import
// (`vite.config.ts` pulls `@rolldown/plugin-babel`, `@vitejs/plugin-react`,
// `@tailwindcss/vite`; `bundle-electron-main.mjs` pulls `esbuild`; the renderer
// imports the rest). A hand-maintained list drifts the moment a new import
// lands, so `checkRootInstall` unions the floor with the manifest's declared
// `dependencies` + `devDependencies` β a partial install is refused whichever
// package it happened to drop. `optionalDependencies` are excluded by design:
// npm legitimately skips them (platform-gated natives like `get-windows`).
const BUILD_CRITICAL_PACKAGES = ["vite", "katex", "electron", "electron-builder"]
export { BUILD_CRITICAL_PACKAGES }
// Resolve the way Node's own lookup does β walk `node_modules` upward β rather
// than through `require.resolve`. A package whose `exports` map does not expose
// `./package.json` is not resolvable by path even when correctly installed, and
// that must not read as "missing". Scoped names (`@scope/name`) are a nested
// directory under `node_modules`, which `join` handles.
function packageIsInstalled(name, fromDir) {
let dir = fromDir
for (;;) {
if (existsSync(join(dir, "node_modules", name, "package.json"))) return true
const parent = dirname(dir)
if (parent === dir) return false
dir = parent
}
}
// Every package the workspace manifest at `appDir` declares as required
// (`dependencies` + `devDependencies`; never `optionalDependencies`). An
// unreadable or malformed manifest yields [] β the floor still applies, and
// the build's own manifest read fails loudly on its own.
export function requiredPackages(appDir) {
try {
const manifest = JSON.parse(readFileSync(join(appDir, "package.json"), "utf8"))
return [
...Object.keys(manifest.dependencies ?? {}),
...Object.keys(manifest.devDependencies ?? {}),
]
} catch {
return []
}
}
// Pure check β returns { ok: true } or { ok: false, error: "..." }.
// Kept side-effect-free so it can be unit tested without spawning a process.
export function checkRootInstall(appDir, rootDir) {
const wanted = [...new Set([...BUILD_CRITICAL_PACKAGES, ...requiredPackages(appDir)])]
const missing = wanted.filter(pkg => !packageIsInstalled(pkg, appDir))
if (missing.length > 0) {
return {
ok: false,
error:
`the desktop build needs ${missing.join(", ")}, which the current install ` +
`does not provide. A partial root install leaves the workspace looking ` +
`present while the build cannot complete. Reinstall from the repo root: ` +
`cd ${rootDir} && npm ci`
}
}
// `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(appDir, "package.json"))
const installedVersion = pkg =>
JSON.parse(readFileSync(requireFromApp.resolve(`${pkg}/package.json`), "utf8")).version
let react
let reactDom
try {
react = installedVersion("react")
reactDom = installedVersion("react-dom")
} catch (err) {
// Both are in BUILD_CRITICAL_PACKAGES' spirit but not its list: they are
// checked by version, and an unreadable package.json is a broken install
// rather than an absent one. Report it as such instead of throwing.
return {
ok: false,
error: `could not read the installed react/react-dom versions (${err.message}). Reinstall from the repo root: cd ${rootDir} && npm ci`
}
}
if (react !== reactDom) {
return {
ok: false,
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(appDir, "package.json")}, then reinstall: cd ${rootDir} && npm ci`
}
}
return { ok: true }
}
function main() {
const app = resolve(import.meta.dirname, "..")
const root = resolve(app, "..", "..")
const result = checkRootInstall(app, root)
if (!result.ok) {
console.error(`β assert-root-install: ${result.error}`)
process.exit(1)
}
}
if (isMain(import.meta.url)) {
main()
}
|