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
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The heading a page's title came from. A page without a frontmatter `title`
3
+ * takes the text of its first `#` heading as its title (`deriveTitle` in
4
+ * `core/sources/normalize.ts`), and the page template renders that title as
5
+ * the page's `<h1>`, so a page that opens with the heading showed it twice,
6
+ * one above the other. This Satteri hast plugin drops that opening `<h1>`
7
+ * from such a page. Only a Markdown heading with nothing rendered before it
8
+ * counts: a raw HTML or JSX `<h1>` isn't the scan's heading, and one after
9
+ * other content stays where it is. A page in a mode without page chrome
10
+ * (`custom`, `frame`) renders no title, so its heading stays too.
11
+ *
12
+ * It runs after `heading-anchors`, so every other heading keeps the id the
13
+ * scan gave it (`setup`, `setup-1`): only the title's own element goes. Its
14
+ * id doesn't: the anchor index lists it, and links to `#setup` still land,
15
+ * because the dropped heading's id goes out through the render's front matter
16
+ * under {@link TITLE_ID_KEY}, which the page template sets on its `<h1>`.
17
+ */
18
+
19
+ import { pageModeLayout } from "../core/page-modes.ts";
20
+ import type { PageMode } from "../core/page-modes.ts";
21
+
22
+ /**
23
+ * Front matter key carrying the id of the heading this plugin dropped out of
24
+ * a render. The page template reads it back via `remarkPluginFrontmatter` and
25
+ * gives the title `<h1>` it renders in the heading's place that id.
26
+ */
27
+ export const TITLE_ID_KEY = "blumeTitleId";
28
+
29
+ /** A minimal hast node (avoids a hast type dependency). */
30
+ interface HastNode {
31
+ children?: HastNode[];
32
+ /** An element's properties: a heading's `id` is the slug `heading-anchors` set. */
33
+ properties?: { id?: string };
34
+ type: string;
35
+ value?: string;
36
+ }
37
+
38
+ /** The front matter keys this plugin reads and writes. */
39
+ interface TitleFrontmatter {
40
+ [TITLE_ID_KEY]?: string;
41
+ mode?: PageMode;
42
+ title?: string;
43
+ }
44
+
45
+ /** The slice of Satteri's hast visitor context this plugin reads. */
46
+ interface TitleHeadingContext {
47
+ data?: { astro?: { frontmatter?: TitleFrontmatter } };
48
+ indexOf: (node: HastNode) => number | undefined;
49
+ parent: (node: HastNode) => HastNode | undefined;
50
+ removeNode: (node: HastNode) => void;
51
+ }
52
+
53
+ /** A Satteri hast plugin, typed structurally to avoid a Satteri dep. */
54
+ export interface TitleHeadingPlugin {
55
+ before: (root: HastNode, ctx: TitleHeadingContext) => void;
56
+ name: string;
57
+ element: {
58
+ filter: string[];
59
+ visit: (node: HastNode, ctx: TitleHeadingContext) => void;
60
+ };
61
+ }
62
+
63
+ // An MDX expression that is only a comment, `{/* … */}`, and raw HTML that is
64
+ // only a comment, `<!-- … -->`.
65
+ const COMMENT_EXPRESSION = /^\s*\/\*[\s\S]*\*\/\s*$/u;
66
+ const HTML_COMMENT = /^\s*<!--[\s\S]*-->\s*$/u;
67
+
68
+ /** Whether a node renders nothing: whitespace, a comment, or an MDX import. */
69
+ const rendersNothing = (node: HastNode): boolean => {
70
+ switch (node.type) {
71
+ case "comment":
72
+ case "mdxjsEsm": {
73
+ return true;
74
+ }
75
+ case "text": {
76
+ return (node.value ?? "").trim() === "";
77
+ }
78
+ case "mdxFlowExpression": {
79
+ return COMMENT_EXPRESSION.test(node.value ?? "");
80
+ }
81
+ case "raw": {
82
+ return HTML_COMMENT.test(node.value ?? "");
83
+ }
84
+ default: {
85
+ return false;
86
+ }
87
+ }
88
+ };
89
+
90
+ // The renders whose first `<h1>` has been seen. The plugin instance is shared
91
+ // across every page; the render-scoped `astro` data object is a stable,
92
+ // unique key for one render (as in `heading-anchors`).
93
+ const seen = new WeakSet<object>();
94
+
95
+ export const titleHeadingPlugin = (): TitleHeadingPlugin => ({
96
+ // A render of an entry Astro rendered before can get the front matter
97
+ // object that render wrote to, so a heading dropped then leaves no id.
98
+ before(_root, ctx) {
99
+ const frontmatter = ctx.data?.astro?.frontmatter;
100
+ if (frontmatter) {
101
+ Reflect.deleteProperty(frontmatter, TITLE_ID_KEY);
102
+ }
103
+ },
104
+ element: {
105
+ filter: ["h1"],
106
+ visit(node, ctx) {
107
+ const astro = ctx.data?.astro;
108
+ const frontmatter = astro?.frontmatter;
109
+ if (
110
+ !(astro && frontmatter) ||
111
+ frontmatter.title ||
112
+ node.type !== "element" ||
113
+ seen.has(astro)
114
+ ) {
115
+ return;
116
+ }
117
+ seen.add(astro);
118
+ const parent = ctx.parent(node);
119
+ const before = parent?.children?.slice(0, ctx.indexOf(node) ?? 0) ?? [];
120
+ if (
121
+ parent?.type === "root" &&
122
+ before.every(rendersNothing) &&
123
+ pageModeLayout(frontmatter.mode).chrome
124
+ ) {
125
+ const id = node.properties?.id;
126
+ if (id) {
127
+ frontmatter[TITLE_ID_KEY] = id;
128
+ }
129
+ ctx.removeNode(node);
130
+ }
131
+ },
132
+ },
133
+ name: "blume:title-heading",
134
+ });
@@ -19,17 +19,41 @@ export const MIGRATE_SOURCES = [
19
19
  "docusaurus",
20
20
  "starlight",
21
21
  "nextra",
22
+ "vitepress",
23
+ "vuepress",
24
+ "docus",
25
+ "mkdocs",
26
+ "mdbook",
27
+ "fern",
28
+ "gitbook",
29
+ "redocly",
30
+ "readme",
31
+ "docsify",
32
+ "jekyll",
33
+ "github-wiki",
22
34
  ] as const;
