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.
Files changed (306) hide show
  1. package/dist/config.d.ts +1 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +1 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/globals.d.ts +16 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/config.d.ts +28 -0
  11. package/dist/internal/config.d.ts.map +1 -1
  12. package/dist/internal/config.js +10 -2
  13. package/dist/internal/config.js.map +1 -1
  14. package/dist/internal/llms.d.ts +21 -2
  15. package/dist/internal/llms.d.ts.map +1 -1
  16. package/dist/internal/llms.js +41 -1
  17. package/dist/internal/llms.js.map +1 -1
  18. package/dist/internal/markdown-negotiation.d.ts +36 -0
  19. package/dist/internal/markdown-negotiation.d.ts.map +1 -0
  20. package/dist/internal/markdown-negotiation.js +81 -0
  21. package/dist/internal/markdown-negotiation.js.map +1 -0
  22. package/dist/internal/markdown.d.ts.map +1 -1
  23. package/dist/internal/markdown.js +5 -0
  24. package/dist/internal/markdown.js.map +1 -1
  25. package/dist/internal/openapi/anchors.d.ts +25 -0
  26. package/dist/internal/openapi/anchors.d.ts.map +1 -0
  27. package/dist/internal/openapi/anchors.js +37 -0
  28. package/dist/internal/openapi/anchors.js.map +1 -0
  29. package/dist/internal/openapi/app.d.ts +89 -0
  30. package/dist/internal/openapi/app.d.ts.map +1 -0
  31. package/dist/internal/openapi/app.js +62 -0
  32. package/dist/internal/openapi/app.js.map +1 -0
  33. package/dist/internal/openapi/index.d.ts +8 -0
  34. package/dist/internal/openapi/index.d.ts.map +1 -0
  35. package/dist/internal/openapi/index.js +5 -0
  36. package/dist/internal/openapi/index.js.map +1 -0
  37. package/dist/internal/openapi/markdown.d.ts +42 -0
  38. package/dist/internal/openapi/markdown.d.ts.map +1 -0
  39. package/dist/internal/openapi/markdown.js +235 -0
  40. package/dist/internal/openapi/markdown.js.map +1 -0
  41. package/dist/internal/openapi/openapi.d.ts +187 -0
  42. package/dist/internal/openapi/openapi.d.ts.map +1 -0
  43. package/dist/internal/openapi/openapi.js +44 -0
  44. package/dist/internal/openapi/openapi.js.map +1 -0
  45. package/dist/internal/openapi/openrpc.d.ts +90 -0
  46. package/dist/internal/openapi/openrpc.d.ts.map +1 -0
  47. package/dist/internal/openapi/openrpc.js +213 -0
  48. package/dist/internal/openapi/openrpc.js.map +1 -0
  49. package/dist/internal/openapi/parser.d.ts +181 -0
  50. package/dist/internal/openapi/parser.d.ts.map +1 -0
  51. package/dist/internal/openapi/parser.js +329 -0
  52. package/dist/internal/openapi/parser.js.map +1 -0
  53. package/dist/internal/openapi/registry.d.ts +36 -0
  54. package/dist/internal/openapi/registry.d.ts.map +1 -0
  55. package/dist/internal/openapi/registry.js +79 -0
  56. package/dist/internal/openapi/registry.js.map +1 -0
  57. package/dist/internal/openapi/sample.d.ts +115 -0
  58. package/dist/internal/openapi/sample.d.ts.map +1 -0
  59. package/dist/internal/openapi/sample.js +434 -0
  60. package/dist/internal/openapi/sample.js.map +1 -0
  61. package/dist/internal/openapi/search.d.ts +19 -0
  62. package/dist/internal/openapi/search.d.ts.map +1 -0
  63. package/dist/internal/openapi/search.js +98 -0
  64. package/dist/internal/openapi/search.js.map +1 -0
  65. package/dist/internal/openapi/sidebar.d.ts +30 -0
  66. package/dist/internal/openapi/sidebar.d.ts.map +1 -0
  67. package/dist/internal/openapi/sidebar.js +67 -0
  68. package/dist/internal/openapi/sidebar.js.map +1 -0
  69. package/dist/internal/openapi/union.d.ts +36 -0
  70. package/dist/internal/openapi/union.d.ts.map +1 -0
  71. package/dist/internal/openapi/union.js +69 -0
  72. package/dist/internal/openapi/union.js.map +1 -0
  73. package/dist/internal/search.d.ts.map +1 -1
  74. package/dist/internal/search.js +20 -0
  75. package/dist/internal/search.js.map +1 -1
  76. package/dist/internal/vite-plugins.d.ts +12 -0
  77. package/dist/internal/vite-plugins.d.ts.map +1 -1
  78. package/dist/internal/vite-plugins.js +102 -11
  79. package/dist/internal/vite-plugins.js.map +1 -1
  80. package/dist/react/Badge.d.ts +2 -3
  81. package/dist/react/Badge.d.ts.map +1 -1
  82. package/dist/react/Layout.client.d.ts.map +1 -1
  83. package/dist/react/Layout.client.js +2 -2
  84. package/dist/react/Layout.client.js.map +1 -1
  85. package/dist/react/OpenApi.d.ts +6 -0
  86. package/dist/react/OpenApi.d.ts.map +1 -0
  87. package/dist/react/OpenApi.js +6 -0
  88. package/dist/react/OpenApi.js.map +1 -0
  89. package/dist/react/internal/CodeToHtml.client.d.ts +53 -2
  90. package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -1
  91. package/dist/react/internal/CodeToHtml.client.js +154 -21
  92. package/dist/react/internal/CodeToHtml.client.js.map +1 -1
  93. package/dist/react/internal/Sidebar.d.ts.map +1 -1
  94. package/dist/react/internal/Sidebar.js +99 -2
  95. package/dist/react/internal/Sidebar.js.map +1 -1
  96. package/dist/react/internal/openapi/CodeSample.client.d.ts +21 -0
  97. package/dist/react/internal/openapi/CodeSample.client.d.ts.map +1 -0
  98. package/dist/react/internal/openapi/CodeSample.client.js +134 -0
  99. package/dist/react/internal/openapi/CodeSample.client.js.map +1 -0
  100. package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts +17 -0
  101. package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts.map +1 -0
  102. package/dist/react/internal/openapi/CollapsibleChildren.client.js +18 -0
  103. package/dist/react/internal/openapi/CollapsibleChildren.client.js.map +1 -0
  104. package/dist/react/internal/openapi/Disclosure.client.d.ts +28 -0
  105. package/dist/react/internal/openapi/Disclosure.client.d.ts.map +1 -0
  106. package/dist/react/internal/openapi/Disclosure.client.js +38 -0
  107. package/dist/react/internal/openapi/Disclosure.client.js.map +1 -0
  108. package/dist/react/internal/openapi/Endpoints.d.ts +26 -0
  109. package/dist/react/internal/openapi/Endpoints.d.ts.map +1 -0
  110. package/dist/react/internal/openapi/Endpoints.js +33 -0
  111. package/dist/react/internal/openapi/Endpoints.js.map +1 -0
  112. package/dist/react/internal/openapi/EndpointsView.d.ts +24 -0
  113. package/dist/react/internal/openapi/EndpointsView.d.ts.map +1 -0
  114. package/dist/react/internal/openapi/EndpointsView.js +26 -0
  115. package/dist/react/internal/openapi/EndpointsView.js.map +1 -0
  116. package/dist/react/internal/openapi/EnumValues.client.d.ts +14 -0
  117. package/dist/react/internal/openapi/EnumValues.client.d.ts.map +1 -0
  118. package/dist/react/internal/openapi/EnumValues.client.js +20 -0
  119. package/dist/react/internal/openapi/EnumValues.client.js.map +1 -0
  120. package/dist/react/internal/openapi/HeadingAnchor.d.ts +15 -0
  121. package/dist/react/internal/openapi/HeadingAnchor.d.ts.map +1 -0
  122. package/dist/react/internal/openapi/HeadingAnchor.js +12 -0
  123. package/dist/react/internal/openapi/HeadingAnchor.js.map +1 -0
  124. package/dist/react/internal/openapi/OpenApiPage.d.ts +79 -0
  125. package/dist/react/internal/openapi/OpenApiPage.d.ts.map +1 -0
  126. package/dist/react/internal/openapi/OpenApiPage.js +72 -0
  127. package/dist/react/internal/openapi/OpenApiPage.js.map +1 -0
  128. package/dist/react/internal/openapi/Operation.d.ts +25 -0
  129. package/dist/react/internal/openapi/Operation.d.ts.map +1 -0
  130. package/dist/react/internal/openapi/Operation.js +101 -0
  131. package/dist/react/internal/openapi/Operation.js.map +1 -0
  132. package/dist/react/internal/openapi/Playground.client.d.ts +33 -0
  133. package/dist/react/internal/openapi/Playground.client.d.ts.map +1 -0
  134. package/dist/react/internal/openapi/Playground.client.js +170 -0
  135. package/dist/react/internal/openapi/Playground.client.js.map +1 -0
  136. package/dist/react/internal/openapi/PropertyExample.client.d.ts +17 -0
  137. package/dist/react/internal/openapi/PropertyExample.client.d.ts.map +1 -0
  138. package/dist/react/internal/openapi/PropertyExample.client.js +21 -0
  139. package/dist/react/internal/openapi/PropertyExample.client.js.map +1 -0
  140. package/dist/react/internal/openapi/Reference.d.ts +55 -0
  141. package/dist/react/internal/openapi/Reference.d.ts.map +1 -0
  142. package/dist/react/internal/openapi/Reference.js +42 -0
  143. package/dist/react/internal/openapi/Reference.js.map +1 -0
  144. package/dist/react/internal/openapi/Schema.d.ts +110 -0
  145. package/dist/react/internal/openapi/Schema.d.ts.map +1 -0
  146. package/dist/react/internal/openapi/Schema.js +239 -0
  147. package/dist/react/internal/openapi/Schema.js.map +1 -0
  148. package/dist/react/internal/openapi/SchemaUnion.client.d.ts +25 -0
  149. package/dist/react/internal/openapi/SchemaUnion.client.d.ts.map +1 -0
  150. package/dist/react/internal/openapi/SchemaUnion.client.js +48 -0
  151. package/dist/react/internal/openapi/SchemaUnion.client.js.map +1 -0
  152. package/dist/react/internal/openapi/anchor-navigation.client.d.ts +47 -0
  153. package/dist/react/internal/openapi/anchor-navigation.client.d.ts.map +1 -0
  154. package/dist/react/internal/openapi/anchor-navigation.client.js +120 -0
  155. package/dist/react/internal/openapi/anchor-navigation.client.js.map +1 -0
  156. package/dist/react/internal/openapi/auth.d.ts +28 -0
  157. package/dist/react/internal/openapi/auth.d.ts.map +1 -0
  158. package/dist/react/internal/openapi/auth.js +75 -0
  159. package/dist/react/internal/openapi/auth.js.map +1 -0
  160. package/dist/react/useLayout.d.ts +2 -0
  161. package/dist/react/useLayout.d.ts.map +1 -1
  162. package/dist/react/useLayout.js +2 -0
  163. package/dist/react/useLayout.js.map +1 -1
  164. package/dist/server/handlers.d.ts +1 -1
  165. package/dist/server/handlers.d.ts.map +1 -1
  166. package/dist/server/handlers.js +26 -5
  167. package/dist/server/handlers.js.map +1 -1
  168. package/dist/server/og-assets.d.ts +5 -0
  169. package/dist/server/og-assets.d.ts.map +1 -0
  170. package/dist/server/og-assets.js +11 -0
  171. package/dist/server/og-assets.js.map +1 -0
  172. package/dist/server/openapi/assets.d.ts +33 -0
  173. package/dist/server/openapi/assets.d.ts.map +1 -0
  174. package/dist/server/openapi/assets.generated.d.ts +9 -0
  175. package/dist/server/openapi/assets.generated.d.ts.map +1 -0
  176. package/dist/server/openapi/assets.generated.js +1091 -0
  177. package/dist/server/openapi/assets.generated.js.map +1 -0
  178. package/dist/server/openapi/assets.js +32 -0
  179. package/dist/server/openapi/assets.js.map +1 -0
  180. package/dist/server/openapi/handler.d.ts +103 -0
  181. package/dist/server/openapi/handler.d.ts.map +1 -0
  182. package/dist/server/openapi/handler.js +198 -0
  183. package/dist/server/openapi/handler.js.map +1 -0
  184. package/dist/server/openapi/handler.test.d.ts +2 -0
  185. package/dist/server/openapi/handler.test.d.ts.map +1 -0
  186. package/dist/server/openapi/handler.test.js +203 -0
  187. package/dist/server/openapi/handler.test.js.map +1 -0
  188. package/dist/server/openapi/html.d.ts +16 -0
  189. package/dist/server/openapi/html.d.ts.map +1 -0
  190. package/dist/server/openapi/html.js +75 -0
  191. package/dist/server/openapi/html.js.map +1 -0
  192. package/dist/server/openapi/pages.d.ts +33 -0
  193. package/dist/server/openapi/pages.d.ts.map +1 -0
  194. package/dist/server/openapi/pages.js +130 -0
  195. package/dist/server/openapi/pages.js.map +1 -0
  196. package/dist/server/openapi/pages.test.d.ts +2 -0
  197. package/dist/server/openapi/pages.test.d.ts.map +1 -0
  198. package/dist/server/openapi/pages.test.js +94 -0
  199. package/dist/server/openapi/pages.test.js.map +1 -0
  200. package/dist/server/openapi/state.d.ts +42 -0
  201. package/dist/server/openapi/state.d.ts.map +1 -0
  202. package/dist/server/openapi/state.js +101 -0
  203. package/dist/server/openapi/state.js.map +1 -0
  204. package/dist/styles/index.css +16 -0
  205. package/dist/styles/markdown.css +9 -7
  206. package/dist/styles/openapi-playground.css +80 -0
  207. package/dist/styles/openapi.css +660 -0
  208. package/dist/vite.d.ts.map +1 -1
  209. package/dist/vite.js +1 -0
  210. package/dist/vite.js.map +1 -1
  211. package/dist/waku/internal/middleware/md-router.d.ts +0 -4
  212. package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
  213. package/dist/waku/internal/middleware/md-router.js +3 -48
  214. package/dist/waku/internal/middleware/md-router.js.map +1 -1
  215. package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js +1 -1
  216. package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js.map +1 -1
  217. package/dist/waku/internal/patches/router.d.ts.map +1 -1
  218. package/dist/waku/internal/patches/router.js +114 -1
  219. package/dist/waku/internal/patches/router.js.map +1 -1
  220. package/package.json +5 -1
  221. package/src/config.ts +1 -0
  222. package/src/globals.d.ts +16 -0
  223. package/src/index.ts +1 -0
  224. package/src/internal/config.ts +40 -1
  225. package/src/internal/llms.ts +51 -1
  226. package/src/internal/markdown-negotiation.test.ts +42 -0
  227. package/src/internal/markdown-negotiation.ts +95 -0
  228. package/src/internal/markdown.ts +5 -0
  229. package/src/internal/openapi/anchors.ts +44 -0
  230. package/src/internal/openapi/app.ts +127 -0
  231. package/src/internal/openapi/index.ts +24 -0
  232. package/src/internal/openapi/markdown.test.ts +115 -0
  233. package/src/internal/openapi/markdown.ts +275 -0
  234. package/src/internal/openapi/openapi.ts +212 -0
  235. package/src/internal/openapi/openrpc.test.ts +239 -0
  236. package/src/internal/openapi/openrpc.ts +295 -0
  237. package/src/internal/openapi/parser.test.ts +203 -0
  238. package/src/internal/openapi/parser.ts +613 -0
  239. package/src/internal/openapi/registry.test.ts +89 -0
  240. package/src/internal/openapi/registry.ts +89 -0
  241. package/src/internal/openapi/sample.test.ts +283 -0
  242. package/src/internal/openapi/sample.ts +562 -0
  243. package/src/internal/openapi/search.test.ts +62 -0
  244. package/src/internal/openapi/search.ts +108 -0
  245. package/src/internal/openapi/sidebar.test.ts +131 -0
  246. package/src/internal/openapi/sidebar.ts +94 -0
  247. package/src/internal/openapi/union.test.ts +51 -0
  248. package/src/internal/openapi/union.ts +74 -0
  249. package/src/internal/search.ts +20 -0
  250. package/src/internal/test/virtual-config.stub.ts +14 -0
  251. package/src/internal/vite-plugins.ts +106 -11
  252. package/src/openapi-app/App.tsx +64 -0
  253. package/src/openapi-app/blocks.tsx +33 -0
  254. package/src/openapi-app/client.tsx +25 -0
  255. package/src/openapi-app/links.test.ts +84 -0
  256. package/src/openapi-app/links.ts +66 -0
  257. package/src/openapi-app/payload.ts +20 -0
  258. package/src/openapi-app/virtual/config.ts +7 -0
  259. package/src/openapi-app/virtual/group-icons.ts +2 -0
  260. package/src/openapi-app/virtual/langs.ts +6 -0
  261. package/src/openapi-app/virtual/openapi.ts +10 -0
  262. package/src/openapi-app/virtual/search-index.ts +21 -0
  263. package/src/openapi-app/virtual/slots.ts +4 -0
  264. package/src/openapi-app/virtual/user-styles.ts +2 -0
  265. package/src/openapi-app/waku.tsx +154 -0
  266. package/src/react/Badge.tsx +2 -3
  267. package/src/react/Layout.client.tsx +17 -4
  268. package/src/react/OpenApi.tsx +5 -0
  269. package/src/react/internal/CodeToHtml.client.tsx +283 -22
  270. package/src/react/internal/Sidebar.tsx +126 -22
  271. package/src/react/internal/openapi/CodeSample.client.tsx +294 -0
  272. package/src/react/internal/openapi/CollapsibleChildren.client.tsx +41 -0
  273. package/src/react/internal/openapi/Disclosure.client.tsx +67 -0
  274. package/src/react/internal/openapi/Endpoints.tsx +58 -0
  275. package/src/react/internal/openapi/EndpointsView.tsx +76 -0
  276. package/src/react/internal/openapi/EnumValues.client.tsx +49 -0
  277. package/src/react/internal/openapi/HeadingAnchor.tsx +28 -0
  278. package/src/react/internal/openapi/OpenApiPage.tsx +173 -0
  279. package/src/react/internal/openapi/Operation.test.tsx +101 -0
  280. package/src/react/internal/openapi/Operation.tsx +335 -0
  281. package/src/react/internal/openapi/Playground.client.tsx +234 -0
  282. package/src/react/internal/openapi/PropertyExample.client.tsx +55 -0
  283. package/src/react/internal/openapi/Reference.tsx +120 -0
  284. package/src/react/internal/openapi/Schema.tsx +467 -0
  285. package/src/react/internal/openapi/SchemaUnion.client.tsx +123 -0
  286. package/src/react/internal/openapi/anchor-navigation.client.ts +154 -0
  287. package/src/react/internal/openapi/auth.ts +69 -0
  288. package/src/react/useLayout.ts +4 -0
  289. package/src/server/handlers.ts +31 -6
  290. package/src/server/og-assets.ts +14 -0
  291. package/src/server/openapi/assets.generated.ts +1093 -0
  292. package/src/server/openapi/assets.ts +57 -0
  293. package/src/server/openapi/handler.test.ts +244 -0
  294. package/src/server/openapi/handler.ts +277 -0
  295. package/src/server/openapi/html.ts +84 -0
  296. package/src/server/openapi/pages.test.ts +111 -0
  297. package/src/server/openapi/pages.ts +153 -0
  298. package/src/server/openapi/state.ts +136 -0
  299. package/src/styles/index.css +16 -0
  300. package/src/styles/markdown.css +9 -7
  301. package/src/styles/openapi-playground.css +80 -0
  302. package/src/styles/openapi.css +660 -0
  303. package/src/vite.ts +1 -0
  304. package/src/waku/internal/middleware/md-router.ts +8 -52
  305. package/src/waku/internal/patches/adapters/vercel-build-enhancer.ts +1 -1
  306. 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
+ })