blume 0.1.5 → 0.3.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 (85) hide show
  1. package/dist/cli/index.js +2123 -555
  2. package/dist/cli/index.js.map +39 -25
  3. package/dist/types/core/data.d.ts +16 -0
  4. package/dist/types/core/define-components.d.ts +9 -2
  5. package/dist/types/core/diagnostics.d.ts +5 -0
  6. package/dist/types/core/schema.d.ts +136 -508
  7. package/dist/types/core/types.d.ts +2 -2
  8. package/docs/02-deployment.mdx +21 -2
  9. package/docs/advanced/changelog.mdx +28 -1
  10. package/docs/advanced/custom-pages.mdx +63 -2
  11. package/docs/configuration/ai.mdx +20 -3
  12. package/docs/configuration/customization.mdx +103 -5
  13. package/docs/configuration/index.mdx +25 -11
  14. package/docs/configuration/search.mdx +13 -1
  15. package/docs/configuration/seo.mdx +5 -0
  16. package/docs/configuration/theming.mdx +51 -0
  17. package/docs/content/components.mdx +18 -0
  18. package/docs/content/islands.mdx +73 -0
  19. package/docs/content/navigation.mdx +25 -0
  20. package/docs/content/sources.mdx +43 -0
  21. package/docs/content/syntax.mdx +1 -1
  22. package/docs/index.mdx +3 -12
  23. package/docs/reference/cli.mdx +49 -1
  24. package/docs/reference/frontmatter.mdx +9 -1
  25. package/package.json +3 -1
  26. package/src/ai/ask-context.ts +131 -0
  27. package/src/ai/ask-data.ts +25 -0
  28. package/src/astro/component-slots.ts +165 -0
  29. package/src/astro/generate.ts +162 -26
  30. package/src/astro/integration.ts +59 -0
  31. package/src/astro/pages.ts +5 -12
  32. package/src/astro/templates.ts +102 -45
  33. package/src/blume-modules.d.ts +25 -0
  34. package/src/cli/commands/build.ts +186 -1
  35. package/src/cli/commands/check.ts +62 -0
  36. package/src/cli/commands/dev.ts +21 -1
  37. package/src/cli/commands/doctor.ts +23 -6
  38. package/src/cli/commands/init.ts +163 -15
  39. package/src/cli/commands/validate.ts +16 -2
  40. package/src/cli/env.ts +84 -0
  41. package/src/cli/index.ts +20 -0
  42. package/src/cli/internal-error.ts +63 -0
  43. package/src/cli/log.ts +30 -1
  44. package/src/cli/prepare.ts +22 -3
  45. package/src/cli/required-secrets.ts +44 -0
  46. package/src/components/BlumePage.astro +107 -0
  47. package/src/components/content/CodeBlock.astro +7 -2
  48. package/src/components/index.ts +3 -3
  49. package/src/components/islands/ask-ai.tsx +15 -1
  50. package/src/components/islands/hooks.ts +188 -0
  51. package/src/components/layout/Empty.astro +6 -0
  52. package/src/components/layout/Header.astro +24 -39
  53. package/src/components/layout/Logo.astro +50 -0
  54. package/src/components/layout/NavSelector.astro +75 -0
  55. package/src/components/layout/PageLayout.astro +38 -2
  56. package/src/components/layout/RootLayout.astro +70 -4
  57. package/src/components/layout/hydration-hint.ts +30 -0
  58. package/src/components/layout/overrides.ts +6 -4
  59. package/src/components/props.ts +68 -0
  60. package/src/core/builtin-tags.ts +39 -0
  61. package/src/core/component-diagnostics.ts +44 -0
  62. package/src/core/component-overrides.ts +478 -0
  63. package/src/core/config.ts +8 -0
  64. package/src/core/data.ts +14 -0
  65. package/src/core/define-components.ts +9 -2
  66. package/src/core/diagnostics.ts +90 -1
  67. package/src/core/graph.ts +7 -0
  68. package/src/core/nav-diagnostics.ts +205 -0
  69. package/src/core/project-graph.ts +40 -1
  70. package/src/core/schema.ts +54 -96
  71. package/src/core/sources/github-releases.ts +200 -0
  72. package/src/core/sources/normalize.ts +51 -0
  73. package/src/core/sources/resolve.ts +16 -0
  74. package/src/core/types.ts +2 -2
  75. package/src/deploy/redirects.ts +43 -0
  76. package/src/markdown/index.ts +24 -0
  77. package/src/migrate/mintlify/config.ts +1 -176
  78. package/src/migrate/starlight/config.ts +0 -4
  79. package/src/og/card.ts +163 -38
  80. package/src/registry/eject.ts +39 -9
  81. package/src/registry/registry.ts +166 -0
  82. package/src/runtime/index.ts +61 -0
  83. package/src/vite-env.d.ts +14 -0
  84. package/docs/changelog/v0-1-0.mdx +0 -12
  85. package/docs/changelog/v0-2-0.mdx +0 -16
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli/index.ts
6
- import { defineCommand as defineCommand11, runMain } from "citty";
6
+ import { defineCommand as defineCommand12, runMain } from "citty";
7
7
 
8
8
  // src/core/version.ts
9
9
  import { readFileSync } from "node:fs";
@@ -72,6 +72,145 @@ var layoutComponent = (config) => {
72
72
  ]
73
73
  };
74
74
  };
75
+ var contentComponent = (config) => {
76
+ const target = `components/blume/${config.file}`;
77
+ return {
78
+ description: config.description,
79
+ files: [
80
+ {
81
+ rewrite: true,
82
+ source: `components/content/${config.file}`,
83
+ target
84
+ }
85
+ ],
86
+ name: config.name,
87
+ postInstall: [
88
+ "Register it in components.ts:",
89
+ ' import { defineComponents } from "blume";',
90
+ ` import ${config.tag} from "./${target}";`,
91
+ "",
92
+ ` export default defineComponents({ mdx: { ${config.tag} } });`,
93
+ "",
94
+ "It imports the rest from `blume/*`, so it matches the built-in until you edit it."
95
+ ]
96
+ };
97
+ };
98
+ var CONTENT_COMPONENTS = [
99
+ {
100
+ description: "Aside for notes, tips, and warnings.",
101
+ file: "Callout.astro",
102
+ name: "callout",
103
+ tag: "Callout"
104
+ },
105
+ {
106
+ description: "A linkable card with icon, title, and body.",
107
+ file: "Card.astro",
108
+ name: "card",
109
+ tag: "Card"
110
+ },
111
+ {
112
+ description: "A responsive grid of cards.",
113
+ file: "CardGroup.astro",
114
+ name: "card-group",
115
+ tag: "CardGroup"
116
+ },
117
+ {
118
+ description: "Tabbed code blocks for multiple languages.",
119
+ file: "CodeGroup.astro",
120
+ name: "code-group",
121
+ tag: "CodeGroup"
122
+ },
123
+ {
124
+ description: "A small status/label badge.",
125
+ file: "Badge.astro",
126
+ name: "badge",
127
+ tag: "Badge"
128
+ },
129
+ {
130
+ description: "A numbered list of steps.",
131
+ file: "Steps.astro",
132
+ name: "steps",
133
+ tag: "Steps"
134
+ },
135
+ {
136
+ description: "A single step within Steps.",
137
+ file: "Step.astro",
138
+ name: "step",
139
+ tag: "Step"
140
+ },
141
+ {
142
+ description: "A tabbed content panel.",
143
+ file: "Tabs.astro",
144
+ name: "tabs",
145
+ tag: "Tabs"
146
+ },
147
+ {
148
+ description: "A single tab within Tabs.",
149
+ file: "Tab.astro",
150
+ name: "tab",
151
+ tag: "Tab"
152
+ },
153
+ {
154
+ description: "A collapsible accordion group.",
155
+ file: "Accordion.astro",
156
+ name: "accordion",
157
+ tag: "Accordion"
158
+ },
159
+ {
160
+ description: "A single item within an Accordion.",
161
+ file: "AccordionItem.astro",
162
+ name: "accordion-item",
163
+ tag: "AccordionItem"
164
+ },
165
+ {
166
+ description: "A multi-column layout.",
167
+ file: "Columns.astro",
168
+ name: "columns",
169
+ tag: "Columns"
170
+ },
171
+ {
172
+ description: "A single column within Columns.",
173
+ file: "Column.astro",
174
+ name: "column",
175
+ tag: "Column"
176
+ },
177
+ {
178
+ description: "A bordered frame around an image or embed.",
179
+ file: "Frame.astro",
180
+ name: "frame",
181
+ tag: "Frame"
182
+ },
183
+ {
184
+ description: "An inline expand/collapse disclosure.",
185
+ file: "Expandable.astro",
186
+ name: "expandable",
187
+ tag: "Expandable"
188
+ },
189
+ {
190
+ description: "A titled content panel.",
191
+ file: "Panel.astro",
192
+ name: "panel",
193
+ tag: "Panel"
194
+ },
195
+ {
196
+ description: "A hover tooltip.",
197
+ file: "Tooltip.astro",
198
+ name: "tooltip",
199
+ tag: "Tooltip"
200
+ },
201
+ {
202
+ description: "A compact linkable tile.",
203
+ file: "Tile.astro",
204
+ name: "tile",
205
+ tag: "Tile"
206
+ },
207
+ {
208
+ description: "A styled prompt / terminal block.",
209
+ file: "Prompt.astro",
210
+ name: "prompt",
211
+ tag: "Prompt"
212
+ }
213
+ ];
75
214
  var registry = [
76
215
  layoutComponent({
77
216
  description: "The top navigation bar (logo, search, nav links).",
@@ -102,7 +241,14 @@ var registry = [
102
241
  file: "Pagination.astro",
103
242
  name: "pagination",
104
243
  slot: "Pagination"
105
- })
244
+ }),
245
+ layoutComponent({
246
+ description: 'The "Was this page helpful?" feedback rating.',
247
+ file: "PageFeedback.astro",
248
+ name: "feedback",
249
+ slot: "Feedback"
250
+ }),
251
+ ...CONTENT_COMPONENTS.map(contentComponent)
106
252
  ];
107
253
  var findItem = (name) => registry.find((item) => item.name === name);
108
254
 
@@ -123,6 +269,7 @@ var rewriteImports = (content, sourceFile, srcRoot) => content.replaceAll(RELATI
123
269
 
124
270
  // src/cli/log.ts
125
271
  import { consola } from "consola";
272
+ import { relative as relative3 } from "pathe";
126
273
 
127
274
  // src/core/diagnostics.ts
128
275
  import { relative as relative2 } from "pathe";
@@ -135,12 +282,77 @@ class BlumeError extends Error {
135
282
  this.diagnostic = diagnostic;
136
283
  }
137
284
  }
