fix(webapp): isolate webpack chunk global per build - #22
Draft
calebroseland wants to merge 1 commit into
Draft
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.chunkLoadingGlobalderives fromoutput.uniqueName, which was unset —webapp/package.jsonhas nonamefield — so both runtimes collapsed onto the bareself.webpackChunk:That
forEachmakes 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 subsequentimport()indocs_root_lazy.tsx:11then 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.clean—disthad grown to 190 files / 460 MB of stale contenthashed chunks. The serverRemoveAlls 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--modeon 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
b97ee320…b97ee320…npm testb97ee320…src/index.tsxb43b777d…b97ee320…npm run debug6b215439…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.xmlis a gitignored jest artifact that sat inside the hashed tree, sonpm testwas changing the token with zero source change.Gates:
npm run lint0 ·npm run check-types0 ·golangci-lint0 issues ·npm run build0 · CodeRabbit 0 findings.Notes for reviewers
uniqueNamecovers 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.main.jsand all chunk filenames change whenever any webapp file changes. That's inherent — the mechanism requires the string to differ per deployed build.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).