mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix(tasks): ship task registry control runtime
This commit is contained in:
@@ -87,6 +87,7 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
}
|
||||
|
||||
if (runAll) {
|
||||
add("runtime sidecar loader guard", ["check:runtime-sidecar-loaders"]);
|
||||
addTypecheck("typecheck all", ["tsgo:all"]);
|
||||
addLint("lint", ["lint"]);
|
||||
add("runtime import cycles", ["check:import-cycles"]);
|
||||
@@ -130,6 +131,7 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
}
|
||||
|
||||
if (lanes.core || lanes.extensions) {
|
||||
add("runtime sidecar loader guard", ["check:runtime-sidecar-loaders"]);
|
||||
add("runtime import cycles", ["check:import-cycles"]);
|
||||
}
|
||||
if (lanes.core) {
|
||||
|
||||
266
scripts/check-runtime-sidecar-loaders.mjs
Normal file
266
scripts/check-runtime-sidecar-loaders.mjs
Normal file
@@ -0,0 +1,266 @@
|
||||
#!/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);
|
||||
@@ -39,6 +39,7 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
name: "plugin-sdk wildcard re-exports",
|
||||
args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"],
|
||||
},
|
||||
{ name: "runtime sidecar loader guard", args: ["check:runtime-sidecar-loaders"] },
|
||||
{ name: "tool display", args: ["tool-display:check"] },
|
||||
{ name: "host env policy", args: ["check:host-env-policy:swift"] },
|
||||
],
|
||||
|
||||
@@ -71,6 +71,7 @@ const requiredPathGroups = [
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/compat.js",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/task-registry-control.runtime.js",
|
||||
"dist/build-info.json",
|
||||
"dist/channel-catalog.json",
|
||||
"dist/control-ui/index.html",
|
||||
@@ -474,6 +475,27 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
|
||||
}
|
||||
}
|
||||
|
||||
function runPackedTaskRegistryControlRuntimeSmoke(packageRoot: string): void {
|
||||
const runtimePath = join(packageRoot, "dist", "task-registry-control.runtime.js");
|
||||
if (!existsSync(runtimePath)) {
|
||||
throw new Error("release-check: packed task-registry control runtime is missing.");
|
||||
}
|
||||
const source = `
|
||||
const runtime = await import(${JSON.stringify(pathToFileURL(runtimePath).href)});
|
||||
if (typeof runtime.getAcpSessionManager !== "function") {
|
||||
throw new Error("missing getAcpSessionManager export");
|
||||
}
|
||||
if (typeof runtime.killSubagentRunAdmin !== "function") {
|
||||
throw new Error("missing killSubagentRunAdmin export");
|
||||
}
|
||||
`;
|
||||
execFileSync(process.execPath, ["--input-type=module", "--eval", source], {
|
||||
cwd: packageRoot,
|
||||
stdio: "inherit",
|
||||
env: createPackedCliSmokeEnv(process.env),
|
||||
});
|
||||
}
|
||||
|
||||
function runPackedCliSmoke(params: {
|
||||
prefixDir: string;
|
||||
cwd: string;
|
||||
@@ -536,6 +558,7 @@ function runPackedBundledChannelEntrySmoke(): void {
|
||||
});
|
||||
runPackedBundledPluginPostinstall(packageRoot);
|
||||
runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot);
|
||||
runPackedTaskRegistryControlRuntimeSmoke(packageRoot);
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user