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,110 @@
# service-registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `service-registry` module defines the single shared interface that
plugins augment to publish typed services to other plugins. Plugin A
registers a `db` adapter; plugin B retrieves it with `ctx.getService("db")`
and receives the augmented type — no cast, no `unknown`, no runtime
shape-check. The interface ships **empty** so a consumer who has not
augmented it cannot accidentally type-pun a service slot.
This is the canonical solution to upstream's `getService<T>(name)` cast
hazard: there, `T` was a free type parameter, so a typo in the name or a
mismatch between producer and consumer compiled cleanly and crashed at
runtime. With module augmentation, the interface itself is the contract;
both ends agree on every key.
## Features
- `ServiceRegistry` — empty by default, augmented via `declare module`.
- `ServiceKey = keyof ServiceRegistry` — `never` until augmented.
- Zero runtime — types-only export.
- Subpath export `@suaveplan/api-plugin/service-registry` for direct
augmentation imports.
## Usage: Basic
Declare your plugin's service slot once, anywhere in the consumer code
that runs before the registry is created:
```ts
import type { DbAdapter } from "./db-adapter.js";
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry {
db: DbAdapter;
}
}
```
Then registration and lookup are both type-safe:
```ts
import { definePlugin } from "@suaveplan/api-plugin";
export const dbPlugin = definePlugin({
name: "db",
version: "0.1.0",
onInit(ctx) {
ctx.registerService("db", createDbAdapter());
},
});
export const consumerPlugin = definePlugin({
name: "consumer",
version: "0.1.0",
dependencies: ["db"],
onReady(ctx) {
const db = ctx.getService("db"); // DbAdapter | undefined
db?.query("SELECT 1");
},
});
```
## Usage: Advanced
Multiple consumers may augment independently — TypeScript merges the
interface across declarations:
```ts
// package-a/services.d.ts
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { cache: CacheAdapter; }
}
// package-b/services.d.ts
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { auth: AuthAdapter; }
}
// Anywhere: both keys are now type-safe.
ctx.getService("cache"); // CacheAdapter | undefined
ctx.getService("auth"); // AuthAdapter | undefined
ctx.getService("xyz"); // type error — not augmented
```
## API Reference
| Export | Kind | Notes |
|---|---|---|
| `ServiceRegistry` | interface | Empty extension surface — augment via `declare module`. |
| `ServiceKey` | type | `keyof ServiceRegistry`; resolves to `never` without augmentation. |
## Implementation Notes
Biome's `noEmptyInterface` rule would flag a literally-empty interface
declaration. The module sidesteps the lint cleanly by extending a
`Record<never, never>` alias — semantically equivalent to `{}` for the
augmentation pattern but no longer "empty" in the syntactic sense Biome
inspects. The alias is private to the module; consumers only see the
public `ServiceRegistry` interface and the `ServiceKey` helper.
No runtime artefacts are emitted from this file — Vite/dts strip
declaration-only modules during the library build. Subpath consumers
benefit from a sub-1KB import that is entirely tree-shaken at consumer
build time.