blume 0.2.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.
- package/dist/cli/index.js +2429 -792
- package/dist/cli/index.js.map +63 -44
- package/dist/types/core/data.d.ts +16 -0
- package/dist/types/core/define-components.d.ts +9 -2
- package/dist/types/core/diagnostics.d.ts +5 -0
- package/dist/types/core/schema.d.ts +313 -778
- package/dist/types/core/types.d.ts +2 -2
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +26 -40
- package/docs/advanced/api-reference.mdx +10 -37
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +66 -61
- package/docs/configuration/ai.mdx +47 -91
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +92 -27
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +78 -85
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +18 -44
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +42 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +79 -33
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +42 -56
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +13 -51
- package/docs/reference/cli.mdx +49 -18
- package/docs/reference/frontmatter.mdx +2 -5
- package/package.json +3 -1
- package/src/ai/ask-context.ts +131 -0
- package/src/ai/ask-data.ts +25 -0
- package/src/astro/component-slots.ts +165 -0
- package/src/astro/generate.ts +132 -13
- package/src/astro/integration.ts +85 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +11 -13
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +120 -50
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +209 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +32 -3
- package/src/cli/commands/doctor.ts +32 -6
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +184 -16
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +27 -2
- package/src/cli/dev-lock.ts +84 -0
- package/src/cli/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +41 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +109 -0
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +29 -15
- package/src/components/islands/hooks.ts +188 -0
- package/src/components/layout/Empty.astro +6 -0
- package/src/components/layout/Header.astro +24 -39
- package/src/components/layout/Logo.astro +50 -0
- package/src/components/layout/NavSelector.astro +75 -0
- package/src/components/layout/PageLayout.astro +38 -2
- package/src/components/layout/RootLayout.astro +70 -4
- package/src/components/layout/hydration-hint.ts +30 -0
- package/src/components/layout/overrides.ts +6 -4
- package/src/components/props.ts +71 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +40 -0
- package/src/core/component-diagnostics.ts +44 -0
- package/src/core/component-overrides.ts +478 -0
- package/src/core/config.ts +8 -0
- package/src/core/data.ts +14 -0
- package/src/core/define-components.ts +9 -2
- package/src/core/diagnostics.ts +95 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/graph.ts +7 -0
- package/src/core/links.ts +60 -19
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +35 -96
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/normalize.ts +57 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +1 -176
- package/src/migrate/mintlify/index.ts +53 -45
- package/src/migrate/shared.ts +12 -27
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +175 -38
- package/src/registry/eject.ts +52 -12
- package/src/registry/registry.ts +172 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/runtime/index.ts +61 -0
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/palette.ts +26 -7
- package/src/vite-env.d.ts +14 -0
|
@@ -10,7 +10,14 @@ const WHITESPACE = /\s+/u;
|
|
|
10
10
|
const WHITESPACE_RUN = /\s+/gu;
|
|
11
11
|
const GLOBAL_FLAGS = new Set(["-g", "--global"]);
|
|
12
12
|
|
|
13
|
-
type Operation =
|
|
13
|
+
type Operation =
|
|
14
|
+
| "add"
|
|
15
|
+
| "ci"
|
|
16
|
+
| "create"
|
|
17
|
+
| "exec"
|
|
18
|
+
| "install"
|
|
19
|
+
| "remove"
|
|
20
|
+
| "run";
|
|
14
21
|
|
|
15
22
|
interface Intent {
|
|
16
23
|
args: string[];
|
|
@@ -26,6 +33,9 @@ const normalizeVerb = (verb: string): Operation | null => {
|
|
|
26
33
|
case "install": {
|
|
27
34
|
return "add";
|
|
28
35
|
}
|
|
36
|
+
case "ci": {
|
|
37
|
+
return "ci";
|
|
38
|
+
}
|
|
29
39
|
case "create":
|
|
30
40
|
case "init": {
|
|
31
41
|
return "create";
|
|
@@ -125,10 +135,22 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
125
135
|
}
|
|
126
136
|
return `${manager} dlx ${args}`;
|
|
127
137
|
}
|
|
128
|
-
case "
|
|
138
|
+
case "ci": {
|
|
139
|
+
// `npm ci` maps to a frozen, lockfile-faithful install elsewhere.
|
|
129
140
|
return manager === "npm"
|
|
130
|
-
?
|
|
131
|
-
: `${manager}
|
|
141
|
+
? "npm ci"
|
|
142
|
+
: `${manager} install --frozen-lockfile`;
|
|
143
|
+
}
|
|
144
|
+
case "remove": {
|
|
145
|
+
if (manager === "npm") {
|
|
146
|
+
return `npm uninstall ${args}`;
|
|
147
|
+
}
|
|
148
|
+
// Yarn Classic has no `remove -g`; the global form is `yarn global remove`.
|
|
149
|
+
if (manager === "yarn" && intent.args.some((a) => GLOBAL_FLAGS.has(a))) {
|
|
150
|
+
const pkgs = intent.args.filter((a) => !GLOBAL_FLAGS.has(a)).join(" ");
|
|
151
|
+
return `yarn global remove ${pkgs}`;
|
|
152
|
+
}
|
|
153
|
+
return `${manager} remove ${args}`;
|
|
132
154
|
}
|
|
133
155
|
case "run": {
|
|
134
156
|
return `${manager} run ${args}`;
|
|
@@ -4,7 +4,12 @@ import { readFile as readFileFromDisk } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, resolve } from "pathe";
|
|
5
5
|
|
|
6
6
|
import matter from "../../core/frontmatter.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
findOpenTagEnd,
|
|
9
|
+
isInsideRoot,
|
|
10
|
+
renameTag,
|
|
11
|
+
rewriteCallouts,
|
|
12
|
+
} from "../shared.ts";
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* Source-to-source rewrites that turn Fumadocs-only MDX into idiomatic Blume
|
|
@@ -291,6 +296,8 @@ const INCLUDE = /<include\b[^>]*>(?<path>[\s\S]*?)<\/include>/gu;
|
|
|
291
296
|
interface IncludeOptions {
|
|
292
297
|
filePath: string;
|
|
293
298
|
readFile?: (file: string) => Promise<string>;
|
|
299
|
+
/** Docs root the include must stay within; targets escaping it are skipped. */
|
|
300
|
+
root: string;
|
|
294
301
|
seen?: Set<string>;
|
|
295
302
|
}
|
|
296
303
|
|
|
@@ -321,6 +328,12 @@ export const inlineFumadocsIncludes = async (
|
|
|
321
328
|
continue;
|
|
322
329
|
}
|
|
323
330
|
const target = resolve(dirname(options.filePath), rawPath);
|
|
331
|
+
if (!isInsideRoot(options.root, target)) {
|
|
332
|
+
warnings.push(
|
|
333
|
+
`<include> target "${rawPath}" is outside the docs tree — left as-is.`
|
|
334
|
+
);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
324
337
|
if (seen.has(target)) {
|
|
325
338
|
warnings.push(`Circular <include> "${rawPath}" — left as-is.`);
|
|
326
339
|
continue;
|
|
@@ -3,6 +3,7 @@ import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
|
3
3
|
|
|
4
4
|
import { basename, join } from "pathe";
|
|
5
5
|
|
|
6
|
+
import { isInsideRoot } from "../shared.ts";
|
|
6
7
|
import { renderMetaModule } from "./meta.ts";
|
|
7
8
|
import type {
|
|
8
9
|
FumadocsPageItem,
|
|
@@ -47,6 +48,12 @@ const isDirectory = (path: string): boolean => {
|
|
|
47
48
|
|
|
48
49
|
/** Resolve a `pages` name to its on-disk page file or folder under `docsDir`. */
|
|
49
50
|
const resolveEntry = (docsDir: string, name: string): ResolvedEntry | null => {
|
|
51
|
+
// A `pages` entry is author-controlled; reject any that escapes `docsDir`
|
|
52
|
+
// (e.g. `"../../victim"`) so the later `rename` can't move a file out of the
|
|
53
|
+
// docs tree.
|
|
54
|
+
if (!isInsideRoot(docsDir, join(docsDir, name))) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
50
57
|
for (const ext of PAGE_EXTS) {
|
|
51
58
|
const file = join(docsDir, `${name}${ext}`);
|
|
52
59
|
if (existsSync(file)) {
|
|
@@ -5,9 +5,9 @@ import { dirname, join, relative } from "pathe";
|
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
6
6
|
|
|
7
7
|
import matter from "../../core/frontmatter.ts";
|
|
8
|
+
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
8
9
|
import type { FolderMeta } from "../../core/schema.ts";
|
|
9
10
|
import {
|
|
10
|
-
ensureGitignore,
|
|
11
11
|
leftoverFiles,
|
|
12
12
|
rewriteFrameworkScripts,
|
|
13
13
|
writeBlumeConfig,
|
|
@@ -85,7 +85,10 @@ const movePage = async (
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const raw = await readFile(abs, "utf-8");
|
|
88
|
-
const included = await inlineFumadocsIncludes(raw, {
|
|
88
|
+
const included = await inlineFumadocsIncludes(raw, {
|
|
89
|
+
filePath: abs,
|
|
90
|
+
root: base,
|
|
91
|
+
});
|
|
89
92
|
let text = stripFumadocsImports(included.content);
|
|
90
93
|
text = rewriteFumadocsCallouts(text);
|
|
91
94
|
text = rewriteFumadocsContainers(text);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BlumeConfig } from "../../core/schema.ts";
|
|
2
|
+
|
|
3
|
+
/** Asset paths referenced by the resolved config (logo, favicon, backgrounds). */
|
|
4
|
+
const assetRefs = (config: BlumeConfig): unknown[] => {
|
|
5
|
+
const refs: unknown[] = ["/images"];
|
|
6
|
+
const logo = config.logo as
|
|
7
|
+
| string
|
|
8
|
+
| { dark?: string; light?: string }
|
|
9
|
+
| undefined;
|
|
10
|
+
if (typeof logo === "string") {
|
|
11
|
+
refs.push(logo);
|
|
12
|
+
} else if (logo) {
|
|
13
|
+
refs.push(logo.light, logo.dark);
|
|
14
|
+
}
|
|
15
|
+
const favicon = config.favicon as
|
|
16
|
+
| string
|
|
17
|
+
| { dark?: string; light?: string }
|
|
18
|
+
| undefined;
|
|
19
|
+
if (typeof favicon === "string") {
|
|
20
|
+
refs.push(favicon);
|
|
21
|
+
} else if (favicon) {
|
|
22
|
+
refs.push(favicon.light, favicon.dark);
|
|
23
|
+
}
|
|
24
|
+
refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
|
|
25
|
+
return refs;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Top-level path segments referenced as static assets by a Mintlify config
|
|
30
|
+
* (the conventional `/images`, plus logo/favicon/background paths). These are
|
|
31
|
+
* the root-served folders Mintlify exposes at the site root; Blume serves them
|
|
32
|
+
* via `content.assets` (bridge) or relocates them under `public/` (migrator).
|
|
33
|
+
*/
|
|
34
|
+
export const assetSegments = (config: BlumeConfig): string[] => {
|
|
35
|
+
const segments = new Set<string>();
|
|
36
|
+
for (const ref of assetRefs(config)) {
|
|
37
|
+
if (typeof ref !== "string" || !ref.startsWith("/")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const [segment] = ref.replace(/^\/+/u, "").split("/");
|
|
41
|
+
if (segment) {
|
|
42
|
+
segments.add(segment);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...segments];
|
|
46
|
+
};
|
|
@@ -579,72 +579,6 @@ const mintlifySelectors = (spec: JsonObject): NavigationSelectors => {
|
|
|
579
579
|
].filter((selector) => selector.items.length > 0);
|
|
580
580
|
};
|
|
581
581
|
|
|
582
|
-
const navbarTypeLabel = (type: string | undefined): string | undefined => {
|
|
583
|
-
if (type === "github") {
|
|
584
|
-
return "GitHub";
|
|
585
|
-
}
|
|
586
|
-
if (type === "discord") {
|
|
587
|
-
return "Discord";
|
|
588
|
-
}
|
|
589
|
-
return undefined;
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
const navbarLinkType = (
|
|
593
|
-
type: string | undefined
|
|
594
|
-
): "github" | "discord" | undefined =>
|
|
595
|
-
type === "github" || type === "discord" ? type : undefined;
|
|
596
|
-
|
|
597
|
-
const navbarPrimaryType = (
|
|
598
|
-
type: string | undefined
|
|
599
|
-
): "button" | "github" | "discord" =>
|
|
600
|
-
type === "github" || type === "discord" ? type : "button";
|
|
601
|
-
|
|
602
|
-
const mintlifyNavbar = (value: unknown): NonNullable<BlumeConfig["navbar"]> => {
|
|
603
|
-
const object = asObject(value);
|
|
604
|
-
if (!object) {
|
|
605
|
-
return { links: [] };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const links = asArray(object.links).flatMap((item) => {
|
|
609
|
-
const itemObject = asObject(item);
|
|
610
|
-
const href = itemObject ? asString(itemObject.href) : undefined;
|
|
611
|
-
const type = itemObject ? asString(itemObject.type) : undefined;
|
|
612
|
-
const label = itemObject
|
|
613
|
-
? (asString(itemObject.label) ?? navbarTypeLabel(type))
|
|
614
|
-
: undefined;
|
|
615
|
-
if (!itemObject || !href || !label) {
|
|
616
|
-
return [];
|
|
617
|
-
}
|
|
618
|
-
return [
|
|
619
|
-
withoutUndefined({
|
|
620
|
-
href,
|
|
621
|
-
icon: asString(itemObject.icon),
|
|
622
|
-
label,
|
|
623
|
-
type: navbarLinkType(type),
|
|
624
|
-
}),
|
|
625
|
-
];
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
const primaryObject = asObject(object.primary);
|
|
629
|
-
const primaryHref = primaryObject ? asString(primaryObject.href) : undefined;
|
|
630
|
-
const primaryType = primaryObject
|
|
631
|
-
? (asString(primaryObject.type) ?? "button")
|
|
632
|
-
: undefined;
|
|
633
|
-
const primaryLabel = primaryObject
|
|
634
|
-
? (asString(primaryObject.label) ?? navbarTypeLabel(primaryType))
|
|
635
|
-
: undefined;
|
|
636
|
-
const primary =
|
|
637
|
-
primaryObject && primaryHref && primaryLabel
|
|
638
|
-
? withoutUndefined({
|
|
639
|
-
href: primaryHref,
|
|
640
|
-
label: primaryLabel,
|
|
641
|
-
type: navbarPrimaryType(primaryType),
|
|
642
|
-
})
|
|
643
|
-
: undefined;
|
|
644
|
-
|
|
645
|
-
return withoutUndefined({ links, primary });
|
|
646
|
-
};
|
|
647
|
-
|
|
648
582
|
const mintignorePatterns = async (root: string): Promise<string[]> => {
|
|
649
583
|
try {
|
|
650
584
|
const raw = await readFile(resolve(root, ".mintignore"), "utf-8");
|
|
@@ -678,78 +612,6 @@ const mintlifyRedirects = (
|
|
|
678
612
|
return [{ from, to }];
|
|
679
613
|
});
|
|
680
614
|
|
|
681
|
-
const mintlifyContextual = (
|
|
682
|
-
value: unknown
|
|
683
|
-
): NonNullable<BlumeConfig["contextual"]> => {
|
|
684
|
-
const object = asObject(value);
|
|
685
|
-
if (!object) {
|
|
686
|
-
return { options: [] };
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const display = object.display === "toc" ? "toc" : "header";
|
|
690
|
-
const options: NonNullable<BlumeConfig["contextual"]>["options"] = [];
|
|
691
|
-
for (const option of asArray(object.options)) {
|
|
692
|
-
if (typeof option === "string") {
|
|
693
|
-
options.push(option);
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const optionObject = asObject(option);
|
|
698
|
-
const title = optionObject ? asString(optionObject.title) : undefined;
|
|
699
|
-
if (!optionObject || !title) {
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
options.push(
|
|
704
|
-
withoutUndefined({
|
|
705
|
-
description: asString(optionObject.description),
|
|
706
|
-
href: asString(optionObject.href),
|
|
707
|
-
icon: asString(optionObject.icon),
|
|
708
|
-
title,
|
|
709
|
-
})
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return { display, options };
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
const mintlifyFooter = (value: unknown): NonNullable<BlumeConfig["footer"]> => {
|
|
717
|
-
const object = asObject(value);
|
|
718
|
-
const socials = asObject(object?.socials);
|
|
719
|
-
const links = asArray(object?.links)
|
|
720
|
-
.flatMap((group) => {
|
|
721
|
-
const groupObject = asObject(group);
|
|
722
|
-
const items = asArray(groupObject?.items).flatMap((item) => {
|
|
723
|
-
const itemObject = asObject(item);
|
|
724
|
-
const label = itemObject ? asString(itemObject.label) : undefined;
|
|
725
|
-
const href = itemObject ? asString(itemObject.href) : undefined;
|
|
726
|
-
return label && href ? [{ href, label }] : [];
|
|
727
|
-
});
|
|
728
|
-
if (!groupObject || items.length === 0) {
|
|
729
|
-
return [];
|
|
730
|
-
}
|
|
731
|
-
return [
|
|
732
|
-
withoutUndefined({
|
|
733
|
-
header: asString(groupObject.header),
|
|
734
|
-
items,
|
|
735
|
-
}),
|
|
736
|
-
];
|
|
737
|
-
})
|
|
738
|
-
.slice(0, 4);
|
|
739
|
-
|
|
740
|
-
return {
|
|
741
|
-
links,
|
|
742
|
-
socials: socials
|
|
743
|
-
? Object.fromEntries(
|
|
744
|
-
Object.entries(socials).flatMap(([label, href]) => {
|
|
745
|
-
const hrefValue = asString(href);
|
|
746
|
-
return hrefValue ? [[label, hrefValue]] : [];
|
|
747
|
-
})
|
|
748
|
-
)
|
|
749
|
-
: {},
|
|
750
|
-
};
|
|
751
|
-
};
|
|
752
|
-
|
|
753
615
|
const mintlifyLogo = (value: unknown): BlumeConfig["logo"] => {
|
|
754
616
|
if (typeof value === "string") {
|
|
755
617
|
return value;
|
|
@@ -826,21 +688,13 @@ const mintlifyChromeVariants = (spec: JsonObject): NavigationChromeVariants => {
|
|
|
826
688
|
const banner = hasOwn(object, "banner")
|
|
827
689
|
? mintlifyBanner(object.banner)
|
|
828
690
|
: undefined;
|
|
829
|
-
|
|
830
|
-
? mintlifyFooter(object.footer)
|
|
831
|
-
: undefined;
|
|
832
|
-
const navbar = hasOwn(object, "navbar")
|
|
833
|
-
? mintlifyNavbar(object.navbar)
|
|
834
|
-
: undefined;
|
|
835
|
-
if (!banner && !footer && !navbar) {
|
|
691
|
+
if (!banner) {
|
|
836
692
|
return [];
|
|
837
693
|
}
|
|
838
694
|
|
|
839
695
|
return [
|
|
840
696
|
withoutUndefined({
|
|
841
697
|
banner,
|
|
842
|
-
footer,
|
|
843
|
-
navbar,
|
|
844
698
|
path,
|
|
845
699
|
}),
|
|
846
700
|
];
|
|
@@ -914,19 +768,6 @@ const mintlifyMarkdown = (
|
|
|
914
768
|
});
|
|
915
769
|
};
|
|
916
770
|
|
|
917
|
-
const mintlifyStyling = (
|
|
918
|
-
value: unknown
|
|
919
|
-
): NonNullable<BlumeConfig["styling"]> => {
|
|
920
|
-
const object = asObject(value);
|
|
921
|
-
const eyebrows = asString(object?.eyebrows);
|
|
922
|
-
return withoutUndefined({
|
|
923
|
-
eyebrows:
|
|
924
|
-
eyebrows === "breadcrumbs" || eyebrows === "section"
|
|
925
|
-
? eyebrows
|
|
926
|
-
: undefined,
|
|
927
|
-
});
|
|
928
|
-
};
|
|
929
|
-
|
|
930
771
|
const mintlifySeo = (value: unknown): NonNullable<BlumeConfig["seo"]> => {
|
|
931
772
|
const object = asObject(value);
|
|
932
773
|
const metatags = asObject(object?.metatags);
|
|
@@ -942,17 +783,6 @@ const mintlifySeo = (value: unknown): NonNullable<BlumeConfig["seo"]> => {
|
|
|
942
783
|
};
|
|
943
784
|
};
|
|
944
785
|
|
|
945
|
-
const mintlifyIcons = (value: unknown): NonNullable<BlumeConfig["icons"]> => {
|
|
946
|
-
const object = asObject(value);
|
|
947
|
-
const library = object?.library;
|
|
948
|
-
return withoutUndefined({
|
|
949
|
-
library:
|
|
950
|
-
library === "fontawesome" || library === "lucide" || library === "tabler"
|
|
951
|
-
? library
|
|
952
|
-
: undefined,
|
|
953
|
-
});
|
|
954
|
-
};
|
|
955
|
-
|
|
956
786
|
export const loadMintlifyConfig = async (
|
|
957
787
|
root: string,
|
|
958
788
|
file: string
|
|
@@ -993,14 +823,10 @@ export const loadMintlifyConfig = async (
|
|
|
993
823
|
],
|
|
994
824
|
root: ".",
|
|
995
825
|
},
|
|
996
|
-
contextual: mintlifyContextual(spec.contextual),
|
|
997
826
|
description: asString(spec.description),
|
|
998
827
|
favicon: mintlifyFavicon(spec.favicon),
|
|
999
|
-
footer: mintlifyFooter(spec.footer),
|
|
1000
|
-
icons: mintlifyIcons(spec.icons),
|
|
1001
828
|
logo: mintlifyLogo(spec.logo),
|
|
1002
829
|
markdown: mintlifyMarkdown(spec.markdown, styling),
|
|
1003
|
-
navbar: mintlifyNavbar(spec.navbar),
|
|
1004
830
|
navigation: {
|
|
1005
831
|
chromeVariants: mintlifyChromeVariants(spec),
|
|
1006
832
|
selectors: mintlifySelectors(spec),
|
|
@@ -1016,7 +842,6 @@ export const loadMintlifyConfig = async (
|
|
|
1016
842
|
prompt: asString(search.prompt),
|
|
1017
843
|
},
|
|
1018
844
|
seo: mintlifySeo(seo),
|
|
1019
|
-
styling: mintlifyStyling(styling),
|
|
1020
845
|
theme: {
|
|
1021
846
|
accent: asString(colors.primary) ?? "blue",
|
|
1022
847
|
accentDark: asString(colors.light),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { dirname, join } from "pathe";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
6
6
|
|
|
7
7
|
import type { BlumeConfig } from "../../core/schema.ts";
|
|
8
|
+
import { assetSegments } from "./assets.ts";
|
|
8
9
|
import { loadMintlifyConfig } from "./config.ts";
|
|
9
10
|
import { mintlifyI18n } from "./i18n.ts";
|
|
10
11
|
import { transformMintlifyContent } from "./transform.ts";
|
|
@@ -52,28 +53,37 @@ const writeBlumeConfig = async (
|
|
|
52
53
|
await writeFile(join(root, "blume.config.ts"), body, "utf-8");
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
interface RelocatedAssets {
|
|
57
|
+
/** Top-level dirs served in place via `content.assets` (no files moved). */
|
|
58
|
+
served: string[];
|
|
59
|
+
/** Top-level files moved under `public/`. */
|
|
60
|
+
moved: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Make referenced top-level assets resolvable in Blume. Directories (e.g.
|
|
65
|
+
* Mintlify's `images/`) are left in place and served via `content.assets`, so
|
|
66
|
+
* the migration doesn't churn every file under them; loose top-level files
|
|
67
|
+
* (a root `favicon.png`, `logo.png`) are moved under `public/` since a mount
|
|
68
|
+
* points at a directory.
|
|
69
|
+
*/
|
|
56
70
|
const relocateAssets = async (
|
|
57
71
|
root: string,
|
|
58
|
-
|
|
59
|
-
): Promise<
|
|
60
|
-
const
|
|
61
|
-
for (const ref of refs) {
|
|
62
|
-
if (typeof ref !== "string" || !ref.startsWith("/")) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const [segment] = ref.replace(/^\/+/u, "").split("/");
|
|
66
|
-
if (segment) {
|
|
67
|
-
segments.add(segment);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
72
|
+
segments: string[]
|
|
73
|
+
): Promise<RelocatedAssets> => {
|
|
74
|
+
const served: string[] = [];
|
|
71
75
|
const moved: string[] = [];
|
|
72
76
|
for (const segment of segments) {
|
|
73
77
|
const source = join(root, segment);
|
|
74
78
|
if (!existsSync(source) || segment === "public") {
|
|
75
79
|
continue;
|
|
76
80
|
}
|
|
81
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential fs stats
|
|
82
|
+
const stats = await stat(source);
|
|
83
|
+
if (stats.isDirectory()) {
|
|
84
|
+
served.push(segment);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
77
87
|
const dest = join(root, "public", segment);
|
|
78
88
|
if (existsSync(dest)) {
|
|
79
89
|
continue;
|
|
@@ -84,7 +94,32 @@ const relocateAssets = async (
|
|
|
84
94
|
await rename(source, dest);
|
|
85
95
|
moved.push(segment);
|
|
86
96
|
}
|
|
87
|
-
return moved;
|
|
97
|
+
return { moved, served };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fold relocated assets into the config (served dirs become `content.assets`)
|
|
102
|
+
* and record what happened. Served dirs stay in place; only loose files moved.
|
|
103
|
+
*/
|
|
104
|
+
const applyRelocatedAssets = (
|
|
105
|
+
config: BlumeConfig,
|
|
106
|
+
assets: RelocatedAssets,
|
|
107
|
+
warnings: string[]
|
|
108
|
+
): void => {
|
|
109
|
+
if (assets.served.length > 0) {
|
|
110
|
+
config.content = {
|
|
111
|
+
...config.content,
|
|
112
|
+
assets: [
|
|
113
|
+
...new Set([...(config.content?.assets ?? []), ...assets.served]),
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
warnings.push(
|
|
117
|
+
`Kept asset dir(s) in place, served via content.assets: ${assets.served.join(", ")}.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (assets.moved.length > 0) {
|
|
121
|
+
warnings.push(`Moved assets into public/: ${assets.moved.join(", ")}.`);
|
|
122
|
+
}
|
|
88
123
|
};
|
|
89
124
|
|
|
90
125
|
/**
|
|
@@ -121,31 +156,6 @@ const cleanupSnippets = async (
|
|
|
121
156
|
}
|
|
122
157
|
};
|
|
123
158
|
|
|
124
|
-
/** Asset paths referenced by the resolved config (logo, favicon, backgrounds). */
|
|
125
|
-
const assetRefs = (config: BlumeConfig): unknown[] => {
|
|
126
|
-
const refs: unknown[] = ["/images"];
|
|
127
|
-
const logo = config.logo as
|
|
128
|
-
| string
|
|
129
|
-
| { dark?: string; light?: string }
|
|
130
|
-
| undefined;
|
|
131
|
-
if (typeof logo === "string") {
|
|
132
|
-
refs.push(logo);
|
|
133
|
-
} else if (logo) {
|
|
134
|
-
refs.push(logo.light, logo.dark);
|
|
135
|
-
}
|
|
136
|
-
const favicon = config.favicon as
|
|
137
|
-
| string
|
|
138
|
-
| { dark?: string; light?: string }
|
|
139
|
-
| undefined;
|
|
140
|
-
if (typeof favicon === "string") {
|
|
141
|
-
refs.push(favicon);
|
|
142
|
-
} else if (favicon) {
|
|
143
|
-
refs.push(favicon.light, favicon.dark);
|
|
144
|
-
}
|
|
145
|
-
refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
|
|
146
|
-
return refs;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
159
|
/**
|
|
150
160
|
* Migrate a Mintlify project to Blume: translate `docs.json`/`mint.json` into
|
|
151
161
|
* `blume.config.ts`, rewrite every page to idiomatic Blume MDX in place, and
|
|
@@ -232,12 +242,13 @@ export const migrateMintlifyProject = async (
|
|
|
232
242
|
moved += 1;
|
|
233
243
|
}
|
|
234
244
|
|
|
235
|
-
const
|
|
245
|
+
const assets = await relocateAssets(root, assetSegments(config));
|
|
236
246
|
await cleanupSnippets(root, keptComponents, warnings);
|
|
237
247
|
|
|
238
248
|
if (config.content?.exclude) {
|
|
239
249
|
config.content.exclude = [...new Set(config.content.exclude)];
|
|
240
250
|
}
|
|
251
|
+
applyRelocatedAssets(config, assets, warnings);
|
|
241
252
|
await writeBlumeConfig(root, config);
|
|
242
253
|
|
|
243
254
|
if (Object.keys(variables).length > 0) {
|
|
@@ -245,9 +256,6 @@ export const migrateMintlifyProject = async (
|
|
|
245
256
|
`Inlined ${Object.keys(variables).length} docs.json variable(s) into content; Blume has no runtime variable substitution.`
|
|
246
257
|
);
|
|
247
258
|
}
|
|
248
|
-
if (movedAssets.length > 0) {
|
|
249
|
-
warnings.push(`Moved assets into public/: ${movedAssets.join(", ")}.`);
|
|
250
|
-
}
|
|
251
259
|
if (removedKeys.size > 0) {
|
|
252
260
|
warnings.push(
|
|
253
261
|
`Dropped unsupported page frontmatter keys: ${[...removedKeys].join(", ")}.`
|
package/src/migrate/shared.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import { join } from "pathe";
|
|
4
|
+
import { isAbsolute, join, relative } from "pathe";
|
|
5
5
|
|
|
6
6
|
import type { BlumeConfig } from "../core/schema.ts";
|
|
7
7
|
import { pageMetaSchema } from "../core/schema.ts";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Whether `candidate` resolves to a path inside `root` (or is `root` itself).
|
|
11
|
+
* Guards migrators against `../` traversal in author-controlled source paths
|
|
12
|
+
* (`pages` entries, `<include>` targets) that would otherwise read or move
|
|
13
|
+
* files outside the docs tree.
|
|
14
|
+
*/
|
|
15
|
+
export const isInsideRoot = (root: string, candidate: string): boolean => {
|
|
16
|
+
const rel = relative(root, candidate);
|
|
17
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
18
|
+
};
|
|
19
|
+
|
|
9
20
|
/**
|
|
10
21
|
* Framework-agnostic helpers shared by more than one migrator. Each piece here
|
|
11
22
|
* was generalized from a migrator-specific implementation so Mintlify, Nextra,
|
|
@@ -86,32 +97,6 @@ export const rewriteFrameworkScripts = async (
|
|
|
86
97
|
return changed;
|
|
87
98
|
};
|
|
88
99
|
|
|
89
|
-
/** A `.gitignore` line, normalized for comparison (trailing slashes dropped). */
|
|
90
|
-
const gitignoreKey = (line: string): string => line.trim().replace(/\/+$/u, "");
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Ensure `.gitignore` ignores each of `entries`, appending any that are missing
|
|
94
|
-
* (creating the file when absent). Trailing-slash differences (`dist` vs
|
|
95
|
-
* `dist/`) count as already present. Returns the entries actually added.
|
|
96
|
-
*/
|
|
97
|
-
export const ensureGitignore = async (
|
|
98
|
-
root: string,
|
|
99
|
-
entries: string[]
|
|
100
|
-
): Promise<string[]> => {
|
|
101
|
-
const path = join(root, ".gitignore");
|
|
102
|
-
const existing = existsSync(path) ? await readFile(path, "utf-8") : "";
|
|
103
|
-
const present = new Set(
|
|
104
|
-
existing.split("\n").map(gitignoreKey).filter(Boolean)
|
|
105
|
-
);
|
|
106
|
-
const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
|
|
107
|
-
if (added.length === 0) {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
const gap = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
111
|
-
await writeFile(path, `${existing}${gap}${added.join("\n")}\n`, "utf-8");
|
|
112
|
-
return added;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
100
|
/** Of the candidate project-relative paths, the ones that still exist — the old
|
|
116
101
|
* framework files a migration leaves behind for the user to remove by hand. */
|
|
117
102
|
export const leftoverFiles = (root: string, candidates: string[]): string[] =>
|
|
@@ -425,10 +425,6 @@ export const mapStarlightConfig = (
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
const social = mapSocial(options.social);
|
|
428
|
-
if (Object.keys(social.socials).length > 0) {
|
|
429
|
-
config.footer = { socials: social.socials };
|
|
430
|
-
}
|
|
431
|
-
|
|
432
428
|
const github = mapEditLink(options.editLink) ?? social.github;
|
|
433
429
|
if (github) {
|
|
434
430
|
config.github = github;
|