[GH-ISSUE #4724] [Bug] CommonJS re-exports written with Object.defineProperty are missing from export_names #2466

Closed
opened 2026-10-17 21:05:09 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @rodricksz4h5 on GitHub (Oct 17, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4724

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 drops every CommonJS re-export that Babel or Rollup emits. Both compilers write export { Client } from "./client" as Object.defineProperty(exports, "Client", { enumerable: true, get: ... }), and the semantic projection only recognises export assignments (exports.X = ...). The barrel's source module node reports export_names: ["VERSION"] with no exposes edge for Client or RenamedClient; nothing marks the list as incomplete.

tsc output is not affected in practice, because tsc also writes exports.Client = void 0 for the same names, which the assignment path already reads.

Reproduction

  1. Build a CommonJS barrel with the real compilers (Babel 8.0.7 with @babel/plugin-transform-modules-commonjs, Rollup 4.64.1 format: "cjs", both from REA's own node_modules), from:

    // index.mjs
    export * from "./types.mjs";
    export { Client } from "./client.mjs";
    export { Client as RenamedClient } from "./client.mjs";
    export const VERSION = "1.0";
    

    Babel's output for lib/index.js (abridged):

    Object.defineProperty(exports, "__esModule", { value: true });
    Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return _client.Client; } });
    Object.defineProperty(exports, "RenamedClient", { enumerable: true, get: function () { return _client.Client; } });
    exports.VERSION = void 0;
    var _client = require("./client.js");
    const VERSION = exports.VERSION = "1.0";
    
  2. Put lib/index.js, lib/client.js, lib/types.js and a package.json ("main": "lib/index.js") in a directory and run node scripts/rea.mjs analyze-javascript-application <dir> --format json.

  3. Read export_names of the lib/index.js source module node.

Expected behavior

Client and RenamedClient appear in export_names with exposes edges, as they do for the same names written as exports.Client = .... Object.defineProperty(exports, name, descriptor) defines the same own property of the exports object as the assignment. REA already reads this call as an export in its bundler-factory export collector (collectCallExports in src/domain/javascript/javascriptAstFingerprint.ts).

Impact and scope

Any Babel- or Rollup-built CommonJS package with a re-export (common in node_modules and Electron apps) loses those names from its module export inventory and exposes edges, with no limitation saying the list is incomplete. Observed for every re-export in the Babel and Rollup barrels, on every run; the tsc build of the same source keeps them through its void 0 pre-declaration. No crash; the rest of the graph is unaffected. Other compilers and real packages were not surveyed.

REA version

main c7e9e096 (6.5.0); unchanged in the affected files at 70de7021

Runtime and platform

Node 24.18.0, npm 11.16.0, Linux (node:24.18.0-bookworm container; REA built from source with npm ci and npm run compile).

Target and artifact context

Generated fixtures only: one barrel compiled by Babel, Rollup and tsc.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

lib/index.js export_names on main c7e9e096 (observed):

babel   ["VERSION"]                       exposes lib/index.js:Client: false
rollup  ["VERSION"]                       exposes lib/index.js:Client: false
tsc     ["Client","RenamedClient","VERSION"]   exposes lib/index.js:Client: true

Typed outcome or recovery expected

The defined names appear in export_names and exposes. A data descriptor ({ value: x }) is linked like exports.name = x; an accessor's result stays unknown (localName/callableId null), since the descriptor does not prove the getter's value. Receiver lifetime rules for a replaced exports/module.exports apply as they do to assignments.

Analysis or suspected cause

Source-derived: in collectSemanticModuleLink (src/domain/javascript/javascriptSemanticProjection.ts), an AssignmentExpression goes to collectCommonJsExport; a CallExpression only goes to collectUnboundRequireLink, so a defineProperty call on exports produces no commonjs-export link. The receiver index (indexCommonJsExportReceiversSteps) also only captures assignments.

Additional context

Searched open and closed issues and PRs for "defineProperty exports", "commonjs re-export", "export_names" and "tsc commonjs export". Nearest: #3552 (renamed bundler factory parameters, in the fingerprint collector) and #3566 (detached receivers); neither covers defineProperty exports in the semantic projection. esbuild's __export(...) helper form is a separate pattern and is not covered here.

