blume 0.3.0 → 0.4.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 (81) hide show
  1. package/dist/cli/index.js +747 -471
  2. package/dist/cli/index.js.map +45 -38
  3. package/dist/types/core/schema.d.ts +289 -278
  4. package/dist/types/migrate/mintlify/assets.d.ts +8 -0
  5. package/docs/01-quickstart.mdx +5 -16
  6. package/docs/02-deployment.mdx +21 -54
  7. package/docs/advanced/api-reference.mdx +10 -37
  8. package/docs/advanced/blog.mdx +9 -25
  9. package/docs/advanced/changelog.mdx +10 -33
  10. package/docs/advanced/custom-pages.mdx +21 -78
  11. package/docs/configuration/ai.mdx +42 -103
  12. package/docs/configuration/analytics.mdx +20 -38
  13. package/docs/configuration/customization.mdx +40 -73
  14. package/docs/configuration/export.mdx +9 -34
  15. package/docs/configuration/index.mdx +67 -87
  16. package/docs/configuration/search.mdx +17 -54
  17. package/docs/configuration/seo.mdx +17 -48
  18. package/docs/configuration/theming.mdx +20 -42
  19. package/docs/content/components.mdx +42 -101
  20. package/docs/content/i18n.mdx +21 -72
  21. package/docs/content/index.mdx +18 -48
  22. package/docs/content/islands.mdx +25 -52
  23. package/docs/content/meta.mdx +23 -50
  24. package/docs/content/navigation.mdx +23 -62
  25. package/docs/content/sources.mdx +20 -83
  26. package/docs/content/syntax.mdx +37 -105
  27. package/docs/index.mdx +11 -40
  28. package/docs/reference/cli.mdx +18 -29
  29. package/docs/reference/frontmatter.mdx +2 -5
  30. package/package.json +1 -1
  31. package/src/astro/integration.ts +26 -3
  32. package/src/astro/islands.ts +6 -2
  33. package/src/astro/markdown-negotiation.ts +17 -3
  34. package/src/astro/pages.ts +6 -1
  35. package/src/astro/static-assets.ts +117 -0
  36. package/src/astro/templates.ts +48 -26
  37. package/src/cli/args.ts +23 -0
  38. package/src/cli/commands/build.ts +23 -0
  39. package/src/cli/commands/dev.ts +11 -2
  40. package/src/cli/commands/doctor.ts +10 -1
  41. package/src/cli/commands/eject.ts +3 -1
  42. package/src/cli/commands/init.ts +21 -1
  43. package/src/cli/commands/preview.ts +2 -1
  44. package/src/cli/commands/validate.ts +12 -1
  45. package/src/cli/dev-lock.ts +84 -0
  46. package/src/cli/log.ts +11 -0
  47. package/src/components/BlumePage.astro +2 -0
  48. package/src/components/content/YouTube.astro +35 -0
  49. package/src/components/content/youtube.ts +46 -0
  50. package/src/components/islands/ask-ai.tsx +14 -14
  51. package/src/components/props.ts +3 -0
  52. package/src/core/assets.ts +31 -0
  53. package/src/core/bridge.ts +10 -0
  54. package/src/core/builtin-tags.ts +1 -0
  55. package/src/core/diagnostics.ts +6 -1
  56. package/src/core/gitignore.ts +30 -0
  57. package/src/core/links.ts +60 -19
  58. package/src/core/schema.ts +7 -0
  59. package/src/core/sources/mdx-remote.ts +54 -8
  60. package/src/core/sources/normalize.ts +6 -1
  61. package/src/core/sources/notion.ts +49 -5
  62. package/src/core/sources/sanity.ts +5 -1
  63. package/src/deploy/rss.ts +1 -8
  64. package/src/deploy/sitemap.ts +20 -1
  65. package/src/deploy/xml.ts +8 -0
  66. package/src/markdown/directives.ts +15 -7
  67. package/src/markdown/package-commands.ts +26 -4
  68. package/src/migrate/fumadocs/content.ts +14 -1
  69. package/src/migrate/fumadocs/groups.ts +7 -0
  70. package/src/migrate/fumadocs/index.ts +5 -2
  71. package/src/migrate/mintlify/assets.ts +46 -0
  72. package/src/migrate/mintlify/index.ts +53 -45
  73. package/src/migrate/shared.ts +12 -27
  74. package/src/og/card.ts +14 -2
  75. package/src/registry/eject.ts +13 -3
  76. package/src/registry/registry.ts +6 -0
  77. package/src/registry/rewrite-imports.ts +31 -19
  78. package/src/search/documents.ts +23 -5
  79. package/src/search/sync/algolia.ts +5 -1
  80. package/src/search/sync/typesense.ts +24 -16
  81. package/src/theme/palette.ts +26 -7
package/dist/cli/index.js CHANGED
@@ -209,6 +209,12 @@ var CONTENT_COMPONENTS = [
209
209
  file: "Prompt.astro",
210
210
  name: "prompt",
211
211
  tag: "Prompt"
212
+ },
213
+ {
214
+ description: "A responsive, privacy-friendly YouTube embed.",
215
+ file: "YouTube.astro",
216
+ name: "youtube",
217
+ tag: "YouTube"
212
218
  }
213
219
  ];
