@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2
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/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- package/package.json +198 -4
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `docsUi()`: a {@link BunRouter} serving the API-docs viewer — the HTML shell
|
|
3
|
+
* for every client route under `basePath`, the hashed bundle under
|
|
4
|
+
* `<basePath>/assets/`, and a `document` thunk's document at
|
|
5
|
+
* `<basePath>/document.json`.
|
|
6
|
+
*
|
|
7
|
+
* Like `jobsUi()`, its routes are registered **relative to the mount** (`/`,
|
|
8
|
+
* `/*`, `/assets/:file`, `/document.json`), so it is mounted with
|
|
9
|
+
* `app.use(ui.basePath, ui.router)` and `ui.router.fetch("/")` works on its
|
|
10
|
+
* own. The mount path must equal `basePath`: the injected configuration and
|
|
11
|
+
* every asset URL are built from it.
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
BunRequest,
|
|
15
|
+
BunResponse,
|
|
16
|
+
Logger,
|
|
17
|
+
RouterHandler,
|
|
18
|
+
} from "@kingsleyweb/bun-common";
|
|
19
|
+
import type { UiAsset, UiAssets } from "./assets";
|
|
20
|
+
import type { DocsUiConfig, DocsUiTheme } from "./browser/config";
|
|
21
|
+
import type { UiUnauthorized } from "../kit/browser/unauthorized";
|
|
22
|
+
import type { ShellRenderer } from "./shell";
|
|
23
|
+
import type {
|
|
24
|
+
DocsUi,
|
|
25
|
+
DocsUiAuthorize,
|
|
26
|
+
DocsUiAuthorizeContext,
|
|
27
|
+
DocsUiOptions,
|
|
28
|
+
} from "./types";
|
|
29
|
+
import { BunRouter, ConfigError, resolveLogger } from "@kingsleyweb/bun-common";
|
|
30
|
+
import { Buffer } from "node:buffer";
|
|
31
|
+
import { ASSET_CACHE_CONTROL } from "../kit/browser/shared";
|
|
32
|
+
import { DEFAULT_DIST_DIR, DEFAULT_ENTRY, resolveAssetSource } from "./assets";
|
|
33
|
+
import { DEFAULT_EXTENSION_PREFIXES } from "./browser/config";
|
|
34
|
+
import {
|
|
35
|
+
createNonce,
|
|
36
|
+
createShellRenderer,
|
|
37
|
+
cspHeader,
|
|
38
|
+
documentOriginSource,
|
|
39
|
+
} from "./shell";
|
|
40
|
+
import {
|
|
41
|
+
redirectsDenial,
|
|
42
|
+
resolveUnauthorized,
|
|
43
|
+
sendUnauthorizedRedirect,
|
|
44
|
+
} from "../kit/unauthorized";
|
|
45
|
+
|
|
46
|
+
/** The viewer's default mount path. */
|
|
47
|
+
export const DEFAULT_BASE_PATH = "/docs";
|
|
48
|
+
|
|
49
|
+
/** The default page title. */
|
|
50
|
+
export const DEFAULT_TITLE = "API docs";
|
|
51
|
+
|
|
52
|
+
/** The path under `basePath` the bundle is served from. */
|
|
53
|
+
export const ASSETS_SUBPATH = "/assets";
|
|
54
|
+
|
|
55
|
+
/** The path under `basePath` a `document` thunk is served from. */
|
|
56
|
+
export const DOCUMENT_SUBPATH = "/document.json";
|
|
57
|
+
|
|
58
|
+
export { ASSET_CACHE_CONTROL };
|
|
59
|
+
|
|
60
|
+
/** A path segment `basePath` may contain — the bun-jobs API's own rule. */
|
|
61
|
+
const PATH_SEGMENT = /^[\w.~-]+$/;
|
|
62
|
+
|
|
63
|
+
/** A header name (an RFC 9110 token). */
|
|
64
|
+
const HEADER_NAME = /^[!#$%&'*+.^`|~\w-]+$/;
|
|
65
|
+
|
|
66
|
+
/** An extension prefix (`x-bun-common`): `x-`, then a dotted-lowercase name. */
|
|
67
|
+
const EXTENSION_PREFIX = /^x-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
68
|
+
|
|
69
|
+
/** Where the bundle comes from; tests point these at a fixture. */
|
|
70
|
+
export interface DocsUiInternals {
|
|
71
|
+
/** The dist directory read when present. Defaults to the package's `dist/`. */
|
|
72
|
+
distDir?: string;
|
|
73
|
+
/** The entry built in memory without a dist. Defaults to `app/main.tsx`. */
|
|
74
|
+
entry?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validates and normalises an absolute mount path: absolute, trailing slashes
|
|
79
|
+
* dropped, not `"/"`, and only segments of letters, digits, `_`, `.`, `~` and
|
|
80
|
+
* `-`.
|
|
81
|
+
*/
|
|
82
|
+
export function normalizeBasePath(value: unknown, what: string): string {
|
|
83
|
+
if (typeof value !== "string" || !value.startsWith("/")) {
|
|
84
|
+
throw new ConfigError(
|
|
85
|
+
`${what} must be an absolute path starting with "/"`,
|
|
86
|
+
{
|
|
87
|
+
value,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
92
|
+
if (trimmed === "") {
|
|
93
|
+
throw new ConfigError(
|
|
94
|
+
`${what} may not be "/": the viewer's catch-all would answer for every GET route of the host`,
|
|
95
|
+
{ value },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
for (const segment of trimmed.slice(1).split("/")) {
|
|
99
|
+
if (!PATH_SEGMENT.test(segment) || segment === "." || segment === "..") {
|
|
100
|
+
throw new ConfigError(
|
|
101
|
+
`${what} may only contain "/"-separated segments of letters, digits, "_", ".", "~" and "-"`,
|
|
102
|
+
{ value },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return trimmed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validates a `document` URL: a same-origin absolute path (no `//`), or an
|
|
111
|
+
* `http(s)://` URL without credentials whose path follows the segment rules.
|
|
112
|
+
* The rules are checked on the **raw** string, because `new URL()` has already
|
|
113
|
+
* resolved `..`, percent-encoded a space and turned `\` into `/`.
|
|
114
|
+
*/
|
|
115
|
+
function resolveDocumentUrl(documentUrl: string): string {
|
|
116
|
+
if (documentUrl === "") {
|
|
117
|
+
throw new ConfigError(
|
|
118
|
+
"document must be an absolute path, an http(s) URL or a thunk",
|
|
119
|
+
{ document: documentUrl },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (documentUrl.startsWith("/") && !documentUrl.startsWith("//")) {
|
|
123
|
+
normalizeBasePath(documentUrl.split(/[?#]/)[0], "document");
|
|
124
|
+
return documentUrl;
|
|
125
|
+
}
|
|
126
|
+
let url: URL;
|
|
127
|
+
try {
|
|
128
|
+
url = new URL(documentUrl);
|
|
129
|
+
} catch {
|
|
130
|
+
throw new ConfigError(
|
|
131
|
+
"document must be an absolute path, an http(s) URL or a thunk",
|
|
132
|
+
{ document: documentUrl },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
136
|
+
throw new ConfigError("document must use http: or https:", {
|
|
137
|
+
document: documentUrl,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (url.username !== "" || url.password !== "") {
|
|
141
|
+
throw new ConfigError("document may not carry credentials", {
|
|
142
|
+
document: documentUrl,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const rawPath = /^https?:\/\/[^/?#]*([^?#]*)/i.exec(documentUrl)?.[1] ?? "";
|
|
146
|
+
const path = /^\/*$/.test(rawPath)
|
|
147
|
+
? ""
|
|
148
|
+
: normalizeBasePath(rawPath, "document");
|
|
149
|
+
if (url.pathname.replace(/\/+$/, "") !== path) {
|
|
150
|
+
throw new ConfigError(
|
|
151
|
+
`document's path may only contain "/"-separated segments of letters, digits, "_", ".", "~" and "-"`,
|
|
152
|
+
{ document: documentUrl },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return documentUrl;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Validates `extensions`. */
|
|
159
|
+
function resolveExtensions(value: unknown): readonly string[] {
|
|
160
|
+
if (value === undefined) {
|
|
161
|
+
return DEFAULT_EXTENSION_PREFIXES;
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
164
|
+
throw new ConfigError(
|
|
165
|
+
"extensions must be a non-empty array of extension prefixes",
|
|
166
|
+
{ extensions: value },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
for (const prefix of value) {
|
|
170
|
+
if (typeof prefix !== "string" || !EXTENSION_PREFIX.test(prefix)) {
|
|
171
|
+
throw new ConfigError(
|
|
172
|
+
`extensions entries must look like "x-bun-common": "x-", then a lowercase name`,
|
|
173
|
+
{ extensions: value },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return Object.freeze([...value]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Validates `theme`. */
|
|
181
|
+
function resolveTheme(value: unknown): DocsUiTheme {
|
|
182
|
+
if (value === undefined) {
|
|
183
|
+
return "system";
|
|
184
|
+
}
|
|
185
|
+
if (value === "system" || value === "light" || value === "dark") {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
throw new ConfigError(`theme must be "system", "light" or "dark"`, {
|
|
189
|
+
value,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Validates `csrfHeader`. */
|
|
194
|
+
function resolveCsrfHeader(value: unknown): string | null {
|
|
195
|
+
if (value === undefined || value === false) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (typeof value !== "string" || !HEADER_NAME.test(value)) {
|
|
199
|
+
throw new ConfigError("csrfHeader must be a header name or false", {
|
|
200
|
+
value,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A frozen copy of the config, nested objects included. */
|
|
207
|
+
function freezeConfig(config: DocsUiConfig): Readonly<DocsUiConfig> {
|
|
208
|
+
for (const value of Object.values(config)) {
|
|
209
|
+
if (typeof value === "object" && value !== null) {
|
|
210
|
+
Object.freeze(value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return Object.freeze(config);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Sends an RFC 9457 problem. */
|
|
217
|
+
function sendProblem(
|
|
218
|
+
req: BunRequest,
|
|
219
|
+
res: BunResponse,
|
|
220
|
+
status: number,
|
|
221
|
+
code: string,
|
|
222
|
+
title: string,
|
|
223
|
+
detail: string,
|
|
224
|
+
): void {
|
|
225
|
+
res.status(status);
|
|
226
|
+
res.setHeader("Content-Type", "application/problem+json");
|
|
227
|
+
res.setHeader("Cache-Control", "no-store");
|
|
228
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
229
|
+
if (req.method === "HEAD") {
|
|
230
|
+
void res.end();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
res.send(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
type: `urn:bun-api-docs-ui:error:${code}`,
|
|
236
|
+
title,
|
|
237
|
+
status,
|
|
238
|
+
code,
|
|
239
|
+
detail,
|
|
240
|
+
instance: req.path,
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Normalises an `authorize` answer: a denial is 401 only for `status: 401`,
|
|
247
|
+
* 403 for any other status, and anything unrecognised denies with 403.
|
|
248
|
+
*/
|
|
249
|
+
function decide(result: unknown):
|
|
250
|
+
| { allow: true }
|
|
251
|
+
| {
|
|
252
|
+
allow: false;
|
|
253
|
+
status: 401 | 403;
|
|
254
|
+
reason?: string;
|
|
255
|
+
} {
|
|
256
|
+
if (result === true) {
|
|
257
|
+
return { allow: true };
|
|
258
|
+
}
|
|
259
|
+
if (typeof result === "object" && result !== null && "allow" in result) {
|
|
260
|
+
const answer = result as {
|
|
261
|
+
allow: unknown;
|
|
262
|
+
status?: unknown;
|
|
263
|
+
reason?: unknown;
|
|
264
|
+
};
|
|
265
|
+
if (answer.allow === true) {
|
|
266
|
+
return { allow: true };
|
|
267
|
+
}
|
|
268
|
+
if (answer.allow === false) {
|
|
269
|
+
return {
|
|
270
|
+
allow: false,
|
|
271
|
+
status: answer.status === 401 ? 401 : 403,
|
|
272
|
+
...(typeof answer.reason === "string" && answer.reason !== ""
|
|
273
|
+
? { reason: answer.reason }
|
|
274
|
+
: {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { allow: false, status: 403 };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The `authorize` step of every viewer route. With `unauthorized`, a denial
|
|
283
|
+
* whose status it lists answers a page navigation of the shell with a 303 to
|
|
284
|
+
* the target; a bundle file, the document and anything that is not a
|
|
285
|
+
* navigation keep the problem.
|
|
286
|
+
*/
|
|
287
|
+
function authorizeStep(
|
|
288
|
+
authorize: DocsUiAuthorize,
|
|
289
|
+
route: "asset" | "document" | "shell",
|
|
290
|
+
logger: Logger,
|
|
291
|
+
unauthorized: UiUnauthorized | null,
|
|
292
|
+
): RouterHandler {
|
|
293
|
+
const asset = route === "asset";
|
|
294
|
+
const shell = route === "shell";
|
|
295
|
+
return async (req, res, next) => {
|
|
296
|
+
const context: DocsUiAuthorizeContext = { asset, path: req.path };
|
|
297
|
+
let decision: ReturnType<typeof decide>;
|
|
298
|
+
try {
|
|
299
|
+
decision = decide(await authorize(req, context));
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logger.error("api docs ui authorize threw; refusing the request", {
|
|
302
|
+
error,
|
|
303
|
+
path: req.path,
|
|
304
|
+
});
|
|
305
|
+
sendProblem(
|
|
306
|
+
req,
|
|
307
|
+
res,
|
|
308
|
+
500,
|
|
309
|
+
"INTERNAL",
|
|
310
|
+
"Internal server error",
|
|
311
|
+
"Internal server error",
|
|
312
|
+
);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (decision.allow) {
|
|
316
|
+
return next();
|
|
317
|
+
}
|
|
318
|
+
if (redirectsDenial(unauthorized, shell, decision.status, req)) {
|
|
319
|
+
sendUnauthorizedRedirect(req, res, unauthorized);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (decision.status === 401) {
|
|
323
|
+
sendProblem(
|
|
324
|
+
req,
|
|
325
|
+
res,
|
|
326
|
+
401,
|
|
327
|
+
"UNAUTHORIZED",
|
|
328
|
+
"Authentication required",
|
|
329
|
+
decision.reason ?? "Authentication required",
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
sendProblem(
|
|
333
|
+
req,
|
|
334
|
+
res,
|
|
335
|
+
403,
|
|
336
|
+
"FORBIDDEN",
|
|
337
|
+
"Forbidden",
|
|
338
|
+
decision.reason ?? "Not allowed to open this page",
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Creates the viewer with an explicit bundle source. {@link docsUi} is this
|
|
346
|
+
* with the package's own `dist/` and `app/main.tsx`; tests point it at a
|
|
347
|
+
* fixture.
|
|
348
|
+
*/
|
|
349
|
+
export function createDocsUi(
|
|
350
|
+
options: DocsUiOptions,
|
|
351
|
+
internals: DocsUiInternals = {},
|
|
352
|
+
): DocsUi {
|
|
353
|
+
if (typeof options !== "object" || options === null) {
|
|
354
|
+
throw new ConfigError("docsUi() needs an options object", {});
|
|
355
|
+
}
|
|
356
|
+
const { document } = options;
|
|
357
|
+
const thunk = typeof document === "function" ? document : undefined;
|
|
358
|
+
if (thunk === undefined && typeof document !== "string") {
|
|
359
|
+
throw new ConfigError(
|
|
360
|
+
"document must be an absolute path, an http(s) URL or a thunk",
|
|
361
|
+
{ document },
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const basePath = normalizeBasePath(
|
|
366
|
+
options.basePath ?? DEFAULT_BASE_PATH,
|
|
367
|
+
"basePath",
|
|
368
|
+
);
|
|
369
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
370
|
+
if (typeof title !== "string" || title.trim() === "") {
|
|
371
|
+
throw new ConfigError("title must be a non-empty string", { title });
|
|
372
|
+
}
|
|
373
|
+
const middleware = options.middleware ?? [];
|
|
374
|
+
if (
|
|
375
|
+
!Array.isArray(middleware) ||
|
|
376
|
+
!middleware.every((handler) => typeof handler === "function")
|
|
377
|
+
) {
|
|
378
|
+
throw new ConfigError("middleware must be an array of functions", {});
|
|
379
|
+
}
|
|
380
|
+
if (
|
|
381
|
+
options.authorize !== undefined &&
|
|
382
|
+
typeof options.authorize !== "function"
|
|
383
|
+
) {
|
|
384
|
+
throw new ConfigError("authorize must be a function", {});
|
|
385
|
+
}
|
|
386
|
+
if (options.dev !== undefined && typeof options.dev !== "boolean") {
|
|
387
|
+
throw new ConfigError("dev must be a boolean", { dev: options.dev });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const documentUrl =
|
|
391
|
+
thunk !== undefined
|
|
392
|
+
? `${basePath}${DOCUMENT_SUBPATH}`
|
|
393
|
+
: resolveDocumentUrl(document as string);
|
|
394
|
+
|
|
395
|
+
const config = freezeConfig({
|
|
396
|
+
version: 1,
|
|
397
|
+
title,
|
|
398
|
+
basePath,
|
|
399
|
+
assetsPath: `${basePath}${ASSETS_SUBPATH}`,
|
|
400
|
+
documentUrl,
|
|
401
|
+
csrfHeader: resolveCsrfHeader(options.csrfHeader),
|
|
402
|
+
extensions: resolveExtensions(options.extensions),
|
|
403
|
+
theme: resolveTheme(options.theme),
|
|
404
|
+
unauthorized: resolveUnauthorized(
|
|
405
|
+
options.unauthorized,
|
|
406
|
+
basePath,
|
|
407
|
+
(message, context) => new ConfigError(message, context),
|
|
408
|
+
),
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const logger = resolveLogger(options.logger).child({
|
|
412
|
+
component: "api-docs-ui",
|
|
413
|
+
});
|
|
414
|
+
const loadAssets = resolveAssetSource({
|
|
415
|
+
dev: options.dev,
|
|
416
|
+
distDir: internals.distDir ?? DEFAULT_DIST_DIR,
|
|
417
|
+
entry: internals.entry ?? DEFAULT_ENTRY,
|
|
418
|
+
logger,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// The shell is rendered from the bundle, so it waits for the first load.
|
|
422
|
+
let renderer: { assets: UiAssets; render: ShellRenderer } | undefined;
|
|
423
|
+
const ready = async (): Promise<{
|
|
424
|
+
assets: UiAssets;
|
|
425
|
+
render: ShellRenderer;
|
|
426
|
+
}> => {
|
|
427
|
+
if (renderer === undefined) {
|
|
428
|
+
const assets = await loadAssets();
|
|
429
|
+
renderer ??= { assets, render: createShellRenderer(config, assets) };
|
|
430
|
+
}
|
|
431
|
+
return renderer;
|
|
432
|
+
};
|
|
433
|
+
/** Loads the bundle, or answers 500 and returns `undefined`. */
|
|
434
|
+
const readyOrFail = async (
|
|
435
|
+
req: BunRequest,
|
|
436
|
+
res: BunResponse,
|
|
437
|
+
): Promise<{ assets: UiAssets; render: ShellRenderer } | undefined> => {
|
|
438
|
+
try {
|
|
439
|
+
return await ready();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger.error("api docs ui could not load its bundle", { error });
|
|
442
|
+
sendProblem(
|
|
443
|
+
req,
|
|
444
|
+
res,
|
|
445
|
+
500,
|
|
446
|
+
"INTERNAL",
|
|
447
|
+
"Internal server error",
|
|
448
|
+
"Internal server error",
|
|
449
|
+
);
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// A cross-origin document URL is the one connection beyond this origin.
|
|
455
|
+
const origin = documentOriginSource(documentUrl);
|
|
456
|
+
const connectSrc = origin === undefined ? [] : [origin];
|
|
457
|
+
const serveShell: RouterHandler = async (req, res) => {
|
|
458
|
+
const loaded = await readyOrFail(req, res);
|
|
459
|
+
if (!loaded) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const nonce = createNonce();
|
|
463
|
+
const html = loaded.render(nonce);
|
|
464
|
+
res.status(200);
|
|
465
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
466
|
+
res.setHeader("Cache-Control", "no-store");
|
|
467
|
+
res.setHeader("Content-Security-Policy", cspHeader({ nonce, connectSrc }));
|
|
468
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
469
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
470
|
+
if (req.method === "HEAD") {
|
|
471
|
+
void res.end();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
res.send(html);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const serveAsset: RouterHandler = async (req, res) => {
|
|
478
|
+
const loaded = await readyOrFail(req, res);
|
|
479
|
+
if (!loaded) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const name = (req.params as Record<string, string | undefined>).file;
|
|
483
|
+
const file: UiAsset | undefined =
|
|
484
|
+
name === undefined ? undefined : loaded.assets.files.get(name);
|
|
485
|
+
if (file === undefined) {
|
|
486
|
+
sendProblem(
|
|
487
|
+
req,
|
|
488
|
+
res,
|
|
489
|
+
404,
|
|
490
|
+
"NOT_FOUND",
|
|
491
|
+
"Not found",
|
|
492
|
+
`No asset ${req.path}`,
|
|
493
|
+
);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
res.setHeader("Content-Type", file.type);
|
|
497
|
+
res.setHeader("Cache-Control", ASSET_CACHE_CONTROL);
|
|
498
|
+
res.setHeader("ETag", file.etag);
|
|
499
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
500
|
+
const match = req.get("if-none-match");
|
|
501
|
+
if (
|
|
502
|
+
match !== null &&
|
|
503
|
+
match
|
|
504
|
+
.split(",")
|
|
505
|
+
.some((tag) => tag.trim().replace(/^W\//, "") === file.etag)
|
|
506
|
+
) {
|
|
507
|
+
res.status(304);
|
|
508
|
+
void res.end();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
res.status(200);
|
|
512
|
+
if (req.method === "HEAD") {
|
|
513
|
+
res.setHeader("Content-Length", String(file.size));
|
|
514
|
+
void res.end();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
res.send(file.body);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/** Serves a `document` thunk's document. */
|
|
521
|
+
const serveDocument: RouterHandler = async (req, res) => {
|
|
522
|
+
if (thunk === undefined) {
|
|
523
|
+
sendProblem(
|
|
524
|
+
req,
|
|
525
|
+
res,
|
|
526
|
+
404,
|
|
527
|
+
"NOT_FOUND",
|
|
528
|
+
"Not found",
|
|
529
|
+
`No document ${req.path}`,
|
|
530
|
+
);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
let body: object;
|
|
534
|
+
try {
|
|
535
|
+
body = await thunk();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
logger.error("api docs ui document thunk threw", { error });
|
|
538
|
+
sendProblem(
|
|
539
|
+
req,
|
|
540
|
+
res,
|
|
541
|
+
500,
|
|
542
|
+
"INTERNAL",
|
|
543
|
+
"Internal server error",
|
|
544
|
+
"Internal server error",
|
|
545
|
+
);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
549
|
+
logger.error("api docs ui document thunk returned no object", {});
|
|
550
|
+
sendProblem(
|
|
551
|
+
req,
|
|
552
|
+
res,
|
|
553
|
+
500,
|
|
554
|
+
"INTERNAL",
|
|
555
|
+
"Internal server error",
|
|
556
|
+
"Internal server error",
|
|
557
|
+
);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
res.status(200);
|
|
561
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
562
|
+
res.setHeader("Cache-Control", "no-store");
|
|
563
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
564
|
+
const json = JSON.stringify(body);
|
|
565
|
+
if (req.method === "HEAD") {
|
|
566
|
+
res.setHeader("Content-Length", String(Buffer.byteLength(json)));
|
|
567
|
+
void res.end();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
res.send(json);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const notFound: RouterHandler = (req, res) => {
|
|
574
|
+
sendProblem(
|
|
575
|
+
req,
|
|
576
|
+
res,
|
|
577
|
+
404,
|
|
578
|
+
"NOT_FOUND",
|
|
579
|
+
"Not found",
|
|
580
|
+
`No asset ${req.path}`,
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const guards = (route: "asset" | "document" | "shell"): RouterHandler[] => [
|
|
585
|
+
...middleware,
|
|
586
|
+
...(options.authorize
|
|
587
|
+
? [authorizeStep(options.authorize, route, logger, config.unauthorized)]
|
|
588
|
+
: []),
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
const router = new BunRouter();
|
|
592
|
+
for (const verb of ["get", "head"] as const) {
|
|
593
|
+
const register = router[verb].bind(router) as unknown as (
|
|
594
|
+
path: string,
|
|
595
|
+
...callbacks: RouterHandler[]
|
|
596
|
+
) => unknown;
|
|
597
|
+
// `:file` is one segment; anything deeper under the assets path is a 404,
|
|
598
|
+
// never the shell. (A named `*file` would lose its name when the router is
|
|
599
|
+
// mounted — bun-common flattens it to a positional wildcard.)
|
|
600
|
+
register(`${ASSETS_SUBPATH}/:file`, ...guards("asset"), serveAsset);
|
|
601
|
+
register(ASSETS_SUBPATH, ...guards("asset"), notFound);
|
|
602
|
+
register(`${ASSETS_SUBPATH}/*`, ...guards("asset"), notFound);
|
|
603
|
+
register(DOCUMENT_SUBPATH, ...guards("document"), serveDocument);
|
|
604
|
+
register("/", ...guards("shell"), serveShell);
|
|
605
|
+
register("/*", ...guards("shell"), serveShell);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return Object.freeze({ router, basePath, config });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Creates the API-docs viewer: validates the options (throwing `ConfigError`
|
|
613
|
+
* for anything unusable), resolves the configuration the app is given, and
|
|
614
|
+
* builds the router.
|
|
615
|
+
*
|
|
616
|
+
* ```ts
|
|
617
|
+
* const ui = docsUi({ document: () => api.openapi(), basePath: "/docs" });
|
|
618
|
+
* app.use(ui.basePath, ui.router);
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
export function docsUi(options: DocsUiOptions): DocsUi {
|
|
622
|
+
return createDocsUi(options);
|
|
623
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundle + shell surface a host router needs to serve the viewer itself
|
|
3
|
+
* (bun-common's `docsRouter` `"self-hosted"` viewer): the package's `dist/`
|
|
4
|
+
* resolution and the shell/CSP builders, re-exported from one module so the
|
|
5
|
+
* host imports one specifier. Everything here is server-side.
|
|
6
|
+
*/
|
|
7
|
+
import type { Logger } from "@kingsleyweb/bun-common";
|
|
8
|
+
import type { UiAssets } from "./assets";
|
|
9
|
+
import { DEFAULT_DIST_DIR, DEFAULT_ENTRY, resolveAssetSource } from "./assets";
|
|
10
|
+
|
|
11
|
+
export { DEFAULT_DIST_DIR, DEFAULT_ENTRY, resolveAssetSource } from "./assets";
|
|
12
|
+
export type { UiAsset, UiAssets, UiManifest, UiManifestFile } from "./assets";
|
|
13
|
+
export { ASSET_CACHE_CONTROL } from "./docsUi";
|
|
14
|
+
export { createNonce, createShellRenderer, cspHeader } from "./shell";
|
|
15
|
+
export type { CspOptions, ShellRenderer } from "./shell";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the viewer's bundle for a host: the prebuilt `dist/` when present
|
|
19
|
+
* (verified against its manifest, SRI computed over the local bytes), else an
|
|
20
|
+
* in-memory `Bun.build` on the first request, once per process. This is the
|
|
21
|
+
* `dev: undefined` rule of `docsUi()` — a host serving the viewer gets the
|
|
22
|
+
* same assets the package would serve.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveHostedAssets(logger: Logger): () => Promise<UiAssets> {
|
|
25
|
+
return resolveAssetSource({
|
|
26
|
+
dev: undefined,
|
|
27
|
+
distDir: DEFAULT_DIST_DIR,
|
|
28
|
+
entry: DEFAULT_ENTRY,
|
|
29
|
+
logger,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ASSET_CACHE_CONTROL,
|
|
3
|
+
DEFAULT_BASE_PATH,
|
|
4
|
+
DEFAULT_TITLE,
|
|
5
|
+
DOCUMENT_SUBPATH,
|
|
6
|
+
docsUi,
|
|
7
|
+
} from "./docsUi";
|
|
8
|
+
/**
|
|
9
|
+
* `@kingsleyweb/bun-ui/docs`: a self-hosted viewer for one OpenAPI 3.1 or
|
|
10
|
+
* AsyncAPI 3.0 document, served by a `docsUi()` handler. Needs no peer.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const ui = docsUi({ document: "/jobs-api/openapi.json", basePath: "/docs" });
|
|
14
|
+
* app.use(ui.basePath, ui.router);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The bundle machinery (`lib/docs/assets.ts`: building, writing and loading
|
|
18
|
+
* `dist/docs/`) is internal: `scripts/build.ts` and the tests import it directly.
|
|
19
|
+
*/
|
|
20
|
+
export type { DocsUiConfig, DocsUiTheme } from "./browser/config";
|
|
21
|
+
export type {
|
|
22
|
+
UiUnauthorized,
|
|
23
|
+
UiUnauthorizedStatus,
|
|
24
|
+
} from "../kit/browser/unauthorized";
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_EXTENSION_PREFIXES,
|
|
27
|
+
DOCS_UI_CONFIG_ELEMENT_ID,
|
|
28
|
+
} from "./browser/config";
|
|
29
|
+
export type {
|
|
30
|
+
DocsUi,
|
|
31
|
+
DocsUiAuthorize,
|
|
32
|
+
DocsUiAuthorizeContext,
|
|
33
|
+
DocsUiAuthorizeResult,
|
|
34
|
+
DocsUiDocumentThunk,
|
|
35
|
+
DocsUiOptions,
|
|
36
|
+
DocsUiUnauthorizedOptions,
|
|
37
|
+
} from "./types";
|