refactor: migrate a2ui bundle script to typescript

Replace the bash-based bundling script with a TypeScript implementation
to improve cross-platform compatibility and maintainability. The new
script includes logic for change detection via file hashing to skip
unnecessary rebuilds.
This commit is contained in:
Rory Roy Ulloa 2026-01-28 17:15:33 -06:00
parent 72a3046541
commit 624c422422
2 changed files with 120 additions and 1 deletions

View File

@ -143,7 +143,7 @@
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/MoltbotProtocol/GatewayModels.swift",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
"canvas:a2ui:bundle": "node --import tsx scripts/bundle-a2ui.ts",
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
},
"keywords": [],

119
scripts/bundle-a2ui.ts Normal file
View File

@ -0,0 +1,119 @@
import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, "..");
const HASH_FILE = path.join(ROOT_DIR, "src/canvas-host/a2ui/.bundle.hash");
const OUTPUT_FILE = path.join(ROOT_DIR, "src/canvas-host/a2ui/a2ui.bundle.js");
const A2UI_RENDERER_DIR = path.join(ROOT_DIR, "vendor/a2ui/renderers/lit");
const A2UI_APP_DIR = path.join(ROOT_DIR, "apps/shared/MoltbotKit/Tools/CanvasA2UI");
async function exists(p: string) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function main() {
if (!(await exists(A2UI_RENDERER_DIR)) || !(await exists(A2UI_APP_DIR))) {
console.log("A2UI sources missing; keeping prebuilt bundle.");
process.exit(0);
}
const INPUT_PATHS = [
path.join(ROOT_DIR, "package.json"),
path.join(ROOT_DIR, "pnpm-lock.yaml"),
A2UI_RENDERER_DIR,
A2UI_APP_DIR,
];
const currentHash = await computeHash(INPUT_PATHS);
let skip = false;
if (await exists(HASH_FILE)) {
const previousHash = (await fs.readFile(HASH_FILE, "utf8")).trim();
if (previousHash === currentHash && (await exists(OUTPUT_FILE))) {
console.log("A2UI bundle up to date; skipping.");
skip = true;
}
}
if (!skip) {
try {
// We use `npx` or local `node_modules/.bin` executables.
// Since we are running with pnpm, `tsc` and `rolldown` should be in path if run via pnpm exec or similar.
// The bash script used `pnpm -s exec tsc`.
console.log("Running tsc...");
execSync(`pnpm -s exec tsc -p "${path.join(A2UI_RENDERER_DIR, "tsconfig.json")}"`, {
stdio: "inherit",
cwd: ROOT_DIR
});
console.log("Running rolldown...");
// rolldown might need to be run via pnpm exec or npx if not in path
execSync(`pnpm exec rolldown -c "${path.join(A2UI_APP_DIR, "rolldown.config.mjs")}"`, {
stdio: "inherit",
cwd: ROOT_DIR
});
await fs.writeFile(HASH_FILE, currentHash);
} catch (e) {
console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle");
console.error("If this persists, verify pnpm deps and try again.");
process.exit(1);
}
}
}
async function computeHash(inputs: string[]) {
const files: string[] = [];
async function walk(entryPath: string) {
const st = await fs.stat(entryPath);
if (st.isDirectory()) {
const entries = await fs.readdir(entryPath);
for (const entry of entries) {
await walk(path.join(entryPath, entry));
}
} else {
files.push(entryPath);
}
}
for (const input of inputs) {
await walk(input);
}
// Normalize paths for consistent sorting
files.sort((a, b) => normalize(a).localeCompare(normalize(b)));
const hash = createHash("sha256");
for (const filePath of files) {
const rel = normalize(path.relative(ROOT_DIR, filePath));
hash.update(rel);
hash.update("\0");
const content = await fs.readFile(filePath);
hash.update(content);
hash.update("\0");
}
return hash.digest("hex");
}
function normalize(p: string) {
return p.split(path.sep).join("/");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});