blume 0.1.0 → 0.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 (106) hide show
  1. package/dist/cli/index.js +1213 -473
  2. package/dist/cli/index.js.map +33 -29
  3. package/dist/types/core/bridge.d.ts +24 -0
  4. package/dist/types/core/config.d.ts +35 -0
  5. package/dist/types/core/data.d.ts +129 -0
  6. package/dist/types/core/define-components.d.ts +27 -0
  7. package/dist/types/core/define-meta.d.ts +16 -0
  8. package/dist/types/core/deployment-env.d.ts +9 -0
  9. package/dist/types/core/diagnostics.d.ts +17 -0
  10. package/dist/types/core/i18n-ui.d.ts +500 -0
  11. package/dist/types/core/load-module.d.ts +7 -0
  12. package/dist/types/core/package-root.d.ts +17 -0
  13. package/dist/types/core/project.d.ts +9 -0
  14. package/dist/types/core/schema.d.ts +3461 -0
  15. package/dist/types/core/sources/types.d.ts +107 -0
  16. package/dist/types/core/types.d.ts +245 -0
  17. package/dist/types/core/ui-packs/ar.d.ts +3 -0
  18. package/dist/types/core/ui-packs/bg.d.ts +3 -0
  19. package/dist/types/core/ui-packs/bn.d.ts +3 -0
  20. package/dist/types/core/ui-packs/ca.d.ts +3 -0
  21. package/dist/types/core/ui-packs/cs.d.ts +3 -0
  22. package/dist/types/core/ui-packs/da.d.ts +3 -0
  23. package/dist/types/core/ui-packs/de.d.ts +3 -0
  24. package/dist/types/core/ui-packs/el.d.ts +3 -0
  25. package/dist/types/core/ui-packs/es.d.ts +3 -0
  26. package/dist/types/core/ui-packs/fa.d.ts +3 -0
  27. package/dist/types/core/ui-packs/fi.d.ts +3 -0
  28. package/dist/types/core/ui-packs/fr.d.ts +3 -0
  29. package/dist/types/core/ui-packs/he.d.ts +3 -0
  30. package/dist/types/core/ui-packs/hi.d.ts +3 -0
  31. package/dist/types/core/ui-packs/hr.d.ts +3 -0
  32. package/dist/types/core/ui-packs/hu.d.ts +3 -0
  33. package/dist/types/core/ui-packs/id.d.ts +3 -0
  34. package/dist/types/core/ui-packs/index.d.ts +13 -0
  35. package/dist/types/core/ui-packs/it.d.ts +3 -0
  36. package/dist/types/core/ui-packs/ja.d.ts +3 -0
  37. package/dist/types/core/ui-packs/ko.d.ts +3 -0
  38. package/dist/types/core/ui-packs/nl.d.ts +3 -0
  39. package/dist/types/core/ui-packs/no.d.ts +3 -0
  40. package/dist/types/core/ui-packs/pl.d.ts +3 -0
  41. package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
  42. package/dist/types/core/ui-packs/pt.d.ts +3 -0
  43. package/dist/types/core/ui-packs/ro.d.ts +3 -0
  44. package/dist/types/core/ui-packs/ru.d.ts +3 -0
  45. package/dist/types/core/ui-packs/sk.d.ts +3 -0
  46. package/dist/types/core/ui-packs/sr.d.ts +3 -0
  47. package/dist/types/core/ui-packs/sv.d.ts +3 -0
  48. package/dist/types/core/ui-packs/th.d.ts +3 -0
  49. package/dist/types/core/ui-packs/tr.d.ts +3 -0
  50. package/dist/types/core/ui-packs/uk.d.ts +3 -0
  51. package/dist/types/core/ui-packs/vi.d.ts +3 -0
  52. package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
  53. package/dist/types/core/ui-packs/zh.d.ts +3 -0
  54. package/dist/types/core/version.d.ts +8 -0
  55. package/dist/types/index.d.ts +10 -0
  56. package/dist/types/migrate/mintlify/config.d.ts +2 -0
  57. package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
  58. package/dist/types/theme/fonts.d.ts +163 -0
  59. package/docs/advanced/custom-pages.mdx +91 -10
  60. package/docs/configuration/index.mdx +19 -0
  61. package/docs/content/components.mdx +103 -1
  62. package/package.json +13 -3
  63. package/src/ai/llms.ts +1 -2
  64. package/src/astro/examples.ts +106 -0
  65. package/src/astro/generate.ts +300 -75
  66. package/src/astro/index.ts +1 -1
  67. package/src/astro/islands.ts +1 -1
  68. package/src/astro/pages.ts +59 -0
  69. package/src/astro/templates.ts +180 -22
  70. package/src/cli/commands/validate.ts +1 -0
  71. package/src/components/content/CodeBlock.astro +28 -0
  72. package/src/components/content/Component.astro +65 -0
  73. package/src/components/content/Diff.astro +44 -0
  74. package/src/components/content/Tab.astro +8 -2
  75. package/src/components/content/diff.ts +95 -0
  76. package/src/components/layout/Favicon.astro +11 -4
  77. package/src/components/layout/PageLayout.astro +173 -0
  78. package/src/components/layout/ReferenceLayout.astro +6 -1
  79. package/src/components/layout/RootLayout.astro +9 -5
  80. package/src/components/layout/head-scripts.ts +19 -0
  81. package/src/core/data.ts +128 -0
  82. package/src/core/frontmatter.ts +43 -0
  83. package/src/core/links.ts +26 -10
  84. package/src/core/schema.ts +8 -0
  85. package/src/core/sources/filesystem.ts +1 -1
  86. package/src/core/sources/mdx-remote.ts +1 -2
  87. package/src/core/sources/mintlify.ts +1 -1
  88. package/src/core/sources/normalize.ts +19 -3
  89. package/src/core/sources/notion.ts +1 -1
  90. package/src/core/sources/sanity.ts +1 -1
  91. package/src/core/tsconfig-aliases.ts +201 -0
  92. package/src/index.ts +12 -0
  93. package/src/markdown/index.ts +69 -0
  94. package/src/migrate/fumadocs/config.ts +53 -4
  95. package/src/migrate/fumadocs/content.ts +1 -1
  96. package/src/migrate/fumadocs/groups.ts +230 -0
  97. package/src/migrate/fumadocs/index.ts +104 -19
  98. package/src/migrate/fumadocs/meta.ts +140 -10
  99. package/src/migrate/mintlify/snippets.ts +2 -1
  100. package/src/migrate/mintlify/transform.ts +1 -2
  101. package/src/migrate/nextra/index.ts +1 -1
  102. package/src/migrate/shared.ts +98 -1
  103. package/src/migrate/starlight/index.ts +1 -1
  104. package/src/registry/eject.ts +57 -10
  105. package/src/search/documents.ts +1 -2
  106. package/src/theme/entry.ts +29 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Curated Google Fonts exposed through `theme.fonts`.