214
220
  var registry = [
@@ -254,18 +260,22 @@ var findItem = (name) => registry.find((item) => item.name === name);
254
260
 
255
261
  // src/registry/rewrite-imports.ts
256
262
  import { dirname as dirname2, relative, resolve } from "pathe";
257
- var RELATIVE_IMPORT = /(?<kw>\bfrom|\bimport)(?<gap>\s+)(?<quote>["'])(?<spec>\.[^"']*)\k<quote>/gu;
258
- var rewriteImports = (content, sourceFile, srcRoot) => content.replaceAll(RELATIVE_IMPORT, (match, kw, gap, quote, spec) => {
259
- const resolved = resolve(dirname2(sourceFile), spec);
260
- if (resolved === sourceFile) {
261
- return match;
262
- }
263
- const rel = relative(srcRoot, resolved);
264
- if (rel.startsWith("..")) {
265
- return match;
266
- }
267
- return `${kw}${gap}${quote}blume/${rel}${quote}`;
268
- });
263
+ var FROM_IMPORT = /(?<prefix>^[ \t]*(?:import|export)\b[^;]*?\bfrom[ \t]*)(?<quote>["'])(?<spec>\.[^"']*)\k<quote>/gmu;
264
+ var SIDE_EFFECT_IMPORT = /(?<prefix>^[ \t]*import[ \t]+)(?<quote>["'])(?<spec>\.[^"']*)\k<quote>/gmu;
265
+ var rewriteImports = (content, sourceFile, srcRoot) => {
266
+ const rewrite = (match, prefix, quote, spec) => {
267
+ const resolved = resolve(dirname2(sourceFile), spec);
268
+ if (resolved === sourceFile) {
269
+ return match;
270
+ }
271
+ const rel = relative(srcRoot, resolved);
272
+ if (rel.startsWith("..")) {
273
+ return match;
274
+ }
275
+ return `${prefix}${quote}blume/${rel}${quote}`;
276
+ };
277
+ return content.replaceAll(FROM_IMPORT, rewrite).replaceAll(SIDE_EFFECT_IMPORT, rewrite);
278
+ };
269
279
 
270
280
  // src/cli/log.ts
271
281
  import { consola } from "consola";
@@ -326,7 +336,7 @@ var locatePath = (source, path) => {
326
336
  if (typeof segment !== "string") {
327
337
  continue;
328
338
  }
329
- const matcher = new RegExp(`${escapeRegExp(segment)}\\s*[:=]`, "gu");
339
+ const matcher = new RegExp(`(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`, "gu");
330
340
  matcher.lastIndex = cursor;
331
341
  const match = matcher.exec(source);
332
342
  if (!match) {
@@ -408,6 +418,9 @@ var countBySeverity = (diagnostics) => {
408
418
 
409
419
  // src/cli/log.ts
410
420
  var logger = consola.withTag("blume");
421
+ var flushStdout = () => new Promise((resolve2) => {
422
+ process.stdout.write("", () => resolve2());
423
+ });
411
424
  var reportDiagnosticsJson = (diagnostics, root) => {
412
425
  const enriched = diagnostics.map((diagnostic) => {
413
426
  const withDocs = enrichDiagnostic(diagnostic);
@@ -500,11 +513,11 @@ Next steps:
500
513
  });
501
514
 
502
515
  // src/cli/commands/build.ts
503
- import { existsSync as existsSync15 } from "node:fs";
516
+ import { existsSync as existsSync16 } from "node:fs";
504
517
  import { readdir, stat, writeFile as writeFile6 } from "node:fs/promises";
505
518
  import { build } from "astro";
506
519
  import { defineCommand as defineCommand2 } from "citty";
507
- import { join as join24 } from "pathe";
520
+ import { join as join26 } from "pathe";
508
521
 
509
522
  // src/core/frontmatter.ts
510
523
  import baseMatter from "gray-matter";
@@ -699,14 +712,24 @@ var buildRobots = (project) => {
699
712
  `;
700
713
  };
701
714
 
715
+ // src/deploy/xml.ts
716
+ var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
717
+
702
718
  // src/deploy/sitemap.ts
719
+ var lastmodTag = (value) => {
720
+ if (!value) {
721
+ return "";
722
+ }
723
+ const date = new Date(value);
724
+ return Number.isNaN(date.getTime()) ? "" : `<lastmod>${date.toISOString().slice(0, 10)}</lastmod>`;
725
+ };
703
726
  var buildSitemap = (project) => {
704
727
  const { site } = project.config.deployment;
705
728
  if (!(site && project.config.seo.sitemap)) {
706
729
  return null;
707
730
  }
708
731
  const base = site.replace(/\/$/u, "");
709
- const urls = project.graph.pages.filter((page) => !(page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex)).map((page) => ` <url><loc>${base}${page.route}</loc></url>`).toSorted();
732
+ const urls = project.graph.pages.filter((page) => !(page.meta.draft || page.meta.sidebar.hidden || page.meta.seo.noindex)).map((page) => ` <url><loc>${escapeXml(encodeURI(`${base}${page.route}`))}</loc>${lastmodTag(page.lastModified)}</url>`).toSorted();
710
733
  return `<?xml version="1.0" encoding="UTF-8"?>
711
734
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
712
735
  ${urls.join(`
@@ -2580,7 +2603,19 @@ var LINK = /\[(?<text>[^\]]*)\]\([^)]*\)/gu;
2580
2603
  var HEADING_MARK = /^#{1,6}\s+/gmu;
2581
2604
  var MARKDOWN_PUNCT = /[*_~>]+/gu;
2582
2605
  var WHITESPACE = /\s+/gu;
2583
- var toPlainText = (markdown) => markdown.replaceAll(CODE_FENCE, " ").replaceAll(IMAGE, " ").replaceAll(LINK, "$<text>").replaceAll(HTML_OR_JSX, " ").replaceAll(INLINE_CODE, "$<code>").replaceAll(HEADING_MARK, "").replaceAll(MARKDOWN_PUNCT, " ").replaceAll(WHITESPACE, " ").trim();
2606
+ var toPlainText = (markdown) => {
2607
+ const withoutBlocks = markdown.replaceAll(CODE_FENCE, " ").replaceAll(IMAGE, " ").replaceAll(LINK, "$<text>");
2608
+ const pieces = [];
2609
+ let cursor = 0;
2610
+ for (const match of withoutBlocks.matchAll(INLINE_CODE)) {
2611
+ const start = match.index ?? 0;
2612
+ pieces.push(withoutBlocks.slice(cursor, start).replaceAll(HTML_OR_JSX, " "));
2613
+ pieces.push(match.groups?.code ?? "");
2614
+ cursor = start + match[0].length;
2615
+ }
2616
+ pieces.push(withoutBlocks.slice(cursor).replaceAll(HTML_OR_JSX, " "));
2617
+ return pieces.join("").replaceAll(HEADING_MARK, "").replaceAll(MARKDOWN_PUNCT, " ").replaceAll(WHITESPACE, " ").trim();
2618
+ };
2584
2619
  var buildCrumbIndex = (sidebar) => {
2585
2620
  const index = new Map;
2586
2621
  const walk = (nodes, trail) => {
@@ -2654,7 +2689,7 @@ var syncAlgolia = async (records, config) => {
2654
2689
  }
2655
2690
  const { algoliasearch } = await import("algoliasearch");
2656
2691
  const client = algoliasearch(config.appId, adminKey);
2657
- await client.saveObjects({
2692
+ await client.replaceAllObjects({
2658
2693
  indexName: config.indexName,
2659
2694
  objects: records.map((record) => ({ ...record, objectID: record._id }))
2660
2695
  });
@@ -2706,20 +2741,21 @@ var syncTypesense = async (records, config) => {
2706
2741
  }
2707
2742
  ]
2708
2743
  });
2709
- try {
2710
- await client.collections(config.collection).retrieve();
2711
- } catch {
2712
- await client.collections().create({
2713
- fields: [
2714
- { name: "title", type: "string" },
2715
- { name: "description", optional: true, type: "string" },
2716
- { name: "content", type: "string" },
2717
- { name: "url", type: "string" },
2718
- { facet: true, name: "tag", optional: true, type: "string" }
2719
- ],
2720
- name: config.collection
2721
- });
2722
- }
2744
+ const collection = client.collections(config.collection);
2745
+ const exists = await collection.retrieve().then(() => true).catch(() => false);
2746
+ if (exists) {
2747
+ await collection.delete();
2748
+ }
2749
+ await client.collections().create({
2750
+ fields: [
2751
+ { name: "title", type: "string" },
2752
+ { name: "description", optional: true, type: "string" },
2753
+ { name: "content", type: "string" },
2754
+ { name: "url", type: "string" },
2755
+ { facet: true, name: "tag", optional: true, type: "string" }
2756
+ ],
2757
+ name: config.collection
2758
+ });
2723
2759
  const documents = records.map((record) => ({
2724
2760
  content: record.content,
2725
2761
  description: record.description,
@@ -2753,8 +2789,58 @@ var syncSearchProvider = async (project, reporter) => {
2753
2789
  }
2754
2790
  };
2755
2791
 
2792
+ // src/cli/dev-lock.ts
2793
+ import {
2794
+ existsSync as existsSync3,
2795
+ mkdirSync,
2796
+ readFileSync as readFileSync2,
2797
+ rmSync,
2798
+ writeFileSync
2799
+ } from "node:fs";
2800
+ import { join as join6 } from "pathe";
2801
+ var lockPath = (outDir) => join6(outDir, "dev.lock");
2802
+ var isDevLocked = (outDir) => {
2803
+ const path = lockPath(outDir);
2804
+ if (!existsSync3(path)) {
2805
+ return false;
2806
+ }
2807
+ const pid = Number.parseInt(readFileSync2(path, "utf-8").trim(), 10);
2808
+ if (!(Number.isInteger(pid) && pid > 0)) {
2809
+ return false;
2810
+ }
2811
+ try {
2812
+ process.kill(pid, 0);
2813
+ return true;
2814
+ } catch {
2815
+ return false;
2816
+ }
2817
+ };
2818
+ var acquireDevLock = (outDir) => {
2819
+ const path = lockPath(outDir);
2820
+ mkdirSync(outDir, { recursive: true });
2821
+ writeFileSync(path, String(process.pid));
2822
+ let released = false;
2823
+ return () => {
2824
+ if (released) {
2825
+ return;
2826
+ }
2827
+ released = true;
2828
+ try {
2829
+ if (existsSync3(path) && readFileSync2(path, "utf-8").trim() === String(process.pid)) {
2830
+ rmSync(path, { force: true });
2831
+ }
2832
+ } catch {}
2833
+ };
2834
+ };
2835
+ var refuseIfDevRunning = (root, action) => {
2836
+ if (isDevLocked(join6(root, ".blume"))) {
2837
+ logger.error(`A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server first.`);
2838
+ process.exit(1);
2839
+ }
2840
+ };
2841
+
2756
2842
  // src/astro/generate.ts
2757
- import { existsSync as existsSync6, readFileSync as readFileSync5, realpathSync } from "node:fs";
2843
+ import { existsSync as existsSync7, readFileSync as readFileSync6, realpathSync } from "node:fs";
2758
2844
  import {
2759
2845
  lstat,
2760
2846
  mkdir as mkdir2,
@@ -2766,7 +2852,7 @@ import {
2766
2852
  } from "node:fs/promises";
2767
2853
  import { createRequire as createRequire4 } from "node:module";
2768
2854
  import { pathToFileURL as pathToFileURL2 } from "node:url";
2769
- import { basename as basename2, dirname as dirname7, join as join11, normalize, relative as relative6 } from "pathe";
2855
+ import { basename as basename2, dirname as dirname7, join as join13, normalize, relative as relative6 } from "pathe";
2770
2856
  import { glob as glob4 } from "tinyglobby";
2771
2857
 
2772
2858
  // src/ai/ask-data.ts
@@ -2971,7 +3057,8 @@ var BUILTIN_MDX_TAGS = new Set([
2971
3057
  "Tooltip",
2972
3058
  "Tree",
2973
3059
  "TypeTable",
2974
- "Visibility"
3060
+ "Visibility",
3061
+ "YouTube"
2975
3062
  ]);
2976
3063
 
2977
3064
  // src/core/component-diagnostics.ts
@@ -3000,7 +3087,7 @@ var validateUsedComponents = (pages, extraTags, registryNames) => {
3000
3087
  };
3001
3088
 
3002
3089
  // src/core/component-overrides.ts
3003
- import { existsSync as existsSync3 } from "node:fs";
3090
+ import { existsSync as existsSync4 } from "node:fs";
3004
3091
  import { dirname as dirname4, extname, isAbsolute, resolve as resolve2 } from "pathe";
3005
3092
  import ts from "typescript";
3006
3093
  var GROUPS = ["mdx", "layout", "islands"];
@@ -3089,7 +3176,7 @@ var findDefaultExportObject = (sourceFile) => {
3089
3176
  var probeExtension = (base) => {
3090
3177
  for (const extension of COMPONENT_EXTS) {
3091
3178
  const candidate = `${base}.${extension}`;
3092
- if (existsSync3(candidate)) {
3179
+ if (existsSync4(candidate)) {
3093
3180
  return candidate;
3094
3181
  }
3095
3182
  }
@@ -3621,10 +3708,10 @@ var validateNavStructure = (navigation, pages) => [
3621
3708
  ];
3622
3709
 
3623
3710
  // src/core/tsconfig-aliases.ts
3624
- import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "node:fs";
3711
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "node:fs";
3625
3712
  import { createRequire as createRequire2 } from "node:module";
3626
3713
  import { pathToFileURL } from "node:url";
3627
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "pathe";
3714
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "pathe";
3628
3715
  var stripJsonComments = (text) => {
3629
3716
  let out = "";
3630
3717
  let inString = false;
@@ -3687,7 +3774,7 @@ var resolveExtends = (spec, fromDir) => {
3687
3774
  return candidates.find(isFile) ?? null;
3688
3775
  }
3689
3776
  try {
3690
- const require_ = createRequire2(pathToFileURL(join6(fromDir, "_.js")).href);
3777
+ const require_ = createRequire2(pathToFileURL(join7(fromDir, "_.js")).href);
3691
3778
  for (const sub of [`${spec}/tsconfig.json`, spec]) {
3692
3779
  try {
3693
3780
  return require_.resolve(sub);
@@ -3697,11 +3784,11 @@ var resolveExtends = (spec, fromDir) => {
3697
3784
  return null;
3698
3785
  };
3699
3786
  var loadPaths = (file, seen) => {
3700
- if (seen.has(file) || !existsSync4(file)) {
3787
+ if (seen.has(file) || !existsSync5(file)) {
3701
3788
  return null;
3702
3789
  }
3703
3790
  seen.add(file);
3704
- const json = parseJsonc(readFileSync2(file, "utf-8"));
3791
+ const json = parseJsonc(readFileSync3(file, "utf-8"));
3705
3792
  if (!json) {
3706
3793
  return null;
3707
3794
  }
@@ -3739,7 +3826,7 @@ var toAlias = (key, value, baseDir) => {
3739
3826
  return { find, replacement: resolve3(baseDir, target) };
3740
3827
  };
3741
3828
  var resolveTsconfigAliases = (root) => {
3742
- const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join6(root, name)).find((file) => existsSync4(file));
3829
+ const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join7(root, name)).find((file) => existsSync5(file));
3743
3830
  if (!entry) {
3744
3831
  return {};
3745
3832
  }
@@ -3798,7 +3885,6 @@ var buildRssFeeds = (project) => {
3798
3885
  }
3799
3886
  return feeds;
3800
3887
  };
3801
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
3802
3888
  var renderItem = (item) => {
3803
3889
  const parts = [
3804
3890
  ` <title>${escapeXml(item.title)}</title>`,
@@ -3838,11 +3924,18 @@ ${items}
3838
3924
 
3839
3925
  // src/openapi/scalar.ts
3840
3926
  import { readFile as readFile4 } from "node:fs/promises";
3841
- import { isAbsolute as isAbsolute3, join as join8 } from "pathe";
3927
+ import { isAbsolute as isAbsolute3, join as join10 } from "pathe";
3842
3928
 
3843
3929
  // src/astro/templates.ts
3844
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
3845
- import { dirname as dirname6, join as join7 } from "pathe";
3930
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
3931
+ import { dirname as dirname6, join as join9 } from "pathe";
3932
+
3933
+ // src/core/assets.ts
3934
+ import { join as join8 } from "pathe";
3935
+ var resolveAssetMounts = (root, assets) => assets.map((entry) => {
3936
+ const rel = entry.replace(/^[./]+/u, "").replaceAll(/\.\.\/?/gu, "").replace(/\/+$/u, "");
3937
+ return { dir: join8(root, rel), url: `/${rel}` };
3938
+ });
3846
3939
 
3847
3940
  // src/theme/fonts.ts
3848
3941
  var FALLBACKS = {
@@ -4000,17 +4093,17 @@ var WORKSPACE_MARKERS = [
4000
4093
  "yarn.lock"
4001
4094
  ];
4002
4095
  var hasWorkspacesField = (pkgPath) => {
4003
- if (!existsSync5(pkgPath)) {
4096
+ if (!existsSync6(pkgPath)) {
4004
4097
  return false;
4005
4098
  }
4006
4099
  try {
4007
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
4100
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
4008
4101
  return pkg.workspaces !== undefined;
4009
4102
  } catch {
4010
4103
  return false;
4011
4104
  }
4012
4105
  };
4013
- var hasWorkspaceMarker = (dir) => hasWorkspacesField(join7(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync5(join7(dir, marker)));
4106
+ var hasWorkspaceMarker = (dir) => hasWorkspacesField(join9(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync6(join9(dir, marker)));
4014
4107
  var findWorkspaceRoot = (start) => {
4015
4108
  let dir = start;
4016
4109
  for (;; ) {
@@ -4144,7 +4237,8 @@ var astroConfigTemplate = (options) => {
4144
4237
  if (needsSvelte) {
4145
4238
  integrations.push("svelte()");
4146
4239
  }
4147
- integrations.push(`blumeIntegration(${JSON.stringify({ contentRoutes, pages })})`);
4240
+ const assets = resolveAssetMounts(context.root, config.content.assets);
4241
+ integrations.push(`blumeIntegration(${JSON.stringify({ assets, base: deployment.base, contentRoutes, pages })})`);
4148
4242
  return `// Generated by Blume. Do not edit; this file is recreated on each run.
4149
4243
  ${defineConfigImport}
4150
4244
  import mdx from "@astrojs/mdx";
@@ -4200,7 +4294,7 @@ export default defineConfig({
4200
4294
  });
4201
4295
  `;
4202
4296
  };
4203
- var stagedContentDir = (outDir) => join7(outDir, "content");
4297
+ var stagedContentDir = (outDir) => join9(outDir, "content");
4204
4298
  var contentConfigTemplate = (options) => {
4205
4299
  const { context, config } = options;
4206
4300
  const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
@@ -4257,31 +4351,44 @@ const provider = createOpenAICompatible({
4257
4351
  modelExpr = `provider(${JSON.stringify(backend.model)})`;
4258
4352
  }
4259
4353
  if (grounded) {
4260
- imports.push('import { createAskContext } from "blume/ai/ask-context.ts";', 'import askData from "../generated/ask-data.json";');
4354
+ imports.push('import { createAskContext } from "blume/ai/ask-context.ts";', 'import askData from "../../generated/ask-data.json";');
4261
4355
  setup += `
4262
4356
  const ground = createAskContext(askData);
4263
4357
  `;
4264
4358
  }
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 }) => {
4277
- const { messages } = await request.json();
4278
- const result = streamText({
4279
- model: ${modelExpr},
4280
- system:
4281
- "You are a helpful documentation assistant. Answer using the project's documentation.",
4282
- messages,
4283
- });
4284
- return result.toTextStreamResponse();
4359
+ const validate = ` const body = await request.json().catch(() => null);
4360
+ const messages = body?.messages;
4361
+ if (
4362
+ !Array.isArray(messages) ||
4363
+ messages.length === 0 ||
4364
+ messages.length > 40 ||
4365
+ JSON.stringify(messages).length > 24_000
4366
+ ) {
4367
+ return new Response("Invalid request: send 1-40 messages.", {
4368
+ status: 400,
4369
+ });
4370
+ }`;
4371
+ const stream = grounded ? ` const system =
4372
+ (await ground(messages, body.page)) ??
4373
+ "You are a helpful documentation assistant. Answer using the project's documentation.";
4374
+ const result = streamText({
4375
+ model: ${modelExpr},
4376
+ system,
4377
+ messages,
4378
+ });` : ` const result = streamText({
4379
+ model: ${modelExpr},
4380
+ system:
4381
+ "You are a helpful documentation assistant. Answer using the project's documentation.",
4382
+ messages,
4383
+ });`;
4384
+ const handler = `export const POST: APIRoute = async ({ request }) => {
4385
+ ${validate}
4386
+ try {
4387
+ ${stream}
4388
+ return result.toTextStreamResponse();
4389
+ } catch {
4390
+ return new Response("Failed to generate a response.", { status: 500 });
4391
+ }
4285
4392
  };`;
4286
4393
  return `// Generated by Blume. Do not edit.
4287
4394
  ${imports.join(`
@@ -4605,6 +4712,7 @@ import TreeFile from "blume/components/content/TreeFile.astro";
4605
4712
  import TreeFolder from "blume/components/content/TreeFolder.astro";
4606
4713
  import TypeTable from "blume/components/content/TypeTable.astro";
4607
4714
  import Visibility from "blume/components/content/Visibility.astro";
4715
+ import YouTube from "blume/components/content/YouTube.astro";
4608
4716
  import Icon from "blume/components/Icon.astro";
4609
4717
  ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
4610
4718
  import { islandComponents } from "../generated/islands.ts";
@@ -4650,6 +4758,7 @@ const components = {
4650
4758
  Tree,
4651
4759
  TypeTable,
4652
4760
  Visibility,
4761
+ YouTube,
4653
4762
  ${mathEntry}...islandComponents,
4654
4763
  ...userMdx,
4655
4764
  };
@@ -5061,8 +5170,9 @@ var runtimeTsconfigTemplate = () => `${JSON.stringify({
5061
5170
  `;
5062
5171
 
5063
5172
  // src/theme/palette.ts
5173
+ var FALLBACK_ACCENT = "oklch(0.62 0.16 250)";
5064
5174
  var ACCENTS = {
5065
- blue: "oklch(0.62 0.16 250)",
5175
+ blue: FALLBACK_ACCENT,
5066
5176
  green: "oklch(0.6 0.16 150)",
5067
5177
  orange: "oklch(0.68 0.17 50)",
5068
5178
  pink: "oklch(0.65 0.2 350)",
@@ -5070,6 +5180,9 @@ var ACCENTS = {
5070
5180
  red: "oklch(0.58 0.22 25)",
5071
5181
  teal: "oklch(0.6 0.12 195)"
5072
5182
  };
5183
+ var CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
5184
+ var safeColor = (value, fallback) => CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
5185
+ var safeColorOrNull = (value) => value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
5073
5186
  var RADII = {
5074
5187
  lg: "0.75rem",
5075
5188
  md: "0.5rem",
@@ -5104,7 +5217,7 @@ var themeRootCss = (theme, options) => [
5104
5217
  ` --blume-accent: ${options.accent};`,
5105
5218
  ...cssToken("--blume-action", options.action),
5106
5219
  ...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
5107
- ...cssToken("--blume-background", theme.background),
5220
+ ...cssToken("--blume-background", safeColorOrNull(theme.background)),
5108
5221
  ...cssToken("--blume-background-image", theme.backgroundImage ? backgroundImageCss(theme.backgroundImage) : null),
5109
5222
  options.backgroundDecoration.trimEnd(),
5110
5223
  ` --blume-radius: ${options.radius};`
@@ -5113,7 +5226,7 @@ var themeRootCss = (theme, options) => [
5113
5226
  var themeDarkCss = (theme, accentDark) => {
5114
5227
  const tokens = [
5115
5228
  ...cssToken("--blume-accent", accentDark),
5116
- ...cssToken("--blume-background", theme.backgroundDark),
5229
+ ...cssToken("--blume-background", safeColorOrNull(theme.backgroundDark)),
5117
5230
  ...cssToken("--blume-background-image", theme.backgroundImageDark ? backgroundImageCss(theme.backgroundImageDark) : null)
5118
5231
  ];
5119
5232
  if (tokens.length === 0) {
@@ -5125,12 +5238,12 @@ ${tokens.join(`
5125
5238
  }
5126
5239
  `;
5127
5240
  };
5128
- var resolveAccent = (theme) => ACCENTS[theme.accent] ?? theme.accent;
5241
+ var resolveAccent = (theme) => ACCENTS[theme.accent] ?? safeColor(theme.accent, FALLBACK_ACCENT);
5129
5242
  var resolveRadius = (theme) => RADII[theme.radius];
5130
5243
  var buildThemeCss = (theme) => {
5131
- const accent = ACCENTS[theme.accent] ?? theme.accent;
5132
- const accentDark = theme.accentDark ? ACCENTS[theme.accentDark] ?? theme.accentDark : null;
5133
- const action = theme.action ? ACCENTS[theme.action] ?? theme.action : null;
5244
+ const accent = ACCENTS[theme.accent] ?? safeColor(theme.accent, FALLBACK_ACCENT);
5245
+ const accentDark = theme.accentDark ? ACCENTS[theme.accentDark] ?? safeColor(theme.accentDark, FALLBACK_ACCENT) : null;
5246
+ const action = theme.action ? ACCENTS[theme.action] ?? safeColor(theme.action, FALLBACK_ACCENT) : null;
5134
5247
  const backgroundDecoration = backgroundDecorationCss(theme.backgroundDecoration);
5135
5248
  const radius = RADII[theme.radius];
5136
5249
  const root = themeRootCss(theme, {
@@ -5224,7 +5337,7 @@ var specConfiguration = async (spec, root) => {
5224
5337
  if (URL_SPEC.test(spec)) {
5225
5338
  return { config: { url: spec } };
5226
5339
  }
5227
- const absolute = isAbsolute3(spec) ? spec : join8(root, spec);
5340
+ const absolute = isAbsolute3(spec) ? spec : join10(root, spec);
5228
5341
  try {
5229
5342
  return { config: { content: await readFile4(absolute, "utf-8") } };
5230
5343
  } catch {
@@ -5897,7 +6010,7 @@ ${options.userTheme}
5897
6010
  `;
5898
6011
 
5899
6012
  // src/theme/twoslash.ts
5900
- import { readFileSync as readFileSync4 } from "node:fs";
6013
+ import { readFileSync as readFileSync5 } from "node:fs";
5901
6014
  import { createRequire as createRequire3 } from "node:module";
5902
6015
  var require2 = createRequire3(import.meta.url);
5903
6016
  var OVERRIDES = `
@@ -5959,7 +6072,7 @@ var OVERRIDES = `
5959
6072
  `;
5960
6073
  var twoslashCss = () => {
5961
6074
  const file = require2.resolve("@shikijs/twoslash/style-rich.css");
5962
- return `${readFileSync4(file, "utf-8")}
6075
+ return `${readFileSync5(file, "utf-8")}
5963
6076
  ${OVERRIDES}`;
5964
6077
  };
5965
6078
 
@@ -6062,12 +6175,12 @@ export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.leng
6062
6175
 
6063
6176
  // src/astro/examples.ts
6064
6177
  import { readFile as readFile6 } from "node:fs/promises";
6065
- import { join as join10, relative as relative4 } from "pathe";
6178
+ import { join as join12, relative as relative4 } from "pathe";
6066
6179
  import { glob as glob2 } from "tinyglobby";
6067
6180
 
6068
6181
  // src/astro/islands.ts
6069
6182
  import { readFile as readFile5 } from "node:fs/promises";
6070
- import { basename, join as join9 } from "pathe";
6183
+ import { basename, join as join11 } from "pathe";
6071
6184
  import { glob } from "tinyglobby";
6072
6185
  var DEFAULT_CLIENT = "visible";
6073
6186
  var VALID_MODES = new Set([
@@ -6096,7 +6209,7 @@ var readClientMode = (source, file, warnings) => {
6096
6209
  return mode;
6097
6210
  };
6098
6211
  var discoverIslands = async (root) => {
6099
- const dir = join9(root, "islands");
6212
+ const dir = join11(root, "islands");
6100
6213
  const matches = await glob(["**/*.{jsx,svelte,tsx,vue}"], {
6101
6214
  absolute: true,
6102
6215
  cwd: dir,
@@ -6115,8 +6228,8 @@ var discoverIslands = async (root) => {
6115
6228
  continue;
6116
6229
  }
6117
6230
  const name = base.replace(ISLAND_FILE, "");
6118
- if (!/^[A-Z]/u.test(name)) {
6119
- warnings.push(`Island "${file}" must have a PascalCase filename to be used in MDX (e.g. Counter.tsx → <Counter />); skipping it.`);
6231
+ if (!/^[A-Z][A-Za-z0-9_]*$/u.test(name)) {
6232
+ warnings.push(`Island "${file}" must have a PascalCase identifier filename to be used in MDX (letters, digits, and underscores only, e.g. Counter.tsx → <Counter />); skipping it.`);
6120
6233
  continue;
6121
6234
  }
6122
6235
  const existing = seen.get(name);
@@ -6159,7 +6272,7 @@ var splitGlobBase = (pattern) => {
6159
6272
  };
6160
6273
  var discoverExamples = async (root, pattern = "examples") => {
6161
6274
  const { base, rest } = GLOB_MAGIC.test(pattern) ? splitGlobBase(pattern) : { base: pattern, rest: DEFAULT_EXAMPLE_GLOB };
6162
- const dir = join10(root, base);
6275
+ const dir = join12(root, base);
6163
6276
  const matches = await glob2([rest], {
6164
6277
  absolute: true,
6165
6278
  cwd: dir,
@@ -6209,7 +6322,10 @@ var discoverPages = async (pagesRoot) => {
6209
6322
  return files.map((file) => {
6210
6323
  const rel = relative5(pagesRoot, file);
6211
6324
  const withoutExt = rel.slice(0, rel.length - extname2(rel).length);
6212
- const parts = withoutExt.split("/").filter((part) => part !== "index");
6325
+ const parts = withoutExt.split("/");
6326
+ if (parts.at(-1) === "index") {
6327
+ parts.pop();
6328
+ }
6213
6329
  const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
6214
6330
  return { entrypoint: file, pattern };
6215
6331
  });
@@ -6237,10 +6353,10 @@ var customOgRoutes = (pages, siteTitle) => {
6237
6353
  };
6238
6354
 
6239
6355
  // src/astro/generate.ts
6240
- var BLUME_SRC = join11(packageRoot(), "src");
6356
+ var BLUME_SRC = join13(packageRoot(), "src");
6241
6357
  var canResolveFrom = (fromDir, spec) => {
6242
6358
  try {
6243
- createRequire4(pathToFileURL2(join11(fromDir, "_.js")).href).resolve(spec);
6359
+ createRequire4(pathToFileURL2(join13(fromDir, "_.js")).href).resolve(spec);
6244
6360
  return true;
6245
6361
  } catch {
6246
6362
  return false;
@@ -6248,15 +6364,15 @@ var canResolveFrom = (fromDir, spec) => {
6248
6364
  };
6249
6365
  var resolvedAstroPath = (fromDir) => {
6250
6366
  try {
6251
- const pkg = createRequire4(pathToFileURL2(join11(fromDir, "_.js")).href).resolve("astro/package.json");
6367
+ const pkg = createRequire4(pathToFileURL2(join13(fromDir, "_.js")).href).resolve("astro/package.json");
6252
6368
  return realpathSync(pkg);
6253
6369
  } catch {
6254
6370
  return null;
6255
6371
  }
6256
6372
  };
6257
6373
  var blumeDepsDir = (pkgDir = packageRoot()) => {
6258
- const candidates = [join11(pkgDir, "node_modules"), dirname7(pkgDir)];
6259
- return candidates.find((dir) => existsSync6(join11(dir, "astro"))) ?? null;
6374
+ const candidates = [join13(pkgDir, "node_modules"), dirname7(pkgDir)];
6375
+ return candidates.find((dir) => existsSync7(join13(dir, "astro"))) ?? null;
6260
6376
  };
6261
6377
  var linkDepsJunction = async (link, depsDir) => {
6262
6378
  const existing = await lstat(link).catch(() => null);
@@ -6274,7 +6390,7 @@ var readPkgVersion = (pkgJsonPath) => {
6274
6390
  return null;
6275
6391
  }
6276
6392
  try {
6277
- return JSON.parse(readFileSync5(pkgJsonPath, "utf-8")).version ?? null;
6393
+ return JSON.parse(readFileSync6(pkgJsonPath, "utf-8")).version ?? null;
6278
6394
  } catch {
6279
6395
  return null;
6280
6396
  }
@@ -6296,8 +6412,8 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
6296
6412
  if (blumeAstro && outDirAstro === blumeAstro) {
6297
6413
  return null;
6298
6414
  }
6299
- if (existsSync6(join11(depsDir, "@astrojs", "mdx"))) {
6300
- await linkDepsJunction(join11(outDir, "node_modules"), depsDir);
6415
+ if (existsSync7(join13(depsDir, "@astrojs", "mdx"))) {
6416
+ await linkDepsJunction(join13(outDir, "node_modules"), depsDir);
6301
6417
  return null;
6302
6418
  }
6303
6419
  return astroConflictWarning(blumeAstro, outDirAstro);
@@ -6376,11 +6492,11 @@ var writeStagedContent = async (out, staged) => {
6376
6492
  const contentDir = stagedContentDir(out);
6377
6493
  const written = new Set;
6378
6494
  await Promise.all([...staged].map(async ([entryId, text]) => {
6379
- const path = join11(contentDir, entryId);
6495
+ const path = join13(contentDir, entryId);
6380
6496
  written.add(normalize(path));
6381
6497
  await writeIfChanged(path, text);
6382
6498
  }));
6383
- if (existsSync6(contentDir)) {
6499
+ if (existsSync7(contentDir)) {
6384
6500
  await pruneOrphans(contentDir, written);
6385
6501
  }
6386
6502
  };
@@ -6397,11 +6513,11 @@ var resolveLogo = (project) => {
6397
6513
  if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
6398
6514
  const rel = light.replace(/^\//u, "");
6399
6515
  const file = [
6400
- join11(project.context.root, "public", rel),
6401
- join11(project.context.root, rel)
6402
- ].find((path) => existsSync6(path));
6516
+ join13(project.context.root, "public", rel),
6517
+ join13(project.context.root, rel)
6518
+ ].find((path) => existsSync7(path));
6403
6519
  if (file) {
6404
- return { alt, href, svg: readFileSync5(file, "utf-8") };
6520
+ return { alt, href, svg: readFileSync6(file, "utf-8") };
6405
6521
  }
6406
6522
  }
6407
6523
  return { alt, dark, href, light };
@@ -6425,9 +6541,9 @@ var faviconType = (name) => {
6425
6541
  const ext = name.split(".").pop()?.toLowerCase();
6426
6542
  return ext ? FAVICON_TYPES[ext] : undefined;
6427
6543
  };
6428
- var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync5(file).toString("base64")}`;
6544
+ var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync6(file).toString("base64")}`;
6429
6545
  var defaultFavicon = () => ({
6430
- href: inlineDataUri(join11(BLUME_SRC, "assets", "icon.png"), "image/png"),
6546
+ href: inlineDataUri(join13(BLUME_SRC, "assets", "icon.png"), "image/png"),
6431
6547
  type: "image/png"
6432
6548
  });
6433
6549
  var APPLE_ICON_CANDIDATES = [
@@ -6439,13 +6555,13 @@ var APPLE_ICON_CANDIDATES = [
6439
6555
  var resolveIconFile = (project, candidates) => {
6440
6556
  const { root } = project.context;
6441
6557
  for (const name of candidates) {
6442
- if (existsSync6(join11(root, "public", name))) {
6558
+ if (existsSync7(join13(root, "public", name))) {
6443
6559
  return { href: `/${name}`, type: faviconType(name) };
6444
6560
  }
6445
6561
  }
6446
6562
  for (const name of candidates) {
6447
- const file = join11(root, name);
6448
- if (existsSync6(file)) {
6563
+ const file = join13(root, name);
6564
+ if (existsSync7(file)) {
6449
6565
  const type = faviconType(name);
6450
6566
  return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
6451
6567
  }
@@ -6573,7 +6689,7 @@ var buildRuntimeData = (project) => {
6573
6689
  var planMcp = (project, srcDir) => {
6574
6690
  const { config } = project;
6575
6691
  const { route } = config.mcp;
6576
- const dir = join11(srcDir, "blume-mcp");
6692
+ const dir = join13(srcDir, "blume-mcp");
6577
6693
  const base = {
6578
6694
  dir,
6579
6695
  discoveryPages: [],
@@ -6597,11 +6713,11 @@ var planMcp = (project, srcDir) => {
6597
6713
  ...base,
6598
6714
  discoveryPages: [
6599
6715
  {
6600
- entrypoint: join11(dir, "discovery.ts"),
6716
+ entrypoint: join13(dir, "discovery.ts"),
6601
6717
  pattern: "/.well-known/mcp.json"
6602
6718
  },
6603
6719
  {
6604
- entrypoint: join11(dir, "server-card.ts"),
6720
+ entrypoint: join13(dir, "server-card.ts"),
6605
6721
  pattern: "/.well-known/mcp/server-card.json"
6606
6722
  }
6607
6723
  ],
@@ -6620,11 +6736,11 @@ var writeMcpFiles = async (project, plan, write) => {
6620
6736
  version: data.version
6621
6737
  };
6622
6738
  await Promise.all([
6623
- write(join11(plan.srcDir, "generated", "mcp-data.json"), `${JSON.stringify(data)}
6739
+ write(join13(plan.srcDir, "generated", "mcp-data.json"), `${JSON.stringify(data)}
6624
6740
  `),
6625
- write(join11(plan.srcDir, "pages", mcpPageFile(plan.route)), mcpEndpointTemplate(plan.route)),
6626
- write(join11(plan.dir, "discovery.ts"), staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput))),
6627
- write(join11(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
6741
+ write(join13(plan.srcDir, "pages", mcpPageFile(plan.route)), mcpEndpointTemplate(plan.route)),
6742
+ write(join13(plan.dir, "discovery.ts"), staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput))),
6743
+ write(join13(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
6628
6744
  ]);
6629
6745
  };
6630
6746
  var writeAskFiles = async (project, srcDir, write) => {
@@ -6634,16 +6750,16 @@ var writeAskFiles = async (project, srcDir, write) => {
6634
6750
  }
6635
6751
  const grounded = ask.provider !== "inkeep";
6636
6752
  if (grounded) {
6637
- await write(join11(srcDir, "generated", "ask-data.json"), `${JSON.stringify(await buildAskData(project))}
6753
+ await write(join13(srcDir, "generated", "ask-data.json"), `${JSON.stringify(await buildAskData(project))}
6638
6754
  `);
6639
6755
  }
6640
- await write(join11(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded));
6756
+ await write(join13(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded));
6641
6757
  };
6642
6758
  var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
6643
6759
  if (routeIsTaken(pages, contentPages, "/404")) {
6644
6760
  return;
6645
6761
  }
6646
- await write(join11(srcDir, "pages", "404.astro"), notFoundPageTemplate());
6762
+ await write(join13(srcDir, "pages", "404.astro"), notFoundPageTemplate());
6647
6763
  };
6648
6764
  var shouldGenerateChangelog = (project) => {
6649
6765
  const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
@@ -6662,11 +6778,11 @@ var buildComponentSlots = async (componentsFile) => {
6662
6778
  var generateRuntime = async (project) => {
6663
6779
  const { context, config } = project;
6664
6780
  const out = context.outDir;
6665
- const srcDir = join11(out, "src");
6666
- const dataPath = join11(srcDir, "generated", "data.json");
6667
- const themePath = join11(srcDir, "generated", "app.css");
6668
- const searchClientPath = join11(srcDir, "generated", "search-client.ts");
6669
- const examplesPath = join11(srcDir, "generated", "examples.ts");
6781
+ const srcDir = join13(out, "src");
6782
+ const dataPath = join13(srcDir, "generated", "data.json");
6783
+ const themePath = join13(srcDir, "generated", "app.css");
6784
+ const searchClientPath = join13(srcDir, "generated", "search-client.ts");
6785
+ const examplesPath = join13(srcDir, "generated", "examples.ts");
6670
6786
  const written = new Set;
6671
6787
  const write = (path, content) => {
6672
6788
  written.add(normalize(path));
@@ -6702,7 +6818,7 @@ var generateRuntime = async (project) => {
6702
6818
  const staged = collectStaged(project);
6703
6819
  const hasStaged = staged.size > 0;
6704
6820
  const structural = await Promise.all([
6705
- write(join11(out, "astro.config.mjs"), astroConfigTemplate({
6821
+ write(join13(out, "astro.config.mjs"), astroConfigTemplate({
6706
6822
  aliases: resolveTsconfigAliases(context.root),
6707
6823
  config,
6708
6824
  contentRoutes: project.manifest.routes.map((route) => route.path),
@@ -6716,20 +6832,20 @@ var generateRuntime = async (project) => {
6716
6832
  searchClientPath,
6717
6833
  themePath
6718
6834
  })),
6719
- write(join11(out, "package.json"), runtimePackageTemplate(runtimeDependencies({ config, needsReact, needsSvelte, needsVue }))),
6720
- write(join11(out, "tsconfig.json"), runtimeTsconfigTemplate()),
6721
- write(join11(srcDir, "env.d.ts"), envTemplate()),
6722
- write(join11(srcDir, "content.config.ts"), contentConfigTemplate({ config, context, staged: hasStaged })),
6723
- write(join11(srcDir, "pages", "[...slug].astro"), catchAllPageTemplate({
6835
+ write(join13(out, "package.json"), runtimePackageTemplate(runtimeDependencies({ config, needsReact, needsSvelte, needsVue }))),
6836
+ write(join13(out, "tsconfig.json"), runtimeTsconfigTemplate()),
6837
+ write(join13(srcDir, "env.d.ts"), envTemplate()),
6838
+ write(join13(srcDir, "content.config.ts"), contentConfigTemplate({ config, context, staged: hasStaged })),
6839
+ write(join13(srcDir, "pages", "[...slug].astro"), catchAllPageTemplate({
6724
6840
  askEnabled,
6725
6841
  exportEpub,
6726
6842
  exportPdf,
6727
6843
  mathEnabled: config.markdown.math,
6728
6844
  needsReact
6729
6845
  })),
6730
- write(join11(srcDir, "generated", "components.ts"), slotPlan.module),
6731
- write(join11(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
6732
- write(join11(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples)),
6846
+ write(join13(srcDir, "generated", "components.ts"), slotPlan.module),
6847
+ write(join13(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
6848
+ write(join13(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples)),
6733
6849
  write(themePath, tailwindEntryTemplate({
6734
6850
  configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
6735
6851
  sources: [
@@ -6740,16 +6856,16 @@ var generateRuntime = async (project) => {
6740
6856
  userTheme
6741
6857
  }))
6742
6858
  ]);
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)));
6745
- await Promise.all(exampleDiscovery.examples.map((example) => write(join11(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example))));
6859
+ await Promise.all(islandDiscovery.islands.map((island) => write(join13(srcDir, "generated", "islands", `${island.name}.astro`), islandWrapperTemplate(island))));
6860
+ await Promise.all(slotPlan.wrappers.map((wrapper) => write(join13(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content)));
6861
+ await Promise.all(exampleDiscovery.examples.map((example) => write(join13(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example))));
6746
6862
  await writeAskFiles(project, srcDir, write);
6747
6863
  await writeMcpFiles(project, mcp, write);
6748
6864
  if (config.seo.og.enabled) {
6749
- await write(join11(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
6865
+ await write(join13(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
6750
6866
  }
6751
6867
  if (shouldGenerateChangelog(project)) {
6752
- await write(join11(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
6868
+ await write(join13(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
6753
6869
  askEnabled,
6754
6870
  exportEpub,
6755
6871
  exportPdf,
@@ -6761,27 +6877,27 @@ var generateRuntime = async (project) => {
6761
6877
  await write(searchClientPath, searchClientTemplate(config));
6762
6878
  if (servesStaticIndex(config.search.provider)) {
6763
6879
  const documents = await buildSearchDocuments(project);
6764
- await write(join11(srcDir, "generated", "search.json"), `${JSON.stringify(documents)}
6880
+ await write(join13(srcDir, "generated", "search.json"), `${JSON.stringify(documents)}
6765
6881
  `);
6766
- await write(join11(srcDir, "pages", "blume-search.json.ts"), searchEndpointTemplate());
6882
+ await write(join13(srcDir, "pages", "blume-search.json.ts"), searchEndpointTemplate());
6767
6883
  }
6768
6884
  if (config.search.provider === "mixedbread") {
6769
- await write(join11(srcDir, "pages", "api", "search.ts"), mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""));
6885
+ await write(join13(srcDir, "pages", "api", "search.ts"), mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""));
6770
6886
  }
6771
6887
  const rawMarkdown = await buildRawMarkdown(project);
6772
6888
  await Promise.all([
6773
- write(join11(srcDir, "generated", "raw-markdown.json"), `${JSON.stringify(rawMarkdown)}
6889
+ write(join13(srcDir, "generated", "raw-markdown.json"), `${JSON.stringify(rawMarkdown)}
6774
6890
  `),
6775
- write(join11(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate()),
6776
- write(join11(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate())
6891
+ write(join13(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate()),
6892
+ write(join13(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate())
6777
6893
  ]);
6778
6894
  const feeds = buildRssFeeds(project);
6779
6895
  if (feeds.length > 0) {
6780
6896
  const feedXml = Object.fromEntries(feeds.map((feed) => [feed.type, renderRssFeed(feed)]));
6781
6897
  await Promise.all([
6782
- write(join11(srcDir, "generated", "rss.json"), `${JSON.stringify(feedXml)}
6898
+ write(join13(srcDir, "generated", "rss.json"), `${JSON.stringify(feedXml)}
6783
6899
  `),
6784
- write(join11(srcDir, "pages", "[section]", "rss.xml.ts"), rssEndpointTemplate())
6900
+ write(join13(srcDir, "pages", "[section]", "rss.xml.ts"), rssEndpointTemplate())
6785
6901
  ]);
6786
6902
  }
6787
6903
  const warnings = [
@@ -6818,10 +6934,10 @@ var generateRuntime = async (project) => {
6818
6934
  root: context.root
6819
6935
  });
6820
6936
  warnings.push(...references.warnings);
6821
- await Promise.all(references.files.map((file) => write(join11(srcDir, "pages", file.pagePath), file.content)));
6937
+ await Promise.all(references.files.map((file) => write(join13(srcDir, "pages", file.pagePath), file.content)));
6822
6938
  }
6823
- await write(join11(srcDir, "generated", "data.json"), buildRuntimeData(project));
6824
- await write(join11(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
6939
+ await write(join13(srcDir, "generated", "data.json"), buildRuntimeData(project));
6940
+ await write(join13(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
6825
6941
  `);
6826
6942
  await writeStagedContent(out, staged);
6827
6943
  await pruneOrphans(srcDir, written);
@@ -6829,12 +6945,44 @@ var generateRuntime = async (project) => {
6829
6945
  };
6830
6946
 
6831
6947
  // src/core/config.ts
6832
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
6948
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
6833
6949
 
6834
6950
  // src/core/bridge.ts
6835
- import { existsSync as existsSync7 } from "node:fs";
6951
+ import { existsSync as existsSync8 } from "node:fs";
6836
6952
  import { readFile as readFile9 } from "node:fs/promises";
6837
- import { join as join12 } from "pathe";
6953
+ import { join as join14 } from "pathe";
6954
+
6955
+ // src/migrate/mintlify/assets.ts
6956
+ var assetRefs = (config) => {
6957
+ const refs = ["/images"];
6958
+ const logo = config.logo;
6959
+ if (typeof logo === "string") {
6960
+ refs.push(logo);
6961
+ } else if (logo) {
6962
+ refs.push(logo.light, logo.dark);
6963
+ }
6964
+ const favicon = config.favicon;
6965
+ if (typeof favicon === "string") {
6966
+ refs.push(favicon);
6967
+ } else if (favicon) {
6968
+ refs.push(favicon.light, favicon.dark);
6969
+ }
6970
+ refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
6971
+ return refs;
6972
+ };
6973
+ var assetSegments = (config) => {
6974
+ const segments = new Set;
6975
+ for (const ref of assetRefs(config)) {
6976
+ if (typeof ref !== "string" || !ref.startsWith("/")) {
6977
+ continue;
6978
+ }
6979
+ const [segment] = ref.replace(/^\/+/u, "").split("/");
6980
+ if (segment) {
6981
+ segments.add(segment);
6982
+ }
6983
+ }
6984
+ return [...segments];
6985
+ };
6838
6986
 
6839
6987
  // src/migrate/mintlify/config.ts
6840
6988
  import { readFile as readFile8 } from "node:fs/promises";
@@ -7503,7 +7651,7 @@ var mintlifyI18n = (spec) => {
7503
7651
  // src/core/bridge.ts
7504
7652
  var MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
7505
7653
  var detectMintlifyBridge = async (root) => {
7506
- const configFile = MINTLIFY_CONFIG_FILES.map((name) => join12(root, name)).find((candidate) => existsSync7(candidate));
7654
+ const configFile = MINTLIFY_CONFIG_FILES.map((name) => join14(root, name)).find((candidate) => existsSync8(candidate));
7507
7655
  if (!configFile) {
7508
7656
  return null;
7509
7657
  }
@@ -7519,11 +7667,13 @@ var detectMintlifyBridge = async (root) => {
7519
7667
  const variables = config.variables ?? {};
7520
7668
  const root_ = config.content?.root ?? ".";
7521
7669
  const exclude = config.content?.exclude ?? [];
7670
+ const assets = assetSegments(config).filter((segment) => segment !== "public" && existsSync8(join14(root, segment)));
7522
7671
  return {
7523
7672
  configFile,
7524
7673
  raw: {
7525
7674
  ...config,
7526
7675
  content: {
7676
+ assets,
7527
7677
  exclude,
7528
7678
  root: root_,
7529
7679
  sources: [
@@ -7592,8 +7742,8 @@ var createModuleLoader = () => {
7592
7742
  };
7593
7743
 
7594
7744
  // src/core/project.ts
7595
- import { existsSync as existsSync8 } from "node:fs";
7596
- import { isAbsolute as isAbsolute4, join as join13, resolve as resolve5 } from "pathe";
7745
+ import { existsSync as existsSync9 } from "node:fs";
7746
+ import { isAbsolute as isAbsolute4, join as join15, resolve as resolve5 } from "pathe";
7597
7747
  var CONFIG_FILENAMES = [
7598
7748
  "blume.config.ts",
7599
7749
  "blume.config.mjs",
@@ -7603,8 +7753,8 @@ var THEME_FILENAMES = ["theme.css"];
7603
7753
  var COMPONENTS_FILENAMES = ["components.tsx", "components.ts"];
7604
7754
  var firstExisting = (root, names) => {
7605
7755
  for (const name of names) {
7606
- const candidate = join13(root, name);
7607
- if (existsSync8(candidate)) {
7756
+ const candidate = join15(root, name);
7757
+ if (existsSync9(candidate)) {
7608
7758
  return candidate;
7609
7759
  }
7610
7760
  }
@@ -7613,14 +7763,14 @@ var firstExisting = (root, names) => {
7613
7763
  var findConfigFile = (root) => firstExisting(root, CONFIG_FILENAMES);
7614
7764
  var resolveProjectContext = (root, config) => {
7615
7765
  const absoluteRoot = resolve5(root);
7616
- const contentRoot = isAbsolute4(config.content.root) ? config.content.root : join13(absoluteRoot, config.content.root);
7617
- const pagesPath = join13(absoluteRoot, config.content.pages);
7618
- const pagesRoot = existsSync8(pagesPath) ? pagesPath : null;
7766
+ const contentRoot = isAbsolute4(config.content.root) ? config.content.root : join15(absoluteRoot, config.content.root);
7767
+ const pagesPath = join15(absoluteRoot, config.content.pages);
7768
+ const pagesRoot = existsSync9(pagesPath) ? pagesPath : null;
7619
7769
  return {
7620
7770
  componentsFile: firstExisting(absoluteRoot, COMPONENTS_FILENAMES),
7621
7771
  configFile: findConfigFile(absoluteRoot),
7622
7772
  contentRoot,
7623
- outDir: join13(absoluteRoot, ".blume"),
7773
+ outDir: join15(absoluteRoot, ".blume"),
7624
7774
  pagesRoot,
7625
7775
  root: absoluteRoot,
7626
7776
  themeFile: firstExisting(absoluteRoot, THEME_FILENAMES)
@@ -7810,6 +7960,7 @@ var contentSourceSchema = z2.discriminatedUnion("type", [
7810
7960
  customSourceSchema
7811
7961
  ]);
7812
7962
  var contentConfigSchema = z2.object({
7963
+ assets: z2.array(z2.string()).default([]),
7813
7964
  defaultType: z2.string().default("doc"),
7814
7965
  exclude: z2.array(z2.string()).default(["**/_*", "**/.*"]),
7815
7966
  include: z2.array(z2.string()).default(["**/*.{md,mdx}"]),
@@ -8180,7 +8331,7 @@ var loadConfig = async (root, options = {}) => {
8180
8331
  const sourceFile = bridge?.configFile ?? configFile;
8181
8332
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
8182
8333
  if (!parsed.success) {
8183
- const source = sourceFile && existsSync9(sourceFile) ? readFileSync6(sourceFile, "utf-8") : undefined;
8334
+ const source = sourceFile && existsSync10(sourceFile) ? readFileSync7(sourceFile, "utf-8") : undefined;
8184
8335
  const diagnostics = diagnosticsFromZod(parsed.error, {
8185
8336
  code: "BLUME_CONFIG_INVALID",
8186
8337
  file: sourceFile ?? undefined,
@@ -8657,7 +8808,7 @@ var discoverFolderMeta = async (contentRoot) => {
8657
8808
  };
8658
8809
 
8659
8810
  // src/core/sources/normalize.ts
8660
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
8811
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
8661
8812
  import GithubSlugger from "github-slugger";
8662
8813
  import { extname as extname4 } from "pathe";
8663
8814
  var NUMERIC_PREFIX2 = /^\d+[-_.]/u;
@@ -8731,8 +8882,9 @@ var extractLinks = (body) => {
8731
8882
  if (target === undefined || match.index === undefined) {
8732
8883
  continue;
8733
8884
  }
8885
+ const targetOffset = match.index + match[0].indexOf("](") + "](".length;
8734
8886
  links.push({
8735
- column: line.indexOf(target, match.index) + 1,
8887
+ column: targetOffset + 1,
8736
8888
  line: lineNumber,
8737
8889
  target
8738
8890
  });
@@ -8782,7 +8934,7 @@ var normalizeEntry2 = (entry, ctx) => {
8782
8934
  const ext = format === "mdx" ? ".mdx" : ".md";
8783
8935
  const result = pageMetaSchema.safeParse(entry.data);
8784
8936
  if (!result.success) {
8785
- const source = entry.raw ?? (entry.sourcePath && existsSync10(entry.sourcePath) ? readFileSync7(entry.sourcePath, "utf-8") : undefined);
8937
+ const source = entry.raw ?? (entry.sourcePath && existsSync11(entry.sourcePath) ? readFileSync8(entry.sourcePath, "utf-8") : undefined);
8786
8938
  return {
8787
8939
  diagnostics: diagnosticsFromZod(result.error, {
8788
8940
  code: "BLUME_FRONTMATTER_INVALID",
@@ -8831,15 +8983,15 @@ var normalizeEntry2 = (entry, ctx) => {
8831
8983
  };
8832
8984
 
8833
8985
  // src/core/sources/resolve.ts
8834
- import { join as join22 } from "pathe";
8986
+ import { join as join24 } from "pathe";
8835
8987
 
8836
8988
  // src/core/sources/filesystem.ts
8837
- import { existsSync as existsSync11, watch as fsWatch } from "node:fs";
8989
+ import { existsSync as existsSync12, watch as fsWatch } from "node:fs";
8838
8990
  import { readFile as readFile10 } from "node:fs/promises";
8839
- import { extname as extname5, isAbsolute as isAbsolute5, join as join14, relative as relative10, resolve as resolve6 } from "pathe";
8991
+ import { extname as extname5, isAbsolute as isAbsolute5, join as join16, relative as relative10, resolve as resolve6 } from "pathe";
8840
8992
  import { glob as glob6 } from "tinyglobby";
8841
8993
  var filesystemSource = (options) => {
8842
- const contentRoot = isAbsolute5(options.root) ? options.root : join14(resolve6(options.projectRoot), options.root);
8994
+ const contentRoot = isAbsolute5(options.root) ? options.root : join16(resolve6(options.projectRoot), options.root);
8843
8995
  const load2 = async () => {
8844
8996
  const files = await glob6(options.include, {
8845
8997
  absolute: true,
@@ -8863,7 +9015,7 @@ var filesystemSource = (options) => {
8863
9015
  return { diagnostics: [], entries };
8864
9016
  };
8865
9017
  const validate = () => {
8866
- if (!existsSync11(contentRoot)) {
9018
+ if (!existsSync12(contentRoot)) {
8867
9019
  throw new BlumeError({
8868
9020
  code: options.missingCode ?? "BLUME_CONTENT_ROOT_MISSING",
8869
9021
  file: contentRoot,
@@ -8874,7 +9026,7 @@ var filesystemSource = (options) => {
8874
9026
  }
8875
9027
  };
8876
9028
  const watch = (onChange) => {
8877
- if (!existsSync11(contentRoot)) {
9029
+ if (!existsSync12(contentRoot)) {
8878
9030
  return () => {};
8879
9031
  }
8880
9032
  const watcher = fsWatch(contentRoot, { recursive: true }, onChange);
@@ -8885,7 +9037,7 @@ var filesystemSource = (options) => {
8885
9037
  load: load2,
8886
9038
  name: options.name,
8887
9039
  prefix: options.prefix,
8888
- read: (ref) => readFile10(join14(contentRoot, ref), "utf-8"),
9040
+ read: (ref) => readFile10(join16(contentRoot, ref), "utf-8"),
8889
9041
  staged: false,
8890
9042
  validate,
8891
9043
  watch
@@ -8894,7 +9046,7 @@ var filesystemSource = (options) => {
8894
9046
 
8895
9047
  // src/core/sources/cache.ts
8896
9048
  import { mkdir as mkdir3, readFile as readFile11, writeFile as writeFile3 } from "node:fs/promises";
8897
- import { join as join15 } from "pathe";
9049
+ import { join as join17 } from "pathe";
8898
9050
  var hashText = (text) => {
8899
9051
  let hash = 5381;
8900
9052
  for (let i = 0;i < text.length; i += 1) {
@@ -8921,7 +9073,7 @@ var pollingWatch = (load2, intervalSeconds) => (onChange) => {
8921
9073
  return () => clearInterval(timer);
8922
9074
  };
8923
9075
  var snapshotCache = (cacheDir) => {
8924
- const file = join15(cacheDir, "entries.json");
9076
+ const file = join17(cacheDir, "entries.json");
8925
9077
  return {
8926
9078
  read: async () => {
8927
9079
  try {
@@ -9135,11 +9287,12 @@ var enumerateGithub = async (github, include, doFetch) => {
9135
9287
  }
9136
9288
  const body = await res.json();
9137
9289
  const prefix = base ? `${base}/` : "";
9138
- return (body.tree ?? []).filter((node) => node.type === "blob" && node.path.startsWith(prefix)).map((node) => node.path.slice(prefix.length)).filter((rel) => matchesInclude(rel, include)).map((rel) => ({
9290
+ const refs = (body.tree ?? []).filter((node) => node.type === "blob" && node.path.startsWith(prefix)).map((node) => node.path.slice(prefix.length)).filter((rel) => matchesInclude(rel, include)).map((rel) => ({
9139
9291
  editUrl: `https://github.com/${owner}/${repo}/edit/${ref}/${prefix}${rel}`,
9140
9292
  fetchUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${prefix}${rel}`,
9141
9293
  ref: rel
9142
9294
  }));
9295
+ return { refs, truncated: body.truncated === true };
9143
9296
  };
9144
9297
  var mdxRemoteSource = (options, ctx) => {
9145
9298
  const doFetch = options.fetchImpl ?? globalThis.fetch;
@@ -9151,11 +9304,12 @@ var mdxRemoteSource = (options, ctx) => {
9151
9304
  }
9152
9305
  if (options.files && options.url) {
9153
9306
  const base = options.url.replace(/\/$/u, "");
9154
- return options.files.filter((ref) => matchesInclude(ref, options.include)).map((ref) => ({
9307
+ const refs = options.files.filter((ref) => matchesInclude(ref, options.include)).map((ref) => ({
9155
9308
  editUrl: `${base}/${ref}`,
9156
9309
  fetchUrl: `${base}/${ref}`,
9157
9310
  ref
9158
9311
  }));
9312
+ return { refs, truncated: false };
9159
9313
  }
9160
9314
  throw new BlumeError({
9161
9315
  code: "BLUME_SOURCE_MISCONFIGURED",
@@ -9181,12 +9335,40 @@ var mdxRemoteSource = (options, ctx) => {
9181
9335
  };
9182
9336
  };
9183
9337
  const load2 = async () => {
9338
+ const skipped = [];
9184
9339
  const result = await loadWithCache(options.name, cache, async () => {
9185
- const refs = await enumerate();
9186
- return await Promise.all(refs.map(fetchEntry));
9340
+ const { refs, truncated } = await enumerate();
9341
+ if (truncated) {
9342
+ skipped.push({
9343
+ code: "BLUME_SOURCE_TRUNCATED",
9344
+ message: `Source "${options.name}" hit GitHub's tree listing limit; some files were not enumerated. Narrow the source path or split the repo.`,
9345
+ severity: "warning"
9346
+ });
9347
+ }
9348
+ const settled = await Promise.all(refs.map(async (ref) => {
9349
+ try {
9350
+ return await fetchEntry(ref);
9351
+ } catch (error) {
9352
+ skipped.push({
9353
+ code: "BLUME_SOURCE_FETCH_FAILED",
9354
+ message: `Source "${options.name}" skipped "${ref.ref}" (${error.message}); the rest were imported.`,
9355
+ severity: "warning"
9356
+ });
9357
+ return null;
9358
+ }
9359
+ }));
9360
+ const entries = settled.filter((entry) => entry !== null);
9361
+ if (refs.length > 0 && entries.length === 0) {
9362
+ skipped.length = 0;
9363
+ throw new Error(`all ${refs.length} remote file(s) failed to fetch`);
9364
+ }
9365
+ return entries;
9187
9366
  }, ctx.refresh ?? true);
9188
9367
  snapshot = new Map(result.entries.map((entry) => [entry.ref, entry]));
9189
- return result;
9368
+ return {
9369
+ ...result,
9370
+ diagnostics: [...result.diagnostics, ...skipped]
9371
+ };
9190
9372
  };
9191
9373
  const read = async (ref) => {
9192
9374
  const cached3 = snapshot.get(ref);
@@ -9208,21 +9390,25 @@ var mdxRemoteSource = (options, ctx) => {
9208
9390
  };
9209
9391
 
9210
9392
  // src/core/sources/mintlify.ts
9211
- import { existsSync as existsSync13, watch as fsWatch2 } from "node:fs";
9393
+ import { existsSync as existsSync14, watch as fsWatch2 } from "node:fs";
9212
9394
  import { readFile as readFile13 } from "node:fs/promises";
9213
- import { isAbsolute as isAbsolute6, join as join18, relative as relative13, resolve as resolve8 } from "pathe";
9395
+ import { isAbsolute as isAbsolute7, join as join20, relative as relative14, resolve as resolve8 } from "pathe";
9214
9396
  import { glob as glob7 } from "tinyglobby";
9215
9397
 
9216
9398
  // src/migrate/shared.ts
9217
- import { existsSync as existsSync12 } from "node:fs";
9399
+ import { existsSync as existsSync13 } from "node:fs";
9218
9400
  import { readFile as readFile12, writeFile as writeFile4 } from "node:fs/promises";
9219
- import { join as join16 } from "pathe";
9401
+ import { isAbsolute as isAbsolute6, join as join18, relative as relative11 } from "pathe";
9402
+ var isInsideRoot2 = (root, candidate) => {
9403
+ const rel = relative11(root, candidate);
9404
+ return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
9405
+ };
9220
9406
  var writeBlumeConfig = async (root, config) => {
9221
9407
  const body = `import { defineConfig } from "blume";
9222
9408
 
9223
9409
  export default defineConfig(${JSON.stringify(config, null, 2)});
9224
9410
  `;
9225
- await writeFile4(join16(root, "blume.config.ts"), body, "utf-8");
9411
+ await writeFile4(join18(root, "blume.config.ts"), body, "utf-8");
9226
9412
  };
9227
9413
  var BLUME_SCRIPTS = {
9228
9414
  build: "blume build",
@@ -9230,8 +9416,8 @@ var BLUME_SCRIPTS = {
9230
9416
  start: "blume preview"
9231
9417
  };
9232
9418
  var rewriteFrameworkScripts = async (root, cli, remove) => {
9233
- const pkgPath = join16(root, "package.json");
9234
- if (!existsSync12(pkgPath)) {
9419
+ const pkgPath = join18(root, "package.json");
9420
+ if (!existsSync13(pkgPath)) {
9235
9421
  return false;
9236
9422
  }
9237
9423
  let pkg;
@@ -9264,25 +9450,7 @@ var rewriteFrameworkScripts = async (root, cli, remove) => {
9264
9450
  }
9265
9451
  return changed;
9266
9452
  };
9267
- var gitignoreKey = (line) => line.trim().replace(/\/+$/u, "");
9268
- var ensureGitignore = async (root, entries) => {
9269
- const path = join16(root, ".gitignore");
9270
- const existing = existsSync12(path) ? await readFile12(path, "utf-8") : "";
9271
- const present = new Set(existing.split(`
9272
- `).map(gitignoreKey).filter(Boolean));
9273
- const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
9274
- if (added.length === 0) {
9275
- return [];
9276
- }
9277
- const gap = existing.length > 0 && !existing.endsWith(`
9278
- `) ? `
9279
- ` : "";
9280
- await writeFile4(path, `${existing}${gap}${added.join(`
9281
- `)}
9282
- `, "utf-8");
9283
- return added;
9284
- };
9285
- var leftoverFiles = (root, candidates) => candidates.filter((candidate) => existsSync12(join16(root, candidate)));
9453
+ var leftoverFiles = (root, candidates) => candidates.filter((candidate) => existsSync13(join18(root, candidate)));
9286
9454
  var attribute = (attrs, name) => {
9287
9455
  const match = attrs.match(new RegExp(`\\b${name}=(?:"(?<dq>[^"]*)"|'(?<sq>[^']*)')`, "u"));
9288
9456
  return match?.groups?.dq ?? match?.groups?.sq;
@@ -9679,7 +9847,7 @@ var isLiteralObject = (value) => typeof value === "object" && value !== null &&
9679
9847
  var asLiteralArray = (value) => Array.isArray(value) ? value : undefined;
9680
9848
 
9681
9849
  // src/migrate/mintlify/content.ts
9682
- import { dirname as dirname10, join as join17, relative as relative11 } from "pathe";
9850
+ import { dirname as dirname10, join as join19, relative as relative12 } from "pathe";
9683
9851
  var CALLOUT_DIRECTIVES = {
9684
9852
  Check: "success",
9685
9853
  Danger: "danger",
@@ -9715,9 +9883,9 @@ var rewriteSnippetImports = (source, options) => {
9715
9883
  if (/\.mdx?$/u.test(importSource)) {
9716
9884
  return "";
9717
9885
  }
9718
- const target = join17(options.root, importSource.replace(/^\/+/u, ""));
9886
+ const target = join19(options.root, importSource.replace(/^\/+/u, ""));
9719
9887
  components.push(importSource.replace(/^\/+/u, ""));
9720
- let rel = relative11(dirname10(options.filePath), target);
9888
+ let rel = relative12(dirname10(options.filePath), target);
9721
9889
  if (!rel.startsWith(".")) {
9722
9890
  rel = `./${rel}`;
9723
9891
  }
@@ -9907,7 +10075,7 @@ var rewriteMintlifySvgIconProps = (source) => {
9907
10075
 
9908
10076
  // src/migrate/mintlify/snippets.ts
9909
10077
  import { readFile as readFileFromDisk } from "node:fs/promises";
9910
- import { dirname as dirname11, relative as relative12, resolve as resolve7 } from "pathe";
10078
+ import { dirname as dirname11, relative as relative13, resolve as resolve7 } from "pathe";
9911
10079
  var MARKDOWN_SNIPPET_IMPORT = /^import\s+(?<name>[$A-Z_a-z][$\w]*)\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
9912
10080
  var NAMED_SNIPPET_IMPORT = /^import\s+\{(?<names>[^}]+)\}\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
9913
10081
  var EXPORTED_STRING_CONST = /^export\s+const\s+(?<name>[$A-Z_a-z][$\w]*)\s*=\s*(?:"(?<double>(?:\\.|[^"\\])*)"|'(?<single>(?:\\.|[^'\\])*)'|`(?<template>(?:\\.|[^`\\])*)`)\s*;?\s*$/gmu;
@@ -9916,15 +10084,15 @@ var PLACEHOLDER = /\{(?<name>[$A-Z_a-z][$\w]*)\}/gu;
9916
10084
  var GLOBAL_VARIABLE = /\{\{\s*(?<name>[A-Za-z0-9-]+)\s*\}\}/gu;
9917
10085
  var FRONTMATTER_BLOCK = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/u;
9918
10086
  var USER_EXPORT = /^(?:export\s+)?(?:const|let|var)\s+user\s*=|^import\s+\{\s*user\s*\}/mu;
9919
- var isInsideRoot2 = (root, candidate) => {
9920
- const rel = relative12(root, candidate);
10087
+ var isInsideRoot3 = (root, candidate) => {
10088
+ const rel = relative13(root, candidate);
9921
10089
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
9922
10090
  };
9923
10091
  var escapeRegExp2 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
9924
10092
  var snippetSelfClosingTagPattern = (name) => new RegExp(`<${escapeRegExp2(name)}(?<attrs>[^>]*?)\\s*/>`, "gu");
9925
10093
  var snippetPairedTagPattern = (name) => new RegExp(`<${escapeRegExp2(name)}(?<attrs>[^>]*?)>[\\s\\S]*?</${escapeRegExp2(name)}>`, "gu");
9926
10094
  var rootRelativePath = (root, file) => {
9927
- const rel = relative12(root, file);
10095
+ const rel = relative13(root, file);
9928
10096
  return rel ? `/${rel}` : "/";
9929
10097
  };
9930
10098
  var snippetCycleMessage = (root, file, trail) => {
@@ -9934,7 +10102,7 @@ var snippetCycleMessage = (root, file, trail) => {
9934
10102
  };
9935
10103
  var resolveSnippetPath = (options) => {
9936
10104
  const target = options.source.startsWith("/") ? resolve7(options.root, options.source.slice(1)) : resolve7(dirname11(options.filePath), options.source);
9937
- return isInsideRoot2(options.root, target) ? target : null;
10105
+ return isInsideRoot3(options.root, target) ? target : null;
9938
10106
  };
9939
10107
  var collectImports2 = (source) => [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
9940
10108
  const name = match.groups?.name;
@@ -10127,7 +10295,7 @@ var MINTLIFY_SOURCE_IGNORES = [
10127
10295
  "snippets/**"
10128
10296
  ];
10129
10297
  var mintlifySource = (options) => {
10130
- const contentRoot = isAbsolute6(options.root) ? options.root : join18(resolve8(options.projectRoot), options.root);
10298
+ const contentRoot = isAbsolute7(options.root) ? options.root : join20(resolve8(options.projectRoot), options.root);
10131
10299
  const ignore = [...new Set([...options.exclude, ...MINTLIFY_SOURCE_IGNORES])];
10132
10300
  const transform = (raw, file) => transformMintlifyContent(raw, {
10133
10301
  filePath: file,
@@ -10153,7 +10321,7 @@ var mintlifySource = (options) => {
10153
10321
  body: { format: "mdx", text: parsed.content },
10154
10322
  data: parsed.data,
10155
10323
  raw: result.content,
10156
- ref: relative13(contentRoot, file),
10324
+ ref: relative14(contentRoot, file),
10157
10325
  sourcePath: file
10158
10326
  };
10159
10327
  }));
@@ -10167,7 +10335,7 @@ var mintlifySource = (options) => {
10167
10335
  return { diagnostics, entries };
10168
10336
  };
10169
10337
  const validate = () => {
10170
- if (!existsSync13(contentRoot)) {
10338
+ if (!existsSync14(contentRoot)) {
10171
10339
  throw new BlumeError({
10172
10340
  code: "BLUME_CONTENT_ROOT_MISSING",
10173
10341
  file: contentRoot,
@@ -10179,11 +10347,11 @@ var mintlifySource = (options) => {
10179
10347
  };
10180
10348
  const watch = (onChange) => {
10181
10349
  const disposers = [];
10182
- if (existsSync13(contentRoot)) {
10350
+ if (existsSync14(contentRoot)) {
10183
10351
  const watcher = fsWatch2(contentRoot, { recursive: true }, onChange);
10184
10352
  disposers.push(() => watcher.close());
10185
10353
  }
10186
- if (options.configFile && existsSync13(options.configFile)) {
10354
+ if (options.configFile && existsSync14(options.configFile)) {
10187
10355
  const watcher = fsWatch2(options.configFile, onChange);
10188
10356
  disposers.push(() => watcher.close());
10189
10357
  }
@@ -10194,7 +10362,7 @@ var mintlifySource = (options) => {
10194
10362
  };
10195
10363
  };
10196
10364
  const read = async (ref) => {
10197
- const file = join18(contentRoot, ref);
10365
+ const file = join20(contentRoot, ref);
10198
10366
  const result = await transform(await readFile13(file, "utf-8"), file);
10199
10367
  return result.content;
10200
10368
  };
@@ -10211,11 +10379,12 @@ var mintlifySource = (options) => {
10211
10379
  };
10212
10380
 
10213
10381
  // src/core/sources/notion.ts
10214
- import { join as join20 } from "pathe";
10382
+ import { setTimeout as sleep } from "node:timers/promises";
10383
+ import { join as join22 } from "pathe";
10215
10384
 
10216
10385
  // src/core/sources/assets.ts
10217
10386
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
10218
- import { extname as extname6, join as join19 } from "pathe";
10387
+ import { extname as extname6, join as join21 } from "pathe";
10219
10388
  var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
10220
10389
  var REMOTE = /^https?:\/\//u;
10221
10390
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
@@ -10244,7 +10413,7 @@ var materializeAssets = async (markdown, ctx) => {
10244
10413
  const bytes = new Uint8Array(await res.arrayBuffer());
10245
10414
  const file = `${hashText(url)}${extFor(url)}`;
10246
10415
  await mkdir4(ctx.assetsDir, { recursive: true });
10247
- await writeFile5(join19(ctx.assetsDir, file), bytes);
10416
+ await writeFile5(join21(ctx.assetsDir, file), bytes);
10248
10417
  rewrites.set(url, `${ctx.assetsBaseUrl}/${file}`);
10249
10418
  } catch (error) {
10250
10419
  diagnostics.push({
@@ -10279,6 +10448,28 @@ var richToMarkdown = (rich = []) => rich.map((node) => {
10279
10448
  return node.href ? `[${text}](${node.href})` : text;
10280
10449
  }).join("");
10281
10450
  var blockField = (block) => block[block.type]?.rich_text ?? [];
10451
+ var RATE_LIMITED = 429;
10452
+ var MAX_RETRIES = 4;
10453
+ var BASE_DELAY_MS = 500;
10454
+ var SECOND_MS = 1000;
10455
+ var withNotionRetry = async (call) => {
10456
+ let lastError;
10457
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt += 1) {
10458
+ try {
10459
+ return await call();
10460
+ } catch (error) {
10461
+ lastError = error;
10462
+ const { status } = error;
10463
+ if (status !== RATE_LIMITED || attempt === MAX_RETRIES) {
10464
+ throw error;
10465
+ }
10466
+ const retryAfter = Number(error.headers?.["retry-after"]);
10467
+ const wait = retryAfter > 0 ? retryAfter * SECOND_MS : BASE_DELAY_MS * 2 ** attempt;
10468
+ await sleep(wait);
10469
+ }
10470
+ }
10471
+ throw lastError instanceof Error ? lastError : new Error("Notion request failed after retries.");
10472
+ };
10282
10473
  var collectAll = async (page, cursor, acc = []) => {
10283
10474
  const res = await page(cursor);
10284
10475
  const all = [...acc, ...res.results];
@@ -10338,8 +10529,8 @@ ${text}
10338
10529
  };
10339
10530
  var notionSource = (options, ctx) => {
10340
10531
  const props = options.properties ?? {};
10341
- const cache = snapshotCache(ctx?.cacheDir ?? join20(".blume", "cache", options.name));
10342
- const assetsDir = ctx?.assetsDir ?? join20(".blume", "public", "blume-assets", options.name);
10532
+ const cache = snapshotCache(ctx?.cacheDir ?? join22(".blume", "cache", options.name));
10533
+ const assetsDir = ctx?.assetsDir ?? join22(".blume", "public", "blume-assets", options.name);
10343
10534
  const assetsBaseUrl = ctx?.assetsBaseUrl ?? `/blume-assets/${options.name}`;
10344
10535
  let snapshot = new Map;
10345
10536
  const resolveClient = async () => {
@@ -10358,7 +10549,7 @@ var notionSource = (options, ctx) => {
10358
10549
  }
10359
10550
  return new Client({ auth: options.token ?? process.env.NOTION_TOKEN });
10360
10551
  };
10361
- const childrenOf = (client, blockId) => collectAll((cursor) => client.blocks.children.list({ block_id: blockId, start_cursor: cursor }));
10552
+ const childrenOf = (client, blockId) => collectAll((cursor) => withNotionRetry(() => client.blocks.children.list({ block_id: blockId, start_cursor: cursor })));
10362
10553
  const renderContainer = async (client, block, render) => {
10363
10554
  const children = async (target) => {
10364
10555
  if (!target.has_children) {
@@ -10472,10 +10663,10 @@ ${rendered.join(`
10472
10663
  const assetDiagnostics = [];
10473
10664
  const result = await loadWithCache(options.name, cache, async () => {
10474
10665
  const client = await resolveClient();
10475
- const pages = await collectAll((cursor) => client.databases.query({
10666
+ const pages = await collectAll((cursor) => withNotionRetry(() => client.databases.query({
10476
10667
  database_id: options.database,
10477
10668
  start_cursor: cursor
10478
- }));
10669
+ })));
10479
10670
  const built = await Promise.all(pages.map((page) => toEntry(client, page)));
10480
10671
  for (const item of built) {
10481
10672
  assetDiagnostics.push(...item.diagnostics);
@@ -10507,7 +10698,7 @@ ${rendered.join(`
10507
10698
  };
10508
10699
 
10509
10700
  // src/core/sources/sanity.ts
10510
- import { join as join21 } from "pathe";
10701
+ import { join as join23 } from "pathe";
10511
10702
 
10512
10703
  // src/core/sources/portable-text.ts
10513
10704
  var HEADING_STYLES = {
@@ -10646,11 +10837,11 @@ var resolveClient = async (options, preview) => {
10646
10837
  };
10647
10838
  var sanitySource = (options, ctx) => {
10648
10839
  const fields = options.fields ?? {};
10649
- const cache = snapshotCache(ctx?.cacheDir ?? join21(".blume", "cache", options.name));
10840
+ const cache = snapshotCache(ctx?.cacheDir ?? join23(".blume", "cache", options.name));
10650
10841
  let snapshot = new Map;
10651
10842
  const toEntry = (doc) => {
10652
10843
  const slugValue = asString2(getPath(doc, fields.slug ?? "slug.current")) ?? asString2(doc._id) ?? "untitled";
10653
- const slug = slugify2(slugValue) || "untitled";
10844
+ const slug = slugify2(slugValue) || slugify2(asString2(doc._id) ?? "") || "untitled";
10654
10845
  const data = {};
10655
10846
  const title = asString2(getPath(doc, fields.title ?? "title"));
10656
10847
  const description = asString2(getPath(doc, fields.description ?? "description"));
@@ -10721,8 +10912,8 @@ var uniqueNamer = () => {
10721
10912
  };
10722
10913
  var sourceContext = (context, name, runtime) => ({
10723
10914
  assetsBaseUrl: `/blume-assets/${name}`,
10724
- assetsDir: join22(context.outDir, "public", "blume-assets", name),
10725
- cacheDir: join22(context.outDir, "cache", name),
10915
+ assetsDir: join24(context.outDir, "public", "blume-assets", name),
10916
+ cacheDir: join24(context.outDir, "cache", name),
10726
10917
  mode: runtime.mode,
10727
10918
  preview: runtime.preview,
10728
10919
  projectRoot: context.root,
@@ -10916,8 +11107,8 @@ var scanProject = async (root, options = {}) => {
10916
11107
  };
10917
11108
 
10918
11109
  // src/cli/env.ts
10919
- import { existsSync as existsSync14, readFileSync as readFileSync8 } from "node:fs";
10920
- import { dirname as dirname12, join as join23, resolve as resolve9 } from "pathe";
11110
+ import { existsSync as existsSync15, readFileSync as readFileSync9 } from "node:fs";
11111
+ import { dirname as dirname12, join as join25, resolve as resolve9 } from "pathe";
10921
11112
  var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
10922
11113
  var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
10923
11114
  var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
@@ -10955,8 +11146,8 @@ var applyEnv = (parsed) => {
10955
11146
  };
10956
11147
  var loadFile = (path) => {
10957
11148
  try {
10958
- if (existsSync14(path)) {
10959
- applyEnv(parseEnv(readFileSync8(path, "utf-8")));
11149
+ if (existsSync15(path)) {
11150
+ applyEnv(parseEnv(readFileSync9(path, "utf-8")));
10960
11151
  }
10961
11152
  } catch {}
10962
11153
  };
@@ -10964,10 +11155,10 @@ var loadEnvFiles = (startDir) => {
10964
11155
  let dir = resolve9(startDir);
10965
11156
  let done = false;
10966
11157
  while (!done) {
10967
- loadFile(join23(dir, ".env.local"));
10968
- loadFile(join23(dir, ".env"));
11158
+ loadFile(join25(dir, ".env.local"));
11159
+ loadFile(join25(dir, ".env"));
10969
11160
  const parent = dirname12(dir);
10970
- done = existsSync14(join23(dir, ".git")) || parent === dir;
11161
+ done = existsSync15(join25(dir, ".git")) || parent === dir;
10971
11162
  dir = parent;
10972
11163
  }
10973
11164
  };
@@ -11082,29 +11273,38 @@ var prepareProject = async (options) => {
11082
11273
 
11083
11274
  // src/cli/commands/build.ts
11084
11275
  var ADAPTERS = ["vercel", "node", "netlify", "cloudflare"];
11276
+ var validateBudgetFlags = (args) => {
11277
+ for (const flag of ["budget-js", "budget-css"]) {
11278
+ const value = args[flag];
11279
+ if (value !== undefined && !(Number(value) > 0)) {
11280
+ logger.error(`Invalid --${flag} "${value}" (expected a positive number of kB).`);
11281
+ process.exit(1);
11282
+ }
11283
+ }
11284
+ };
11085
11285
  var emitRedirectFiles = async (config, distDir) => {
11086
11286
  const { redirects } = config;
11087
11287
  if (redirects.length === 0 || config.deployment.output !== "static") {
11088
11288
  return;
11089
11289
  }
11090
- await writeFile6(join24(distDir, "blume-redirects.json"), buildRedirectManifest(redirects), "utf-8");
11290
+ await writeFile6(join26(distDir, "blume-redirects.json"), buildRedirectManifest(redirects), "utf-8");
11091
11291
  const platformFiles = [
11092
11292
  { content: buildNetlifyRedirects(redirects), name: "_redirects" },
11093
11293
  { content: buildVercelConfig(redirects), name: "vercel.json" }
11094
11294
  ];
11095
- await Promise.all(platformFiles.map((file) => existsSync15(join24(distDir, file.name)) ? Promise.resolve() : writeFile6(join24(distDir, file.name), file.content, "utf-8")));
11295
+ await Promise.all(platformFiles.map((file) => existsSync16(join26(distDir, file.name)) ? Promise.resolve() : writeFile6(join26(distDir, file.name), file.content, "utf-8")));
11096
11296
  logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
11097
11297
  };
11098
11298
  var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(bytes < 1024 * 100 ? 1 : 0)} kB`;
11099
11299
  var astroAssets = async (distDir, ext) => {
11100
- const astroDir = join24(distDir, "_astro");
11101
- if (!existsSync15(astroDir)) {
11300
+ const astroDir = join26(distDir, "_astro");
11301
+ if (!existsSync16(astroDir)) {
11102
11302
  return [];
11103
11303
  }
11104
11304
  const entries = await readdir(astroDir);
11105
11305
  const files = entries.filter((name) => name.endsWith(`.${ext}`));
11106
11306
  const sized = await Promise.all(files.map(async (name) => {
11107
- const info = await stat(join24(astroDir, name));
11307
+ const info = await stat(join26(astroDir, name));
11108
11308
  return { name, size: info.size };
11109
11309
  }));
11110
11310
  return sized.toSorted((a, b) => b.size - a.size);
@@ -11184,6 +11384,7 @@ var buildCommand = defineCommand2({
11184
11384
  },
11185
11385
  async run({ args }) {
11186
11386
  const root = process.cwd();
11387
+ refuseIfDevRunning(root, "building");
11187
11388
  if (args.output && args.output !== "static" && args.output !== "server") {
11188
11389
  logger.error(`Invalid --output "${args.output}" (use static | server).`);
11189
11390
  process.exit(1);
@@ -11192,6 +11393,7 @@ var buildCommand = defineCommand2({
11192
11393
  logger.error(`Invalid --adapter "${args.adapter}" (use ${ADAPTERS.join(" | ")}).`);
11193
11394
  process.exit(1);
11194
11395
  }
11396
+ validateBudgetFlags(args);
11195
11397
  const project = await prepareProject({
11196
11398
  mode: "build",
11197
11399
  overrides: {
@@ -11208,7 +11410,7 @@ var buildCommand = defineCommand2({
11208
11410
  logLevel: "info",
11209
11411
  root: project.context.outDir
11210
11412
  });
11211
- const distDir = join24(root, "dist");
11413
+ const distDir = join26(root, "dist");
11212
11414
  if (project.config.search.provider === "pagefind") {
11213
11415
  logger.start("Building search index");
11214
11416
  const indexed = await buildSearchIndex(distDir);
@@ -11222,19 +11424,19 @@ var buildCommand = defineCommand2({
11222
11424
  if (project.config.ai.llmsTxt) {
11223
11425
  const { index, full } = await buildLlmsFiles(project);
11224
11426
  await Promise.all([
11225
- writeFile6(join24(distDir, "llms.txt"), index, "utf-8"),
11226
- writeFile6(join24(distDir, "llms-full.txt"), full, "utf-8")
11427
+ writeFile6(join26(distDir, "llms.txt"), index, "utf-8"),
11428
+ writeFile6(join26(distDir, "llms-full.txt"), full, "utf-8")
11227
11429
  ]);
11228
11430
  logger.success("Generated llms.txt and llms-full.txt");
11229
11431
  }
11230
11432
  const sitemap = buildSitemap(project);
11231
- if (sitemap && !existsSync15(join24(distDir, "sitemap.xml"))) {
11232
- await writeFile6(join24(distDir, "sitemap.xml"), sitemap, "utf-8");
11433
+ if (sitemap && !existsSync16(join26(distDir, "sitemap.xml"))) {
11434
+ await writeFile6(join26(distDir, "sitemap.xml"), sitemap, "utf-8");
11233
11435
  logger.success("Generated sitemap.xml");
11234
11436
  }
11235
11437
  const robots = buildRobots(project);
11236
- if (robots && !existsSync15(join24(distDir, "robots.txt"))) {
11237
- await writeFile6(join24(distDir, "robots.txt"), robots, "utf-8");
11438
+ if (robots && !existsSync16(join26(distDir, "robots.txt"))) {
11439
+ await writeFile6(join26(distDir, "robots.txt"), robots, "utf-8");
11238
11440
  logger.success("Generated robots.txt");
11239
11441
  }
11240
11442
  await emitRedirectFiles(project.config, distDir);
@@ -11263,11 +11465,11 @@ var buildCommand = defineCommand2({
11263
11465
  });
11264
11466
 
11265
11467
  // src/cli/commands/check.ts
11266
- import { existsSync as existsSync16 } from "node:fs";
11468
+ import { existsSync as existsSync17 } from "node:fs";
11267
11469
  import { check } from "@astrojs/check";
11268
11470
  import { sync } from "astro";
11269
11471
  import { defineCommand as defineCommand3 } from "citty";
11270
- import { join as join25 } from "pathe";
11472
+ import { join as join27 } from "pathe";
11271
11473
  var checkCommand = defineCommand3({
11272
11474
  args: {
11273
11475
  preview: {
@@ -11293,13 +11495,13 @@ var checkCommand = defineCommand3({
11293
11495
  });
11294
11496
  const { outDir } = project.context;
11295
11497
  await sync({ logLevel: "warn", root: outDir });
11296
- const tsconfig = join25(root, "tsconfig.json");
11498
+ const tsconfig = join27(root, "tsconfig.json");
11297
11499
  logger.start(`Type-checking ${project.graph.pages.length} page(s)`);
11298
11500
  const failed = await check({
11299
11501
  minimumFailingSeverity: "error",
11300
11502
  minimumSeverity: "hint",
11301
11503
  root: outDir,
11302
- tsconfig: existsSync16(tsconfig) ? tsconfig : undefined,
11504
+ tsconfig: existsSync17(tsconfig) ? tsconfig : undefined,
11303
11505
  watch: false
11304
11506
  });
11305
11507
  if (failed) {
@@ -11315,6 +11517,9 @@ import { watch } from "node:fs";
11315
11517
  import { dev } from "astro";
11316
11518
  import { defineCommand as defineCommand4 } from "citty";
11317
11519
 
11520
+ // src/astro/static-assets.ts
11521
+ import { extname as extname7, join as join28, relative as relative15, resolve as resolve10, sep } from "pathe";
11522
+
11318
11523
  // src/astro/integration.ts
11319
11524
  var overlayServer = null;
11320
11525
  var overlayChannel = () => overlayServer?.ws ?? overlayServer?.hot;
@@ -11348,6 +11553,20 @@ ${body}`,
11348
11553
  });
11349
11554
  };
11350
11555
 
11556
+ // src/cli/args.ts
11557
+ var MAX_PORT = 65535;
11558
+ var parsePort = (value) => {
11559
+ if (value === undefined) {
11560
+ return;
11561
+ }
11562
+ const port = Number(value);
11563
+ if (!(Number.isInteger(port) && port >= 1 && port <= MAX_PORT)) {
11564
+ logger.error(`Invalid --port "${value}" (expected an integer 1-${MAX_PORT}).`);
11565
+ process.exit(1);
11566
+ }
11567
+ return port;
11568
+ };
11569
+
11351
11570
  // src/cli/commands/dev.ts
11352
11571
  var devCommand = defineCommand4({
11353
11572
  args: {
@@ -11376,7 +11595,8 @@ var devCommand = defineCommand4({
11376
11595
  const root = process.cwd();
11377
11596
  const preview = args.preview ?? false;
11378
11597
  const overrides = args["content-dir"] ? { contentRoot: args["content-dir"] } : undefined;
11379
- const port = args.port ? Number(args.port) : 4321;
11598
+ const explicitPort = parsePort(args.port);
11599
+ const port = explicitPort ?? 4321;
11380
11600
  const devServerUrl = `http://localhost:${port}`;
11381
11601
  const project = await prepareProject({
11382
11602
  devServerUrl,
@@ -11389,13 +11609,15 @@ var devCommand = defineCommand4({
11389
11609
  if (project.bridge) {
11390
11610
  logger.info('Detected docs.json — running in Mintlify bridge mode (no migration). Run "blume migrate mintlify" to convert permanently.');
11391
11611
  }
11612
+ const releaseLock = acquireDevLock(project.context.outDir);
11613
+ process.on("exit", releaseLock);
11392
11614
  const server = await dev({
11393
11615
  logLevel: args.debug ? "debug" : "info",
11394
11616
  root: project.context.outDir,
11395
11617
  server: {
11396
11618
  host: args.host ?? false,
11397
11619
  open: args.open ?? false,
11398
- port: args.port ? Number(args.port) : undefined
11620
+ port: explicitPort
11399
11621
  }
11400
11622
  });
11401
11623
  showBlumeErrorOverlay(project.diagnostics);
@@ -11436,6 +11658,7 @@ var devCommand = defineCommand4({
11436
11658
  for (const dispose of disposers) {
11437
11659
  dispose();
11438
11660
  }
11661
+ releaseLock();
11439
11662
  await server.stop();
11440
11663
  process.exit(0);
11441
11664
  };
@@ -11505,6 +11728,7 @@ var doctorCommand = defineCommand5({
11505
11728
  }
11506
11729
  if (args.json) {
11507
11730
  if (reportDiagnosticsJson(diagnostics, root)) {
11731
+ await flushStdout();
11508
11732
  process.exit(1);
11509
11733
  }
11510
11734
  return;
@@ -11522,12 +11746,12 @@ var doctorCommand = defineCommand5({
11522
11746
  // src/cli/commands/eject.ts
11523
11747
  import { readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
11524
11748
  import { defineCommand as defineCommand6 } from "citty";
11525
- import { join as join27, relative as relative15 } from "pathe";
11749
+ import { join as join30, relative as relative17 } from "pathe";
11526
11750
 
11527
11751
  // src/registry/eject.ts
11528
- import { existsSync as existsSync17 } from "node:fs";
11752
+ import { existsSync as existsSync18 } from "node:fs";
11529
11753
  import { cp, mkdir as mkdir5, readFile as readFile14, rm as rm2, writeFile as writeFile7 } from "node:fs/promises";
11530
- import { join as join26, relative as relative14 } from "pathe";
11754
+ import { join as join29, relative as relative16 } from "pathe";
11531
11755
  var POSIX = (path) => path.split("\\").join("/");
11532
11756
  var askFiles = async (project, srcDir, genDir) => {
11533
11757
  const { ask } = project.config.ai;
@@ -11538,14 +11762,14 @@ var askFiles = async (project, srcDir, genDir) => {
11538
11762
  const files = [
11539
11763
  {
11540
11764
  content: askEndpointTemplate(resolveAskBackend(ask), grounded),
11541
- path: join26(srcDir, "pages", "api", "ask.ts")
11765
+ path: join29(srcDir, "pages", "api", "ask.ts")
11542
11766
  }
11543
11767
  ];
11544
11768
  if (grounded) {
11545
11769
  files.push({
11546
11770
  content: `${JSON.stringify(await buildAskData(project))}
11547
11771
  `,
11548
- path: join26(genDir, "ask-data.json")
11772
+ path: join29(genDir, "ask-data.json")
11549
11773
  });
11550
11774
  }
11551
11775
  return files;
@@ -11553,8 +11777,8 @@ var askFiles = async (project, srcDir, genDir) => {
11553
11777
  var eject = async (root) => {
11554
11778
  const project = await scanProject(root, { mode: "build" });
11555
11779
  const { context, config } = project;
11556
- const srcDir = join26(root, "src");
11557
- const genDir = join26(srcDir, "generated");
11780
+ const srcDir = join29(root, "src");
11781
+ const genDir = join29(srcDir, "generated");
11558
11782
  const askEnabled = config.ai.ask?.enabled ?? false;
11559
11783
  const exportPdf = config.export.pdf;
11560
11784
  const exportEpub = config.export.epub;
@@ -11575,13 +11799,13 @@ var eject = async (root) => {
11575
11799
  const needsSvelte = frameworks.has("svelte");
11576
11800
  const relContext = {
11577
11801
  ...context,
11578
- contentRoot: POSIX(relative14(root, context.contentRoot)),
11802
+ contentRoot: POSIX(relative16(root, context.contentRoot)),
11579
11803
  outDir: ".",
11580
11804
  root: "."
11581
11805
  };
11582
- const componentsImport = context.componentsFile ? `../../${POSIX(relative14(root, context.componentsFile))}` : null;
11806
+ const componentsImport = context.componentsFile ? `../../${POSIX(relative16(root, context.componentsFile))}` : null;
11583
11807
  const relPages = pages.map((page) => ({
11584
- entrypoint: POSIX(relative14(root, page.entrypoint)),
11808
+ entrypoint: POSIX(relative16(root, page.entrypoint)),
11585
11809
  pattern: page.pattern
11586
11810
  }));
11587
11811
  const staged = collectStaged(project);
@@ -11602,13 +11826,14 @@ var eject = async (root) => {
11602
11826
  searchClientPath: "./src/generated/search-client.ts",
11603
11827
  themePath: "./src/generated/app.css"
11604
11828
  }),
11605
- path: join26(root, "astro.config.mjs")
11829
+ path: join29(root, "astro.config.mjs")
11606
11830
  },
11607
11831
  {
11608
11832
  content: runtimeTsconfigTemplate(),
11609
- path: join26(root, "tsconfig.json")
11833
+ path: join29(root, "tsconfig.json"),
11834
+ skipIfExists: true
11610
11835
  },
11611
- { content: envTemplate(), path: join26(srcDir, "env.d.ts") },
11836
+ { content: envTemplate(), path: join29(srcDir, "env.d.ts") },
11612
11837
  {
11613
11838
  content: contentConfigTemplate({
11614
11839
  config,
@@ -11616,7 +11841,7 @@ var eject = async (root) => {
11616
11841
  staged: hasStaged,
11617
11842
  stagedBase: stagedDir
11618
11843
  }),
11619
- path: join26(srcDir, "content.config.ts")
11844
+ path: join29(srcDir, "content.config.ts")
11620
11845
  },
11621
11846
  {
11622
11847
  content: catchAllPageTemplate({
@@ -11626,19 +11851,19 @@ var eject = async (root) => {
11626
11851
  mathEnabled: config.markdown.math,
11627
11852
  needsReact
11628
11853
  }),
11629
- path: join26(srcDir, "pages", "[...slug].astro")
11854
+ path: join29(srcDir, "pages", "[...slug].astro")
11630
11855
  },
11631
11856
  {
11632
11857
  content: planComponentSlots(componentsImport, null).module,
11633
- path: join26(genDir, "components.ts")
11858
+ path: join29(genDir, "components.ts")
11634
11859
  },
11635
11860
  {
11636
11861
  content: islandMapTemplate(islands.islands),
11637
- path: join26(genDir, "islands.ts")
11862
+ path: join29(genDir, "islands.ts")
11638
11863
  },
11639
11864
  {
11640
11865
  content: exampleMapTemplate(examples.examples),
11641
- path: join26(genDir, "examples.ts")
11866
+ path: join29(genDir, "examples.ts")
11642
11867
  },
11643
11868
  {
11644
11869
  content: tailwindEntryTemplate({
@@ -11650,21 +11875,21 @@ var eject = async (root) => {
11650
11875
  twoslashCss: twoslashCss(),
11651
11876
  userTheme
11652
11877
  }),
11653
- path: join26(genDir, "app.css")
11878
+ path: join29(genDir, "app.css")
11654
11879
  },
11655
- { content: buildRuntimeData(project), path: join26(genDir, "data.json") },
11880
+ { content: buildRuntimeData(project), path: join29(genDir, "data.json") },
11656
11881
  {
11657
11882
  content: `${JSON.stringify(rawMarkdown)}
11658
11883
  `,
11659
- path: join26(genDir, "raw-markdown.json")
11884
+ path: join29(genDir, "raw-markdown.json")
11660
11885
  },
11661
11886
  {
11662
11887
  content: rawMarkdownEndpointTemplate(),
11663
- path: join26(srcDir, "pages", "[...slug].md.ts")
11888
+ path: join29(srcDir, "pages", "[...slug].md.ts")
11664
11889
  },
11665
11890
  {
11666
11891
  content: rawMarkdownEndpointTemplate(),
11667
- path: join26(srcDir, "pages", "[...slug].mdx.ts")
11892
+ path: join29(srcDir, "pages", "[...slug].mdx.ts")
11668
11893
  }
11669
11894
  ];
11670
11895
  if (askEnabled) {
@@ -11673,34 +11898,34 @@ var eject = async (root) => {
11673
11898
  if (config.seo.og.enabled) {
11674
11899
  files.push({
11675
11900
  content: ogEndpointTemplate(customOgRoutes(pages, config.title)),
11676
- path: join26(srcDir, "pages", "og", "[...slug].png.ts")
11901
+ path: join29(srcDir, "pages", "og", "[...slug].png.ts")
11677
11902
  });
11678
11903
  }
11679
11904
  if (!routeIsTaken(pages, project.graph.pages, "/404")) {
11680
11905
  files.push({
11681
11906
  content: notFoundPageTemplate(),
11682
- path: join26(srcDir, "pages", "404.astro")
11907
+ path: join29(srcDir, "pages", "404.astro")
11683
11908
  });
11684
11909
  }
11685
11910
  files.push({
11686
11911
  content: searchClientTemplate(config),
11687
- path: join26(genDir, "search-client.ts")
11912
+ path: join29(genDir, "search-client.ts")
11688
11913
  });
11689
11914
  if (servesStaticIndex(config.search.provider)) {
11690
11915
  const documents = await buildSearchDocuments(project);
11691
11916
  files.push({
11692
11917
  content: `${JSON.stringify(documents)}
11693
11918
  `,
11694
- path: join26(genDir, "search.json")
11919
+ path: join29(genDir, "search.json")
11695
11920
  }, {
11696
11921
  content: searchEndpointTemplate(),
11697
- path: join26(srcDir, "pages", "blume-search.json.ts")
11922
+ path: join29(srcDir, "pages", "blume-search.json.ts")
11698
11923
  });
11699
11924
  }
11700
11925
  if (config.search.provider === "mixedbread") {
11701
11926
  files.push({
11702
11927
  content: mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""),
11703
- path: join26(srcDir, "pages", "api", "search.ts")
11928
+ path: join29(srcDir, "pages", "api", "search.ts")
11704
11929
  });
11705
11930
  }
11706
11931
  const feeds = buildRssFeeds(project);
@@ -11709,10 +11934,10 @@ var eject = async (root) => {
11709
11934
  files.push({
11710
11935
  content: `${JSON.stringify(feedXml)}
11711
11936
  `,
11712
- path: join26(genDir, "rss.json")
11937
+ path: join29(genDir, "rss.json")
11713
11938
  }, {
11714
11939
  content: rssEndpointTemplate(),
11715
- path: join26(srcDir, "pages", "[section]", "rss.xml.ts")
11940
+ path: join29(srcDir, "pages", "[section]", "rss.xml.ts")
11716
11941
  });
11717
11942
  }
11718
11943
  if (hasReferences(config)) {
@@ -11724,37 +11949,38 @@ var eject = async (root) => {
11724
11949
  for (const file of references.files) {
11725
11950
  files.push({
11726
11951
  content: file.content,
11727
- path: join26(srcDir, "pages", file.pagePath)
11952
+ path: join29(srcDir, "pages", file.pagePath)
11728
11953
  });
11729
11954
  }
11730
11955
  }
11731
11956
  files.push(...islands.islands.map((island) => ({
11732
11957
  content: islandWrapperTemplate(island),
11733
- path: join26(genDir, "islands", `${island.name}.astro`)
11958
+ path: join29(genDir, "islands", `${island.name}.astro`)
11734
11959
  })), ...examples.examples.map((example) => ({
11735
11960
  content: exampleWrapperTemplate(example),
11736
- path: join26(genDir, "examples", `${exampleSlug(example.path)}.astro`)
11961
+ path: join29(genDir, "examples", `${exampleSlug(example.path)}.astro`)
11737
11962
  })));
11738
11963
  for (const [entryId, content] of staged) {
11739
- files.push({ content, path: join26(root, stagedDir, entryId) });
11964
+ files.push({ content, path: join29(root, stagedDir, entryId) });
11740
11965
  }
11741
- await Promise.all(files.map(async (file) => {
11742
- await mkdir5(join26(file.path, ".."), { recursive: true });
11966
+ const written = files.filter((file) => !(file.skipIfExists && existsSync18(file.path)));
11967
+ await Promise.all(written.map(async (file) => {
11968
+ await mkdir5(join29(file.path, ".."), { recursive: true });
11743
11969
  await writeFile7(file.path, file.content, "utf-8");
11744
11970
  }));
11745
- const assetsSrc = join26(context.outDir, "public", "blume-assets");
11746
- if (existsSync17(assetsSrc)) {
11747
- await cp(assetsSrc, join26(root, "public", "blume-assets"), {
11971
+ const assetsSrc = join29(context.outDir, "public", "blume-assets");
11972
+ if (existsSync18(assetsSrc)) {
11973
+ await cp(assetsSrc, join29(root, "public", "blume-assets"), {
11748
11974
  recursive: true
11749
11975
  });
11750
11976
  }
11751
11977
  await rm2(context.outDir, { force: true, recursive: true });
11752
- return files.map((file) => file.path);
11978
+ return written.map((file) => file.path);
11753
11979
  };
11754
11980
 
11755
11981
  // src/cli/commands/eject.ts
11756
11982
  var updatePackageScripts = async (root) => {
11757
- const pkgPath = join27(root, "package.json");
11983
+ const pkgPath = join30(root, "package.json");
11758
11984
  let pkg;
11759
11985
  try {
11760
11986
  pkg = JSON.parse(await readFile15(pkgPath, "utf-8"));
@@ -11781,8 +12007,9 @@ var ejectCommand = defineCommand6({
11781
12007
  },
11782
12008
  async run({ args }) {
11783
12009
  const root = process.cwd();
12010
+ refuseIfDevRunning(root, "ejecting");
11784
12011
  if (!args.yes) {
11785
- logger.warn("Eject is one-way: it writes astro.config.mjs and src/ into your project and removes .blume.");
12012
+ logger.warn("Eject is one-way: it writes astro.config.mjs, src/, and (if absent) tsconfig.json, rewrites your package.json scripts, and removes .blume. An existing tsconfig.json is left untouched.");
11786
12013
  logger.info("Re-run with --yes to proceed.");
11787
12014
  return;
11788
12015
  }
@@ -11790,7 +12017,7 @@ var ejectCommand = defineCommand6({
11790
12017
  await updatePackageScripts(root);
11791
12018
  logger.success(`Ejected ${files.length} file(s):`);
11792
12019
  for (const file of files) {
11793
- process.stdout.write(` ${relative15(root, file)}
12020
+ process.stdout.write(` ${relative17(root, file)}
11794
12021
  `);
11795
12022
  }
11796
12023
  logger.box(`Your project is now a standalone Astro app.
@@ -11803,10 +12030,35 @@ The blume package remains importable.`);
11803
12030
  });
11804
12031
 
11805
12032
  // src/cli/commands/init.ts
11806
- import { existsSync as existsSync18 } from "node:fs";
11807
- import { mkdir as mkdir6, writeFile as writeFile9 } from "node:fs/promises";
12033
+ import { existsSync as existsSync20 } from "node:fs";
12034
+ import { mkdir as mkdir6, writeFile as writeFile10 } from "node:fs/promises";
11808
12035
  import { defineCommand as defineCommand7 } from "citty";
11809
- import { basename as basename4, dirname as dirname13, join as join28 } from "pathe";
12036
+ import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute8, join as join32, relative as relative18 } from "pathe";
12037
+
12038
+ // src/core/gitignore.ts
12039
+ import { existsSync as existsSync19 } from "node:fs";
12040
+ import { readFile as readFile16, writeFile as writeFile9 } from "node:fs/promises";
12041
+ import { join as join31 } from "pathe";
12042
+ var gitignoreKey = (line) => line.trim().replace(/\/+$/u, "");
12043
+ var ensureGitignore = async (root, entries) => {
12044
+ const path = join31(root, ".gitignore");
12045
+ const existing = existsSync19(path) ? await readFile16(path, "utf-8") : "";
12046
+ const present = new Set(existing.split(`
12047
+ `).map(gitignoreKey).filter(Boolean));
12048
+ const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
12049
+ if (added.length === 0) {
12050
+ return [];
12051
+ }
12052
+ const gap = existing.length > 0 && !existing.endsWith(`
12053
+ `) ? `
12054
+ ` : "";
12055
+ await writeFile9(path, `${existing}${gap}${added.join(`
12056
+ `)}
12057
+ `, "utf-8");
12058
+ return added;
12059
+ };
12060
+
12061
+ // src/cli/commands/init.ts
11810
12062
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
11811
12063
  var packageTemplate = (name, version) => `{
11812
12064
  "name": ${JSON.stringify(name)},
@@ -11854,7 +12106,7 @@ var STARTERS = {
11854
12106
  files: (dir) => [
11855
12107
  {
11856
12108
  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")
12109
+ path: join32(dir, "index.mdx")
11858
12110
  }
11859
12111
  ]
11860
12112
  },
@@ -11869,7 +12121,7 @@ var STARTERS = {
11869
12121
  files: (dir) => [
11870
12122
  {
11871
12123
  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")
12124
+ path: join32(dir, "index.mdx")
11873
12125
  },
11874
12126
  {
11875
12127
  content: `---
@@ -11880,7 +12132,7 @@ date: 2026-01-01
11880
12132
 
11881
12133
  The first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries beside it.
11882
12134
  `,
11883
- path: join28(dir, "changelog", "v1-0-0.mdx")
12135
+ path: join32(dir, "changelog", "v1-0-0.mdx")
11884
12136
  }
11885
12137
  ]
11886
12138
  },
@@ -11893,7 +12145,7 @@ The first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries besid
11893
12145
  Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
11894
12146
 
11895
12147
  Edit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`),
11896
- path: join28(dir, "index.mdx")
12148
+ path: join32(dir, "index.mdx")
11897
12149
  }
11898
12150
  ]
11899
12151
  },
@@ -11904,11 +12156,11 @@ Edit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`),
11904
12156
  content: page("Introduction", "Get started with the SDK.", `# Introduction
11905
12157
 
11906
12158
  Install the SDK and make your first call. See [Installation](/installation).`),
11907
- path: join28(dir, "index.mdx")
12159
+ path: join32(dir, "index.mdx")
11908
12160
  },
11909
12161
  {
11910
12162
  content: page("Installation", "Install the SDK.", "# Installation\n\n```package-install\nyour-sdk\n```"),
11911
- path: join28(dir, "installation.mdx")
12163
+ path: join32(dir, "installation.mdx")
11912
12164
  }
11913
12165
  ]
11914
12166
  }
@@ -11918,12 +12170,12 @@ var commandsFor = (pm) => ({
11918
12170
  install: `${pm} install`
11919
12171
  });
11920
12172
  var writeFileSafe = async (path, content) => {
11921
- if (existsSync18(path)) {
12173
+ if (existsSync20(path)) {
11922
12174
  logger.info(`Skipped existing ${path}`);
11923
12175
  return false;
11924
12176
  }
11925
12177
  await mkdir6(dirname13(path), { recursive: true });
11926
- await writeFile9(path, content, "utf-8");
12178
+ await writeFile10(path, content, "utf-8");
11927
12179
  logger.success(`Created ${path}`);
11928
12180
  return true;
11929
12181
  };
@@ -11955,6 +12207,10 @@ var initCommand = defineCommand7({
11955
12207
  async run({ args }) {
11956
12208
  const root = process.cwd();
11957
12209
  const contentDir = args["content-dir"] ?? "docs";
12210
+ if (isAbsolute8(contentDir) || relative18(root, join32(root, contentDir)).startsWith("..")) {
12211
+ logger.error(`Invalid --content-dir "${contentDir}" (must be a path inside the project).`);
12212
+ process.exit(1);
12213
+ }
11958
12214
  const template = args.template ?? "docs";
11959
12215
  if (!TEMPLATES.includes(template)) {
11960
12216
  logger.error(`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`);
@@ -11966,9 +12222,13 @@ var initCommand = defineCommand7({
11966
12222
  process.exit(1);
11967
12223
  }
11968
12224
  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)));
12225
+ const createdPackage = await writeFileSafe(join32(root, "package.json"), packageTemplate(toPackageName(basename4(root)), getBlumeVersion()));
12226
+ await writeFileSafe(join32(root, "blume.config.ts"), starter.config);
12227
+ await Promise.all(starter.files(contentDir).map((file) => writeFileSafe(join32(root, file.path), file.content)));
12228
+ const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
12229
+ if (ignored.length > 0) {
12230
+ logger.success(`Added ${ignored.join(", ")} to .gitignore`);
12231
+ }
11972
12232
  const commands = commandsFor(pm);
11973
12233
  if (args.eject) {
11974
12234
  try {
@@ -12005,15 +12265,15 @@ var initCommand = defineCommand7({
12005
12265
  import { defineCommand as defineCommand8 } from "citty";
12006
12266
 
12007
12267
  // src/migrate/fumadocs/index.ts
12008
- import { existsSync as existsSync22 } from "node:fs";
12009
- import { mkdir as mkdir8, readFile as readFile17, rename as rename3, rm as rm3, writeFile as writeFile11 } from "node:fs/promises";
12010
- import { dirname as dirname16, join as join31, relative as relative16 } from "pathe";
12268
+ import { existsSync as existsSync24 } from "node:fs";
12269
+ import { mkdir as mkdir8, readFile as readFile18, rename as rename3, rm as rm3, writeFile as writeFile12 } from "node:fs/promises";
12270
+ import { dirname as dirname16, join as join35, relative as relative19 } from "pathe";
12011
12271
  import { glob as glob8 } from "tinyglobby";
12012
12272
 
12013
12273
  // src/migrate/fumadocs/config.ts
12014
- import { existsSync as existsSync19 } from "node:fs";
12015
- import { readFile as readFile16 } from "node:fs/promises";
12016
- import { basename as basename5, dirname as dirname14, join as join29 } from "pathe";
12274
+ import { existsSync as existsSync21 } from "node:fs";
12275
+ import { readFile as readFile17 } from "node:fs/promises";
12276
+ import { basename as basename5, dirname as dirname14, join as join33 } from "pathe";
12017
12277
  var SOURCE_FILES = [
12018
12278
  "lib/source.ts",
12019
12279
  "app/source.ts",
@@ -12043,7 +12303,7 @@ var GENERIC_NAMES = new Set([
12043
12303
  var gitRepoRoot = (start) => {
12044
12304
  let dir = start;
12045
12305
  for (;; ) {
12046
- if (existsSync19(join29(dir, ".git"))) {
12306
+ if (existsSync21(join33(dir, ".git"))) {
12047
12307
  return dir;
12048
12308
  }
12049
12309
  const parent = dirname14(dir);
@@ -12055,12 +12315,12 @@ var gitRepoRoot = (start) => {
12055
12315
  };
12056
12316
  var bareName = (name) => name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
12057
12317
  var readTitle = async (root) => {
12058
- const packageJson = join29(root, "package.json");
12059
- if (!existsSync19(packageJson)) {
12318
+ const packageJson = join33(root, "package.json");
12319
+ if (!existsSync21(packageJson)) {
12060
12320
  return "Documentation";
12061
12321
  }
12062
12322
  try {
12063
- const parsed = JSON.parse(await readFile16(packageJson, "utf-8"));
12323
+ const parsed = JSON.parse(await readFile17(packageJson, "utf-8"));
12064
12324
  const { name } = parsed;
12065
12325
  if (typeof name !== "string" || !name.trim()) {
12066
12326
  return "Documentation";
@@ -12081,11 +12341,11 @@ var readTitle = async (root) => {
12081
12341
  };
12082
12342
  var scrapeBaseUrl = async (root) => {
12083
12343
  for (const candidate of SOURCE_FILES) {
12084
- const file = join29(root, candidate);
12085
- if (!existsSync19(file)) {
12344
+ const file = join33(root, candidate);
12345
+ if (!existsSync21(file)) {
12086
12346
  continue;
12087
12347
  }
12088
- const base = BASE_URL.exec(await readFile16(file, "utf-8"))?.groups?.base;
12348
+ const base = BASE_URL.exec(await readFile17(file, "utf-8"))?.groups?.base;
12089
12349
  if (base) {
12090
12350
  return base;
12091
12351
  }
@@ -12114,9 +12374,9 @@ var loadFumadocsConfig = async (root) => {
12114
12374
  };
12115
12375
 
12116
12376
  // src/migrate/fumadocs/content.ts
12117
- import { existsSync as existsSync20 } from "node:fs";
12377
+ import { existsSync as existsSync22 } from "node:fs";
12118
12378
  import { readFile as readFileFromDisk2 } from "node:fs/promises";
12119
- import { dirname as dirname15, resolve as resolve10 } from "pathe";
12379
+ import { dirname as dirname15, resolve as resolve11 } from "pathe";
12120
12380
  var FUMADOCS_IMPORT = /^import\s+[\s\S]*?\s+from\s+["']fumadocs-(?:ui|core|mdx)(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
12121
12381
  var stripFumadocsImports = (source) => {
12122
12382
  const stripped = source.replace(FUMADOCS_IMPORT, "");
@@ -12317,12 +12577,16 @@ var inlineFumadocsIncludes = async (source, options) => {
12317
12577
  if (!rawPath) {
12318
12578
  continue;
12319
12579
  }
12320
- const target = resolve10(dirname15(options.filePath), rawPath);
12580
+ const target = resolve11(dirname15(options.filePath), rawPath);
12581
+ if (!isInsideRoot2(options.root, target)) {
12582
+ warnings.push(`<include> target "${rawPath}" is outside the docs tree — left as-is.`);
12583
+ continue;
12584
+ }
12321
12585
  if (seen.has(target)) {
12322
12586
  warnings.push(`Circular <include> "${rawPath}" — left as-is.`);
12323
12587
  continue;
12324
12588
  }
12325
- if (!existsSync20(target)) {
12589
+ if (!existsSync22(target)) {
12326
12590
  warnings.push(`<include> target "${rawPath}" not found — left as-is.`);
12327
12591
  continue;
12328
12592
  }
@@ -12354,9 +12618,9 @@ var normalizeFumadocsPageMeta = (value) => {
12354
12618
  };
12355
12619
 
12356
12620
  // src/migrate/fumadocs/groups.ts
12357
- import { existsSync as existsSync21, statSync as statSync2 } from "node:fs";
12358
- import { mkdir as mkdir7, rename as rename2, writeFile as writeFile10 } from "node:fs/promises";
12359
- import { basename as basename6, join as join30 } from "pathe";
12621
+ import { existsSync as existsSync23, statSync as statSync2 } from "node:fs";
12622
+ import { mkdir as mkdir7, rename as rename2, writeFile as writeFile11 } from "node:fs/promises";
12623
+ import { basename as basename6, join as join34 } from "pathe";
12360
12624
 
12361
12625
  // src/migrate/fumadocs/meta.ts
12362
12626
  var SEPARATOR = /^---(?<label>.*)---$/u;
@@ -12501,13 +12765,16 @@ var isDirectory = (path) => {
12501
12765
  }
12502
12766
  };
12503
12767
  var resolveEntry = (docsDir, name) => {
12768
+ if (!isInsideRoot2(docsDir, join34(docsDir, name))) {
12769
+ return null;
12770
+ }
12504
12771
  for (const ext of PAGE_EXTS) {
12505
- const file = join30(docsDir, `${name}${ext}`);
12506
- if (existsSync21(file)) {
12772
+ const file = join34(docsDir, `${name}${ext}`);
12773
+ if (existsSync23(file)) {
12507
12774
  return { kind: "file", path: file };
12508
12775
  }
12509
12776
  }
12510
- const folder = join30(docsDir, name);
12777
+ const folder = join34(docsDir, name);
12511
12778
  return isDirectory(folder) ? { kind: "folder", path: folder } : null;
12512
12779
  };
12513
12780
  var humanize2 = (name) => name.split(WORD_SPLIT3).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
@@ -12542,8 +12809,8 @@ var moveItemIntoGroup = async (item, docsDir, groupDir, label, warnings) => {
12542
12809
  warnings.push(`Sidebar entry "${item.name}" in section "${label}" matched no page or folder; skipped.`);
12543
12810
  return null;
12544
12811
  }
12545
- const dest = join30(groupDir, basename6(resolved.path));
12546
- if (existsSync21(dest)) {
12812
+ const dest = join34(groupDir, basename6(resolved.path));
12813
+ if (existsSync23(dest)) {
12547
12814
  warnings.push(`Skipped moving "${item.name}" into section "${label}" (target already exists).`);
12548
12815
  return null;
12549
12816
  }
@@ -12570,7 +12837,7 @@ var reshapeSection = async (section, docsDir, order, warnings) => {
12570
12837
  }
12571
12838
  return;
12572
12839
  }
12573
- const groupDir = join30(docsDir, `(${section.label})`);
12840
+ const groupDir = join34(docsDir, `(${section.label})`);
12574
12841
  const sectionKeys = [];
12575
12842
  for (const item of section.items) {
12576
12843
  const key = await moveItemIntoGroup(item, docsDir, groupDir, section.label, warnings);
@@ -12583,7 +12850,7 @@ var reshapeSection = async (section, docsDir, order, warnings) => {
12583
12850
  }
12584
12851
  order.push(section.label);
12585
12852
  if (sectionKeys.length > 1) {
12586
- await writeFile10(join30(groupDir, "meta.ts"), renderMetaModule({ pages: sectionKeys }), "utf-8");
12853
+ await writeFile11(join34(groupDir, "meta.ts"), renderMetaModule({ pages: sectionKeys }), "utf-8");
12587
12854
  }
12588
12855
  };
12589
12856
  var reshapeFumadocsGroups = async (structure, docsDir) => {
@@ -12615,9 +12882,9 @@ var FUMADOCS_LEFTOVERS = [
12615
12882
  "app"
12616
12883
  ];
12617
12884
  var movePage = async (abs, base, root) => {
12618
- const rel = relative16(base, abs);
12619
- const dest = join31(root, "docs", rel);
12620
- if (existsSync22(dest)) {
12885
+ const rel = relative19(base, abs);
12886
+ const dest = join35(root, "docs", rel);
12887
+ if (existsSync24(dest)) {
12621
12888
  return {
12622
12889
  includeWarnings: [],
12623
12890
  moved: 0,
@@ -12626,8 +12893,11 @@ var movePage = async (abs, base, root) => {
12626
12893
  unsupported: []
12627
12894
  };
12628
12895
  }
12629
- const raw = await readFile17(abs, "utf-8");
12630
- const included = await inlineFumadocsIncludes(raw, { filePath: abs });
12896
+ const raw = await readFile18(abs, "utf-8");
12897
+ const included = await inlineFumadocsIncludes(raw, {
12898
+ filePath: abs,
12899
+ root: base
12900
+ });
12631
12901
  let text = stripFumadocsImports(included.content);
12632
12902
  text = rewriteFumadocsCallouts(text);
12633
12903
  text = rewriteFumadocsContainers(text);
@@ -12637,7 +12907,7 @@ var movePage = async (abs, base, root) => {
12637
12907
  const { data, removed } = normalizeFumadocsPageMeta(parsed.data);
12638
12908
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
12639
12909
  await mkdir8(dirname16(dest), { recursive: true });
12640
- await writeFile11(dest, content, "utf-8");
12910
+ await writeFile12(dest, content, "utf-8");
12641
12911
  await rm3(abs, { force: true });
12642
12912
  return {
12643
12913
  includeWarnings: included.warnings,
@@ -12651,22 +12921,22 @@ var writeMeta = async (dest, meta, rel, warnings) => {
12651
12921
  if (Object.keys(meta).length === 0) {
12652
12922
  return warnings;
12653
12923
  }
12654
- if (existsSync22(dest)) {
12924
+ if (existsSync24(dest)) {
12655
12925
  return [...warnings, `Skipped ${rel} (target already exists)`];
12656
12926
  }
12657
12927
  await mkdir8(dirname16(dest), { recursive: true });
12658
- await writeFile11(dest, renderMetaModule(meta), "utf-8");
12928
+ await writeFile12(dest, renderMetaModule(meta), "utf-8");
12659
12929
  return warnings;
12660
12930
  };
12661
12931
  var convertMeta = async (abs, base, root) => {
12662
- const rel = relative16(base, abs);
12663
- const raw = await readFile17(abs, "utf-8");
12932
+ const rel = relative19(base, abs);
12933
+ const raw = await readFile18(abs, "utf-8");
12664
12934
  let parsed;
12665
12935
  try {
12666
12936
  parsed = JSON.parse(raw);
12667
12937
  } catch {
12668
- const dest2 = join31(root, "docs", rel);
12669
- if (existsSync22(dest2)) {
12938
+ const dest2 = join35(root, "docs", rel);
12939
+ if (existsSync24(dest2)) {
12670
12940
  return [`Skipped ${rel} (target already exists)`];
12671
12941
  }
12672
12942
  await mkdir8(dirname16(dest2), { recursive: true });
@@ -12676,10 +12946,10 @@ var convertMeta = async (abs, base, root) => {
12676
12946
  ];
12677
12947
  }
12678
12948
  const dir = dirname16(rel) === "." ? "" : dirname16(rel);
12679
- const docsDir = join31(root, "docs", dir);
12680
- const dest = join31(docsDir, "meta.ts");
12949
+ const docsDir = join35(root, "docs", dir);
12950
+ const dest = join35(docsDir, "meta.ts");
12681
12951
  const structure = parseFumadocsPages(parsed.pages);
12682
- if (structure.hasSections && !existsSync22(dest)) {
12952
+ if (structure.hasSections && !existsSync24(dest)) {
12683
12953
  const self = translateFumadocsSelfMeta(parsed);
12684
12954
  const reshape = await reshapeFumadocsGroups(structure, docsDir);
12685
12955
  const meta2 = { ...self.meta };
@@ -12726,8 +12996,8 @@ var summarizePages = (results) => {
12726
12996
  };
12727
12997
  };
12728
12998
  var cleanupSourceDirs = async (root) => {
12729
- const docs = join31(root, "content", "docs");
12730
- if (existsSync22(docs)) {
12999
+ const docs = join35(root, "content", "docs");
13000
+ if (existsSync24(docs)) {
12731
13001
  const remaining = await glob8(["**/*"], { cwd: docs, dot: true });
12732
13002
  if (remaining.length > 0) {
12733
13003
  return [
@@ -12736,8 +13006,8 @@ var cleanupSourceDirs = async (root) => {
12736
13006
  }
12737
13007
  await rm3(docs, { force: true, recursive: true });
12738
13008
  }
12739
- const content = join31(root, "content");
12740
- if (existsSync22(content)) {
13009
+ const content = join35(root, "content");
13010
+ if (existsSync24(content)) {
12741
13011
  const remaining = await glob8(["**/*"], { cwd: content, dot: true });
12742
13012
  if (remaining.length === 0) {
12743
13013
  await rm3(content, { force: true, recursive: true });
@@ -12747,8 +13017,8 @@ var cleanupSourceDirs = async (root) => {
12747
13017
  };
12748
13018
  var migrateFumadocsProject = async (root) => {
12749
13019
  const { config, warnings: configWarnings } = await loadFumadocsConfig(root);
12750
- const base = join31(root, SOURCE_DIR);
12751
- if (!existsSync22(base)) {
13020
+ const base = join35(root, SOURCE_DIR);
13021
+ if (!existsSync24(base)) {
12752
13022
  await writeBlumeConfig(root, config);
12753
13023
  return {
12754
13024
  moved: 0,
@@ -12806,9 +13076,9 @@ var migrateFumadocsProject = async (root) => {
12806
13076
  };
12807
13077
 
12808
13078
  // src/migrate/mintlify/index.ts
12809
- import { existsSync as existsSync23 } from "node:fs";
12810
- import { mkdir as mkdir9, readFile as readFile18, rename as rename4, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
12811
- import { dirname as dirname17, join as join32 } from "pathe";
13079
+ import { existsSync as existsSync25 } from "node:fs";
13080
+ import { mkdir as mkdir9, readFile as readFile19, rename as rename4, rm as rm4, stat as stat2, writeFile as writeFile13 } from "node:fs/promises";
13081
+ import { dirname as dirname17, join as join36 } from "pathe";
12812
13082
  import { glob as glob9 } from "tinyglobby";
12813
13083
  var prune = (value) => {
12814
13084
  if (Array.isArray(value)) {
@@ -12838,38 +13108,48 @@ var writeBlumeConfig2 = async (root, config) => {
12838
13108
 
12839
13109
  export default defineConfig(${JSON.stringify(prune(config), null, 2)});
12840
13110
  `;
12841
- await writeFile12(join32(root, "blume.config.ts"), body, "utf-8");
13111
+ await writeFile13(join36(root, "blume.config.ts"), body, "utf-8");
12842
13112
  };
12843
- var relocateAssets = async (root, refs) => {
12844
- const segments = new Set;
12845
- for (const ref of refs) {
12846
- if (typeof ref !== "string" || !ref.startsWith("/")) {
12847
- continue;
12848
- }
12849
- const [segment] = ref.replace(/^\/+/u, "").split("/");
12850
- if (segment) {
12851
- segments.add(segment);
12852
- }
12853
- }
13113
+ var relocateAssets = async (root, segments) => {
13114
+ const served = [];
12854
13115
  const moved = [];
12855
13116
  for (const segment of segments) {
12856
- const source = join32(root, segment);
12857
- if (!existsSync23(source) || segment === "public") {
13117
+ const source = join36(root, segment);
13118
+ if (!existsSync25(source) || segment === "public") {
12858
13119
  continue;
12859
13120
  }
12860
- const dest = join32(root, "public", segment);
12861
- if (existsSync23(dest)) {
13121
+ const stats = await stat2(source);
13122
+ if (stats.isDirectory()) {
13123
+ served.push(segment);
12862
13124
  continue;
12863
13125
  }
12864
- await mkdir9(join32(root, "public"), { recursive: true });
13126
+ const dest = join36(root, "public", segment);
13127
+ if (existsSync25(dest)) {
13128
+ continue;
13129
+ }
13130
+ await mkdir9(join36(root, "public"), { recursive: true });
12865
13131
  await rename4(source, dest);
12866
13132
  moved.push(segment);
12867
13133
  }
12868
- return moved;
13134
+ return { moved, served };
13135
+ };
13136
+ var applyRelocatedAssets = (config, assets, warnings) => {
13137
+ if (assets.served.length > 0) {
13138
+ config.content = {
13139
+ ...config.content,
13140
+ assets: [
13141
+ ...new Set([...config.content?.assets ?? [], ...assets.served])
13142
+ ]
13143
+ };
13144
+ warnings.push(`Kept asset dir(s) in place, served via content.assets: ${assets.served.join(", ")}.`);
13145
+ }
13146
+ if (assets.moved.length > 0) {
13147
+ warnings.push(`Moved assets into public/: ${assets.moved.join(", ")}.`);
13148
+ }
12869
13149
  };
12870
13150
  var cleanupSnippets = async (root, kept, warnings) => {
12871
- const dir = join32(root, "snippets");
12872
- if (!existsSync23(dir)) {
13151
+ const dir = join36(root, "snippets");
13152
+ if (!existsSync25(dir)) {
12873
13153
  return;
12874
13154
  }
12875
13155
  const markdown = await glob9(["**/*.{md,mdx}"], { absolute: true, cwd: dir });
@@ -12887,30 +13167,13 @@ var cleanupSnippets = async (root, kept, warnings) => {
12887
13167
  warnings.push(`Rewrote ${kept.size} component snippet import(s) to relative paths; verify they resolve.`);
12888
13168
  }
12889
13169
  };
12890
- var assetRefs = (config) => {
12891
- const refs = ["/images"];
12892
- const logo = config.logo;
12893
- if (typeof logo === "string") {
12894
- refs.push(logo);
12895
- } else if (logo) {
12896
- refs.push(logo.light, logo.dark);
12897
- }
12898
- const favicon = config.favicon;
12899
- if (typeof favicon === "string") {
12900
- refs.push(favicon);
12901
- } else if (favicon) {
12902
- refs.push(favicon.light, favicon.dark);
12903
- }
12904
- refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
12905
- return refs;
12906
- };
12907
13170
  var migrateMintlifyProject = async (root) => {
12908
13171
  const warnings = [];
12909
- const configFile = existsSync23(join32(root, "docs.json")) ? join32(root, "docs.json") : join32(root, "mint.json");
13172
+ const configFile = existsSync25(join36(root, "docs.json")) ? join36(root, "docs.json") : join36(root, "mint.json");
12910
13173
  let config;
12911
- if (existsSync23(configFile)) {
13174
+ if (existsSync25(configFile)) {
12912
13175
  config = await loadMintlifyConfig(root, configFile);
12913
- const spec = JSON.parse(await readFile18(configFile, "utf-8"));
13176
+ const spec = JSON.parse(await readFile19(configFile, "utf-8"));
12914
13177
  const i18n = mintlifyI18n(spec);
12915
13178
  if (i18n) {
12916
13179
  config.i18n = i18n;
@@ -12941,7 +13204,7 @@ var migrateMintlifyProject = async (root) => {
12941
13204
  const unsupported = new Set;
12942
13205
  const keptComponents = new Set;
12943
13206
  for (const file of files) {
12944
- const raw = await readFile18(file, "utf-8");
13207
+ const raw = await readFile19(file, "utf-8");
12945
13208
  const result = await transformMintlifyContent(raw, {
12946
13209
  filePath: file,
12947
13210
  root,
@@ -12949,7 +13212,7 @@ var migrateMintlifyProject = async (root) => {
12949
13212
  });
12950
13213
  if (result.content !== raw) {
12951
13214
  await mkdir9(dirname17(file), { recursive: true });
12952
- await writeFile12(file, result.content, "utf-8");
13215
+ await writeFile13(file, result.content, "utf-8");
12953
13216
  }
12954
13217
  for (const key of result.removed) {
12955
13218
  removedKeys.add(key);
@@ -12962,18 +13225,16 @@ var migrateMintlifyProject = async (root) => {
12962
13225
  }
12963
13226
  moved += 1;
12964
13227
  }
12965
- const movedAssets = await relocateAssets(root, assetRefs(config));
13228
+ const assets = await relocateAssets(root, assetSegments(config));
12966
13229
  await cleanupSnippets(root, keptComponents, warnings);
12967
13230
  if (config.content?.exclude) {
12968
13231
  config.content.exclude = [...new Set(config.content.exclude)];
12969
13232
  }
13233
+ applyRelocatedAssets(config, assets, warnings);
12970
13234
  await writeBlumeConfig2(root, config);
12971
13235
  if (Object.keys(variables).length > 0) {
12972
13236
  warnings.push(`Inlined ${Object.keys(variables).length} docs.json variable(s) into content; Blume has no runtime variable substitution.`);
12973
13237
  }
12974
- if (movedAssets.length > 0) {
12975
- warnings.push(`Moved assets into public/: ${movedAssets.join(", ")}.`);
12976
- }
12977
13238
  if (removedKeys.size > 0) {
12978
13239
  warnings.push(`Dropped unsupported page frontmatter keys: ${[...removedKeys].join(", ")}.`);
12979
13240
  }
@@ -12985,9 +13246,9 @@ var migrateMintlifyProject = async (root) => {
12985
13246
  };
12986
13247
 
12987
13248
  // src/migrate/nextra/index.ts
12988
- import { existsSync as existsSync24 } from "node:fs";
12989
- import { mkdir as mkdir10, readFile as readFile19, rename as rename5, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
12990
- import { basename as basename7, dirname as dirname18, extname as extname7, join as join33, relative as relative17 } from "pathe";
13249
+ import { existsSync as existsSync26 } from "node:fs";
13250
+ import { mkdir as mkdir10, readFile as readFile20, rename as rename5, rm as rm5, writeFile as writeFile14 } from "node:fs/promises";
13251
+ import { basename as basename7, dirname as dirname18, extname as extname8, join as join37, relative as relative20 } from "pathe";
12991
13252
  import { glob as glob10 } from "tinyglobby";
12992
13253
 
12993
13254
  // src/migrate/nextra/content.ts
@@ -13222,7 +13483,7 @@ var indexFolders = (base, pageFiles, metaFiles) => {
13222
13483
  }
13223
13484
  };
13224
13485
  for (const abs of pageFiles) {
13225
- const rel = relative17(base, abs);
13486
+ const rel = relative20(base, abs);
13226
13487
  const dir = normalizeDir(dirname18(rel));
13227
13488
  const slug = basename7(rel).replace(/\.mdx?$/u, "");
13228
13489
  const folder = pagesByDir.get(dir) ?? new Map;
@@ -13231,7 +13492,7 @@ var indexFolders = (base, pageFiles, metaFiles) => {
13231
13492
  registerDir(dir);
13232
13493
  }
13233
13494
  for (const abs of metaFiles) {
13234
- registerDir(normalizeDir(dirname18(relative17(base, abs))));
13495
+ registerDir(normalizeDir(dirname18(relative20(base, abs))));
13235
13496
  }
13236
13497
  const childDirs = new Map;
13237
13498
  for (const dir of allDirs) {
@@ -13269,7 +13530,7 @@ var planMetas = (metas, index) => {
13269
13530
  plan.metaByDir.set(dir, conversion.folderMeta);
13270
13531
  plan.consumedMetas.push(meta.abs);
13271
13532
  for (const [slug, title] of Object.entries(conversion.folderTitles)) {
13272
- plan.folderTitleByDir.set(normalizeDir(join33(dir, slug)), title);
13533
+ plan.folderTitleByDir.set(normalizeDir(join37(dir, slug)), title);
13273
13534
  }
13274
13535
  for (const [slug, label] of Object.entries(conversion.pageLabels)) {
13275
13536
  const pageAbs = index.pagesByDir.get(dir)?.get(slug);
@@ -13293,19 +13554,19 @@ var planMetas = (metas, index) => {
13293
13554
  return plan;
13294
13555
  };
13295
13556
  var movePage2 = async (abs, options) => {
13296
- const rel = relative17(options.base, abs);
13297
- const dest = join33(options.root, "docs", rel);
13298
- if (existsSync24(dest)) {
13557
+ const rel = relative20(options.base, abs);
13558
+ const dest = join37(options.root, "docs", rel);
13559
+ if (existsSync26(dest)) {
13299
13560
  return { moved: 0, removed: [], skipped: rel, unsupported: [] };
13300
13561
  }
13301
- const raw = await readFile19(abs, "utf-8");
13562
+ const raw = await readFile20(abs, "utf-8");
13302
13563
  const text = rewriteNextraCallouts(stripNextraImports(raw));
13303
13564
  const unsupported = unsupportedNextraComponents(text);
13304
13565
  const parsed = frontmatter_default(text);
13305
13566
  const { data, removed } = normalizeNextraPageMeta(parsed.data, options.overrides.get(abs) ?? {});
13306
13567
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
13307
13568
  await mkdir10(dirname18(dest), { recursive: true });
13308
- await writeFile13(dest, content, "utf-8");
13569
+ await writeFile14(dest, content, "utf-8");
13309
13570
  await rm5(abs, { force: true });
13310
13571
  return { moved: 1, removed, skipped: null, unsupported };
13311
13572
  };
@@ -13347,9 +13608,9 @@ var writeFolderMetas = async (root, plan) => {
13347
13608
  if (Object.keys(finalMeta).length === 0) {
13348
13609
  return;
13349
13610
  }
13350
- const dest = join33(root, "docs", dir, "meta.ts");
13611
+ const dest = join37(root, "docs", dir, "meta.ts");
13351
13612
  await mkdir10(dirname18(dest), { recursive: true });
13352
- await writeFile13(dest, `import { defineMeta } from "blume";
13613
+ await writeFile14(dest, `import { defineMeta } from "blume";
13353
13614
 
13354
13615
  export default defineMeta(${JSON.stringify(finalMeta, null, 2)});
13355
13616
  `, "utf-8");
@@ -13358,8 +13619,8 @@ export default defineMeta(${JSON.stringify(finalMeta, null, 2)});
13358
13619
  var relocateUnparseableMetas = async (root, metas) => {
13359
13620
  const warnings = [];
13360
13621
  await Promise.all(metas.map(async ({ abs, rel }) => {
13361
- const dest = join33(root, "docs", rel);
13362
- if (existsSync24(dest)) {
13622
+ const dest = join37(root, "docs", rel);
13623
+ if (existsSync26(dest)) {
13363
13624
  warnings.push(`Skipped ${rel} (target already exists)`);
13364
13625
  return;
13365
13626
  }
@@ -13371,7 +13632,7 @@ var relocateUnparseableMetas = async (root, metas) => {
13371
13632
  };
13372
13633
  var buildConfig = (tabs) => tabs.length > 0 ? { navigation: { tabs }, title: "Documentation" } : { title: "Documentation" };
13373
13634
  var migrateNextraProject = async (root) => {
13374
- const sourceDir = SOURCE_DIRS.find((dir) => existsSync24(join33(root, dir)));
13635
+ const sourceDir = SOURCE_DIRS.find((dir) => existsSync26(join37(root, dir)));
13375
13636
  if (!sourceDir) {
13376
13637
  await writeBlumeConfig(root, { title: "Documentation" });
13377
13638
  return {
@@ -13381,7 +13642,7 @@ var migrateNextraProject = async (root) => {
13381
13642
  ]
13382
13643
  };
13383
13644
  }
13384
- const base = join33(root, sourceDir);
13645
+ const base = join37(root, sourceDir);
13385
13646
  const pageFiles = await glob10([PAGE_GLOB2], {
13386
13647
  absolute: true,
13387
13648
  cwd: base,
@@ -13395,9 +13656,9 @@ var migrateNextraProject = async (root) => {
13395
13656
  const index = indexFolders(base, pageFiles, metaFiles);
13396
13657
  const metas = await Promise.all(metaFiles.map(async (abs) => ({
13397
13658
  abs,
13398
- ext: extname7(abs),
13399
- raw: await readFile19(abs, "utf-8"),
13400
- rel: relative17(base, abs)
13659
+ ext: extname8(abs),
13660
+ raw: await readFile20(abs, "utf-8"),
13661
+ rel: relative20(base, abs)
13401
13662
  })));
13402
13663
  const plan = planMetas(metas, index);
13403
13664
  const moves = await Promise.all(pageFiles.map((abs) => movePage2(abs, { base, overrides: plan.pageOverrides, root })));
@@ -13422,15 +13683,15 @@ var migrateNextraProject = async (root) => {
13422
13683
  };
13423
13684
 
13424
13685
  // src/migrate/starlight/index.ts
13425
- import { existsSync as existsSync26 } from "node:fs";
13426
- import { readFile as readFile21, writeFile as writeFile14 } from "node:fs/promises";
13427
- import { join as join35 } from "pathe";
13686
+ import { existsSync as existsSync28 } from "node:fs";
13687
+ import { readFile as readFile22, writeFile as writeFile15 } from "node:fs/promises";
13688
+ import { join as join39 } from "pathe";
13428
13689
  import { glob as glob11 } from "tinyglobby";
13429
13690
 
13430
13691
  // src/migrate/starlight/config.ts
13431
- import { existsSync as existsSync25 } from "node:fs";
13432
- import { readFile as readFile20 } from "node:fs/promises";
13433
- import { join as join34 } from "pathe";
13692
+ import { existsSync as existsSync27 } from "node:fs";
13693
+ import { readFile as readFile21 } from "node:fs/promises";
13694
+ import { join as join38 } from "pathe";
13434
13695
  var CONFIG_FILES = [
13435
13696
  "astro.config.mjs",
13436
13697
  "astro.config.mts",
@@ -13461,7 +13722,7 @@ var extractStarlightOptions = (source) => {
13461
13722
  return isLiteralObject(parsed) ? parsed : "unparseable";
13462
13723
  };
13463
13724
  var loadStarlightConfig = async (root) => {
13464
- const file = CONFIG_FILES.map((name) => join34(root, name)).find((path) => existsSync25(path));
13725
+ const file = CONFIG_FILES.map((name) => join38(root, name)).find((path) => existsSync27(path));
13465
13726
  if (!file) {
13466
13727
  return {
13467
13728
  options: {},
@@ -13470,7 +13731,7 @@ var loadStarlightConfig = async (root) => {
13470
13731
  ]
13471
13732
  };
13472
13733
  }
13473
- const source = await readFile20(file, "utf-8");
13734
+ const source = await readFile21(file, "utf-8");
13474
13735
  const result = extractStarlightOptions(source);
13475
13736
  if (result === "missing") {
13476
13737
  return {
@@ -13868,7 +14129,7 @@ var starlightI18n = (options) => {
13868
14129
  // src/migrate/starlight/index.ts
13869
14130
  var CONTENT_DIR = "src/content/docs";
13870
14131
  var transformPage = async (file) => {
13871
- const raw = await readFile21(file, "utf-8");
14132
+ const raw = await readFile22(file, "utf-8");
13872
14133
  let text = stripStarlightImports(raw);
13873
14134
  text = rewriteStarlightAsides(text);
13874
14135
  text = rewriteStarlightComponents(text);
@@ -13878,7 +14139,7 @@ var transformPage = async (file) => {
13878
14139
  const { data, removed } = normalizeStarlightPageMeta(parsed.data);
13879
14140
  const content = Object.keys(data).length > 0 ? frontmatter_default.stringify(parsed.content, data) : parsed.content;
13880
14141
  if (content !== raw) {
13881
- await writeFile14(file, content, "utf-8");
14142
+ await writeFile15(file, content, "utf-8");
13882
14143
  }
13883
14144
  return { aliased, removed, unsupported };
13884
14145
  };
@@ -13890,8 +14151,8 @@ var migrateStarlightProject = async (root) => {
13890
14151
  config.i18n = i18n;
13891
14152
  warnings.push(`Mapped ${i18n.locales.length} locale(s) to i18n (default: ${i18n.defaultLocale}); review the locale labels.`);
13892
14153
  }
13893
- const base = join35(root, CONTENT_DIR);
13894
- if (!existsSync26(base)) {
14154
+ const base = join39(root, CONTENT_DIR);
14155
+ if (!existsSync28(base)) {
13895
14156
  await writeBlumeConfig(root, config);
13896
14157
  return {
13897
14158
  moved: 0,
@@ -13979,10 +14240,10 @@ var migrateCommand = defineCommand8({
13979
14240
  });
13980
14241
 
13981
14242
  // src/cli/commands/preview.ts
13982
- import { existsSync as existsSync27 } from "node:fs";
14243
+ import { existsSync as existsSync29 } from "node:fs";
13983
14244
  import { preview } from "astro";
13984
14245
  import { defineCommand as defineCommand9 } from "citty";
13985
- import { join as join36 } from "pathe";
14246
+ import { join as join40 } from "pathe";
13986
14247
  var previewCommand = defineCommand9({
13987
14248
  args: {
13988
14249
  host: { description: "Network host to bind.", type: "string" },
@@ -13996,7 +14257,7 @@ var previewCommand = defineCommand9({
13996
14257
  const root = process.cwd();
13997
14258
  const { config } = await loadConfig(root);
13998
14259
  const context = resolveProjectContext(root, config);
13999
- if (!existsSync27(join36(context.outDir, "astro.config.mjs"))) {
14260
+ if (!existsSync29(join40(context.outDir, "astro.config.mjs"))) {
14000
14261
  logger.error("No build found. Run `blume build` first.");
14001
14262
  process.exit(1);
14002
14263
  }
@@ -14005,7 +14266,7 @@ var previewCommand = defineCommand9({
14005
14266
  root: context.outDir,
14006
14267
  server: {
14007
14268
  host: args.host ?? false,
14008
- port: args.port ? Number(args.port) : undefined
14269
+ port: parsePort(args.port)
14009
14270
  }
14010
14271
  });
14011
14272
  }
@@ -14014,7 +14275,7 @@ var previewCommand = defineCommand9({
14014
14275
  // src/cli/commands/sync.ts
14015
14276
  import { rm as rm6 } from "node:fs/promises";
14016
14277
  import { defineCommand as defineCommand10 } from "citty";
14017
- import { join as join37 } from "pathe";
14278
+ import { join as join41 } from "pathe";
14018
14279
  var syncCommand = defineCommand10({
14019
14280
  args: {
14020
14281
  force: {
@@ -14036,7 +14297,7 @@ var syncCommand = defineCommand10({
14036
14297
  if (args.force) {
14037
14298
  const { config } = await loadConfig(root);
14038
14299
  const context = resolveProjectContext(root, config);
14039
- await rm6(join37(context.outDir, "cache"), { force: true, recursive: true });
14300
+ await rm6(join41(context.outDir, "cache"), { force: true, recursive: true });
14040
14301
  logger.info("Cleared source cache.");
14041
14302
  }
14042
14303
  await prepareProject({
@@ -14051,13 +14312,13 @@ var syncCommand = defineCommand10({
14051
14312
  });
14052
14313
 
14053
14314
  // src/cli/commands/validate.ts
14054
- import { existsSync as existsSync29 } from "node:fs";
14315
+ import { existsSync as existsSync31 } from "node:fs";
14055
14316
  import { defineCommand as defineCommand11 } from "citty";
14056
- import { join as join39 } from "pathe";
14317
+ import { join as join43 } from "pathe";
14057
14318
 
14058
14319
  // src/core/links.ts
14059
- import { existsSync as existsSync28 } from "node:fs";
14060
- import { join as join38 } from "pathe";
14320
+ import { existsSync as existsSync30 } from "node:fs";
14321
+ import { basename as basename8, join as join42 } from "pathe";
14061
14322
  var HTTP = /^https?:\/\//iu;
14062
14323
  var PROTOCOL_RELATIVE = /^\/\//u;
14063
14324
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -14069,9 +14330,21 @@ var STATUS_NOT_FOUND = 404;
14069
14330
  var STATUS_GONE = 410;
14070
14331
  var STATUS_METHOD_NOT_ALLOWED = 405;
14071
14332
  var STATUS_NOT_IMPLEMENTED = 501;
14072
- var resolveRelative = (pageRoute, target) => {
14333
+ var assetIsPresent = (resolved, ctx) => {
14334
+ if (ctx.publicDir && existsSync30(join42(ctx.publicDir, resolved))) {
14335
+ return true;
14336
+ }
14337
+ return ctx.assetMounts.some((mount) => (resolved === mount.url || resolved.startsWith(`${mount.url}/`)) && existsSync30(join42(mount.dir, resolved.slice(mount.url.length))));
14338
+ };
14339
+ var isIndexPage = (page2) => {
14340
+ const ref = page2.source?.ref ?? page2.sourcePath ?? "";
14341
+ return /^index\.(?:md|mdx)$/iu.test(basename8(ref));
14342
+ };
14343
+ var resolveRelative = (pageRoute, target, isIndex) => {
14073
14344
  const segments = pageRoute.split("/").filter(Boolean);
14074
- segments.pop();
14345
+ if (!isIndex) {
14346
+ segments.pop();
14347
+ }
14075
14348
  for (const part of target.split("/")) {
14076
14349
  if (part === "" || part === ".") {
14077
14350
  continue;
@@ -14113,13 +14386,20 @@ var checkAnchor = (route, fragment, site, ctx) => {
14113
14386
  };
14114
14387
  };
14115
14388
  var checkPathLink = (resolved, fragment, target, site, ctx) => {
14389
+ const route = toRoute(resolved);
14390
+ if (ctx.routes.has(route)) {
14391
+ return fragment ? checkAnchor(route, fragment, site, ctx) : null;
14392
+ }
14393
+ if (ctx.redirects.has(route)) {
14394
+ return null;
14395
+ }
14116
14396
  if (FILE_EXT.test(resolved) && !DOC_EXT.test(resolved)) {
14117
- if (ctx.publicDir === null) {
14118
- return "asset-unchecked";
14119
- }
14120
- if (existsSync28(join38(ctx.publicDir, resolved))) {
14397
+ if (assetIsPresent(resolved, ctx)) {
14121
14398
  return null;
14122
14399
  }
14400
+ if (ctx.publicDir === null && ctx.assetMounts.length === 0) {
14401
+ return "asset-unchecked";
14402
+ }
14123
14403
  return {
14124
14404
  ...site,
14125
14405
  code: "BLUME_BROKEN_ASSET",
@@ -14128,13 +14408,6 @@ var checkPathLink = (resolved, fragment, target, site, ctx) => {
14128
14408
  suggestion: `Add the file at public${resolved} or fix the link.`
14129
14409
  };
14130
14410
  }
14131
- const route = toRoute(resolved);
14132
- if (ctx.routes.has(route)) {
14133
- return fragment ? checkAnchor(route, fragment, site, ctx) : null;
14134
- }
14135
- if (ctx.redirects.has(route)) {
14136
- return null;
14137
- }
14138
14411
  return {
14139
14412
  ...site,
14140
14413
  code: "BLUME_BROKEN_LINK",
@@ -14243,12 +14516,13 @@ var classifyLink = (page2, link, ctx, onExternal) => {
14243
14516
  if (rawPath === "") {
14244
14517
  return fragment ? checkAnchor(page2.route, fragment, site, ctx) : null;
14245
14518
  }
14246
- const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page2.route, rawPath);
14519
+ const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page2.route, rawPath, isIndexPage(page2));
14247
14520
  return checkPathLink(resolved, fragment, target, site, ctx);
14248
14521
  };
14249
14522
  var validateLinks = async (graph, options) => {
14250
14523
  const ctx = {
14251
14524
  anchors: buildAnchorIndex(graph.pages),
14525
+ assetMounts: options.assetMounts ?? [],
14252
14526
  publicDir: options.publicDir,
14253
14527
  redirects: new Set((options.redirects ?? []).map((redirect) => toRoute(redirect.from))),
14254
14528
  routes: new Set(graph.routes.keys())
@@ -14305,10 +14579,11 @@ var validateCommand = defineCommand11({
14305
14579
  try {
14306
14580
  const project = await scanProject(root, { mode: "build" });
14307
14581
  diagnostics.push(...project.diagnostics);
14308
- const publicDir = join39(root, "public");
14582
+ const publicDir = join43(root, "public");
14309
14583
  diagnostics.push(...await validateLinks(project.graph, {
14584
+ assetMounts: resolveAssetMounts(root, project.config.content.assets),
14310
14585
  checkExternal: Boolean(args.external),
14311
- publicDir: existsSync29(publicDir) ? publicDir : null,
14586
+ publicDir: existsSync31(publicDir) ? publicDir : null,
14312
14587
  redirects: project.config.redirects
14313
14588
  }));
14314
14589
  } catch (error) {
@@ -14322,6 +14597,7 @@ var validateCommand = defineCommand11({
14322
14597
  if (args.json) {
14323
14598
  const hadErrors2 = reportDiagnosticsJson(diagnostics, root);
14324
14599
  if (hadErrors2 || Boolean(args.strict) && diagnostics.length > 0) {
14600
+ await flushStdout();
14325
14601
  process.exit(1);
14326
14602
  }
14327
14603
  return;
@@ -14368,5 +14644,5 @@ process.on("unhandledRejection", (error) => {
14368
14644
  });
14369
14645
  runMain(main);
14370
14646
 
14371
- //# debugId=8EA79E97DE44177D64756E2164756E21
14647
+ //# debugId=114B8B7438537BD664756E2164756E21
14372
14648
  //# sourceMappingURL=index.js.map