@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,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app bundle, as the server serves it: every file in memory, keyed by its
|
|
3
|
+
* hashed name, with the content type, SRI integrity and ETag worked out once.
|
|
4
|
+
*
|
|
5
|
+
* Two sources, one shape. A published package ships `dist/` (built on
|
|
6
|
+
* `prepack` by `scripts/build.ts`): `dist/manifest.json` plus the hashed files
|
|
7
|
+
* under `dist/assets/`. Inside the repo `dist/` is usually absent, so the same
|
|
8
|
+
* {@link buildAssets} runs in memory on the first request instead, once per
|
|
9
|
+
* entry, and logs one line saying so.
|
|
10
|
+
*/
|
|
11
|
+
import type { Logger } from "@kingsleyweb/bun-common";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
|
+
import { ConfigError } from "@kingsleyweb/bun-common";
|
|
15
|
+
|
|
16
|
+
/** The package root (`packages/bun-ui`, or `node_modules/@kingsleyweb/bun-ui`). */
|
|
17
|
+
export const PACKAGE_DIR = resolve(import.meta.dir, "..", "..");
|
|
18
|
+
|
|
19
|
+
/** The React app's entry point, bundled by {@link buildAssets}. */
|
|
20
|
+
export const DEFAULT_ENTRY = join(PACKAGE_DIR, "app", "docs", "main.tsx");
|
|
21
|
+
|
|
22
|
+
/** Where `scripts/build.ts` writes the bundle and where `docsUi()` looks for it. */
|
|
23
|
+
export const DEFAULT_DIST_DIR = join(PACKAGE_DIR, "dist", "docs");
|
|
24
|
+
|
|
25
|
+
/** The manifest's file name inside a dist directory. */
|
|
26
|
+
export const MANIFEST_FILE = "manifest.json";
|
|
27
|
+
|
|
28
|
+
/** The sub-directory of a dist directory holding the hashed files. */
|
|
29
|
+
export const ASSETS_DIR = "assets";
|
|
30
|
+
|
|
31
|
+
/** One file of the bundle, as `dist/manifest.json` records it. */
|
|
32
|
+
export interface UiManifestFile {
|
|
33
|
+
/** The `Content-Type` it is served with, e.g. `"text/javascript; charset=utf-8"`. */
|
|
34
|
+
type: string;
|
|
35
|
+
/** Its size in bytes. */
|
|
36
|
+
size: number;
|
|
37
|
+
/** Its Subresource Integrity digest, `"sha384-<base64>"`. */
|
|
38
|
+
integrity: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `dist/manifest.json`: what the build produced, and which files the shell links. */
|
|
42
|
+
export interface UiManifest {
|
|
43
|
+
/** Shape version of the manifest. Currently `1`. */
|
|
44
|
+
version: 1;
|
|
45
|
+
/** The files the HTML shell references directly. */
|
|
46
|
+
entry: {
|
|
47
|
+
/** File name of the entry module (loaded with `<script type="module">`). */
|
|
48
|
+
js: string;
|
|
49
|
+
/** File names of every stylesheet the build emitted, in build order. */
|
|
50
|
+
css: string[];
|
|
51
|
+
};
|
|
52
|
+
/** Every file under `dist/assets/`, by file name. */
|
|
53
|
+
files: Record<string, UiManifestFile>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One file of the bundle, held in memory. */
|
|
57
|
+
export interface UiAsset extends UiManifestFile {
|
|
58
|
+
/** The hashed file name, e.g. `"main-3kx9a2f1.js"`. It is also the URL segment. */
|
|
59
|
+
name: string;
|
|
60
|
+
/** The bytes served. */
|
|
61
|
+
body: Uint8Array<ArrayBuffer>;
|
|
62
|
+
/** A strong ETag derived from the integrity digest, quoted. */
|
|
63
|
+
etag: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The whole bundle, ready to serve. */
|
|
67
|
+
export interface UiAssets {
|
|
68
|
+
/** The files the shell links: the entry module and the stylesheets. */
|
|
69
|
+
entry: UiManifest["entry"];
|
|
70
|
+
/** Every file, by its hashed name. */
|
|
71
|
+
files: ReadonlyMap<string, UiAsset>;
|
|
72
|
+
/** Where the bundle came from: a `dist/` directory or an in-memory build. */
|
|
73
|
+
source: "dist" | "memory";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Options for {@link buildAssets}. */
|
|
77
|
+
export interface BuildAssetsOptions {
|
|
78
|
+
/** The entry point to bundle. Defaults to {@link DEFAULT_ENTRY}, the React app. */
|
|
79
|
+
entry?: string;
|
|
80
|
+
/** Minify the output. Defaults to `true`. */
|
|
81
|
+
minify?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Source maps: `"linked"` (default) emits `.map` files the bundle points
|
|
84
|
+
* at, `"external"` emits them unreferenced, `"none"` emits none.
|
|
85
|
+
*/
|
|
86
|
+
sourcemap?: "linked" | "external" | "none";
|
|
87
|
+
/**
|
|
88
|
+
* Type-check the entry and everything it imports first, with Bun.build's
|
|
89
|
+
* `check` (Bun ≥ 1.4.3; app/tsconfig.json's options), and fail the build on
|
|
90
|
+
* a type error. Defaults to `false`: `scripts/build.ts` (prepack) turns it
|
|
91
|
+
* on; the in-memory build `docsUi()` runs on a request never does, which
|
|
92
|
+
* also keeps it off a 1.4.2 runtime. Adds about 175 ms.
|
|
93
|
+
*/
|
|
94
|
+
check?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Content types by extension, so both sources agree whatever Bun reports. */
|
|
98
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
99
|
+
js: "text/javascript; charset=utf-8",
|
|
100
|
+
mjs: "text/javascript; charset=utf-8",
|
|
101
|
+
css: "text/css; charset=utf-8",
|
|
102
|
+
map: "application/json; charset=utf-8",
|
|
103
|
+
json: "application/json; charset=utf-8",
|
|
104
|
+
svg: "image/svg+xml",
|
|
105
|
+
png: "image/png",
|
|
106
|
+
jpg: "image/jpeg",
|
|
107
|
+
jpeg: "image/jpeg",
|
|
108
|
+
gif: "image/gif",
|
|
109
|
+
webp: "image/webp",
|
|
110
|
+
ico: "image/x-icon",
|
|
111
|
+
woff: "font/woff",
|
|
112
|
+
woff2: "font/woff2",
|
|
113
|
+
ttf: "font/ttf",
|
|
114
|
+
otf: "font/otf",
|
|
115
|
+
txt: "text/plain; charset=utf-8",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** A served file name: one segment, no dot segments, nothing a URL must escape. */
|
|
119
|
+
const FILE_NAME = /^[\w.~-]+$/;
|
|
120
|
+
|
|
121
|
+
/** The `Content-Type` for a file name, falling back to what the bundler reported. */
|
|
122
|
+
export function contentTypeFor(name: string, reported?: string): string {
|
|
123
|
+
const extension = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
|
|
124
|
+
return (
|
|
125
|
+
CONTENT_TYPES[extension] ??
|
|
126
|
+
reported?.replace(/;\s*/, "; ") ??
|
|
127
|
+
"application/octet-stream"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The SRI digest of some bytes, `"sha384-<base64>"`. */
|
|
132
|
+
export function integrityOf(bytes: Uint8Array): string {
|
|
133
|
+
return `sha384-${new Bun.CryptoHasher("sha384").update(bytes).digest("base64")}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Wraps bytes as a served asset. */
|
|
137
|
+
function toAsset(
|
|
138
|
+
name: string,
|
|
139
|
+
body: Uint8Array<ArrayBuffer>,
|
|
140
|
+
type: string,
|
|
141
|
+
): UiAsset {
|
|
142
|
+
const integrity = integrityOf(body);
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
body,
|
|
146
|
+
type,
|
|
147
|
+
size: body.byteLength,
|
|
148
|
+
integrity,
|
|
149
|
+
// The digest already identifies the bytes; the prefix and padding are noise.
|
|
150
|
+
etag: `"${integrity.slice("sha384-".length, "sha384-".length + 32)}"`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Bundles the app with `Bun.build`, in memory: browser target, content-hashed
|
|
156
|
+
* names (`[name]-[hash].[ext]`), code splitting, `NODE_ENV=production`.
|
|
157
|
+
* Throws with the bundler's messages when the build fails.
|
|
158
|
+
*/
|
|
159
|
+
export async function buildAssets(
|
|
160
|
+
options: BuildAssetsOptions = {},
|
|
161
|
+
): Promise<UiAssets> {
|
|
162
|
+
const entry = options.entry ?? DEFAULT_ENTRY;
|
|
163
|
+
if (!existsSync(entry)) {
|
|
164
|
+
throw new ConfigError(`bun-ui: the app entry ${entry} does not exist`, {
|
|
165
|
+
entry,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const sourcemap = options.sourcemap ?? "linked";
|
|
169
|
+
const result = await Bun.build({
|
|
170
|
+
entrypoints: [entry],
|
|
171
|
+
target: "browser",
|
|
172
|
+
format: "esm",
|
|
173
|
+
minify: options.minify ?? true,
|
|
174
|
+
splitting: true,
|
|
175
|
+
sourcemap,
|
|
176
|
+
naming: {
|
|
177
|
+
entry: "[name]-[hash].[ext]",
|
|
178
|
+
chunk: "[name]-[hash].[ext]",
|
|
179
|
+
asset: "[name]-[hash].[ext]",
|
|
180
|
+
},
|
|
181
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
182
|
+
throw: false,
|
|
183
|
+
// Spread rather than written out: `@types/bun` 1.4.2 pins bun-types
|
|
184
|
+
// 1.4.2, whose `BuildConfig` predates `check`. Write `check` directly
|
|
185
|
+
// once `@types/bun` 1.4.3 is the floor.
|
|
186
|
+
...(options.check === true ? { check: true } : {}),
|
|
187
|
+
});
|
|
188
|
+
if (!result.success) {
|
|
189
|
+
// A type error from `check` is "TS2322: …" alone; its position says where.
|
|
190
|
+
const messages = result.logs.map((log) =>
|
|
191
|
+
log.position === null
|
|
192
|
+
? log.message
|
|
193
|
+
: `${log.position.file}:${log.position.line}:${log.position.column}: ${log.message}`,
|
|
194
|
+
);
|
|
195
|
+
throw new Error(
|
|
196
|
+
`bun-ui: building ${entry} failed:\n${messages.join("\n")}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const files = new Map<string, UiAsset>();
|
|
201
|
+
let js: string | undefined;
|
|
202
|
+
const css: string[] = [];
|
|
203
|
+
for (const output of result.outputs) {
|
|
204
|
+
const name = basename(output.path);
|
|
205
|
+
const body = new Uint8Array(await output.arrayBuffer());
|
|
206
|
+
files.set(name, toAsset(name, body, contentTypeFor(name, output.type)));
|
|
207
|
+
if (output.kind === "entry-point" && name.endsWith(".js")) {
|
|
208
|
+
js = name;
|
|
209
|
+
} else if (name.endsWith(".css")) {
|
|
210
|
+
css.push(name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (js === undefined) {
|
|
214
|
+
throw new Error(`bun-ui: building ${entry} produced no entry module`);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
entry: { js, css: entryStylesheets(js, css) },
|
|
218
|
+
files,
|
|
219
|
+
source: "memory",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A hashed file name without its hash and extension: `main-3kx9.css` → `main`. */
|
|
224
|
+
function stemOf(name: string): string {
|
|
225
|
+
return name.replace(/-[a-z0-9]+\.[a-z]+$/, "");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The stylesheets the shell links. With code splitting, Bun emits the entry's
|
|
230
|
+
* stylesheet holding every rule, lazily imported ones included, plus one per
|
|
231
|
+
* split chunk repeating that chunk's rules, and marks them all "asset". The
|
|
232
|
+
* entry's is the one named after the entry module; linking the others too
|
|
233
|
+
* would download the same rules twice. If none matches (a bundler change),
|
|
234
|
+
* every stylesheet is linked, which is redundant but never unstyled.
|
|
235
|
+
*/
|
|
236
|
+
export function entryStylesheets(js: string, css: readonly string[]): string[] {
|
|
237
|
+
const own = css.filter((name) => stemOf(name) === stemOf(js));
|
|
238
|
+
return own.length > 0 ? own : [...css];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The manifest describing a bundle. */
|
|
242
|
+
export function manifestOf(assets: UiAssets): UiManifest {
|
|
243
|
+
const files: Record<string, UiManifestFile> = {};
|
|
244
|
+
for (const [name, file] of assets.files) {
|
|
245
|
+
files[name] = {
|
|
246
|
+
type: file.type,
|
|
247
|
+
size: file.size,
|
|
248
|
+
integrity: file.integrity,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
version: 1,
|
|
253
|
+
entry: { js: assets.entry.js, css: [...assets.entry.css] },
|
|
254
|
+
files,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Writes a bundle as a dist directory: the files under `<dir>/assets/` (the
|
|
260
|
+
* directory is emptied first, so stale hashes do not accumulate) and
|
|
261
|
+
* `<dir>/manifest.json`. Returns the manifest written.
|
|
262
|
+
*/
|
|
263
|
+
export async function writeAssets(
|
|
264
|
+
assets: UiAssets,
|
|
265
|
+
dir: string = DEFAULT_DIST_DIR,
|
|
266
|
+
): Promise<UiManifest> {
|
|
267
|
+
const assetsDir = join(dir, ASSETS_DIR);
|
|
268
|
+
rmSync(assetsDir, { recursive: true, force: true });
|
|
269
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
270
|
+
for (const [name, file] of assets.files) {
|
|
271
|
+
await Bun.write(join(assetsDir, name), file.body);
|
|
272
|
+
}
|
|
273
|
+
const manifest = manifestOf(assets);
|
|
274
|
+
await Bun.write(
|
|
275
|
+
join(dir, MANIFEST_FILE),
|
|
276
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
277
|
+
);
|
|
278
|
+
return manifest;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Whether `dir` holds a built bundle (its manifest exists). */
|
|
282
|
+
export function hasDist(dir: string = DEFAULT_DIST_DIR): boolean {
|
|
283
|
+
return existsSync(join(dir, MANIFEST_FILE));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** A string property of an unknown value, or `undefined`. */
|
|
287
|
+
function stringField(value: unknown, key: string): string | undefined {
|
|
288
|
+
const field =
|
|
289
|
+
typeof value === "object" && value !== null
|
|
290
|
+
? (value as Record<string, unknown>)[key]
|
|
291
|
+
: undefined;
|
|
292
|
+
return typeof field === "string" ? field : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Reads a dist directory into memory, synchronously. Every file the manifest
|
|
297
|
+
* lists must exist and match its recorded integrity — a half-copied or
|
|
298
|
+
* hand-edited dist fails here, with the file named, rather than as a browser
|
|
299
|
+
* refusing a script over SRI.
|
|
300
|
+
*/
|
|
301
|
+
export function loadDistAssets(dir: string = DEFAULT_DIST_DIR): UiAssets {
|
|
302
|
+
const manifestPath = join(dir, MANIFEST_FILE);
|
|
303
|
+
const corrupt = (why: string): ConfigError =>
|
|
304
|
+
new ConfigError(
|
|
305
|
+
`bun-ui: ${manifestPath} is not a usable build (${why}); rebuild it with \`bun scripts/build.ts\``,
|
|
306
|
+
{ dir },
|
|
307
|
+
);
|
|
308
|
+
let manifest: unknown;
|
|
309
|
+
try {
|
|
310
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw corrupt(error instanceof Error ? error.message : String(error));
|
|
313
|
+
}
|
|
314
|
+
const record = manifest as Partial<UiManifest> | null;
|
|
315
|
+
if (
|
|
316
|
+
typeof record !== "object" ||
|
|
317
|
+
record === null ||
|
|
318
|
+
record.version !== 1 ||
|
|
319
|
+
typeof record.files !== "object" ||
|
|
320
|
+
record.files === null
|
|
321
|
+
) {
|
|
322
|
+
throw corrupt("unknown manifest version or shape");
|
|
323
|
+
}
|
|
324
|
+
const js = stringField(record.entry, "js");
|
|
325
|
+
const cssList = (record.entry as { css?: unknown } | undefined)?.css;
|
|
326
|
+
if (
|
|
327
|
+
js === undefined ||
|
|
328
|
+
!Array.isArray(cssList) ||
|
|
329
|
+
!cssList.every((name) => typeof name === "string")
|
|
330
|
+
) {
|
|
331
|
+
throw corrupt("no entry");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const files = new Map<string, UiAsset>();
|
|
335
|
+
for (const [name, info] of Object.entries(record.files)) {
|
|
336
|
+
if (!FILE_NAME.test(name) || name === "." || name === "..") {
|
|
337
|
+
throw corrupt(`bad file name ${JSON.stringify(name)}`);
|
|
338
|
+
}
|
|
339
|
+
let body: Uint8Array<ArrayBuffer>;
|
|
340
|
+
try {
|
|
341
|
+
body = new Uint8Array(readFileSync(join(dir, ASSETS_DIR, name)));
|
|
342
|
+
} catch {
|
|
343
|
+
throw corrupt(`${name} is missing`);
|
|
344
|
+
}
|
|
345
|
+
const asset = toAsset(
|
|
346
|
+
name,
|
|
347
|
+
body,
|
|
348
|
+
stringField(info, "type") ?? contentTypeFor(name),
|
|
349
|
+
);
|
|
350
|
+
if (asset.integrity !== stringField(info, "integrity")) {
|
|
351
|
+
throw corrupt(`${name} does not match its recorded integrity`);
|
|
352
|
+
}
|
|
353
|
+
files.set(name, asset);
|
|
354
|
+
}
|
|
355
|
+
for (const name of [js, ...cssList]) {
|
|
356
|
+
if (!files.has(name)) {
|
|
357
|
+
throw corrupt(`the entry file ${name} is not in the manifest`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { entry: { js, css: cssList }, files, source: "dist" };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** In-memory builds, by entry path: each entry is built at most once per process. */
|
|
364
|
+
const memoryBuilds = new Map<string, Promise<UiAssets>>();
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Builds an entry in memory, once per process: concurrent and later callers
|
|
368
|
+
* share the first build. A failed build is forgotten, so the next call tries
|
|
369
|
+
* again. Logs one `info` line when a build starts.
|
|
370
|
+
*/
|
|
371
|
+
export function buildAssetsOnce(
|
|
372
|
+
entry: string,
|
|
373
|
+
logger: Logger,
|
|
374
|
+
): Promise<UiAssets> {
|
|
375
|
+
const key = resolve(entry);
|
|
376
|
+
let build = memoryBuilds.get(key);
|
|
377
|
+
if (build === undefined) {
|
|
378
|
+
logger.info(
|
|
379
|
+
"bun-ui: no dist/docs build, bundling the app in memory (run `bun scripts/build.ts` to prebuild)",
|
|
380
|
+
{ entry: key },
|
|
381
|
+
);
|
|
382
|
+
build = buildAssets({ entry: key });
|
|
383
|
+
memoryBuilds.set(key, build);
|
|
384
|
+
build.catch(() => {
|
|
385
|
+
if (memoryBuilds.get(key) === build) {
|
|
386
|
+
memoryBuilds.delete(key);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return build;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Where `docsUi()` gets its bundle from, and how. */
|
|
394
|
+
export interface AssetSourceOptions {
|
|
395
|
+
/** `false`: require `distDir`; `true`: always build `entry` in memory; `undefined`: dist when present, else build. */
|
|
396
|
+
dev: boolean | undefined;
|
|
397
|
+
/** The dist directory to read. */
|
|
398
|
+
distDir: string;
|
|
399
|
+
/** The entry to build in memory. */
|
|
400
|
+
entry: string;
|
|
401
|
+
/** Receives the one-line notice of an in-memory build. */
|
|
402
|
+
logger: Logger;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Resolves the bundle source when `docsUi()` is called. A dist directory is
|
|
407
|
+
* read synchronously right away, so a missing or corrupt one fails the call,
|
|
408
|
+
* not the first request; an in-memory build is deferred to the first request.
|
|
409
|
+
*
|
|
410
|
+
* @throws ConfigError with `dev: false` and no dist, or a corrupt dist.
|
|
411
|
+
*/
|
|
412
|
+
export function resolveAssetSource(
|
|
413
|
+
options: AssetSourceOptions,
|
|
414
|
+
): () => Promise<UiAssets> {
|
|
415
|
+
const { dev, distDir, entry, logger } = options;
|
|
416
|
+
if (dev !== true && hasDist(distDir)) {
|
|
417
|
+
const assets = Promise.resolve(loadDistAssets(distDir));
|
|
418
|
+
return () => assets;
|
|
419
|
+
}
|
|
420
|
+
if (dev === false) {
|
|
421
|
+
throw new ConfigError(
|
|
422
|
+
`bun-ui: no build found at ${join(distDir, MANIFEST_FILE)} and \`dev: false\` forbids building in memory; run \`bun scripts/build.ts\` in the package, or leave \`dev\` unset`,
|
|
423
|
+
{ distDir },
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return () => buildAssetsOnce(entry, logger);
|
|
427
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between `docsUi()` (server, `lib/docs/`) and the React viewer
|
|
3
|
+
* (`app/docs/`).
|
|
4
|
+
*
|
|
5
|
+
* The server resolves this once per mount and inlines it into the HTML shell
|
|
6
|
+
* as `<script type="application/json" id="bun-api-docs-ui-config" nonce="…">`.
|
|
7
|
+
* The viewer reads it before its first request. It must stay free of runtime
|
|
8
|
+
* imports: both sides import it, and the app side is bundled for the browser.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { UiTheme } from "../../kit/browser/shared";
|
|
12
|
+
import type { UiUnauthorized } from "../../kit/browser/unauthorized";
|
|
13
|
+
|
|
14
|
+
export type { UiTheme };
|
|
15
|
+
|
|
16
|
+
/** Id of the `<script type="application/json">` element carrying {@link DocsUiConfig}. */
|
|
17
|
+
export const DOCS_UI_CONFIG_ELEMENT_ID = "bun-api-docs-ui-config";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The specification-extension prefixes the viewers read, in priority order:
|
|
21
|
+
* an extension named on an operation, channel or scheme as
|
|
22
|
+
* `<prefix>-<name>` is read from the first prefix that carries it (so
|
|
23
|
+
* `x-bun-common-schema` wins over `x-bun-jobs-schema` when a document has
|
|
24
|
+
* both). An extension under any other prefix is ignored.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_EXTENSION_PREFIXES = ["x-bun-common", "x-bun-jobs"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Colour scheme the viewer starts in: the package's shared {@link UiTheme}
|
|
30
|
+
* (`"system"` follows `prefers-color-scheme`), under the docs viewer's name.
|
|
31
|
+
*/
|
|
32
|
+
export type DocsUiTheme = UiTheme;
|
|
33
|
+
|
|
34
|
+
/** Resolved configuration handed to the browser. Never carries secrets. */
|
|
35
|
+
export interface DocsUiConfig {
|
|
36
|
+
/** Shape version of this object, bumped on a breaking change. Currently `1`. */
|
|
37
|
+
version: 1;
|
|
38
|
+
/** Page title. Defaults to `"API docs"`. */
|
|
39
|
+
title: string;
|
|
40
|
+
/** Absolute path the viewer is mounted at, no trailing slash (e.g. `"/docs"`). Client routes live under it. */
|
|
41
|
+
basePath: string;
|
|
42
|
+
/** Path the hashed assets are served from, e.g. `"/docs/assets"`. */
|
|
43
|
+
assetsPath: string;
|
|
44
|
+
/**
|
|
45
|
+
* Where the viewer fetches the one document it shows: `<basePath>/document.json`
|
|
46
|
+
* when `docsUi()` was given a thunk (the router serves it there), else the
|
|
47
|
+
* URL exactly as configured (a same-origin path or an `http(s)` URL).
|
|
48
|
+
*/
|
|
49
|
+
documentUrl: string;
|
|
50
|
+
/**
|
|
51
|
+
* Header the try-it panel adds on every mutation (any non-empty value), or
|
|
52
|
+
* `null` when none is configured. Mirrors the documented API's CSRF rule.
|
|
53
|
+
*/
|
|
54
|
+
csrfHeader: string | null;
|
|
55
|
+
/** The extension prefixes in effect (see {@link DEFAULT_EXTENSION_PREFIXES}). */
|
|
56
|
+
extensions: readonly string[];
|
|
57
|
+
/** Initial colour scheme. */
|
|
58
|
+
theme: DocsUiTheme;
|
|
59
|
+
/**
|
|
60
|
+
* Where an authorization failure sends the browser (`docsUi()`'s
|
|
61
|
+
* `unauthorized`), or `null` when it is off: the viewer then shows the
|
|
62
|
+
* failure, as before. A host app that embeds the viewer's screens passes
|
|
63
|
+
* `null` and redirects through its own option.
|
|
64
|
+
*/
|
|
65
|
+
unauthorized: UiUnauthorized | null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@kingsleyweb/bun-ui/docs/browser`: the docs viewer's browser-safe
|
|
3
|
+
* contract, what `docsUi()` inlines into the page and the viewer reads before
|
|
4
|
+
* its first request, and the `unauthorized` option's browser-safe helpers
|
|
5
|
+
* (the redirect URL, the shape check, the defaults). No runtime import beyond
|
|
6
|
+
* constants and pure functions, and no domain package.
|
|
7
|
+
*/
|
|
8
|
+
export type { DocsUiConfig, DocsUiTheme, UiTheme } from "./config";
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_EXTENSION_PREFIXES,
|
|
11
|
+
DOCS_UI_CONFIG_ELEMENT_ID,
|
|
12
|
+
} from "./config";
|
|
13
|
+
export {
|
|
14
|
+
DEFAULT_RETURN_PARAM,
|
|
15
|
+
DEFAULT_UNAUTHORIZED_STATUSES,
|
|
16
|
+
isUiUnauthorized,
|
|
17
|
+
relativeReturnPath,
|
|
18
|
+
unauthorizedRedirectUrl,
|
|
19
|
+
} from "../../kit/browser/unauthorized";
|
|
20
|
+
export type {
|
|
21
|
+
UiUnauthorized,
|
|
22
|
+
UiUnauthorizedStatus,
|
|
23
|
+
UnauthorizedOptions,
|
|
24
|
+
} from "../../kit/browser/unauthorized";
|