[GH-ISSUE #1883] [Bug] Bare ESM package specifiers lose query and fragment identity during exports lookup #637

Closed
opened 2026-10-15 18:26:51 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

  • Searched open/closed issues and recent/open pull requests, including #1787, which concerns URL-reference percent decoding rather than bare package export lookup.
  • This report uses only source-owned files, with no credentials or private target data.

Area

Evidence, artifacts, or reconstruction — JavaScript module target resolution.

Observed behavior

REA strips the query/fragment portion of a bare ESM package specifier before looking up package exports. It therefore reports an internal dependency that Node explicitly refuses. Bare package subpaths are exact lookup keys; query/fragment stripping is appropriate for URL references, not for those package keys.

Reproduction

Create the following files in a directory named fixture:

fixture/entry.mjs:

import value from "example/feature?cache=1";
console.log(value);

fixture/node_modules/example/package.json:

{"exports":{"./feature":"./feature.cjs"}}

fixture/node_modules/example/feature.cjs:

module.exports = 42;

Run in a built REA source checkout:

node fixture/entry.mjs
# exit 1: ERR_PACKAGE_PATH_NOT_EXPORTED for './feature?cache=1'
node scripts/rea.mjs analyze-javascript-application "$(pwd)/fixture" --json > observed.json
node --input-type=module -e '
import fs from "node:fs";
const graph = JSON.parse(fs.readFileSync("observed.json", "utf8")).normalized_result.graph;
console.log(graph.edges.filter(e => e.properties.module_link_kind === "import").map(e => e.properties));
'

REA exits 0 and reports:

{"specifier":"example/feature?cache=1","resolved_path":"node_modules/example/feature.cjs","resolution_status":"resolved"}

Repeated twice through the affected built CLI with the same result.

Expected behavior

Preserve the full bare specifier for package name/subpath/exports lookup. With this fixture, the unexported ./feature?cache=1 must remain unresolved/external; it must not silently become ./feature. The same applies to fragments. Explicitly exported keys containing that punctuation must still resolve by their exact spelling. Relative/file URL imports should continue using URL query/fragment semantics.

Impact and scope

Static dependency and binding graphs can invent an available local module for an import that fails in the actual runtime, or miss a valid export keyed by the full subpath. Original specifier text is retained in the output but is currently resolved using a different key.

Version and runtime

  • Source revision: fb94d9dde0ccda83ae520ac6fb22b1d283562cf3 (main).
  • Node 24.18.0, npm 11.16.0, macOS 27.0.1 arm64.
  • Interface: CLI.
  • Earliest failure stage: module-specifier normalization, before package lookup.

Source-derived analysis

contextualCandidate calls stripQueryAndFragment for every import-loader module specifier before distinguishing bare package requests from relative/file URLs. Keep the raw bare request through package lookup and apply URL normalization only on the URL path branch.

Originally created by @xu91102 on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1883 ### Before submitting - [x] Searched open/closed issues and recent/open pull requests, including #1787, which concerns URL-reference percent decoding rather than bare package export lookup. - [x] This report uses only source-owned files, with no credentials or private target data. ### Area Evidence, artifacts, or reconstruction — JavaScript module target resolution. ### Observed behavior REA strips the query/fragment portion of a bare ESM package specifier before looking up package exports. It therefore reports an internal dependency that Node explicitly refuses. Bare package subpaths are exact lookup keys; query/fragment stripping is appropriate for URL references, not for those package keys. ### Reproduction Create the following files in a directory named `fixture`: `fixture/entry.mjs`: ```js import value from "example/feature?cache=1"; console.log(value); ``` `fixture/node_modules/example/package.json`: ```json {"exports":{"./feature":"./feature.cjs"}} ``` `fixture/node_modules/example/feature.cjs`: ```js module.exports = 42; ``` Run in a built REA source checkout: ```sh node fixture/entry.mjs # exit 1: ERR_PACKAGE_PATH_NOT_EXPORTED for './feature?cache=1' node scripts/rea.mjs analyze-javascript-application "$(pwd)/fixture" --json > observed.json node --input-type=module -e ' import fs from "node:fs"; const graph = JSON.parse(fs.readFileSync("observed.json", "utf8")).normalized_result.graph; console.log(graph.edges.filter(e => e.properties.module_link_kind === "import").map(e => e.properties)); ' ``` REA exits 0 and reports: ```json {"specifier":"example/feature?cache=1","resolved_path":"node_modules/example/feature.cjs","resolution_status":"resolved"} ``` Repeated twice through the affected built CLI with the same result. ### Expected behavior Preserve the full bare specifier for package name/subpath/exports lookup. With this fixture, the unexported `./feature?cache=1` must remain unresolved/external; it must not silently become `./feature`. The same applies to fragments. Explicitly exported keys containing that punctuation must still resolve by their exact spelling. Relative/file URL imports should continue using URL query/fragment semantics. ### Impact and scope Static dependency and binding graphs can invent an available local module for an import that fails in the actual runtime, or miss a valid export keyed by the full subpath. Original specifier text is retained in the output but is currently resolved using a different key. ### Version and runtime - Source revision: `fb94d9dde0ccda83ae520ac6fb22b1d283562cf3` (`main`). - Node 24.18.0, npm 11.16.0, macOS 27.0.1 arm64. - Interface: CLI. - Earliest failure stage: module-specifier normalization, before package lookup. ### Source-derived analysis `contextualCandidate` calls `stripQueryAndFragment` for every import-loader module specifier before distinguishing bare package requests from relative/file URLs. Keep the raw bare request through package lookup and apply URL normalization only on the URL path branch.
bsakaguchi 2026-10-15 18:26:51 +00:00
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#637
No description provided.