Replace raw fileURLToPath in getDefaultPackageRoot with safePackageRootFromImportUrl which returns null instead of throwing when the URL is not a valid local file URL. This prevents the standalone bundle from crashing on Windows when import.meta.url is baked in at build time with a Linux file:// path. Co-authored-by: trek-e <trek-e@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b155c92708
commit
ba71db3b28
3 changed files with 113 additions and 16 deletions
71
src/tests/bridge-package-root.test.ts
Normal file
71
src/tests/bridge-package-root.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
/**
|
||||
* Regression test for #1881: Windows web mode — hardcoded Linux CI path in
|
||||
* standalone build.
|
||||
*
|
||||
* The Next.js standalone build bakes import.meta.url into compiled chunks as
|
||||
* the CI runner's absolute Linux path (file:///home/runner/work/gsd-2/gsd-2/…).
|
||||
* On Windows, fileURLToPath() rejects this with "File URL path must be
|
||||
* absolute". The fix wraps the derivation in safePackageRootFromImportUrl()
|
||||
* so the module-level constant never throws, and resolveBridgeRuntimeConfig
|
||||
* falls through to the GSD_WEB_PACKAGE_ROOT env var.
|
||||
*/
|
||||
|
||||
import { safePackageRootFromImportUrl } from '../web/safe-import-meta-resolve.ts'
|
||||
|
||||
test('safePackageRootFromImportUrl returns a path for a valid native file URL', () => {
|
||||
const result = safePackageRootFromImportUrl(import.meta.url)
|
||||
assert.ok(result !== null, 'should return a path for a valid native file URL')
|
||||
assert.ok(typeof result === 'string')
|
||||
assert.ok(result.length > 0)
|
||||
})
|
||||
|
||||
test('safePackageRootFromImportUrl returns null for a non-file URL', () => {
|
||||
const result = safePackageRootFromImportUrl('https://example.com/foo/bar.ts')
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
test('safePackageRootFromImportUrl returns null for empty input', () => {
|
||||
const result = safePackageRootFromImportUrl('')
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
test('safePackageRootFromImportUrl returns null for malformed URL', () => {
|
||||
const result = safePackageRootFromImportUrl('not-a-url')
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
test('safePackageRootFromImportUrl respects ancestorLevels', () => {
|
||||
// With 0 levels, should return the directory of the module itself
|
||||
const level0 = safePackageRootFromImportUrl(import.meta.url, 0)
|
||||
const level2 = safePackageRootFromImportUrl(import.meta.url, 2)
|
||||
assert.ok(level0 !== null)
|
||||
assert.ok(level2 !== null)
|
||||
// level0 is deeper than level2
|
||||
assert.ok(level0.length > level2.length)
|
||||
})
|
||||
|
||||
test('bridge-service.ts uses safePackageRootFromImportUrl for DEFAULT_PACKAGE_ROOT', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src', 'web', 'bridge-service.ts'), 'utf-8')
|
||||
assert.ok(
|
||||
source.includes('safePackageRootFromImportUrl(import.meta.url)'),
|
||||
'bridge-service.ts must derive DEFAULT_PACKAGE_ROOT via the safe helper',
|
||||
)
|
||||
const rawPattern = 'const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url'
|
||||
assert.ok(
|
||||
!source.includes(rawPattern),
|
||||
'bridge-service.ts must not use raw fileURLToPath for DEFAULT_PACKAGE_ROOT',
|
||||
)
|
||||
})
|
||||
|
||||
test('bridge-service resolveBridgeRuntimeConfig falls back to lazy default', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src', 'web', 'bridge-service.ts'), 'utf-8')
|
||||
assert.ok(
|
||||
source.includes('env.GSD_WEB_PACKAGE_ROOT || getDefaultPackageRoot()'),
|
||||
'resolveBridgeRuntimeConfig must fall back to lazy default package root',
|
||||
)
|
||||
})
|
||||
|
|
@ -2,9 +2,10 @@ import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:chil
|
|||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import type { Readable } from "node:stream";
|
||||
import { join, resolve, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts";
|
||||
import { safePackageRootFromImportUrl } from "./safe-import-meta-resolve.ts";
|
||||
|
||||
import type { AgentSessionEvent, SessionStateChangeReason } from "../../packages/pi-coding-agent/src/core/agent-session.ts";
|
||||
import type {
|
||||
|
|
@ -39,23 +40,14 @@ import {
|
|||
} from "./auto-dashboard-service.ts";
|
||||
import { resolveGsdCliEntry } from "./cli-entry.ts";
|
||||
|
||||
// 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.
|
||||
// The standalone Next.js bundle bakes import.meta.url at build time with the
|
||||
// CI runner's absolute path. On Windows, fileURLToPath() rejects a Linux
|
||||
// file:// URL at module load time. Use a lazy getter so the derivation is
|
||||
// deferred to first use (not module load) and falls back to cwd on failure.
|
||||
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();
|
||||
}
|
||||
_defaultPackageRoot = safePackageRootFromImportUrl(import.meta.url) ?? process.cwd();
|
||||
return _defaultPackageRoot;
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +55,7 @@ function getDefaultPackageRoot(): string {
|
|||
export function resetDefaultPackageRootForTests(): void {
|
||||
_defaultPackageRoot = undefined;
|
||||
}
|
||||
|
||||
const RESPONSE_TIMEOUT_MS = 30_000;
|
||||
const START_TIMEOUT_MS = 150_000;
|
||||
const MAX_STDERR_BUFFER = 8_000;
|
||||
|
|
|
|||
33
src/web/safe-import-meta-resolve.ts
Normal file
33
src/web/safe-import-meta-resolve.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* Derive a package root from an import.meta.url, returning null on failure.
|
||||
*
|
||||
* The Next.js standalone build bakes import.meta.url as the CI runner's
|
||||
* absolute path (e.g. file:///home/runner/work/gsd-2/gsd-2/src/web/bridge-service.ts).
|
||||
* On Windows, fileURLToPath() rejects this Linux path with
|
||||
* "File URL path must be absolute".
|
||||
*
|
||||
* This helper catches that error so the module-level constant never throws,
|
||||
* letting resolveBridgeRuntimeConfig() fall through to the GSD_WEB_PACKAGE_ROOT
|
||||
* env var that web-mode.ts always sets at launch time.
|
||||
*
|
||||
* @param importUrl - The value of import.meta.url at the call site.
|
||||
* @param ancestorLevels - How many directory levels to ascend from the module's
|
||||
* directory to reach the package root (default 2: src/web/ -> root).
|
||||
* @returns Resolved absolute package root path, or null if the URL cannot be
|
||||
* converted to a native path on this platform.
|
||||
*/
|
||||
export function safePackageRootFromImportUrl(
|
||||
importUrl: string,
|
||||
ancestorLevels = 2,
|
||||
): string | null {
|
||||
try {
|
||||
const moduleDir = dirname(fileURLToPath(importUrl));
|
||||
const segments = Array.from({ length: ancestorLevels }, () => "..");
|
||||
return resolve(moduleDir, ...segments);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue