[GH-ISSUE #3307] [Bug] ESM imports and HTML scripts resolve to the wrong file after an empty URL path segment #1177

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

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

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 resolves ./assets//../app.js to app.js for both an ESM import and an HTML module-script reference. URL parsing resolves that reference to assets/app.js. When both files exist, the returned graph links the importer and renderer to the wrong file with resolution_status: "resolved" and complete graph coverage. Both the static-reference and source-module ESM import edges select the root file. The CommonJS interpretation of the same text correctly selects app.js, so the loader distinction matters.

The analysis exited successfully. The two target files have different contents, so this changes the graph's target identity rather than only its path spelling. The CLI graph, Node ESM/CommonJS resolution results, and URL pathname were executed and inspected.

Reproduction

  1. Start from a clean checkout of https://github.com/morluto/rea at revision 8dde6370d0809ad04cda3342d4e6f290f48aa063, then install the locked dependencies and build the runtime:
npm ci
npm run build:cached
  1. Run these commands from the repository root. They create only local fixture files and an isolated configuration home:
mkdir -p sample-url/assets repro-home
cat > sample-url/index.html <<'HTML'
<script type="module" src="./assets//../app.js"></script>
HTML
cat > sample-url/main.mjs <<'JS'
import "./assets//../app.js";
JS
cat > sample-url/main.cjs <<'JS'
require("./assets//../app.js");
JS
cat > sample-url/app.js <<'JS'
export const selected = "root";
JS
cat > sample-url/assets/app.js <<'JS'
export const selected = "assets";
JS
HOME="$PWD/repro-home" node scripts/rea.mjs analyze-javascript-application "$PWD/sample-url" --format json > url-evidence.json
python3 - <<'PYTHON'
import json
result = json.load(open("url-evidence.json"))["normalized_result"]
rows = set()
for edge in result["graph"]["edges"]:
    if edge["relation"] not in ("loads", "imports"):
        continue
    location = edge["evidence"]["location"]
    source = location["value"].get("source") if location["available"] else None
    if source is not None:
        properties = edge["properties"]
        rows.add((source, edge["relation"], properties.get("resolved_path"), properties.get("resolution_status")))
for row in sorted(rows):
    print(*row)
print("coverage:", result["graph"]["coverage"]["status"])
PYTHON
  1. Check the two loader interpretations without executing either fixture JavaScript file:
(cd sample-url && node --input-type=module -e '
import { createRequire } from "node:module";
const request = "./assets//../app.js";
console.log("ESM:", import.meta.resolve(request).split("/sample-url/")[1]);
console.log("CommonJS:", createRequire(import.meta.url).resolve(request).split("/sample-url/")[1]);
console.log("HTML URL:", new URL(request, "http://127.0.0.1/index.html").pathname);
')

Expected behavior

The ESM and HTML references must not become resolved relationships to app.js. Their URL interpretation selects assets/app.js: the .. segment removes the preceding empty segment, rather than removing assets. If the analyzer cannot represent that URL identity, it should retain an explicit unresolved outcome. The CommonJS control must continue selecting app.js because it uses filesystem path semantics.

docs/javascript-artifact-reconstruction.md:603-628 gives ESM URL resolution and exact HTML resources after document-base handling; src/application/javascript/JavaScriptHtmlBaseResolution.test.ts:86-106 compares admitted HTML paths against URL parsing.

Impact and scope

Impact level: P2. The supported assessment is that valid URL references with an empty path segment followed by a parent segment can produce high-confidence links to a different present file. The observed result has complete coverage, so downstream tracing can follow the wrong artifact without an unresolved warning. The demonstrated scope is these path forms, not all application analysis.

Workaround status: unverified. A caller can independently resolve the URL to inspect the correct file, as demonstrated, but no verified REA option restores the original application's incorrect graph relationships without changing the analyzed input.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1. Architecture was not recorded in the supplied reproduction evidence. The reproduction ran through the built CLI. No application JavaScript was executed by the analysis or the loader-resolution checks.

Target and artifact context

A local HTML/JavaScript application directory containing index.html, main.mjs, main.cjs, root app.js, and assets/app.js. The two candidate target files contain distinct selected string values. The analysis uses an isolated configuration home.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The successful CLI analysis and projection command printed the first four lines; the independent resolution command printed the final three lines:

index.html loads app.js resolved
main.cjs imports app.js resolved
main.mjs imports app.js resolved
coverage: complete
ESM: assets/app.js
CommonJS: app.js
HTML URL: /assets/app.js

Typed outcome or recovery expected

Run the same fixture and inspect both ESM import edges and the HTML load edge. None may select the root app.js; a resolved result must name assets/app.js, or an unsupported identity must remain explicitly unresolved. The CommonJS edges and the independent require.resolve control must still select app.js. Ordinary ./assets/app.js URL references must retain their existing exact-file behavior.

Analysis or suspected cause

Source-derived reasoning: src/application/javascript/JavaScriptArtifactPathResolution.ts:565-594 locates the module resolution path, whose relativeCandidate uses a filesystem path join before exact URL lookup. src/application/javascript/JavaScriptArtifactPathResolution.ts:1037-1089 locates HTML resolution, which also joins relative references as filesystem path text. These joins are consistent with collapsing the empty path segment before processing .., losing the URL interpretation demonstrated by the independent checks. No implementation repair was tested.

Additional context

Related open report #3214 concerns tabs/newlines leaving relative ESM imports unresolved, while this report concerns empty and parent segments selecting a different present file for ESM and HTML references.

Related completed report #1236 concerns encoded letters, surrounding whitespace, and encoded HTML bases producing missing-resource results, while this report concerns literal empty-segment normalization producing false resolved relationships.

Related completed report #2827 concerns encoded query/fragment bytes rejected as pathname syntax; this fixture contains no encoded suffix bytes.

Related completed report #2960 concerns percent-encoded dot/separator admission; this fixture contains literal empty and parent segments.

Related completed reports #2021 and #1039 concern ESM and HTML extension/directory fallbacks, respectively; this fixture names an exact .js file and instead fails URL segment normalization.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3307 ### 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` resolves `./assets//../app.js` to `app.js` for both an ESM import and an HTML module-script reference. URL parsing resolves that reference to `assets/app.js`. When both files exist, the returned graph links the importer and renderer to the wrong file with `resolution_status: "resolved"` and complete graph coverage. Both the static-reference and source-module ESM import edges select the root file. The CommonJS interpretation of the same text correctly selects `app.js`, so the loader distinction matters. The analysis exited successfully. The two target files have different contents, so this changes the graph's target identity rather than only its path spelling. The CLI graph, Node ESM/CommonJS resolution results, and URL pathname were executed and inspected. ### Reproduction 1. Start from a clean checkout of `https://github.com/morluto/rea` at revision `8dde6370d0809ad04cda3342d4e6f290f48aa063`, then install the locked dependencies and build the runtime: ```bash npm ci npm run build:cached ``` 2. Run these commands from the repository root. They create only local fixture files and an isolated configuration home: ```bash mkdir -p sample-url/assets repro-home cat > sample-url/index.html <<'HTML' <script type="module" src="./assets//../app.js"></script> HTML cat > sample-url/main.mjs <<'JS' import "./assets//../app.js"; JS cat > sample-url/main.cjs <<'JS' require("./assets//../app.js"); JS cat > sample-url/app.js <<'JS' export const selected = "root"; JS cat > sample-url/assets/app.js <<'JS' export const selected = "assets"; JS HOME="$PWD/repro-home" node scripts/rea.mjs analyze-javascript-application "$PWD/sample-url" --format json > url-evidence.json python3 - <<'PYTHON' import json result = json.load(open("url-evidence.json"))["normalized_result"] rows = set() for edge in result["graph"]["edges"]: if edge["relation"] not in ("loads", "imports"): continue location = edge["evidence"]["location"] source = location["value"].get("source") if location["available"] else None if source is not None: properties = edge["properties"] rows.add((source, edge["relation"], properties.get("resolved_path"), properties.get("resolution_status"))) for row in sorted(rows): print(*row) print("coverage:", result["graph"]["coverage"]["status"]) PYTHON ``` 3. Check the two loader interpretations without executing either fixture JavaScript file: ```bash (cd sample-url && node --input-type=module -e ' import { createRequire } from "node:module"; const request = "./assets//../app.js"; console.log("ESM:", import.meta.resolve(request).split("/sample-url/")[1]); console.log("CommonJS:", createRequire(import.meta.url).resolve(request).split("/sample-url/")[1]); console.log("HTML URL:", new URL(request, "http://127.0.0.1/index.html").pathname); ') ``` ### Expected behavior The ESM and HTML references must not become resolved relationships to `app.js`. Their URL interpretation selects `assets/app.js`: the `..` segment removes the preceding empty segment, rather than removing `assets`. If the analyzer cannot represent that URL identity, it should retain an explicit unresolved outcome. The CommonJS control must continue selecting `app.js` because it uses filesystem path semantics. `docs/javascript-artifact-reconstruction.md:603-628` gives ESM URL resolution and exact HTML resources after document-base handling; `src/application/javascript/JavaScriptHtmlBaseResolution.test.ts:86-106` compares admitted HTML paths against URL parsing. ### Impact and scope Impact level: P2. The supported assessment is that valid URL references with an empty path segment followed by a parent segment can produce high-confidence links to a different present file. The observed result has complete coverage, so downstream tracing can follow the wrong artifact without an unresolved warning. The demonstrated scope is these path forms, not all application analysis. Workaround status: unverified. A caller can independently resolve the URL to inspect the correct file, as demonstrated, but no verified REA option restores the original application's incorrect graph relationships without changing the analyzed input. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1. Architecture was not recorded in the supplied reproduction evidence. The reproduction ran through the built CLI. No application JavaScript was executed by the analysis or the loader-resolution checks. ### Target and artifact context A local HTML/JavaScript application directory containing `index.html`, `main.mjs`, `main.cjs`, root `app.js`, and `assets/app.js`. The two candidate target files contain distinct `selected` string values. The analysis uses an isolated configuration home. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The successful CLI analysis and projection command printed the first four lines; the independent resolution command printed the final three lines: ```text index.html loads app.js resolved main.cjs imports app.js resolved main.mjs imports app.js resolved coverage: complete ESM: assets/app.js CommonJS: app.js HTML URL: /assets/app.js ``` ### Typed outcome or recovery expected Run the same fixture and inspect both ESM import edges and the HTML load edge. None may select the root `app.js`; a resolved result must name `assets/app.js`, or an unsupported identity must remain explicitly unresolved. The CommonJS edges and the independent `require.resolve` control must still select `app.js`. Ordinary `./assets/app.js` URL references must retain their existing exact-file behavior. ### Analysis or suspected cause Source-derived reasoning: `src/application/javascript/JavaScriptArtifactPathResolution.ts:565-594` locates the module resolution path, whose `relativeCandidate` uses a filesystem path join before exact URL lookup. `src/application/javascript/JavaScriptArtifactPathResolution.ts:1037-1089` locates HTML resolution, which also joins relative references as filesystem path text. These joins are consistent with collapsing the empty path segment before processing `..`, losing the URL interpretation demonstrated by the independent checks. No implementation repair was tested. ### Additional context Related open report [#3214](https://github.com/morluto/rea/issues/3214) concerns tabs/newlines leaving relative ESM imports unresolved, while this report concerns empty and parent segments selecting a different present file for ESM and HTML references. Related completed report [#1236](https://github.com/morluto/rea/issues/1236) concerns encoded letters, surrounding whitespace, and encoded HTML bases producing missing-resource results, while this report concerns literal empty-segment normalization producing false resolved relationships. Related completed report [#2827](https://github.com/morluto/rea/issues/2827) concerns encoded query/fragment bytes rejected as pathname syntax; this fixture contains no encoded suffix bytes. Related completed report [#2960](https://github.com/morluto/rea/issues/2960) concerns percent-encoded dot/separator admission; this fixture contains literal empty and parent segments. Related completed reports [#2021](https://github.com/morluto/rea/issues/2021) and [#1039](https://github.com/morluto/rea/issues/1039) concern ESM and HTML extension/directory fallbacks, respectively; this fixture names an exact `.js` file and instead fails URL segment normalization.
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#1177
No description provided.