blume 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +160 -182
- package/dist/cli/index.js.map +17 -16
- package/dist/types/core/schema.d.ts +0 -26
- package/dist/types/core/types.d.ts +0 -6
- package/dist/types/migrate/mintlify/config.d.ts +14 -0
- package/docs/01-quickstart.mdx +6 -2
- package/docs/02-deployment.mdx +3 -1
- package/docs/advanced/api-reference.mdx +6 -2
- package/docs/advanced/bridge.mdx +3 -1
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/migrate.mdx +6 -2
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/export.mdx +6 -2
- package/docs/configuration/seo.mdx +3 -1
- package/docs/content/i18n.mdx +6 -2
- package/docs/content/islands.mdx +6 -2
- package/docs/content/meta.mdx +3 -1
- package/docs/content/syntax.mdx +40 -14
- package/package.json +1 -1
- package/src/astro/generate.ts +11 -2
- package/src/astro/templates.ts +34 -8
- package/src/cli/commands/init.ts +2 -27
- package/src/core/graph.ts +0 -3
- package/src/core/nav-diagnostics.ts +2 -12
- package/src/core/navigation.ts +0 -10
- package/src/core/package-json.ts +32 -0
- package/src/core/schema.ts +0 -8
- package/src/core/sources/mintlify.ts +44 -1
- package/src/core/types.ts +0 -7
- package/src/migrate/mintlify/config.ts +40 -99
- package/src/migrate/mintlify/content.ts +16 -0
- package/src/migrate/mintlify/index.ts +42 -1
- package/src/migrate/mintlify/transform.ts +2 -0
- package/src/migrate/shared.ts +24 -1
package/src/core/navigation.ts
CHANGED
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
import type {
|
|
9
9
|
NavChromeVariant,
|
|
10
10
|
NavNode,
|
|
11
|
-
NavSidebarVariant,
|
|
12
11
|
Navigation,
|
|
13
12
|
NavSelector,
|
|
14
13
|
NavTab,
|
|
@@ -348,7 +347,6 @@ export const buildNavigation = (
|
|
|
348
347
|
selectors?: NavSelector[];
|
|
349
348
|
tabs?: NavTab[];
|
|
350
349
|
sidebar?: SidebarItemConfig[];
|
|
351
|
-
sidebarVariants?: { path: string; items: SidebarItemConfig[] }[];
|
|
352
350
|
/** Locale dir prefix for folder-meta lookup (`""` for the default locale). */
|
|
353
351
|
metaPrefix?: string;
|
|
354
352
|
/**
|
|
@@ -372,19 +370,12 @@ export const buildNavigation = (
|
|
|
372
370
|
page,
|
|
373
371
|
])
|
|
374
372
|
);
|
|
375
|
-
const sidebarVariants: NavSidebarVariant[] = (
|
|
376
|
-
options.sidebarVariants ?? []
|
|
377
|
-
).map((variant) => ({
|
|
378
|
-
path: variant.path,
|
|
379
|
-
sidebar: buildConfigSidebar(variant.items, byRoute),
|
|
380
|
-
}));
|
|
381
373
|
|
|
382
374
|
if (options.sidebar) {
|
|
383
375
|
return {
|
|
384
376
|
chromeVariants,
|
|
385
377
|
selectors,
|
|
386
378
|
sidebar: buildConfigSidebar(options.sidebar, byRoute),
|
|
387
|
-
sidebarVariants,
|
|
388
379
|
tabs,
|
|
389
380
|
};
|
|
390
381
|
}
|
|
@@ -398,7 +389,6 @@ export const buildNavigation = (
|
|
|
398
389
|
sharedFolderMeta,
|
|
399
390
|
metaPrefix
|
|
400
391
|
),
|
|
401
|
-
sidebarVariants,
|
|
402
392
|
tabs,
|
|
403
393
|
};
|
|
404
394
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getBlumeVersion } from "./version.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a valid npm package name from a directory name, falling back to
|
|
5
|
+
* `docs` when nothing usable remains.
|
|
6
|
+
*/
|
|
7
|
+
export const toPackageName = (raw: string): string =>
|
|
8
|
+
raw
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replaceAll(/[^a-z0-9._-]+/gu, "-")
|
|
11
|
+
.replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A minimal, runnable `package.json` body for a Blume project: the `blume`
|
|
15
|
+
* dependency pinned to the installed version plus `dev`/`build`/`doctor`
|
|
16
|
+
* scripts, so `npm install && npm run dev` works immediately. Shared by
|
|
17
|
+
* `blume init` and the migrators, which scaffold one when a project has none.
|
|
18
|
+
*/
|
|
19
|
+
export const blumePackageJson = (name: string): string => `{
|
|
20
|
+
"name": ${JSON.stringify(name)},
|
|
21
|
+
"private": true,
|
|
22
|
+
"type": "module",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "blume dev",
|
|
25
|
+
"build": "blume build",
|
|
26
|
+
"doctor": "blume doctor"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"blume": "^${getBlumeVersion()}"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`;
|
package/src/core/schema.ts
CHANGED
|
@@ -483,13 +483,6 @@ const sidebarItemSchema: z.ZodType<SidebarItemConfig> = z.lazy(() =>
|
|
|
483
483
|
])
|
|
484
484
|
);
|
|
485
485
|
|
|
486
|
-
const sidebarVariantSchema = z
|
|
487
|
-
.object({
|
|
488
|
-
items: z.array(sidebarItemSchema).default([]),
|
|
489
|
-
path: z.string(),
|
|
490
|
-
})
|
|
491
|
-
.strict();
|
|
492
|
-
|
|
493
486
|
const variablesConfigSchema = z
|
|
494
487
|
.record(z.string().regex(/^[A-Za-z0-9-]+$/u), z.string())
|
|
495
488
|
.default({});
|
|
@@ -666,7 +659,6 @@ const navigationConfigSchema = z
|
|
|
666
659
|
selectors: z.array(navSelectorSchema).default([]),
|
|
667
660
|
/** Explicit sidebar override; when omitted the sidebar is generated. */
|
|
668
661
|
sidebar: z.array(sidebarItemSchema).optional(),
|
|
669
|
-
sidebarVariants: z.array(sidebarVariantSchema).default([]),
|
|
670
662
|
tabs: z.array(navTabSchema).optional(),
|
|
671
663
|
})
|
|
672
664
|
.strict();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, watch as fsWatch } from "node:fs";
|
|
2
|
+
import type { WatchListener } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
|
|
4
5
|
import { isAbsolute, join, relative, resolve } from "pathe";
|
|
@@ -42,6 +43,41 @@ const MINTLIFY_SOURCE_IGNORES = [
|
|
|
42
43
|
"snippets/**",
|
|
43
44
|
];
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Directory names the recursive dev watcher must ignore. In bridge mode the
|
|
48
|
+
* content root is the project root, so a naive recursive `fs.watch` also sees
|
|
49
|
+
* Blume's own `.blume/` output — which the dev server rewrites on every request
|
|
50
|
+
* (`.blume/.astro/data-store.json`). Left unfiltered, each such write re-triggers
|
|
51
|
+
* a full rescan + runtime regeneration, whose writes land back under `.blume/`
|
|
52
|
+
* and fire the watcher again: a self-sustaining storm that stalls page renders
|
|
53
|
+
* and floods the console. `fs.watch` has no ignore option, so we filter by the
|
|
54
|
+
* changed path in the callback. Derived from {@link MINTLIFY_SOURCE_IGNORES}
|
|
55
|
+
* (dir prefixes) plus VCS metadata.
|
|
56
|
+
*/
|
|
57
|
+
const WATCH_IGNORE_DIRS = new Set([
|
|
58
|
+
...MINTLIFY_SOURCE_IGNORES.map((pattern) => pattern.replace(/\/\*\*$/u, "")),
|
|
59
|
+
".git",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the recursive-watch listener: fire `onChange` for content changes but
|
|
64
|
+
* ignore events whose path crosses a {@link WATCH_IGNORE_DIRS} segment (Blume's
|
|
65
|
+
* own `.blume/` output, `node_modules`, VCS metadata, …). A missing `filename`
|
|
66
|
+
* — rare; the platform couldn't name the changed path — falls through to
|
|
67
|
+
* regenerate rather than silently dropping a real edit. Exported for testing.
|
|
68
|
+
*/
|
|
69
|
+
export const mintlifyWatchListener =
|
|
70
|
+
(onChange: () => void): WatchListener<string> =>
|
|
71
|
+
(_event, filename) => {
|
|
72
|
+
if (
|
|
73
|
+
typeof filename === "string" &&
|
|
74
|
+
filename.split(/[/\\]/u).some((segment) => WATCH_IGNORE_DIRS.has(segment))
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
onChange();
|
|
79
|
+
};
|
|
80
|
+
|
|
45
81
|
/**
|
|
46
82
|
* The Mintlify bridge content source. Reads an unconverted Mintlify project in
|
|
47
83
|
* place and transforms each page to Blume MDX at scan time (callouts → `:::`
|
|
@@ -126,7 +162,14 @@ export const mintlifySource = (
|
|
|
126
162
|
const watch = (onChange: () => void): (() => void) => {
|
|
127
163
|
const disposers: (() => void)[] = [];
|
|
128
164
|
if (existsSync(contentRoot)) {
|
|
129
|
-
|
|
165
|
+
// Recursively watch the content root, but skip Blume's own output and
|
|
166
|
+
// other non-content trees so the dev server's `.blume/` writes don't feed
|
|
167
|
+
// a regeneration loop (`fs.watch` has no ignore option, so filter here).
|
|
168
|
+
const watcher = fsWatch(
|
|
169
|
+
contentRoot,
|
|
170
|
+
{ recursive: true },
|
|
171
|
+
mintlifyWatchListener(onChange)
|
|
172
|
+
);
|
|
130
173
|
disposers.push(() => watcher.close());
|
|
131
174
|
}
|
|
132
175
|
// Watch docs.json directly: it lives at the content root but a non-recursive
|
package/src/core/types.ts
CHANGED
|
@@ -180,12 +180,6 @@ export interface NavSelector {
|
|
|
180
180
|
items: NavSelectorItem[];
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
/** Sidebar tree used when the current route belongs to a nav partition. */
|
|
184
|
-
export interface NavSidebarVariant {
|
|
185
|
-
path: string;
|
|
186
|
-
sidebar: NavNode[];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
183
|
/** Chrome overrides used when the current route belongs to a nav partition. */
|
|
190
184
|
export interface NavChromeVariant {
|
|
191
185
|
path: string;
|
|
@@ -198,7 +192,6 @@ export interface Navigation {
|
|
|
198
192
|
selectors: NavSelector[];
|
|
199
193
|
chromeVariants: NavChromeVariant[];
|
|
200
194
|
sidebar: NavNode[];
|
|
201
|
-
sidebarVariants: NavSidebarVariant[];
|
|
202
195
|
/** Repo URL for the header link, or null when hidden (`navigation.repo`). */
|
|
203
196
|
repoUrl?: string | null;
|
|
204
197
|
}
|
|
@@ -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
|
-
|
|
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
|
-
):
|
|
610
|
-
|
|
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
|
-
|
|
547
|
+
continue;
|
|
614
548
|
}
|
|
615
|
-
const
|
|
616
|
-
const
|
|
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 (!
|
|
621
|
-
|
|
554
|
+
if (!source || !destination) {
|
|
555
|
+
continue;
|
|
622
556
|
}
|
|
623
|
-
|
|
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:
|
|
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
|
|
|
@@ -4,9 +4,11 @@ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join } from "pathe";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
6
6
|
|
|
7
|
+
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
7
8
|
import type { BlumeConfig } from "../../core/schema.ts";
|
|
9
|
+
import { ensurePackageJson } from "../shared.ts";
|
|
8
10
|
import { assetSegments } from "./assets.ts";
|
|
9
|
-
import { loadMintlifyConfig } from "./config.ts";
|
|
11
|
+
import { loadMintlifyConfig, partitionMintlifyRedirects } from "./config.ts";
|
|
10
12
|
import { mintlifyI18n } from "./i18n.ts";
|
|
11
13
|
import { transformMintlifyContent } from "./transform.ts";
|
|
12
14
|
|
|
@@ -65,6 +67,22 @@ const droppedChromeWarnings = (
|
|
|
65
67
|
return warnings;
|
|
66
68
|
};
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Warn about dynamic (wildcard/param) redirects the migrator dropped. Blume
|
|
72
|
+
* redirects are static path-to-path, so a `:slug*`/`:id` source becomes an
|
|
73
|
+
* unroutable Astro destination — kept ones crash the build. Point the user at
|
|
74
|
+
* host-level rules that do support wildcards.
|
|
75
|
+
*/
|
|
76
|
+
const droppedRedirectWarnings = (spec: Record<string, unknown>): string[] => {
|
|
77
|
+
const { dropped } = partitionMintlifyRedirects(spec);
|
|
78
|
+
if (dropped.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
`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).`,
|
|
83
|
+
];
|
|
84
|
+
};
|
|
85
|
+
|
|
68
86
|
/** Recursively drop `undefined`, empty arrays, and empty objects. */
|
|
69
87
|
const prune = (value: unknown): unknown => {
|
|
70
88
|
if (Array.isArray(value)) {
|
|
@@ -172,6 +190,27 @@ const applyRelocatedAssets = (
|
|
|
172
190
|
}
|
|
173
191
|
};
|
|
174
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Scaffold the project files a config-only Mintlify repo lacks: a runnable
|
|
195
|
+
* `package.json` (it ships no npm manifest) and a `.gitignore` for Blume's
|
|
196
|
+
* generated `.blume/` runtime and `dist/` build output. Both are idempotent —
|
|
197
|
+
* an existing file is extended, not overwritten — and noted in the warnings.
|
|
198
|
+
*/
|
|
199
|
+
const scaffoldProjectFiles = async (
|
|
200
|
+
root: string,
|
|
201
|
+
warnings: string[]
|
|
202
|
+
): Promise<void> => {
|
|
203
|
+
if (await ensurePackageJson(root)) {
|
|
204
|
+
warnings.push(
|
|
205
|
+
"Created a package.json with blume as a dependency; run `npm install`, then `npm run dev`."
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
209
|
+
if (ignored.length > 0) {
|
|
210
|
+
warnings.push(`Added ${ignored.join(", ")} to .gitignore.`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
175
214
|
/**
|
|
176
215
|
* Delete the inlined markdown snippets. Component files (e.g. `.jsx`) are kept
|
|
177
216
|
* because their imports were rewritten to resolve against `/snippets`.
|
|
@@ -247,6 +286,7 @@ export const migrateMintlifyProject = async (
|
|
|
247
286
|
);
|
|
248
287
|
}
|
|
249
288
|
warnings.push(...droppedChromeWarnings(spec, config));
|
|
289
|
+
warnings.push(...droppedRedirectWarnings(spec));
|
|
250
290
|
} else {
|
|
251
291
|
warnings.push("No docs.json or mint.json found; writing a default config.");
|
|
252
292
|
config = { content: { root: "." }, title: "Documentation" };
|
|
@@ -307,6 +347,7 @@ export const migrateMintlifyProject = async (
|
|
|
307
347
|
}
|
|
308
348
|
applyRelocatedAssets(config, assets, warnings);
|
|
309
349
|
await writeBlumeConfig(root, config);
|
|
350
|
+
await scaffoldProjectFiles(root, warnings);
|
|
310
351
|
|
|
311
352
|
if (Object.keys(variables).length > 0) {
|
|
312
353
|
warnings.push(
|
|
@@ -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);
|
package/src/migrate/shared.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import { isAbsolute, join, relative } from "pathe";
|
|
4
|
+
import { basename, isAbsolute, join, relative } from "pathe";
|
|
5
5
|
|
|
6
|
+
import { blumePackageJson, toPackageName } from "../core/package-json.ts";
|
|
6
7
|
import type { BlumeConfig } from "../core/schema.ts";
|
|
7
8
|
import { pageMetaSchema } from "../core/schema.ts";
|
|
8
9
|
|
|
@@ -102,6 +103,28 @@ export const rewriteFrameworkScripts = async (
|
|
|
102
103
|
export const leftoverFiles = (root: string, candidates: string[]): string[] =>
|
|
103
104
|
candidates.filter((candidate) => existsSync(join(root, candidate)));
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Scaffold a minimal, runnable `package.json` when the migrated project has
|
|
108
|
+
* none. Config-only sources (e.g. a Mintlify `docs.json`) ship no npm manifest,
|
|
109
|
+
* so a fresh migration has nothing to run `blume dev` with; this writes a stub
|
|
110
|
+
* with `blume` as a dependency and `dev`/`build`/`doctor` scripts, making
|
|
111
|
+
* `npm install && npm run dev` work immediately. A pre-existing `package.json`
|
|
112
|
+
* is left untouched — {@link rewriteFrameworkScripts} repoints those instead.
|
|
113
|
+
* Returns true when a file was created.
|
|
114
|
+
*/
|
|
115
|
+
export const ensurePackageJson = async (root: string): Promise<boolean> => {
|
|
116
|
+
const pkgPath = join(root, "package.json");
|
|
117
|
+
if (existsSync(pkgPath)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
await writeFile(
|
|
121
|
+
pkgPath,
|
|
122
|
+
blumePackageJson(toPackageName(basename(root))),
|
|
123
|
+
"utf-8"
|
|
124
|
+
);
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
105
128
|
// ---------------------------------------------------------------------------
|
|
106
129
|
// Callout components -> Blume `:::` directives
|
|
107
130
|
// ---------------------------------------------------------------------------
|