285
+ var DOCS_BASE = "https://useblume.dev";
286
+ var DOCS_PATHS = {
287
+ BLUME_ADAPTER_REQUIRED: "/docs/deployment",
288
+ BLUME_ASSETS_UNCHECKED: "/docs/reference/cli",
289
+ BLUME_ASSET_FETCH_FAILED: "/docs/content/sources",
290
+ BLUME_BROKEN_ANCHOR: "/docs/reference/cli",
291
+ BLUME_BROKEN_ASSET: "/docs/reference/cli",
292
+ BLUME_BROKEN_LINK: "/docs/reference/cli",
293
+ BLUME_CONFIG_INVALID: "/docs/configuration",
294
+ BLUME_CONFIG_LOAD_FAILED: "/docs/configuration",
295
+ BLUME_CONTENT_ROOT_MISSING: "/docs/content/sources",
296
+ BLUME_DEAD_LINK: "/docs/reference/cli",
297
+ BLUME_DUPLICATE_ROUTE: "/docs/content/navigation",
298
+ BLUME_FRONTMATTER_INVALID: "/docs/reference/frontmatter",
299
+ BLUME_META_INVALID: "/docs/content/meta",
300
+ BLUME_META_LOAD_FAILED: "/docs/content/meta",
301
+ BLUME_MISSING_SECRET: "/docs/deployment",
302
+ BLUME_NAV_DUPLICATE_LABEL: "/docs/content/navigation",
303
+ BLUME_NAV_HIDDEN_IN_SIDEBAR: "/docs/content/navigation",
304
+ BLUME_NAV_MISSING_PAGE: "/docs/content/navigation",
305
+ BLUME_NODE_VERSION: "/docs/quickstart",
306
+ BLUME_SERVER_FEATURE_REQUIRED: "/docs/deployment",
307
+ BLUME_SOURCE_FETCH_FAILED: "/docs/content/sources",
308
+ BLUME_SOURCE_MISCONFIGURED: "/docs/content/sources",
309
+ BLUME_SOURCE_OFFLINE: "/docs/content/sources",
310
+ BLUME_SOURCE_SDK_MISSING: "/docs/content/sources",
311
+ BLUME_SOURCE_UNAVAILABLE: "/docs/content/sources",
312
+ BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
313
+ BLUME_UNKNOWN_ICON: "/docs/content/navigation"
314
+ };
315
+ var resolveDocsUrl = (code) => {
316
+ const path = DOCS_PATHS[code];
317
+ return path ? `${DOCS_BASE}${path}` : undefined;
318
+ };
319
+ var enrichDiagnostic = (diagnostic) => diagnostic.docsUrl ? diagnostic : { ...diagnostic, docsUrl: resolveDocsUrl(diagnostic.code) };
320
+ var REGEXP_SPECIAL = /[$()*+.?[\\\]^{|}]/gu;
321
+ var escapeRegExp = (value) => value.replaceAll(REGEXP_SPECIAL, String.raw`\$&`);
322
+ var locatePath = (source, path) => {
323
+ let cursor = 0;
324
+ let found = -1;
325
+ for (const segment of path) {
326
+ if (typeof segment !== "string") {
327
+ continue;
328
+ }
329
+ const matcher = new RegExp(`${escapeRegExp(segment)}\\s*[:=]`, "gu");
330
+ matcher.lastIndex = cursor;
331
+ const match = matcher.exec(source);
332
+ if (!match) {
333
+ break;
334
+ }
335
+ found = match.index;
336
+ cursor = matcher.lastIndex;
337
+ }
338
+ if (found < 0) {
339
+ return;
340
+ }
341
+ const before = source.slice(0, found);
342
+ const lastNewline = before.lastIndexOf(`
343
+ `);
344
+ return { column: found - lastNewline, line: before.split(`
345
+ `).length };
346
+ };
138
347
  var diagnosticsFromZod = (error, options) => error.issues.map((issue) => {
139
348
  const schemaPath = issue.path.join(".");
140
349
  const received = "received" in issue ? ` (received: ${JSON.stringify(issue.received)})` : "";
350
+ const position = options.source ? locatePath(options.source, issue.path) : undefined;
141
351
  return {
142
352
  code: options.code,
353
+ column: position?.column,
143
354
  file: options.file,
355
+ line: position?.line,
144
356
  message: schemaPath ? `${schemaPath}: ${issue.message}${received}` : `${issue.message}${received}`,
145
357
  schemaPath: schemaPath || undefined,
146
358
  severity: "error"
@@ -196,12 +408,21 @@ var countBySeverity = (diagnostics) => {
196
408
 
197
409
  // src/cli/log.ts
198
410
  var logger = consola.withTag("blume");
411
+ var reportDiagnosticsJson = (diagnostics, root) => {
412
+ const enriched = diagnostics.map((diagnostic) => {
413
+ const withDocs = enrichDiagnostic(diagnostic);
414
+ return withDocs.file && root ? { ...withDocs, file: relative3(root, withDocs.file) } : withDocs;
415
+ });
416
+ process.stdout.write(`${JSON.stringify({ diagnostics: enriched, summary: countBySeverity(diagnostics) }, null, 2)}
417
+ `);
418
+ return hasErrors(diagnostics);
419
+ };
199
420
  var reportDiagnostics = (diagnostics, root) => {
200
421
  if (diagnostics.length === 0) {
201
422
  return false;
202
423
  }
203
424
  for (const diagnostic of diagnostics) {
204
- process.stderr.write(`${formatDiagnostic(diagnostic, root)}
425
+ process.stderr.write(`${formatDiagnostic(enrichDiagnostic(diagnostic), root)}
205
426
  `);
206
427
  }
207
428
  const counts = countBySeverity(diagnostics);
@@ -279,11 +500,11 @@ Next steps:
279
500
  });
280
501
 
281
502
  // src/cli/commands/build.ts
282
- import { existsSync as existsSync11 } from "node:fs";
283
- import { writeFile as writeFile6 } from "node:fs/promises";
503
+ import { existsSync as existsSync15 } from "node:fs";
504
+ import { readdir, stat, writeFile as writeFile6 } from "node:fs/promises";
284
505
  import { build } from "astro";
285
506
  import { defineCommand as defineCommand2 } from "citty";
286
- import { join as join23 } from "pathe";
507
+ import { join as join24 } from "pathe";
287
508
 
288
509
  // src/core/frontmatter.ts
289
510
  import baseMatter from "gray-matter";
@@ -443,6 +664,25 @@ var serverFeatures = (config) => {
443
664
  return features;
444
665
  };
445
666
 
667
+ // src/deploy/redirects.ts
668
+ var buildNetlifyRedirects = (redirects) => `${redirects.map((redirect) => `${redirect.from} ${redirect.to} ${redirect.status}`).join(`
669
+ `)}
670
+ `;
671
+ var buildVercelConfig = (redirects) => `${JSON.stringify({
672
+ redirects: redirects.map((redirect) => ({
673
+ destination: redirect.to,
674
+ permanent: redirect.status === 301 || redirect.status === 308,
675
+ source: redirect.from
676
+ }))
677
+ }, null, 2)}
678
+ `;
679
+ var buildRedirectManifest = (redirects) => `${JSON.stringify(redirects.map((redirect) => ({
680
+ from: redirect.from,
681
+ status: redirect.status,
682
+ to: redirect.to
683
+ })), null, 2)}
684
+ `;
685
+
446
686
  // src/deploy/robots.ts
447
687
  var buildRobots = (project) => {
448
688
  const { config } = project;
@@ -2514,7 +2754,7 @@ var syncSearchProvider = async (project, reporter) => {
2514
2754
  };
2515
2755
 
2516
2756
  // src/astro/generate.ts
2517
- import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync } from "node:fs";
2757
+ import { existsSync as existsSync6, readFileSync as readFileSync5, realpathSync } from "node:fs";
2518
2758
  import {
2519
2759
  lstat,
2520
2760
  mkdir as mkdir2,
@@ -2526,9 +2766,26 @@ import {
2526
2766
  } from "node:fs/promises";
2527
2767
  import { createRequire as createRequire4 } from "node:module";
2528
2768
  import { pathToFileURL as pathToFileURL2 } from "node:url";
2529
- import { basename as basename2, dirname as dirname6, join as join11, normalize, relative as relative5 } from "pathe";
2769
+ import { basename as basename2, dirname as dirname7, join as join11, normalize, relative as relative6 } from "pathe";
2530
2770
  import { glob as glob4 } from "tinyglobby";
2531
2771
 
2772
+ // src/ai/ask-data.ts
2773
+ var buildAskData = async (project) => {
2774
+ const documents = await buildSearchDocuments(project, {
2775
+ includeWhenDisabled: true
2776
+ });
2777
+ return {
2778
+ documents: documents.map((doc) => ({
2779
+ content: doc.content,
2780
+ description: doc.description,
2781
+ locale: doc.locale,
2782
+ route: doc.route,
2783
+ title: doc.title
2784
+ })),
2785
+ site: project.config.deployment.site ?? null
2786
+ };
2787
+ };
2788
+
2532
2789
  // src/ai/ask.ts
2533
2790
  var ASK_PRESETS = {
2534
2791
  inkeep: {
@@ -2682,6 +2939,323 @@ var buildMcpServerCard = (input) => ({
2682
2939
  version: input.version
2683
2940
  });
2684
2941
 
2942
+ // src/core/builtin-tags.ts
2943
+ var BUILTIN_MDX_TAGS = new Set([
2944
+ "Accordion",
2945
+ "AccordionItem",
2946
+ "AutoTypeTable",
2947
+ "Badge",
2948
+ "Callout",
2949
+ "Card",
2950
+ "CardGroup",
2951
+ "CodeBlock",
2952
+ "CodeGroup",
2953
+ "Color",
2954
+ "Column",
2955
+ "Columns",
2956
+ "Component",
2957
+ "Diff",
2958
+ "Expandable",
2959
+ "FileTree",
2960
+ "Frame",
2961
+ "GithubInfo",
2962
+ "Icon",
2963
+ "Math",
2964
+ "Panel",
2965
+ "Prompt",
2966
+ "Step",
2967
+ "Steps",
2968
+ "Tab",
2969
+ "Tabs",
2970
+ "Tile",
2971
+ "Tooltip",
2972
+ "Tree",
2973
+ "TypeTable",
2974
+ "Visibility"
2975
+ ]);
2976
+
2977
+ // src/core/component-diagnostics.ts
2978
+ var toKebab = (tag) => tag.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
2979
+ var validateUsedComponents = (pages, extraTags, registryNames) => {
2980
+ const diagnostics = [];
2981
+ const seen = new Set;
2982
+ for (const page of pages) {
2983
+ for (const tag of page.componentsUsed ?? []) {
2984
+ if (BUILTIN_MDX_TAGS.has(tag) || extraTags.has(tag) || seen.has(tag)) {
2985
+ continue;
2986
+ }
2987
+ seen.add(tag);
2988
+ const name = toKebab(tag);
2989
+ const suggestion = registryNames.has(name) ? `Run \`blume add ${name}\` to install it, or register <${tag}> in components.ts (mdx).` : `Register <${tag}> in components.ts (mdx), or add an islands/${tag}.tsx component.`;
2990
+ diagnostics.push({
2991
+ code: "BLUME_UNKNOWN_COMPONENT",
2992
+ file: page.sourcePath ?? page.id,
2993
+ message: `<${tag}> is used in ${page.route} but isn't a known component.`,
2994
+ severity: "warning",
2995
+ suggestion
2996
+ });
2997
+ }
2998
+ }
2999
+ return diagnostics;
3000
+ };
3001
+
3002
+ // src/core/component-overrides.ts
3003
+ import { existsSync as existsSync3 } from "node:fs";
3004
+ import { dirname as dirname4, extname, isAbsolute, resolve as resolve2 } from "pathe";
3005
+ import ts from "typescript";
3006
+ var GROUPS = ["mdx", "layout", "islands"];
3007
+ var FRAMEWORK_BY_EXT = {
3008
+ jsx: "react",
3009
+ svelte: "svelte",
3010
+ tsx: "react",
3011
+ vue: "vue"
3012
+ };
3013
+ var FRAMEWORK_LABEL = {
3014
+ react: "React",
3015
+ svelte: "Svelte",
3016
+ vue: "Vue"
3017
+ };
3018
+ var COMPONENT_EXTS = [
3019
+ "astro",
3020
+ "tsx",
3021
+ "ts",
3022
+ "jsx",
3023
+ "js",
3024
+ "mjs",
3025
+ "vue",
3026
+ "svelte"
3027
+ ];
3028
+ var HYDRATION_MODES = new Set([
3029
+ "idle",
3030
+ "load",
3031
+ "media",
3032
+ "only",
3033
+ "visible"
3034
+ ]);
3035
+ var emptyAnalysis = () => ({
3036
+ islands: [],
3037
+ layout: [],
3038
+ mdx: [],
3039
+ warnings: []
3040
+ });
3041
+ var propName = (name) => ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
3042
+ var collectImports = (sourceFile) => {
3043
+ const map = new Map;
3044
+ for (const statement of sourceFile.statements) {
3045
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
3046
+ continue;
3047
+ }
3048
+ const specifier = statement.moduleSpecifier.text;
3049
+ const clause = statement.importClause;
3050
+ if (!clause) {
3051
+ continue;
3052
+ }
3053
+ if (clause.name) {
3054
+ map.set(clause.name.text, { imported: "default", specifier });
3055
+ }
3056
+ const named = clause.namedBindings;
3057
+ if (named && ts.isNamedImports(named)) {
3058
+ for (const element of named.elements) {
3059
+ map.set(element.name.text, {
3060
+ imported: (element.propertyName ?? element.name).text,
3061
+ specifier
3062
+ });
3063
+ }
3064
+ }
3065
+ }
3066
+ return map;
3067
+ };
3068
+ var unwrapObject = (expression) => {
3069
+ if (ts.isObjectLiteralExpression(expression)) {
3070
+ return expression;
3071
+ }
3072
+ if (ts.isCallExpression(expression)) {
3073
+ const [arg] = expression.arguments;
3074
+ return arg && ts.isObjectLiteralExpression(arg) ? arg : undefined;
3075
+ }
3076
+ if (ts.isAsExpression(expression) || ts.isParenthesizedExpression(expression)) {
3077
+ return unwrapObject(expression.expression);
3078
+ }
3079
+ return;
3080
+ };
3081
+ var findDefaultExportObject = (sourceFile) => {
3082
+ for (const statement of sourceFile.statements) {
3083
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
3084
+ return unwrapObject(statement.expression);
3085
+ }
3086
+ }
3087
+ return;
3088
+ };
3089
+ var probeExtension = (base) => {
3090
+ for (const extension of COMPONENT_EXTS) {
3091
+ const candidate = `${base}.${extension}`;
3092
+ if (existsSync3(candidate)) {
3093
+ return candidate;
3094
+ }
3095
+ }
3096
+ return null;
3097
+ };
3098
+ var toImport = (specifier, imported, dir) => {
3099
+ const relative4 = specifier.startsWith(".") || isAbsolute(specifier);
3100
+ let path = specifier;
3101
+ let extension = extname(specifier).slice(1).toLowerCase();
3102
+ if (relative4) {
3103
+ const absolute = isAbsolute(specifier) ? specifier : resolve2(dir, specifier);
3104
+ if (extension) {
3105
+ path = absolute;
3106
+ } else {
3107
+ const probed = probeExtension(absolute);
3108
+ path = probed ?? absolute;
3109
+ extension = probed ? extname(probed).slice(1).toLowerCase() : "";
3110
+ }
3111
+ }
3112
+ return {
3113
+ framework: FRAMEWORK_BY_EXT[extension] ?? null,
3114
+ name: imported,
3115
+ path
3116
+ };
3117
+ };
3118
+ var resolveIdentifier = (name, imports, dir) => {
3119
+ const binding = imports.get(name);
3120
+ return binding ? toImport(binding.specifier, binding.imported, dir) : null;
3121
+ };
3122
+ var readDescriptor = (object, imports, dir) => {
3123
+ const descriptor = { hadComponent: false, source: null };
3124
+ for (const property of object.properties) {
3125
+ if (ts.isShorthandPropertyAssignment(property)) {
3126
+ if (property.name.text === "component") {
3127
+ descriptor.hadComponent = true;
3128
+ descriptor.source = resolveIdentifier(property.name.text, imports, dir);
3129
+ }
3130
+ continue;
3131
+ }
3132
+ if (!ts.isPropertyAssignment(property)) {
3133
+ continue;
3134
+ }
3135
+ const name = propName(property.name);
3136
+ const init = property.initializer;
3137
+ if (name === "component") {
3138
+ descriptor.hadComponent = true;
3139
+ if (ts.isStringLiteral(init)) {
3140
+ descriptor.source = toImport(init.text, "default", dir);
3141
+ } else if (ts.isIdentifier(init)) {
3142
+ descriptor.source = resolveIdentifier(init.text, imports, dir);
3143
+ }
3144
+ } else if (name === "client" && ts.isStringLiteral(init) && HYDRATION_MODES.has(init.text)) {
3145
+ descriptor.client = init.text;
3146
+ } else if (name === "media" && ts.isStringLiteral(init)) {
3147
+ descriptor.media = init.text;
3148
+ }
3149
+ }
3150
+ return descriptor;
3151
+ };
3152
+ var finalize = (key, group, descriptor, label, identifier, warnings) => {
3153
+ const { client, media, source } = descriptor;
3154
+ if (group === "islands") {
3155
+ if (!source) {
3156
+ warnings.push(`Island override "${key}" couldn't be resolved to a file. Reference it by an imported component or a path string with an extension.`);
3157
+ return null;
3158
+ }
3159
+ if (!source.framework) {
3160
+ warnings.push(`Island override "${key}" (${label}) is not a React, Vue, or Svelte component; only framework components can be islands.`);
3161
+ return null;
3162
+ }
3163
+ }
3164
+ if (client === "media" && !media) {
3165
+ warnings.push(`Override "${key}" uses client: "media" but no \`media\` query was given; it will hydrate as if \`client: "load"\`.`);
3166
+ }
3167
+ if (client === "only" && source && !source.framework) {
3168
+ warnings.push(`Override "${key}" uses client: "only" but its framework couldn't be inferred; reference a .tsx/.jsx/.vue/.svelte file.`);
3169
+ }
3170
+ if (client && !source) {
3171
+ warnings.push(`Override "${key}" declares client: "${client}" but its component couldn't be resolved to a file, so it can't hydrate. Reference it by an imported component or a path string.`);
3172
+ return { identifier, key, source: null };
3173
+ }
3174
+ if (!client && source?.framework) {
3175
+ warnings.push(`Override "${key}" points to a ${FRAMEWORK_LABEL[source.framework]} component (${label}) but has no hydration mode, so it renders as static HTML with no interactivity. Add one, e.g. \`${key}: { component: ${JSON.stringify(label)}, client: "load" }\`.`);
3176
+ }
3177
+ return {
3178
+ identifier,
3179
+ key,
3180
+ ...client ? { client } : {},
3181
+ ...media ? { media } : {},
3182
+ source
3183
+ };
3184
+ };
3185
+ var normalizeEntry = (entry, group, imports, dir, warnings) => {
3186
+ const defaultClient = group === "islands" ? "visible" : undefined;
3187
+ if (ts.isShorthandPropertyAssignment(entry)) {
3188
+ const name = entry.name.text;
3189
+ return finalize(name, group, {
3190
+ client: defaultClient,
3191
+ hadComponent: true,
3192
+ source: resolveIdentifier(name, imports, dir)
3193
+ }, name, true, warnings);
3194
+ }
3195
+ if (!ts.isPropertyAssignment(entry)) {
3196
+ return null;
3197
+ }
3198
+ const key = propName(entry.name);
3199
+ if (!key) {
3200
+ return null;
3201
+ }
3202
+ const value = entry.initializer;
3203
+ if (ts.isIdentifier(value)) {
3204
+ return finalize(key, group, {
3205
+ client: defaultClient,
3206
+ hadComponent: true,
3207
+ source: resolveIdentifier(value.text, imports, dir)
3208
+ }, value.text, true, warnings);
3209
+ }
3210
+ if (ts.isStringLiteral(value)) {
3211
+ return finalize(key, group, {
3212
+ client: defaultClient,
3213
+ hadComponent: true,
3214
+ source: toImport(value.text, "default", dir)
3215
+ }, value.text, false, warnings);
3216
+ }
3217
+ if (ts.isObjectLiteralExpression(value)) {
3218
+ const descriptor = readDescriptor(value, imports, dir);
3219
+ if (!descriptor.hadComponent) {
3220
+ warnings.push(`Override "${key}" is an object without a \`component\` field; expected \`{ component, client }\`.`);
3221
+ return null;
3222
+ }
3223
+ if (!descriptor.source) {
3224
+ warnings.push(`Override "${key}"'s \`component\` couldn't be resolved to a file. Reference an imported component or a path string with an extension.`);
3225
+ return null;
3226
+ }
3227
+ return finalize(key, group, { ...descriptor, client: descriptor.client ?? defaultClient }, key, false, warnings);
3228
+ }
3229
+ return { identifier: false, key, source: null };
3230
+ };
3231
+ var analyzeComponentOverrides = (source, filePath) => {
3232
+ const result = emptyAnalysis();
3233
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, filePath.endsWith("tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
3234
+ const object = findDefaultExportObject(sourceFile);
3235
+ if (!object) {
3236
+ return result;
3237
+ }
3238
+ const imports = collectImports(sourceFile);
3239
+ const dir = dirname4(filePath);
3240
+ for (const property of object.properties) {
3241
+ if (!ts.isPropertyAssignment(property)) {
3242
+ continue;
3243
+ }
3244
+ const name = propName(property.name);
3245
+ if (!(name && GROUPS.includes(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
3246
+ continue;
3247
+ }
3248
+ const group = name;
3249
+ for (const entry of property.initializer.properties) {
3250
+ const normalized = normalizeEntry(entry, group, imports, dir, result.warnings);
3251
+ if (normalized) {
3252
+ result[group].push(normalized);
3253
+ }
3254
+ }
3255
+ }
3256
+ return result;
3257
+ };
3258
+
2685
3259
  // src/core/i18n-ui.ts
2686
3260
  import { z } from "zod";
2687
3261
  var uiStringsObject = z.object({
@@ -2773,11 +3347,284 @@ var resolveUIStrings = (locale, options) => {
2773
3347
  return dict;
2774
3348
  };
2775
3349
 
3350
+ // src/theme/icons.ts
3351
+ var icons = {
3352
+ "arrow-left": '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
3353
+ "arrow-right": '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
3354
+ "arrow-up": '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
3355
+ "arrow-up-right": '<path d="M7 7h10v10"/><path d="M7 17 17 7"/>',
3356
+ "badge-alert": '<path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.78 4.78 4 4 0 0 1-6.74 0 4 4 0 0 1-4.78-4.78 4 4 0 0 1 0-6.75Z"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
3357
+ ban: '<circle cx="12" cy="12" r="10"/><path d="m4.93 4.93 14.14 14.14"/>',
3358
+ "book-open": '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>',
3359
+ "book-open-cover": '<path d="M12 7v14"/><path d="M3 18a2 2 0 0 1 2-2h7V5H5a2 2 0 0 0-2 2Z"/><path d="M21 18a2 2 0 0 0-2-2h-7V5h7a2 2 0 0 1 2 2Z"/>',
3360
+ "brand-x": '<path d="m4 4 11.7 16H20L8.3 4Z"/><path d="M4 20 20 4"/>',
3361
+ check: '<path d="M20 6 9 17l-5-5"/>',
3362
+ "chevron-down": '<path d="m6 9 6 6 6-6"/>',
3363
+ "chevron-right": '<path d="m9 18 6-6-6-6"/>',
3364
+ "circle-check": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
3365
+ "circle-x": '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
3366
+ clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
3367
+ copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
3368
+ download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
3369
+ "external-link": '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
3370
+ file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
3371
+ flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><path d="M4 22V15"/>',
3372
+ folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
3373
+ gear: '<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/>',
3374
+ github: '<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.3-.8 2.1-.36.16-.78.24-1.2.24-1.4 0-2.4-.7-3-2-.3-.6-.8-.9-1.2-.9-.4 0-.8.2-.8.5 0 .5.7.8 1 1.2.7 1.5 2 2.4 4 2.4.43 0 .84-.04 1.2-.13V22"/>',
3375
+ globe: '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
3376
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
3377
+ js: '<path d="M8 8v7a2 2 0 1 1-4 0"/><path d="M16 15a2 2 0 1 0 2-2 2 2 0 1 1 2-2"/><path d="M20 8v.01"/>',
3378
+ key: '<path d="M21 2 11.4 11.6"/><circle cx="7.5" cy="16.5" r="5.5"/><path d="m15 7 2 2"/><path d="m12 10 2 2"/>',
3379
+ leaf: '<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/>',
3380
+ lightbulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
3381
+ link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
3382
+ linkedin: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6z"/><rect width="4" height="12" x="2" y="9"/><circle cx="4" cy="4" r="2"/>',
3383
+ lock: '<rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
3384
+ menu: '<line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/>',
3385
+ "message-circle": '<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>',
3386
+ moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
3387
+ "panel-left": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>',
3388
+ "panel-left-close": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/>',
3389
+ "panel-right": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/>',
3390
+ "panel-right-close": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m8 9 3 3-3 3"/>',
3391
+ paperclip: '<path d="m16 6-8.41 8.41a2 2 0 0 0 2.83 2.83L18.83 8.83a4 4 0 0 0-5.66-5.66L4.76 11.59a6 6 0 0 0 8.49 8.49L21.66 11.66"/>',
3392
+ "puzzle-piece": '<path d="M15.39 4.39a2.1 2.1 0 0 0-2.97 0L12 4.82l-.42-.43a2.1 2.1 0 1 0-2.97 2.97l.43.42L7.6 9.22H4.75A1.75 1.75 0 0 0 3 10.97v8.28C3 20.22 3.78 21 4.75 21h8.28c.97 0 1.75-.78 1.75-1.75V16.4l1.44-1.44.42.43a2.1 2.1 0 1 0 2.97-2.97l-.43-.42.43-.42a2.1 2.1 0 1 0-2.97-2.97l-.42.43-1.44-1.44.61-.61a2.1 2.1 0 0 0 0-2.97Z"/>',
3393
+ python: '<path d="M12 2h4a4 4 0 0 1 4 4v3H8a4 4 0 0 0-4 4v1"/><path d="M12 22H8a4 4 0 0 1-4-4v-3h12a4 4 0 0 0 4-4v-1"/><path d="M9 6h.01"/><path d="M15 18h.01"/>',
3394
+ rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
3395
+ rss: '<path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/>',
3396
+ search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
3397
+ sparkles: '<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/><path d="M5 3v4"/><path d="M3 5h4"/><path d="M19 17v4"/><path d="M17 19h4"/>',
3398
+ star: '<path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01Z"/>',
3399
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
3400
+ "text-align-start": '<path d="M4 6h16"/><path d="M4 10h10"/><path d="M4 14h16"/><path d="M4 18h10"/>',
3401
+ "thumbs-down": '<path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/>',
3402
+ "thumbs-up": '<path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/>',
3403
+ "triangle-alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
3404
+ x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
3405
+ };
3406
+ var iconAliases = {
3407
+ "alien-8bit": "sparkles",
3408
+ "arrow-up-right-from-square": "external-link",
3409
+ "book-open-reader": "book-open",
3410
+ "circle-info": "info",
3411
+ close: "x",
3412
+ "external-link-alt": "external-link",
3413
+ "fa-github": "github",
3414
+ "fa-linkedin": "linkedin",
3415
+ "fa-x-twitter": "brand-x",
3416
+ "file-lines": "file",
3417
+ javascript: "js",
3418
+ "panel-left-open": "panel-left",
3419
+ "panel-right-open": "panel-right",
3420
+ times: "x",
3421
+ "x-twitter": "brand-x"
3422
+ };
3423
+ var libraryPrefixes = [
3424
+ "fa-brands",
3425
+ "fa-duotone",
3426
+ "fa-light",
3427
+ "fa-regular",
3428
+ "fa-sharp-solid",
3429
+ "fa-solid",
3430
+ "fa-thin",
3431
+ "fa",
3432
+ "fab",
3433
+ "fad",
3434
+ "fal",
3435
+ "far",
3436
+ "fas",
3437
+ "fat",
3438
+ "lucide",
3439
+ "tabler",
3440
+ "ti"
3441
+ ];
3442
+ var normalizedIconName = (name) => name.trim().toLowerCase().replaceAll(/[\s_]+/gu, "-");
3443
+ var isString = (value) => typeof value === "string";
3444
+ var withoutLibraryPrefix = (name) => {
3445
+ let normalized = normalizedIconName(name).replaceAll(/^icon-/gu, "");
3446
+ let changed = true;
3447
+ while (changed) {
3448
+ changed = false;
3449
+ for (const prefix of libraryPrefixes) {
3450
+ if (normalized.startsWith(`${prefix}-`)) {
3451
+ normalized = normalized.slice(prefix.length + 1);
3452
+ changed = true;
3453
+ }
3454
+ if (normalized.startsWith(`${prefix}:`)) {
3455
+ normalized = normalized.slice(prefix.length + 1);
3456
+ changed = true;
3457
+ }
3458
+ }
3459
+ }
3460
+ return normalized;
3461
+ };
3462
+ var resolveIcon = (name, iconType) => {
3463
+ const normalized = normalizedIconName(name);
3464
+ const stripped = withoutLibraryPrefix(name);
3465
+ const type = iconType ? normalizedIconName(iconType) : null;
3466
+ const candidates = [
3467
+ normalized,
3468
+ stripped,
3469
+ type ? `${type}-${stripped}` : null,
3470
+ iconAliases[normalized],
3471
+ iconAliases[stripped]
3472
+ ].filter(isString);
3473
+ for (const candidate of candidates) {
3474
+ const markup = icons[candidate];
3475
+ if (markup) {
3476
+ return { markup, name: candidate };
3477
+ }
3478
+ }
3479
+ return null;
3480
+ };
3481
+ var hasIcon = (name, iconType) => resolveIcon(name, iconType) !== null;
3482
+
3483
+ // src/core/nav-diagnostics.ts
3484
+ var IMAGE_ICON = /^(?:https?:\/\/|data:image\/|\/|\.{1,2}\/)|\.(?:avif|gif|jpe?g|png|svg|webp)$/iu;
3485
+ var isAssetIcon = (value) => value.startsWith("<") || IMAGE_ICON.test(value);
3486
+ var flattenNodes = (nodes) => nodes.flatMap((node) => node.kind === "group" ? [node, ...flattenNodes(node.children)] : [node]);
3487
+ var collectIcons = (navigation) => {
3488
+ const icons2 = [];
3489
+ const push = (icon, where) => {
3490
+ if (icon) {
3491
+ icons2.push({ icon, where });
3492
+ }
3493
+ };
3494
+ for (const tab of navigation.tabs) {
3495
+ push(tab.icon, `tab "${tab.label}"`);
3496
+ for (const item of tab.items ?? []) {
3497
+ push(item.icon, `tab item "${item.label}"`);
3498
+ }
3499
+ }
3500
+ for (const selector of navigation.selectors) {
3501
+ for (const item of selector.items) {
3502
+ push(item.icon, `selector "${item.label}"`);
3503
+ }
3504
+ }
3505
+ const sidebars = [
3506
+ navigation.sidebar,
3507
+ ...navigation.sidebarVariants.map((variant) => variant.sidebar)
3508
+ ];
3509
+ for (const sidebar of sidebars) {
3510
+ for (const node of flattenNodes(sidebar)) {
3511
+ push(node.icon, `"${node.label}"`);
3512
+ }
3513
+ }
3514
+ return icons2;
3515
+ };
3516
+ var validateNavIcons = (navigation) => {
3517
+ const seen = new Set;
3518
+ const diagnostics = [];
3519
+ for (const { icon, where } of collectIcons(navigation)) {
3520
+ if (isAssetIcon(icon) || hasIcon(icon) || seen.has(icon)) {
3521
+ continue;
3522
+ }
3523
+ seen.add(icon);
3524
+ diagnostics.push({
3525
+ code: "BLUME_UNKNOWN_ICON",
3526
+ message: `Unknown icon "${icon}" (${where}) — it isn't in Blume's icon set.`,
3527
+ severity: "warning",
3528
+ suggestion: "Use a built-in icon name, an image path/URL, or inline SVG markup."
3529
+ });
3530
+ }
3531
+ return diagnostics;
3532
+ };
3533
+ var resolvesToPages = (routes, path) => routes.has(path) || [...routes].some((route) => route.startsWith(`${path}/`));
3534
+ var validateNavTargets = (navigation, routes) => {
3535
+ const targets = [
3536
+ ...navigation.tabs.map((tab) => ({ label: tab.label, path: tab.path })),
3537
+ ...navigation.selectors.flatMap((selector) => selector.items.map((item) => ({ label: item.label, path: item.path })))
3538
+ ];
3539
+ const diagnostics = [];
3540
+ const seen = new Set;
3541
+ for (const { label, path } of targets) {
3542
+ if (!path.startsWith("/") || path.startsWith("/#") || seen.has(path)) {
3543
+ continue;
3544
+ }
3545
+ if (!resolvesToPages(routes, path.split("#")[0] ?? path)) {
3546
+ seen.add(path);
3547
+ diagnostics.push({
3548
+ code: "BLUME_NAV_MISSING_PAGE",
3549
+ message: `Navigation entry "${label}" points to ${path}, but no page matches it.`,
3550
+ severity: "warning",
3551
+ suggestion: "Fix the path, or add a page at that route."
3552
+ });
3553
+ }
3554
+ }
3555
+ return diagnostics;
3556
+ };
3557
+ var duplicateLabelDiagnostics = (navigation) => {
3558
+ const diagnostics = [];
3559
+ const checkLevel = (nodes, where) => {
3560
+ const counts = new Map;
3561
+ for (const node of nodes) {
3562
+ counts.set(node.label, (counts.get(node.label) ?? 0) + 1);
3563
+ }
3564
+ for (const [label, count] of counts) {
3565
+ if (count > 1) {
3566
+ diagnostics.push({
3567
+ code: "BLUME_NAV_DUPLICATE_LABEL",
3568
+ message: `Duplicate sidebar label "${label}" appears ${count} times ${where}.`,
3569
+ severity: "warning",
3570
+ suggestion: "Give the entries distinct titles."
3571
+ });
3572
+ }
3573
+ }
3574
+ for (const node of nodes) {
3575
+ if (node.kind === "group") {
3576
+ checkLevel(node.children, `under "${node.label}"`);
3577
+ }
3578
+ }
3579
+ };
3580
+ const sidebars = [
3581
+ { nodes: navigation.sidebar, where: "at the top level" },
3582
+ ...navigation.sidebarVariants.map((variant) => ({
3583
+ nodes: variant.sidebar,
3584
+ where: `in the "${variant.path}" section`
3585
+ }))
3586
+ ];
3587
+ for (const { nodes, where } of sidebars) {
3588
+ checkLevel(nodes, where);
3589
+ }
3590
+ return diagnostics;
3591
+ };
3592
+ var hiddenInSidebarDiagnostics = (navigation, pages) => {
3593
+ const hidden = new Set(pages.filter((page) => page.meta.sidebar.hidden).map((page) => page.id));
3594
+ if (hidden.size === 0) {
3595
+ return [];
3596
+ }
3597
+ const sidebars = [
3598
+ navigation.sidebar,
3599
+ ...navigation.sidebarVariants.map((variant) => variant.sidebar)
3600
+ ];
3601
+ const diagnostics = [];
3602
+ const seen = new Set;
3603
+ for (const sidebar of sidebars) {
3604
+ for (const node of flattenNodes(sidebar)) {
3605
+ if (node.kind === "page" && hidden.has(node.pageId) && !seen.has(node.pageId)) {
3606
+ seen.add(node.pageId);
3607
+ diagnostics.push({
3608
+ code: "BLUME_NAV_HIDDEN_IN_SIDEBAR",
3609
+ message: `Page "${node.label}" is marked hidden but appears in the sidebar (and its pagination).`,
3610
+ severity: "warning",
3611
+ suggestion: "Remove it from the navigation config, or unset sidebar.hidden."
3612
+ });
3613
+ }
3614
+ }
3615
+ }
3616
+ return diagnostics;
3617
+ };
3618
+ var validateNavStructure = (navigation, pages) => [
3619
+ ...duplicateLabelDiagnostics(navigation),
3620
+ ...hiddenInSidebarDiagnostics(navigation, pages)
3621
+ ];
3622
+
2776
3623
  // src/core/tsconfig-aliases.ts
2777
- import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "node:fs";
3624
+ import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "node:fs";
2778
3625
  import { createRequire as createRequire2 } from "node:module";
2779
3626
  import { pathToFileURL } from "node:url";
2780
- import { dirname as dirname4, isAbsolute, join as join6, resolve as resolve2 } from "pathe";
3627
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "pathe";
2781
3628
  var stripJsonComments = (text) => {
2782
3629
  let out = "";
2783
3630
  let inString = false;
@@ -2831,11 +3678,11 @@ var isFile = (path) => {
2831
3678
  }
2832
3679
  };
2833
3680
  var resolveExtends = (spec, fromDir) => {
2834
- if (spec.startsWith(".") || isAbsolute(spec)) {
2835
- const candidates = spec.endsWith(".json") ? [resolve2(fromDir, spec)] : [
2836
- resolve2(fromDir, `${spec}.json`),
2837
- resolve2(fromDir, spec, "tsconfig.json"),
2838
- resolve2(fromDir, spec)
3681
+ if (spec.startsWith(".") || isAbsolute2(spec)) {
3682
+ const candidates = spec.endsWith(".json") ? [resolve3(fromDir, spec)] : [
3683
+ resolve3(fromDir, `${spec}.json`),
3684
+ resolve3(fromDir, spec, "tsconfig.json"),
3685
+ resolve3(fromDir, spec)
2839
3686
  ];
2840
3687
  return candidates.find(isFile) ?? null;
2841
3688
  }
@@ -2850,7 +3697,7 @@ var resolveExtends = (spec, fromDir) => {
2850
3697
  return null;
2851
3698
  };
2852
3699
  var loadPaths = (file, seen) => {
2853
- if (seen.has(file) || !existsSync3(file)) {
3700
+ if (seen.has(file) || !existsSync4(file)) {
2854
3701
  return null;
2855
3702
  }
2856
3703
  seen.add(file);
@@ -2862,7 +3709,7 @@ var loadPaths = (file, seen) => {
2862
3709
  if (options.paths && typeof options.paths === "object") {
2863
3710
  const baseUrl = typeof options.baseUrl === "string" ? options.baseUrl : ".";
2864
3711
  return {
2865
- baseDir: resolve2(dirname4(file), baseUrl),
3712
+ baseDir: resolve3(dirname5(file), baseUrl),
2866
3713
  paths: options.paths
2867
3714
  };
2868
3715
  }
@@ -2871,7 +3718,7 @@ var loadPaths = (file, seen) => {
2871
3718
  if (typeof base !== "string") {
2872
3719
  continue;
2873
3720
  }
2874
- const resolved = resolveExtends(base, dirname4(file));
3721
+ const resolved = resolveExtends(base, dirname5(file));
2875
3722
  const found = resolved ? loadPaths(resolved, seen) : null;
2876
3723
  if (found) {
2877
3724
  return found;
@@ -2889,10 +3736,10 @@ var toAlias = (key, value, baseDir) => {
2889
3736
  if (find === "" || find === "*") {
2890
3737
  return null;
2891
3738
  }
2892
- return { find, replacement: resolve2(baseDir, target) };
3739
+ return { find, replacement: resolve3(baseDir, target) };
2893
3740
  };
2894
3741
  var resolveTsconfigAliases = (root) => {
2895
- const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join6(root, name)).find((file) => existsSync3(file));
3742
+ const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join6(root, name)).find((file) => existsSync4(file));
2896
3743
  if (!entry) {
2897
3744
  return {};
2898
3745
  }
@@ -2991,11 +3838,11 @@ ${items}
2991
3838
 
2992
3839
  // src/openapi/scalar.ts
2993
3840
  import { readFile as readFile4 } from "node:fs/promises";
2994
- import { isAbsolute as isAbsolute2, join as join8 } from "pathe";
3841
+ import { isAbsolute as isAbsolute3, join as join8 } from "pathe";
2995
3842
 
2996
3843
  // src/astro/templates.ts
2997
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
2998
- import { dirname as dirname5, join as join7 } from "pathe";
3844
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
3845
+ import { dirname as dirname6, join as join7 } from "pathe";
2999
3846
 
3000
3847
  // src/theme/fonts.ts
3001
3848
  var FALLBACKS = {
@@ -3153,7 +4000,7 @@ var WORKSPACE_MARKERS = [
3153
4000
  "yarn.lock"
3154
4001
  ];
3155
4002
  var hasWorkspacesField = (pkgPath) => {
3156
- if (!existsSync4(pkgPath)) {
4003
+ if (!existsSync5(pkgPath)) {
3157
4004
  return false;
3158
4005
  }
3159
4006
  try {
@@ -3163,14 +4010,14 @@ var hasWorkspacesField = (pkgPath) => {
3163
4010
  return false;
3164
4011
  }
3165
4012
  };
3166
- var hasWorkspaceMarker = (dir) => hasWorkspacesField(join7(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync4(join7(dir, marker)));
4013
+ var hasWorkspaceMarker = (dir) => hasWorkspacesField(join7(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync5(join7(dir, marker)));
3167
4014
  var findWorkspaceRoot = (start) => {
3168
4015
  let dir = start;
3169
4016
  for (;; ) {
3170
4017
  if (hasWorkspaceMarker(dir)) {
3171
4018
  return dir;
3172
4019
  }
3173
- const parent = dirname5(dir);
4020
+ const parent = dirname6(dir);
3174
4021
  if (parent === dir) {
3175
4022
  return start;
3176
4023
  }
@@ -3385,7 +4232,7 @@ ${stagedBlock}
3385
4232
  export const collections = { docs${options.staged ? ", staged" : ""} };
3386
4233
  `;
3387
4234
  };
3388
- var askEndpointTemplate = (backend) => {
4235
+ var askEndpointTemplate = (backend, grounded) => {
3389
4236
  const imports = [
3390
4237
  'import type { APIRoute } from "astro";',
3391
4238
  'import { streamText } from "ai";'
@@ -3409,13 +4256,24 @@ const provider = createOpenAICompatible({
3409
4256
  `;
3410
4257
  modelExpr = `provider(${JSON.stringify(backend.model)})`;
3411
4258
  }
3412
- return `// Generated by Blume. Do not edit.
3413
- ${imports.join(`
3414
- `)}
3415
-
3416
- export const prerender = false;
3417
- ${setup}
3418
- export const POST: APIRoute = async ({ request }) => {
4259
+ if (grounded) {
4260
+ imports.push('import { createAskContext } from "blume/ai/ask-context.ts";', 'import askData from "../generated/ask-data.json";');
4261
+ setup += `
4262
+ const ground = createAskContext(askData);
4263
+ `;
4264
+ }
4265
+ const handler = grounded ? `export const POST: APIRoute = async ({ request }) => {
4266
+ const { messages, page } = await request.json();
4267
+ const system =
4268
+ (await ground(messages, page)) ??
4269
+ "You are a helpful documentation assistant. Answer using the project's documentation.";
4270
+ const result = streamText({
4271
+ model: ${modelExpr},
4272
+ system,
4273
+ messages,
4274
+ });
4275
+ return result.toTextStreamResponse();
4276
+ };` : `export const POST: APIRoute = async ({ request }) => {
3419
4277
  const { messages } = await request.json();
3420
4278
  const result = streamText({
3421
4279
  model: ${modelExpr},
@@ -3424,7 +4282,14 @@ export const POST: APIRoute = async ({ request }) => {
3424
4282
  messages,
3425
4283
  });
3426
4284
  return result.toTextStreamResponse();
3427
- };
4285
+ };`;
4286
+ return `// Generated by Blume. Do not edit.
4287
+ ${imports.join(`
4288
+ `)}
4289
+
4290
+ export const prerender = false;
4291
+ ${setup}
4292
+ ${handler}
3428
4293
  `;
3429
4294
  };
3430
4295
  var searchEndpointTemplate = () => `// Generated by Blume. Do not edit.
@@ -3611,31 +4476,47 @@ const customRoutes = ${JSON.stringify(customRoutes)};
3611
4476
  export function getStaticPaths() {
3612
4477
  const seen = new Set();
3613
4478
  const paths = [];
3614
- const add = (slug, title, eyebrow) => {
4479
+ const add = (slug, title) => {
3615
4480
  if (seen.has(slug)) {
3616
4481
  return;
3617
4482
  }
3618
4483
  seen.add(slug);
3619
- paths.push({ params: { slug }, props: { eyebrow, title } });
4484
+ paths.push({ params: { slug }, props: { title } });
3620
4485
  };
3621
4486
  // A custom page wins over a content route sharing its path, so add it first.
3622
4487
  for (const route of customRoutes) {
3623
- add(route.slug, route.title, route.eyebrow);
4488
+ add(route.slug, route.title);
3624
4489
  }
3625
4490
  for (const route of data.routes) {
3626
- add(
3627
- route.path === "/" ? "index" : route.path.slice(1),
3628
- route.title,
3629
- data.config.title
3630
- );
4491
+ add(route.path === "/" ? "index" : route.path.slice(1), route.title);
3631
4492
  }
3632
4493
  return paths;
3633
4494
  }
3634
4495
 
4496
+ // Footer branding shared by every card, derived once from the resolved config.
4497
+ // The repo slug reuses the header link URL; the host comes from the site URL.
4498
+ const repoSlug = data.config.repoUrl
4499
+ ? data.config.repoUrl.split("github.com/")[1]
4500
+ : undefined;
4501
+ const siteHost = (() => {
4502
+ if (!data.config.site) {
4503
+ return undefined;
4504
+ }
4505
+ try {
4506
+ return new URL(data.config.site).host;
4507
+ } catch {
4508
+ return undefined;
4509
+ }
4510
+ })();
4511
+
3635
4512
  export async function GET({ props }) {
3636
4513
  const png = await renderOgImage({
3637
4514
  accent: data.config.theme.accent,
3638
- eyebrow: props.eyebrow,
4515
+ brand: data.config.title,
4516
+ description: data.config.description,
4517
+ logo: data.config.logo?.svg,
4518
+ repo: repoSlug,
4519
+ site: siteHost,
3639
4520
  title: props.title,
3640
4521
  });
3641
4522
  return new Response(png, {
@@ -3683,10 +4564,13 @@ var catchAllPageTemplate = (options) => {
3683
4564
  ` : "";
3684
4565
  const mathEntry = options.mathEnabled ? `Math,
3685
4566
  ` : "";
4567
+ const clientData = options.needsReact ? `
4568
+ clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}` : "";
3686
4569
  return `---
3687
4570
  // Generated by Blume. Do not edit.
3688
4571
  import { getEntry, render } from "astro:content";
3689
4572
  import RootLayout from "blume/components/layout/RootLayout.astro";
4573
+ import { resolveSlot } from "blume/components/layout/overrides.ts";
3690
4574
  ${askImport}
3691
4575
  import Accordion from "blume/components/content/Accordion.astro";
3692
4576
  import AccordionItem from "blume/components/content/AccordionItem.astro";
@@ -3862,11 +4746,15 @@ const localeSwitch = i18n
3862
4746
  };
3863
4747
  })
3864
4748
  : [];
4749
+
4750
+ // The whole page shell is overridable via \`layout.Layout\`; it receives the same
4751
+ // props as the built-in RootLayout, plus the \`layout\` map for its inner slots.
4752
+ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
3865
4753
  ---
3866
4754
 
3867
- <RootLayout
4755
+ <LayoutComponent
3868
4756
  site={{ title: data.config.title, description: data.config.description }}
3869
- layout={layoutOverrides}
4757
+ layout={layoutOverrides}${clientData}
3870
4758
  logo={data.config.logo}
3871
4759
  mcp={data.config.mcp}
3872
4760
  favicon={data.config.favicon}
@@ -3885,6 +4773,7 @@ const localeSwitch = i18n
3885
4773
  localeSwitch={localeSwitch}
3886
4774
  page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
3887
4775
  headings={headings}
4776
+ toc={data.config.toc}
3888
4777
  themeMode={data.config.theme.mode}
3889
4778
  fontCssVars={data.fontCssVars}
3890
4779
  searchEnabled={data.config.search.enabled}
@@ -3907,7 +4796,7 @@ const localeSwitch = i18n
3907
4796
  <h1>{title}</h1>
3908
4797
  {frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
3909
4798
  <Content components={components} />
3910
- </RootLayout>
4799
+ </LayoutComponent>
3911
4800
  `;
3912
4801
  };
3913
4802
  var changelogIndexTemplate = (options) => {
@@ -3915,11 +4804,16 @@ var changelogIndexTemplate = (options) => {
3915
4804
  ` : "";
3916
4805
  const askSlot = options.askEnabled ? `
3917
4806
  <AskAI slot="ask" />` : "";
4807
+ const clientData = options.needsReact ? `
4808
+ clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: data.config.title + " changelog" } }}` : "";
4809
+ const stagedSpread = options.staged ? `
4810
+ ...(await getCollection("staged")),` : "";
3918
4811
  return `---
3919
4812
  // Generated by Blume. Do not edit.
3920
4813
  import { getCollection, render } from "astro:content";
3921
4814
  import RootLayout from "blume/components/layout/RootLayout.astro";
3922
4815
  import Update from "blume/components/content/Update.astro";
4816
+ import { resolveSlot } from "blume/components/layout/overrides.ts";
3923
4817
  import { layoutOverrides } from "../generated/components.ts";
3924
4818
  ${askImport}import data from "../generated/data.json";
3925
4819
 
@@ -3953,7 +4847,9 @@ const slugify = (text) =>
3953
4847
  text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
3954
4848
  "update";
3955
4849
 
3956
- const changelogEntries = (await getCollection("docs"))
4850
+ const changelogEntries = [
4851
+ ...(await getCollection("docs")),${stagedSpread}
4852
+ ]
3957
4853
  .filter(
3958
4854
  (entry) =>
3959
4855
  entry.data.type === "changelog" &&
@@ -3989,11 +4885,13 @@ const headings = items.map((item) => ({
3989
4885
 
3990
4886
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
3991
4887
  const canonical = base ? base + "/changelog" : null;
4888
+
4889
+ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
3992
4890
  ---
3993
4891
 
3994
- <RootLayout
4892
+ <LayoutComponent
3995
4893
  site={{ title: data.config.title, description: data.config.description }}
3996
- layout={layoutOverrides}
4894
+ layout={layoutOverrides}${clientData}
3997
4895
  logo={data.config.logo}
3998
4896
  mcp={data.config.mcp}
3999
4897
  favicon={data.config.favicon}
@@ -4009,6 +4907,7 @@ const canonical = base ? base + "/changelog" : null;
4009
4907
  route: "/changelog",
4010
4908
  }}
4011
4909
  headings={headings}
4910
+ toc={data.config.toc}
4012
4911
  themeMode={data.config.theme.mode}
4013
4912
  fontCssVars={data.fontCssVars}
4014
4913
  searchEnabled={data.config.search.enabled}
@@ -4037,7 +4936,7 @@ const canonical = base ? base + "/changelog" : null;
4037
4936
  </div>
4038
4937
  )
4039
4938
  }
4040
- </RootLayout>
4939
+ </LayoutComponent>
4041
4940
  `;
4042
4941
  };
4043
4942
  var notFoundPageTemplate = () => `---
@@ -4078,19 +4977,6 @@ const nf = data.ui.notFound;
4078
4977
  </div>
4079
4978
  </PageLayout>
4080
4979
  `;
4081
- var userComponentsTemplate = (componentsFile) => {
4082
- if (!componentsFile) {
4083
- return `// Generated by Blume. Do not edit.
4084
- export const mdxComponents = {};
4085
- export const layoutOverrides = {};
4086
- `;
4087
- }
4088
- return `// Generated by Blume. Do not edit.
4089
- import overrides from ${JSON.stringify(componentsFile)};
4090
- export const mdxComponents = overrides.mdx ?? {};
4091
- export const layoutOverrides = overrides.layout ?? {};
4092
- `;
4093
- };
4094
4980
  var islandDirective = (spec) => spec.client === "only" ? `client:only="${spec.framework}"` : `client:${spec.client}`;
4095
4981
  var islandWrapperTemplate = (spec) => `---
4096
4982
  // Generated by Blume. Do not edit.
@@ -4152,6 +5038,12 @@ declare module "blume:data" {
4152
5038
  const data: import("blume").BlumeData;
4153
5039
  export default data;
4154
5040
  }
5041
+
5042
+ declare module "blume:search-client" {
5043
+ export const createSearch: () =>
5044
+ | import("blume/components/layout/search/types.ts").SearchFn
5045
+ | Promise<import("blume/components/layout/search/types.ts").SearchFn>;
5046
+ }
4155
5047
  `;
4156
5048
  var runtimePackageTemplate = (dependencies = []) => `${JSON.stringify({
4157
5049
  dependencies: Object.fromEntries([...dependencies].toSorted().map((name) => [name, "*"])),
@@ -4332,7 +5224,7 @@ var specConfiguration = async (spec, root) => {
4332
5224
  if (URL_SPEC.test(spec)) {
4333
5225
  return { config: { url: spec } };
4334
5226
  }
4335
- const absolute = isAbsolute2(spec) ? spec : join8(root, spec);
5227
+ const absolute = isAbsolute3(spec) ? spec : join8(root, spec);
4336
5228
  try {
4337
5229
  return { config: { content: await readFile4(absolute, "utf-8") } };
4338
5230
  } catch {
@@ -5071,9 +5963,106 @@ var twoslashCss = () => {
5071
5963
  ${OVERRIDES}`;
5072
5964
  };
5073
5965
 
5966
+ // src/astro/component-slots.ts
5967
+ var EMPTY_MODULE = `// Generated by Blume. Do not edit.
5968
+ export const mdxComponents = {};
5969
+ export const layoutOverrides = {};
5970
+ `;
5971
+ var directiveFor = (override) => {
5972
+ const framework = override.source?.framework;
5973
+ switch (override.client) {
5974
+ case "idle": {
5975
+ return "client:idle";
5976
+ }
5977
+ case "visible": {
5978
+ return "client:visible";
5979
+ }
5980
+ case "media": {
5981
+ return override.media ? `client:media="${override.media}"` : "client:load";
5982
+ }
5983
+ case "only": {
5984
+ return framework ? `client:only="${framework}"` : "client:load";
5985
+ }
5986
+ default: {
5987
+ return "client:load";
5988
+ }
5989
+ }
5990
+ };
5991
+ var importClause = (variable, name, path) => name === "default" ? `import ${variable} from ${JSON.stringify(path)};` : `import { ${name} as ${variable} } from ${JSON.stringify(path)};`;
5992
+ var wrapperContent = (override) => {
5993
+ const { name, path } = override.source;
5994
+ const clause = name === "default" ? `import Component from ${JSON.stringify(path)};` : `import { ${name} as Component } from ${JSON.stringify(path)};`;
5995
+ return `---
5996
+ // Generated by Blume. Do not edit.
5997
+ ${clause}
5998
+ ---
5999
+ <Component ${directiveFor(override)} {...Astro.props}><slot /></Component>
6000
+ `;
6001
+ };
6002
+ var sanitize = (value) => value.replaceAll(/[^A-Za-z0-9]/gu, "_");
6003
+ var planComponentSlots = (componentsFile, analysis) => {
6004
+ const frameworks = new Set;
6005
+ if (!componentsFile) {
6006
+ return { frameworks, module: EMPTY_MODULE, wrappers: [] };
6007
+ }
6008
+ if (!analysis) {
6009
+ return {
6010
+ frameworks,
6011
+ module: `// Generated by Blume. Do not edit.
6012
+ import overrides from ${JSON.stringify(componentsFile)};
6013
+ export const mdxComponents = overrides.mdx ?? {};
6014
+ export const layoutOverrides = overrides.layout ?? {};
6015
+ `,
6016
+ wrappers: []
6017
+ };
6018
+ }
6019
+ const wrappers = [];
6020
+ const importLines = [];
6021
+ const mdxEntries = [];
6022
+ const layoutEntries = [];
6023
+ let counter = 0;
6024
+ const mdxOverrides = [...analysis.mdx, ...analysis.islands];
6025
+ const plan = (override, group, entries) => {
6026
+ const { source } = override;
6027
+ if (source?.framework) {
6028
+ frameworks.add(source.framework);
6029
+ }
6030
+ if (override.identifier && !override.client) {
6031
+ return;
6032
+ }
6033
+ if (!source) {
6034
+ return;
6035
+ }
6036
+ const variable = `__blumeSlot${counter}`;
6037
+ counter += 1;
6038
+ if (override.client) {
6039
+ const name = `${group}-${sanitize(override.key)}`;
6040
+ wrappers.push({ content: wrapperContent(override), name });
6041
+ importLines.push(importClause(variable, "default", `./component-slots/${name}.astro`));
6042
+ } else {
6043
+ importLines.push(importClause(variable, source.name, source.path));
6044
+ }
6045
+ entries.push(`${JSON.stringify(override.key)}: ${variable}`);
6046
+ };
6047
+ for (const override of mdxOverrides) {
6048
+ plan(override, "mdx", mdxEntries);
6049
+ }
6050
+ for (const override of analysis.layout) {
6051
+ plan(override, "layout", layoutEntries);
6052
+ }
6053
+ const moduleSource = `// Generated by Blume. Do not edit.
6054
+ import overrides from ${JSON.stringify(componentsFile)};
6055
+ ${importLines.join(`
6056
+ `)}${importLines.length ? `
6057
+ ` : ""}export const mdxComponents = { ...(overrides.mdx ?? {})${mdxEntries.length ? `, ${mdxEntries.join(", ")}` : ""} };
6058
+ export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.length ? `, ${layoutEntries.join(", ")}` : ""} };
6059
+ `;
6060
+ return { frameworks, module: moduleSource, wrappers };
6061
+ };
6062
+
5074
6063
  // src/astro/examples.ts
5075
6064
  import { readFile as readFile6 } from "node:fs/promises";
5076
- import { join as join10, relative as relative3 } from "pathe";
6065
+ import { join as join10, relative as relative4 } from "pathe";
5077
6066
  import { glob as glob2 } from "tinyglobby";
5078
6067
 
5079
6068
  // src/astro/islands.ts
@@ -5087,7 +6076,7 @@ var VALID_MODES = new Set([
5087
6076
  "only",
5088
6077
  "visible"
5089
6078
  ]);
5090
- var FRAMEWORK_BY_EXT = {
6079
+ var FRAMEWORK_BY_EXT2 = {
5091
6080
  jsx: "react",
5092
6081
  svelte: "svelte",
5093
6082
  tsx: "react",
@@ -5121,7 +6110,7 @@ var discoverIslands = async (root) => {
5121
6110
  for (const [index, file] of files.entries()) {
5122
6111
  const base = basename(file);
5123
6112
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
5124
- const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
6113
+ const framework = ext ? FRAMEWORK_BY_EXT2[ext] : undefined;
5125
6114
  if (!framework) {
5126
6115
  continue;
5127
6116
  }
@@ -5147,7 +6136,7 @@ var discoverIslands = async (root) => {
5147
6136
  };
5148
6137
 
5149
6138
  // src/astro/examples.ts
5150
- var FRAMEWORK_BY_EXT2 = {
6139
+ var FRAMEWORK_BY_EXT3 = {
5151
6140
  astro: "astro",
5152
6141
  jsx: "react",
5153
6142
  svelte: "svelte",
@@ -5183,11 +6172,11 @@ var discoverExamples = async (root, pattern = "examples") => {
5183
6172
  const seen = new Map;
5184
6173
  for (const [index, file] of files.entries()) {
5185
6174
  const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
5186
- const framework = ext ? FRAMEWORK_BY_EXT2[ext] : undefined;
6175
+ const framework = ext ? FRAMEWORK_BY_EXT3[ext] : undefined;
5187
6176
  if (!(ext && framework)) {
5188
6177
  continue;
5189
6178
  }
5190
- const path = relative3(dir, file).slice(0, -(ext.length + 1));
6179
+ const path = relative4(dir, file).slice(0, -(ext.length + 1));
5191
6180
  const existing = seen.get(path);
5192
6181
  if (existing) {
5193
6182
  warnings.push(`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`);
@@ -5208,7 +6197,7 @@ var discoverExamples = async (root, pattern = "examples") => {
5208
6197
  };
5209
6198
 
5210
6199
  // src/astro/pages.ts
5211
- import { extname, relative as relative4 } from "pathe";
6200
+ import { extname as extname2, relative as relative5 } from "pathe";
5212
6201
  import { glob as glob3 } from "tinyglobby";
5213
6202
  var discoverPages = async (pagesRoot) => {
5214
6203
  const files = await glob3(["**/*.astro"], {
@@ -5218,8 +6207,8 @@ var discoverPages = async (pagesRoot) => {
5218
6207
  });
5219
6208
  files.sort();
5220
6209
  return files.map((file) => {
5221
- const rel = relative4(pagesRoot, file);
5222
- const withoutExt = rel.slice(0, rel.length - extname(rel).length);
6210
+ const rel = relative5(pagesRoot, file);
6211
+ const withoutExt = rel.slice(0, rel.length - extname2(rel).length);
5223
6212
  const parts = withoutExt.split("/").filter((part) => part !== "index");
5224
6213
  const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
5225
6214
  return { entrypoint: file, pattern };
@@ -5228,7 +6217,7 @@ var discoverPages = async (pagesRoot) => {
5228
6217
  var routeIsTaken = (pages, contentPages, route) => pages.some((page) => page.pattern === route) || contentPages.some((page) => page.route === route);
5229
6218
  var PRIVATE_SEGMENT = /^[._]/u;
5230
6219
  var humanizeSegment = (segment) => segment.split(/[-_]/u).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
5231
- var customOgRoutes = (pages, siteTitle, siteDescription) => {
6220
+ var customOgRoutes = (pages, siteTitle) => {
5232
6221
  const seen = new Set;
5233
6222
  const routes = [];
5234
6223
  for (const { pattern } of pages) {
@@ -5242,7 +6231,7 @@ var customOgRoutes = (pages, siteTitle, siteDescription) => {
5242
6231
  }
5243
6232
  seen.add(slug);
5244
6233
  const last = segments.at(-1);
5245
- routes.push(last ? { eyebrow: siteTitle, slug, title: humanizeSegment(last) } : { eyebrow: siteDescription, slug, title: siteTitle });
6234
+ routes.push({ slug, title: last ? humanizeSegment(last) : siteTitle });
5246
6235
  }
5247
6236
  return routes;
5248
6237
  };
@@ -5266,8 +6255,8 @@ var resolvedAstroPath = (fromDir) => {
5266
6255
  }
5267
6256
  };
5268
6257
  var blumeDepsDir = (pkgDir = packageRoot()) => {
5269
- const candidates = [join11(pkgDir, "node_modules"), dirname6(pkgDir)];
5270
- return candidates.find((dir) => existsSync5(join11(dir, "astro"))) ?? null;
6258
+ const candidates = [join11(pkgDir, "node_modules"), dirname7(pkgDir)];
6259
+ return candidates.find((dir) => existsSync6(join11(dir, "astro"))) ?? null;
5271
6260
  };
5272
6261
  var linkDepsJunction = async (link, depsDir) => {
5273
6262
  const existing = await lstat(link).catch(() => null);
@@ -5277,7 +6266,7 @@ var linkDepsJunction = async (link, depsDir) => {
5277
6266
  }
5278
6267
  await rm(link, { force: true });
5279
6268
  }
5280
- await mkdir2(dirname6(link), { recursive: true });
6269
+ await mkdir2(dirname7(link), { recursive: true });
5281
6270
  await symlink(depsDir, link, "junction");
5282
6271
  };
5283
6272
  var readPkgVersion = (pkgJsonPath) => {
@@ -5307,7 +6296,7 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
5307
6296
  if (blumeAstro && outDirAstro === blumeAstro) {
5308
6297
  return null;
5309
6298
  }
5310
- if (existsSync5(join11(depsDir, "@astrojs", "mdx"))) {
6299
+ if (existsSync6(join11(depsDir, "@astrojs", "mdx"))) {
5311
6300
  await linkDepsJunction(join11(outDir, "node_modules"), depsDir);
5312
6301
  return null;
5313
6302
  }
@@ -5355,7 +6344,7 @@ var writeIfChanged = async (path, content) => {
5355
6344
  if (existing === content) {
5356
6345
  return false;
5357
6346
  }
5358
- await mkdir2(dirname6(path), { recursive: true });
6347
+ await mkdir2(dirname7(path), { recursive: true });
5359
6348
  const tmp = `${path}.${process.pid}.tmp`;
5360
6349
  await writeFile2(tmp, content, "utf-8");
5361
6350
  try {
@@ -5391,7 +6380,7 @@ var writeStagedContent = async (out, staged) => {
5391
6380
  written.add(normalize(path));
5392
6381
  await writeIfChanged(path, text);
5393
6382
  }));
5394
- if (existsSync5(contentDir)) {
6383
+ if (existsSync6(contentDir)) {
5395
6384
  await pruneOrphans(contentDir, written);
5396
6385
  }
5397
6386
  };
@@ -5410,7 +6399,7 @@ var resolveLogo = (project) => {
5410
6399
  const file = [
5411
6400
  join11(project.context.root, "public", rel),
5412
6401
  join11(project.context.root, rel)
5413
- ].find((path) => existsSync5(path));
6402
+ ].find((path) => existsSync6(path));
5414
6403
  if (file) {
5415
6404
  return { alt, href, svg: readFileSync5(file, "utf-8") };
5416
6405
  }
@@ -5450,13 +6439,13 @@ var APPLE_ICON_CANDIDATES = [
5450
6439
  var resolveIconFile = (project, candidates) => {
5451
6440
  const { root } = project.context;
5452
6441
  for (const name of candidates) {
5453
- if (existsSync5(join11(root, "public", name))) {
6442
+ if (existsSync6(join11(root, "public", name))) {
5454
6443
  return { href: `/${name}`, type: faviconType(name) };
5455
6444
  }
5456
6445
  }
5457
6446
  for (const name of candidates) {
5458
6447
  const file = join11(root, name);
5459
- if (existsSync5(file)) {
6448
+ if (existsSync6(file)) {
5460
6449
  const type = faviconType(name);
5461
6450
  return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
5462
6451
  }
@@ -5489,7 +6478,7 @@ var buildRuntimeData = (project) => {
5489
6478
  if (!(editBase && sourcePath)) {
5490
6479
  return null;
5491
6480
  }
5492
- const rel = relative5(context.root, sourcePath).split("\\").join("/");
6481
+ const rel = relative6(context.root, sourcePath).split("\\").join("/");
5493
6482
  return `${editBase}/${github?.dir ? `${github.dir}/${rel}` : rel}`;
5494
6483
  };
5495
6484
  const { i18n } = config;
@@ -5550,7 +6539,8 @@ var buildRuntimeData = (project) => {
5550
6539
  site: config.deployment.site ?? null,
5551
6540
  structuredData: config.seo.structuredData,
5552
6541
  theme: config.theme,
5553
- title: config.title
6542
+ title: config.title,
6543
+ toc: config.toc
5554
6544
  },
5555
6545
  feeds: buildRssFeeds(project).map((feed) => ({
5556
6546
  href: feed.path,
@@ -5637,12 +6627,38 @@ var writeMcpFiles = async (project, plan, write) => {
5637
6627
  write(join11(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
5638
6628
  ]);
5639
6629
  };
6630
+ var writeAskFiles = async (project, srcDir, write) => {
6631
+ const { ask } = project.config.ai;
6632
+ if (!ask?.enabled) {
6633
+ return;
6634
+ }
6635
+ const grounded = ask.provider !== "inkeep";
6636
+ if (grounded) {
6637
+ await write(join11(srcDir, "generated", "ask-data.json"), `${JSON.stringify(await buildAskData(project))}
6638
+ `);
6639
+ }
6640
+ await write(join11(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded));
6641
+ };
5640
6642
  var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
5641
6643
  if (routeIsTaken(pages, contentPages, "/404")) {
5642
6644
  return;
5643
6645
  }
5644
6646
  await write(join11(srcDir, "pages", "404.astro"), notFoundPageTemplate());
5645
6647
  };
6648
+ var shouldGenerateChangelog = (project) => {
6649
+ const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
6650
+ const hasChangelogSource = (project.config.content.sources ?? []).some((source) => source.type === "github-releases");
6651
+ const changelogRouteTaken = project.graph.pages.some((page) => page.route === "/changelog");
6652
+ return (hasChangelog || hasChangelogSource) && !changelogRouteTaken;
6653
+ };
6654
+ var buildComponentSlots = async (componentsFile) => {
6655
+ const analysis = componentsFile ? analyzeComponentOverrides(await readFile7(componentsFile, "utf-8"), componentsFile) : null;
6656
+ return {
6657
+ plan: planComponentSlots(componentsFile, analysis),
6658
+ tags: analysis ? [...analysis.mdx, ...analysis.islands].map((entry) => entry.key) : [],
6659
+ warnings: analysis ? analysis.warnings : []
6660
+ };
6661
+ };
5646
6662
  var generateRuntime = async (project) => {
5647
6663
  const { context, config } = project;
5648
6664
  const out = context.outDir;
@@ -5667,14 +6683,20 @@ var generateRuntime = async (project) => {
5667
6683
  discoverIslands(context.root),
5668
6684
  discoverExamples(context.root, config.examples)
5669
6685
  ]);
6686
+ const {
6687
+ plan: slotPlan,
6688
+ tags: overrideTags,
6689
+ warnings: overrideWarnings
6690
+ } = await buildComponentSlots(context.componentsFile);
5670
6691
  const frameworks = new Set([
5671
6692
  ...islandDiscovery.islands.map((island) => island.framework),
5672
- ...exampleDiscovery.examples.map((example) => example.framework)
6693
+ ...exampleDiscovery.examples.map((example) => example.framework),
6694
+ ...slotPlan.frameworks
5673
6695
  ]);
5674
6696
  const needsReact = detectedReact || askEnabled || frameworks.has("react");
5675
6697
  const needsVue = frameworks.has("vue");
5676
6698
  const needsSvelte = frameworks.has("svelte");
5677
- const ogRoutes = customOgRoutes(pages, config.title, config.description);
6699
+ const ogRoutes = customOgRoutes(pages, config.title);
5678
6700
  const mcp = planMcp(project, srcDir);
5679
6701
  pages.push(...mcp.discoveryPages);
5680
6702
  const staged = collectStaged(project);
@@ -5702,9 +6724,10 @@ var generateRuntime = async (project) => {
5702
6724
  askEnabled,
5703
6725
  exportEpub,
5704
6726
  exportPdf,
5705
- mathEnabled: config.markdown.math
6727
+ mathEnabled: config.markdown.math,
6728
+ needsReact
5706
6729
  })),
5707
- write(join11(srcDir, "generated", "components.ts"), userComponentsTemplate(context.componentsFile)),
6730
+ write(join11(srcDir, "generated", "components.ts"), slotPlan.module),
5708
6731
  write(join11(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
5709
6732
  write(join11(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples)),
5710
6733
  write(themePath, tailwindEntryTemplate({
@@ -5718,18 +6741,21 @@ var generateRuntime = async (project) => {
5718
6741
  }))
5719
6742
  ]);
5720
6743
  await Promise.all(islandDiscovery.islands.map((island) => write(join11(srcDir, "generated", "islands", `${island.name}.astro`), islandWrapperTemplate(island))));
6744
+ await Promise.all(slotPlan.wrappers.map((wrapper) => write(join11(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content)));
5721
6745
  await Promise.all(exampleDiscovery.examples.map((example) => write(join11(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example))));
5722
- if (askEnabled) {
5723
- await write(join11(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(config.ai.ask)));
5724
- }
6746
+ await writeAskFiles(project, srcDir, write);
5725
6747
  await writeMcpFiles(project, mcp, write);
5726
6748
  if (config.seo.og.enabled) {
5727
6749
  await write(join11(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
5728
6750
  }
5729
- const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
5730
- const changelogRouteTaken = project.graph.pages.some((page) => page.route === "/changelog");
5731
- if (hasChangelog && !changelogRouteTaken) {
5732
- await write(join11(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({ askEnabled, exportEpub, exportPdf }));
6751
+ if (shouldGenerateChangelog(project)) {
6752
+ await write(join11(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
6753
+ askEnabled,
6754
+ exportEpub,
6755
+ exportPdf,
6756
+ needsReact,
6757
+ staged: hasStaged
6758
+ }));
5733
6759
  }
5734
6760
  await writeNotFoundPage(write, srcDir, pages, project.graph.pages);
5735
6761
  await write(searchClientPath, searchClientTemplate(config));
@@ -5762,8 +6788,23 @@ var generateRuntime = async (project) => {
5762
6788
  ...depsLinkWarning ? [depsLinkWarning] : [],
5763
6789
  ...mcp.warnings,
5764
6790
  ...islandDiscovery.warnings,
5765
- ...exampleDiscovery.warnings
6791
+ ...exampleDiscovery.warnings,
6792
+ ...overrideWarnings
5766
6793
  ];
6794
+ const navTargetRoutes = new Set([
6795
+ ...project.graph.routes.keys(),
6796
+ ...pages.map((page) => page.pattern),
6797
+ ...referenceTabs(config).map((tab) => tab.path)
6798
+ ]);
6799
+ if (shouldGenerateChangelog(project)) {
6800
+ navTargetRoutes.add("/changelog");
6801
+ }
6802
+ warnings.push(...validateNavTargets(project.graph.navigation, navTargetRoutes).map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
6803
+ const knownComponentTags = new Set([
6804
+ ...islandDiscovery.islands.map((island) => island.name),
6805
+ ...overrideTags
6806
+ ]);
6807
+ warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
5767
6808
  for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
5768
6809
  if (!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))) {
5769
6810
  warnings.push(`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
@@ -5787,14 +6828,17 @@ var generateRuntime = async (project) => {
5787
6828
  return { structuralChange: structural.some(Boolean), warnings };
5788
6829
  };
5789
6830
 
6831
+ // src/core/config.ts
6832
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
6833
+
5790
6834
  // src/core/bridge.ts
5791
- import { existsSync as existsSync6 } from "node:fs";
6835
+ import { existsSync as existsSync7 } from "node:fs";
5792
6836
  import { readFile as readFile9 } from "node:fs/promises";
5793
6837
  import { join as join12 } from "pathe";
5794
6838
 
5795
6839
  // src/migrate/mintlify/config.ts
5796
6840
  import { readFile as readFile8 } from "node:fs/promises";
5797
- import { dirname as dirname7, relative as relative6, resolve as resolve3 } from "pathe";
6841
+ import { dirname as dirname8, relative as relative7, resolve as resolve4 } from "pathe";
5798
6842
  var MINTLIFY_DEFAULT_IGNORES = [
5799
6843
  "**/_*",
5800
6844
  "**/.*",
@@ -5830,7 +6874,7 @@ var asDirectoryMode = (value) => value === "accordion" || value === "card" || va
5830
6874
  var withoutUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
5831
6875
  var hasOwn = (object, key) => Object.hasOwn(object, key);
5832
6876
  var isInsideRoot = (root, candidate) => {
5833
- const rel = relative6(root, candidate);
6877
+ const rel = relative7(root, candidate);
5834
6878
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
5835
6879
  };
5836
6880
  var readJsonFile = async (file) => {
@@ -5855,7 +6899,7 @@ var resolveRefs = async (value, options) => {
5855
6899
  }
5856
6900
  const ref = asString(object.$ref);
5857
6901
  if (ref) {
5858
- const refFile = resolve3(dirname7(options.file), ref);
6902
+ const refFile = resolve4(dirname8(options.file), ref);
5859
6903
  if (!isInsideRoot(options.root, refFile)) {
5860
6904
  throw new BlumeError({
5861
6905
  code: "BLUME_MINTLIFY_REF_OUTSIDE_ROOT",
@@ -6209,53 +7253,9 @@ var mintlifySelectors = (spec) => {
6209
7253
  }
6210
7254
  ].filter((selector) => selector.items.length > 0);
6211
7255
  };
6212
- var navbarTypeLabel = (type) => {
6213
- if (type === "github") {
6214
- return "GitHub";
6215
- }
6216
- if (type === "discord") {
6217
- return "Discord";
6218
- }
6219
- return;
6220
- };
6221
- var navbarLinkType = (type) => type === "github" || type === "discord" ? type : undefined;
6222
- var navbarPrimaryType = (type) => type === "github" || type === "discord" ? type : "button";
6223
- var mintlifyNavbar = (value) => {
6224
- const object = asObject(value);
6225
- if (!object) {
6226
- return { links: [] };
6227
- }
6228
- const links = asArray(object.links).flatMap((item) => {
6229
- const itemObject = asObject(item);
6230
- const href = itemObject ? asString(itemObject.href) : undefined;
6231
- const type = itemObject ? asString(itemObject.type) : undefined;
6232
- const label = itemObject ? asString(itemObject.label) ?? navbarTypeLabel(type) : undefined;
6233
- if (!itemObject || !href || !label) {
6234
- return [];
6235
- }
6236
- return [
6237
- withoutUndefined({
6238
- href,
6239
- icon: asString(itemObject.icon),
6240
- label,
6241
- type: navbarLinkType(type)
6242
- })
6243
- ];
6244
- });
6245
- const primaryObject = asObject(object.primary);
6246
- const primaryHref = primaryObject ? asString(primaryObject.href) : undefined;
6247
- const primaryType = primaryObject ? asString(primaryObject.type) ?? "button" : undefined;
6248
- const primaryLabel = primaryObject ? asString(primaryObject.label) ?? navbarTypeLabel(primaryType) : undefined;
6249
- const primary = primaryObject && primaryHref && primaryLabel ? withoutUndefined({
6250
- href: primaryHref,
6251
- label: primaryLabel,
6252
- type: navbarPrimaryType(primaryType)
6253
- }) : undefined;
6254
- return withoutUndefined({ links, primary });
6255
- };
6256
7256
  var mintignorePatterns = async (root) => {
6257
7257
  try {
6258
- const raw = await readFile8(resolve3(root, ".mintignore"), "utf-8");
7258
+ const raw = await readFile8(resolve4(root, ".mintignore"), "utf-8");
6259
7259
  return raw.split(`
6260
7260
  `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).filter((line) => !line.startsWith("!")).map((line) => line.endsWith("/") ? `${line}**` : line);
6261
7261
  } catch {
@@ -6274,61 +7274,6 @@ var mintlifyRedirects = (spec) => asArray(spec.redirects).flatMap((redirect) =>
6274
7274
  }
6275
7275
  return [{ from, to }];
6276
7276
  });
6277
- var mintlifyContextual = (value) => {
6278
- const object = asObject(value);
6279
- if (!object) {
6280
- return { options: [] };
6281
- }
6282
- const display = object.display === "toc" ? "toc" : "header";
6283
- const options = [];
6284
- for (const option of asArray(object.options)) {
6285
- if (typeof option === "string") {
6286
- options.push(option);
6287
- continue;
6288
- }
6289
- const optionObject = asObject(option);
6290
- const title = optionObject ? asString(optionObject.title) : undefined;
6291
- if (!optionObject || !title) {
6292
- continue;
6293
- }
6294
- options.push(withoutUndefined({
6295
- description: asString(optionObject.description),
6296
- href: asString(optionObject.href),
6297
- icon: asString(optionObject.icon),
6298
- title
6299
- }));
6300
- }
6301
- return { display, options };
6302
- };
6303
- var mintlifyFooter = (value) => {
6304
- const object = asObject(value);
6305
- const socials = asObject(object?.socials);
6306
- const links = asArray(object?.links).flatMap((group) => {
6307
- const groupObject = asObject(group);
6308
- const items = asArray(groupObject?.items).flatMap((item) => {
6309
- const itemObject = asObject(item);
6310
- const label = itemObject ? asString(itemObject.label) : undefined;
6311
- const href = itemObject ? asString(itemObject.href) : undefined;
6312
- return label && href ? [{ href, label }] : [];
6313
- });
6314
- if (!groupObject || items.length === 0) {
6315
- return [];
6316
- }
6317
- return [
6318
- withoutUndefined({
6319
- header: asString(groupObject.header),
6320
- items
6321
- })
6322
- ];
6323
- }).slice(0, 4);
6324
- return {
6325
- links,
6326
- socials: socials ? Object.fromEntries(Object.entries(socials).flatMap(([label, href]) => {
6327
- const hrefValue = asString(href);
6328
- return hrefValue ? [[label, hrefValue]] : [];
6329
- })) : {}
6330
- };
6331
- };
6332
7277
  var mintlifyLogo = (value) => {
6333
7278
  if (typeof value === "string") {
6334
7279
  return value;
@@ -6393,16 +7338,12 @@ var mintlifyChromeVariants = (spec) => {
6393
7338
  return [];
6394
7339
  }
6395
7340
  const banner = hasOwn(object, "banner") ? mintlifyBanner(object.banner) : undefined;
6396
- const footer = hasOwn(object, "footer") ? mintlifyFooter(object.footer) : undefined;
6397
- const navbar = hasOwn(object, "navbar") ? mintlifyNavbar(object.navbar) : undefined;
6398
- if (!banner && !footer && !navbar) {
7341
+ if (!banner) {
6399
7342
  return [];
6400
7343
  }
6401
7344
  return [
6402
7345
  withoutUndefined({
6403
7346
  banner,
6404
- footer,
6405
- navbar,
6406
7347
  path
6407
7348
  })
6408
7349
  ];
@@ -6454,13 +7395,6 @@ var mintlifyMarkdown = (value, styling) => {
6454
7395
  schema: object?.schema === false ? false : undefined
6455
7396
  });
6456
7397
  };
6457
- var mintlifyStyling = (value) => {
6458
- const object = asObject(value);
6459
- const eyebrows = asString(object?.eyebrows);
6460
- return withoutUndefined({
6461
- eyebrows: eyebrows === "breadcrumbs" || eyebrows === "section" ? eyebrows : undefined
6462
- });
6463
- };
6464
7398
  var mintlifySeo = (value) => {
6465
7399
  const object = asObject(value);
6466
7400
  const metatags = asObject(object?.metatags);
@@ -6471,16 +7405,9 @@ var mintlifySeo = (value) => {
6471
7405
  })) : {}
6472
7406
  };
6473
7407
  };
6474
- var mintlifyIcons = (value) => {
6475
- const object = asObject(value);
6476
- const library = object?.library;
6477
- return withoutUndefined({
6478
- library: library === "fontawesome" || library === "lucide" || library === "tabler" ? library : undefined
6479
- });
6480
- };
6481
7408
  var loadMintlifyConfig = async (root, file) => {
6482
- const projectRoot = resolve3(root);
6483
- const configFile = resolve3(file);
7409
+ const projectRoot = resolve4(root);
7410
+ const configFile = resolve4(file);
6484
7411
  const spec = asObject(await resolveRefs(await readJsonFile(configFile), {
6485
7412
  file: configFile,
6486
7413
  root: projectRoot,
@@ -6511,14 +7438,10 @@ var loadMintlifyConfig = async (root, file) => {
6511
7438
  ],
6512
7439
  root: "."
6513
7440
  },
6514
- contextual: mintlifyContextual(spec.contextual),
6515
7441
  description: asString(spec.description),
6516
7442
  favicon: mintlifyFavicon(spec.favicon),
6517
- footer: mintlifyFooter(spec.footer),
6518
- icons: mintlifyIcons(spec.icons),
6519
7443
  logo: mintlifyLogo(spec.logo),
6520
7444
  markdown: mintlifyMarkdown(spec.markdown, styling),
6521
- navbar: mintlifyNavbar(spec.navbar),
6522
7445
  navigation: {
6523
7446
  chromeVariants: mintlifyChromeVariants(spec),
6524
7447
  selectors: mintlifySelectors(spec),
@@ -6534,7 +7457,6 @@ var loadMintlifyConfig = async (root, file) => {
6534
7457
  prompt: asString(search.prompt)
6535
7458
  },
6536
7459
  seo: mintlifySeo(seo),
6537
- styling: mintlifyStyling(styling),
6538
7460
  theme: {
6539
7461
  accent: asString(colors.primary) ?? "blue",
6540
7462
  accentDark: asString(colors.light),
@@ -6581,7 +7503,7 @@ var mintlifyI18n = (spec) => {
6581
7503
  // src/core/bridge.ts
6582
7504
  var MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
6583
7505
  var detectMintlifyBridge = async (root) => {
6584
- const configFile = MINTLIFY_CONFIG_FILES.map((name) => join12(root, name)).find((candidate) => existsSync6(candidate));
7506
+ const configFile = MINTLIFY_CONFIG_FILES.map((name) => join12(root, name)).find((candidate) => existsSync7(candidate));
6585
7507
  if (!configFile) {
6586
7508
  return null;
6587
7509
  }
@@ -6670,8 +7592,8 @@ var createModuleLoader = () => {
6670
7592
  };
6671
7593
 
6672
7594
  // src/core/project.ts
6673
- import { existsSync as existsSync7 } from "node:fs";
6674
- import { isAbsolute as isAbsolute3, join as join13, resolve as resolve4 } from "pathe";
7595
+ import { existsSync as existsSync8 } from "node:fs";
7596
+ import { isAbsolute as isAbsolute4, join as join13, resolve as resolve5 } from "pathe";
6675
7597
  var CONFIG_FILENAMES = [
6676
7598
  "blume.config.ts",
6677
7599
  "blume.config.mjs",
@@ -6682,7 +7604,7 @@ var COMPONENTS_FILENAMES = ["components.tsx", "components.ts"];
6682
7604
  var firstExisting = (root, names) => {
6683
7605
  for (const name of names) {
6684
7606
  const candidate = join13(root, name);
6685
- if (existsSync7(candidate)) {
7607
+ if (existsSync8(candidate)) {
6686
7608
  return candidate;
6687
7609
  }
6688
7610
  }
@@ -6690,10 +7612,10 @@ var firstExisting = (root, names) => {
6690
7612
  };
6691
7613
  var findConfigFile = (root) => firstExisting(root, CONFIG_FILENAMES);
6692
7614
  var resolveProjectContext = (root, config) => {
6693
- const absoluteRoot = resolve4(root);
6694
- const contentRoot = isAbsolute3(config.content.root) ? config.content.root : join13(absoluteRoot, config.content.root);
7615
+ const absoluteRoot = resolve5(root);
7616
+ const contentRoot = isAbsolute4(config.content.root) ? config.content.root : join13(absoluteRoot, config.content.root);
6695
7617
  const pagesPath = join13(absoluteRoot, config.content.pages);
6696
- const pagesRoot = existsSync7(pagesPath) ? pagesPath : null;
7618
+ const pagesRoot = existsSync8(pagesPath) ? pagesPath : null;
6697
7619
  return {
6698
7620
  componentsFile: firstExisting(absoluteRoot, COMPONENTS_FILENAMES),
6699
7621
  configFile: findConfigFile(absoluteRoot),
@@ -6855,6 +7777,16 @@ var notionSourceSchema = z2.object({
6855
7777
  publishedValue: z2.string().optional(),
6856
7778
  type: z2.literal("notion")
6857
7779
  });
7780
+ var githubReleasesSourceSchema = z2.object({
7781
+ drafts: z2.boolean().optional(),
7782
+ limit: z2.number().positive().optional(),
7783
+ owner: z2.string(),
7784
+ pollInterval: z2.number().positive().optional(),
7785
+ prefix: z2.string().optional(),
7786
+ prereleases: z2.boolean().optional(),
7787
+ repo: z2.string(),
7788
+ type: z2.literal("github-releases")
7789
+ }).strict();
6858
7790
  var mintlifySourceSchema = z2.object({
6859
7791
  configFile: z2.string().optional(),
6860
7792
  exclude: z2.array(z2.string()).default([]),
@@ -6871,6 +7803,7 @@ var customSourceSchema = z2.object({
6871
7803
  var contentSourceSchema = z2.discriminatedUnion("type", [
6872
7804
  filesystemSourceSchema,
6873
7805
  mdxRemoteSourceSchema,
7806
+ githubReleasesSourceSchema,
6874
7807
  sanitySourceSchema,
6875
7808
  notionSourceSchema,
6876
7809
  mintlifySourceSchema,
@@ -6927,26 +7860,6 @@ var sidebarVariantSchema = z2.object({
6927
7860
  items: z2.array(sidebarItemSchema).default([]),
6928
7861
  path: z2.string()
6929
7862
  }).strict();
6930
- var navbarLinkTypeSchema = z2.enum(["github", "discord"]);
6931
- var navbarLinkSchema = z2.object({
6932
- href: z2.string(),
6933
- icon: iconName.optional(),
6934
- label: z2.string().optional(),
6935
- type: navbarLinkTypeSchema.optional()
6936
- }).strict().refine((value) => value.label !== undefined || value.type !== undefined, {
6937
- message: "Navbar links require either label or type."
6938
- });
6939
- var navbarPrimarySchema = z2.object({
6940
- href: z2.string(),
6941
- label: z2.string().optional(),
6942
- type: z2.enum(["button", "github", "discord"]).default("button")
6943
- }).strict().refine((value) => value.label !== undefined || value.type !== "button", {
6944
- message: "Navbar primary button links require a label."
6945
- });
6946
- var navbarConfigSchema = z2.object({
6947
- links: z2.array(navbarLinkSchema).default([]),
6948
- primary: navbarPrimarySchema.optional()
6949
- }).strict();
6950
7863
  var variablesConfigSchema = z2.record(z2.string().regex(/^[A-Za-z0-9-]+$/u), z2.string()).default({});
6951
7864
  var fontSlug = z2.string().refine(isFontSlug, (value) => ({
6952
7865
  message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`
@@ -6970,9 +7883,6 @@ var themeConfigSchema = z2.object({
6970
7883
  radius: z2.enum(["none", "sm", "md", "lg"]).default("md"),
6971
7884
  strict: z2.boolean().default(false)
6972
7885
  }).strict();
6973
- var iconsConfigSchema = z2.object({
6974
- library: z2.enum(["fontawesome", "lucide", "tabler"]).default("lucide")
6975
- }).strict();
6976
7886
  var algoliaSearchSchema = z2.object({
6977
7887
  appId: z2.string(),
6978
7888
  indexName: z2.string(),
@@ -7054,33 +7964,8 @@ var aiConfigSchema = z2.object({
7054
7964
  }).optional(),
7055
7965
  llmsTxt: z2.boolean().default(false)
7056
7966
  }).strict();
7057
- var contextualOptionSchema = z2.union([
7058
- z2.string(),
7059
- z2.object({
7060
- description: z2.string().optional(),
7061
- href: z2.string().optional(),
7062
- icon: iconName.optional(),
7063
- title: z2.string()
7064
- }).passthrough()
7065
- ]);
7066
- var contextualConfigSchema = z2.object({
7067
- display: z2.enum(["header", "toc"]).default("header"),
7068
- options: z2.array(contextualOptionSchema).default([])
7069
- }).strict();
7070
- var footerConfigSchema = z2.object({
7071
- links: z2.array(z2.object({
7072
- header: z2.string().optional(),
7073
- items: z2.array(z2.object({
7074
- href: z2.string(),
7075
- label: z2.string()
7076
- }).strict()).default([])
7077
- }).strict()).max(4).default([]),
7078
- socials: z2.record(z2.string(), z2.string()).default({})
7079
- }).strict();
7080
7967
  var chromeVariantSchema = z2.object({
7081
7968
  banner: bannerConfigSchema.optional(),
7082
- footer: footerConfigSchema.optional(),
7083
- navbar: navbarConfigSchema.optional(),
7084
7969
  path: z2.string()
7085
7970
  }).strict();
7086
7971
  var navigationConfigSchema = z2.object({
@@ -7205,9 +8090,6 @@ var markdownConfigSchema = z2.object({
7205
8090
  imageZoom: z2.boolean().default(true),
7206
8091
  math: z2.boolean().default(false)
7207
8092
  }).strict();
7208
- var stylingConfigSchema = z2.object({
7209
- eyebrows: z2.enum(["breadcrumbs", "section"]).default("section")
7210
- }).strict();
7211
8093
  var openapiSourceSchema = z2.object({
7212
8094
  label: z2.string().optional(),
7213
8095
  route: z2.string().optional(),
@@ -7227,36 +8109,48 @@ var asyncapiConfigSchema = z2.object({
7227
8109
  spec: z2.string().optional(),
7228
8110
  theme: z2.string().optional()
7229
8111
  }).strict();
8112
+ var tocConfigSchema = z2.union([
8113
+ z2.boolean(),
8114
+ z2.object({
8115
+ maxHeadingLevel: z2.number().int().min(1).max(6).optional(),
8116
+ minHeadingLevel: z2.number().int().min(1).max(6).optional()
8117
+ }).strict()
8118
+ ]).default(true).transform((value) => {
8119
+ if (typeof value === "boolean") {
8120
+ return { enabled: value, maxLevel: 3, minLevel: 2 };
8121
+ }
8122
+ return {
8123
+ enabled: true,
8124
+ maxLevel: value.maxHeadingLevel ?? 3,
8125
+ minLevel: value.minHeadingLevel ?? 2
8126
+ };
8127
+ });
7230
8128
  var blumeConfigSchema = z2.object({
7231
8129
  ai: aiConfigSchema.default({}),
7232
8130
  analytics: analyticsConfigSchema.optional(),
7233
8131
  asyncapi: asyncapiConfigSchema.default({}),
7234
8132
  banner: bannerConfigSchema.optional(),
7235
8133
  content: contentConfigSchema.default({}),
7236
- contextual: contextualConfigSchema.default({}),
7237
8134
  deployment: deploymentConfigSchema.default({}),
7238
8135
  description: z2.string().optional(),
7239
8136
  examples: z2.string().default("examples"),
7240
8137
  export: exportConfigSchema.default(false),
7241
8138
  favicon: faviconConfigSchema.optional(),
7242
8139
  feedback: z2.boolean().default(true),
7243
- footer: footerConfigSchema.default({}),
7244
8140
  github: githubConfigSchema.optional(),
7245
8141
  i18n: i18nConfigSchema.optional(),
7246
- icons: iconsConfigSchema.default({}),
7247
8142
  lastModified: lastModifiedConfigSchema.default(false),
7248
8143
  logo: logoConfigSchema.optional(),
7249
8144
  markdown: markdownConfigSchema.default({}),
7250
8145
  mcp: mcpConfigSchema.default({}),
7251
- navbar: navbarConfigSchema.default({}),
7252
8146
  navigation: navigationConfigSchema.default({}),
7253
8147
  openapi: openapiConfigSchema.default({}),
7254
8148
  redirects: z2.array(redirectSchema).default([]),
7255
8149
  search: searchConfigSchema.default({}),
7256
8150
  seo: seoConfigSchema.default({}),
7257
- styling: stylingConfigSchema.default({}),
7258
8151
  theme: themeConfigSchema.default({}),
7259
8152
  title: z2.string().default("Documentation"),
8153
+ toc: tocConfigSchema,
7260
8154
  variables: variablesConfigSchema
7261
8155
  }).strict();
7262
8156
 
@@ -7286,9 +8180,11 @@ var loadConfig = async (root, options = {}) => {
7286
8180
  const sourceFile = bridge?.configFile ?? configFile;
7287
8181
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
7288
8182
  if (!parsed.success) {
8183
+ const source = sourceFile && existsSync9(sourceFile) ? readFileSync6(sourceFile, "utf-8") : undefined;
7289
8184
  const diagnostics = diagnosticsFromZod(parsed.error, {
7290
8185
  code: "BLUME_CONFIG_INVALID",
7291
- file: sourceFile ?? undefined
8186
+ file: sourceFile ?? undefined,
8187
+ source
7292
8188
  });
7293
8189
  throw new BlumeError(diagnostics[0] ?? {
7294
8190
  code: "BLUME_CONFIG_INVALID",
@@ -7313,7 +8209,7 @@ var loadConfig = async (root, options = {}) => {
7313
8209
  };
7314
8210
 
7315
8211
  // src/core/navigation.ts
7316
- import { extname as extname2 } from "pathe";
8212
+ import { extname as extname3 } from "pathe";
7317
8213
  var NUMERIC_PREFIX = /^(?<order>\d+)[-_.]/u;
7318
8214
  var GROUP_FOLDER = /^\((?<label>.+)\)$/u;
7319
8215
  var WORD_SPLIT = /[-_]/u;
@@ -7350,7 +8246,7 @@ var pageOrder = (page, filename) => {
7350
8246
  if (page.meta.sidebar.order !== undefined) {
7351
8247
  return page.meta.sidebar.order;
7352
8248
  }
7353
- if (filename.replace(extname2(filename), "") === "index") {
8249
+ if (filename.replace(extname3(filename), "") === "index") {
7354
8250
  return Number.NEGATIVE_INFINITY;
7355
8251
  }
7356
8252
  return numericOrder(filename);
@@ -7442,7 +8338,7 @@ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix) => {
7442
8338
  deprecated: page.meta.deprecated || undefined,
7443
8339
  description: page.description,
7444
8340
  icon: page.meta.sidebar.icon,
7445
- key: segmentKey(filename.replace(extname2(filename), "")),
8341
+ key: segmentKey(filename.replace(extname3(filename), "")),
7446
8342
  kind: "page",
7447
8343
  label: page.meta.sidebar.label ?? page.title,
7448
8344
  order: pageOrder(page, filename),
@@ -7641,6 +8537,8 @@ var buildContentGraph = (pages, options) => {
7641
8537
  tabs: options.navigation.tabs
7642
8538
  });
7643
8539
  }
8540
+ diagnostics.push(...validateNavIcons(navigation));
8541
+ diagnostics.push(...validateNavStructure(navigation, pages));
7644
8542
  return {
7645
8543
  diagnostics,
7646
8544
  navigation,
@@ -7652,7 +8550,7 @@ var buildContentGraph = (pages, options) => {
7652
8550
 
7653
8551
  // src/core/last-modified.ts
7654
8552
  import { execFileSync } from "node:child_process";
7655
- import { relative as relative7 } from "pathe";
8553
+ import { relative as relative8 } from "pathe";
7656
8554
  var resolveLastModifiedConfig = (value) => {
7657
8555
  if (value === false) {
7658
8556
  return { enabled: false, source: "git" };
@@ -7692,7 +8590,7 @@ var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
7692
8590
  const byRepoPath = parseGitLog(output);
7693
8591
  const result = new Map;
7694
8592
  for (const sourcePath of sourcePaths) {
7695
- const iso = byRepoPath.get(relative7(gitRoot, sourcePath));
8593
+ const iso = byRepoPath.get(relative8(gitRoot, sourcePath));
7696
8594
  if (iso) {
7697
8595
  result.set(sourcePath, iso);
7698
8596
  }
@@ -7704,7 +8602,7 @@ var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
7704
8602
  };
7705
8603
 
7706
8604
  // src/core/meta.ts
7707
- import { basename as basename3, dirname as dirname8, relative as relative8 } from "pathe";
8605
+ import { basename as basename3, dirname as dirname9, relative as relative9 } from "pathe";
7708
8606
  import { glob as glob5 } from "tinyglobby";
7709
8607
  var META_FILES = [
7710
8608
  "**/meta.ts",
@@ -7734,7 +8632,7 @@ var discoverFolderMeta = async (contentRoot) => {
7734
8632
  const shared = new Map;
7735
8633
  const diagnostics = [];
7736
8634
  for (const entry of loaded) {
7737
- const dir = relative8(contentRoot, dirname8(entry.file));
8635
+ const dir = relative9(contentRoot, dirname9(entry.file));
7738
8636
  if (!entry.ok) {
7739
8637
  diagnostics.push({
7740
8638
  code: "BLUME_META_LOAD_FAILED",
@@ -7759,8 +8657,9 @@ var discoverFolderMeta = async (contentRoot) => {
7759
8657
  };
7760
8658
 
7761
8659
  // src/core/sources/normalize.ts
8660
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
7762
8661
  import GithubSlugger from "github-slugger";
7763
- import { extname as extname3 } from "pathe";
8662
+ import { extname as extname4 } from "pathe";
7764
8663
  var NUMERIC_PREFIX2 = /^\d+[-_.]/u;
7765
8664
  var GROUP_FOLDER2 = /^\((?<label>.+)\)$/u;
7766
8665
  var WORD_SPLIT2 = /[-_]/u;
@@ -7769,7 +8668,7 @@ var groupLabel = (segment) => segment.match(GROUP_FOLDER2)?.groups?.label ?? nul
7769
8668
  var slugify2 = (text) => text.toLowerCase().trim().replaceAll(/[^\w\s-]/gu, "").replaceAll(/[\s_]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "");
7770
8669
  var titleCase = (value) => value.split(WORD_SPLIT2).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
7771
8670
  var mapRoute = (relativePath) => {
7772
- const withoutExt = relativePath.slice(0, relativePath.length - extname3(relativePath).length);
8671
+ const withoutExt = relativePath.slice(0, relativePath.length - extname4(relativePath).length);
7773
8672
  const rawParts = withoutExt.split("/");
7774
8673
  const segments = [];
7775
8674
  const groups = [];
@@ -7841,6 +8740,31 @@ var extractLinks = (body) => {
7841
8740
  }
7842
8741
  return links;
7843
8742
  };
8743
+ var INLINE_CODE2 = /`[^`]*`/gu;
8744
+ var DOUBLE_QUOTED = /"[^"]*"/gu;
8745
+ var JSX_OPEN = /<(?<tag>[A-Z][A-Za-z0-9]*)/gu;
8746
+ var extractComponentTags = (body) => {
8747
+ const tags = new Set;
8748
+ let inFence = false;
8749
+ for (const line of body.split(`
8750
+ `)) {
8751
+ if (CODE_FENCE2.test(line.trimStart())) {
8752
+ inFence = !inFence;
8753
+ continue;
8754
+ }
8755
+ if (inFence) {
8756
+ continue;
8757
+ }
8758
+ const clean = line.replaceAll(INLINE_CODE2, "").replaceAll(DOUBLE_QUOTED, "");
8759
+ for (const match of clean.matchAll(JSX_OPEN)) {
8760
+ const tag = match.groups?.tag;
8761
+ if (tag) {
8762
+ tags.add(tag);
8763
+ }
8764
+ }
8765
+ }
8766
+ return [...tags];
8767
+ };
7844
8768
  var deriveTitle = (meta, headings, id2) => {
7845
8769
  if (meta.title) {
7846
8770
  return meta.title;
@@ -7850,18 +8774,20 @@ var deriveTitle = (meta, headings, id2) => {
7850
8774
  return firstHeading.text;
7851
8775
  }
7852
8776
  const base = id2.split("/").pop() ?? id2;
7853
- return titleCase(stripNumericPrefix(base.replace(extname3(base), "")));
8777
+ return titleCase(stripNumericPrefix(base.replace(extname4(base), "")));
7854
8778
  };
7855
8779
  var withPrefix = (prefix, path) => prefix ? `${prefix}/${path}` : path;
7856
- var normalizeEntry = (entry, ctx) => {
8780
+ var normalizeEntry2 = (entry, ctx) => {
7857
8781
  const { format } = entry.body;
7858
8782
  const ext = format === "mdx" ? ".mdx" : ".md";
7859
8783
  const result = pageMetaSchema.safeParse(entry.data);
7860
8784
  if (!result.success) {
8785
+ const source = entry.raw ?? (entry.sourcePath && existsSync10(entry.sourcePath) ? readFileSync7(entry.sourcePath, "utf-8") : undefined);
7861
8786
  return {
7862
8787
  diagnostics: diagnosticsFromZod(result.error, {
7863
8788
  code: "BLUME_FRONTMATTER_INVALID",
7864
- file: entry.sourcePath ?? `${ctx.source.name}:${entry.ref}`
8789
+ file: entry.sourcePath ?? `${ctx.source.name}:${entry.ref}`,
8790
+ source
7865
8791
  }),
7866
8792
  pages: []
7867
8793
  };
@@ -7877,6 +8803,7 @@ var normalizeEntry = (entry, ctx) => {
7877
8803
  const base = {
7878
8804
  body: staged ? { format, text: entry.raw ?? entry.body.text } : undefined,
7879
8805
  collection: staged ? "staged" : undefined,
8806
+ componentsUsed: format === "mdx" ? extractComponentTags(entry.body.text) : undefined,
7880
8807
  contentType: meta.type ?? ctx.defaultType,
7881
8808
  description: meta.description,
7882
8809
  editUrl: entry.editUrl,
@@ -7907,12 +8834,12 @@ var normalizeEntry = (entry, ctx) => {
7907
8834
  import { join as join22 } from "pathe";
7908
8835
 
7909
8836
  // src/core/sources/filesystem.ts
7910
- import { existsSync as existsSync8, watch as fsWatch } from "node:fs";
8837
+ import { existsSync as existsSync11, watch as fsWatch } from "node:fs";
7911
8838
  import { readFile as readFile10 } from "node:fs/promises";
7912
- import { extname as extname4, isAbsolute as isAbsolute4, join as join14, relative as relative9, resolve as resolve5 } from "pathe";
8839
+ import { extname as extname5, isAbsolute as isAbsolute5, join as join14, relative as relative10, resolve as resolve6 } from "pathe";
7913
8840
  import { glob as glob6 } from "tinyglobby";
7914
8841
  var filesystemSource = (options) => {
7915
- const contentRoot = isAbsolute4(options.root) ? options.root : join14(resolve5(options.projectRoot), options.root);
8842
+ const contentRoot = isAbsolute5(options.root) ? options.root : join14(resolve6(options.projectRoot), options.root);
7916
8843
  const load2 = async () => {
7917
8844
  const files = await glob6(options.include, {
7918
8845
  absolute: true,
@@ -7923,20 +8850,20 @@ var filesystemSource = (options) => {
7923
8850
  files.sort();
7924
8851
  const entries = await Promise.all(files.map(async (file) => {
7925
8852
  const source = await readFile10(file, "utf-8");
7926
- const ext = extname4(file).toLowerCase();
8853
+ const ext = extname5(file).toLowerCase();
7927
8854
  const format = ext === ".mdx" ? "mdx" : "md";
7928
8855
  const parsed = frontmatter_default(source);
7929
8856
  return {
7930
8857
  body: { format, text: parsed.content },
7931
8858
  data: parsed.data,
7932
- ref: relative9(contentRoot, file),
8859
+ ref: relative10(contentRoot, file),
7933
8860
  sourcePath: file
7934
8861
  };
7935
8862
  }));
7936
8863
  return { diagnostics: [], entries };
7937
8864
  };
7938
8865
  const validate = () => {
7939
- if (!existsSync8(contentRoot)) {
8866
+ if (!existsSync11(contentRoot)) {
7940
8867
  throw new BlumeError({
7941
8868
  code: options.missingCode ?? "BLUME_CONTENT_ROOT_MISSING",
7942
8869
  file: contentRoot,
@@ -7947,7 +8874,7 @@ var filesystemSource = (options) => {
7947
8874
  }
7948
8875
  };
7949
8876
  const watch = (onChange) => {
7950
- if (!existsSync8(contentRoot)) {
8877
+ if (!existsSync11(contentRoot)) {
7951
8878
  return () => {};
7952
8879
  }
7953
8880
  const watcher = fsWatch(contentRoot, { recursive: true }, onChange);
@@ -8041,6 +8968,118 @@ var loadWithCache = async (name, cache, fetchEntries, refresh = true) => {
8041
8968
  }
8042
8969
  };
8043
8970
 
8971
+ // src/core/sources/github-releases.ts
8972
+ var DEFAULT_BASE_URL = "https://api.github.com";
8973
+ var DEFAULT_LIMIT = 100;
8974
+ var PER_PAGE = 100;
8975
+ var LEADING_V = /^v/iu;
8976
+ var NON_SLUG2 = /[^a-z0-9]+/gu;
8977
+ var EDGE_DASHES = /^-+|-+$/gu;
8978
+ var slugifyTag = (tag) => tag.toLowerCase().replaceAll(NON_SLUG2, "-").replaceAll(EDGE_DASHES, "");
8979
+ var githubHeaders = () => {
8980
+ const headers = new Headers({ Accept: "application/vnd.github+json" });
8981
+ const token = process.env.GITHUB_TOKEN;
8982
+ if (token) {
8983
+ headers.set("Authorization", `Bearer ${token}`);
8984
+ }
8985
+ return headers;
8986
+ };
8987
+ var releaseToEntry = (release) => {
8988
+ const version = release.tag_name.replace(LEADING_V, "");
8989
+ const title = release.name?.trim() || release.tag_name;
8990
+ const date = release.published_at ?? release.created_at;
8991
+ const category = release.prerelease ? "Prerelease" : "Release";
8992
+ const body = (release.body ?? "").replaceAll(`\r
8993
+ `, `
8994
+ `).trim();
8995
+ const data = {
8996
+ changelog: { category, version },
8997
+ date,
8998
+ title,
8999
+ type: "changelog"
9000
+ };
9001
+ const raw = frontmatter_default.stringify(`${body}
9002
+ `, data);
9003
+ const ref = `${slugifyTag(release.tag_name) || `release-${release.id}`}.md`;
9004
+ return {
9005
+ body: { format: "md", text: body },
9006
+ data,
9007
+ editUrl: release.html_url,
9008
+ hash: hashText(raw),
9009
+ lastModified: date,
9010
+ raw,
9011
+ ref
9012
+ };
9013
+ };
9014
+ var githubReleasesSource = (options, ctx) => {
9015
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
9016
+ const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/u, "");
9017
+ const max = options.limit ?? DEFAULT_LIMIT;
9018
+ const cache = snapshotCache(ctx.cacheDir);
9019
+ let snapshot = new Map;
9020
+ const include = (release) => (options.drafts || !release.draft) && (options.prereleases || !release.prerelease);
9021
+ const fetchPage = async (page) => {
9022
+ const url = `${base}/repos/${options.owner}/${options.repo}/releases?per_page=${PER_PAGE}&page=${page}`;
9023
+ const res = await doFetch(url, { headers: githubHeaders() });
9024
+ if (!res.ok) {
9025
+ throw new Error(`${url} -> ${res.status}`);
9026
+ }
9027
+ return await res.json();
9028
+ };
9029
+ const fetchReleases = async () => {
9030
+ const collected = [];
9031
+ let page = 1;
9032
+ while (collected.length < max) {
9033
+ const batch = await fetchPage(page);
9034
+ collected.push(...batch.filter(include));
9035
+ if (batch.length < PER_PAGE) {
9036
+ break;
9037
+ }
9038
+ page += 1;
9039
+ }
9040
+ return collected.slice(0, max);
9041
+ };
9042
+ const load2 = async () => {
9043
+ try {
9044
+ const result = await loadWithCache(options.name, cache, async () => {
9045
+ const releases = await fetchReleases();
9046
+ return releases.map(releaseToEntry);
9047
+ }, ctx.refresh ?? true);
9048
+ snapshot = new Map(result.entries.map((entry) => [entry.ref, entry]));
9049
+ return result;
9050
+ } catch (error) {
9051
+ snapshot = new Map;
9052
+ return {
9053
+ diagnostics: [
9054
+ {
9055
+ code: "BLUME_SOURCE_UNAVAILABLE",
9056
+ message: `Source "${options.name}" could not fetch GitHub releases (${error.message}); the changelog will be empty. Set GITHUB_TOKEN to include it (required for a private repository).`,
9057
+ severity: "warning"
9058
+ }
9059
+ ],
9060
+ entries: []
9061
+ };
9062
+ }
9063
+ };
9064
+ const read = async (ref) => {
9065
+ const cached3 = snapshot.get(ref);
9066
+ if (cached3) {
9067
+ return cached3.raw ?? cached3.body.text;
9068
+ }
9069
+ const all = await cache.read();
9070
+ const entry = all.find((e) => e.ref === ref);
9071
+ return entry?.raw ?? entry?.body.text ?? "";
9072
+ };
9073
+ return {
9074
+ load: load2,
9075
+ name: options.name,
9076
+ prefix: options.prefix,
9077
+ read,
9078
+ staged: true,
9079
+ watch: options.pollInterval ? pollingWatch(load2, options.pollInterval) : undefined
9080
+ };
9081
+ };
9082
+
8044
9083
  // src/core/sources/mdx-remote.ts
8045
9084
  var REGEX_SPECIAL = /[.*+?^${}()|[\]\\]/u;
8046
9085
  var escapeChar = (char) => REGEX_SPECIAL.test(char) ? `\\${char}` : char;
@@ -8082,7 +9121,7 @@ var globToRegExp = (pattern) => {
8082
9121
  return new RegExp(`^${source}$`, "u");
8083
9122
  };
8084
9123
  var matchesInclude = (ref, patterns) => patterns.some((pattern) => globToRegExp(pattern).test(ref));
8085
- var githubHeaders = () => {
9124
+ var githubHeaders2 = () => {
8086
9125
  const token = process.env.GITHUB_TOKEN;
8087
9126
  return token ? { authorization: `Bearer ${token}` } : {};
8088
9127
  };
@@ -8090,7 +9129,7 @@ var enumerateGithub = async (github, include, doFetch) => {
8090
9129
  const { owner, repo, ref } = github;
8091
9130
  const base = github.path.replaceAll(/^\/|\/$/gu, "");
8092
9131
  const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`;
8093
- const res = await doFetch(treeUrl, { headers: githubHeaders() });
9132
+ const res = await doFetch(treeUrl, { headers: githubHeaders2() });
8094
9133
  if (!res.ok) {
8095
9134
  throw new Error(`${treeUrl} -> ${res.status}`);
8096
9135
  }
@@ -8125,7 +9164,7 @@ var mdxRemoteSource = (options, ctx) => {
8125
9164
  });
8126
9165
  };
8127
9166
  const fetchEntry = async (item) => {
8128
- const res = await doFetch(item.fetchUrl, { headers: githubHeaders() });
9167
+ const res = await doFetch(item.fetchUrl, { headers: githubHeaders2() });
8129
9168
  if (!res.ok) {
8130
9169
  throw new Error(`${item.fetchUrl} -> ${res.status}`);
8131
9170
  }
@@ -8169,13 +9208,13 @@ var mdxRemoteSource = (options, ctx) => {
8169
9208
  };
8170
9209
 
8171
9210
  // src/core/sources/mintlify.ts
8172
- import { existsSync as existsSync10, watch as fsWatch2 } from "node:fs";
9211
+ import { existsSync as existsSync13, watch as fsWatch2 } from "node:fs";
8173
9212
  import { readFile as readFile13 } from "node:fs/promises";
8174
- import { isAbsolute as isAbsolute5, join as join18, relative as relative12, resolve as resolve7 } from "pathe";
9213
+ import { isAbsolute as isAbsolute6, join as join18, relative as relative13, resolve as resolve8 } from "pathe";
8175
9214
  import { glob as glob7 } from "tinyglobby";
8176
9215
 
8177
9216
  // src/migrate/shared.ts
8178
- import { existsSync as existsSync9 } from "node:fs";
9217
+ import { existsSync as existsSync12 } from "node:fs";
8179
9218
  import { readFile as readFile12, writeFile as writeFile4 } from "node:fs/promises";
8180
9219
  import { join as join16 } from "pathe";
8181
9220
  var writeBlumeConfig = async (root, config) => {
@@ -8192,7 +9231,7 @@ var BLUME_SCRIPTS = {
8192
9231
  };
8193
9232
  var rewriteFrameworkScripts = async (root, cli, remove) => {
8194
9233
  const pkgPath = join16(root, "package.json");
8195
- if (!existsSync9(pkgPath)) {
9234
+ if (!existsSync12(pkgPath)) {
8196
9235
  return false;
8197
9236
  }
8198
9237
  let pkg;
@@ -8228,7 +9267,7 @@ var rewriteFrameworkScripts = async (root, cli, remove) => {
8228
9267
  var gitignoreKey = (line) => line.trim().replace(/\/+$/u, "");
8229
9268
  var ensureGitignore = async (root, entries) => {
8230
9269
  const path = join16(root, ".gitignore");
8231
- const existing = existsSync9(path) ? await readFile12(path, "utf-8") : "";
9270
+ const existing = existsSync12(path) ? await readFile12(path, "utf-8") : "";
8232
9271
  const present = new Set(existing.split(`
8233
9272
  `).map(gitignoreKey).filter(Boolean));
8234
9273
  const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
@@ -8243,7 +9282,7 @@ var ensureGitignore = async (root, entries) => {
8243
9282
  `, "utf-8");
8244
9283
  return added;
8245
9284
  };
8246
- var leftoverFiles = (root, candidates) => candidates.filter((candidate) => existsSync9(join16(root, candidate)));
9285
+ var leftoverFiles = (root, candidates) => candidates.filter((candidate) => existsSync12(join16(root, candidate)));
8247
9286
  var attribute = (attrs, name) => {
8248
9287
  const match = attrs.match(new RegExp(`\\b${name}=(?:"(?<dq>[^"]*)"|'(?<sq>[^']*)')`, "u"));
8249
9288
  return match?.groups?.dq ?? match?.groups?.sq;
@@ -8286,7 +9325,7 @@ var findOpenTagEnd = (source, from) => {
8286
9325
  }
8287
9326
  return -1;
8288
9327
  };
8289
- var directiveFor = (tag, attrs, options) => {
9328
+ var directiveFor2 = (tag, attrs, options) => {
8290
9329
  if (tag in options.tagDirectives) {
8291
9330
  return options.tagDirectives[tag];
8292
9331
  }
@@ -8316,7 +9355,7 @@ var rewriteCallouts = (source, options) => {
8316
9355
  continue;
8317
9356
  }
8318
9357
  const attrs = source.slice(start + tag.length + 1, openEnd);
8319
- const directive = directiveFor(tag, attrs, options);
9358
+ const directive = directiveFor2(tag, attrs, options);
8320
9359
  const closeTag = `</${tag}>`;
8321
9360
  const selfClosing = attrs.trimEnd().endsWith("/");
8322
9361
  const closeIndex = selfClosing ? openEnd : source.indexOf(closeTag, openEnd + 1);
@@ -8640,7 +9679,7 @@ var isLiteralObject = (value) => typeof value === "object" && value !== null &&
8640
9679
  var asLiteralArray = (value) => Array.isArray(value) ? value : undefined;
8641
9680
 
8642
9681
  // src/migrate/mintlify/content.ts
8643
- import { dirname as dirname9, join as join17, relative as relative10 } from "pathe";
9682
+ import { dirname as dirname10, join as join17, relative as relative11 } from "pathe";
8644
9683
  var CALLOUT_DIRECTIVES = {
8645
9684
  Check: "success",
8646
9685
  Danger: "danger",
@@ -8678,7 +9717,7 @@ var rewriteSnippetImports = (source, options) => {
8678
9717
  }
8679
9718
  const target = join17(options.root, importSource.replace(/^\/+/u, ""));
8680
9719
  components.push(importSource.replace(/^\/+/u, ""));
8681
- let rel = relative10(dirname9(options.filePath), target);
9720
+ let rel = relative11(dirname10(options.filePath), target);
8682
9721
  if (!rel.startsWith(".")) {
8683
9722
  rel = `./${rel}`;
8684
9723
  }
@@ -8868,7 +9907,7 @@ var rewriteMintlifySvgIconProps = (source) => {
8868
9907
 
8869
9908
  // src/migrate/mintlify/snippets.ts
8870
9909
  import { readFile as readFileFromDisk } from "node:fs/promises";
8871
- import { dirname as dirname10, relative as relative11, resolve as resolve6 } from "pathe";
9910
+ import { dirname as dirname11, relative as relative12, resolve as resolve7 } from "pathe";
8872
9911
  var MARKDOWN_SNIPPET_IMPORT = /^import\s+(?<name>[$A-Z_a-z][$\w]*)\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
8873
9912
  var NAMED_SNIPPET_IMPORT = /^import\s+\{(?<names>[^}]+)\}\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
8874
9913
  var EXPORTED_STRING_CONST = /^export\s+const\s+(?<name>[$A-Z_a-z][$\w]*)\s*=\s*(?:"(?<double>(?:\\.|[^"\\])*)"|'(?<single>(?:\\.|[^'\\])*)'|`(?<template>(?:\\.|[^`\\])*)`)\s*;?\s*$/gmu;
@@ -8878,14 +9917,14 @@ var GLOBAL_VARIABLE = /\{\{\s*(?<name>[A-Za-z0-9-]+)\s*\}\}/gu;
8878
9917
  var FRONTMATTER_BLOCK = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/u;
8879
9918
  var USER_EXPORT = /^(?:export\s+)?(?:const|let|var)\s+user\s*=|^import\s+\{\s*user\s*\}/mu;
8880
9919
  var isInsideRoot2 = (root, candidate) => {
8881
- const rel = relative11(root, candidate);
9920
+ const rel = relative12(root, candidate);
8882
9921
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
8883
9922
  };
8884
- var escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
8885
- var snippetSelfClosingTagPattern = (name) => new RegExp(`<${escapeRegExp(name)}(?<attrs>[^>]*?)\\s*/>`, "gu");
8886
- var snippetPairedTagPattern = (name) => new RegExp(`<${escapeRegExp(name)}(?<attrs>[^>]*?)>[\\s\\S]*?</${escapeRegExp(name)}>`, "gu");
9923
+ var escapeRegExp2 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
9924
+ var snippetSelfClosingTagPattern = (name) => new RegExp(`<${escapeRegExp2(name)}(?<attrs>[^>]*?)\\s*/>`, "gu");
9925
+ var snippetPairedTagPattern = (name) => new RegExp(`<${escapeRegExp2(name)}(?<attrs>[^>]*?)>[\\s\\S]*?</${escapeRegExp2(name)}>`, "gu");
8887
9926
  var rootRelativePath = (root, file) => {
8888
- const rel = relative11(root, file);
9927
+ const rel = relative12(root, file);
8889
9928
  return rel ? `/${rel}` : "/";
8890
9929
  };
8891
9930
  var snippetCycleMessage = (root, file, trail) => {
@@ -8894,10 +9933,10 @@ var snippetCycleMessage = (root, file, trail) => {
8894
9933
  return `Circular Mintlify snippet import detected: ${cycle}`;
8895
9934
  };
8896
9935
  var resolveSnippetPath = (options) => {
8897
- const target = options.source.startsWith("/") ? resolve6(options.root, options.source.slice(1)) : resolve6(dirname10(options.filePath), options.source);
9936
+ const target = options.source.startsWith("/") ? resolve7(options.root, options.source.slice(1)) : resolve7(dirname11(options.filePath), options.source);
8898
9937
  return isInsideRoot2(options.root, target) ? target : null;
8899
9938
  };
8900
- var collectImports = (source) => [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
9939
+ var collectImports2 = (source) => [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
8901
9940
  const name = match.groups?.name;
8902
9941
  const importSource = match.groups?.source;
8903
9942
  if (!(name && importSource)) {
@@ -8945,7 +9984,7 @@ var interpolateProps = (source, props) => source.replaceAll(PLACEHOLDER, (value,
8945
9984
  var stripImport = (source, importText) => source.replace(importText, "").replaceAll(/\n{3,}/gu, `
8946
9985
 
8947
9986
  `);
8948
- var replacePlaceholder = (source, name, value) => source.replaceAll(new RegExp(`\\{${escapeRegExp(name)}\\}`, "gu"), value);
9987
+ var replacePlaceholder = (source, name, value) => source.replaceAll(new RegExp(`\\{${escapeRegExp2(name)}\\}`, "gu"), value);
8949
9988
  var inlineSnippetTags = (options) => {
8950
9989
  const inline = (_value, attrs) => interpolateProps(options.snippet, parseAttributes(attrs));
8951
9990
  return options.source.replaceAll(snippetSelfClosingTagPattern(options.name), inline).replaceAll(snippetPairedTagPattern(options.name), inline);
@@ -8989,7 +10028,7 @@ var rewriteMintlifyMarkdownSnippets = async (source, options) => {
8989
10028
  });
8990
10029
  return next === current ? current : stripImport(next, snippetImport.importText);
8991
10030
  };
8992
- const imports = collectImports(source);
10031
+ const imports = collectImports2(source);
8993
10032
  const inlineAt = async (index, current) => {
8994
10033
  const snippetImport = imports[index];
8995
10034
  if (!snippetImport) {
@@ -9088,11 +10127,11 @@ var MINTLIFY_SOURCE_IGNORES = [
9088
10127
  "snippets/**"
9089
10128
  ];
9090
10129
  var mintlifySource = (options) => {
9091
- const contentRoot = isAbsolute5(options.root) ? options.root : join18(resolve7(options.projectRoot), options.root);
10130
+ const contentRoot = isAbsolute6(options.root) ? options.root : join18(resolve8(options.projectRoot), options.root);
9092
10131
  const ignore = [...new Set([...options.exclude, ...MINTLIFY_SOURCE_IGNORES])];
9093
10132
  const transform = (raw, file) => transformMintlifyContent(raw, {
9094
10133
  filePath: file,
9095
- root: resolve7(options.projectRoot),
10134
+ root: resolve8(options.projectRoot),
9096
10135
  variables: options.variables
9097
10136
  });
9098
10137
  const load2 = async () => {
@@ -9114,7 +10153,7 @@ var mintlifySource = (options) => {
9114
10153
  body: { format: "mdx", text: parsed.content },
9115
10154
  data: parsed.data,
9116
10155
  raw: result.content,
9117
- ref: relative12(contentRoot, file),
10156
+ ref: relative13(contentRoot, file),
9118
10157
  sourcePath: file
9119
10158
  };
9120
10159
  }));
@@ -9128,7 +10167,7 @@ var mintlifySource = (options) => {
9128
10167
  return { diagnostics, entries };
9129
10168
  };
9130
10169
  const validate = () => {
9131
- if (!existsSync10(contentRoot)) {
10170
+ if (!existsSync13(contentRoot)) {
9132
10171
  throw new BlumeError({
9133
10172
  code: "BLUME_CONTENT_ROOT_MISSING",
9134
10173
  file: contentRoot,
@@ -9140,11 +10179,11 @@ var mintlifySource = (options) => {
9140
10179
  };
9141
10180
  const watch = (onChange) => {
9142
10181
  const disposers = [];
9143
- if (existsSync10(contentRoot)) {
10182
+ if (existsSync13(contentRoot)) {
9144
10183
  const watcher = fsWatch2(contentRoot, { recursive: true }, onChange);
9145
10184
  disposers.push(() => watcher.close());
9146
10185
  }
9147
- if (options.configFile && existsSync10(options.configFile)) {
10186
+ if (options.configFile && existsSync13(options.configFile)) {
9148
10187
  const watcher = fsWatch2(options.configFile, onChange);
9149
10188
  disposers.push(() => watcher.close());
9150
10189
  }
@@ -9176,13 +10215,13 @@ import { join as join20 } from "pathe";
9176
10215
 
9177
10216
  // src/core/sources/assets.ts
9178
10217
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
9179
- import { extname as extname5, join as join19 } from "pathe";
10218
+ import { extname as extname6, join as join19 } from "pathe";
9180
10219
  var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
9181
10220
  var REMOTE = /^https?:\/\//u;
9182
10221
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
9183
10222
  var extFor = (url) => {
9184
10223
  const clean = url.split("?")[0] ?? url;
9185
- const ext = extname5(clean);
10224
+ const ext = extname6(clean);
9186
10225
  return SAFE_EXT.test(ext) ? ext.toLowerCase() : ".png";
9187
10226
  };
9188
10227
  var materializeAssets = async (markdown, ctx) => {
@@ -9733,8 +10772,20 @@ var buildSource = (def, name, context, runtime) => {
9733
10772
  name,
9734
10773
  pollInterval: def.pollInterval,
9735
10774
  prefix: def.prefix,
9736
- properties: def.properties,
9737
- publishedValue: def.publishedValue
10775
+ properties: def.properties,
10776
+ publishedValue: def.publishedValue
10777
+ }, sourceContext(context, name, runtime));
10778
+ }
10779
+ if (def.type === "github-releases") {
10780
+ return githubReleasesSource({
10781
+ drafts: def.drafts,
10782
+ limit: def.limit,
10783
+ name,
10784
+ owner: def.owner,
10785
+ pollInterval: def.pollInterval,
10786
+ prefix: def.prefix,
10787
+ prereleases: def.prereleases,
10788
+ repo: def.repo
9738
10789
  }, sourceContext(context, name, runtime));
9739
10790
  }
9740
10791
  return mdxRemoteSource({
@@ -9771,12 +10822,32 @@ var resolveSources = (config, context, runtime) => {
9771
10822
  };
9772
10823
 
9773
10824
  // src/core/project-graph.ts
10825
+ var applyConfigOverrides = (config, overrides) => {
10826
+ if (!overrides) {
10827
+ return config;
10828
+ }
10829
+ return {
10830
+ ...config,
10831
+ content: {
10832
+ ...config.content,
10833
+ root: overrides.contentRoot ?? config.content.root
10834
+ },
10835
+ deployment: {
10836
+ ...config.deployment,
10837
+ adapter: overrides.adapter ?? config.deployment.adapter,
10838
+ base: overrides.base ?? config.deployment.base,
10839
+ output: overrides.output ?? config.deployment.output
10840
+ }
10841
+ };
10842
+ };
9774
10843
  var scanProject = async (root, options = {}) => {
9775
10844
  const mode = options.mode ?? "dev";
9776
10845
  const preview = options.preview ?? false;
9777
- const { bridge, config } = await loadConfig(root, {
10846
+ const configResult = await loadConfig(root, {
9778
10847
  devServerUrl: options.devServerUrl
9779
10848
  });
10849
+ const { bridge } = configResult;
10850
+ const config = applyConfigOverrides(configResult.config, options.overrides);
9780
10851
  const context = resolveProjectContext(root, config);
9781
10852
  const sources = resolveSources(config, context, {
9782
10853
  mode,
@@ -9795,7 +10866,7 @@ var scanProject = async (root, options = {}) => {
9795
10866
  for (const { source, entries, diagnostics } of loaded) {
9796
10867
  contentDiagnostics.push(...diagnostics);
9797
10868
  for (const entry of entries) {
9798
- const normalized = normalizeEntry(entry, {
10869
+ const normalized = normalizeEntry2(entry, {
9799
10870
  defaultType: config.content.defaultType,
9800
10871
  i18n: config.i18n,
9801
10872
  source: {
@@ -9844,22 +10915,140 @@ var scanProject = async (root, options = {}) => {
9844
10915
  };
9845
10916
  };
9846
10917
 
10918
+ // src/cli/env.ts
10919
+ import { existsSync as existsSync14, readFileSync as readFileSync8 } from "node:fs";
10920
+ import { dirname as dirname12, join as join23, resolve as resolve9 } from "pathe";
10921
+ var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
10922
+ var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
10923
+ var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
10924
+ var unquote = (raw) => {
10925
+ const double = raw.match(DOUBLE_QUOTED2)?.groups?.body;
10926
+ if (double !== undefined) {
10927
+ return double.replaceAll("\\n", `
10928
+ `).replaceAll("\\t", "\t").replaceAll("\\\"", '"').replaceAll("\\\\", "\\");
10929
+ }
10930
+ const single = raw.match(SINGLE_QUOTED)?.groups?.body;
10931
+ if (single !== undefined) {
10932
+ return single;
10933
+ }
10934
+ return raw;
10935
+ };
10936
+ var parseEnv = (content) => {
10937
+ const env = {};
10938
+ for (const line of content.split(/\r?\n/u)) {
10939
+ if (line.trim() === "" || line.trimStart().startsWith("#")) {
10940
+ continue;
10941
+ }
10942
+ const groups = line.match(ENV_LINE)?.groups;
10943
+ if (groups?.key !== undefined && groups.value !== undefined) {
10944
+ env[groups.key] = unquote(groups.value);
10945
+ }
10946
+ }
10947
+ return env;
10948
+ };
10949
+ var applyEnv = (parsed) => {
10950
+ for (const [key, value] of Object.entries(parsed)) {
10951
+ if (!(key in process.env)) {
10952
+ process.env[key] = value;
10953
+ }
10954
+ }
10955
+ };
10956
+ var loadFile = (path) => {
10957
+ try {
10958
+ if (existsSync14(path)) {
10959
+ applyEnv(parseEnv(readFileSync8(path, "utf-8")));
10960
+ }
10961
+ } catch {}
10962
+ };
10963
+ var loadEnvFiles = (startDir) => {
10964
+ let dir = resolve9(startDir);
10965
+ let done = false;
10966
+ while (!done) {
10967
+ loadFile(join23(dir, ".env.local"));
10968
+ loadFile(join23(dir, ".env"));
10969
+ const parent = dirname12(dir);
10970
+ done = existsSync14(join23(dir, ".git")) || parent === dir;
10971
+ dir = parent;
10972
+ }
10973
+ };
10974
+
10975
+ // src/cli/internal-error.ts
10976
+ var ESC2 = String.fromCodePoint(27);
10977
+ var DIM = `${ESC2}[2m`;
10978
+ var RED = `${ESC2}[31m`;
10979
+ var BOLD = `${ESC2}[1m`;
10980
+ var RESET = `${ESC2}[0m`;
10981
+ var ISSUES_URL = "https://github.com/haydenbleasel/blume/issues";
10982
+ var BLUME_FRAME = /(?<abs>\/[^\s()]*\/\.blume\/[^\s()]*)/gu;
10983
+ var remapBlumeStack = (stack) => stack.replaceAll(BLUME_FRAME, (match) => {
10984
+ const marker = match.indexOf("/.blume/");
10985
+ return `${match.slice(marker + 1)} (generated)`;
10986
+ });
10987
+ var reportInternalError = (error) => {
10988
+ const err = error instanceof Error ? error : new Error(String(error));
10989
+ const lines = [
10990
+ `${RED}${BOLD}BLUME_INTERNAL${RESET} An unexpected error occurred.`,
10991
+ ` ${err.message}`
10992
+ ];
10993
+ const stack = remapBlumeStack(err.stack ?? "").split(`
10994
+ `).slice(1, 5).map((line) => line.trim()).filter(Boolean);
10995
+ if (stack.length > 0) {
10996
+ lines.push("", `${DIM}${stack.join(`
10997
+ `)}${RESET}`);
10998
+ }
10999
+ lines.push("", "This is likely a bug in Blume. Please report it with the details below:", ` ${DIM}Blume: ${getBlumeVersion()}`, ` Node: ${process.version}`, ` Platform: ${process.platform} ${process.arch}${RESET}`, ` ${ISSUES_URL}`);
11000
+ process.stderr.write(`${lines.join(`
11001
+ `)}
11002
+ `);
11003
+ };
11004
+
11005
+ // src/cli/required-secrets.ts
11006
+ var checkRequiredSecrets = (config) => {
11007
+ const diagnostics = [];
11008
+ const requireSecret = (feature, env, note) => {
11009
+ if (process.env[env]) {
11010
+ return;
11011
+ }
11012
+ diagnostics.push({
11013
+ code: "BLUME_MISSING_SECRET",
11014
+ message: `${feature} is enabled but ${env} is not set${note ? ` (${note})` : ""}.`,
11015
+ severity: "warning",
11016
+ suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`
11017
+ });
11018
+ };
11019
+ if (config.ai.ask?.enabled) {
11020
+ const backend = resolveAskBackend(config.ai.ask);
11021
+ if (backend.kind === "gateway") {
11022
+ requireSecret("Ask AI (AI Gateway)", "AI_GATEWAY_API_KEY", "on Vercel the gateway can also authenticate via OIDC");
11023
+ } else {
11024
+ requireSecret("Ask AI", backend.apiKeyEnv);
11025
+ }
11026
+ }
11027
+ if (config.search.provider === "mixedbread") {
11028
+ requireSecret("Mixedbread search", "MIXEDBREAD_API_KEY");
11029
+ }
11030
+ return diagnostics;
11031
+ };
11032
+
9847
11033
  // src/cli/prepare.ts
9848
11034
  var prepareProject = async (options) => {
11035
+ loadEnvFiles(options.root);
9849
11036
  let project;
9850
11037
  try {
9851
11038
  project = await scanProject(options.root, {
9852
11039
  devServerUrl: options.devServerUrl,
9853
11040
  mode: options.mode,
11041
+ overrides: options.overrides,
9854
11042
  preview: options.preview,
9855
11043
  refresh: options.refresh
9856
11044
  });
9857
11045
  } catch (error) {
9858
11046
  if (error instanceof BlumeError) {
9859
11047
  reportDiagnostics([error.diagnostic], options.root);
9860
- process.exit(1);
11048
+ } else {
11049
+ reportInternalError(error);
9861
11050
  }
9862
- throw error;
11051
+ process.exit(1);
9863
11052
  }
9864
11053
  if (options.mode === "build" && project.config.deployment.output === "static") {
9865
11054
  const features = serverFeatures(project.config);
@@ -9887,12 +11076,102 @@ var prepareProject = async (options) => {
9887
11076
  for (const warning of warnings) {
9888
11077
  logger.warn(warning);
9889
11078
  }
11079
+ reportDiagnostics(checkRequiredSecrets(project.config), options.root);
9890
11080
  return project;
9891
11081
  };
9892
11082
 
9893
11083
  // src/cli/commands/build.ts
11084
+ var ADAPTERS = ["vercel", "node", "netlify", "cloudflare"];
11085
+ var emitRedirectFiles = async (config, distDir) => {
11086
+ const { redirects } = config;
11087
+ if (redirects.length === 0 || config.deployment.output !== "static") {
11088
+ return;
11089
+ }
11090
+ await writeFile6(join24(distDir, "blume-redirects.json"), buildRedirectManifest(redirects), "utf-8");
11091
+ const platformFiles = [
11092
+ { content: buildNetlifyRedirects(redirects), name: "_redirects" },
11093
+ { content: buildVercelConfig(redirects), name: "vercel.json" }
11094
+ ];
11095
+ await Promise.all(platformFiles.map((file) => existsSync15(join24(distDir, file.name)) ? Promise.resolve() : writeFile6(join24(distDir, file.name), file.content, "utf-8")));
11096
+ logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
11097
+ };
11098
+ var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(bytes < 1024 * 100 ? 1 : 0)} kB`;
11099
+ var astroAssets = async (distDir, ext) => {
11100
+ const astroDir = join24(distDir, "_astro");
11101
+ if (!existsSync15(astroDir)) {
11102
+ return [];
11103
+ }
11104
+ const entries = await readdir(astroDir);
11105
+ const files = entries.filter((name) => name.endsWith(`.${ext}`));
11106
+ const sized = await Promise.all(files.map(async (name) => {
11107
+ const info = await stat(join24(astroDir, name));
11108
+ return { name, size: info.size };
11109
+ }));
11110
+ return sized.toSorted((a, b) => b.size - a.size);
11111
+ };
11112
+ var totalSize = (assets) => assets.reduce((sum, asset) => sum + asset.size, 0);
11113
+ var reportBundleSizes = async (distDir) => {
11114
+ const sized = await astroAssets(distDir, "js");
11115
+ if (sized.length === 0) {
11116
+ logger.info("No client JavaScript emitted — the site ships zero JS.");
11117
+ return;
11118
+ }
11119
+ const rows = sized.slice(0, 15).map((file) => ` ${formatBytes(file.size).padStart(8)} ${file.name}`);
11120
+ logger.box([
11121
+ `Client JavaScript — ${sized.length} file(s), ${formatBytes(totalSize(sized))} total`,
11122
+ "",
11123
+ ...rows,
11124
+ sized.length > 15 ? ` … and ${sized.length - 15} more` : null
11125
+ ].filter((line) => line !== null).join(`
11126
+ `));
11127
+ };
11128
+ var enforceBudget = async (distDir, args) => {
11129
+ const checks = [
11130
+ ...args["budget-js"] ? [{ ext: "js", limitKb: Number(args["budget-js"]), name: "JavaScript" }] : [],
11131
+ ...args["budget-css"] ? [{ ext: "css", limitKb: Number(args["budget-css"]), name: "CSS" }] : []
11132
+ ];
11133
+ if (checks.length === 0) {
11134
+ return "skip";
11135
+ }
11136
+ let passed = true;
11137
+ for (const check of checks) {
11138
+ const total = totalSize(await astroAssets(distDir, check.ext));
11139
+ const limit = check.limitKb * 1024;
11140
+ if (total > limit) {
11141
+ passed = false;
11142
+ logger.error(`${check.name} budget exceeded: ${formatBytes(total)} > ${check.limitKb} kB`);
11143
+ } else {
11144
+ logger.success(`${check.name} budget: ${formatBytes(total)} / ${check.limitKb} kB`);
11145
+ }
11146
+ }
11147
+ return passed ? "pass" : "fail";
11148
+ };
9894
11149
  var buildCommand = defineCommand2({
9895
11150
  args: {
11151
+ adapter: {
11152
+ description: "Server adapter: vercel | node | netlify | cloudflare.",
11153
+ type: "string"
11154
+ },
11155
+ analyze: {
11156
+ description: "Report client JavaScript bundle sizes after the build.",
11157
+ type: "boolean"
11158
+ },
11159
+ base: {
11160
+ description: "Base path the site is served under (e.g. /docs).",
11161
+ type: "string"
11162
+ },
11163
+ "budget-css": {
11164
+ description: "Fail if total client CSS exceeds this many kB.",
11165
+ type: "string"
11166
+ },
11167
+ "budget-js": {
11168
+ description: "Fail if total client JavaScript exceeds this many kB.",
11169
+ type: "string"
11170
+ },
11171
+ output: {
11172
+ description: "Output mode: static | server.",
11173
+ type: "string"
11174
+ },
9896
11175
  preview: {
9897
11176
  description: "Include drafts and unpublished CMS content.",
9898
11177
  type: "boolean"
@@ -9905,8 +11184,21 @@ var buildCommand = defineCommand2({
9905
11184
  },
9906
11185
  async run({ args }) {
9907
11186
  const root = process.cwd();
11187
+ if (args.output && args.output !== "static" && args.output !== "server") {
11188
+ logger.error(`Invalid --output "${args.output}" (use static | server).`);
11189
+ process.exit(1);
11190
+ }
11191
+ if (args.adapter && !ADAPTERS.includes(args.adapter)) {
11192
+ logger.error(`Invalid --adapter "${args.adapter}" (use ${ADAPTERS.join(" | ")}).`);
11193
+ process.exit(1);
11194
+ }
9908
11195
  const project = await prepareProject({
9909
11196
  mode: "build",
11197
+ overrides: {
11198
+ adapter: args.adapter,
11199
+ base: args.base,
11200
+ output: args.output
11201
+ },
9910
11202
  preview: args.preview,
9911
11203
  root,
9912
11204
  strict: args.strict
@@ -9916,7 +11208,7 @@ var buildCommand = defineCommand2({
9916
11208
  logLevel: "info",
9917
11209
  root: project.context.outDir
9918
11210
  });
9919
- const distDir = join23(root, "dist");
11211
+ const distDir = join24(root, "dist");
9920
11212
  if (project.config.search.provider === "pagefind") {
9921
11213
  logger.start("Building search index");
9922
11214
  const indexed = await buildSearchIndex(distDir);
@@ -9930,21 +11222,22 @@ var buildCommand = defineCommand2({
9930
11222
  if (project.config.ai.llmsTxt) {
9931
11223
  const { index, full } = await buildLlmsFiles(project);
9932
11224
  await Promise.all([
9933
- writeFile6(join23(distDir, "llms.txt"), index, "utf-8"),
9934
- writeFile6(join23(distDir, "llms-full.txt"), full, "utf-8")
11225
+ writeFile6(join24(distDir, "llms.txt"), index, "utf-8"),
11226
+ writeFile6(join24(distDir, "llms-full.txt"), full, "utf-8")
9935
11227
  ]);
9936
11228
  logger.success("Generated llms.txt and llms-full.txt");
9937
11229
  }
9938
11230
  const sitemap = buildSitemap(project);
9939
- if (sitemap && !existsSync11(join23(distDir, "sitemap.xml"))) {
9940
- await writeFile6(join23(distDir, "sitemap.xml"), sitemap, "utf-8");
11231
+ if (sitemap && !existsSync15(join24(distDir, "sitemap.xml"))) {
11232
+ await writeFile6(join24(distDir, "sitemap.xml"), sitemap, "utf-8");
9941
11233
  logger.success("Generated sitemap.xml");
9942
11234
  }
9943
11235
  const robots = buildRobots(project);
9944
- if (robots && !existsSync11(join23(distDir, "robots.txt"))) {
9945
- await writeFile6(join23(distDir, "robots.txt"), robots, "utf-8");
11236
+ if (robots && !existsSync15(join24(distDir, "robots.txt"))) {
11237
+ await writeFile6(join24(distDir, "robots.txt"), robots, "utf-8");
9946
11238
  logger.success("Generated robots.txt");
9947
11239
  }
11240
+ await emitRedirectFiles(project.config, distDir);
9948
11241
  const { config } = project;
9949
11242
  const features = serverFeatures(config);
9950
11243
  logger.box([
@@ -9959,16 +11252,113 @@ var buildCommand = defineCommand2({
9959
11252
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`
9960
11253
  ].join(`
9961
11254
  `));
11255
+ if (args.analyze) {
11256
+ await reportBundleSizes(distDir);
11257
+ }
11258
+ if (await enforceBudget(distDir, args) === "fail") {
11259
+ process.exit(1);
11260
+ }
9962
11261
  logger.success(`Built to ${distDir}`);
9963
11262
  }
9964
11263
  });
9965
11264
 
11265
+ // src/cli/commands/check.ts
11266
+ import { existsSync as existsSync16 } from "node:fs";
11267
+ import { check } from "@astrojs/check";
11268
+ import { sync } from "astro";
11269
+ import { defineCommand as defineCommand3 } from "citty";
11270
+ import { join as join25 } from "pathe";
11271
+ var checkCommand = defineCommand3({
11272
+ args: {
11273
+ preview: {
11274
+ description: "Include drafts and unpublished CMS content.",
11275
+ type: "boolean"
11276
+ },
11277
+ strict: {
11278
+ description: "Fail on content diagnostics as well as type errors.",
11279
+ type: "boolean"
11280
+ }
11281
+ },
11282
+ meta: {
11283
+ description: "Type-check the docs site with astro check.",
11284
+ name: "check"
11285
+ },
11286
+ async run({ args }) {
11287
+ const root = process.cwd();
11288
+ const project = await prepareProject({
11289
+ mode: "build",
11290
+ preview: args.preview,
11291
+ root,
11292
+ strict: args.strict
11293
+ });
11294
+ const { outDir } = project.context;
11295
+ await sync({ logLevel: "warn", root: outDir });
11296
+ const tsconfig = join25(root, "tsconfig.json");
11297
+ logger.start(`Type-checking ${project.graph.pages.length} page(s)`);
11298
+ const failed = await check({
11299
+ minimumFailingSeverity: "error",
11300
+ minimumSeverity: "hint",
11301
+ root: outDir,
11302
+ tsconfig: existsSync16(tsconfig) ? tsconfig : undefined,
11303
+ watch: false
11304
+ });
11305
+ if (failed) {
11306
+ logger.error("Type check failed.");
11307
+ process.exit(1);
11308
+ }
11309
+ logger.success("No type errors.");
11310
+ }
11311
+ });
11312
+
9966
11313
  // src/cli/commands/dev.ts
9967
11314
  import { watch } from "node:fs";
9968
11315
  import { dev } from "astro";
9969
- import { defineCommand as defineCommand3 } from "citty";
9970
- var devCommand = defineCommand3({
11316
+ import { defineCommand as defineCommand4 } from "citty";
11317
+
11318
+ // src/astro/integration.ts
11319
+ var overlayServer = null;
11320
+ var overlayChannel = () => overlayServer?.ws ?? overlayServer?.hot;
11321
+ var showBlumeErrorOverlay = (diagnostics) => {
11322
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map(enrichDiagnostic);
11323
+ const channel = overlayChannel();
11324
+ if (errors.length === 0 || !channel) {
11325
+ return;
11326
+ }
11327
+ const body = errors.map((diagnostic) => {
11328
+ const where = diagnostic.file ? `
11329
+ at ${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}` : ""}` : "";
11330
+ const fix = diagnostic.suggestion ? `
11331
+ fix: ${diagnostic.suggestion}` : "";
11332
+ const docs = diagnostic.docsUrl ? `
11333
+ docs: ${diagnostic.docsUrl}` : "";
11334
+ return `[${diagnostic.code}] ${diagnostic.message}${where}${fix}${docs}`;
11335
+ }).join(`
11336
+
11337
+ `);
11338
+ channel.send({
11339
+ err: {
11340
+ id: errors[0]?.file,
11341
+ message: `Blume found ${errors.length} error(s):
11342
+
11343
+ ${body}`,
11344
+ plugin: "blume",
11345
+ stack: ""
11346
+ },
11347
+ type: "error"
11348
+ });
11349
+ };
11350
+
11351
+ // src/cli/commands/dev.ts
11352
+ var devCommand = defineCommand4({
9971
11353
  args: {
11354
+ "content-dir": {
11355
+ description: "Content folder to scan, overriding config (content.root).",
11356
+ type: "string"
11357
+ },
11358
+ debug: {
11359
+ description: "Verbose Astro/Vite logging for troubleshooting.",
11360
+ type: "boolean"
11361
+ },
9972
11362
  host: { description: "Network host to bind.", type: "string" },
9973
11363
  open: { description: "Open the browser on start.", type: "boolean" },
9974
11364
  port: { description: "Port to listen on.", type: "string" },
@@ -9985,11 +11375,13 @@ var devCommand = defineCommand3({
9985
11375
  async run({ args }) {
9986
11376
  const root = process.cwd();
9987
11377
  const preview = args.preview ?? false;
11378
+ const overrides = args["content-dir"] ? { contentRoot: args["content-dir"] } : undefined;
9988
11379
  const port = args.port ? Number(args.port) : 4321;
9989
11380
  const devServerUrl = `http://localhost:${port}`;
9990
11381
  const project = await prepareProject({
9991
11382
  devServerUrl,
9992
11383
  mode: "dev",
11384
+ overrides,
9993
11385
  preview,
9994
11386
  root,
9995
11387
  strict: args.strict
@@ -9998,7 +11390,7 @@ var devCommand = defineCommand3({
9998
11390
  logger.info('Detected docs.json — running in Mintlify bridge mode (no migration). Run "blume migrate mintlify" to convert permanently.');
9999
11391
  }
10000
11392
  const server = await dev({
10001
- logLevel: "info",
11393
+ logLevel: args.debug ? "debug" : "info",
10002
11394
  root: project.context.outDir,
10003
11395
  server: {
10004
11396
  host: args.host ?? false,
@@ -10006,6 +11398,7 @@ var devCommand = defineCommand3({
10006
11398
  port: args.port ? Number(args.port) : undefined
10007
11399
  }
10008
11400
  });
11401
+ showBlumeErrorOverlay(project.diagnostics);
10009
11402
  let timer = null;
10010
11403
  const regenerate = () => {
10011
11404
  if (timer) {
@@ -10016,9 +11409,11 @@ var devCommand = defineCommand3({
10016
11409
  const next = await scanProject(root, {
10017
11410
  devServerUrl,
10018
11411
  mode: "dev",
11412
+ overrides,
10019
11413
  preview
10020
11414
  });
10021
11415
  await generateRuntime(next);
11416
+ showBlumeErrorOverlay(next.diagnostics);
10022
11417
  } catch (error) {
10023
11418
  logger.error(`Regeneration failed: ${error.message}`);
10024
11419
  }
@@ -10050,14 +11445,20 @@ var devCommand = defineCommand3({
10050
11445
  });
10051
11446
 
10052
11447
  // src/cli/commands/doctor.ts
10053
- import { defineCommand as defineCommand4 } from "citty";
11448
+ import { defineCommand as defineCommand5 } from "citty";
10054
11449
  var MIN_NODE_MAJOR = 20;
10055
- var doctorCommand = defineCommand4({
11450
+ var doctorCommand = defineCommand5({
11451
+ args: {
11452
+ json: {
11453
+ description: "Emit diagnostics as JSON on stdout (for CI/editors).",
11454
+ type: "boolean"
11455
+ }
11456
+ },
10056
11457
  meta: {
10057
11458
  description: "Diagnose common configuration and content problems.",
10058
11459
  name: "doctor"
10059
11460
  },
10060
- async run() {
11461
+ async run({ args }) {
10061
11462
  const root = process.cwd();
10062
11463
  const diagnostics = [];
10063
11464
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
@@ -10089,16 +11490,25 @@ var doctorCommand = defineCommand4({
10089
11490
  suggestion: 'Set deployment.adapter (e.g. "vercel").'
10090
11491
  });
10091
11492
  }
10092
- logger.info(`Pages: ${project.graph.pages.length}`);
10093
- logger.info(`Output: ${config.deployment.output}`);
10094
- logger.info(`Search: ${config.search.provider}`);
11493
+ if (!args.json) {
11494
+ logger.info(`Pages: ${project.graph.pages.length}`);
11495
+ logger.info(`Output: ${config.deployment.output}`);
11496
+ logger.info(`Search: ${config.search.provider}`);
11497
+ }
10095
11498
  } catch (error) {
10096
11499
  if (error instanceof BlumeError) {
10097
11500
  diagnostics.push(error.diagnostic);
10098
11501
  } else {
10099
- throw error;
11502
+ reportInternalError(error);
11503
+ process.exit(1);
10100
11504
  }
10101
11505
  }
11506
+ if (args.json) {
11507
+ if (reportDiagnosticsJson(diagnostics, root)) {
11508
+ process.exit(1);
11509
+ }
11510
+ return;
11511
+ }
10102
11512
  const hadErrors = reportDiagnostics(diagnostics, root);
10103
11513
  if (diagnostics.length === 0) {
10104
11514
  logger.success("No problems found.");
@@ -10111,19 +11521,40 @@ var doctorCommand = defineCommand4({
10111
11521
 
10112
11522
  // src/cli/commands/eject.ts
10113
11523
  import { readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
10114
- import { defineCommand as defineCommand5 } from "citty";
10115
- import { join as join25, relative as relative14 } from "pathe";
11524
+ import { defineCommand as defineCommand6 } from "citty";
11525
+ import { join as join27, relative as relative15 } from "pathe";
10116
11526
 
10117
11527
  // src/registry/eject.ts
10118
- import { existsSync as existsSync12 } from "node:fs";
11528
+ import { existsSync as existsSync17 } from "node:fs";
10119
11529
  import { cp, mkdir as mkdir5, readFile as readFile14, rm as rm2, writeFile as writeFile7 } from "node:fs/promises";
10120
- import { join as join24, relative as relative13 } from "pathe";
11530
+ import { join as join26, relative as relative14 } from "pathe";
10121
11531
  var POSIX = (path) => path.split("\\").join("/");
11532
+ var askFiles = async (project, srcDir, genDir) => {
11533
+ const { ask } = project.config.ai;
11534
+ if (!ask?.enabled) {
11535
+ return [];
11536
+ }
11537
+ const grounded = ask.provider !== "inkeep";
11538
+ const files = [
11539
+ {
11540
+ content: askEndpointTemplate(resolveAskBackend(ask), grounded),
11541
+ path: join26(srcDir, "pages", "api", "ask.ts")
11542
+ }
11543
+ ];
11544
+ if (grounded) {
11545
+ files.push({
11546
+ content: `${JSON.stringify(await buildAskData(project))}
11547
+ `,
11548
+ path: join26(genDir, "ask-data.json")
11549
+ });
11550
+ }
11551
+ return files;
11552
+ };
10122
11553
  var eject = async (root) => {
10123
11554
  const project = await scanProject(root, { mode: "build" });
10124
11555
  const { context, config } = project;
10125
- const srcDir = join24(root, "src");
10126
- const genDir = join24(srcDir, "generated");
11556
+ const srcDir = join26(root, "src");
11557
+ const genDir = join26(srcDir, "generated");
10127
11558
  const askEnabled = config.ai.ask?.enabled ?? false;
10128
11559
  const exportPdf = config.export.pdf;
10129
11560
  const exportEpub = config.export.epub;
@@ -10144,13 +11575,13 @@ var eject = async (root) => {
10144
11575
  const needsSvelte = frameworks.has("svelte");
10145
11576
  const relContext = {
10146
11577
  ...context,
10147
- contentRoot: POSIX(relative13(root, context.contentRoot)),
11578
+ contentRoot: POSIX(relative14(root, context.contentRoot)),
10148
11579
  outDir: ".",
10149
11580
  root: "."
10150
11581
  };
10151
- const componentsImport = context.componentsFile ? `../../${POSIX(relative13(root, context.componentsFile))}` : null;
11582
+ const componentsImport = context.componentsFile ? `../../${POSIX(relative14(root, context.componentsFile))}` : null;
10152
11583
  const relPages = pages.map((page) => ({
10153
- entrypoint: POSIX(relative13(root, page.entrypoint)),
11584
+ entrypoint: POSIX(relative14(root, page.entrypoint)),
10154
11585
  pattern: page.pattern
10155
11586
  }));
10156
11587
  const staged = collectStaged(project);
@@ -10171,13 +11602,13 @@ var eject = async (root) => {
10171
11602
  searchClientPath: "./src/generated/search-client.ts",
10172
11603
  themePath: "./src/generated/app.css"
10173
11604
  }),
10174
- path: join24(root, "astro.config.mjs")
11605
+ path: join26(root, "astro.config.mjs")
10175
11606
  },
10176
11607
  {
10177
11608
  content: runtimeTsconfigTemplate(),
10178
- path: join24(root, "tsconfig.json")
11609
+ path: join26(root, "tsconfig.json")
10179
11610
  },
10180
- { content: envTemplate(), path: join24(srcDir, "env.d.ts") },
11611
+ { content: envTemplate(), path: join26(srcDir, "env.d.ts") },
10181
11612
  {
10182
11613
  content: contentConfigTemplate({
10183
11614
  config,
@@ -10185,28 +11616,29 @@ var eject = async (root) => {
10185
11616
  staged: hasStaged,
10186
11617
  stagedBase: stagedDir
10187
11618
  }),
10188
- path: join24(srcDir, "content.config.ts")
11619
+ path: join26(srcDir, "content.config.ts")
10189
11620
  },
10190
11621
  {
10191
11622
  content: catchAllPageTemplate({
10192
11623
  askEnabled,
10193
11624
  exportEpub,
10194
11625
  exportPdf,
10195
- mathEnabled: config.markdown.math
11626
+ mathEnabled: config.markdown.math,
11627
+ needsReact
10196
11628
  }),
10197
- path: join24(srcDir, "pages", "[...slug].astro")
11629
+ path: join26(srcDir, "pages", "[...slug].astro")
10198
11630
  },
10199
11631
  {
10200
- content: userComponentsTemplate(componentsImport),
10201
- path: join24(genDir, "components.ts")
11632
+ content: planComponentSlots(componentsImport, null).module,
11633
+ path: join26(genDir, "components.ts")
10202
11634
  },
10203
11635
  {
10204
11636
  content: islandMapTemplate(islands.islands),
10205
- path: join24(genDir, "islands.ts")
11637
+ path: join26(genDir, "islands.ts")
10206
11638
  },
10207
11639
  {
10208
11640
  content: exampleMapTemplate(examples.examples),
10209
- path: join24(genDir, "examples.ts")
11641
+ path: join26(genDir, "examples.ts")
10210
11642
  },
10211
11643
  {
10212
11644
  content: tailwindEntryTemplate({
@@ -10218,60 +11650,57 @@ var eject = async (root) => {
10218
11650
  twoslashCss: twoslashCss(),
10219
11651
  userTheme
10220
11652
  }),
10221
- path: join24(genDir, "app.css")
11653
+ path: join26(genDir, "app.css")
10222
11654
  },
10223
- { content: buildRuntimeData(project), path: join24(genDir, "data.json") },
11655
+ { content: buildRuntimeData(project), path: join26(genDir, "data.json") },
10224
11656
  {
10225
11657
  content: `${JSON.stringify(rawMarkdown)}
10226
11658
  `,
10227
- path: join24(genDir, "raw-markdown.json")
11659
+ path: join26(genDir, "raw-markdown.json")
10228
11660
  },
10229
11661
  {
10230
11662
  content: rawMarkdownEndpointTemplate(),
10231
- path: join24(srcDir, "pages", "[...slug].md.ts")
11663
+ path: join26(srcDir, "pages", "[...slug].md.ts")
10232
11664
  },
10233
11665
  {
10234
11666
  content: rawMarkdownEndpointTemplate(),
10235
- path: join24(srcDir, "pages", "[...slug].mdx.ts")
11667
+ path: join26(srcDir, "pages", "[...slug].mdx.ts")
10236
11668
  }
10237
11669
  ];
10238
11670
  if (askEnabled) {
10239
- files.push({
10240
- content: askEndpointTemplate(resolveAskBackend(config.ai.ask)),
10241
- path: join24(srcDir, "pages", "api", "ask.ts")
10242
- });
11671
+ files.push(...await askFiles(project, srcDir, genDir));
10243
11672
  }
10244
11673
  if (config.seo.og.enabled) {
10245
11674
  files.push({
10246
- content: ogEndpointTemplate(customOgRoutes(pages, config.title, config.description)),
10247
- path: join24(srcDir, "pages", "og", "[...slug].png.ts")
11675
+ content: ogEndpointTemplate(customOgRoutes(pages, config.title)),
11676
+ path: join26(srcDir, "pages", "og", "[...slug].png.ts")
10248
11677
  });
10249
11678
  }
10250
11679
  if (!routeIsTaken(pages, project.graph.pages, "/404")) {
10251
11680
  files.push({
10252
11681
  content: notFoundPageTemplate(),
10253
- path: join24(srcDir, "pages", "404.astro")
11682
+ path: join26(srcDir, "pages", "404.astro")
10254
11683
  });
10255
11684
  }
10256
11685
  files.push({
10257
11686
  content: searchClientTemplate(config),
10258
- path: join24(genDir, "search-client.ts")
11687
+ path: join26(genDir, "search-client.ts")
10259
11688
  });
10260
11689
  if (servesStaticIndex(config.search.provider)) {
10261
11690
  const documents = await buildSearchDocuments(project);
10262
11691
  files.push({
10263
11692
  content: `${JSON.stringify(documents)}
10264
11693
  `,
10265
- path: join24(genDir, "search.json")
11694
+ path: join26(genDir, "search.json")
10266
11695
  }, {
10267
11696
  content: searchEndpointTemplate(),
10268
- path: join24(srcDir, "pages", "blume-search.json.ts")
11697
+ path: join26(srcDir, "pages", "blume-search.json.ts")
10269
11698
  });
10270
11699
  }
10271
11700
  if (config.search.provider === "mixedbread") {
10272
11701
  files.push({
10273
11702
  content: mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""),
10274
- path: join24(srcDir, "pages", "api", "search.ts")
11703
+ path: join26(srcDir, "pages", "api", "search.ts")
10275
11704
  });
10276
11705
  }
10277
11706
  const feeds = buildRssFeeds(project);
@@ -10280,10 +11709,10 @@ var eject = async (root) => {
10280
11709
  files.push({
10281
11710
  content: `${JSON.stringify(feedXml)}
10282
11711
  `,
10283
- path: join24(genDir, "rss.json")
11712
+ path: join26(genDir, "rss.json")
10284
11713
  }, {
10285
11714
  content: rssEndpointTemplate(),
10286
- path: join24(srcDir, "pages", "[section]", "rss.xml.ts")
11715
+ path: join26(srcDir, "pages", "[section]", "rss.xml.ts")
10287
11716
  });
10288
11717
  }
10289
11718
  if (hasReferences(config)) {
@@ -10295,27 +11724,27 @@ var eject = async (root) => {
10295
11724
  for (const file of references.files) {
10296
11725
  files.push({
10297
11726
  content: file.content,
10298
- path: join24(srcDir, "pages", file.pagePath)
11727
+ path: join26(srcDir, "pages", file.pagePath)
10299
11728
  });
10300
11729
  }
10301
11730
  }
10302
11731
  files.push(...islands.islands.map((island) => ({
10303
11732
  content: islandWrapperTemplate(island),
10304
- path: join24(genDir, "islands", `${island.name}.astro`)
11733
+ path: join26(genDir, "islands", `${island.name}.astro`)
10305
11734
  })), ...examples.examples.map((example) => ({
10306
11735
  content: exampleWrapperTemplate(example),
10307
- path: join24(genDir, "examples", `${exampleSlug(example.path)}.astro`)
11736
+ path: join26(genDir, "examples", `${exampleSlug(example.path)}.astro`)
10308
11737
  })));
10309
11738
  for (const [entryId, content] of staged) {
10310
- files.push({ content, path: join24(root, stagedDir, entryId) });
11739
+ files.push({ content, path: join26(root, stagedDir, entryId) });
10311
11740
  }
10312
11741
  await Promise.all(files.map(async (file) => {
10313
- await mkdir5(join24(file.path, ".."), { recursive: true });
11742
+ await mkdir5(join26(file.path, ".."), { recursive: true });
10314
11743
  await writeFile7(file.path, file.content, "utf-8");
10315
11744
  }));
10316
- const assetsSrc = join24(context.outDir, "public", "blume-assets");
10317
- if (existsSync12(assetsSrc)) {
10318
- await cp(assetsSrc, join24(root, "public", "blume-assets"), {
11745
+ const assetsSrc = join26(context.outDir, "public", "blume-assets");
11746
+ if (existsSync17(assetsSrc)) {
11747
+ await cp(assetsSrc, join26(root, "public", "blume-assets"), {
10319
11748
  recursive: true
10320
11749
  });
10321
11750
  }
@@ -10325,7 +11754,7 @@ var eject = async (root) => {
10325
11754
 
10326
11755
  // src/cli/commands/eject.ts
10327
11756
  var updatePackageScripts = async (root) => {
10328
- const pkgPath = join25(root, "package.json");
11757
+ const pkgPath = join27(root, "package.json");
10329
11758
  let pkg;
10330
11759
  try {
10331
11760
  pkg = JSON.parse(await readFile15(pkgPath, "utf-8"));
@@ -10342,7 +11771,7 @@ var updatePackageScripts = async (root) => {
10342
11771
  await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
10343
11772
  `, "utf-8");
10344
11773
  };
10345
- var ejectCommand = defineCommand5({
11774
+ var ejectCommand = defineCommand6({
10346
11775
  args: {
10347
11776
  yes: { description: "Skip the confirmation prompt.", type: "boolean" }
10348
11777
  },
@@ -10361,7 +11790,7 @@ var ejectCommand = defineCommand5({
10361
11790
  await updatePackageScripts(root);
10362
11791
  logger.success(`Ejected ${files.length} file(s):`);
10363
11792
  for (const file of files) {
10364
- process.stdout.write(` ${relative14(root, file)}
11793
+ process.stdout.write(` ${relative15(root, file)}
10365
11794
  `);
10366
11795
  }
10367
11796
  logger.box(`Your project is now a standalone Astro app.
@@ -10374,10 +11803,10 @@ The blume package remains importable.`);
10374
11803
  });
10375
11804
 
10376
11805
  // src/cli/commands/init.ts
10377
- import { existsSync as existsSync13 } from "node:fs";
11806
+ import { existsSync as existsSync18 } from "node:fs";
10378
11807
  import { mkdir as mkdir6, writeFile as writeFile9 } from "node:fs/promises";
10379
- import { defineCommand as defineCommand6 } from "citty";
10380
- import { basename as basename4, dirname as dirname11, join as join26 } from "pathe";
11808
+ import { defineCommand as defineCommand7 } from "citty";
11809
+ import { basename as basename4, dirname as dirname13, join as join28 } from "pathe";
10381
11810
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
10382
11811
  var packageTemplate = (name, version) => `{
10383
11812
  "name": ${JSON.stringify(name)},
@@ -10393,41 +11822,130 @@ var packageTemplate = (name, version) => `{
10393
11822
  }
10394
11823
  }
10395
11824
  `;
10396
- var CONFIG_TEMPLATE = `import { defineConfig } from "blume";
11825
+ var TEMPLATES = ["docs", "api", "sdk", "changelog"];
11826
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
11827
+ var configFor = (extra) => `import { defineConfig } from "blume";
10397
11828
 
10398
11829
  export default defineConfig({
10399
11830
  title: "My Docs",
10400
- description: "Documentation powered by Blume.",
11831
+ description: "Documentation powered by Blume.",${extra}
10401
11832
  });
10402
11833
  `;
10403
- var INDEX_TEMPLATE = `---
10404
- title: Introduction
10405
- description: Welcome to your new Blume docs.
11834
+ var page = (title, description, body) => `---
11835
+ title: ${title}
11836
+ description: ${description}
11837
+ ---
11838
+
11839
+ ${body}
11840
+ `;
11841
+ var STARTERS = {
11842
+ api: {
11843
+ config: configFor(`
11844
+ openapi: {
11845
+ enabled: true,
11846
+ route: "/api",
11847
+ sources: [
11848
+ {
11849
+ label: "Petstore",
11850
+ spec: "https://petstore3.swagger.io/api/v3/openapi.json",
11851
+ },
11852
+ ],
11853
+ },`),
11854
+ files: (dir) => [
11855
+ {
11856
+ content: page("API Reference", "Explore the API.", "# API Reference\n\nYour OpenAPI spec renders at [`/api`](/api). Point `openapi.sources` at your own spec in `blume.config.ts`."),
11857
+ path: join28(dir, "index.mdx")
11858
+ }
11859
+ ]
11860
+ },
11861
+ changelog: {
11862
+ config: configFor(`
11863
+ navigation: {
11864
+ tabs: [
11865
+ { label: "Docs", path: "/" },
11866
+ { label: "Changelog", path: "/changelog" },
11867
+ ],
11868
+ },`),
11869
+ files: (dir) => [
11870
+ {
11871
+ content: page("Introduction", "Welcome to your new Blume docs.", "# Introduction\n\nWrite your docs here, and log releases under `changelog/`."),
11872
+ path: join28(dir, "index.mdx")
11873
+ },
11874
+ {
11875
+ content: `---
11876
+ title: v1.0.0
11877
+ type: changelog
11878
+ date: 2026-01-01
10406
11879
  ---
10407
11880
 
10408
- # Introduction
11881
+ The first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries beside it.
11882
+ `,
11883
+ path: join28(dir, "changelog", "v1-0-0.mdx")
11884
+ }
11885
+ ]
11886
+ },
11887
+ docs: {
11888
+ config: configFor(""),
11889
+ files: (dir) => [
11890
+ {
11891
+ content: page("Introduction", "Welcome to your new Blume docs.", `# Introduction
10409
11892
 
10410
11893
  Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
10411
11894
 
10412
- Edit \`docs/index.mdx\` to get started, then run \`blume dev\`.
10413
- `;
11895
+ Edit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`),
11896
+ path: join28(dir, "index.mdx")
11897
+ }
11898
+ ]
11899
+ },
11900
+ sdk: {
11901
+ config: configFor(""),
11902
+ files: (dir) => [
11903
+ {
11904
+ content: page("Introduction", "Get started with the SDK.", `# Introduction
11905
+
11906
+ Install the SDK and make your first call. See [Installation](/installation).`),
11907
+ path: join28(dir, "index.mdx")
11908
+ },
11909
+ {
11910
+ content: page("Installation", "Install the SDK.", "# Installation\n\n```package-install\nyour-sdk\n```"),
11911
+ path: join28(dir, "installation.mdx")
11912
+ }
11913
+ ]
11914
+ }
11915
+ };
11916
+ var commandsFor = (pm) => ({
11917
+ dev: pm === "npm" ? "npm run dev" : `${pm} dev`,
11918
+ install: `${pm} install`
11919
+ });
10414
11920
  var writeFileSafe = async (path, content) => {
10415
- if (existsSync13(path)) {
11921
+ if (existsSync18(path)) {
10416
11922
  logger.info(`Skipped existing ${path}`);
10417
11923
  return false;
10418
11924
  }
10419
- await mkdir6(dirname11(path), { recursive: true });
11925
+ await mkdir6(dirname13(path), { recursive: true });
10420
11926
  await writeFile9(path, content, "utf-8");
10421
11927
  logger.success(`Created ${path}`);
10422
11928
  return true;
10423
11929
  };
10424
- var initCommand = defineCommand6({
11930
+ var initCommand = defineCommand7({
10425
11931
  args: {
10426
11932
  "content-dir": {
10427
11933
  default: "docs",
10428
11934
  description: "Content directory.",
10429
11935
  type: "string"
10430
11936
  },
11937
+ eject: {
11938
+ description: "Eject to a standalone Astro project after scaffolding.",
11939
+ type: "boolean"
11940
+ },
11941
+ "package-manager": {
11942
+ description: "Package manager for the next-steps hint (npm|pnpm|yarn|bun).",
11943
+ type: "string"
11944
+ },
11945
+ template: {
11946
+ description: "Starter template: docs | api | sdk | changelog.",
11947
+ type: "string"
11948
+ },
10431
11949
  yes: { description: "Skip prompts.", type: "boolean" }
10432
11950
  },
10433
11951
  meta: {
@@ -10437,34 +11955,65 @@ var initCommand = defineCommand6({
10437
11955
  async run({ args }) {
10438
11956
  const root = process.cwd();
10439
11957
  const contentDir = args["content-dir"] ?? "docs";
10440
- const createdPackage = await writeFileSafe(join26(root, "package.json"), packageTemplate(toPackageName(basename4(root)), getBlumeVersion()));
10441
- await writeFileSafe(join26(root, "blume.config.ts"), CONFIG_TEMPLATE);
10442
- await writeFileSafe(join26(root, contentDir, "index.mdx"), INDEX_TEMPLATE);
11958
+ const template = args.template ?? "docs";
11959
+ if (!TEMPLATES.includes(template)) {
11960
+ logger.error(`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`);
11961
+ process.exit(1);
11962
+ }
11963
+ const pm = args["package-manager"] ?? "npm";
11964
+ if (!PACKAGE_MANAGERS.includes(pm)) {
11965
+ logger.error(`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`);
11966
+ process.exit(1);
11967
+ }
11968
+ const starter = STARTERS[template];
11969
+ const createdPackage = await writeFileSafe(join28(root, "package.json"), packageTemplate(toPackageName(basename4(root)), getBlumeVersion()));
11970
+ await writeFileSafe(join28(root, "blume.config.ts"), starter.config);
11971
+ await Promise.all(starter.files(contentDir).map((file) => writeFileSafe(join28(root, file.path), file.content)));
11972
+ const commands = commandsFor(pm);
11973
+ if (args.eject) {
11974
+ try {
11975
+ await eject(root);
11976
+ logger.success("Ejected to a standalone Astro project.");
11977
+ logger.box(`Next steps:
11978
+
11979
+ ${commands.install}
11980
+ npx astro dev
11981
+ `);
11982
+ } catch (error) {
11983
+ logger.warn(`Scaffolded, but eject failed: ${error.message}`);
11984
+ logger.box(`Next steps:
11985
+
11986
+ ${commands.install}
11987
+ blume eject --yes
11988
+ `);
11989
+ }
11990
+ return;
11991
+ }
10443
11992
  const nextSteps = createdPackage ? `Next steps:
10444
11993
 
10445
- npm install
10446
- blume dev
11994
+ ${commands.install}
11995
+ ${commands.dev}
10447
11996
  ` : `Next steps:
10448
11997
 
10449
- blume dev
11998
+ ${commands.dev}
10450
11999
  `;
10451
12000
  logger.box(nextSteps);
10452
12001
  }
10453
12002
  });
10454
12003
 
10455
12004
  // src/cli/commands/migrate.ts
10456
- import { defineCommand as defineCommand7 } from "citty";
12005
+ import { defineCommand as defineCommand8 } from "citty";
10457
12006
 
10458
12007
  // src/migrate/fumadocs/index.ts
10459
- import { existsSync as existsSync17 } from "node:fs";
12008
+ import { existsSync as existsSync22 } from "node:fs";
10460
12009
  import { mkdir as mkdir8, readFile as readFile17, rename as rename3, rm as rm3, writeFile as writeFile11 } from "node:fs/promises";
10461
- import { dirname as dirname14, join as join29, relative as relative15 } from "pathe";
12010
+ import { dirname as dirname16, join as join31, relative as relative16 } from "pathe";
10462
12011
  import { glob as glob8 } from "tinyglobby";
10463
12012
 
10464
12013
  // src/migrate/fumadocs/config.ts
10465
- import { existsSync as existsSync14 } from "node:fs";
12014
+ import { existsSync as existsSync19 } from "node:fs";
10466
12015
  import { readFile as readFile16 } from "node:fs/promises";
10467
- import { basename as basename5, dirname as dirname12, join as join27 } from "pathe";
12016
+ import { basename as basename5, dirname as dirname14, join as join29 } from "pathe";
10468
12017
  var SOURCE_FILES = [
10469
12018
  "lib/source.ts",
10470
12019
  "app/source.ts",
@@ -10494,10 +12043,10 @@ var GENERIC_NAMES = new Set([
10494
12043
  var gitRepoRoot = (start) => {
10495
12044
  let dir = start;
10496
12045
  for (;; ) {
10497
- if (existsSync14(join27(dir, ".git"))) {
12046
+ if (existsSync19(join29(dir, ".git"))) {
10498
12047
  return dir;
10499
12048
  }
10500
- const parent = dirname12(dir);
12049
+ const parent = dirname14(dir);
10501
12050
  if (parent === dir) {
10502
12051
  return null;
10503
12052
  }
@@ -10506,8 +12055,8 @@ var gitRepoRoot = (start) => {
10506
12055
  };
10507
12056
  var bareName = (name) => name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
10508
12057
  var readTitle = async (root) => {
10509
- const packageJson = join27(root, "package.json");
10510
- if (!existsSync14(packageJson)) {
12058
+ const packageJson = join29(root, "package.json");
12059
+ if (!existsSync19(packageJson)) {
10511
12060
  return "Documentation";
10512
12061
  }
10513
12062
  try {
@@ -10532,8 +12081,8 @@ var readTitle = async (root) => {
10532
12081
  };
10533
12082
  var scrapeBaseUrl = async (root) => {
10534
12083
  for (const candidate of SOURCE_FILES) {
10535
- const file = join27(root, candidate);
10536
- if (!existsSync14(file)) {
12084
+ const file = join29(root, candidate);
12085
+ if (!existsSync19(file)) {
10537
12086
  continue;
10538
12087
  }
10539
12088
  const base = BASE_URL.exec(await readFile16(file, "utf-8"))?.groups?.base;
@@ -10565,9 +12114,9 @@ var loadFumadocsConfig = async (root) => {
10565
12114
  };
10566
12115
 
10567
12116
  // src/migrate/fumadocs/content.ts
10568
- import { existsSync as existsSync15 } from "node:fs";
12117
+ import { existsSync as existsSync20 } from "node:fs";
10569
12118
  import { readFile as readFileFromDisk2 } from "node:fs/promises";
10570
- import { dirname as dirname13, resolve as resolve8 } from "pathe";
12119
+ import { dirname as dirname15, resolve as resolve10 } from "pathe";
10571
12120
  var FUMADOCS_IMPORT = /^import\s+[\s\S]*?\s+from\s+["']fumadocs-(?:ui|core|mdx)(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
10572
12121
  var stripFumadocsImports = (source) => {
10573
12122
  const stripped = source.replace(FUMADOCS_IMPORT, "");
@@ -10768,12 +12317,12 @@ var inlineFumadocsIncludes = async (source, options) => {
10768
12317
  if (!rawPath) {
10769
12318
  continue;
10770
12319
  }
10771
- const target = resolve8(dirname13(options.filePath), rawPath);
12320
+ const target = resolve10(dirname15(options.filePath), rawPath);
10772
12321
  if (seen.has(target)) {
10773
12322
  warnings.push(`Circular <include> "${rawPath}" — left as-is.`);
10774
12323
  continue;
10775
12324
  }
10776
- if (!existsSync15(target)) {
12325
+ if (!existsSync20(target)) {
10777
12326
  warnings.push(`<include> target "${rawPath}" not found — left as-is.`);
10778
12327
  continue;
10779
12328
  }
@@ -10805,9 +12354,9 @@ var normalizeFumadocsPageMeta = (value) => {
10805
12354
  };
10806
12355
 
10807
12356
  // src/migrate/fumadocs/groups.ts
10808
- import { existsSync as existsSync16, statSync as statSync2 } from "node:fs";
12357
+ import { existsSync as existsSync21, statSync as statSync2 } from "node:fs";
10809
12358
  import { mkdir as mkdir7, rename as rename2, writeFile as writeFile10 } from "node:fs/promises";
10810
- import { basename as basename6, join as join28 } from "pathe";
12359
+ import { basename as basename6, join as join30 } from "pathe";
10811
12360
 
10812
12361
  // src/migrate/fumadocs/meta.ts
10813
12362
  var SEPARATOR = /^---(?<label>.*)---$/u;
@@ -10953,12 +12502,12 @@ var isDirectory = (path) => {
10953
12502
  };
10954
12503
  var resolveEntry = (docsDir, name) => {
10955
12504
  for (const ext of PAGE_EXTS) {
10956
- const file = join28(docsDir, `${name}${ext}`);
10957
- if (existsSync16(file)) {
12505
+ const file = join30(docsDir, `${name}${ext}`);
12506
+ if (existsSync21(file)) {
10958
12507
  return { kind: "file", path: file };
10959
12508
  }
10960
12509
  }
10961
- const folder = join28(docsDir, name);
12510
+ const folder = join30(docsDir, name);
10962
12511
  return isDirectory(folder) ? { kind: "folder", path: folder } : null;
10963
12512
  };
10964
12513
  var humanize2 = (name) => name.split(WORD_SPLIT3).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
@@ -10993,8 +12542,8 @@ var moveItemIntoGroup = async (item, docsDir, groupDir, label, warnings) => {
10993
12542
  warnings.push(`Sidebar entry "${item.name}" in section "${label}" matched no page or folder; skipped.`);
10994
12543
  return null;
10995
12544
  }
10996
- const dest = join28(groupDir, basename6(resolved.path));
10997
- if (existsSync16(dest)) {
12545
+ const dest = join30(groupDir, basename6(resolved.path));
12546
+ if (existsSync21(dest)) {
10998
12547
  warnings.push(`Skipped moving "${item.name}" into section "${label}" (target already exists).`);
10999
12548
  return null;
11000
12549
  }
@@ -11021,7 +12570,7 @@ var reshapeSection = async (section, docsDir, order, warnings) => {
11021
12570
  }
11022
12571
  return;
11023
12572
  }
11024
- const groupDir = join28(docsDir, `(${section.label})`);
12573
+ const groupDir = join30(docsDir, `(${section.label})`);
11025
12574
  const sectionKeys = [];
11026
12575
  for (const item of section.items) {
11027
12576
  const key = await moveItemIntoGroup(item, docsDir, groupDir, section.label, warnings);
@@ -11034,7 +12583,7 @@ var reshapeSection = async (section, docsDir, order, warnings) => {
11034
12583
  }
11035
12584
  order.push(section.label);
11036
12585
  if (sectionKeys.length > 1) {
11037
- await writeFile10(join28(groupDir, "meta.ts"), renderMetaModule({ pages: sectionKeys }), "utf-8");
12586
+ await writeFile10(join30(groupDir, "meta.ts"), renderMetaModule({ pages: sectionKeys }), "utf-8");
11038
12587
  }
11039
12588
  };
11040
12589
  var reshapeFumadocsGroups = async (structure, docsDir) => {
@@ -11066,9 +12615,9 @@ var FUMADOCS_LEFTOVERS = [
11066
12615
  "app"
11067
12616
  ];
11068
12617
  var movePage = async (abs, base, root) => {
11069
- const rel = relative15(base, abs);
11070
- const dest = join29(root, "docs", rel);
11071
- if (existsSync17(dest)) {
12618
+ const rel = relative16(base, abs);
12619
+ const dest = join31(root, "docs", rel);
12620
+ if (existsSync22(dest)) {
11072
12621
  return {
11073
12622
  includeWarnings: [],
11074
12623
  moved: 0,
@@ -11087,7 +12636,7 @@ var movePage = async (abs, base, root) => {
11087
12636
  const parsed = frontmatter_default(text);
11088
12637
  const { data, removed } = normalizeFumadocsPageMeta(parsed.data);
11089
12638
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
11090
- await mkdir8(dirname14(dest), { recursive: true });
12639
+ await mkdir8(dirname16(dest), { recursive: true });
11091
12640
  await writeFile11(dest, content, "utf-8");
11092
12641
  await rm3(abs, { force: true });
11093
12642
  return {
@@ -11102,35 +12651,35 @@ var writeMeta = async (dest, meta, rel, warnings) => {
11102
12651
  if (Object.keys(meta).length === 0) {
11103
12652
  return warnings;
11104
12653
  }
11105
- if (existsSync17(dest)) {
12654
+ if (existsSync22(dest)) {
11106
12655
  return [...warnings, `Skipped ${rel} (target already exists)`];
11107
12656
  }
11108
- await mkdir8(dirname14(dest), { recursive: true });
12657
+ await mkdir8(dirname16(dest), { recursive: true });
11109
12658
  await writeFile11(dest, renderMetaModule(meta), "utf-8");
11110
12659
  return warnings;
11111
12660
  };
11112
12661
  var convertMeta = async (abs, base, root) => {
11113
- const rel = relative15(base, abs);
12662
+ const rel = relative16(base, abs);
11114
12663
  const raw = await readFile17(abs, "utf-8");
11115
12664
  let parsed;
11116
12665
  try {
11117
12666
  parsed = JSON.parse(raw);
11118
12667
  } catch {
11119
- const dest2 = join29(root, "docs", rel);
11120
- if (existsSync17(dest2)) {
12668
+ const dest2 = join31(root, "docs", rel);
12669
+ if (existsSync22(dest2)) {
11121
12670
  return [`Skipped ${rel} (target already exists)`];
11122
12671
  }
11123
- await mkdir8(dirname14(dest2), { recursive: true });
12672
+ await mkdir8(dirname16(dest2), { recursive: true });
11124
12673
  await rename3(abs, dest2);
11125
12674
  return [
11126
12675
  `Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`
11127
12676
  ];
11128
12677
  }
11129
- const dir = dirname14(rel) === "." ? "" : dirname14(rel);
11130
- const docsDir = join29(root, "docs", dir);
11131
- const dest = join29(docsDir, "meta.ts");
12678
+ const dir = dirname16(rel) === "." ? "" : dirname16(rel);
12679
+ const docsDir = join31(root, "docs", dir);
12680
+ const dest = join31(docsDir, "meta.ts");
11132
12681
  const structure = parseFumadocsPages(parsed.pages);
11133
- if (structure.hasSections && !existsSync17(dest)) {
12682
+ if (structure.hasSections && !existsSync22(dest)) {
11134
12683
  const self = translateFumadocsSelfMeta(parsed);
11135
12684
  const reshape = await reshapeFumadocsGroups(structure, docsDir);
11136
12685
  const meta2 = { ...self.meta };
@@ -11177,8 +12726,8 @@ var summarizePages = (results) => {
11177
12726
  };
11178
12727
  };
11179
12728
  var cleanupSourceDirs = async (root) => {
11180
- const docs = join29(root, "content", "docs");
11181
- if (existsSync17(docs)) {
12729
+ const docs = join31(root, "content", "docs");
12730
+ if (existsSync22(docs)) {
11182
12731
  const remaining = await glob8(["**/*"], { cwd: docs, dot: true });
11183
12732
  if (remaining.length > 0) {
11184
12733
  return [
@@ -11187,8 +12736,8 @@ var cleanupSourceDirs = async (root) => {
11187
12736
  }
11188
12737
  await rm3(docs, { force: true, recursive: true });
11189
12738
  }
11190
- const content = join29(root, "content");
11191
- if (existsSync17(content)) {
12739
+ const content = join31(root, "content");
12740
+ if (existsSync22(content)) {
11192
12741
  const remaining = await glob8(["**/*"], { cwd: content, dot: true });
11193
12742
  if (remaining.length === 0) {
11194
12743
  await rm3(content, { force: true, recursive: true });
@@ -11198,8 +12747,8 @@ var cleanupSourceDirs = async (root) => {
11198
12747
  };
11199
12748
  var migrateFumadocsProject = async (root) => {
11200
12749
  const { config, warnings: configWarnings } = await loadFumadocsConfig(root);
11201
- const base = join29(root, SOURCE_DIR);
11202
- if (!existsSync17(base)) {
12750
+ const base = join31(root, SOURCE_DIR);
12751
+ if (!existsSync22(base)) {
11203
12752
  await writeBlumeConfig(root, config);
11204
12753
  return {
11205
12754
  moved: 0,
@@ -11257,9 +12806,9 @@ var migrateFumadocsProject = async (root) => {
11257
12806
  };
11258
12807
 
11259
12808
  // src/migrate/mintlify/index.ts
11260
- import { existsSync as existsSync18 } from "node:fs";
12809
+ import { existsSync as existsSync23 } from "node:fs";
11261
12810
  import { mkdir as mkdir9, readFile as readFile18, rename as rename4, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
11262
- import { dirname as dirname15, join as join30 } from "pathe";
12811
+ import { dirname as dirname17, join as join32 } from "pathe";
11263
12812
  import { glob as glob9 } from "tinyglobby";
11264
12813
  var prune = (value) => {
11265
12814
  if (Array.isArray(value)) {
@@ -11289,7 +12838,7 @@ var writeBlumeConfig2 = async (root, config) => {
11289
12838
 
11290
12839
  export default defineConfig(${JSON.stringify(prune(config), null, 2)});
11291
12840
  `;
11292
- await writeFile12(join30(root, "blume.config.ts"), body, "utf-8");
12841
+ await writeFile12(join32(root, "blume.config.ts"), body, "utf-8");
11293
12842
  };
11294
12843
  var relocateAssets = async (root, refs) => {
11295
12844
  const segments = new Set;
@@ -11304,23 +12853,23 @@ var relocateAssets = async (root, refs) => {
11304
12853
  }
11305
12854
  const moved = [];
11306
12855
  for (const segment of segments) {
11307
- const source = join30(root, segment);
11308
- if (!existsSync18(source) || segment === "public") {
12856
+ const source = join32(root, segment);
12857
+ if (!existsSync23(source) || segment === "public") {
11309
12858
  continue;
11310
12859
  }
11311
- const dest = join30(root, "public", segment);
11312
- if (existsSync18(dest)) {
12860
+ const dest = join32(root, "public", segment);
12861
+ if (existsSync23(dest)) {
11313
12862
  continue;
11314
12863
  }
11315
- await mkdir9(join30(root, "public"), { recursive: true });
12864
+ await mkdir9(join32(root, "public"), { recursive: true });
11316
12865
  await rename4(source, dest);
11317
12866
  moved.push(segment);
11318
12867
  }
11319
12868
  return moved;
11320
12869
  };
11321
12870
  var cleanupSnippets = async (root, kept, warnings) => {
11322
- const dir = join30(root, "snippets");
11323
- if (!existsSync18(dir)) {
12871
+ const dir = join32(root, "snippets");
12872
+ if (!existsSync23(dir)) {
11324
12873
  return;
11325
12874
  }
11326
12875
  const markdown = await glob9(["**/*.{md,mdx}"], { absolute: true, cwd: dir });
@@ -11357,9 +12906,9 @@ var assetRefs = (config) => {
11357
12906
  };
11358
12907
  var migrateMintlifyProject = async (root) => {
11359
12908
  const warnings = [];
11360
- const configFile = existsSync18(join30(root, "docs.json")) ? join30(root, "docs.json") : join30(root, "mint.json");
12909
+ const configFile = existsSync23(join32(root, "docs.json")) ? join32(root, "docs.json") : join32(root, "mint.json");
11361
12910
  let config;
11362
- if (existsSync18(configFile)) {
12911
+ if (existsSync23(configFile)) {
11363
12912
  config = await loadMintlifyConfig(root, configFile);
11364
12913
  const spec = JSON.parse(await readFile18(configFile, "utf-8"));
11365
12914
  const i18n = mintlifyI18n(spec);
@@ -11399,7 +12948,7 @@ var migrateMintlifyProject = async (root) => {
11399
12948
  variables
11400
12949
  });
11401
12950
  if (result.content !== raw) {
11402
- await mkdir9(dirname15(file), { recursive: true });
12951
+ await mkdir9(dirname17(file), { recursive: true });
11403
12952
  await writeFile12(file, result.content, "utf-8");
11404
12953
  }
11405
12954
  for (const key of result.removed) {
@@ -11436,9 +12985,9 @@ var migrateMintlifyProject = async (root) => {
11436
12985
  };
11437
12986
 
11438
12987
  // src/migrate/nextra/index.ts
11439
- import { existsSync as existsSync19 } from "node:fs";
12988
+ import { existsSync as existsSync24 } from "node:fs";
11440
12989
  import { mkdir as mkdir10, readFile as readFile19, rename as rename5, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
11441
- import { basename as basename7, dirname as dirname16, extname as extname6, join as join31, relative as relative16 } from "pathe";
12990
+ import { basename as basename7, dirname as dirname18, extname as extname7, join as join33, relative as relative17 } from "pathe";
11442
12991
  import { glob as glob10 } from "tinyglobby";
11443
12992
 
11444
12993
  // src/migrate/nextra/content.ts
@@ -11668,13 +13217,13 @@ var indexFolders = (base, pageFiles, metaFiles) => {
11668
13217
  let current = normalizeDir(dir);
11669
13218
  allDirs.add(current);
11670
13219
  while (current !== "") {
11671
- current = normalizeDir(dirname16(current));
13220
+ current = normalizeDir(dirname18(current));
11672
13221
  allDirs.add(current);
11673
13222
  }
11674
13223
  };
11675
13224
  for (const abs of pageFiles) {
11676
- const rel = relative16(base, abs);
11677
- const dir = normalizeDir(dirname16(rel));
13225
+ const rel = relative17(base, abs);
13226
+ const dir = normalizeDir(dirname18(rel));
11678
13227
  const slug = basename7(rel).replace(/\.mdx?$/u, "");
11679
13228
  const folder = pagesByDir.get(dir) ?? new Map;
11680
13229
  folder.set(slug, abs);
@@ -11682,14 +13231,14 @@ var indexFolders = (base, pageFiles, metaFiles) => {
11682
13231
  registerDir(dir);
11683
13232
  }
11684
13233
  for (const abs of metaFiles) {
11685
- registerDir(normalizeDir(dirname16(relative16(base, abs))));
13234
+ registerDir(normalizeDir(dirname18(relative17(base, abs))));
11686
13235
  }
11687
13236
  const childDirs = new Map;
11688
13237
  for (const dir of allDirs) {
11689
13238
  if (dir === "") {
11690
13239
  continue;
11691
13240
  }
11692
- const parent = normalizeDir(dirname16(dir));
13241
+ const parent = normalizeDir(dirname18(dir));
11693
13242
  const children = childDirs.get(parent) ?? new Set;
11694
13243
  children.add(basename7(dir));
11695
13244
  childDirs.set(parent, children);
@@ -11707,7 +13256,7 @@ var planMetas = (metas, index) => {
11707
13256
  warnings: []
11708
13257
  };
11709
13258
  for (const meta of metas) {
11710
- const dir = normalizeDir(dirname16(meta.rel));
13259
+ const dir = normalizeDir(dirname18(meta.rel));
11711
13260
  const entries = parseNextraMeta(meta.raw, meta.ext);
11712
13261
  if (!entries) {
11713
13262
  plan.unparseableMetas.push({ abs: meta.abs, rel: meta.rel });
@@ -11720,7 +13269,7 @@ var planMetas = (metas, index) => {
11720
13269
  plan.metaByDir.set(dir, conversion.folderMeta);
11721
13270
  plan.consumedMetas.push(meta.abs);
11722
13271
  for (const [slug, title] of Object.entries(conversion.folderTitles)) {
11723
- plan.folderTitleByDir.set(normalizeDir(join31(dir, slug)), title);
13272
+ plan.folderTitleByDir.set(normalizeDir(join33(dir, slug)), title);
11724
13273
  }
11725
13274
  for (const [slug, label] of Object.entries(conversion.pageLabels)) {
11726
13275
  const pageAbs = index.pagesByDir.get(dir)?.get(slug);
@@ -11744,9 +13293,9 @@ var planMetas = (metas, index) => {
11744
13293
  return plan;
11745
13294
  };
11746
13295
  var movePage2 = async (abs, options) => {
11747
- const rel = relative16(options.base, abs);
11748
- const dest = join31(options.root, "docs", rel);
11749
- if (existsSync19(dest)) {
13296
+ const rel = relative17(options.base, abs);
13297
+ const dest = join33(options.root, "docs", rel);
13298
+ if (existsSync24(dest)) {
11750
13299
  return { moved: 0, removed: [], skipped: rel, unsupported: [] };
11751
13300
  }
11752
13301
  const raw = await readFile19(abs, "utf-8");
@@ -11755,7 +13304,7 @@ var movePage2 = async (abs, options) => {
11755
13304
  const parsed = frontmatter_default(text);
11756
13305
  const { data, removed } = normalizeNextraPageMeta(parsed.data, options.overrides.get(abs) ?? {});
11757
13306
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
11758
- await mkdir10(dirname16(dest), { recursive: true });
13307
+ await mkdir10(dirname18(dest), { recursive: true });
11759
13308
  await writeFile13(dest, content, "utf-8");
11760
13309
  await rm5(abs, { force: true });
11761
13310
  return { moved: 1, removed, skipped: null, unsupported };
@@ -11798,8 +13347,8 @@ var writeFolderMetas = async (root, plan) => {
11798
13347
  if (Object.keys(finalMeta).length === 0) {
11799
13348
  return;
11800
13349
  }
11801
- const dest = join31(root, "docs", dir, "meta.ts");
11802
- await mkdir10(dirname16(dest), { recursive: true });
13350
+ const dest = join33(root, "docs", dir, "meta.ts");
13351
+ await mkdir10(dirname18(dest), { recursive: true });
11803
13352
  await writeFile13(dest, `import { defineMeta } from "blume";
11804
13353
 
11805
13354
  export default defineMeta(${JSON.stringify(finalMeta, null, 2)});
@@ -11809,12 +13358,12 @@ export default defineMeta(${JSON.stringify(finalMeta, null, 2)});
11809
13358
  var relocateUnparseableMetas = async (root, metas) => {
11810
13359
  const warnings = [];
11811
13360
  await Promise.all(metas.map(async ({ abs, rel }) => {
11812
- const dest = join31(root, "docs", rel);
11813
- if (existsSync19(dest)) {
13361
+ const dest = join33(root, "docs", rel);
13362
+ if (existsSync24(dest)) {
11814
13363
  warnings.push(`Skipped ${rel} (target already exists)`);
11815
13364
  return;
11816
13365
  }
11817
- await mkdir10(dirname16(dest), { recursive: true });
13366
+ await mkdir10(dirname18(dest), { recursive: true });
11818
13367
  await rename5(abs, dest);
11819
13368
  warnings.push(`Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`);
11820
13369
  }));
@@ -11822,7 +13371,7 @@ var relocateUnparseableMetas = async (root, metas) => {
11822
13371
  };
11823
13372
  var buildConfig = (tabs) => tabs.length > 0 ? { navigation: { tabs }, title: "Documentation" } : { title: "Documentation" };
11824
13373
  var migrateNextraProject = async (root) => {
11825
- const sourceDir = SOURCE_DIRS.find((dir) => existsSync19(join31(root, dir)));
13374
+ const sourceDir = SOURCE_DIRS.find((dir) => existsSync24(join33(root, dir)));
11826
13375
  if (!sourceDir) {
11827
13376
  await writeBlumeConfig(root, { title: "Documentation" });
11828
13377
  return {
@@ -11832,7 +13381,7 @@ var migrateNextraProject = async (root) => {
11832
13381
  ]
11833
13382
  };
11834
13383
  }
11835
- const base = join31(root, sourceDir);
13384
+ const base = join33(root, sourceDir);
11836
13385
  const pageFiles = await glob10([PAGE_GLOB2], {
11837
13386
  absolute: true,
11838
13387
  cwd: base,
@@ -11846,9 +13395,9 @@ var migrateNextraProject = async (root) => {
11846
13395
  const index = indexFolders(base, pageFiles, metaFiles);
11847
13396
  const metas = await Promise.all(metaFiles.map(async (abs) => ({
11848
13397
  abs,
11849
- ext: extname6(abs),
13398
+ ext: extname7(abs),
11850
13399
  raw: await readFile19(abs, "utf-8"),
11851
- rel: relative16(base, abs)
13400
+ rel: relative17(base, abs)
11852
13401
  })));
11853
13402
  const plan = planMetas(metas, index);
11854
13403
  const moves = await Promise.all(pageFiles.map((abs) => movePage2(abs, { base, overrides: plan.pageOverrides, root })));
@@ -11873,15 +13422,15 @@ var migrateNextraProject = async (root) => {
11873
13422
  };
11874
13423
 
11875
13424
  // src/migrate/starlight/index.ts
11876
- import { existsSync as existsSync21 } from "node:fs";
13425
+ import { existsSync as existsSync26 } from "node:fs";
11877
13426
  import { readFile as readFile21, writeFile as writeFile14 } from "node:fs/promises";
11878
- import { join as join33 } from "pathe";
13427
+ import { join as join35 } from "pathe";
11879
13428
  import { glob as glob11 } from "tinyglobby";
11880
13429
 
11881
13430
  // src/migrate/starlight/config.ts
11882
- import { existsSync as existsSync20 } from "node:fs";
13431
+ import { existsSync as existsSync25 } from "node:fs";
11883
13432
  import { readFile as readFile20 } from "node:fs/promises";
11884
- import { join as join32 } from "pathe";
13433
+ import { join as join34 } from "pathe";
11885
13434
  var CONFIG_FILES = [
11886
13435
  "astro.config.mjs",
11887
13436
  "astro.config.mts",
@@ -11912,7 +13461,7 @@ var extractStarlightOptions = (source) => {
11912
13461
  return isLiteralObject(parsed) ? parsed : "unparseable";
11913
13462
  };
11914
13463
  var loadStarlightConfig = async (root) => {
11915
- const file = CONFIG_FILES.map((name) => join32(root, name)).find((path) => existsSync20(path));
13464
+ const file = CONFIG_FILES.map((name) => join34(root, name)).find((path) => existsSync25(path));
11916
13465
  if (!file) {
11917
13466
  return {
11918
13467
  options: {},
@@ -12155,9 +13704,6 @@ var mapStarlightConfig = (options, warnings) => {
12155
13704
  }
12156
13705
  }
12157
13706
  const social = mapSocial(options.social);
12158
- if (Object.keys(social.socials).length > 0) {
12159
- config.footer = { socials: social.socials };
12160
- }
12161
13707
  const github = mapEditLink(options.editLink) ?? social.github;
12162
13708
  if (github) {
12163
13709
  config.github = github;
@@ -12344,8 +13890,8 @@ var migrateStarlightProject = async (root) => {
12344
13890
  config.i18n = i18n;
12345
13891
  warnings.push(`Mapped ${i18n.locales.length} locale(s) to i18n (default: ${i18n.defaultLocale}); review the locale labels.`);
12346
13892
  }
12347
- const base = join33(root, CONTENT_DIR);
12348
- if (!existsSync21(base)) {
13893
+ const base = join35(root, CONTENT_DIR);
13894
+ if (!existsSync26(base)) {
12349
13895
  await writeBlumeConfig(root, config);
12350
13896
  return {
12351
13897
  moved: 0,
@@ -12399,7 +13945,7 @@ var migrators = {
12399
13945
  };
12400
13946
 
12401
13947
  // src/cli/commands/migrate.ts
12402
- var makeMigrateCommand = (source) => defineCommand7({
13948
+ var makeMigrateCommand = (source) => defineCommand8({
12403
13949
  meta: {
12404
13950
  description: `Migrate a ${source} project to Blume.`,
12405
13951
  name: source
@@ -12419,7 +13965,7 @@ var makeMigrateCommand = (source) => defineCommand7({
12419
13965
  logger.box("Review blume.config.ts and run `blume dev`.");
12420
13966
  }
12421
13967
  });
12422
- var migrateCommand = defineCommand7({
13968
+ var migrateCommand = defineCommand8({
12423
13969
  meta: {
12424
13970
  description: "Migrate from another docs tool to Blume.",
12425
13971
  name: "migrate"
@@ -12433,11 +13979,11 @@ var migrateCommand = defineCommand7({
12433
13979
  });
12434
13980
 
12435
13981
  // src/cli/commands/preview.ts
12436
- import { existsSync as existsSync22 } from "node:fs";
13982
+ import { existsSync as existsSync27 } from "node:fs";
12437
13983
  import { preview } from "astro";
12438
- import { defineCommand as defineCommand8 } from "citty";
12439
- import { join as join34 } from "pathe";
12440
- var previewCommand = defineCommand8({
13984
+ import { defineCommand as defineCommand9 } from "citty";
13985
+ import { join as join36 } from "pathe";
13986
+ var previewCommand = defineCommand9({
12441
13987
  args: {
12442
13988
  host: { description: "Network host to bind.", type: "string" },
12443
13989
  port: { description: "Port to listen on.", type: "string" }
@@ -12450,7 +13996,7 @@ var previewCommand = defineCommand8({
12450
13996
  const root = process.cwd();
12451
13997
  const { config } = await loadConfig(root);
12452
13998
  const context = resolveProjectContext(root, config);
12453
- if (!existsSync22(join34(context.outDir, "astro.config.mjs"))) {
13999
+ if (!existsSync27(join36(context.outDir, "astro.config.mjs"))) {
12454
14000
  logger.error("No build found. Run `blume build` first.");
12455
14001
  process.exit(1);
12456
14002
  }
@@ -12467,9 +14013,9 @@ var previewCommand = defineCommand8({
12467
14013
 
12468
14014
  // src/cli/commands/sync.ts
12469
14015
  import { rm as rm6 } from "node:fs/promises";
12470
- import { defineCommand as defineCommand9 } from "citty";
12471
- import { join as join35 } from "pathe";
12472
- var syncCommand = defineCommand9({
14016
+ import { defineCommand as defineCommand10 } from "citty";
14017
+ import { join as join37 } from "pathe";
14018
+ var syncCommand = defineCommand10({
12473
14019
  args: {
12474
14020
  force: {
12475
14021
  description: "Clear the source cache before refetching.",
@@ -12490,7 +14036,7 @@ var syncCommand = defineCommand9({
12490
14036
  if (args.force) {
12491
14037
  const { config } = await loadConfig(root);
12492
14038
  const context = resolveProjectContext(root, config);
12493
- await rm6(join35(context.outDir, "cache"), { force: true, recursive: true });
14039
+ await rm6(join37(context.outDir, "cache"), { force: true, recursive: true });
12494
14040
  logger.info("Cleared source cache.");
12495
14041
  }
12496
14042
  await prepareProject({
@@ -12505,13 +14051,13 @@ var syncCommand = defineCommand9({
12505
14051
  });
12506
14052
 
12507
14053
  // src/cli/commands/validate.ts
12508
- import { existsSync as existsSync24 } from "node:fs";
12509
- import { defineCommand as defineCommand10 } from "citty";
12510
- import { join as join37 } from "pathe";
14054
+ import { existsSync as existsSync29 } from "node:fs";
14055
+ import { defineCommand as defineCommand11 } from "citty";
14056
+ import { join as join39 } from "pathe";
12511
14057
 
12512
14058
  // src/core/links.ts
12513
- import { existsSync as existsSync23 } from "node:fs";
12514
- import { join as join36 } from "pathe";
14059
+ import { existsSync as existsSync28 } from "node:fs";
14060
+ import { join as join38 } from "pathe";
12515
14061
  var HTTP = /^https?:\/\//iu;
12516
14062
  var PROTOCOL_RELATIVE = /^\/\//u;
12517
14063
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -12550,8 +14096,8 @@ var toRoute = (path) => {
12550
14096
  };
12551
14097
  var buildAnchorIndex = (pages) => {
12552
14098
  const anchors = new Map;
12553
- for (const page of pages) {
12554
- anchors.set(page.route, new Set(page.headings.map((heading) => heading.slug)));
14099
+ for (const page2 of pages) {
14100
+ anchors.set(page2.route, new Set(page2.headings.map((heading) => heading.slug)));
12555
14101
  }
12556
14102
  return anchors;
12557
14103
  };
@@ -12571,7 +14117,7 @@ var checkPathLink = (resolved, fragment, target, site, ctx) => {
12571
14117
  if (ctx.publicDir === null) {
12572
14118
  return "asset-unchecked";
12573
14119
  }
12574
- if (existsSync23(join36(ctx.publicDir, resolved))) {
14120
+ if (existsSync28(join38(ctx.publicDir, resolved))) {
12575
14121
  return null;
12576
14122
  }
12577
14123
  return {
@@ -12670,11 +14216,11 @@ var checkExternalLinks = async (refs) => {
12670
14216
  }
12671
14217
  return diagnostics;
12672
14218
  };
12673
- var classifyLink = (page, link, ctx, onExternal) => {
14219
+ var classifyLink = (page2, link, ctx, onExternal) => {
12674
14220
  const { target } = link;
12675
14221
  const site = {
12676
14222
  column: link.column,
12677
- file: page.sourcePath ?? page.id,
14223
+ file: page2.sourcePath ?? page2.id,
12678
14224
  line: link.line
12679
14225
  };
12680
14226
  if (HTTP.test(target) || PROTOCOL_RELATIVE.test(target)) {
@@ -12695,9 +14241,9 @@ var classifyLink = (page, link, ctx, onExternal) => {
12695
14241
  rawPath = rawPath.slice(0, queryIndex);
12696
14242
  }
12697
14243
  if (rawPath === "") {
12698
- return fragment ? checkAnchor(page.route, fragment, site, ctx) : null;
14244
+ return fragment ? checkAnchor(page2.route, fragment, site, ctx) : null;
12699
14245
  }
12700
- const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page.route, rawPath);
14246
+ const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page2.route, rawPath);
12701
14247
  return checkPathLink(resolved, fragment, target, site, ctx);
12702
14248
  };
12703
14249
  var validateLinks = async (graph, options) => {
@@ -12710,9 +14256,9 @@ var validateLinks = async (graph, options) => {
12710
14256
  const diagnostics = [];
12711
14257
  const external = [];
12712
14258
  let uncheckedAssets = 0;
12713
- for (const page of graph.pages) {
12714
- for (const link of page.links) {
12715
- const result = classifyLink(page, link, ctx, (ref) => external.push(ref));
14259
+ for (const page2 of graph.pages) {
14260
+ for (const link of page2.links) {
14261
+ const result = classifyLink(page2, link, ctx, (ref) => external.push(ref));
12716
14262
  if (result === "asset-unchecked") {
12717
14263
  uncheckedAssets += 1;
12718
14264
  } else if (result) {
@@ -12734,12 +14280,16 @@ var validateLinks = async (graph, options) => {
12734
14280
  };
12735
14281
 
12736
14282
  // src/cli/commands/validate.ts
12737
- var validateCommand = defineCommand10({
14283
+ var validateCommand = defineCommand11({
12738
14284
  args: {
12739
14285
  external: {
12740
14286
  description: "Check external (HTTP) links over the network.",
12741
14287
  type: "boolean"
12742
14288
  },
14289
+ json: {
14290
+ description: "Emit diagnostics as JSON on stdout (for CI/editors).",
14291
+ type: "boolean"
14292
+ },
12743
14293
  strict: {
12744
14294
  description: "Treat warnings as errors.",
12745
14295
  type: "boolean"
@@ -12755,18 +14305,26 @@ var validateCommand = defineCommand10({
12755
14305
  try {
12756
14306
  const project = await scanProject(root, { mode: "build" });
12757
14307
  diagnostics.push(...project.diagnostics);
12758
- const publicDir = join37(root, "public");
14308
+ const publicDir = join39(root, "public");
12759
14309
  diagnostics.push(...await validateLinks(project.graph, {
12760
14310
  checkExternal: Boolean(args.external),
12761
- publicDir: existsSync24(publicDir) ? publicDir : null,
14311
+ publicDir: existsSync29(publicDir) ? publicDir : null,
12762
14312
  redirects: project.config.redirects
12763
14313
  }));
12764
14314
  } catch (error) {
12765
14315
  if (error instanceof BlumeError) {
12766
14316
  diagnostics.push(error.diagnostic);
12767
14317
  } else {
12768
- throw error;
14318
+ reportInternalError(error);
14319
+ process.exit(1);
14320
+ }
14321
+ }
14322
+ if (args.json) {
14323
+ const hadErrors2 = reportDiagnosticsJson(diagnostics, root);
14324
+ if (hadErrors2 || Boolean(args.strict) && diagnostics.length > 0) {
14325
+ process.exit(1);
12769
14326
  }
14327
+ return;
12770
14328
  }
12771
14329
  const hadErrors = reportDiagnostics(diagnostics, root);
12772
14330
  if (diagnostics.length === 0) {
@@ -12779,7 +14337,7 @@ var validateCommand = defineCommand10({
12779
14337
  });
12780
14338
 
12781
14339
  // src/cli/index.ts
12782
- var main = defineCommand11({
14340
+ var main = defineCommand12({
12783
14341
  meta: {
12784
14342
  description: "Markdown-first documentation powered by Astro and Vite.",
12785
14343
  name: "blume",
@@ -12788,6 +14346,7 @@ var main = defineCommand11({
12788
14346
  subCommands: {
12789
14347
  add: addCommand,
12790
14348
  build: buildCommand,
14349
+ check: checkCommand,
12791
14350
  dev: devCommand,
12792
14351
  doctor: doctorCommand,
12793
14352
  eject: ejectCommand,
@@ -12798,7 +14357,16 @@ var main = defineCommand11({
12798
14357
  validate: validateCommand
12799
14358
  }
12800
14359
  });
14360
+ loadEnvFiles(process.cwd());
14361
+ process.on("uncaughtException", (error) => {
14362
+ reportInternalError(error);
14363
+ process.exit(1);
14364
+ });
14365
+ process.on("unhandledRejection", (error) => {
14366
+ reportInternalError(error);
14367
+ process.exit(1);
14368
+ });
12801
14369
  runMain(main);
12802
14370
 
12803
- //# debugId=A553178EDDFDE46E64756E2164756E21
14371
+ //# debugId=8EA79E97DE44177D64756E2164756E21
12804
14372
  //# sourceMappingURL=index.js.map