3
+ *
4
+ * Each slug maps to the official Google family name (Astro's font provider needs
5
+ * the exact name), a docs-appropriate set of weights, and a fallback category.
6
+ * Fonts are self-hosted and optimized by Astro's built-in Fonts API; this module
7
+ * only resolves config slugs into the data that drives it.
8
+ */
9
+ export type FontCategory = "sans" | "serif" | "mono";
10
+ /** The three configurable roles in `theme.fonts`. */
11
+ export type FontSlot = "display" | "body" | "mono";
12
+ /** Resolved theme fonts (a validated slug per role, all optional). */
13
+ export type FontsConfig = Partial<Record<FontSlot, string>> | undefined;
14
+ /** A single Astro `fonts:` entry (sans the literal `fontProviders.google()`). */
15
+ export interface FontEntry {
16
+ cssVariable: string;
17
+ fallbacks: string[];
18
+ name: string;
19
+ weights: number[];
20
+ }
21
+ /** Slug -> Google family + weights + fallback category. Keep keys alphabetical. */
22
+ export declare const GOOGLE_FONTS: {
23
+ "dm-sans": {
24
+ category: "sans";
25
+ family: string;
26
+ weights: number[];
27
+ };
28
+ figtree: {
29
+ category: "sans";
30
+ family: string;
31
+ weights: number[];
32
+ };
33
+ "fira-code": {
34
+ category: "mono";
35
+ family: string;
36
+ weights: number[];
37
+ };
38
+ geist: {
39
+ category: "sans";
40
+ family: string;
41
+ weights: number[];
42
+ };
43
+ "geist-mono": {
44
+ category: "mono";
45
+ family: string;
46
+ weights: number[];
47
+ };
48
+ "ibm-plex-mono": {
49
+ category: "mono";
50
+ family: string;
51
+ weights: number[];
52
+ };
53
+ "ibm-plex-sans": {
54
+ category: "sans";
55
+ family: string;
56
+ weights: number[];
57
+ };
58
+ "ibm-plex-serif": {
59
+ category: "serif";
60
+ family: string;
61
+ weights: number[];
62
+ };
63
+ inter: {
64
+ category: "sans";
65
+ family: string;
66
+ weights: number[];
67
+ };
68
+ "inter-tight": {
69
+ category: "sans";
70
+ family: string;
71
+ weights: number[];
72
+ };
73
+ "jetbrains-mono": {
74
+ category: "mono";
75
+ family: string;
76
+ weights: number[];
77
+ };
78
+ lora: {
79
+ category: "serif";
80
+ family: string;
81
+ weights: number[];
82
+ };
83
+ manrope: {
84
+ category: "sans";
85
+ family: string;
86
+ weights: number[];
87
+ };
88
+ merriweather: {
89
+ category: "serif";
90
+ family: string;
91
+ weights: number[];
92
+ };
93
+ "open-sans": {
94
+ category: "sans";
95
+ family: string;
96
+ weights: number[];
97
+ };
98
+ "playfair-display": {
99
+ category: "serif";
100
+ family: string;
101
+ weights: number[];
102
+ };
103
+ "plus-jakarta-sans": {
104
+ category: "sans";
105
+ family: string;
106
+ weights: number[];
107
+ };
108
+ roboto: {
109
+ category: "sans";
110
+ family: string;
111
+ weights: number[];
112
+ };
113
+ "roboto-mono": {
114
+ category: "mono";
115
+ family: string;
116
+ weights: number[];
117
+ };
118
+ "source-code-pro": {
119
+ category: "mono";
120
+ family: string;
121
+ weights: number[];
122
+ };
123
+ "source-sans-3": {
124
+ category: "sans";
125
+ family: string;
126
+ weights: number[];
127
+ };
128
+ "source-serif-4": {
129
+ category: "serif";
130
+ family: string;
131
+ weights: number[];
132
+ };
133
+ "space-grotesk": {
134
+ category: "sans";
135
+ family: string;
136
+ weights: number[];
137
+ };
138
+ "space-mono": {
139
+ category: "mono";
140
+ family: string;
141
+ weights: number[];
142
+ };
143
+ "work-sans": {
144
+ category: "sans";
145
+ family: string;
146
+ weights: number[];
147
+ };
148
+ };
149
+ export type FontSlug = keyof typeof GOOGLE_FONTS;
150
+ /** All supported slugs, for schema validation and error messages. */
151
+ export declare const FONT_SLUGS: string[];
152
+ /** Type guard: is `value` a supported font slug? */
153
+ export declare const isFontSlug: (value: string) => value is FontSlug;
154
+ /** The unique Astro `fonts:` entries for the configured roles (deduped). */
155
+ export declare const buildFontEntries: (fonts: FontsConfig) => FontEntry[];
156
+ /**
157
+ * The config-token CSS that points each role's `--blume-font-<role>-src` at the
158
+ * Astro-populated family variable. Concatenated into the generated entry's
159
+ * config tokens; empty when no fonts are set so defaults stay the system stacks.
160
+ */
161
+ export declare const buildFontsCss: (fonts: FontsConfig) => string;
162
+ /** The CSS variables to feed Astro's `<Font>` component in the document head. */
163
+ export declare const configuredCssVars: (fonts: FontsConfig) => string[];
@@ -69,32 +69,68 @@ import data from "blume:data";
69
69
  </ul>
