blume 0.0.0 → 0.1.0

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 (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,131 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+
4
+ import matter from "gray-matter";
5
+ import { join } from "pathe";
6
+ import { glob } from "tinyglobby";
7
+
8
+ import { writeBlumeConfig } from "../shared.ts";
9
+ import { loadStarlightConfig, mapStarlightConfig } from "./config.ts";
10
+ import {
11
+ hasAliasedAssets,
12
+ rewriteStarlightAsides,
13
+ rewriteStarlightComponents,
14
+ stripStarlightImports,
15
+ unsupportedStarlightComponents,
16
+ } from "./content.ts";
17
+ import { normalizeStarlightPageMeta } from "./frontmatter.ts";
18
+ import { starlightI18n } from "./i18n.ts";
19
+
20
+ export interface StarlightMigrationResult {
21
+ moved: number;
22
+ warnings: string[];
23
+ }
24
+
25
+ /** Starlight always keeps its docs collection here. */
26
+ const CONTENT_DIR = "src/content/docs";
27
+
28
+ interface PageResult {
29
+ aliased: boolean;
30
+ removed: string[];
31
+ unsupported: string[];
32
+ }
33
+
34
+ /** Rewrite one Starlight page to idiomatic Blume MDX, in place. */
35
+ const transformPage = async (file: string): Promise<PageResult> => {
36
+ const raw = await readFile(file, "utf-8");
37
+ let text = stripStarlightImports(raw);
38
+ text = rewriteStarlightAsides(text);
39
+ text = rewriteStarlightComponents(text);
40
+ const unsupported = unsupportedStarlightComponents(text);
41
+ const aliased = hasAliasedAssets(text);
42
+
43
+ const parsed = matter(text);
44
+ const { data, removed } = normalizeStarlightPageMeta(parsed.data);
45
+ const content =
46
+ Object.keys(data).length > 0
47
+ ? matter.stringify(parsed.content, data)
48
+ : parsed.content;
49
+ if (content !== raw) {
50
+ await writeFile(file, content, "utf-8");
51
+ }
52
+ return { aliased, removed, unsupported };
53
+ };
54
+
55
+ /**
56
+ * Migrate a Starlight project to Blume: translate the `starlight({...})` options
57
+ * out of `astro.config.*` into `blume.config.ts`, then rewrite each page under
58
+ * `src/content/docs` to idiomatic Blume MDX in place (asides → directives,
59
+ * component renames, frontmatter mapping). Content stays put — the generated
60
+ * config points `content.root` at `src/content/docs`.
61
+ */
62
+ export const migrateStarlightProject = async (
63
+ root: string
64
+ ): Promise<StarlightMigrationResult> => {
65
+ const { options, warnings } = await loadStarlightConfig(root);
66
+ const config = mapStarlightConfig(options, warnings);
67
+
68
+ const i18n = starlightI18n(options);
69
+ if (i18n) {
70
+ config.i18n = i18n;
71
+ warnings.push(
72
+ `Mapped ${i18n.locales.length} locale(s) to i18n (default: ${i18n.defaultLocale}); review the locale labels.`
73
+ );
74
+ }
75
+
76
+ const base = join(root, CONTENT_DIR);
77
+ if (!existsSync(base)) {
78
+ await writeBlumeConfig(root, config);
79
+ return {
80
+ moved: 0,
81
+ warnings: [
82
+ ...warnings,
83
+ `Content directory ${CONTENT_DIR} not found; wrote blume.config.ts only.`,
84
+ ],
85
+ };
86
+ }
87
+
88
+ const files = await glob(["**/*.{md,mdx}"], { absolute: true, cwd: base });
89
+ const results = await Promise.all(files.map(transformPage));
90
+
91
+ await writeBlumeConfig(root, config);
92
+
93
+ const removedKeys = new Set<string>();
94
+ const unsupported = new Set<string>();
95
+ let aliasedAssets = false;
96
+ for (const result of results) {
97
+ for (const key of result.removed) {
98
+ removedKeys.add(key);
99
+ }
100
+ for (const name of result.unsupported) {
101
+ unsupported.add(name);
102
+ }
103
+ aliasedAssets ||= result.aliased;
104
+ }
105
+
106
+ if (removedKeys.size > 0) {
107
+ warnings.push(
108
+ `Dropped unsupported page frontmatter keys: ${[...removedKeys].join(", ")}.`
109
+ );
110
+ if (removedKeys.has("hero") || removedKeys.has("template")) {
111
+ warnings.push(
112
+ "Pages using `template: splash` / `hero` have no automatic equivalent — rebuild them as custom pages under `pages/`."
113
+ );
114
+ }
115
+ }
116
+ if (unsupported.size > 0) {
117
+ warnings.push(
118
+ `Components without a drop-in Blume equivalent need manual review: ${[...unsupported].join(", ")}.`
119
+ );
120
+ }
121
+ if (aliasedAssets) {
122
+ warnings.push(
123
+ "Some pages reference images via `~/`/`@/` aliases; rewrite them to relative or /public paths."
124
+ );
125
+ }
126
+ warnings.push(
127
+ "Content stays under src/content/docs; the now-unused astro.config.*, src/content.config.ts, and @astrojs/starlight dependency are safe to remove."
128
+ );
129
+
130
+ return { moved: results.length, warnings };
131
+ };
package/src/og/card.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { Renderer } from "@takumi-rs/core";
2
+ import { container, text } from "@takumi-rs/helpers";
3
+
4
+ // Reuse one renderer (and its loaded default fonts) across all images.
5
+ let renderer: Renderer | null = null;
6
+ const getRenderer = (): Renderer => {
7
+ renderer ??= new Renderer();
8
+ return renderer;
9
+ };
10
+
11
+ const ACCENT_HEX: Record<string, string> = {
12
+ blue: "#3b82f6",
13
+ green: "#22c55e",
14
+ orange: "#f97316",
15
+ pink: "#ec4899",
16
+ purple: "#8b5cf6",
17
+ red: "#ef4444",
18
+ teal: "#14b8a6",
19
+ };
20
+
21
+ // OG rendering uses hex (Takumi's color parser does not accept oklch); named
22
+ // presets map to hex, raw hex passes through, anything else falls back.
23
+ const resolveAccent = (accent: string): string =>
24
+ ACCENT_HEX[accent] ?? (accent.startsWith("#") ? accent : "#3b82f6");
25
+
26
+ export interface OgCardOptions {
27
+ title: string;
28
+ eyebrow?: string;
29
+ accent?: string;
30
+ }
31
+
32
+ const WIDTH = 1200;
33
+ const HEIGHT = 630;
34
+
35
+ /** Render a 1200x630 Open Graph card to a PNG buffer. */
36
+ export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
37
+ const accent = resolveAccent(options.accent ?? "blue");
38
+
39
+ const node = container({
40
+ children: [
41
+ container({
42
+ children: [
43
+ container({
44
+ style: {
45
+ backgroundColor: accent,
46
+ borderRadius: 6,
47
+ height: 32,
48
+ width: 32,
49
+ },
50
+ }),
51
+ options.eyebrow
52
+ ? text(options.eyebrow, {
53
+ color: "#94a3b8",
54
+ fontSize: 30,
55
+ })
56
+ : container({}),
57
+ ],
58
+ style: { alignItems: "center", display: "flex", gap: 16 },
59
+ }),
60
+ text(options.title, {
61
+ color: "#f8fafc",
62
+ fontSize: 76,
63
+ fontWeight: 700,
64
+ lineHeight: 1.1,
65
+ }),
66
+ container({
67
+ style: {
68
+ backgroundColor: accent,
69
+ borderRadius: 4,
70
+ height: 8,
71
+ width: 120,
72
+ },
73
+ }),
74
+ ],
75
+ style: {
76
+ backgroundColor: "#0b1020",
77
+ color: "#ffffff",
78
+ display: "flex",
79
+ flexDirection: "column",
80
+ height: HEIGHT,
81
+ justifyContent: "space-between",
82
+ padding: 80,
83
+ width: WIDTH,
84
+ },
85
+ });
86
+
87
+ return getRenderer().render(node, {
88
+ format: "png",
89
+ height: HEIGHT,
90
+ width: WIDTH,
91
+ });
92
+ };
@@ -0,0 +1,2 @@
1
+ export { renderOgImage } from "./card.ts";
2
+ export type { OgCardOptions } from "./card.ts";
@@ -0,0 +1,246 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { isAbsolute, join } from "pathe";
4
+
5
+ import { scalarReferenceTemplate } from "../astro/templates.ts";
6
+ import type { ResolvedConfig } from "../core/schema.ts";
7
+ import type { NavTab } from "../core/types.ts";
8
+ import { resolveAccent, resolveRadius } from "../theme/palette.ts";
9
+
10
+ /**
11
+ * API reference support, delegated wholesale to Scalar (`@scalar/astro`). Blume
12
+ * resolves the configured spec sources into routes, generates one self-contained
13
+ * Scalar page per source, and adds a nav tab. OpenAPI and AsyncAPI share this
14
+ * exact path — Scalar auto-detects the document type — so the only difference
15
+ * between the two config blocks is their default route and nav label.
16
+ */
17
+
18
+ type ReferenceKind = "openapi" | "asyncapi";
19
+
20
+ /** A spec source resolved to a concrete route and nav label. */
21
+ export interface ReferenceSource {
22
+ kind: ReferenceKind;
23
+ /** Normalized route the reference mounts at, e.g. `/reference`. */
24
+ route: string;
25
+ label: string;
26
+ /** Local path or `http(s)` URL, verbatim from config. */
27
+ spec: string;
28
+ /** Per-block Scalar theme name override, if any. */
29
+ theme?: string;
30
+ }
31
+
32
+ /** A generated reference page, ready to write under `src/pages`. */
33
+ export interface ReferenceFile {
34
+ /** Path relative to `src/pages`, e.g. `reference.astro`, `api/events.astro`. */
35
+ pagePath: string;
36
+ content: string;
37
+ }
38
+
39
+ const URL_SPEC = /^https?:\/\//u;
40
+ const NON_SLUG = /[^a-z0-9]+/gu;
41
+ const SLUG_EDGES = /^-+|-+$/gu;
42
+ const ROUTE_EDGES = /^\/+|\/+$/gu;
43
+ const TRAILING_SLASH = /\/+$/u;
44
+
45
+ const slugify = (text: string): string =>
46
+ text.toLowerCase().replace(NON_SLUG, "-").replace(SLUG_EDGES, "");
47
+
48
+ /** Normalize a configured route to a single leading slash, no trailing slash. */
49
+ const normalizeRoute = (route: string): string => {
50
+ const trimmed = route.trim();
51
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
52
+ const noTrailing = withSlash.replace(TRAILING_SLASH, "");
53
+ return noTrailing === "" ? "/" : noTrailing;
54
+ };
55
+
56
+ /** The `src/pages`-relative file path for a reference route. */
57
+ const referencePagePath = (route: string): string => {
58
+ const segments = route.replace(ROUTE_EDGES, "");
59
+ return `${segments === "" ? "index" : segments}.astro`;
60
+ };
61
+
62
+ /** A spec is a single source (`spec` shorthand prepended to any `sources`). */
63
+ type Block = ResolvedConfig["openapi"] | ResolvedConfig["asyncapi"];
64
+
65
+ const sourcesOf = (
66
+ block: Block
67
+ ): { label?: string; route?: string; spec: string }[] => {
68
+ const sources = [...block.sources];
69
+ if (block.spec) {
70
+ sources.unshift({ spec: block.spec });
71
+ }
72
+ return sources;
73
+ };
74
+
75
+ const referencesFor = (
76
+ kind: ReferenceKind,
77
+ block: Block,
78
+ defaultLabel: string
79
+ ): ReferenceSource[] => {
80
+ if (!block.enabled) {
81
+ return [];
82
+ }
83
+ const sources = sourcesOf(block);
84
+ const base = normalizeRoute(block.route);
85
+
86
+ return sources.map((source, index) => {
87
+ const label =
88
+ source.label ??
89
+ (sources.length > 1 ? `${defaultLabel} ${index + 1}` : defaultLabel);
90
+
91
+ let route: string;
92
+ if (source.route) {
93
+ route = normalizeRoute(source.route);
94
+ } else if (sources.length === 1) {
95
+ route = base;
96
+ } else {
97
+ const suffix = source.label ? slugify(source.label) : "";
98
+ route = normalizeRoute(`${base}/${suffix || index + 1}`);
99
+ }
100
+
101
+ return { kind, label, route, spec: source.spec, theme: block.theme };
102
+ });
103
+ };
104
+
105
+ /**
106
+ * Resolve every enabled reference source into its route and label. Pure (no file
107
+ * IO), so the nav and the page generator stay in sync from one source of truth.
108
+ */
109
+ export const resolveReferences = (
110
+ config: ResolvedConfig
111
+ ): ReferenceSource[] => [
112
+ ...referencesFor("openapi", config.openapi, "API Reference"),
113
+ ...referencesFor("asyncapi", config.asyncapi, "Events"),
114
+ ];
115
+
116
+ /** Nav tabs (header links) for the configured references. */
117
+ export const referenceTabs = (config: ResolvedConfig): NavTab[] =>
118
+ resolveReferences(config).map((ref) => ({
119
+ label: ref.label,
120
+ path: ref.route,
121
+ }));
122
+
123
+ /** Whether any reference block is enabled (gates dependency + page wiring). */
124
+ export const hasReferences = (config: ResolvedConfig): boolean =>
125
+ config.openapi.enabled || config.asyncapi.enabled;
126
+
127
+ const darkModeConfig = (
128
+ mode: ResolvedConfig["theme"]["mode"]
129
+ ): Record<string, boolean> => {
130
+ if (mode === "dark") {
131
+ return { darkMode: true };
132
+ }
133
+ if (mode === "light") {
134
+ return { darkMode: false };
135
+ }
136
+ // "system": leave Scalar to follow the OS preference.
137
+ return {};
138
+ };
139
+
140
+ /**
141
+ * Map Blume's theme onto Scalar's. An explicit `theme` name wins; otherwise we
142
+ * keep Scalar's default theme and layer Blume's accent/radius on top via
143
+ * `customCss`. Scalar re-injects `customCss` after its bundled theme, so these
144
+ * variables reliably override the defaults. Best-effort, not pixel-exact.
145
+ */
146
+ const themeConfiguration = (
147
+ config: ResolvedConfig,
148
+ override?: string
149
+ ): Record<string, unknown> => {
150
+ if (override) {
151
+ return { theme: override };
152
+ }
153
+ const accent = resolveAccent(config.theme);
154
+ const radius = resolveRadius(config.theme);
155
+ return {
156
+ customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent};--scalar-radius:${radius};}`,
157
+ ...darkModeConfig(config.theme.mode),
158
+ };
159
+ };
160
+
161
+ /** Build the Scalar spec config for a source: inline `content` or remote `url`. */
162
+ const specConfiguration = async (
163
+ spec: string,
164
+ root: string
165
+ ): Promise<{ config: Record<string, unknown>; warning?: string }> => {
166
+ if (URL_SPEC.test(spec)) {
167
+ return { config: { url: spec } };
168
+ }
169
+ // A local spec is read at generate time and inlined as `content`, so the page
170
+ // stays self-contained and nothing is copied into the user's source tree.
171
+ const absolute = isAbsolute(spec) ? spec : join(root, spec);
172
+ try {
173
+ return { config: { content: await readFile(absolute, "utf-8") } };
174
+ } catch {
175
+ return {
176
+ config: { url: spec },
177
+ warning: `API reference spec not found: "${spec}" (looked in ${absolute}).`,
178
+ };
179
+ }
180
+ };
181
+
182
+ /**
183
+ * Build the Scalar reference page(s) for the project. Reads local specs, maps
184
+ * the theme, and skips routes that collide with a content page or another
185
+ * source. Returns the files to write under `src/pages` plus any warnings.
186
+ */
187
+ export const buildReferenceFiles = async (options: {
188
+ config: ResolvedConfig;
189
+ root: string;
190
+ contentRoutes: ReadonlySet<string>;
191
+ }): Promise<{ files: ReferenceFile[]; warnings: string[] }> => {
192
+ const { config, root, contentRoutes } = options;
193
+ const warnings: string[] = [];
194
+
195
+ // Resolve and dedupe routes first (sync), then read every spec in parallel.
196
+ const seen = new Set<string>();
197
+ const accepted: ReferenceSource[] = [];
198
+ for (const ref of resolveReferences(config)) {
199
+ if (seen.has(ref.route)) {
200
+ warnings.push(
201
+ `Two API reference sources resolve to ${ref.route}; keeping the first.`
202
+ );
203
+ continue;
204
+ }
205
+ if (contentRoutes.has(ref.route)) {
206
+ warnings.push(
207
+ `API reference route ${ref.route} collides with a content page; skipping the reference there.`
208
+ );
209
+ continue;
210
+ }
211
+ seen.add(ref.route);
212
+ accepted.push(ref);
213
+ }
214
+
215
+ const built = await Promise.all(
216
+ accepted.map(async (ref) => ({
217
+ ref,
218
+ spec: await specConfiguration(ref.spec, root),
219
+ }))
220
+ );
221
+
222
+ const files: ReferenceFile[] = [];
223
+ for (const { ref, spec } of built) {
224
+ if (spec.warning) {
225
+ warnings.push(spec.warning);
226
+ }
227
+ const pagePath = referencePagePath(ref.route);
228
+ // Relative path from the page back to src/generated/data.json: a page one
229
+ // directory deep (api/events.astro) needs an extra "../".
230
+ const depth = pagePath.split("/").length - 1;
231
+ files.push({
232
+ content: scalarReferenceTemplate({
233
+ configuration: {
234
+ ...spec.config,
235
+ ...themeConfiguration(config, ref.theme),
236
+ },
237
+ dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
238
+ route: ref.route,
239
+ title: ref.label,
240
+ }),
241
+ pagePath,
242
+ });
243
+ }
244
+
245
+ return { files, warnings };
246
+ };