openclaw/scripts/lib/runtime-import-closure.mts
Peter Steinberger 39373e69f8
chore(deps): refresh dependencies through September 19 cutoff (#159401)
Refresh application, plugin, native, build and container dependencies through the fixed 2026-09-19T16:27:11Z cutoff. Migrate native TypeScript snapshot/printer APIs while preserving compilation and filesystem contracts; retain existing patches and compatibility holds. Document offline container-image preparation.

Include the verified compiler process-census and loading-clock fixture repairs and deterministic warm-history regression. Adopt the canonical production history fixes from #159924 and #159955.

Land under the maintainer's explicit approval to treat proven pre-existing CI failures as non-blocking and repair main afterward. CI36365552098 failed an unchanged Android Compose fixture's asynchronous catalog projection assertion (3518 passed,1 failed); the Android/Gradle tree matches its main parent byte-for-byte. Security and dependency reviews passed. The final rebase preserves reviewed source changes and regenerates only the intentional Node-image documentation fingerprint. See PR159401 for complete validation and the follow-up repair obligation.
2026-09-27 19:33:08 -07:00

307 lines
11 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { isBuiltin } from "node:module";
import { join, relative, resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
import { transformSync } from "esbuild";
import { resolve as resolvePackageImport } from "import-meta-resolve";
import * as ts from "typescript/unstable/ast";
import { readNativeTypeScriptConfig } from "./native-typescript-config.mts";
import {
createNativeTypeScriptParser,
createNativeTypeScriptProject,
} from "./native-typescript.mts";
import { visitModuleSpecifiers } from "./ts-guard-utils.mts";
const sourceFilePattern = /\.[cm]?[jt]sx?$/;
type ImportReference = {
kind: string;
specifier: string;
resolutionMode?: "import" | "require";
};
type RuntimeImportGraphOptions = {
includeDynamicImports?: boolean;
includeCommonJs?: boolean;
includeTypeOnlyImports?: boolean;
includeImportMetaUrl?: boolean;
sourceImports?: boolean;
normalizeSpecifier?: (specifier: string) => string;
};
function isTypeOnlyReference(node: ts.Node): boolean {
if (ts.isImportDeclaration(node)) {
const clause = node.importClause;
return Boolean(
clause?.phaseModifier === ts.SyntaxKind.TypeKeyword ||
(clause &&
!clause.name &&
clause.namedBindings &&
ts.isNamedImports(clause.namedBindings) &&
clause.namedBindings.elements.length > 0 &&
clause.namedBindings.elements.every((binding) => binding.isTypeOnly)),
);
}
if (ts.isExportDeclaration(node)) {
return Boolean(
node.isTypeOnly ||
(node.exportClause &&
ts.isNamedExports(node.exportClause) &&
node.exportClause.elements.length > 0 &&
node.exportClause.elements.every((binding) => binding.isTypeOnly)),
);
}
return ts.isImportEqualsDeclaration(node) && node.isTypeOnly;
}
function resolutionMode(node: ts.Node, kind: string): "import" | "require" | undefined {
const attributes =
ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isImportTypeNode(node)
? node.attributes?.attributes
: undefined;
const attribute = attributes?.find((item) => item.name.text === "resolution-mode");
if (
attribute &&
ts.isStringLiteral(attribute.value) &&
(attribute.value.text === "import" || attribute.value.text === "require")
) {
return attribute.value.text;
}
if (kind === "commonjs-require") {
return "require";
}
return kind === "dynamic-import" && ts.isCallExpression(node) ? "import" : undefined;
}
/** Resolve selected runtime edges with the native compiler's config and package rules. */
export function createRuntimeImportGraph(
rootPath: string,
inputs: readonly string[],
{
includeDynamicImports = false,
includeCommonJs = includeDynamicImports,
includeTypeOnlyImports = false,
includeImportMetaUrl = false,
sourceImports = false,
normalizeSpecifier = (specifier) => specifier,
}: RuntimeImportGraphOptions = {},
) {
const root = resolve(rootPath);
const configFileName = join(root, ".openclaw-runtime-imports.tsconfig.json");
const roots = new Set(
inputs.filter((file) => sourceFilePattern.test(file)).map((file) => resolve(root, file)),
);
const configText = () =>
JSON.stringify({
extends: join(root, "tsconfig.json"),
compilerOptions: { allowJs: true, noLib: true, types: [] },
files: [...roots],
include: [],
});
const { options } = readNativeTypeScriptConfig({
cwd: root,
configFileName,
readFile: (file) =>
resolve(file) === configFileName ? configText() : readFileSync(file, "utf8"),
});
if (!sourceImports && (options.emitDecoratorMetadata || options.importHelpers)) {
throw new Error(
"Runtime import capture does not support decorator metadata or imported emit helpers",
);
}
const jsx =
typeof options.jsx === "number"
? [undefined, "preserve", "react", "react-native", "react-jsx", "react-jsxdev"][options.jsx]
: options.jsx;
const transformConfig = JSON.stringify({
compilerOptions: {
experimentalDecorators: options.experimentalDecorators,
jsx,
jsxFactory: options.jsxFactory,
jsxFragmentFactory: options.jsxFragmentFactory,
jsxImportSource: options.jsxImportSource,
useDefineForClassFields: options.useDefineForClassFields,
verbatimModuleSyntax: options.verbatimModuleSyntax,
},
});
const parser = createNativeTypeScriptParser({ cwd: root });
const references = new Map<string, ImportReference[]>();
const isDeclaration = (file: string) => /\.d\.[cm]?ts$/.test(file);
let session;
try {
session = createNativeTypeScriptProject({
cwd: root,
configFileName,
fs: {
fileExists: (file) =>
resolve(file) === configFileName
? true
: !sourceImports && isDeclaration(file)
? false
: undefined,
readFile(file) {
if (resolve(file) === configFileName) {
return configText();
}
if (isDeclaration(file)) {
return sourceImports ? undefined : null;
}
if (!/\.[cm]?[jt]sx?$/.test(file) || file.split(/[\\/]/).includes("node_modules")) {
return undefined;
}
if (!existsSync(file)) {
return null;
}
const text = readFileSync(file, "utf8");
// Source guards discard explicitly type-only bindings. Runtime capture follows
// emitted code, including side effects retained by verbatimModuleSyntax.
const sourceText = sourceImports
? text
: transformSync(text, {
sourcefile: file,
loader: /\.[cm]?tsx?$/.test(file)
? file.endsWith("x")
? "tsx"
: "ts"
: file.endsWith("x")
? "jsx"
: "js",
format: "esm",
target: "esnext",
tsconfigRaw: transformConfig,
}).code;
const source = parser.parseSourceFile(file, sourceText);
const selected: ImportReference[] = [];
visitModuleSpecifiers(
source,
({ kind, specifier, node }: ImportReference & { node: ts.Node }) => {
if (
(kind === "dynamic-import" && !includeDynamicImports) ||
(sourceImports && !includeTypeOnlyImports && isTypeOnlyReference(node)) ||
isBuiltin(specifier)
) {
return;
}
selected.push({
kind,
specifier: normalizeSpecifier(specifier),
resolutionMode: resolutionMode(node, kind),
});
},
{ includeCommonJs, includeImportTypes: includeTypeOnlyImports, includeImportMetaUrl },
);
references.set(resolve(file), selected);
// A require literal has no checker symbol. Project each selected edge into
// an import at the original path, retaining NodeNext's require conditions.
return selected
.map(
({ specifier, resolutionMode: mode }, index) =>
`import ${mode ? "type " : ""}* as edge${index} from ${JSON.stringify(specifier)}${mode ? ` with { "resolution-mode": ${JSON.stringify(mode)} }` : ""};`,
)
.concat("export {};")
.join("\n");
},
},
});
} catch (error) {
session?.close();
parser.close();
throw error;
}
let { project, snapshot } = session;
const close = () => {
session.close();
parser.close();
};
return {
compilerOptions: project.compilerOptions,
dependencies(file: string) {
const absolute = resolve(root, file);
let source = project.program.getSourceFile(absolute);
if (!source && sourceFilePattern.test(absolute) && !roots.has(absolute)) {
// Exact runtime files and computed generator inputs can be outside the
// compiler's discovered graph. Admit them into this same projection.
roots.add(absolute);
const updated = snapshot.update({
fileNotifications: { changed: [configFileName] },
ensurePrograms: true,
});
const nextProject = updated.getConfiguredProject(configFileName);
if (!nextProject) {
throw new Error(`Native TypeScript did not reopen runtime graph ${configFileName}`);
}
snapshot.dispose();
snapshot = updated;
project = nextProject;
source = project.program.getSourceFile(absolute);
}
if (!source) {
throw new Error(`Native TypeScript did not load runtime source ${absolute}`);
}
const selected = references.get(absolute) ?? [];
const imports = source.statements.filter(ts.isImportDeclaration);
const symbols = project.checker.getSymbolAtLocation(
imports.map((node) => node.moduleSpecifier),
);
return selected.map((reference, index) => {
const declaration = symbols[index]?.declarations.find(
(candidate) => candidate.kind === ts.SyntaxKind.SourceFile,
);
const resolvedFileName = declaration?.resolve(project)?.getSourceFile().fileName;
return Object.assign({}, reference, {
resolvedFileName,
isExternalLibraryImport: resolvedFileName
? project.program.getSourceFileMetadata(resolvedFileName)?.isFromExternalLibrary ===
true
: false,
});
});
},
close,
[Symbol.dispose]: close,
};
}
export function collectRuntimeImportClosure(
root: string,
inputs: readonly string[],
{
includeDynamicImports = false,
validatePackages = false,
}: { includeDynamicImports?: boolean; validatePackages?: boolean } = {},
): string[] {
const closure = new Set(inputs.map((file) => file.split(sep).join("/")));
const sourceInputs = inputs.filter((file) => /\.[cm]?[jt]s$/.test(file));
if (sourceInputs.length === 0) {
return [...closure].toSorted();
}
const graph = createRuntimeImportGraph(root, sourceInputs, { includeDynamicImports });
try {
for (const file of closure) {
if (!/\.[cm]?[jt]s$/.test(file)) {
continue;
}
for (const { specifier, resolvedFileName, isExternalLibraryImport } of graph.dependencies(
file,
)) {
if (!resolvedFileName && specifier.startsWith(".")) {
throw new Error(`${file}: unresolved ${specifier}`);
}
if (resolvedFileName && !isExternalLibraryImport) {
closure.add(relative(root, resolvedFileName).split(sep).join("/"));
} else if (validatePackages) {
const packageName = specifier
.split("/")
.slice(0, specifier.startsWith("@") ? 2 : 1)
.join("/");
if (!existsSync(join(root, "node_modules", packageName, "package.json"))) {
throw new Error(`${file}: unpinned package ${specifier}`);
}
resolvePackageImport(specifier, pathToFileURL(resolve(root, file)).href);
}
}
}
return [...closure].toSorted();
} finally {
graph.close();
}
}