From c1b370ce0dfcbd5725cf6aebe23b022dfc75ac84 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:39:39 -0400 Subject: [PATCH] fix(javascript): retain NodeNext artifact source facts (#1087) * test(javascript): reproduce omitted NodeNext artifact sources Tests only: qualify current .mts/.cts artifact selection through real directory and ASAR reconstruction and the built public CLI. Signed-off-by: Rudy Celekli * fix(javascript): retain NodeNext artifact source facts Read .mts and .cts artifact files through the existing inert TypeScript parser, preserving selected file identities and analysis facts in directory and ASAR inputs. Document source selection and format the existing regression assertion. Signed-off-by: Rudy Celekli --------- Signed-off-by: Rudy Celekli Co-authored-by: N0zoM1z0 <1705004398@qq.com> --- docs/javascript-artifact-reconstruction.md | 4 ++ .../javascript/JavaScriptArtifactFiles.ts | 2 +- .../cli/javascriptApplicationProgress.test.ts | 34 ++++++++++ .../javascriptArtifactReconstruction.test.ts | 64 +++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 4cb46cfd23..7f2ab5e5db 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -125,6 +125,10 @@ SHA-256 digests, inventory IDs, ASAR container identity, and `.asar.unpacked` status. Direct ASAR inputs and filesystem-backed ASAR files nested beneath a directory are supported. +JavaScript sources (`.js`, `.jsx`, `.mjs`, and `.cjs`) and TypeScript sources +(`.ts`, `.tsx`, `.mts`, and `.cts`) are parsed as inert text; analysis does not +execute them. + JavaScript and HTML source ranges retain an initial UTF-8 BOM as one UTF-16 code unit, matching the original bytes identified by the artifact digest. diff --git a/src/artifacts/javascript/JavaScriptArtifactFiles.ts b/src/artifacts/javascript/JavaScriptArtifactFiles.ts index 84107c366f..1e5a4e5c92 100644 --- a/src/artifacts/javascript/JavaScriptArtifactFiles.ts +++ b/src/artifacts/javascript/JavaScriptArtifactFiles.ts @@ -330,7 +330,7 @@ const relevantKind = (path: string): JavaScriptArtifactFileKind | undefined => { if (lower === "package.json" || lower.endsWith("/package.json")) return "package-json"; if (lower.endsWith(".json")) return "json"; - if (/\.(?:cjs|mjs|js|jsx|ts|tsx)$/u.test(lower)) return "javascript"; + if (/\.(?:cjs|cts|mjs|mts|js|jsx|ts|tsx)$/u.test(lower)) return "javascript"; if (/\.html?$/u.test(lower)) return "html"; if (lower.endsWith(".map")) return "source-map"; if (lower.endsWith(".node")) return "native-addon"; diff --git a/tests/boundary/cli/javascriptApplicationProgress.test.ts b/tests/boundary/cli/javascriptApplicationProgress.test.ts index 06fc1c8cb0..26bf3e5af9 100644 --- a/tests/boundary/cli/javascriptApplicationProgress.test.ts +++ b/tests/boundary/cli/javascriptApplicationProgress.test.ts @@ -72,6 +72,40 @@ cliTest( }, ); +for (const extension of ["mts", "cts"]) { + for (const command of ["analyze-javascript-application", "analyze"]) { + cliTest( + `${command} returns facts for a selected ${extension} source`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-nodenext-cli-"); + const path = `selected.${extension}`; + await writeFile( + join(root, path), + "export function selected_feature(value: string) { return { value }; }", + ); + const output = await cli.run({ arguments: [command, root, "--json"] }); + expect(output.exitCode).toBe(0); + const evidence = parseEvidence(output.json); + const result = javascriptApplicationAnalysisResultSchema.parse( + evidence.normalized_result, + ); + expect(result.statistics).toMatchObject({ + relevant_files: 1, + parsed_javascript_files: 1, + parse_failures: 0, + }); + expect( + result.graph.nodes.some( + ({ kind, observations }) => + kind === "javascript-asset" && + observations.some(({ properties }) => properties.path === path), + ), + ).toBe(true); + }, + ); + } +} + for (const command of ["analyze-javascript-application", "analyze"]) { cliTest( `${command} retains HTML source ranges after a bare carriage return`, diff --git a/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts b/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts index 3f14ac5b40..ec9d47a077 100644 --- a/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts +++ b/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -453,3 +454,66 @@ const fixtureDirectory = async (): Promise => { await writeJavaScriptArtifactFixture(root); return root; }; + +for (const extension of ["ts", "tsx", "mts", "cts", "js", "mjs", "cjs"]) { + it.each(["directory", "asar"] as const)( + `retains ${extension} source facts from a real %s artifact`, + async (format) => { + const root = await createTestTempDirectory("rea-nodenext-artifacts-"); + const directory = join(root, "source"); + await mkdir(directory); + const path = `selected.${extension}`; + const parameter = ["ts", "tsx", "mts", "cts"].includes(extension) + ? "value: string" + : "value"; + const source = ` + globalThis.__rea_nodenext_executed = true; + throw new Error("fixture must remain inert"); + export function selected_feature(${parameter}) { return { value }; } + `; + await writeFile(join(directory, path), source); + let inputPath = directory; + if (format === "asar") { + inputPath = join(root, "application.asar"); + await createPackageWithOptions(directory, inputPath, {}); + } + Reflect.deleteProperty(globalThis, "__rea_nodenext_executed"); + const result = await reconstructJavaScriptArtifact({ + input_path: inputPath, + format, + }); + expect( + Reflect.get(globalThis, "__rea_nodenext_executed"), + ).toBeUndefined(); + expect(result.statistics).toMatchObject({ + relevant_files: 1, + parsed_javascript_files: 1, + text_bytes_read: Buffer.byteLength(source), + parse_failures: 0, + }); + const asset = result.graph.nodes.find( + ({ kind, observations }) => + kind === "javascript-asset" && + observations.some(({ properties }) => properties.path === path), + ); + expect(asset?.identity).toMatchObject({ + sha256: createHash("sha256").update(source).digest("hex"), + }); + expect(asset?.observations[0]?.properties).toMatchObject({ + path, + bytes: Buffer.byteLength(source), + file_kind: "javascript", + parse_status: "complete", + }); + expect( + result.semantic_graph.nodes.some( + ({ identity }) => identity.module_path === path, + ), + ).toBe(true); + expect(result.graph.coverage).toMatchObject({ + status: "complete", + truncated: false, + }); + }, + ); +}