import { copyFile, mkdir, rm } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import esbuild from "esbuild"; /** * Builds the Ekklesia shell into a static browser application. * * This is **not** the module UI build pipeline, and the two must not be conflated. ADR 0023's * `@ekklesia/module-ui-build` compiles a *module's* `src/ui/index.tsx` into the artifact its * manifest declares, is owned by the platform so module authors write no bundler configuration, and * runs when a module is packaged. This script builds *this application*, is owned by this package * alone, and knows nothing about manifests. They happen to share a bundler; they share no * configuration, no contract, and no output. * * Installed module UIs are deliberately **not** bundled in. The shell has no import of any module, * so there is nothing for esbuild to reach: every module artifact is fetched at run time from the * URL the inventory returned. That is the property that makes modules independently installable, and * bundling one would silently destroy it. */ const packageDirectory = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const outputDirectory = path.join(packageDirectory, "dist", "browser"); const serve = process.argv.includes("--serve"); /** * Empty means "the API is on this page's own origin" — the expected production shape. Set it to an * absolute origin for split-origin development (`EKKLESIA_API_BASE_URL=http://127.0.0.1:3000`), and * add this page's own origin to `EKKLESIA_IDENTITY_TRUSTED_ORIGINS` so the API reflects it for CORS * and the session cookie travels. */ const apiBaseUrl = process.env.EKKLESIA_API_BASE_URL ?? ""; /** @type {import("esbuild").BuildOptions} */ const buildOptions = { absWorkingDir: packageDirectory, // Everything the shell needs is bundled; a browser resolves nothing at run time except the module // artifacts it is told about, which are real URLs rather than bare specifiers. bundle: true, define: { __EKKLESIA_API_BASE_URL__: JSON.stringify(apiBaseUrl), // React reads this at module scope and a browser has no `process`. "process.env.NODE_ENV": JSON.stringify(serve ? "development" : "production"), }, entryPoints: { // Emitted under the exact path `apps/api` serves it from (ADR 0044), so the development server's // `servedir` and production answer the same absolute URL `index.html` names. "assets/ekklesia-dashboard/app": path.join(packageDirectory, "src", "browser", "main.tsx"), "display/app": path.join(packageDirectory, "src", "display", "main.tsx"), // The display service worker (ADR 0043). A separate entry because a worker is a separate // realm with its own globals; bundling it into the page would make it unregisterable and its // `self` meaningless. "display/sw": path.join(packageDirectory, "src", "display", "sw.ts"), }, format: "esm", jsx: "automatic", logLevel: "info", // Unminified, matching the module UI build: an operator is expected to be able to read what their // installation actually serves, and nothing else in this repository minifies. minify: false, outdir: outputDirectory, platform: "browser", sourcemap: true, // Matches `browser.json`'s `target`, so what TypeScript typechecks and what esbuild emits agree. target: "es2022", }; await rm(outputDirectory, { force: true, recursive: true }); await mkdir(outputDirectory, { recursive: true }); await mkdir(path.join(outputDirectory, "display"), { recursive: true }); await copyFile(path.join(packageDirectory, "index.html"), path.join(outputDirectory, "index.html")); await copyFile( path.join(packageDirectory, "display.html"), path.join(outputDirectory, "display", "index.html"), ); if (serve) { const context = await esbuild.context(buildOptions); await context.watch(); const { host, port } = await context.serve({ servedir: outputDirectory }); console.log(`Ekklesia shell (development) on http://${host}:${port}`); console.log( apiBaseUrl === "" ? "API origin: this page's own origin. Set EKKLESIA_API_BASE_URL for a split-origin API." : `API origin: ${apiBaseUrl}`, ); } else { await esbuild.build(buildOptions); console.log(`Built the Ekklesia shell into ${path.relative(packageDirectory, outputDirectory)}`); }