Files
openclaw/scripts/prepare-extension-package-boundary-artifacts.mjs
2026-04-07 13:11:30 +01:00

278 lines
8.0 KiB
JavaScript

import { spawn } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import path, { resolve } from "node:path";
const require = createRequire(import.meta.url);
const repoRoot = resolve(import.meta.dirname, "..");
const tscBin = require.resolve("typescript/bin/tsc");
const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]);
const VALID_MODES = new Set(["all", "package-boundary"]);
const ROOT_DTS_INPUTS = [
"tsconfig.json",
"tsconfig.plugin-sdk.dts.json",
"src",
"packages/memory-host-sdk/src",
];
const PACKAGE_DTS_INPUTS = [
"tsconfig.json",
"packages/plugin-sdk/tsconfig.json",
"src/plugin-sdk",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",
"src/types",
];
const ENTRY_SHIMS_INPUTS = [
"scripts/write-plugin-sdk-entry-dts.ts",
"scripts/lib/plugin-sdk-entrypoints.json",
"scripts/lib/plugin-sdk-entries.mjs",
];
function isRelevantTypeInput(filePath) {
const basename = path.basename(filePath);
if (basename.endsWith(".test.ts")) {
return false;
}
return TYPE_INPUT_EXTENSIONS.has(path.extname(filePath));
}
export function parseMode(argv = process.argv.slice(2)) {
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
const mode = modeArg?.slice("--mode=".length) ?? "all";
if (!VALID_MODES.has(mode)) {
throw new Error(`Unknown mode: ${mode}`);
}
return mode;
}
function collectNewestMtime(paths, params = {}) {
const rootDir = params.rootDir ?? repoRoot;
const includeFile = params.includeFile ?? (() => true);
let newestMtimeMs = 0;
function visit(entryPath) {
if (!fs.existsSync(entryPath)) {
return;
}
const stats = fs.statSync(entryPath);
if (stats.isDirectory()) {
for (const child of fs.readdirSync(entryPath)) {
visit(path.join(entryPath, child));
}
return;
}
if (!includeFile(entryPath)) {
return;
}
newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs);
}
for (const relativePath of paths) {
visit(resolve(rootDir, relativePath));
}
return newestMtimeMs;
}
function collectOldestMtime(paths, params = {}) {
const rootDir = params.rootDir ?? repoRoot;
let oldestMtimeMs = Number.POSITIVE_INFINITY;
for (const relativePath of paths) {
const absolutePath = resolve(rootDir, relativePath);
if (!fs.existsSync(absolutePath)) {
return null;
}
oldestMtimeMs = Math.min(oldestMtimeMs, fs.statSync(absolutePath).mtimeMs);
}
return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : null;
}
export function isArtifactSetFresh(params) {
const newestInputMtimeMs = collectNewestMtime(params.inputPaths, {
rootDir: params.rootDir,
includeFile: params.includeFile,
});
const oldestOutputMtimeMs = collectOldestMtime(params.outputPaths, { rootDir: params.rootDir });
return oldestOutputMtimeMs !== null && oldestOutputMtimeMs >= newestInputMtimeMs;
}
export function createPrefixedOutputWriter(label, target) {
let buffered = "";
const prefix = `[${label}] `;
return {
write(chunk) {
buffered += chunk;
while (true) {
const newlineIndex = buffered.indexOf("\n");
if (newlineIndex === -1) {
return;
}
const line = buffered.slice(0, newlineIndex + 1);
buffered = buffered.slice(newlineIndex + 1);
target.write(`${prefix}${line}`);
}
},
flush() {
if (!buffered) {
return;
}
target.write(`${prefix}${buffered}`);
buffered = "";
},
};
}
function abortSiblingSteps(abortController) {
if (abortController && !abortController.signal.aborted) {
abortController.abort();
}
}
export function runNodeStep(label, args, timeoutMs, params = {}) {
const abortController = params.abortController;
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(process.execPath, args, {
cwd: repoRoot,
env: process.env,
signal: abortController?.signal,
stdio: ["ignore", "pipe", "pipe"],
});
let settled = false;
const stdoutWriter = createPrefixedOutputWriter(label, process.stdout);
const stderrWriter = createPrefixedOutputWriter(label, process.stderr);
const timer = setTimeout(() => {
if (settled) {
return;
}
child.kill("SIGTERM");
settled = true;
stdoutWriter.flush();
stderrWriter.flush();
abortSiblingSteps(abortController);
rejectPromise(new Error(`${label} timed out after ${timeoutMs}ms`));
}, timeoutMs);
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdoutWriter.write(chunk);
});
child.stderr.on("data", (chunk) => {
stderrWriter.write(chunk);
});
child.on("error", (error) => {
if (settled) {
return;
}
clearTimeout(timer);
settled = true;
stdoutWriter.flush();
stderrWriter.flush();
if (error.name === "AbortError" && abortController?.signal.aborted) {
rejectPromise(new Error(`${label} canceled after sibling failure`));
return;
}
abortSiblingSteps(abortController);
rejectPromise(new Error(`${label} failed to start: ${error.message}`));
});
child.on("close", (code) => {
if (settled) {
return;
}
clearTimeout(timer);
settled = true;
stdoutWriter.flush();
stderrWriter.flush();
if (code === 0) {
resolvePromise();
return;
}
abortSiblingSteps(abortController);
rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`));
});
});
}
export async function runNodeStepsInParallel(steps) {
const abortController = new AbortController();
const results = await Promise.allSettled(
steps.map((step) => runNodeStep(step.label, step.args, step.timeoutMs, { abortController })),
);
const firstFailure = results.find((result) => result.status === "rejected");
if (firstFailure) {
throw firstFailure.reason;
}
}
export async function main(argv = process.argv.slice(2)) {
try {
const mode = parseMode(argv);
const rootDtsFresh = isArtifactSetFresh({
inputPaths: ROOT_DTS_INPUTS,
outputPaths: ["dist/plugin-sdk/.tsbuildinfo"],
includeFile: isRelevantTypeInput,
});
const packageDtsFresh = isArtifactSetFresh({
inputPaths: PACKAGE_DTS_INPUTS,
outputPaths: ["packages/plugin-sdk/dist/.tsbuildinfo"],
includeFile: isRelevantTypeInput,
});
const entryShimsFresh = isArtifactSetFresh({
inputPaths: [
...ENTRY_SHIMS_INPUTS,
"dist/plugin-sdk/.tsbuildinfo",
"packages/plugin-sdk/dist/.tsbuildinfo",
],
outputPaths: ["dist/plugin-sdk/.boundary-entry-shims.stamp"],
});
const pendingSteps = [];
if (mode === "all") {
if (!rootDtsFresh) {
pendingSteps.push({
label: "plugin-sdk boundary dts",
args: [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"],
timeoutMs: 300_000,
});
} else {
process.stdout.write("[plugin-sdk boundary dts] fresh; skipping\n");
}
}
if (!packageDtsFresh) {
pendingSteps.push({
label: "plugin-sdk package boundary dts",
args: [tscBin, "-p", "packages/plugin-sdk/tsconfig.json"],
timeoutMs: 300_000,
});
} else {
process.stdout.write("[plugin-sdk package boundary dts] fresh; skipping\n");
}
if (pendingSteps.length > 0) {
await runNodeStepsInParallel(pendingSteps);
}
if (mode === "all" && (!entryShimsFresh || pendingSteps.length > 0)) {
await runNodeStep(
"plugin-sdk boundary root shims",
["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")],
120_000,
);
} else if (mode === "all") {
process.stdout.write("[plugin-sdk boundary root shims] fresh; skipping\n");
}
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
}
}
if (import.meta.main) {
await main();
}