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,81 @@
# @suaveplan/api-openapi-ui-swagger — Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system. Exposes createSwaggerUiHandler / createPerPluginSwaggerUiHandler — pure HTTP-handler factories returning a text/html shell pointing at the host's OpenAPI document and bootstrapping the real swagger-ui-dist runtime (CDN or bundled). Zod-validated SwaggerUiOptions (tryItOutEnabled, deepLinking, displayRequestDuration, filter, theme), stable OPENAPI_UI_SWAGGER.* error-code registry (ASSETS_MISSING, INVALID_OPTIONS), and sibling-shaped to api-openapi-ui-scalar so api-host can pick a UI uniformly.
Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/html` Swagger UI shell pointing at the host's OpenAPI document and bootstrapping the upstream `swagger-ui-dist` runtime — from the documented CDN by default, or from a host-bundled asset pipeline when operators opt in.
<!-- begin:auto-docs -->
## Documentation tree
- [assets](./src/assets/assets.md) — This module owns the asset-resolution policy for the Swagger UI shell. The shell needs two payloads at boot — the upstr…
- [assets](./src/assets/assets.md) — This module owns the asset-resolution policy for the Swagger UI shell. The shell needs two payloads at boot — the upstr…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/api-openapi-ui-swagger`. Every public failure path rais…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/api-openapi-ui-swagger`. Every public failure path rais…
- [handler](./src/handler/handler.md) — This module exposes the primary entry point of `@suaveplan/api-openapi-ui-swagger`: a factory that returns an HTTP hand…
- [handler](./src/handler/handler.md) — This module exposes the primary entry point of `@suaveplan/api-openapi-ui-swagger`: a factory that returns an HTTP hand…
- [html](./src/html/render-shell.md) — This module produces the deterministic HTML payload returned by `createSwaggerUiHandler`. It accepts the OpenAPI docume…
- [render-shell](./src/html/render-shell.md) — This module produces the deterministic HTML payload returned by `createSwaggerUiHandler`. It accepts the OpenAPI docume…
- [options](./src/options/options.md) — This module owns the operator-facing options surface for `@suaveplan/api-openapi-ui-swagger`. It defines `swaggerUiOpti…
- [options](./src/options/options.md) — This module owns the operator-facing options surface for `@suaveplan/api-openapi-ui-swagger`. It defines `swaggerUiOpti…
- [per-plugin](./src/per-plugin/per-plugin.md) — This module exposes the per-plugin variant of the Swagger UI handler. The host's `/docs/{plugin}` route serves a UI sco…
- [per-plugin](./src/per-plugin/per-plugin.md) — This module exposes the per-plugin variant of the Swagger UI handler. The host's `/docs/{plugin}` route serves a UI sco…
<!-- end:auto-docs -->
## Why
`/docs/ui/swagger` is one of the pluggable UI choices for The OpenAPI surface. Operators familiar with Swagger's interactive try-it-out shell keep that surface by selecting this adapter in `host.openapi.ui = "swagger"`. The package matches the shape of the sibling Scalar adapter so the host's selection logic remains uniform across UI choices.
## Modules
- `handler/` — `createSwaggerUiHandler` and `createSwaggerUiHandlerArtifacts`. Eager-validating factory that returns a pure HTTP handler.
- `per-plugin/` — `createPerPluginSwaggerUiHandler` for the `/docs/{plugin}` filtered view.
- `options/` — Zod-validated `SwaggerUiOptions` (`title`, `tryItOutEnabled`, `deepLinking`, `displayRequestDuration`, `filter`, `theme`, `uiAssets`, `version`).
- `assets/` — CDN vs bundled asset resolution; embedded asset registry; ASSETS_MISSING strict-mode guarantee.
- `html/` — deterministic HTML shell renderer with break-out-safe JSON config island.
- `error-codes/` — `OPENAPI_UI_SWAGGER.*` registry (`ASSETS_MISSING`, `INVALID_OPTIONS`).
## Quick start
```ts
import { createSwaggerUiHandler } from "@suaveplan/api-openapi-ui-swagger";
const handler = createSwaggerUiHandler({
openapiUrl: "/openapi.json",
title: "Acme — Customer API",
tryItOutEnabled: true,
});
// Hono / worker bridge / in-memory router — call handler() and
// translate the SwaggerUiResponse into the adapter's native Response.
const res = handler();
console.log(res.headers["Content-Type"]); // text/html; charset=utf-8
```
## Bundled assets
```ts
import {
createSwaggerUiHandler,
listBundledAssetNames,
readBundledAsset,
} from "@suaveplan/api-openapi-ui-swagger";
const handler = createSwaggerUiHandler({
openapiUrl: "/openapi.json",
uiAssets: "bundled",
assetBasePath: "/docs/ui/swagger/assets",
});
// Host mounts an asset route — serves the bundled bytes by name.
for (const name of listBundledAssetNames()) {
const bytes = readBundledAsset(name);
// host.serve(`/docs/ui/swagger/assets/${name}`, bytes)
void bytes;
}
```
## Error codes
- `OPENAPI_UI_SWAGGER.INVALID_OPTIONS` — caller-supplied options failed Zod validation.
- `OPENAPI_UI_SWAGGER.ASSETS_MISSING` — bundled mode requested without `assetBasePath`, or a bundled asset name is not registered.
Both surfaces raise at factory-call time (eager validation), so misconfiguration is caught at boot rather than at first request.

View File

@@ -0,0 +1,72 @@
# `assets.ts` — Swagger UI asset resolution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the asset-resolution policy for the Swagger UI shell. The shell needs two payloads at boot — the upstream `swagger-ui.css` stylesheet and the `swagger-ui-bundle.js` runtime — and operators get to choose how the host serves them: from the documented `swagger-ui-dist` CDN or from a bundled drop the host ships under its own assets pipeline.
Centralising the dispatch here keeps the rendering side of the package pure: the HTML shell takes a `ResolvedAssets` triple and never branches on the mode. That separation is what makes the package portable across `api-host` (which exposes its own asset routes) and standalone-tier deployments that prefer the CDN to keep the wire payload tiny.
## Features
- `resolveCdnAssets(version)` — produces `https://cdn.jsdelivr.net/npm/swagger-ui-dist@<version>/{swagger-ui.css,swagger-ui-bundle.js}`.
- `resolveBundledAssets(basePath)` — anchors the embedded asset URLs under the host-supplied prefix.
- `readBundledAsset(name)` — exposes the literal bytes of a bundled asset; the host serves these from its asset route.
- `listBundledAssetNames()` — registry introspection.
- `resolveAssets({mode, version, basePath?})` — dispatch helper consumed by the handler.
- `OPENAPI_UI_SWAGGER.ASSETS_MISSING` raised whenever a bundled name is unknown or `basePath` is omitted in bundled mode.
## Usage: Basic
```ts
import { resolveCdnAssets } from "@suaveplan/api-openapi-ui-swagger/assets";
const assets = resolveCdnAssets("5.17.14");
// assets.cssHref === "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14/swagger-ui.css"
```
## Usage: Advanced
```ts
import {
listBundledAssetNames,
readBundledAsset,
resolveAssets,
} from "@suaveplan/api-openapi-ui-swagger/assets";
const assetRoute = "/docs/ui/swagger/assets";
const resolved = resolveAssets({
mode: "bundled",
version: "5.17.14",
basePath: assetRoute,
});
// Host mounts an asset route that serves the bundled bytes from this map.
for (const name of listBundledAssetNames()) {
const bytes = readBundledAsset(name);
// host.serve(`${assetRoute}/${name}`, bytes)
void bytes;
}
void resolved;
```
## API Reference
- `interface ResolvedAssets { cssHref; jsHref; mode: "cdn"|"bundled" }`.
- `resolveCdnAssets(version: string): ResolvedAssets`.
- `resolveBundledAssets(basePath: string): ResolvedAssets`.
- `readBundledAsset(name: string): string` — throws `OpenapiUiSwaggerError` if `name` is not registered.
- `listBundledAssetNames(): readonly string[]`.
- `resolveAssets({mode, version, basePath?}): ResolvedAssets` — `mode = "bundled"` without `basePath` throws ASSETS_MISSING.
## Implementation Notes
The bundled asset registry is closed by design — adding a new asset is a package release, not a runtime extension point. That guarantees the host never serves an asset the package can't account for in spec deltas.
The "strict-mode" guarantee from the spec is implemented inline: `resolveAssets` raises ASSETS_MISSING whenever bundled mode is requested without a `basePath`. The package never falls back silently to the CDN — operators who want CDN behaviour set `mode: "cdn"` explicitly.
CDN base is hard-coded to `https://cdn.jsdelivr.net/npm/swagger-ui-dist` matching the upstream `@hono/swagger-ui` adapter. Changing the CDN base requires a package release; pinning to a different CDN is out of scope (consumers proxying through their own CDN can serve via bundled mode).

