Skip to content

fix: scope parsed module sources by asset - #734

Open
Menny1337 wants to merge 3 commits into
webpack:mainfrom
Menny1337:fix/scope-parsed-sources-by-asset
Open

fix: scope parsed module sources by asset#734
Menny1337 wants to merge 3 commits into
webpack:mainfrom
Menny1337:fix/scope-parsed-sources-by-asset

Conversation

@Menny1337

@Menny1337 Menny1337 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #732.

Problem

Parsed module sources were attributed globally in two ways.

1. Module IDs were treated as globally unique. getViewerData() merged every asset's parsed
module map into one object:

Object.assign(parsedModules, bundleInfo.modules);

Webpack module IDs are unique only within a compilation. Separate compilations can reuse the same
IDs. The last parsed asset then overwrote sources from earlier assets.

2. Modules shared by several assets were changed in place. Each asset received the same stats
object for a shared module. The analyzer wrote parsedSrc to that object. The module tree read the
value after all assets were processed, so the last asset set the value for every asset.

Both cases made modules report parsed and compressed sizes from another asset.

Fix

  • Store parsed sources by asset name. Each asset reads only its own module map.
  • Use Object.hasOwn for asset and module lookups.
  • Copy the module objects for each asset before writing parsedSrc.

The asset and module indexes added in #723 remain unchanged. Lookup complexity remains linear.

Regression tests

The tests cover both failure modes.

  • Duplicate IDs across compilations: The existing
    test/stats/with-worker-loader-dynamic-import fixture has a root bundle and a worker bundle.
    Both compilations use module ID 0 for different modules.
  • One module in two assets: The new test/stats/with-module-in-multiple-assets fixture comes
    from a Webpack 5.105.2 production build. Two entry files import different exports from
    messages.js. Webpack reports the source size as 87 for both assets, but tree shaking creates
    factories of 59 and 37 characters.

The new fixture includes its source files, Webpack configuration, generated bundles, reduced
stats, and a README.

A separate test covers the fallback used when the analyzer cannot parse any bundle. The local
coverage report covers all changed statements and branches in src/analyzer.js.

test/analyzerUtils.js now checks the returned chart data instead of depending on mutation of the
input stats.

Notes

  • Patch changeset included.
  • This PR does not change the remaining mutation of the bundleStats asset array, asset names, or
    isChild flags.

AI assistance

This change was developed with meaningful AI assistance for investigation, patch drafting, and
test authoring. I reviewed and verified all code, tests, and validation output before submitting.

`getViewerData()` merged the parsed module map of every asset into a
single flat object, but Webpack module IDs are only unique within a
compilation. Assets produced by separate compilations - a main bundle
and a worker bundle, or a multi-config build - both number their
modules from `0`, so the last parsed asset overwrote the earlier ones
and modules were attributed another asset's parsed source and size.

Store parsed sources per asset name and look them up using only the
current asset's map, so each module reports the source that was
actually parsed out of the asset it belongs to.

Fixes webpack#732
@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4e4250a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-bundle-analyzer Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Scoping the parsed module maps by asset was not enough on its own. A
module that belongs to several assets is represented by a single stats
object, and every asset containing it receives that same object. The
module tree keeps a live reference to it and reads `parsedSrc` lazily,
after the per-asset loop has finished, so the asset processed last
decided what all of them displayed. `Module.mergeData()` accumulated
`size` and `parsedSrc` onto those shared objects for the same reason.

Give each asset its own copies of its modules before attributing any
parsed source, so writing a source for one asset can no longer change
what another asset reports.

`test/analyzerUtils.js` asserted on the stats objects it passed in, so
it now asserts on the returned chart data instead, matching parsed size
against the source length and pinning the source itself through its
gzip size.
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.35%. Comparing base (a5b70c3) to head (4e4250a).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #734      +/-   ##
==========================================
+ Coverage   78.58%   85.35%   +6.76%     
==========================================
  Files          17       17              
  Lines        1060     1065       +5     
  Branches      383      387       +4     
==========================================
+ Hits          833      909      +76     
+ Misses        199      142      -57     
+ Partials       28       14      -14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@valscion valscion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Can you explain what kind of source code and webpack configuration produces these test bundles and the stats.json? I'm not sure I follow the test fixture logic here. The stats.json seems misleasing already from the webpack side? The shared module seems like it should have same size in both bundles if stats.jsln stat size is to be believed but the bundle contents differ for the shared module.

Replace the hand-written shared-module fixture with output from a documented Webpack 5 build. Include the source and configuration, use purpose-based bundle names, cover the stat-only fallback, and remove the unreachable array fallback.
@Menny1337

Copy link
Copy Markdown
Contributor Author

This test reproduces a Webpack build with two entry points. Both entry points import the same
module:

// messages.js
export const longMessage = "This message is longer.";
export const shortMessage = "x";

// long-message-entry.js
import { longMessage } from "./messages.js";
console.log(longMessage);

// short-message-entry.js
import { shortMessage } from "./messages.js";
console.log(shortMessage);

messages.js is the shared module because both entry files import it.

Webpack creates one bundle for each entry:

  • long-message.js uses longMessage.
  • short-message.js uses shortMessage.

The configuration disables module concatenation, split chunks, and the separate runtime chunk.
These settings keep messages.js visible in both bundles.

Webpack gives messages.js module ID 906. The stats report its source size as 87 for both
bundles.

Webpack removes the unused export from each bundle. The emitted factory in long-message.js has
59 characters. The factory in short-message.js has 37 characters.

The stats contain one record for module ID 906 in each chunk. getBundleModules keeps one record
for that ID, so both asset lists use the same stats object.

The analyzer wrote each asset's parsedSrc value to that shared object. The last asset overwrote
the first value. The fix copies the module objects for each asset before it writes parsedSrc.

I added the source files, Webpack configuration, and a README to the fixture. I also removed the
unreachable || [] fallback and added coverage for the stat-size fallback. Local coverage includes
all changed lines and branches in src/analyzer.js.

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.

Parsed sources collide across assets, and getViewerData mutates input stats

2 participants