blume 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (450) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  3. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  4. package/dist/cli/{chunk-43zcf4gz.js → chunk-0pmb4tr4.js} +541 -368
  5. package/dist/cli/chunk-0pmb4tr4.js.map +35 -0
  6. package/dist/cli/{chunk-9geymqns.js → chunk-0w6hs479.js} +15 -15
  7. package/dist/cli/{chunk-xkz5xbtv.js → chunk-124fvmzq.js} +2802 -1422
  8. package/dist/cli/chunk-124fvmzq.js.map +220 -0
  9. package/dist/cli/chunk-150jk7km.js +52 -0
  10. package/dist/cli/chunk-150jk7km.js.map +10 -0
  11. package/dist/cli/{chunk-y4ykbark.js → chunk-1f882900.js} +2 -2
  12. package/dist/cli/{chunk-5ffywsmb.js → chunk-297ajdcv.js} +8 -6
  13. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-297ajdcv.js.map} +3 -3
  14. package/dist/cli/{chunk-h2a48yhd.js → chunk-32khwcb6.js} +120 -16
  15. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-32khwcb6.js.map} +10 -10
  16. package/dist/cli/{chunk-5cg2rzgf.js → chunk-36bxs9c7.js} +20 -49
  17. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-36bxs9c7.js.map} +4 -5
  18. package/dist/cli/{chunk-py62fewh.js → chunk-53jqw8z0.js} +3 -3
  19. package/dist/cli/{chunk-5wj2qwn2.js → chunk-58bt3hag.js} +4 -4
  20. package/dist/cli/{chunk-e7vvk5ya.js → chunk-5cmts6dv.js} +3 -3
  21. package/dist/cli/{chunk-y5h4r2y0.js → chunk-60hz265r.js} +15 -5
  22. package/dist/cli/chunk-60hz265r.js.map +10 -0
  23. package/dist/cli/chunk-74wfhv79.js +65 -0
  24. package/dist/cli/chunk-74wfhv79.js.map +10 -0
  25. package/dist/cli/{chunk-g2z9x01v.js → chunk-9qzgg6mj.js} +3 -3
  26. package/dist/cli/{chunk-q3wxxq39.js → chunk-a57yh0h9.js} +5 -5
  27. package/dist/cli/{chunk-713mngdq.js → chunk-ave1wcbr.js} +76 -10
  28. package/dist/cli/chunk-ave1wcbr.js.map +14 -0
  29. package/dist/cli/{chunk-2n4ps1b3.js → chunk-bqxaqn5p.js} +40 -22
  30. package/dist/cli/chunk-bqxaqn5p.js.map +15 -0
  31. package/dist/cli/{chunk-jnwj5kjj.js → chunk-bxwhtvw8.js} +20 -7
  32. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-bxwhtvw8.js.map} +3 -3
  33. package/dist/cli/chunk-ckrcm08h.js +324 -0
  34. package/dist/cli/chunk-ckrcm08h.js.map +14 -0
  35. package/dist/cli/{chunk-2f3ndkpw.js → chunk-desvm72z.js} +3 -3
  36. package/dist/cli/{chunk-gp84za0h.js → chunk-dhwtqnnd.js} +2 -2
  37. package/dist/cli/{chunk-4h8z9vv5.js → chunk-dpy8qdjf.js} +93 -47
  38. package/dist/cli/chunk-dpy8qdjf.js.map +17 -0
  39. package/dist/cli/{chunk-2f7n4qzt.js → chunk-dqhg54mr.js} +1 -1
  40. package/dist/cli/{chunk-0gz36akr.js → chunk-dv2z0rzf.js} +21 -5
  41. package/dist/cli/chunk-dv2z0rzf.js.map +12 -0
  42. package/dist/cli/{chunk-cm89scdv.js → chunk-es8h0v3j.js} +9 -11
  43. package/dist/cli/{chunk-cm89scdv.js.map → chunk-es8h0v3j.js.map} +3 -3
  44. package/dist/cli/{chunk-zhvd6adm.js → chunk-gfc5fqvf.js} +14 -14
  45. package/dist/cli/{chunk-nd1821sk.js → chunk-gxtg6ed2.js} +33 -19
  46. package/dist/cli/chunk-gxtg6ed2.js.map +10 -0
  47. package/dist/cli/{chunk-p804f3pe.js → chunk-hvcxng5f.js} +6 -6
  48. package/dist/cli/{chunk-ta27feev.js → chunk-j1x92mx9.js} +21 -2
  49. package/dist/cli/chunk-j1x92mx9.js.map +10 -0
  50. package/dist/cli/{chunk-rekqz4qg.js → chunk-k3gr1nm9.js} +19 -16
  51. package/dist/cli/chunk-k3gr1nm9.js.map +10 -0
  52. package/dist/cli/{chunk-1n228xqb.js → chunk-kdpzw38a.js} +2 -2
  53. package/dist/cli/{chunk-cy5bm0j0.js → chunk-ms9z66nz.js} +2 -2
  54. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-ms9z66nz.js.map} +2 -2
  55. package/dist/cli/{chunk-zf748avv.js → chunk-nbyvgnnm.js} +193 -286
  56. package/dist/cli/chunk-nbyvgnnm.js.map +36 -0
  57. package/dist/cli/{chunk-6bm0xpgd.js → chunk-s0bdnc7v.js} +2 -2
  58. package/dist/cli/{chunk-yxmk4pec.js → chunk-sc93apfs.js} +17 -16
  59. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-sc93apfs.js.map} +3 -3
  60. package/dist/cli/{chunk-cmtcnh9f.js → chunk-tfycddbt.js} +2 -2
  61. package/dist/cli/{chunk-9xr4dgee.js → chunk-twy7psdz.js} +29 -16
  62. package/dist/cli/chunk-twy7psdz.js.map +10 -0
  63. package/dist/cli/{chunk-ntv4qrt7.js → chunk-y4msrwm9.js} +15 -15
  64. package/dist/cli/{chunk-yj17vs39.js → chunk-ydp3065p.js} +5 -5
  65. package/dist/cli/index.js +24 -19
  66. package/dist/cli/index.js.map +3 -3
  67. package/dist/types/ai/ask-context.d.ts +6 -2
  68. package/dist/types/ai/ask.d.ts +24 -5
  69. package/dist/types/ai/component-markdown.d.ts +2 -0
  70. package/dist/types/ai/openapi-components.d.ts +4 -1
  71. package/dist/types/analytics/clics.d.ts +37 -0
  72. package/dist/types/analytics/index.d.ts +2 -0
  73. package/dist/types/analytics/schema.d.ts +18 -0
  74. package/dist/types/astro/integration.d.ts +11 -4
  75. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  76. package/dist/types/astro/pages.d.ts +10 -0
  77. package/dist/types/cli/init/scaffold.d.ts +1 -1
  78. package/dist/types/components/content/api-field.d.ts +4 -1
  79. package/dist/types/components/content/api-page.d.ts +6 -1
  80. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  81. package/dist/types/components/openapi/helpers.d.ts +22 -1
  82. package/dist/types/components/openapi/security.d.ts +10 -0
  83. package/dist/types/components/openapi/snippets.d.ts +60 -0
  84. package/dist/types/core/changelog-index.d.ts +8 -0
  85. package/dist/types/core/config-input.d.ts +37 -6
  86. package/dist/types/core/data.d.ts +4 -0
  87. package/dist/types/core/i18n-ui.d.ts +2 -0
  88. package/dist/types/core/links.d.ts +8 -0
  89. package/dist/types/core/powered-by.d.ts +12 -0
  90. package/dist/types/core/probe.d.ts +2 -0
  91. package/dist/types/core/project-graph.d.ts +6 -0
  92. package/dist/types/core/redirect-patterns.d.ts +11 -3
  93. package/dist/types/core/schema.d.ts +228 -3
  94. package/dist/types/core/sources/cache.d.ts +3 -0
  95. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  96. package/dist/types/core/sources/contentful.d.ts +17 -1
  97. package/dist/types/core/sources/lexical.d.ts +5 -0
  98. package/dist/types/core/sources/lower.d.ts +10 -2
  99. package/dist/types/core/sources/normalize.d.ts +20 -1
  100. package/dist/types/core/sources/notion.d.ts +8 -0
  101. package/dist/types/core/sources/remote.d.ts +37 -10
  102. package/dist/types/core/sources/sanity.d.ts +1 -1
  103. package/dist/types/core/types.d.ts +17 -0
  104. package/dist/types/deploy/adapter-output.d.ts +2 -1
  105. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  106. package/dist/types/deploy/adapters/node.d.ts +58 -23
  107. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  108. package/dist/types/deploy/adapters/types.d.ts +3 -2
  109. package/dist/types/deploy/cloudflare-negotiation.d.ts +5 -2
  110. package/dist/types/deploy/node-headers.d.ts +12 -4
  111. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  112. package/dist/types/deploy/platforms/node.d.ts +3 -1
  113. package/dist/types/deploy/platforms/types.d.ts +18 -7
  114. package/dist/types/deploy/platforms/vercel.d.ts +21 -2
  115. package/dist/types/deploy/redirects.d.ts +17 -1
  116. package/dist/types/deploy/sitemap.d.ts +2 -1
  117. package/dist/types/deploy/vercel-negotiation.d.ts +44 -21
  118. package/dist/types/narration/build.d.ts +34 -12
  119. package/dist/types/narration/provider.d.ts +96 -8
  120. package/dist/types/openapi/checks.d.ts +27 -0
  121. package/dist/types/openapi/graphql.d.ts +6 -0
  122. package/dist/types/openapi/model.d.ts +15 -4
  123. package/dist/types/openapi/parse.d.ts +3 -0
  124. package/dist/types/openapi/references.d.ts +16 -0
  125. package/dist/types/openapi/sentence.d.ts +11 -1
  126. package/dist/types/ratelimit/index.d.ts +1 -1
  127. package/dist/types/ratelimit/schema.d.ts +8 -0
  128. package/dist/types/ratelimit/upstash.d.ts +39 -14
  129. package/dist/types/reference/graphql.d.ts +76 -1
  130. package/dist/types/reference/index.d.ts +1 -1
  131. package/dist/types/reference/openapi.d.ts +2 -2
  132. package/dist/types/reference/options.d.ts +37 -1
  133. package/dist/types/reference/schema.d.ts +84 -0
  134. package/dist/types/search/adapters/registry.d.ts +3 -0
  135. package/dist/types/search/adapters/typesense.d.ts +14 -4
  136. package/dist/types/search/build.d.ts +3 -1
  137. package/dist/types/search/orama-index.d.ts +9 -9
  138. package/dist/types/search/sync/algolia.d.ts +5 -2
  139. package/dist/types/search/sync/index.d.ts +3 -1
  140. package/dist/types/search/sync/typesense.d.ts +11 -7
  141. package/dist/types/seo/jsonld.d.ts +11 -6
  142. package/dist/types/sources/contentful.d.ts +16 -0
  143. package/dist/types/sources/github-releases.d.ts +8 -1
  144. package/dist/types/sources/notion.d.ts +8 -0
  145. package/dist/types/sources/payload.d.ts +2 -0
  146. package/dist/types/sources/registry.d.ts +63 -0
  147. package/dist/types/sources/shared.d.ts +3 -0
  148. package/dist/types/sources/strapi.d.ts +2 -0
  149. package/docs/01-quickstart.mdx +4 -2
  150. package/docs/02-deployment.mdx +59 -13
  151. package/docs/03-upgrading.mdx +2 -1
  152. package/docs/04-migrating.mdx +9 -9
  153. package/docs/08-faq.mdx +12 -12
  154. package/docs/advanced/changelog.mdx +30 -2
  155. package/docs/advanced/custom-pages.mdx +1 -1
  156. package/docs/advanced/skills.mdx +1 -1
  157. package/docs/cli/audit.mdx +30 -7
  158. package/docs/cli/doctor.mdx +6 -2
  159. package/docs/cli/evals.mdx +4 -4
  160. package/docs/cli/translate.mdx +1 -0
  161. package/docs/cli/validate.mdx +4 -3
  162. package/docs/configuration/analytics.mdx +19 -1
  163. package/docs/configuration/assistant.mdx +6 -2
  164. package/docs/configuration/customization.mdx +4 -2
  165. package/docs/configuration/export.mdx +5 -3
  166. package/docs/configuration/index.mdx +4 -2
  167. package/docs/configuration/narration.mdx +43 -6
  168. package/docs/configuration/rate-limiting.mdx +23 -2
  169. package/docs/configuration/search.mdx +37 -14
  170. package/docs/content/components.mdx +2 -2
  171. package/docs/content/frontmatter.mdx +5 -3
  172. package/docs/content/i18n.mdx +2 -2
  173. package/docs/content/includes.mdx +1 -1
  174. package/docs/content/navigation.mdx +8 -6
  175. package/docs/content/sources/contentful.mdx +9 -4
  176. package/docs/content/sources/custom.mdx +7 -3
  177. package/docs/content/sources/github-releases.mdx +12 -2
  178. package/docs/content/sources/index.mdx +3 -3
  179. package/docs/content/sources/notion.mdx +17 -2
  180. package/docs/content/sources/obsidian.mdx +3 -1
  181. package/docs/content/sources/payload.mdx +5 -2
  182. package/docs/content/sources/remote-mdx.mdx +6 -0
  183. package/docs/content/sources/sanity.mdx +4 -2
  184. package/docs/content/sources/strapi.mdx +5 -2
  185. package/docs/content/syntax.mdx +2 -2
  186. package/docs/discoverability/agent-discovery.mdx +1 -1
  187. package/docs/discoverability/index.mdx +1 -1
  188. package/docs/discoverability/json-api.mdx +2 -0
  189. package/docs/discoverability/llms-txt.mdx +1 -1
  190. package/docs/discoverability/markdown.mdx +3 -1
  191. package/docs/discoverability/mcp.mdx +6 -0
  192. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  193. package/docs/discoverability/structured-data.mdx +8 -6
  194. package/docs/references/api-pages.mdx +1 -1
  195. package/docs/references/asyncapi.mdx +5 -1
  196. package/docs/references/graphql.mdx +15 -1
  197. package/docs/references/openapi.mdx +64 -8
  198. package/package.json +9 -9
  199. package/skills/blume/SKILL.md +1 -1
  200. package/skills/blume-migrate/SKILL.md +2 -2
  201. package/skills/blume-migrate/assets/{oxfmt@0.67.0.patch → oxfmt@0.71.0.patch} +7 -7
  202. package/skills/blume-migrate/references/docusaurus.md +4 -4
  203. package/skills/blume-migrate/references/fumadocs.md +17 -13
  204. package/skills/blume-migrate/references/monorepo.md +5 -5
  205. package/skills/blume-migrate/references/nextra.md +8 -5
  206. package/skills/blume-migrate/references/starlight.md +9 -7
  207. package/src/ai/ask-context.ts +33 -3
  208. package/src/ai/ask-limits.ts +13 -0
  209. package/src/ai/ask.ts +24 -6
  210. package/src/ai/changelog-markdown.ts +11 -6
  211. package/src/ai/component-markdown.ts +1 -1
  212. package/src/ai/cors.ts +12 -3
  213. package/src/ai/mcp/query.ts +14 -6
  214. package/src/ai/openapi-components.ts +28 -4
  215. package/src/ai/relative-links.ts +4 -38
  216. package/src/analytics/clics.ts +58 -0
  217. package/src/analytics/head.ts +5 -0
  218. package/src/analytics/index.ts +2 -0
  219. package/src/analytics/schema.ts +2 -0
  220. package/src/astro/generate.ts +91 -56
  221. package/src/astro/integration.ts +72 -22
  222. package/src/astro/markdown-negotiation.ts +3 -0
  223. package/src/astro/module-types.ts +3 -1
  224. package/src/astro/pages.ts +23 -0
  225. package/src/astro/render-deps.ts +10 -5
  226. package/src/astro/runtime-deps.ts +23 -2
  227. package/src/astro/templates.ts +129 -81
  228. package/src/audit/catalog.ts +19 -3
  229. package/src/audit/checks/links.ts +14 -1
  230. package/src/audit/checks/network.ts +110 -6
  231. package/src/audit/checks/redirects.ts +6 -4
  232. package/src/audit/checks/robots.ts +58 -2
  233. package/src/audit/run.ts +3 -0
  234. package/src/audit/types.ts +2 -0
  235. package/src/audit/url.ts +14 -5
  236. package/src/blume-modules.d.ts +5 -1
  237. package/src/cli/args.ts +51 -0
  238. package/src/cli/build-failure.ts +23 -0
  239. package/src/cli/commands/audit.ts +10 -1
  240. package/src/cli/commands/dev.ts +18 -2
  241. package/src/cli/commands/doctor.ts +45 -12
  242. package/src/cli/commands/eval.ts +36 -4
  243. package/src/cli/commands/init.ts +6 -4
  244. package/src/cli/commands/validate.ts +10 -1
  245. package/src/cli/init/scaffold.ts +9 -6
  246. package/src/cli/prepare.ts +6 -3
  247. package/src/cli/required-secrets.ts +3 -2
  248. package/src/components/content/ApiPlayground.astro +6 -2
  249. package/src/components/content/api-field.ts +4 -1
  250. package/src/components/content/api-page.ts +31 -7
  251. package/src/components/content/mermaid-element.ts +15 -1
  252. package/src/components/content/view-switcher.ts +7 -4
  253. package/src/components/islands/hooks.ts +27 -1
  254. package/src/components/layout/NavTree.astro +49 -5
  255. package/src/components/layout/NavTreeScript.astro +18 -0
  256. package/src/components/layout/PageActions.astro +139 -24
  257. package/src/components/layout/PageLayout.astro +13 -3
  258. package/src/components/layout/RootLayout.astro +27 -14
  259. package/src/components/layout/Search.astro +17 -3
  260. package/src/components/layout/TableOfContents.astro +24 -17
  261. package/src/components/layout/analytics-client.ts +8 -8
  262. package/src/components/layout/head-scripts.ts +6 -1
  263. package/src/components/layout/search/algolia.ts +10 -1
  264. package/src/components/layout/search/analytics.ts +42 -14
  265. package/src/components/layout/search/endpoint.ts +48 -5
  266. package/src/components/layout/search/pagefind.ts +114 -5
  267. package/src/components/layout/search/types.ts +12 -0
  268. package/src/components/layout/search/typesense.ts +18 -6
  269. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  270. package/src/components/openapi/Authorization.astro +10 -0
  271. package/src/components/openapi/GraphqlOperation.astro +5 -0
  272. package/src/components/openapi/Operation.astro +24 -1
  273. package/src/components/openapi/ParametersTable.astro +8 -4
  274. package/src/components/openapi/Playground.astro +7 -1
  275. package/src/components/openapi/RequestBody.astro +19 -2
  276. package/src/components/openapi/Responses.astro +1 -0
  277. package/src/components/openapi/SchemaTable.astro +15 -3
  278. package/src/components/openapi/async-snippets.ts +61 -5
  279. package/src/components/openapi/async.ts +123 -29
  280. package/src/components/openapi/graphql-helpers.ts +35 -12
  281. package/src/components/openapi/helpers.ts +36 -2
  282. package/src/components/openapi/message-model.ts +54 -11
  283. package/src/components/openapi/message.ts +14 -4
  284. package/src/components/openapi/operation-model.ts +16 -7
  285. package/src/components/openapi/playground-client.ts +77 -58
  286. package/src/components/openapi/playground-schema.ts +1 -1
  287. package/src/components/openapi/schema-tree.ts +44 -14
  288. package/src/components/openapi/security.ts +10 -0
  289. package/src/components/openapi/snippets.ts +5 -3
  290. package/src/components/openapi/validate-json.ts +4 -3
  291. package/src/components/powered-by-middleware.ts +33 -0
  292. package/src/core/changelog-index.ts +31 -1
  293. package/src/core/component-diagnostics.ts +53 -0
  294. package/src/core/component-overrides.ts +32 -0
  295. package/src/core/config-input.ts +42 -6
  296. package/src/core/data.ts +2 -0
  297. package/src/core/diagnostics.ts +5 -0
  298. package/src/core/i18n-ui.ts +2 -0
  299. package/src/core/includes.ts +1 -1
  300. package/src/core/links.ts +47 -17
  301. package/src/core/powered-by.ts +12 -0
  302. package/src/core/probe.ts +26 -6
  303. package/src/core/project-graph.ts +59 -18
  304. package/src/core/redirect-patterns.ts +36 -7
  305. package/src/core/schema.ts +26 -6
  306. package/src/core/sources/cache.ts +6 -0
  307. package/src/core/sources/contentful-rich-text.ts +29 -6
  308. package/src/core/sources/contentful.ts +135 -25
  309. package/src/core/sources/filesystem.ts +26 -4
  310. package/src/core/sources/github-releases.ts +12 -1
  311. package/src/core/sources/lexical.ts +15 -10
  312. package/src/core/sources/lower.ts +19 -3
  313. package/src/core/sources/mdx-remote.ts +62 -10
  314. package/src/core/sources/normalize.ts +227 -41
  315. package/src/core/sources/notion.ts +34 -10
  316. package/src/core/sources/obsidian.ts +16 -3
  317. package/src/core/sources/payload.ts +38 -15
  318. package/src/core/sources/remote.ts +72 -15
  319. package/src/core/sources/resolve.ts +11 -1
  320. package/src/core/sources/sanity.ts +43 -25
  321. package/src/core/sources/strapi.ts +2 -2
  322. package/src/core/types.ts +18 -0
  323. package/src/core/ui-packs/ar.ts +1 -0
  324. package/src/core/ui-packs/bg.ts +1 -0
  325. package/src/core/ui-packs/bn.ts +1 -0
  326. package/src/core/ui-packs/ca.ts +1 -0
  327. package/src/core/ui-packs/cs.ts +1 -0
  328. package/src/core/ui-packs/da.ts +1 -0
  329. package/src/core/ui-packs/de.ts +1 -0
  330. package/src/core/ui-packs/el.ts +1 -0
  331. package/src/core/ui-packs/es.ts +1 -0
  332. package/src/core/ui-packs/fa.ts +1 -0
  333. package/src/core/ui-packs/fi.ts +1 -0
  334. package/src/core/ui-packs/fr.ts +1 -0
  335. package/src/core/ui-packs/he.ts +1 -0
  336. package/src/core/ui-packs/hi.ts +1 -0
  337. package/src/core/ui-packs/hr.ts +1 -0
  338. package/src/core/ui-packs/hu.ts +1 -0
  339. package/src/core/ui-packs/id.ts +1 -0
  340. package/src/core/ui-packs/it.ts +1 -0
  341. package/src/core/ui-packs/ja.ts +1 -0
  342. package/src/core/ui-packs/ko.ts +1 -0
  343. package/src/core/ui-packs/nl.ts +1 -0
  344. package/src/core/ui-packs/no.ts +1 -0
  345. package/src/core/ui-packs/pl.ts +1 -0
  346. package/src/core/ui-packs/pt-br.ts +1 -0
  347. package/src/core/ui-packs/pt.ts +1 -0
  348. package/src/core/ui-packs/ro.ts +1 -0
  349. package/src/core/ui-packs/ru.ts +1 -0
  350. package/src/core/ui-packs/sk.ts +1 -0
  351. package/src/core/ui-packs/sr.ts +1 -0
  352. package/src/core/ui-packs/sv.ts +1 -0
  353. package/src/core/ui-packs/th.ts +1 -0
  354. package/src/core/ui-packs/tr.ts +1 -0
  355. package/src/core/ui-packs/uk.ts +1 -0
  356. package/src/core/ui-packs/vi.ts +1 -0
  357. package/src/core/ui-packs/zh-tw.ts +1 -0
  358. package/src/core/ui-packs/zh.ts +1 -0
  359. package/src/deploy/adapter-output.ts +7 -2
  360. package/src/deploy/adapters/cloudflare.ts +7 -5
  361. package/src/deploy/adapters/node.ts +47 -9
  362. package/src/deploy/adapters/types.ts +7 -3
  363. package/src/deploy/artifacts.ts +2 -1
  364. package/src/deploy/cloudflare-negotiation.ts +36 -7
  365. package/src/deploy/headers.ts +15 -1
  366. package/src/deploy/node-headers.ts +32 -20
  367. package/src/deploy/platforms/cloudflare.ts +14 -9
  368. package/src/deploy/platforms/netlify.ts +2 -1
  369. package/src/deploy/platforms/node.ts +12 -2
  370. package/src/deploy/platforms/types.ts +21 -7
  371. package/src/deploy/platforms/vercel.ts +115 -17
  372. package/src/deploy/redirects.ts +66 -6
  373. package/src/deploy/rss.ts +3 -1
  374. package/src/deploy/sitemap.ts +39 -5
  375. package/src/deploy/vercel-negotiation.ts +325 -103
  376. package/src/eval/agents.ts +18 -1
  377. package/src/eval/prompts.ts +6 -3
  378. package/src/eval/report.ts +34 -13
  379. package/src/eval/run.ts +5 -0
  380. package/src/markdown/base-links.ts +15 -14
  381. package/src/markdown/package-commands.ts +9 -8
  382. package/src/markdown/relative-links.ts +3 -2
  383. package/src/narration/build.ts +218 -40
  384. package/src/narration/provider.ts +89 -9
  385. package/src/openapi/checks.ts +273 -0
  386. package/src/openapi/graphql-build.ts +21 -0
  387. package/src/openapi/graphql.ts +11 -0
  388. package/src/openapi/model.ts +15 -3
  389. package/src/openapi/parse.ts +127 -2
  390. package/src/openapi/references.ts +43 -3
  391. package/src/openapi/render-mdx.ts +68 -8
  392. package/src/openapi/sentence.ts +29 -2
  393. package/src/openapi/source.ts +39 -14
  394. package/src/ratelimit/index.ts +1 -1
  395. package/src/ratelimit/runtime.ts +37 -19
  396. package/src/ratelimit/upstash.ts +40 -10
  397. package/src/reference/graphql.ts +15 -1
  398. package/src/reference/index.ts +1 -0
  399. package/src/reference/openapi.ts +2 -2
  400. package/src/reference/options.ts +29 -1
  401. package/src/registry/eject.ts +8 -2
  402. package/src/search/adapters/locale-scope.ts +30 -0
  403. package/src/search/adapters/typesense.ts +14 -4
  404. package/src/search/build.ts +21 -3
  405. package/src/search/orama-index.ts +163 -31
  406. package/src/search/source-pages.ts +29 -0
  407. package/src/search/sync/algolia.ts +99 -9
  408. package/src/search/sync/index.ts +52 -13
  409. package/src/search/sync/typesense.ts +72 -27
  410. package/src/seo/jsonld.ts +38 -20
  411. package/src/sources/contentful.ts +12 -0
  412. package/src/sources/github-releases.ts +7 -1
  413. package/src/sources/notion.ts +7 -0
  414. package/src/sources/shared.ts +3 -0
  415. package/src/theme/color.ts +9 -10
  416. package/src/theme/entry.ts +43 -6
  417. package/src/translate/validate.ts +23 -1
  418. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  419. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  420. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  421. package/dist/cli/chunk-4ejxbzee.js +0 -39
  422. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  423. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  424. package/dist/cli/chunk-713mngdq.js.map +0 -14
  425. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  426. package/dist/cli/chunk-amrb6hgh.js +0 -145
  427. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  428. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  429. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  430. package/dist/cli/chunk-ta27feev.js.map +0 -10
  431. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  432. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  433. package/dist/cli/chunk-zf748avv.js.map +0 -37
  434. /package/dist/cli/{chunk-9geymqns.js.map → chunk-0w6hs479.js.map} +0 -0
  435. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-1f882900.js.map} +0 -0
  436. /package/dist/cli/{chunk-py62fewh.js.map → chunk-53jqw8z0.js.map} +0 -0
  437. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-58bt3hag.js.map} +0 -0
  438. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-5cmts6dv.js.map} +0 -0
  439. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-9qzgg6mj.js.map} +0 -0
  440. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-a57yh0h9.js.map} +0 -0
  441. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-desvm72z.js.map} +0 -0
  442. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-dhwtqnnd.js.map} +0 -0
  443. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-dqhg54mr.js.map} +0 -0
  444. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-gfc5fqvf.js.map} +0 -0
  445. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-hvcxng5f.js.map} +0 -0
  446. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-kdpzw38a.js.map} +0 -0
  447. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-s0bdnc7v.js.map} +0 -0
  448. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-tfycddbt.js.map} +0 -0
  449. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-y4msrwm9.js.map} +0 -0
  450. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-ydp3065p.js.map} +0 -0