23
35
 
24
36
  export type MigrateSourceId = (typeof MIGRATE_SOURCES)[number];
25
37
 
26
38
  /** Display names, for messages and the prompt. */
27
39
  export const MIGRATE_SOURCE_NAMES: Record<MigrateSourceId, string> = {
40
+ docsify: "Docsify",
41
+ docus: "Docus",
28
42
  docusaurus: "Docusaurus",
43
+ fern: "Fern",
29
44
  fumadocs: "Fumadocs",
45
+ gitbook: "GitBook",
46
+ "github-wiki": "GitHub Wiki",
47
+ jekyll: "Jekyll",
48
+ mdbook: "mdBook",
30
49
  mintlify: "Mintlify",
50
+ mkdocs: "MkDocs",
31
51
  nextra: "Nextra",
52
+ readme: "ReadMe",
53
+ redocly: "Redocly",
32
54
  starlight: "Starlight",
55
+ vitepress: "VitePress",
56
+ vuepress: "VuePress",
33
57
  };
34
58
 
35
59
  /** The skill's directory inside the published package. */
@@ -72,7 +96,9 @@ const dependencyNames = async (root: string): Promise<Set<string>> => {
72
96
 
73
97
  /**
74
98
  * Each source's tell, in the order the skill's workflow checks them: a config
75
- * file the framework owns, or a dependency only it installs.
99
+ * file the framework owns, or a dependency only it installs. A GitHub wiki
100
+ * clone has neither (its tell is the `.wiki.git` remote), so `github-wiki` is
101
+ * named-only.
76
102
  */
77
103
  const SIGNALS: {
78
104
  deps?: string[];
@@ -96,6 +122,93 @@ const SIGNALS: {
96
122
  },
97
123
  { deps: ["nextra"], source: "nextra" },
98
124
  { deps: ["@astrojs/starlight"], source: "starlight" },
125
+ // The config usually sits in docs/.vitepress, often with the dependency in
126
+ // docs/package.json rather than the root one.
127
+ {
128
+ deps: ["vitepress"],
129
+ files: [".vitepress", "docs/.vitepress"].flatMap((dir) =>
130
+ ["ts", "mts", "js", "mjs"].map((ext) => `${dir}/config.${ext}`)
131
+ ),
132
+ source: "vitepress",
133
+ },
134
+ // v1 also reads YAML and TOML configs; v2 also reads vuepress.config.* from
135
+ // the directory it runs in. vuepress-vite and vuepress-webpack were the v2
136
+ // beta wrappers.
137
+ {
138
+ deps: ["vuepress", "vuepress-vite", "vuepress-webpack"],
139
+ files: [
140
+ ...[".vuepress", "docs/.vuepress"].flatMap((dir) =>
141
+ ["ts", "js", "mjs", "yml", "toml"].map((ext) => `${dir}/config.${ext}`)
142
+ ),
143
+ "vuepress.config.ts",
144
+ "vuepress.config.js",
145
+ "vuepress.config.mjs",
146
+ ],
147
+ source: "vuepress",
148
+ },
149
+ // The Nuxt docs theme (not Docusaurus); 1.x shipped as @nuxt-themes/docus.
150
+ { deps: ["docus", "@nuxt-themes/docus"], source: "docus" },
151
+ // MkDocs and the tools that build its projects (Zensical, ProperDocs). They
152
+ // come after the JavaScript frameworks, so a stale mkdocs.yml doesn't win.
153
+ {
154
+ files: [
155
+ "mkdocs.yml",
156
+ "mkdocs.yaml",
157
+ "zensical.toml",
158
+ "properdocs.yml",
159
+ "properdocs.yaml",
160
+ ],
161
+ source: "mkdocs",
162
+ },
163
+ // A code repo usually keeps its book in a subfolder.
164
+ {
165
+ files: ["book.toml", "docs/book.toml", "book/book.toml"],
166
+ source: "mdbook",
167
+ },
168
+ // Not fern.config.json alone: plenty of repos run Fern only to generate SDKs.
169
+ { files: ["fern/docs.yml"], source: "fern" },
170
+ // Not SUMMARY.md (mdBook and HonKit books have one too) or book.json (too
171
+ // generic a name).
172
+ {
173
+ deps: ["honkit", "gitbook-cli"],
174
+ files: ["gitbook-docs.yaml", ".gitbook.yaml", ".gitbook.yml"],
175
+ source: "gitbook",
176
+ },
177
+ // After the config-file sources, and never on redocly.yaml alone: that file
178
+ // is just as often Redocly CLI lint config in a repo built with another
179
+ // framework.
180
+ {
181
+ deps: [
182
+ "@redocly/realm",
183
+ "@redocly/redoc",
184
+ "@redocly/revel",
185
+ "@redocly/reef",
186
+ "@redocly/redoc-revel",
187
+ "@redocly/redoc-reef",
188
+ "@redocly/revel-reef",
189
+ ],
190
+ files: ["sidebars.yaml"],
191
+ source: "redocly",
192
+ },
193
+ // ReadMe's bi-directional Git sync; repos that only push with the rdme CLI
194
+ // leave no tell, so those are named-only.
195
+ {
196
+ files: [
197
+ "docs/_order.yaml",
198
+ "reference/_order.yaml",
199
+ "recipes/_order.yaml",
200
+ "custom_pages/_order.yaml",
201
+ ],
202
+ source: "readme",
203
+ },
204
+ // A Docsify site is an index.html that loads the library from a CDN, so only
205
+ // the ones with a manifest give themselves away. Not _sidebar.md: on a
206
+ // case-insensitive filesystem it also matches a GitHub wiki's _Sidebar.md.
207
+ { deps: ["docsify-cli", "docsify"], source: "docsify" },
208
+ // Last: _config.yml is a generic name (Hexo and Jupyter Book use it too), so
209
+ // every more specific tell wins first, and the reference says how to spot
210
+ // the others.
211
+ { files: ["_config.yml", "docs/_config.yml"], source: "jekyll" },
99
212
  ];
100
213
 
101
214
  /**
@@ -120,6 +233,15 @@ export const detectMigrateSource = async (
120
233
  return null;
121
234
  };
122
235
 
236
+ /**
237
+ * The prompt's URL rule, where a source can't follow the default: a GitHub
238
+ * wiki's old URLs stay on github.com, where no redirect can reach them.
239
+ */
240
+ const URL_RULES: Partial<Record<MigrateSourceId, string>> = {
241
+ "github-wiki":
242
+ "Keep every content page. The old wiki URLs stay on github.com and can't be redirected, so map them with the reference's route table and link stubs instead: add no `redirects`, and never push to the wiki; prepare the stubs for the user.",
243
+ };
244
+
123
245
  /**
124
246
  * The handoff prompt: follow the skill, where its mappings for this source
125
247
  * are, and the few rules worth restating — the target version, and that no
@@ -139,11 +261,14 @@ export const migratePrompt = (options: {
139
261
  const sourceLine = source
140
262
  ? `The source is ${MIGRATE_SOURCE_NAMES[source]}${evidence}. Its exact mappings are in ${join(skillDir, "references", `${source}.md`)}.`
141
263
  : `The source framework wasn't detected, so inventory the repo first. If it's ${MIGRATE_SOURCES.map((id) => MIGRATE_SOURCE_NAMES[id]).join(", ")}, read that framework's file in ${join(skillDir, "references")}; otherwise follow the skill's mental model directly.`;
264
+ const urlRule =
265
+ (source && URL_RULES[source]) ??
266
+ "Keep every page, and add a redirect for any URL that moves.";
142
267
  return `Migrate this documentation project to Blume ${version}.
143
268
 
144
269
  Follow the blume-migrate skill: read ${join(skillDir, "SKILL.md")} first and work through its migration workflow. Wherever the skill says \`<skill>\`, it means ${skillDir}.
145
270
 
146
271
  ${sourceLine}
147
272
 
148
- Add \`blume@^${version}\` as the docs package's dependency. Keep every page, and add a redirect for any URL that moves. Report everything you drop or approximate, and run the skill's verification steps before you finish.`;
273
+ Add \`blume@^${version}\` as the docs package's dependency. ${urlRule} Report everything you drop or approximate, and run the skill's verification steps before you finish.`;
149
274
  };
package/src/og/card.ts CHANGED
@@ -52,9 +52,10 @@ export type OgFont =
52
52
  export type OgGoogleFont = Exclude<OgFont, OgLocalFont>;
53
53
 
54
54
  /**
55
- * Which loaded family each card role renders in. Takumi still falls back
56
- * across every loaded font per glyph, so a family that misses a script
57
- * degrades to the rest of the chain instead of tofu.
55
+ * Which loaded family each card role renders in, by plain family name (the
56
+ * card quotes it for the renderer). Takumi still falls back across every
57
+ * loaded font per glyph, so a family that misses a script degrades to the
58
+ * rest of the chain instead of tofu.
58
59
  */
59
60
  export interface OgFontFamilies {
60
61
  /** Family for the description and footer text. */
@@ -423,9 +424,18 @@ const titleSize = (title: string): number => {
423
424
  return 76;
424
425
  };
425
426
 
427
+ /**
428
+ * `family` as a quoted CSS string. Takumi parses `fontFamily` with the CSS
429
+ * grammar, where an unquoted family must be a run of identifiers, so a name
430
+ * with a token that starts with a digit ("Source Sans 3", "3270") fails the
431
+ * render. A quoted string accepts any name, escaped per CSS.
432
+ */
433
+ const cssFamilyName = (family: string): string =>
434
+ `"${family.replaceAll(/["\\]/gu, "\\$&")}"`;
435
+
426
436
  /** A spreadable `fontFamily` style, empty when no family is configured. */
427
437
  const familyStyle = (family?: string): { fontFamily?: string } =>
428
- family ? { fontFamily: family } : {};
438
+ family ? { fontFamily: cssFamilyName(family) } : {};
429
439
 
430
440
  /** Render a 1200x630 Open Graph card to a PNG buffer. */
431
441
  export const renderOgImage = async (
@@ -1,4 +1,8 @@
1
- import type { ApiOperationRef, ApiTagRef } from "./model.ts";
1
+ import type {
2
+ ApiOperationRef,
3
+ ApiTagMeta,
4
+ CollectedOperations,
5
+ } from "./model.ts";
2
6
  import { operationCollector, operationKey } from "./model.ts";
3
7
 
4
8
  /**
@@ -285,10 +289,11 @@ const operationSite = (
285
289
  return { action, address: channelAddress(channelId, channel), channelId };
286
290
  };
287
291
 
288
- /** The route-mapped operations, ordered tags, and skip warnings of one document. */
289
- export interface AsyncApiOperationCatalog {
290
- operations: ApiOperationRef[];
291
- tags: ApiTagRef[];
292
+ /**
293
+ * The route-mapped operations, ordered tags, moved routes, and skip warnings
294
+ * of one document.
295
+ */
296
+ export interface AsyncApiOperationCatalog extends CollectedOperations {
292
297
  warnings: string[];
293
298
  }
294
299
 
@@ -315,7 +320,10 @@ export const extractAsyncApiOperations = (
315
320
  (tag): tag is { name: string; description?: string } =>
316
321
  typeof tag?.name === "string"
317
322
  )
318
- .map((tag): [string, string] => [tag.name, tag.description ?? ""])
323
+ .map((tag): [string, ApiTagMeta] => [
324
+ tag.name,
325
+ { description: tag.description ?? "" },
326
+ ])
319
327
  );
320
328
  const collector = operationCollector(baseRoute, tagMeta);
321
329
  const channels = document.channels ?? {};
@@ -1,7 +1,10 @@
1
1
  import type { OperationObject } from "@scalar/openapi-types/3.2";
2
2
 
3
+ import type { JsonValue } from "../core/adapter.ts";
4
+ import type { AsyncApiSpecValue } from "./asyncapi.ts";
3
5
  import type { ApiDocument } from "./model.ts";
4
- import { HTTP_METHODS } from "./model.ts";
6
+ import { HTTP_METHODS, operationLabel, withServerDefaults } from "./model.ts";
7
+ import { isJsonObject } from "./overlay.ts";
5
8
 
6
9
  /**
7
10
  * Targeted checks for spec mistakes that would otherwise render silently
@@ -32,19 +35,6 @@ const NAMED_OPERATIONS = 3;
32
35
  /** One entry of a document's `paths` or `webhooks` map. */
33
36
  type PathItem = NonNullable<ApiDocument["paths"]>[string];
34
37
 
35
- /**
36
- * How a warning names an operation: `GET /pets`, or a webhook by its name
37
- * (`Webhook "newPet" (POST)`), since a webhook has no path.
38
- */
39
- export const operationLabel = (
40
- name: string,
41
- method: string,
42
- webhook: boolean
43
- ): string =>
44
- webhook
45
- ? `Webhook "${name}" (${method.toUpperCase()})`
46
- : `${method.toUpperCase()} ${name}`;
47
-
48
38
  /** One entry of a `parameters` list: a parameter, or a `$ref` to one. */
49
39
  type ParameterEntry = NonNullable<OperationObject["parameters"]>[number];
50
40
 
@@ -209,6 +199,66 @@ const xWebhooksIssues = (document: ApiDocument): SpecIssue[] =>
209
199
  ]
210
200
  : [];
211
201
 
202
+ /**
203
+ * A loopback or unspecified host (`localhost`, `127.0.0.1`, `0.0.0.0`,
204
+ * `[::1]`): one only the machine that wrote the spec can reach.
205
+ */
206
+ const LOCAL_HOST =
207
+ /^(?:localhost|.+\.localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[::1?\])$/u;
208
+
209
+ /** One `servers` entry, as a hand-written spec may have written it. */
210
+ type ServerEntry = NonNullable<ApiDocument["servers"]>[number];
211
+
212
+ /** A server's URL, variables at their defaults, when its host is local. */
213
+ const localServerUrl = (server: ServerEntry): string | undefined => {
214
+ if (!isName(server.url)) {
215
+ return undefined;
216
+ }
217
+ // SAFETY: a parsed spec is JSON, and `withServerDefaults` checks the shape
218
+ // of the `variables` map it reads.
219
+ const variables = server.variables as AsyncApiSpecValue;
220
+ const url = withServerDefaults(server.url, variables);
221
+ try {
222
+ return LOCAL_HOST.test(new URL(url).hostname) ? url : undefined;
223
+ } catch {
224
+ // A relative URL (`/v1`) or an unresolved variable names no host.
225
+ return undefined;
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Servers the spec, a path, or an operation declares at a local address,
231
+ * which a framework exporting its spec from a dev machine writes (Laravel's
232
+ * Scramble writes `http://localhost/api` from an `.env`-less CI run). The
233
+ * reference publishes it as the server Try it sends to and the code samples
234
+ * call, which no reader can reach. One warning per URL.
235
+ */
236
+ const localServerIssues = (document: ApiDocument): SpecIssue[] => {
237
+ const lists = [
238
+ document.servers,
239
+ ...Object.values(document.paths ?? {}).flatMap((item) =>
240
+ isObject(item) && !("$ref" in item)
241
+ ? [
242
+ item.servers,
243
+ ...HTTP_METHODS.map((method) => {
244
+ const operation = item[method];
245
+ return isObject(operation) ? operation.servers : undefined;
246
+ }),
247
+ ]
248
+ : []
249
+ ),
250
+ ];
251
+ const urls = new Set(
252
+ lists.flatMap((servers) => listOf(servers).map(localServerUrl))
253
+ );
254
+ return [...urls].filter(isName).map((url) => ({
255
+ code: "BLUME_OPENAPI_LOCAL_SERVER",
256
+ message: `The spec's server ${url} is a local address, so the reference publishes it as the server Try it sends requests to and the code samples call, which readers can't reach.`,
257
+ suggestion:
258
+ "List the API's public URL in the spec's `servers`, or replace it with an overlay (an action with `target: $.servers[0]` and `update: { url: \"https://api.example.com\" }`).",
259
+ }));
260
+ };
261
+
212
262
  /**
213
263
  * Check a parsed document for the mistakes above, and for the OpenAPI 3.2
214
264
  * features Blume doesn't render yet. `$ref` path items are skipped: the
@@ -269,5 +319,100 @@ export const specIssues = (document: ApiDocument): SpecIssue[] => {
269
319
  for (const [name, item] of Object.entries(document.webhooks ?? {})) {
270
320
  visit(name, item, true);
271
321
  }
272
- return [...issues, ...securitySchemeIssues(document, requirers)];
322
+ return [
323
+ ...issues,
324
+ ...securitySchemeIssues(document, requirers),
325
+ ...localServerIssues(document),
326
+ ];
327
+ };
328
+
329
+ /** The OpenAPI versions that have no `nullable` keyword: 3.1 and later. */
330
+ const NO_NULLABLE = /^3\.[1-9]/u;
331
+
332
+ /**
333
+ * Keys whose values are data rather than schemas, so a `nullable` inside
334
+ * one belongs to an example. Extensions (`x-…`) are skipped too.
335
+ */
336
+ const DATA_KEYS = new Set(["const", "default", "enum", "example", "examples"]);
337
+
338
+ /**
339
+ * Keys whose values map names to objects: the names aren't keywords, so a
340
+ * property named `nullable` isn't the keyword, and a `default` response
341
+ * isn't a default value.
342
+ */
343
+ const NAME_MAPS = new Set([
344
+ "$defs",
345
+ "definitions",
346
+ "dependentSchemas",
347
+ "patternProperties",
348
+ "properties",
349
+ "responses",
350
+ "schemas",
351
+ ]);
352
+
353
+ /** A JSON Pointer reference token, `~` and `/` escaped (RFC 6901). */
354
+ const pointerToken = (key: string): string =>
355
+ key.replaceAll("~", "~0").replaceAll("/", "~1");
356
+
357
+ const isBoolean = (value: JsonValue): value is boolean =>
358
+ typeof value === "boolean";
359
+
360
+ /**
361
+ * Where `node` sets `nullable` to a boolean, as JSON Pointers from
362
+ * `pointer`. `names` says `node`'s keys are names (see {@link NAME_MAPS}).
363
+ */
364
+ const nullablePointers = (
365
+ node: JsonValue,
366
+ pointer: string,
367
+ names: boolean
368
+ ): string[] => {
369
+ if (Array.isArray(node)) {
370
+ return node.flatMap((item, index) =>
371
+ nullablePointers(item, `${pointer}/${index}`, false)
372
+ );
373
+ }
374
+ if (!isJsonObject(node)) {
375
+ return [];
376
+ }
377
+ return Object.entries(node).flatMap(([key, value]) => {
378
+ const at = `${pointer}/${pointerToken(key)}`;
379
+ if (names) {
380
+ return nullablePointers(value, at, false);
381
+ }
382
+ if (key === "nullable" && isBoolean(value)) {
383
+ return [at];
384
+ }
385
+ return DATA_KEYS.has(key) || key.startsWith("x-")
386
+ ? []
387
+ : nullablePointers(value, at, NAME_MAPS.has(key));
388
+ });
389
+ };
390
+
391
+ /**
392
+ * A spec written as OpenAPI 3.1 or later that still uses `nullable`, which
393
+ * 3.1 removed for `type: [T, "null"]`. Read as written (before Blume
394
+ * upgrades a 3.0 spec, which rewrites `nullable` itself), since that's the
395
+ * document the author edits. Blume still renders those values as nullable,
396
+ * but validators and SDK generators that follow 3.1 read them as never
397
+ * null. One warning per spec, naming the first few places.
398
+ */
399
+ export const nullableIssues = (document: JsonValue): SpecIssue[] => {
400
+ const version =
401
+ isJsonObject(document) && isName(document.openapi) ? document.openapi : "";
402
+ if (!NO_NULLABLE.test(version)) {
403
+ return [];
404
+ }
405
+ const pointers = nullablePointers(document, "#", false);
406
+ if (pointers.length === 0) {
407
+ return [];
408
+ }
409
+ const more = pointers.length - NAMED_OPERATIONS;
410
+ return [
411
+ {
412
+ code: "BLUME_OPENAPI_NULLABLE",
413
+ message: `The spec is OpenAPI ${version}, which removed \`nullable\`, but sets it at ${pointers.slice(0, NAMED_OPERATIONS).join(", ")}${more > 0 ? ` and ${more} more` : ""}. Blume still shows those values as nullable, but tools that follow ${version}, like validators and SDK generators, read them as never null.`,
414
+ suggestion:
415
+ 'Add "null" to the schema\'s `type` instead (`type: [string, "null"]`), or beside a `$ref`, use `anyOf` with `{ type: "null" }`. If the spec is really 3.0, declare `openapi: 3.0.3`, and Blume upgrades it.',
416
+ },
417
+ ];
273
418
  };