[GH-ISSUE #2056] [Bug] CTS static imports use ESM lookup despite NodeNext CommonJS emission #681

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

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction — JavaScript/TypeScript source relationships.

Observed behavior

A .cts source reports module_format esm, leaves an extensionless JavaScript dependency unresolved, and selects the import branch of a conditional package export for both its static import and re-export. Actual TypeScript NodeNext emission uses require and executes successfully, printing [42,43].

Reproduction

From the repository root at the revision below, install dependencies and run npm run build:cached. This self-contained probe creates and removes its owned temporary fixture and compiler output:

python3 - <<'PY'
import json, os, shutil, subprocess, tempfile
from pathlib import Path
repo = Path.cwd()
env = dict(os.environ, REA_LOG_LEVEL="silent")
with tempfile.TemporaryDirectory(prefix="rea-repro-") as temp:
    fixture = Path(temp) / "source"
    fixture.mkdir()
    files = {'package.json': '{"type":"commonjs"}',
     'main.cts': 'import value from "./dep"; import { marker } from "conditional"; export { marker as '
                 'retainedMarker } from "conditional"; console.log(JSON.stringify([value, marker]));',
     'dep.js': 'module.exports = 42;',
     'node_modules/conditional/package.json': '{"exports":{"require":"./actual.cjs","import":"./decoy.mjs"}}',
     'node_modules/conditional/actual.cjs': 'exports.marker = 43;',
     'node_modules/conditional/actual.d.cts': 'export const marker: number;',
     'node_modules/conditional/decoy.mjs': 'export const marker = 99;',
     'node_modules/conditional/decoy.d.mts': 'export const marker: number;'}
    for name, text in files.items():
        path = fixture / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(text + "\n")
    emitted = Path(temp) / "emitted"
    emitted.mkdir()
    args = ["node", str(repo / "node_modules/typescript/bin/tsc"), "--module", "NodeNext",
            "--moduleResolution", "NodeNext", "--target", "ES2022", "--skipLibCheck",
            "--allowJs", "--outDir", str(emitted), "main.cts", "dep.js"]
    subprocess.run(args, cwd=fixture, check=True)
    (emitted / "package.json").write_text('{"type":"commonjs"}\n')
    if (fixture / "node_modules").exists():
        shutil.copytree(fixture / "node_modules", emitted / "node_modules")
    result = subprocess.run(["node", "main.cjs"], cwd=emitted, capture_output=True, text=True)
    print("Node emitted main.cjs", "exit", result.returncode, "stdout", result.stdout.strip())
    result = subprocess.run(["node", str(repo / "scripts/rea.mjs"),
                             "analyze-javascript-application", str(fixture), "--format", "json"],
                            cwd=repo, env=env, capture_output=True, text=True, check=True)
    graph = json.loads(result.stdout)["normalized_result"]["graph"]
    links = [e["properties"] for e in graph["edges"] if e["relation"] == "imports"
             and "module_link_kind" in e["properties"]
             and e["evidence"]["location"].get("value", {}).get("source") == "main.cts"]
    formats = [o["properties"]["module_format"] for n in graph["nodes"] for o in n["observations"]
               if o.get("properties", {}).get("semantic_role") == "source-module"
               and o["properties"].get("path") == "main.cts"]
    print("REA coverage", graph["coverage"]["status"], "format", formats)
    print("REA semantic imports", json.dumps(links))
PY

Expected behavior

Use CommonJS lookup for static imports and re-exports from .cts: ./dep resolves to dep.js, and both conditional declarations resolve to node_modules/conditional/actual.cjs. Preserve the original syntax and source ranges. A dynamic import() in the same file must still use ESM conditions.

Impact and scope

The dependency graph can point to the wrong implementation or miss a dependency loaded by the emitted program. The target is an owned local JavaScript/TypeScript directory; no target execution or native provider is needed by REA.

REA version

Source revision 3bdc8730bd98e073522bcb8ba86527be402c0cbe (package version 6.4.0).

Runtime and platform

Node.js 24.18.0, npm 11.16.0, TypeScript 7.0.2; macOS 27.0.1 arm64.

Target and artifact context

Local source directory fixture. No browser, Hopper, Ghidra or credentials.

Interface

Compiled CLI analyze-javascript-application; regression coverage also uses real stdio MCP analyze_javascript_application.

Earliest failure stage

Transport or result projection — static source dependency relationships.

Relevant output or evidence

NodeNext compiler exit 0; Node main.cjs exit 0, stdout [42,43].
REA main.cts format esm; ./dep not-found.
REA conditional static import and re-export resolve decoy.mjs.
CLI result reports complete coverage; repeated twice on the revision below.

Analysis or suspected cause

Source-derived: static import lookup currently assumes ESM semantics without accounting for the unambiguous CommonJS output of the .cts source extension.

Originally created by @xu91102 on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2056 ### Before submitting - [x] I searched open and closed issues and PRs for a related report. - [x] I removed credentials, private paths and sensitive target data. ### Area Evidence, artifacts, or reconstruction — JavaScript/TypeScript source relationships. ### Observed behavior A .cts source reports module_format esm, leaves an extensionless JavaScript dependency unresolved, and selects the import branch of a conditional package export for both its static import and re-export. Actual TypeScript NodeNext emission uses require and executes successfully, printing [42,43]. ### Reproduction From the repository root at the revision below, install dependencies and run `npm run build:cached`. This self-contained probe creates and removes its owned temporary fixture and compiler output: ```sh python3 - <<'PY' import json, os, shutil, subprocess, tempfile from pathlib import Path repo = Path.cwd() env = dict(os.environ, REA_LOG_LEVEL="silent") with tempfile.TemporaryDirectory(prefix="rea-repro-") as temp: fixture = Path(temp) / "source" fixture.mkdir() files = {'package.json': '{"type":"commonjs"}', 'main.cts': 'import value from "./dep"; import { marker } from "conditional"; export { marker as ' 'retainedMarker } from "conditional"; console.log(JSON.stringify([value, marker]));', 'dep.js': 'module.exports = 42;', 'node_modules/conditional/package.json': '{"exports":{"require":"./actual.cjs","import":"./decoy.mjs"}}', 'node_modules/conditional/actual.cjs': 'exports.marker = 43;', 'node_modules/conditional/actual.d.cts': 'export const marker: number;', 'node_modules/conditional/decoy.mjs': 'export const marker = 99;', 'node_modules/conditional/decoy.d.mts': 'export const marker: number;'} for name, text in files.items(): path = fixture / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text + "\n") emitted = Path(temp) / "emitted" emitted.mkdir() args = ["node", str(repo / "node_modules/typescript/bin/tsc"), "--module", "NodeNext", "--moduleResolution", "NodeNext", "--target", "ES2022", "--skipLibCheck", "--allowJs", "--outDir", str(emitted), "main.cts", "dep.js"] subprocess.run(args, cwd=fixture, check=True) (emitted / "package.json").write_text('{"type":"commonjs"}\n') if (fixture / "node_modules").exists(): shutil.copytree(fixture / "node_modules", emitted / "node_modules") result = subprocess.run(["node", "main.cjs"], cwd=emitted, capture_output=True, text=True) print("Node emitted main.cjs", "exit", result.returncode, "stdout", result.stdout.strip()) result = subprocess.run(["node", str(repo / "scripts/rea.mjs"), "analyze-javascript-application", str(fixture), "--format", "json"], cwd=repo, env=env, capture_output=True, text=True, check=True) graph = json.loads(result.stdout)["normalized_result"]["graph"] links = [e["properties"] for e in graph["edges"] if e["relation"] == "imports" and "module_link_kind" in e["properties"] and e["evidence"]["location"].get("value", {}).get("source") == "main.cts"] formats = [o["properties"]["module_format"] for n in graph["nodes"] for o in n["observations"] if o.get("properties", {}).get("semantic_role") == "source-module" and o["properties"].get("path") == "main.cts"] print("REA coverage", graph["coverage"]["status"], "format", formats) print("REA semantic imports", json.dumps(links)) PY ``` ### Expected behavior Use CommonJS lookup for static imports and re-exports from .cts: ./dep resolves to dep.js, and both conditional declarations resolve to node_modules/conditional/actual.cjs. Preserve the original syntax and source ranges. A dynamic import() in the same file must still use ESM conditions. ### Impact and scope The dependency graph can point to the wrong implementation or miss a dependency loaded by the emitted program. The target is an owned local JavaScript/TypeScript directory; no target execution or native provider is needed by REA. ### REA version Source revision `3bdc8730bd98e073522bcb8ba86527be402c0cbe` (package version 6.4.0). ### Runtime and platform Node.js 24.18.0, npm 11.16.0, TypeScript 7.0.2; macOS 27.0.1 arm64. ### Target and artifact context Local source directory fixture. No browser, Hopper, Ghidra or credentials. ### Interface Compiled CLI `analyze-javascript-application`; regression coverage also uses real stdio MCP `analyze_javascript_application`. ### Earliest failure stage Transport or result projection — static source dependency relationships. ### Relevant output or evidence ```text NodeNext compiler exit 0; Node main.cjs exit 0, stdout [42,43]. REA main.cts format esm; ./dep not-found. REA conditional static import and re-export resolve decoy.mjs. CLI result reports complete coverage; repeated twice on the revision below. ``` ### Analysis or suspected cause Source-derived: static import lookup currently assumes ESM semantics without accounting for the unambiguous CommonJS output of the .cts source extension.
bsakaguchi 2026-10-15 18:27:20 +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#681
No description provided.