Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/dist-contract.ts
T
2026-05-31 11:45:10 +08:00

206 lines
6.4 KiB
TypeScript

import fs from "node:fs";
import { readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const cloudWebDistPath = "web/hwlab-cloud-web/dist";
export const cloudWebDistBuildCommand = "cd web/hwlab-cloud-web && bun run build";
export const cloudWebDistFreshnessCommand = cloudWebDistBuildCommand;
export const cloudWebAppSourceFiles = Object.freeze([
"app.ts",
"app-skills.ts",
"app-device-pod.ts",
"app-conversation.ts",
"app-trace.ts",
"app-helpers.ts"
]);
export const cloudWebDistRuntimeFiles = Object.freeze([
"index.html",
"styles.css",
"app.js",
"favicon.svg",
"favicon.ico",
"help.md",
"third_party/marked/marked.esm.js",
"third_party/marked/LICENSE"
]);
export const cloudWebDistAliasFiles = Object.freeze(["gate/index.html", "diagnostics/gate/index.html", "help/index.html", "skills/index.html"]);
const staticRuntimeFiles = cloudWebDistRuntimeFiles.filter((file) => file !== "app.js");
function absolutePath(rootDir, relativePath) {
return path.resolve(rootDir, relativePath);
}
function exists(filePath) {
return fs.existsSync(filePath);
}
export function readCloudWebAppSource(rootDir) {
return cloudWebAppSourceFiles.map((file) => fs.readFileSync(absolutePath(rootDir, file), "utf8")).join("\n");
}
async function compareFile({ files, mismatches, kind, relativePath, sourcePath, distPath }) {
const entry = {
kind,
path: relativePath,
sourcePath,
distPath,
status: "match"
};
if (!exists(sourcePath)) {
entry.status = "missing_source";
entry.reason = "source file is missing";
} else if (!exists(distPath)) {
entry.status = "missing_dist";
entry.reason = "dist file is missing";
} else {
const [sourceText, distText] = await Promise.all([
readFile(sourcePath, "utf8"),
readFile(distPath, "utf8")
]);
if (sourceText !== distText) {
entry.status = "mismatch";
entry.reason = "dist file differs from source";
}
}
if (entry.status !== "match") {
mismatches.push(relativePath);
}
files.push(entry);
}
export async function buildCloudWebDist(rootDir) {
const distRoot = absolutePath(rootDir, "dist");
fs.rmSync(distRoot, { recursive: true, force: true });
fs.mkdirSync(distRoot, { recursive: true });
for (const relativePath of staticRuntimeFiles) {
const distPath = absolutePath(distRoot, relativePath);
fs.mkdirSync(path.dirname(distPath), { recursive: true });
fs.copyFileSync(absolutePath(rootDir, relativePath), distPath);
}
await buildAppBundle(rootDir, absolutePath(distRoot, "app.js"));
for (const aliasPath of cloudWebDistAliasFiles) {
const distPath = absolutePath(distRoot, aliasPath);
fs.mkdirSync(path.dirname(distPath), { recursive: true });
fs.copyFileSync(absolutePath(rootDir, "index.html"), distPath);
}
return distRoot;
}
export async function inspectCloudWebDistFreshness(rootDir) {
const distRoot = absolutePath(rootDir, "dist");
const files = [];
const mismatches = [];
for (const relativePath of staticRuntimeFiles) {
await compareFile({
files,
mismatches,
kind: "runtime",
relativePath,
sourcePath: absolutePath(rootDir, relativePath),
distPath: absolutePath(distRoot, relativePath)
});
}
await compareGeneratedApp({ files, mismatches, rootDir, distRoot });
for (const aliasPath of cloudWebDistAliasFiles) {
await compareFile({
files,
mismatches,
kind: "alias",
relativePath: aliasPath,
sourcePath: absolutePath(rootDir, "index.html"),
distPath: absolutePath(distRoot, aliasPath)
});
}
return {
status: mismatches.length === 0 ? "pass" : "blocked",
distPath: cloudWebDistPath,
buildCommand: cloudWebDistBuildCommand,
freshnessCommand: cloudWebDistFreshnessCommand,
files,
mismatches
};
}
export function cloudWebDistFreshnessMessage(distFreshness) {
const mismatches = distFreshness.mismatches.length > 0 ? distFreshness.mismatches.join(", ") : "unknown";
return [
`Cloud Web dist is stale or missing before image build: ${mismatches}.`,
`Run ${cloudWebDistBuildCommand} before publishing ${cloudWebDistPath}.`
].join(" ");
}
export async function assertCloudWebDistFresh(rootDir) {
const distFreshness = await inspectCloudWebDistFreshness(rootDir);
if (distFreshness.status !== "pass") {
throw new Error(cloudWebDistFreshnessMessage(distFreshness));
}
return distFreshness;
}
async function compareGeneratedApp({ files, mismatches, rootDir, distRoot }) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hwlab-cloud-web-app-"));
const generatedPath = path.join(tmpDir, "app.js");
try {
await buildAppBundle(rootDir, generatedPath);
await compareFile({
files,
mismatches,
kind: "runtime",
relativePath: "app.js",
sourcePath: generatedPath,
distPath: absolutePath(distRoot, "app.js")
});
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
async function buildAppBundle(rootDir, outFile) {
const entryPath = absolutePath(rootDir, ".app-entry.generated.ts");
const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "hwlab-cloud-web-bun-"));
fs.writeFileSync(entryPath, `${readCloudWebAppSource(rootDir)}\n`);
try {
const result = await Bun.build({
entrypoints: [entryPath],
outdir: buildDir,
target: "browser",
format: "esm",
sourcemap: "none",
minify: false,
naming: "app.js"
});
if (!result.success) {
throw new Error(`Bun build failed:\n${result.logs.map((log) => String(log)).join("\n")}`);
}
fs.mkdirSync(path.dirname(outFile), { recursive: true });
fs.copyFileSync(path.join(buildDir, "app.js"), outFile);
} finally {
fs.rmSync(entryPath, { force: true });
fs.rmSync(buildDir, { recursive: true, force: true });
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const args = process.argv.slice(2);
const rootIndex = args.indexOf("--root");
const rootDir = rootIndex >= 0 ? path.resolve(args[rootIndex + 1]) : path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
if (args.includes("--build")) {
await buildCloudWebDist(rootDir);
}
const freshness = await inspectCloudWebDistFreshness(rootDir);
process.stdout.write(`${JSON.stringify(freshness, null, args.includes("--pretty") ? 2 : 0)}\n`);
}