mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
334 lines
9.8 KiB
TypeScript
334 lines
9.8 KiB
TypeScript
import type {
|
|
CallExpression,
|
|
File,
|
|
OptionalCallExpression,
|
|
Node,
|
|
} from "@babel/types";
|
|
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
|
|
import { readExactJavaScriptLiteral } from "./javascript/javascriptAstValues.js";
|
|
import {
|
|
parseRecoveredJavaScript,
|
|
parserPluginsForPath,
|
|
} from "./javascript/javascriptSourceParser.js";
|
|
import {
|
|
referenceSourceRequireCalls,
|
|
type ReferenceSourceRequireCall,
|
|
} from "./referenceSourceRequireBindings.js";
|
|
import { isRelativeJavaScriptModuleSpecifier } from "./javascript/javascriptModuleSpecifier.js";
|
|
import {
|
|
isCallExpression,
|
|
isExportAllDeclaration,
|
|
isExportNamedDeclaration,
|
|
isImportDeclaration,
|
|
isImportExpression,
|
|
isOptionalCallExpression,
|
|
isTSImportType,
|
|
isStringLiteral,
|
|
isTSExternalModuleReference,
|
|
isTSImportEqualsDeclaration,
|
|
isTSModuleDeclaration,
|
|
} from "@babel/types";
|
|
|
|
type ReferenceSourceImportKind =
|
|
| "imports"
|
|
| "requires"
|
|
| "references"
|
|
| "declares-module";
|
|
|
|
type ReferenceSourceImportResolution =
|
|
| "internal"
|
|
| "external"
|
|
| "unresolved"
|
|
| "unknown";
|
|
|
|
type ReferenceSourceImportParseState = "parsed" | "partial" | "unknown";
|
|
|
|
interface ReferenceSourceImportRelationship {
|
|
readonly from_path: string;
|
|
readonly to: string;
|
|
readonly kind: ReferenceSourceImportKind;
|
|
readonly resolution: ReferenceSourceImportResolution;
|
|
readonly parse_state: ReferenceSourceImportParseState;
|
|
/** Distinguishes call-form CommonJS require from TS import-equals `requires`. */
|
|
readonly resolutionStrategy?: "runtime-require";
|
|
/** Internal lookup origin for a proven createRequire URL base. */
|
|
readonly resolutionBasePath?: string;
|
|
}
|
|
|
|
interface ReferenceSourceParseFailure {
|
|
readonly path: string;
|
|
readonly parser: string;
|
|
readonly reason: string;
|
|
}
|
|
|
|
export interface ReferenceSourceImportParseResult {
|
|
readonly relationships: readonly ReferenceSourceImportRelationship[];
|
|
readonly parse_failures: readonly ReferenceSourceParseFailure[];
|
|
readonly limitations: readonly string[];
|
|
}
|
|
|
|
export const PARSEABLE_REFERENCE_SOURCE_LANGUAGES: ReadonlySet<string> =
|
|
new Set(["JavaScript", "TypeScript", "JSX", "TSX"]);
|
|
|
|
const safeModuleName = (node: Node | null | undefined): string | undefined => {
|
|
if (isStringLiteral(node)) return node.value;
|
|
return undefined;
|
|
};
|
|
|
|
const exactStringModuleName = (
|
|
node: Node | null | undefined,
|
|
): string | undefined => {
|
|
if (node === null || node === undefined) return undefined;
|
|
const literal = readExactJavaScriptLiteral(node);
|
|
return literal.found && typeof literal.value === "string"
|
|
? literal.value
|
|
: undefined;
|
|
};
|
|
|
|
const resolveSpecifier = (
|
|
specifier: string,
|
|
): ReferenceSourceImportResolution => {
|
|
if (
|
|
isRelativeJavaScriptModuleSpecifier(specifier) ||
|
|
specifier.startsWith("/")
|
|
)
|
|
return "internal";
|
|
return "external";
|
|
};
|
|
|
|
const appendRelationship = (
|
|
relationships: ReferenceSourceImportRelationship[],
|
|
limitations: string[],
|
|
input: {
|
|
readonly fromPath: string;
|
|
readonly to: string | undefined;
|
|
readonly kind: ReferenceSourceImportKind;
|
|
readonly parseState: ReferenceSourceImportParseState;
|
|
readonly resolution?: ReferenceSourceImportResolution;
|
|
readonly resolutionStrategy?: "runtime-require";
|
|
readonly resolutionBasePath?: string;
|
|
},
|
|
): void => {
|
|
if (input.to === undefined) return;
|
|
if (input.to.length === 0) {
|
|
limitations.push(
|
|
`Empty module specifier in ${input.fromPath} was not recorded as a relationship.`,
|
|
);
|
|
return;
|
|
}
|
|
relationships.push({
|
|
from_path: input.fromPath,
|
|
to: input.to,
|
|
kind: input.kind,
|
|
resolution: input.resolution ?? resolveSpecifier(input.to),
|
|
parse_state: input.parseState,
|
|
...(input.resolutionStrategy === undefined
|
|
? {}
|
|
: { resolutionStrategy: input.resolutionStrategy }),
|
|
...(input.resolutionBasePath === undefined
|
|
? {}
|
|
: { resolutionBasePath: input.resolutionBasePath }),
|
|
});
|
|
};
|
|
|
|
const appendImportEquals = (
|
|
statement: Node,
|
|
from_path: string,
|
|
relationships: ReferenceSourceImportRelationship[],
|
|
limitations: string[],
|
|
): void => {
|
|
if (!isTSImportEqualsDeclaration(statement)) return;
|
|
const reference = statement.moduleReference;
|
|
if (!isTSExternalModuleReference(reference)) return;
|
|
appendRelationship(relationships, limitations, {
|
|
fromPath: from_path,
|
|
to: safeModuleName(reference.expression),
|
|
kind: "requires",
|
|
parseState: "parsed",
|
|
});
|
|
};
|
|
|
|
/** One AST owner retains historical declarations, types and runtime requests. */
|
|
const extractImports = (
|
|
ast: File,
|
|
from_path: string,
|
|
relationships: ReferenceSourceImportRelationship[],
|
|
limitations: string[],
|
|
requireCalls: ReadonlyMap<
|
|
CallExpression | OptionalCallExpression,
|
|
ReferenceSourceRequireCall
|
|
>,
|
|
): void => {
|
|
traverseJavaScriptAst(ast.program, {
|
|
enter: (node) => {
|
|
if (
|
|
isImportDeclaration(node) ||
|
|
isExportAllDeclaration(node) ||
|
|
(isExportNamedDeclaration(node) && node.source != null)
|
|
) {
|
|
appendRelationship(relationships, limitations, {
|
|
fromPath: from_path,
|
|
to: safeModuleName(node.source),
|
|
kind: "imports",
|
|
parseState: "parsed",
|
|
});
|
|
} else if (isTSImportEqualsDeclaration(node)) {
|
|
appendImportEquals(node, from_path, relationships, limitations);
|
|
} else if (isTSModuleDeclaration(node)) {
|
|
if (node.declare === true && isStringLiteral(node.id))
|
|
appendRelationship(relationships, limitations, {
|
|
fromPath: from_path,
|
|
to: node.id.value,
|
|
kind: "declares-module",
|
|
parseState: "parsed",
|
|
resolution: "unknown",
|
|
});
|
|
} else if (isImportExpression(node) || isTSImportType(node)) {
|
|
const specifier = exactStringModuleName(
|
|
isTSImportType(node) ? node.argument : node.source,
|
|
);
|
|
appendRelationship(relationships, limitations, {
|
|
fromPath: from_path,
|
|
to: specifier ?? "<dynamic-import>",
|
|
kind: "imports",
|
|
parseState: specifier === undefined ? "partial" : "parsed",
|
|
...(specifier === undefined ? { resolution: "unknown" } : {}),
|
|
});
|
|
} else if (isCallExpression(node) || isOptionalCallExpression(node)) {
|
|
const requireCall = requireCalls.get(node);
|
|
if (requireCall === undefined) return;
|
|
const specifier = exactStringModuleName(requireCall.arguments?.[0]);
|
|
if (specifier === undefined) return;
|
|
const uncertainResolution = requireCall.resolutionBase === "unknown";
|
|
appendRelationship(relationships, limitations, {
|
|
fromPath: from_path,
|
|
to: specifier,
|
|
kind: "requires",
|
|
parseState: uncertainResolution ? "partial" : "parsed",
|
|
...(uncertainResolution ? { resolution: "unknown" } : {}),
|
|
...(uncertainResolution
|
|
? {}
|
|
: { resolutionStrategy: "runtime-require" as const }),
|
|
...(typeof requireCall.resolutionBase === "object"
|
|
? { resolutionBasePath: requireCall.resolutionBase.path }
|
|
: {}),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const parseWithBabel = (
|
|
path: string,
|
|
source: string,
|
|
language: string | null,
|
|
): {
|
|
ast: File | undefined;
|
|
reasons: readonly string[];
|
|
moduleOnlyErrors: readonly string[];
|
|
} => {
|
|
// Single parser-mode owner: path-driven plugins (dts/JSX/mts) shared with
|
|
// the semantic pipeline. `language` only gates non-JS/TS callers upstream;
|
|
// TypeScript syntax must parse identically here and in semantic analysis.
|
|
void language;
|
|
const plugins = parserPluginsForPath(path);
|
|
|
|
try {
|
|
const { file: ast, moduleOnlyErrors } = parseRecoveredJavaScript(
|
|
source,
|
|
path,
|
|
{
|
|
allowImportExportEverywhere: false,
|
|
allowReturnOutsideFunction: true,
|
|
allowUndeclaredExports: true,
|
|
plugins,
|
|
},
|
|
);
|
|
return {
|
|
ast,
|
|
reasons: ast.errors.map((error) => error.message),
|
|
moduleOnlyErrors,
|
|
};
|
|
} catch (cause: unknown) {
|
|
return {
|
|
ast: undefined,
|
|
reasons: [cause instanceof Error ? cause.message : "Parse failed"],
|
|
moduleOnlyErrors: [],
|
|
};
|
|
}
|
|
};
|
|
|
|
/** Parse ESM/CommonJS imports and dynamic imports from source bytes. */
|
|
export const parseReferenceSourceImports = (
|
|
path: string,
|
|
bytes: Uint8Array,
|
|
language: string | null,
|
|
packageType?: "module" | "commonjs" | "unknown",
|
|
): ReferenceSourceImportParseResult => {
|
|
if (language !== null && !PARSEABLE_REFERENCE_SOURCE_LANGUAGES.has(language))
|
|
return { relationships: [], parse_failures: [], limitations: [] };
|
|
|
|
let source: string;
|
|
try {
|
|
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
} catch {
|
|
return {
|
|
relationships: [],
|
|
parse_failures: [
|
|
{
|
|
path,
|
|
parser: "utf-8",
|
|
reason:
|
|
"Source bytes are not valid UTF-8; import targets were not parsed.",
|
|
},
|
|
],
|
|
limitations: [],
|
|
};
|
|
}
|
|
const { ast, reasons, moduleOnlyErrors } = parseWithBabel(
|
|
path,
|
|
source,
|
|
language,
|
|
);
|
|
const limitations =
|
|
moduleOnlyErrors.length === 0
|
|
? []
|
|
: [
|
|
`Strict-mode errors apply only if this file is an ES module: ${moduleOnlyErrors.join("; ")}`,
|
|
];
|
|
const parseFailures = reasons.map((reason) => ({
|
|
path,
|
|
parser: "babel",
|
|
reason,
|
|
}));
|
|
|
|
if (ast === undefined) {
|
|
return {
|
|
relationships: [],
|
|
parse_failures: parseFailures,
|
|
limitations,
|
|
};
|
|
}
|
|
|
|
const relationships: ReferenceSourceImportRelationship[] = [];
|
|
extractImports(
|
|
ast,
|
|
path,
|
|
relationships,
|
|
limitations,
|
|
referenceSourceRequireCalls(ast, path, packageType),
|
|
);
|
|
|
|
return {
|
|
relationships: relationships.map((relationship) => ({
|
|
...relationship,
|
|
parse_state:
|
|
parseFailures.length > 0 && relationship.parse_state === "parsed"
|
|
? "partial"
|
|
: relationship.parse_state,
|
|
})),
|
|
parse_failures: parseFailures,
|
|
limitations,
|
|
};
|
|
};
|