import { mkdir, cp, rm, readdir } from "fs/promises"; import { join } from "path"; import { renderIndex } from "./src/index"; import { renderLogin } from "./src/login"; import { renderKostenrechner } from "./src/kostenrechner"; import { renderFristenrechner } from "./src/fristenrechner"; import { renderDownloads } from "./src/downloads"; import { renderLinks } from "./src/links"; import { renderGlossary } from "./src/glossary"; import { renderGebuehrentabellen } from "./src/gebuehrentabellen"; import { renderChecklists } from "./src/checklists"; import { renderChecklistsDetail } from "./src/checklists-detail"; import { renderChecklistsInstance } from "./src/checklists-instance"; import { renderCourts } from "./src/courts"; import { renderProjects } from "./src/projects"; import { renderProjectsNew } from "./src/projects-new"; import { renderProjectsDetail } from "./src/projects-detail"; import { renderDeadlines } from "./src/deadlines"; import { renderDeadlinesNew } from "./src/deadlines-new"; import { renderDeadlinesDetail } from "./src/deadlines-detail"; import { renderDeadlinesCalendar } from "./src/deadlines-calendar"; import { renderAppointments } from "./src/appointments"; import { renderAppointmentsNew } from "./src/appointments-new"; import { renderAppointmentsDetail } from "./src/appointments-detail"; import { renderAppointmentsCalendar } from "./src/appointments-calendar"; import { renderSettings } from "./src/settings"; import { renderDashboard } from "./src/dashboard"; import { renderAgenda } from "./src/agenda"; import { renderOnboarding } from "./src/onboarding"; import { renderChangelog } from "./src/changelog"; import { renderTeam } from "./src/team"; import { renderAdmin } from "./src/admin"; import { renderAdminTeam } from "./src/admin-team"; import { renderNotFound } from "./src/notfound"; const DIST = join(import.meta.dir, "dist"); async function build() { // Clean dist/ await rm(DIST, { recursive: true, force: true }); await mkdir(join(DIST, "assets"), { recursive: true }); // Bundle client-side JS const result = await Bun.build({ entrypoints: [ // app.ts is loaded on every page (SW registration + bottom-nav init + // install prompt). Keep it ahead of per-page bundles so name collisions // surface fast. join(import.meta.dir, "src/client/app.ts"), join(import.meta.dir, "src/client/index.ts"), join(import.meta.dir, "src/client/login.ts"), join(import.meta.dir, "src/client/kostenrechner.ts"), join(import.meta.dir, "src/client/fristenrechner.ts"), join(import.meta.dir, "src/client/downloads.ts"), join(import.meta.dir, "src/client/links.ts"), join(import.meta.dir, "src/client/glossary.ts"), join(import.meta.dir, "src/client/gebuehrentabellen.ts"), join(import.meta.dir, "src/client/checklists.ts"), join(import.meta.dir, "src/client/checklists-detail.ts"), join(import.meta.dir, "src/client/checklists-instance.ts"), join(import.meta.dir, "src/client/courts.ts"), join(import.meta.dir, "src/client/projects.ts"), join(import.meta.dir, "src/client/projects-new.ts"), join(import.meta.dir, "src/client/projects-detail.ts"), join(import.meta.dir, "src/client/deadlines.ts"), join(import.meta.dir, "src/client/deadlines-new.ts"), join(import.meta.dir, "src/client/deadlines-detail.ts"), join(import.meta.dir, "src/client/deadlines-calendar.ts"), join(import.meta.dir, "src/client/appointments.ts"), join(import.meta.dir, "src/client/appointments-new.ts"), join(import.meta.dir, "src/client/appointments-detail.ts"), join(import.meta.dir, "src/client/appointments-calendar.ts"), join(import.meta.dir, "src/client/settings.ts"), join(import.meta.dir, "src/client/dashboard.ts"), join(import.meta.dir, "src/client/agenda.ts"), join(import.meta.dir, "src/client/onboarding.ts"), join(import.meta.dir, "src/client/changelog.ts"), join(import.meta.dir, "src/client/team.ts"), join(import.meta.dir, "src/client/admin.ts"), join(import.meta.dir, "src/client/admin-team.ts"), join(import.meta.dir, "src/client/notfound.ts"), ], outdir: join(DIST, "assets"), naming: "[name].js", minify: true, // IIFE wraps each bundle's top-level scope so per-page modules don't // leak globals into each other. Without this, app.js's minified // `var d = "patholo-sidebar-pinned"` (the legacy sidebar storage key) // clobbered projects.js's minified `function d()` (applyTranslations) // on the global object — projects.js then crashed with // "TypeError: d is not a function" inside its DOMContentLoaded init, // making /projects appear empty (t-paliad-043). format: "iife", }); if (!result.success) { console.error("JS build failed:"); for (const log of result.logs) { console.error(log); } process.exit(1); } // Copy CSS await cp( join(import.meta.dir, "src/styles/global.css"), join(DIST, "assets/global.css"), ); // Copy public/ → dist/ (manifest.json, sw.js, icons/) — served at the // application root so the service worker can claim scope=/ and so the // manifest is reachable at /manifest.json without a sub-path rewrite. await cp( join(import.meta.dir, "public"), DIST, { recursive: true }, ); // Stamp a unique version into sw.js so each deploy opens a fresh cache. // Activate-time eviction (in sw.js) deletes any prior cache, including // pre-versioning names like paliad-v1-static — that's what stops a stale // /assets/projects.js from a previous deploy lingering on a user's device. const swPath = join(DIST, "sw.js"); const swSrc = await Bun.file(swPath).text(); const buildVersion = `v${Date.now()}`; await Bun.write(swPath, swSrc.replace("__PALIAD_BUILD_VERSION__", buildVersion)); // Render HTML pages await Bun.write(join(DIST, "index.html"), renderIndex()); await Bun.write(join(DIST, "login.html"), renderLogin("login.js")); await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner()); await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner()); await Bun.write(join(DIST, "downloads.html"), renderDownloads()); await Bun.write(join(DIST, "links.html"), renderLinks()); await Bun.write(join(DIST, "glossary.html"), renderGlossary()); await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen()); await Bun.write(join(DIST, "checklists.html"), renderChecklists()); await Bun.write(join(DIST, "checklists-detail.html"), renderChecklistsDetail()); await Bun.write(join(DIST, "checklists-instance.html"), renderChecklistsInstance()); await Bun.write(join(DIST, "courts.html"), renderCourts()); await Bun.write(join(DIST, "projects.html"), renderProjects()); await Bun.write(join(DIST, "projects-new.html"), renderProjectsNew()); await Bun.write(join(DIST, "projects-detail.html"), renderProjectsDetail()); await Bun.write(join(DIST, "deadlines.html"), renderDeadlines()); await Bun.write(join(DIST, "deadlines-new.html"), renderDeadlinesNew()); await Bun.write(join(DIST, "deadlines-detail.html"), renderDeadlinesDetail()); await Bun.write(join(DIST, "deadlines-calendar.html"), renderDeadlinesCalendar()); await Bun.write(join(DIST, "appointments.html"), renderAppointments()); await Bun.write(join(DIST, "appointments-new.html"), renderAppointmentsNew()); await Bun.write(join(DIST, "appointments-detail.html"), renderAppointmentsDetail()); await Bun.write(join(DIST, "appointments-calendar.html"), renderAppointmentsCalendar()); await Bun.write(join(DIST, "settings.html"), renderSettings()); await Bun.write(join(DIST, "dashboard.html"), renderDashboard()); await Bun.write(join(DIST, "agenda.html"), renderAgenda()); await Bun.write(join(DIST, "onboarding.html"), renderOnboarding()); await Bun.write(join(DIST, "changelog.html"), renderChangelog()); await Bun.write(join(DIST, "team.html"), renderTeam()); await Bun.write(join(DIST, "admin.html"), renderAdmin()); await Bun.write(join(DIST, "admin-team.html"), renderAdminTeam()); await Bun.write(join(DIST, "notfound.html"), renderNotFound()); // Append ?v= to every /assets/*.js and /assets/*.css URL in // every emitted HTML file. Cache-Control alone isn't enough: a browser that // cached a script in a previous deploy keeps serving it from disk because // the cache entry was stored without the no-cache directive. Versioning the // URL changes the cache key, so the next page load fetches a fresh bundle // unconditionally \u2014 this is what guarantees t-paliad-043's IIFE wrap fix // actually reaches users on their next visit even without a SW. const htmlFiles = (await readdir(DIST)).filter((f) => f.endsWith(".html")); for (const f of htmlFiles) { const path = join(DIST, f); const html = await Bun.file(path).text(); const stamped = html.replace( /(\/assets\/[\w-]+\.(?:js|css))/g, `$1?v=${buildVersion}`, ); await Bun.write(path, stamped); } console.log("Build complete \u2192 dist/"); } build();