Skip to content

Commit f4f5f91

Browse files
authored
fix(frontend): drop playcanvas manualChunk + raise Docker build heap + add fork-friendly prepare hook (#184)
* fix(frontend): drop playcanvas manualChunk + raise Docker build heap The Vite config split `node_modules/playcanvas` into its own chunk for caching and parallel-fetch reasons. That split surfaced a cross-chunk module-evaluation race: a minified class identifier inside the playcanvas chunk was undefined at the moment an engine-chunk module called `new` on it, manifesting as "q is not a constructor" at runtime. The race depends on Rollup version, native-binary platform, and Node major — so the same source compiled on Mac and inside `node:20-alpine` can disagree on whether the dashboard loads. Letting Rollup co-locate playcanvas with the engine code that imports it keeps the class definition and its instantiation inside the same module record, removing the race entirely. The engine chunk is still lazy-loaded by the dashboard route so non-dashboard pages remain unaffected, and dashboard users were already loading both chunks so bytes-over-the-wire to that route are unchanged. Bundle shape after the change (verified inside `node:20-alpine`): dist/assets/engine-*.js ~3.1 MB (PlayCanvas now inlined here) dist/assets/mermaid-*.js ~4.4 MB (unchanged) dist/assets/rapier-*.js ~233 kB (unchanged) dist/assets/colyseus-*.js ~154 kB (unchanged) no dist/assets/playcanvas-*.js — split removed The larger engine chunk pushes Rollup's rendering pass close to Node's default ~1.7 GB old-space heap, so bump the Docker build to 4 GB via `NODE_OPTIONS`. Local Mac builds with `npm run build` were unaffected because npm scripts inherit a more generous default; pinning it at the Dockerfile level keeps the production build deterministic across hosts with different system memory. Signed-off-by: Arun Rajkumar <mickyarunr@gmail.com> * chore(frontend): add no-op prepare hook for downstream fork conflict avoidance Add a `prepare` script that registers `npm-merge-driver` for package-lock.json conflicts. Upstream itself has no merge conflicts to reconcile, so the registered driver is a no-op here; the value is that downstream forks of this repo can rely on the same line being present in their package.json without having to add it themselves — which would otherwise produce a one-line drift in the `scripts` block and a guaranteed `package.json` merge conflict on every fork sync that touches an adjacent script. The script only runs when invoked from inside a git working tree (`test -d ../.git`) so the Docker build (which copies only the frontend tree, not .git) is unaffected. `npx --yes` lets the package resolve on first use without a separate install step. Background: a downstream Docker deploy was bitten this week by a chain of upstream-merge -> lockfile conflict -> "regenerate the lockfile" -> silent transitive drift, which ultimately broke production. This hook is the upstream half of the fix that prevents the same shape of pain on any fork tracking this repo. Signed-off-by: Arun Rajkumar <mickyarunr@gmail.com> --------- Signed-off-by: Arun Rajkumar <mickyarunr@gmail.com>
1 parent 84ca5a6 commit f4f5f91

3 files changed

Lines changed: 23 additions & 6 deletions

File tree

frontend/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ RUN npm ci --no-audit --no-fund
1616
COPY frontend/ ./
1717
COPY shared/ /app/shared/
1818

19+
# Node's default old-space heap (~1.7 GB) is enough headroom for the
20+
# build today, but the engine chunk now bundles PlayCanvas inline (see
21+
# vite.config.ts — the dedicated playcanvas chunk was removed because
22+
# it caused a cross-chunk module-eval race in production). The larger
23+
# engine chunk pushes Rollup's rendering pass close to the default
24+
# limit; lifting to 4 GB gives comfortable margin and keeps the build
25+
# deterministic across hosts with different system memory.
26+
ENV NODE_OPTIONS="--max-old-space-size=4096"
27+
1928
RUN npx vite build && \
2029
# Ensure every static asset is world-readable. macOS sometimes hands us
2130
# 0600 files (e.g. items saved by image tools or copied with cp -p) and

frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"preview:landing": "vite preview --config vite.landing.config.ts --outDir dist-landing",
1111
"lint": "eslint . --ext .vue,.ts",
1212
"test": "vitest run",
13-
"test:watch": "vitest"
13+
"test:watch": "vitest",
14+
"prepare": "test -d ../.git && npx --yes npm-merge-driver install --driver-name=npm-merge-driver || true"
1415
},
1516
"dependencies": {
1617
"@colyseus/sdk": "^0.17.42",

frontend/vite.config.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@ export default defineConfig({
3535
build: {
3636
rollupOptions: {
3737
output: {
38-
// Split heavy 3rd-party + engine code into stable chunks so:
39-
// - dashboard route doesn't ship 1.5MB of PlayCanvas as one bundle
40-
// - engine edits don't invalidate the playcanvas chunk hash
41-
// - browser parallelises the network fetches
38+
// PlayCanvas is intentionally NOT split into its own manualChunk.
39+
// A dedicated `playcanvas` chunk surfaced a cross-chunk module-eval
40+
// race: a minified class identifier inside the playcanvas chunk
41+
// was undefined at the moment an engine-chunk module called `new`
42+
// on it, manifesting as "q is not a constructor" at runtime. The
43+
// failure depends on Rollup version, native-binary platform, and
44+
// Node major — so Mac and Linux builds can disagree on the same
45+
// source. Letting Rollup co-locate playcanvas with the engine
46+
// code that imports it keeps the class definition and its
47+
// instantiation inside the same module record, removing the race.
48+
// The engine chunk is still lazy-loaded by the dashboard route,
49+
// so non-dashboard pages remain unaffected.
4250
manualChunks(id) {
43-
if (id.includes('node_modules/playcanvas')) return 'playcanvas'
4451
if (id.includes('node_modules/@dimforge/rapier3d')) return 'rapier'
4552
if (id.includes('node_modules/colyseus.js') || id.includes('node_modules/@colyseus')) {
4653
return 'colyseus'

0 commit comments

Comments
 (0)