mara flagged (PR #4297) that the comment-block lint doesn't catch this file at all -- check-comment-blocks.sh (and check-issue-refs.sh) scope to a fixed git ls-files extension list that omits .mjs. That's a real gap (dashboard/build.mjs already has a 49-line block, pre-existing), tracked separately -- but the fix here is to just respect the 30-line convention regardless of whether the lint enforces it on this extension. Cut the redundant per-file output-layout table (the code below already names every output path) and kept only the genuinely non-obvious decisions -- no content lost, verified against a plain awk run of the lint's own line-counting logic.
167 lines
6.7 KiB
JavaScript
167 lines
6.7 KiB
JavaScript
// esbuild build for @hive/swarm-ui (project-bootstrap scope). Bundles
|
|
// main.tsx + its companion CSS, theme.css, and swarm-ui.css into
|
|
// `dist/static/`, content-hashed (`[name]-[hash]`) so a redeploy's new
|
|
// bundle gets a new URL, then rewrites those real URLs into index.html
|
|
// and sw.js. See each output file's own name in the code below for what
|
|
// it's for — not repeated here to avoid this comment drifting out of
|
|
// sync with the actual build steps.
|
|
//
|
|
// `colors.css` is the ONE static asset deliberately left UNHASHED, at a
|
|
// fixed `dist/static/colors.css`: nix/host-modules/swarm-ui.nix's stylix
|
|
// theming swaps that exact path via an `= /static/colors.css` nginx
|
|
// location override, no npm/esbuild rebuild involved. Hashing it would
|
|
// silently break that swap. See `colors.css`'s own top comment.
|
|
//
|
|
// `dist/sw.js` keeps its own fixed/unhashed filename too — the browser
|
|
// polls this exact URL to detect a SW update, hashing it would make
|
|
// every deploy register as a brand new worker. Its `CACHE_VERSION`
|
|
// placeholder and shell-asset URLs get rewritten below, same as
|
|
// index.html's.
|
|
//
|
|
// PWA icons are NOT produced here — like favicon.svg, they're
|
|
// rasterized from branding/*.svg and copied in by
|
|
// nix/packages/swarm-ui.nix's installPhase, outside the npm tree this
|
|
// script builds.
|
|
//
|
|
// Not yet wired into any Rust binary's `ServeDir` — swarm-controller
|
|
// only serves `/health` today; this just makes `dist/` buildable so
|
|
// nix/packages/swarm-ui.nix has something to package. `.tsx` → JS:
|
|
// esbuild transpiles TypeScript natively (types stripped, not checked)
|
|
// — run `npm run typecheck` separately for real type errors.
|
|
|
|
import { build } from "esbuild";
|
|
import { mkdirSync, rmSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { dirname, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const src = (p) => resolve(here, "src", p);
|
|
const dist = (p) => resolve(here, "dist", p);
|
|
const staticDir = (p) => resolve(here, "dist", "static", p);
|
|
|
|
// Every metafile path esbuild returns is relative to `absWorkingDir`
|
|
// (pinned to `here` on every build call below, regardless of the caller's
|
|
// own cwd) and always POSIX-style (`dist/static/main-<hash>.js`) — turn
|
|
// one into the URL nginx actually serves it at. A plain prefix strip, not
|
|
// `path.relative`: the latter resolves a relative argument against
|
|
// `process.cwd()`, which is exactly the caller-cwd dependency pinning
|
|
// `absWorkingDir` on every build call below was meant to avoid.
|
|
const toUrl = (metafilePath) => "/" + metafilePath.replace(/^dist\//, "");
|
|
|
|
rmSync(dist(""), { recursive: true, force: true });
|
|
mkdirSync(staticDir(""), { recursive: true });
|
|
|
|
const mainResult = await build({
|
|
absWorkingDir: here,
|
|
entryPoints: [src("main.tsx")],
|
|
outdir: staticDir(""),
|
|
entryNames: "[name]-[hash]",
|
|
bundle: true,
|
|
format: "esm",
|
|
platform: "browser",
|
|
target: ["es2022"],
|
|
sourcemap: true,
|
|
logLevel: "info",
|
|
jsx: "automatic",
|
|
jsxImportSource: "preact",
|
|
metafile: true,
|
|
});
|
|
|
|
// Look up a build's real output path for one of its entry points, by the
|
|
// entry's own source path — rather than guessing esbuild's hash
|
|
// algorithm or assuming an output ordering. `entryPoint` in the metafile
|
|
// is relative to `absWorkingDir` (`here`, pinned on every build call
|
|
// below); `path.relative` with two absolute arguments never touches
|
|
// `process.cwd()`, so this is stable regardless of the caller's own cwd.
|
|
const outputFor = (result, entrySrcPath) => {
|
|
const wantEntry = relative(here, entrySrcPath);
|
|
const found = Object.entries(result.metafile.outputs).find(
|
|
([, o]) => o.entryPoint === wantEntry,
|
|
);
|
|
if (!found) {
|
|
throw new Error(
|
|
`esbuild produced no output for entry point ${wantEntry} -- ` +
|
|
"either it failed silently or the metafile's entryPoint " +
|
|
"convention changed; update outputFor() to match.",
|
|
);
|
|
}
|
|
return found;
|
|
};
|
|
|
|
// main.tsx's bundle: the JS entry itself, plus its companion CSS output
|
|
// (`cssBundle` — every component's own `import './Foo.css'`, folded into
|
|
// one file alongside the JS, same as before this hashed).
|
|
const [mainJsPath, mainJsInfo] = outputFor(mainResult, src("main.tsx"));
|
|
if (!mainJsInfo.cssBundle) {
|
|
throw new Error(
|
|
"main.tsx produced no companion CSS bundle -- index.html/sw.js still " +
|
|
"reference /static/main.css, so this build can no longer keep that " +
|
|
"promise. Either a component's own CSS import got removed, or " +
|
|
"esbuild's output shape changed; update both the build and the " +
|
|
"files that reference /static/main.css together.",
|
|
);
|
|
}
|
|
const mainJsUrl = toUrl(mainJsPath);
|
|
const mainCssUrl = toUrl(mainJsInfo.cssBundle);
|
|
|
|
// theme.css / swarm-ui.css: hashed, same as main.tsx above. colors.css is
|
|
// deliberately NOT in this list — see the top comment's explanation of
|
|
// why it stays at a fixed path.
|
|
const hashedCssResult = await build({
|
|
absWorkingDir: here,
|
|
entryPoints: [src("theme.css"), src("swarm-ui.css")],
|
|
outdir: staticDir(""),
|
|
entryNames: "[name]-[hash]",
|
|
bundle: true,
|
|
loader: { ".css": "css" },
|
|
logLevel: "info",
|
|
metafile: true,
|
|
});
|
|
const themeCssUrl = toUrl(outputFor(hashedCssResult, src("theme.css"))[0]);
|
|
const swarmUiCssUrl = toUrl(outputFor(hashedCssResult, src("swarm-ui.css"))[0]);
|
|
|
|
// colors.css: fixed, unhashed path — see the top comment.
|
|
await build({
|
|
absWorkingDir: here,
|
|
entryPoints: [src("colors.css")],
|
|
outfile: staticDir("colors.css"),
|
|
bundle: true,
|
|
loader: { ".css": "css" },
|
|
logLevel: "info",
|
|
});
|
|
|
|
// A build-derived cache-bust version for sw.js's own CACHE_NAME: any
|
|
// change to any hashed shell-asset URL (a real content change, or an
|
|
// asset added/removed) changes this, which changes CACHE_NAME, which
|
|
// makes the SW's own `activate` handler evict the old cache in full —
|
|
// no more hand-bumping a CACHE_VERSION const on every shell change.
|
|
const buildVersion = createHash("sha256")
|
|
.update(JSON.stringify([mainJsUrl, mainCssUrl, themeCssUrl, swarmUiCssUrl]))
|
|
.digest("hex")
|
|
.slice(0, 12);
|
|
|
|
const rewriteAssetUrls = (text) =>
|
|
text
|
|
.replaceAll("/static/main.js", mainJsUrl)
|
|
.replaceAll("/static/main.css", mainCssUrl)
|
|
.replaceAll("/static/theme.css", themeCssUrl)
|
|
.replaceAll("/static/swarm-ui.css", swarmUiCssUrl);
|
|
|
|
writeFileSync(
|
|
dist("index.html"),
|
|
rewriteAssetUrls(readFileSync(src("index.html"), "utf8")),
|
|
);
|
|
writeFileSync(
|
|
dist("sw.js"),
|
|
rewriteAssetUrls(readFileSync(src("sw.js"), "utf8")).replace(
|
|
'"__BUILD_VERSION__"',
|
|
JSON.stringify(buildVersion),
|
|
),
|
|
);
|
|
writeFileSync(
|
|
dist("manifest.webmanifest"),
|
|
readFileSync(src("manifest.webmanifest")),
|
|
);
|
|
|
|
console.log("swarm-ui build ok →", dist(""));
|