mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 19:35:56 +00:00
fix(javascript): select JSON and addon loaders by exact suffix (#7821)
Node's CommonJS loader matches registered extensions case-sensitively, so
require("./data.JSON") and require("./addon.NODE") run as JavaScript.
Derive target_json_status and the native loads relation from the exact
loader suffix instead of the inventory's case-folded file kind.
Fixes #7623
This commit is contained in:
4 files changed
+70
-6
No files matched your search
@@ -36,6 +36,7 @@ import {
|
||||
import {
|
||||
moduleResolutionModeForJavaScriptReference,
|
||||
isJsonModuleTarget,
|
||||
isNativeAddonModuleTarget,
|
||||
} from "../../domain/javascript/javascriptModuleResolutionMode.js";
|
||||
|
||||
type StaticReference = JavaScriptStaticAnalysis["references"][number];
|
||||
@@ -309,7 +310,8 @@ const addReference = (
|
||||
coverage: input.coverage,
|
||||
relation:
|
||||
input.value.operation === "load" &&
|
||||
resolved?.file?.kind === "native-addon"
|
||||
resolved?.path != null &&
|
||||
isNativeAddonModuleTarget(resolved.path, resolved.file?.kind)
|
||||
? "loads"
|
||||
: "imports",
|
||||
properties: {
|
||||
|
||||
@@ -18,7 +18,10 @@ import type {
|
||||
JavaScriptModuleOrigin,
|
||||
JavaScriptSemanticModuleLink,
|
||||
} from "../../domain/javascript/javascriptSemanticIr.js";
|
||||
import { moduleResolutionModeForJavaScriptReference } from "../../domain/javascript/javascriptModuleResolutionMode.js";
|
||||
import {
|
||||
isJsonModuleTarget,
|
||||
moduleResolutionModeForJavaScriptReference,
|
||||
} from "../../domain/javascript/javascriptModuleResolutionMode.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
addAstContainsEdge,
|
||||
@@ -327,10 +330,9 @@ const resolveModuleTarget = (
|
||||
node,
|
||||
path,
|
||||
file,
|
||||
jsonStatus:
|
||||
file.kind === "json" || file.kind === "package-json"
|
||||
? input.context.analysis.jsonDocuments.read(file).status
|
||||
: null,
|
||||
jsonStatus: isJsonModuleTarget(path, file.kind)
|
||||
? input.context.analysis.jsonDocuments.read(file).status
|
||||
: null,
|
||||
status: "resolved",
|
||||
limitations: resolution.limitations,
|
||||
resolution,
|
||||
|
||||
@@ -8,6 +8,12 @@ export const isJsonModuleTarget = (
|
||||
): boolean =>
|
||||
path.endsWith(".json") && (kind === "json" || kind === "package-json");
|
||||
|
||||
/** Native-addon dispatch likewise needs the exact `.node` loader suffix. */
|
||||
export const isNativeAddonModuleTarget = (
|
||||
path: string,
|
||||
kind: JavaScriptArtifactFileKind | undefined,
|
||||
): boolean => path.endsWith(".node") && kind === "native-addon";
|
||||
|
||||
/** Loader evidence retained for a source-level module reference. */
|
||||
export type JavaScriptModuleResolutionMode =
|
||||
| "import"
|
||||
|
||||
@@ -27,6 +27,32 @@ const analyze = async (root: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const analyzeLoaderSuffixCase = async () => {
|
||||
const root = await createTestTempDirectory("rea-require-suffix-case-");
|
||||
const javascript = 'module.exports = { loadedBy: "javascript" };';
|
||||
// Case-insensitive hosts cannot hold both spellings in one directory.
|
||||
await mkdir(join(root, "lower"));
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(root, "main.cjs"),
|
||||
["./data.JSON", "./addon.NODE", "./lower/data.json", "./lower/addon.node"]
|
||||
.map((specifier) => `require(${JSON.stringify(specifier)});`)
|
||||
.join("\n"),
|
||||
),
|
||||
writeFile(join(root, "data.JSON"), javascript),
|
||||
writeFile(join(root, "addon.NODE"), javascript),
|
||||
writeFile(join(root, "lower", "data.json"), '{"loadedBy":"json"}'),
|
||||
writeFile(join(root, "lower", "addon.node"), Buffer.from([0, 1, 2, 3])),
|
||||
]);
|
||||
// Node matches registered loader extensions case-sensitively.
|
||||
const nodeRequire = createRequire(join(root, "main.cjs"));
|
||||
expect(nodeRequire("./data.JSON")).toEqual({ loadedBy: "javascript" });
|
||||
expect(nodeRequire("./addon.NODE")).toEqual({ loadedBy: "javascript" });
|
||||
const { graph } = await analyze(root);
|
||||
return (specifier: string) =>
|
||||
graph.edges.filter((edge) => edge.properties.specifier === specifier);
|
||||
};
|
||||
|
||||
describe("literal CommonJS paths in application graphs", () => {
|
||||
it("agrees with Node for both require projections while preserving ESM suffixes", async () => {
|
||||
const root = await createTestTempDirectory("rea-literal-require-");
|
||||
@@ -77,6 +103,34 @@ describe("literal CommonJS paths in application graphs", () => {
|
||||
expect(graph.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("attaches JSON loader status only to Node's exact .json suffix", async () => {
|
||||
const edges = await analyzeLoaderSuffixCase();
|
||||
const relationship = (specifier: string) =>
|
||||
edges(specifier).find(
|
||||
(edge) => edge.properties.module_link_kind === "require",
|
||||
)?.properties;
|
||||
expect(relationship("./data.JSON")).toMatchObject({
|
||||
resolved_path: "data.JSON",
|
||||
target_file_kind: "json",
|
||||
target_json_status: null,
|
||||
});
|
||||
expect(relationship("./lower/data.json")).toMatchObject({
|
||||
resolved_path: "lower/data.json",
|
||||
target_file_kind: "json",
|
||||
target_json_status: "included",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits a native loads edge only for Node's exact .node suffix", async () => {
|
||||
const edges = await analyzeLoaderSuffixCase();
|
||||
const relations = (specifier: string) =>
|
||||
edges(specifier)
|
||||
.filter((edge) => edge.properties.kind === "require")
|
||||
.map((edge) => edge.relation);
|
||||
expect(relations("./addon.NODE")).toEqual(["imports"]);
|
||||
expect(relations("./lower/addon.node")).toEqual(["loads"]);
|
||||
});
|
||||
|
||||
it.each(["literal", "stripped", "both"] as const)(
|
||||
"keeps native graph/count projections aligned with %s files",
|
||||
async (layout) => {
|
||||
|
||||
Reference in new issue
Block a user