Files
hermes-agent/apps/desktop/scripts/check-chunks-parse.mjs
Chip 94c15c7b70 fix(desktop): syntax-check every renderer chunk in one process
`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.
2026-10-01 15:28:55 -07:00

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}`)
}