70
70
  ```
71
71
 
72
+ Inside a Blume project the module is typed automatically. You can also pull the
73
+ shape in explicitly — for typed helpers, props, or your own tsconfig — with
74
+ `import type { BlumeData } from "blume"`:
75
+
76
+ ```ts
77
+ import type { BlumeData, BlumeRoute } from "blume";
78
+
79
+ const indexable = (data: BlumeData): BlumeRoute[] =>
80
+ data.routes.filter((route) => route.indexable);
81
+ ```
82
+
72
83
  The module exposes:
73
84
 
74
85
  <TypeTable
75
86
  type={{
76
87
  config: {
77
- type: "object",
88
+ type: "BlumeDataConfig",
78
89
  required: true,
79
90
  description:
80
- "Resolved site config title, description, logo, banner, theme, site URL, repoUrl, and search settings.",
91
+ "Resolved site settings: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, search, i18n, mcp, og, analytics, feedback, structuredData, codeWrap, and imageZoom.",
81
92
  },
82
93
  navigation: {
83
- type: "object",
94
+ type: "Navigation",
95
+ required: true,
96
+ description:
97
+ "The sidebar, tabs, and selectors inferred from your content (default locale).",
98
+ },
99
+ navigationByLocale: {
100
+ type: "Record<string, Navigation>",
84
101
  required: true,
85
- description: "The sidebar and tab tree inferred from your content.",
102
+ description:
103
+ "Per-locale navigation trees, keyed by locale code. Empty unless i18n is configured.",
86
104
  },
87
105
  routes: {
88
- type: "Route[]",
106
+ type: "BlumeRoute[]",
89
107
  required: true,
90
108
  description:
91
- "Every content page: { id, path, title, indexable, hidden, draft, editUrl }.",
109
+ "Every content page: { id, path, title, locale, indexable, hidden, draft, fallback, editUrl, lastModified, alternates, collection, entryId }.",
92
110
  },
93
111
  feeds: {
94
- type: "Feed[]",
112
+ type: "BlumeFeed[]",
95
113
  required: true,
96
114
  description: "Generated RSS feeds: { href, title }.",
97
115
  },
116
+ fontCssVars: {
117
+ type: "string[]",
118
+ required: true,
119
+ description:
120
+ "CSS variable names for the configured fonts (Astro <Font> integration).",
121
+ },
122
+ ui: {
123
+ type: "UIStrings",
124
+ required: true,
125
+ description:
126
+ "Resolved UI chrome strings for the default locale (search, sidebar, and footer labels).",
127
+ },
128
+ uiByLocale: {
129
+ type: "Record<string, UIStrings>",
130
+ required: true,
131
+ description:
132
+ "Per-locale UI strings, keyed by locale code. Empty unless i18n is configured.",
133
+ },
98
134
  }}
99
135
  />
100
136
 
@@ -133,9 +169,54 @@ const posts = (await getCollection("docs"))
133
169
 
134
170
  ## Using the site layout
135
171
 
136
- To give a custom page the full Blume chrome — header, sidebar, search, and theme
137
- wrap it in `RootLayout`, the same layout the generated pages use. Pull the
138
- required props straight from `blume:data`:
172
+ `RootLayout` gives a custom page the full docs chrome — header, sidebar, search,
173
+ TOC, and theme by wrapping it in the same 3-column grid the generated pages
174
+ use. For a landing or marketing page that grid is in the way, so reach for
175
+ **`PageLayout`** instead: it provides the document shell, header, theme, and
176
+ fonts, then a single full-width `<slot />` (no sidebar, no prose, no TOC). An
177
+ optional `footer` slot renders after `<main>`:
178
+
179
+ ```astro pages/index.astro lineNumbers
180
+ ---
181
+ import PageLayout from "blume/components/layout/PageLayout.astro";
182
+ import data from "blume:data";
183
+ import Footer from "./_home/Footer.astro";
184
+
185
+ const { config } = data;
186
+ ---
187
+
188
+ <PageLayout
189
+ site={{ title: config.title, description: config.description }}
190
+ logo={config.logo}
191
+ banner={config.banner}
192
+ analytics={config.analytics}
193
+ navigation={data.navigation}
194
+ favicon={config.favicon}
195
+ fontCssVars={data.fontCssVars}
196
+ themeMode={config.theme.mode}
197
+ searchEnabled={config.search.enabled}
198
+ siteUrl={config.site}
199
+ ogEnabled={config.og.enabled}
200
+ page={{ title: "Acme — the fastest docs", description: config.description }}
201
+ >
202
+ <section class="mx-auto max-w-5xl px-6 py-24">
203
+ <h1>Build docs that fly</h1>
204
+ </section>
205
+ <Footer slot="footer" />
206
+ </PageLayout>
207
+ ```
208
+
209
+ Passing `siteUrl` (and `ogEnabled`) derives the page's `canonical` and a
210
+ generated `og:image` automatically: Blume renders an Open Graph card for every
211
+ static custom page — the home included, the most-shared URL — served at
212
+ `/og/<route>.png` (`/og/index.png` for `/`). The home card uses the site title
213
+ with the description as its eyebrow; a deeper page is titled from its last path
214
+ segment. Set `ogImage` or `canonical` explicitly to override.
215
+
216
+ `page.title` is used verbatim as the document title (no `- siteTitle` suffix),
217
+ since marketing pages usually set their own. To give a custom page the full docs
218
+ chrome instead — sidebar, TOC, and all — wrap it in `RootLayout`, the layout the
219
+ generated pages use. Pull the required props straight from `blume:data`:
139
220
 
140
221
  ```astro pages/pricing.astro lineNumbers
