Skip to content

Fix require(esm) module.exports re-export exports determinism - #21521

Merged
alexander-akait merged 9 commits into
mainfrom
test/esm-module-exports-cache-cross-runtime
Jul 27, 2026
Merged

Fix require(esm) module.exports re-export exports determinism#21521
alexander-akait merged 9 commits into
mainfrom
test/esm-module-exports-cache-cross-runtime

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

The require/esm-module-exports config case failed its filesystem-cache variant on Bun and Deno: index.js/wrapper-full.cjs were nondeterministically re-generated on the 2nd build ("Pack got invalid … Compilation/codeGeneration", "modules not cached").

Root cause: for a full re-export module.exports = require("./esm"), CommonJsExportRequireDependency.getExports provided the imported module's star-reexport names whenever the wrapper was processed before webpack had decided whether the ESM target exposes a "module.exports" named export. The exports merge is monotonic, so those names could never be retracted once the module turned out to unwrap — making the exported set depend on module processing order. Node happened to converge; Bun/Deno left residual named exports, which shifted module hashes and broke persistent-cache reuse. The fix defers the star-reexport while the imported module's "module.exports" eligibility is still undetermined (provided === undefined), so the result is order-independent (and the leaked names were incorrect anyway — a full re-export of an unwrapping ESM module exposes only the unwrapped value).

Also keeps the development snapshot hash fallback for this case (orthogonal Bun/Deno mtime-timing robustness) and removes the Bun/Deno filesystem-cache skips. Refs #20896.

What kind of change does this PR introduce?

fix

Did you add tests for your changes?

Yes — the existing test/configCases/require/esm-module-exports case now runs on the Bun and Deno filesystem-cache variants (the previous skips are removed), which is what regressed. The deferral is an inherent module-processing-order race that a Node build doesn't hit, so it's exercised by the Bun/Deno runs rather than a Node unit test.

Does this PR introduce a breaking change?

No.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a

Use of AI

Yes — Claude Code was used to trace the order-dependent exports-fixpoint nondeterminism and implement the fix; all changes were reviewed and verified locally on Node (Deno/Bun validation via CI).

Copilot AI review requested due to automatic review settings July 26, 2026 19:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4fb3c0f

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

