# `ModuleResolver` _(no submodules)_ ## Purpose `ModuleResolver` is the API plugin host's abstraction over `import()`. It imports a plugin entry under a cache-busted ESM specifier, returns an opaque `ModuleHandle`, and releases the host's strong reference when `dispose` is called so the previous module becomes eligible for GC. The contract lives here in `@suaveplan/api-loader`; concrete implementations (Bun-native ESM, ZIP-backed plugins, sandboxed worker proxy) land in `@suaveplan/api-host` Stage 2 and conform to the `runLoaderConformance` harness exported from `@suaveplan/api-loader/testing`. Design intent: design doc §8.1 and §8.2. The contract is types-only. No runtime ships from this module beyond the type re-export shim — the type definitions exist so the lifecycle code in `api-host` and the spec for §8 have a single home that downstream tests can `expectTypeOf` against. ## Features - `resolve({ specifier, version })` — imports the plugin entry under `bustSpecifier(specifier, version)` and returns a `ModuleHandle`. - `dispose(handle)` — releases the host's strong ref to the handle so the previous module can be collected. - `walkNativeBindings(specifier)` — returns paths of `.node` files reachable from the entry's import subgraph; the host uses this at install to gate hot-reload eligibility. - Generic over the resolved module type `M` so consumers can pin a strict plugin-entry shape (e.g. `{ default: PluginFactory }`). ## Usage: Basic ```ts import type { ModuleResolver } from "@suaveplan/api-loader/resolver"; declare const resolver: ModuleResolver<{ default: () => void }>; const handle = await resolver.resolve({ specifier: "file:///opt/plugin/index.js", version: 1, }); handle.module.default(); await resolver.dispose(handle); ``` ## Usage: Advanced Combining `walkNativeBindings` with `assertNoNativeBindings` at install to gate hot-reload eligibility: ```ts import type { ModuleResolver } from "@suaveplan/api-loader/resolver"; import { assertNoNativeBindings } from "@suaveplan/api-loader/native-binding-detector"; declare const resolver: ModuleResolver; const bindings = await resolver.walkNativeBindings( "file:///opt/plugin/index.js" ); assertNoNativeBindings(bindings); // throws LOADER.NATIVE_BINDING_HELD if non-empty ``` ## API Reference ```ts export interface ResolveOptions { readonly specifier: string; readonly version: number; } export interface ModuleHandle { readonly specifier: string; readonly version: number; readonly module: M; readonly resolvedAt: string; } export interface ModuleResolver { resolve(options: ResolveOptions): Promise>; dispose(handle: ModuleHandle): Promise; walkNativeBindings(specifier: string): Promise; } export type ModuleResolverFactory = () => ModuleResolver; ``` ## Implementation Notes `resolve` returns the *evaluated namespace*, not a thenable wrapping it — the host's lifecycle FSM treats a successful return as a synchronous "module loaded" signal even though the call itself is async. Concrete implementations are free to memoise nothing (the cache-bust strategy in §8.2 makes memoisation incorrect) and to allocate a fresh `ModuleHandle` on every call. `dispose` is `Promise` rather than `void` because the in-host implementation may need to await a `gc()` round-trip in test environments; the contract surface does not assume synchronous release. `walkNativeBindings` is `async` because the canonical implementation reads the dependency graph from a manifest or via a static traversal of `import` statements — both can involve disk I/O. The return type is `readonly string[]` (paths of `.node` files) rather than a single boolean so the operator-facing error envelope can show the exact bindings that blocked the reload.