import { execFileSync } from "child_process"; import * as chokidar from "chokidar"; import esbuild from "esbuild"; import fs from "fs"; import { globSync } from "glob"; import path from "path"; import { cwd } from "process"; import process from "process"; import { fileURLToPath } from "url"; // Hack replaces what was lost when using Bun / later Node versions const __dirname = fileURLToPath(new URL(".", import.meta.url)); const isProdBuild = process.env.NODE_ENV === "production"; const definitions = { "process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"), "process.env.CWD": JSON.stringify(cwd()), }; // If you have assets in your src folder that won't be built/bundled, put them into "otherFiles" to // copy them. All this is a replacement for rollup-copy-plugin, which I used to use. const otherFiles = [ ["./src/*.css", "."], ["./src/*.png", "."] ]; const isFile = (filePath) => fs.statSync(filePath).isFile(); function nameCopyTarget(src, dest, strip) { const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base); return [src, target]; } function copyOthers() { for (const [source, rawdest, strip] of otherFiles) { const matchedPaths = globSync(source); const dest = path.join("dist", rawdest); const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip)); for (const [src, dest] of copyTargets) { if (isFile(src)) { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); } } } } // This starts the definitions used for esbuild: Targets, arguments, the function for running a // build, and the options for building: building or watching. If you're building more than one app, // order them by the approximately largest project to smallest to build even faster. const apps = [ ["index.ts", "."] ]; const baseArgs = { bundle: true, write: true, sourcemap: true, minify: isProdBuild, splitting: true, treeShaking: true, external: ["*.woff", "*.woff2"], tsconfig: "./tsconfig.json", loader: { ".css": "text", ".md": "text" }, define: definitions, format: "esm", }; async function buildOneSource(source, dest) { const DIST = path.join(__dirname, "./dist", dest); console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`); try { const start = Date.now(); copyOthers(); await esbuild.build({ ...baseArgs, entryPoints: [`./src/${source}`], entryNames: '[dir]/[name]', outdir: DIST, }); const end = Date.now(); console.log( `[${new Date(end).toISOString()}] Finished build for target ${source} in ${ Date.now() - start }ms`, ); } catch (exc) { console.error(`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`); } } async function buildAll(apps) { await Promise.allSettled(apps.map(([source, dest]) => buildOneSource(source, dest))); } let timeoutId = null; function debouncedBuild() { if (timeoutId !== null) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { console.log("\x1bc"); buildAll(apps); }, 250); } if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) { console.log(`Build: options: -w, --watch: Build all ${apps.length} applications -h, --help: This help message `); process.exit(0); } if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === "--watch")) { console.log("Watching ./src for changes"); chokidar.watch("./src").on("all", (event, path) => { if (!["add", "change", "unlink"].includes(event)) { return; } if (!/(\.css|\.ts|\.js)$/.test(path)) { return; } debouncedBuild(); }); } else { await buildAll(apps); }