`assert-dist-built.mjs` validated the emitted renderer chunks by spawning `node --input-type=module --check` once per chunk. A real bundle here emits ~960 `.js` chunks (31 MB), so the postbuild step performed ~960 sequential process creations: 47-80 s wall clock measured on an idle machine (median 77 ms per spawn, worst 446 ms). That made the update's build step depend on ~960 successful process creations. On Windows one spawn stalled past its 60 s budget and returned ETIMEDOUT (`could not run node to syntax-check nord-D92QzZWU.js`), which failed `npm run build` and aborted the entire update (receipt outcome "failed", exit 1) even though vite had reported `built in 54.73s`. Nothing was wrong with the bundle or with node - a single process creation was slow. Parse every chunk in one child instead: `scripts/check-chunks-parse.mjs` uses `vm.SourceTextModule` - the in-process module parser, hence `--experimental-vm-modules` - and writes a JSON verdict on stdout. It reports the same early errors as `node --check`: invalid destructuring patterns and duplicate declarations still fail, so the guard keeps catching the corrupted-silent-fail bundle it was written for. Same verification now costs one process creation and ~1.5 s instead of ~960 spawns and 47-80 s. The verdict carries `kind`, and the guard reports `harness` when the check could not run versus `bundle` when the build is defective. The line is drawn at the checker: only a SyntaxError is a bundle defect; an unreadable chunk or an unavailable vm API is a failure of the check itself. Previously every failure printed "The renderer bundle is missing or incomplete ... Re-run the build", so a blocked or stalled node process was reported as a bundle defect and sent at least one investigation after a problem that did not exist.
75 lines
3.3 KiB
JavaScript
75 lines
3.3 KiB
JavaScript
// Parse every emitted renderer chunk as an ES module in ONE node process.
|
|
//
|
|
// assert-dist-built.mjs used to spawn `node --input-type=module --check` once per
|
|
// chunk. With ~1000 emitted chunks the update's desktop build therefore depended on
|
|
// ~1000 successful process creations, and on Windows a single spawn can stall for
|
|
// minutes under AV scanning or memory pressure. One such stall aborted the whole
|
|
// update (2026-09-27: `could not run node to syntax-check nord-*.js: spawnSync
|
|
// ...node.exe ETIMEDOUT`, which failed the update with exit 1 even though vite had
|
|
// built the bundle cleanly). Measured on the failing tree: 960 chunks, ~80 s of
|
|
// sequential spawns, worst single spawn 0.45 s on an idle box.
|
|
//
|
|
// `vm.SourceTextModule` is the in-process ES module parser, so the same check now
|
|
// costs one spawn. It reports the same early errors as `node --check` (invalid
|
|
// destructuring patterns such as the `{,:n}` token-drop this guard was written for,
|
|
// duplicate declarations) and throws SyntaxError for them. The caller passes
|
|
// --experimental-vm-modules, which the vm module API requires.
|
|
//
|
|
// Usage: node --experimental-vm-modules check-chunks-parse.mjs <assetsDir>
|
|
// stdout: {"ok":true,"checked":N}
|
|
// | {"ok":false,"kind":"bundle","name":...,"detail":...} the chunk does not parse
|
|
// | {"ok":false,"kind":"harness","name"?:...,"detail":...} this CHECK could not run
|
|
// `kind` is what lets the guard report the truth: only a SyntaxError means the bundle is
|
|
// defective. A missing vm API (a node build ignoring --experimental-vm-modules) or an
|
|
// unreadable chunk is a failure of the CHECK, and must not be reported as invalid bundle
|
|
// syntax — that misattribution is exactly what the guard's `harness` branch exists to stop.
|
|
import { readFileSync, readdirSync } from "fs"
|
|
import { join } from "path"
|
|
import vm from "vm"
|
|
|
|
function report(payload) {
|
|
process.stdout.write(JSON.stringify(payload))
|
|
}
|
|
|
|
// The check could not run (no directory, unreadable chunk, vm API unavailable): say so
|
|
// without claiming anything about the bundle's syntax.
|
|
function harnessFailure(detail, name) {
|
|
report(name ? { ok: false, kind: "harness", name, detail } : { ok: false, kind: "harness", detail })
|
|
process.exit(2)
|
|
}
|
|
|
|
const assetsDir = process.argv[2]
|
|
if (!assetsDir) {
|
|
harnessFailure("no assets directory given")
|
|
}
|
|
|
|
try {
|
|
const chunks = readdirSync(assetsDir).filter(name => name.endsWith(".js"))
|
|
for (const name of chunks) {
|
|
let source
|
|
try {
|
|
source = readFileSync(join(assetsDir, name), "utf8")
|
|
} catch (err) {
|
|
harnessFailure(`${err.name || "Error"}: ${String(err.message || err)}`, name)
|
|
}
|
|
try {
|
|
new vm.SourceTextModule(source, { identifier: name })
|
|
} catch (err) {
|
|
const detail = `${err.name || "Error"}: ${String(err.message || err)}`
|
|
.split("\n")
|
|
.slice(0, 4)
|
|
.join(" / ")
|
|
if (err instanceof SyntaxError) {
|
|
report({ ok: false, kind: "bundle", name, detail })
|
|
process.exit(1)
|
|
}
|
|
// e.g. `TypeError: vm.SourceTextModule is not a constructor` when this node build does
|
|
// not honour --experimental-vm-modules: the check did not run, the bundle is not at fault.
|
|
harnessFailure(detail, name)
|
|
}
|
|
}
|
|
report({ ok: true, checked: chunks.length })
|
|
} catch (err) {
|
|
harnessFailure(`${err.name || "Error"}: ${err.message || err}`)
|
|
}
|