blume 0.2.0 → 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 (71) hide show
  1. package/dist/cli/index.js +1921 -560
  2. package/dist/cli/index.js.map +36 -24
  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 +26 -502
  7. package/dist/types/core/types.d.ts +2 -2
  8. package/docs/02-deployment.mdx +21 -2
  9. package/docs/advanced/custom-pages.mdx +63 -1
  10. package/docs/configuration/ai.mdx +20 -3
  11. package/docs/configuration/customization.mdx +103 -5
  12. package/docs/configuration/index.mdx +13 -0
  13. package/docs/configuration/seo.mdx +5 -0
  14. package/docs/content/islands.mdx +73 -0
  15. package/docs/content/navigation.mdx +25 -0
  16. package/docs/index.mdx +3 -12
  17. package/docs/reference/cli.mdx +42 -0
  18. package/package.json +3 -1
  19. package/src/ai/ask-context.ts +131 -0
  20. package/src/ai/ask-data.ts +25 -0
  21. package/src/astro/component-slots.ts +165 -0
  22. package/src/astro/generate.ts +132 -13
  23. package/src/astro/integration.ts +59 -0
  24. package/src/astro/pages.ts +5 -12
  25. package/src/astro/templates.ts +92 -44
  26. package/src/blume-modules.d.ts +25 -0
  27. package/src/cli/commands/build.ts +186 -1
  28. package/src/cli/commands/check.ts +62 -0
  29. package/src/cli/commands/dev.ts +21 -1
  30. package/src/cli/commands/doctor.ts +23 -6
  31. package/src/cli/commands/init.ts +163 -15
  32. package/src/cli/commands/validate.ts +16 -2
  33. package/src/cli/index.ts +15 -0
  34. package/src/cli/internal-error.ts +63 -0
  35. package/src/cli/log.ts +30 -1
  36. package/src/cli/prepare.ts +17 -3
  37. package/src/cli/required-secrets.ts +44 -0
  38. package/src/components/BlumePage.astro +107 -0
  39. package/src/components/index.ts +3 -3
  40. package/src/components/islands/ask-ai.tsx +15 -1
  41. package/src/components/islands/hooks.ts +188 -0
  42. package/src/components/layout/Empty.astro +6 -0
  43. package/src/components/layout/Header.astro +24 -39
  44. package/src/components/layout/Logo.astro +50 -0
  45. package/src/components/layout/NavSelector.astro +75 -0
  46. package/src/components/layout/PageLayout.astro +38 -2
  47. package/src/components/layout/RootLayout.astro +70 -4
  48. package/src/components/layout/hydration-hint.ts +30 -0
  49. package/src/components/layout/overrides.ts +6 -4
  50. package/src/components/props.ts +68 -0
  51. package/src/core/builtin-tags.ts +39 -0
  52. package/src/core/component-diagnostics.ts +44 -0
  53. package/src/core/component-overrides.ts +478 -0
  54. package/src/core/config.ts +8 -0
  55. package/src/core/data.ts +14 -0
  56. package/src/core/define-components.ts +9 -2
  57. package/src/core/diagnostics.ts +90 -1
  58. package/src/core/graph.ts +7 -0
  59. package/src/core/nav-diagnostics.ts +205 -0
  60. package/src/core/project-graph.ts +40 -1
  61. package/src/core/schema.ts +28 -96
  62. package/src/core/sources/normalize.ts +51 -0
  63. package/src/core/types.ts +2 -2
  64. package/src/deploy/redirects.ts +43 -0
  65. package/src/migrate/mintlify/config.ts +1 -176
  66. package/src/migrate/starlight/config.ts +0 -4
  67. package/src/og/card.ts +163 -38
  68. package/src/registry/eject.ts +39 -9
  69. package/src/registry/registry.ts +166 -0
  70. package/src/runtime/index.ts +61 -0
  71. package/src/vite-env.d.ts +14 -0
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,8 +500,8 @@ Next steps:
279
500
  });
280
501
 
281
502
  // src/cli/commands/build.ts
282
- import { existsSync as existsSync12 } 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
507
  import { join as join24 } from "pathe";
@@ -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,6 +4804,8 @@ 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" } }}` : "";
3918
4809
  const stagedSpread = options.staged ? `
3919
4810
  ...(await getCollection("staged")),` : "";
3920
4811
  return `---
@@ -3922,6 +4813,7 @@ var changelogIndexTemplate = (options) => {
3922
4813
  import { getCollection, render } from "astro:content";
3923
4814
  import RootLayout from "blume/components/layout/RootLayout.astro";
3924
4815
  import Update from "blume/components/content/Update.astro";
4816
+ import { resolveSlot } from "blume/components/layout/overrides.ts";
3925
4817
  import { layoutOverrides } from "../generated/components.ts";
3926
4818
  ${askImport}import data from "../generated/data.json";
3927
4819
 
@@ -3993,11 +4885,13 @@ const headings = items.map((item) => ({
3993
4885
 
3994
4886
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
3995
4887
  const canonical = base ? base + "/changelog" : null;
4888
+
4889
+ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
3996
4890
  ---
3997
4891
 
3998
- <RootLayout
4892
+ <LayoutComponent
3999
4893
  site={{ title: data.config.title, description: data.config.description }}
4000
- layout={layoutOverrides}
4894
+ layout={layoutOverrides}${clientData}
4001
4895
  logo={data.config.logo}
4002
4896
  mcp={data.config.mcp}
4003
4897
  favicon={data.config.favicon}
@@ -4013,6 +4907,7 @@ const canonical = base ? base + "/changelog" : null;
4013
4907
  route: "/changelog",
4014
4908
  }}
4015
4909
  headings={headings}
4910
+ toc={data.config.toc}
4016
4911
  themeMode={data.config.theme.mode}
4017
4912
  fontCssVars={data.fontCssVars}
4018
4913
  searchEnabled={data.config.search.enabled}
@@ -4041,7 +4936,7 @@ const canonical = base ? base + "/changelog" : null;
4041
4936
  </div>
4042
4937
  )
4043
4938
  }
4044
- </RootLayout>
4939
+ </LayoutComponent>
4045
4940
  `;
4046
4941
  };
4047
4942
  var notFoundPageTemplate = () => `---
@@ -4082,19 +4977,6 @@ const nf = data.ui.notFound;
4082
4977
  </div>
4083
4978
  </PageLayout>
4084
4979
  `;
4085
- var userComponentsTemplate = (componentsFile) => {
4086
- if (!componentsFile) {
4087
- return `// Generated by Blume. Do not edit.
4088
- export const mdxComponents = {};
4089
- export const layoutOverrides = {};
4090
- `;
4091
- }
4092
- return `// Generated by Blume. Do not edit.
4093
- import overrides from ${JSON.stringify(componentsFile)};
4094
- export const mdxComponents = overrides.mdx ?? {};
4095
- export const layoutOverrides = overrides.layout ?? {};
4096
- `;
4097
- };
4098
4980
  var islandDirective = (spec) => spec.client === "only" ? `client:only="${spec.framework}"` : `client:${spec.client}`;
4099
4981
  var islandWrapperTemplate = (spec) => `---
4100
4982
  // Generated by Blume. Do not edit.
@@ -4156,6 +5038,12 @@ declare module "blume:data" {
4156
5038
  const data: import("blume").BlumeData;
4157
5039
  export default data;
4158
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
+ }
4159
5047
  `;
4160
5048
  var runtimePackageTemplate = (dependencies = []) => `${JSON.stringify({
4161
5049
  dependencies: Object.fromEntries([...dependencies].toSorted().map((name) => [name, "*"])),
@@ -4336,7 +5224,7 @@ var specConfiguration = async (spec, root) => {
4336
5224
  if (URL_SPEC.test(spec)) {
4337
5225
  return { config: { url: spec } };
4338
5226
  }
4339
- const absolute = isAbsolute2(spec) ? spec : join8(root, spec);
5227
+ const absolute = isAbsolute3(spec) ? spec : join8(root, spec);
4340
5228
  try {
4341
5229
  return { config: { content: await readFile4(absolute, "utf-8") } };
4342
5230
  } catch {
@@ -5075,9 +5963,106 @@ var twoslashCss = () => {
5075
5963
  ${OVERRIDES}`;
5076
5964
  };
5077
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
+
5078
6063
  // src/astro/examples.ts
5079
6064
  import { readFile as readFile6 } from "node:fs/promises";
5080
- import { join as join10, relative as relative3 } from "pathe";
6065
+ import { join as join10, relative as relative4 } from "pathe";
5081
6066
  import { glob as glob2 } from "tinyglobby";
5082
6067
 
5083
6068
  // src/astro/islands.ts
@@ -5091,7 +6076,7 @@ var VALID_MODES = new Set([
5091
6076
  "only",
5092
6077
  "visible"
5093
6078
  ]);
