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 <rudy@gradiahq.com>

* 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 <rudy@gradiahq.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
This commit is contained in:
Rudy Mizrahi Celekli
2026-10-08 12:39:39 -04:00
committed by GitHub
parent 5a67c7fc54
commit c1b370ce0d
4 changed files with 103 additions and 1 deletions
@@ -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.
@@ -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";
@@ -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`,
@@ -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<string> => {
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,
});
},
);
}