141
222
  ---
@@ -137,6 +137,25 @@ SVG wins over PNG over ICO when several are present, and a file in `public/` is
137
137
  preferred over one at the root. If Blume finds no icon, it falls back to its own
138
138
  mark.
139
139
 
140
+ ### Apple touch icon
141
+
142
+ The icon iOS uses when someone adds your site to their home screen is detected
143
+ the same way. Drop an `apple-icon` file (`.png`, `.jpg`, or `.jpeg`) — or an
144
+ `apple-touch-icon.png`, the name most favicon generators emit — in your project
145
+ root or `public/` directory and Blume wires up `<link rel="apple-touch-icon">`
146
+ for you. There's no default; if no file is found, no tag is emitted.
147
+
148
+ ```
149
+ my-docs/
150
+ ├─ blume.config.ts
151
+ ├─ apple-icon.png ← picked up automatically
152
+ └─ docs/
153
+ ```
154
+
155
+ Put the file in `public/` rather than the project root: iOS ignores the inlined
156
+ data URI Blume uses for a root-level icon, so only a `public/` file (served at
157
+ `/apple-icon.png`) reliably reaches the home screen.
158
+
140
159
  ### Banner
141
160
 
142
161
  Show a site-wide announcement bar above the header. Pass a string, or an object
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Components
3
- description: The built-in JSX components — cards, columns, steps, tabs, accordions, badges, code groups, frames, trees, tooltips, type tables, and a GitHub card — usable in any MDX page.
3
+ description: The built-in JSX components — cards, columns, steps, tabs, accordions, badges, code groups, frames, trees, tooltips, type tables, live component previews, diffs, and a GitHub card — usable in any MDX page.
4
4
  ---