Originally created by @rodricksz4h5 on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4724 ### 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` drops every CommonJS re-export that Babel or Rollup emits. Both compilers write `export { Client } from "./client"` as `Object.defineProperty(exports, "Client", { enumerable: true, get: ... })`, and the semantic projection only recognises export assignments (`exports.X = ...`). The barrel's source module node reports `export_names: ["VERSION"]` with no `exposes` edge for `Client` or `RenamedClient`; nothing marks the list as incomplete. tsc output is not affected in practice, because tsc also writes `exports.Client = void 0` for the same names, which the assignment path already reads. ### Reproduction 1. Build a CommonJS barrel with the real compilers (Babel 8.0.7 with `@babel/plugin-transform-modules-commonjs`, Rollup 4.64.1 `format: "cjs"`, both from REA's own `node_modules`), from: ```js // index.mjs export * from "./types.mjs"; export { Client } from "./client.mjs"; export { Client as RenamedClient } from "./client.mjs"; export const VERSION = "1.0"; ``` Babel's output for `lib/index.js` (abridged): ```js Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return _client.Client; } }); Object.defineProperty(exports, "RenamedClient", { enumerable: true, get: function () { return _client.Client; } }); exports.VERSION = void 0; var _client = require("./client.js"); const VERSION = exports.VERSION = "1.0"; ``` 2. Put `lib/index.js`, `lib/client.js`, `lib/types.js` and a `package.json` (`"main": "lib/index.js"`) in a directory and run `node scripts/rea.mjs analyze-javascript-application <dir> --format json`. 3. Read `export_names` of the `lib/index.js` source module node. ### Expected behavior `Client` and `RenamedClient` appear in `export_names` with `exposes` edges, as they do for the same names written as `exports.Client = ...`. `Object.defineProperty(exports, name, descriptor)` defines the same own property of the exports object as the assignment. REA already reads this call as an export in its bundler-factory export collector (`collectCallExports` in `src/domain/javascript/javascriptAstFingerprint.ts`). ### Impact and scope Any Babel- or Rollup-built CommonJS package with a re-export (common in `node_modules` and Electron apps) loses those names from its module export inventory and `exposes` edges, with no limitation saying the list is incomplete. Observed for every re-export in the Babel and Rollup barrels, on every run; the tsc build of the same source keeps them through its `void 0` pre-declaration. No crash; the rest of the graph is unaffected. Other compilers and real packages were not surveyed. ### REA version main c7e9e096 (6.5.0); unchanged in the affected files at 70de7021 ### Runtime and platform Node 24.18.0, npm 11.16.0, Linux (`node:24.18.0-bookworm` container; REA built from source with `npm ci` and `npm run compile`). ### Target and artifact context Generated fixtures only: one barrel compiled by Babel, Rollup and tsc. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence `lib/index.js` `export_names` on main c7e9e096 (observed): ``` babel ["VERSION"] exposes lib/index.js:Client: false rollup ["VERSION"] exposes lib/index.js:Client: false tsc ["Client","RenamedClient","VERSION"] exposes lib/index.js:Client: true ``` ### Typed outcome or recovery expected The defined names appear in `export_names` and `exposes`. A data descriptor (`{ value: x }`) is linked like `exports.name = x`; an accessor's result stays unknown (`localName`/`callableId` null), since the descriptor does not prove the getter's value. Receiver lifetime rules for a replaced `exports`/`module.exports` apply as they do to assignments. ### Analysis or suspected cause Source-derived: in `collectSemanticModuleLink` (`src/domain/javascript/javascriptSemanticProjection.ts`), an `AssignmentExpression` goes to `collectCommonJsExport`; a `CallExpression` only goes to `collectUnboundRequireLink`, so a `defineProperty` call on `exports` produces no `commonjs-export` link. The receiver index (`indexCommonJsExportReceiversSteps`) also only captures assignments. ### Additional context Searched open and closed issues and PRs for "defineProperty exports", "commonjs re-export", "export_names" and "tsc commonjs export". Nearest: #3552 (renamed bundler factory parameters, in the fingerprint collector) and #3566 (detached receivers); neither covers `defineProperty` exports in the semantic projection. esbuild's `__export(...)` helper form is a separate pattern and is not covered here.
Author
Owner

@XinwangTao commented on GitHub (Oct 17, 2026):

Confirmed — this matches how Babel and Rollup emit re-exports today: both produce Object.defineProperty(exports, name, { enumerable: true, get }) getters instead of plain assignment, so the assignment-only path silently drops Client/RenamedClient and, as you note, nothing marks export_names as incomplete. The tsc escape hatch (exports.Client = void 0) is a nice detail that explains why this only bites non-tsc pipelines. Tracking the getter-descriptor names in the semantic projection (and ideally flagging the list as partial) seems like the right fix — the draft PR direction looks reasonable. One thing worth double-checking: export * from re-exports can also be emitted as a helper (__exportStar/__reExport) or a defineProperty loop depending on compiler and target, so the fix should cover those forms too, not just the single-name getter pattern.

<!-- gh-comment-id:6231660598 --> @XinwangTao commented on GitHub (Oct 17, 2026): Confirmed — this matches how Babel and Rollup emit re-exports today: both produce `Object.defineProperty(exports, name, { enumerable: true, get })` getters instead of plain assignment, so the assignment-only path silently drops `Client`/`RenamedClient` and, as you note, nothing marks `export_names` as incomplete. The tsc escape hatch (`exports.Client = void 0`) is a nice detail that explains why this only bites non-tsc pipelines. Tracking the getter-descriptor names in the semantic projection (and ideally flagging the list as partial) seems like the right fix — the draft PR direction looks reasonable. One thing worth double-checking: `export * from` re-exports can also be emitted as a helper (`__exportStar`/`__reExport`) or a defineProperty loop depending on compiler and target, so the fix should cover those forms too, not just the single-name getter pattern.
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#2466
No description provided.