From 97d589c200b8aacba363eab0336b0f5f538faae0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 15:28:01 -0400 Subject: [PATCH] fix: graceful fallback when native addon is unavailable on unsupported platforms (#1225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: graceful fallback when native addon is unavailable on unsupported platforms On platforms without a pre-built native binary (e.g., win32-arm64), the native loader threw at import time, crashing the entire application. Now returns a Proxy object that: - Allows the import to succeed (no startup crash) - Throws per-function when a native function is actually called - Individual consumers (GSD parser bridge, fuzzy find, autocomplete) already wrap native calls in try/catch and fall back to JS implementations Also exports nativeAvailable boolean for consumers that want to check upfront. Prints a one-line warning to stderr on startup. GSD is now fully functional on unsupported platforms — just slower for file parsing, grep, and fuzzy search. Fixes #1223 * fix: remove __nativeUnavailable from exported type to fix TS2352 cast errors The __nativeUnavailable boolean property on the native type caused TS2352 errors in consumers that cast native as Record (ast/index.ts, diff/index.ts, gsd-parser/index.ts). Replaced with a module-level _loadedSuccessfully flag and exported nativeAvailable boolean. The proxy no longer needs a sentinel property. --- packages/native/src/native.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/native/src/native.ts b/packages/native/src/native.ts index 944c50c69..a8f81b2f9 100644 --- a/packages/native/src/native.ts +++ b/packages/native/src/native.ts @@ -27,6 +27,8 @@ const platformPackageMap: Record = { "win32-x64": "win32-x64-msvc", }; +let _loadedSuccessfully = false; + function loadNative(): Record { const errors: string[] = []; @@ -34,7 +36,7 @@ function loadNative(): Record { const packageSuffix = platformPackageMap[platformTag]; if (packageSuffix) { try { - return require(`@gsd-build/engine-${packageSuffix}`) as Record; + _loadedSuccessfully = true; return require(`@gsd-build/engine-${packageSuffix}`) as Record; } catch (err) { const message = err instanceof Error ? err.message : String(err); errors.push(`@gsd-build/engine-${packageSuffix}: ${message}`); @@ -44,7 +46,7 @@ function loadNative(): Record { // 2. Try local release build (native/addon/gsd_engine.{platform}.node) const releasePath = path.join(addonDir, `gsd_engine.${platformTag}.node`); try { - return require(releasePath) as Record; + _loadedSuccessfully = true; return require(releasePath) as Record; } catch (err) { const message = err instanceof Error ? err.message : String(err); errors.push(`${releasePath}: ${message}`); @@ -53,7 +55,7 @@ function loadNative(): Record { // 3. Try local dev build (native/addon/gsd_engine.dev.node) const devPath = path.join(addonDir, "gsd_engine.dev.node"); try { - return require(devPath) as Record; + _loadedSuccessfully = true; return require(devPath) as Record; } catch (err) { const message = err instanceof Error ? err.message : String(err); errors.push(`${devPath}: ${message}`); @@ -61,13 +63,22 @@ function loadNative(): Record { const details = errors.map((e) => ` - ${e}`).join("\n"); const supportedPlatforms = Object.keys(platformPackageMap); - throw new Error( - `Failed to load gsd_engine native addon for ${platformTag}.\n\n` + - `Tried:\n${details}\n\n` + - `Supported platforms: ${supportedPlatforms.join(", ")}\n` + - `If your platform is listed, try reinstalling: npm i -g gsd-pi\n` + - `Otherwise, please open an issue: https://github.com/gsd-build/gsd-2/issues`, + + // Graceful fallback: on unsupported platforms (e.g., win32-arm64), return a + // proxy that throws on individual function calls rather than crashing the + // entire import chain at startup (#1223). Consumers with JS fallbacks + // (parseRoadmap, parsePlan, fuzzyFind, etc.) catch these and degrade gracefully. + process.stderr.write( + `[gsd] Native addon not available for ${platformTag}. Falling back to JS implementations (slower).\n` + + ` Supported native platforms: ${supportedPlatforms.join(", ")}\n`, ); + return new Proxy({} as Record, { + get(_target, prop) { + return (..._args: unknown[]) => { + throw new Error(`Native function '${String(prop)}' is not available on ${platformTag}`); + }; + }, + }); } export const native = loadNative() as { @@ -140,3 +151,6 @@ export const native = loadNative() as { parseStreamingJson: (text: string) => unknown; xxHash32: (input: string, seed: number) => number; }; + +/** True when the native addon loaded successfully. False on unsupported platforms. */ +export const nativeAvailable = _loadedSuccessfully;