vocs 2.0.17 → 2.1.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/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/globals.d.ts +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/config.d.ts +28 -0
- package/dist/internal/config.d.ts.map +1 -1
- package/dist/internal/config.js +10 -2
- package/dist/internal/config.js.map +1 -1
- package/dist/internal/llms.d.ts +21 -2
- package/dist/internal/llms.d.ts.map +1 -1
- package/dist/internal/llms.js +41 -1
- package/dist/internal/llms.js.map +1 -1
- package/dist/internal/markdown-negotiation.d.ts +36 -0
- package/dist/internal/markdown-negotiation.d.ts.map +1 -0
- package/dist/internal/markdown-negotiation.js +81 -0
- package/dist/internal/markdown-negotiation.js.map +1 -0
- package/dist/internal/markdown.d.ts.map +1 -1
- package/dist/internal/markdown.js +5 -0
- package/dist/internal/markdown.js.map +1 -1
- package/dist/internal/openapi/anchors.d.ts +25 -0
- package/dist/internal/openapi/anchors.d.ts.map +1 -0
- package/dist/internal/openapi/anchors.js +37 -0
- package/dist/internal/openapi/anchors.js.map +1 -0
- package/dist/internal/openapi/app.d.ts +89 -0
- package/dist/internal/openapi/app.d.ts.map +1 -0
- package/dist/internal/openapi/app.js +62 -0
- package/dist/internal/openapi/app.js.map +1 -0
- package/dist/internal/openapi/index.d.ts +8 -0
- package/dist/internal/openapi/index.d.ts.map +1 -0
- package/dist/internal/openapi/index.js +5 -0
- package/dist/internal/openapi/index.js.map +1 -0
- package/dist/internal/openapi/markdown.d.ts +42 -0
- package/dist/internal/openapi/markdown.d.ts.map +1 -0
- package/dist/internal/openapi/markdown.js +235 -0
- package/dist/internal/openapi/markdown.js.map +1 -0
- package/dist/internal/openapi/openapi.d.ts +187 -0
- package/dist/internal/openapi/openapi.d.ts.map +1 -0
- package/dist/internal/openapi/openapi.js +44 -0
- package/dist/internal/openapi/openapi.js.map +1 -0
- package/dist/internal/openapi/openrpc.d.ts +90 -0
- package/dist/internal/openapi/openrpc.d.ts.map +1 -0
- package/dist/internal/openapi/openrpc.js +213 -0
- package/dist/internal/openapi/openrpc.js.map +1 -0
- package/dist/internal/openapi/parser.d.ts +181 -0
- package/dist/internal/openapi/parser.d.ts.map +1 -0
- package/dist/internal/openapi/parser.js +329 -0
- package/dist/internal/openapi/parser.js.map +1 -0
- package/dist/internal/openapi/registry.d.ts +36 -0
- package/dist/internal/openapi/registry.d.ts.map +1 -0
- package/dist/internal/openapi/registry.js +79 -0
- package/dist/internal/openapi/registry.js.map +1 -0
- package/dist/internal/openapi/sample.d.ts +115 -0
- package/dist/internal/openapi/sample.d.ts.map +1 -0
- package/dist/internal/openapi/sample.js +434 -0
- package/dist/internal/openapi/sample.js.map +1 -0
- package/dist/internal/openapi/search.d.ts +19 -0
- package/dist/internal/openapi/search.d.ts.map +1 -0
- package/dist/internal/openapi/search.js +98 -0
- package/dist/internal/openapi/search.js.map +1 -0
- package/dist/internal/openapi/sidebar.d.ts +30 -0
- package/dist/internal/openapi/sidebar.d.ts.map +1 -0
- package/dist/internal/openapi/sidebar.js +67 -0
- package/dist/internal/openapi/sidebar.js.map +1 -0
- package/dist/internal/openapi/union.d.ts +36 -0
- package/dist/internal/openapi/union.d.ts.map +1 -0
- package/dist/internal/openapi/union.js +69 -0
- package/dist/internal/openapi/union.js.map +1 -0
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +20 -0
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/vite-plugins.d.ts +12 -0
- package/dist/internal/vite-plugins.d.ts.map +1 -1
- package/dist/internal/vite-plugins.js +102 -11
- package/dist/internal/vite-plugins.js.map +1 -1
- package/dist/react/Badge.d.ts +2 -3
- package/dist/react/Badge.d.ts.map +1 -1
- package/dist/react/Layout.client.d.ts.map +1 -1
- package/dist/react/Layout.client.js +2 -2
- package/dist/react/Layout.client.js.map +1 -1
- package/dist/react/OpenApi.d.ts +6 -0
- package/dist/react/OpenApi.d.ts.map +1 -0
- package/dist/react/OpenApi.js +6 -0
- package/dist/react/OpenApi.js.map +1 -0
- package/dist/react/internal/CodeToHtml.client.d.ts +53 -2
- package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -1
- package/dist/react/internal/CodeToHtml.client.js +154 -21
- package/dist/react/internal/CodeToHtml.client.js.map +1 -1
- package/dist/react/internal/Sidebar.d.ts.map +1 -1
- package/dist/react/internal/Sidebar.js +99 -2
- package/dist/react/internal/Sidebar.js.map +1 -1
- package/dist/react/internal/openapi/CodeSample.client.d.ts +21 -0
- package/dist/react/internal/openapi/CodeSample.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/CodeSample.client.js +134 -0
- package/dist/react/internal/openapi/CodeSample.client.js.map +1 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts +17 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.js +18 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.js.map +1 -0
- package/dist/react/internal/openapi/Disclosure.client.d.ts +28 -0
- package/dist/react/internal/openapi/Disclosure.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/Disclosure.client.js +38 -0
- package/dist/react/internal/openapi/Disclosure.client.js.map +1 -0
- package/dist/react/internal/openapi/Endpoints.d.ts +26 -0
- package/dist/react/internal/openapi/Endpoints.d.ts.map +1 -0
- package/dist/react/internal/openapi/Endpoints.js +33 -0
- package/dist/react/internal/openapi/Endpoints.js.map +1 -0
- package/dist/react/internal/openapi/EndpointsView.d.ts +24 -0
- package/dist/react/internal/openapi/EndpointsView.d.ts.map +1 -0
- package/dist/react/internal/openapi/EndpointsView.js +26 -0
- package/dist/react/internal/openapi/EndpointsView.js.map +1 -0
- package/dist/react/internal/openapi/EnumValues.client.d.ts +14 -0
- package/dist/react/internal/openapi/EnumValues.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/EnumValues.client.js +20 -0
- package/dist/react/internal/openapi/EnumValues.client.js.map +1 -0
- package/dist/react/internal/openapi/HeadingAnchor.d.ts +15 -0
- package/dist/react/internal/openapi/HeadingAnchor.d.ts.map +1 -0
- package/dist/react/internal/openapi/HeadingAnchor.js +12 -0
- package/dist/react/internal/openapi/HeadingAnchor.js.map +1 -0
- package/dist/react/internal/openapi/OpenApiPage.d.ts +79 -0
- package/dist/react/internal/openapi/OpenApiPage.d.ts.map +1 -0
- package/dist/react/internal/openapi/OpenApiPage.js +72 -0
- package/dist/react/internal/openapi/OpenApiPage.js.map +1 -0
- package/dist/react/internal/openapi/Operation.d.ts +25 -0
- package/dist/react/internal/openapi/Operation.d.ts.map +1 -0
- package/dist/react/internal/openapi/Operation.js +101 -0
- package/dist/react/internal/openapi/Operation.js.map +1 -0
- package/dist/react/internal/openapi/Playground.client.d.ts +33 -0
- package/dist/react/internal/openapi/Playground.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/Playground.client.js +170 -0
- package/dist/react/internal/openapi/Playground.client.js.map +1 -0
- package/dist/react/internal/openapi/PropertyExample.client.d.ts +17 -0
- package/dist/react/internal/openapi/PropertyExample.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/PropertyExample.client.js +21 -0
- package/dist/react/internal/openapi/PropertyExample.client.js.map +1 -0
- package/dist/react/internal/openapi/Reference.d.ts +55 -0
- package/dist/react/internal/openapi/Reference.d.ts.map +1 -0
- package/dist/react/internal/openapi/Reference.js +42 -0
- package/dist/react/internal/openapi/Reference.js.map +1 -0
- package/dist/react/internal/openapi/Schema.d.ts +110 -0
- package/dist/react/internal/openapi/Schema.d.ts.map +1 -0
- package/dist/react/internal/openapi/Schema.js +239 -0
- package/dist/react/internal/openapi/Schema.js.map +1 -0
- package/dist/react/internal/openapi/SchemaUnion.client.d.ts +25 -0
- package/dist/react/internal/openapi/SchemaUnion.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/SchemaUnion.client.js +48 -0
- package/dist/react/internal/openapi/SchemaUnion.client.js.map +1 -0
- package/dist/react/internal/openapi/anchor-navigation.client.d.ts +47 -0
- package/dist/react/internal/openapi/anchor-navigation.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/anchor-navigation.client.js +120 -0
- package/dist/react/internal/openapi/anchor-navigation.client.js.map +1 -0
- package/dist/react/internal/openapi/auth.d.ts +28 -0
- package/dist/react/internal/openapi/auth.d.ts.map +1 -0
- package/dist/react/internal/openapi/auth.js +75 -0
- package/dist/react/internal/openapi/auth.js.map +1 -0
- package/dist/react/useLayout.d.ts +2 -0
- package/dist/react/useLayout.d.ts.map +1 -1
- package/dist/react/useLayout.js +2 -0
- package/dist/react/useLayout.js.map +1 -1
- package/dist/server/handlers.d.ts +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/handlers.js +26 -5
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/og-assets.d.ts +5 -0
- package/dist/server/og-assets.d.ts.map +1 -0
- package/dist/server/og-assets.js +11 -0
- package/dist/server/og-assets.js.map +1 -0
- package/dist/server/openapi/assets.d.ts +33 -0
- package/dist/server/openapi/assets.d.ts.map +1 -0
- package/dist/server/openapi/assets.generated.d.ts +9 -0
- package/dist/server/openapi/assets.generated.d.ts.map +1 -0
- package/dist/server/openapi/assets.generated.js +1091 -0
- package/dist/server/openapi/assets.generated.js.map +1 -0
- package/dist/server/openapi/assets.js +32 -0
- package/dist/server/openapi/assets.js.map +1 -0
- package/dist/server/openapi/handler.d.ts +103 -0
- package/dist/server/openapi/handler.d.ts.map +1 -0
- package/dist/server/openapi/handler.js +198 -0
- package/dist/server/openapi/handler.js.map +1 -0
- package/dist/server/openapi/handler.test.d.ts +2 -0
- package/dist/server/openapi/handler.test.d.ts.map +1 -0
- package/dist/server/openapi/handler.test.js +203 -0
- package/dist/server/openapi/handler.test.js.map +1 -0
- package/dist/server/openapi/html.d.ts +16 -0
- package/dist/server/openapi/html.d.ts.map +1 -0
- package/dist/server/openapi/html.js +75 -0
- package/dist/server/openapi/html.js.map +1 -0
- package/dist/server/openapi/pages.d.ts +33 -0
- package/dist/server/openapi/pages.d.ts.map +1 -0
- package/dist/server/openapi/pages.js +130 -0
- package/dist/server/openapi/pages.js.map +1 -0
- package/dist/server/openapi/pages.test.d.ts +2 -0
- package/dist/server/openapi/pages.test.d.ts.map +1 -0
- package/dist/server/openapi/pages.test.js +94 -0
- package/dist/server/openapi/pages.test.js.map +1 -0
- package/dist/server/openapi/state.d.ts +42 -0
- package/dist/server/openapi/state.d.ts.map +1 -0
- package/dist/server/openapi/state.js +101 -0
- package/dist/server/openapi/state.js.map +1 -0
- package/dist/styles/index.css +16 -0
- package/dist/styles/markdown.css +9 -7
- package/dist/styles/openapi-playground.css +80 -0
- package/dist/styles/openapi.css +660 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +1 -0
- package/dist/vite.js.map +1 -1
- package/dist/waku/internal/middleware/md-router.d.ts +0 -4
- package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
- package/dist/waku/internal/middleware/md-router.js +3 -48
- package/dist/waku/internal/middleware/md-router.js.map +1 -1
- package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js +1 -1
- package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js.map +1 -1
- package/dist/waku/internal/patches/router.d.ts.map +1 -1
- package/dist/waku/internal/patches/router.js +114 -1
- package/dist/waku/internal/patches/router.js.map +1 -1
- package/package.json +5 -1
- package/src/config.ts +1 -0
- package/src/globals.d.ts +16 -0
- package/src/index.ts +1 -0
- package/src/internal/config.ts +40 -1
- package/src/internal/llms.ts +51 -1
- package/src/internal/markdown-negotiation.test.ts +42 -0
- package/src/internal/markdown-negotiation.ts +95 -0
- package/src/internal/markdown.ts +5 -0
- package/src/internal/openapi/anchors.ts +44 -0
- package/src/internal/openapi/app.ts +127 -0
- package/src/internal/openapi/index.ts +24 -0
- package/src/internal/openapi/markdown.test.ts +115 -0
- package/src/internal/openapi/markdown.ts +275 -0
- package/src/internal/openapi/openapi.ts +212 -0
- package/src/internal/openapi/openrpc.test.ts +239 -0
- package/src/internal/openapi/openrpc.ts +295 -0
- package/src/internal/openapi/parser.test.ts +203 -0
- package/src/internal/openapi/parser.ts +613 -0
- package/src/internal/openapi/registry.test.ts +89 -0
- package/src/internal/openapi/registry.ts +89 -0
- package/src/internal/openapi/sample.test.ts +283 -0
- package/src/internal/openapi/sample.ts +562 -0
- package/src/internal/openapi/search.test.ts +62 -0
- package/src/internal/openapi/search.ts +108 -0
- package/src/internal/openapi/sidebar.test.ts +131 -0
- package/src/internal/openapi/sidebar.ts +94 -0
- package/src/internal/openapi/union.test.ts +51 -0
- package/src/internal/openapi/union.ts +74 -0
- package/src/internal/search.ts +20 -0
- package/src/internal/test/virtual-config.stub.ts +14 -0
- package/src/internal/vite-plugins.ts +106 -11
- package/src/openapi-app/App.tsx +64 -0
- package/src/openapi-app/blocks.tsx +33 -0
- package/src/openapi-app/client.tsx +25 -0
- package/src/openapi-app/links.test.ts +84 -0
- package/src/openapi-app/links.ts +66 -0
- package/src/openapi-app/payload.ts +20 -0
- package/src/openapi-app/virtual/config.ts +7 -0
- package/src/openapi-app/virtual/group-icons.ts +2 -0
- package/src/openapi-app/virtual/langs.ts +6 -0
- package/src/openapi-app/virtual/openapi.ts +10 -0
- package/src/openapi-app/virtual/search-index.ts +21 -0
- package/src/openapi-app/virtual/slots.ts +4 -0
- package/src/openapi-app/virtual/user-styles.ts +2 -0
- package/src/openapi-app/waku.tsx +154 -0
- package/src/react/Badge.tsx +2 -3
- package/src/react/Layout.client.tsx +17 -4
- package/src/react/OpenApi.tsx +5 -0
- package/src/react/internal/CodeToHtml.client.tsx +283 -22
- package/src/react/internal/Sidebar.tsx +126 -22
- package/src/react/internal/openapi/CodeSample.client.tsx +294 -0
- package/src/react/internal/openapi/CollapsibleChildren.client.tsx +41 -0
- package/src/react/internal/openapi/Disclosure.client.tsx +67 -0
- package/src/react/internal/openapi/Endpoints.tsx +58 -0
- package/src/react/internal/openapi/EndpointsView.tsx +76 -0
- package/src/react/internal/openapi/EnumValues.client.tsx +49 -0
- package/src/react/internal/openapi/HeadingAnchor.tsx +28 -0
- package/src/react/internal/openapi/OpenApiPage.tsx +173 -0
- package/src/react/internal/openapi/Operation.test.tsx +101 -0
- package/src/react/internal/openapi/Operation.tsx +335 -0
- package/src/react/internal/openapi/Playground.client.tsx +234 -0
- package/src/react/internal/openapi/PropertyExample.client.tsx +55 -0
- package/src/react/internal/openapi/Reference.tsx +120 -0
- package/src/react/internal/openapi/Schema.tsx +467 -0
- package/src/react/internal/openapi/SchemaUnion.client.tsx +123 -0
- package/src/react/internal/openapi/anchor-navigation.client.ts +154 -0
- package/src/react/internal/openapi/auth.ts +69 -0
- package/src/react/useLayout.ts +4 -0
- package/src/server/handlers.ts +31 -6
- package/src/server/og-assets.ts +14 -0
- package/src/server/openapi/assets.generated.ts +1093 -0
- package/src/server/openapi/assets.ts +57 -0
- package/src/server/openapi/handler.test.ts +244 -0
- package/src/server/openapi/handler.ts +277 -0
- package/src/server/openapi/html.ts +84 -0
- package/src/server/openapi/pages.test.ts +111 -0
- package/src/server/openapi/pages.ts +153 -0
- package/src/server/openapi/state.ts +136 -0
- package/src/styles/index.css +16 -0
- package/src/styles/markdown.css +9 -7
- package/src/styles/openapi-playground.css +80 -0
- package/src/styles/openapi.css +660 -0
- package/src/vite.ts +1 -0
- package/src/waku/internal/middleware/md-router.ts +8 -52
- package/src/waku/internal/patches/adapters/vercel-build-enhancer.ts +1 -1
- package/src/waku/internal/patches/router.ts +131 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { SearchDocuments } from '../search.js'
|
|
2
|
+
import type { Ir, IrOperation } from './parser.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds search index documents for an OpenAPI section so the auto-generated
|
|
6
|
+
* reference is discoverable by Vocs's search (which otherwise only crawls
|
|
7
|
+
* `.md`/`.mdx` files).
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the page/anchor structure of the rendered reference:
|
|
10
|
+
* - the `/api` landing page (one `page` document),
|
|
11
|
+
* - each category page `/api/<group>` (one `page` document), and
|
|
12
|
+
* - each operation as a `section` document anchored on its category page
|
|
13
|
+
* (`/api/<group>#<operation>`).
|
|
14
|
+
*
|
|
15
|
+
* Operation documents fold the method, path, parameter names/descriptions and
|
|
16
|
+
* response descriptions into their searchable `text` so endpoints surface for a
|
|
17
|
+
* wide range of queries (summary, path, parameter, status code, …).
|
|
18
|
+
*/
|
|
19
|
+
export function toSearchDocuments(ir: Ir): SearchDocuments.Document[] {
|
|
20
|
+
const category = ir.info.title
|
|
21
|
+
const documents: SearchDocuments.Document[] = []
|
|
22
|
+
// Strip a trailing slash so a root mount (`/`) doesn't yield `//group` hrefs.
|
|
23
|
+
const base = ir.path === '/' ? '' : ir.path.replace(/\/$/, '')
|
|
24
|
+
|
|
25
|
+
// Section landing page (`/api`).
|
|
26
|
+
documents.push({
|
|
27
|
+
category,
|
|
28
|
+
href: ir.path,
|
|
29
|
+
id: `openapi:${ir.path}`,
|
|
30
|
+
searchPriority: undefined,
|
|
31
|
+
subtitle: '',
|
|
32
|
+
text: plainText(ir.info.description),
|
|
33
|
+
title: ir.info.title,
|
|
34
|
+
titles: [],
|
|
35
|
+
type: 'page',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
for (const group of ir.groups) {
|
|
39
|
+
const groupHref = `${base}/${group.id}`
|
|
40
|
+
|
|
41
|
+
// Category page (`/api/<group>`).
|
|
42
|
+
documents.push({
|
|
43
|
+
category,
|
|
44
|
+
href: groupHref,
|
|
45
|
+
id: `openapi:${groupHref}`,
|
|
46
|
+
searchPriority: undefined,
|
|
47
|
+
subtitle: '',
|
|
48
|
+
text: plainText(group.description),
|
|
49
|
+
title: group.name,
|
|
50
|
+
titles: [ir.info.title],
|
|
51
|
+
type: 'page',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Each operation, anchored on its category page.
|
|
55
|
+
for (const operation of group.operations) {
|
|
56
|
+
const methodPath = `${operation.method} ${operation.path}`
|
|
57
|
+
documents.push({
|
|
58
|
+
category,
|
|
59
|
+
href: `${groupHref}#${operation.id}`,
|
|
60
|
+
id: `openapi:${groupHref}#${operation.id}`,
|
|
61
|
+
searchPriority: undefined,
|
|
62
|
+
subtitle: methodPath,
|
|
63
|
+
text: operationText(operation),
|
|
64
|
+
title: operation.summary || methodPath,
|
|
65
|
+
titles: [ir.info.title, group.name],
|
|
66
|
+
type: 'section',
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return documents
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Folds an operation's searchable content into a single plain-text string. */
|
|
75
|
+
function operationText(operation: IrOperation): string {
|
|
76
|
+
const parts = [
|
|
77
|
+
`${operation.method} ${operation.path}`,
|
|
78
|
+
plainText(operation.description),
|
|
79
|
+
plainText(operation.summary),
|
|
80
|
+
...operation.parameters
|
|
81
|
+
.filter((parameter) => !parameter.deprecated)
|
|
82
|
+
.map((parameter) => `${parameter.name} ${plainText(parameter.description)}`.trim()),
|
|
83
|
+
...operation.responses.map((response) =>
|
|
84
|
+
`${response.status} ${plainText(response.description)}`.trim(),
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
return parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reduces a Markdown string to plain text suitable for the search index. Strips
|
|
92
|
+
* the common inline/block syntax (links, emphasis, code, headings, lists,
|
|
93
|
+
* blockquotes, images) so only human-readable words remain for tokenization.
|
|
94
|
+
*/
|
|
95
|
+
function plainText(markdown: string | undefined): string {
|
|
96
|
+
if (!markdown) return ''
|
|
97
|
+
return markdown
|
|
98
|
+
.replace(/```[\s\S]*?```/g, ' ') // fenced code blocks
|
|
99
|
+
.replace(/`([^`]+)`/g, '$1') // inline code
|
|
100
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') // images → alt text
|
|
101
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // links → label
|
|
102
|
+
.replace(/^\s{0,3}#{1,6}\s+/gm, '') // headings
|
|
103
|
+
.replace(/^\s{0,3}>\s?/gm, '') // blockquotes
|
|
104
|
+
.replace(/^\s*[-*+]\s+/gm, '') // unordered list markers
|
|
105
|
+
.replace(/[*_~]/g, '') // emphasis / strikethrough
|
|
106
|
+
.replace(/\s+/g, ' ')
|
|
107
|
+
.trim()
|
|
108
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import type { Ir } from './parser.js'
|
|
3
|
+
import { methodVariant, toSidebar } from './sidebar.js'
|
|
4
|
+
|
|
5
|
+
const ir: Ir = {
|
|
6
|
+
path: '/api',
|
|
7
|
+
client: { url: 'https://example.com/openapi.json' },
|
|
8
|
+
info: { title: 'Petstore' },
|
|
9
|
+
servers: [],
|
|
10
|
+
securitySchemes: {},
|
|
11
|
+
traits: [],
|
|
12
|
+
groups: [
|
|
13
|
+
{
|
|
14
|
+
id: 'pets',
|
|
15
|
+
name: 'pets',
|
|
16
|
+
operations: [
|
|
17
|
+
{
|
|
18
|
+
id: 'listpets',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
path: '/pets',
|
|
21
|
+
summary: 'List pets',
|
|
22
|
+
parameters: [],
|
|
23
|
+
responses: [],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'createpet',
|
|
27
|
+
method: 'POST',
|
|
28
|
+
path: '/pets',
|
|
29
|
+
parameters: [],
|
|
30
|
+
responses: [],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('toSidebar', () => {
|
|
38
|
+
test('groups are non-link headers; operations anchor onto per-category pages', () => {
|
|
39
|
+
expect(toSidebar(ir)).toEqual([
|
|
40
|
+
// Root "Introduction" links to the section landing page.
|
|
41
|
+
{ text: 'Introduction', link: '/api' },
|
|
42
|
+
{
|
|
43
|
+
text: 'pets',
|
|
44
|
+
collapsed: false,
|
|
45
|
+
items: [
|
|
46
|
+
{
|
|
47
|
+
// The category itself is reached via an "Overview" entry; the
|
|
48
|
+
// top-level item is a non-link header.
|
|
49
|
+
text: 'Overview',
|
|
50
|
+
link: '/api/pets',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
text: 'List pets',
|
|
54
|
+
link: '/api/pets#listpets',
|
|
55
|
+
badge: { text: 'GET', variant: 'info' },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
// Falls back to "METHOD path" when there's no summary.
|
|
59
|
+
text: 'POST /pets',
|
|
60
|
+
link: '/api/pets#createpet',
|
|
61
|
+
badge: { text: 'POST', variant: 'success' },
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('nests intro items under the Introduction group', () => {
|
|
69
|
+
const sidebar = toSidebar(ir, {
|
|
70
|
+
intro: [
|
|
71
|
+
{ text: 'Authentication', link: '/api/authentication' },
|
|
72
|
+
{ text: 'Versioning', link: '/api/versioning' },
|
|
73
|
+
],
|
|
74
|
+
})
|
|
75
|
+
expect(sidebar[0]).toEqual({
|
|
76
|
+
text: 'Introduction',
|
|
77
|
+
collapsed: false,
|
|
78
|
+
items: [
|
|
79
|
+
{ text: 'Overview', link: '/api' },
|
|
80
|
+
{ text: 'Authentication', link: '/api/authentication' },
|
|
81
|
+
{ text: 'Versioning', link: '/api/versioning' },
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('collapses generated category groups but not Introduction', () => {
|
|
87
|
+
const sidebar = toSidebar(ir, {
|
|
88
|
+
intro: [{ text: 'Authentication', link: '/api/authentication' }],
|
|
89
|
+
collapsed: true,
|
|
90
|
+
})
|
|
91
|
+
// Introduction stays expanded.
|
|
92
|
+
expect((sidebar[0] as { collapsed: boolean }).collapsed).toBe(false)
|
|
93
|
+
// Category groups start collapsed.
|
|
94
|
+
expect((sidebar[1] as { collapsed: boolean }).collapsed).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('injects groupExtras after a group Overview link', () => {
|
|
98
|
+
const sidebar = toSidebar(ir, {
|
|
99
|
+
groupExtras: new Map([['pets', [{ text: 'Rate limits', link: '/api/rate-limits' }]]]),
|
|
100
|
+
})
|
|
101
|
+
expect(sidebar[1]).toEqual({
|
|
102
|
+
text: 'pets',
|
|
103
|
+
collapsed: false,
|
|
104
|
+
items: [
|
|
105
|
+
{ text: 'Overview', link: '/api/pets' },
|
|
106
|
+
{ text: 'Rate limits', link: '/api/rate-limits' },
|
|
107
|
+
{
|
|
108
|
+
text: 'List pets',
|
|
109
|
+
link: '/api/pets#listpets',
|
|
110
|
+
badge: { text: 'GET', variant: 'info' },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
text: 'POST /pets',
|
|
114
|
+
link: '/api/pets#createpet',
|
|
115
|
+
badge: { text: 'POST', variant: 'success' },
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('methodVariant', () => {
|
|
123
|
+
test('maps methods to badge variants', () => {
|
|
124
|
+
expect(methodVariant('GET')).toBe('info')
|
|
125
|
+
expect(methodVariant('POST')).toBe('success')
|
|
126
|
+
expect(methodVariant('PUT')).toBe('warning')
|
|
127
|
+
expect(methodVariant('PATCH')).toBe('warning')
|
|
128
|
+
expect(methodVariant('DELETE')).toBe('danger')
|
|
129
|
+
expect(methodVariant('OPTIONS')).toBe('note')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { SidebarItem } from '../sidebar.js'
|
|
2
|
+
import type { Ir, IrOperation } from './parser.js'
|
|
3
|
+
|
|
4
|
+
export type Badge = NonNullable<SidebarItem['badge']>
|
|
5
|
+
export type BadgeVariant = NonNullable<Exclude<Badge, string>['variant']>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Maps an HTTP method to a sidebar badge variant.
|
|
9
|
+
*/
|
|
10
|
+
export function methodVariant(method: string): BadgeVariant {
|
|
11
|
+
switch (method.toUpperCase()) {
|
|
12
|
+
case 'GET':
|
|
13
|
+
return 'info'
|
|
14
|
+
case 'POST':
|
|
15
|
+
return 'success'
|
|
16
|
+
case 'PUT':
|
|
17
|
+
case 'PATCH':
|
|
18
|
+
return 'warning'
|
|
19
|
+
case 'DELETE':
|
|
20
|
+
return 'danger'
|
|
21
|
+
default:
|
|
22
|
+
return 'note'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds an isolated sidebar (categories → operation anchors) from a parsed
|
|
28
|
+
* OpenAPI IR.
|
|
29
|
+
*
|
|
30
|
+
* Each category is its own page; operation links are in-page anchors on that
|
|
31
|
+
* page (`/api/{group}#{operation}`).
|
|
32
|
+
*/
|
|
33
|
+
export function toSidebar(ir: Ir, options: toSidebar.Options = {}): SidebarItem<true>[] {
|
|
34
|
+
// Strip a trailing slash so a root mount (`/`) doesn't yield `//group`.
|
|
35
|
+
const base = ir.path === '/' ? '' : ir.path.replace(/\/$/, '')
|
|
36
|
+
const operationLink = (operation: IrOperation, groupId: string) =>
|
|
37
|
+
`${base}/${groupId}#${operation.id}`
|
|
38
|
+
|
|
39
|
+
const groupLink = (groupId: string) => `${base}/${groupId}`
|
|
40
|
+
|
|
41
|
+
// When `intro` items are supplied, the "Introduction" leaf becomes a
|
|
42
|
+
// collapsible group: an "Overview" link to the landing page followed by the
|
|
43
|
+
// extra items (e.g. authentication/versioning guide pages).
|
|
44
|
+
const intro = options.intro ?? []
|
|
45
|
+
const introduction: SidebarItem<true> =
|
|
46
|
+
intro.length > 0
|
|
47
|
+
? {
|
|
48
|
+
text: 'Introduction',
|
|
49
|
+
collapsed: false,
|
|
50
|
+
items: [{ text: 'Overview', link: ir.path }, ...intro] as SidebarItem<true>[],
|
|
51
|
+
}
|
|
52
|
+
: { text: 'Introduction', link: ir.path }
|
|
53
|
+
|
|
54
|
+
// Extra items (e.g. `x-parent` guide pages) injected into a group, keyed by
|
|
55
|
+
// group id, rendered after the group's "Overview" link.
|
|
56
|
+
const groupExtras = options.groupExtras ?? new Map<string, SidebarItem[]>()
|
|
57
|
+
|
|
58
|
+
// Generated category groups start collapsed when `collapsed` is set (the
|
|
59
|
+
// active group still auto-expands at render). `Introduction` is unaffected.
|
|
60
|
+
const groupCollapsed = options.collapsed ?? false
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
introduction,
|
|
64
|
+
...ir.groups.map((group) => ({
|
|
65
|
+
text: group.name,
|
|
66
|
+
collapsed: groupCollapsed,
|
|
67
|
+
items: [
|
|
68
|
+
// An "Overview" entry links to the category itself; the top-level item
|
|
69
|
+
// is a non-link header so its label doesn't compete with the operations.
|
|
70
|
+
{ text: 'Overview', link: groupLink(group.id) },
|
|
71
|
+
...((groupExtras.get(group.id) ?? []) as SidebarItem<true>[]),
|
|
72
|
+
...group.operations.map((operation) => ({
|
|
73
|
+
text: operation.summary || `${operation.method} ${operation.path}`,
|
|
74
|
+
link: operationLink(operation, group.id),
|
|
75
|
+
badge: { text: operation.method, variant: methodVariant(operation.method) },
|
|
76
|
+
})),
|
|
77
|
+
],
|
|
78
|
+
})),
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export declare namespace toSidebar {
|
|
83
|
+
type Options = {
|
|
84
|
+
/** Items nested under the generated `Introduction` entry. */
|
|
85
|
+
intro?: SidebarItem[] | undefined
|
|
86
|
+
/** Extra items injected into a generated group, keyed by group id. */
|
|
87
|
+
groupExtras?: Map<string, SidebarItem[]> | undefined
|
|
88
|
+
/**
|
|
89
|
+
* Collapse the generated category groups by default (the active group still
|
|
90
|
+
* auto-expands). `Introduction` is unaffected. @default false
|
|
91
|
+
*/
|
|
92
|
+
collapsed?: boolean | undefined
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { unionVariantSchemas, unwrapSingleVariant } from './union.js'
|
|
3
|
+
|
|
4
|
+
describe('unwrapSingleVariant', () => {
|
|
5
|
+
test('unwraps a nullable object union to its sole member', () => {
|
|
6
|
+
const schema = {
|
|
7
|
+
description: 'Access-key provisioning authorization.',
|
|
8
|
+
oneOf: [
|
|
9
|
+
{ title: 'Null', type: 'null' },
|
|
10
|
+
{ type: 'object', properties: { keyId: { type: 'string' } } },
|
|
11
|
+
],
|
|
12
|
+
}
|
|
13
|
+
expect(unwrapSingleVariant(schema)).toEqual({
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: { keyId: { type: 'string' } },
|
|
16
|
+
// the wrapper's description is preferred so field-level docs survive
|
|
17
|
+
description: 'Access-key provisioning authorization.',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('prefers the member description when the wrapper has none', () => {
|
|
22
|
+
const schema = {
|
|
23
|
+
anyOf: [{ type: 'null' }, { type: 'object', description: 'inner' }],
|
|
24
|
+
}
|
|
25
|
+
expect(unwrapSingleVariant(schema)).toEqual({ type: 'object', description: 'inner' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('unwraps a single-member union without an explicit null', () => {
|
|
29
|
+
const schema = { oneOf: [{ type: 'object', properties: {} }] }
|
|
30
|
+
expect(unwrapSingleVariant(schema)).toEqual({ type: 'object', properties: {} })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('returns undefined for two or more non-null members', () => {
|
|
34
|
+
const schema = {
|
|
35
|
+
oneOf: [{ type: 'null' }, { type: 'string' }, { type: 'number' }],
|
|
36
|
+
}
|
|
37
|
+
expect(unwrapSingleVariant(schema)).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('returns undefined for non-unions', () => {
|
|
41
|
+
expect(unwrapSingleVariant({ type: 'object', properties: {} })).toBeUndefined()
|
|
42
|
+
expect(unwrapSingleVariant(undefined)).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('does not collide with the variant picker (which needs >= 2 members)', () => {
|
|
46
|
+
const nullable = { oneOf: [{ type: 'null' }, { type: 'object', properties: {} }] }
|
|
47
|
+
// single non-null member: unwrapped, not shown as a picker
|
|
48
|
+
expect(unwrapSingleVariant(nullable)).toBeDefined()
|
|
49
|
+
expect(unionVariantSchemas(nullable)).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `oneOf`/`anyOf` union detection so the schema documentation rows (which
|
|
3
|
+
* render a variant picker) and the response example sampler (which samples one
|
|
4
|
+
* variant) agree on which variants exist and in what order. Keeping this
|
|
5
|
+
* framework-agnostic lets both the React renderer and the sample serializer
|
|
6
|
+
* derive identical variant-qualified anchor ids.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type SchemaObject = Record<string, unknown>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The displayable variant schemas of a union rendered as a variant picker, or
|
|
13
|
+
* `undefined` when the schema is not shown as a picker (no `oneOf`/`anyOf`,
|
|
14
|
+
* fewer than two non-null members, or a plain scalar union like `string |
|
|
15
|
+
* number` with nothing extra to show). Array schemas are unwrapped to their item
|
|
16
|
+
* schema first. The returned order is the index space used by
|
|
17
|
+
* {@link unionVariantSegment}.
|
|
18
|
+
*/
|
|
19
|
+
export function unionVariantSchemas(schema: SchemaObject | undefined): SchemaObject[] | undefined {
|
|
20
|
+
if (!schema) return undefined
|
|
21
|
+
const target =
|
|
22
|
+
schema['type'] === 'array' && schema['items'] ? (schema['items'] as SchemaObject) : schema
|
|
23
|
+
const oneOf = target['oneOf'] as SchemaObject[] | undefined
|
|
24
|
+
const members = (oneOf ?? target['anyOf']) as SchemaObject[] | undefined
|
|
25
|
+
if (!Array.isArray(members)) return undefined
|
|
26
|
+
const variants = members.filter(
|
|
27
|
+
(member) => member && typeof member === 'object' && member['type'] !== 'null',
|
|
28
|
+
)
|
|
29
|
+
if (variants.length < 2) return undefined
|
|
30
|
+
const rich = variants.some(
|
|
31
|
+
(member) =>
|
|
32
|
+
member['properties'] ||
|
|
33
|
+
member['allOf'] ||
|
|
34
|
+
typeof member['title'] === 'string' ||
|
|
35
|
+
Array.isArray(member['enum']) ||
|
|
36
|
+
'const' in member ||
|
|
37
|
+
typeof member['format'] === 'string',
|
|
38
|
+
)
|
|
39
|
+
if (!rich) return undefined
|
|
40
|
+
return variants
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The path segment qualifying a union variant by its index (see {@link unionVariantSchemas}). */
|
|
44
|
+
export function unionVariantSegment(index: number): string {
|
|
45
|
+
return `variant-${index}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the sole non-`null` member of a `oneOf`/`anyOf` union when the union
|
|
50
|
+
* has exactly one non-`null` member — the ubiquitous "nullable"/optional
|
|
51
|
+
* pattern, e.g. `oneOf: [{ type: 'null' }, { ...object }]`.
|
|
52
|
+
*
|
|
53
|
+
* Such a union is never shown as a variant picker (see
|
|
54
|
+
* {@link unionVariantSchemas}, which needs ≥2 non-null members), so without
|
|
55
|
+
* unwrapping it renders as a bare `null | object` type label with no expandable
|
|
56
|
+
* children. Callers resolve a schema through this first so the single member's
|
|
57
|
+
* type and nested properties surface instead. The wrapper's `description` wins
|
|
58
|
+
* over the member's so field-level docs are preserved.
|
|
59
|
+
*
|
|
60
|
+
* Returns `undefined` for non-unions or unions with 0 or ≥2 non-null members.
|
|
61
|
+
*/
|
|
62
|
+
export function unwrapSingleVariant(schema: SchemaObject | undefined): SchemaObject | undefined {
|
|
63
|
+
if (!schema) return undefined
|
|
64
|
+
const members = (schema['oneOf'] ?? schema['anyOf']) as SchemaObject[] | undefined
|
|
65
|
+
if (!Array.isArray(members)) return undefined
|
|
66
|
+
const nonNull = members.filter(
|
|
67
|
+
(member) => member && typeof member === 'object' && member['type'] !== 'null',
|
|
68
|
+
)
|
|
69
|
+
if (nonNull.length !== 1) return undefined
|
|
70
|
+
// biome-ignore lint/style/noNonNullAssertion: length checked above
|
|
71
|
+
const member = nonNull[0]!
|
|
72
|
+
const description = schema['description'] ?? member['description']
|
|
73
|
+
return description === undefined ? member : { ...member, description }
|
|
74
|
+
}
|
package/src/internal/search.ts
CHANGED
|
@@ -15,6 +15,8 @@ import * as UnistUtil from 'unist-util-visit'
|
|
|
15
15
|
import * as yaml from 'yaml'
|
|
16
16
|
import type * as Config from './config.js'
|
|
17
17
|
import { extractSubheading, getPhrasingContentText } from './mdx.js'
|
|
18
|
+
import * as OpenApiRegistry from './openapi/registry.js'
|
|
19
|
+
import * as OpenApiSearch from './openapi/search.js'
|
|
18
20
|
import * as Path from './path.js'
|
|
19
21
|
import { searchFields, storeFields, tokenize } from './search.client.js'
|
|
20
22
|
import * as Sidebar from './sidebar.js'
|
|
@@ -92,9 +94,27 @@ export namespace SearchDocuments {
|
|
|
92
94
|
const externalLinks = extractExternalLinks(config)
|
|
93
95
|
allDocuments.push(...externalLinks)
|
|
94
96
|
|
|
97
|
+
const openapiDocuments = await fromOpenApi(config)
|
|
98
|
+
allDocuments.push(...openapiDocuments)
|
|
99
|
+
|
|
95
100
|
return allDocuments
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Builds search documents for every configured OpenAPI section from its parsed
|
|
105
|
+
* IR, so the auto-generated reference is indexed alongside MDX pages. Failures
|
|
106
|
+
* are swallowed so a broken/unreachable spec can't break the whole index.
|
|
107
|
+
*/
|
|
108
|
+
async function fromOpenApi(config: Config.Config): Promise<Document[]> {
|
|
109
|
+
if (!config.openapi || config.openapi.length === 0) return []
|
|
110
|
+
try {
|
|
111
|
+
const specs = await OpenApiRegistry.build(config)
|
|
112
|
+
return Object.values(specs).flatMap((ir) => OpenApiSearch.toSearchDocuments(ir))
|
|
113
|
+
} catch {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
export function extractExternalLinks(config: Config.Config): Document[] {
|
|
99
119
|
const documents: Document[] = []
|
|
100
120
|
const seenHrefs = new Set<string>()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime stub for the `virtual:vocs/config` module used in unit tests.
|
|
3
|
+
*
|
|
4
|
+
* The real module is provided by a Vite plugin at dev/build time, so it does not
|
|
5
|
+
* exist under vitest. Components that import it (e.g. `CodeToHtml`) only read
|
|
6
|
+
* `config` inside lazy, effect-driven code paths that never run during static
|
|
7
|
+
* rendering, so this minimal shape is enough to let those modules import.
|
|
8
|
+
*/
|
|
9
|
+
export const config = {
|
|
10
|
+
codeHighlight: {
|
|
11
|
+
langAlias: {},
|
|
12
|
+
themes: { light: 'github-light', dark: 'github-dark-dimmed' },
|
|
13
|
+
},
|
|
14
|
+
}
|
|
@@ -12,6 +12,8 @@ import * as Icons from './icons.js'
|
|
|
12
12
|
import * as Langs from './langs.js'
|
|
13
13
|
import * as Llms from './llms.js'
|
|
14
14
|
import * as Mdx from './mdx.js'
|
|
15
|
+
import type * as OpenApi from './openapi/index.js'
|
|
16
|
+
import * as OpenApiRegistry from './openapi/registry.js'
|
|
15
17
|
import { SearchDocuments, SearchIndex } from './search.js'
|
|
16
18
|
import * as ShikiTransformers from './shiki-transformers.js'
|
|
17
19
|
import * as TaskRunner from './task-runner.js'
|
|
@@ -174,8 +176,12 @@ export function llms(config: Config.Config): PluginOption {
|
|
|
174
176
|
async function buildLlmsContent() {
|
|
175
177
|
const pagesDir = path.resolve(viteConfig.root, config.srcDir, config.pagesDir)
|
|
176
178
|
const pages = await Llms.getPagesFromDir(pagesDir)
|
|
179
|
+
const openapiPages = await Llms.getOpenApiPages(config)
|
|
180
|
+
// Generated OpenAPI pages first so they win over any consumer override at the
|
|
181
|
+
// same generated route (`.md` serves the full reference); authored-only guide
|
|
182
|
+
// pages under the section keep their own content.
|
|
177
183
|
return Llms.buildLlmsContent({
|
|
178
|
-
pages,
|
|
184
|
+
pages: [...openapiPages, ...pages],
|
|
179
185
|
title,
|
|
180
186
|
description,
|
|
181
187
|
rehypePlugins,
|
|
@@ -777,7 +783,7 @@ export function virtualConfig(config: Config.Config): PluginOption {
|
|
|
777
783
|
},
|
|
778
784
|
load(id) {
|
|
779
785
|
if (id === resolvedVirtualModuleId) {
|
|
780
|
-
const currentConfig = Config.getGlobal() ?? config
|
|
786
|
+
const currentConfig = OpenApiRegistry.mergeSidebar(Config.getGlobal() ?? config)
|
|
781
787
|
const serializedConfig =
|
|
782
788
|
mode === 'development' ? { ...currentConfig, baseUrl: undefined } : currentConfig
|
|
783
789
|
return `export const config = ${ConfigSerializer.serialize(serializedConfig)}`
|
|
@@ -816,26 +822,30 @@ export function virtualLangs(config: Config.Config): PluginOption {
|
|
|
816
822
|
return 'export const langs = []'
|
|
817
823
|
}
|
|
818
824
|
|
|
819
|
-
const
|
|
820
|
-
const langExports: string[] = []
|
|
825
|
+
const langIds = new Set<string>()
|
|
821
826
|
|
|
822
827
|
for (let i = 0; i < transformers.length; i++) {
|
|
823
828
|
const t = transformers[i]
|
|
824
829
|
if (t && 'langs' in t && Array.isArray(t.langs) && t.langs.length > 0) {
|
|
825
|
-
const
|
|
826
|
-
.
|
|
827
|
-
.filter(Boolean) as string[]
|
|
828
|
-
for (const langId of langIds) {
|
|
829
|
-
imports.push(`import ${langId}Lang from 'shiki/langs/${langId}.mjs'`)
|
|
830
|
-
langExports.push(`...${langId}Lang`)
|
|
830
|
+
for (const lang of t.langs) {
|
|
831
|
+
if (lang.name) langIds.add(lang.name)
|
|
831
832
|
}
|
|
832
833
|
}
|
|
833
834
|
}
|
|
834
835
|
|
|
835
|
-
if (!
|
|
836
|
+
if (!langIds.size) {
|
|
836
837
|
return 'export const langs = []'
|
|
837
838
|
}
|
|
838
839
|
|
|
840
|
+
const imports: string[] = []
|
|
841
|
+
const langExports: string[] = []
|
|
842
|
+
let index = 0
|
|
843
|
+
for (const langId of langIds) {
|
|
844
|
+
const importName = `lang${index++}`
|
|
845
|
+
imports.push(`import ${importName} from 'shiki/langs/${langId}.mjs'`)
|
|
846
|
+
langExports.push(`...${importName}`)
|
|
847
|
+
}
|
|
848
|
+
|
|
839
849
|
return `${imports.join('\n')}\nexport const langs = [${langExports.join(', ')}]`
|
|
840
850
|
}
|
|
841
851
|
return
|
|
@@ -1082,3 +1092,88 @@ export { matchIcon, resolveIcon, resolveIconSync } from './icons.js'
|
|
|
1082
1092
|
},
|
|
1083
1093
|
}
|
|
1084
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Vite plugin that parses configured OpenAPI specs and exposes them as a
|
|
1098
|
+
* virtual module (`virtual:vocs/openapi`).
|
|
1099
|
+
*
|
|
1100
|
+
* The module exports `specs`, a map of mount path to parsed
|
|
1101
|
+
* {@link OpenApi.Ir | intermediate representation}, consumed by the OpenAPI
|
|
1102
|
+
* route component and sidebar generator.
|
|
1103
|
+
*
|
|
1104
|
+
* Local spec files are watched in dev: changes invalidate the module and
|
|
1105
|
+
* trigger a full reload so the section regenerates.
|
|
1106
|
+
*/
|
|
1107
|
+
export function openapi(config: Config.Config): PluginOption {
|
|
1108
|
+
const virtualModuleId = 'virtual:vocs/openapi'
|
|
1109
|
+
const resolvedVirtualModuleId = `\0${virtualModuleId}`
|
|
1110
|
+
|
|
1111
|
+
/** Resolve absolute paths of local file specs for dev watching. */
|
|
1112
|
+
function specFilePaths(currentConfig: Config.Config): string[] {
|
|
1113
|
+
const paths: string[] = []
|
|
1114
|
+
for (const entry of currentConfig.openapi ?? []) {
|
|
1115
|
+
const { spec } = entry
|
|
1116
|
+
if (typeof spec !== 'string') continue
|
|
1117
|
+
if (spec.startsWith('http://') || spec.startsWith('https://')) continue
|
|
1118
|
+
paths.push(path.isAbsolute(spec) ? spec : path.resolve(currentConfig.rootDir, spec))
|
|
1119
|
+
}
|
|
1120
|
+
return paths
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return {
|
|
1124
|
+
name: 'vocs:openapi',
|
|
1125
|
+
enforce: 'pre',
|
|
1126
|
+
async buildStart() {
|
|
1127
|
+
// Parse specs up front so generated sidebars are available when the
|
|
1128
|
+
// `virtual:vocs/config` module is serialized.
|
|
1129
|
+
try {
|
|
1130
|
+
await OpenApiRegistry.build(Config.getGlobal() ?? config)
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
this.error(
|
|
1133
|
+
`Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : String(error)}`,
|
|
1134
|
+
)
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
resolveId(id) {
|
|
1138
|
+
if (id === virtualModuleId) return resolvedVirtualModuleId
|
|
1139
|
+
return
|
|
1140
|
+
},
|
|
1141
|
+
configureServer(server) {
|
|
1142
|
+
server.watcher.on('change', async (changedPath) => {
|
|
1143
|
+
const currentConfig = Config.getGlobal() ?? config
|
|
1144
|
+
if (!specFilePaths(currentConfig).includes(changedPath)) return
|
|
1145
|
+
|
|
1146
|
+
// Re-parse before reloading so the new sidebar + page are ready.
|
|
1147
|
+
OpenApiRegistry.invalidate()
|
|
1148
|
+
try {
|
|
1149
|
+
await OpenApiRegistry.build(currentConfig)
|
|
1150
|
+
} catch {}
|
|
1151
|
+
|
|
1152
|
+
for (const moduleId of [resolvedVirtualModuleId, '\0virtual:vocs/config']) {
|
|
1153
|
+
const mod = server.moduleGraph.getModuleById(moduleId)
|
|
1154
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
1155
|
+
}
|
|
1156
|
+
server.ws.send({ type: 'full-reload' })
|
|
1157
|
+
})
|
|
1158
|
+
},
|
|
1159
|
+
async load(id) {
|
|
1160
|
+
if (id !== resolvedVirtualModuleId) return
|
|
1161
|
+
|
|
1162
|
+
const currentConfig = Config.getGlobal() ?? config
|
|
1163
|
+
|
|
1164
|
+
// Watch local spec files so the build re-runs when they change.
|
|
1165
|
+
for (const filePath of specFilePaths(currentConfig)) this.addWatchFile(filePath)
|
|
1166
|
+
|
|
1167
|
+
let specs: Record<string, OpenApi.Ir> = {}
|
|
1168
|
+
try {
|
|
1169
|
+
specs = await OpenApiRegistry.build(currentConfig)
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
this.error(
|
|
1172
|
+
`Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : String(error)}`,
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return `export const specs = ${JSON.stringify(specs)}`
|
|
1177
|
+
},
|
|
1178
|
+
}
|
|
1179
|
+
}
|