Skip to content

fix(webapp): isolate webpack chunk global per build - #22

Draft
calebroseland wants to merge 1 commit into
masterfrom
fix/webpack-chunk-global-collision
Draft

fix(webapp): isolate webpack chunk global per build#22
calebroseland wants to merge 1 commit into
masterfrom
fix/webpack-chunk-global-collision

Conversation

@calebroseland

Copy link
Copy Markdown
Member

Summary

Redeploying the plugin left the lazy-loaded Docs product broken until a hard refresh. Root cause is a webpack chunk-loading global shared between two live runtimes.

Mattermost hot-swaps a plugin bundle by appending a new <script> tag without removing the old one (webapp/channels/src/plugins/index.ts:177-184), so two webpack runtimes coexist in the page. output.chunkLoadingGlobal derives from output.uniqueName, which was unset — webapp/package.json has no name field — so both runtimes collapsed onto the bare self.webpackChunk:

var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));

That forEach makes the new runtime replay the old build's chunk registry: old module factories land in the new registry and chunk ids get marked already-installed. The subsequent import() in docs_root_lazy.tsx:11 then resolves against stale module ids instead of fetching.

Changes

All in webapp/webpack.config.js.

  • output.uniqueName — a SHA-256 digest of the webapp tree plus the mode, so each distinct build owns its global. The digest excludes (node_modules, dist, dotfiles, junit.xml) rather than allowlisting inputs, so adding a source directory can't silently leave the token stale and reintroduce the collision.
  • output.cleandist had grown to 190 files / 460 MB of stale contenthashed chunks. The server RemoveAlls its target then copies the entire dist directory on every deploy (server/public/plugin/environment.go:555-560), so all of it was being recopied each time.
  • mode — held 'eval-source-map', a devtool value, in the mode slot. Builds were unaffected because every npm script passes --mode on the CLI, which overrides it. This only removes a latent trap for any script that drops the flag.

Comments in the file were also cut from 43 lines to 8 single-line ones.

Verification

# Scenario Token Result
1 baseline build b97ee320…
2 rebuild, no changes b97ee320… stable
3 after full npm test b97ee320… stable
4 one line added to src/index.tsx b43b777d… changes
5 edit reverted b97ee320… returns to baseline
6 npm run debug 6b215439… distinct from production

Row 5 is the load-bearing one: reverting an edit reproduces the exact original token and a byte-identical main.js, which a timestamp or random token could not do. Row 3 covers a bug caught in review — src/junit.xml is a gitignored jest artifact that sat inside the hashed tree, so npm test was changing the token with zero source change.

Gates: npm run lint 0 · npm run check-types 0 · golangci-lint 0 issues · npm run build 0 · CodeRabbit 0 findings.

Notes for reviewers

  • This is a workaround for a core defect, not the real fix. Webpack's documented guidance for uniqueName covers different bundles sharing a page, not two versions of the same bundle — that only happens because core leaks the old runtime. The proper fix is core-side and is planned separately: the upgrade path also skips websocket, reconnect, admin-console, and translation teardown, leaving handlers registered and firing against superseded state.
  • Per-build tokens mean main.js and all chunk filenames change whenever any webapp file changes. That's inherent — the mechanism requires the string to differ per deployed build.
  • Alternatives considered and rejected: stable uniqueName (doesn't fix same-plugin upgrade), timestamp or git-SHA token (loses reproducibility; SHA also doesn't move in a dirty dev tree), post-emit content stamping (worked, but needed a custom webpack plugin and left chunk [contenthash] filenames no longer matching their bytes).

Mattermost hot-swaps a plugin bundle by appending a new script tag
without removing the previous one, so two webpack runtimes stay live in
one page. Both derived the same chunk-loading global from an unset
output.uniqueName, letting the newer runtime replay the older build's
chunk registry on boot -- marking async chunks as already installed and
resolving imports against stale module ids. This broke the lazy-loaded
Docs product after every redeploy.

Derive uniqueName from a SHA-256 digest of the webapp tree and the mode
so each distinct build owns its global, while identical sources still
reproduce the same token. The digest excludes node_modules, dist,
dotfiles, and junit.xml rather than allowlisting inputs, so a new source
directory cannot silently leave the token stale and reintroduce the
collision.

Two unrelated build-config fixes ride along, same file:
- output.clean: dist had grown to 190 files / 460MB of stale chunks, all
  of which the server recopies into the webapp plugin dir every deploy.
- mode: held a devtool value ('eval-source-map') rather than a valid
  mode. Builds were unaffected because every npm script passes --mode on
  the CLI, which overrides it; this only removes a latent trap for any
  script that drops the flag.

Context: investigating chunk-load failures after plugin redeploy.
Core-side plugin loader lifecycle defects tracked separately.
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.

1 participant