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:
commit
7cf4084a1e
2 changed files with 95 additions and 2 deletions
70
src/tests/web-bridge-package-root.test.ts
Normal file
70
src/tests/web-bridge-package-root.test.ts
Normal 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");
|
||||
});
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue