fix(ui): survive an unset BUN_PUBLIC_BASE_PATH in the browser

Bun inlines a literal `process.env.FOO` reference only when FOO is set. With
it unset — a bare `bun --hot src/index.ts`, which is what the README tells you
to run — the reference survived into the client bundle and the app died on
load with `ReferenceError: process is not defined`, before rendering anything.
Production builds were unaffected, because build.ts passes an explicit define.

Reading the value now goes through readBasePathEnv(), which catches. Catching
rather than a `typeof process` guard is deliberate: after inlining the literal
becomes a string, but a typeof test would still run in the browser and discard
that inlined value, silently breaking every subpath deploy. Verified all four
paths: dev unset (reference survives, caught, base "/"), dev set (inlined),
and root + subpath production builds (inlined by define).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5 committed 2026-09-02 01:19:51 +02:00
1 parent ed1667f8a0
commit d1d6735596
2 files changed
+37 -4

No files matched your search

+16 -1
View File
@@ -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;
}
});
+21 -3
View File
@@ -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);