5
5
 
6
6
  Blume ships an accessible, themeable component set available in any `.mdx` page
@@ -563,3 +563,105 @@ variable to lift the API rate limit.
563
563
  <!-- Or point it at any repository -->
564
564
  <GithubInfo owner="withastro" repo="astro" />
565
565
  ```
566
+
567
+ ## Component
568
+
569
+ `Component` renders an example file from your project's `examples/` directory as
570
+ a live preview alongside its highlighted source, in tabs. Point it at a file with
571
+ `path` — its location under `examples/`, without the extension (so
572
+ `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro
573
+ examples are all supported; framework examples hydrate, Astro ones render
574
+ statically. It keeps the preview and the code in sync from a single file.
575
+
576
+ The directory is configurable — set `examples` in `blume.config.ts` when your
577
+ examples live elsewhere (e.g. a registry layout). `path` is always relative to
578
+ it:
579
+
580
+ ```ts
581
+ // blume.config.ts
582
+ export default defineConfig({
583
+ examples: "registry/files-sdk",
584
+ });
585
+ ```
586
+
587
+ ```astro
588
+ <!-- registry/files-sdk/file-list/basic.tsx -->
589
+ <Component path="file-list/basic" />
590
+ ```
591
+
592
+ <Component path="counter" />
593
+
594
+ ```astro
595
+ <!-- examples/counter.tsx -->
596
+ <Component path="counter" />
597
+ ```
598
+
599
+ An Astro example renders live with no client JavaScript:
600
+
601
+ <Component path="card" />
602
+
603
+ ## CodeBlock
604
+
605
+ `CodeBlock` highlights a code string with the same Shiki theme and transformers
606
+ as your fenced code — including the light/dark swap — for places a fence can't
607
+ go, like a landing page or a custom component. Pass `code` and a `lang`:
608
+
609
+ <CodeBlock
610
+ lang="ts"
611
+ code={`export const greet = (name: string): string =>
612
+ \`Hello, \${name}!\`;`}
613
+ />
614
+
615
+ ```astro
616
+ ---
617
+ import CodeBlock from "blume/components/content/CodeBlock.astro";
618
+ ---
619
+
620
+ <CodeBlock lang="ts" code={source} />
621
+ ```
622
+
623
+ To highlight to an HTML string yourself (e.g. inside your own component), import
624
+ the underlying helper from `blume/markdown`:
625
+
626
+ ```ts
627
+ import { highlightCode } from "blume/markdown";
628
+
629
+ const html = await highlightCode(source, "ts");
630
+ ```
631
+
632
+ ## Diff
633
+
634
+ `Diff` renders a git-style diff, highlighted with the same Shiki theme as your
635
+ code blocks and produced entirely at build time — no client JavaScript. Give it
636
+ two inline strings (`old` / `new`), two file paths (`before` / `after`), or a
637
+ unified patch (an inline `patch` string or a `src` file).
638
+
639
+ <Diff
640
+ lang="ts"
641
+ old={`export function greet(name) {
642
+ return "Hi, " + name;
643
+ }`}
644
+ new={`export function greet(name: string): string {
645
+ return "Hi, " + name + "!";
646
+ }`}
647
+ />
648
+
649
+ ```astro
650
+ <Diff
651
+ lang="ts"
652
+ old={`export function greet(name) {
653
+ return "Hi, " + name;
654
+ }`}
655
+ new={`export function greet(name: string): string {
656
+ return "Hi, " + name + "!";
657
+ }`}
658
+ />
659
+ ```
660
+
661
+ Diff two files in your project, relative to its root:
662
+
663
+ <Diff before="diffs/button-before.ts" after="diffs/button-after.ts" />
664
+
665
+ Or render a unified patch from a file:
666
+
667
+ <Diff src="diffs/greet.patch" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -31,10 +31,17 @@
31
31
  "docs"