5094
- var FRAMEWORK_BY_EXT = {
6079
+ var FRAMEWORK_BY_EXT2 = {
5095
6080
  jsx: "react",
5096
6081
  svelte: "svelte",
5097
6082
  tsx: "react",
@@ -5125,7 +6110,7 @@ var discoverIslands = async (root) => {
5125
6110
  for (const [index, file] of files.entries()) {
5126
6111
  const base = basename(file);
5127
6112
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
5128
- const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
6113
+ const framework = ext ? FRAMEWORK_BY_EXT2[ext] : undefined;
5129
6114
  if (!framework) {
5130
6115
  continue;
5131
6116
  }
@@ -5151,7 +6136,7 @@ var discoverIslands = async (root) => {
5151
6136
  };
5152
6137
 
5153
6138
  // src/astro/examples.ts
5154
- var FRAMEWORK_BY_EXT2 = {
6139
+ var FRAMEWORK_BY_EXT3 = {
5155
6140
  astro: "astro",
5156
6141
  jsx: "react",
5157
6142
  svelte: "svelte",
@@ -5187,11 +6172,11 @@ var discoverExamples = async (root, pattern = "examples") => {
5187
6172
  const seen = new Map;
5188
6173
  for (const [index, file] of files.entries()) {
5189
6174
  const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
5190
- const framework = ext ? FRAMEWORK_BY_EXT2[ext] : undefined;
6175
+ const framework = ext ? FRAMEWORK_BY_EXT3[ext] : undefined;
5191
6176
  if (!(ext && framework)) {
5192
6177
  continue;
5193
6178
  }
5194
- const path = relative3(dir, file).slice(0, -(ext.length + 1));
6179
+ const path = relative4(dir, file).slice(0, -(ext.length + 1));
5195
6180
  const existing = seen.get(path);
5196
6181
  if (existing) {
5197
6182
  warnings.push(`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`);
@@ -5212,7 +6197,7 @@ var discoverExamples = async (root, pattern = "examples") => {
5212
6197
  };
5213
6198
 
5214
6199
  // src/astro/pages.ts
5215
- import { extname, relative as relative4 } from "pathe";
6200
+ import { extname as extname2, relative as relative5 } from "pathe";
5216
6201
  import { glob as glob3 } from "tinyglobby";
5217
6202
  var discoverPages = async (pagesRoot) => {
5218
6203
  const files = await glob3(["**/*.astro"], {
@@ -5222,8 +6207,8 @@ var discoverPages = async (pagesRoot) => {
5222
6207
  });
5223
6208
  files.sort();
5224
6209
  return files.map((file) => {
5225
- const rel = relative4(pagesRoot, file);
5226
- 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);
5227
6212
  const parts = withoutExt.split("/").filter((part) => part !== "index");
5228
6213
  const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
5229
6214
  return { entrypoint: file, pattern };
@@ -5232,7 +6217,7 @@ var discoverPages = async (pagesRoot) => {
5232
6217
  var routeIsTaken = (pages, contentPages, route) => pages.some((page) => page.pattern === route) || contentPages.some((page) => page.route === route);
5233
6218
  var PRIVATE_SEGMENT = /^[._]/u;
5234
6219
  var humanizeSegment = (segment) => segment.split(/[-_]/u).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
5235
- var customOgRoutes = (pages, siteTitle, siteDescription) => {
6220
+ var customOgRoutes = (pages, siteTitle) => {
5236
6221
  const seen = new Set;
5237
6222
  const routes = [];
5238
6223
  for (const { pattern } of pages) {
@@ -5246,7 +6231,7 @@ var customOgRoutes = (pages, siteTitle, siteDescription) => {
5246
6231
  }
5247
6232
  seen.add(slug);
5248
6233
  const last = segments.at(-1);
5249
- routes.push(last ? { eyebrow: siteTitle, slug, title: humanizeSegment(last) } : { eyebrow: siteDescription, slug, title: siteTitle });
6234
+ routes.push({ slug, title: last ? humanizeSegment(last) : siteTitle });
5250
6235
  }
5251
6236
  return routes;
5252
6237
  };
@@ -5270,8 +6255,8 @@ var resolvedAstroPath = (fromDir) => {
5270
6255
  }
5271
6256
  };
5272
6257
  var blumeDepsDir = (pkgDir = packageRoot()) => {
5273
- const candidates = [join11(pkgDir, "node_modules"), dirname6(pkgDir)];
5274
- 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;
5275
6260
  };
5276
6261
  var linkDepsJunction = async (link, depsDir) => {
5277
6262
  const existing = await lstat(link).catch(() => null);
@@ -5281,7 +6266,7 @@ var linkDepsJunction = async (link, depsDir) => {
5281
6266
  }
5282
6267
  await rm(link, { force: true });
5283
6268
  }
5284
- await mkdir2(dirname6(link), { recursive: true });
6269
+ await mkdir2(dirname7(link), { recursive: true });
5285
6270
  await symlink(depsDir, link, "junction");
5286
6271
  };
5287
6272
  var readPkgVersion = (pkgJsonPath) => {
@@ -5311,7 +6296,7 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
5311
6296
  if (blumeAstro && outDirAstro === blumeAstro) {
5312
6297
  return null;
5313
6298
  }
5314
- if (existsSync5(join11(depsDir, "@astrojs", "mdx"))) {
6299
+ if (existsSync6(join11(depsDir, "@astrojs", "mdx"))) {
5315
6300
  await linkDepsJunction(join11(outDir, "node_modules"), depsDir);
5316
6301
  return null;
5317
6302
  }
@@ -5359,7 +6344,7 @@ var writeIfChanged = async (path, content) => {
5359
6344
  if (existing === content) {
5360
6345
  return false;
5361
6346
  }
5362
- await mkdir2(dirname6(path), { recursive: true });
6347
+ await mkdir2(dirname7(path), { recursive: true });
5363
6348
  const tmp = `${path}.${process.pid}.tmp`;
5364
6349
  await writeFile2(tmp, content, "utf-8");
5365
6350
  try {
@@ -5395,7 +6380,7 @@ var writeStagedContent = async (out, staged) => {
5395
6380
  written.add(normalize(path));
5396
6381
  await writeIfChanged(path, text);
5397
6382
  }));
5398
- if (existsSync5(contentDir)) {
6383
+ if (existsSync6(contentDir)) {
5399
6384
  await pruneOrphans(contentDir, written);
5400
6385
  }
5401
6386
  };
@@ -5414,7 +6399,7 @@ var resolveLogo = (project) => {
5414
6399
  const file = [
5415
6400
  join11(project.context.root, "public", rel),
5416
6401
  join11(project.context.root, rel)
5417
- ].find((path) => existsSync5(path));
6402
+ ].find((path) => existsSync6(path));
5418
6403
  if (file) {
5419
6404
  return { alt, href, svg: readFileSync5(file, "utf-8") };
5420
6405
  }
@@ -5454,13 +6439,13 @@ var APPLE_ICON_CANDIDATES = [
5454
6439
  var resolveIconFile = (project, candidates) => {
5455
6440
  const { root } = project.context;
5456
6441
  for (const name of candidates) {
5457
- if (existsSync5(join11(root, "public", name))) {
6442
+ if (existsSync6(join11(root, "public", name))) {
5458
6443
  return { href: `/${name}`, type: faviconType(name) };
5459
6444
  }
5460
6445
  }
5461
6446
  for (const name of candidates) {
5462
6447
  const file = join11(root, name);
5463
- if (existsSync5(file)) {
6448
+ if (existsSync6(file)) {
5464
6449
  const type = faviconType(name);
5465
6450
  return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
5466
6451
  }
@@ -5493,7 +6478,7 @@ var buildRuntimeData = (project) => {
5493
6478
  if (!(editBase && sourcePath)) {
5494
6479
  return null;
5495
6480
  }
5496
- const rel = relative5(context.root, sourcePath).split("\\").join("/");
6481
+ const rel = relative6(context.root, sourcePath).split("\\").join("/");
5497
6482
  return `${editBase}/${github?.dir ? `${github.dir}/${rel}` : rel}`;
5498
6483
  };
5499
6484
  const { i18n } = config;
@@ -5554,7 +6539,8 @@ var buildRuntimeData = (project) => {
5554
6539
  site: config.deployment.site ?? null,
5555
6540
  structuredData: config.seo.structuredData,
5556
6541
  theme: config.theme,
5557
- title: config.title
6542
+ title: config.title,
6543
+ toc: config.toc
5558
6544
  },
5559
6545
  feeds: buildRssFeeds(project).map((feed) => ({
5560
6546
  href: feed.path,
@@ -5641,6 +6627,18 @@ var writeMcpFiles = async (project, plan, write) => {
5641
6627
  write(join11(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
5642
6628
  ]);
5643
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
+ };
5644
6642
  var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
5645
6643
  if (routeIsTaken(pages, contentPages, "/404")) {
5646
6644
  return;
@@ -5653,6 +6651,14 @@ var shouldGenerateChangelog = (project) => {
5653
6651
  const changelogRouteTaken = project.graph.pages.some((page) => page.route === "/changelog");
5654
6652
  return (hasChangelog || hasChangelogSource) && !changelogRouteTaken;
5655
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
+ };
5656
6662
  var generateRuntime = async (project) => {
5657
6663
  const { context, config } = project;
5658
6664
  const out = context.outDir;
@@ -5677,14 +6683,20 @@ var generateRuntime = async (project) => {
5677
6683
  discoverIslands(context.root),
5678
6684
  discoverExamples(context.root, config.examples)
5679
6685
  ]);
6686
+ const {
6687
+ plan: slotPlan,
6688
+ tags: overrideTags,
6689
+ warnings: overrideWarnings
6690
+ } = await buildComponentSlots(context.componentsFile);
5680
6691
  const frameworks = new Set([
5681
6692
  ...islandDiscovery.islands.map((island) => island.framework),
5682
- ...exampleDiscovery.examples.map((example) => example.framework)
6693
+ ...exampleDiscovery.examples.map((example) => example.framework),
6694
+ ...slotPlan.frameworks
5683
6695
  ]);
5684
6696
  const needsReact = detectedReact || askEnabled || frameworks.has("react");
5685
6697
  const needsVue = frameworks.has("vue");
5686
6698
  const needsSvelte = frameworks.has("svelte");
5687
- const ogRoutes = customOgRoutes(pages, config.title, config.description);
6699
+ const ogRoutes = customOgRoutes(pages, config.title);
5688
6700
  const mcp = planMcp(project, srcDir);
5689
6701
  pages.push(...mcp.discoveryPages);
5690
6702
  const staged = collectStaged(project);
@@ -5712,9 +6724,10 @@ var generateRuntime = async (project) => {
5712
6724
  askEnabled,
5713
6725
  exportEpub,
5714
6726
  exportPdf,
5715
- mathEnabled: config.markdown.math
6727
+ mathEnabled: config.markdown.math,
6728
+ needsReact
5716
6729
  })),
5717
- write(join11(srcDir, "generated", "components.ts"), userComponentsTemplate(context.componentsFile)),
6730
+ write(join11(srcDir, "generated", "components.ts"), slotPlan.module),
5718
6731
  write(join11(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
5719
6732
  write(join11(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples)),
5720
6733
  write(themePath, tailwindEntryTemplate({
@@ -5728,10 +6741,9 @@ var generateRuntime = async (project) => {
5728
6741
  }))
5729
6742
  ]);
5730
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)));
5731
6745
  await Promise.all(exampleDiscovery.examples.map((example) => write(join11(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example))));
5732
- if (askEnabled) {
5733
- await write(join11(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(config.ai.ask)));
5734
- }
6746
+ await writeAskFiles(project, srcDir, write);
5735
6747
  await writeMcpFiles(project, mcp, write);
5736
6748
  if (config.seo.og.enabled) {
5737
6749
  await write(join11(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
@@ -5741,6 +6753,7 @@ var generateRuntime = async (project) => {
5741
6753
  askEnabled,
5742
6754
  exportEpub,
5743
6755
  exportPdf,
6756
+ needsReact,
5744
6757
  staged: hasStaged
5745
6758
  }));
5746
6759
  }
@@ -5775,8 +6788,23 @@ var generateRuntime = async (project) => {
5775
6788
  ...depsLinkWarning ? [depsLinkWarning] : [],
5776
6789
  ...mcp.warnings,
5777
6790
  ...islandDiscovery.warnings,
5778
- ...exampleDiscovery.warnings
6791
+ ...exampleDiscovery.warnings,
6792
+ ...overrideWarnings
5779
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));
5780
6808
  for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
5781
6809
  if (!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))) {
5782
6810
  warnings.push(`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
@@ -5800,14 +6828,17 @@ var generateRuntime = async (project) => {
5800
6828
  return { structuralChange: structural.some(Boolean), warnings };
5801
6829
  };
5802
6830
 
6831
+ // src/core/config.ts
6832
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
6833
+
5803
6834
  // src/core/bridge.ts
5804
- import { existsSync as existsSync6 } from "node:fs";
6835
+ import { existsSync as existsSync7 } from "node:fs";
5805
6836
  import { readFile as readFile9 } from "node:fs/promises";
5806
6837
  import { join as join12 } from "pathe";
5807
6838
 
5808
6839
  // src/migrate/mintlify/config.ts
5809
6840
  import { readFile as readFile8 } from "node:fs/promises";
5810
- 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";
5811
6842
  var MINTLIFY_DEFAULT_IGNORES = [
5812
6843
  "**/_*",
5813
6844
  "**/.*",
@@ -5843,7 +6874,7 @@ var asDirectoryMode = (value) => value === "accordion" || value === "card" || va
5843
6874
  var withoutUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
5844
6875
  var hasOwn = (object, key) => Object.hasOwn(object, key);
5845
6876
  var isInsideRoot = (root, candidate) => {
5846
- const rel = relative6(root, candidate);
6877
+ const rel = relative7(root, candidate);
5847
6878
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
5848
6879
  };
5849
6880
  var readJsonFile = async (file) => {
@@ -5868,7 +6899,7 @@ var resolveRefs = async (value, options) => {
5868
6899
  }
5869
6900
  const ref = asString(object.$ref);
5870
6901
  if (ref) {
5871
- const refFile = resolve3(dirname7(options.file), ref);
6902
+ const refFile = resolve4(dirname8(options.file), ref);
5872
6903
  if (!isInsideRoot(options.root, refFile)) {
5873
6904
  throw new BlumeError({
5874
6905
  code: "BLUME_MINTLIFY_REF_OUTSIDE_ROOT",
@@ -6222,53 +7253,9 @@ var mintlifySelectors = (spec) => {
6222
7253
  }
6223
7254
  ].filter((selector) => selector.items.length > 0);
6224
7255
  };
6225
- var navbarTypeLabel = (type) => {
6226
- if (type === "github") {
6227
- return "GitHub";
6228
- }
6229
- if (type === "discord") {
6230
- return "Discord";
6231
- }
6232
- return;
6233
- };
6234
- var navbarLinkType = (type) => type === "github" || type === "discord" ? type : undefined;
6235
- var navbarPrimaryType = (type) => type === "github" || type === "discord" ? type : "button";
6236
- var mintlifyNavbar = (value) => {
6237
- const object = asObject(value);
6238
- if (!object) {
6239
- return { links: [] };
6240
- }
6241
- const links = asArray(object.links).flatMap((item) => {
6242
- const itemObject = asObject(item);
6243
- const href = itemObject ? asString(itemObject.href) : undefined;
6244
- const type = itemObject ? asString(itemObject.type) : undefined;
6245
- const label = itemObject ? asString(itemObject.label) ?? navbarTypeLabel(type) : undefined;
6246
- if (!itemObject || !href || !label) {
6247
- return [];
6248
- }
6249
- return [
6250
- withoutUndefined({
6251
- href,
6252
- icon: asString(itemObject.icon),
6253
- label,
6254
- type: navbarLinkType(type)
6255
- })
6256
- ];
6257
- });
6258
- const primaryObject = asObject(object.primary);
6259
- const primaryHref = primaryObject ? asString(primaryObject.href) : undefined;
6260
- const primaryType = primaryObject ? asString(primaryObject.type) ?? "button" : undefined;
6261
- const primaryLabel = primaryObject ? asString(primaryObject.label) ?? navbarTypeLabel(primaryType) : undefined;
6262
- const primary = primaryObject && primaryHref && primaryLabel ? withoutUndefined({
6263
- href: primaryHref,
6264
- label: primaryLabel,
6265
- type: navbarPrimaryType(primaryType)
6266
- }) : undefined;
6267
- return withoutUndefined({ links, primary });
6268
- };
6269
7256
  var mintignorePatterns = async (root) => {
6270
7257
  try {
6271
- const raw = await readFile8(resolve3(root, ".mintignore"), "utf-8");
7258
+ const raw = await readFile8(resolve4(root, ".mintignore"), "utf-8");
6272
7259
  return raw.split(`
6273
7260
  `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).filter((line) => !line.startsWith("!")).map((line) => line.endsWith("/") ? `${line}**` : line);
6274
7261
  } catch {
@@ -6287,61 +7274,6 @@ var mintlifyRedirects = (spec) => asArray(spec.redirects).flatMap((redirect) =>
6287
7274
  }
6288
7275
  return [{ from, to }];
6289
7276
  });
6290
- var mintlifyContextual = (value) => {
6291
- const object = asObject(value);
6292
- if (!object) {
6293
- return { options: [] };
6294
- }
6295
- const display = object.display === "toc" ? "toc" : "header";
6296
- const options = [];
6297
- for (const option of asArray(object.options)) {
6298
- if (typeof option === "string") {
6299
- options.push(option);
6300
- continue;
6301
- }
6302
- const optionObject = asObject(option);
6303
- const title = optionObject ? asString(optionObject.title) : undefined;
6304
- if (!optionObject || !title) {
6305
- continue;
6306
- }
6307
- options.push(withoutUndefined({
6308
- description: asString(optionObject.description),
6309
- href: asString(optionObject.href),
6310
- icon: asString(optionObject.icon),
6311
- title
6312
- }));
6313
- }
6314
- return { display, options };
6315
- };
6316
- var mintlifyFooter = (value) => {
6317
- const object = asObject(value);
6318
- const socials = asObject(object?.socials);
6319
- const links = asArray(object?.links).flatMap((group) => {
6320
- const groupObject = asObject(group);
6321
- const items = asArray(groupObject?.items).flatMap((item) => {
6322
- const itemObject = asObject(item);
6323
- const label = itemObject ? asString(itemObject.label) : undefined;
6324
- const href = itemObject ? asString(itemObject.href) : undefined;
6325
- return label && href ? [{ href, label }] : [];
6326
- });
6327
- if (!groupObject || items.length === 0) {
6328
- return [];
6329
- }
6330
- return [
6331
- withoutUndefined({
6332
- header: asString(groupObject.header),
6333
- items
6334
- })
6335
- ];
6336
- }).slice(0, 4);
6337
- return {
6338
- links,
6339
- socials: socials ? Object.fromEntries(Object.entries(socials).flatMap(([label, href]) => {
6340
- const hrefValue = asString(href);
6341
- return hrefValue ? [[label, hrefValue]] : [];
6342
- })) : {}
6343
- };
6344
- };
6345
7277
  var mintlifyLogo = (value) => {
6346
7278
  if (typeof value === "string") {
6347
7279
  return value;
@@ -6406,16 +7338,12 @@ var mintlifyChromeVariants = (spec) => {
6406
7338
  return [];
6407
7339
  }
6408
7340
  const banner = hasOwn(object, "banner") ? mintlifyBanner(object.banner) : undefined;
6409
- const footer = hasOwn(object, "footer") ? mintlifyFooter(object.footer) : undefined;
6410
- const navbar = hasOwn(object, "navbar") ? mintlifyNavbar(object.navbar) : undefined;
6411
- if (!banner && !footer && !navbar) {
7341
+ if (!banner) {
6412
7342
  return [];
6413
7343
  }
6414
7344
  return [
6415
7345
  withoutUndefined({
6416
7346
  banner,
6417
- footer,
6418
- navbar,
6419
7347
  path
6420
7348
  })
6421
7349
  ];
@@ -6467,13 +7395,6 @@ var mintlifyMarkdown = (value, styling) => {
6467
7395
  schema: object?.schema === false ? false : undefined
6468
7396
  });
6469
7397
  };
6470
- var mintlifyStyling = (value) => {
6471
- const object = asObject(value);
6472
- const eyebrows = asString(object?.eyebrows);
6473
- return withoutUndefined({
6474
- eyebrows: eyebrows === "breadcrumbs" || eyebrows === "section" ? eyebrows : undefined
6475
- });
6476
- };
6477
7398
  var mintlifySeo = (value) => {
6478
7399
  const object = asObject(value);
6479
7400
  const metatags = asObject(object?.metatags);
@@ -6484,16 +7405,9 @@ var mintlifySeo = (value) => {
6484
7405
  })) : {}
6485
7406
  };
6486
7407
  };
6487
- var mintlifyIcons = (value) => {
6488
- const object = asObject(value);
6489
- const library = object?.library;
6490
- return withoutUndefined({
6491
- library: library === "fontawesome" || library === "lucide" || library === "tabler" ? library : undefined
6492
- });
6493
- };
6494
7408
  var loadMintlifyConfig = async (root, file) => {
6495
- const projectRoot = resolve3(root);
6496
- const configFile = resolve3(file);
7409
+ const projectRoot = resolve4(root);
7410
+ const configFile = resolve4(file);
6497
7411
  const spec = asObject(await resolveRefs(await readJsonFile(configFile), {
6498
7412
  file: configFile,
6499
7413
  root: projectRoot,
@@ -6524,14 +7438,10 @@ var loadMintlifyConfig = async (root, file) => {
6524
7438
  ],
6525
7439
  root: "."
6526
7440
  },
6527
- contextual: mintlifyContextual(spec.contextual),
6528
7441
  description: asString(spec.description),
6529
7442
  favicon: mintlifyFavicon(spec.favicon),
6530
- footer: mintlifyFooter(spec.footer),
6531
- icons: mintlifyIcons(spec.icons),
6532
7443
  logo: mintlifyLogo(spec.logo),
6533
7444
  markdown: mintlifyMarkdown(spec.markdown, styling),
6534
- navbar: mintlifyNavbar(spec.navbar),
6535
7445
  navigation: {
6536
7446
  chromeVariants: mintlifyChromeVariants(spec),
6537
7447
  selectors: mintlifySelectors(spec),
@@ -6547,7 +7457,6 @@ var loadMintlifyConfig = async (root, file) => {
6547
7457
  prompt: asString(search.prompt)
6548
7458
  },
6549
7459
  seo: mintlifySeo(seo),
6550
- styling: mintlifyStyling(styling),
6551
7460
  theme: {
6552
7461
  accent: asString(colors.primary) ?? "blue",
6553
7462
  accentDark: asString(colors.light),
@@ -6594,7 +7503,7 @@ var mintlifyI18n = (spec) => {
6594
7503
  // src/core/bridge.ts
6595
7504
  var MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
6596
7505
  var detectMintlifyBridge = async (root) => {
6597
- 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));
6598
7507
  if (!configFile) {
6599
7508
  return null;
6600
7509
  }
@@ -6683,8 +7592,8 @@ var createModuleLoader = () => {
6683
7592
  };
6684
7593
 
6685
7594
  // src/core/project.ts
6686
- import { existsSync as existsSync7 } from "node:fs";
6687
- 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";
6688
7597
  var CONFIG_FILENAMES = [
6689
7598
  "blume.config.ts",
6690
7599
  "blume.config.mjs",
@@ -6695,7 +7604,7 @@ var COMPONENTS_FILENAMES = ["components.tsx", "components.ts"];
6695
7604
  var firstExisting = (root, names) => {
6696
7605
  for (const name of names) {
6697
7606
  const candidate = join13(root, name);
6698
- if (existsSync7(candidate)) {
7607
+ if (existsSync8(candidate)) {
6699
7608
  return candidate;
6700
7609
  }
6701
7610
  }
@@ -6703,10 +7612,10 @@ var firstExisting = (root, names) => {
6703
7612
  };
6704
7613
  var findConfigFile = (root) => firstExisting(root, CONFIG_FILENAMES);
6705
7614
  var resolveProjectContext = (root, config) => {
6706
- const absoluteRoot = resolve4(root);
6707
- 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);
6708
7617
  const pagesPath = join13(absoluteRoot, config.content.pages);
6709
- const pagesRoot = existsSync7(pagesPath) ? pagesPath : null;
7618
+ const pagesRoot = existsSync8(pagesPath) ? pagesPath : null;
6710
7619
  return {
6711
7620
  componentsFile: firstExisting(absoluteRoot, COMPONENTS_FILENAMES),
6712
7621
  configFile: findConfigFile(absoluteRoot),
@@ -6951,26 +7860,6 @@ var sidebarVariantSchema = z2.object({
6951
7860
  items: z2.array(sidebarItemSchema).default([]),
6952
7861
  path: z2.string()
6953
7862
  }).strict();
6954
- var navbarLinkTypeSchema = z2.enum(["github", "discord"]);
6955
- var navbarLinkSchema = z2.object({
6956
- href: z2.string(),
6957
- icon: iconName.optional(),
6958
- label: z2.string().optional(),
6959
- type: navbarLinkTypeSchema.optional()
6960
- }).strict().refine((value) => value.label !== undefined || value.type !== undefined, {
6961
- message: "Navbar links require either label or type."
6962
- });
6963
- var navbarPrimarySchema = z2.object({
6964
- href: z2.string(),
6965
- label: z2.string().optional(),
6966
- type: z2.enum(["button", "github", "discord"]).default("button")
6967
- }).strict().refine((value) => value.label !== undefined || value.type !== "button", {
6968
- message: "Navbar primary button links require a label."
6969
- });
6970
- var navbarConfigSchema = z2.object({
6971
- links: z2.array(navbarLinkSchema).default([]),
6972
- primary: navbarPrimarySchema.optional()
6973
- }).strict();
6974
7863
  var variablesConfigSchema = z2.record(z2.string().regex(/^[A-Za-z0-9-]+$/u), z2.string()).default({});
6975
7864
  var fontSlug = z2.string().refine(isFontSlug, (value) => ({
6976
7865
  message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`
@@ -6994,9 +7883,6 @@ var themeConfigSchema = z2.object({
6994
7883
  radius: z2.enum(["none", "sm", "md", "lg"]).default("md"),
6995
7884
  strict: z2.boolean().default(false)
6996
7885
  }).strict();
6997
- var iconsConfigSchema = z2.object({
6998
- library: z2.enum(["fontawesome", "lucide", "tabler"]).default("lucide")
6999
- }).strict();
7000
7886
  var algoliaSearchSchema = z2.object({
7001
7887
  appId: z2.string(),
7002
7888
  indexName: z2.string(),
@@ -7078,33 +7964,8 @@ var aiConfigSchema = z2.object({
7078
7964
  }).optional(),
7079
7965
  llmsTxt: z2.boolean().default(false)
7080
7966
  }).strict();
7081
- var contextualOptionSchema = z2.union([
7082
- z2.string(),
7083
- z2.object({
7084
- description: z2.string().optional(),
7085
- href: z2.string().optional(),
7086
- icon: iconName.optional(),
7087
- title: z2.string()
7088
- }).passthrough()
7089
- ]);
7090
- var contextualConfigSchema = z2.object({
7091
- display: z2.enum(["header", "toc"]).default("header"),
7092
- options: z2.array(contextualOptionSchema).default([])
7093
- }).strict();
7094
- var footerConfigSchema = z2.object({
7095
- links: z2.array(z2.object({
7096
- header: z2.string().optional(),
7097
- items: z2.array(z2.object({
7098
- href: z2.string(),
7099
- label: z2.string()
7100
- }).strict()).default([])
7101
- }).strict()).max(4).default([]),
7102
- socials: z2.record(z2.string(), z2.string()).default({})
7103
- }).strict();
7104
7967
  var chromeVariantSchema = z2.object({
7105
7968
  banner: bannerConfigSchema.optional(),
7106
- footer: footerConfigSchema.optional(),
7107
- navbar: navbarConfigSchema.optional(),
7108
7969
  path: z2.string()
7109
7970
  }).strict();
7110
7971
  var navigationConfigSchema = z2.object({
@@ -7229,9 +8090,6 @@ var markdownConfigSchema = z2.object({
7229
8090
  imageZoom: z2.boolean().default(true),
7230
8091
  math: z2.boolean().default(false)
7231
8092
  }).strict();
7232
- var stylingConfigSchema = z2.object({
7233
- eyebrows: z2.enum(["breadcrumbs", "section"]).default("section")
7234
- }).strict();
7235
8093
  var openapiSourceSchema = z2.object({
7236
8094
  label: z2.string().optional(),
7237
8095
  route: z2.string().optional(),
@@ -7251,36 +8109,48 @@ var asyncapiConfigSchema = z2.object({
7251
8109
  spec: z2.string().optional(),
7252
8110
  theme: z2.string().optional()
7253
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
+ });
7254
8128
  var blumeConfigSchema = z2.object({
7255
8129
  ai: aiConfigSchema.default({}),
7256
8130
  analytics: analyticsConfigSchema.optional(),
7257
8131
  asyncapi: asyncapiConfigSchema.default({}),
7258
8132
  banner: bannerConfigSchema.optional(),
7259
8133
  content: contentConfigSchema.default({}),
7260
- contextual: contextualConfigSchema.default({}),
7261
8134
  deployment: deploymentConfigSchema.default({}),
7262
8135
  description: z2.string().optional(),
7263
8136
  examples: z2.string().default("examples"),
7264
8137
  export: exportConfigSchema.default(false),
7265
8138
  favicon: faviconConfigSchema.optional(),
7266
8139
  feedback: z2.boolean().default(true),
7267
- footer: footerConfigSchema.default({}),
7268
8140
  github: githubConfigSchema.optional(),
7269
8141
  i18n: i18nConfigSchema.optional(),
7270
- icons: iconsConfigSchema.default({}),
7271
8142
  lastModified: lastModifiedConfigSchema.default(false),
7272
8143
  logo: logoConfigSchema.optional(),
7273
8144
  markdown: markdownConfigSchema.default({}),
7274
8145
  mcp: mcpConfigSchema.default({}),
7275
- navbar: navbarConfigSchema.default({}),
7276
8146
  navigation: navigationConfigSchema.default({}),
7277
8147
  openapi: openapiConfigSchema.default({}),
7278
8148
  redirects: z2.array(redirectSchema).default([]),
7279
8149
  search: searchConfigSchema.default({}),
7280
8150
  seo: seoConfigSchema.default({}),
7281
- styling: stylingConfigSchema.default({}),
7282
8151
  theme: themeConfigSchema.default({}),
7283
8152
  title: z2.string().default("Documentation"),
8153
+ toc: tocConfigSchema,
7284
8154
  variables: variablesConfigSchema
7285
8155
  }).strict();
7286
8156
 
@@ -7310,9 +8180,11 @@ var loadConfig = async (root, options = {}) => {
7310
8180
  const sourceFile = bridge?.configFile ?? configFile;
7311
8181
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
7312
8182
  if (!parsed.success) {
8183
+ const source = sourceFile && existsSync9(sourceFile) ? readFileSync6(sourceFile, "utf-8") : undefined;
7313
8184
  const diagnostics = diagnosticsFromZod(parsed.error, {
7314
8185
  code: "BLUME_CONFIG_INVALID",
7315
- file: sourceFile ?? undefined
8186
+ file: sourceFile ?? undefined,
8187
+ source
7316
8188
  });
7317
8189
  throw new BlumeError(diagnostics[0] ?? {
7318
8190
  code: "BLUME_CONFIG_INVALID",
@@ -7337,7 +8209,7 @@ var loadConfig = async (root, options = {}) => {
7337
8209
  };
7338
8210
 
7339
8211
  // src/core/navigation.ts
7340
- import { extname as extname2 } from "pathe";
8212
+ import { extname as extname3 } from "pathe";
7341
8213
  var NUMERIC_PREFIX = /^(?<order>\d+)[-_.]/u;
7342
8214
  var GROUP_FOLDER = /^\((?<label>.+)\)$/u;
7343
8215
  var WORD_SPLIT = /[-_]/u;
@@ -7374,7 +8246,7 @@ var pageOrder = (page, filename) => {
7374
8246
  if (page.meta.sidebar.order !== undefined) {
7375
8247
  return page.meta.sidebar.order;
7376
8248
  }
7377
- if (filename.replace(extname2(filename), "") === "index") {
8249
+ if (filename.replace(extname3(filename), "") === "index") {
7378
8250
  return Number.NEGATIVE_INFINITY;
7379
8251
  }
7380
8252
  return numericOrder(filename);
@@ -7466,7 +8338,7 @@ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix) => {
7466
8338
  deprecated: page.meta.deprecated || undefined,
7467
8339
  description: page.description,
7468
8340
  icon: page.meta.sidebar.icon,
7469
- key: segmentKey(filename.replace(extname2(filename), "")),
8341
+ key: segmentKey(filename.replace(extname3(filename), "")),
7470
8342
  kind: "page",
7471
8343
  label: page.meta.sidebar.label ?? page.title,
7472
8344
  order: pageOrder(page, filename),
@@ -7665,6 +8537,8 @@ var buildContentGraph = (pages, options) => {
7665
8537
  tabs: options.navigation.tabs
7666
8538
  });
7667
8539
  }
8540
+ diagnostics.push(...validateNavIcons(navigation));
8541
+ diagnostics.push(...validateNavStructure(navigation, pages));
7668
8542
  return {
7669
8543
  diagnostics,
7670
8544
  navigation,
@@ -7676,7 +8550,7 @@ var buildContentGraph = (pages, options) => {
7676
8550
 
7677
8551
  // src/core/last-modified.ts
7678
8552
  import { execFileSync } from "node:child_process";
7679
- import { relative as relative7 } from "pathe";
8553
+ import { relative as relative8 } from "pathe";
7680
8554
  var resolveLastModifiedConfig = (value) => {
7681
8555
  if (value === false) {
7682
8556
  return { enabled: false, source: "git" };
@@ -7716,7 +8590,7 @@ var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
7716
8590
  const byRepoPath = parseGitLog(output);
7717
8591
  const result = new Map;
7718
8592
  for (const sourcePath of sourcePaths) {
7719
- const iso = byRepoPath.get(relative7(gitRoot, sourcePath));
8593
+ const iso = byRepoPath.get(relative8(gitRoot, sourcePath));
7720
8594
  if (iso) {
7721
8595
  result.set(sourcePath, iso);
7722
8596
  }
@@ -7728,7 +8602,7 @@ var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
7728
8602
  };
7729
8603
 
7730
8604
  // src/core/meta.ts
7731
- 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";
7732
8606
  import { glob as glob5 } from "tinyglobby";
7733
8607
  var META_FILES = [
7734
8608
  "**/meta.ts",
@@ -7758,7 +8632,7 @@ var discoverFolderMeta = async (contentRoot) => {
7758
8632
  const shared = new Map;
7759
8633
  const diagnostics = [];
7760
8634
  for (const entry of loaded) {
7761
- const dir = relative8(contentRoot, dirname8(entry.file));
8635
+ const dir = relative9(contentRoot, dirname9(entry.file));
7762
8636
  if (!entry.ok) {
7763
8637
  diagnostics.push({
7764
8638
  code: "BLUME_META_LOAD_FAILED",
@@ -7783,8 +8657,9 @@ var discoverFolderMeta = async (contentRoot) => {
7783
8657
  };
7784
8658
 
7785
8659
  // src/core/sources/normalize.ts
8660
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
7786
8661
  import GithubSlugger from "github-slugger";
7787
- import { extname as extname3 } from "pathe";
8662
+ import { extname as extname4 } from "pathe";
7788
8663
  var NUMERIC_PREFIX2 = /^\d+[-_.]/u;
7789
8664
  var GROUP_FOLDER2 = /^\((?<label>.+)\)$/u;
7790
8665
  var WORD_SPLIT2 = /[-_]/u;
@@ -7793,7 +8668,7 @@ var groupLabel = (segment) => segment.match(GROUP_FOLDER2)?.groups?.label ?? nul
7793
8668
  var slugify2 = (text) => text.toLowerCase().trim().replaceAll(/[^\w\s-]/gu, "").replaceAll(/[\s_]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "");
7794
8669
  var titleCase = (value) => value.split(WORD_SPLIT2).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
7795
8670
  var mapRoute = (relativePath) => {
7796
- const withoutExt = relativePath.slice(0, relativePath.length - extname3(relativePath).length);
8671
+ const withoutExt = relativePath.slice(0, relativePath.length - extname4(relativePath).length);
7797
8672
  const rawParts = withoutExt.split("/");
7798
8673
  const segments = [];
7799
8674
  const groups = [];
@@ -7840,10 +8715,39 @@ var MD_LINK = /\[[^\]]*\]\((?<target>[^)\s]+)(?:\s+"[^"]*")?\)/gu;
7840
8715
  var extractLinks = (body) => {
7841
8716
  const links = [];
7842
8717
  let inFence = false;
7843
- let lineNumber = 0;
8718
+ let lineNumber = 0;
8719
+ for (const line of body.split(`
8720
+ `)) {
8721
+ lineNumber += 1;
8722
+ if (CODE_FENCE2.test(line.trimStart())) {
8723
+ inFence = !inFence;
8724
+ continue;
8725
+ }
8726
+ if (inFence) {
8727
+ continue;
8728
+ }
8729
+ for (const match of line.matchAll(MD_LINK)) {
8730
+ const target = match.groups?.target;
8731
+ if (target === undefined || match.index === undefined) {
8732
+ continue;
8733
+ }
8734
+ links.push({
8735
+ column: line.indexOf(target, match.index) + 1,
8736
+ line: lineNumber,
8737
+ target
8738
+ });
8739
+ }
8740
+ }
8741
+ return links;
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;
7844
8749
  for (const line of body.split(`
7845
8750
  `)) {
7846
- lineNumber += 1;
7847
8751
  if (CODE_FENCE2.test(line.trimStart())) {
7848
8752
  inFence = !inFence;
7849
8753
  continue;
@@ -7851,19 +8755,15 @@ var extractLinks = (body) => {
7851
8755
  if (inFence) {
7852
8756
  continue;
7853
8757
  }
7854
- for (const match of line.matchAll(MD_LINK)) {
7855
- const target = match.groups?.target;
7856
- if (target === undefined || match.index === undefined) {
7857
- continue;
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);
7858
8763
  }
7859
- links.push({
7860
- column: line.indexOf(target, match.index) + 1,
7861
- line: lineNumber,
7862
- target
7863
- });
7864
8764
  }
7865
8765
  }
7866
- return links;
8766
+ return [...tags];
7867
8767
  };
7868
8768
  var deriveTitle = (meta, headings, id2) => {
7869
8769
  if (meta.title) {
@@ -7874,18 +8774,20 @@ var deriveTitle = (meta, headings, id2) => {
7874
8774
  return firstHeading.text;
7875
8775
  }
7876
8776
  const base = id2.split("/").pop() ?? id2;
7877
- return titleCase(stripNumericPrefix(base.replace(extname3(base), "")));
8777
+ return titleCase(stripNumericPrefix(base.replace(extname4(base), "")));
7878
8778
  };
7879
8779
  var withPrefix = (prefix, path) => prefix ? `${prefix}/${path}` : path;
7880
- var normalizeEntry = (entry, ctx) => {
8780
+ var normalizeEntry2 = (entry, ctx) => {
7881
8781
  const { format } = entry.body;
7882
8782
  const ext = format === "mdx" ? ".mdx" : ".md";
7883
8783
  const result = pageMetaSchema.safeParse(entry.data);
7884
8784
  if (!result.success) {
8785
+ const source = entry.raw ?? (entry.sourcePath && existsSync10(entry.sourcePath) ? readFileSync7(entry.sourcePath, "utf-8") : undefined);
7885
8786
  return {
7886
8787
  diagnostics: diagnosticsFromZod(result.error, {
7887
8788
  code: "BLUME_FRONTMATTER_INVALID",
7888
- file: entry.sourcePath ?? `${ctx.source.name}:${entry.ref}`
8789
+ file: entry.sourcePath ?? `${ctx.source.name}:${entry.ref}`,
8790
+ source
7889
8791
  }),
7890
8792
  pages: []
7891
8793
  };
@@ -7901,6 +8803,7 @@ var normalizeEntry = (entry, ctx) => {
7901
8803
  const base = {
7902
8804
  body: staged ? { format, text: entry.raw ?? entry.body.text } : undefined,
7903
8805
  collection: staged ? "staged" : undefined,
8806
+ componentsUsed: format === "mdx" ? extractComponentTags(entry.body.text) : undefined,
7904
8807
  contentType: meta.type ?? ctx.defaultType,
7905
8808
  description: meta.description,
7906
8809
  editUrl: entry.editUrl,
@@ -7931,12 +8834,12 @@ var normalizeEntry = (entry, ctx) => {
7931
8834
  import { join as join22 } from "pathe";
7932
8835
 
7933
8836
  // src/core/sources/filesystem.ts
7934
- import { existsSync as existsSync8, watch as fsWatch } from "node:fs";
8837
+ import { existsSync as existsSync11, watch as fsWatch } from "node:fs";
7935
8838
  import { readFile as readFile10 } from "node:fs/promises";
7936
- 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";
7937
8840
  import { glob as glob6 } from "tinyglobby";
7938
8841
  var filesystemSource = (options) => {
7939
- 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);
7940
8843
  const load2 = async () => {
7941
8844
  const files = await glob6(options.include, {
7942
8845
  absolute: true,
@@ -7947,20 +8850,20 @@ var filesystemSource = (options) => {
7947
8850
  files.sort();
7948
8851
  const entries = await Promise.all(files.map(async (file) => {
7949
8852
  const source = await readFile10(file, "utf-8");
7950
- const ext = extname4(file).toLowerCase();
8853
+ const ext = extname5(file).toLowerCase();
7951
8854
  const format = ext === ".mdx" ? "mdx" : "md";
7952
8855
  const parsed = frontmatter_default(source);
7953
8856
  return {
7954
8857
  body: { format, text: parsed.content },
7955
8858
  data: parsed.data,
7956
- ref: relative9(contentRoot, file),
8859
+ ref: relative10(contentRoot, file),
7957
8860
  sourcePath: file
7958
8861
  };
7959
8862
  }));
7960
8863
  return { diagnostics: [], entries };
7961
8864
  };
7962
8865
  const validate = () => {
7963
- if (!existsSync8(contentRoot)) {
8866
+ if (!existsSync11(contentRoot)) {
7964
8867
  throw new BlumeError({
7965
8868
  code: options.missingCode ?? "BLUME_CONTENT_ROOT_MISSING",
7966
8869
  file: contentRoot,
@@ -7971,7 +8874,7 @@ var filesystemSource = (options) => {
7971
8874
  }
7972
8875
  };
7973
8876
  const watch = (onChange) => {
7974
- if (!existsSync8(contentRoot)) {
8877
+ if (!existsSync11(contentRoot)) {
7975
8878
  return () => {};
7976
8879
  }
7977
8880
  const watcher = fsWatch(contentRoot, { recursive: true }, onChange);
@@ -8305,13 +9208,13 @@ var mdxRemoteSource = (options, ctx) => {
8305
9208
  };
8306
9209
 
8307
9210
  // src/core/sources/mintlify.ts
8308
- import { existsSync as existsSync10, watch as fsWatch2 } from "node:fs";
9211
+ import { existsSync as existsSync13, watch as fsWatch2 } from "node:fs";
8309
9212
  import { readFile as readFile13 } from "node:fs/promises";
8310
- 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";
8311
9214
  import { glob as glob7 } from "tinyglobby";
8312
9215
 
8313
9216
  // src/migrate/shared.ts
8314
- import { existsSync as existsSync9 } from "node:fs";
9217
+ import { existsSync as existsSync12 } from "node:fs";
8315
9218
  import { readFile as readFile12, writeFile as writeFile4 } from "node:fs/promises";
8316
9219
  import { join as join16 } from "pathe";
8317
9220
  var writeBlumeConfig = async (root, config) => {
@@ -8328,7 +9231,7 @@ var BLUME_SCRIPTS = {
8328
9231
  };
8329
9232
  var rewriteFrameworkScripts = async (root, cli, remove) => {
8330
9233
  const pkgPath = join16(root, "package.json");
8331
- if (!existsSync9(pkgPath)) {
9234
+ if (!existsSync12(pkgPath)) {
8332
9235
  return false;
8333
9236
  }
8334
9237
  let pkg;
@@ -8364,7 +9267,7 @@ var rewriteFrameworkScripts = async (root, cli, remove) => {
8364
9267
  var gitignoreKey = (line) => line.trim().replace(/\/+$/u, "");
8365
9268
  var ensureGitignore = async (root, entries) => {
8366
9269
  const path = join16(root, ".gitignore");
8367
- const existing = existsSync9(path) ? await readFile12(path, "utf-8") : "";
9270
+ const existing = existsSync12(path) ? await readFile12(path, "utf-8") : "";
8368
9271
  const present = new Set(existing.split(`
8369
9272
  `).map(gitignoreKey).filter(Boolean));
8370
9273
  const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
@@ -8379,7 +9282,7 @@ var ensureGitignore = async (root, entries) => {
8379
9282
  `, "utf-8");
8380
9283
  return added;
8381
9284
  };
8382
- var leftoverFiles = (root, candidates) => candidates.filter((candidate) => existsSync9(join16(root, candidate)));
9285
+ var leftoverFiles = (root, candidates) => candidates.filter((candidate) => existsSync12(join16(root, candidate)));
8383
9286
  var attribute = (attrs, name) => {
8384
9287
  const match = attrs.match(new RegExp(`\\b${name}=(?:"(?<dq>[^"]*)"|'(?<sq>[^']*)')`, "u"));
8385
9288
  return match?.groups?.dq ?? match?.groups?.sq;
@@ -8422,7 +9325,7 @@ var findOpenTagEnd = (source, from) => {
8422
9325
  }
8423
9326
  return -1;
8424
9327
  };
8425
- var directiveFor = (tag, attrs, options) => {
9328
+ var directiveFor2 = (tag, attrs, options) => {
8426
9329
  if (tag in options.tagDirectives) {
8427
9330
  return options.tagDirectives[tag];
8428
9331
  }
@@ -8452,7 +9355,7 @@ var rewriteCallouts = (source, options) => {
8452
9355
  continue;
8453
9356
  }
8454
9357
  const attrs = source.slice(start + tag.length + 1, openEnd);
8455
- const directive = directiveFor(tag, attrs, options);
9358
+ const directive = directiveFor2(tag, attrs, options);
8456
9359
  const closeTag = `</${tag}>`;
8457
9360
  const selfClosing = attrs.trimEnd().endsWith("/");
8458
9361
  const closeIndex = selfClosing ? openEnd : source.indexOf(closeTag, openEnd + 1);
@@ -8776,7 +9679,7 @@ var isLiteralObject = (value) => typeof value === "object" && value !== null &&
8776
9679
  var asLiteralArray = (value) => Array.isArray(value) ? value : undefined;
8777
9680
 
8778
9681
  // src/migrate/mintlify/content.ts
8779
- 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";
8780
9683
  var CALLOUT_DIRECTIVES = {
8781
9684
  Check: "success",
8782
9685
  Danger: "danger",
@@ -8814,7 +9717,7 @@ var rewriteSnippetImports = (source, options) => {
8814
9717
  }
8815
9718
  const target = join17(options.root, importSource.replace(/^\/+/u, ""));
8816
9719
  components.push(importSource.replace(/^\/+/u, ""));
8817
- let rel = relative10(dirname9(options.filePath), target);
9720
+ let rel = relative11(dirname10(options.filePath), target);
8818
9721
  if (!rel.startsWith(".")) {
8819
9722
  rel = `./${rel}`;
8820
9723
  }
@@ -9004,7 +9907,7 @@ var rewriteMintlifySvgIconProps = (source) => {
9004
9907
 
9005
9908
  // src/migrate/mintlify/snippets.ts
9006
9909
  import { readFile as readFileFromDisk } from "node:fs/promises";
9007
- 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";
9008
9911
  var MARKDOWN_SNIPPET_IMPORT = /^import\s+(?<name>[$A-Z_a-z][$\w]*)\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
9009
9912
  var NAMED_SNIPPET_IMPORT = /^import\s+\{(?<names>[^}]+)\}\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
9010
9913
  var EXPORTED_STRING_CONST = /^export\s+const\s+(?<name>[$A-Z_a-z][$\w]*)\s*=\s*(?:"(?<double>(?:\\.|[^"\\])*)"|'(?<single>(?:\\.|[^'\\])*)'|`(?<template>(?:\\.|[^`\\])*)`)\s*;?\s*$/gmu;
@@ -9014,14 +9917,14 @@ var GLOBAL_VARIABLE = /\{\{\s*(?<name>[A-Za-z0-9-]+)\s*\}\}/gu;
9014
9917
  var FRONTMATTER_BLOCK = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/u;
9015
9918
  var USER_EXPORT = /^(?:export\s+)?(?:const|let|var)\s+user\s*=|^import\s+\{\s*user\s*\}/mu;
9016
9919
  var isInsideRoot2 = (root, candidate) => {
9017
- const rel = relative11(root, candidate);
9920
+ const rel = relative12(root, candidate);
9018
9921
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
9019
9922
  };
9020
- var escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
9021
- var snippetSelfClosingTagPattern = (name) => new RegExp(`<${escapeRegExp(name)}(?<attrs>[^>]*?)\\s*/>`, "gu");
9022
- 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");
9023
9926
  var rootRelativePath = (root, file) => {
9024
- const rel = relative11(root, file);
9927
+ const rel = relative12(root, file);
9025
9928
  return rel ? `/${rel}` : "/";
9026
9929
  };
9027
9930
  var snippetCycleMessage = (root, file, trail) => {
@@ -9030,10 +9933,10 @@ var snippetCycleMessage = (root, file, trail) => {
9030
9933
  return `Circular Mintlify snippet import detected: ${cycle}`;
9031
9934
  };
9032
9935
  var resolveSnippetPath = (options) => {
9033
- 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);
9034
9937
  return isInsideRoot2(options.root, target) ? target : null;
9035
9938
  };
9036
- var collectImports = (source) => [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
9939
+ var collectImports2 = (source) => [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
9037
9940
  const name = match.groups?.name;
9038
9941
  const importSource = match.groups?.source;
9039
9942
  if (!(name && importSource)) {
@@ -9081,7 +9984,7 @@ var interpolateProps = (source, props) => source.replaceAll(PLACEHOLDER, (value,
9081
9984
  var stripImport = (source, importText) => source.replace(importText, "").replaceAll(/\n{3,}/gu, `
9082
9985
 
9083
9986
  `);
9084
- 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);
9085
9988
  var inlineSnippetTags = (options) => {
9086
9989
  const inline = (_value, attrs) => interpolateProps(options.snippet, parseAttributes(attrs));
9087
9990
  return options.source.replaceAll(snippetSelfClosingTagPattern(options.name), inline).replaceAll(snippetPairedTagPattern(options.name), inline);
@@ -9125,7 +10028,7 @@ var rewriteMintlifyMarkdownSnippets = async (source, options) => {
9125
10028
  });
9126
10029
  return next === current ? current : stripImport(next, snippetImport.importText);
9127
10030
  };
9128
- const imports = collectImports(source);
10031
+ const imports = collectImports2(source);
9129
10032
  const inlineAt = async (index, current) => {
9130
10033
  const snippetImport = imports[index];
9131
10034
  if (!snippetImport) {
@@ -9224,11 +10127,11 @@ var MINTLIFY_SOURCE_IGNORES = [
9224
10127
  "snippets/**"
9225
10128
  ];
9226
10129
  var mintlifySource = (options) => {
9227
- 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);
9228
10131
  const ignore = [...new Set([...options.exclude, ...MINTLIFY_SOURCE_IGNORES])];
9229
10132
  const transform = (raw, file) => transformMintlifyContent(raw, {
9230
10133
  filePath: file,
9231
- root: resolve7(options.projectRoot),
10134
+ root: resolve8(options.projectRoot),
9232
10135
  variables: options.variables
9233
10136
  });
9234
10137
  const load2 = async () => {
@@ -9250,7 +10153,7 @@ var mintlifySource = (options) => {
9250
10153
  body: { format: "mdx", text: parsed.content },
9251
10154
  data: parsed.data,
9252
10155
  raw: result.content,
9253
- ref: relative12(contentRoot, file),
10156
+ ref: relative13(contentRoot, file),
9254
10157
  sourcePath: file
9255
10158
  };
9256
10159
  }));
@@ -9264,7 +10167,7 @@ var mintlifySource = (options) => {
9264
10167
  return { diagnostics, entries };
9265
10168
  };
9266
10169
  const validate = () => {
9267
- if (!existsSync10(contentRoot)) {
10170
+ if (!existsSync13(contentRoot)) {
9268
10171
  throw new BlumeError({
9269
10172
  code: "BLUME_CONTENT_ROOT_MISSING",
9270
10173
  file: contentRoot,
@@ -9276,11 +10179,11 @@ var mintlifySource = (options) => {
9276
10179
  };
9277
10180
  const watch = (onChange) => {
9278
10181
  const disposers = [];
9279
- if (existsSync10(contentRoot)) {
10182
+ if (existsSync13(contentRoot)) {
9280
10183
  const watcher = fsWatch2(contentRoot, { recursive: true }, onChange);
9281
10184
  disposers.push(() => watcher.close());
9282
10185
  }
9283
- if (options.configFile && existsSync10(options.configFile)) {
10186
+ if (options.configFile && existsSync13(options.configFile)) {
9284
10187
  const watcher = fsWatch2(options.configFile, onChange);
9285
10188
  disposers.push(() => watcher.close());
9286
10189
  }
@@ -9312,13 +10215,13 @@ import { join as join20 } from "pathe";
9312
10215
 
9313
10216
  // src/core/sources/assets.ts
9314
10217
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
9315
- import { extname as extname5, join as join19 } from "pathe";
10218
+ import { extname as extname6, join as join19 } from "pathe";
9316
10219
  var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
9317
10220
  var REMOTE = /^https?:\/\//u;
9318
10221
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
9319
10222
  var extFor = (url) => {
9320
10223
  const clean = url.split("?")[0] ?? url;
9321
- const ext = extname5(clean);
10224
+ const ext = extname6(clean);
9322
10225
  return SAFE_EXT.test(ext) ? ext.toLowerCase() : ".png";
9323
10226
  };
9324
10227
  var materializeAssets = async (markdown, ctx) => {
@@ -9919,12 +10822,32 @@ var resolveSources = (config, context, runtime) => {
9919
10822
  };
9920
10823
 
9921
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
+ };
9922
10843
  var scanProject = async (root, options = {}) => {
9923
10844
  const mode = options.mode ?? "dev";
9924
10845
  const preview = options.preview ?? false;
9925
- const { bridge, config } = await loadConfig(root, {
10846
+ const configResult = await loadConfig(root, {
9926
10847
  devServerUrl: options.devServerUrl
9927
10848
  });
10849
+ const { bridge } = configResult;
10850
+ const config = applyConfigOverrides(configResult.config, options.overrides);
9928
10851
  const context = resolveProjectContext(root, config);
9929
10852
  const sources = resolveSources(config, context, {
9930
10853
  mode,
@@ -9943,7 +10866,7 @@ var scanProject = async (root, options = {}) => {
9943
10866
  for (const { source, entries, diagnostics } of loaded) {
9944
10867
  contentDiagnostics.push(...diagnostics);
9945
10868
  for (const entry of entries) {
9946
- const normalized = normalizeEntry(entry, {
10869
+ const normalized = normalizeEntry2(entry, {
9947
10870
  defaultType: config.content.defaultType,
9948
10871
  i18n: config.i18n,
9949
10872
  source: {
@@ -9993,13 +10916,13 @@ var scanProject = async (root, options = {}) => {
9993
10916
  };
9994
10917
 
9995
10918
  // src/cli/env.ts
9996
- import { existsSync as existsSync11, readFileSync as readFileSync6 } from "node:fs";
9997
- import { dirname as dirname11, join as join23, resolve as resolve8 } from "pathe";
10919
+ import { existsSync as existsSync14, readFileSync as readFileSync8 } from "node:fs";
10920
+ import { dirname as dirname12, join as join23, resolve as resolve9 } from "pathe";
9998
10921
  var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
9999
- var DOUBLE_QUOTED = /^"(?<body>[\s\S]*)"$/u;
10922
+ var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
10000
10923
  var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
10001
10924
  var unquote = (raw) => {
10002
- const double = raw.match(DOUBLE_QUOTED)?.groups?.body;
10925
+ const double = raw.match(DOUBLE_QUOTED2)?.groups?.body;
10003
10926
  if (double !== undefined) {
10004
10927
  return double.replaceAll("\\n", `
10005
10928
  `).replaceAll("\\t", "\t").replaceAll("\\\"", '"').replaceAll("\\\\", "\\");
@@ -10032,23 +10955,81 @@ var applyEnv = (parsed) => {
10032
10955
  };
10033
10956
  var loadFile = (path) => {
10034
10957
  try {
10035
- if (existsSync11(path)) {
10036
- applyEnv(parseEnv(readFileSync6(path, "utf-8")));
10958
+ if (existsSync14(path)) {
10959
+ applyEnv(parseEnv(readFileSync8(path, "utf-8")));
10037
10960
  }
10038
10961
  } catch {}
10039
10962
  };
10040
10963
  var loadEnvFiles = (startDir) => {
10041
- let dir = resolve8(startDir);
10964
+ let dir = resolve9(startDir);
10042
10965
  let done = false;
10043
10966
  while (!done) {
10044
10967
  loadFile(join23(dir, ".env.local"));
10045
10968
  loadFile(join23(dir, ".env"));
10046
- const parent = dirname11(dir);
10047
- done = existsSync11(join23(dir, ".git")) || parent === dir;
10969
+ const parent = dirname12(dir);
10970
+ done = existsSync14(join23(dir, ".git")) || parent === dir;
10048
10971
  dir = parent;
10049
10972
  }
10050
10973
  };
10051
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
+
10052
11033
  // src/cli/prepare.ts
10053
11034
  var prepareProject = async (options) => {
10054
11035
  loadEnvFiles(options.root);
@@ -10057,15 +11038,17 @@ var prepareProject = async (options) => {
10057
11038
  project = await scanProject(options.root, {
10058
11039
  devServerUrl: options.devServerUrl,
10059
11040
  mode: options.mode,
11041
+ overrides: options.overrides,
10060
11042
  preview: options.preview,
10061
11043
  refresh: options.refresh
10062
11044
  });
10063
11045
  } catch (error) {
10064
11046
  if (error instanceof BlumeError) {
10065
11047
  reportDiagnostics([error.diagnostic], options.root);
10066
- process.exit(1);
11048
+ } else {
11049
+ reportInternalError(error);
10067
11050
  }
10068
- throw error;
11051
+ process.exit(1);
10069
11052
  }
10070
11053
  if (options.mode === "build" && project.config.deployment.output === "static") {
10071
11054
  const features = serverFeatures(project.config);
@@ -10093,12 +11076,102 @@ var prepareProject = async (options) => {
10093
11076
  for (const warning of warnings) {
10094
11077
  logger.warn(warning);
10095
11078
  }
11079
+ reportDiagnostics(checkRequiredSecrets(project.config), options.root);
10096
11080
  return project;
10097
11081
  };
10098
11082
 
10099
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
+ };
10100
11149
  var buildCommand = defineCommand2({
10101
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
+ },
10102
11175
  preview: {
10103
11176
  description: "Include drafts and unpublished CMS content.",
10104
11177
  type: "boolean"
@@ -10111,8 +11184,21 @@ var buildCommand = defineCommand2({
10111
11184
  },
10112
11185
  async run({ args }) {
10113
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
+ }
10114
11195
  const project = await prepareProject({
10115
11196
  mode: "build",
11197
+ overrides: {
11198
+ adapter: args.adapter,
11199
+ base: args.base,
11200
+ output: args.output
11201
+ },
10116
11202
  preview: args.preview,
10117
11203
  root,
10118
11204
  strict: args.strict
@@ -10142,15 +11228,16 @@ var buildCommand = defineCommand2({
10142
11228
  logger.success("Generated llms.txt and llms-full.txt");
10143
11229
  }
10144
11230
  const sitemap = buildSitemap(project);
10145
- if (sitemap && !existsSync12(join24(distDir, "sitemap.xml"))) {
11231
+ if (sitemap && !existsSync15(join24(distDir, "sitemap.xml"))) {
10146
11232
  await writeFile6(join24(distDir, "sitemap.xml"), sitemap, "utf-8");
10147
11233
  logger.success("Generated sitemap.xml");
10148
11234
  }
10149
11235
  const robots = buildRobots(project);
10150
- if (robots && !existsSync12(join24(distDir, "robots.txt"))) {
11236
+ if (robots && !existsSync15(join24(distDir, "robots.txt"))) {
10151
11237
  await writeFile6(join24(distDir, "robots.txt"), robots, "utf-8");
10152
11238
  logger.success("Generated robots.txt");
10153
11239
  }
11240
+ await emitRedirectFiles(project.config, distDir);
10154
11241
  const { config } = project;
10155
11242
  const features = serverFeatures(config);
10156
11243
  logger.box([
@@ -10165,16 +11252,113 @@ var buildCommand = defineCommand2({
10165
11252
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`
10166
11253
  ].join(`
10167
11254
  `));
11255
+ if (args.analyze) {
11256
+ await reportBundleSizes(distDir);
11257
+ }
11258
+ if (await enforceBudget(distDir, args) === "fail") {
11259
+ process.exit(1);
11260
+ }
10168
11261
  logger.success(`Built to ${distDir}`);
10169
11262
  }
10170
11263
  });
10171
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
+
10172
11313
  // src/cli/commands/dev.ts
10173
11314
  import { watch } from "node:fs";
10174
11315
  import { dev } from "astro";
10175
- import { defineCommand as defineCommand3 } from "citty";
10176
- 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({
10177
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
+ },
10178
11362
  host: { description: "Network host to bind.", type: "string" },
10179
11363
  open: { description: "Open the browser on start.", type: "boolean" },
10180
11364
  port: { description: "Port to listen on.", type: "string" },
@@ -10191,11 +11375,13 @@ var devCommand = defineCommand3({
10191
11375
  async run({ args }) {
10192
11376
  const root = process.cwd();
10193
11377
  const preview = args.preview ?? false;
11378
+ const overrides = args["content-dir"] ? { contentRoot: args["content-dir"] } : undefined;
10194
11379
  const port = args.port ? Number(args.port) : 4321;
10195
11380
  const devServerUrl = `http://localhost:${port}`;
10196
11381
  const project = await prepareProject({
10197
11382
  devServerUrl,
10198
11383
  mode: "dev",
11384
+ overrides,
10199
11385
  preview,
10200
11386
  root,
10201
11387
  strict: args.strict
@@ -10204,7 +11390,7 @@ var devCommand = defineCommand3({
10204
11390
  logger.info('Detected docs.json — running in Mintlify bridge mode (no migration). Run "blume migrate mintlify" to convert permanently.');
10205
11391
  }
10206
11392
  const server = await dev({
10207
- logLevel: "info",
11393
+ logLevel: args.debug ? "debug" : "info",
10208
11394
  root: project.context.outDir,
10209
11395
  server: {
10210
11396
  host: args.host ?? false,
@@ -10212,6 +11398,7 @@ var devCommand = defineCommand3({
10212
11398
  port: args.port ? Number(args.port) : undefined
10213
11399
  }
10214
11400
  });
11401
+ showBlumeErrorOverlay(project.diagnostics);
10215
11402
  let timer = null;
10216
11403
  const regenerate = () => {
10217
11404
  if (timer) {
@@ -10222,9 +11409,11 @@ var devCommand = defineCommand3({
10222
11409
  const next = await scanProject(root, {
10223
11410
  devServerUrl,
10224
11411
  mode: "dev",
11412
+ overrides,
10225
11413
  preview
10226
11414
  });
10227
11415
  await generateRuntime(next);
11416
+ showBlumeErrorOverlay(next.diagnostics);
10228
11417
  } catch (error) {
10229
11418
  logger.error(`Regeneration failed: ${error.message}`);
10230
11419
  }
@@ -10256,14 +11445,20 @@ var devCommand = defineCommand3({
10256
11445
  });
10257
11446
 
10258
11447
  // src/cli/commands/doctor.ts
10259
- import { defineCommand as defineCommand4 } from "citty";
11448
+ import { defineCommand as defineCommand5 } from "citty";
10260
11449
  var MIN_NODE_MAJOR = 20;
10261
- 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
+ },
10262
11457
  meta: {
10263
11458
  description: "Diagnose common configuration and content problems.",
10264
11459
  name: "doctor"
10265
11460
  },
10266
- async run() {
11461
+ async run({ args }) {
10267
11462
  const root = process.cwd();
10268
11463
  const diagnostics = [];
10269
11464
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
@@ -10295,15 +11490,24 @@ var doctorCommand = defineCommand4({
10295
11490
  suggestion: 'Set deployment.adapter (e.g. "vercel").'
10296
11491
  });
10297
11492
  }
10298
- logger.info(`Pages: ${project.graph.pages.length}`);
10299
- logger.info(`Output: ${config.deployment.output}`);
10300
- 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
+ }
10301
11498
  } catch (error) {
10302
11499
  if (error instanceof BlumeError) {
10303
11500
  diagnostics.push(error.diagnostic);
10304
11501
  } else {
10305
- throw error;
11502
+ reportInternalError(error);
11503
+ process.exit(1);
11504
+ }
11505
+ }
11506
+ if (args.json) {
11507
+ if (reportDiagnosticsJson(diagnostics, root)) {
11508
+ process.exit(1);
10306
11509
  }
11510
+ return;
10307
11511
  }
10308
11512
  const hadErrors = reportDiagnostics(diagnostics, root);
10309
11513
  if (diagnostics.length === 0) {
@@ -10317,19 +11521,40 @@ var doctorCommand = defineCommand4({
10317
11521
 
10318
11522
  // src/cli/commands/eject.ts
10319
11523
  import { readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
10320
- import { defineCommand as defineCommand5 } from "citty";
10321
- import { join as join26, relative as relative14 } from "pathe";
11524
+ import { defineCommand as defineCommand6 } from "citty";
11525
+ import { join as join27, relative as relative15 } from "pathe";
10322
11526
 
10323
11527
  // src/registry/eject.ts
10324
- import { existsSync as existsSync13 } from "node:fs";
11528
+ import { existsSync as existsSync17 } from "node:fs";
10325
11529
  import { cp, mkdir as mkdir5, readFile as readFile14, rm as rm2, writeFile as writeFile7 } from "node:fs/promises";
10326
- import { join as join25, relative as relative13 } from "pathe";
11530
+ import { join as join26, relative as relative14 } from "pathe";
10327
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
+ };
10328
11553
  var eject = async (root) => {
10329
11554
  const project = await scanProject(root, { mode: "build" });
10330
11555
  const { context, config } = project;
10331
- const srcDir = join25(root, "src");
10332
- const genDir = join25(srcDir, "generated");
11556
+ const srcDir = join26(root, "src");
11557
+ const genDir = join26(srcDir, "generated");
10333
11558
  const askEnabled = config.ai.ask?.enabled ?? false;
10334
11559
  const exportPdf = config.export.pdf;
10335
11560
  const exportEpub = config.export.epub;
@@ -10350,13 +11575,13 @@ var eject = async (root) => {
10350
11575
  const needsSvelte = frameworks.has("svelte");
10351
11576
  const relContext = {
10352
11577
  ...context,
10353
- contentRoot: POSIX(relative13(root, context.contentRoot)),
11578
+ contentRoot: POSIX(relative14(root, context.contentRoot)),
10354
11579
  outDir: ".",
10355
11580
  root: "."
10356
11581
  };
10357
- const componentsImport = context.componentsFile ? `../../${POSIX(relative13(root, context.componentsFile))}` : null;
11582
+ const componentsImport = context.componentsFile ? `../../${POSIX(relative14(root, context.componentsFile))}` : null;
10358
11583
  const relPages = pages.map((page) => ({
10359
- entrypoint: POSIX(relative13(root, page.entrypoint)),
11584
+ entrypoint: POSIX(relative14(root, page.entrypoint)),
10360
11585
  pattern: page.pattern
10361
11586
  }));
10362
11587
  const staged = collectStaged(project);
@@ -10377,13 +11602,13 @@ var eject = async (root) => {
10377
11602
  searchClientPath: "./src/generated/search-client.ts",
10378
11603
  themePath: "./src/generated/app.css"
10379
11604
  }),
10380
- path: join25(root, "astro.config.mjs")
11605
+ path: join26(root, "astro.config.mjs")
10381
11606
  },
10382
11607
  {
10383
11608
  content: runtimeTsconfigTemplate(),
10384
- path: join25(root, "tsconfig.json")
11609
+ path: join26(root, "tsconfig.json")
10385
11610
  },
10386
- { content: envTemplate(), path: join25(srcDir, "env.d.ts") },
11611
+ { content: envTemplate(), path: join26(srcDir, "env.d.ts") },
10387
11612
  {
10388
11613
  content: contentConfigTemplate({
10389
11614
  config,
@@ -10391,28 +11616,29 @@ var eject = async (root) => {
10391
11616
  staged: hasStaged,
10392
11617
  stagedBase: stagedDir
10393
11618
  }),
10394
- path: join25(srcDir, "content.config.ts")
11619
+ path: join26(srcDir, "content.config.ts")
10395
11620
  },
10396
11621
  {
10397
11622
  content: catchAllPageTemplate({
10398
11623
  askEnabled,
10399
11624
  exportEpub,
10400
11625
  exportPdf,
10401
- mathEnabled: config.markdown.math
11626
+ mathEnabled: config.markdown.math,
11627
+ needsReact
10402
11628
  }),
10403
- path: join25(srcDir, "pages", "[...slug].astro")
11629
+ path: join26(srcDir, "pages", "[...slug].astro")
10404
11630
  },
10405
11631
  {
10406
- content: userComponentsTemplate(componentsImport),
10407
- path: join25(genDir, "components.ts")
11632
+ content: planComponentSlots(componentsImport, null).module,
11633
+ path: join26(genDir, "components.ts")
10408
11634
  },
10409
11635
  {
10410
11636
  content: islandMapTemplate(islands.islands),
10411
- path: join25(genDir, "islands.ts")
11637
+ path: join26(genDir, "islands.ts")
10412
11638
  },
10413
11639
  {
10414
11640
  content: exampleMapTemplate(examples.examples),
10415
- path: join25(genDir, "examples.ts")
11641
+ path: join26(genDir, "examples.ts")
10416
11642
  },
10417
11643
  {
10418
11644
  content: tailwindEntryTemplate({
@@ -10424,60 +11650,57 @@ var eject = async (root) => {
10424
11650
  twoslashCss: twoslashCss(),
10425
11651
  userTheme
10426
11652
  }),
10427
- path: join25(genDir, "app.css")
11653
+ path: join26(genDir, "app.css")
10428
11654
  },
10429
- { content: buildRuntimeData(project), path: join25(genDir, "data.json") },
11655
+ { content: buildRuntimeData(project), path: join26(genDir, "data.json") },
10430
11656
  {
10431
11657
  content: `${JSON.stringify(rawMarkdown)}
10432
11658
  `,
10433
- path: join25(genDir, "raw-markdown.json")
11659
+ path: join26(genDir, "raw-markdown.json")
10434
11660
  },
10435
11661
  {
10436
11662
  content: rawMarkdownEndpointTemplate(),
10437
- path: join25(srcDir, "pages", "[...slug].md.ts")
11663
+ path: join26(srcDir, "pages", "[...slug].md.ts")
10438
11664
  },
10439
11665
  {
10440
11666
  content: rawMarkdownEndpointTemplate(),
10441
- path: join25(srcDir, "pages", "[...slug].mdx.ts")
11667
+ path: join26(srcDir, "pages", "[...slug].mdx.ts")
10442
11668
  }
10443
11669
  ];
10444
11670
  if (askEnabled) {
10445
- files.push({
10446
- content: askEndpointTemplate(resolveAskBackend(config.ai.ask)),
10447
- path: join25(srcDir, "pages", "api", "ask.ts")
10448
- });
11671
+ files.push(...await askFiles(project, srcDir, genDir));
10449
11672
  }
10450
11673
  if (config.seo.og.enabled) {
10451
11674
  files.push({
10452
- content: ogEndpointTemplate(customOgRoutes(pages, config.title, config.description)),
10453
- path: join25(srcDir, "pages", "og", "[...slug].png.ts")
11675
+ content: ogEndpointTemplate(customOgRoutes(pages, config.title)),
11676
+ path: join26(srcDir, "pages", "og", "[...slug].png.ts")
10454
11677
  });
10455
11678
  }
10456
11679
  if (!routeIsTaken(pages, project.graph.pages, "/404")) {
10457
11680
  files.push({
10458
11681
  content: notFoundPageTemplate(),
10459
- path: join25(srcDir, "pages", "404.astro")
11682
+ path: join26(srcDir, "pages", "404.astro")
10460
11683
  });
10461
11684
  }
10462
11685
  files.push({
10463
11686
  content: searchClientTemplate(config),
10464
- path: join25(genDir, "search-client.ts")
11687
+ path: join26(genDir, "search-client.ts")
10465
11688
  });
10466
11689
  if (servesStaticIndex(config.search.provider)) {
10467
11690
  const documents = await buildSearchDocuments(project);
10468
11691
  files.push({
10469
11692
  content: `${JSON.stringify(documents)}
10470
11693
  `,
10471
- path: join25(genDir, "search.json")
11694
+ path: join26(genDir, "search.json")
10472
11695
  }, {
10473
11696
  content: searchEndpointTemplate(),
10474
- path: join25(srcDir, "pages", "blume-search.json.ts")
11697
+ path: join26(srcDir, "pages", "blume-search.json.ts")
10475
11698
  });
10476
11699
  }
10477
11700
  if (config.search.provider === "mixedbread") {
10478
11701
  files.push({
10479
11702
  content: mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""),
10480
- path: join25(srcDir, "pages", "api", "search.ts")
11703
+ path: join26(srcDir, "pages", "api", "search.ts")
10481
11704
  });
10482
11705
  }
10483
11706
  const feeds = buildRssFeeds(project);
@@ -10486,10 +11709,10 @@ var eject = async (root) => {
10486
11709
  files.push({
10487
11710
  content: `${JSON.stringify(feedXml)}
10488
11711
  `,
10489
- path: join25(genDir, "rss.json")
11712
+ path: join26(genDir, "rss.json")
10490
11713
  }, {
10491
11714
  content: rssEndpointTemplate(),
10492
- path: join25(srcDir, "pages", "[section]", "rss.xml.ts")
11715
+ path: join26(srcDir, "pages", "[section]", "rss.xml.ts")
10493
11716
  });
10494
11717
  }
10495
11718
  if (hasReferences(config)) {
@@ -10501,27 +11724,27 @@ var eject = async (root) => {
10501
11724
  for (const file of references.files) {
10502
11725
  files.push({
10503
11726
  content: file.content,
10504
- path: join25(srcDir, "pages", file.pagePath)
11727
+ path: join26(srcDir, "pages", file.pagePath)
10505
11728
  });
10506
11729
  }
10507
11730
  }
10508
11731
  files.push(...islands.islands.map((island) => ({
10509
11732
  content: islandWrapperTemplate(island),
10510
- path: join25(genDir, "islands", `${island.name}.astro`)
11733
+ path: join26(genDir, "islands", `${island.name}.astro`)
10511
11734
  })), ...examples.examples.map((example) => ({
10512
11735
  content: exampleWrapperTemplate(example),
10513
- path: join25(genDir, "examples", `${exampleSlug(example.path)}.astro`)
11736
+ path: join26(genDir, "examples", `${exampleSlug(example.path)}.astro`)
10514
11737
  })));
10515
11738
  for (const [entryId, content] of staged) {
10516
- files.push({ content, path: join25(root, stagedDir, entryId) });
11739
+ files.push({ content, path: join26(root, stagedDir, entryId) });
10517
11740
  }
10518
11741
  await Promise.all(files.map(async (file) => {
10519
- await mkdir5(join25(file.path, ".."), { recursive: true });
11742
+ await mkdir5(join26(file.path, ".."), { recursive: true });
10520
11743
  await writeFile7(file.path, file.content, "utf-8");
10521
11744
  }));
10522
- const assetsSrc = join25(context.outDir, "public", "blume-assets");
10523
- if (existsSync13(assetsSrc)) {
10524
- await cp(assetsSrc, join25(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"), {
10525
11748
  recursive: true
10526
11749
  });
10527
11750
  }
@@ -10531,7 +11754,7 @@ var eject = async (root) => {
10531
11754
 
10532
11755
  // src/cli/commands/eject.ts
10533
11756
  var updatePackageScripts = async (root) => {
10534
- const pkgPath = join26(root, "package.json");
11757
+ const pkgPath = join27(root, "package.json");
10535
11758
  let pkg;
10536
11759
  try {
10537
11760
  pkg = JSON.parse(await readFile15(pkgPath, "utf-8"));
@@ -10548,7 +11771,7 @@ var updatePackageScripts = async (root) => {
10548
11771
  await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
10549
11772
  `, "utf-8");
10550
11773
  };
10551
- var ejectCommand = defineCommand5({
11774
+ var ejectCommand = defineCommand6({
10552
11775
  args: {
10553
11776
  yes: { description: "Skip the confirmation prompt.", type: "boolean" }
10554
11777
  },
@@ -10567,7 +11790,7 @@ var ejectCommand = defineCommand5({
10567
11790
  await updatePackageScripts(root);
10568
11791
  logger.success(`Ejected ${files.length} file(s):`);
10569
11792
  for (const file of files) {
10570
- process.stdout.write(` ${relative14(root, file)}
11793
+ process.stdout.write(` ${relative15(root, file)}
10571
11794
  `);
10572
11795
  }
10573
11796
  logger.box(`Your project is now a standalone Astro app.
@@ -10580,10 +11803,10 @@ The blume package remains importable.`);
10580
11803
  });
10581
11804
 
10582
11805
  // src/cli/commands/init.ts
10583
- import { existsSync as existsSync14 } from "node:fs";
11806
+ import { existsSync as existsSync18 } from "node:fs";
10584
11807
  import { mkdir as mkdir6, writeFile as writeFile9 } from "node:fs/promises";
10585
- import { defineCommand as defineCommand6 } from "citty";
10586
- import { basename as basename4, dirname as dirname12, join as join27 } from "pathe";
11808
+ import { defineCommand as defineCommand7 } from "citty";
11809
+ import { basename as basename4, dirname as dirname13, join as join28 } from "pathe";
10587
11810
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
10588
11811
  var packageTemplate = (name, version) => `{
10589
11812
  "name": ${JSON.stringify(name)},
@@ -10599,41 +11822,130 @@ var packageTemplate = (name, version) => `{
10599
11822
  }
10600
11823
  }
10601
11824
  `;
10602
- 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";
10603
11828
 
10604
11829
  export default defineConfig({
10605
11830
  title: "My Docs",
10606
- description: "Documentation powered by Blume.",
11831
+ description: "Documentation powered by Blume.",${extra}
10607
11832
  });
10608
11833
  `;
10609
- var INDEX_TEMPLATE = `---
10610
- title: Introduction
10611
- 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
10612
11879
  ---
10613
11880
 
10614
- # 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
10615
11892
 
10616
11893
  Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
10617
11894
 
10618
- Edit \`docs/index.mdx\` to get started, then run \`blume dev\`.
10619
- `;
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
+ });
10620
11920
  var writeFileSafe = async (path, content) => {
10621
- if (existsSync14(path)) {
11921
+ if (existsSync18(path)) {
10622
11922
  logger.info(`Skipped existing ${path}`);
10623
11923
  return false;
10624
11924
  }
10625
- await mkdir6(dirname12(path), { recursive: true });
11925
+ await mkdir6(dirname13(path), { recursive: true });
10626
11926
  await writeFile9(path, content, "utf-8");
10627
11927
  logger.success(`Created ${path}`);
10628
11928
  return true;
10629
11929
  };
10630
- var initCommand = defineCommand6({
11930
+ var initCommand = defineCommand7({
10631
11931
  args: {
10632
11932
  "content-dir": {
10633
11933
  default: "docs",
10634
11934
  description: "Content directory.",
10635
11935
  type: "string"
10636
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
+ },
10637
11949
  yes: { description: "Skip prompts.", type: "boolean" }
10638
11950
  },
10639
11951
  meta: {
@@ -10643,34 +11955,65 @@ var initCommand = defineCommand6({
10643
11955
  async run({ args }) {
10644
11956
  const root = process.cwd();
10645
11957
  const contentDir = args["content-dir"] ?? "docs";
10646
- const createdPackage = await writeFileSafe(join27(root, "package.json"), packageTemplate(toPackageName(basename4(root)), getBlumeVersion()));
10647
- await writeFileSafe(join27(root, "blume.config.ts"), CONFIG_TEMPLATE);
10648
- await writeFileSafe(join27(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
+ }
10649
11992
  const nextSteps = createdPackage ? `Next steps:
10650
11993
 
10651
- npm install
10652
- blume dev
11994
+ ${commands.install}
11995
+ ${commands.dev}
10653
11996
  ` : `Next steps:
10654
11997
 
10655
- blume dev
11998
+ ${commands.dev}
10656
11999
  `;
10657
12000
  logger.box(nextSteps);
10658
12001
  }
10659
12002
  });
10660
12003
 
10661
12004
  // src/cli/commands/migrate.ts
10662
- import { defineCommand as defineCommand7 } from "citty";
12005
+ import { defineCommand as defineCommand8 } from "citty";
10663
12006
 
10664
12007
  // src/migrate/fumadocs/index.ts
10665
- import { existsSync as existsSync18 } from "node:fs";
12008
+ import { existsSync as existsSync22 } from "node:fs";
10666
12009
  import { mkdir as mkdir8, readFile as readFile17, rename as rename3, rm as rm3, writeFile as writeFile11 } from "node:fs/promises";
10667
- import { dirname as dirname15, join as join30, relative as relative15 } from "pathe";
12010
+ import { dirname as dirname16, join as join31, relative as relative16 } from "pathe";
10668
12011
  import { glob as glob8 } from "tinyglobby";
10669
12012
 
10670
12013
  // src/migrate/fumadocs/config.ts
10671
- import { existsSync as existsSync15 } from "node:fs";
12014
+ import { existsSync as existsSync19 } from "node:fs";
10672
12015
  import { readFile as readFile16 } from "node:fs/promises";
10673
- import { basename as basename5, dirname as dirname13, join as join28 } from "pathe";
12016
+ import { basename as basename5, dirname as dirname14, join as join29 } from "pathe";
10674
12017
  var SOURCE_FILES = [
10675
12018
  "lib/source.ts",
10676
12019
  "app/source.ts",
@@ -10700,10 +12043,10 @@ var GENERIC_NAMES = new Set([
10700
12043
  var gitRepoRoot = (start) => {
10701
12044
  let dir = start;
10702
12045
  for (;; ) {
10703
- if (existsSync15(join28(dir, ".git"))) {
12046
+ if (existsSync19(join29(dir, ".git"))) {
10704
12047
  return dir;
10705
12048
  }
10706
- const parent = dirname13(dir);
12049
+ const parent = dirname14(dir);
10707
12050
  if (parent === dir) {
10708
12051
  return null;
10709
12052
  }
@@ -10712,8 +12055,8 @@ var gitRepoRoot = (start) => {
10712
12055
  };
10713
12056
  var bareName = (name) => name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
10714
12057
  var readTitle = async (root) => {
10715
- const packageJson = join28(root, "package.json");
10716
- if (!existsSync15(packageJson)) {
12058
+ const packageJson = join29(root, "package.json");
12059
+ if (!existsSync19(packageJson)) {
10717
12060
  return "Documentation";
10718
12061
  }
10719
12062
  try {
@@ -10738,8 +12081,8 @@ var readTitle = async (root) => {
10738
12081
  };
10739
12082
  var scrapeBaseUrl = async (root) => {
10740
12083
  for (const candidate of SOURCE_FILES) {
10741
- const file = join28(root, candidate);
10742
- if (!existsSync15(file)) {
12084
+ const file = join29(root, candidate);
12085
+ if (!existsSync19(file)) {
10743
12086
  continue;
10744
12087
  }
10745
12088
  const base = BASE_URL.exec(await readFile16(file, "utf-8"))?.groups?.base;
@@ -10771,9 +12114,9 @@ var loadFumadocsConfig = async (root) => {
10771
12114
  };
10772
12115
 
10773
12116
  // src/migrate/fumadocs/content.ts
10774
- import { existsSync as existsSync16 } from "node:fs";
12117
+ import { existsSync as existsSync20 } from "node:fs";
10775
12118
  import { readFile as readFileFromDisk2 } from "node:fs/promises";
10776
- import { dirname as dirname14, resolve as resolve9 } from "pathe";
12119
+ import { dirname as dirname15, resolve as resolve10 } from "pathe";
10777
12120
  var FUMADOCS_IMPORT = /^import\s+[\s\S]*?\s+from\s+["']fumadocs-(?:ui|core|mdx)(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
10778
12121
  var stripFumadocsImports = (source) => {
10779
12122
  const stripped = source.replace(FUMADOCS_IMPORT, "");
@@ -10974,12 +12317,12 @@ var inlineFumadocsIncludes = async (source, options) => {
10974
12317
  if (!rawPath) {
10975
12318
  continue;
10976
12319
  }
10977
- const target = resolve9(dirname14(options.filePath), rawPath);
12320
+ const target = resolve10(dirname15(options.filePath), rawPath);
10978
12321
  if (seen.has(target)) {
10979
12322
  warnings.push(`Circular <include> "${rawPath}" — left as-is.`);
10980
12323
  continue;
10981
12324
  }
10982
- if (!existsSync16(target)) {
12325
+ if (!existsSync20(target)) {
10983
12326
  warnings.push(`<include> target "${rawPath}" not found — left as-is.`);
10984
12327
  continue;
10985
12328
  }
@@ -11011,9 +12354,9 @@ var normalizeFumadocsPageMeta = (value) => {
11011
12354
  };
11012
12355
 
11013
12356
  // src/migrate/fumadocs/groups.ts
11014
- import { existsSync as existsSync17, statSync as statSync2 } from "node:fs";
12357
+ import { existsSync as existsSync21, statSync as statSync2 } from "node:fs";
11015
12358
  import { mkdir as mkdir7, rename as rename2, writeFile as writeFile10 } from "node:fs/promises";
11016
- import { basename as basename6, join as join29 } from "pathe";
12359
+ import { basename as basename6, join as join30 } from "pathe";
11017
12360
 
11018
12361
  // src/migrate/fumadocs/meta.ts
11019
12362
  var SEPARATOR = /^---(?<label>.*)---$/u;
@@ -11159,12 +12502,12 @@ var isDirectory = (path) => {
11159
12502
  };
11160
12503
  var resolveEntry = (docsDir, name) => {
11161
12504
  for (const ext of PAGE_EXTS) {
11162
- const file = join29(docsDir, `${name}${ext}`);
11163
- if (existsSync17(file)) {
12505
+ const file = join30(docsDir, `${name}${ext}`);
12506
+ if (existsSync21(file)) {
11164
12507
  return { kind: "file", path: file };
11165
12508
  }
11166
12509
  }
11167
- const folder = join29(docsDir, name);
12510
+ const folder = join30(docsDir, name);
11168
12511
  return isDirectory(folder) ? { kind: "folder", path: folder } : null;
11169
12512
  };
11170
12513
  var humanize2 = (name) => name.split(WORD_SPLIT3).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
@@ -11199,8 +12542,8 @@ var moveItemIntoGroup = async (item, docsDir, groupDir, label, warnings) => {
11199
12542
  warnings.push(`Sidebar entry "${item.name}" in section "${label}" matched no page or folder; skipped.`);
11200
12543
  return null;
11201
12544
  }
11202
- const dest = join29(groupDir, basename6(resolved.path));
11203
- if (existsSync17(dest)) {
12545
+ const dest = join30(groupDir, basename6(resolved.path));
12546
+ if (existsSync21(dest)) {
11204
12547
  warnings.push(`Skipped moving "${item.name}" into section "${label}" (target already exists).`);
11205
12548
  return null;
11206
12549
  }
@@ -11227,7 +12570,7 @@ var reshapeSection = async (section, docsDir, order, warnings) => {
11227
12570
  }
11228
12571
  return;
11229
12572
  }
11230
- const groupDir = join29(docsDir, `(${section.label})`);
12573
+ const groupDir = join30(docsDir, `(${section.label})`);
11231
12574
  const sectionKeys = [];
11232
12575
  for (const item of section.items) {
11233
12576
  const key = await moveItemIntoGroup(item, docsDir, groupDir, section.label, warnings);
@@ -11240,7 +12583,7 @@ var reshapeSection = async (section, docsDir, order, warnings) => {
11240
12583
  }
11241
12584
  order.push(section.label);
11242
12585
  if (sectionKeys.length > 1) {
11243
- await writeFile10(join29(groupDir, "meta.ts"), renderMetaModule({ pages: sectionKeys }), "utf-8");
12586
+ await writeFile10(join30(groupDir, "meta.ts"), renderMetaModule({ pages: sectionKeys }), "utf-8");
11244
12587
  }
11245
12588
  };
11246
12589
  var reshapeFumadocsGroups = async (structure, docsDir) => {
@@ -11272,9 +12615,9 @@ var FUMADOCS_LEFTOVERS = [
11272
12615
  "app"
11273
12616
  ];
11274
12617
  var movePage = async (abs, base, root) => {
11275
- const rel = relative15(base, abs);
11276
- const dest = join30(root, "docs", rel);
11277
- if (existsSync18(dest)) {
12618
+ const rel = relative16(base, abs);
12619
+ const dest = join31(root, "docs", rel);
12620
+ if (existsSync22(dest)) {
11278
12621
  return {
11279
12622
  includeWarnings: [],
11280
12623
  moved: 0,
@@ -11293,7 +12636,7 @@ var movePage = async (abs, base, root) => {
11293
12636
  const parsed = frontmatter_default(text);
11294
12637
  const { data, removed } = normalizeFumadocsPageMeta(parsed.data);
11295
12638
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
11296
- await mkdir8(dirname15(dest), { recursive: true });
12639
+ await mkdir8(dirname16(dest), { recursive: true });
11297
12640
  await writeFile11(dest, content, "utf-8");
11298
12641
  await rm3(abs, { force: true });
11299
12642
  return {
@@ -11308,35 +12651,35 @@ var writeMeta = async (dest, meta, rel, warnings) => {
11308
12651
  if (Object.keys(meta).length === 0) {
11309
12652
  return warnings;
11310
12653
  }
11311
- if (existsSync18(dest)) {
12654
+ if (existsSync22(dest)) {
11312
12655
  return [...warnings, `Skipped ${rel} (target already exists)`];
11313
12656
  }
11314
- await mkdir8(dirname15(dest), { recursive: true });
12657
+ await mkdir8(dirname16(dest), { recursive: true });
11315
12658
  await writeFile11(dest, renderMetaModule(meta), "utf-8");
11316
12659
  return warnings;
11317
12660
  };
11318
12661
  var convertMeta = async (abs, base, root) => {
11319
- const rel = relative15(base, abs);
12662
+ const rel = relative16(base, abs);
11320
12663
  const raw = await readFile17(abs, "utf-8");
11321
12664
  let parsed;
11322
12665
  try {
11323
12666
  parsed = JSON.parse(raw);
11324
12667
  } catch {
11325
- const dest2 = join30(root, "docs", rel);
11326
- if (existsSync18(dest2)) {
12668
+ const dest2 = join31(root, "docs", rel);
12669
+ if (existsSync22(dest2)) {
11327
12670
  return [`Skipped ${rel} (target already exists)`];
11328
12671
  }
11329
- await mkdir8(dirname15(dest2), { recursive: true });
12672
+ await mkdir8(dirname16(dest2), { recursive: true });
11330
12673
  await rename3(abs, dest2);
11331
12674
  return [
11332
12675
  `Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`
11333
12676
  ];
11334
12677
  }
11335
- const dir = dirname15(rel) === "." ? "" : dirname15(rel);
11336
- const docsDir = join30(root, "docs", dir);
11337
- const dest = join30(docsDir, "meta.ts");
12678
+ const dir = dirname16(rel) === "." ? "" : dirname16(rel);
12679
+ const docsDir = join31(root, "docs", dir);
12680
+ const dest = join31(docsDir, "meta.ts");
11338
12681
  const structure = parseFumadocsPages(parsed.pages);
11339
- if (structure.hasSections && !existsSync18(dest)) {
12682
+ if (structure.hasSections && !existsSync22(dest)) {
11340
12683
  const self = translateFumadocsSelfMeta(parsed);
11341
12684
  const reshape = await reshapeFumadocsGroups(structure, docsDir);
11342
12685
  const meta2 = { ...self.meta };
@@ -11383,8 +12726,8 @@ var summarizePages = (results) => {
11383
12726
  };
11384
12727
  };
11385
12728
  var cleanupSourceDirs = async (root) => {
11386
- const docs = join30(root, "content", "docs");
11387
- if (existsSync18(docs)) {
12729
+ const docs = join31(root, "content", "docs");
12730
+ if (existsSync22(docs)) {
11388
12731
  const remaining = await glob8(["**/*"], { cwd: docs, dot: true });
11389
12732
  if (remaining.length > 0) {
11390
12733
  return [
@@ -11393,8 +12736,8 @@ var cleanupSourceDirs = async (root) => {
11393
12736
  }
11394
12737
  await rm3(docs, { force: true, recursive: true });
11395
12738
  }
11396
- const content = join30(root, "content");
11397
- if (existsSync18(content)) {
12739
+ const content = join31(root, "content");
12740
+ if (existsSync22(content)) {
11398
12741
  const remaining = await glob8(["**/*"], { cwd: content, dot: true });
11399
12742
  if (remaining.length === 0) {
11400
12743
  await rm3(content, { force: true, recursive: true });
@@ -11404,8 +12747,8 @@ var cleanupSourceDirs = async (root) => {
11404
12747
  };
11405
12748
  var migrateFumadocsProject = async (root) => {
11406
12749
  const { config, warnings: configWarnings } = await loadFumadocsConfig(root);
11407
- const base = join30(root, SOURCE_DIR);
11408
- if (!existsSync18(base)) {
12750
+ const base = join31(root, SOURCE_DIR);
12751
+ if (!existsSync22(base)) {
11409
12752
  await writeBlumeConfig(root, config);
11410
12753
  return {
11411
12754
  moved: 0,
@@ -11463,9 +12806,9 @@ var migrateFumadocsProject = async (root) => {
11463
12806
  };
11464
12807
 
11465
12808
  // src/migrate/mintlify/index.ts
11466
- import { existsSync as existsSync19 } from "node:fs";
12809
+ import { existsSync as existsSync23 } from "node:fs";
11467
12810
  import { mkdir as mkdir9, readFile as readFile18, rename as rename4, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
11468
- import { dirname as dirname16, join as join31 } from "pathe";
12811
+ import { dirname as dirname17, join as join32 } from "pathe";
11469
12812
  import { glob as glob9 } from "tinyglobby";
11470
12813
  var prune = (value) => {
11471
12814
  if (Array.isArray(value)) {
@@ -11495,7 +12838,7 @@ var writeBlumeConfig2 = async (root, config) => {
11495
12838
 
11496
12839
  export default defineConfig(${JSON.stringify(prune(config), null, 2)});
11497
12840
  `;
11498
- await writeFile12(join31(root, "blume.config.ts"), body, "utf-8");
12841
+ await writeFile12(join32(root, "blume.config.ts"), body, "utf-8");
11499
12842
  };
11500
12843
  var relocateAssets = async (root, refs) => {
11501
12844
  const segments = new Set;
@@ -11510,23 +12853,23 @@ var relocateAssets = async (root, refs) => {
11510
12853
  }
11511
12854
  const moved = [];
11512
12855
  for (const segment of segments) {
11513
- const source = join31(root, segment);
11514
- if (!existsSync19(source) || segment === "public") {
12856
+ const source = join32(root, segment);
12857
+ if (!existsSync23(source) || segment === "public") {
11515
12858
  continue;
11516
12859
  }
11517
- const dest = join31(root, "public", segment);
11518
- if (existsSync19(dest)) {
12860
+ const dest = join32(root, "public", segment);
12861
+ if (existsSync23(dest)) {
11519
12862
  continue;
11520
12863
  }
11521
- await mkdir9(join31(root, "public"), { recursive: true });
12864
+ await mkdir9(join32(root, "public"), { recursive: true });
11522
12865
  await rename4(source, dest);
11523
12866
  moved.push(segment);
11524
12867
  }
11525
12868
  return moved;
11526
12869
  };
11527
12870
  var cleanupSnippets = async (root, kept, warnings) => {
11528
- const dir = join31(root, "snippets");
11529
- if (!existsSync19(dir)) {
12871
+ const dir = join32(root, "snippets");
12872
+ if (!existsSync23(dir)) {
11530
12873
  return;
11531
12874
  }
11532
12875
  const markdown = await glob9(["**/*.{md,mdx}"], { absolute: true, cwd: dir });
@@ -11563,9 +12906,9 @@ var assetRefs = (config) => {
11563
12906
  };
11564
12907
  var migrateMintlifyProject = async (root) => {
11565
12908
  const warnings = [];
11566
- const configFile = existsSync19(join31(root, "docs.json")) ? join31(root, "docs.json") : join31(root, "mint.json");
12909
+ const configFile = existsSync23(join32(root, "docs.json")) ? join32(root, "docs.json") : join32(root, "mint.json");
11567
12910
  let config;
11568
- if (existsSync19(configFile)) {
12911
+ if (existsSync23(configFile)) {
11569
12912
  config = await loadMintlifyConfig(root, configFile);
11570
12913
  const spec = JSON.parse(await readFile18(configFile, "utf-8"));
11571
12914
  const i18n = mintlifyI18n(spec);
@@ -11605,7 +12948,7 @@ var migrateMintlifyProject = async (root) => {
11605
12948
  variables
11606
12949
  });
11607
12950
  if (result.content !== raw) {
11608
- await mkdir9(dirname16(file), { recursive: true });
12951
+ await mkdir9(dirname17(file), { recursive: true });
11609
12952
  await writeFile12(file, result.content, "utf-8");
11610
12953
  }
11611
12954
  for (const key of result.removed) {
@@ -11642,9 +12985,9 @@ var migrateMintlifyProject = async (root) => {
11642
12985
  };
11643
12986
 
11644
12987
  // src/migrate/nextra/index.ts
11645
- import { existsSync as existsSync20 } from "node:fs";
12988
+ import { existsSync as existsSync24 } from "node:fs";
11646
12989
  import { mkdir as mkdir10, readFile as readFile19, rename as rename5, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
11647
- import { basename as basename7, dirname as dirname17, extname as extname6, join as join32, relative as relative16 } from "pathe";
12990
+ import { basename as basename7, dirname as dirname18, extname as extname7, join as join33, relative as relative17 } from "pathe";
11648
12991
  import { glob as glob10 } from "tinyglobby";
11649
12992
 
11650
12993
  // src/migrate/nextra/content.ts
@@ -11874,13 +13217,13 @@ var indexFolders = (base, pageFiles, metaFiles) => {
11874
13217
  let current = normalizeDir(dir);
11875
13218
  allDirs.add(current);
11876
13219
  while (current !== "") {
11877
- current = normalizeDir(dirname17(current));
13220
+ current = normalizeDir(dirname18(current));
11878
13221
  allDirs.add(current);
11879
13222
  }
11880
13223
  };
11881
13224
  for (const abs of pageFiles) {
11882
- const rel = relative16(base, abs);
11883
- const dir = normalizeDir(dirname17(rel));
13225
+ const rel = relative17(base, abs);
13226
+ const dir = normalizeDir(dirname18(rel));
11884
13227
  const slug = basename7(rel).replace(/\.mdx?$/u, "");
11885
13228
  const folder = pagesByDir.get(dir) ?? new Map;
11886
13229
  folder.set(slug, abs);
@@ -11888,14 +13231,14 @@ var indexFolders = (base, pageFiles, metaFiles) => {
11888
13231
  registerDir(dir);
11889
13232
  }
11890
13233
  for (const abs of metaFiles) {
11891
- registerDir(normalizeDir(dirname17(relative16(base, abs))));
13234
+ registerDir(normalizeDir(dirname18(relative17(base, abs))));
11892
13235
  }
11893
13236
  const childDirs = new Map;
11894
13237
  for (const dir of allDirs) {
11895
13238
  if (dir === "") {
11896
13239
  continue;
11897
13240
  }
11898
- const parent = normalizeDir(dirname17(dir));
13241
+ const parent = normalizeDir(dirname18(dir));
11899
13242
  const children = childDirs.get(parent) ?? new Set;
11900
13243
  children.add(basename7(dir));
11901
13244
  childDirs.set(parent, children);
@@ -11913,7 +13256,7 @@ var planMetas = (metas, index) => {
11913
13256
  warnings: []
11914
13257
  };
11915
13258
  for (const meta of metas) {
11916
- const dir = normalizeDir(dirname17(meta.rel));
13259
+ const dir = normalizeDir(dirname18(meta.rel));
11917
13260
  const entries = parseNextraMeta(meta.raw, meta.ext);
11918
13261
  if (!entries) {
11919
13262
  plan.unparseableMetas.push({ abs: meta.abs, rel: meta.rel });
@@ -11926,7 +13269,7 @@ var planMetas = (metas, index) => {
11926
13269
  plan.metaByDir.set(dir, conversion.folderMeta);
11927
13270
  plan.consumedMetas.push(meta.abs);
11928
13271
  for (const [slug, title] of Object.entries(conversion.folderTitles)) {
11929
- plan.folderTitleByDir.set(normalizeDir(join32(dir, slug)), title);
13272
+ plan.folderTitleByDir.set(normalizeDir(join33(dir, slug)), title);
11930
13273
  }
11931
13274
  for (const [slug, label] of Object.entries(conversion.pageLabels)) {
11932
13275
  const pageAbs = index.pagesByDir.get(dir)?.get(slug);
@@ -11950,9 +13293,9 @@ var planMetas = (metas, index) => {
11950
13293
  return plan;
11951
13294
  };
11952
13295
  var movePage2 = async (abs, options) => {
11953
- const rel = relative16(options.base, abs);
11954
- const dest = join32(options.root, "docs", rel);
11955
- if (existsSync20(dest)) {
13296
+ const rel = relative17(options.base, abs);
13297
+ const dest = join33(options.root, "docs", rel);
13298
+ if (existsSync24(dest)) {
11956
13299
  return { moved: 0, removed: [], skipped: rel, unsupported: [] };
11957
13300
  }
11958
13301
  const raw = await readFile19(abs, "utf-8");
@@ -11961,7 +13304,7 @@ var movePage2 = async (abs, options) => {
11961
13304
  const parsed = frontmatter_default(text);
11962
13305
  const { data, removed } = normalizeNextraPageMeta(parsed.data, options.overrides.get(abs) ?? {});
11963
13306
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
11964
- await mkdir10(dirname17(dest), { recursive: true });
13307
+ await mkdir10(dirname18(dest), { recursive: true });
11965
13308
  await writeFile13(dest, content, "utf-8");
11966
13309
  await rm5(abs, { force: true });
11967
13310
  return { moved: 1, removed, skipped: null, unsupported };
@@ -12004,8 +13347,8 @@ var writeFolderMetas = async (root, plan) => {
12004
13347
  if (Object.keys(finalMeta).length === 0) {
12005
13348
  return;
12006
13349
  }
12007
- const dest = join32(root, "docs", dir, "meta.ts");
12008
- await mkdir10(dirname17(dest), { recursive: true });
13350
+ const dest = join33(root, "docs", dir, "meta.ts");
13351
+ await mkdir10(dirname18(dest), { recursive: true });
12009
13352
  await writeFile13(dest, `import { defineMeta } from "blume";
12010
13353
 
12011
13354
  export default defineMeta(${JSON.stringify(finalMeta, null, 2)});
@@ -12015,12 +13358,12 @@ export default defineMeta(${JSON.stringify(finalMeta, null, 2)});
12015
13358
  var relocateUnparseableMetas = async (root, metas) => {
12016
13359
  const warnings = [];
12017
13360
  await Promise.all(metas.map(async ({ abs, rel }) => {
12018
- const dest = join32(root, "docs", rel);
12019
- if (existsSync20(dest)) {
13361
+ const dest = join33(root, "docs", rel);
13362
+ if (existsSync24(dest)) {
12020
13363
  warnings.push(`Skipped ${rel} (target already exists)`);
12021
13364
  return;
12022
13365
  }
12023
- await mkdir10(dirname17(dest), { recursive: true });
13366
+ await mkdir10(dirname18(dest), { recursive: true });
12024
13367
  await rename5(abs, dest);
12025
13368
  warnings.push(`Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`);
12026
13369
  }));
@@ -12028,7 +13371,7 @@ var relocateUnparseableMetas = async (root, metas) => {
12028
13371
  };
12029
13372
  var buildConfig = (tabs) => tabs.length > 0 ? { navigation: { tabs }, title: "Documentation" } : { title: "Documentation" };
12030
13373
  var migrateNextraProject = async (root) => {
12031
- const sourceDir = SOURCE_DIRS.find((dir) => existsSync20(join32(root, dir)));
13374
+ const sourceDir = SOURCE_DIRS.find((dir) => existsSync24(join33(root, dir)));
12032
13375
  if (!sourceDir) {
12033
13376
  await writeBlumeConfig(root, { title: "Documentation" });
12034
13377
  return {
@@ -12038,7 +13381,7 @@ var migrateNextraProject = async (root) => {
12038
13381
  ]
12039
13382
  };
12040
13383
  }
12041
- const base = join32(root, sourceDir);
13384
+ const base = join33(root, sourceDir);
12042
13385
  const pageFiles = await glob10([PAGE_GLOB2], {
12043
13386
  absolute: true,
12044
13387
  cwd: base,
@@ -12052,9 +13395,9 @@ var migrateNextraProject = async (root) => {
12052
13395
  const index = indexFolders(base, pageFiles, metaFiles);
12053
13396
  const metas = await Promise.all(metaFiles.map(async (abs) => ({
12054
13397
  abs,
12055
- ext: extname6(abs),
13398
+ ext: extname7(abs),
12056
13399
  raw: await readFile19(abs, "utf-8"),
12057
- rel: relative16(base, abs)
13400
+ rel: relative17(base, abs)
12058
13401
  })));
12059
13402
  const plan = planMetas(metas, index);
12060
13403
  const moves = await Promise.all(pageFiles.map((abs) => movePage2(abs, { base, overrides: plan.pageOverrides, root })));
@@ -12079,15 +13422,15 @@ var migrateNextraProject = async (root) => {
12079
13422
  };
12080
13423
 
12081
13424
  // src/migrate/starlight/index.ts
12082
- import { existsSync as existsSync22 } from "node:fs";
13425
+ import { existsSync as existsSync26 } from "node:fs";
12083
13426
  import { readFile as readFile21, writeFile as writeFile14 } from "node:fs/promises";
12084
- import { join as join34 } from "pathe";
13427
+ import { join as join35 } from "pathe";
12085
13428
  import { glob as glob11 } from "tinyglobby";
12086
13429
 
12087
13430
  // src/migrate/starlight/config.ts
12088
- import { existsSync as existsSync21 } from "node:fs";
13431
+ import { existsSync as existsSync25 } from "node:fs";
12089
13432
  import { readFile as readFile20 } from "node:fs/promises";
12090
- import { join as join33 } from "pathe";
13433
+ import { join as join34 } from "pathe";
12091
13434
  var CONFIG_FILES = [
12092
13435
  "astro.config.mjs",
12093
13436
  "astro.config.mts",
@@ -12118,7 +13461,7 @@ var extractStarlightOptions = (source) => {
12118
13461
  return isLiteralObject(parsed) ? parsed : "unparseable";
12119
13462
  };
12120
13463
  var loadStarlightConfig = async (root) => {
12121
- const file = CONFIG_FILES.map((name) => join33(root, name)).find((path) => existsSync21(path));
13464
+ const file = CONFIG_FILES.map((name) => join34(root, name)).find((path) => existsSync25(path));
12122
13465
  if (!file) {
12123
13466
  return {
12124
13467
  options: {},
@@ -12361,9 +13704,6 @@ var mapStarlightConfig = (options, warnings) => {
12361
13704
  }
12362
13705
  }
12363
13706
  const social = mapSocial(options.social);
12364
- if (Object.keys(social.socials).length > 0) {
12365
- config.footer = { socials: social.socials };
12366
- }
12367
13707
  const github = mapEditLink(options.editLink) ?? social.github;
12368
13708
  if (github) {
12369
13709
  config.github = github;
@@ -12550,8 +13890,8 @@ var migrateStarlightProject = async (root) => {
12550
13890
  config.i18n = i18n;
12551
13891
  warnings.push(`Mapped ${i18n.locales.length} locale(s) to i18n (default: ${i18n.defaultLocale}); review the locale labels.`);
12552
13892
  }
12553
- const base = join34(root, CONTENT_DIR);
12554
- if (!existsSync22(base)) {
13893
+ const base = join35(root, CONTENT_DIR);
13894
+ if (!existsSync26(base)) {
12555
13895
  await writeBlumeConfig(root, config);
12556
13896
  return {
12557
13897
  moved: 0,
@@ -12605,7 +13945,7 @@ var migrators = {
12605
13945
  };
12606
13946
 
12607
13947
  // src/cli/commands/migrate.ts
12608
- var makeMigrateCommand = (source) => defineCommand7({
13948
+ var makeMigrateCommand = (source) => defineCommand8({
12609
13949
  meta: {
12610
13950
  description: `Migrate a ${source} project to Blume.`,
12611
13951
  name: source
@@ -12625,7 +13965,7 @@ var makeMigrateCommand = (source) => defineCommand7({
12625
13965
  logger.box("Review blume.config.ts and run `blume dev`.");
12626
13966
  }
12627
13967
  });
12628
- var migrateCommand = defineCommand7({
13968
+ var migrateCommand = defineCommand8({
12629
13969
  meta: {
12630
13970
  description: "Migrate from another docs tool to Blume.",
12631
13971
  name: "migrate"
@@ -12639,11 +13979,11 @@ var migrateCommand = defineCommand7({
12639
13979
  });
12640
13980
 
12641
13981
  // src/cli/commands/preview.ts
12642
- import { existsSync as existsSync23 } from "node:fs";
13982
+ import { existsSync as existsSync27 } from "node:fs";
12643
13983
  import { preview } from "astro";
12644
- import { defineCommand as defineCommand8 } from "citty";
12645
- import { join as join35 } from "pathe";
12646
- var previewCommand = defineCommand8({
13984
+ import { defineCommand as defineCommand9 } from "citty";
13985
+ import { join as join36 } from "pathe";
13986
+ var previewCommand = defineCommand9({
12647
13987
  args: {
12648
13988
  host: { description: "Network host to bind.", type: "string" },
12649
13989
  port: { description: "Port to listen on.", type: "string" }
@@ -12656,7 +13996,7 @@ var previewCommand = defineCommand8({
12656
13996
  const root = process.cwd();
12657
13997
  const { config } = await loadConfig(root);
12658
13998
  const context = resolveProjectContext(root, config);
12659
- if (!existsSync23(join35(context.outDir, "astro.config.mjs"))) {
13999
+ if (!existsSync27(join36(context.outDir, "astro.config.mjs"))) {
12660
14000
  logger.error("No build found. Run `blume build` first.");
12661
14001
  process.exit(1);
12662
14002
  }
@@ -12673,9 +14013,9 @@ var previewCommand = defineCommand8({
12673
14013
 
12674
14014
  // src/cli/commands/sync.ts
12675
14015
  import { rm as rm6 } from "node:fs/promises";
12676
- import { defineCommand as defineCommand9 } from "citty";
12677
- import { join as join36 } from "pathe";
12678
- var syncCommand = defineCommand9({
14016
+ import { defineCommand as defineCommand10 } from "citty";
14017
+ import { join as join37 } from "pathe";
14018
+ var syncCommand = defineCommand10({
12679
14019
  args: {
12680
14020
  force: {
12681
14021
  description: "Clear the source cache before refetching.",
@@ -12696,7 +14036,7 @@ var syncCommand = defineCommand9({
12696
14036
  if (args.force) {
12697
14037
  const { config } = await loadConfig(root);
12698
14038
  const context = resolveProjectContext(root, config);
12699
- await rm6(join36(context.outDir, "cache"), { force: true, recursive: true });
14039
+ await rm6(join37(context.outDir, "cache"), { force: true, recursive: true });
12700
14040
  logger.info("Cleared source cache.");
12701
14041
  }
12702
14042
  await prepareProject({
@@ -12711,13 +14051,13 @@ var syncCommand = defineCommand9({
12711
14051
  });
12712
14052
 
12713
14053
  // src/cli/commands/validate.ts
12714
- import { existsSync as existsSync25 } from "node:fs";
12715
- import { defineCommand as defineCommand10 } from "citty";
12716
- import { join as join38 } from "pathe";
14054
+ import { existsSync as existsSync29 } from "node:fs";
14055
+ import { defineCommand as defineCommand11 } from "citty";
14056
+ import { join as join39 } from "pathe";
12717
14057
 
12718
14058
  // src/core/links.ts
12719
- import { existsSync as existsSync24 } from "node:fs";
12720
- import { join as join37 } from "pathe";
14059
+ import { existsSync as existsSync28 } from "node:fs";
14060
+ import { join as join38 } from "pathe";
12721
14061
  var HTTP = /^https?:\/\//iu;
12722
14062
  var PROTOCOL_RELATIVE = /^\/\//u;
12723
14063
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -12756,8 +14096,8 @@ var toRoute = (path) => {
12756
14096
  };
12757
14097
  var buildAnchorIndex = (pages) => {
12758
14098
  const anchors = new Map;
12759
- for (const page of pages) {
12760
- 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)));
12761
14101
  }
12762
14102
  return anchors;
12763
14103
  };
@@ -12777,7 +14117,7 @@ var checkPathLink = (resolved, fragment, target, site, ctx) => {
12777
14117
  if (ctx.publicDir === null) {
12778
14118
  return "asset-unchecked";
12779
14119
  }
12780
- if (existsSync24(join37(ctx.publicDir, resolved))) {
14120
+ if (existsSync28(join38(ctx.publicDir, resolved))) {
12781
14121
  return null;
12782
14122
  }
12783
14123
  return {
@@ -12876,11 +14216,11 @@ var checkExternalLinks = async (refs) => {
12876
14216
  }
12877
14217
  return diagnostics;
12878
14218
  };
12879
- var classifyLink = (page, link, ctx, onExternal) => {
14219
+ var classifyLink = (page2, link, ctx, onExternal) => {
12880
14220
  const { target } = link;
12881
14221
  const site = {
12882
14222
  column: link.column,
12883
- file: page.sourcePath ?? page.id,
14223
+ file: page2.sourcePath ?? page2.id,
12884
14224
  line: link.line
12885
14225
  };
12886
14226
  if (HTTP.test(target) || PROTOCOL_RELATIVE.test(target)) {
@@ -12901,9 +14241,9 @@ var classifyLink = (page, link, ctx, onExternal) => {
12901
14241
  rawPath = rawPath.slice(0, queryIndex);
12902
14242
  }
12903
14243
  if (rawPath === "") {
12904
- return fragment ? checkAnchor(page.route, fragment, site, ctx) : null;
14244
+ return fragment ? checkAnchor(page2.route, fragment, site, ctx) : null;
12905
14245
  }
12906
- const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page.route, rawPath);
14246
+ const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page2.route, rawPath);
12907
14247
  return checkPathLink(resolved, fragment, target, site, ctx);
12908
14248
  };
12909
14249
  var validateLinks = async (graph, options) => {
@@ -12916,9 +14256,9 @@ var validateLinks = async (graph, options) => {
12916
14256
  const diagnostics = [];
12917
14257
  const external = [];
12918
14258
  let uncheckedAssets = 0;
12919
- for (const page of graph.pages) {
12920
- for (const link of page.links) {
12921
- 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));
12922
14262
  if (result === "asset-unchecked") {
12923
14263
  uncheckedAssets += 1;
12924
14264
  } else if (result) {
@@ -12940,12 +14280,16 @@ var validateLinks = async (graph, options) => {
12940
14280
  };
12941
14281
 
12942
14282
  // src/cli/commands/validate.ts
12943
- var validateCommand = defineCommand10({
14283
+ var validateCommand = defineCommand11({
12944
14284
  args: {
12945
14285
  external: {
12946
14286
  description: "Check external (HTTP) links over the network.",
12947
14287
  type: "boolean"
12948
14288
  },
14289
+ json: {
14290
+ description: "Emit diagnostics as JSON on stdout (for CI/editors).",
14291
+ type: "boolean"
14292
+ },
12949
14293
  strict: {
12950
14294
  description: "Treat warnings as errors.",
12951
14295
  type: "boolean"
@@ -12961,18 +14305,26 @@ var validateCommand = defineCommand10({
12961
14305
  try {
12962
14306
  const project = await scanProject(root, { mode: "build" });
12963
14307
  diagnostics.push(...project.diagnostics);
12964
- const publicDir = join38(root, "public");
14308
+ const publicDir = join39(root, "public");
12965
14309
  diagnostics.push(...await validateLinks(project.graph, {
12966
14310
  checkExternal: Boolean(args.external),
12967
- publicDir: existsSync25(publicDir) ? publicDir : null,
14311
+ publicDir: existsSync29(publicDir) ? publicDir : null,
12968
14312
  redirects: project.config.redirects
12969
14313
  }));
12970
14314
  } catch (error) {
12971
14315
  if (error instanceof BlumeError) {
12972
14316
  diagnostics.push(error.diagnostic);
12973
14317
  } else {
12974
- 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);
12975
14326
  }
14327
+ return;
12976
14328
  }
12977
14329
  const hadErrors = reportDiagnostics(diagnostics, root);
12978
14330
  if (diagnostics.length === 0) {
@@ -12985,7 +14337,7 @@ var validateCommand = defineCommand10({
12985
14337
  });
12986
14338
 
12987
14339
  // src/cli/index.ts
12988
- var main = defineCommand11({
14340
+ var main = defineCommand12({
12989
14341
  meta: {
12990
14342
  description: "Markdown-first documentation powered by Astro and Vite.",
12991
14343
  name: "blume",
@@ -12994,6 +14346,7 @@ var main = defineCommand11({
12994
14346
  subCommands: {
12995
14347
  add: addCommand,
12996
14348
  build: buildCommand,
14349
+ check: checkCommand,
12997
14350
  dev: devCommand,
12998
14351
  doctor: doctorCommand,
12999
14352
  eject: ejectCommand,
@@ -13005,7 +14358,15 @@ var main = defineCommand11({
13005
14358
  }
13006
14359
  });
13007
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
+ });
13008
14369
  runMain(main);
13009
14370
 
13010
- //# debugId=1F31417850B615D064756E2164756E21
14371
+ //# debugId=8EA79E97DE44177D64756E2164756E21
13011
14372
  //# sourceMappingURL=index.js.map