[GH-ISSUE #2021] [Bug] Relative ESM imports invent extension and directory fallback targets #671

Closed
opened 2026-10-15 18:27:14 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @xu91102 on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2021

Before submitting

  • Searched open/closed issues and recent/open PRs; this is a different behavior from #1939 and the open package-pattern work #2018.
  • Uses source-owned fixtures with no credentials or private target data.

Area

Evidence, artifacts, or reconstruction — JavaScript application analysis.

Observed behavior

Node exits 1 with ERR_MODULE_NOT_FOUND. REA reports ./feature resolved to feature.js. A directory with only index.js and a missing explicit path with a .js.js decoy produce the same false resolution; Node refuses those imports too.

Reproduction

In a built source checkout, materialize the fixture:

node --input-type=module <<'JS'
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
const root = resolve("fixture");
const files = {
  "entry.mjs": "import value from \"./feature\"; console.log(value);",
  "feature.js": "module.exports = 42;"
};
for (const [path, text] of Object.entries(files)) {
 const target = resolve(root,path);
 mkdirSync(dirname(target),{recursive:true});
 writeFileSync(target,text);
}
JS
node fixture/entry.mjs
node scripts/rea.mjs analyze-javascript-application "$(pwd)/fixture" --format json > observed.json

Inspect normalized_result.graph.edges for specifier, resolved_path, and resolution_status, or source-map-original nodes and the graph coverage. The affected source checkout was built and the public CLI was executed on these exact fixture shapes before this report.

Expected behavior

Apply exact ESM URL lookup to relative JavaScript imports. Missing filenames and directories must remain explicit failures instead of selecting appended extensions or an index. Preserve TypeScript emitted-source substitution and CommonJS lookup.

Format/loader reference. Runtime/decoder controls were executed rather than inferred from documentation.

Impact and scope

Static dependency graphs disagree with actual Node module lookup. No live browser or native engine is required to reproduce the local directory-analysis failure.

Version and runtime

  • Source revision: 2ff850de68dc9b05c6faf2bbfd26d56bc2d5d27c (main at investigation start).
  • Node 24.18.0, npm 11.16.0, macOS 27.0.1 arm64.
  • Interface: compiled CLI.
  • Target: source-owned local directory.

Source-derived analysis

The relative module URL branch falls into the generic resolveCandidate extension and directory machinery.

Originally created by @xu91102 on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2021 ### Before submitting - [x] Searched open/closed issues and recent/open PRs; this is a different behavior from #1939 and the open package-pattern work #2018. - [x] Uses source-owned fixtures with no credentials or private target data. ### Area Evidence, artifacts, or reconstruction — JavaScript application analysis. ### Observed behavior Node exits 1 with ERR_MODULE_NOT_FOUND. REA reports ./feature resolved to feature.js. A directory with only index.js and a missing explicit path with a .js.js decoy produce the same false resolution; Node refuses those imports too. ### Reproduction In a built source checkout, materialize the fixture: ```sh node --input-type=module <<'JS' import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; const root = resolve("fixture"); const files = { "entry.mjs": "import value from \"./feature\"; console.log(value);", "feature.js": "module.exports = 42;" }; for (const [path, text] of Object.entries(files)) { const target = resolve(root,path); mkdirSync(dirname(target),{recursive:true}); writeFileSync(target,text); } JS node fixture/entry.mjs node scripts/rea.mjs analyze-javascript-application "$(pwd)/fixture" --format json > observed.json ``` Inspect `normalized_result.graph.edges` for `specifier`, `resolved_path`, and `resolution_status`, or source-map-original nodes and the graph coverage. The affected source checkout was built and the public CLI was executed on these exact fixture shapes before this report. ### Expected behavior Apply exact ESM URL lookup to relative JavaScript imports. Missing filenames and directories must remain explicit failures instead of selecting appended extensions or an index. Preserve TypeScript emitted-source substitution and CommonJS lookup. [Format/loader reference](https://nodejs.org/api/esm.html#mandatory-file-extensions). Runtime/decoder controls were executed rather than inferred from documentation. ### Impact and scope Static dependency graphs disagree with actual Node module lookup. No live browser or native engine is required to reproduce the local directory-analysis failure. ### Version and runtime - Source revision: `2ff850de68dc9b05c6faf2bbfd26d56bc2d5d27c` (main at investigation start). - Node 24.18.0, npm 11.16.0, macOS 27.0.1 arm64. - Interface: compiled CLI. - Target: source-owned local directory. ### Source-derived analysis The relative module URL branch falls into the generic resolveCandidate extension and directory machinery.
bsakaguchi 2026-10-15 18:27:14 +00:00
Author
Owner

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

TL;DR: I could not reproduce this on main. I did see the bug on the commit just before the fix. The fix is c0904b4c1.

Thanks, xu91102, for the clear report. I ran your fixture on Linux. The ./dir and ./feature.js columns come from your prose, so I built those files myself.

Run ./feature ./dir ./feature.js
Node 22.23.1 error error error
Node 24.18.0 error error error
Node 26.11.1 error error error
REA before the fix resolved resolved resolved
REA on main not found not found not found

REA before the fix is d9d2a1738. REA on main is d96550f2f.

The fixture for ./feature holds only feature.js. The one for ./dir holds only dir/index.js. The one for ./feature.js holds only feature.js.js. Node exits 1 each time.

Not run

  • The TypeScript and CommonJS cases you want kept.
  • The MCP interface. I used the compiled CLI only.
  • macOS and Windows.
Commands

Fixture files, as in your issue, under /tmp/nodebatch/fx/2021 (the ./feature column):

entry.mjs     import value from "./feature"; console.log(value);
feature.js    module.exports = 42;

The other two use the same entry.mjs with the import changed, and these files:

2021-extra-dir-index/entry.mjs        import value from "./dir"; console.log(value);
2021-extra-dir-index/dir/index.js     module.exports = 42;
2021-extra-js-js-decoy/entry.mjs      import value from "./feature.js"; console.log(value);
2021-extra-js-js-decoy/feature.js.js  module.exports = 42;

Node error codes, all three versions:

./feature, ./feature.js:  ERR_MODULE_NOT_FOUND
./dir:                    ERR_UNSUPPORTED_DIR_IMPORT
# build each tree (Node 24.18.0 first on PATH, npm 11.16.0)
npm ci && npm run build && npm run test:prepare

# Node, exit codes read from each run
/usr/bin/node /tmp/nodebatch/fx/2021/entry.mjs                                  # 22.23.1 (also run with 24.18.0 and 26.11.1)

# REA, once per tree
node /tmp/nodebatch/main/scripts/rea.mjs analyze-javascript-application /tmp/nodebatch/fx/2021 --format json > main-2021.json
node /tmp/nodebatch/base/scripts/rea.mjs analyze-javascript-application /tmp/nodebatch/fx/2021 --format json > base-2021.json

REA fields, as seen in the graph edges:

d9d2a1738 (before the fix), resolution_status "resolved":
  ./feature     resolved_path "feature.js"
  ./dir         resolved_path "dir/index.js"
  ./feature.js  resolved_path "feature.js.js"
d96550f2f (main), all three imports: resolution_status "not-found", resolved_path null

Both trees print 6.4.0 for --version, so the version string does not tell them apart. The shas do.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 22.23.1, 24.18.0, 26.11.1
  • Head / base: d96550f2f (main) / d9d2a1738 (the parent of c0904b4c1)

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6129431978 --> @CassieFox23 commented on GitHub (Oct 12, 2026): **TL;DR:** I could not reproduce this on main. I did see the bug on the commit just before the fix. The fix is c0904b4c1. Thanks, xu91102, for the clear report. I ran your fixture on Linux. The ./dir and ./feature.js columns come from your prose, so I built those files myself. | Run | ./feature | ./dir | ./feature.js | |---|---|---|---| | Node 22.23.1 | error | error | error | | Node 24.18.0 | error | error | error | | Node 26.11.1 | error | error | error | | REA before the fix | resolved | resolved | resolved | | REA on main | not found | not found | not found | REA before the fix is d9d2a1738. REA on main is d96550f2f. The fixture for ./feature holds only feature.js. The one for ./dir holds only dir/index.js. The one for ./feature.js holds only feature.js.js. Node exits 1 each time. ### Not run - The TypeScript and CommonJS cases you want kept. - The MCP interface. I used the compiled CLI only. - macOS and Windows. <details> <summary>Commands</summary> Fixture files, as in your issue, under /tmp/nodebatch/fx/2021 (the ./feature column): ``` entry.mjs import value from "./feature"; console.log(value); feature.js module.exports = 42; ``` The other two use the same entry.mjs with the import changed, and these files: ``` 2021-extra-dir-index/entry.mjs import value from "./dir"; console.log(value); 2021-extra-dir-index/dir/index.js module.exports = 42; 2021-extra-js-js-decoy/entry.mjs import value from "./feature.js"; console.log(value); 2021-extra-js-js-decoy/feature.js.js module.exports = 42; ``` Node error codes, all three versions: ``` ./feature, ./feature.js: ERR_MODULE_NOT_FOUND ./dir: ERR_UNSUPPORTED_DIR_IMPORT ``` ```sh # build each tree (Node 24.18.0 first on PATH, npm 11.16.0) npm ci && npm run build && npm run test:prepare # Node, exit codes read from each run /usr/bin/node /tmp/nodebatch/fx/2021/entry.mjs # 22.23.1 (also run with 24.18.0 and 26.11.1) # REA, once per tree node /tmp/nodebatch/main/scripts/rea.mjs analyze-javascript-application /tmp/nodebatch/fx/2021 --format json > main-2021.json node /tmp/nodebatch/base/scripts/rea.mjs analyze-javascript-application /tmp/nodebatch/fx/2021 --format json > base-2021.json ``` REA fields, as seen in the graph edges: ``` d9d2a1738 (before the fix), resolution_status "resolved": ./feature resolved_path "feature.js" ./dir resolved_path "dir/index.js" ./feature.js resolved_path "feature.js.js" d96550f2f (main), all three imports: resolution_status "not-found", resolved_path null ``` Both trees print 6.4.0 for --version, so the version string does not tell them apart. The shas do. </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 22.23.1, 24.18.0, 26.11.1 - Head / base: d96550f2f (main) / d9d2a1738 (the parent of c0904b4c1) </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
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#671
No description provided.