From c418a4cc06e34d90791dd555c12a88ce5721ff45 Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Sun, 26 Jul 2026 23:25:23 +0200 Subject: [PATCH 1/2] fix: say which package is missing when the native binding fails The platform binaries are optionalDependencies, so any installer that declines them leaves this package importable and dead, throwing only on first use. The message it threw then listed every supported target, including the one it was running on, which reads as a portability problem that does not exist and sends the reader looking in the wrong place. It now separates the two cases: a platform that is not a build target, and a platform that is one whose package was not installed. The second names the package and the usual reasons an optionalDependency goes missing. The loader had no tests despite taking every input it consults as an injectable option, which is how this stayed invisible. Both branches are now covered. --- .../stdnum/src/__test__/native-node.test.ts | 94 +++++++++++++++++++ packages/stdnum/src/native-node.ts | 23 ++++- 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packages/stdnum/src/__test__/native-node.test.ts diff --git a/packages/stdnum/src/__test__/native-node.test.ts b/packages/stdnum/src/__test__/native-node.test.ts new file mode 100644 index 0000000..0989da2 --- /dev/null +++ b/packages/stdnum/src/__test__/native-node.test.ts @@ -0,0 +1,94 @@ +/** + * The loader had no tests, which is how a real failure stayed invisible: the + * platform binaries are optionalDependencies, so an installer that declines + * them leaves this package importable and dead, throwing only on first use. + * The message that surfaced then said "supported targets: …, linux-x64-gnu, + * …" while failing ON linux-x64-gnu, which points the reader at a + * portability problem that does not exist. + * + * Every input the loader consults is injectable, so both branches are + * testable without touching the filesystem or the real platform. + */ + +import { describe, expect, test } from "bun:test"; + +import type { NativeStdnumBinding } from "../native"; +import { loadNativeStdnumBinding } from "../native-node"; + +/** Nothing resolves: the shape an install with no platform package has. */ +const requireNothing = (specifier: string): unknown => { + throw new Error(`Cannot find module '${specifier}'`); +}; + +describe("loadNativeStdnumBinding", () => { + test("names the uninstalled package when the platform IS a build target", () => { + expect(() => + loadNativeStdnumBinding({ + platform: "linux", + arch: "x64", + libc: "gnu", + env: {}, + requireModule: requireNothing, + }), + ).toThrow(/@stll\/stdnum-linux-x64-gnu/u); + }); + + test("says the target exists rather than blaming the platform", () => { + let message = ""; + try { + loadNativeStdnumBinding({ + platform: "darwin", + arch: "arm64", + env: {}, + requireModule: requireNothing, + }); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + + expect(message).toContain("IS a build target"); + expect(message).toContain("optionalDependency"); + // The old message listed every supported target and nothing else, which + // read as "your platform is unsupported" for a platform that is. + expect(message).not.toMatch(/^Unable to load native stdnum binding.*supported targets/u); + }); + + test("reports an unsupported platform as unsupported", () => { + let message = ""; + try { + loadNativeStdnumBinding({ + platform: "linux", + arch: "x64", + libc: "musl", + env: {}, + requireModule: requireNothing, + }); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + + expect(message).toContain("not among the build targets"); + expect(message).toContain("linux-x64-musl"); + }); + + test("prefers an explicit library path over the platform package", () => { + // Every member the binding contract requires; a partial object is + // rejected as "does not match the stdnum binding contract". + const binding = new Proxy( + {}, + { get: () => () => undefined }, + ) as NativeStdnumBinding; + const loaded = loadNativeStdnumBinding({ + platform: "linux", + arch: "x64", + libc: "gnu", + env: { STELLA_STDNUM_NATIVE_LIBRARY_PATH: "/tmp/custom.node" }, + requireModule: (specifier) => { + if (specifier === "/tmp/custom.node") return binding; + throw new Error(`Cannot find module '${specifier}'`); + }, + }); + + expect(loaded).toBe(binding); + }); +}); diff --git a/packages/stdnum/src/native-node.ts b/packages/stdnum/src/native-node.ts index 77792d8..122c4de 100644 --- a/packages/stdnum/src/native-node.ts +++ b/packages/stdnum/src/native-node.ts @@ -69,12 +69,31 @@ export const loadNativeStdnumBinding = ( ); } } + const current = [platform, arch, libc] + .filter(Boolean) + .join("-"); const supported = targets .map(([p, a, l]) => [p, a, l].filter(Boolean).join("-")) .join(", "); + + // Distinguish "we do not build for this platform" from "we do, but the + // package holding the binary is not installed". They read identically in a + // stack trace and have completely different fixes, and the second is the + // common one: the platform packages are optionalDependencies, so anything + // that declines to install them — `--no-optional`, an install-time version + // gate such as a release-age policy, a partial lockfile — leaves this + // package importable and non-functional, with no error until first use. + const reason = + match === undefined + ? `This platform is not among the build targets (${supported}).` + : `This platform IS a build target, so ${match[3]} exists but was not ` + + `installed. It is an optionalDependency: check that optional ` + + `dependencies are enabled, that ${match[3]} is not excluded by an ` + + `install policy, and that it appears in your lockfile.`; + throw new Error( - `Unable to load native stdnum binding for ${[platform, arch, libc].filter(Boolean).join("-")}; ` + - `supported targets: ${supported}.\n${errors.join("\n")}`, + `Unable to load the native stdnum binding for ${current}. ${reason}\n` + + `Tried:\n${errors.map((line) => ` ${line}`).join("\n")}`, ); }; From 046b7d04710c4527c3288db18ba34e7c9c06c1a0 Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Sun, 26 Jul 2026 23:28:39 +0200 Subject: [PATCH 2/2] chore: add changeset and apply repo formatting --- .changeset/loud-pandas-repeat.md | 9 ++++++ .../stdnum/src/__test__/native-node.test.ts | 30 ++++++++++++++----- 2 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 .changeset/loud-pandas-repeat.md diff --git a/.changeset/loud-pandas-repeat.md b/.changeset/loud-pandas-repeat.md new file mode 100644 index 0000000..28dea61 --- /dev/null +++ b/.changeset/loud-pandas-repeat.md @@ -0,0 +1,9 @@ +--- +"@stll/stdnum": patch +--- + +Say which package is missing when the native binding cannot load. The platform +binaries are optional dependencies, so an installer that declines them leaves +the package importable and dead, throwing only on first use. The previous error +listed every supported target, including the one it was running on, which reads +as a portability problem rather than a missing install. diff --git a/packages/stdnum/src/__test__/native-node.test.ts b/packages/stdnum/src/__test__/native-node.test.ts index 0989da2..146a0fd 100644 --- a/packages/stdnum/src/__test__/native-node.test.ts +++ b/packages/stdnum/src/__test__/native-node.test.ts @@ -43,14 +43,19 @@ describe("loadNativeStdnumBinding", () => { requireModule: requireNothing, }); } catch (error) { - message = error instanceof Error ? error.message : String(error); + message = + error instanceof Error + ? error.message + : String(error); } expect(message).toContain("IS a build target"); expect(message).toContain("optionalDependency"); // The old message listed every supported target and nothing else, which // read as "your platform is unsupported" for a platform that is. - expect(message).not.toMatch(/^Unable to load native stdnum binding.*supported targets/u); + expect(message).not.toMatch( + /^Unable to load native stdnum binding.*supported targets/u, + ); }); test("reports an unsupported platform as unsupported", () => { @@ -64,10 +69,15 @@ describe("loadNativeStdnumBinding", () => { requireModule: requireNothing, }); } catch (error) { - message = error instanceof Error ? error.message : String(error); + message = + error instanceof Error + ? error.message + : String(error); } - expect(message).toContain("not among the build targets"); + expect(message).toContain( + "not among the build targets", + ); expect(message).toContain("linux-x64-musl"); }); @@ -82,10 +92,16 @@ describe("loadNativeStdnumBinding", () => { platform: "linux", arch: "x64", libc: "gnu", - env: { STELLA_STDNUM_NATIVE_LIBRARY_PATH: "/tmp/custom.node" }, + env: { + STELLA_STDNUM_NATIVE_LIBRARY_PATH: + "/tmp/custom.node", + }, requireModule: (specifier) => { - if (specifier === "/tmp/custom.node") return binding; - throw new Error(`Cannot find module '${specifier}'`); + if (specifier === "/tmp/custom.node") + return binding; + throw new Error( + `Cannot find module '${specifier}'`, + ); }, });