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,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
+ }
@@ -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 imports: string[] = []
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 langIds = t.langs
826
- .map((l: { name?: string }) => l.name)
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 (!imports.length) {
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
+ }