fix(reference): retain dynamic import relationships
This commit is contained in:
parent
19f8cddfd6
commit
b89de8feab
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseReferenceSourceImports } from "./referenceSourceImportParsing.js";
|
||||
|
||||
const parse = (source: string) =>
|
||||
parseReferenceSourceImports(
|
||||
"main.ts",
|
||||
new TextEncoder().encode(source),
|
||||
"TypeScript",
|
||||
);
|
||||
|
||||
describe("historical source dynamic imports", () => {
|
||||
it.each([
|
||||
'import("./lazy.js");',
|
||||
'const module = import("./lazy.js");',
|
||||
'async function load() { return await import("./lazy.js"); }',
|
||||
'import("./lazy.js", { with: { type: "json" } });',
|
||||
])("retains the literal dependency in %s", (source) => {
|
||||
expect(parse(source)).toEqual({
|
||||
relationships: [
|
||||
{
|
||||
from_path: "main.ts",
|
||||
to: "./lazy.js",
|
||||
kind: "imports",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
],
|
||||
parse_failures: [],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"import(moduleName);",
|
||||
"async function load(name: string) { return await import(name); }",
|
||||
"import(`./${moduleName}.js`);",
|
||||
])("keeps a computed dependency unknown in %s", (source) => {
|
||||
expect(parse(source)).toEqual({
|
||||
relationships: [
|
||||
{
|
||||
from_path: "main.ts",
|
||||
to: "<dynamic-import>",
|
||||
kind: "imports",
|
||||
resolution: "unknown",
|
||||
parse_state: "partial",
|
||||
},
|
||||
],
|
||||
parse_failures: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("retains static import and require relationships alongside dynamic imports", () => {
|
||||
const result = parse(
|
||||
'import "./static.js"; require("./common.cjs"); import("node:fs");',
|
||||
);
|
||||
expect(result.parse_failures).toEqual([]);
|
||||
expect(result.relationships).toEqual([
|
||||
{
|
||||
from_path: "main.ts",
|
||||
to: "./static.js",
|
||||
kind: "imports",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
{
|
||||
from_path: "main.ts",
|
||||
to: "./common.cjs",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
{
|
||||
from_path: "main.ts",
|
||||
to: "node:fs",
|
||||
kind: "imports",
|
||||
resolution: "external",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
import { parse, type ParserPlugin } from "@babel/parser";
|
||||
import type { CallExpression, File, Node, StringLiteral } from "@babel/types";
|
||||
import type {
|
||||
CallExpression,
|
||||
File,
|
||||
ImportExpression,
|
||||
Node,
|
||||
StringLiteral,
|
||||
} from "@babel/types";
|
||||
import {
|
||||
isCallExpression,
|
||||
isExportAllDeclaration,
|
||||
|
|
@ -7,6 +13,7 @@ import {
|
|||
isIdentifier,
|
||||
isImport,
|
||||
isImportDeclaration,
|
||||
isImportExpression,
|
||||
isMemberExpression,
|
||||
isNode,
|
||||
isStringLiteral,
|
||||
|
|
@ -196,18 +203,18 @@ const isRequireCallee = (callee: Node | null | undefined): boolean => {
|
|||
const isImportCallee = (callee: Node | null | undefined): boolean =>
|
||||
isImport(callee);
|
||||
|
||||
const collectCallExpressions = (
|
||||
const collectModuleExpressions = (
|
||||
node: unknown,
|
||||
targets: CallExpression[],
|
||||
targets: Array<CallExpression | ImportExpression>,
|
||||
): void => {
|
||||
if (node === null || node === undefined) return;
|
||||
if (typeof node !== "object") return;
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) collectCallExpressions(item, targets);
|
||||
for (const item of node) collectModuleExpressions(item, targets);
|
||||
return;
|
||||
}
|
||||
if (!isNode(node)) return;
|
||||
if (isCallExpression(node)) {
|
||||
if (isCallExpression(node) || isImportExpression(node)) {
|
||||
targets.push(node);
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
|
|
@ -217,7 +224,7 @@ const collectCallExpressions = (
|
|||
typeof value === "object" &&
|
||||
!isSourceLocation(value)
|
||||
) {
|
||||
collectCallExpressions(value, targets);
|
||||
collectModuleExpressions(value, targets);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -234,10 +241,22 @@ const extractRequireAndDynamicImports = (
|
|||
from_path: string,
|
||||
relationships: ReferenceSourceImportRelationship[],
|
||||
): void => {
|
||||
const calls: CallExpression[] = [];
|
||||
for (const statement of body) collectCallExpressions(statement, calls);
|
||||
const expressions: Array<CallExpression | ImportExpression> = [];
|
||||
for (const statement of body)
|
||||
collectModuleExpressions(statement, expressions);
|
||||
|
||||
for (const call of calls) {
|
||||
for (const call of expressions) {
|
||||
if (isImportExpression(call)) {
|
||||
const specifier = safeModuleName(call.source);
|
||||
appendRelationship(relationships, {
|
||||
fromPath: from_path,
|
||||
to: specifier ?? "<dynamic-import>",
|
||||
kind: "imports",
|
||||
parseState: specifier === undefined ? "partial" : "parsed",
|
||||
...(specifier === undefined ? { resolution: "unknown" } : {}),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const first = call.arguments[0];
|
||||
if (isRequireCallee(call.callee) && isModuleExpression(first)) {
|
||||
const result = moduleSpecifierFromExpression(first);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { importReferenceSource } from "../../../src/application/ReferenceSourceImport.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("resolves dynamic source imports while retaining computed targets as unknown", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-dynamic-import-");
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(root, "main.ts"),
|
||||
[
|
||||
'import "./static";',
|
||||
'require("./common");',
|
||||
'const lazy = import("./lazy");',
|
||||
'async function load() { return await import("./data.json", { with: { type: "json" } }); }',
|
||||
'const name = "./not-guessed.js"; import(name);',
|
||||
].join("\n"),
|
||||
),
|
||||
...["static.ts", "common.js", "lazy.ts"].map((path) =>
|
||||
writeFile(join(root, path), "export {};\n"),
|
||||
),
|
||||
writeFile(join(root, "data.json"), '{"value":1}\n'),
|
||||
]);
|
||||
|
||||
const result = await importReferenceSource({
|
||||
root,
|
||||
caller: "reference-dynamic-import-test",
|
||||
policy: { secretPatterns: [] },
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.parse_failures).toEqual([]);
|
||||
expect(result.value.relationships).toHaveLength(5);
|
||||
for (const [to, kind] of [
|
||||
["static.ts", "imports"],
|
||||
["common.js", "requires"],
|
||||
["lazy.ts", "imports"],
|
||||
["data.json", "imports"],
|
||||
])
|
||||
expect(result.value.relationships).toContainEqual({
|
||||
from_path: "main.ts",
|
||||
to,
|
||||
kind,
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
});
|
||||
expect(result.value.relationships).toContainEqual({
|
||||
from_path: "main.ts",
|
||||
to: "<dynamic-import>",
|
||||
kind: "imports",
|
||||
resolution: "unknown",
|
||||
parse_state: "partial",
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue