#!/usr/bin/env bun // Generates src/apps/frontend/index.gen.html from index.html, substituting __PUBLIC_URL__ with // PUBLIC_URL from .env. // // Favicon and OpenGraph hrefs have to be absolute. Bun's HTML bundler treats a root-relative href as // an asset to resolve on disk and fails the build ("Could not resolve: /favicon.ico"), and crawlers // fetch OG tags standalone so they cannot resolve a relative URL either. Hardcoding the domain would // force every deployment to carry its own edit of index.html, and Bun's HTML import offers no // substitution hook — so the swap happens here and the server imports the generated file. // // index.gen.html is gitignored; index.html is the tracked template and is identical on every instance. import { existsSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const template = join(root, "src/apps/frontend/index.html"); const output = join(root, "src/apps/frontend/index.gen.html"); // Where the URL comes from, most specific first: // // 1. the first argument `bun gen:index https://example.com` // 2. PUBLIC_URL in this project's .env // 3. PUBLIC_URL in the environment // // Note the order: .env BEATS the ambient environment, which is the reverse of the usual convention. // A host that exports its own PUBLIC_URL into every process it spawns would otherwise stamp its // domain onto this app's favicon hrefs. A value in this project's own .env is a statement about this // project; an inherited one is a statement about whatever launched it. const argUrl = process.argv[2]?.trim(); let dotenvUrl: string | undefined; const envPath = join(root, ".env"); if (!argUrl && existsSync(envPath)) { for (const line of (await Bun.file(envPath).text()).split("\n")) { const match = line.match(/^\s*PUBLIC_URL\s*=\s*(.*)$/); if (match) dotenvUrl = match[1]!.trim().replace(/^["']|["']$/g, ""); } } const publicUrl = (argUrl || dotenvUrl || process.env.PUBLIC_URL || "").replace(/\/+$/, ""); if (!publicUrl) { console.error("[gen-index] no public URL. Pass one — `bun gen:index http://localhost:3001` —"); console.error("[gen-index] or set PUBLIC_URL in .env."); process.exit(1); } // Caught here rather than left to a crawler: a relative or scheme-less value substitutes without // complaint and produces tags nothing can resolve, which stays invisible until someone shares a link. try { const parsed = new URL(publicUrl); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("not http(s)"); } catch { console.error(`[gen-index] "${publicUrl}" is not an absolute http(s) URL.`); process.exit(1); } const html = await Bun.file(template).text(); const generated = html.replaceAll("__PUBLIC_URL__", publicUrl); if (generated.includes("__PUBLIC_URL__")) { console.error("[gen-index] substitution left placeholders behind"); process.exit(1); } // Only write when the content actually changes, so `bun --hot` does not restart in a loop. if (existsSync(output) && (await Bun.file(output).text()) === generated) { console.log(`[gen-index] up to date (${publicUrl})`); } else { await Bun.write(output, generated); console.log(`[gen-index] wrote index.gen.html (${publicUrl})`); }