diff --git a/src/ui/basePath.test.ts b/src/ui/basePath.test.ts index 0f102ba..5d8c088 100644 --- a/src/ui/basePath.test.ts +++ b/src/ui/basePath.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { withBase, normalizeBase } from "./basePath.ts"; +import { withBase, normalizeBase, readBasePathEnv } from "./basePath.ts"; test("normalizeBase defaults to root", () => { expect(normalizeBase(undefined)).toBe("/"); @@ -21,3 +21,18 @@ test("withBase joins without doubling slashes", () => { ); expect(withBase("/sw.js", "/")).toBe("/sw.js"); }); + +// Regression: Bun only inlines `process.env.FOO` when the variable is SET. When +// it is unset (e.g. a bare `bun --hot src/index.ts`), the reference survives +// into the browser bundle and throws `ReferenceError: process is not defined` +// before the app can boot. Reading the value must therefore tolerate `process` +// not existing at all. +test("reading the base path env does not throw when process is undefined", () => { + const saved = (globalThis as { process?: unknown }).process; + try { + delete (globalThis as { process?: unknown }).process; + expect(readBasePathEnv()).toBeUndefined(); + } finally { + (globalThis as { process?: unknown }).process = saved; + } +}); diff --git a/src/ui/basePath.ts b/src/ui/basePath.ts index 3da17f7..58c6efe 100644 --- a/src/ui/basePath.ts +++ b/src/ui/basePath.ts @@ -6,10 +6,28 @@ export function normalizeBase(raw: string | undefined): string { } /** - * Bun substitutes process.env.BUN_PUBLIC_* into client code at build time, so - * this becomes a literal string in the bundle. + * Reads the build-time base path. + * + * Bun inlines a literal `process.env.FOO` reference ONLY when FOO is actually + * set. When it is unset — a bare `bun --hot src/index.ts`, say — the reference + * survives into the browser bundle, where `process` does not exist, and the app + * dies with `ReferenceError: process is not defined` before it can boot. + * + * try/catch rather than a `typeof process` guard, deliberately: after inlining + * the literal becomes a string, but a `typeof process === "undefined"` test + * would still run in the browser and discard that inlined value, silently + * breaking every subpath deploy. Catching leaves the inlined value untouched + * and only absorbs the unset case. */ -export const BASE_PATH = normalizeBase(process.env.BUN_PUBLIC_BASE_PATH); +export function readBasePathEnv(): string | undefined { + try { + return process.env.BUN_PUBLIC_BASE_PATH; + } catch { + return undefined; + } +} + +export const BASE_PATH = normalizeBase(readBasePathEnv()); /** react-router wants the basename without a trailing slash ("" for root). */ export const ROUTER_BASENAME = BASE_PATH === "/" ? "" : BASE_PATH.slice(0, -1);