# `NativeBindingDetector` + `assertNoNativeBindings` _(no submodules)_ ## Purpose Hot reload is the heart of the API plugin system, but it has one non-negotiable boundary: native bindings (`.node` files compiled to a specific Node-API version) cannot be reloaded. Bun's ESM cache-bust trick re-imports a fresh JS module under a new specifier, but the underlying native dynamic library is loaded into the process's address space exactly once — re-running `dlopen` would either be ignored or, worse, corrupt the existing handle. The loader has to refuse the reload before it tries. This module owns two pieces: the `NativeBindingDetector` function type (host-injected — `@suaveplan/api-host` ships a Bun-native implementation that walks the import subgraph), and the `assertNoNativeBindings` helper that throws the canonical `LOADER.NATIVE_BINDING_HELD` error when the detector reports a non-empty result. Per design doc §8.2: "Native bindings (`.node` files) cannot be reloaded; loader walks the dep graph at install time and refuses reload if any are present." ## Features - Host-injected `NativeBindingDetector` function type — runtime-portable across Bun, Node, and a future Deno port. - `assertNoNativeBindings(bindings)` synchronous helper — throws `LOADER.NATIVE_BINDING_HELD` when the array is non-empty, no-ops otherwise. - `LoaderError` subclass widening the protected `SuaveplanError` constructor to public for the `LOADER.*` namespace. - Defensive copy on the offending paths: mutating the input after the throw cannot corrupt the error's `context.bindings`. ## Usage: Basic ```ts import { type NativeBindingDetector, assertNoNativeBindings, } from "@suaveplan/api-loader/native-binding-detector"; declare const detector: NativeBindingDetector; const bindings = await detector("file:///opt/plugin/index.js"); assertNoNativeBindings(bindings); // safe pass-through when bindings.length === 0 ``` ## Usage: Advanced Combine with the resolver to wrap the install-time reload-eligibility check: ```ts import { assertNoNativeBindings } from "@suaveplan/api-loader/native-binding-detector"; import type { ModuleResolver } from "@suaveplan/api-loader/resolver"; async function assertReloadable( resolver: ModuleResolver, specifier: string ): Promise { const bindings = await resolver.walkNativeBindings(specifier); assertNoNativeBindings(bindings); } ``` ## API Reference ```ts export type NativeBindingDetector = ( specifier: string ) => Promise; export class LoaderError extends SuaveplanError { constructor( code: LoaderCode, messageKey: string, options?: SuaveplanErrorOptions ); } export function assertNoNativeBindings(bindings: readonly string[]): void; ``` ## Implementation Notes The thrown `LoaderError` attaches the offending paths via `context.bindings`, copying the input array (`[...bindings]`) so a caller mutating their array after the throw cannot retroactively change the error envelope. The copied form is `string[]` (mutable) but the type annotation on the input is `readonly string[]` — callers are encouraged to pass an immutable array, but the helper is defensive regardless. The `messageKey` (`"loader.native_binding_held"`) is the i18n catalogue key used by `SuaveplanError`'s resolver; when the host's i18n catalogue is unwired, the resolver falls back to returning the key verbatim, which is adequate for the operator-facing logs the §8.7 failure-mode table cites. `assertNoNativeBindings` is intentionally synchronous: it operates on the already-resolved output of `walkNativeBindings`, so adding an `await` would buy nothing. Callers usually wrap it in `async` code anyway because the detector itself is async.