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,613 @@
|
|
|
1
|
+
import { dereference, upgrade } from '@scalar/openapi-parser'
|
|
2
|
+
import GithubSlugger from 'github-slugger'
|
|
3
|
+
import type * as OpenApi from './openapi.js'
|
|
4
|
+
import * as OpenRpc from './openrpc.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HTTP methods recognized on an OpenAPI path item, in canonical display order.
|
|
8
|
+
*/
|
|
9
|
+
export const methods = [
|
|
10
|
+
'get',
|
|
11
|
+
'post',
|
|
12
|
+
'put',
|
|
13
|
+
'patch',
|
|
14
|
+
'delete',
|
|
15
|
+
'head',
|
|
16
|
+
'options',
|
|
17
|
+
'trace',
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
export type Method = (typeof methods)[number]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Intermediate representation of a parsed OpenAPI document.
|
|
24
|
+
*
|
|
25
|
+
* This is the serializable shape consumed by the React rendering layer (via the
|
|
26
|
+
* `virtual:vocs/openapi` module) and the sidebar generator. It intentionally
|
|
27
|
+
* keeps raw JSON Schema objects (`parameters`, `requestBody`, `responses`) so
|
|
28
|
+
* the renderer can display them without a lossy transform.
|
|
29
|
+
*/
|
|
30
|
+
export type Ir = {
|
|
31
|
+
/** Mount path the section is served at (e.g. `/api`). */
|
|
32
|
+
path: string
|
|
33
|
+
/**
|
|
34
|
+
* Spec source handed to the interactive client (Scalar API client). A `url`
|
|
35
|
+
* when the configured spec is a URL (the client fetches it directly), else
|
|
36
|
+
* the upgraded document `content` so the client works for file/inline specs.
|
|
37
|
+
*/
|
|
38
|
+
client: { url: string } | { content: Record<string, unknown> }
|
|
39
|
+
/** Document metadata. */
|
|
40
|
+
info: {
|
|
41
|
+
title: string
|
|
42
|
+
version?: string | undefined
|
|
43
|
+
description?: string | undefined
|
|
44
|
+
}
|
|
45
|
+
/** Servers the API is hosted at. */
|
|
46
|
+
servers: IrServer[]
|
|
47
|
+
/** Operations grouped by category (tag or path segment). */
|
|
48
|
+
groups: IrGroup[]
|
|
49
|
+
/**
|
|
50
|
+
* Doc-only "trait" pages: tags marked `x-traitTag: true` (Redoc convention),
|
|
51
|
+
* which carry Markdown `description` content but no operations. The standalone
|
|
52
|
+
* handler renders each as a guide page nested under `Introduction`.
|
|
53
|
+
*/
|
|
54
|
+
traits: IrTrait[]
|
|
55
|
+
/** Named security schemes from `components.securitySchemes`. */
|
|
56
|
+
securitySchemes: Record<string, IrSecurityScheme>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type IrTrait = {
|
|
60
|
+
/** Stable slug used as the page route segment. */
|
|
61
|
+
id: string
|
|
62
|
+
/** Display name (the page title fallback). */
|
|
63
|
+
name: string
|
|
64
|
+
/** Page body (Markdown). */
|
|
65
|
+
description?: string | undefined
|
|
66
|
+
/** Optional subtitle rendered under the title (`x-subtitle`). */
|
|
67
|
+
subtitle?: string | undefined
|
|
68
|
+
/**
|
|
69
|
+
* Sidebar group to nest the page under (`x-parent`, a tag/group name). When
|
|
70
|
+
* omitted, the page nests under `Introduction`. When it names an existing
|
|
71
|
+
* operation group, the page joins that group; otherwise a new sidebar group
|
|
72
|
+
* with that name is created.
|
|
73
|
+
*/
|
|
74
|
+
parent?: string | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type IrServer = {
|
|
78
|
+
url: string
|
|
79
|
+
description?: string | undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type IrGroup = {
|
|
83
|
+
/** Stable slug used as the section anchor on the page. */
|
|
84
|
+
id: string
|
|
85
|
+
/** Display name of the category. */
|
|
86
|
+
name: string
|
|
87
|
+
/** Optional category description (Markdown). */
|
|
88
|
+
description?: string | undefined
|
|
89
|
+
/** Operations belonging to this category. */
|
|
90
|
+
operations: IrOperation[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type IrOperation = {
|
|
94
|
+
/** Stable slug used as the operation's anchor on the page. */
|
|
95
|
+
id: string
|
|
96
|
+
/** HTTP method (uppercased for display, e.g. `GET`). */
|
|
97
|
+
method: string
|
|
98
|
+
/** Templated path (e.g. `/pets/{petId}`). */
|
|
99
|
+
path: string
|
|
100
|
+
/**
|
|
101
|
+
* Name of the host operation's request example to preselect in the
|
|
102
|
+
* interactive client. Set for JSON-RPC operations expanded from an OpenRPC
|
|
103
|
+
* document (the JSON-RPC method name); lets "Try" prefill the right envelope
|
|
104
|
+
* even though every method shares the same path + verb.
|
|
105
|
+
*/
|
|
106
|
+
rpcExample?: string | undefined
|
|
107
|
+
summary?: string | undefined
|
|
108
|
+
description?: string | undefined
|
|
109
|
+
deprecated?: boolean | undefined
|
|
110
|
+
/** Merged path-level + operation-level parameters. */
|
|
111
|
+
parameters: IrParameter[]
|
|
112
|
+
/** Request body (if any). */
|
|
113
|
+
requestBody?: IrBody | undefined
|
|
114
|
+
/** Responses keyed by status code (e.g. `200`, `default`). */
|
|
115
|
+
responses: IrResponse[]
|
|
116
|
+
/** Security requirements that apply to this operation. */
|
|
117
|
+
security?: Record<string, string[]>[] | undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type IrParameter = {
|
|
121
|
+
name: string
|
|
122
|
+
/**
|
|
123
|
+
* Where the parameter is supplied. `rpc` is a Vocs extension for JSON-RPC
|
|
124
|
+
* method params expanded from an OpenRPC document (rendered like query/path
|
|
125
|
+
* params but carried in the request body envelope).
|
|
126
|
+
*/
|
|
127
|
+
in: 'path' | 'query' | 'header' | 'cookie' | 'rpc'
|
|
128
|
+
required?: boolean | undefined
|
|
129
|
+
deprecated?: boolean | undefined
|
|
130
|
+
description?: string | undefined
|
|
131
|
+
/** Raw JSON Schema for the parameter value. */
|
|
132
|
+
schema?: Record<string, unknown> | undefined
|
|
133
|
+
/** Example value provided at the parameter level. */
|
|
134
|
+
example?: unknown
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type IrBody = {
|
|
138
|
+
required?: boolean | undefined
|
|
139
|
+
description?: string | undefined
|
|
140
|
+
/**
|
|
141
|
+
* Suppress the rendered "Request Body" section while still using the body to
|
|
142
|
+
* generate request code samples. Set for JSON-RPC operations expanded from an
|
|
143
|
+
* OpenRPC document, whose params are shown in a "Parameters" section instead.
|
|
144
|
+
*/
|
|
145
|
+
hidden?: boolean | undefined
|
|
146
|
+
/** Content keyed by media type (e.g. `application/json`). */
|
|
147
|
+
content: IrMediaType[]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type IrMediaType = {
|
|
151
|
+
mediaType: string
|
|
152
|
+
/** Raw JSON Schema for the media type. */
|
|
153
|
+
schema?: Record<string, unknown> | undefined
|
|
154
|
+
/** Example value(s), if provided. */
|
|
155
|
+
example?: unknown
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type IrResponse = {
|
|
159
|
+
/** Status code or `default`. */
|
|
160
|
+
status: string
|
|
161
|
+
description?: string | undefined
|
|
162
|
+
content: IrMediaType[]
|
|
163
|
+
/** Response headers, keyed by header name in the spec. */
|
|
164
|
+
headers: IrHeader[]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type IrHeader = {
|
|
168
|
+
name: string
|
|
169
|
+
required?: boolean | undefined
|
|
170
|
+
deprecated?: boolean | undefined
|
|
171
|
+
description?: string | undefined
|
|
172
|
+
/** Raw JSON Schema for the header value. */
|
|
173
|
+
schema?: Record<string, unknown> | undefined
|
|
174
|
+
/** Example value provided at the header level. */
|
|
175
|
+
example?: unknown
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type IrSecurityScheme = Record<string, unknown> & {
|
|
179
|
+
type: string
|
|
180
|
+
description?: string | undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Minimal structural typing for the dereferenced OpenAPI 3.1 document we read. */
|
|
184
|
+
type Document = {
|
|
185
|
+
info?: { title?: string; version?: string; description?: string }
|
|
186
|
+
servers?: { url?: string; description?: string }[]
|
|
187
|
+
tags?: {
|
|
188
|
+
name?: string
|
|
189
|
+
description?: string
|
|
190
|
+
'x-traitTag'?: boolean
|
|
191
|
+
'x-subtitle'?: string
|
|
192
|
+
'x-parent'?: string
|
|
193
|
+
}[]
|
|
194
|
+
paths?: Record<string, PathItem>
|
|
195
|
+
components?: { securitySchemes?: Record<string, IrSecurityScheme> }
|
|
196
|
+
security?: Record<string, string[]>[]
|
|
197
|
+
}
|
|
198
|
+
type PathItem = Record<string, unknown> & {
|
|
199
|
+
parameters?: RawParameter[]
|
|
200
|
+
} & Partial<Record<Method, Operation>>
|
|
201
|
+
type Operation = {
|
|
202
|
+
operationId?: string
|
|
203
|
+
summary?: string
|
|
204
|
+
description?: string
|
|
205
|
+
deprecated?: boolean
|
|
206
|
+
tags?: string[]
|
|
207
|
+
parameters?: RawParameter[]
|
|
208
|
+
requestBody?: RawBody
|
|
209
|
+
responses?: Record<string, RawResponse>
|
|
210
|
+
security?: Record<string, string[]>[]
|
|
211
|
+
/**
|
|
212
|
+
* Vocs extension: a path/URL to (or inline) OpenRPC document. When present,
|
|
213
|
+
* this single operation is expanded into one operation per JSON-RPC method.
|
|
214
|
+
*/
|
|
215
|
+
'x-openrpc'?: string | OpenRpc.Document
|
|
216
|
+
}
|
|
217
|
+
type RawParameter = {
|
|
218
|
+
name?: string
|
|
219
|
+
in?: string
|
|
220
|
+
required?: boolean
|
|
221
|
+
deprecated?: boolean
|
|
222
|
+
description?: string
|
|
223
|
+
schema?: Record<string, unknown>
|
|
224
|
+
example?: unknown
|
|
225
|
+
}
|
|
226
|
+
type RawBody = {
|
|
227
|
+
required?: boolean
|
|
228
|
+
description?: string
|
|
229
|
+
content?: Record<string, { schema?: Record<string, unknown>; example?: unknown }>
|
|
230
|
+
}
|
|
231
|
+
type RawResponse = {
|
|
232
|
+
description?: string
|
|
233
|
+
content?: Record<string, { schema?: Record<string, unknown>; example?: unknown }>
|
|
234
|
+
headers?: Record<string, RawHeader>
|
|
235
|
+
}
|
|
236
|
+
type RawHeader = {
|
|
237
|
+
required?: boolean
|
|
238
|
+
deprecated?: boolean
|
|
239
|
+
description?: string
|
|
240
|
+
schema?: Record<string, unknown>
|
|
241
|
+
example?: unknown
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parses an OpenAPI spec into the Vocs intermediate representation.
|
|
246
|
+
*
|
|
247
|
+
* Loads the spec (inline object, file path, raw content, or URL), upgrades it
|
|
248
|
+
* to OpenAPI 3.1, dereferences all `$ref`s, then groups operations into
|
|
249
|
+
* categories (by tag) for the sidebar and per-category pages.
|
|
250
|
+
*/
|
|
251
|
+
export async function parse(config: OpenApi.Config, options: parse.Options = {}): Promise<Ir> {
|
|
252
|
+
const { rootDir = typeof process !== 'undefined' ? process.cwd() : '.' } = options
|
|
253
|
+
|
|
254
|
+
// Resolve a lazy provider or an already-started promise (e.g. a
|
|
255
|
+
// runtime-generated spec) to its value.
|
|
256
|
+
const input = typeof config.spec === 'function' ? config.spec() : config.spec
|
|
257
|
+
const spec = await input
|
|
258
|
+
|
|
259
|
+
const raw = await load(spec, rootDir)
|
|
260
|
+
const { specification } = upgrade(raw as never)
|
|
261
|
+
const { schema } = dereference(specification as never)
|
|
262
|
+
const document = (schema ?? specification) as Document
|
|
263
|
+
|
|
264
|
+
const isUrl =
|
|
265
|
+
typeof spec === 'string' && (spec.startsWith('http://') || spec.startsWith('https://'))
|
|
266
|
+
const specUrl = isUrl ? spec : undefined
|
|
267
|
+
|
|
268
|
+
// Base for resolving relative `x-openrpc` URLs (e.g. `/openrpc.json`): the
|
|
269
|
+
// spec URL when the spec was loaded from a URL, else the caller-provided base
|
|
270
|
+
// (the request origin in the standalone server handler).
|
|
271
|
+
const openrpcBaseUrl = specUrl ?? options.baseUrl
|
|
272
|
+
|
|
273
|
+
const servers = (document.servers ?? [])
|
|
274
|
+
.map((server) => ({
|
|
275
|
+
url: resolveServerUrl(server.url ?? '', specUrl),
|
|
276
|
+
description: server.description,
|
|
277
|
+
}))
|
|
278
|
+
.filter((server) => server.url)
|
|
279
|
+
|
|
280
|
+
const securitySchemes = document.components?.securitySchemes ?? {}
|
|
281
|
+
|
|
282
|
+
const { groups, injections } = await buildGroups(document, { baseUrl: openrpcBaseUrl })
|
|
283
|
+
|
|
284
|
+
// Inject JSON-RPC examples onto the host operation in the spec the
|
|
285
|
+
// interactive client loads, so each "Try" can preselect its method's
|
|
286
|
+
// envelope. The examples must live in the document Scalar reads; when the
|
|
287
|
+
// spec was a URL it would otherwise fetch the un-augmented version, so fall
|
|
288
|
+
// back to handing it the augmented `content` instead.
|
|
289
|
+
for (const injection of injections)
|
|
290
|
+
injectRpcExamples(specification as Record<string, unknown>, injection)
|
|
291
|
+
|
|
292
|
+
const client =
|
|
293
|
+
isUrl && injections.length === 0
|
|
294
|
+
? { url: spec as string }
|
|
295
|
+
: { content: specification as Record<string, unknown> }
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
path: config.path ?? '/',
|
|
299
|
+
client,
|
|
300
|
+
info: {
|
|
301
|
+
title: document.info?.title ?? 'API Reference',
|
|
302
|
+
version: document.info?.version,
|
|
303
|
+
description: document.info?.description,
|
|
304
|
+
},
|
|
305
|
+
servers,
|
|
306
|
+
groups,
|
|
307
|
+
traits: buildTraits(document),
|
|
308
|
+
securitySchemes,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Adds named JSON-RPC request examples to a host operation's
|
|
314
|
+
* `application/json` request body in the (upgraded) spec document. Scalar
|
|
315
|
+
* builds one selectable request example per key, which the "Try" button
|
|
316
|
+
* navigates to via the operation's `rpcExample`.
|
|
317
|
+
*/
|
|
318
|
+
function injectRpcExamples(specification: Record<string, unknown>, injection: RpcInjection): void {
|
|
319
|
+
const paths = specification['paths'] as Record<string, Record<string, unknown>> | undefined
|
|
320
|
+
const operation = paths?.[injection.path]?.[injection.method] as
|
|
321
|
+
| Record<string, unknown>
|
|
322
|
+
| undefined
|
|
323
|
+
if (!operation) return
|
|
324
|
+
|
|
325
|
+
let requestBody = operation['requestBody'] as Record<string, unknown> | undefined
|
|
326
|
+
// A `$ref`'d or absent body can't be merged in place; replace with an inline
|
|
327
|
+
// one carrying just the examples (the params already render in the docs).
|
|
328
|
+
if (!requestBody || typeof requestBody !== 'object' || '$ref' in requestBody) {
|
|
329
|
+
requestBody = {}
|
|
330
|
+
operation['requestBody'] = requestBody
|
|
331
|
+
}
|
|
332
|
+
if (!requestBody['content']) requestBody['content'] = {}
|
|
333
|
+
const content = requestBody['content'] as Record<string, Record<string, unknown>>
|
|
334
|
+
if (!content['application/json']) content['application/json'] = {}
|
|
335
|
+
const media = content['application/json']
|
|
336
|
+
media['examples'] = { ...(media['examples'] as Record<string, unknown>), ...injection.examples }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Builds doc-only "trait" pages from tags marked `x-traitTag: true`. */
|
|
340
|
+
function buildTraits(document: Document): IrTrait[] {
|
|
341
|
+
const slugger = new GithubSlugger()
|
|
342
|
+
const traits: IrTrait[] = []
|
|
343
|
+
for (const tag of document.tags ?? []) {
|
|
344
|
+
if (!tag.name || !tag['x-traitTag']) continue
|
|
345
|
+
traits.push({
|
|
346
|
+
id: slugger.slug(tag.name),
|
|
347
|
+
name: tag.name,
|
|
348
|
+
description: tag.description,
|
|
349
|
+
subtitle: tag['x-subtitle'],
|
|
350
|
+
parent: tag['x-parent'],
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
return traits
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export declare namespace parse {
|
|
357
|
+
type Options = {
|
|
358
|
+
/** Directory file-path specs are resolved against. @default process.cwd() */
|
|
359
|
+
rootDir?: string | undefined
|
|
360
|
+
/**
|
|
361
|
+
* Base URL relative `x-openrpc` URLs (e.g. `/openrpc.json`) resolve against
|
|
362
|
+
* when the spec itself wasn't loaded from a URL — the request origin in the
|
|
363
|
+
* standalone server handler.
|
|
364
|
+
*/
|
|
365
|
+
baseUrl?: string | undefined
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Resolves a server URL to an absolute one.
|
|
371
|
+
*
|
|
372
|
+
* OpenAPI server URLs are often relative (e.g. `/` or `/v2`), meaning "relative
|
|
373
|
+
* to the host serving this document". When the spec was loaded from a URL we
|
|
374
|
+
* resolve such URLs against that origin (matching how Scalar and other tools
|
|
375
|
+
* behave) so generated code samples point at a real, callable host. Absolute
|
|
376
|
+
* URLs are returned untouched; relative URLs for file/inline specs are kept
|
|
377
|
+
* as-is. Any trailing slash is trimmed so callers can append paths directly.
|
|
378
|
+
*/
|
|
379
|
+
function resolveServerUrl(url: string, specUrl: string | undefined): string {
|
|
380
|
+
if (!url) return ''
|
|
381
|
+
if (/^https?:\/\//.test(url)) return url.replace(/\/$/, '')
|
|
382
|
+
if (!specUrl) return url
|
|
383
|
+
try {
|
|
384
|
+
return new URL(url, specUrl).toString().replace(/\/$/, '')
|
|
385
|
+
} catch {
|
|
386
|
+
return url
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Resolves a spec input to a raw definition (object or string content). */
|
|
391
|
+
async function load(
|
|
392
|
+
spec: OpenApi.Spec,
|
|
393
|
+
rootDir: string,
|
|
394
|
+
): Promise<Record<string, unknown> | string> {
|
|
395
|
+
if (typeof spec === 'object') return spec as Record<string, unknown>
|
|
396
|
+
|
|
397
|
+
if (spec.startsWith('http://') || spec.startsWith('https://')) {
|
|
398
|
+
const response = await fetch(spec)
|
|
399
|
+
if (!response.ok)
|
|
400
|
+
throw new Error(`Failed to fetch OpenAPI spec from ${spec}: ${response.statusText}`)
|
|
401
|
+
return response.text()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Raw JSON/YAML content passed inline as a string — no filesystem needed.
|
|
405
|
+
// (Avoids importing `node:*` in non-Node runtimes like Cloudflare Workers,
|
|
406
|
+
// where local-file specs are unsupported anyway.)
|
|
407
|
+
if (looksLikeRawContent(spec)) return spec
|
|
408
|
+
|
|
409
|
+
// Treat as a file path relative to rootDir; fall back to raw content. `node:*`
|
|
410
|
+
// is imported dynamically so URL/inline specs stay runtime-portable.
|
|
411
|
+
try {
|
|
412
|
+
const [{ default: fs }, path] = await Promise.all([
|
|
413
|
+
import('node:fs/promises').then((module) => ({ default: module })),
|
|
414
|
+
import('node:path'),
|
|
415
|
+
])
|
|
416
|
+
const filePath = path.isAbsolute(spec) ? spec : path.resolve(rootDir, spec)
|
|
417
|
+
return await fs.readFile(filePath, 'utf-8')
|
|
418
|
+
} catch {
|
|
419
|
+
return spec
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Heuristic for raw inline spec content (vs. a file path): JSON starts with `{`,
|
|
425
|
+
* and a YAML document typically contains a newline or an `openapi:`/`swagger:`
|
|
426
|
+
* key. File paths are single-line and do not.
|
|
427
|
+
*/
|
|
428
|
+
function looksLikeRawContent(spec: string): boolean {
|
|
429
|
+
const trimmed = spec.trimStart()
|
|
430
|
+
if (trimmed.startsWith('{')) return true
|
|
431
|
+
if (/\n/.test(spec) && /(^|\n)\s*(openapi|swagger)\s*:/.test(spec)) return true
|
|
432
|
+
return false
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** A set of named JSON-RPC examples to inject onto a host spec operation. */
|
|
436
|
+
type RpcInjection = {
|
|
437
|
+
path: string
|
|
438
|
+
method: Method
|
|
439
|
+
examples: Record<string, OpenRpc.expand.RpcExample>
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
type BuildGroupsResult = { groups: IrGroup[]; injections: RpcInjection[] }
|
|
443
|
+
|
|
444
|
+
/** Groups operations by their first tag. */
|
|
445
|
+
async function buildGroups(
|
|
446
|
+
document: Document,
|
|
447
|
+
options: { baseUrl?: string | undefined } = {},
|
|
448
|
+
): Promise<BuildGroupsResult> {
|
|
449
|
+
const slugger = new GithubSlugger()
|
|
450
|
+
const order: string[] = []
|
|
451
|
+
const byName = new Map<string, IrOperation[]>()
|
|
452
|
+
const descriptions = new Map<string, string | undefined>()
|
|
453
|
+
const injections: RpcInjection[] = []
|
|
454
|
+
|
|
455
|
+
// Seed group order from document-level `tags` so authoring order is preserved.
|
|
456
|
+
// Skip `x-traitTag` tags — they're doc-only pages, not operation categories.
|
|
457
|
+
for (const tag of document.tags ?? []) {
|
|
458
|
+
if (!tag.name || tag['x-traitTag']) continue
|
|
459
|
+
if (!byName.has(tag.name)) {
|
|
460
|
+
byName.set(tag.name, [])
|
|
461
|
+
order.push(tag.name)
|
|
462
|
+
descriptions.set(tag.name, tag.description)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const paths = document.paths ?? {}
|
|
467
|
+
for (const [pathname, item] of Object.entries(paths)) {
|
|
468
|
+
if (!item || typeof item !== 'object') continue
|
|
469
|
+
const pathParameters = item.parameters ?? []
|
|
470
|
+
|
|
471
|
+
for (const method of methods) {
|
|
472
|
+
const operation = item[method]
|
|
473
|
+
if (!operation) continue
|
|
474
|
+
|
|
475
|
+
const groupName = operation.tags?.[0] ?? 'default'
|
|
476
|
+
|
|
477
|
+
if (!byName.has(groupName)) {
|
|
478
|
+
byName.set(groupName, [])
|
|
479
|
+
order.push(groupName)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const built = buildOperation({
|
|
483
|
+
method,
|
|
484
|
+
pathname,
|
|
485
|
+
operation,
|
|
486
|
+
pathParameters,
|
|
487
|
+
slugger,
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// `x-openrpc` expands one operation into one operation per JSON-RPC method
|
|
491
|
+
// (each its own sidebar entry/section). On failure, fall back to the
|
|
492
|
+
// single host operation so the docs still render.
|
|
493
|
+
if (operation['x-openrpc']) {
|
|
494
|
+
try {
|
|
495
|
+
const { operations, examples } = await OpenRpc.expand(built, operation['x-openrpc'], {
|
|
496
|
+
baseUrl: options.baseUrl,
|
|
497
|
+
})
|
|
498
|
+
byName.get(groupName)?.push(...(operations.length > 0 ? operations : [built]))
|
|
499
|
+
// Record the named JSON-RPC examples so the caller can inject them
|
|
500
|
+
// onto the host operation in the spec handed to the interactive
|
|
501
|
+
// client (Scalar selects the right one per "Try" click).
|
|
502
|
+
if (operations.length > 0 && Object.keys(examples).length > 0)
|
|
503
|
+
injections.push({ path: pathname, method, examples })
|
|
504
|
+
} catch (error) {
|
|
505
|
+
// Fall back to the single host operation so the docs still render, but
|
|
506
|
+
// surface why the JSON-RPC methods are missing (e.g. a relative
|
|
507
|
+
// `x-openrpc` URL with no base, or an unreachable document).
|
|
508
|
+
console.warn(
|
|
509
|
+
`[vocs] Failed to expand x-openrpc for ${method.toUpperCase()} ${pathname}: ${
|
|
510
|
+
error instanceof Error ? error.message : String(error)
|
|
511
|
+
}`,
|
|
512
|
+
)
|
|
513
|
+
byName.get(groupName)?.push(built)
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
byName.get(groupName)?.push(built)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const groups = order
|
|
522
|
+
.map((name) => ({
|
|
523
|
+
id: slugger.slug(name),
|
|
524
|
+
name,
|
|
525
|
+
description: descriptions.get(name),
|
|
526
|
+
operations: byName.get(name) ?? [],
|
|
527
|
+
}))
|
|
528
|
+
.filter((group) => group.operations.length > 0)
|
|
529
|
+
|
|
530
|
+
return { groups, injections }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function buildOperation(options: {
|
|
534
|
+
method: Method
|
|
535
|
+
pathname: string
|
|
536
|
+
operation: Operation
|
|
537
|
+
pathParameters: RawParameter[]
|
|
538
|
+
slugger: GithubSlugger
|
|
539
|
+
}): IrOperation {
|
|
540
|
+
const { method, pathname, operation, pathParameters, slugger } = options
|
|
541
|
+
|
|
542
|
+
const idSource = operation.operationId || `${method}-${pathname}`
|
|
543
|
+
|
|
544
|
+
// Merge path-level parameters with operation-level (operation wins on conflict).
|
|
545
|
+
const merged = new Map<string, RawParameter>()
|
|
546
|
+
for (const parameter of [...pathParameters, ...(operation.parameters ?? [])]) {
|
|
547
|
+
if (!parameter?.name || !parameter.in) continue
|
|
548
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
id: slugger.slug(idSource),
|
|
553
|
+
method: method.toUpperCase(),
|
|
554
|
+
path: pathname,
|
|
555
|
+
summary: operation.summary,
|
|
556
|
+
description: operation.description,
|
|
557
|
+
deprecated: operation.deprecated,
|
|
558
|
+
parameters: [...merged.values()].map((parameter) => ({
|
|
559
|
+
name: parameter.name as string,
|
|
560
|
+
in: parameter.in as IrParameter['in'],
|
|
561
|
+
required: parameter.required,
|
|
562
|
+
deprecated: parameter.deprecated,
|
|
563
|
+
description: parameter.description,
|
|
564
|
+
schema: parameter.schema,
|
|
565
|
+
example: parameter.example,
|
|
566
|
+
})),
|
|
567
|
+
requestBody: buildBody(operation.requestBody),
|
|
568
|
+
responses: buildResponses(operation.responses),
|
|
569
|
+
security: operation.security,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function buildBody(body: RawBody | undefined): IrBody | undefined {
|
|
574
|
+
if (!body) return undefined
|
|
575
|
+
return {
|
|
576
|
+
required: body.required,
|
|
577
|
+
description: body.description,
|
|
578
|
+
content: buildContent(body.content),
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function buildResponses(responses: Record<string, RawResponse> | undefined): IrResponse[] {
|
|
583
|
+
if (!responses) return []
|
|
584
|
+
return Object.entries(responses).map(([status, response]) => ({
|
|
585
|
+
status,
|
|
586
|
+
description: response?.description,
|
|
587
|
+
content: buildContent(response?.content),
|
|
588
|
+
headers: buildHeaders(response?.headers),
|
|
589
|
+
}))
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function buildHeaders(headers: Record<string, RawHeader> | undefined): IrHeader[] {
|
|
593
|
+
if (!headers) return []
|
|
594
|
+
return Object.entries(headers).map(([name, header]) => ({
|
|
595
|
+
name,
|
|
596
|
+
required: header?.required,
|
|
597
|
+
deprecated: header?.deprecated,
|
|
598
|
+
description: header?.description,
|
|
599
|
+
schema: header?.schema,
|
|
600
|
+
example: header?.example,
|
|
601
|
+
}))
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function buildContent(
|
|
605
|
+
content: Record<string, { schema?: Record<string, unknown>; example?: unknown }> | undefined,
|
|
606
|
+
): IrMediaType[] {
|
|
607
|
+
if (!content) return []
|
|
608
|
+
return Object.entries(content).map(([mediaType, value]) => ({
|
|
609
|
+
mediaType,
|
|
610
|
+
schema: value?.schema,
|
|
611
|
+
example: value?.example,
|
|
612
|
+
}))
|
|
613
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'vitest'
|
|
2
|
+
import * as Config from '../config.js'
|
|
3
|
+
import type { SidebarItem } from '../sidebar.js'
|
|
4
|
+
import * as OpenApi from './openapi.js'
|
|
5
|
+
import * as Registry from './registry.js'
|
|
6
|
+
|
|
7
|
+
const spec = {
|
|
8
|
+
openapi: '3.1.0',
|
|
9
|
+
info: { title: 'Petstore' },
|
|
10
|
+
paths: {
|
|
11
|
+
'/pets': { get: { operationId: 'listPets', tags: ['pets'], responses: { '200': {} } } },
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function config(sidebar?: Config.Config['sidebar'], extras?: OpenApi.SidebarExtras): Config.Config {
|
|
16
|
+
return Config.define({
|
|
17
|
+
openapi: [OpenApi.from({ spec, path: '/api', sidebar: extras })],
|
|
18
|
+
sidebar,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
Registry.invalidate()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('mergeSidebar', () => {
|
|
27
|
+
test('returns config unchanged when specs not built', () => {
|
|
28
|
+
const cfg = config()
|
|
29
|
+
expect(Registry.mergeSidebar(cfg)).toBe(cfg)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('injects the OpenAPI sidebar under the mount path (no user sidebar)', async () => {
|
|
33
|
+
const cfg = config()
|
|
34
|
+
await Registry.build(cfg)
|
|
35
|
+
|
|
36
|
+
const merged = Registry.mergeSidebar(cfg)
|
|
37
|
+
const sidebar = merged.sidebar as Record<string, { backLink: boolean; items: SidebarItem[] }>
|
|
38
|
+
expect(Object.keys(sidebar)).toEqual(['/api'])
|
|
39
|
+
const section = sidebar['/api']
|
|
40
|
+
expect(section?.backLink).toBe(true)
|
|
41
|
+
// A root "Introduction" links to the section landing page.
|
|
42
|
+
expect(section?.items[0]).toEqual({ text: 'Introduction', link: '/api' })
|
|
43
|
+
// The category itself is a non-link header; an "Overview" subitem links to it.
|
|
44
|
+
expect(section?.items[1]).toMatchObject({ text: 'pets' })
|
|
45
|
+
expect(section?.items[1]?.link).toBeUndefined()
|
|
46
|
+
expect(section?.items[1]?.items?.[0]).toEqual({ text: 'Overview', link: '/api/pets' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('converts an array sidebar to path-keyed form under "/"', async () => {
|
|
50
|
+
const cfg = config([{ text: 'Home', link: '/' }])
|
|
51
|
+
await Registry.build(cfg)
|
|
52
|
+
|
|
53
|
+
const sidebar = Registry.mergeSidebar(cfg).sidebar as Record<string, unknown>
|
|
54
|
+
expect(Object.keys(sidebar).sort()).toEqual(['/', '/api'])
|
|
55
|
+
expect(sidebar['/']).toEqual([{ text: 'Home', link: '/' }])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('preserves an existing object sidebar and adds the mount key', async () => {
|
|
59
|
+
const cfg = config({ '/guide': [{ text: 'Intro', link: '/guide' }] })
|
|
60
|
+
await Registry.build(cfg)
|
|
61
|
+
|
|
62
|
+
const sidebar = Registry.mergeSidebar(cfg).sidebar as Record<string, unknown>
|
|
63
|
+
expect(Object.keys(sidebar).sort()).toEqual(['/api', '/guide'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('prepends `sidebar.top` and appends `sidebar.bottom` items', async () => {
|
|
67
|
+
const cfg = config(undefined, {
|
|
68
|
+
top: [{ text: 'Authentication', link: '/api/auth' }],
|
|
69
|
+
bottom: [{ text: 'Errors', link: '/api/errors' }],
|
|
70
|
+
})
|
|
71
|
+
await Registry.build(cfg)
|
|
72
|
+
|
|
73
|
+
const sidebar = Registry.mergeSidebar(cfg).sidebar as Record<string, { items: SidebarItem[] }>
|
|
74
|
+
const items = sidebar['/api']?.items ?? []
|
|
75
|
+
// Top item comes before the generated "Introduction".
|
|
76
|
+
expect(items[0]).toEqual({ text: 'Authentication', link: '/api/auth' })
|
|
77
|
+
expect(items[1]).toEqual({ text: 'Introduction', link: '/api' })
|
|
78
|
+
// Bottom item comes after the generated categories.
|
|
79
|
+
expect(items.at(-1)).toEqual({ text: 'Errors', link: '/api/errors' })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('does not mutate the input config', async () => {
|
|
83
|
+
const cfg = config([{ text: 'Home', link: '/' }])
|
|
84
|
+
await Registry.build(cfg)
|
|
85
|
+
|
|
86
|
+
Registry.mergeSidebar(cfg)
|
|
87
|
+
expect(Array.isArray(cfg.sidebar)).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
})
|