blume 0.5.2 → 0.5.4
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 +2235 -5942
- package/dist/cli/index.js.map +36 -65
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +2 -2
- package/dist/types/core/schema.d.ts +58 -453
- package/dist/types/core/types.d.ts +1 -7
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/theming.mdx +3 -11
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +1 -5
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +4 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/mcp/discovery.ts +3 -1
- package/src/ai/mcp/server.ts +3 -1
- package/src/astro/component-slots.ts +10 -2
- package/src/astro/generate.ts +15 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +62 -37
- package/src/cli/coalesce.ts +43 -0
- package/src/cli/commands/dev.ts +30 -22
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +4 -2
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +8 -5
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/Prompt.astro +8 -4
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/Tabs.astro +13 -2
- package/src/components/content/TypeTable.astro +5 -5
- package/src/components/content/Update.astro +1 -1
- package/src/components/content/mermaid-element.ts +20 -2
- package/src/components/islands/ask-ai.tsx +4 -8
- package/src/components/islands/base-path.ts +30 -0
- package/src/components/islands/hooks.ts +12 -8
- package/src/components/layout/Header.astro +26 -3
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +14 -14
- package/src/components/layout/PageActions.astro +17 -11
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +31 -2
- package/src/components/layout/Search.astro +9 -6
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/layout/search/types.ts +16 -5
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/SchemaProperty.astro +1 -1
- package/src/components/openapi/SchemaTable.astro +3 -3
- package/src/components/openapi/helpers.ts +17 -8
- package/src/components/openapi/snippets.ts +17 -4
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +19 -33
- package/src/core/data.ts +2 -2
- package/src/core/graph.ts +7 -5
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/navigation.ts +5 -6
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +13 -90
- package/src/core/sources/filesystem.ts +19 -1
- package/src/core/sources/mdx-remote.ts +20 -4
- package/src/core/sources/normalize.ts +28 -6
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +45 -0
- package/src/core/types.ts +1 -8
- package/src/markdown/code-title.ts +6 -3
- package/src/markdown/package-install.ts +3 -1
- package/src/og/card.ts +16 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +54 -11
- package/src/openapi/source.ts +19 -2
- package/src/search/sync/orama-cloud.ts +2 -0
- package/src/search/sync/typesense.ts +4 -0
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +22 -104
- package/src/theme/palette.ts +35 -47
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -123
- package/src/astro/static-assets.ts +0 -117
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -204
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -378
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -340
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -958
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -372
- package/src/migrate/mintlify/snippets.ts +0 -306
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -374
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -728
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -78
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import matter from "../../core/frontmatter.ts";
|
|
2
|
-
import { stripUnknownPageMeta } from "../shared.ts";
|
|
3
|
-
import {
|
|
4
|
-
rewriteMintlifyAccordions,
|
|
5
|
-
rewriteMintlifyCallouts,
|
|
6
|
-
rewriteMintlifyExampleBlocks,
|
|
7
|
-
rewriteSnippetImports,
|
|
8
|
-
unsupportedMintlifyComponents,
|
|
9
|
-
} from "./content.ts";
|
|
10
|
-
import { normalizeMintlifyPageMeta } from "./frontmatter.ts";
|
|
11
|
-
import { rewriteMintlifySvgIconProps } from "./icons.ts";
|
|
12
|
-
import {
|
|
13
|
-
rewriteMintlifyGlobalVariables,
|
|
14
|
-
rewriteMintlifyMarkdownSnippets,
|
|
15
|
-
rewriteMintlifySnippetVariables,
|
|
16
|
-
rewriteMintlifyUserVariable,
|
|
17
|
-
} from "./snippets.ts";
|
|
18
|
-
|
|
19
|
-
const USER_REFERENCE = /\{[^{}]*\buser\b/u;
|
|
20
|
-
|
|
21
|
-
/** The outcome of transforming a single Mintlify page to Blume MDX. */
|
|
22
|
-
export interface MintlifyTransformResult {
|
|
23
|
-
/** Component snippet imports kept (rewritten to relative paths). */
|
|
24
|
-
components: string[];
|
|
25
|
-
/** The rewritten file text, including normalized frontmatter. */
|
|
26
|
-
content: string;
|
|
27
|
-
/** Frontmatter keys dropped because Blume has no equivalent. */
|
|
28
|
-
removed: string[];
|
|
29
|
-
/** Components encountered that have no Blume equivalent. */
|
|
30
|
-
unsupported: string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Apply the per-file source transforms that turn Mintlify MDX into Blume MDX:
|
|
35
|
-
* inline snippets and variables, rewrite callouts (`<Note>` → `:::note`),
|
|
36
|
-
* example blocks, and inline-SVG icon props, then normalize frontmatter. Pure
|
|
37
|
-
* text-in/text-out (snippets read from disk relative to `root`), so both the
|
|
38
|
-
* one-shot migrator and the live bridge source share identical behavior.
|
|
39
|
-
*/
|
|
40
|
-
export const transformMintlifyContent = async (
|
|
41
|
-
raw: string,
|
|
42
|
-
options: { filePath: string; root: string; variables: Record<string, string> }
|
|
43
|
-
): Promise<MintlifyTransformResult> => {
|
|
44
|
-
let text = await rewriteMintlifyMarkdownSnippets(raw, {
|
|
45
|
-
filePath: options.filePath,
|
|
46
|
-
root: options.root,
|
|
47
|
-
});
|
|
48
|
-
text = await rewriteMintlifySnippetVariables(text, {
|
|
49
|
-
filePath: options.filePath,
|
|
50
|
-
root: options.root,
|
|
51
|
-
});
|
|
52
|
-
text = rewriteMintlifyGlobalVariables(text, options.variables);
|
|
53
|
-
if (USER_REFERENCE.test(text)) {
|
|
54
|
-
text = rewriteMintlifyUserVariable(text);
|
|
55
|
-
}
|
|
56
|
-
const snippetImports = rewriteSnippetImports(text, {
|
|
57
|
-
filePath: options.filePath,
|
|
58
|
-
root: options.root,
|
|
59
|
-
});
|
|
60
|
-
text = snippetImports.source;
|
|
61
|
-
text = rewriteMintlifySvgIconProps(text);
|
|
62
|
-
text = rewriteMintlifyExampleBlocks(text);
|
|
63
|
-
text = rewriteMintlifyAccordions(text);
|
|
64
|
-
text = rewriteMintlifyCallouts(text);
|
|
65
|
-
|
|
66
|
-
const unsupported = unsupportedMintlifyComponents(text);
|
|
67
|
-
|
|
68
|
-
const parsed = matter(text);
|
|
69
|
-
const mapped = normalizeMintlifyPageMeta(parsed.data);
|
|
70
|
-
const { data, removed } = stripUnknownPageMeta(mapped);
|
|
71
|
-
const content =
|
|
72
|
-
Object.keys(data).length > 0
|
|
73
|
-
? matter.stringify(parsed.content, data)
|
|
74
|
-
: parsed.content;
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
components: snippetImports.components,
|
|
78
|
-
content,
|
|
79
|
-
removed,
|
|
80
|
-
unsupported,
|
|
81
|
-
};
|
|
82
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { rewriteCallouts } from "../shared.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Source-to-source rewrites that turn Nextra-only MDX into idiomatic Blume
|
|
5
|
-
* markup. Runs once at migration time.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Nextra `<Callout type="X">` values mapped to Blume directive names. */
|
|
9
|
-
const CALLOUT_TYPE_DIRECTIVES: Record<string, string> = {
|
|
10
|
-
default: "note",
|
|
11
|
-
error: "danger",
|
|
12
|
-
info: "info",
|
|
13
|
-
warning: "warning",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Convert Nextra `<Callout>` components (with an optional `type`) into Blume
|
|
18
|
-
* `:::` directives. A bare `<Callout>` becomes `:::note`; the `emoji` prop is
|
|
19
|
-
* dropped.
|
|
20
|
-
*/
|
|
21
|
-
export const rewriteNextraCallouts = (source: string): string =>
|
|
22
|
-
rewriteCallouts(source, {
|
|
23
|
-
defaultDirective: "note",
|
|
24
|
-
tagDirectives: {},
|
|
25
|
-
tags: ["Callout"],
|
|
26
|
-
typeDirectives: CALLOUT_TYPE_DIRECTIVES,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const NEXTRA_IMPORT =
|
|
30
|
-
/^import\s+[\s\S]*?\s+from\s+["']nextra(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Drop `import … from "nextra/…"` statements. Blume injects its components
|
|
34
|
-
* globally, so these imports would fail to resolve once `nextra` is gone.
|
|
35
|
-
*/
|
|
36
|
-
export const stripNextraImports = (source: string): string =>
|
|
37
|
-
source.replace(NEXTRA_IMPORT, "");
|
|
38
|
-
|
|
39
|
-
/** Nextra components with no drop-in Blume equivalent — flagged for review. */
|
|
40
|
-
const UNSUPPORTED_COMPONENTS = ["Bleed", "Cards", "FileTree", "Steps", "Tabs"];
|
|
41
|
-
|
|
42
|
-
/** Names of Nextra components in `source` that need manual attention. */
|
|
43
|
-
export const unsupportedNextraComponents = (source: string): string[] =>
|
|
44
|
-
UNSUPPORTED_COMPONENTS.filter((name) =>
|
|
45
|
-
new RegExp(`<${name}\\b`, "u").test(source)
|
|
46
|
-
);
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { stripUnknownPageMeta } from "../shared.ts";
|
|
2
|
-
|
|
3
|
-
export interface NextraPageOverride {
|
|
4
|
-
hidden?: boolean;
|
|
5
|
-
label?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Fold Nextra `_meta` overrides (a sidebar label, `display: "hidden"`) into a
|
|
10
|
-
* page's frontmatter, then drop any keys Blume's strict page schema rejects.
|
|
11
|
-
* Nextra's own `title`/`description`/`sidebarTitle` already validate and pass
|
|
12
|
-
* through untouched. Explicit page frontmatter wins over `_meta` overrides.
|
|
13
|
-
*/
|
|
14
|
-
export const normalizeNextraPageMeta = (
|
|
15
|
-
value: unknown,
|
|
16
|
-
override: NextraPageOverride = {}
|
|
17
|
-
): { data: Record<string, unknown>; removed: string[] } => {
|
|
18
|
-
const source =
|
|
19
|
-
value && typeof value === "object"
|
|
20
|
-
? (value as Record<string, unknown>)
|
|
21
|
-
: {};
|
|
22
|
-
const data: Record<string, unknown> = { ...source };
|
|
23
|
-
|
|
24
|
-
const sidebar: Record<string, unknown> = {
|
|
25
|
-
...(typeof source.sidebar === "object" && source.sidebar !== null
|
|
26
|
-
? (source.sidebar as Record<string, unknown>)
|
|
27
|
-
: {}),
|
|
28
|
-
};
|
|
29
|
-
if (override.label !== undefined && sidebar.label === undefined) {
|
|
30
|
-
sidebar.label = override.label;
|
|
31
|
-
}
|
|
32
|
-
if (override.hidden && sidebar.hidden === undefined) {
|
|
33
|
-
sidebar.hidden = true;
|
|
34
|
-
}
|
|
35
|
-
if (Object.keys(sidebar).length > 0) {
|
|
36
|
-
data.sidebar = sidebar;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return stripUnknownPageMeta(data);
|
|
40
|
-
};
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
import { basename, dirname, extname, join, relative } from "pathe";
|
|
5
|
-
import { glob } from "tinyglobby";
|
|
6
|
-
|
|
7
|
-
import matter from "../../core/frontmatter.ts";
|
|
8
|
-
import type { BlumeConfig, FolderMeta } from "../../core/schema.ts";
|
|
9
|
-
import { writeBlumeConfig } from "../shared.ts";
|
|
10
|
-
import {
|
|
11
|
-
rewriteNextraCallouts,
|
|
12
|
-
stripNextraImports,
|
|
13
|
-
unsupportedNextraComponents,
|
|
14
|
-
} from "./content.ts";
|
|
15
|
-
import type { NextraPageOverride } from "./frontmatter.ts";
|
|
16
|
-
import { normalizeNextraPageMeta } from "./frontmatter.ts";
|
|
17
|
-
import { parseNextraMeta, toBlumeFolderMeta } from "./meta.ts";
|
|
18
|
-
|
|
19
|
-
export interface NextraMigrationResult {
|
|
20
|
-
moved: number;
|
|
21
|
-
warnings: string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface Tab {
|
|
25
|
-
label: string;
|
|
26
|
-
path: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface FolderIndex {
|
|
30
|
-
childDirs: Map<string, Set<string>>;
|
|
31
|
-
pagesByDir: Map<string, Map<string, string>>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface MetaPlan {
|
|
35
|
-
consumedMetas: string[];
|
|
36
|
-
folderTitleByDir: Map<string, string>;
|
|
37
|
-
metaByDir: Map<string, FolderMeta>;
|
|
38
|
-
pageOverrides: Map<string, NextraPageOverride>;
|
|
39
|
-
tabs: Tab[];
|
|
40
|
-
unparseableMetas: { abs: string; rel: string }[];
|
|
41
|
-
warnings: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface PageMove {
|
|
45
|
-
moved: number;
|
|
46
|
-
removed: string[];
|
|
47
|
-
skipped: string | null;
|
|
48
|
-
unsupported: string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Nextra keeps docs under `content/` (App Router) or `pages/` (Pages Router). */
|
|
52
|
-
const SOURCE_DIRS = ["content", "pages"];
|
|
53
|
-
|
|
54
|
-
const PAGE_GLOB = "**/*.{md,mdx}";
|
|
55
|
-
const META_GLOB = "**/_meta.{js,mjs,cjs,ts,jsx,tsx,json}";
|
|
56
|
-
const IGNORE = ["**/node_modules/**"];
|
|
57
|
-
|
|
58
|
-
/** Normalize a `dirname` result so the content root is "" rather than ".". */
|
|
59
|
-
const normalizeDir = (dir: string): string => (dir === "." ? "" : dir);
|
|
60
|
-
|
|
61
|
-
const mergeOverride = (
|
|
62
|
-
map: Map<string, NextraPageOverride>,
|
|
63
|
-
key: string,
|
|
64
|
-
patch: NextraPageOverride
|
|
65
|
-
): void => {
|
|
66
|
-
map.set(key, { ...map.get(key), ...patch });
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/** Index which child slugs in each folder are pages vs subdirectories. */
|
|
70
|
-
const indexFolders = (
|
|
71
|
-
base: string,
|
|
72
|
-
pageFiles: string[],
|
|
73
|
-
metaFiles: string[]
|
|
74
|
-
): FolderIndex => {
|
|
75
|
-
const pagesByDir = new Map<string, Map<string, string>>();
|
|
76
|
-
const allDirs = new Set<string>([""]);
|
|
77
|
-
const registerDir = (dir: string): void => {
|
|
78
|
-
let current = normalizeDir(dir);
|
|
79
|
-
allDirs.add(current);
|
|
80
|
-
while (current !== "") {
|
|
81
|
-
current = normalizeDir(dirname(current));
|
|
82
|
-
allDirs.add(current);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
for (const abs of pageFiles) {
|
|
87
|
-
const rel = relative(base, abs);
|
|
88
|
-
const dir = normalizeDir(dirname(rel));
|
|
89
|
-
const slug = basename(rel).replace(/\.mdx?$/u, "");
|
|
90
|
-
const folder = pagesByDir.get(dir) ?? new Map<string, string>();
|
|
91
|
-
folder.set(slug, abs);
|
|
92
|
-
pagesByDir.set(dir, folder);
|
|
93
|
-
registerDir(dir);
|
|
94
|
-
}
|
|
95
|
-
for (const abs of metaFiles) {
|
|
96
|
-
registerDir(normalizeDir(dirname(relative(base, abs))));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const childDirs = new Map<string, Set<string>>();
|
|
100
|
-
for (const dir of allDirs) {
|
|
101
|
-
if (dir === "") {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const parent = normalizeDir(dirname(dir));
|
|
105
|
-
const children = childDirs.get(parent) ?? new Set<string>();
|
|
106
|
-
children.add(basename(dir));
|
|
107
|
-
childDirs.set(parent, children);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { childDirs, pagesByDir };
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
interface ParsedMeta {
|
|
114
|
-
abs: string;
|
|
115
|
-
ext: string;
|
|
116
|
-
raw: string;
|
|
117
|
-
rel: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Parse every `_meta`, accumulating ordering, titles, and page overrides. */
|
|
121
|
-
const planMetas = (metas: ParsedMeta[], index: FolderIndex): MetaPlan => {
|
|
122
|
-
const plan: MetaPlan = {
|
|
123
|
-
consumedMetas: [],
|
|
124
|
-
folderTitleByDir: new Map(),
|
|
125
|
-
metaByDir: new Map(),
|
|
126
|
-
pageOverrides: new Map(),
|
|
127
|
-
tabs: [],
|
|
128
|
-
unparseableMetas: [],
|
|
129
|
-
warnings: [],
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
for (const meta of metas) {
|
|
133
|
-
const dir = normalizeDir(dirname(meta.rel));
|
|
134
|
-
const entries = parseNextraMeta(meta.raw, meta.ext);
|
|
135
|
-
if (!entries) {
|
|
136
|
-
plan.unparseableMetas.push({ abs: meta.abs, rel: meta.rel });
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const conversion = toBlumeFolderMeta(entries, {
|
|
141
|
-
hasDir: (slug) => index.childDirs.get(dir)?.has(slug) ?? false,
|
|
142
|
-
hasPage: (slug) => index.pagesByDir.get(dir)?.has(slug) ?? false,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
plan.metaByDir.set(dir, conversion.folderMeta);
|
|
146
|
-
plan.consumedMetas.push(meta.abs);
|
|
147
|
-
for (const [slug, title] of Object.entries(conversion.folderTitles)) {
|
|
148
|
-
plan.folderTitleByDir.set(normalizeDir(join(dir, slug)), title);
|
|
149
|
-
}
|
|
150
|
-
for (const [slug, label] of Object.entries(conversion.pageLabels)) {
|
|
151
|
-
const pageAbs = index.pagesByDir.get(dir)?.get(slug);
|
|
152
|
-
if (pageAbs) {
|
|
153
|
-
mergeOverride(plan.pageOverrides, pageAbs, { label });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
for (const slug of conversion.hiddenPages) {
|
|
157
|
-
const pageAbs = index.pagesByDir.get(dir)?.get(slug);
|
|
158
|
-
if (pageAbs) {
|
|
159
|
-
mergeOverride(plan.pageOverrides, pageAbs, { hidden: true });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (dir === "") {
|
|
163
|
-
plan.tabs.push(...conversion.navPages);
|
|
164
|
-
} else if (conversion.navPages.length > 0) {
|
|
165
|
-
plan.warnings.push(
|
|
166
|
-
`Ignored ${conversion.navPages.length} top-nav page entr(y/ies) in ${meta.rel}; type:"page" only maps at the root.`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
plan.warnings.push(...conversion.warnings);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return plan;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
/** Move and rewrite a single page into `docs/`. */
|
|
176
|
-
const movePage = async (
|
|
177
|
-
abs: string,
|
|
178
|
-
options: {
|
|
179
|
-
base: string;
|
|
180
|
-
overrides: Map<string, NextraPageOverride>;
|
|
181
|
-
root: string;
|
|
182
|
-
}
|
|
183
|
-
): Promise<PageMove> => {
|
|
184
|
-
const rel = relative(options.base, abs);
|
|
185
|
-
const dest = join(options.root, "docs", rel);
|
|
186
|
-
if (existsSync(dest)) {
|
|
187
|
-
return { moved: 0, removed: [], skipped: rel, unsupported: [] };
|
|
188
|
-
}
|
|
189
|
-
const raw = await readFile(abs, "utf-8");
|
|
190
|
-
const text = rewriteNextraCallouts(stripNextraImports(raw));
|
|
191
|
-
const unsupported = unsupportedNextraComponents(text);
|
|
192
|
-
const parsed = matter(text);
|
|
193
|
-
const { data, removed } = normalizeNextraPageMeta(
|
|
194
|
-
parsed.data,
|
|
195
|
-
options.overrides.get(abs) ?? {}
|
|
196
|
-
);
|
|
197
|
-
const content =
|
|
198
|
-
Object.keys(data).length > 0
|
|
199
|
-
? matter.stringify(parsed.content, data)
|
|
200
|
-
: parsed.content;
|
|
201
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
202
|
-
await writeFile(dest, content, "utf-8");
|
|
203
|
-
await rm(abs, { force: true });
|
|
204
|
-
return { moved: 1, removed, skipped: null, unsupported };
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
interface PageSummary {
|
|
208
|
-
moved: number;
|
|
209
|
-
removedKeys: string[];
|
|
210
|
-
skipped: string[];
|
|
211
|
-
unsupported: string[];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const summarizePageMoves = (results: PageMove[]): PageSummary => {
|
|
215
|
-
let moved = 0;
|
|
216
|
-
const removedKeys = new Set<string>();
|
|
217
|
-
const unsupported = new Set<string>();
|
|
218
|
-
const skipped: string[] = [];
|
|
219
|
-
for (const result of results) {
|
|
220
|
-
moved += result.moved;
|
|
221
|
-
if (result.skipped) {
|
|
222
|
-
skipped.push(result.skipped);
|
|
223
|
-
}
|
|
224
|
-
for (const key of result.removed) {
|
|
225
|
-
removedKeys.add(key);
|
|
226
|
-
}
|
|
227
|
-
for (const name of result.unsupported) {
|
|
228
|
-
unsupported.add(name);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return {
|
|
232
|
-
moved,
|
|
233
|
-
removedKeys: [...removedKeys],
|
|
234
|
-
skipped,
|
|
235
|
-
unsupported: [...unsupported],
|
|
236
|
-
};
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
/** Write a `meta.ts` per folder, merging own ordering with a parent title. */
|
|
240
|
-
const writeFolderMetas = async (
|
|
241
|
-
root: string,
|
|
242
|
-
plan: MetaPlan
|
|
243
|
-
): Promise<void> => {
|
|
244
|
-
const dirs = new Set<string>([
|
|
245
|
-
...plan.metaByDir.keys(),
|
|
246
|
-
...plan.folderTitleByDir.keys(),
|
|
247
|
-
]);
|
|
248
|
-
await Promise.all(
|
|
249
|
-
[...dirs].map(async (dir) => {
|
|
250
|
-
const finalMeta: FolderMeta = { ...plan.metaByDir.get(dir) };
|
|
251
|
-
const title = plan.folderTitleByDir.get(dir);
|
|
252
|
-
if (title !== undefined) {
|
|
253
|
-
finalMeta.title = title;
|
|
254
|
-
}
|
|
255
|
-
if (Object.keys(finalMeta).length === 0) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const dest = join(root, "docs", dir, "meta.ts");
|
|
259
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
260
|
-
await writeFile(
|
|
261
|
-
dest,
|
|
262
|
-
`import { defineMeta } from "blume";\n\nexport default defineMeta(${JSON.stringify(finalMeta, null, 2)});\n`,
|
|
263
|
-
"utf-8"
|
|
264
|
-
);
|
|
265
|
-
})
|
|
266
|
-
);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
/** Relocate `_meta` files we couldn't parse, untouched, for manual conversion. */
|
|
270
|
-
const relocateUnparseableMetas = async (
|
|
271
|
-
root: string,
|
|
272
|
-
metas: { abs: string; rel: string }[]
|
|
273
|
-
): Promise<string[]> => {
|
|
274
|
-
const warnings: string[] = [];
|
|
275
|
-
await Promise.all(
|
|
276
|
-
metas.map(async ({ abs, rel }) => {
|
|
277
|
-
const dest = join(root, "docs", rel);
|
|
278
|
-
if (existsSync(dest)) {
|
|
279
|
-
warnings.push(`Skipped ${rel} (target already exists)`);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
283
|
-
await rename(abs, dest);
|
|
284
|
-
warnings.push(
|
|
285
|
-
`Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`
|
|
286
|
-
);
|
|
287
|
-
})
|
|
288
|
-
);
|
|
289
|
-
return warnings;
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const buildConfig = (tabs: Tab[]): BlumeConfig =>
|
|
293
|
-
tabs.length > 0
|
|
294
|
-
? { navigation: { tabs }, title: "Documentation" }
|
|
295
|
-
: { title: "Documentation" };
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Migrate a Nextra project to Blume: move every `.md`/`.mdx` page into `docs/`,
|
|
299
|
-
* rewrite Nextra `<Callout>`s to directives, and convert each `_meta` file into
|
|
300
|
-
* a typed `meta.ts`. Nextra declares a folder's title (and per-page sidebar
|
|
301
|
-
* labels) in the *parent* `_meta`, so those are propagated into child `meta.ts`
|
|
302
|
-
* titles and page frontmatter. Content lands in the default `docs/` root.
|
|
303
|
-
*/
|
|
304
|
-
export const migrateNextraProject = async (
|
|
305
|
-
root: string
|
|
306
|
-
): Promise<NextraMigrationResult> => {
|
|
307
|
-
const sourceDir = SOURCE_DIRS.find((dir) => existsSync(join(root, dir)));
|
|
308
|
-
if (!sourceDir) {
|
|
309
|
-
await writeBlumeConfig(root, { title: "Documentation" });
|
|
310
|
-
return {
|
|
311
|
-
moved: 0,
|
|
312
|
-
warnings: [
|
|
313
|
-
"No Nextra content directory (content/ or pages/) found; wrote a default config.",
|
|
314
|
-
],
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
const base = join(root, sourceDir);
|
|
318
|
-
|
|
319
|
-
const pageFiles = await glob([PAGE_GLOB], {
|
|
320
|
-
absolute: true,
|
|
321
|
-
cwd: base,
|
|
322
|
-
ignore: IGNORE,
|
|
323
|
-
});
|
|
324
|
-
const metaFiles = await glob([META_GLOB], {
|
|
325
|
-
absolute: true,
|
|
326
|
-
cwd: base,
|
|
327
|
-
ignore: IGNORE,
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const index = indexFolders(base, pageFiles, metaFiles);
|
|
331
|
-
const metas = await Promise.all(
|
|
332
|
-
metaFiles.map(async (abs) => ({
|
|
333
|
-
abs,
|
|
334
|
-
ext: extname(abs),
|
|
335
|
-
raw: await readFile(abs, "utf-8"),
|
|
336
|
-
rel: relative(base, abs),
|
|
337
|
-
}))
|
|
338
|
-
);
|
|
339
|
-
const plan = planMetas(metas, index);
|
|
340
|
-
|
|
341
|
-
const moves = await Promise.all(
|
|
342
|
-
pageFiles.map((abs) =>
|
|
343
|
-
movePage(abs, { base, overrides: plan.pageOverrides, root })
|
|
344
|
-
)
|
|
345
|
-
);
|
|
346
|
-
const pages = summarizePageMoves(moves);
|
|
347
|
-
|
|
348
|
-
await writeFolderMetas(root, plan);
|
|
349
|
-
await Promise.all(plan.consumedMetas.map((abs) => rm(abs, { force: true })));
|
|
350
|
-
const relocateWarnings = await relocateUnparseableMetas(
|
|
351
|
-
root,
|
|
352
|
-
plan.unparseableMetas
|
|
353
|
-
);
|
|
354
|
-
await writeBlumeConfig(root, buildConfig(plan.tabs));
|
|
355
|
-
|
|
356
|
-
const warnings = [
|
|
357
|
-
...plan.warnings,
|
|
358
|
-
...pages.skipped.map((rel) => `Skipped ${rel} (target already exists)`),
|
|
359
|
-
...relocateWarnings,
|
|
360
|
-
];
|
|
361
|
-
if (pages.removedKeys.length > 0) {
|
|
362
|
-
warnings.push(
|
|
363
|
-
`Dropped unsupported page frontmatter keys: ${pages.removedKeys.join(", ")}.`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
if (pages.unsupported.length > 0) {
|
|
367
|
-
warnings.push(
|
|
368
|
-
`Components without a drop-in Blume equivalent need manual review: ${pages.unsupported.join(", ")}.`
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
warnings.push("Review blume.config.ts and the generated meta.ts files.");
|
|
372
|
-
|
|
373
|
-
return { moved: pages.moved, warnings };
|
|
374
|
-
};
|