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:
1 parent
ed1667f8a0
commit
d1d6735596
2 files changed
+37
-4
No files matched your search
+16
-1
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user