View File

@@ -0,0 +1,63 @@
# `error-codes.ts` — `OPENAPI_UI_SWAGGER.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the stable error-code registry for `@suaveplan/api-openapi-ui-swagger`. Every public failure path raises an `OpenapiUiSwaggerError` carrying one of the two namespaced codes, so consumers can discriminate without parsing message strings. The codes survive across releases and form part of the public contract; renaming one is a breaking change.
The registry is built via `defineCodes("OPENAPI_UI_SWAGGER", [...])` from `@suaveplan/error`. The resulting object freezes the code map at module load, so attempts to mutate it from outside throw immediately. The companion `OpenapiUiSwaggerError` widens the `SuaveplanError` constructor to `public` (no factory required) and re-exposes the parsed code on a `swaggerUiCode` accessor for narrowing in `instanceof` branches.
## Features
- Two-entry registry: `ASSETS_MISSING`, `INVALID_OPTIONS`.
- `OpenapiUiSwaggerCode` union literal type derived from the registry.
- `OpenapiUiSwaggerError` extending `SuaveplanError<OpenapiUiSwaggerCode>`.
- Side-effect import from `src/index.ts` so consumers picking up the package barrel get the registry initialised.
## Usage: Basic
```ts
import {
OPENAPI_UI_SWAGGER_CODES,
OpenapiUiSwaggerError,
} from "@suaveplan/api-openapi-ui-swagger/error-codes";
throw new OpenapiUiSwaggerError(
OPENAPI_UI_SWAGGER_CODES.INVALID_OPTIONS,
"theme must be one of 'light' | 'dark' | 'system'"
);
```
## Usage: Advanced
```ts
import { OpenapiUiSwaggerError } from "@suaveplan/api-openapi-ui-swagger/error-codes";
try {
bootSwaggerUi(configFromOperator);
} catch (err) {
if (
err instanceof OpenapiUiSwaggerError &&
err.swaggerUiCode === "ASSETS_MISSING"
) {
// Fall back to a remote-CDN profile and report the gap.
return bootSwaggerUi({ ...configFromOperator, uiAssets: "cdn" });
}
throw err;
}
```
## API Reference
- `OPENAPI_UI_SWAGGER_CODES: Readonly<Record<"ASSETS_MISSING"|"INVALID_OPTIONS", string>>`.
- `type OpenapiUiSwaggerCode = "OPENAPI_UI_SWAGGER.ASSETS_MISSING" | "OPENAPI_UI_SWAGGER.INVALID_OPTIONS"`.
- `class OpenapiUiSwaggerError extends SuaveplanError<OpenapiUiSwaggerCode>` — public constructor `(code, message, options?)`.
## Implementation Notes
Adding a new code is a public-API change: bump the package version and add a `**Verified by:**` entry in the spec for the throw site that emits it. Removing a code is a breaking change.
The registry is intentionally tiny — Swagger UI is a presentation surface and most failures are configuration mistakes (caught by `INVALID_OPTIONS`) or asset-pipeline gaps (caught by `ASSETS_MISSING`). The conformance gate (`conformance.ts`) walks the registry and asserts each code is thrown from at least one non-test source file in this package.