32
32
  ],
33
33
  "type": "module",
34
+ "types": "./dist/types/index.d.ts",
34
35
  "exports": {
35
- ".": "./src/index.ts",
36
+ ".": {
37
+ "types": "./dist/types/index.d.ts",
38
+ "default": "./src/index.ts"
39
+ },
36
40
  "./ai/*": "./src/ai/*",
37
- "./schema": "./src/core/schema.ts",
41
+ "./schema": {
42
+ "types": "./dist/types/core/schema.d.ts",
43
+ "default": "./src/core/schema.ts"
44
+ },
38
45
  "./runtime": "./src/runtime/index.ts",
39
46
  "./astro": "./src/astro/index.ts",
40
47
  "./og": "./src/og/index.ts",
@@ -62,6 +69,7 @@
62
69
  "@astrojs/vercel": "^11.0.0",
63
70
  "@modelcontextprotocol/sdk": "^1.29.0",
64
71
  "@orama/orama": "^3.1.18",
72
+ "@pierre/diffs": "^1.2.11",
65
73
  "@scalar/astro": "^0.4.5",
66
74
  "@shikijs/transformers": "^4.2.0",
67
75
  "@shikijs/twoslash": "^4.2.0",
@@ -79,6 +87,7 @@
79
87
  "github-slugger": "^2.0.0",
80
88
  "gray-matter": "^4.0.3",
81
89
  "jiti": "^2.4.0",
90
+ "js-yaml": "^4.1.0",
82
91
  "katex": "^0.17.0",
83
92
  "mermaid": "^11.15.0",
84
93
  "pagefind": "^1.3.0",
@@ -93,6 +102,7 @@
93
102
  "zod": "^3.24.0"
94
103
  },
