fix: resolve Node v24 web boot failure — ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING (#1864)
Node v24 forbids --experimental-strip-types for files under node_modules/.
When GSD is globally installed, all src/ files live under node_modules/gsd-pi/,
causing every subprocess worker to crash with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING.
Bug 1: Extract resolveTypeStrippingFlag() into src/web/ts-subprocess-flags.ts.
When the package root is under node_modules/ and Node >= 22.7, the function
returns --experimental-transform-types (which handles node_modules paths).
All 15 service files and cli-entry.ts now call this function instead of
hardcoding --experimental-strip-types.
Bug 2: waitForBootReady() now tracks consecutive 5xx responses and aborts
after 3 in a row, including the response body in the error message.
Connection-level errors (transient during cold start) reset the counter.
Bug 3: The /api/boot route handler now wraps collectBootPayload() in
try/catch and returns { error: message } with status 500, matching the
error response pattern used by other API routes.
Fixes #1849
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7140ee0f53
commit
2e04253c0b
19 changed files with 255 additions and 23 deletions
151
src/tests/web-boot-node24.test.ts
Normal file
151
src/tests/web-boot-node24.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
|
||||
import { resolveTypeStrippingFlag } from "../web/ts-subprocess-flags.ts"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bug 1 — resolveTypeStrippingFlag selects the correct flag
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveTypeStrippingFlag returns --experimental-strip-types for paths outside node_modules", () => {
|
||||
const flag = resolveTypeStrippingFlag("/home/user/projects/gsd")
|
||||
assert.equal(flag, "--experimental-strip-types")
|
||||
})
|
||||
|
||||
test("resolveTypeStrippingFlag returns --experimental-strip-types for path with node_modules substring not as directory", () => {
|
||||
// e.g. /home/user/my_node_modules_backup/gsd — not actually under node_modules/
|
||||
const flag = resolveTypeStrippingFlag("/home/user/my_node_modules_backup/gsd")
|
||||
assert.equal(flag, "--experimental-strip-types")
|
||||
})
|
||||
|
||||
test("resolveTypeStrippingFlag returns --experimental-transform-types for paths under node_modules/ on Node >= 22.7", () => {
|
||||
const [major, minor] = process.versions.node.split(".").map(Number)
|
||||
const flag = resolveTypeStrippingFlag("/usr/lib/node_modules/gsd-pi")
|
||||
|
||||
if (major > 22 || (major === 22 && minor >= 7)) {
|
||||
assert.equal(flag, "--experimental-transform-types")
|
||||
} else {
|
||||
// On older Node, falls back to strip-types since transform-types isn't available
|
||||
assert.equal(flag, "--experimental-strip-types")
|
||||
}
|
||||
})
|
||||
|
||||
test("resolveTypeStrippingFlag handles Windows-style paths under node_modules", () => {
|
||||
const [major, minor] = process.versions.node.split(".").map(Number)
|
||||
const flag = resolveTypeStrippingFlag("C:\\Users\\dev\\AppData\\node_modules\\gsd-pi")
|
||||
|
||||
if (major > 22 || (major === 22 && minor >= 7)) {
|
||||
assert.equal(flag, "--experimental-transform-types")
|
||||
} else {
|
||||
assert.equal(flag, "--experimental-strip-types")
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bug 2 — waitForBootReady fails fast on consecutive 5xx
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The waitForBootReady function is not exported, but the behavior is testable
|
||||
// by verifying the launchWebMode deps injection. We test the core logic
|
||||
// pattern directly: 3 consecutive 5xx should abort without waiting for timeout.
|
||||
|
||||
test("waitForBootReady pattern: consecutive 5xx detection aborts early", async () => {
|
||||
// Simulate the retry logic extracted from waitForBootReady
|
||||
let consecutive5xx = 0
|
||||
const MAX_CONSECUTIVE_5XX = 3
|
||||
const responses = [500, 500, 500] // three deterministic 500s
|
||||
let abortedEarly = false
|
||||
|
||||
for (const statusCode of responses) {
|
||||
if (statusCode >= 500) {
|
||||
consecutive5xx++
|
||||
if (consecutive5xx >= MAX_CONSECUTIVE_5XX) {
|
||||
abortedEarly = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
consecutive5xx = 0
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(abortedEarly, true, "should abort after 3 consecutive 5xx responses")
|
||||
assert.equal(consecutive5xx, 3)
|
||||
})
|
||||
|
||||
test("waitForBootReady pattern: non-5xx responses reset the consecutive counter", () => {
|
||||
let consecutive5xx = 0
|
||||
const MAX_CONSECUTIVE_5XX = 3
|
||||
// 500, 500, connection-refused (resets), 500, 500 — should NOT trigger abort
|
||||
const events = [
|
||||
{ type: "response", status: 500 },
|
||||
{ type: "response", status: 500 },
|
||||
{ type: "error" }, // connection refused resets counter
|
||||
{ type: "response", status: 500 },
|
||||
{ type: "response", status: 500 },
|
||||
]
|
||||
let abortedEarly = false
|
||||
|
||||
for (const event of events) {
|
||||
if (event.type === "response" && (event.status ?? 0) >= 500) {
|
||||
consecutive5xx++
|
||||
if (consecutive5xx >= MAX_CONSECUTIVE_5XX) {
|
||||
abortedEarly = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
consecutive5xx = 0
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(abortedEarly, false, "should not abort when errors reset the counter")
|
||||
})
|
||||
|
||||
test("waitForBootReady pattern: mixed 4xx and 5xx only counts 5xx", () => {
|
||||
let consecutive5xx = 0
|
||||
const MAX_CONSECUTIVE_5XX = 3
|
||||
const responses = [500, 404, 500, 500]
|
||||
let abortedEarly = false
|
||||
|
||||
for (const statusCode of responses) {
|
||||
if (statusCode >= 500) {
|
||||
consecutive5xx++
|
||||
if (consecutive5xx >= MAX_CONSECUTIVE_5XX) {
|
||||
abortedEarly = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
consecutive5xx = 0
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(abortedEarly, false, "404 should reset the consecutive 5xx counter")
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bug 3 — /api/boot route error handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("boot route returns { error } JSON on handler failure", async () => {
|
||||
// Read the route source to verify try/catch wrapping is present
|
||||
const { readFileSync } = await import("node:fs")
|
||||
const { join } = await import("node:path")
|
||||
|
||||
const routeSource = readFileSync(
|
||||
join(process.cwd(), "web", "app", "api", "boot", "route.ts"),
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
// The route must catch errors and return { error: message }
|
||||
assert.match(routeSource, /try\s*\{/, "boot route must have try block")
|
||||
assert.match(routeSource, /catch\s*\(/, "boot route must have catch block")
|
||||
assert.match(
|
||||
routeSource,
|
||||
/\{\s*error:\s*message\s*\}/,
|
||||
"boot route must return { error: message } on failure",
|
||||
)
|
||||
assert.match(
|
||||
routeSource,
|
||||
/status:\s*500/,
|
||||
"boot route must return status 500 on error",
|
||||
)
|
||||
})
|
||||
|
|
@ -451,7 +451,10 @@ async function waitForBootReady(url: string, timeoutMs = 180_000, stderr?: Writa
|
|||
const deadline = Date.now() + timeoutMs
|
||||
const startedAt = Date.now()
|
||||
let lastError: string | null = null
|
||||
let lastBody: string | null = null
|
||||
let hostUp = false
|
||||
let consecutive5xx = 0
|
||||
const MAX_CONSECUTIVE_5XX = 3
|
||||
// Print a progress dot every N ms while waiting so the terminal isn't silent
|
||||
const TICKER_INTERVAL_MS = 5_000
|
||||
let lastTickAt = startedAt
|
||||
|
|
@ -468,12 +471,29 @@ async function waitForBootReady(url: string, timeoutMs = 180_000, stderr?: Writa
|
|||
hostUp = true
|
||||
stderr?.write(`[gsd] Web host ready.\n`)
|
||||
}
|
||||
consecutive5xx = 0
|
||||
// Host responded successfully — it's ready for the browser
|
||||
return
|
||||
} else if (response.statusCode >= 500) {
|
||||
consecutive5xx++
|
||||
lastError = `http ${response.statusCode}`
|
||||
lastBody = response.body || null
|
||||
if (consecutive5xx >= MAX_CONSECUTIVE_5XX) {
|
||||
const detail = lastBody ? `: ${lastBody.slice(0, 500)}` : ''
|
||||
throw new Error(
|
||||
`boot route returned ${MAX_CONSECUTIVE_5XX} consecutive 5xx responses (last: ${response.statusCode})${detail}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
consecutive5xx = 0
|
||||
lastError = `http ${response.statusCode}`
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('boot route returned')) {
|
||||
throw error
|
||||
}
|
||||
// Connection refused, timeout, etc. — transient during cold start
|
||||
consecutive5xx = 0
|
||||
lastError = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path";
|
|||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import type { AutoDashboardData } from "./bridge-service.ts";
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
|
||||
const AUTO_DASHBOARD_MAX_BUFFER = 1024 * 1024;
|
||||
const TEST_AUTO_DASHBOARD_MODULE_ENV = "GSD_WEB_TEST_AUTO_DASHBOARD_MODULE";
|
||||
|
|
@ -73,7 +74,7 @@ export async function collectAuthoritativeAutoDashboardData(
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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 { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts";
|
||||
|
||||
import type { AgentSessionEvent, SessionStateChangeReason } from "../../packages/pi-coding-agent/src/core/agent-session.ts";
|
||||
import type {
|
||||
|
|
@ -924,7 +925,7 @@ async function loadWorkspaceIndexViaChildProcess(basePath: string, packageRoot:
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { CapturesData, CaptureResolveRequest, CaptureResolveResult } from "../../web/lib/knowledge-captures-types.ts"
|
||||
|
||||
const CAPTURES_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -51,7 +52,7 @@ export async function collectCapturesData(projectCwdOverride?: string): Promise<
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
@ -120,7 +121,7 @@ export async function resolveCaptureAction(request: CaptureResolveRequest, proje
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { CleanupData, CleanupResult } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const CLEANUP_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -65,7 +66,7 @@ export async function collectCleanupData(projectCwdOverride?: string): Promise<C
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
@ -152,7 +153,7 @@ export async function executeCleanup(
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts";
|
||||
|
||||
export interface GsdCliEntry {
|
||||
command: string;
|
||||
|
|
@ -46,7 +47,7 @@ export function resolveGsdCliEntry(options: ResolveGsdCliEntryOptions): GsdCliEn
|
|||
args: [
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(options.packageRoot),
|
||||
sourceEntry,
|
||||
...extraArgs,
|
||||
...messageArgs,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { DoctorReport, DoctorFixResult } from "../../web/lib/diagnostics-types.ts"
|
||||
|
||||
const DOCTOR_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -42,7 +43,7 @@ function runDoctorChild(
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { ExportResult } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const EXPORT_MAX_BUFFER = 4 * 1024 * 1024
|
||||
|
|
@ -60,7 +61,7 @@ export async function collectExportData(
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { ForensicReport } from "../../web/lib/diagnostics-types.ts"
|
||||
|
||||
const FORENSICS_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -79,7 +80,7 @@ export async function collectForensicsData(projectCwdOverride?: string): Promise
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { HistoryData } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const HISTORY_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -53,7 +54,7 @@ export async function collectHistoryData(projectCwdOverride?: string): Promise<H
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { HooksData } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const HOOKS_MAX_BUFFER = 512 * 1024
|
||||
|
|
@ -54,7 +55,7 @@ export async function collectHooksData(projectCwdOverride?: string): Promise<Hoo
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
collectSelectiveLiveStatePayload,
|
||||
resolveBridgeRuntimeConfig,
|
||||
} from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type {
|
||||
WorkspaceRecoveryBrowserAction,
|
||||
WorkspaceRecoveryCodeSummary,
|
||||
|
|
@ -473,7 +474,7 @@ async function collectRecoveryDiagnosticsChildPayload(
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { SettingsData } from "../../web/lib/settings-types.ts"
|
||||
|
||||
const SETTINGS_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -110,7 +111,7 @@ export async function collectSettingsData(projectCwdOverride?: string): Promise<
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { SkillHealthReport } from "../../web/lib/diagnostics-types.ts"
|
||||
|
||||
const SKILL_HEALTH_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -48,7 +49,7 @@ export async function collectSkillHealthData(projectCwdOverride?: string): Promi
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
38
src/web/ts-subprocess-flags.ts
Normal file
38
src/web/ts-subprocess-flags.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Returns the correct Node.js type-stripping flag for subprocess spawning.
|
||||
*
|
||||
* Node v24 enforces ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING for files
|
||||
* resolved under `node_modules/`. When GSD is installed globally via npm,
|
||||
* all source files live under `node_modules/gsd-pi/src/...`, so
|
||||
* `--experimental-strip-types` fails deterministically.
|
||||
*
|
||||
* `--experimental-transform-types` applies a full TypeScript transform that
|
||||
* works regardless of whether the file is under `node_modules/`. On older
|
||||
* Node versions (< 22.7) that lack both flags, this falls back to
|
||||
* `--experimental-strip-types` (the caller's loader handles the rest).
|
||||
*/
|
||||
export function resolveTypeStrippingFlag(packageRoot: string): string {
|
||||
const needsTransform =
|
||||
isUnderNodeModules(packageRoot) && supportsTransformTypes()
|
||||
return needsTransform
|
||||
? "--experimental-transform-types"
|
||||
: "--experimental-strip-types"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the given path sits inside a `node_modules/` directory.
|
||||
* Handles both Unix and Windows path separators.
|
||||
*/
|
||||
function isUnderNodeModules(filePath: string): boolean {
|
||||
const normalized = filePath.replace(/\\/g, "/")
|
||||
return normalized.includes("/node_modules/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the running Node version supports
|
||||
* `--experimental-transform-types` (available since Node v22.7.0).
|
||||
*/
|
||||
function supportsTransformTypes(): boolean {
|
||||
const [major, minor] = process.versions.node.split(".").map(Number)
|
||||
return major > 22 || (major === 22 && minor >= 7)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import type { UndoInfo, UndoResult } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const UNDO_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
|
@ -182,7 +183,7 @@ export async function executeUndo(projectCwdOverride?: string): Promise<UndoResu
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
|
||||
const VISUALIZER_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const VISUALIZER_MODULE_ENV = "GSD_VISUALIZER_MODULE"
|
||||
|
|
@ -85,7 +86,7 @@ export async function collectVisualizerData(projectCwdOverride?: string): Promis
|
|||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
"--experimental-strip-types",
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
script,
|
||||
|
|
|
|||
|
|
@ -28,11 +28,19 @@ export async function GET(request: Request): Promise<Response> {
|
|||
});
|
||||
}
|
||||
|
||||
const bootPayload = await collectBootPayload(projectCwd);
|
||||
try {
|
||||
const bootPayload = await collectBootPayload(projectCwd);
|
||||
|
||||
return Response.json(bootPayload, {
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
return Response.json(bootPayload, {
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return Response.json(
|
||||
{ error: message },
|
||||
{ status: 500, headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue