blume 2.1.3 → 2.2.1

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 (397) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +134 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-y4msrwm9.js → chunk-080mfch6.js} +15 -15
  5. package/dist/cli/{chunk-gfc5fqvf.js → chunk-1ch3ct3r.js} +18 -18
  6. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-1ch3ct3r.js.map} +1 -1
  7. package/dist/cli/{chunk-32khwcb6.js → chunk-2ssh60qh.js} +42 -69
  8. package/dist/cli/{chunk-32khwcb6.js.map → chunk-2ssh60qh.js.map} +9 -10
  9. package/dist/cli/{chunk-9qzgg6mj.js → chunk-34wap3ed.js} +3 -3
  10. package/dist/cli/{chunk-k3gr1nm9.js → chunk-40vk8kgf.js} +13 -16
  11. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-40vk8kgf.js.map} +3 -3
  12. package/dist/cli/chunk-487b95qy.js +329 -0
  13. package/dist/cli/chunk-487b95qy.js.map +13 -0
  14. package/dist/cli/chunk-4zxh25hc.js +157 -0
  15. package/dist/cli/chunk-4zxh25hc.js.map +10 -0
  16. package/dist/cli/chunk-69rjf786.js +146 -0
  17. package/dist/cli/chunk-69rjf786.js.map +11 -0
  18. package/dist/cli/{chunk-dqhg54mr.js → chunk-9h8mh2tk.js} +3 -3
  19. package/dist/cli/{chunk-dqhg54mr.js.map → chunk-9h8mh2tk.js.map} +2 -2
  20. package/dist/cli/{chunk-bqxaqn5p.js → chunk-9jqmq5hv.js} +8 -8
  21. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-9jqmq5hv.js.map} +1 -1
  22. package/dist/cli/{chunk-dv2z0rzf.js → chunk-aafhwx74.js} +38 -13
  23. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-aafhwx74.js.map} +5 -4
  24. package/dist/cli/{chunk-desvm72z.js → chunk-b680sv30.js} +22 -26
  25. package/dist/cli/chunk-b680sv30.js.map +13 -0
  26. package/dist/cli/{chunk-0w6hs479.js → chunk-beb0z983.js} +30 -18
  27. package/dist/cli/chunk-beb0z983.js.map +10 -0
  28. package/dist/cli/{chunk-1f882900.js → chunk-c5bjxq8v.js} +2 -2
  29. package/dist/cli/chunk-e1n41w9m.js +155 -0
  30. package/dist/cli/chunk-e1n41w9m.js.map +11 -0
  31. package/dist/cli/{chunk-sc93apfs.js → chunk-etckvbrh.js} +17 -17
  32. package/dist/cli/{chunk-sc93apfs.js.map → chunk-etckvbrh.js.map} +2 -2
  33. package/dist/cli/{chunk-bxwhtvw8.js → chunk-f260jdrn.js} +5 -5
  34. package/dist/cli/{chunk-74wfhv79.js → chunk-fnxgtjeq.js} +1 -1
  35. package/dist/cli/{chunk-ms9z66nz.js → chunk-gwy55cx3.js} +2 -2
  36. package/dist/cli/{chunk-ms9z66nz.js.map → chunk-gwy55cx3.js.map} +2 -2
  37. package/dist/cli/{chunk-dhwtqnnd.js → chunk-gxhcf2hs.js} +2 -2
  38. package/dist/cli/{chunk-tfycddbt.js → chunk-h0qwdbem.js} +2 -2
  39. package/dist/cli/{chunk-nbyvgnnm.js → chunk-hktq4j5r.js} +109 -49
  40. package/dist/cli/chunk-hktq4j5r.js.map +36 -0
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-hn3g2b89.js} +14 -10
  42. package/dist/cli/chunk-hn3g2b89.js.map +10 -0
  43. package/dist/cli/{chunk-j1x92mx9.js → chunk-j4nfgn2j.js} +1 -1
  44. package/dist/cli/{chunk-dpy8qdjf.js → chunk-k45sq16q.js} +74 -183
  45. package/dist/cli/chunk-k45sq16q.js.map +18 -0
  46. package/dist/cli/{chunk-5cmts6dv.js → chunk-kmswjxj9.js} +7 -7
  47. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-kmswjxj9.js.map} +1 -1
  48. package/dist/cli/{chunk-36bxs9c7.js → chunk-nqna8dzp.js} +27 -23
  49. package/dist/cli/chunk-nqna8dzp.js.map +10 -0
  50. package/dist/cli/{chunk-a57yh0h9.js → chunk-p834rkcn.js} +7 -7
  51. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-p834rkcn.js.map} +1 -1
  52. package/dist/cli/{chunk-60hz265r.js → chunk-ppa6mg4d.js} +24 -11
  53. package/dist/cli/chunk-ppa6mg4d.js.map +10 -0
  54. package/dist/cli/{chunk-s0bdnc7v.js → chunk-q1wwrxzw.js} +5 -4
  55. package/dist/cli/chunk-q1wwrxzw.js.map +10 -0
  56. package/dist/cli/{chunk-ave1wcbr.js → chunk-qcyxkj44.js} +1 -1
  57. package/dist/cli/{chunk-124fvmzq.js → chunk-r5nm9w0c.js} +11183 -8806
  58. package/dist/cli/{chunk-124fvmzq.js.map → chunk-r5nm9w0c.js.map} +82 -66
  59. package/dist/cli/{chunk-ckrcm08h.js → chunk-rtvgmg2j.js} +48 -62
  60. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-rtvgmg2j.js.map} +4 -5
  61. package/dist/cli/{chunk-297ajdcv.js → chunk-t2h8pwy1.js} +6 -6
  62. package/dist/cli/{chunk-297ajdcv.js.map → chunk-t2h8pwy1.js.map} +3 -3
  63. package/dist/cli/{chunk-gxtg6ed2.js → chunk-x60e15st.js} +21 -21
  64. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-x60e15st.js.map} +3 -3
  65. package/dist/cli/{chunk-58bt3hag.js → chunk-x7bh6d9j.js} +4 -4
  66. package/dist/cli/{chunk-hvcxng5f.js → chunk-ydqzkhjn.js} +6 -6
  67. package/dist/cli/{chunk-kdpzw38a.js → chunk-yg8ckc3g.js} +95 -7
  68. package/dist/cli/chunk-yg8ckc3g.js.map +11 -0
  69. package/dist/cli/{chunk-0pmb4tr4.js → chunk-zxd5jrgx.js} +1205 -222
  70. package/dist/cli/chunk-zxd5jrgx.js.map +44 -0
  71. package/dist/cli/index.js +78 -42
  72. package/dist/cli/index.js.map +7 -6
  73. package/dist/types/ai/changelog-components.d.ts +18 -0
  74. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  75. package/dist/types/ai/openapi-components.d.ts +7 -7
  76. package/dist/types/ai/operation-details.d.ts +39 -0
  77. package/dist/types/ai/relative-links.d.ts +3 -1
  78. package/dist/types/ai/serializers.d.ts +6 -4
  79. package/dist/types/analytics/head.d.ts +4 -2
  80. package/dist/types/analytics/script.d.ts +8 -2
  81. package/dist/types/components/content/youtube.d.ts +20 -0
  82. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  83. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  84. package/dist/types/components/openapi/helpers.d.ts +53 -11
  85. package/dist/types/components/openapi/snippets.d.ts +7 -1
  86. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  87. package/dist/types/core/builtin-tags.d.ts +16 -0
  88. package/dist/types/core/changelog-list.d.ts +48 -0
  89. package/dist/types/core/closest-match.d.ts +6 -0
  90. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  91. package/dist/types/core/code-fences.d.ts +8 -1
  92. package/dist/types/core/config-input.d.ts +38 -6
  93. package/dist/types/core/content-assets.d.ts +16 -18
  94. package/dist/types/core/data.d.ts +6 -2
  95. package/dist/types/core/diagnostics.d.ts +14 -0
  96. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  97. package/dist/types/core/former-routes.d.ts +14 -0
  98. package/dist/types/core/github.d.ts +5 -4
  99. package/dist/types/core/heading-markers.d.ts +65 -2
  100. package/dist/types/core/hidden-pages.d.ts +3 -0
  101. package/dist/types/core/includes.d.ts +23 -0
  102. package/dist/types/core/last-modified.d.ts +18 -6
  103. package/dist/types/core/links.d.ts +42 -2
  104. package/dist/types/core/load-module.d.ts +19 -4
  105. package/dist/types/core/manifest.d.ts +14 -1
  106. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  107. package/dist/types/core/mcp-clients.d.ts +47 -0
  108. package/dist/types/core/mdx-elements.d.ts +32 -0
  109. package/dist/types/core/mdx-syntax.d.ts +100 -0
  110. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  111. package/dist/types/core/meta.d.ts +32 -5
  112. package/dist/types/core/nav-diagnostics.d.ts +16 -2
  113. package/dist/types/core/navigation.d.ts +13 -0
  114. package/dist/types/core/ordering-prefix.d.ts +6 -0
  115. package/dist/types/core/pinned-ids.d.ts +23 -0
  116. package/dist/types/core/project-graph.d.ts +9 -1
  117. package/dist/types/core/redirect-patterns.d.ts +18 -0
  118. package/dist/types/core/relative-files.d.ts +31 -0
  119. package/dist/types/core/schema.d.ts +322 -11
  120. package/dist/types/core/sources/normalize.d.ts +101 -9
  121. package/dist/types/core/sources/types.d.ts +16 -0
  122. package/dist/types/core/static-files.d.ts +10 -0
  123. package/dist/types/core/static-host.d.ts +42 -0
  124. package/dist/types/core/svg-images.d.ts +26 -0
  125. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  126. package/dist/types/core/types.d.ts +42 -1
  127. package/dist/types/deploy/generated-files.d.ts +41 -0
  128. package/dist/types/deploy/redirects.d.ts +21 -7
  129. package/dist/types/markdown/code-title.d.ts +9 -5
  130. package/dist/types/markdown/directives.d.ts +8 -0
  131. package/dist/types/markdown/fence-language.d.ts +51 -0
  132. package/dist/types/markdown/fence-meta.d.ts +5 -0
  133. package/dist/types/markdown/github-alerts.d.ts +35 -0
  134. package/dist/types/openapi/asyncapi.d.ts +6 -5
  135. package/dist/types/openapi/checks.d.ts +10 -5
  136. package/dist/types/openapi/model.d.ts +55 -8
  137. package/dist/types/openapi/parse.d.ts +4 -1
  138. package/dist/types/ratelimit/index.d.ts +2 -0
  139. package/dist/types/ratelimit/memory.d.ts +1 -1
  140. package/dist/types/ratelimit/schema.d.ts +40 -0
  141. package/dist/types/ratelimit/unkey.d.ts +48 -0
  142. package/dist/types/sources/filesystem.d.ts +14 -4
  143. package/dist/types/sources/mdx-remote.d.ts +6 -3
  144. package/dist/types/sources/registry.d.ts +3 -3
  145. package/dist/types/theme/color.d.ts +33 -0
  146. package/dist/types/theme/palette.d.ts +122 -0
  147. package/docs/01-quickstart.mdx +1 -1
  148. package/docs/02-deployment.mdx +24 -8
  149. package/docs/04-migrating.mdx +16 -4
  150. package/docs/advanced/changelog.mdx +27 -1
  151. package/docs/advanced/skills.mdx +1 -1
  152. package/docs/cli/audit.mdx +1 -1
  153. package/docs/cli/doctor.mdx +2 -2
  154. package/docs/cli/evals.mdx +1 -1
  155. package/docs/cli/index.mdx +4 -2
  156. package/docs/cli/validate.mdx +10 -7
  157. package/docs/configuration/analytics.mdx +2 -0
  158. package/docs/configuration/customization.mdx +3 -1
  159. package/docs/configuration/index.mdx +7 -6
  160. package/docs/configuration/rate-limiting.mdx +20 -1
  161. package/docs/configuration/theming.mdx +5 -1
  162. package/docs/content/components.mdx +26 -6
  163. package/docs/content/frontmatter.mdx +1 -1
  164. package/docs/content/i18n.mdx +1 -1
  165. package/docs/content/includes.mdx +6 -2
  166. package/docs/content/index.mdx +11 -1
  167. package/docs/content/islands.mdx +2 -0
  168. package/docs/content/meta.mdx +9 -3
  169. package/docs/content/navigation.mdx +14 -6
  170. package/docs/content/sources/obsidian.mdx +1 -1
  171. package/docs/content/sources/remote-mdx.mdx +3 -1
  172. package/docs/content/syntax.mdx +76 -8
  173. package/docs/content/variables.mdx +3 -1
  174. package/docs/discoverability/agent-discovery.mdx +1 -1
  175. package/docs/discoverability/json-api.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/markdown.mdx +2 -2
  178. package/docs/discoverability/mcp.mdx +40 -8
  179. package/docs/discoverability/open-graph.mdx +1 -1
  180. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  181. package/docs/references/graphql.mdx +2 -0
  182. package/docs/references/openapi.mdx +45 -7
  183. package/package.json +4 -4
  184. package/skills/blume/SKILL.md +2 -2
  185. package/skills/blume-migrate/SKILL.md +51 -29
  186. package/skills/blume-migrate/references/docsify.md +262 -0
  187. package/skills/blume-migrate/references/docus.md +298 -0
  188. package/skills/blume-migrate/references/docusaurus.md +5 -5
  189. package/skills/blume-migrate/references/fern.md +327 -0
  190. package/skills/blume-migrate/references/fumadocs.md +1 -1
  191. package/skills/blume-migrate/references/gitbook.md +267 -0
  192. package/skills/blume-migrate/references/github-wiki.md +259 -0
  193. package/skills/blume-migrate/references/jekyll.md +253 -0
  194. package/skills/blume-migrate/references/mdbook.md +256 -0
  195. package/skills/blume-migrate/references/mintlify.md +5 -5
  196. package/skills/blume-migrate/references/mkdocs.md +236 -0
  197. package/skills/blume-migrate/references/monorepo.md +20 -8
  198. package/skills/blume-migrate/references/nextra.md +5 -5
  199. package/skills/blume-migrate/references/readme.md +257 -0
  200. package/skills/blume-migrate/references/redocly.md +198 -0
  201. package/skills/blume-migrate/references/starlight.md +3 -3
  202. package/skills/blume-migrate/references/vitepress.md +204 -0
  203. package/skills/blume-migrate/references/vuepress.md +271 -0
  204. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  205. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  206. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  207. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  208. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  209. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  210. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  211. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  212. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  213. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  214. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  215. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  216. package/src/ai/changelog-components.ts +24 -0
  217. package/src/ai/changelog-markdown.ts +45 -41
  218. package/src/ai/component-markdown.ts +22 -9
  219. package/src/ai/llms.ts +36 -5
  220. package/src/ai/markdown.ts +2 -2
  221. package/src/ai/openapi-components.ts +25 -9
  222. package/src/ai/operation-details.ts +208 -0
  223. package/src/ai/relative-links.ts +48 -8
  224. package/src/ai/serializers.ts +7 -6
  225. package/src/ai/site-skill.ts +6 -2
  226. package/src/analytics/head.ts +11 -5
  227. package/src/analytics/script.ts +13 -3
  228. package/src/astro/generate.ts +47 -11
  229. package/src/astro/index.ts +1 -0
  230. package/src/astro/integration.ts +27 -1
  231. package/src/astro/mdx-source-errors.ts +46 -0
  232. package/src/astro/templates.ts +93 -189
  233. package/src/audit/checks/redirects.ts +5 -1
  234. package/src/audit/redirects.ts +36 -19
  235. package/src/audit/report.ts +22 -9
  236. package/src/audit/run.ts +12 -28
  237. package/src/audit/terms.ts +23 -0
  238. package/src/audit/url.ts +2 -2
  239. package/src/cli/build-failure.ts +256 -7
  240. package/src/cli/command-meta.ts +1 -1
  241. package/src/cli/commands/audit.ts +1 -1
  242. package/src/cli/commands/build.ts +12 -3
  243. package/src/cli/commands/check.ts +13 -2
  244. package/src/cli/commands/dev.ts +1 -1
  245. package/src/cli/commands/doctor.ts +4 -2
  246. package/src/cli/commands/init.ts +3 -3
  247. package/src/cli/commands/preview.ts +17 -1
  248. package/src/cli/commands/validate.ts +22 -10
  249. package/src/cli/prepare.ts +1 -1
  250. package/src/cli/preview-pages.ts +161 -0
  251. package/src/cli/required-secrets.ts +0 -1
  252. package/src/cli/unknown-flags.ts +1 -42
  253. package/src/components/code-copy.ts +51 -0
  254. package/src/components/code-language.ts +75 -0
  255. package/src/components/content/AccordionItem.astro +16 -1
  256. package/src/components/content/Callout.astro +11 -1
  257. package/src/components/content/Changelog.astro +189 -0
  258. package/src/components/content/CodeBlock.astro +8 -11
  259. package/src/components/content/Tabs.astro +16 -2
  260. package/src/components/content/YouTube.astro +6 -4
  261. package/src/components/content/code-block.ts +33 -0
  262. package/src/components/content/content-strings.ts +5 -1
  263. package/src/components/content/inline-markdown.ts +35 -8
  264. package/src/components/content/youtube.ts +49 -7
  265. package/src/components/layout/Analytics.astro +5 -1
  266. package/src/components/layout/NavTree.astro +8 -3
  267. package/src/components/layout/PageActions.astro +81 -23
  268. package/src/components/layout/RootLayout.astro +35 -32
  269. package/src/components/layout/SiteFooter.astro +31 -8
  270. package/src/components/layout/nav-utils.ts +14 -0
  271. package/src/components/layout/search/pagefind.ts +2 -1
  272. package/src/components/openapi/Operation.astro +4 -0
  273. package/src/components/openapi/PanelTabs.astro +9 -3
  274. package/src/components/openapi/RequestBody.astro +42 -8
  275. package/src/components/openapi/RequestPanel.astro +34 -53
  276. package/src/components/openapi/example-panels.ts +110 -0
  277. package/src/components/openapi/helpers.ts +195 -20
  278. package/src/components/openapi/operation-model.ts +63 -26
  279. package/src/components/openapi/request.ts +13 -0
  280. package/src/components/openapi/snippets.ts +14 -2
  281. package/src/core/alert-diagnostics.ts +89 -0
  282. package/src/core/builtin-tags.ts +76 -0
  283. package/src/core/changelog-list.ts +87 -0
  284. package/src/core/closest-match.ts +41 -0
  285. package/src/core/code-fence-diagnostics.ts +267 -0
  286. package/src/core/code-fences.ts +43 -7
  287. package/src/core/component-diagnostics.ts +65 -12
  288. package/src/core/config-input.ts +39 -5
  289. package/src/core/config.ts +46 -1
  290. package/src/core/content-assets.ts +64 -144
  291. package/src/core/data.ts +10 -3
  292. package/src/core/diagnostics.ts +36 -3
  293. package/src/core/directive-diagnostics.ts +265 -47
  294. package/src/core/former-routes.ts +46 -0
  295. package/src/core/git-ignored.ts +53 -0
  296. package/src/core/github.ts +5 -4
  297. package/src/core/heading-markers.ts +156 -2
  298. package/src/core/hidden-pages.ts +64 -0
  299. package/src/core/includes.ts +181 -41
  300. package/src/core/last-modified.ts +47 -10
  301. package/src/core/links.ts +438 -83
  302. package/src/core/load-module.ts +98 -9
  303. package/src/core/manifest.ts +12 -9
  304. package/src/core/markdown-mirrors.ts +52 -0
  305. package/src/core/mcp-clients.ts +87 -0
  306. package/src/core/mdx-elements.ts +153 -0
  307. package/src/core/mdx-syntax.ts +377 -0
  308. package/src/core/meta-diagnostics.ts +134 -0
  309. package/src/core/meta.ts +154 -55
  310. package/src/core/nav-diagnostics.ts +41 -4
  311. package/src/core/navigation.ts +70 -10
  312. package/src/core/ordering-prefix.ts +22 -2
  313. package/src/core/pinned-ids.ts +141 -0
  314. package/src/core/project-graph.ts +221 -29
  315. package/src/core/redirect-patterns.ts +41 -0
  316. package/src/core/relative-files.ts +145 -0
  317. package/src/core/schema.ts +110 -6
  318. package/src/core/sources/mdx-remote.ts +75 -14
  319. package/src/core/sources/normalize.ts +885 -179
  320. package/src/core/sources/notion.ts +5 -4
  321. package/src/core/sources/obsidian.ts +2 -5
  322. package/src/core/sources/types.ts +17 -0
  323. package/src/core/static-files.ts +40 -0
  324. package/src/core/static-host.ts +125 -0
  325. package/src/core/svg-images.ts +98 -0
  326. package/src/core/syntax-diagnostics.ts +491 -0
  327. package/src/core/types.ts +43 -1
  328. package/src/deploy/artifacts.ts +2 -28
  329. package/src/deploy/generated-files.ts +128 -0
  330. package/src/deploy/platforms/vercel.ts +25 -1
  331. package/src/deploy/redirects.ts +43 -16
  332. package/src/deploy/sitemap.ts +2 -1
  333. package/src/markdown/base-links.ts +87 -6
  334. package/src/markdown/code-title.ts +29 -13
  335. package/src/markdown/content-assets.ts +210 -0
  336. package/src/markdown/directives.ts +69 -5
  337. package/src/markdown/fence-language.ts +106 -0
  338. package/src/markdown/fence-meta.ts +42 -0
  339. package/src/markdown/github-alerts.ts +172 -0
  340. package/src/markdown/heading-anchors.ts +265 -56
  341. package/src/markdown/include.ts +30 -3
  342. package/src/markdown/index.ts +145 -40
  343. package/src/markdown/prompt-text.ts +95 -0
  344. package/src/markdown/relative-links.ts +37 -2
  345. package/src/markdown/route-snapshot.ts +22 -12
  346. package/src/markdown/svg-fallback.ts +51 -0
  347. package/src/markdown/title-heading.ts +134 -0
  348. package/src/migrate/migrate.ts +127 -2
  349. package/src/og/card.ts +14 -4
  350. package/src/openapi/asyncapi.ts +14 -6
  351. package/src/openapi/checks.ts +160 -15
  352. package/src/openapi/model.ts +172 -33
  353. package/src/openapi/parse.ts +13 -4
  354. package/src/openapi/render-mdx.ts +55 -9
  355. package/src/openapi/source.ts +87 -6
  356. package/src/ratelimit/index.ts +2 -0
  357. package/src/ratelimit/memory.ts +1 -1
  358. package/src/ratelimit/runtime.ts +81 -0
  359. package/src/ratelimit/schema.ts +2 -0
  360. package/src/ratelimit/unkey.ts +61 -0
  361. package/src/registry/eject.ts +0 -1
  362. package/src/registry/registry.ts +1 -1
  363. package/src/search/documents.ts +10 -2
  364. package/src/sources/filesystem.ts +28 -3
  365. package/src/sources/mdx-remote.ts +7 -15
  366. package/src/theme/color.ts +14 -0
  367. package/src/theme/entry.ts +22 -1
  368. package/src/theme/palette.ts +74 -1
  369. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  370. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  371. package/dist/cli/chunk-150jk7km.js +0 -52
  372. package/dist/cli/chunk-150jk7km.js.map +0 -10
  373. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  374. package/dist/cli/chunk-53jqw8z0.js +0 -89
  375. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  376. package/dist/cli/chunk-60hz265r.js.map +0 -10
  377. package/dist/cli/chunk-desvm72z.js.map +0 -14
  378. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  379. package/dist/cli/chunk-es8h0v3j.js +0 -441
  380. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  381. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  382. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  383. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  384. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  385. package/dist/cli/chunk-ydp3065p.js +0 -64
  386. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  387. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-080mfch6.js.map} +0 -0
  388. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-34wap3ed.js.map} +0 -0
  389. /package/dist/cli/{chunk-1f882900.js.map → chunk-c5bjxq8v.js.map} +0 -0
  390. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-f260jdrn.js.map} +0 -0
  391. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-fnxgtjeq.js.map} +0 -0
  392. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-gxhcf2hs.js.map} +0 -0
  393. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-h0qwdbem.js.map} +0 -0
  394. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-j4nfgn2j.js.map} +0 -0
  395. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-qcyxkj44.js.map} +0 -0
  396. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-x7bh6d9j.js.map} +0 -0
  397. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-ydqzkhjn.js.map} +0 -0