95
104
  "devDependencies": {
105
+ "@types/js-yaml": "^4.0.9",
96
106
  "@types/node": "^22.10.0",
97
107
  "@types/react": "^19.0.0",
98
108
  "@types/react-dom": "^19.0.0",
package/src/ai/llms.ts CHANGED
@@ -1,5 +1,4 @@
1
- import matter from "gray-matter";
2
-
1
+ import matter from "../core/frontmatter.ts";
3
2
  import type { BlumeProject } from "../core/project-graph.ts";
4
3
  import { readEntryText } from "../core/sources/read.ts";
5
4
  import type { PageRecord } from "../core/types.ts";
@@ -0,0 +1,106 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { join, relative } from "pathe";
4
+ import { glob } from "tinyglobby";
5
+
6
+ import type { IslandClientMode } from "./islands.ts";
7
+ import { readClientMode } from "./islands.ts";
8
+
9
+ /** Framework an example is authored in, inferred from its extension. */
10
+ export type ExampleFramework = "astro" | "react" | "svelte" | "vue";
11
+
12
+ /** A discovered `examples/` file, ready to wrap (live) and show (source). */
13
+ export interface ExampleSpec {
14
+ /** Hydration directive for a framework example; `undefined` for `.astro`. */
15
+ client?: IslandClientMode;
16
+ /** Absolute path to the example source file. */
17
+ file: string;
18
+ /** Framework, or `astro` for a server-rendered (static) example. */
19
+ framework: ExampleFramework;
20
+ /** Shiki language for the source pane — the file extension. */
21
+ lang: string;
22
+ /** MDX-facing key: the path under `examples/`, sans extension, `/`-joined. */
23
+ path: string;
24
+ /** Raw source text shown in the code tab. */
25
+ source: string;
26
+ }
27
+
28
+ export interface ExampleDiscovery {
29
+ examples: ExampleSpec[];
30
+ warnings: string[];
31
+ }
32
+
33
+ /** Example extensions mapped to the framework that renders them. */
34
+ const FRAMEWORK_BY_EXT: Record<string, ExampleFramework> = {
35
+ astro: "astro",
36
+ jsx: "react",
37
+ svelte: "svelte",
38
+ tsx: "react",
39
+ vue: "vue",
40
+ };
41
+
42
+ // Captures the extension so we can strip it from the path key and pick the
43
+ // framework. Kept in sync with the glob below.
44
+ const EXAMPLE_FILE = /\.(?<ext>astro|jsx|svelte|tsx|vue)$/u;
45
+
46
+ /**
47
+ * Discover preview examples under `<root>/<subdir>` (the `examples` config,
48
+ * default `examples`). Every `.astro`/`.tsx`/`.jsx`/`.vue`/`.svelte` file
49
+ * becomes addressable by `<Component path="...">`, where the path is the file's
50
+ * location under that directory without its extension (e.g. `forms/login.tsx` →
51
+ * `forms/login`). Discovery is path-based (a glob), so no example code is
52
+ * executed. Framework examples carry a hydration mode (default `client:visible`,
53
+ * overridable via `export const client`); `.astro` examples render statically
54
+ * with no client directive.
55
+ */
56
+ export const discoverExamples = async (
57
+ root: string,
58
+ subdir = "examples"
59
+ ): Promise<ExampleDiscovery> => {
60
+ const dir = join(root, subdir);
61
+ const matches = await glob(["**/*.{astro,jsx,svelte,tsx,vue}"], {
62
+ absolute: true,
63
+ cwd: dir,
64
+ onlyFiles: true,
65
+ });
66
+ const files = matches.toSorted();
67
+ const sources = await Promise.all(
68
+ files.map((file) => readFile(file, "utf-8"))
69
+ );
70
+
71
+ const examples: ExampleSpec[] = [];
72
+ const warnings: string[] = [];
73
+ const seen = new Map<string, string>();
74
+
75
+ for (const [index, file] of files.entries()) {
76
+ const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
77
+ const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
78
+ if (!(ext && framework)) {
79
+ continue;
80
+ }
81
+ // Strip the trailing `.<ext>` to form the `<Component path>` key.
82
+ const path = relative(dir, file).slice(0, -(ext.length + 1));
83
+ const existing = seen.get(path);
84
+ if (existing) {
85
+ warnings.push(
86
+ `Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`
87
+ );
88
+ continue;
89
+ }
90
+ seen.set(path, file);
91
+ const source = sources[index] ?? "";
92
+ examples.push({
93
+ client:
94
+ framework === "astro"
95
+ ? undefined
96
+ : readClientMode(source, file, warnings),
97
+ file,
98
+ framework,
99
+ lang: ext,
100
+ path,
101
+ source,
102
+ });
103
+ }
104
+
105
+ return { examples, warnings };
106
+ };