diff --git a/docs/website-module-trace.md b/docs/website-module-trace.md index 5dc2d57a6d..0b36c22de0 100644 --- a/docs/website-module-trace.md +++ b/docs/website-module-trace.md @@ -83,3 +83,9 @@ closes only the owned browser; cleanup failure cannot return a successful trace. the host requires it; it does not enable an REA security boundary. See [testing](testing.md) for the native loading oracle and CLI/MCP real lane. + +The selected local source filename supplies the parser grammar. JavaScript files +retain native dynamic imports even in relational expressions; TypeScript files +use the shared TypeScript grammar and omit erased import-type expressions from +runtime module tracing. Historical-source import retains those type dependencies +separately. Parser observations and native URL resolution do not prove execution. diff --git a/src/application/WebModuleTraceService.test.ts b/src/application/WebModuleTraceService.test.ts index 73b44fd1f9..62f6d27147 100644 --- a/src/application/WebModuleTraceService.test.ts +++ b/src/application/WebModuleTraceService.test.ts @@ -369,3 +369,26 @@ it("retains a completed engine-free trace when artifact loading finishes under c }, }); }); + +it("resolves imports from relational JavaScript using the selected source grammar", async () => { + const artifacts = webModuleArtifactsFixture( + 'const order = left < import("./dep.js") > (right);', + ); + const result = await new WebModuleTraceService( + { load: async () => ok(artifacts) }, + webModuleResolverFixture, + ).trace(args); + if (!result.ok) throw result.error; + expect( + webModuleTraceResultSchema.parse(result.value.normalized_result), + ).toMatchObject({ + parser: { state: "parsed", diagnostics: [] }, + imports: [ + { + kind: "dynamic-import", + specifier: "./dep.js", + resolution: { state: "resolved", url: "https://app.test/dep.js" }, + }, + ], + }); +}); diff --git a/src/application/WebModuleTraceService.ts b/src/application/WebModuleTraceService.ts index 26c8f4ce5f..1a31ad65d4 100644 --- a/src/application/WebModuleTraceService.ts +++ b/src/application/WebModuleTraceService.ts @@ -129,7 +129,7 @@ export class WebModuleTraceService { const importerUrl = input.importer_url ?? selected.url; const importerAdmission = admitContext(importerUrl, ["importer_url"]); if (!importerAdmission.ok) return importerAdmission; - const syntax = collectWebModuleImports(data.source); + const syntax = collectWebModuleImports(data.source, data.sourceFile.path); const literals = syntax.imports .filter((reference) => reference.specifier !== null) .map((reference) => reference.specifier ?? ""); diff --git a/src/domain/webModuleImports.test.ts b/src/domain/webModuleImports.test.ts index c0bcabdbfb..e02998203b 100644 --- a/src/domain/webModuleImports.test.ts +++ b/src/domain/webModuleImports.test.ts @@ -9,7 +9,7 @@ describe("native module syntax observations", () => { it("collects native imports and re-exports, preserving empty strings and computed unknowns", () => { const source = 'import ""; import x from "./x.js"; export * from "./all.js"; export {x} from "pkg"; import(`./literal.js`); import(`./${name}.js`); import(expr);'; - const result = collectWebModuleImports(source); + const result = collectWebModuleImports(source, "main.js"); expect(result.state).toBe("parsed"); expect(result.imports.map((item) => [item.kind, item.specifier])).toEqual([ ["static-import", ""], @@ -28,6 +28,7 @@ describe("native module syntax observations", () => { it("excludes whole type declarations and retains inline type dependency syntax", () => { const result = collectWebModuleImports( 'import type {T} from "types"; import {type X} from "also-types"; export type {T} from "export-types"; export {type Y} from "only-types"; import {type T, value} from "mixed"; __webpack_require__.e(4); __vitePreload(() => 1, ["chunk.js"]);', + "main.ts", ); expect(result.imports.map((item) => item.specifier)).toEqual([ "also-types", @@ -38,9 +39,22 @@ describe("native module syntax observations", () => { "Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.", ); }); + it("keeps JavaScript relational imports distinct from TypeScript import types", () => { + const source = 'const order = left < import("./dep.js") > (right);'; + const javascript = collectWebModuleImports(source, "main.js"); + expect(javascript.diagnostics).toEqual([]); + expect(javascript.imports).toEqual([ + expect.objectContaining({ + kind: "dynamic-import", + specifier: "./dep.js", + expression: '"./dep.js"', + }), + ]); + expect(collectWebModuleImports(source, "main.ts").imports).toEqual([]); + }); it("uses UTF-16 source locations after astral characters", () => { const source = 'const emoji = "😀";\nimport "./x.js";'; - const item = collectWebModuleImports(source).imports[0]; + const item = collectWebModuleImports(source, "main.js").imports[0]; expect(item?.start).toEqual({ offset: source.indexOf('"./x.js"'), line: 2, @@ -48,7 +62,10 @@ describe("native module syntax observations", () => { }); }); it("reports unparseable input without asserting an empty complete graph", () => { - const result = collectWebModuleImports('import "./ok.js"; function {'); + const result = collectWebModuleImports( + 'import "./ok.js"; function {', + "main.js", + ); expect(result).toMatchObject({ state: "unparseable", imports: [] }); expect(result.diagnostics.length).toBeGreaterThan(0); }); @@ -57,7 +74,7 @@ describe("native module syntax observations", () => { describe("deep native module syntax", () => { it("retains imports and their locations after a parser-admitted deep property chain", () => { const source = `const value = root${".next".repeat(12_000)};\nimport "./last.js";\nexport * from "./reexport.js";\nimport("./lazy.js");`; - const result = collectWebModuleImports(source); + const result = collectWebModuleImports(source, "main.js"); expect(result.state).toBe("parsed"); expect(result.diagnostics).toEqual([]); expect( diff --git a/src/domain/webModuleImports.ts b/src/domain/webModuleImports.ts index c7cb8a639c..c94b98e426 100644 --- a/src/domain/webModuleImports.ts +++ b/src/domain/webModuleImports.ts @@ -13,12 +13,13 @@ import { /** Collect module declarations, omitting whole-statement type-only forms. */ export const collectWebModuleImports = ( source: string, + sourcePath: string, ): { readonly imports: readonly WebModuleImport[]; readonly state: "parsed" | "unparseable"; readonly diagnostics: readonly string[]; } => { - const ast = parseJavaScriptSource(source); + const ast = parseJavaScriptSource(source, sourcePath); if (ast === null) return { imports: [],