Merge pull request #1887 from snowdamiz/fix/windows-standalone-package-root

fix(web): lazily compute default package root to avoid Windows standalone crash
This commit is contained in:
TÂCHES 2026-03-25 22:16:02 -06:00 committed by GitHub
commit 7cf4084a1e
2 changed files with 95 additions and 2 deletions

View file

@ -0,0 +1,70 @@
/**
* Regression tests for the default package root fallback in bridge-service.
*
* Issue: gsd-build/gsd-2#1881
* The standalone Next.js bundle bakes import.meta.url at build time with the
* CI runner's absolute path. On Windows, fileURLToPath() rejects the Unix
* file:// URL at module load time, 500-ing all API routes.
*
* The fix makes the fallback lazy and catch-guarded so the module loads safely
* on any OS regardless of what import.meta.url resolved to at build time.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { resolve } from "node:path";
const bridge = await import("../web/bridge-service.ts");
test("resolveBridgeRuntimeConfig uses GSD_WEB_PACKAGE_ROOT when set", () => {
const env = {
GSD_WEB_PACKAGE_ROOT: "/custom/package/root",
GSD_WEB_PROJECT_CWD: "/some/project",
} as unknown as NodeJS.ProcessEnv;
const config = bridge.resolveBridgeRuntimeConfig(env);
assert.equal(config.packageRoot, "/custom/package/root");
});
test("resolveBridgeRuntimeConfig falls back to lazy default when GSD_WEB_PACKAGE_ROOT is absent", () => {
// Reset the memoized value so we exercise the lazy computation path.
bridge.resetDefaultPackageRootForTests();
const env = {
GSD_WEB_PROJECT_CWD: "/some/project",
} as unknown as NodeJS.ProcessEnv;
// Should not throw — the lazy getter catches cross-platform failures.
const config = bridge.resolveBridgeRuntimeConfig(env);
assert.equal(typeof config.packageRoot, "string");
assert.ok(config.packageRoot.length > 0, "packageRoot must be a non-empty string");
});
test("lazy default package root is an absolute path", () => {
bridge.resetDefaultPackageRootForTests();
const env = {
GSD_WEB_PROJECT_CWD: "/some/project",
} as unknown as NodeJS.ProcessEnv;
const config = bridge.resolveBridgeRuntimeConfig(env);
// resolve() returns the same path if already absolute.
assert.equal(config.packageRoot, resolve(config.packageRoot));
});
test("lazy default package root is memoized across calls", () => {
bridge.resetDefaultPackageRootForTests();
const env = {} as unknown as NodeJS.ProcessEnv;
const first = bridge.resolveBridgeRuntimeConfig(env).packageRoot;
const second = bridge.resolveBridgeRuntimeConfig(env).packageRoot;
assert.equal(first, second, "memoized value should be stable across calls");
});
test("module loads without throwing (regression: eager fileURLToPath crash)", () => {
// The fact that we can import bridge-service at the top of this file without
// an unhandled exception is itself the primary regression gate. This test
// makes that contract explicit.
assert.ok(typeof bridge.resolveBridgeRuntimeConfig === "function");
});

View file

@ -39,7 +39,30 @@ import {
} from "./auto-dashboard-service.ts";
import { resolveGsdCliEntry } from "./cli-entry.ts";
const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
// Lazily computed fallback — import.meta.url is baked in at build time by
// webpack, so when the standalone bundle built on Linux CI runs on Windows the
// literal file:// URL contains a Unix path that fileURLToPath() rejects.
// Deferring the computation means it only fires when GSD_WEB_PACKAGE_ROOT is
// absent, and if it does fire we handle the cross-platform failure gracefully.
let _defaultPackageRoot: string | undefined;
function getDefaultPackageRoot(): string {
if (_defaultPackageRoot !== undefined) return _defaultPackageRoot;
try {
_defaultPackageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
} catch {
// Standalone bundle running on a different OS than the builder — the
// baked-in import.meta.url is not a valid local file URL. Fall back to
// cwd which is the best available approximation; callers that need the
// real package root should set GSD_WEB_PACKAGE_ROOT.
_defaultPackageRoot = process.cwd();
}
return _defaultPackageRoot;
}
/** @internal — test-only: reset the memoized default package root */
export function resetDefaultPackageRootForTests(): void {
_defaultPackageRoot = undefined;
}
const RESPONSE_TIMEOUT_MS = 30_000;
const START_TIMEOUT_MS = 150_000;
const MAX_STDERR_BUFFER = 8_000;
@ -1058,7 +1081,7 @@ async function fallbackWorkspaceIndex(basePath: string): Promise<GSDWorkspaceInd
export function resolveBridgeRuntimeConfig(env: NodeJS.ProcessEnv = getBridgeDeps().env ?? process.env, projectCwdOverride?: string): BridgeRuntimeConfig {
const projectCwd = projectCwdOverride || env.GSD_WEB_PROJECT_CWD || process.cwd();
const projectSessionsDir = env.GSD_WEB_PROJECT_SESSIONS_DIR || getProjectSessionsDir(projectCwd);
const packageRoot = env.GSD_WEB_PACKAGE_ROOT || DEFAULT_PACKAGE_ROOT;
const packageRoot = env.GSD_WEB_PACKAGE_ROOT || getDefaultPackageRoot();
return { projectCwd, projectSessionsDir, packageRoot };
}