first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
# `ModuleResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<M>`.
- `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<M = unknown> {
readonly specifier: string;
readonly version: number;
readonly module: M;
readonly resolvedAt: string;
}
export interface ModuleResolver<M = unknown> {
resolve(options: ResolveOptions): Promise<ModuleHandle<M>>;
dispose(handle: ModuleHandle<M>): Promise<void>;
walkNativeBindings(specifier: string): Promise<readonly string[]>;
}
export type ModuleResolverFactory<M = unknown> = () => ModuleResolver<M>;
```
## 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<void>` 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.