View File

@@ -0,0 +1,67 @@
# `handler.ts` — `createSwaggerUiHandler` factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes the primary entry point of `@suaveplan/api-openapi-ui-swagger`: a factory that returns an HTTP handler serving the Swagger UI HTML shell. The factory composes three earlier modules — options validation, asset resolution, and HTML rendering — into a single call site that the host's router can mount under `/docs/ui/swagger`.
Construction is eager: validation, asset resolution, and shell rendering all happen at factory-call time. The returned handler is therefore an O(1) lookup that returns a pre-baked response. That puts all the failure modes (`INVALID_OPTIONS`, `ASSETS_MISSING`) at boot, where operators see them, never at first request.
## Features
- `createSwaggerUiHandler({ openapiUrl, ...options })` — returns a `SwaggerUiHandler`.
- `createSwaggerUiHandlerArtifacts(...)` — handler plus parsed options for hosts that want introspection.
- Strict response headers: `Content-Type: text/html; charset=utf-8`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`.
- Pure handler — identical bytes returned on every call (no per-request work).
- Eager validation at boot time.
## Usage: Basic
```ts
import { createSwaggerUiHandler } from "@suaveplan/api-openapi-ui-swagger/handler";
const handler = createSwaggerUiHandler({ openapiUrl: "/openapi.json" });
const response = handler();
// response.status === 200; response.body is the rendered HTML
```
## Usage: Advanced
```ts
import { createSwaggerUiHandlerArtifacts } from "@suaveplan/api-openapi-ui-swagger/handler";
const { handler, options } = createSwaggerUiHandlerArtifacts({
openapiUrl: "/openapi.json",
title: "Acme — Customer API",
theme: "dark",
tryItOutEnabled: false,
uiAssets: "bundled",
assetBasePath: "/docs/ui/swagger/assets",
});
// `options` is the parsed, defaults-applied record; host can log it.
console.log("[swagger-ui] mounted", options.title, options.uiAssets);
// `handler` is a pure function — call it from every route adapter.
const res = handler();
void res;
```
## API Reference
- `interface CreateSwaggerUiHandlerInput extends SwaggerUiOptionsInput { openapiUrl: string; assetBasePath?: string }`.
- `interface SwaggerUiResponse { status; headers; body }`.
- `type SwaggerUiHandler = () => SwaggerUiResponse`.
- `createSwaggerUiHandler(input): SwaggerUiHandler`.
- `createSwaggerUiHandlerArtifacts(input): { handler; options; openapiUrl }`.
## Implementation Notes
The factory is eager by design. The alternative — lazy validation per request — would let operators ship misconfigured handlers that look healthy until a doc-fetch arrives. The package picks reliability over latency: a single render per handler at boot.
`assetBasePath` is required when `uiAssets = "bundled"`; omitting it raises `OPENAPI_UI_SWAGGER.ASSETS_MISSING` via `resolveAssets`. The factory does not infer a base path because the host owns the asset-route mount point — guessing would create cross-host inconsistency.
The returned `SwaggerUiResponse` is an adapter-agnostic record. Hosts running on Hono call `c.html(body, status, headers)`; hosts on the in-memory router pass the record straight back; hosts on Node's `http` write `(status, headers, body)` directly. Avoiding `Response` keeps the package universal.

View File

@@ -0,0 +1,78 @@
# `render-shell.ts` — Swagger UI HTML shell renderer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module produces the deterministic HTML payload returned by `createSwaggerUiHandler`. It accepts the OpenAPI document URL, the parsed Swagger UI options, and a resolved asset descriptor, then renders the complete page — including a JSON configuration block consumed by the inline bootstrap script.
The shell is intentionally small. It loads `swagger-ui.css` and `swagger-ui-bundle.js` from the resolved hrefs (CDN or bundled), defines a single `<div id="swagger-ui">` mount point, and ships a bootstrap snippet that reads the JSON config block and calls `SwaggerUIBundle({...})`. Every caller-supplied string is HTML-escaped before reaching the page, and configuration values never touch raw JS source text — they ride a `<script type="application/json">` block that the bootstrap parses at runtime.
## Features
- Deterministic output — identical inputs always yield identical bytes.
- HTML escapes the document title, theme attribute, and both asset URLs.
- Embeds caller config via `<script type="application/json">` with `</script>` break-out protection (`<` → `<`).
- Wires `deepLinking`, `tryItOutEnabled`, `displayRequestDuration`, and `filter` straight into `SwaggerUIBundle`.
- Defensive bootstrap: bails silently if the bundle has not finished loading.
## Usage: Basic
```ts
import { renderShell } from "@suaveplan/api-openapi-ui-swagger/html";
import { parseSwaggerUiOptions } from "@suaveplan/api-openapi-ui-swagger/options";
import { resolveCdnAssets } from "@suaveplan/api-openapi-ui-swagger/assets";
const options = parseSwaggerUiOptions({});
const assets = resolveCdnAssets(options.version);
const html = renderShell({
openapiUrl: "/openapi.json",
options,
assets,
});
```
## Usage: Advanced
```ts
import {
buildSwaggerConfig,
escapeHtml,
renderShell,
} from "@suaveplan/api-openapi-ui-swagger/html";
import { resolveBundledAssets } from "@suaveplan/api-openapi-ui-swagger/assets";
import { parseSwaggerUiOptions } from "@suaveplan/api-openapi-ui-swagger/options";
const options = parseSwaggerUiOptions({
title: "Acme — Customer API",
theme: "dark",
tryItOutEnabled: false,
filter: true,
});
const assets = resolveBundledAssets("/docs/ui/swagger/assets");
const html = renderShell({
openapiUrl: "/openapi.json",
options,
assets,
});
// `buildSwaggerConfig` is exposed for callers that want to introspect
// the JSON payload before it reaches the shell — useful in tests.
const cfg = buildSwaggerConfig("/openapi.json", options);
console.log(escapeHtml(cfg.url));
```
## API Reference
- `escapeHtml(input: string): string` — five-character entity escape.
- `buildSwaggerConfig(openapiUrl, options): SwaggerUiConfig` — JSON payload threaded into the page.
- `renderShell({openapiUrl, options, assets}): string` — full HTML.
- `interface SwaggerUiConfig { url; dom_id; deepLinking; tryItOutEnabled; displayRequestDuration; filter; theme }`.
## Implementation Notes
The renderer is a pure function — it has no IO and no clock dependency. That makes it trivial to unit-test (input determinism is the test) and safe to call from a hot HTTP path (no per-request allocations beyond the returned string).
The JSON payload uses the unicode-escape trick (`<` → `<`) to prevent any caller-supplied string from terminating the `</script>` block. Without that protection a configurable URL or title containing `</script>` could break out of the JSON island and inject inline scripts. The escape applies to every `<` character; JSON's own syntax never produces a literal `<` so the substitution is value-preserving.

View File

@@ -0,0 +1,69 @@
# `options.ts` — `SwaggerUiOptions` Zod schema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the operator-facing options surface for `@suaveplan/api-openapi-ui-swagger`. It defines `swaggerUiOptionsSchema` (a strict Zod object with defaults baked in) and `parseSwaggerUiOptions` which converts caller input into a fully resolved `SwaggerUiOptions` record or raises `OPENAPI_UI_SWAGGER.INVALID_OPTIONS` if the input is structurally invalid.
Validating at the configuration boundary is non-negotiable: Swagger UI accepts dozens of toggles, every one of which is rendered into the page's bootstrap JavaScript. Letting unchecked input flow into the shell would risk both broken pages (silent type coercions) and HTML-injection vectors. By forcing every field through the schema before render, the package guarantees that the values reaching the HTML emitter are already typed and validated.
## Features
- `title`, `tryItOutEnabled`, `deepLinking`, `displayRequestDuration`, `filter`, `theme` — the user-visible toggles.
- `uiAssets` — `"cdn"` or `"bundled"` chooses asset resolution strategy.
- `version` — pins the `swagger-ui-dist` version embedded in CDN URLs (default `5.17.14`).
- `.strict()` — unknown keys raise INVALID_OPTIONS (typo guard).
- Theme literal accepts only `light` / `dark` / `system`.
- Defaults: `tryItOutEnabled: true`, `deepLinking: true`, `displayRequestDuration: false`, `filter: false`, `theme: "system"`, `uiAssets: "cdn"`.
## Usage: Basic
```ts
import { parseSwaggerUiOptions } from "@suaveplan/api-openapi-ui-swagger/options";
const options = parseSwaggerUiOptions({ tryItOutEnabled: false });
// options.theme === "system" (default), tryItOutEnabled === false
```
## Usage: Advanced
```ts
import {
parseSwaggerUiOptions,
type SwaggerUiOptionsInput,
} from "@suaveplan/api-openapi-ui-swagger/options";
import { OpenapiUiSwaggerError } from "@suaveplan/api-openapi-ui-swagger/error-codes";
function bootSafe(input: SwaggerUiOptionsInput): void {
try {
const opts = parseSwaggerUiOptions(input);
// proceed with opts...
void opts;
} catch (err) {
if (err instanceof OpenapiUiSwaggerError) {
// Operator-facing log; do not bubble Zod internals to consumers.
console.error("[swagger-ui] invalid options", err.context);
throw err;
}
throw err;
}
}
bootSafe({ theme: "dark", filter: true, uiAssets: "bundled" });
```
## API Reference
- `swaggerUiOptionsSchema: z.ZodObject` — strict, with defaults.
- `parseSwaggerUiOptions(input?: SwaggerUiOptionsInput): SwaggerUiOptions` — throws `OpenapiUiSwaggerError` on failure.
- `type SwaggerUiOptions = z.infer<typeof swaggerUiOptionsSchema>` — resolved output.
- `type SwaggerUiOptionsInput = z.input<typeof swaggerUiOptionsSchema>` — caller input.
## Implementation Notes
The schema uses `z.union([z.literal(...), ...])` for closed enumerations rather than `z.enum` so the literal types survive Zod's transform pipeline. `parseSwaggerUiOptions` uses `safeParse` so the failure path can wrap the Zod issue list inside the `OpenapiUiSwaggerError.context.issues` slot — that preserves the original `cause` chain for downstream tooling that walks `error.cause`.
Adding a new toggle: extend the schema with a default, add a sentence to this doc's Features list, add scenario coverage in the spec, and add a `**Verified by:**` line pointing at the test that asserts the toggle reaches the rendered HTML.

View File

@@ -0,0 +1,63 @@
# `per-plugin.ts` — `createPerPluginSwaggerUiHandler`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes the per-plugin variant of the Swagger UI handler. The host's `/docs/{plugin}` route serves a UI scoped to a single plugin's routes, and that view points at a plugin-filtered OpenAPI document (typically `?plugin=<id>` or `/openapi/<id>.json`). The factory takes a derivation callback (`openapiUrlForPlugin`) so the host owns the URL-shape convention.
The factory is a thin composition over `createSwaggerUiHandler`: it derives the document URL by calling `openapiUrlForPlugin(plugin)`, then forwards every other option unchanged. All validation, asset resolution, and HTML rendering happen inside the underlying handler factory — this module never duplicates those concerns.
## Features
- `createPerPluginSwaggerUiHandler({ openapiUrlForPlugin, plugin, ...options })` — returns a `SwaggerUiHandler` pointing at the filtered URL.
- Forwards every `SwaggerUiOptionsInput` field to the underlying handler.
- Same eager validation contract — bad options throw at construction.
- Same asset-resolution contract — bundled mode without basePath throws ASSETS_MISSING.
## Usage: Basic
```ts
import { createPerPluginSwaggerUiHandler } from "@suaveplan/api-openapi-ui-swagger/per-plugin";
const handler = createPerPluginSwaggerUiHandler({
openapiUrlForPlugin: (p) => `/openapi.json?plugin=${encodeURIComponent(p)}`,
plugin: "billing",
});
const res = handler();
// res.body references /openapi.json?plugin=billing
```
## Usage: Advanced
```ts
import { createPerPluginSwaggerUiHandler } from "@suaveplan/api-openapi-ui-swagger/per-plugin";
const handlers = new Map(
["billing", "auth", "contacts"].map((plugin) => [
plugin,
createPerPluginSwaggerUiHandler({
openapiUrlForPlugin: (p) => `/openapi/${p}.json`,
plugin,
title: `${plugin} — API`,
theme: "dark",
tryItOutEnabled: false,
}),
])
);
// host.router mounts handlers.get(plugin) at /docs/<plugin>
```
## API Reference
- `interface CreatePerPluginSwaggerUiHandlerInput extends SwaggerUiOptionsInput { openapiUrlForPlugin: (plugin: string) => string; plugin: string; assetBasePath?: string }`.
- `createPerPluginSwaggerUiHandler(input): SwaggerUiHandler`.
## Implementation Notes
`openapiUrlForPlugin` is a function (not a template string) because hosts wire their OpenAPI emitter to a wide range of URL shapes — query-parameter filtering, path-based filtering, host-based multi-tenancy. A callback keeps the package out of that decision.
The handler returned here is identical in shape to `createSwaggerUiHandler`'s output, so the host's route adapter does not need a per-handler branch. Operators wanting a single UI mount at `/docs/ui/swagger/{plugin}` create one handler per plugin and dispatch by path segment.