@@ -6,8 +6,9 @@ import type {
6
6
  AsyncApiDocument,
7
7
  AsyncApiSpecValue,
8
8
  } from "./asyncapi.ts";
9
- // Type-only, so the import can't cycle at runtime (graphql.ts imports the
10
- // collector from here).
9
+ // Type-only, so the import can't cycle at runtime (checks.ts and graphql.ts
10
+ // import from here).
11
+ import type { SpecIssue } from "./checks.ts";
11
12
  import type { GraphqlDocument, GraphqlMember } from "./graphql.ts";
12
13
  import type { ReferenceKind } from "./references.ts";
13
14
  import { slugify } from "./references.ts";
@@ -294,6 +295,12 @@ const isText = <Value>(value: Value): value is Value & (string | number) =>
294
295
  const specText = <Value>(value: Value): string | undefined =>
295
296
  isText(value) ? String(value) : undefined;
296
297
 
298
+ /**
299
+ * A tag's slug before collisions are resolved, its word boundaries hyphenated
300
+ * the way an operation id's are (`InboxesThreads` -> `inboxes-threads`).
301
+ */
302
+ const wordSlug = (name: string): string => slugify(splitIdentifier(name));
303
+
297
304
  /**
298
305
  * Assign each distinct tag name a unique slug. `slugify` can collapse
299
306
  * different names onto one value — any two punctuation-only tags (`!!!`,
@@ -301,9 +308,13 @@ const specText = <Value>(value: Value): string | undefined =>
301
308
  * silently merges the tags' routes, sidebar groups, and overview sections.
302
309
  * Collisions gain `-2`, `-3`, … in first-seen order. Shared with the AsyncAPI
303
310
  * extractor (`asyncapi.ts`), whose untagged fallback groups are channel
304
- * addresses.
311
+ * addresses. `slug` is the rule before collisions: the collector also runs
312
+ * the one earlier releases used (`slugify` alone, so `InboxesThreads` ->
313
+ * `inboxesthreads`) to redirect the routes they gave.
305
314
  */