@@ -10,27 +10,40 @@ import { duration, money, seconds } from "../cli/report-format.ts";
10
10
  import { countBySeverity } from "../core/diagnostics.ts";
11
11
  import type { EvalResult, QuestionResult, QuestionStatus } from "./run.ts";
12
12
 
13
+ /** How a question reads in the report: its status, or `warn` for a warned miss. */
14
+ type ShownStatus = QuestionStatus | "warn";
15
+
13
16
  const GLYPH = {
14
17
  error: "!",
15
18
  fail: "✖",
16
19
  pass: "✔",
17
20
  skip: "⊘",
18
- } satisfies Record<QuestionStatus, string>;
21
+ warn: "⚠",
22
+ } satisfies Record<ShownStatus, string>;
19
23
 
20
24
  const STATUS_COLOR = {
21
25
  error: colors.yellow,
22
26
  fail: colors.red,
23
27
  pass: colors.green,
24
28
  skip: colors.dim,
25
- } satisfies Record<QuestionStatus, ColorFunction>;
29
+ warn: colors.yellow,
30
+ } satisfies Record<ShownStatus, ColorFunction>;
31
+
32
+ /**
33
+ * A failed `severity: warning` question: its miss never counts against the
34
+ * gate, so the report shows and counts it as a warning, not a failure.
35
+ */
36
+ const warned = (result: QuestionResult): boolean =>
37
+ result.status === "fail" && result.severity === "warning";
26
38
 
