Skip to content

fix: stop webpack from rewriting unquoted chunk names - #854

Merged
katomm merged 4 commits into
cardano-foundation:stagingfrom
lodl:fix-geman
Sep 11, 2026
Merged

katomm merged 4 commits into
cardano-foundation:stagingfrom
lodl:fix-geman

Conversation

@lodl

@lodl lodl commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Checklist

  • I have read the Contributing Guidelines.
  • I have run yarn build after adding my changes without getting any errors.
  • I have not committed any changes to yarn.lock.

Updating documentation or Bugfix

The German site is broken in production right now: /de/ ships a bundle that does not parse, so nothing hydrates.

Every German page logs two errors:

SyntaxError: Invalid or unexpected token
  https://cardano.org/de/assets/js/runtime~main.270c1a64.js:0:55122
  https://cardano.org/de/assets/js/main.12b97c92.js:1:179994

React therefore never hydrates: the navbar toggle does nothing, the homepage hero animation never mounts (#medusa-root stays empty), and the locale dropdown and search are dead. en, ja, es and vi are unaffected. A deploy after merge fixes it.

Root cause

One object key in the emitted German bundles lost its quotes:

...,"4e2ae1fb":"13383",4003635d:"13401",ae7589f1:"13459",...

fe25bf46 is a chunk name: Docusaurus names route chunks simpleHash(modulePath, 8), which looks exactly like an 8-character content hash. Webpack's RealContentHashPlugin replaces content hashes by plain string replacement in every emitted asset, so it cannot tell a hash from a chunk name that has the same shape: it also replaced this name (with the real hash 4003635d) wherever the string appeared, including as an object key. Terser had already dropped the quotes around that name (it is a valid identifier, so unquoting is legal), so the replacement produced 4003635d: -> Identifier directly after number -> the whole bundle is unparsable.

Stage trace from the German client compilation:

webpack stage quoted fe25bf46 bare 4003635d asset parses
PROCESS_ASSETS_STAGE_OPTIMIZE (before Terser) yes - yes
PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE (after Terser) unquoted (legal) - yes
PROCESS_ASSETS_STAGE_OPTIMIZE_HASH (RealContentHashPlugin) gone rewritten, unquoted no

Only de broke because only in that locale did an intermediate content hash collide with a chunk name. The build itself stayed green, so nothing caught it before deploy.

Why not just redeploy?

A deploy with new content probably makes the symptom disappear: the collision is with an
intermediate content hash, and any content change shuffles those hashes. That is luck
though, not a fix - the same deployment shows it:

  • the identical chunk name fe25bf46 (same module, @site/blog/2021-09-20-september/index.md?truncated=true) was rewritten in the German bundle (fe25bf46 0x, 4003635d 3x) and left untouched in the English one (fe25bf46 3x, 4003635d 0x),
  • so the collision depends on what else happens to be in that compilation, not on the code.

Any future content or dependency change can collide again, in any locale, and the build
stays green while that locale is dead in the browser. This PR removes the mechanism (no
more textual hash rewriting) and adds the guard, so a recurrence fails the build instead
of reaching users.

Approach

  1. docusaurus.config.js: optimization: { realContentHash: false } in the existing custom-webpack-config plugin, with a comment explaining why. Docusaurus deep-merges configureWebpack results (mergeWithCustomize), so minimize, minimizer and splitChunks are untouched - verified: the output is still minified, common.*.js and *.LICENSE.txt are still emitted. Filenames keep a [contenthash:8]; it is just no longer recomputed by a second textual pass.

  2. scripts/check-js-assets.js (new, 48 lines, no dependency - parses with Node's own vm.Script): exits non-zero when any emitted JS asset does not parse.

    yarn test:js-assets                 # or: node scripts/check-js-assets.js [dir]
    # -> check-js-assets: 7415 JS assets parsed successfully.
  3. package.json: the guard runs at the end of yarn build, so both CI (yarn build --locale en) and the deploy build fail on an unparsable asset. It is deliberately not part of yarn test: CI runs yarn test before any build exists, so the check would fail there for the wrong reason.

Note on CI

.github/workflows/yarn-build.yml runs yarn build --locale en. Yarn appends extra arguments to the end of a chained script, so the flag never reaches docusaurus build (measured: that command runs five client compilations and emits build/de, build/es, build/ja, build/vi as well - which is why CI can catch a locale-specific asset like this one). The guard therefore takes its directory explicitly (node scripts/check-js-assets.js build) and ignores any further arguments, so yarn build --locale en stays green: verified, exit 0, 7415 JS assets parsed successfully.

Verification

  • yarn build (all locales) before the fix: 2 of 7415 assets unparsable (de/assets/js/main, de/assets/js/runtime~main). After the fix: check-js-assets: 7415 JS assets parsed successfully.
  • Served the production build locally: /de/ renders German with a clean console, the hero canvas mounts, the mobile menu opens.
  • yarn build --locale en (the exact CI command): exit 0, 7415 JS assets parsed successfully.
  • Negative test: a bundle containing a bare 4003635d: key makes the guard exit 1 with Invalid or unexpected token; a non-existent directory also exits 1.

How to test

  1. yarn build
  2. yarn serve, then open http://localhost:3000/de/ - hero animation runs, menu works, console stays clean (before this change: two Invalid or unexpected token errors).
  3. Optional: node scripts/check-js-assets.js build/de against the old build -> exit 1.

Notes

  • The underlying defect is already tracked upstream: webpack issue #14058 (circular hash dependency) and the open PR #19110 "fix: real content hash accidentally overwrite user's code". Once that ships, realContentHash: false can be dropped again; the guard stays useful either way.
  • No documentation change needed: the guard is documented in its own header and the workaround is explained inline in the config. Happy to add a line to the contributor docs if you prefer it there.

Files changed

  • docusaurus.config.js (+14)
  • package.json (+3/-1)
  • scripts/check-js-assets.js (new, 50 lines)

(3 files changed, 64 insertions, 1 deletion)

lodl and others added 4 commits September 11, 2026 22:03
RealContentHashPlugin replaces content hashes by string replacement in every emitted asset. Route chunk names are simpleHash(modulePath, 8) - the same shape as a hash - so a colliding name was rewritten as well, after Terser had already unquoted it. That produced an invalid object key (4003635d:"13401") and made the whole German bundle unparsable: no React hydration, so dead menu and dead hero animation on every /de page, while the build stayed green. Disabling realContentHash keeps the chunk names intact.
Parses every build/**/assets/js/*.js with Node's own parser (vm.Script, no dependency). Runs at the end of yarn build, not in yarn test, because CI calls yarn test before a build exists.
`yarn build --locale en` (the CI invocation) appends the flag to the end of the script chain, so the guard received --locale as its directory and exited 1, while docusaurus build kept building every locale. Take the directory explicitly and ignore further arguments.

Also fall back to entry.path for Node < 20.12 (.node-version pins 20.0, where Dirent.parentPath does not exist yet) and correct the header comment: fe25bf46 was the chunk name, 4003635d the hash that replaced it.
@katomm
katomm merged commit b9c1769 into cardano-foundation:staging Sep 11, 2026
1 check passed
@lodl
lodl deleted the fix-geman branch September 12, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants