mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): preserve TypeScript loader and dependency semantics
Share loader selection across static findings and module relationships: CTS static imports and re-exports use CommonJS lookup while dynamic imports retain ESM lookup. Retain runtime import-equals dependencies, bindings, and exported names; do not resolve require file-URL strings as local artifacts. Restore import-map descriptions in CLI help. Validated resolver and semantic tests, application reconstruction boundaries, and CLI help. Installed TypeScript NodeNext emission and Node require behavior were checked independently. Refs #2056, #2057, #2058, #2053.
This commit is contained in:
1 parent
966f7cc84b
commit
e4532b5376
8 files changed
+242
-13
No files matched your search
@@ -22,7 +22,10 @@ import {
|
||||
astObservationEvidence,
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import {
|
||||
moduleKindForJavaScriptReference,
|
||||
resolveArtifactPathByContext,
|
||||
} from "./JavaScriptArtifactPathResolution.js";
|
||||
|
||||
type StaticReference = JavaScriptStaticAnalysis["references"][number];
|
||||
type StaticEndpoint = JavaScriptStaticAnalysis["endpoints"][number];
|
||||
@@ -400,12 +403,11 @@ const resolveReference = (
|
||||
specifier,
|
||||
sourcePath,
|
||||
context.filesByPath,
|
||||
reference.kind === "require"
|
||||
? "require"
|
||||
: reference.kind === "static-import" ||
|
||||
reference.kind === "dynamic-import"
|
||||
? "import"
|
||||
: undefined,
|
||||
reference.kind === "require" ||
|
||||
reference.kind === "static-import" ||
|
||||
reference.kind === "dynamic-import"
|
||||
? moduleKindForJavaScriptReference(sourcePath, reference.kind)
|
||||
: undefined,
|
||||
);
|
||||
if (path === null) return null;
|
||||
const file = context.filesByPath.get(path);
|
||||
|
||||
@@ -576,6 +576,27 @@ describe("contextual JavaScript artifact path resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommonJS file URL resolution", () => {
|
||||
it("keeps file URL resolution specific to the import loader", () => {
|
||||
const files = fileMap([file("entry.cjs", "root"), file("dep.cjs", "root")]);
|
||||
const input = {
|
||||
declaredPath: "file:///dep.cjs",
|
||||
sourcePath: "entry.cjs",
|
||||
context: "module-specifier" as const,
|
||||
files,
|
||||
};
|
||||
|
||||
expect(resolve({ ...input, moduleKind: "require" })).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(resolve({ ...input, moduleKind: "import" })).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "dep.cjs",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("contextual JavaScript package path resolution", () => {
|
||||
it("follows package entrypoints beyond the former recursion ceiling", () => {
|
||||
const files = fileMap([
|
||||
|
||||
@@ -64,6 +64,17 @@ export interface ResolveArtifactPathInput {
|
||||
readonly moduleKind?: "import" | "require";
|
||||
}
|
||||
|
||||
/** Loader selected by a source-level JavaScript module reference. */
|
||||
export const moduleKindForJavaScriptReference = (
|
||||
sourcePath: string,
|
||||
referenceKind: "static-import" | "dynamic-import" | "require",
|
||||
): "import" | "require" =>
|
||||
referenceKind === "require" ||
|
||||
(referenceKind === "static-import" &&
|
||||
posix.extname(sourcePath).toLowerCase() === ".cts")
|
||||
? "require"
|
||||
: "import";
|
||||
|
||||
const EXTENSIONS = [
|
||||
".js",
|
||||
".cjs",
|
||||
@@ -189,7 +200,12 @@ const contextualCandidate = (
|
||||
if (context === "html-reference") return htmlCandidate(input);
|
||||
if (context === "module-specifier") {
|
||||
const fileUrl = fileUrlPath(declared);
|
||||
if (fileUrl !== undefined) return fileUrl;
|
||||
if (fileUrl !== undefined)
|
||||
return input.moduleKind === "require"
|
||||
? unresolvedCandidate("external", [
|
||||
"Node CommonJS require specifiers do not resolve file URLs as local artifacts.",
|
||||
])
|
||||
: fileUrl;
|
||||
if (hasScheme(declared))
|
||||
return unresolvedCandidate("external", [
|
||||
"URL and Node builtin schemes are outside static artifact module resolution.",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import {
|
||||
moduleKindForJavaScriptReference,
|
||||
resolveArtifactPathByContext,
|
||||
type ArtifactPathResolution,
|
||||
} from "./JavaScriptArtifactPathResolution.js";
|
||||
@@ -275,10 +276,12 @@ const resolveModuleTarget = (
|
||||
sourcePath: input.file.path,
|
||||
context: "module-specifier",
|
||||
files: input.context.filesByPath,
|
||||
moduleKind:
|
||||
moduleKind: moduleKindForJavaScriptReference(
|
||||
input.file.path,
|
||||
input.link.kind === "require" || input.link.kind === "commonjs-export"
|
||||
? "require"
|
||||
: "import",
|
||||
: "static-import",
|
||||
),
|
||||
});
|
||||
if (resolution.resolution_status !== "resolved")
|
||||
return {
|
||||
@@ -423,8 +426,8 @@ const moduleFormat = (
|
||||
ir: JavaScriptModuleSemanticIr,
|
||||
): "commonjs" | "esm" | "mixed" | "unknown" => {
|
||||
const extension = posix.extname(path).toLowerCase();
|
||||
if (extension === ".mjs") return "esm";
|
||||
if (extension === ".cjs") return "commonjs";
|
||||
if (extension === ".mjs" || extension === ".mts") return "esm";
|
||||
if (extension === ".cjs" || extension === ".cts") return "commonjs";
|
||||
const esm = ir.moduleLinks.some(({ kind }) =>
|
||||
["import", "export", "re-export"].includes(kind),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,51 @@ import {
|
||||
topLevelBinding,
|
||||
} from "./javascriptSemanticAnalysis.fixture.js";
|
||||
|
||||
describe("TypeScript import-equals links", () => {
|
||||
it("retains runtime external import-equals as a require dependency", () => {
|
||||
const links = analyzeJavaScriptSemantics(`
|
||||
import dependency = require("./dependency.cjs");
|
||||
import type types = require("./types");
|
||||
import alias = Namespace.Dependency;
|
||||
`).moduleLinks;
|
||||
|
||||
expect(links).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "require",
|
||||
specifier: "./dependency.cjs",
|
||||
importedName: null,
|
||||
localName: "dependency",
|
||||
exportedName: null,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the exported binding for exported import-equals", () => {
|
||||
const source = 'export import dependency = require("./dependency");';
|
||||
const links = analyzeJavaScriptSemantics(source).moduleLinks;
|
||||
|
||||
expect(links).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "require",
|
||||
specifier: "./dependency",
|
||||
localName: "dependency",
|
||||
exportedName: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "export",
|
||||
specifier: null,
|
||||
localName: "dependency",
|
||||
exportedName: "dependency",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
links.find(({ kind }) => kind === "require")?.location.start,
|
||||
).toEqual({ line: 1, column: 7 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommonJS dependency links", () => {
|
||||
it.each([
|
||||
'require("dependency");',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
collectSemanticModuleLink,
|
||||
collectSemanticCallable,
|
||||
semanticImportEqualsSpecifier,
|
||||
collectSemanticReferences,
|
||||
collectSemanticReferencesSteps,
|
||||
immutableSemanticBindings,
|
||||
@@ -752,6 +753,8 @@ const bindInnerDeclaration = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
if (t.isImportDeclaration(node)) bindImports(node, scope, state);
|
||||
else if (t.isTSImportEqualsDeclaration(node))
|
||||
bindImportEquals(node, scope, state);
|
||||
else if (t.isVariableDeclarator(node))
|
||||
bindPattern({
|
||||
pattern: node.id,
|
||||
@@ -785,6 +788,25 @@ const bindInnerDeclaration = (
|
||||
});
|
||||
};
|
||||
|
||||
const bindImportEquals = (
|
||||
node: t.TSImportEqualsDeclaration,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
const specifier = semanticImportEqualsSpecifier(node);
|
||||
if (specifier === null) return;
|
||||
addBinding({
|
||||
state,
|
||||
scope,
|
||||
name: node.id.name,
|
||||
kind: "import",
|
||||
mutable: false,
|
||||
definitionNode: node.id,
|
||||
initializer: null,
|
||||
directOrigin: { specifier, importedPath: [] },
|
||||
});
|
||||
};
|
||||
|
||||
const bindAssignment = (
|
||||
node: t.Node,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
|
||||
@@ -69,7 +69,8 @@ export const collectSemanticModuleLink = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
if (t.isImportDeclaration(node)) collectImports(node, state);
|
||||
if (t.isTSImportEqualsDeclaration(node)) collectImportEquals(node, state);
|
||||
else if (t.isImportDeclaration(node)) collectImports(node, state);
|
||||
else if (t.isExportAllDeclaration(node))
|
||||
addModuleLink(
|
||||
state,
|
||||
@@ -103,6 +104,36 @@ export const collectSemanticModuleLink = (
|
||||
else if (t.isCallExpression(node)) collectUnboundRequireLink(node, state);
|
||||
};
|
||||
|
||||
const collectImportEquals = (
|
||||
node: t.TSImportEqualsDeclaration,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
const specifier = semanticImportEqualsSpecifier(node);
|
||||
if (specifier === null) return;
|
||||
addModuleLink(
|
||||
state,
|
||||
{
|
||||
kind: "require",
|
||||
specifier,
|
||||
importedName: null,
|
||||
localName: node.id.name,
|
||||
exportedName: null,
|
||||
location: range(node),
|
||||
},
|
||||
node,
|
||||
);
|
||||
};
|
||||
|
||||
/** Runtime external-module target for a value import-equals declaration. */
|
||||
export const semanticImportEqualsSpecifier = (
|
||||
node: t.TSImportEqualsDeclaration,
|
||||
): string | null =>
|
||||
node.importKind !== "type" &&
|
||||
t.isTSExternalModuleReference(node.moduleReference) &&
|
||||
t.isStringLiteral(node.moduleReference.expression)
|
||||
? node.moduleReference.expression.value
|
||||
: null;
|
||||
|
||||
/** Collect lexical references after definitions exist, optionally for one name. */
|
||||
export const collectSemanticReferences = (
|
||||
program: t.Program,
|
||||
@@ -707,6 +738,11 @@ const declarationNames = (declaration: t.Declaration): string[] => {
|
||||
t.isIdentifier(declaration.id)
|
||||
)
|
||||
return [declaration.id.name];
|
||||
if (
|
||||
t.isTSImportEqualsDeclaration(declaration) &&
|
||||
semanticImportEqualsSpecifier(declaration) !== null
|
||||
)
|
||||
return [declaration.id.name];
|
||||
return [];
|
||||
};
|
||||
|
||||
|
||||
+84
@@ -14,6 +14,90 @@ import {
|
||||
writeFixtureFiles,
|
||||
} from "../../../support/javascriptApplicationFixture.js";
|
||||
|
||||
describe("TypeScript source module loader semantics", () => {
|
||||
it("uses emitted TypeScript loader semantics without changing dynamic imports", async () => {
|
||||
const root = await createTestTempDirectory("rea-cts-module-loader-");
|
||||
await writeFixtureFiles(root, {
|
||||
"package.json": '{"type":"module"}',
|
||||
"main.cts": `
|
||||
import local = require("./local");
|
||||
import selected from "conditional";
|
||||
export { retained } from "conditional";
|
||||
void import("conditional");
|
||||
void local;
|
||||
void selected;
|
||||
`,
|
||||
"consumer.mts": `
|
||||
import local = require("./local");
|
||||
export { local };
|
||||
`,
|
||||
"local.js": "module.exports = 42;",
|
||||
"node_modules/conditional/package.json": JSON.stringify({
|
||||
exports: {
|
||||
".": {
|
||||
import: "./esm.mjs",
|
||||
require: "./cjs.cjs",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"node_modules/conditional/esm.mjs": "export const retained = 'esm';",
|
||||
"node_modules/conditional/cjs.cjs": "exports.retained = 'cjs';",
|
||||
});
|
||||
|
||||
const { graph } = await reconstructJavaScriptFixture(root);
|
||||
const localRequireEdge = (sourcePath: string) => {
|
||||
const source = findSourceModule(graph, sourcePath);
|
||||
return graph.edges.find(
|
||||
({ source_node_id, relation, properties }) =>
|
||||
source_node_id === source?.node_id &&
|
||||
relation === "imports" &&
|
||||
properties.module_link_kind === "require" &&
|
||||
properties.specifier === "./local",
|
||||
);
|
||||
};
|
||||
|
||||
expect(
|
||||
findSourceModule(graph, "main.cts")?.observations[0]?.properties,
|
||||
).toMatchObject({ module_format: "commonjs" });
|
||||
expect(localRequireEdge("main.cts")).toMatchObject({
|
||||
properties: { resolved_path: "local.js", local_name: "local" },
|
||||
});
|
||||
expect(
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "import",
|
||||
specifier: "conditional",
|
||||
resolvedPath: "node_modules/conditional/cjs.cjs",
|
||||
localName: "selected",
|
||||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "re-export",
|
||||
specifier: "conditional",
|
||||
resolvedPath: "node_modules/conditional/cjs.cjs",
|
||||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.kind === "dynamic-import" &&
|
||||
properties.specifier === "conditional",
|
||||
),
|
||||
).toMatchObject({
|
||||
properties: {
|
||||
resolved_path: "node_modules/conditional/esm.mjs",
|
||||
},
|
||||
});
|
||||
expect(
|
||||
findSourceModule(graph, "consumer.mts")?.observations[0]?.properties,
|
||||
).toMatchObject({ module_format: "esm" });
|
||||
expect(localRequireEdge("consumer.mts")).toMatchObject({
|
||||
properties: { resolved_path: "local.js", local_name: "local" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommonJS and ESM module relationships", () => {
|
||||
it("composes bindings, re-exports, dynamic imports, and JSON modules", async () => {
|
||||
const root = await moduleFixture();
|
||||
|
||||
Reference in new issue
Block a user