fix: graceful fallback when native addon is unavailable on unsupported platforms (#1225)

* 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<string, Function>
(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.
This commit is contained in:
Tom Boucher 2026-03-18 15:28:01 -04:00 committed by GitHub
parent ec5cf03ae8
commit 97d589c200

View file

@ -27,6 +27,8 @@ const platformPackageMap: Record<string, string> = {
"win32-x64": "win32-x64-msvc",
};
let _loadedSuccessfully = false;
function loadNative(): Record<string, unknown> {
const errors: string[] = [];
@ -34,7 +36,7 @@ function loadNative(): Record<string, unknown> {
const packageSuffix = platformPackageMap[platformTag];
if (packageSuffix) {
try {
return require(`@gsd-build/engine-${packageSuffix}`) as Record<string, unknown>;
_loadedSuccessfully = true; return require(`@gsd-build/engine-${packageSuffix}`) as Record<string, unknown>;
} 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<string, unknown> {
// 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<string, unknown>;
_loadedSuccessfully = true; return require(releasePath) as Record<string, unknown>;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errors.push(`${releasePath}: ${message}`);
@ -53,7 +55,7 @@ function loadNative(): Record<string, unknown> {
// 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<string, unknown>;
_loadedSuccessfully = true; return require(devPath) as Record<string, unknown>;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errors.push(`${devPath}: ${message}`);
@ -61,13 +63,22 @@ function loadNative(): Record<string, unknown> {
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<string, unknown>, {
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;