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 @@
# @suaveplan/api-graphql-ui-graphiql — GraphiQL UI mount for the Suaveplan API plugin system. Exposes createGraphiqlUi() — a factory returning a RouterPlugin that registers GET /graphql/ui (or any configured endpoint) and serves a self-contained HTML shell + bundled assets with a strict CSP. Default GraphQL explorer when operators want GraphiQL; sibling api-graphql-ui-altair covers the Altair alternative.
GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of
the umbrella plan.
`createGraphiqlUi(opts)` returns a `RouterPlugin` that registers
`GET /graphql/ui` (or any configured endpoint) on a host
`@suaveplan/api-router` `Router`. The endpoint serves a self-contained
HTML shell plus bundled JS/CSS assets — no CDN fetches.
<!-- begin:auto-docs -->
## Documentation tree
- [assets](./src/assets/assets.md) — Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell references two assets (`graphiql.js`, `graphiq…
- [assets](./src/assets/assets.md) — Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell references two assets (`graphiql.js`, `graphiq…
- [error-codes](./src/error-codes/error-codes.md) — Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the GraphiQL UI plugin. Every code in the regis…
- [error-codes](./src/error-codes/error-codes.md) — Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the GraphiQL UI plugin. Every code in the regis…
- [factory](./src/factory/create-graphiql-ui.md) — Public entry point of the package. `createGraphiqlUi(opts)` returns a `RouterPlugin` — a plain value with `name`, `vers…
- [create-graphiql-ui](./src/factory/create-graphiql-ui.md) — Public entry point of the package. `createGraphiqlUi(opts)` returns a `RouterPlugin` — a plain value with `name`, `vers…
- [headers](./src/headers/csp.md) — Owns the `Content-Security-Policy` header values emitted on the GraphiQL HTML response. The policy is hard-coded to a p…
- [csp](./src/headers/csp.md) — Owns the `Content-Security-Policy` header values emitted on the GraphiQL HTML response. The policy is hard-coded to a p…
- [html](./src/html/render-shell.md) — Renders the GraphiQL HTML shell. The output embeds the configured GraphQL endpoint, subscription endpoint, theme, and e…
- [render-shell](./src/html/render-shell.md) — Renders the GraphiQL HTML shell. The output embeds the configured GraphQL endpoint, subscription endpoint, theme, and e…
- [instruments](./src/instruments/instruments.md) — Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI plugin. A single `Counter` (`graphql.ui.requests_to…
- [instruments](./src/instruments/instruments.md) — Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI plugin. A single `Counter` (`graphql.ui.requests_to…
- [options](./src/options/options.md) — Zod-validated configuration surface for `createGraphiqlUi`. Owns `GraphiqlUiOptions` (resolved shape with every default…
- [options](./src/options/options.md) — Zod-validated configuration surface for `createGraphiqlUi`. Owns `GraphiqlUiOptions` (resolved shape with every default…
<!-- end:auto-docs -->
## Why opt-in
Per the design doc (`docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`
§10.6) the API foundation reserves `GET /graphql/ui` for an interactive
explorer but does not bundle one by default — UI choice is operator-
chosen so the runtime image stays minimal for production deployments
that don't need it.
Operators wire ONE of:
- `@suaveplan/api-graphql-ui-graphiql` — canonical GraphiQL (this
package).
- `@suaveplan/api-graphql-ui-altair` — Altair alternative.
The GraphQL engine adapters (`api-graphql-yoga`, `api-graphql-apollo`)
return 404 at `/graphql/ui` when no UI plugin is wired.
## Quick start
```ts
import { Router } from "@suaveplan/api-router";
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
const router = new Router({ plugin: "host" });
createGraphiqlUi({}).register(router);
// GET /graphql/ui -> the explorer
// GET /graphql/ui/assets/graphiql.<hash>.js -> bundled JS
// GET /graphql/ui/assets/graphiql.<hash>.css -> bundled CSS
```
## Configuration
| Option | Default | Notes |
|---|---|---|
| `uiEndpoint` | `/graphql/ui` | Must start with `/`. |
| `graphqlEndpoint` | `/graphql` | Embedded into the explorer config. |
| `subscriptionEndpoint` | `/graphql/ws` | Embedded into the explorer config. |
| `theme` | `"system"` | `"light"` / `"dark"` / `"system"`. |
| `defaultQuery` | `""` | HTML-escaped before embedding. |
| `defaultVariableEditorOpen` | `false` | Opens the variables pane on load. |
| `editorOptions` | `{}` | CodeMirror passthroughs. |
| `meterProvider` | OTel global | OTel `MeterProvider` for the counter. |
## Security posture
- **Bundled assets only.** Every `src` / `href` in the rendered HTML
points inside the package's own `/<uiEndpoint>/assets/` namespace.
Cross-checked by `src/html/render-shell.test.ts`.
- **Strict CSP.** `default-src 'self'; style-src 'self' 'unsafe-inline';
script-src 'self'; img-src 'self' data:; connect-src 'self'`.
Inline scripts are NOT permitted. Cross-checked by
`src/headers/csp.test.ts`.
- **Cache-immutable assets.** Bundled JS/CSS carries
`Cache-Control: public, max-age=31536000, immutable`.
- **Stale-hash tolerance.** Hashed filenames resolve any matching
stem+ext to the current asset bytes — rolling deploys don't
404 in-flight sessions.
## OTel
- `graphql.ui.requests_total { ui: "graphiql" }` — counter incremented
on each successful HTML response. Asset hits are NOT counted.
## Error codes
- `GRAPHQL_UI_GRAPHIQL.ASSET_MISSING` — asset lookup failure.
- `GRAPHQL_UI_GRAPHIQL.ENDPOINT_COLLISION` — `<uiEndpoint>` already
registered by another plugin.
## Tests
```bash
bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-graphql-ui-graphiql
```
100% statements / branches / functions / lines required on touched
modules (`provider: istanbul`).

View File

@@ -0,0 +1,78 @@
# `assets`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell
references two assets (`graphiql.js`, `graphiql.css`) via hashed filenames
under `<uiEndpoint>/assets/`. Hash bytes come from FNV-1a-32 of the asset
content — non-cryptographic, used purely for cache-busting on deploy.
The asset payloads are shipped inline as TypeScript template strings so
the package has zero filesystem requirements at runtime. Operators who
want a richer GraphiQL distribution (the full React app) can swap
`GRAPHIQL_ASSETS` content on a fork without touching the rest of the
plugin.
## Features
- `GRAPHIQL_ASSETS` — frozen `Map<name, asset>` exposing every shipped
static.
- `hashedAssetPath(name)` — builds `<stem>.<hash>.<ext>` for embedding in
the HTML shell.
- `resolveAsset(filename)` — server-side lookup. Tolerates stale hashes
during rolling deploys (resolves any matching stem+ext to the current
asset).
- `fnv1a32(input)` — public so callers can hash their own additions in
consistent shape.
## Usage: Basic
```ts
import { GRAPHIQL_ASSETS, resolveAsset } from "@suaveplan/api-graphql-ui-graphiql";
for (const asset of GRAPHIQL_ASSETS.values()) {
console.log(asset.name, asset.hash, asset.contentType);
}
const css = resolveAsset("graphiql.css");
```
## Usage: Advanced
```ts
import { fnv1a32, hashedAssetPath, resolveAsset } from "@suaveplan/api-graphql-ui-graphiql";
// Build the served filename for the JS asset.
const path = `/graphql/ui/assets/${hashedAssetPath("graphiql.js")}`;
// Verify integrity in a custom asset middleware.
const asset = resolveAsset("graphiql.js");
if (fnv1a32(asset.content) !== asset.hash) {
throw new Error("asset content drift");
}
```
## API Reference
- `interface GraphiqlAsset { name, content, contentType, hash }`.
- `GRAPHIQL_ASSETS: ReadonlyMap<string, GraphiqlAsset>`.
- `fnv1a32(input: string): string` — 8-char lowercase hex hash.
- `hashedAssetPath(name: string): string` — throws `ASSET_MISSING` if
`name` is not registered.
- `resolveAsset(filename: string): GraphiqlAsset` — throws
`ASSET_MISSING` on lookup failure.
## Implementation Notes
The stale-hash tolerance is intentional. Picture a CDN-cached `index.html`
served before a deploy with the new asset bundle — that `index.html`
references `graphiql.<old-hash>.js`. Returning 404 on the old hash would
break the in-flight session for ~CDN-TTL seconds. The resolver instead
serves the new bytes under the old name, traded against a slightly larger
attack surface (an adversary who knows the asset name+ext can fetch the
asset at any hash). The Cache-Control header on the response is
`immutable` for the matched name, so the browser still caches the
content correctly.

View File

@@ -0,0 +1,85 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the
GraphiQL UI plugin. Every code in the registry is thrown by a production
path in this package — the `error-codes.spec-conformance.test.ts` walks
the registry against the source tree to prove that no code is unused and
that no production throw site references an unregistered code.
The module also exports `GraphqlUiGraphiqlError`, a concrete
`SuaveplanError` subclass with a `graphiqlCode` mirror property. Callers
can discriminate failures from this package via `instanceof` or by
checking the stable `error.code` field; the mirror property exists so
TypeScript narrows to the narrow union without the consumer importing
the `GraphqlUiGraphiqlCode` alias.
## Features
- Two-code registry — `ASSET_MISSING`, `ENDPOINT_COLLISION`.
- `defineCodes("GRAPHQL_UI_GRAPHIQL", …)` namespace.
- `GraphqlUiGraphiqlError` extends `SuaveplanError` with a public
constructor (required by the repo's `check-error-subclass-constructors`
lint).
- Frozen registry — `Object.isFrozen(GRAPHQL_UI_GRAPHIQL_CODES) === true`.
## Usage: Basic
```ts
import {
GRAPHQL_UI_GRAPHIQL_CODES,
GraphqlUiGraphiqlError,
} from "@suaveplan/api-graphql-ui-graphiql";
throw new GraphqlUiGraphiqlError(
GRAPHQL_UI_GRAPHIQL_CODES.ASSET_MISSING,
"Bundled asset \"missing.js\" is not registered"
);
```
## Usage: Advanced
```ts
import {
GRAPHQL_UI_GRAPHIQL_CODES,
GraphqlUiGraphiqlError,
} from "@suaveplan/api-graphql-ui-graphiql";
try {
mountGraphiqlUi();
} catch (err) {
if (err instanceof GraphqlUiGraphiqlError) {
if (err.code === GRAPHQL_UI_GRAPHIQL_CODES.ENDPOINT_COLLISION) {
// Operator-facing: log the existing plugin from err.context.
log.error({
msg: "graphiql collision",
existing: err.context?.existing,
});
}
}
throw err;
}
```
## API Reference
- `GRAPHQL_UI_GRAPHIQL_CODES` — frozen `Record<string, string>` of fully
qualified codes.
- `GraphqlUiGraphiqlCode` — union of every value resolved through the
registry.
- `class GraphqlUiGraphiqlError extends SuaveplanError<GraphqlUiGraphiqlCode>`
— public constructor `(code, message, options?)` mirrors
`SuaveplanError`'s signature.
## Implementation Notes
The class name (`GraphqlUiGraphiqlError`) is intentionally explicit so
stack traces and logs distinguish errors from this package vs the sibling
`api-graphql-ui-altair`. The public constructor is required by the
project-wide `check-error-subclass-constructors` lint added in
`cd454b3924`.

View File

@@ -0,0 +1,96 @@
# `factory/create-graphiql-ui`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Public entry point of the package. `createGraphiqlUi(opts)` returns a
`RouterPlugin` — a plain value with `name`, `version`, parsed `options`,
the OTel instrument bundle, and a `register(router)` method that mounts
two routes on a host `Router`:
- `GET <uiEndpoint>` — serves the GraphiQL HTML shell with the configured
CSP header.
- `GET <uiEndpoint>/assets/:filename` — serves bundled static assets with
an `immutable` Cache-Control header.
The plugin is opt-in: the host's plugin wiring decides whether to call
`register(router)`. Mount-time validation runs Zod options parsing AND
checks the host router for an existing route at the same path; collision
raises `GRAPHQL_UI_GRAPHIQL.ENDPOINT_COLLISION` so operators see it at
boot.
## Features
- Single-call factory — `createGraphiqlUi(input)` returns a frozen
`RouterPlugin`.
- Endpoint-collision detection at register time via
`router.collidesWith` plus a defence-in-depth catch of
`ROUTER.ROUTE_COLLISION` from the `router.get` call itself.
- Stale-hash-tolerant asset routing — the same `:filename` param matches
every served permutation of the asset name.
- Counter increments only on HTML hits, never on asset hits.
- Custom OTel `MeterProvider` accepted via the optional `meterProvider`
field.
## Usage: Basic
```ts
import { Router } from "@suaveplan/api-router";
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
const router = new Router({ plugin: "host" });
createGraphiqlUi({}).register(router);
// GET /graphql/ui now serves the explorer.
```
## Usage: Advanced
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { Router } from "@suaveplan/api-router";
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
const meterProvider = new MeterProvider({ /* ... */ });
const plugin = createGraphiqlUi({
uiEndpoint: "/api/explorer",
graphqlEndpoint: "/api/gql",
subscriptionEndpoint: "/api/gql/ws",
theme: "dark",
defaultQuery: "{ viewer { id } }",
defaultVariableEditorOpen: true,
editorOptions: { lineWrapping: true },
meterProvider,
});
const router = new Router({ plugin: "host" });
plugin.register(router);
```
## API Reference
- `interface RouterPlugin { name; version; options; instruments;
register(router) }`.
- `interface CreateGraphiqlUiInput extends GraphiqlUiOptionsInput {
meterProvider?: MeterProvider }`.
- `interface RouterResponseValue { status; headers; body }` — the shape
returned by the route handlers.
- `type GraphiqlRouteHandler` — the handler signature consumed by the
in-memory `Router`.
- `createGraphiqlUi(input?): RouterPlugin`.
- `listGraphiqlAssetNames(): readonly string[]`.
## Implementation Notes
The handler signature is intentionally adapter-agnostic — it accepts a
plain `{ method, path }` request and returns `{ status, headers, body }`.
Adapter routers (`api-router-hono`, worker bridges) wrap this with their
native request context. The in-memory `Router` from `@suaveplan/api-router`
uses the same shape directly, which is what the package's unit tests
exercise. The `RouterError` -> `GraphqlUiGraphiqlError` translation in
`register()` exists because `router.collidesWith` is best-effort (it
only checks the UI endpoint, not the asset sub-route); the second
`router.get` for assets may still throw `ROUTE_COLLISION` if some other
plugin owns the asset prefix.

View File

@@ -0,0 +1,64 @@
# `headers/csp`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the `Content-Security-Policy` header values emitted on the GraphiQL
HTML response. The policy is hard-coded to a posture that's safe for
production deployments: `default-src 'self'`, `script-src 'self'`,
`style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`, and
`connect-src 'self'`.
`'unsafe-inline'` is granted to `style-src` because GraphiQL's bundled
CSS ships inline `<style>` declarations. It is NOT granted to
`script-src` — the bundled bootstrap is loaded from `/graphql/ui/assets/`
with a hashed filename, never inlined.
## Features
- `CSP_HEADER_VALUE` — single canonical value used on every HTML response.
- `buildCspHeader()` — returns the `{ name, value }` pair so future
per-environment relaxations have a single touch-point.
- `cspAllowsInlineScripts(value)` — predicate used by the conformance
test to guard against accidental `'unsafe-inline'` creep into
`script-src`.
## Usage: Basic
```ts
import { buildCspHeader } from "@suaveplan/api-graphql-ui-graphiql";
const { name, value } = buildCspHeader();
// name === "Content-Security-Policy"
// value === "default-src 'self'; style-src 'self' 'unsafe-inline'; ..."
```
## Usage: Advanced
```ts
import { cspAllowsInlineScripts } from "@suaveplan/api-graphql-ui-graphiql";
if (cspAllowsInlineScripts(myCspValue)) {
throw new Error("script-src must NOT include 'unsafe-inline'");
}
```
## API Reference
- `CSP_HEADER_NAME: "Content-Security-Policy"`.
- `CSP_HEADER_VALUE: string` — the canonical value emitted on the
HTML response.
- `buildCspHeader(): { name: string; value: string }` — returns the
name/value pair.
- `cspAllowsInlineScripts(value): boolean` — predicate matching
`'unsafe-inline'` inside a `script-src` directive.
## Implementation Notes
Inline-style allowance is documented in the module header so future
maintainers don't tighten the policy without understanding why. The
predicate operates on the raw header string rather than parsing the CSP
grammar — sufficient for the conformance check.

View File

@@ -0,0 +1,77 @@
# `html/render-shell`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Renders the GraphiQL HTML shell. The output embeds the configured GraphQL
endpoint, subscription endpoint, theme, and editor passthroughs into a
`<script id="graphiql-config" type="application/json">` block — NOT inline
JavaScript — which the bundled `graphiql.js` parses at boot. This split
keeps `script-src 'self'` viable.
The shell is intentionally minimalist. The bundled bootstrap drives the
explorer end-to-end (text area for the query, JSON pre for the response).
Operators who want the full React-based GraphiQL distribution can swap
out the asset payload on a fork.
## Features
- Single-call render — `renderShell(options)` returns the full document.
- Defensive escaping — every caller-supplied string (default query,
endpoints, theme, editor options blob) is HTML-escaped.
- No external host references — every `src` / `href` points inside
`<uiEndpoint>/assets/`.
- Asset paths embed FNV-1a hashes for cache-busting.
## Usage: Basic
```ts
import {
parseGraphiqlUiOptions,
renderShell,
} from "@suaveplan/api-graphql-ui-graphiql";
const html = renderShell(parseGraphiqlUiOptions({}));
// html starts with <!DOCTYPE html> and references /graphql/ui/assets/...
```
## Usage: Advanced
```ts
import {
parseGraphiqlUiOptions,
renderShell,
} from "@suaveplan/api-graphql-ui-graphiql";
const html = renderShell(
parseGraphiqlUiOptions({
uiEndpoint: "/explorer",
graphqlEndpoint: "/api/v2/gql",
subscriptionEndpoint: "/api/v2/gql/ws",
theme: "dark",
defaultQuery: "{ viewer { id name } }",
editorOptions: { lineWrapping: true, tabSize: 2 },
})
);
```
## API Reference
- `renderShell(options: GraphiqlUiOptions): string` — produces the
fully-substituted HTML document.
- `escapeHtml(input: string): string` — replaces `&`, `<`, `>`, `"`,
and `'` with HTML entities.
## Implementation Notes
The config block uses `application/json` so the bootstrap can call
`JSON.parse(node.textContent)` without sniffing or eval. Escaping the
JSON via `escapeHtml` is technically over-protective (JSON inside a
`<script type="application/json">` block doesn't need entity encoding
of `<`/`>`/`&` for syntax correctness, but it does need it for
defense-in-depth when the JSON contains a `</script>` substring) — we
err on the safe side and run every embedded string through
`escapeHtml`.

View File

@@ -0,0 +1,72 @@
# `instruments`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI
plugin. A single `Counter` (`graphql.ui.requests_total`) is constructed
at mount time and incremented on each successful HTML response. The
counter is always labelled `{ ui: "graphiql" }` so dashboards can sum
across sibling UI plugins (`api-graphql-ui-altair` etc.) without
collision.
Asset sub-requests (`<uiEndpoint>/assets/...`) do NOT increment this
counter — they are covered by the host router's standard request
metrics.
## Features
- Single `Counter` instrument — `graphql.ui.requests_total`.
- Optional `meterProvider` parameter — defaults to the OTel global
(which falls back to a no-op when no SDK is installed).
- Frozen instrument bundle — meter + counter created once at plugin
mount time.
- `recordUiRequest(instruments)` helper — applies the `{ ui: "graphiql" }`
label automatically so call sites never get it wrong.
## Usage: Basic
```ts
import {
createGraphiqlInstruments,
recordUiRequest,
} from "@suaveplan/api-graphql-ui-graphiql";
const instruments = createGraphiqlInstruments();
recordUiRequest(instruments);
```
## Usage: Advanced
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { createGraphiqlInstruments } from "@suaveplan/api-graphql-ui-graphiql";
const meterProvider = new MeterProvider({ /* readers, views, ... */ });
const instruments = createGraphiqlInstruments({ meterProvider });
// instruments.meter / instruments.requestsTotal are now wired to the
// caller-controlled provider rather than the OTel global.
```
## API Reference
- `interface GraphiqlInstrumentsConfig { meterProvider?: MeterProvider }`.
- `interface GraphiqlInstruments { meter: Meter; requestsTotal: Counter }`
— frozen at construction.
- `createGraphiqlInstruments(config?)` — returns the frozen bundle.
- `recordUiRequest(instruments)` — increments the counter with the
canonical `{ ui: "graphiql" }` label.
- `UI_LABEL_VALUE: "graphiql"` — the literal used in every observation.
## Implementation Notes
`UI_LABEL_VALUE` is exported as a named constant so the sibling Altair
package can reference it via a `not-equal` predicate in shared tests
("the two UI plugins MUST emit distinct `ui` labels"). The counter name
is shared across UI plugins by design — operators get a single rollup of
"explorer requests" without per-UI configuration. The Stage-5
testing-kit's in-memory `MeterProvider` is used in tests; production
operators register their preferred SDK once at process boot.

View File

@@ -0,0 +1,81 @@
# `options`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod-validated configuration surface for `createGraphiqlUi`. Owns
`GraphiqlUiOptions` (resolved shape with every default applied),
`GraphiqlUiOptionsInput` (caller-facing shape where every field is
optional), and `parseGraphiqlUiOptions` (the boundary parser).
The schema is `.strict()` so typos in caller-supplied option keys throw
at parse time rather than silently no-op'ing — empirically the most
common operator mistake when wiring a new UI plugin.
## Features
- Three endpoint paths (`uiEndpoint`, `graphqlEndpoint`,
`subscriptionEndpoint`) — each must start with `/`.
- Theme literal — `"light"` / `"dark"` / `"system"`, default `"system"`.
- `defaultQuery` — pre-fills the editor textarea.
- `defaultVariableEditorOpen` — opens the variables pane on first render.
- `editorOptions` — open passthrough record forwarded to the bundled
bootstrap (CodeMirror options).
## Usage: Basic
```ts
import { parseGraphiqlUiOptions } from "@suaveplan/api-graphql-ui-graphiql";
const options = parseGraphiqlUiOptions({});
// options.uiEndpoint === "/graphql/ui"
```
## Usage: Advanced
```ts
import { parseGraphiqlUiOptions } from "@suaveplan/api-graphql-ui-graphiql";
const options = parseGraphiqlUiOptions({
uiEndpoint: "/api/explorer",
graphqlEndpoint: "/api/gql",
subscriptionEndpoint: "/api/gql/ws",
theme: "dark",
defaultQuery: "{ viewer { id } }",
defaultVariableEditorOpen: true,
editorOptions: { lineWrapping: true, tabSize: 2 },
});
```
## API Reference
- `graphiqlUiOptionsSchema` — strict Zod object with defaults baked in.
- `GraphiqlUiOptions` — `z.infer<typeof graphiqlUiOptionsSchema>`.
- `GraphiqlUiOptionsInput` — `z.input<typeof graphiqlUiOptionsSchema>`.
- `parseGraphiqlUiOptions(input?)` — throws `ZodError` on invalid input;
returns `GraphiqlUiOptions` with every default applied.
## Implementation Notes
The endpoint paths use `.startsWith("/")` rather than a regex so the
error message remains operator-friendly. The schema does NOT validate
whether `subscriptionEndpoint` is reachable — subscription wiring is
the engine adapter's responsibility. Strict mode is on, so a stray
`graphqlEnpdoint` (typo) becomes a hard parse failure.
`GraphiqlUiOptionsInput` carries every field as optional with the
defaults baked in via Zod's `.default(...)`. The factory uses this
shape on its public surface so callers always receive a fully-resolved
`GraphiqlUiOptions` regardless of how many fields they supplied.
The `theme` field intentionally uses a discriminated union of three
literals rather than a string-with-enum because Zod's strict literal
union narrows TypeScript correctly while preserving exhaustiveness
checks at downstream call sites — important for the HTML shell, which
emits a `data-theme="<value>"` attribute. The `editorOptions` record is
typed as `Record<string, unknown>` so it can accept any CodeMirror
configuration without churn on the schema when new editor knobs land
upstream.