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,275 @@
1
+ import type { Config } from '../config.js'
2
+ import type { Ir, IrGroup, IrOperation, IrParameter, IrResponse } from './parser.js'
3
+ import * as Registry from './registry.js'
4
+ import { codeSamples } from './sample.js'
5
+ import { unwrapSingleVariant } from './union.js'
6
+
7
+ /**
8
+ * A generated Markdown page for an OpenAPI route, ready to be served verbatim by
9
+ * the `.md` router / written as an `/assets/md` asset.
10
+ */
11
+ export type Page = {
12
+ path: string
13
+ title: string
14
+ description?: string | undefined
15
+ /** Final Markdown, used verbatim (no MDX reprocessing). */
16
+ content: string
17
+ }
18
+
19
+ /**
20
+ * Builds Markdown pages for every configured OpenAPI section.
21
+ *
22
+ * Produces one page per mount (the section overview, listing every category and
23
+ * its endpoints) plus one page per category (`${mount}/${group.id}`), rendering
24
+ * each operation's parameters, request body, responses, and an example request.
25
+ *
26
+ * These mirror the routes created in the Waku router so `/<mount>.md` and
27
+ * `/<mount>/<group>.md` resolve to a readable, agent-friendly version of the
28
+ * same reference shown in the browser.
29
+ */
30
+ export async function toPages(config: Config): Promise<Page[]> {
31
+ if (!config.openapi?.length) return []
32
+
33
+ const specs = await Registry.build(config)
34
+ return Object.values(specs).flatMap(fromIr)
35
+ }
36
+
37
+ /**
38
+ * Builds the Markdown pages for a single parsed spec: the section overview plus
39
+ * one page per category. Pure (no registry/network), so it's the unit-testable
40
+ * seam under {@link toPages}.
41
+ */
42
+ export function fromIr(ir: Ir): Page[] {
43
+ return [overviewPage(ir), ...ir.groups.map((group) => groupPage(ir, group))]
44
+ }
45
+
46
+ /** Section overview page: API metadata + every category's endpoints. */
47
+ function overviewPage(ir: Ir): Page {
48
+ return {
49
+ path: ir.path,
50
+ title: ir.info.title,
51
+ description: ir.info.description ? oneLine(ir.info.description) : undefined,
52
+ content: renderOverview(ir, normalizeMount(ir.path)),
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Markdown for the section overview: API metadata + every category's endpoints.
58
+ *
59
+ * `linkBase` is prepended to operation anchors so the endpoint links resolve to
60
+ * wherever the section is mounted (the site uses the configured spec path; the
61
+ * standalone handler passes the live host mount).
62
+ */
63
+ export function renderOverview(ir: Ir, linkBase: string): string {
64
+ const base = normalizeMount(linkBase)
65
+ const lines: string[] = [`# ${ir.info.title}`, '']
66
+ if (ir.info.version) lines.push(`Version: \`${ir.info.version}\``, '')
67
+ if (ir.info.description) lines.push(ir.info.description.trim(), '')
68
+
69
+ if (ir.servers.length > 0) {
70
+ lines.push('## Servers', '')
71
+ for (const server of ir.servers)
72
+ lines.push(
73
+ `- \`${server.url}\`${server.description ? `: ${oneLine(server.description)}` : ''}`,
74
+ )
75
+ lines.push('')
76
+ }
77
+
78
+ lines.push('## Endpoints', '')
79
+ for (const group of ir.groups) {
80
+ lines.push(`### ${group.name}`, '')
81
+ if (group.description) lines.push(oneLine(group.description), '')
82
+ for (const operation of group.operations) {
83
+ const href = `${base}/${group.id}#${operation.id}`
84
+ const label = operation.summary ? `: ${oneLine(operation.summary)}` : ''
85
+ lines.push(`- [\`${operation.method} ${operation.path}\`](${href})${label}`)
86
+ }
87
+ lines.push('')
88
+ }
89
+
90
+ return `${lines.join('\n').trimEnd()}\n`
91
+ }
92
+
93
+ /** Category page: full detail for each operation in the group. */
94
+ function groupPage(ir: Ir, group: IrGroup): Page {
95
+ return {
96
+ path: `${normalizeMount(ir.path)}/${group.id}`,
97
+ title: group.name,
98
+ description: group.description ? oneLine(group.description) : undefined,
99
+ content: renderGroup(ir, group),
100
+ }
101
+ }
102
+
103
+ /** Markdown for a single category: full detail for each of its operations. */
104
+ export function renderGroup(ir: Ir, group: IrGroup): string {
105
+ const lines: string[] = [`# ${group.name}`, '']
106
+ if (group.description) lines.push(group.description.trim(), '')
107
+
108
+ const server = ir.servers[0]?.url
109
+ for (const operation of group.operations) lines.push(...operationLines(operation, server))
110
+
111
+ return `${lines.join('\n').trimEnd()}\n`
112
+ }
113
+
114
+ function operationLines(operation: IrOperation, server?: string): string[] {
115
+ const lines: string[] = []
116
+ const title = operation.summary ?? `${operation.method} ${operation.path}`
117
+ lines.push(`## ${title}`, '')
118
+ lines.push(`\`${operation.method} ${operation.path}\``, '')
119
+ if (operation.deprecated) lines.push('> **Deprecated**', '')
120
+ if (operation.description) lines.push(operation.description.trim(), '')
121
+
122
+ const byLocation = (location: IrParameter['in']) =>
123
+ operation.parameters.filter((parameter) => parameter.in === location && !parameter.deprecated)
124
+
125
+ for (const [location, heading] of [
126
+ ['path', 'Path parameters'],
127
+ ['query', 'Query parameters'],
128
+ ['header', 'Header parameters'],
129
+ ['rpc', 'Parameters'],
130
+ ] as const) {
131
+ const params = byLocation(location)
132
+ if (params.length === 0) continue
133
+ lines.push(`### ${heading}`, '')
134
+ for (const parameter of params) lines.push(...parameterLines(parameter))
135
+ lines.push('')
136
+ }
137
+
138
+ if (operation.requestBody && !operation.requestBody.hidden) {
139
+ const body = operation.requestBody
140
+ const media = body.content[0]
141
+ const mediaType = media ? ` (\`${media.mediaType}\`)` : ''
142
+ lines.push(`### Request body${body.required ? ' (required)' : ''}${mediaType}`, '')
143
+ if (body.description) lines.push(oneLine(body.description), '')
144
+ if (media?.schema) {
145
+ const props = schemaLines(media.schema, 0)
146
+ if (props.length > 0) lines.push(...props, '')
147
+ }
148
+ }
149
+
150
+ if (operation.responses.length > 0) {
151
+ lines.push('### Responses', '')
152
+ for (const response of operation.responses) lines.push(...responseLines(response))
153
+ }
154
+
155
+ const samples = codeSamples(operation, server)
156
+ if (samples.length > 0) {
157
+ lines.push('### Example request', '')
158
+ for (const sample of samples) {
159
+ lines.push(`\`\`\`${sample.lang}`, sample.code.trim(), '```', '')
160
+ }
161
+ }
162
+
163
+ return lines
164
+ }
165
+
166
+ function parameterLines(parameter: IrParameter): string[] {
167
+ // Unwrap a nullable single-variant union so an object param's member type and
168
+ // nested properties surface (e.g. an RPC `Transaction` object's fields).
169
+ const schema = parameter.schema
170
+ ? (unwrapSingleVariant(parameter.schema) ?? parameter.schema)
171
+ : parameter.schema
172
+ const required = parameter.required ? ' _(required)_' : ''
173
+ const type = `\`${typeLabel(schema)}\``
174
+ const description = parameter.description ? `: ${oneLine(parameter.description)}` : ''
175
+ return [`- \`${parameter.name}\` ${type}${required}${description}`, ...schemaLines(schema, 1)]
176
+ }
177
+
178
+ function responseLines(response: IrResponse): string[] {
179
+ const lines: string[] = []
180
+ const description = response.description ? `: ${oneLine(response.description)}` : ''
181
+ lines.push(`#### \`${response.status}\`${description}`, '')
182
+
183
+ if (response.headers.length > 0) {
184
+ lines.push('Headers:', '')
185
+ for (const header of response.headers)
186
+ lines.push(
187
+ `- \`${header.name}\` \`${typeLabel(header.schema)}\`${
188
+ header.description ? `: ${oneLine(header.description)}` : ''
189
+ }`,
190
+ )
191
+ lines.push('')
192
+ }
193
+
194
+ const media = response.content[0]
195
+ if (media?.schema) {
196
+ const props = schemaLines(media.schema, 0)
197
+ if (props.length > 0) {
198
+ if (media.mediaType) lines.push(`Body (\`${media.mediaType}\`):`, '')
199
+ lines.push(...props, '')
200
+ }
201
+ }
202
+
203
+ return lines
204
+ }
205
+
206
+ const maxDepth = 4
207
+
208
+ /**
209
+ * Renders an object schema's properties as an indented Markdown bullet list,
210
+ * recursing into nested objects (and array-of-object items) up to {@link
211
+ * maxDepth}. Returns an empty array for non-object schemas.
212
+ */
213
+ function schemaLines(rawSchema: SchemaObject | undefined, depth: number): string[] {
214
+ if (!rawSchema || depth > maxDepth) return []
215
+
216
+ // Unwrap a nullable single-variant union (e.g. `oneOf: [null, X]`) so the
217
+ // member's properties are listed instead of nothing.
218
+ const schema = unwrapSingleVariant(rawSchema) ?? rawSchema
219
+ const target = schema['type'] === 'array' ? (schema['items'] as SchemaObject) : schema
220
+ const properties = target?.['properties'] as Record<string, SchemaObject> | undefined
221
+ if (!properties) return []
222
+
223
+ const required = new Set((target?.['required'] as string[] | undefined) ?? [])
224
+ const indent = ' '.repeat(depth)
225
+ const lines: string[] = []
226
+
227
+ for (const [name, rawProperty] of Object.entries(properties)) {
228
+ const property = unwrapSingleVariant(rawProperty) ?? rawProperty
229
+ const requiredFlag = required.has(name) ? ' _(required)_' : ''
230
+ const description = property['description']
231
+ ? `: ${oneLine(String(property['description']))}`
232
+ : ''
233
+ lines.push(`${indent}- \`${name}\` \`${typeLabel(property)}\`${requiredFlag}${description}`)
234
+ lines.push(...schemaLines(property, depth + 1))
235
+ }
236
+
237
+ return lines
238
+ }
239
+
240
+ type SchemaObject = Record<string, unknown>
241
+
242
+ /** Compact, human-readable type label for a JSON Schema (post-dereference). */
243
+ function typeLabel(schema: SchemaObject | undefined): string {
244
+ if (!schema) return 'unknown'
245
+
246
+ if (Array.isArray(schema['type'])) return (schema['type'] as string[]).join(' | ')
247
+
248
+ const composite = (schema['oneOf'] ?? schema['anyOf']) as SchemaObject[] | undefined
249
+ if (composite) return [...new Set(composite.map(typeLabel))].join(' | ')
250
+
251
+ if (Array.isArray(schema['allOf']))
252
+ return (schema['allOf'] as SchemaObject[]).map(typeLabel).join(' & ')
253
+
254
+ if (schema['type'] === 'array') return `${typeLabel(schema['items'] as SchemaObject)}[]`
255
+
256
+ if (typeof schema['type'] === 'string') {
257
+ const format = schema['format'] ? ` <${schema['format']}>` : ''
258
+ return `${schema['type']}${format}`
259
+ }
260
+
261
+ if (Array.isArray(schema['enum'])) return 'enum'
262
+ if (schema['properties']) return 'object'
263
+
264
+ return 'unknown'
265
+ }
266
+
267
+ /** Collapse Markdown/whitespace to a single readable line (for summaries). */
268
+ function oneLine(value: string): string {
269
+ return value.replace(/\s+/g, ' ').trim()
270
+ }
271
+
272
+ /** Mount path without a trailing slash (e.g. `/` -> ``, `/api/` -> `/api`). */
273
+ function normalizeMount(path: string): string {
274
+ return path === '/' ? '' : path.replace(/\/$/, '')
275
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * OpenAPI integration config.
3
+ *
4
+ * Mounts an isolated, auto-generated API reference section in the docs from an
5
+ * OpenAPI spec. Use {@link from} to define an entry in `vocs.config.ts`.
6
+ */
7
+
8
+ import type * as VocsConfig from '../config.js'
9
+ import type { SidebarItem } from '../sidebar.js'
10
+
11
+ /**
12
+ * Spec source.
13
+ *
14
+ * - A file path relative to the project root (e.g. `./openapi.yaml`).
15
+ * - A URL (e.g. `https://example.com/openapi.json`).
16
+ * - Raw JSON/YAML content.
17
+ * - An inline OpenAPI document object.
18
+ */
19
+ export type Spec = string | Record<string, unknown>
20
+
21
+ /**
22
+ * A spec source, an already-started promise resolving to one, or a (lazy)
23
+ * provider that returns one. Promises/providers are awaited once when the
24
+ * reference is first rendered (and the result reused), so specs generated at
25
+ * runtime — e.g. from a server framework like `hono-openapi` — can be passed
26
+ * without resolving them up front. This lets `Handler.openApi` be constructed
27
+ * synchronously (and mounted via `app.route(...)`) even when the spec is
28
+ * produced asynchronously: build it eagerly and pass the promise directly.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const spec = generateSpecs(app) // Promise<Spec>, started eagerly
33
+ * app.route('/', Handler.openApi({ spec }, { fallback: 'next' }))
34
+ * ```
35
+ */
36
+ export type SpecInput = Spec | Promise<Spec> | (() => Spec | Promise<Spec>)
37
+
38
+ /**
39
+ * Extra sidebar items to add around the auto-generated section.
40
+ *
41
+ * These are typically links to consumer-authored override/guide pages mounted
42
+ * under the same `path` (e.g. `pages/api/auth.mdx`).
43
+ */
44
+ export type SidebarExtras = {
45
+ /** Items prepended above the generated `Introduction`/categories. */
46
+ top?: SidebarItem[] | undefined
47
+ /** Items appended below the generated categories. */
48
+ bottom?: SidebarItem[] | undefined
49
+ /**
50
+ * Items nested under the generated `Introduction` entry. When provided, the
51
+ * `Introduction` leaf becomes a collapsible group whose first child is an
52
+ * `Overview` link to the landing page, followed by these items (typically
53
+ * links to guide `pages` mounted under the section, e.g. authentication or
54
+ * versioning).
55
+ */
56
+ intro?: SidebarItem[] | undefined
57
+ /**
58
+ * Collapse the generated category groups by default. The group matching the
59
+ * active page still auto-expands, so deep links stay navigable. The generated
60
+ * `Introduction` entry is unaffected.
61
+ *
62
+ * @default false
63
+ */
64
+ collapsed?: boolean | undefined
65
+ }
66
+
67
+ /**
68
+ * A consumer-authored `.md`/`.mdx` page mounted into an OpenAPI section.
69
+ *
70
+ * In the Vite/site integration these are discovered automatically by the file
71
+ * router (e.g. `pages/api/auth.mdx`). In the standalone server handler
72
+ * ({@link file://./../../server/handlers.ts `Handler.openApi`}) — which has no
73
+ * filesystem router — they are supplied explicitly so override/guide content can
74
+ * still be rendered.
75
+ *
76
+ * - `path` is the route relative to the section mount (e.g. `/` overrides the
77
+ * landing intro, `/auth` is a guide page, `/<group>` overrides a category
78
+ * header).
79
+ * - `file` is the path to the `.md`/`.mdx` source, resolved against the handler
80
+ * `rootDir` (default `process.cwd()`). Reading a file requires `node:fs`, so
81
+ * on filesystem-less runtimes (e.g. Cloudflare Workers) supply `content`
82
+ * instead.
83
+ * - `content` is the `.md`/`.mdx` source inline, an alternative to `file` that
84
+ * needs no filesystem access. Exactly one of `file`/`content` is required.
85
+ * - `title` overrides the document title otherwise taken from frontmatter or the
86
+ * first `# heading`, and renders as the page's heading on standalone guide
87
+ * pages.
88
+ * - `description` renders as a subtitle below the title on standalone guide
89
+ * pages (Markdown). Also taken from frontmatter `description` when omitted.
90
+ */
91
+ export type Page = {
92
+ path: string
93
+ file?: string | undefined
94
+ content?: string | undefined
95
+ title?: string | undefined
96
+ description?: string | undefined
97
+ }
98
+
99
+ export type Config = {
100
+ /**
101
+ * OpenAPI spec source: file path, URL, raw content, inline object, or a (lazy)
102
+ * provider function returning any of those.
103
+ */
104
+ spec: SpecInput
105
+ /**
106
+ * Mount path for the generated API reference section.
107
+ *
108
+ * The section gets its own isolated sidebar scoped to this path.
109
+ *
110
+ * Required for the Vite/site integration. Optional for the standalone
111
+ * {@link file://./../../server/handlers.ts `Handler.openApi`} handler (the
112
+ * Hono mount provides the location; `path` only seeds generated link bases and
113
+ * defaults to `/`).
114
+ *
115
+ * @example "/api"
116
+ */
117
+ path?: string | undefined
118
+ /**
119
+ * Extra sidebar items to add around the auto-generated section (e.g. links to
120
+ * consumer-authored guide pages mounted under `path`).
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * sidebar: {
125
+ * top: [{ text: 'Authentication', link: '/api/auth' }],
126
+ * bottom: [{ text: 'Errors', link: '/api/errors' }],
127
+ * }
128
+ * ```
129
+ */
130
+ sidebar?: SidebarExtras | undefined
131
+ /**
132
+ * Consumer-authored `.md`/`.mdx` override/guide pages.
133
+ *
134
+ * Only used by the standalone {@link file://./../../server/handlers.ts
135
+ * `Handler.openApi`} handler — the Vite/site integration discovers these via
136
+ * the file router instead.
137
+ */
138
+ pages?: Page[] | undefined
139
+ /**
140
+ * Vocs config passthrough for the standalone {@link
141
+ * file://./../../server/handlers.ts `Handler.openApi`} handler.
142
+ *
143
+ * The handler renders the real Vocs layout, so these options customize the
144
+ * chrome (theme, top navigation, logo, socials, …) exactly like a
145
+ * `vocs.config.ts`.
146
+ *
147
+ * `title` and `description` default to the spec's `info.title`/`info.description`
148
+ * but can be overridden here. `sidebar` is derived from the generated section
149
+ * (plus the `sidebar` config above) and cannot be set here.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * vocs: {
154
+ * title: 'Acme Docs',
155
+ * logoUrl: '/logo.svg',
156
+ * theme: { accentColor: '#7c3aed' },
157
+ * topNav: [{ text: 'Home', link: 'https://acme.com' }],
158
+ * }
159
+ * ```
160
+ */
161
+ vocs?: Omit<VocsConfig.define.Options, 'sidebar' | 'openapi'> | undefined
162
+ }
163
+
164
+ /**
165
+ * Config for the Vite/site integration, where `path` is required (it scopes the
166
+ * generated sidebar and routes).
167
+ */
168
+ export type SiteConfig = Config & {
169
+ path: string
170
+ }
171
+
172
+ /**
173
+ * Defines an OpenAPI integration entry.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * import { OpenApi } from 'vocs/config'
178
+ *
179
+ * export default defineConfig({
180
+ * openapi: [
181
+ * OpenApi.from({ spec: './openapi.yaml', path: '/api' }),
182
+ * ],
183
+ * })
184
+ * ```
185
+ */
186
+ export function from(config: from.Options): SiteConfig {
187
+ const { spec, path, sidebar, pages } = config
188
+
189
+ if (!spec) throw new Error('[vocs] `openapi` entry is missing a `spec`.')
190
+ if (!path) throw new Error('[vocs] `openapi` entry is missing a `path`.')
191
+
192
+ return {
193
+ spec,
194
+ path: normalizePath(path),
195
+ ...(sidebar ? { sidebar } : {}),
196
+ ...(pages ? { pages } : {}),
197
+ }
198
+ }
199
+
200
+ export declare namespace from {
201
+ type Options = SiteConfig
202
+ }
203
+
204
+ /**
205
+ * Normalizes a mount path: ensures a leading slash and strips any trailing
206
+ * slash (except for the root `/`).
207
+ */
208
+ export function normalizePath(path: string): string {
209
+ let normalized = path.startsWith('/') ? path : `/${path}`
210
+ if (normalized.length > 1 && normalized.endsWith('/')) normalized = normalized.slice(0, -1)
211
+ return normalized
212
+ }