Files
aurorabot/web/build.ts

246 lines
7.0 KiB
TypeScript

#!/usr/bin/env bun
import plugin from "bun-plugin-tailwind";
import { existsSync } from "fs";
import { rm } from "fs/promises";
import path from "path";
if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
🏗️ Bun Build Script
Usage: bun run build.ts [options]
Common Options:
--outdir <path> Output directory (default: "dist")
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
--sourcemap <type> Sourcemap type: none|linked|inline|external
--target <target> Build target: browser|bun|node
--format <format> Output format: esm|cjs|iife
--splitting Enable code splitting
--packages <type> Package handling: bundle|external
--public-path <path> Public path for assets
--env <mode> Environment handling: inline|disable|prefix*
--conditions <list> Package.json export conditions (comma separated)
--external <list> External packages (comma separated)
--banner <text> Add banner text to output
--footer <text> Add footer text to output
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
--help, -h Show this help message
Example:
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
`);
process.exit(0);
}
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1]?.toUpperCase() || "");
const parseValue = (value: string): any => {
if (value === "true") return true;
if (value === "false") return false;
if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
if (value.includes(",")) return value.split(",").map(v => v.trim());
return value;
};
function parseArgs(): Partial<Bun.BuildConfig> {
const config: Partial<Bun.BuildConfig> = {};
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === undefined) continue;
if (!arg.startsWith("--")) continue;
if (arg.startsWith("--no-")) {
const key = toCamelCase(arg.slice(5));
// @ts-ignore
config[key] = false;
continue;
}
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
const key = toCamelCase(arg.slice(2));
// @ts-ignore
config[key] = true;
continue;
}
let key: string;
let value: string;
if (arg.includes("=")) {
[key, value] = arg.slice(2).split("=", 2) as [string, string];
} else {
key = arg.slice(2);
value = args[++i] ?? "";
}
key = toCamelCase(key);
if (key.includes(".")) {
const [parentKey, childKey] = key.split(".");
// @ts-ignore
config[parentKey] = config[parentKey] || {};
// @ts-ignore
config[parentKey][childKey] = parseValue(value);
} else {
// @ts-ignore
config[key] = parseValue(value);
}
}
return config;
}
const formatFileSize = (bytes: number): string => {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
console.log("\n🚀 Starting build process...\n");
const cliConfig = parseArgs();
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
if (existsSync(outdir)) {
console.log(`🗑️ Cleaning previous build at ${outdir}`);
await rm(outdir, { recursive: true, force: true });
}
const start = performance.now();
const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
.map(a => path.resolve("src", a))
.filter(dir => !dir.includes("node_modules"));
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
const build = async () => {
const result = await Bun.build({
entrypoints,
outdir,
plugins: [plugin],
minify: true,
target: "browser",
sourcemap: "linked",
publicPath: "/", // Use absolute paths for SPA routing compatibility
define: {
"process.env.NODE_ENV": JSON.stringify((cliConfig as any).watch ? "development" : "production"),
},
...cliConfig,
});
const outputTable = result.outputs.map(output => ({
File: path.relative(process.cwd(), output.path),
Type: output.kind,
Size: formatFileSize(output.size),
}));
console.table(outputTable);
return result;
};
const result = await build();
const end = performance.now();
const buildTime = (end - start).toFixed(2);
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
if ((cliConfig as any).watch) {
console.log("👀 Watching for changes...\n");
// Polling-based file watcher for Docker compatibility
// Docker volumes don't propagate filesystem events (inotify) reliably
const srcDir = path.join(process.cwd(), "src");
const POLL_INTERVAL_MS = 1000;
let lastMtimes = new Map<string, number>();
let isRebuilding = false;
// Collect all file mtimes in src directory
const collectMtimes = async (): Promise<Map<string, number>> => {
const mtimes = new Map<string, number>();
const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,css,html}");
for await (const file of glob.scan({ cwd: srcDir, absolute: true })) {
try {
const stat = await Bun.file(file).stat();
if (stat) {
mtimes.set(file, stat.mtime.getTime());
}
} catch {
// File may have been deleted, skip
}
}
return mtimes;
};
// Initial collection
lastMtimes = await collectMtimes();
// Polling loop
const poll = async () => {
if (isRebuilding) return;
const currentMtimes = await collectMtimes();
const changedFiles: string[] = [];
// Check for new or modified files
for (const [file, mtime] of currentMtimes) {
const lastMtime = lastMtimes.get(file);
if (lastMtime === undefined || lastMtime < mtime) {
changedFiles.push(path.relative(srcDir, file));
}
}
// Check for deleted files
for (const file of lastMtimes.keys()) {
if (!currentMtimes.has(file)) {
changedFiles.push(path.relative(srcDir, file) + " (deleted)");
}
}
if (changedFiles.length > 0) {
isRebuilding = true;
console.log(`\n🔄 Changes detected:`);
changedFiles.forEach(f => console.log(`${f}`));
console.log("");
try {
const rebuildStart = performance.now();
await build();
const rebuildEnd = performance.now();
console.log(`\n✅ Rebuild completed in ${(rebuildEnd - rebuildStart).toFixed(2)}ms\n`);
} catch (err) {
console.error("❌ Rebuild failed:", err);
}
lastMtimes = currentMtimes;
isRebuilding = false;
}
};
const interval = setInterval(poll, POLL_INTERVAL_MS);
// Handle manual exit
process.on("SIGINT", () => {
clearInterval(interval);
console.log("\n👋 Stopping build watcher...");
process.exit(0);
});
// Keep process alive
process.stdin.resume();
}