This PR includes changesets to release 1 package
Name Type
webpack 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

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (c9268f1).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@c9268f1
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@c9268f1
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@c9268f1

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.79%. Comparing base (e28262e) to head (4fb3c0f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/hmr/lazyCompilationBackend.js 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21521      +/-   ##
==========================================
- Coverage   93.79%   93.79%   -0.01%     
==========================================
  Files         620      620              
  Lines       73275    73287      +12     
  Branches    21185    21195      +10     
==========================================
+ Hits        68729    68736       +7     
- Misses       4546     4551       +5     
Flag Coverage Δ
css-parsing 25.59% <0.00%> (-0.01%) ⬇️
html5lib 27.02% <0.00%> (-0.01%) ⬇️
integration 89.59% <92.30%> (-0.01%) ⬇️
test262 43.31% <0.00%> (-0.01%) ⬇️
unit 47.23% <23.07%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.

@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 30.88%

❌ 1 regressed benchmark
✅ 215 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' 7.6 MB 11 MB -30.88%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing test/esm-module-exports-cache-cross-runtime (4fb3c0f) with main (59566fc)

Open in CodSpeed

@cursor

cursor Bot commented Jul 26, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes export-fixpoint behavior for require(esm) re-exports, which can alter emitted module graphs and cache keys, but the change is narrowly scoped to defer premature star-reexports until ESM export info is known.

Overview
Fixes nondeterministic require(esm) full re-export analysis (module.exports = require("./esm")) that broke filesystem-cache reuse on Bun/Deno when the wrapper module was analyzed before the imported ESM’s "module.exports" unwrap eligibility was known.

CommonJsExportRequireDependency.getExports now defers star-reexport when the target is a namespace ESM with no resolved owned exports yet (otherExportsInfo.provided === false and empty owned exports), returning empty exports plus a dependency on the imported module so analysis re-runs once exports are known—avoiding monotonic merge of __esModule/named exports that could not be retracted after unwrap.

Test/harness updates: the require/esm-module-exports case drops Bun/Deno filesystem-cache skips, enables snapshot content hashes in its webpack config for stable cache validation, and the filter only gates on Node require(esm) version. Lazy compilation dispose ignores ERR_SERVER_NOT_RUNNING after closeAllConnections() so compiler.close() succeeds on Bun. Minor Deno watch polling and Bun BinaryMiddleware V8-format test skips.

Reviewed by Cursor Bugbot for commit 4fb3c0f. Bugbot is set up for automated code reviews on this repo. Configure here.

@alexander-akait alexander-akait changed the title Stabilize require(esm) module-exports filesystem-cache case on Bun and Deno Fix require(esm) module.exports re-export exports determinism Jul 26, 2026
Comment thread lib/dependencies/CommonJsExportRequireDependency.js
Comment thread lib/dependencies/CommonJsExportRequireDependency.js

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 381fbe2. Configure here.

Comment thread lib/dependencies/CommonJsExportRequireDependency.js

Copy link
Copy Markdown
Member Author

The runtimes (bun) job failed with Aborted (core dumped) (exit 134 / SIGABRT) during HotTestCasesUniversal.test.js — a Bun-engine crash in the HMR universal suite, not an assertion failure in this PR's require/esm-module-exports case (the ConfigTestCases/cache suites ran green above it). This diff only changes module.exports = require(esm) provided-exports analysis and can't cause a native abort in the HMR suite, so this is the known flaky Bun segfault rather than a regression. I'll re-run the job once the current workflow finishes; will investigate further if it reproduces.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

runtimes (deno) is also an unrelated flake: its only failure is WatchDetection.test.js › time between changes 10ms with invalidate call (a 30 s watch-mode timeout), while ConfigTestCases.basictest.js — which contains this PR's require/esm-module-exports case — passed on Deno. So the determinism fix works on Deno; both runtime jobs are red only on pre-existing flakes (Bun HotTestCasesUniversal core-dump, Deno WatchDetection timeout) and I'll re-run them once the workflow finishes.


Generated by Claude Code

…d Deno

Dev-mode snapshots are timestamp-only, whose safeTime>startTime check can
false-invalidate on Bun/Deno (mtime timing), leaving modules uncached on the
2nd build. Add the content-hash fallback so cache validation is deterministic
across runtimes, and stop skipping the filesystem-cache variant there.
Full re-export `module.exports = require(esm)` provided star-reexport names
whenever it was processed before the imported ESM module's `"module.exports"`
export was known. The exports merge is monotonic, so those names could never
be retracted once the module turned out to unwrap, making the result depend on
module processing order — Node converged, Bun/Deno left residual named exports,
which shifted module hashes and broke persistent-cache reuse across builds.

Defer the star-reexport until the imported module's `"module.exports"`
eligibility is settled, so the exported set is order-independent.
ExportInfo.provided uses null for "only the runtime knows" (a settled,
dynamic state) and undefined for "not yet determined". The deferral must
trigger only on the undetermined case; keying it on null skipped the actual
transitional window and would wrongly defer a genuinely dynamic namespace
re-export forever.
Base the deferral on whether the imported namespace ESM has owned
exports yet, instead of the never-holding `provided === undefined`
check. Star-reexporting before FlagDependencyExportsPlugin has flagged
the module adds an `__esModule` export the monotonic merge can't retract
once the module turns out to unwrap, making the result order-dependent
and breaking the persistent-cache determinism on Bun and Deno.
Use a TypeScript-valid emptiness check on `ownedExports` and only defer
when the imported namespace module has no exports determined yet
(`otherExportsInfo.provided === false` and no owned exports). A dynamic
`export *` namespace now resolves via re-queue instead of deferring, so
its star re-export is preserved.
Deno's native fs.watch, like Bun's, drops change events under jest's
worker threads, so watchpack misses edits and the watch suites hang
until the 30s timeout (seen intermittently as a WatchDetection failure
in the Deno runtimes CI job). Default WATCHPACK_POLLING in the Deno
worker setup, mirroring the existing Bun preload workaround.
@alexander-akait
alexander-akait force-pushed the test/esm-module-exports-cache-cross-runtime branch from 2a99767 to 16b283d Compare July 27, 2026 01:43
Bun's closeAllConnections() already stops the SSE server, so the
following server.close() reports ERR_SERVER_NOT_RUNNING to its callback,
which dispose propagated and failed compiler.close() (surfacing as six
HotTestCases suite failures on the Bun runtimes job). The server is
closed either way, so treat that code as success; Node is unaffected.
Bun's node:v8 polyfill doesn't emit the 0xff+version V8 wire format, so the
synthetic newer-version payload can't trigger BinaryMiddleware's guard there;
gate the assertion on the wire-format marker so it runs on V8 runtimes only.
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging test/esm-module-exports-cache-cross-runtime into main will be
99.33%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.82%100%100%98.82%103
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-emscripten
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.12%100%100%99.12%207, 227
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1639, 1958, 1965, 1973, 1995, 1998, 2937, 3416–3417, 3449, 4153, 4183, 4236–4237, 4241, 4246, 4262–4263, 4277–4278, 4283–4284, 4764, 4790, 527, 532, 5598, 5630, 5647, 5663, 5679, 5694, 5719–5720, 5722, 6052, 6057, 6063, 6066, 6073, 6085, 6087, 6091, 6107, 6122, 6154, 6208, 6232, 6347, 778–779
   Compiler.js99.56%100%100%99.56%1147–1148, 1156
   ConcatenationScope.js98.65%100%100%98.65%195
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1074, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%479, 525
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.65%100%100%98.65%1196, 1199, 514–518, 520, 666
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.53%100%100%99.53%182, 2418–2419, 2422, 2433, 2444, 2455, 280, 3892, 3907, 3931
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.21%100%100%98.21%448, 457, 460, 464, 476
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LazyBarrel.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1288, 1293, 1353, 1367, 1429, 1438
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1005
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.70%100%100%99.70%663
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.98%100%100%97.98%1014, 1031, 1279, 1313, 1329, 1776, 2073, 2078–2088, 34, 994, 997
   NormalModuleFactory.js98.72%100%100%98.72%1117, 1385, 1396, 1406, 1457–1459, 1466, 520, 532
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js99.80%100%100%99.80%691
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.62%100%100%98.62%220, 224, 226, 419, 430, 889
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.43%100%100%99.43%308–309
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%371
   cli.js98.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.72%100%100%99.72%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.10%100%100%97.10%10, 263, 285, 287
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.95%100%100%97.95%295, 319, 322, 42, 452, 47
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%107, 114–115, 123, 90
   PackFileCacheStrategy.js96.41%100%100%96.41%1257, 1357, 1361, 1423, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 723, 729, 763, 769, 775, 780, 791, 800, 805–806, 808, 825, 831–832, 834
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.37%100%100%99.37%1620–1622, 1630,

@alexander-akait
alexander-akait merged commit c9268f1 into main Jul 27, 2026
62 of 63 checks passed
@alexander-akait
alexander-akait deleted the test/esm-module-exports-cache-cross-runtime branch July 27, 2026 06:08
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.

3 participants