306
- export const tagSlugger = (): ((name: string) => string) => {
315
+ export const tagSlugger = (
316
+ slug: (name: string) => string = wordSlug
317
+ ): ((name: string) => string) => {
307
318
  const assigned = new Map<string, string>();
308
319
  const taken = new Set<string>();
309
320
  return (name) => {
@@ -311,17 +322,30 @@ export const tagSlugger = (): ((name: string) => string) => {
311
322
  if (existing) {
312
323
  return existing;
313
324
  }
314
- const base = slugify(name) || "operations";
315
- let slug = base;
316
- for (let suffix = 2; taken.has(slug); suffix += 1) {
317
- slug = `${base}-${suffix}`;
325
+ const base = slug(name) || "operations";
326
+ let unique = base;
327
+ for (let suffix = 2; taken.has(unique); suffix += 1) {
328
+ unique = `${base}-${suffix}`;
318
329
  }
319
- taken.add(slug);
320
- assigned.set(name, slug);
321
- return slug;
330
+ taken.add(unique);
331
+ assigned.set(name, unique);
332
+ return unique;
322
333
  };
323
334
  };
324
335
 
336
+ /**
337
+ * How a warning names an operation: `GET /pets`, or a webhook by its name
338
+ * (`Webhook "newPet" (POST)`), since a webhook has no path.
339
+ */
340
+ export const operationLabel = (
341
+ name: string,
342
+ method: string,
343
+ webhook: boolean
344
+ ): string =>
345
+ webhook
346
+ ? `Webhook "${name}" (${method.toUpperCase()})`
347
+ : `${method.toUpperCase()} ${name}`;
348
+
325
349
  /** One entry of a document's `paths` or `webhooks` map. */
326
350
  type PathItemEntry =
327
351
  | NonNullable<ApiDocument["paths"]>[string]
@@ -330,15 +354,39 @@ type PathItemEntry =
330
354
  /** An operation before the collector assigns its unique key and route. */
331
355
  type CollectedOperation = Omit<ApiOperationRef, "route" | "tagSlug">;
332
356
 
357
+ /** What a spec's `tags` list says about one tag. */
358
+ export interface ApiTagMeta {
359
+ description: string;
360
+ /**
361
+ * The tag's label, in place of its name, from Redocly's `x-displayName`
362
+ * extension. Only the label changes: the tag's slug, and so its routes,
363
+ * still come from its name.
364
+ */
365
+ displayName?: string;
366
+ }
367
+
368
+ /**
369
+ * A route an earlier release gave an operation's page, and the route the
370
+ * page has now: the source turns each into a redirect, so links to the old
371
+ * URL keep working.
372
+ */
373
+ export interface MovedRoute {
374
+ from: string;
375
+ to: string;
376
+ }
377
+
333
378
  /** The flattened output both extractors produce. */
334
379
  export interface CollectedOperations {
335
380
  operations: ApiOperationRef[];
336
381
  tags: ApiTagRef[];
382
+ /** Operation routes that moved from what earlier slug rules gave them. */
383
+ moved: MovedRoute[];
337
384
  }
338
385
 
339
386
  /** The collector handle: feed operations in, read the flattened output out. */
340
387
  export interface OperationCollector {
341
- add: (entry: CollectedOperation) => void;
388
+ /** Add an operation, returning it with its key and route assigned. */
389
+ add: (entry: CollectedOperation) => ApiOperationRef;
342
390
  finish: () => CollectedOperations;
343
391
  }
344
392
 
@@ -353,18 +401,34 @@ export interface ExtractedOperations extends CollectedOperations {
353
401
  * (a repeated key gains its method/action as a suffix), and the shared route
354
402
  * template — so URL shape and slug rules can never drift between the two spec
355
403
  * kinds.
404
+ *
405
+ * A key is unique across the spec, since it names the operation in
406
+ * `<Operation id>`. Its page's URL only has to be unique within its tag's
407
+ * folder, so the last route segment de-duplicates per tag: `list` in two
408
+ * tags is `/pets/list` and `/stores/list`, not `/stores/list-get`. Earlier
409
+ * releases de-duplicated the route like the key, and slugged tags without
410
+ * splitting their words; each route those rules gave differently is
411
+ * reported in `moved`.
356
412
  */
357
413
  export const operationCollector = (
358
414
  baseRoute: string,
359
- tagMeta: ReadonlyMap<string, string>
415
+ tagMeta: ReadonlyMap<string, ApiTagMeta>
360
416
  ): OperationCollector => {
361
417
  const operations: ApiOperationRef[] = [];
418
+ const moved: MovedRoute[] = [];
362
419
  const tagOrder: string[] = [];
363
420
  const tagsSeen = new Set<string>();
364
421
  const seen = new Set<string>();
422
+ // Tag slug -> the route segments its operations took.
423
+ const segments = new Map<string, Set<string>>();
365
424
  const slugForTag = tagSlugger();
425
+ const legacySlugForTag = tagSlugger(slugify);
426
+ // A root-mounted reference (`route: "/"`) must not emit `//tag/key`.
427
+ const base = baseRoute === "/" ? "" : baseRoute;
428
+ const label = (name: string): string =>
429
+ tagMeta.get(name)?.displayName ?? name;
366
430
 
367
- const add = (entry: CollectedOperation): void => {
431
+ const add = (entry: CollectedOperation): ApiOperationRef => {
368
432
  const tagSlug = slugForTag(entry.tag);
369
433
  if (!tagsSeen.has(entry.tag)) {
370
434
  tagsSeen.add(entry.tag);
@@ -375,13 +439,27 @@ export const operationCollector = (
375
439
  key = `${key}-${entry.method}`;
376
440
  }
377
441
  seen.add(key);
378
- operations.push({
442
+ const taken = segments.get(tagSlug) ?? new Set<string>();
443
+ segments.set(tagSlug, taken);
444
+ let segment = entry.key;
445
+ while (taken.has(segment)) {
446
+ segment = `${segment}-${entry.method}`;
447
+ }
448
+ taken.add(segment);
449
+ const route = `${base}/${tagSlug}/${segment}`;
450
+ const legacy = `${base}/${legacySlugForTag(entry.tag)}/${key}`;
451
+ if (legacy !== route) {
452
+ moved.push({ from: legacy, to: route });
453
+ }
454
+ const operation: ApiOperationRef = {
379
455
  ...entry,
380
456
  key,
381
- // A root-mounted reference (`route: "/"`) must not emit `//tag/key`.
382
- route: `${baseRoute === "/" ? "" : baseRoute}/${tagSlug}/${key}`,
457
+ route,
458
+ tag: label(entry.tag),
383
459
  tagSlug,
384
- });
460
+ };
461
+ operations.push(operation);
462
+ return operation;
385
463
  };
386
464
 
387
465
  // Declared tags in the order the spec's `tags` list gives them, then any
@@ -395,12 +473,13 @@ export const operationCollector = (
395
473
  };
396
474
 
397
475
  const finish = (): CollectedOperations => ({
476
+ moved,
398
477
  operations,
399
478
  tags: tagOrder
400
479
  .toSorted((a, b) => rank(a) - rank(b))
401
480
  .map((name) => ({
402
- description: tagMeta.get(name) ?? "",
403
- name,
481
+ description: tagMeta.get(name)?.description ?? "",
482
+ name: label(name),
404
483
  // The same slugger instance, so every tag resolves to the slug its
405
484
  // operations were routed under.
406
485
  slug: slugForTag(name),
@@ -410,25 +489,52 @@ export const operationCollector = (
410
489
  return { add, finish };
411
490
  };
412
491
 
492
+ /** {@link ExtractedOperations} plus the spec mistakes extraction noticed. */
493
+ export interface ExtractedApiOperations extends ExtractedOperations {
494
+ issues: SpecIssue[];
495
+ }
496
+
497
+ /** A spec's `tags` as the collector reads them: name -> description and label. */
498
+ const declaredTags = (document: ApiDocument): Map<string, ApiTagMeta> =>
499
+ new Map(
500
+ (document.tags ?? []).flatMap((tag): [string, ApiTagMeta][] => {
501
+ const name = specText(tag.name);
502
+ if (name === undefined) {
503
+ return [];
504
+ }
505
+ const meta: ApiTagMeta = { description: tag.description ?? "" };
506
+ const displayName = specText(tag["x-displayName"]);
507
+ if (displayName) {
508
+ meta.displayName = displayName;
509
+ }
510
+ return [[name, meta]];
511
+ })
512
+ );
513
+
413
514
  /**
414
515
  * Flatten a 3.1 document into a route-mapped operation list and its ordered
415
516
  * tags, webhooks included. Operations inherit the first tag they declare; keys
416
- * are de-duplicated so a repeated `operationId` still yields distinct routes.
417
- * `warnings` reports anything skipped (a `$ref` path item), so missing
418
- * operations aren't silent.
517
+ * are de-duplicated so a repeated `operationId` still yields distinct routes,
518
+ * with an issue naming both operations. `warnings` reports anything skipped (a
519
+ * `$ref` path item), so missing operations aren't silent.
520
+ *
521
+ * Operations come out in the spec's order: paths as listed, and each path's
522
+ * methods as written. They're keyed in the method order earlier releases
523
+ * walked (`HTTP_METHODS`), so a repeated id resolves to the same key, and
524
+ * the same route, it always did.
419
525
  */
420
526
  export const extractOperations = (
421
527
  document: ApiDocument,
422
528
  baseRoute: string
423
- ): ExtractedOperations => {
529
+ ): ExtractedApiOperations => {
424
530
  const warnings: string[] = [];
425
- const tagMeta = new Map(
426
- (document.tags ?? []).flatMap((tag): [string, string][] => {
427
- const name = specText(tag.name);
428
- return name === undefined ? [] : [[name, tag.description ?? ""]];
429
- })
430
- );
431
- const collector = operationCollector(baseRoute, tagMeta);
531
+ const issues: SpecIssue[] = [];
532
+ const collector = operationCollector(baseRoute, declaredTags(document));
533
+ // Each operation's place in the spec, and the first operation to declare
534
+ // each operationId.
535
+ const position = new Map<ApiOperationRef, number>();
536
+ const firstWithId = new Map<string, string>();
537
+ let cursor = 0;
432
538
 
433
539
  // A path item's operations, filed under `name`: a path for `paths`, the
434
540
  // webhook's key for `webhooks`.
@@ -449,6 +555,9 @@ export const extractOperations = (
449
555
  );
450
556
  return;
451
557
  }
558
+ const written = Object.keys(item);
559
+ const offset = cursor;
560
+ cursor += written.length;
452
561
  for (const method of HTTP_METHODS) {
453
562
  const operation = item[method];
454
563
  if (!isOperation(operation)) {
@@ -473,7 +582,28 @@ export const extractOperations = (
473
582
  specText(operation.tags?.[0]) ??
474
583
  (webhook ? UNTAGGED_WEBHOOKS : UNTAGGED),
475
584
  };
476
- collector.add(webhook ? { ...entry, webhook: true } : entry);
585
+ const added = collector.add(
586
+ webhook ? { ...entry, webhook: true } : entry
587
+ );
588
+ position.set(added, offset + written.indexOf(method));
589
+ if (operationId === undefined) {
590
+ continue;
591
+ }
592
+ const label = operationLabel(name, method, webhook);
593
+ const first = firstWithId.get(operationId);
594
+ if (first === undefined) {
595
+ firstWithId.set(operationId, label);
596
+ continue;
597
+ }
598
+ // The key always gains a suffix; the URL only when the two share a tag.
599
+ const url = added.route.endsWith(`/${entry.key}`)
600
+ ? ""
601
+ : ` and serves its page at ${added.route}`;
602
+ issues.push({
603
+ code: "BLUME_OPENAPI_DUPLICATE_OPERATION_ID",
604
+ message: `${first} and ${label} share the operationId "${operationId}", which must be unique in a spec, so Blume names the second one "${added.key}" in \`<Operation id>\`${url}.`,
605
+ suggestion: `Give each operation its own operationId.`,
606
+ });
477
607
  }
478
608
  };
479
609
 
@@ -486,7 +616,16 @@ export const extractOperations = (
486
616
  addPathItem(name, item, true);
487
617
  }
488
618
 
489
- return { ...collector.finish(), warnings };
619
+ const collected = collector.finish();
620
+ return {
621
+ ...collected,
622
+ issues,
623
+ // `position` holds every operation the collector added.
624
+ operations: collected.operations.toSorted(
625
+ (a, b) => (position.get(a) ?? 0) - (position.get(b) ?? 0)
626
+ ),
627
+ warnings,
628
+ };
490
629
  };
491
630
 
492
631
  /** Resolve the operation object for a ref out of its (OpenAPI) document. */
@@ -15,11 +15,11 @@ import { nodeRequire } from "../core/node-require.ts";
15
15
  import { hashText } from "../core/sources/cache.ts";
16
16
  import type { AsyncApiDocument } from "./asyncapi.ts";
17
17
  import { normalizeAsyncApiDocument } from "./asyncapi.ts";
18
- import { operationLabel } from "./checks.ts";
18
+ import { nullableIssues } from "./checks.ts";
19
19
  import type { SpecIssue } from "./checks.ts";
20
20
  import { buildGraphqlDocument } from "./graphql-build.ts";
21
21
  import type { GraphqlDocument } from "./graphql.ts";
22
- import { HTTP_METHODS } from "./model.ts";
22
+ import { HTTP_METHODS, operationLabel } from "./model.ts";
23
23
  import type { ApiDocument } from "./model.ts";
24
24
  import { applyOverlay, isJsonObject, overlayDocument } from "./overlay.ts";
25
25
  import { SpecDependencyError } from "./spec-dependency-error.ts";
@@ -63,7 +63,10 @@ const PROXY_ENV_VARS = [
63
63
 
64
64
  export interface ParsedSpec {
65
65
  document: ApiDocument;
66
- /** `x-codeSamples` entries whose `$ref` couldn't be inlined. */
66
+ /**
67
+ * Issues only the spec as written shows: `nullable` in a 3.1 spec, and
68
+ * `x-codeSamples` entries whose `$ref` couldn't be inlined.
69
+ */
67
70
  issues: SpecIssue[];
68
71
  warnings: string[];
69
72
  }
@@ -494,6 +497,8 @@ export const parseSpec = async (
494
497
  root,
495
498
  options
496
499
  );
500
+ // Checked as written: upgrading a 3.0 spec rewrites its `nullable`s.
501
+ const nullable = isJsonObject(normalized) ? nullableIssues(normalized) : [];
497
502
  const { specification } = upgrade(normalized);
498
503
  // Reject a non-mapping here so the renderer never sees a non-document.
499
504
  if (!isApiDocument(specification)) {
@@ -502,7 +507,11 @@ export const parseSpec = async (
502
507
  );
503
508
  }
504
509
  const issues = await inlineSampleRefs(specification, spec, root, options);
505
- return { document: specification, issues, warnings };
510
+ return {
511
+ document: specification,
512
+ issues: [...nullable, ...issues],
513
+ warnings,
514
+ };
506
515
  };
507
516
 
508
517
  export interface ParsedAsyncApiSpec {
@@ -1,4 +1,4 @@
1
- import type { List, Nodes, Paragraph, Root } from "mdast";
1
+ import type { Heading, List, Nodes, Paragraph, Root } from "mdast";
2
2
  import { fromMarkdown } from "mdast-util-from-markdown";
3
3
  import { toString as mdastToString } from "mdast-util-to-string";
4
4
  import stringWidth from "string-width";
@@ -161,9 +161,55 @@ const closeOpenFence = (text: string): string => {
161
161
  : `${text}\n${fence}`;
162
162
  };
163
163
 
164
- /** Spec prose as MDX that is safe to follow with a component. */
165
- const descriptionMdx = (text: string): string =>
166
- mdxSafe(closeOpenFence(text.trim()));
164
+ /** The deepest heading Markdown can write. */
165
+ const MAX_HEADING_DEPTH = 6;
166
+
167
+ /** A line break inside a heading's text, with any quote or list indentation. */
168
+ const HEADING_BREAK = /[ \t]*\r?\n[\s>]*/gu;
169
+
170
+ /** Every heading in a Markdown tree, in document order. */
171
+ const headingsOf = (node: Nodes): Heading[] => {
172
+ if (node.type === "heading") {
173
+ return [node];
174
+ }
175
+ return "children" in node ? node.children.flatMap(headingsOf) : [];
176
+ };
177
+
178
+ /**
179
+ * Spec prose with every heading `by` levels deeper, written as an ATX
180
+ * heading (`#` × depth, capped at 6). A spec's `info.description` is often a
181
+ * whole document under `# Introduction` and `# Authentication` headings, so
182
+ * on a page whose title is already its `<h1>` it would add more: the
183
+ * overview's description and an operation's sit under the title (`#` becomes
184
+ * `##`), and a tag's under its `##` section (`#` becomes `###`). Headings are
185
+ * found in the same `<`-masked parse as `mdxSafe`'s, since the emitted MDX
186
+ * has no HTML blocks to hide one in; a setext heading's lines join into one.
187
+ */
188
+ const demoteHeadings = (text: string, by: number): string => {
189
+ let out = "";
190
+ let cursor = 0;
191
+ for (const heading of headingsOf(
192
+ fromMarkdown(text.replaceAll("<", HTML_MASK))
193
+ )) {
194
+ // fromMarkdown always stamps positions; 0 is an unreachable guard.
195
+ const start = heading.position?.start.offset ?? 0;
196
+ const from = heading.children[0]?.position?.start.offset ?? start;
197
+ const to = heading.children.at(-1)?.position?.end.offset ?? from;
198
+ const depth = Math.min(heading.depth + by, MAX_HEADING_DEPTH);
199
+ out += `${text.slice(cursor, start)}${"#".repeat(depth)} ${text
200
+ .slice(from, to)
201
+ .replace(HEADING_BREAK, " ")}`.trimEnd();
202
+ cursor = heading.position?.end.offset ?? start;
203
+ }
204
+ return out + text.slice(cursor);
205
+ };
206
+
207
+ /**
208
+ * Spec prose as MDX that is safe to follow with a component, its headings
209
+ * demoted `by` levels under the page's own (see {@link demoteHeadings}).
210
+ */
211
+ const descriptionMdx = (text: string, by: number): string =>
212
+ mdxSafe(closeOpenFence(demoteHeadings(text.trim(), by)));
167
213
 
168
214
  /**
169
215
  * Frontmatter emitted for one operation or overview page. Boolean flags are
@@ -349,7 +395,7 @@ const operationDescription = (
349
395
  /** Prepend a markdown description (if any) above a component invocation. */
350
396
  const withDescription = (description: string, component: string): string =>
351
397
  description.trim()
352
- ? `${descriptionMdx(description)}\n\n${component}`
398
+ ? `${descriptionMdx(description, 1)}\n\n${component}`
353
399
  : component;
354
400
 
355
401
  export const operationMdx = (
@@ -397,9 +443,9 @@ export const operationMdx = (
397
443
  if (reference?.noindex) {
398
444
  seo.noindex = true;
399
445
  }
400
- const sidebar: RenderedPageData["sidebar"] = {
401
- label: operation.summary || operation.path,
402
- };
446
+ // An operation without a summary is labeled as its page is titled, by its
447
+ // method and path: the path alone repeats for `GET /pets` and `POST /pets`.
448
+ const sidebar: RenderedPageData["sidebar"] = { label: title };
403
449
  // GraphQL operation kinds badge like HTTP methods, but a type page's kind
404
450
  // already heads its sidebar group ("Objects", "Enums", …) — an `OBJECT`
405
451
  // badge on every row would only repeat it, so type pages get none. The
@@ -468,7 +514,7 @@ export const overviewMdx = (
468
514
  continue;
469
515
  }
470
516
  const description = tag.description.trim()
471
- ? [descriptionMdx(tag.description)]
517
+ ? [descriptionMdx(tag.description, 2)]
472
518
  : [];
473
519
  tagSections.push(
474
520
  [
@@ -1,5 +1,10 @@
1
+ import {
2
+ SAMPLE_LANGUAGE_IDS,
3
+ unknownSampleLanguages,
4
+ } from "../components/openapi/snippets.ts";
1
5
  import { withBasePath } from "../core/base-path.ts";
2
6
  import matter from "../core/frontmatter.ts";
7
+ import { stripOrderingPrefix } from "../core/ordering-prefix.ts";
3
8
  import type { FolderMeta } from "../core/schema.ts";
4
9
  import { hashText } from "../core/sources/cache.ts";
5
10
  import type {
@@ -7,6 +12,7 @@ import type {
7
12
  SourceContext,
8
13
  SourceEntry,
9
14
  SourceLoadResult,
15
+ SourceRedirect,
10
16
  } from "../core/sources/types.ts";
11
17
  import type { Diagnostic } from "../core/types.ts";
12
18
  import { extractAsyncApiOperations } from "./asyncapi.ts";
@@ -21,6 +27,7 @@ import type {
21
27
  ApiOperationRef,
22
28
  ApiSpecData,
23
29
  ApiTagRef,
30
+ MovedRoute,
24
31
  OpenApiData,
25
32
  } from "./model.ts";
26
33
  import {
@@ -136,6 +143,41 @@ const failureSuggestion = (
136
143
  return "Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY.";
137
144
  };
138
145
 
146
+ /** The adapter that configures each kind, as a warning names it. */
147
+ const ADAPTER_NAMES = {
148
+ asyncapi: "asyncapi()",
149
+ graphql: "graphql()",
150
+ openapi: "openapi()",
151
+ } satisfies Record<BlumeReferenceSource["kind"], string>;
152
+
153
+ /**
154
+ * A warning for the `codeSamples` ids Blume generates no sample for, which
155
+ * the operation pages would otherwise leave out without a sign. AsyncAPI's
156
+ * ids name tools, matched against each operation's protocol binding, so only
157
+ * the HTTP kinds are checked here.
158
+ */
159
+ const unknownLanguageDiagnostics = (
160
+ reference: BlumeReferenceSource
161
+ ): Diagnostic[] => {
162
+ if (reference.kind === "asyncapi") {
163
+ return [];
164
+ }
165
+ const unknown = unknownSampleLanguages(reference.display.codeSamples);
166
+ if (unknown.length === 0) {
167
+ return [];
168
+ }
169
+ const names = unknown.map((id) => `"${id}"`).join(", ");
170
+ const one = unknown.length === 1;
171
+ return [
172
+ {
173
+ code: `${CODE_PREFIXES[reference.kind]}_UNKNOWN_CODE_SAMPLE`,
174
+ message: `\`codeSamples\` in ${ADAPTER_NAMES[reference.kind]} lists ${names}, which ${one ? "isn't a language" : "aren't languages"} Blume generates samples in, so ${one ? "it's" : "they're"} left out of every operation page.`,
175
+ severity: "warning",
176
+ suggestion: `Use the ids Blume generates (${SAMPLE_LANGUAGE_IDS.join(", ")}) or an alias the reference docs list, or show your own samples with \`x-codeSamples\` in the spec.`,
177
+ },
178
+ ];
179
+ };
180
+
139
181
  const toEntry = (rendered: RenderedPage, ref: string): SourceEntry => {
140
182
  const raw = matter.stringify(`${rendered.body}\n`, rendered.data);
141
183
  return {
@@ -185,17 +227,31 @@ const specEntries = (
185
227
  * not "Github V2"). Keys are the directories under the reference route, the
186
228
  * same group paths `meta.ts` files use, so user-authored meta still
187
229
  * overrides these.
230
+ *
231
+ * Each group lists its operation pages in `operations` order, the spec's own
232
+ * (see `extractOperations`), as the overview does, rather than letting the
233
+ * sidebar sort them by label. A page's nav key is its file name without an
234
+ * ordering prefix, as `meta.ts` `pages` entries are.
188
235
  */
189
236
  const tagFolderMeta = (
190
237
  spec: ApiSpecData,
191
238
  tags: { slug: string; name: string }[],
239
+ operations: ApiOperationRef[],
192
240
  groupLabel: string | undefined
193
241
  ): Record<string, FolderMeta> => {
194
242
  const base = routeToRef(spec.route);
195
243
  const meta: Record<string, FolderMeta> = Object.fromEntries(
196
244
  tags.map((tag, order) => [
197
245
  base ? `${base}/${tag.slug}` : tag.slug,
198
- { order, title: tag.name },
246
+ {
247
+ order,
248
+ pages: operations
249
+ .filter((operation) => operation.tagSlug === tag.slug)
250
+ .map((operation) =>
251
+ stripOrderingPrefix(operation.route.split("/").at(-1) ?? "")
252
+ ),
253
+ title: tag.name,
254
+ },
199
255
  ])
200
256
  );
201
257
  // A root-mounted reference has no group of its own to name.
@@ -211,6 +267,8 @@ interface LoadedSpec {
211
267
  entries: SourceEntry[];
212
268
  /** Sidebar-group labels for the spec's tag directories. */
213
269
  folderMeta: Record<string, FolderMeta>;
270
+ /** Redirects from the routes earlier releases gave its operations. */
271
+ redirects: SourceRedirect[];
214
272
  /** Non-fatal notes from the load (e.g. an offline cache fallback). */
215
273
  diagnostics: Diagnostic[];
216
274
  }
@@ -221,6 +279,8 @@ interface ParsedReference {
221
279
  warnings: string[];
222
280
  operations: ApiOperationRef[];
223
281
  tags: ApiTagRef[];
282
+ /** Operation routes earlier releases gave elsewhere (see `operationCollector`). */
283
+ moved: MovedRoute[];
224
284
  extractWarnings: string[];
225
285
  /** Spec mistakes that would render silently wrong (OpenAPI only). */
226
286
  issues?: SpecIssue[];
@@ -241,6 +301,7 @@ const parseReference = async (
241
301
  return {
242
302
  document,
243
303
  extractWarnings: extracted.warnings,
304
+ moved: extracted.moved,
244
305
  operations: extracted.operations,
245
306
  tags: extracted.tags,
246
307
  warnings,
@@ -256,6 +317,7 @@ const parseReference = async (
256
317
  return {
257
318
  document,
258
319
  extractWarnings: extracted.warnings,
320
+ moved: extracted.moved,
259
321
  operations: extracted.operations,
260
322
  tags: extracted.tags,
261
323
  warnings,
@@ -270,7 +332,8 @@ const parseReference = async (
270
332
  return {
271
333
  document,
272
334
  extractWarnings: extracted.warnings,
273
- issues: [...issues, ...specIssues(document)],
335
+ issues: [...issues, ...specIssues(document), ...extracted.issues],
336
+ moved: extracted.moved,
274
337
  operations: extracted.operations,
275
338
  tags: extracted.tags,
276
339
  warnings,
@@ -291,8 +354,15 @@ export const openApiSource = (
291
354
  const kindLabel = KIND_LABELS[reference.kind];
292
355
  const codePrefix = CODE_PREFIXES[reference.kind];
293
356
  try {
294
- const { document, warnings, operations, tags, extractWarnings, issues } =
295
- await parseReference(reference, ctx);
357
+ const {
358
+ document,
359
+ warnings,
360
+ operations,
361
+ tags,
362
+ moved,
363
+ extractWarnings,
364
+ issues,
365
+ } = await parseReference(reference, ctx);
296
366
  const info = document.info ?? { title: reference.label, version: "" };
297
367
  // The playground proxy resolves here, not client-side: `true` selects
298
368
  // the built-in `/_api-proxy` route (mounted under the site `basePath`,
@@ -375,7 +445,8 @@ export const openApiSource = (
375
445
  : []),
376
446
  ],
377
447
  entries: specEntries(spec, operations, reference),
378
- folderMeta: tagFolderMeta(spec, tags, reference.groupLabel),
448
+ folderMeta: tagFolderMeta(spec, tags, operations, reference.groupLabel),
449
+ redirects: moved,
379
450
  slug: reference.slug,
380
451
  spec,
381
452
  };
@@ -408,8 +479,17 @@ export const openApiSource = (
408
479
  severity: "warning" as const,
409
480
  }))
410
481
  );
482
+ // Display options belong to the adapter, which every one of its sources
483
+ // repeats: one warning per distinct message.
484
+ const languageWarnings = new Map(
485
+ references
486
+ .flatMap(unknownLanguageDiagnostics)
487
+ .map((diagnostic) => [diagnostic.message, diagnostic])
488
+ );
489
+ diagnostics.push(...languageWarnings.values());
411
490
  const data: OpenApiData = {};
412
491
  const folderMeta: Record<string, FolderMeta> = {};
492
+ const redirects: SourceRedirect[] = [];
413
493
  for (const result of results) {
414
494
  if ("severity" in result) {
415
495
  diagnostics.push(result);
@@ -418,10 +498,11 @@ export const openApiSource = (
418
498
  data[result.slug] = result.spec;
419
499
  entries.push(...result.entries);
420
500
  Object.assign(folderMeta, result.folderMeta);
501
+ redirects.push(...result.redirects);
421
502
  diagnostics.push(...result.diagnostics);
422
503
  }
423
504
  parsed = data;
424
- return { diagnostics, entries, folderMeta };
505
+ return { diagnostics, entries, folderMeta, redirects };
425
506
  };
426
507
 
427
508
  return {
@@ -28,3 +28,5 @@ export type { MemoryAdapter, RateLimitOptions } from "./memory.ts";
28
28
  export type { RateLimitAdapter } from "./schema.ts";
29
29
  export { upstash } from "./upstash.ts";
30
30
  export type { UpstashAdapter, UpstashOptions } from "./upstash.ts";
31
+ export { unkey } from "./unkey.ts";
32
+ export type { UnkeyAdapter, UnkeyOptions } from "./unkey.ts";
@@ -35,7 +35,7 @@ export const memoryAdapterSchema = adapterDescriptorSchema(
35
35
  * platform that runs many short-lived instances (Vercel, Netlify,
36
36
  * Cloudflare) each instance keeps its own, so it stops a burst from one
37
37
  * reader rather than enforcing the limit exactly. For that, share the count
38
- * with `upstash()` or `cloudflare()`.
38
+ * with `upstash()`, `cloudflare()`, or `unkey()`.
39
39
  */
40
40
  export const memory = (options: RateLimitOptions = {}): MemoryAdapter => ({
41
41
  kind: "memory",