@luckystack/docs-ui 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +6 -1
- package/README.md +2 -87
- package/dist/docsHtml.d.ts +14 -0
- package/dist/index.d.ts +6 -9
- package/dist/register.d.ts +1 -2
- package/docs/extension-fields.md +3 -167
- package/docs/html-generation.md +2 -191
- package/docs/mounting.md +2 -223
- package/docs/theming.md +3 -170
- package/package.json +4 -4
- package/CLAUDE.md +0 -55
package/AGENTS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @luckystack/docs-ui — agent guidance
|
|
2
|
+
|
|
3
|
+
This is a development documentation surface, not a public API catalog by default. Only expose it in production after an explicit access-control decision; generated endpoint metadata and the optional request runner can disclose information or exercise authenticated endpoints. See `README.md` for a minimal start and `docs/mounting.md` for the access boundary.
|
|
4
|
+
|
|
5
|
+
Escaping endpoint metadata before HTML insertion is a security invariant. Authorization and environment checks belong before serving both page and JSON, and a custom template must uphold its own escaping. Keep this package independent of a React build step; source and generated API docs, not this file, define supported options and field shapes.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,12 @@ All notable changes to `@luckystack/docs-ui` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.11.0] - 2026-10-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Type declarations are emitted per source file by TypeScript 7 (`tsc -b`) instead of one bundled `.d.ts` per entry, so the framework build no longer runs package by package. The exported types are unchanged; relative imports inside the declarations carry `.js`, so `moduleResolution: "node16"`/`"nodenext"` projects resolve them as before.
|
|
13
|
+
- Package design guidance now ships as `AGENTS.md`, not `CLAUDE.md`. Handwritten docs focus on rationale and safety; exact API contracts remain in installed declarations/source.
|
|
9
14
|
|
|
10
15
|
## [0.8.1] - 2026-07-27
|
|
11
16
|
|
package/README.md
CHANGED
|
@@ -1,90 +1,5 @@
|
|
|
1
1
|
# @luckystack/docs-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A development UI for generated LuckyStack API documentation. In a scaffolded project use `npx luckystack add docs-ui`, or install `@luckystack/docs-ui` and mount its handler through the server's custom routes. Generate API artifacts with the project's `generateArtifacts` script if the UI reports that they are missing. See [mounting](./docs/mounting.md) for the access boundary.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`apiDocs.generated.json` and renders every API endpoint grouped by page,
|
|
7
|
-
with HTTP method, auth requirements, rate limit, input shape, and output
|
|
8
|
-
shape.
|
|
9
|
-
|
|
10
|
-
## Install
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install @luckystack/docs-ui
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Peer deps (already installed if you have any other LuckyStack package):
|
|
17
|
-
|
|
18
|
-
- `@luckystack/core`
|
|
19
|
-
- `@luckystack/server` (the docs UI is mounted via `createLuckyStackServer`'s `customRoutes` option)
|
|
20
|
-
|
|
21
|
-
## Quickstart
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
// luckystack/docs-ui/index.ts (overlay file — auto-imported by bootstrapLuckyStack)
|
|
25
|
-
import { mountDocsUi } from '@luckystack/docs-ui';
|
|
26
|
-
|
|
27
|
-
export const docsUiHandler = mountDocsUi({
|
|
28
|
-
// Title shown in the header + browser tab. Defaults to 'LuckyStack — API docs'.
|
|
29
|
-
// Pass your project name explicitly — there's no implicit lookup from
|
|
30
|
-
// ProjectConfig because consumer config shapes vary too widely to introspect.
|
|
31
|
-
pageTitle: 'My App — API docs',
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
// server/server.ts
|
|
37
|
-
import { docsUiHandler } from '../luckystack/docs-ui';
|
|
38
|
-
import { bootstrapLuckyStack } from '@luckystack/server';
|
|
39
|
-
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
40
|
-
|
|
41
|
-
await (await bootstrapLuckyStack({
|
|
42
|
-
serveFile,
|
|
43
|
-
serveFavicon,
|
|
44
|
-
customRoutes: docsUiHandler,
|
|
45
|
-
})).listen();
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Open `http://localhost/_docs` and you'll see every API in your project,
|
|
49
|
-
grouped by page, with one-click expand for each endpoint's input/output.
|
|
50
|
-
|
|
51
|
-
## Production
|
|
52
|
-
|
|
53
|
-
By default the route returns 404 in production (`NODE_ENV === 'production'`).
|
|
54
|
-
Pass `{ enabledInProd: true }` to render it anyway — useful for an internal
|
|
55
|
-
developer-portal deployment, but **never** safe on a public surface unless
|
|
56
|
-
your APIs are intentionally documented to the world.
|
|
57
|
-
|
|
58
|
-
## Options
|
|
59
|
-
|
|
60
|
-
| Option | Default | Notes |
|
|
61
|
-
| --- | --- | --- |
|
|
62
|
-
| `routePath` | `/_docs` | Path the page is served from. The JSON is at `${routePath}/api.json`. |
|
|
63
|
-
| `pageTitle` | `LuckyStack — API docs` | Shown in the header + browser tab. Pass your project name explicitly — there is no implicit lookup from `ProjectConfig`. |
|
|
64
|
-
| `enabledInProd` | `false` | Set to `true` to render the docs in production. |
|
|
65
|
-
| `apiDocsPath` | `getGeneratedApiDocsPath()` | Override the JSON file path. Default reads from `ProjectConfig.paths.generatedApiDocs`. |
|
|
66
|
-
| `branding` | `{}` | `{ logoUrl?, brandColor?, fontFamily? }` applied by the default template (logo in the header, accent color, font). Ignored when a custom `template` is supplied. |
|
|
67
|
-
| `template` | `undefined` | `DocsTemplateBuilder` — a custom HTML builder `({ jsonPath, pageTitle, branding }) => string`. When set, the default `renderDocsHtml` is bypassed entirely. |
|
|
68
|
-
| `enableTryItOut` | `false` | Renders an inline request runner (textarea + Send) under each endpoint that calls the live server with `credentials: 'include'`. Needs a logged-in browser session. |
|
|
69
|
-
| `authorize` | `undefined` | `(req) => boolean \| Promise<boolean>` per-request hook, run after the env/bind-address gate. Return `false` to serve `403`. Use to restrict docs access on non-loopback deployments. |
|
|
70
|
-
|
|
71
|
-
## How it works
|
|
72
|
-
|
|
73
|
-
The docs page is a single embedded HTML file (no build step, no React, no
|
|
74
|
-
external dependencies). It fetches `${routePath}/api.json` at runtime,
|
|
75
|
-
which is served from the project's `apiDocs.generated.json` — generated by
|
|
76
|
-
`@luckystack/devkit`'s type-map emitter on every dev-server reload and on
|
|
77
|
-
every `npm run generateArtifacts`.
|
|
78
|
-
|
|
79
|
-
The handler is a `customRoutes` predicate: it returns `true` when it
|
|
80
|
-
matches the configured paths, `false` otherwise — so it composes cleanly
|
|
81
|
-
with any other custom HTTP handler.
|
|
82
|
-
|
|
83
|
-
## Related architecture docs
|
|
84
|
-
|
|
85
|
-
- [`docs/ARCHITECTURE_API.md`](../../docs/ARCHITECTURE_API.md) — the lifecycle these endpoints document.
|
|
86
|
-
- [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — how `apiDocs.generated.json` is produced.
|
|
87
|
-
|
|
88
|
-
## License
|
|
89
|
-
|
|
90
|
-
MIT — see the [repository LICENSE](https://github.com/ItsLucky23/LuckyStack-v2/blob/master/LICENSE).
|
|
5
|
+
**Do not expose the documentation route publicly by accident.** Production access is disabled by default; if you intentionally enable it, add authorization and review generated metadata for sensitive detail. The optional interactive request runner uses the browser's credentials and should only be enabled for trusted developers. Custom HTML templates must escape content from generated metadata; see [HTML generation](./docs/html-generation.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RenderDocsHtmlOptions {
|
|
2
|
+
branding?: {
|
|
3
|
+
logoUrl?: string;
|
|
4
|
+
brandColor?: string;
|
|
5
|
+
fontFamily?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* When true, the renderer injects an inline "try-it-out" form per
|
|
9
|
+
* endpoint. The form calls `apiRequest` from `@luckystack/core` against
|
|
10
|
+
* the live server. Disabled by default (requires a logged-in session).
|
|
11
|
+
*/
|
|
12
|
+
enableTryItOut?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const renderDocsHtml: (jsonPath: string, pageTitle: string, options?: RenderDocsHtmlOptions) => string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
|
|
3
|
-
interface DocsBranding {
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export interface DocsBranding {
|
|
4
3
|
/**
|
|
5
4
|
* Logo URL shown in the header. Accepts `https:`, `http:`, and scheme-free
|
|
6
5
|
* relative URLs. `data:` and `javascript:` URLs are rejected (the logo will
|
|
@@ -18,12 +17,12 @@ interface DocsBranding {
|
|
|
18
17
|
* (`<!doctype html>` + everything). Use to swap layout entirely (sidebar
|
|
19
18
|
* vs tabs, dark/light theme, embedded auth-token field).
|
|
20
19
|
*/
|
|
21
|
-
type DocsTemplateBuilder = (input: {
|
|
20
|
+
export type DocsTemplateBuilder = (input: {
|
|
22
21
|
jsonPath: string;
|
|
23
22
|
pageTitle: string;
|
|
24
23
|
branding: DocsBranding;
|
|
25
24
|
}) => string;
|
|
26
|
-
interface MountDocsUiOptions {
|
|
25
|
+
export interface MountDocsUiOptions {
|
|
27
26
|
/** Path the docs UI is served from. Default: `/_docs`. */
|
|
28
27
|
routePath?: string;
|
|
29
28
|
/**
|
|
@@ -70,12 +69,10 @@ interface MountDocsUiOptions {
|
|
|
70
69
|
*/
|
|
71
70
|
authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
72
71
|
}
|
|
73
|
-
type DocsRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
72
|
+
export type DocsRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
74
73
|
/**
|
|
75
74
|
* Returns a route handler compatible with `createLuckyStackServer`'s
|
|
76
75
|
* `customRoutes` option. The handler returns `true` (response sent) when
|
|
77
76
|
* the URL matches the configured docs route, `false` otherwise.
|
|
78
77
|
*/
|
|
79
|
-
declare const mountDocsUi: (options?: MountDocsUiOptions) => DocsRouteHandler;
|
|
80
|
-
|
|
81
|
-
export { type DocsBranding, type DocsRouteHandler, type DocsTemplateBuilder, type MountDocsUiOptions, mountDocsUi };
|
|
78
|
+
export declare const mountDocsUi: (options?: MountDocsUiOptions) => DocsRouteHandler;
|
package/dist/register.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { }
|
|
1
|
+
export {};
|
package/docs/extension-fields.md
CHANGED
|
@@ -1,169 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Extending generated API documentation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional metadata lets projects annotate documentation without coupling every producer to a fixed UI release. Renderers should ignore unknown fields rather than failing when a newer producer adds them. Producers should not assume every consumer renders an optional field. The generated JSON and producer/renderer source are the authoritative schema; do not duplicate its field inventory here.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This document covers:
|
|
8
|
-
|
|
9
|
-
- The top-level JSON shape (including the legacy fallback).
|
|
10
|
-
- Core fields the renderer reads at the top level of each endpoint entry.
|
|
11
|
-
- Optional extension fields rendered when present (`stream`, `owner`, `tags`, `deprecated`).
|
|
12
|
-
- How to add your own metadata via the devkit emitter hooks.
|
|
13
|
-
|
|
14
|
-
For the rendering pipeline that consumes these fields see `./html-generation.md`. For the route that serves the JSON see `./mounting.md`.
|
|
15
|
-
|
|
16
|
-
## Top-level JSON shape
|
|
17
|
-
|
|
18
|
-
The current artifact has two top-level maps — `apis` and `syncs` — each keyed by `page`. The value under each page is a **flat array** of endpoint entries (not a nested `name → version` object). Every entry carries its own `name`, `version`, and `path`:
|
|
19
|
-
|
|
20
|
-
```jsonc
|
|
21
|
-
{
|
|
22
|
-
"apis": {
|
|
23
|
-
"<page>": [
|
|
24
|
-
{
|
|
25
|
-
"page": "<page>",
|
|
26
|
-
"name": "<name>",
|
|
27
|
-
"version": "<version>",
|
|
28
|
-
"method": "POST",
|
|
29
|
-
"auth": { "login": true, "additional": [] },
|
|
30
|
-
"rateLimit": 60,
|
|
31
|
-
"input": "{ userId: string }",
|
|
32
|
-
"output": "{ status: 'success', result: { ... } }",
|
|
33
|
-
"stream": "never",
|
|
34
|
-
"path": "api/<page>/<name>/<version>",
|
|
35
|
-
"meta": { "owner": "@team", "tags": ["internal"] }
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
"syncs": {
|
|
40
|
-
"<page>": [
|
|
41
|
-
{
|
|
42
|
-
"page": "<page>",
|
|
43
|
-
"name": "<name>",
|
|
44
|
-
"version": "<version>",
|
|
45
|
-
"clientInput": "{ ... }",
|
|
46
|
-
"serverOutput": "{ ... }",
|
|
47
|
-
"clientOutput": "{ ... }",
|
|
48
|
-
"serverStream": "never",
|
|
49
|
-
"clientStream": "never",
|
|
50
|
-
"path": "sync/<page>/<name>/<version>"
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Legacy/forward fallback: if the root object is the apis map itself (no `apis` wrapper), the renderer normalizes via `data && data.apis ? data.apis : data`. Both shapes are accepted; new code should always emit the wrapped form.
|
|
58
|
-
|
|
59
|
-
The grouping is `page` → array of entries; `name`, `version`, `method`, `path`, etc. are fields ON each entry. This matches the file-based routing convention (`src/<page>/_api/<name>_v<version>.ts`). The renderer iterates the pages in object-iteration order and each page's array in array order; the emitter is responsible for any sorting.
|
|
60
|
-
|
|
61
|
-
## Core fields (always read by the renderer)
|
|
62
|
-
|
|
63
|
-
These are the fields used to build the always-visible row + the default expanded panel:
|
|
64
|
-
|
|
65
|
-
| Field | Type | Default in renderer | Purpose |
|
|
66
|
-
| --- | --- | --- | --- |
|
|
67
|
-
| `method` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE'` (any string, uppercased) | `'POST'` | Method pill text + CSS class on `.method`. |
|
|
68
|
-
| `rateLimit` | `number \| false \| undefined` | `'default rate'` label | Numeric ⇒ `"<n>/min"`. `false` ⇒ `"no rate limit"`. `undefined` ⇒ `"default rate"`. |
|
|
69
|
-
| `auth` | `{ login?: boolean; additional?: Array<{ key: string; value?: unknown }> }` | `'public'` badge | `auth.login` becomes a `"login required"` badge. Each `additional[]` entry becomes a `"<key>"` or `"<key>=<JSON-stringified value>"` badge. Empty / missing ⇒ single `"public"` badge. |
|
|
70
|
-
| `input` | `string` (already-stringified TS shape) | `'{}'` | Rendered inside `<pre>` in the expanded panel. |
|
|
71
|
-
| `output` | `string` (already-stringified TS shape) | `'unknown'` | Rendered inside `<pre>` in the expanded panel. |
|
|
72
|
-
|
|
73
|
-
The renderer does not try to JSON-parse `input` / `output`. They are emitted as pre-formatted strings by the devkit emitter so the docs page can display the original TypeScript shape verbatim — including unions, mapped types, and template-literal types — without re-stringifying.
|
|
74
|
-
|
|
75
|
-
Unknown core fields fall through to their defaults silently; emit a missing `method`, for example, and the row shows `POST`.
|
|
76
|
-
|
|
77
|
-
## Optional extension fields
|
|
78
|
-
|
|
79
|
-
The default template renders these only when present. They are additive: omit any of them and the corresponding section is left out of the expanded panel.
|
|
80
|
-
|
|
81
|
-
### `stream` (top-level)
|
|
82
|
-
|
|
83
|
-
- Where: a **top-level** field on the entry (a sibling of `input` / `output`), **not** under `meta`. The renderer reads `entry.stream` and shows the section only when the value is truthy and not the `"never"` sentinel.
|
|
84
|
-
- Type: `string` (already-stringified TS shape, same convention as `input` / `output`).
|
|
85
|
-
- Renders as: a `stream` detail section with the value inside a `<pre>` block.
|
|
86
|
-
- Use for endpoints that stream chunks back to the client (e.g. SSE or chunked-transfer responses). The string typically describes the shape of a single chunk.
|
|
87
|
-
- Example payload field:
|
|
88
|
-
|
|
89
|
-
```jsonc
|
|
90
|
-
{
|
|
91
|
-
"stream": "{ delta: string } | { done: true }"
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### `meta.owner`
|
|
96
|
-
|
|
97
|
-
- Type: `string`.
|
|
98
|
-
- Renders as: an `owner` detail section with the raw string.
|
|
99
|
-
- Use for triage-friendly attribution — team name, Slack channel, GitHub handle, on-call rotation, etc. The value is escaped before injection so it is safe to include arbitrary text.
|
|
100
|
-
- Example:
|
|
101
|
-
|
|
102
|
-
```jsonc
|
|
103
|
-
{ "owner": "@platform-team (#help-platform)" }
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### `meta.tags`
|
|
107
|
-
|
|
108
|
-
- Type: `string[]` (or array of values coerced via `String(...)`).
|
|
109
|
-
- Renders as: inline badges in a `tags` detail section, space-separated.
|
|
110
|
-
- Use for cross-cutting categorization the page/name hierarchy can't express — `"v2-migration"`, `"internal"`, `"experimental"`, etc.
|
|
111
|
-
- Example:
|
|
112
|
-
|
|
113
|
-
```jsonc
|
|
114
|
-
{ "tags": ["experimental", "internal"] }
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Note: the renderer treats `tags` as an `Array.isArray(value)` check; arbitrary iterables are not supported. Emit a plain array.
|
|
118
|
-
|
|
119
|
-
### `meta.deprecated`
|
|
120
|
-
|
|
121
|
-
- Type: `string | true` (anything truthy; `String(value)` is used for display).
|
|
122
|
-
- Renders as: a `deprecated` detail section with red text (`color: var(--delete)`).
|
|
123
|
-
- Use for a removal notice or migration pointer. The truthiness check means `"true"`, `"yes"`, `"removing v2026.05"`, etc. all render — but the renderer always shows the string value, so prefer a human-readable migration note over a bare boolean.
|
|
124
|
-
- Example:
|
|
125
|
-
|
|
126
|
-
```jsonc
|
|
127
|
-
{
|
|
128
|
-
"deprecated": "Removed in v2026.05 — use users/getProfile/v2 instead."
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Forward compatibility
|
|
133
|
-
|
|
134
|
-
- Adding a new field to a `meta` object is non-breaking. The renderer ignores it.
|
|
135
|
-
- Removing one of the four extension fields above does **not** break the renderer either — each is conditional on truthiness or array length.
|
|
136
|
-
- The renderer does no schema validation. Malformed top-level JSON (e.g. wrong types at the page/name/version levels) shows the page's empty state with a hint to regenerate.
|
|
137
|
-
|
|
138
|
-
If you need extra fields on the page, you have two options:
|
|
139
|
-
|
|
140
|
-
1. Extend the devkit emitter to include the field, then customize the rendering side via a `DocsTemplateBuilder` (see `./theming.md`) or, for trivial additions, contribute the field to this package.
|
|
141
|
-
2. Keep your metadata out-of-band (a sibling JSON file) and consume it from a custom template. The default renderer will not surface it, but a `DocsTemplateBuilder` can fetch both files.
|
|
142
|
-
|
|
143
|
-
## Producer contract — how fields are populated
|
|
144
|
-
|
|
145
|
-
`apiDocs.generated.json` is written by `@luckystack/devkit`'s type-map emitter on each dev-server reload and on every `npm run generateArtifacts`. Path resolution goes through `ProjectConfig.paths.generatedApiDocs` (consumed by `getGeneratedApiDocsPath()` in `@luckystack/core`).
|
|
146
|
-
|
|
147
|
-
Emission steps the devkit performs per endpoint:
|
|
148
|
-
|
|
149
|
-
1. Read the route file (`src/<page>/_api/<name>_v<version>.ts`) and extract exports: `method`, `rateLimit`, `auth`, the `ApiParams.data` interface (rendered into `input`), and the `main` return type (rendered into `output`).
|
|
150
|
-
2. Stringify the types into source-faithful strings — no re-stringification, no JSON.stringify of types.
|
|
151
|
-
3. Run the emitter's `emitterArtifacts` extension hooks (`packages/devkit/src/typeMap/emitterArtifacts.ts`). Hooks can attach `stream`, `owner`, `tags`, `deprecated`, or any new field.
|
|
152
|
-
4. Group by `page` (one flat array of entries per page, each entry carrying its own `name` / `version` / `path`) and write the JSON.
|
|
153
|
-
|
|
154
|
-
Consumer-side extension (e.g. reading a `@deprecated` JSDoc tag and mapping it to `meta.deprecated`) belongs in an emitter artifact hook, not in `@luckystack/docs-ui`. The docs-ui package only renders what the emitter writes.
|
|
155
|
-
|
|
156
|
-
## Validation behavior
|
|
157
|
-
|
|
158
|
-
- No schema validation. The renderer trusts the JSON shape.
|
|
159
|
-
- A missing artifact (`apiDocs.generated.json` not on disk) yields a `404 JSON` response from the `mountDocsUi` handler with `expectedAt` + `hint` — surfaced as an inline error message in the page.
|
|
160
|
-
- An empty / invalid `apis` map yields the page's empty state: `"No API docs available. Run npm run generateArtifacts to generate them."`.
|
|
161
|
-
- Per-endpoint malformations (e.g. `auth` is a string instead of an object) fall back to defaults silently. Bad data does not crash the page; it just shows degraded content.
|
|
162
|
-
|
|
163
|
-
## Related
|
|
164
|
-
|
|
165
|
-
- Rendering pipeline that consumes these fields: `./html-generation.md`
|
|
166
|
-
- Mount + route: `./mounting.md`
|
|
167
|
-
- Branding / custom templates: `./theming.md`
|
|
168
|
-
- AI summary + function index: `../CLAUDE.md`
|
|
169
|
-
- Producer: `packages/devkit/src/typeMap/emitter.ts` and `packages/devkit/src/typeMap/emitterArtifacts.ts`
|
|
5
|
+
Treat documentation metadata as publishable information. Ownership labels, tags, stream hints and deprecation messages can disclose private design details if the docs are exposed outside the team. Review the access boundary in `mounting.md` before enabling production serving or a custom renderer.
|
package/docs/html-generation.md
CHANGED
|
@@ -1,194 +1,5 @@
|
|
|
1
1
|
# HTML generation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The default documentation page is self-contained HTML rather than a separate React bundle, so a development server can serve generated docs without a frontend build. The UI fetches generated metadata at request time rather than embedding a hand-maintained API inventory. See the renderer source for current selectors, field handling and template-builder parameters.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- `renderDocsHtml(jsonPath, pageTitle, options?)` — the default HTML builder.
|
|
8
|
-
- `RenderDocsHtmlOptions` — the option shape.
|
|
9
|
-
- `DocsTemplateBuilder` — the hook for replacing the renderer entirely.
|
|
10
|
-
- The embedded client pipeline: fetch, group, render, filter, "try it out".
|
|
11
|
-
|
|
12
|
-
For the mount/route side see `./mounting.md`. For branding tokens see `./theming.md`. For the JSON fields read by the renderer see `./extension-fields.md`.
|
|
13
|
-
|
|
14
|
-
## Function signature
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
export const renderDocsHtml = (
|
|
18
|
-
jsonPath: string,
|
|
19
|
-
pageTitle: string,
|
|
20
|
-
options?: RenderDocsHtmlOptions,
|
|
21
|
-
): string;
|
|
22
|
-
|
|
23
|
-
export interface RenderDocsHtmlOptions {
|
|
24
|
-
branding?: {
|
|
25
|
-
logoUrl?: string;
|
|
26
|
-
brandColor?: string;
|
|
27
|
-
fontFamily?: string;
|
|
28
|
-
};
|
|
29
|
-
enableTryItOut?: boolean;
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Return value: a complete `<!DOCTYPE html>...</html>` document as a single UTF-8 string. The caller is responsible for setting the `Content-Type: text/html; charset=utf-8` header (the `mountDocsUi` handler already does this).
|
|
34
|
-
|
|
35
|
-
### Arguments
|
|
36
|
-
|
|
37
|
-
| Arg | Type | Required | Purpose |
|
|
38
|
-
| --- | --- | --- | --- |
|
|
39
|
-
| `jsonPath` | `string` | Yes | Path the embedded script fetches with `fetch(jsonPath, { credentials: 'include' })`. Typically the `${routePath}/api.json` sibling produced by `mountDocsUi`, but any URL that returns the expected JSON shape works. |
|
|
40
|
-
| `pageTitle` | `string` | Yes | Used for `<title>` and `<h1>`. Passed through `escapeHtml` before interpolation. |
|
|
41
|
-
| `options.branding` | `DocsBranding` | No | Logo, accent color, font family. See `./theming.md`. |
|
|
42
|
-
| `options.enableTryItOut` | `boolean` | No (default `false`) | When `true`, each endpoint detail panel gets a textarea + Send button that posts to the live server. |
|
|
43
|
-
|
|
44
|
-
## Document structure
|
|
45
|
-
|
|
46
|
-
The produced HTML is one document with three logical sections:
|
|
47
|
-
|
|
48
|
-
1. **`<head>`** — meta tags, `<title>`, and a single inline `<style>` block.
|
|
49
|
-
2. **`<body>`** — a `.layout` wrapper containing the brand row, the filter bar, a summary pill row, and an empty `<div id="content">` filled in by the script.
|
|
50
|
-
3. **Inline `<script>`** — defines render helpers, fetches the JSON, wires up the filter input, and (optionally) the "try it out" runner.
|
|
51
|
-
|
|
52
|
-
There is no external `<link>`, `<script src="...">`, or web-font import. The page renders correctly when opened with a local file URL too, provided the JSON endpoint is reachable.
|
|
53
|
-
|
|
54
|
-
### CSS variables in `:root`
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
--bg, --container, --container-hover, --border
|
|
58
|
-
--title, --common, --muted
|
|
59
|
-
--accent (from branding.brandColor; default #58a6ff)
|
|
60
|
-
--font-family (from branding.fontFamily; default system stack)
|
|
61
|
-
--get (#3fb950)
|
|
62
|
-
--post (= --accent)
|
|
63
|
-
--put (#d29922)
|
|
64
|
-
--delete (#f85149)
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
A `@media (prefers-color-scheme: light)` block redefines `--bg`, `--container`, `--container-hover`, `--border`, `--title`, `--common`, `--muted` to a light palette. The accent and method colors do not switch — they are chosen to be legible on both backgrounds.
|
|
68
|
-
|
|
69
|
-
### Layout selectors
|
|
70
|
-
|
|
71
|
-
| Selector | Purpose |
|
|
72
|
-
| --- | --- |
|
|
73
|
-
| `.layout` | Outer max-1200px container with vertical padding. |
|
|
74
|
-
| `.brand-row` | Holds the optional `<img>` logo and the `<h1>` page title. |
|
|
75
|
-
| `.summary` / `.summary-pill` | Top-of-page count pills ("`<visible>` of `<total>` endpoints", "`<pages>` pages"). |
|
|
76
|
-
| `.filter-bar input[type="search"]` | The single search input. Autofocus on load. |
|
|
77
|
-
| `.group` / `.group-header` | One block per `page` key in the JSON. |
|
|
78
|
-
| `.endpoint` / `.endpoint-summary` / `.endpoint-detail` | One row per endpoint. The detail panel is hidden until the row is clicked. |
|
|
79
|
-
| `.method.GET / .POST / .PUT / .DELETE` | Method pill, color-coded against the CSS variables above. |
|
|
80
|
-
| `.auth-tag` | Inline badges for each auth rule. |
|
|
81
|
-
| `.try-it-out` | Optional runner panel (only emitted when `enableTryItOut` is true). |
|
|
82
|
-
|
|
83
|
-
The styles are intentionally close to the framework's Tailwind tokens (background, container, muted, common) so the page looks at home in a LuckyStack project without configuration.
|
|
84
|
-
|
|
85
|
-
## Client-side rendering pipeline
|
|
86
|
-
|
|
87
|
-
The embedded `<script>` runs the following sequence after the HTML parses:
|
|
88
|
-
|
|
89
|
-
1. **`fetch(JSON_PATH, { credentials: 'include' })`** — pulls the JSON from `${routePath}/api.json` (or the explicit URL passed to `renderDocsHtml`). Credentials are included so a session cookie reaches the JSON endpoint when one is required.
|
|
90
|
-
2. On a successful response, the parsed payload is cached on `cachedData` and passed to `render(data, '')`.
|
|
91
|
-
3. On error (non-OK status or network failure) the page replaces `<div id="content">` with an inline error message — no console-only logging — so the user sees the failure without opening devtools.
|
|
92
|
-
4. The `<input type="search" id="filter">` registers an `input` listener that re-invokes `render(cachedData, filterValue)` on every keystroke. The fetch is not repeated.
|
|
93
|
-
|
|
94
|
-
### `render(data, filter)`
|
|
95
|
-
|
|
96
|
-
- Accepts either `{ apis, syncs }` (current shape) or a bare `{ [page]: Entry[] }` (legacy shape) — the renderer normalizes via `data && data.apis ? data.apis : data` (and `data.syncs` ?? `{}`), then delegates to `buildGroups(apis, syncs, filter)`.
|
|
97
|
-
- `buildGroups` walks each page's flat array of entries (and the parallel `syncs` map the same way), increments `total`, applies `passesFilter`, and accumulates HTML rows via `renderEndpoint` / `renderSyncEntry`.
|
|
98
|
-
- Pages that have zero matching rows are omitted entirely (no empty `.group` boxes).
|
|
99
|
-
- Writes the `summary-pill` counts (`visible of total`, `pages count`) and the joined `.group` HTML into `<div id="content">`.
|
|
100
|
-
- Open/closed state is toggled by a single delegated `click` listener on `#content` (registered once via `bindEndpointToggles`, not re-bound per render), persisting state in `stateByKey` (keyed by `page + '/' + name + '@' + version`). The state is in-memory only; a hard reload resets all panels to closed.
|
|
101
|
-
|
|
102
|
-
### `renderEndpoint(entry)`
|
|
103
|
-
|
|
104
|
-
Takes a single entry object and pulls these fields directly off it:
|
|
105
|
-
|
|
106
|
-
- `method` — uppercased and used as both the class and the label of the pill. Falls back to `POST` when undefined.
|
|
107
|
-
- `rateLimit` — `false` ⇒ "no rate limit"; `undefined` ⇒ "default rate"; number ⇒ "`<n>`/min".
|
|
108
|
-
- `auth` — passed to `renderAuth`. `auth.login` becomes `"login required"`. Each `auth.additional[]` entry contributes a `"<key>"` or `"<key>=<value>"` badge (value is `JSON.stringify`'d). Empty auth renders a `"public"` badge.
|
|
109
|
-
- `input`, `output` — already-stringified shapes from the type-map emitter. Rendered inside `<pre>` blocks. The fallbacks are `'{}'` and `'unknown'` respectively.
|
|
110
|
-
- `stream` — a top-level field on the entry, rendered when truthy and not `'never'`.
|
|
111
|
-
- `meta.owner`, `meta.tags`, `meta.deprecated` — optional fields under the entry's `meta` object, rendered conditionally. See `./extension-fields.md`.
|
|
112
|
-
|
|
113
|
-
The displayed endpoint path is `/api/<page>/<name>/<version>`; the path used by the runner is `<page>/<name>/<version>` posted to `/<route>?stream=false` (no leading `/api/`).
|
|
114
|
-
|
|
115
|
-
### `passesFilter(filter, page, name)`
|
|
116
|
-
|
|
117
|
-
Lowercase substring match against `page + '/' + name`. The version is not part of the haystack — filtering by version is intentional only via the page/route name.
|
|
118
|
-
|
|
119
|
-
### `escapeHtml` — two copies
|
|
120
|
-
|
|
121
|
-
The renderer ships two `escapeHtml` helpers that look identical and **must stay identical**:
|
|
122
|
-
|
|
123
|
-
1. The TypeScript helper at the bottom of `src/docsHtml.ts` is used to escape `pageTitle`, `jsonPath`, and `branding.logoUrl` before they are interpolated into the template literal at build time of the response.
|
|
124
|
-
2. The JS helper inside the inline `<script>` is used by the client to escape every value pulled from `apiDocs.generated.json` before it is injected into `innerHTML`.
|
|
125
|
-
|
|
126
|
-
Both map `&`, `<`, `>`, `"`, `'` to their HTML entity equivalents. The renderer never inserts unescaped user-supplied strings.
|
|
127
|
-
|
|
128
|
-
## "Try it out" runner
|
|
129
|
-
|
|
130
|
-
Enabled by `options.enableTryItOut === true`. When on:
|
|
131
|
-
|
|
132
|
-
- `ENABLE_TRY_IT_OUT` is `true` inside the inline script.
|
|
133
|
-
- `renderTryItOut(route, method)` appends a `<div class="try-it-out">` containing a `<textarea>` (default content `{}`), a `Send` button, and an empty `<pre class="result">`.
|
|
134
|
-
- The Send click invokes `runEndpoint(button, route, method, dataField)`.
|
|
135
|
-
|
|
136
|
-
`runEndpoint` performs:
|
|
137
|
-
|
|
138
|
-
```js
|
|
139
|
-
const httpMethod = (method || 'POST').toUpperCase();
|
|
140
|
-
const hasBody = httpMethod !== 'GET' && httpMethod !== 'DELETE';
|
|
141
|
-
fetch('/' + route + '?stream=false', {
|
|
142
|
-
method: httpMethod,
|
|
143
|
-
headers: hasBody ? { 'Content-Type': 'application/json' } : {},
|
|
144
|
-
credentials: 'include',
|
|
145
|
-
body: hasBody ? JSON.stringify(parsed) : undefined,
|
|
146
|
-
})
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Behavior:
|
|
150
|
-
|
|
151
|
-
- The endpoint's declared HTTP method is threaded through (the framework returns `405` on a method mismatch, so a hardcoded `POST` would break every documented GET/PUT/DELETE route). For `GET`/`DELETE` the JSON body and `Content-Type` header are omitted.
|
|
152
|
-
- `dataField.value.trim()` empty ⇒ sends `{}` (for body-bearing methods). Otherwise the textarea value is parsed with `JSON.parse`. A parse error is shown in the result pane and no request is sent.
|
|
153
|
-
- The button is disabled during flight and re-enabled in `finally`.
|
|
154
|
-
- The response body is parsed as JSON when possible (pretty-printed via `JSON.stringify(..., null, 2)`) and otherwise displayed as raw text — this keeps non-JSON error pages readable.
|
|
155
|
-
- `?stream=false` instructs the HTTP API surface to return the full final payload instead of a streamed chunked response, which is what the textarea-and-pre UI is built for.
|
|
156
|
-
- Authentication is implicit via the browser session cookie (`credentials: 'include'`). The runner does not prompt for a token.
|
|
157
|
-
|
|
158
|
-
Disable the runner (`enableTryItOut: false`) when you ship to environments where developers don't have an authenticated session or you don't want side-effecting POSTs from the docs page.
|
|
159
|
-
|
|
160
|
-
## `DocsTemplateBuilder` — full replacement
|
|
161
|
-
|
|
162
|
-
When `mountDocsUi` is called with `template: DocsTemplateBuilder`, the default `renderDocsHtml` pipeline is bypassed entirely. The builder is invoked with:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
type DocsTemplateBuilder = (input: {
|
|
166
|
-
jsonPath: string;
|
|
167
|
-
pageTitle: string;
|
|
168
|
-
branding: DocsBranding;
|
|
169
|
-
}) => string;
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Contract for custom builders:
|
|
173
|
-
|
|
174
|
-
- Must return a full HTML document. The `mountDocsUi` handler streams the string as-is into `res.end(...)` with `Content-Type: text/html; charset=utf-8`.
|
|
175
|
-
- Should `fetch(jsonPath)` itself (or use `<script>` to do so). The handler does not pre-fetch the JSON.
|
|
176
|
-
- Should HTML-escape any inputs that end up in markup. The builder receives them as raw strings — `mountDocsUi` does not pre-escape.
|
|
177
|
-
- May ignore `branding` if the layout has its own theming primitives.
|
|
178
|
-
- May call `renderDocsHtml` from inside the builder if you only want to wrap the default rendering with extra chrome — but in that case you can usually use `branding` instead.
|
|
179
|
-
|
|
180
|
-
Use a template builder for full layout replacements (sidebar navigation, tabs, marketing-page chrome, dark/light toggle beyond OS preference). Use `branding` for cosmetic tweaks. See `./theming.md` for the trade-off.
|
|
181
|
-
|
|
182
|
-
## Forward compatibility
|
|
183
|
-
|
|
184
|
-
- Unknown `meta` fields on endpoints are ignored by the renderer.
|
|
185
|
-
- The renderer never schema-validates the JSON; malformed input falls back to the empty state with a `Run npm run generateArtifacts` hint.
|
|
186
|
-
- The two-shape acceptance (`{ apis }` vs bare map) is intentional to preserve compatibility with older artifact generators.
|
|
187
|
-
|
|
188
|
-
## Related
|
|
189
|
-
|
|
190
|
-
- Mounting + route handler: `./mounting.md`
|
|
191
|
-
- Branding tokens + template trade-off: `./theming.md`
|
|
192
|
-
- Extension fields rendered by the default template: `./extension-fields.md`
|
|
193
|
-
- AI summary + function index: `../CLAUDE.md`
|
|
194
|
-
- Source: `../src/docsHtml.ts`
|
|
5
|
+
Generated endpoint metadata is not inherently safe HTML: escape text and attribute content before inserting it. Reject dangerous image schemes such as `data:` and `javascript:` for branding logos, including SVG payloads. A replacement template takes ownership of that escaping and URL validation. The optional interactive request runner sends live requests using the browser's session; enabling it on an untrusted surface is not just a documentation choice. Restrict access before making it available, and avoid reflecting sensitive response bodies in logs.
|
package/docs/mounting.md
CHANGED
|
@@ -1,226 +1,5 @@
|
|
|
1
1
|
# Mounting the docs UI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The docs handler composes with the server's custom-route mechanism rather than taking over general request routing. Mount it only where generated API docs should be available. See `README.md` for a short start and the installed `mountDocsUi` types for current options and handler signatures.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This document is the deep reference for:
|
|
8
|
-
|
|
9
|
-
- `mountDocsUi(options?)` — the factory that builds the handler.
|
|
10
|
-
- `DocsRouteHandler` — the handler contract.
|
|
11
|
-
- `MountDocsUiOptions` — the full option surface.
|
|
12
|
-
|
|
13
|
-
For the HTML rendering side of the pipeline see `./html-generation.md`. For optional branding/template overrides see `./theming.md`. For the JSON fields the renderer consumes see `./extension-fields.md`.
|
|
14
|
-
|
|
15
|
-
## Function signatures
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
export const mountDocsUi = (options?: MountDocsUiOptions): DocsRouteHandler;
|
|
19
|
-
|
|
20
|
-
export type DocsRouteHandler = (
|
|
21
|
-
req: IncomingMessage,
|
|
22
|
-
res: ServerResponse,
|
|
23
|
-
) => Promise<boolean>;
|
|
24
|
-
|
|
25
|
-
export interface MountDocsUiOptions {
|
|
26
|
-
routePath?: string;
|
|
27
|
-
pageTitle?: string;
|
|
28
|
-
enabledInProd?: boolean;
|
|
29
|
-
apiDocsPath?: string;
|
|
30
|
-
branding?: DocsBranding;
|
|
31
|
-
template?: DocsTemplateBuilder;
|
|
32
|
-
enableTryItOut?: boolean;
|
|
33
|
-
authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## `MountDocsUiOptions` argument breakdown
|
|
38
|
-
|
|
39
|
-
| Option | Type | Default | Purpose |
|
|
40
|
-
| --- | --- | --- | --- |
|
|
41
|
-
| `routePath` | `string` | `/_docs` | URL path the HTML page is served from. The JSON sibling is automatically derived as `${routePath}/api.json`. Both paths are claimed by the handler. |
|
|
42
|
-
| `pageTitle` | `string` | `LuckyStack — API docs` | Used for the `<title>` element and the `<h1>` in the header. Pass your project name explicitly — there is no implicit lookup from `ProjectConfig` because consumer config shapes vary too widely to introspect. |
|
|
43
|
-
| `enabledInProd` | `boolean` | `false` | Set to `true` to render the docs in production (`NODE_ENV === 'production'`). Use only for an internal developer-portal deployment with its own auth layer in front. |
|
|
44
|
-
| `apiDocsPath` | `string` | `getGeneratedApiDocsPath()` | Absolute filesystem path to `apiDocs.generated.json`. Default reads from `ProjectConfig.paths.generatedApiDocs` via `@luckystack/core`. Override only when running a non-standard artifact layout. |
|
|
45
|
-
| `branding` | `DocsBranding` | `{}` | Optional logo / accent color / font family. Applied by the default template only. See `./theming.md`. |
|
|
46
|
-
| `template` | `DocsTemplateBuilder` | `undefined` | Custom HTML builder. When provided, the default `renderDocsHtml` is bypassed entirely. See `./html-generation.md` and `./theming.md`. |
|
|
47
|
-
| `enableTryItOut` | `boolean` | `false` | Renders an inline request runner under each endpoint. Off by default — the runner needs a logged-in browser session because it hits the live server with `credentials: 'include'`. |
|
|
48
|
-
| `authorize` | `(req: IncomingMessage) => boolean \| Promise<boolean>` | `undefined` | Optional per-request authorization hook. Called after the env/bind-address gate passes (so it only runs when the route is being served). Return `true` to allow the request, `false` to serve `403 Forbidden`. Use to restrict docs access to authenticated or IP-allowlisted callers on non-loopback deployments (e.g. an internal developer-portal opened with `enabledInProd: true`). |
|
|
49
|
-
|
|
50
|
-
All options are individually optional; calling `mountDocsUi()` with no arguments is valid and produces the default behavior.
|
|
51
|
-
|
|
52
|
-
## `DocsRouteHandler` contract
|
|
53
|
-
|
|
54
|
-
The factory returns an async function with this exact shape:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
type DocsRouteHandler = (
|
|
58
|
-
req: IncomingMessage,
|
|
59
|
-
res: ServerResponse,
|
|
60
|
-
) => Promise<boolean>;
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Return-value contract:
|
|
64
|
-
|
|
65
|
-
- `true` — the handler has fully processed the request and called `res.end(...)`. The caller (typically `@luckystack/server`'s `customRoutes` orchestration) must not invoke any further handler for this request.
|
|
66
|
-
- `false` — the handler did not match the URL. The request body is untouched and the response is unwritten. The caller is free to delegate to the next handler or fall through to the framework's default routing.
|
|
67
|
-
|
|
68
|
-
The handler is safe to compose: it inspects only `req.url` and `req.method` and never reads the body. It is always async because reading `apiDocs.generated.json` from disk happens via `fs.promises.readFile` wrapped in `tryCatch`.
|
|
69
|
-
|
|
70
|
-
## Compositing with `createLuckyStackServer`
|
|
71
|
-
|
|
72
|
-
Minimum integration — pass the handler as `customRoutes`:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
// luckystack/docs-ui/index.ts (overlay file)
|
|
76
|
-
import { mountDocsUi } from '@luckystack/docs-ui';
|
|
77
|
-
|
|
78
|
-
export const docsUiHandler = mountDocsUi({
|
|
79
|
-
pageTitle: 'My App — API docs',
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
// server/server.ts
|
|
85
|
-
import { bootstrapLuckyStack } from '@luckystack/server';
|
|
86
|
-
import { docsUiHandler } from '../luckystack/docs-ui';
|
|
87
|
-
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
88
|
-
|
|
89
|
-
await (await bootstrapLuckyStack({
|
|
90
|
-
serveFile,
|
|
91
|
-
serveFavicon,
|
|
92
|
-
customRoutes: docsUiHandler,
|
|
93
|
-
})).listen();
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Composing with another custom handler — chain by `await`-ing the docs handler first and only continuing when it returns `false`:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
100
|
-
import { mountDocsUi } from '@luckystack/docs-ui';
|
|
101
|
-
import { metricsHandler } from './metrics';
|
|
102
|
-
|
|
103
|
-
const docsHandler = mountDocsUi();
|
|
104
|
-
|
|
105
|
-
export const customRoutes = async (
|
|
106
|
-
req: IncomingMessage,
|
|
107
|
-
res: ServerResponse,
|
|
108
|
-
): Promise<boolean> => {
|
|
109
|
-
if (await docsHandler(req, res)) return true;
|
|
110
|
-
if (await metricsHandler(req, res)) return true;
|
|
111
|
-
return false;
|
|
112
|
-
};
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The order matters only when two handlers would claim the same path; `mountDocsUi` only ever claims `routePath` + `${routePath}/api.json`, so collisions are easy to avoid by picking a unique `routePath`.
|
|
116
|
-
|
|
117
|
-
## Behavior matrix
|
|
118
|
-
|
|
119
|
-
The handler's branches map directly to the request shape. The order is exactly as evaluated in `src/index.ts`:
|
|
120
|
-
|
|
121
|
-
| Step | Condition | Result |
|
|
122
|
-
| --- | --- | --- |
|
|
123
|
-
| 1 | `pathOnly !== routePath && pathOnly !== ${routePath}/api.json` | Returns `false` (pass-through). No response written. |
|
|
124
|
-
| 2 | (`NODE_ENV === 'production'` **or** `!isLoopbackIp(getBindAddress().ip)`) and `!options.enabledInProd` | `404 Not Found` (`text/plain`). Returns `true`. The non-loopback bind-address check means a server bound to a public interface is gated even outside `NODE_ENV=production`. |
|
|
125
|
-
| 3 | `options.authorize` is set and returns/resolves `false` | `403 Forbidden` (`text/plain`). Returns `true`. Runs only after the env/bind gate (step 2) passes. |
|
|
126
|
-
| 4 | `req.method !== 'GET'` | `405 Method Not Allowed` (`text/plain`). Returns `true`. |
|
|
127
|
-
| 5 | `pathOnly === ${routePath}/api.json` and the file is readable | `200 OK` (`application/json`, `Cache-Control: no-store`) with the raw file contents. Returns `true`. |
|
|
128
|
-
| 6 | `pathOnly === ${routePath}/api.json` and the file is missing/unreadable | `404 Not Found` (`application/json`) with `{ error, expectedAt, hint }`. Returns `true`. |
|
|
129
|
-
| 7 | `pathOnly === routePath` and `options.template` is set | `200 OK` (`text/html; charset=utf-8`) with the template builder's output. Returns `true`. |
|
|
130
|
-
| 8 | `pathOnly === routePath` and no template | `200 OK` (`text/html; charset=utf-8`) with `renderDocsHtml(...)`. Returns `true`. |
|
|
131
|
-
|
|
132
|
-
The order is critical: the env/bind gate runs before the `authorize` hook and before method-checking, so a `POST /_docs` in production (or on a public-bind server) still returns `404` — it's gated first — not `405`, and `authorize` is never consulted on a gated request. The gate fires on `NODE_ENV === 'production'` **or** a non-loopback bind address (`!isLoopbackIp(getBindAddress().ip)`), so a staging/preview server bound to a public interface is locked down even when `NODE_ENV` is not `production`; set `enabledInProd: true` to serve the route there. The match itself ignores query strings (the path is split on `?` before comparing) and is strict on trailing slashes — `/_docs/` does NOT match the default route.
|
|
133
|
-
|
|
134
|
-
## Response shapes
|
|
135
|
-
|
|
136
|
-
### HTML page (`GET ${routePath}`)
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
HTTP/1.1 200 OK
|
|
140
|
-
Content-Type: text/html; charset=utf-8
|
|
141
|
-
Cache-Control: no-store
|
|
142
|
-
|
|
143
|
-
<!DOCTYPE html>
|
|
144
|
-
<html lang="en">
|
|
145
|
-
...self-contained docs page (see ./html-generation.md)...
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### JSON endpoint, success (`GET ${routePath}/api.json`)
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
HTTP/1.1 200 OK
|
|
152
|
-
Content-Type: application/json
|
|
153
|
-
Cache-Control: no-store
|
|
154
|
-
|
|
155
|
-
{ "apis": { "<page>": [ { "name": "<name>", "version": "<version>", "method": "POST", ... } ] }, "syncs": { "<page>": [ { ... } ] } }
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
The file is streamed as-is from disk; the handler does not parse, validate, or transform it. Forward-compatible extension fields (see `./extension-fields.md`) pass through unchanged.
|
|
159
|
-
|
|
160
|
-
### JSON endpoint, missing file
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
HTTP/1.1 404 Not Found
|
|
164
|
-
Content-Type: application/json
|
|
165
|
-
|
|
166
|
-
{
|
|
167
|
-
"error": "apiDocs.generated.json not found",
|
|
168
|
-
"expectedAt": "<resolved apiDocsPath>",
|
|
169
|
-
"hint": "Run `npm run generateArtifacts` to generate it."
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
This is intentionally a structured payload so the client-side renderer can surface a helpful empty state. `Cache-Control` is omitted on the error path — the request should be retried after running the artifact generator.
|
|
174
|
-
|
|
175
|
-
`expectedAt` is **dev-only**: it exposes the absolute filesystem path and is omitted when `NODE_ENV === 'production'` to avoid leaking internal directory structure (DOCSUI-8). In production the payload contains only `error` and `hint`.
|
|
176
|
-
|
|
177
|
-
### Production lockdown (`enabledInProd: false`)
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
HTTP/1.1 404 Not Found
|
|
181
|
-
Content-Type: text/plain
|
|
182
|
-
|
|
183
|
-
Not Found
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
The same plain `Not Found` is returned for both the HTML and the JSON sub-route, so probing tools cannot infer which routes the handler owns.
|
|
187
|
-
|
|
188
|
-
### Wrong method
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
HTTP/1.1 405 Method Not Allowed
|
|
192
|
-
Content-Type: text/plain
|
|
193
|
-
|
|
194
|
-
Method Not Allowed
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
`POST`, `PUT`, `DELETE`, and other verbs all map to `405`. The `enableTryItOut` runner posts to `/api/<page>/<name>/<version>?stream=false` (the framework's HTTP API surface), not to the docs route — so `GET`-only is sufficient.
|
|
198
|
-
|
|
199
|
-
## `apiDocsPath` resolution
|
|
200
|
-
|
|
201
|
-
The default `apiDocsPath` is resolved lazily on every request via `getGeneratedApiDocsPath()` from `@luckystack/core`. That helper reads `ProjectConfig.paths.generatedApiDocs`, which is populated either via the framework defaults or via an overlay's `registerProjectConfig` call.
|
|
202
|
-
|
|
203
|
-
When you pass `apiDocsPath` explicitly:
|
|
204
|
-
|
|
205
|
-
- The value must be an absolute path. The handler does no resolution relative to `process.cwd()`.
|
|
206
|
-
- The file is read with `fs.promises.readFile(path, 'utf8')` on every request — there is no in-memory cache. The doc-set is dev-only and the artifact regenerates often; caching is intentionally avoided.
|
|
207
|
-
- Read errors are caught via `tryCatch`. The response is a structured `404` with `expectedAt` pointing at the resolved path so you can diff it against your actual layout.
|
|
208
|
-
|
|
209
|
-
## Edge cases
|
|
210
|
-
|
|
211
|
-
- **Missing `apiDocs.generated.json`** — Returns the structured `404` JSON payload above. Run `npm run generateArtifacts` to produce it.
|
|
212
|
-
- **Trailing slashes** — The handler does strict string equality on the resolved path. `/_docs/` is treated as a different route from `/_docs`. Configure `routePath` to include or omit the trailing slash consistently with the rest of your router.
|
|
213
|
-
- **Query strings** — The path is split on `?` before comparison, so `/_docs?theme=dark` matches. Query parameters are otherwise ignored by the handler.
|
|
214
|
-
- **Custom `routePath`** — Any string is accepted; the `${routePath}/api.json` sibling is derived by simple concatenation. Avoid trailing slashes in `routePath` (`/_docs/` would yield `/_docs//api.json`).
|
|
215
|
-
- **`HEAD` requests** — Not special-cased. They fall into the `405` branch. If you need `HEAD` support, wrap the handler.
|
|
216
|
-
- **Concurrent requests** — Each request reads the JSON file independently. There is no shared mutable state inside the handler.
|
|
217
|
-
- **`req.url` missing** — The handler treats an undefined `url` as an empty string, which never matches `routePath`. It returns `false` (pass-through) cleanly.
|
|
218
|
-
|
|
219
|
-
## Related
|
|
220
|
-
|
|
221
|
-
- HTML rendering pipeline: `./html-generation.md`
|
|
222
|
-
- Branding and full template replacement: `./theming.md`
|
|
223
|
-
- Extension fields rendered from `apiDocs.generated.json`: `./extension-fields.md`
|
|
224
|
-
- AI summary + function index: `../CLAUDE.md`
|
|
225
|
-
- Consumer quickstart: `../README.md`
|
|
226
|
-
- Source: `../src/index.ts`
|
|
5
|
+
**Default production lockdown is intentional.** Even without an interactive request runner, generated endpoint names, auth requirements and data shapes may be sensitive. If production access is necessary, explicitly enable it and enforce per-request authorization before returning both the page and its JSON. A non-loopback development bind also warrants access review. A missing generated file is not authorization to serve old or invented documentation: regenerate artifacts instead.
|
package/docs/theming.md
CHANGED
|
@@ -1,172 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Docs UI theming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lightweight branding lets an internal documentation page resemble its host project without requiring a frontend build. Use the built-in branding option for cosmetic changes; a full custom template is appropriate only when the document structure must change. The custom template takes responsibility for escaped content, accessibility and secure request-runner behavior.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- `DocsBranding` — the three branding tokens and how they map to CSS variables.
|
|
8
|
-
- The default palette + dark/light auto-switch.
|
|
9
|
-
- `DocsTemplateBuilder` — the escape hatch for full layout replacement.
|
|
10
|
-
- When to choose `branding` vs `template`.
|
|
11
|
-
|
|
12
|
-
For the renderer that consumes these tokens see `./html-generation.md`. For the route handler that wires them in see `./mounting.md`.
|
|
13
|
-
|
|
14
|
-
## `DocsBranding`
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
export interface DocsBranding {
|
|
18
|
-
logoUrl?: string;
|
|
19
|
-
brandColor?: string;
|
|
20
|
-
fontFamily?: string;
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
All three keys are optional. Pass through `mountDocsUi({ branding: { ... } })` or directly to `renderDocsHtml(jsonPath, pageTitle, { branding })`.
|
|
25
|
-
|
|
26
|
-
| Token | Type | Default | Mapping |
|
|
27
|
-
| --- | --- | --- | --- |
|
|
28
|
-
| `logoUrl` | `string` | _(no logo)_ | Injected as `<img src="..." alt="logo" style="height:32px;width:auto;margin-right:12px;" />` in the brand row, to the left of the `<h1>`. The URL is HTML-escaped before injection. |
|
|
29
|
-
| `brandColor` | `string` (CSS color) | `#58a6ff` | Sets the CSS variable `--accent` and (because POST is the canonical authenticated verb in LuckyStack) also `--post`, the POST method pill color. |
|
|
30
|
-
| `fontFamily` | `string` (CSS font-family) | `system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif` | Sets `--font-family`. Applied to `<body>` and inherited by inputs (the search bar uses `font-family: inherit`). |
|
|
31
|
-
|
|
32
|
-
The renderer applies these tokens in the inline `<style>` block at the top of the document. There is no runtime style swap; changing branding requires a fresh HTML response.
|
|
33
|
-
|
|
34
|
-
### Logo handling
|
|
35
|
-
|
|
36
|
-
- The renderer accepts `https:`, `http:`, and scheme-free relative URLs (e.g. `/logo.svg`). `data:` and `javascript:` URLs are **rejected** — the logo will be silently omitted — because SVG data-URIs can carry script.
|
|
37
|
-
- The string is escaped via the same `escapeHtml` helper used for `pageTitle` and `jsonPath`. Embedding hostile content in the `logoUrl` cannot break out of the `src` attribute.
|
|
38
|
-
- The img tag is fixed at `height: 32px; width: auto;`. If you need a different size, embed an SVG with a `viewBox` so it scales correctly, or use a `template` builder.
|
|
39
|
-
- When `logoUrl` is omitted, the brand row contains only the `<h1>`. There is no placeholder block.
|
|
40
|
-
|
|
41
|
-
### Brand color cascade
|
|
42
|
-
|
|
43
|
-
The branding color is wired into two CSS variables:
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
--accent: <brandColor>
|
|
47
|
-
--post: <brandColor> // (POST method pill)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
All other method colors are fixed:
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
--get: #3fb950 (green)
|
|
54
|
-
--put: #d29922 (amber, with dark text)
|
|
55
|
-
--delete: #f85149 (red)
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
This intentional split keeps GET/PUT/DELETE recognizable across themes while letting the POST pill match the brand. If you need per-method theming, use a `template` builder.
|
|
59
|
-
|
|
60
|
-
### Font handling
|
|
61
|
-
|
|
62
|
-
`fontFamily` flows into `--font-family` and is consumed by `<body>` via `font-family: var(--font-family);`. The `<input type="search">` uses `font-family: inherit` so the search bar adopts the brand font. Code blocks (`pre`, `.endpoint`, `.try-it-out textarea`) use a fixed monospace stack (`ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace`) and are not affected by `fontFamily`.
|
|
63
|
-
|
|
64
|
-
Quoting and escaping: `fontFamily` is interpolated **as-is** into the CSS string. Provide a valid CSS font-family value, including quotes around multi-word family names when needed (e.g. `'Inter', system-ui, sans-serif`).
|
|
65
|
-
|
|
66
|
-
## Dark / light auto-switch
|
|
67
|
-
|
|
68
|
-
The page respects the OS preference via `prefers-color-scheme`. There is no in-page theme toggle.
|
|
69
|
-
|
|
70
|
-
Dark palette (default `:root` values):
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
--bg: #0e1116 --container: #161b22 --container-hover: #1c232b
|
|
74
|
-
--border: #2a313c --title: #e6edf3 --common: #c9d1d9
|
|
75
|
-
--muted: #8b949e
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Light palette (overridden inside `@media (prefers-color-scheme: light)`):
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
--bg: #ffffff --container: #f6f8fa --container-hover: #eaeef2
|
|
82
|
-
--border: #d0d7de --title: #1f2328 --common: #1f2328
|
|
83
|
-
--muted: #59636e
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Variables that are **not** overridden in the light block — `--accent`, `--font-family`, `--get`, `--post`, `--put`, `--delete` — are chosen to be legible against both surfaces. The PUT pill flips its text color to dark via `.method.PUT { color: #1f2328; }` to remain legible on amber.
|
|
87
|
-
|
|
88
|
-
## When to use `branding` vs `template`
|
|
89
|
-
|
|
90
|
-
Use `branding` (default template) when:
|
|
91
|
-
|
|
92
|
-
- You only need to swap the logo, the accent color, and/or the font.
|
|
93
|
-
- You're happy with the single-column endpoints-grouped-by-page layout.
|
|
94
|
-
- You want light/dark auto-switching for free.
|
|
95
|
-
- You don't need to change the inline runner, the filter UI, or the JSON-fetch flow.
|
|
96
|
-
|
|
97
|
-
Use `template: DocsTemplateBuilder` when:
|
|
98
|
-
|
|
99
|
-
- You need a different layout — sidebar navigation, tabs, a marketing-page wrapper, an embedded auth-token field, a two-pane editor/inspector view.
|
|
100
|
-
- You want a per-method palette beyond the POST = `brandColor` cascade.
|
|
101
|
-
- You need to integrate with a corporate design system (component library, web fonts loaded from a CDN, etc.).
|
|
102
|
-
- You want an in-page light/dark toggle independent of the OS preference.
|
|
103
|
-
|
|
104
|
-
## Example — corporate brand palette
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import { mountDocsUi } from '@luckystack/docs-ui';
|
|
108
|
-
|
|
109
|
-
export const docsUiHandler = mountDocsUi({
|
|
110
|
-
pageTitle: 'Acme — API docs',
|
|
111
|
-
branding: {
|
|
112
|
-
logoUrl: 'https://cdn.acme.example/logo.svg',
|
|
113
|
-
brandColor: '#7c3aed',
|
|
114
|
-
fontFamily: `'Inter', system-ui, -apple-system, sans-serif`,
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
This renders the default layout with the Acme logo, purple accent (also applied to the POST pill), and Inter as the body font. GET/PUT/DELETE pills remain green/amber/red.
|
|
120
|
-
|
|
121
|
-
## Example — full template replacement
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
import { mountDocsUi, type DocsTemplateBuilder } from '@luckystack/docs-ui';
|
|
125
|
-
|
|
126
|
-
const sidebarTemplate: DocsTemplateBuilder = ({ jsonPath, pageTitle, branding }) => `
|
|
127
|
-
<!DOCTYPE html>
|
|
128
|
-
<html lang="en">
|
|
129
|
-
<head>
|
|
130
|
-
<meta charset="utf-8" />
|
|
131
|
-
<title>${pageTitle}</title>
|
|
132
|
-
<link rel="stylesheet" href="https://cdn.example.com/corporate-tokens.css" />
|
|
133
|
-
</head>
|
|
134
|
-
<body>
|
|
135
|
-
<aside class="sidebar">${branding.logoUrl ? `<img src="${branding.logoUrl}" />` : ''}</aside>
|
|
136
|
-
<main id="content">Loading...</main>
|
|
137
|
-
<script>
|
|
138
|
-
fetch(${JSON.stringify(jsonPath)}, { credentials: 'include' })
|
|
139
|
-
.then(r => r.json())
|
|
140
|
-
.then(data => { /* render however you like */ });
|
|
141
|
-
</script>
|
|
142
|
-
</body>
|
|
143
|
-
</html>`;
|
|
144
|
-
|
|
145
|
-
export const docsUiHandler = mountDocsUi({
|
|
146
|
-
template: sidebarTemplate,
|
|
147
|
-
branding: { logoUrl: 'https://cdn.example.com/logo.svg' },
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Contract reminders for custom templates (see `./html-generation.md` for the full contract):
|
|
152
|
-
|
|
153
|
-
- Return a complete `<!DOCTYPE html>...</html>` document.
|
|
154
|
-
- HTML-escape any inputs that end up in markup — the builder receives raw strings.
|
|
155
|
-
- Fetch `jsonPath` yourself; the handler does not pre-fetch.
|
|
156
|
-
- You may receive a non-empty `branding` even when you don't use it — that's fine, ignore the fields you don't need.
|
|
157
|
-
|
|
158
|
-
## Limits
|
|
159
|
-
|
|
160
|
-
- No per-endpoint color override. The method pill colors are fixed CSS variables.
|
|
161
|
-
- No runtime theme switcher beyond OS preference in the default template. Implement one inside a custom template if needed.
|
|
162
|
-
- No web-font loading. The default template does not inject `<link rel="stylesheet">` tags — bring your own via a custom template.
|
|
163
|
-
- No localization. Strings like "Try it out (live request)", "No matches.", "Run `npm run generateArtifacts`" are hardcoded English. Replace via a custom template if you need translations.
|
|
164
|
-
- `brandColor` must be a valid single CSS color value (hex, named, `rgb()`, `hsl()`). Gradients are not supported.
|
|
165
|
-
|
|
166
|
-
## Related
|
|
167
|
-
|
|
168
|
-
- The renderer that consumes these tokens: `./html-generation.md`
|
|
169
|
-
- The mount handler that accepts them: `./mounting.md`
|
|
170
|
-
- The JSON fields the page renders: `./extension-fields.md`
|
|
171
|
-
- AI summary + function index: `../CLAUDE.md`
|
|
172
|
-
- Source: `../src/index.ts`, `../src/docsHtml.ts`
|
|
5
|
+
The installed renderer and types define supported branding fields. Avoid copying a palette/default table here: values and theme behavior belong in source, while the important design boundary is that appearance changes do not bypass the environment or authorization gate. See `mounting.md` before enabling production access.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckystack/docs-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"files": [
|
|
45
45
|
"dist",
|
|
46
46
|
"README.md",
|
|
47
|
-
"
|
|
47
|
+
"AGENTS.md",
|
|
48
48
|
"docs",
|
|
49
49
|
"LICENSE",
|
|
50
50
|
"CHANGELOG.md"
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"test": "vitest run"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@luckystack/core": "^0.
|
|
58
|
+
"@luckystack/core": "^0.11.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@luckystack/server": "^0.
|
|
61
|
+
"@luckystack/server": "^0.11.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
64
64
|
"@luckystack/server": {
|
package/CLAUDE.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# @luckystack/docs-ui
|
|
2
|
-
|
|
3
|
-
> AI summary + function INDEX. For deep specs see `docs/` next to this file.
|
|
4
|
-
|
|
5
|
-
## What this package does
|
|
6
|
-
|
|
7
|
-
Dev-only API docs browser for LuckyStack. Mounts a single page (default `/_docs`) that fetches the framework's generated `apiDocs.generated.json` and renders every API endpoint grouped by page, with HTTP method, auth, rate limit, input shape, and output shape. Sync events appear alongside APIs when the type-map emitter has produced metadata for them. Ships a `customRoutes` handler — composable with `createLuckyStackServer`'s `customRoutes` option without taking ownership of the request pipeline.
|
|
8
|
-
|
|
9
|
-
## When to USE this package
|
|
10
|
-
|
|
11
|
-
- Local development: developer wants a Swagger-style overview of every route in the project without leaving the browser.
|
|
12
|
-
- Internal developer-portal: opt-in production rendering via `enabledInProd: true` for an authenticated team-only deploy.
|
|
13
|
-
- Custom branded API docs: replace the default template via the `template: DocsTemplateBuilder` option to render in a sidebar/tabs/marketing layout while reusing the JSON endpoint.
|
|
14
|
-
- Inline request runner: enable `enableTryItOut` so each endpoint gets a textarea + Send button that posts against the live server using the browser session cookie.
|
|
15
|
-
|
|
16
|
-
## When to NOT suggest this (yet)
|
|
17
|
-
|
|
18
|
-
- Public production surface where API discoverability is sensitive — keep the default `enabledInProd: false` and don't mount in prod.
|
|
19
|
-
- Replacement for `apiDocs.generated.json` itself — this package only reads/renders that file; regenerate via `@luckystack/devkit`'s type-map emitter (`npm run generateArtifacts`).
|
|
20
|
-
- A non-HTTP transport — the handler is bound to `IncomingMessage` / `ServerResponse`; do not wire it directly into Socket.io handlers.
|
|
21
|
-
- React-tree integration — the docs UI is a single self-contained HTML document with no React/Tailwind dependency on the client; do not import `renderDocsHtml` into the app bundle.
|
|
22
|
-
|
|
23
|
-
## Function Index
|
|
24
|
-
|
|
25
|
-
| Function / Export | 1-regel | Deep doc |
|
|
26
|
-
|---|---|---|
|
|
27
|
-
| `mountDocsUi(options?)` | Returns a `DocsRouteHandler` for `customRoutes`; serves HTML + JSON | → docs/mounting.md |
|
|
28
|
-
| `renderDocsHtml(jsonPath, pageTitle, options?)` | Builds the self-contained docs HTML document | → docs/html-generation.md |
|
|
29
|
-
| Type: `DocsRouteHandler` | `(req, res) => Promise<boolean>`; returns `true` when route handled | → docs/mounting.md |
|
|
30
|
-
| Type: `DocsTemplateBuilder` | Custom HTML builder hook: `({ jsonPath, pageTitle, branding }) => string` | → docs/html-generation.md |
|
|
31
|
-
| Type: `MountDocsUiOptions` | All mount inputs (routePath, enabledInProd, branding, template, enableTryItOut, ...) | → docs/mounting.md |
|
|
32
|
-
| Type: `DocsBranding` | `{ logoUrl?, brandColor?, fontFamily? }` applied by default template | → docs/theming.md |
|
|
33
|
-
| Type: `RenderDocsHtmlOptions` | `{ branding?, enableTryItOut? }` passed to `renderDocsHtml` | → docs/html-generation.md |
|
|
34
|
-
| Extension field: `meta.stream` | Per-endpoint stream shape rendered in expanded detail panel | → docs/extension-fields.md |
|
|
35
|
-
| Extension field: `meta.owner` | Owner string rendered in expanded detail panel | → docs/extension-fields.md |
|
|
36
|
-
| Extension field: `meta.tags` | Tag badges rendered in expanded detail panel | → docs/extension-fields.md |
|
|
37
|
-
| Extension field: `meta.deprecated` | Deprecation note (red) rendered in expanded detail panel | → docs/extension-fields.md |
|
|
38
|
-
|
|
39
|
-
## Config keys (env vars + registerProjectConfig slots)
|
|
40
|
-
|
|
41
|
-
- `NODE_ENV` (env) — when `'production'`, the route returns `404` unless `enabledInProd: true` is set.
|
|
42
|
-
- `ProjectConfig.paths.generatedApiDocs` (config, resolved via `getGeneratedApiDocsPath()` from `@luckystack/core`) — default JSON file path; override per-mount via `options.apiDocsPath`.
|
|
43
|
-
- No package-private env vars. All behavior is controlled via `MountDocsUiOptions`.
|
|
44
|
-
|
|
45
|
-
## Peer dependencies
|
|
46
|
-
|
|
47
|
-
- **Required**: `@luckystack/core` (provides `getGeneratedApiDocsPath` + `tryCatch`).
|
|
48
|
-
- **Required (runtime composition)**: `@luckystack/server` — the handler is intended to be passed to `createLuckyStackServer({ customRoutes })` / `bootstrapLuckyStack({ customRoutes })`. The package itself does not import `@luckystack/server`; the dependency is on the consumer side.
|
|
49
|
-
- **Required (data source)**: `@luckystack/devkit` (build-time) — emits `apiDocs.generated.json`. Without it the JSON endpoint returns a 404 with a hint to run `npm run generateArtifacts`.
|
|
50
|
-
|
|
51
|
-
## Related
|
|
52
|
-
|
|
53
|
-
- Architecture deep-dives: `/docs/ARCHITECTURE_API.md` (lifecycle of documented endpoints), `/docs/ARCHITECTURE_PACKAGING.md` (how the JSON artifact is produced).
|
|
54
|
-
- README (consumer quickstart): `./README.md`.
|
|
55
|
-
- Source: `./src/index.ts` (mount + route handling), `./src/docsHtml.ts` (embedded HTML renderer).
|