Files
openclaw/scripts/check-script-erasability.mjs
T
2026-08-16 23:31:27 -07:00

120 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
// Verifies that script TypeScript uses Node's transformation-free syntax subset.
import fs from "node:fs";
import { stripTypeScriptTypes } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
const SCRIPT_ROOT = path.dirname(fileURLToPath(import.meta.url));
const TYPESCRIPT_IMPLEMENTATION_RE = /\.(?:cts|mts|ts)$/u;
const TYPESCRIPT_DECLARATION_RE = /\.d\.(?:cts|mts|ts)$/u;
const SKIPPED_DIRECTORY_NAMES = new Set([
".cache",
".git",
"build",
"coverage",
"dist",
"generated",
"node_modules",
"out",
"vendor",
]);
function compareNames(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function listTypeScriptImplementationFiles(rootDir) {
const files = [];
function visit(directory) {
const entries = fs
.readdirSync(directory, { withFileTypes: true })
.toSorted((left, right) => compareNames(left.name, right.name));
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) {
visit(entryPath);
}
continue;
}
if (
entry.isFile() &&
TYPESCRIPT_IMPLEMENTATION_RE.test(entry.name) &&
!TYPESCRIPT_DECLARATION_RE.test(entry.name)
) {
files.push(entryPath);
}
}
}
visit(rootDir);
return files;
}
function diagnosticLine(error, sourceUrl) {
if (!(error instanceof Error) || !error.stack) {
return undefined;
}
const firstLine = error.stack.split("\n", 1)[0];
const prefix = `${sourceUrl}:`;
if (!firstLine.startsWith(prefix)) {
return undefined;
}
const line = Number.parseInt(firstLine.slice(prefix.length), 10);
return Number.isFinite(line) ? line : undefined;
}
/**
* Checks each TypeScript implementation file under a scripts tree without resolving imports.
*/
export function checkScriptErasability(rootDir = SCRIPT_ROOT) {
const files = listTypeScriptImplementationFiles(rootDir);
const rootLabel = path.basename(rootDir);
const errors = [];
for (const filePath of files) {
const relativePath = path.relative(rootDir, filePath).split(path.sep).join("/");
const sourceUrl = `${rootLabel}/${relativePath}`;
try {
stripTypeScriptTypes(fs.readFileSync(filePath, "utf8"), {
mode: "strip",
sourceUrl,
});
} catch (error) {
errors.push({
file: sourceUrl,
line: diagnosticLine(error, sourceUrl),
message: error instanceof Error ? error.message : String(error),
});
}
}
return { checkedFiles: files.length, errors };
}
export function main() {
const result = checkScriptErasability();
if (result.errors.length === 0) {
console.log(
`[script-erasability] checked ${result.checkedFiles} TypeScript implementation files`,
);
return;
}
console.error(
"TypeScript syntax under scripts/ must be erasable by Node without transformation:",
);
for (const error of result.errors) {
const location = error.line === undefined ? error.file : `${error.file}:${error.line}`;
console.error(`- ${location}: ${error.message}`);
}
process.exitCode = 1;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main();
}