diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 661244f82..0023b7e66 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -231,6 +231,11 @@ unknown instead of silently treating them as absent or verified. Plain `.ts` artifact sources use TypeScript syntax without JSX, including angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing. +Relative TypeScript imports can name their emitted `.js`, `.mjs`, or `.cjs` +counterparts while resolving to inventoried source files. This substitution +requires import or require semantics. Literal Electron preload, renderer, and +utility-process runtime paths retain the runtime filename selected by the caller. + Selected bounded text is then parsed as inert data to recover: - `package.json` metadata and declared main or renderer entrypoints; diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts index 94fc46645..bd3bcdaaf 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts @@ -60,6 +60,7 @@ export interface ResolveArtifactPathInput { readonly context: ArtifactPathResolutionContext; readonly files: ReadonlyMap; readonly htmlBaseHref?: string | null; + /** Import/require semantics; runtime resource paths omit this provenance. */ readonly moduleKind?: "import" | "require"; } @@ -149,6 +150,7 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => { confined, new Set(), input.context === "module-specifier" && + input.moduleKind !== undefined && (input.declaredPath.startsWith(".") || input.declaredPath.startsWith("/")) ? typescriptEmittedSourceCandidates(input.sourcePath, confined) : [], diff --git a/tests/boundary/cli/javascriptSelfImportRelationships.test.ts b/tests/boundary/cli/javascriptSelfImportRelationships.test.ts index 00b97e85d..44b741d4c 100644 --- a/tests/boundary/cli/javascriptSelfImportRelationships.test.ts +++ b/tests/boundary/cli/javascriptSelfImportRelationships.test.ts @@ -11,6 +11,78 @@ import { writeFixtureFiles, } from "../../support/javascriptApplicationFixture.js"; +cliTest( + "resolves emitted module imports without substituting Electron runtime paths", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-typescript-runtime-paths-"); + await writeFixtureFiles(root, { + "package.json": JSON.stringify({ main: "main.ts" }), + "main.ts": `import { BrowserWindow, utilityProcess } from "electron"; + import "./worker.js"; + import "./config.mjs"; + import "./legacy.cjs"; + import "./view.js"; + import "./runtime.js"; + const win = new BrowserWindow({webPreferences:{preload:"./worker.js"}}); + win.loadURL("./renderer.js"); + utilityProcess.fork("./worker.js"); + utilityProcess.fork("./runtime.js"); + throw new Error("source must stay inert");`, + "worker.ts": "export const worker = 1;", + "config.mts": "export const config = 1;", + "legacy.cts": "export const legacy = 1;", + "view.tsx": "export const view =
;", + "renderer.ts": "export const renderer = 1;", + "runtime.ts": "export const source = 1;", + "runtime.js": "throw new Error('runtime must stay inert');", + }); + const output = await cli.run({ + arguments: ["analyze-javascript-application", root, "--json"], + }); + expect(output.exitCode, output.stderr).toBe(0); + const result = javascriptApplicationAnalysisResultSchema.parse( + parseEvidence(output.json).normalized_result, + ); + const { graph } = result; + const source = findSourceModule(graph, "main.ts"); + for (const [specifier, target] of [ + ["./worker.js", "worker.ts"], + ["./config.mjs", "config.mts"], + ["./legacy.cjs", "legacy.cts"], + ["./view.js", "view.tsx"], + ["./runtime.js", "runtime.js"], + ] as const) + expect( + findGraphEdge(graph, source, findSourceModule(graph, target), "imports") + ?.properties, + ).toMatchObject({ specifier, resolved_path: target }); + expect(result.summary).toMatchObject({ + utility_processes: 2, + resolved_utility_entrypoints: 1, + }); + expect( + graph.edges.find( + (edge) => + edge.evidence.extractor.operation === + "associate-browser-window-preload", + )?.properties, + ).toMatchObject({ + declared_path: "./worker.js", + resolved_path: null, + resolution_status: "not-found", + }); + expect( + graph.edges + .filter( + (edge) => + edge.properties.declared_path === "./worker.js" || + edge.properties.declared_path === "./renderer.js", + ) + .some((edge) => edge.properties.resolved_path != null), + ).toBe(false); + }, +); + cliTest( "returns literal self-import evidence while preserving sibling imports and self-re-exports", async ({ cli }) => {