206 lines
6.4 KiB
TypeScript
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`);
|
|
}
|