blume 0.5.0 → 0.5.1

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.
@@ -17,8 +17,6 @@ type NavigationSelectorItem = NavigationSelectors[number]["items"][number];
17
17
  type NavigationChromeVariants = NonNullable<
18
18
  BlumeConfig["navigation"]
19
19
  >["chromeVariants"];
20
- type NavigationSidebarVariants =
21
- ResolvedConfig["navigation"]["sidebarVariants"];
22
20
 
23
21
  const MINTLIFY_DEFAULT_IGNORES = [
24
22
  "**/_*",
@@ -307,18 +305,6 @@ const mintlifyBackgroundDecoration = (
307
305
  return undefined;
308
306
  };
309
307
 
310
- const sidebarItemPaths = (items: SidebarItemConfig[]): string[] =>
311
- items.flatMap((item) => {
312
- if (typeof item === "string") {
313
- return [tabPathFromRef(item)];
314
- }
315
- if (item.href) {
316
- return [item.href];
317
- }
318
- const paths = item.root ? [tabPathFromRef(item.root)] : [];
319
- return item.items ? [...paths, ...sidebarItemPaths(item.items)] : paths;
320
- });
321
-
322
308
  const collapsedFromExpanded = (value: unknown): boolean | undefined => {
323
309
  if (value === false) {
324
310
  return true;
@@ -439,79 +425,6 @@ const isExternalRoute = (path: string): boolean =>
439
425
  path.startsWith("mailto:") ||
440
426
  path.startsWith("tel:");
441
427
 
442
- const hasOwnNavigationContent = (item: JsonObject): boolean =>
443
- Boolean(asString(item.root)) || childItemsFor(item).length > 0;
444
-
445
- const mintlifySidebarVariants = async (
446
- spec: JsonObject
447
- ): Promise<NavigationSidebarVariants> => {
448
- const navigation = asObject(spec.navigation) ?? {};
449
- const global = asObject(navigation.global) ?? {};
450
- interface SidebarVariantCandidate {
451
- item: unknown;
452
- path: string;
453
- }
454
- const candidates: SidebarVariantCandidate[] = [];
455
-
456
- const addCandidate = (item: unknown): void => {
457
- const path = navItemPath(item);
458
- if (!path || isExternalRoute(path)) {
459
- return;
460
- }
461
-
462
- candidates.push({ item, path });
463
- };
464
-
465
- for (const item of [
466
- ...asArray(navigation.tabs),
467
- ...asArray(navigation.anchors),
468
- ...asArray(global.anchors),
469
- ]) {
470
- const object = asObject(item);
471
- if (!object) {
472
- addCandidate(item);
473
- continue;
474
- }
475
-
476
- for (const menuItem of asArray(object.menu)) {
477
- addCandidate(menuItem);
478
- }
479
-
480
- if (hasOwnNavigationContent(object)) {
481
- addCandidate(item);
482
- }
483
- }
484
-
485
- for (const item of [
486
- ...asArray(navigation.dropdowns),
487
- ...asArray(navigation.products),
488
- ...asArray(navigation.versions),
489
- ...asArray(navigation.languages),
490
- ]) {
491
- addCandidate(item);
492
- }
493
-
494
- const resolved = await Promise.all(
495
- candidates.map(async (candidate) => {
496
- const items = await toSidebarItems([candidate.item], {});
497
- if (items.length === 0) {
498
- return [];
499
- }
500
- return [candidate.path, ...sidebarItemPaths(items)]
501
- .filter((path) => !isExternalRoute(path))
502
- .map((path) => ({ items, path }));
503
- })
504
- );
505
- const seen = new Set<string>();
506
- return resolved.flat().flatMap((variant) => {
507
- if (!variant || seen.has(variant.path)) {
508
- return [];
509
- }
510
- seen.add(variant.path);
511
- return [variant];
512
- });
513
- };
514
-
515
428
  const mintlifyTabs = (
516
429
  spec: JsonObject
517
430
  ): NonNullable<BlumeConfig["navigation"]>["tabs"] => {
@@ -604,24 +517,53 @@ const toAstroRedirectPath = (path: string): string =>
604
517
  modifier === "*" || modifier === "+" ? `[...${name}]` : `[${name}]`
605
518
  );
606
519
 
607
- const mintlifyRedirects = (
520
+ // A converted path is dynamic once it holds an Astro segment (`[id]`/`[...slug]`
521
+ // from a `:param`). Blume redirects are static path-to-path: a dynamic segment
522
+ // has no matching route, so Astro aborts the build ("destination does not match
523
+ // any existing route"), and the platform redirect files emit the segment
524
+ // verbatim, which no host understands. Such redirects are dropped, not emitted.
525
+ const isDynamicRedirectPath = (path: string): boolean => path.includes("[");
526
+
527
+ export interface MintlifyRedirectPartition {
528
+ /** Static redirects Blume can honor, translated to Blume's `from`/`to` shape. */
529
+ kept: NonNullable<BlumeConfig["redirects"]>;
530
+ /** Source paths of dynamic redirects dropped because Blume can't model them. */
531
+ dropped: string[];
532
+ }
533
+
534
+ /**
535
+ * Split a spec's redirects into the static ones Blume emits and the dynamic
536
+ * (wildcard/param) ones it drops. Keeping a dynamic redirect crashes the Astro
537
+ * build, so the migrator surfaces the dropped sources as a warning instead.
538
+ */
539
+ export const partitionMintlifyRedirects = (
608
540
  spec: JsonObject
609
- ): NonNullable<BlumeConfig["redirects"]> =>
610
- asArray(spec.redirects).flatMap((redirect) => {
541
+ ): MintlifyRedirectPartition => {
542
+ const kept: NonNullable<BlumeConfig["redirects"]> = [];
543
+ const dropped: string[] = [];
544
+ for (const redirect of asArray(spec.redirects)) {
611
545
  const object = asObject(redirect);
612
546
  if (!object) {
613
- return [];
547
+ continue;
614
548
  }
615
- const from = asString(object.source) ?? asString(object.from);
616
- const to =
549
+ const source = asString(object.source) ?? asString(object.from);
550
+ const destination =
617
551
  asString(object.destination) ??
618
552
  asString(object.to) ??
619
553
  asString(object.redirect);
620
- if (!from || !to) {
621
- return [];
554
+ if (!source || !destination) {
555
+ continue;
622
556
  }
623
- return [{ from: toAstroRedirectPath(from), to: toAstroRedirectPath(to) }];
624
- });
557
+ const from = toAstroRedirectPath(source);
558
+ const to = toAstroRedirectPath(destination);
559
+ if (isDynamicRedirectPath(from) || isDynamicRedirectPath(to)) {
560
+ dropped.push(source);
561
+ } else {
562
+ kept.push({ from, to });
563
+ }
564
+ }
565
+ return { dropped, kept };
566
+ };
625
567
 
626
568
  interface OpenApiSourceDraft {
627
569
  label?: string;
@@ -981,11 +923,10 @@ export const loadMintlifyConfig = async (
981
923
  chromeVariants: mintlifyChromeVariants(spec),
982
924
  selectors: mintlifySelectors(spec),
983
925
  sidebar: await toSidebarItems(mintlifyNavigationItems(navigation), {}),
984
- sidebarVariants: await mintlifySidebarVariants(spec),
985
926
  tabs: mintlifyTabs(spec),
986
927
  },
987
928
  openapi: mintlifyOpenapi(spec),
988
- redirects: mintlifyRedirects(spec),
929
+ redirects: partitionMintlifyRedirects(spec).kept,
989
930
  search: {
990
931
  indexing: {
991
932
  includeHiddenPages: seo.indexing === "all",
@@ -56,6 +56,22 @@ export const rewriteMintlifyExampleBlocks = (source: string): string =>
56
56
  "<$<close>CodeGroup"
57
57
  );
58
58
 
59
+ /**
60
+ * Mintlify nests `<Accordion title="…">` items inside an `<AccordionGroup>`.
61
+ * Blume inverts that: `<Accordion>` is the container and each item is an
62
+ * `<AccordionItem title="…">`. Rewrite both in a single pass — the `\b` after
63
+ * `Accordion` keeps `<AccordionGroup>` (the `Group` branch) distinct from an
64
+ * item, so open/close tags of either map correctly regardless of order. Without
65
+ * this, migrated pages keep `<AccordionGroup>`, which Blume doesn't ship and the
66
+ * MDX build rejects with "Expected component AccordionGroup to be defined".
67
+ */
68
+ export const rewriteMintlifyAccordions = (source: string): string =>
69
+ source.replaceAll(
70
+ /<(?<close>\/?)Accordion(?<group>Group)?\b/gu,
71
+ (_match, close: string, group: string | undefined) =>
72
+ group ? `<${close}Accordion` : `<${close}AccordionItem`
73
+ );
74
+
59
75
  const SNIPPET_IMPORT =
60
76
  /^import\s+[\s\S]*?\s+from\s+["'](?<source>\/snippets\/[^"']+)["'];?[ \t]*\n?/gmu;
61
77
 
@@ -6,7 +6,7 @@ import { glob } from "tinyglobby";
6
6
 
7
7
  import type { BlumeConfig } from "../../core/schema.ts";
8
8
  import { assetSegments } from "./assets.ts";
9
- import { loadMintlifyConfig } from "./config.ts";
9
+ import { loadMintlifyConfig, partitionMintlifyRedirects } from "./config.ts";
10
10
  import { mintlifyI18n } from "./i18n.ts";
11
11
  import { transformMintlifyContent } from "./transform.ts";
12
12
 
@@ -65,6 +65,22 @@ const droppedChromeWarnings = (
65
65
  return warnings;
66
66
  };
67
67
 
68
+ /**
69
+ * Warn about dynamic (wildcard/param) redirects the migrator dropped. Blume
70
+ * redirects are static path-to-path, so a `:slug*`/`:id` source becomes an
71
+ * unroutable Astro destination — kept ones crash the build. Point the user at
72
+ * host-level rules that do support wildcards.
73
+ */
74
+ const droppedRedirectWarnings = (spec: Record<string, unknown>): string[] => {
75
+ const { dropped } = partitionMintlifyRedirects(spec);
76
+ if (dropped.length === 0) {
77
+ return [];
78
+ }
79
+ return [
80
+ `Dropped ${dropped.length} dynamic redirect(s) Blume can't model as static path-to-path (${dropped.join(", ")}); re-add them as host-level rules (e.g. _redirects or vercel.json).`,
81
+ ];
82
+ };
83
+
68
84
  /** Recursively drop `undefined`, empty arrays, and empty objects. */
69
85
  const prune = (value: unknown): unknown => {
70
86
  if (Array.isArray(value)) {
@@ -247,6 +263,7 @@ export const migrateMintlifyProject = async (
247
263
  );
248
264
  }
249
265
  warnings.push(...droppedChromeWarnings(spec, config));
266
+ warnings.push(...droppedRedirectWarnings(spec));
250
267
  } else {
251
268
  warnings.push("No docs.json or mint.json found; writing a default config.");
252
269
  config = { content: { root: "." }, title: "Documentation" };
@@ -1,6 +1,7 @@
1
1
  import matter from "../../core/frontmatter.ts";
2
2
  import { stripUnknownPageMeta } from "../shared.ts";
3
3
  import {
4
+ rewriteMintlifyAccordions,
4
5
  rewriteMintlifyCallouts,
5
6
  rewriteMintlifyExampleBlocks,
6
7
  rewriteSnippetImports,
@@ -59,6 +60,7 @@ export const transformMintlifyContent = async (
59
60
  text = snippetImports.source;
60
61
  text = rewriteMintlifySvgIconProps(text);
61
62
  text = rewriteMintlifyExampleBlocks(text);
63
+ text = rewriteMintlifyAccordions(text);
62
64
  text = rewriteMintlifyCallouts(text);
63
65
 
64
66
  const unsupported = unsupportedMintlifyComponents(text);