[GH-ISSUE #3551] [Bug] Renaming a Webpack factory's module parameter silently drops CommonJS exports #1320

Closed
opened 2026-10-16 21:04:12 +00:00 by bsakaguchi · 4 comments
Owner

Originally created by @dakjdakd on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3551

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction.

Observed behavior

analyze-javascript-application loses a statically declared CommonJS export when the first parameter of a Webpack module factory is renamed. An otherwise identical factory using the name module reports exports: ["parse"]; the renamed factory reports exports: []. Both commands finish successfully with exit code 0. The application graph reports complete coverage, and the recovered factory's observation is marked observed, exact, and complete in both results.

The trigger is ordinary identifier renaming, with no dynamic property access, obfuscation, missing source map, or malformed JavaScript involved. The factory is still recovered as module 17. This makes the empty list easy to interpret as a real absence of exports rather than an extraction failure.

Reproduction

I ran this against the published rea-agents 6.5.0 package installed in an isolated npm prefix. The two fixture directories contain one file each. Their complete contents are:

export-original/bundle.js:

globalThis.webpackChunkApp.push([[1], {17(module, exports) { module.exports = {parse: value=>value}; }}]);

export-minified/bundle.js:

globalThis.webpackChunkApp.push([[1], {17(e, t) { e.exports = {parse: value=>value}; }}]);

For a fresh local installation, the following command creates a dedicated prefix containing the affected published release. Save the two source files above in the corresponding fixture directories before running the analysis:

npm.cmd install --global --ignore-scripts --prefix ./update-frida-global rea-agents@6.5.0 --no-audit --no-fund

The following PowerShell analysis commands were executed from the directory containing those fixtures and the isolated installation. update-frida-global is the name of that local npm prefix; it is not a required installation location.

$rea = (Resolve-Path './update-frida-global/node_modules/rea-agents/scripts/rea.mjs').Path
node --version
node $rea --version
node $rea analyze-javascript-application ./export-original --json > ./submission-webpack-original.json
node $rea analyze-javascript-application ./export-minified --json > ./submission-webpack-minified.json

The runtime reported v24.11.1, and REA reported 6.5.0. Both analysis commands exited 0. In each result, select the javascript-module node whose first observation has properties.module_key === "17"; the relevant field is observations[0].properties.exports under normalized_result.graph.nodes.

Result Original factory Renamed factory
Module key "17" "17"
Export names ["parse"] []
Observation state observed observed
Observation confidence exact exact
Observation coverage complete complete
Application graph coverage complete complete
Process exit code 0 0

The result was reproduced in the released CLI and independently at the source analysis boundary. I also ran a small Node oracle on these source-owned fixtures: a replacement webpackChunkApp.push captured the module table, and calling factory 17 with { exports: {} } returned Object.keys(module.exports) === ["parse"] for both variants. This oracle execution is separate from REA's static analysis; REA did not evaluate either bundle. I have not measured a failure rate across real-world bundles.

Expected behavior

Both variants should report ["parse"]. The first argument is the module object by the recognized bundler factory convention. Its local identifier spelling does not change the role of module.exports, and the object literal exposes the same own property in both sources.

The current collector already resolves the second, exports, factory parameter using lexical binding identity. The same identity rule should apply to the first parameter. It should also reject unrelated variables named module, including nested function parameters and block-local shadows. Preserving names through minification must not introduce false exports from a different lexical binding.

Impact and scope

The observed impact is incorrect export metadata in a successful application-analysis result. A caller examining the recovered module can miss an entry point that is present in the source. Downstream filtering, module inspection, or agent reasoning based on that list can consequently overlook the exported API. Those downstream effects are implications of the returned data, rather than separately measured application failures.

This is particularly relevant to minified bundles because shortening factory parameter names is a normal transformation. The reduced public reproduction uses the Webpack chunk registration shape. The shared collector is also consumed by the Webpack/Rspack recovery path, but this report does not claim that arbitrary Rspack output, every bundler version, or all CommonJS patterns have been reproduced. Existing uncertainty elsewhere in the semantic graph is separate from the module observation described here.

REA version, runtime, and artifact context

  • Released CLI reproduction: rea-agents 6.5.0, Node 24.11.1, Windows x64.
  • Source diagnosis: also reproduced in checkout c99c6a89d6985881bcad92754f429e39718073d7.
  • Fix regression baseline: ca1b2f8608c52ed721d3576ba5eeb1d66717b08c.
  • Artifact: two synthetic JavaScript directories, each containing the single inert bundle above.
  • Interface: CLI; the proposed fix also has real stdio MCP regression coverage.
  • Earliest established stage: export extraction while recovering a bundler module, before application graph projection and CLI serialization.
  • Last known good / first known bad release: NOT_ENOUGH_INFO; no historical release bisect was performed.

Relevant output and evidence

The original released-CLI run retained these Evidence IDs:

original: ev_3988c7274ace99dc490e038bf6577f9e7759ead99595c817493b33699490c896
renamed:  ev_a690fd5230314576250afa4fa060a3d8779e44519c0815624e2ec06c0b50c53b

These identify the recorded results; they are not an expectation that a different fixture path or installation will produce identical IDs. The concise comparison above is the relevant observable difference.

Analysis and proposed ownership

The source-derived cause is in collectJavaScriptExports: it compares the second parameter by semantic binding identity, while assignments and declaration calls separately recognize the first receiver through the literal spelling module. Renaming that binding therefore falls outside both hardcoded checks. Literal-name matching can also accept a shadowed module that does not belong to the factory.

The fix belongs in this producer. Resolve the first two factory bindings once and share receiver interpretation between assignments, Object.defineProperty, and bundler export-declaration calls. The graph builder and CLI/MCP adapters should consume the corrected facts without adding their own name-based repair.

#2171 and #2234 are related to lexical identity for the second factory parameter, the direct exports receiver. This report concerns the first parameter and its .exports property, plus the remaining literal module checks. It is a distinct remaining path, not a request to reopen the already addressed second-parameter behavior. A focused accompanying fix and regression suite are prepared for review.

Implementation

The accompanying fix is available in https://github.com/morluto/rea/pull/3552, from the dedicated fix/webpack-module-export-bindings branch. The PR is a draft pending the author's final review and readiness declaration.

Originally created by @dakjdakd on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3551 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Evidence, artifacts, or reconstruction. ## Observed behavior `analyze-javascript-application` loses a statically declared CommonJS export when the first parameter of a Webpack module factory is renamed. An otherwise identical factory using the name `module` reports `exports: ["parse"]`; the renamed factory reports `exports: []`. Both commands finish successfully with exit code 0. The application graph reports complete coverage, and the recovered factory's observation is marked `observed`, `exact`, and complete in both results. The trigger is ordinary identifier renaming, with no dynamic property access, obfuscation, missing source map, or malformed JavaScript involved. The factory is still recovered as module `17`. This makes the empty list easy to interpret as a real absence of exports rather than an extraction failure. ## Reproduction I ran this against the published `rea-agents` **6.5.0** package installed in an isolated npm prefix. The two fixture directories contain one file each. Their complete contents are: `export-original/bundle.js`: ```js globalThis.webpackChunkApp.push([[1], {17(module, exports) { module.exports = {parse: value=>value}; }}]); ``` `export-minified/bundle.js`: ```js globalThis.webpackChunkApp.push([[1], {17(e, t) { e.exports = {parse: value=>value}; }}]); ``` For a fresh local installation, the following command creates a dedicated prefix containing the affected published release. Save the two source files above in the corresponding fixture directories before running the analysis: ```powershell npm.cmd install --global --ignore-scripts --prefix ./update-frida-global rea-agents@6.5.0 --no-audit --no-fund ``` The following PowerShell analysis commands were executed from the directory containing those fixtures and the isolated installation. `update-frida-global` is the name of that local npm prefix; it is not a required installation location. ```powershell $rea = (Resolve-Path './update-frida-global/node_modules/rea-agents/scripts/rea.mjs').Path node --version node $rea --version node $rea analyze-javascript-application ./export-original --json > ./submission-webpack-original.json node $rea analyze-javascript-application ./export-minified --json > ./submission-webpack-minified.json ``` The runtime reported `v24.11.1`, and REA reported `6.5.0`. Both analysis commands exited 0. In each result, select the `javascript-module` node whose first observation has `properties.module_key === "17"`; the relevant field is `observations[0].properties.exports` under `normalized_result.graph.nodes`. | Result | Original factory | Renamed factory | | --- | --- | --- | | Module key | `"17"` | `"17"` | | Export names | `["parse"]` | `[]` | | Observation state | `observed` | `observed` | | Observation confidence | `exact` | `exact` | | Observation coverage | `complete` | `complete` | | Application graph coverage | `complete` | `complete` | | Process exit code | `0` | `0` | The result was reproduced in the released CLI and independently at the source analysis boundary. I also ran a small Node oracle on these source-owned fixtures: a replacement `webpackChunkApp.push` captured the module table, and calling factory `17` with `{ exports: {} }` returned `Object.keys(module.exports) === ["parse"]` for both variants. This oracle execution is separate from REA's static analysis; REA did not evaluate either bundle. I have not measured a failure rate across real-world bundles. ## Expected behavior Both variants should report `["parse"]`. The first argument is the module object by the recognized bundler factory convention. Its local identifier spelling does not change the role of `module.exports`, and the object literal exposes the same own property in both sources. The current collector already resolves the second, `exports`, factory parameter using lexical binding identity. The same identity rule should apply to the first parameter. It should also reject unrelated variables named `module`, including nested function parameters and block-local shadows. Preserving names through minification must not introduce false exports from a different lexical binding. ## Impact and scope The observed impact is incorrect export metadata in a successful application-analysis result. A caller examining the recovered module can miss an entry point that is present in the source. Downstream filtering, module inspection, or agent reasoning based on that list can consequently overlook the exported API. Those downstream effects are implications of the returned data, rather than separately measured application failures. This is particularly relevant to minified bundles because shortening factory parameter names is a normal transformation. The reduced public reproduction uses the Webpack chunk registration shape. The shared collector is also consumed by the Webpack/Rspack recovery path, but this report does not claim that arbitrary Rspack output, every bundler version, or all CommonJS patterns have been reproduced. Existing uncertainty elsewhere in the semantic graph is separate from the module observation described here. ## REA version, runtime, and artifact context - Released CLI reproduction: `rea-agents` **6.5.0**, Node **24.11.1**, Windows **x64**. - Source diagnosis: also reproduced in checkout `c99c6a89d6985881bcad92754f429e39718073d7`. - Fix regression baseline: `ca1b2f8608c52ed721d3576ba5eeb1d66717b08c`. - Artifact: two synthetic JavaScript directories, each containing the single inert bundle above. - Interface: CLI; the proposed fix also has real stdio MCP regression coverage. - Earliest established stage: export extraction while recovering a bundler module, before application graph projection and CLI serialization. - Last known good / first known bad release: `NOT_ENOUGH_INFO`; no historical release bisect was performed. ## Relevant output and evidence The original released-CLI run retained these Evidence IDs: ```text original: ev_3988c7274ace99dc490e038bf6577f9e7759ead99595c817493b33699490c896 renamed: ev_a690fd5230314576250afa4fa060a3d8779e44519c0815624e2ec06c0b50c53b ``` These identify the recorded results; they are not an expectation that a different fixture path or installation will produce identical IDs. The concise comparison above is the relevant observable difference. ## Analysis and proposed ownership The source-derived cause is in [`collectJavaScriptExports`](https://github.com/morluto/rea/blob/ca1b2f8608c52ed721d3576ba5eeb1d66717b08c/src/domain/javascript/javascriptAstFingerprint.ts): it compares the second parameter by semantic binding identity, while assignments and declaration calls separately recognize the first receiver through the literal spelling `module`. Renaming that binding therefore falls outside both hardcoded checks. Literal-name matching can also accept a shadowed `module` that does not belong to the factory. The fix belongs in this producer. Resolve the first two factory bindings once and share receiver interpretation between assignments, `Object.defineProperty`, and bundler export-declaration calls. The graph builder and CLI/MCP adapters should consume the corrected facts without adding their own name-based repair. ## Related reports and follow-up #2171 and #2234 are related to lexical identity for the **second** factory parameter, the direct `exports` receiver. This report concerns the **first** parameter and its `.exports` property, plus the remaining literal `module` checks. It is a distinct remaining path, not a request to reopen the already addressed second-parameter behavior. A focused accompanying fix and regression suite are prepared for review. ## Implementation The accompanying fix is available in https://github.com/morluto/rea/pull/3552, from the dedicated `fix/webpack-module-export-bindings` branch. The PR is a draft pending the author's final review and readiness declaration.
Author
Owner

@x1mm4nu3l commented on GitHub (Oct 20, 2026):

Reproduced on main 2538214e2, Linux/Node 24.18.0, through CLI and stdio MCP: factory 17 reports parse with module.exports, loses it with e.exports, and retains it through the second-parameter alias t.parse. A nested function parameter named module also leaks its unrelated leak property into factory 17's export list. That negative control supports fixing lexical receiver ownership at the collector.

<!-- gh-comment-id:6286506480 --> @x1mm4nu3l commented on GitHub (Oct 20, 2026): Reproduced on main 2538214e2, Linux/Node 24.18.0, through CLI and stdio MCP: factory 17 reports parse with `module.exports`, loses it with `e.exports`, and retains it through the second-parameter alias `t.parse`. A nested function parameter named module also leaks its unrelated `leak` property into factory 17's export list. That negative control supports fixing lexical receiver ownership at the collector.
Author
Owner

@CassieFox23 commented on GitHub (Oct 21, 2026):

Thanks, dakjdakd, for the two-file fixture. From reading main at 01788bac8, not a run: still valid. The file src/domain/javascript/javascriptAstFingerprint.ts:41-57 binds the factory's second parameter as the exports alias. It does so by binding identity. But line 153 matches the first parameter only by the literal name "module". So e.exports = {...} is skipped while t.parse = ... is kept. That gives the same result as x1mm4nu3l's on 2538214e2. Also, #2171 fixed the exports parameter; this is the module parameter. PR #3552 closed on 2026-10-21 without merging. Likely area: JavaScript bundler export collection.

🤖 Written with Claude Code (Fable 5.1, verified by Opus 5.5), posted with my OK 🦊

<!-- gh-comment-id:6321734455 --> @CassieFox23 commented on GitHub (Oct 21, 2026): Thanks, dakjdakd, for the two-file fixture. From reading main at 01788bac8, not a run: still valid. The file `src/domain/javascript/javascriptAstFingerprint.ts:41-57` binds the factory's second parameter as the exports alias. It does so by binding identity. But line 153 matches the first parameter only by the literal name `"module"`. So `e.exports = {...}` is skipped while `t.parse = ...` is kept. That gives the same result as x1mm4nu3l's on 2538214e2. Also, #2171 fixed the exports parameter; this is the module parameter. PR #3552 closed on 2026-10-21 without merging. Likely area: `JavaScript bundler export collection`. 🤖 Written with Claude Code (Fable 5.1, verified by Opus 5.5), posted with my OK 🦊
Author
Owner

@kaoru0822-kitauji commented on GitHub (Oct 25, 2026):

No longer reproduces on main bc143125e. Run on macOS with Node 26.5.0, REA 6.8.0 built from source, through the public CLI.

I ran node scripts/rea.mjs analyze-javascript-application <dir> --format json on the issue's two one-file fixtures, copied word for word. Both runs exited 0 with application graph coverage complete.

Fixture Module key exports
export-original (17(module, exports)) 17 ["parse"]
export-minified (17(e, t)) 17 ["parse"]

From reading the source, not bisected: since 5cbcd5855 and f774dc420 (2026-10-24), recoverBundlerModule in src/domain/javascript/javascriptStaticAnalysisBundler.ts assigns the factory's module, exports and loader roles by parameter position. Then collectJavaScriptExports in src/domain/javascript/javascriptAstFingerprint.ts matches every receiver by the binding identity of those roles, so the literal name module no longer matters. tests/acceptance/applications/webpackRenamedExportsCli.test.ts and src/domain/javascript/javascriptFactoryExportRoles.test.ts already cover renamed module parameters through CLI and stdio MCP.

I also ran one more fixture through the same CLI command, with 15 factories in one Webpack 5 chunk. I compared each result with a Node oracle that runs each factory with {exports: {}} and a stub d and lists Object.getOwnPropertyNames(module.exports). 13 of 15 match:

  • renamed e.exports = {...}, e.exports.x =, e["exports"]["x"] =
  • Object.defineProperty(e.exports, ...), n.d(e.exports, {...})
  • arrow and function factories
  • nested function inner(e) and a block-scoped let e shadow, both correctly excluded
  • e = {exports: {}} rebinding, correctly excluded
  • a second-position parameter named module, which REA reports as export exports, as the runtime does
  • an empty factory and the plain module spelling

Remaining gap, separate from this report: a defaulted first parameter, 111(e = {}, t) { e.exports = {defaulted: 1}; }, and a destructured one, 112({exports: x}, t) { x.destructured = 1; }, both report exports: []. The module observation stays observed / exact and graph coverage stays complete, but the oracle gives ["defaulted"] and ["destructured"]. Source-derived cause: identifierAt in recoverBundlerModule takes the role only when the parameter is a plain Identifier. I have not seen a bundler emit these shapes; that is a guess, not a survey of bundlers. If they matter, they could go in a separate issue: resolve them, or mark the module's export coverage as not complete.

🤖 Written with Claude Code (Opus 5.5)

<!-- gh-comment-id:6396170214 --> @kaoru0822-kitauji commented on GitHub (Oct 25, 2026): **No longer reproduces on main `bc143125e`.** Run on macOS with Node 26.5.0, REA 6.8.0 built from source, through the public CLI. I ran `node scripts/rea.mjs analyze-javascript-application <dir> --format json` on the issue's two one-file fixtures, copied word for word. Both runs exited 0 with application graph coverage `complete`. | Fixture | Module key | `exports` | | --- | --- | --- | | `export-original` (`17(module, exports)`) | `17` | `["parse"]` | | `export-minified` (`17(e, t)`) | `17` | `["parse"]` | From reading the source, not bisected: since 5cbcd5855 and f774dc420 (2026-10-24), `recoverBundlerModule` in `src/domain/javascript/javascriptStaticAnalysisBundler.ts` assigns the factory's `module`, `exports` and loader roles by parameter position. Then `collectJavaScriptExports` in `src/domain/javascript/javascriptAstFingerprint.ts` matches every receiver by the binding identity of those roles, so the literal name `module` no longer matters. `tests/acceptance/applications/webpackRenamedExportsCli.test.ts` and `src/domain/javascript/javascriptFactoryExportRoles.test.ts` already cover renamed module parameters through CLI and stdio MCP. I also ran one more fixture through the same CLI command, with 15 factories in one Webpack 5 chunk. I compared each result with a Node oracle that runs each factory with `{exports: {}}` and a stub `d` and lists `Object.getOwnPropertyNames(module.exports)`. 13 of 15 match: - renamed `e.exports = {...}`, `e.exports.x =`, `e["exports"]["x"] =` - `Object.defineProperty(e.exports, ...)`, `n.d(e.exports, {...})` - arrow and `function` factories - nested `function inner(e)` and a block-scoped `let e` shadow, both correctly excluded - `e = {exports: {}}` rebinding, correctly excluded - a second-position parameter named `module`, which REA reports as export `exports`, as the runtime does - an empty factory and the plain `module` spelling **Remaining gap, separate from this report:** a defaulted first parameter, `111(e = {}, t) { e.exports = {defaulted: 1}; }`, and a destructured one, `112({exports: x}, t) { x.destructured = 1; }`, both report `exports: []`. The module observation stays `observed` / `exact` and graph coverage stays `complete`, but the oracle gives `["defaulted"]` and `["destructured"]`. Source-derived cause: `identifierAt` in `recoverBundlerModule` takes the role only when the parameter is a plain `Identifier`. I have not seen a bundler emit these shapes; that is a guess, not a survey of bundlers. If they matter, they could go in a separate issue: resolve them, or mark the module's export coverage as not complete. 🤖 Written with Claude Code (Opus 5.5)
Author
Owner

@morluto commented on GitHub (Oct 26, 2026):

Thank you for the precise reproduction and lexical-identity analysis. Main now binds Webpack factory roles by argument position rather than identifier spelling (including the shared role owner in #7117). The current source regression includes your exact renamed e.exports = {parse: ...} shape and compares it with a runtime oracle; public CLI/stdio MCP coverage also exercises factory export roles. I am closing this as fixed. This triage checked the source and existing coverage; it did not rerun the published Windows package.

<!-- gh-comment-id:6428561713 --> @morluto commented on GitHub (Oct 26, 2026): Thank you for the precise reproduction and lexical-identity analysis. Main now binds Webpack factory roles by argument position rather than identifier spelling (including the shared role owner in #7117). The current source regression includes your exact renamed `e.exports = {parse: ...}` shape and compares it with a runtime oracle; public CLI/stdio MCP coverage also exercises factory export roles. I am closing this as fixed. This triage checked the source and existing coverage; it did not rerun the published Windows package.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1320
No description provided.