mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 20:46:57 +02:00
267 lines
7.9 KiB
JavaScript
267 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import ts from "typescript";
|
|
import {
|
|
collectTypeScriptFilesFromRoots,
|
|
resolveRepoRoot,
|
|
runAsScript,
|
|
toLine,
|
|
unwrapExpression,
|
|
} from "./lib/ts-guard-utils.mjs";
|
|
|
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
const defaultSourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
|
|
const localRuntimeSpecifierPattern = /^\.{1,2}\/.*\.runtime\.(?:js|ts)$/;
|
|
|
|
function toPosixPath(value) {
|
|
return value.split(path.sep).join("/");
|
|
}
|
|
|
|
function normalizeRelativePath(value) {
|
|
return path.posix.normalize(toPosixPath(value).replace(/^\.\//, ""));
|
|
}
|
|
|
|
function unwrapInitializer(expression) {
|
|
let current = unwrapExpression(expression);
|
|
while (ts.isSatisfiesExpression(current)) {
|
|
current = unwrapExpression(current.expression);
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function readStringLiteral(node) {
|
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
return node.text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function readArrayStrings(node) {
|
|
const expression = unwrapInitializer(node);
|
|
if (!ts.isArrayLiteralExpression(expression)) {
|
|
return null;
|
|
}
|
|
const values = [];
|
|
for (const element of expression.elements) {
|
|
const value = readStringLiteral(unwrapInitializer(element));
|
|
if (value === null) {
|
|
return null;
|
|
}
|
|
values.push(value);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function isCreateRequireCall(node, createRequireNames) {
|
|
return (
|
|
ts.isCallExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
createRequireNames.has(node.expression.text)
|
|
);
|
|
}
|
|
|
|
function isLocalRuntimeSpecifier(specifier) {
|
|
return localRuntimeSpecifierPattern.test(specifier);
|
|
}
|
|
|
|
function resolveRuntimeSpecifierSource(importerPath, specifier) {
|
|
const importerDir = path.posix.dirname(normalizeRelativePath(importerPath));
|
|
const resolved = path.posix.normalize(path.posix.join(importerDir, specifier));
|
|
return resolved.replace(/\.js$/, ".ts");
|
|
}
|
|
|
|
function readObjectEntrySources(entry) {
|
|
if (!entry || Array.isArray(entry) || typeof entry !== "object") {
|
|
return [];
|
|
}
|
|
return Object.values(entry).filter((value) => typeof value === "string");
|
|
}
|
|
|
|
export function collectTsdownEntrySources(config) {
|
|
const configs = Array.isArray(config) ? config : [config];
|
|
return new Set(
|
|
configs.flatMap((entry) => readObjectEntrySources(entry?.entry)).map(normalizeRelativePath),
|
|
);
|
|
}
|
|
|
|
export function findRuntimeSidecarLoaderViolations(content, importerPath, explicitEntrySources) {
|
|
const sourceFile = ts.createSourceFile(importerPath, content, ts.ScriptTarget.Latest, true);
|
|
const createRequireNames = new Set();
|
|
const requireNames = new Set();
|
|
const stringConstants = new Map();
|
|
const stringArrays = new Map();
|
|
const forOfRuntimeValues = [];
|
|
const violations = [];
|
|
const seen = new Set();
|
|
|
|
const currentForOfValueMap = () => {
|
|
const merged = new Map();
|
|
for (const scope of forOfRuntimeValues) {
|
|
for (const [name, values] of scope) {
|
|
merged.set(name, values);
|
|
}
|
|
}
|
|
return merged;
|
|
};
|
|
|
|
const addSpecifier = (specifier, node) => {
|
|
if (!isLocalRuntimeSpecifier(specifier)) {
|
|
return;
|
|
}
|
|
const sourcePath = resolveRuntimeSpecifierSource(importerPath, specifier);
|
|
if (explicitEntrySources.has(sourcePath)) {
|
|
return;
|
|
}
|
|
const key = `${sourcePath}:${toLine(sourceFile, node)}`;
|
|
if (seen.has(key)) {
|
|
return;
|
|
}
|
|
seen.add(key);
|
|
violations.push({
|
|
line: toLine(sourceFile, node),
|
|
specifier,
|
|
sourcePath,
|
|
reason:
|
|
`hidden local runtime loader "${specifier}" resolves to ${sourcePath}, ` +
|
|
"but that source is not an explicit tsdown entry",
|
|
});
|
|
};
|
|
|
|
const readRequireArgumentSpecifiers = (node) => {
|
|
const arg = node.arguments[0];
|
|
if (!arg) {
|
|
return [];
|
|
}
|
|
const unwrapped = unwrapInitializer(arg);
|
|
const literal = readStringLiteral(unwrapped);
|
|
if (literal !== null) {
|
|
return [literal];
|
|
}
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
const loopValues = currentForOfValueMap().get(unwrapped.text);
|
|
if (loopValues) {
|
|
return loopValues;
|
|
}
|
|
const constant = stringConstants.get(unwrapped.text);
|
|
if (constant !== undefined) {
|
|
return [constant];
|
|
}
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const visit = (node) => {
|
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
if (node.moduleSpecifier.text === "node:module") {
|
|
const bindings = node.importClause?.namedBindings;
|
|
if (bindings && ts.isNamedImports(bindings)) {
|
|
for (const element of bindings.elements) {
|
|
if (
|
|
element.propertyName?.text === "createRequire" ||
|
|
element.name.text === "createRequire"
|
|
) {
|
|
createRequireNames.add(element.name.text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
const initializer = unwrapInitializer(node.initializer);
|
|
const literal = readStringLiteral(initializer);
|
|
if (literal !== null) {
|
|
stringConstants.set(node.name.text, literal);
|
|
}
|
|
const arrayValues = readArrayStrings(initializer);
|
|
if (arrayValues) {
|
|
stringArrays.set(node.name.text, arrayValues);
|
|
}
|
|
if (isCreateRequireCall(initializer, createRequireNames)) {
|
|
requireNames.add(node.name.text);
|
|
}
|
|
}
|
|
|
|
if (ts.isForOfStatement(node)) {
|
|
const initializer = node.initializer;
|
|
const expression = unwrapInitializer(node.expression);
|
|
if (
|
|
ts.isVariableDeclarationList(initializer) &&
|
|
initializer.declarations.length === 1 &&
|
|
ts.isIdentifier(initializer.declarations[0].name) &&
|
|
ts.isIdentifier(expression)
|
|
) {
|
|
const values = stringArrays.get(expression.text);
|
|
if (values) {
|
|
forOfRuntimeValues.push(new Map([[initializer.declarations[0].name.text, values]]));
|
|
ts.forEachChild(node.statement, visit);
|
|
forOfRuntimeValues.pop();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
requireNames.has(node.expression.text)
|
|
) {
|
|
for (const specifier of readRequireArgumentSpecifiers(node)) {
|
|
addSpecifier(specifier, node);
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
|
|
visit(sourceFile);
|
|
return violations;
|
|
}
|
|
|
|
export async function collectRuntimeSidecarLoaderViolations(params) {
|
|
const files = await collectTypeScriptFilesFromRoots(params.sourceRoots, {
|
|
extraTestSuffixes: [".test-support.ts", ".test-helpers.ts"],
|
|
});
|
|
const violations = [];
|
|
for (const filePath of files) {
|
|
if (filePath.endsWith(".d.ts")) {
|
|
continue;
|
|
}
|
|
const relativePath = normalizeRelativePath(path.relative(params.repoRoot, filePath));
|
|
const content = await fs.readFile(filePath, "utf8");
|
|
for (const violation of findRuntimeSidecarLoaderViolations(
|
|
content,
|
|
relativePath,
|
|
params.explicitEntrySources,
|
|
)) {
|
|
violations.push({ path: relativePath, ...violation });
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
async function main() {
|
|
const { default: tsdownConfig } = await import("../tsdown.config.ts");
|
|
const violations = await collectRuntimeSidecarLoaderViolations({
|
|
repoRoot,
|
|
sourceRoots: defaultSourceRoots,
|
|
explicitEntrySources: collectTsdownEntrySources(tsdownConfig),
|
|
});
|
|
if (violations.length === 0) {
|
|
console.log("runtime-sidecar-loaders: local runtime sidecar loaders look OK.");
|
|
return;
|
|
}
|
|
console.error("runtime-sidecar-loaders: hidden local runtime loaders found:");
|
|
for (const violation of violations) {
|
|
console.error(
|
|
`- ${violation.path}:${violation.line}: ${violation.reason}. ` +
|
|
'Use cached import("./x.runtime.js") or add the sidecar as a stable tsdown entry.',
|
|
);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
runAsScript(import.meta.url, main);
|