Files
openclaw/scripts/check-cli-bootstrap-imports.mts
T
Peter Steinberger c70aee247e refactor(scripts): migrate JavaScript tools to TypeScript (#121005)
* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
2026-08-09 07:21:35 -07:00

281 lines
7.9 KiB
TypeScript

#!/usr/bin/env node
// Checks CLI bootstrap chunks for forbidden eager imports and size regressions.
import fs from "node:fs";
import module from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
const DEFAULT_ENTRYPOINTS = ["dist/entry.js", "dist/cli/run-main.js"];
const DEFAULT_GATEWAY_RUN_CHUNK_MAX_BYTES = 70 * 1024;
const GATEWAY_RUN_CHUNK_MARKER_SETS = [
["const GATEWAY_AUTH_MODES", "function addGatewayRunCommand"],
["const GATEWAY_RUN_VALUE_KEYS", "function addGatewayRunCommand"],
];
const GATEWAY_RUN_FORBIDDEN_STATIC_IMPORTS = [
"control-ui-assets",
"diagnostic-stability-bundle",
"onboard-helpers",
"process-respawn",
"restart-sentinel",
"server-close",
"server-reload-handlers",
];
const STATIC_IMPORT_RE =
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<specifier>[^"']+)["']/gu;
type CliBootstrapCheckParams = {
rootDir?: string;
entrypoints?: string[];
distDir?: string;
gatewayRunChunkMaxBytes?: number;
fs?: typeof fs;
logger?: { error(message: string): void };
};
function isMainModule() {
return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
}
function isBuiltinSpecifier(specifier: string) {
return specifier.startsWith("node:") || module.isBuiltin(specifier);
}
function isRelativeSpecifier(specifier: string) {
return specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
}
function resolveRelativeImport(importer: string, specifier: string, fsImpl: typeof fs = fs) {
const base = specifier.startsWith("/")
? specifier
: path.resolve(path.dirname(importer), specifier);
const candidates = [
base,
`${base}.js`,
`${base}.mjs`,
`${base}.cjs`,
path.join(base, "index.js"),
path.join(base, "index.mjs"),
path.join(base, "index.cjs"),
];
return candidates.find((candidate) => {
try {
return fsImpl.statSync(candidate).isFile();
} catch {
return false;
}
});
}
/**
* Lists static import/export specifiers from a JavaScript source string.
*/
export function listStaticImportSpecifiers(source: string) {
return [...source.matchAll(STATIC_IMPORT_RE)].map((match) => match.groups?.specifier ?? "");
}
function walkStaticImportGraph(
fsImpl: typeof fs,
rootDir: string,
roots: string[],
onExternalSpecifier?: (filePath: string, specifier: string) => string,
onRelativeSpecifier?: (
filePath: string,
resolved: string,
specifier: string,
) => string | undefined,
) {
const queue = roots.map((entrypoint) => path.resolve(rootDir, entrypoint));
const visited = new Set<string>();
const errors = [];
for (const filePath of queue) {
if (!filePath || visited.has(filePath)) {
continue;
}
visited.add(filePath);
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
errors.push(
`CLI bootstrap import guard could not read ${path.relative(rootDir, filePath) || filePath}. Run pnpm build first.`,
);
continue;
}
for (const specifier of listStaticImportSpecifiers(source)) {
if (!specifier || isBuiltinSpecifier(specifier)) {
continue;
}
if (!isRelativeSpecifier(specifier)) {
const error = onExternalSpecifier?.(filePath, specifier);
if (error) {
errors.push(error);
}
continue;
}
const resolved = resolveRelativeImport(filePath, specifier, fsImpl);
if (!resolved) {
errors.push(
`CLI bootstrap import guard could not resolve "${specifier}" from ${path.relative(
rootDir,
filePath,
)}.`,
);
continue;
}
const error = onRelativeSpecifier?.(filePath, resolved, specifier);
if (error) {
errors.push(error);
}
if (!visited.has(resolved)) {
queue.push(resolved);
}
}
}
return errors;
}
/**
* Collects forbidden external import errors for CLI bootstrap entrypoints.
*/
export function collectCliBootstrapExternalImportErrors(params: CliBootstrapCheckParams = {}) {
const rootDir = params.rootDir ?? process.cwd();
const entrypoints = params.entrypoints ?? DEFAULT_ENTRYPOINTS;
const fsImpl = params.fs ?? fs;
const errors = walkStaticImportGraph(
fsImpl,
rootDir,
entrypoints,
(filePath, specifier) =>
`CLI bootstrap static graph imports external package "${specifier}" from ${path.relative(
rootDir,
filePath,
)}.`,
);
return errors.toSorted((left, right) => left.localeCompare(right));
}
function listJsFiles(dirPath: string, fsImpl: typeof fs = fs): string[] {
let entries;
try {
entries = fsImpl.readdirSync(dirPath, { withFileTypes: true });
} catch {
return [];
}
const files = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
files.push(...listJsFiles(fullPath, fsImpl));
continue;
}
if (entry.isFile() && entry.name.endsWith(".js")) {
files.push(fullPath);
}
}
return files;
}
/**
* Collects gateway-run chunk budget errors from built CLI output.
*/
export function collectGatewayRunChunkBudgetErrors(params: CliBootstrapCheckParams = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const distDir = path.resolve(rootDir, params.distDir ?? "dist");
const maxBytes = params.gatewayRunChunkMaxBytes ?? DEFAULT_GATEWAY_RUN_CHUNK_MAX_BYTES;
const chunks = [];
for (const filePath of listJsFiles(distDir, fsImpl)) {
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
continue;
}
if (
GATEWAY_RUN_CHUNK_MARKER_SETS.some((markers) =>
markers.every((marker) => source.includes(marker)),
)
) {
chunks.push({ filePath, source });
}
}
if (chunks.length === 0) {
return [
"CLI bootstrap import guard could not find the bundled gateway run chunk. Run pnpm build first.",
];
}
const errors = [];
for (const { filePath, source } of chunks) {
const relativePath = path.relative(rootDir, filePath) || filePath;
let size = Buffer.byteLength(source, "utf8");
try {
size = fsImpl.statSync(filePath).size;
} catch {
// Fall back to source byte length for in-memory test fixtures.
}
if (size > maxBytes) {
errors.push(
`Gateway run chunk ${relativePath} is ${size} bytes, above budget ${maxBytes} bytes.`,
);
}
errors.push(
...walkStaticImportGraph(
fsImpl,
rootDir,
[filePath],
undefined,
(importerPath, resolved, specifier) => {
const resolvedRelativePath = path.relative(rootDir, resolved) || resolved;
const coldPath = [specifier, resolvedRelativePath].find((candidate) =>
GATEWAY_RUN_FORBIDDEN_STATIC_IMPORTS.some((forbidden) => candidate.includes(forbidden)),
);
return coldPath
? `Gateway run chunk ${relativePath} static graph imports cold path "${coldPath}" from ${
path.relative(rootDir, importerPath) || importerPath
}.`
: undefined;
},
),
);
}
return errors.toSorted((left, right) => left.localeCompare(right));
}
/**
* Runs the CLI bootstrap import and chunk-budget checks.
*/
export function checkCliBootstrapExternalImports(params: CliBootstrapCheckParams = {}) {
const errors = [
...collectCliBootstrapExternalImportErrors(params),
...collectGatewayRunChunkBudgetErrors(params),
];
if (errors.length === 0) {
return;
}
const logger = params.logger ?? console;
logger.error("CLI bootstrap import guard failed:");
for (const error of errors) {
logger.error(` - ${error}`);
}
throw new Error("CLI bootstrap static graph imports external packages.");
}
if (isMainModule()) {
try {
checkCliBootstrapExternalImports();
console.log("CLI bootstrap import guard passed.");
} catch {
process.exit(1);
}
}