27
39
  /** Longest id gets the room; everything shorter aligns to it. */
28
40
  const ID_PAD = 28;
29
41
 
30
42
  /** One question's progress/report line: glyph, id, status, score, time, cost. */
31
43
  export const questionLine = (result: QuestionResult): string => {
32
- const color = STATUS_COLOR[result.status];
33
- const glyph = color(GLYPH[result.status]);
44
+ const shown: ShownStatus = warned(result) ? "warn" : result.status;
45
+ const color = STATUS_COLOR[shown];
46
+ const glyph = color(GLYPH[shown]);
34
47
  const id = result.id.padEnd(ID_PAD);
35
48
  if (result.status === "skip") {
36
49
  return ` ${glyph} ${id} ${colors.dim("skipped")}`;
@@ -38,7 +51,7 @@ export const questionLine = (result: QuestionResult): string => {
38
51
  const score = result.score === undefined ? "" : result.score.toFixed(2);
39
52
  const cost = money(result.costUsd);
40
53
  const cells = [
41
- color(result.status),
54
+ color(shown),
42
55
  score,
43
56
  colors.dim(seconds(result.durationMs)),
44
57
  cost === "" ? "" : colors.dim(cost),
@@ -75,9 +88,12 @@ export const questionDetails = (
75
88
  /** The one-line totals: `9 passed · 2 failed · 1 skipped · 1m 42s · $0.71`. */
76
89
  export const summaryLine = (result: EvalResult): string => {
77
90
  const { counts } = result;
91
+ const warnings = result.results.filter(warned).length;
92
+ const failures = counts.fail - warnings;
78
93
  const parts = [
79
94
  `${counts.pass} passed`,
80
- counts.fail > 0 ? `${counts.fail} failed` : "",
95
+ failures > 0 ? `${failures} failed` : "",
96
+ warnings > 0 ? `${warnings} warned` : "",
81
97
  counts.error > 0 ? `${counts.error} errored` : "",
82
98
  counts.skip > 0 ? `${counts.skip} skipped` : "",
83
99
  duration(result.durationMs),
@@ -98,10 +114,11 @@ export const startLine = (id: string, index: number, total: number): string =>
98
114
  * `fix:` pointers for failed questions, naming the file that resolves each. A
99
115
  * question whose agent run errored gets a `run failed:` line pointing at the
100
116
  * question instead: the run never graded the docs, so there's no page to fix.
117
+ * A `severity: warning` question's miss is a warning line instead.
101
118
  */
102
119
  export const fixLines = (result: EvalResult, root: string): string[] =>
103
120
  result.diagnostics
104
- .filter((diagnostic) => diagnostic.code !== "BLUME_EVAL_ROUTE_UNKNOWN")
121
+ .filter((diagnostic) => diagnostic.severity !== "warning")
105
122
  .map((finding) => {
106
123
  const site = finding.file
107
124
  ? `${relative(root, finding.file)}${finding.line ? `:${finding.line}` : ""}`
@@ -113,10 +130,13 @@ export const fixLines = (result: EvalResult, root: string): string[] =>
113
130
  return ` ${label} ${site} ${colors.dim(finding.message)}`;
114
131
  });
115
132
 
116
- /** Dim warnings for route hints that no longer match a page. */
133
+ /**
134
+ * Dim warnings: route hints that no longer match a page, and the misses of
135
+ * `severity: warning` questions.
136
+ */
117
137
  export const warningLines = (result: EvalResult, root: string): string[] =>
118
138
  result.diagnostics
119
- .filter((diagnostic) => diagnostic.code === "BLUME_EVAL_ROUTE_UNKNOWN")
139
+ .filter((diagnostic) => diagnostic.severity === "warning")
120
140
  .map((finding) => {
121
141
  const site = finding.file
122
142
  ? ` ${relative(root, finding.file)}${finding.line ? `:${finding.line}` : ""}`
@@ -140,10 +160,11 @@ export const formatEvalReport = (
140
160
  }
141
161
  lines.push("");
142
162
 
143
- // The finding tells the author which file fixes which failure.
144
- const failures = fixLines(result, root);
145
- lines.push(...failures);
146
- if (failures.length > 0) {
163
+ // The finding tells the author which file fixes which failure, warnings
164
+ // first, as the command prints them.
165
+ const findings = [...warningLines(result, root), ...fixLines(result, root)];
166
+ lines.push(...findings);
167
+ if (findings.length > 0) {
147
168
  lines.push("");
148
169
  }
149
170
 
package/src/eval/run.ts CHANGED
@@ -40,6 +40,8 @@ export interface QuestionResult {
40
40
  question: string;
41
41
  routes: string[];
42
42
  score?: number;
43
+ /** The question's severity: a `warning` miss is reported as a warning. */
44
+ severity?: EvalQuestion["severity"];
43
45
  status: QuestionStatus;
44
46
  }
45
47
 
@@ -99,6 +101,7 @@ const errored = (
99
101
  missing: [],
100
102
  question: question.question,
101
103
  routes: question.routes,
104
+ severity: question.severity,
102
105
  status: "error",
103
106
  });
104
107
 
@@ -177,6 +180,7 @@ const runQuestion = async (
177
180
  question: question.question,
178
181
  routes: question.routes,
179
182
  score: verdict.score,
183
+ severity: question.severity,
180
184
  status: verdict.pass ? "pass" : "fail",
181
185
  };
182
186
  };
@@ -250,6 +254,7 @@ export const runEval = async (options: EvalRunOptions): Promise<EvalResult> => {
250
254
  missing: [],
251
255
  question: question.question,
252
256
  routes: question.routes,
257
+ severity: question.severity,
253
258
  status: "skip",
254
259
  });
255
260
  continue;
@@ -24,6 +24,21 @@ interface MdastUrlContext {
24
24
  const isUrl = (url: string | null | undefined): url is string =>
25
25
  typeof url === "string";
26
26
 
27
+ /** Record `based(url)` on the node when its root-relative URL changes. */
28
+ const rebase = (
29
+ node: UrlNode,
30
+ ctx: MdastUrlContext,
31
+ based: (url: string) => string
32
+ ): void => {
33
+ const { url } = node;
34
+ if (isUrl(url) && isInternalPath(url)) {
35
+ const next = based(url);
36
+ if (next !== url) {
37
+ ctx.setProperty(node, "url", next);
38
+ }
39
+ }
40
+ };
41
+
27
42
  export interface BaseLinksPluginOptions {
28
43
  /**
29
44
  * The `blume:data` JSON file, for an ejected app: with no CLI in the process
@@ -72,20 +87,6 @@ export const baseLinksPlugin = (
72
87
  const servesPage = (route: string): boolean =>
73
88
  servesRoute(servedRoutes(), route);
74
89
 
75
- /** Record `based(url)` on the node when its root-relative URL changes. */
76
- const rebase = (
77
- node: UrlNode,
78
- ctx: MdastUrlContext,
79
- based: (url: string) => string
80
- ): void => {
81
- const { url } = node;
82
- if (isUrl(url) && isInternalPath(url)) {
83
- const next = based(url);
84
- if (next !== url) {
85
- ctx.setProperty(node, "url", next);
86
- }
87
- }
88
- };
89
90
  const rebaseLink = (node: UrlNode, ctx: MdastUrlContext): void =>
90
91
  rebase(node, ctx, (url) =>
91
92
  withAuthoredBasePath(deployBase, basePath, url, servesPage)
@@ -205,6 +205,9 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
205
205
  return words.join(" ");
206
206
  };
207
207
 
208
+ const normalizeCommand = (command: string): string =>
209
+ command.replaceAll(WHITESPACE_RUN, " ").trim();
210
+
208
211
  /**
209
212
  * Convert an install command into the equivalent for every supported package
210
213
  * manager. Accepts a bare package list (`react`) or a full command
@@ -212,14 +215,12 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
212
215
  */
213
216
  export const toPackageCommands = (input: string) => {
214
217
  const intent = parseIntent(input);
215
- const normalize = (command: string): string =>
216
- command.replaceAll(WHITESPACE_RUN, " ").trim();
217
218
  return {
218
- aube: normalize(buildCommand("aube", intent)),
219
- bun: normalize(buildCommand("bun", intent)),
220
- npm: normalize(buildCommand("npm", intent)),
221
- nub: normalize(buildCommand("nub", intent)),
222
- pnpm: normalize(buildCommand("pnpm", intent)),
223
- yarn: normalize(buildCommand("yarn", intent)),
219
+ aube: normalizeCommand(buildCommand("aube", intent)),
220
+ bun: normalizeCommand(buildCommand("bun", intent)),
221
+ npm: normalizeCommand(buildCommand("npm", intent)),
222
+ nub: normalizeCommand(buildCommand("nub", intent)),
223
+ pnpm: normalizeCommand(buildCommand("pnpm", intent)),
224
+ yarn: normalizeCommand(buildCommand("yarn", intent)),
224
225
  } satisfies Record<PackageManager, string>;
225
226
  };
@@ -266,8 +266,9 @@ export const relativeLinksPlugin = (
266
266
 
267
267
  // A component's string `href` (`<Card href="./install">`) is the same kind
268
268
  // of link, and `blume validate` reads it as one. Lowercase elements are
269
- // raw HTML in a `.md` page, which neither rewrites nor checks, so only
270
- // components are rewritten; an expression-valued `href={…}` is left alone.
269
+ // raw HTML in a `.md` page, which isn't rewritten, so only components are
270
+ // rewritten (validate reads a raw `<a href>` as the browser does); an
271
+ // expression-valued `href={…}` is left alone.
271
272
  const rewriteHref = (node: JsxNode, ctx: RelativeLinksContext): void => {
272
273
  if (!node.name || !COMPONENT_NAME.test(node.name)) {
273
274
  return;
@@ -10,15 +10,24 @@ import {
10
10
  writeFile,
11
11
  } from "node:fs/promises";
12
12
 
13
+ import type * as OpenAISdk from "@ai-sdk/openai";
14
+ import type { OpenAIProvider } from "@ai-sdk/openai";
13
15
  import { createGateway, generateSpeech } from "ai";
16
+ import type { SpeechModel } from "ai";
14
17
  import { HTMLElement, parse, TextNode } from "node-html-parser";
15
18
  import type { Node } from "node-html-parser";
16
19
  import { dirname, join } from "pathe";
17
20
 
18
21
  import { normalizeBasePath } from "../core/base-path.ts";
22
+ import { nodeRequire } from "../core/node-require.ts";
19
23
  import type { BlumeProject } from "../core/project-graph.ts";
20
24
  import type { ProjectContext } from "../core/types.ts";
21
- import type { NarrationProvider } from "./provider.ts";
25
+ import { hasCredentials, OPENAI_SPEECH_DEP } from "./provider.ts";
26
+ import type {
27
+ GatewayNarrationProvider,
28
+ NarrationProvider,
29
+ OpenAINarrationProvider,
30
+ } from "./provider.ts";
22
31
  import {
23
32
  extractNarration,
24
33
  isNarratable,
@@ -37,10 +46,11 @@ import type {
37
46
  * carries the player exactly as the browser will (same walker, same blocks),
38
47
  * split it into sentences, and give each sentence a clip from the configured
39
48
  * speech model. Clips are cached by what decides their sound (text, language,
40
- * model, voice, instructions, provider options), so a rebuild only pays for
41
- * sentences that changed, and one sentence shared by many pages is generated
42
- * once. Each page gets a manifest listing its clips; the player plays them in
43
- * order and falls back to browser voices wherever a manifest is missing.
49
+ * model, voice, instructions, provider options, endpoint), so a rebuild only
50
+ * pays for sentences that changed, and one sentence shared by many pages is
51
+ * generated once. Each page gets a manifest listing its clips; the player
52
+ * plays them in order and falls back to browser voices wherever a manifest is
53
+ * missing.
44
54
  */
45
55
 
46
56
  /** How the build logs: the integration's Astro logger. */
@@ -54,7 +64,10 @@ export type Speak = (text: string, lang: string) => Promise<Uint8Array>;
54
64
 
55
65
  /** What a narration build did, for the log line and tests. */
56
66
  export interface NarrationBuildResult {
57
- /** Clips that failed to generate; their pages get no manifest. */
67
+ /**
68
+ * Clips that failed to generate, or that a missing key left ungenerated;
69
+ * their pages get no manifest.
70
+ */
58
71
  failed: number;
59
72
  /** Clips generated by the speech model on this build. */
60
73
  generated: number;
@@ -67,6 +80,13 @@ export interface NarrationBuildResult {
67
80
  // Bump to regenerate every clip: part of every cache key.
68
81
  const CLIP_FORMAT = 1;
69
82
 
83
+ // Where `openai()` speaks with neither a `baseUrl` nor `OPENAI_BASE_URL`.
84
+ const OPENAI_API_URL = "https://api.openai.com/v1";
85
+
86
+ // What `generateSpeech` reports for bytes it recognized as MP3. Bytes it can't
87
+ // place at all (a web page, raw PCM) default to `audio/mp3` instead.
88
+ const MP3_MEDIA_TYPE = "audio/mpeg";
89
+
70
90
  // Parallel speech requests. Low enough to stay inside provider rate limits on
71
91
  // a first build of a large site, where every sentence is new.
72
92
  const CONCURRENCY = 4;
@@ -144,23 +164,52 @@ export const readPlayerPage = (html: string): PlayerPage | null => {
144
164
  const speechLanguage = (lang: string): string =>
145
165
  lang.toLowerCase().replaceAll("_", "-").split("-")[0] ?? lang;
146
166
 
167
+ /**
168
+ * The server `openai()` speaks to: its `baseUrl`, else `OPENAI_BASE_URL` as
169
+ * OpenAI's SDKs read it, else OpenAI, without a trailing slash, which the SDK
170
+ * drops too. Resolved here rather than left to the SDK, so the clip key names
171
+ * the server the clips come from.
172
+ */
173
+ const openaiEndpoint = (provider: OpenAINarrationProvider): string =>
174
+ (
175
+ provider.options.baseUrl ||
176
+ process.env.OPENAI_BASE_URL ||
177
+ OPENAI_API_URL
178
+ ).replace(/\/+$/u, "");
179
+
180
+ /** The server a provider's clips come from: none named for `gateway()`. */
181
+ const speakerEndpoint = (provider: NarrationProvider): string | undefined =>
182
+ provider.kind === "openai" ? openaiEndpoint(provider) : undefined;
183
+
147
184
  /**
148
185
  * A clip's cache key: a digest of everything that decides how it sounds. The
149
186
  * Blume version is deliberately left out, unlike the OG card's: an upgrade
150
- * shouldn't pay to regenerate audio that would sound the same.
187
+ * shouldn't pay to regenerate audio that would sound the same. A build
188
+ * resolves `endpoint` once and passes it in, rather than reading the
189
+ * environment for every sentence.
151
190
  */
152
191
  export const clipKey = (
153
192
  provider: NarrationProvider,
154
193
  lang: string,
155
- text: string
194
+ text: string,
195
+ endpoint = speakerEndpoint(provider)
156
196
  ): string => {
157
197
  const { instructions, model, providerOptions, voice } = provider.options;
198
+ const gateway = provider.kind === "gateway";
158
199
  return createHash("sha256")
159
200
  .update(
160
201
  JSON.stringify({
202
+ // Which server speaks: none for `gateway()`, which JSON drops, so its
203
+ // clips keep the keys they had before narration took other providers.
204
+ endpoint: gateway
205
+ ? undefined
206
+ : { baseUrl: endpoint, kind: provider.kind },
161
207
  format: CLIP_FORMAT,
162
208
  instructions,
163
- lang: speechLanguage(lang),
209
+ // Only the gateway is sent a language (`openaiSpeaker` sends none), so
210
+ // an `openai()` sentence sounds the same in every locale and is
211
+ // generated once.
212
+ lang: gateway ? speechLanguage(lang) : undefined,
164
213
  model,
165
214
  providerOptions,
166
215
  text,
@@ -172,7 +221,7 @@ export const clipKey = (
172
221
  };
173
222
 
174
223
  /** Speak through the Vercel AI Gateway with the provider's model and voice. */
175
- export const gatewaySpeaker = (provider: NarrationProvider): Speak => {
224
+ export const gatewaySpeaker = (provider: GatewayNarrationProvider): Speak => {
176
225
  const { apiKeyEnv, headers, instructions, model, providerOptions, voice } =
177
226
  provider.options;
178
227
  const gateway = createGateway({ apiKey: process.env[apiKeyEnv], headers });
@@ -190,6 +239,104 @@ export const gatewaySpeaker = (provider: NarrationProvider): Speak => {
190
239
  };
191
240
  };
192
241
 
242
+ /**
243
+ * `fetch` for a keyless request: `createOpenAI` always sends the key as a
244
+ * bearer token, so an empty one would arrive as a bare `Bearer` that servers
245
+ * and proxies checking auth can reject. An `Authorization` from `headers`
246
+ * stays.
247
+ */
248
+ const withoutEmptyBearer = async (
249
+ input: Parameters<typeof fetch>[0],
250
+ init?: RequestInit
251
+ ): Promise<Response> => {
252
+ const headers = new Headers(init?.headers);
253
+ if (headers.get("authorization") === "Bearer") {
254
+ headers.delete("authorization");
255
+ }
256
+ return await fetch(input, { ...init, headers });
257
+ };
258
+
259
+ /**
260
+ * Speak through OpenAI's speech API, or with `baseUrl` through any server
261
+ * that serves it (`POST /audio/speech`): Kokoro-FastAPI, Speaches, LiteLLM.
262
+ * That API takes no language, so none is sent and the model or voice
263
+ * decides it. Without a key, no bearer token is sent (an `Authorization` in
264
+ * `headers` still is). A build resolves `baseURL` once and passes it in.
265
+ */
266
+ export const openaiSpeaker = (
267
+ provider: OpenAINarrationProvider,
268
+ baseURL = openaiEndpoint(provider)
269
+ ): Speak => {
270
+ const { apiKeyEnv, headers, instructions, model, providerOptions, voice } =
271
+ provider.options;
272
+ // Made on the first clip rather than up front, so what can go wrong here
273
+ // fails that clip and the build carries on with browser voices: an ejected
274
+ // app's `astro build` runs no dependency preflight, and `OPENAI_BASE_URL`
275
+ // isn't validated the way `baseUrl` is.
276
+ const connect = (): OpenAIProvider => {
277
+ if (hasCredentials(baseURL)) {
278
+ throw new Error(
279
+ "OPENAI_BASE_URL has credentials in it, which fetch won't send: name the env var holding the key in apiKeyEnv instead"
280
+ );
281
+ }
282
+ // `require`, not `import()`: this runs in `astro:build:done` (see
283
+ // `core/node-require.ts`).
284
+ const { createOpenAI }: typeof OpenAISdk = nodeRequire(OPENAI_SPEECH_DEP);
285
+ const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;
286
+ return createOpenAI({
287
+ // Never undefined: the SDK would fall back to OPENAI_API_KEY and send
288
+ // that to a custom endpoint that names no key.
289
+ apiKey: apiKey ?? "",
290
+ baseURL,
291
+ // SAFETY: the SDK only calls it, so Bun's `fetch.preconnect` (which its
292
+ // `typeof fetch` adds) is never read.
293
+ fetch: apiKey ? undefined : (withoutEmptyBearer as typeof fetch),
294
+ headers,
295
+ });
296
+ };
297
+ let openai: OpenAIProvider | undefined;
298
+ return async (text) => {
299
+ openai ??= connect();
300
+ const { audio, responses } = await generateSpeech({
301
+ instructions,
302
+ // SAFETY: both sides are the same `SpeechModelV4` spec. `ai` and
303
+ // `@ai-sdk/openai` each pin their own `@ai-sdk/provider`, and the type
304
+ // only differs between those copies, not in what `generateSpeech` calls.
305
+ model: openai.speech(model) as SpeechModel,
306
+ outputFormat: "mp3",
307
+ providerOptions,
308
+ text,
309
+ voice,
310
+ });
311
+ // The SDK takes any 2xx body as audio, and the clip is cached and served
312
+ // as an MP3 whatever it holds, so a page a redirect led to (an SSO
313
+ // proxy's sign-in, say) or WAV from a server that ignores
314
+ // `response_format` would be reused on every build after: fail the clip
315
+ // unless its bytes are MP3.
316
+ if (audio.mediaType !== MP3_MEDIA_TYPE) {
317
+ const type = responses[0]?.headers?.["content-type"] ?? "no content type";
318
+ throw new Error(`The speech server answered with ${type}, not MP3 audio`);
319
+ }
320
+ return audio.uint8Array;
321
+ };
322
+ };
323
+
324
+ /**
325
+ * The env var a build needs to generate with `provider` and doesn't have, or
326
+ * `null` when it can go ahead: the key is set, Vercel's OIDC token stands in
327
+ * for the gateway's, or the provider names no key (`openai()` with a
328
+ * `baseUrl` and no `apiKeyEnv`).
329
+ */
330
+ export const missingKey = (provider: NarrationProvider): string | null => {
331
+ const env = provider.options.apiKeyEnv;
332
+ if (env === undefined || process.env[env]) {
333
+ return null;
334
+ }
335
+ return provider.kind === "gateway" && process.env.VERCEL_OIDC_TOKEN
336
+ ? null
337
+ : env;
338
+ };
339
+
193
340
  /** Every `.html` file under `dir`. */
194
341
  const listHtml = async (dir: string): Promise<string[]> => {
195
342
  const entries = await readdir(dir, { recursive: true, withFileTypes: true });
@@ -274,35 +421,23 @@ const manifestFor = (plan: PagePlan): NarrationManifest => ({
274
421
  version: 1,
275
422
  });
276
423
 
424
+ /** What a build narrates: each page's plan, and every clip by its key. */
425
+ interface SitePlan {
426
+ /** Where the clips are published, or `null` with no narratable page. */
427
+ audioDir: string | null;
428
+ plans: PagePlan[];
429
+ texts: Map<string, { lang: string; text: string }>;
430
+ }
431
+
277
432
  /**
278
- * Generate the narration for a built site in `distDir`: a clip per sentence
279
- * and a manifest per page. Does nothing unless `narration.provider` is set,
280
- * and warns instead of generating when the provider's key is missing (the
281
- * pages then read with browser voices). `speak` replaces the gateway call.
433
+ * Read every built page that carries the player and plan its narration,
434
+ * keying each sentence's clip with `keyFor`.
282
435
  */
283
- export const buildNarration = async (
284
- project: BlumeProject,
436
+ const planSite = async (
285
437
  distDir: string,
286
- logger: NarrationLogger,
287
- speak?: Speak
288
- ): Promise<NarrationBuildResult | null> => {
289
- const { narration } = project.config;
290
- const { provider } = narration;
291
- if (!(narration.enabled && provider)) {
292
- return null;
293
- }
294
- const env = provider.options.apiKeyEnv;
295
- if (!(speak || process.env[env] || process.env.VERCEL_OIDC_TOKEN)) {
296
- logger.warn(
297
- `Narration audio was not generated: ${env} is not set, so pages will be read with browser voices.`
298
- );
299
- return null;
300
- }
301
- const say = speak ?? gatewaySpeaker(provider);
302
- const base = normalizeBasePath(project.config.deployment.options.base);
303
-
304
- // Read every page first, so the log can say what generation will cost
305
- // before any of it is spent.
438
+ base: string,
439
+ keyFor: (lang: string, text: string) => string
440
+ ): Promise<SitePlan> => {
306
441
  const plans: PagePlan[] = [];
307
442
  const texts = new Map<string, { lang: string; text: string }>();
308
443
  let audioDir: string | null = null;
@@ -322,7 +457,7 @@ export const buildNarration = async (
322
457
  audioDir ??= distPath(distDir, base, page.audioBase);
323
458
  const segments = segmentNarration(extract, page.lang);
324
459
  const keys = segments.map((segment) => {
325
- const key = clipKey(provider, page.lang, segment.text);
460
+ const key = keyFor(page.lang, segment.text);
326
461
  texts.set(key, { lang: page.lang, text: segment.text });
327
462
  return key;
328
463
  });
@@ -333,6 +468,37 @@ export const buildNarration = async (
333
468
  segments,
334
469
  });
335
470
  }
471
+ return { audioDir, plans, texts };
472
+ };
473
+
474
+ /**
475
+ * Generate the narration for a built site in `distDir`: a clip per sentence
476
+ * and a manifest per page. Does nothing unless `narration.provider` is set.
477
+ * When new clips are needed and the provider's key is missing, it warns and
478
+ * generates nothing, so only pages whose clips are all cached get audio (the
479
+ * rest read with browser voices). `speak` replaces the provider call.
480
+ */
481
+ export const buildNarration = async (
482
+ project: BlumeProject,
483
+ distDir: string,
484
+ logger: NarrationLogger,
485
+ speak?: Speak
486
+ ): Promise<NarrationBuildResult | null> => {
487
+ const { narration } = project.config;
488
+ const { provider } = narration;
489
+ if (!(narration.enabled && provider)) {
490
+ return null;
491
+ }
492
+ const endpoint = speakerEndpoint(provider);
493
+ const base = normalizeBasePath(project.config.deployment.options.base);
494
+
495
+ // Read every page first, so the log can say what generation will cost
496
+ // before any of it is spent.
497
+ const { audioDir, plans, texts } = await planSite(
498
+ distDir,
499
+ base,
500
+ (lang, text) => clipKey(provider, lang, text, endpoint)
501
+ );
336
502
  if (!audioDir || plans.length === 0) {
337
503
  return { failed: 0, generated: 0, pages: 0, reused: 0 };
338
504
  }
@@ -341,7 +507,14 @@ export const buildNarration = async (
341
507
  const cached = (key: string) => join(cacheDir, `${key}.mp3`);
342
508
  const missing = [...texts.keys()].filter((key) => !existsSync(cached(key)));
343
509
  const reused = texts.size - missing.length;
344
- if (missing.length > 0) {
510
+ // Checked only once something needs generating, so a build without the key
511
+ // (a fork's pull request, say) still ships every clip the cache restored.
512
+ const env = speak || missing.length === 0 ? null : missingKey(provider);
513
+ if (env) {
514
+ logger.warn(
515
+ `Narration audio was not generated: ${env} is not set, so pages missing clips will be read with browser voices.`
516
+ );
517
+ } else if (missing.length > 0) {
345
518
  const chars = missing.reduce(
346
519
  (total, key) => total + (texts.get(key)?.text.length ?? 0),
347
520
  0
@@ -350,13 +523,18 @@ export const buildNarration = async (
350
523
  `Generating narration: ${missing.length} new clip(s), ${chars.toLocaleString("en-US")} characters, with ${provider.options.model}`
351
524
  );
352
525
  }
526
+ const say =
527
+ speak ??
528
+ (provider.kind === "gateway"
529
+ ? gatewaySpeaker(provider)
530
+ : openaiSpeaker(provider, endpoint));
353
531
 
354
532
  // The AI SDK already retries a transient failure, so one that gets through
355
533
  // (a bad key, an unknown model, a spent quota) would fail every clip after
356
534
  // it too: stop generating at the first, and keep what's already cached.
357
- const failures = new Set<string>();
535
+ const failures = new Set(env ? missing : []);
358
536
  let firstError: string | null = null;
359
- await eachLimited(missing, CONCURRENCY, async (key) => {
537
+ await eachLimited(env ? [] : missing, CONCURRENCY, async (key) => {
360
538
  const entry = texts.get(key);
361
539
  if (!entry || firstError !== null) {
362
540
  failures.add(key);