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,237 +0,0 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
import { basename, join } from "pathe";
|
|
5
|
-
|
|
6
|
-
import { isInsideRoot } from "../shared.ts";
|
|
7
|
-
import { renderMetaModule } from "./meta.ts";
|
|
8
|
-
import type {
|
|
9
|
-
FumadocsPageItem,
|
|
10
|
-
FumadocsPagesStructure,
|
|
11
|
-
FumadocsSection,
|
|
12
|
-
} from "./meta.ts";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Rebuild Fumadocs `---Section---` separators as Blume group folders.
|
|
16
|
-
*
|
|
17
|
-
* Fumadocs draws a non-collapsible section heading from a `"---Label---"` entry
|
|
18
|
-
* in a folder's `pages` array — exactly what a Blume `(Label)/` group folder
|
|
19
|
-
* renders by default (`display: "flat"`). Blume has no flat-file "separator"
|
|
20
|
-
* primitive, so the only faithful, non-overriding home for a section is a group
|
|
21
|
-
* folder: it is route-transparent (the `(Label)` segment is stripped from URLs,
|
|
22
|
-
* see `core/sources/normalize.ts`) and composes with per-folder `meta.ts`,
|
|
23
|
-
* unlike a global `navigation.sidebar` override (which would disable every other
|
|
24
|
-
* folder's meta site-wide).
|
|
25
|
-
*
|
|
26
|
-
* The reshape runs against the already-migrated `docs/` tree, so each `pages`
|
|
27
|
-
* entry is resolved to a real page file or folder before it is moved. A section
|
|
28
|
-
* that is a single folder is left in place (wrapping it would double the
|
|
29
|
-
* heading); links have no file to move and are dropped with a warning.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const PAGE_EXTS = [".mdx", ".md"];
|
|
33
|
-
const WORD_SPLIT = /[-_]/u;
|
|
34
|
-
const PATH_SEP = /[/\\]/u;
|
|
35
|
-
|
|
36
|
-
interface ResolvedEntry {
|
|
37
|
-
kind: "file" | "folder";
|
|
38
|
-
path: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const isDirectory = (path: string): boolean => {
|
|
42
|
-
try {
|
|
43
|
-
return statSync(path).isDirectory();
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/** Resolve a `pages` name to its on-disk page file or folder under `docsDir`. */
|
|
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
|
-
}
|
|
57
|
-
for (const ext of PAGE_EXTS) {
|
|
58
|
-
const file = join(docsDir, `${name}${ext}`);
|
|
59
|
-
if (existsSync(file)) {
|
|
60
|
-
return { kind: "file", path: file };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const folder = join(docsDir, name);
|
|
64
|
-
return isDirectory(folder) ? { kind: "folder", path: folder } : null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const humanize = (name: string): string =>
|
|
68
|
-
name
|
|
69
|
-
.split(WORD_SPLIT)
|
|
70
|
-
.filter(Boolean)
|
|
71
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
72
|
-
.join(" ");
|
|
73
|
-
|
|
74
|
-
const linkWarning = (item: { href: string; text: string }): string =>
|
|
75
|
-
`Dropped sidebar link "${item.text}" (${item.href}) — add it to navbar.links manually.`;
|
|
76
|
-
|
|
77
|
-
const extractWarning = (name: string): string =>
|
|
78
|
-
`Sidebar extract "...${name}" became a normal "${name}" group (its pages are not flattened into the parent).`;
|
|
79
|
-
|
|
80
|
-
export interface GroupReshapeResult {
|
|
81
|
-
/** Ordering keys for the parent folder's `meta.ts` `pages`. */
|
|
82
|
-
order: string[];
|
|
83
|
-
warnings: string[];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Record an in-place item's ordering key (lead items, or an ungroupable run). */
|
|
87
|
-
const addUngroupedItem = (
|
|
88
|
-
item: FumadocsPageItem,
|
|
89
|
-
order: string[],
|
|
90
|
-
warnings: string[]
|
|
91
|
-
): void => {
|
|
92
|
-
if (item.kind === "link") {
|
|
93
|
-
warnings.push(linkWarning(item));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (item.kind === "extract") {
|
|
97
|
-
warnings.push(extractWarning(item.name));
|
|
98
|
-
}
|
|
99
|
-
order.push(item.name);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/** A section of one folder keeps its place without a wrapping group folder. */
|
|
103
|
-
const reshapeSingleFolderSection = (
|
|
104
|
-
section: FumadocsSection,
|
|
105
|
-
only: FumadocsPageItem & { kind: "extract" | "ref" },
|
|
106
|
-
order: string[],
|
|
107
|
-
warnings: string[]
|
|
108
|
-
): void => {
|
|
109
|
-
order.push(only.name);
|
|
110
|
-
if (humanize(only.name).toLowerCase() !== section.label.toLowerCase()) {
|
|
111
|
-
warnings.push(
|
|
112
|
-
`Section "${section.label}" wraps folder "${only.name}"; set that folder's meta title to "${section.label}" to make the heading match.`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
if (only.kind === "extract") {
|
|
116
|
-
warnings.push(extractWarning(only.name));
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/** Move one section item into the group folder; return its ordering key. */
|
|
121
|
-
const moveItemIntoGroup = async (
|
|
122
|
-
item: FumadocsPageItem,
|
|
123
|
-
docsDir: string,
|
|
124
|
-
groupDir: string,
|
|
125
|
-
label: string,
|
|
126
|
-
warnings: string[]
|
|
127
|
-
): Promise<string | null> => {
|
|
128
|
-
if (item.kind === "link") {
|
|
129
|
-
warnings.push(linkWarning(item));
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
const resolved = resolveEntry(docsDir, item.name);
|
|
133
|
-
if (!resolved) {
|
|
134
|
-
warnings.push(
|
|
135
|
-
`Sidebar entry "${item.name}" in section "${label}" matched no page or folder; skipped.`
|
|
136
|
-
);
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
const dest = join(groupDir, basename(resolved.path));
|
|
140
|
-
if (existsSync(dest)) {
|
|
141
|
-
warnings.push(
|
|
142
|
-
`Skipped moving "${item.name}" into section "${label}" (target already exists).`
|
|
143
|
-
);
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
await mkdir(groupDir, { recursive: true });
|
|
147
|
-
await rename(resolved.path, dest);
|
|
148
|
-
if (item.kind === "extract") {
|
|
149
|
-
warnings.push(extractWarning(item.name));
|
|
150
|
-
}
|
|
151
|
-
return item.name;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const reshapeSection = async (
|
|
155
|
-
section: FumadocsSection,
|
|
156
|
-
docsDir: string,
|
|
157
|
-
order: string[],
|
|
158
|
-
warnings: string[]
|
|
159
|
-
): Promise<void> => {
|
|
160
|
-
const movable = section.items.filter((item) => item.kind !== "link");
|
|
161
|
-
|
|
162
|
-
// A lone folder already renders as its own group; wrapping it in a `(Label)/`
|
|
163
|
-
// folder would stack two headings, so leave it in place.
|
|
164
|
-
if (movable.length === 1) {
|
|
165
|
-
const [only] = movable as [FumadocsPageItem & { kind: "extract" | "ref" }];
|
|
166
|
-
if (resolveEntry(docsDir, only.name)?.kind === "folder") {
|
|
167
|
-
reshapeSingleFolderSection(section, only, order, warnings);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (PATH_SEP.test(section.label)) {
|
|
173
|
-
warnings.push(
|
|
174
|
-
`Section "${section.label}" has a slash in its name and can't become a group folder; its pages stay ungrouped.`
|
|
175
|
-
);
|
|
176
|
-
for (const item of section.items) {
|
|
177
|
-
addUngroupedItem(item, order, warnings);
|
|
178
|
-
}
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const groupDir = join(docsDir, `(${section.label})`);
|
|
183
|
-
const sectionKeys: string[] = [];
|
|
184
|
-
for (const item of section.items) {
|
|
185
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential moves into one group
|
|
186
|
-
const key = await moveItemIntoGroup(
|
|
187
|
-
item,
|
|
188
|
-
docsDir,
|
|
189
|
-
groupDir,
|
|
190
|
-
section.label,
|
|
191
|
-
warnings
|
|
192
|
-
);
|
|
193
|
-
if (key) {
|
|
194
|
-
sectionKeys.push(key);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Nothing movable (e.g. a link-only section) — drop the empty heading.
|
|
199
|
-
if (sectionKeys.length === 0) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// The group folder's key in the nav is its label, so order it by label.
|
|
204
|
-
order.push(section.label);
|
|
205
|
-
// Preserve the authored order of the section's pages.
|
|
206
|
-
if (sectionKeys.length > 1) {
|
|
207
|
-
await writeFile(
|
|
208
|
-
join(groupDir, "meta.ts"),
|
|
209
|
-
renderMetaModule({ pages: sectionKeys }),
|
|
210
|
-
"utf-8"
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Reshape a parsed `pages` structure into group folders under `docsDir`,
|
|
217
|
-
* returning the parent folder's `meta.ts` page ordering and any warnings. Items
|
|
218
|
-
* are moved within the already-migrated `docs/` tree, so this must run after the
|
|
219
|
-
* pages have been moved out of `content/docs`.
|
|
220
|
-
*/
|
|
221
|
-
export const reshapeFumadocsGroups = async (
|
|
222
|
-
structure: FumadocsPagesStructure,
|
|
223
|
-
docsDir: string
|
|
224
|
-
): Promise<GroupReshapeResult> => {
|
|
225
|
-
const order: string[] = [];
|
|
226
|
-
const warnings: string[] = [];
|
|
227
|
-
|
|
228
|
-
for (const item of structure.lead) {
|
|
229
|
-
addUngroupedItem(item, order, warnings);
|
|
230
|
-
}
|
|
231
|
-
for (const section of structure.sections) {
|
|
232
|
-
// oxlint-disable-next-line no-await-in-loop -- sections share `docsDir` state
|
|
233
|
-
await reshapeSection(section, docsDir, order, warnings);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return { order, warnings };
|
|
237
|
-
};
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
import { dirname, join, relative } from "pathe";
|
|
5
|
-
import { glob } from "tinyglobby";
|
|
6
|
-
|
|
7
|
-
import matter from "../../core/frontmatter.ts";
|
|
8
|
-
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
9
|
-
import type { FolderMeta } from "../../core/schema.ts";
|
|
10
|
-
import {
|
|
11
|
-
leftoverFiles,
|
|
12
|
-
rewriteFrameworkScripts,
|
|
13
|
-
writeBlumeConfig,
|
|
14
|
-
} from "../shared.ts";
|
|
15
|
-
import { loadFumadocsConfig } from "./config.ts";
|
|
16
|
-
import {
|
|
17
|
-
inlineFumadocsIncludes,
|
|
18
|
-
rewriteFumadocsCallouts,
|
|
19
|
-
rewriteFumadocsContainers,
|
|
20
|
-
rewriteFumadocsTabs,
|
|
21
|
-
stripFumadocsImports,
|
|
22
|
-
unsupportedFumadocsComponents,
|
|
23
|
-
} from "./content.ts";
|
|
24
|
-
import { normalizeFumadocsPageMeta } from "./frontmatter.ts";
|
|
25
|
-
import { reshapeFumadocsGroups } from "./groups.ts";
|
|
26
|
-
import {
|
|
27
|
-
parseFumadocsPages,
|
|
28
|
-
renderMetaModule,
|
|
29
|
-
translateFumadocsMeta,
|
|
30
|
-
translateFumadocsSelfMeta,
|
|
31
|
-
} from "./meta.ts";
|
|
32
|
-
|
|
33
|
-
export interface FumadocsMigrationResult {
|
|
34
|
-
moved: number;
|
|
35
|
-
warnings: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Fumadocs keeps documentation under `content/docs/`. */
|
|
39
|
-
const SOURCE_DIR = "content/docs";
|
|
40
|
-
const PAGE_GLOB = "**/*.{md,mdx}";
|
|
41
|
-
const META_GLOB = "**/meta.json";
|
|
42
|
-
const IGNORE = ["**/node_modules/**"];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Old Next/Fumadocs scaffolding a migration leaves behind. Blume can't safely
|
|
46
|
-
* auto-delete these (they may hold custom code), so the ones that exist are
|
|
47
|
-
* surfaced as a "safe to delete" checklist once the migration is verified.
|
|
48
|
-
*/
|
|
49
|
-
const FUMADOCS_LEFTOVERS = [
|
|
50
|
-
"next.config.ts",
|
|
51
|
-
"next.config.mjs",
|
|
52
|
-
"next.config.js",
|
|
53
|
-
"source.config.ts",
|
|
54
|
-
"source.config.mjs",
|
|
55
|
-
"source.config.js",
|
|
56
|
-
"mdx-components.tsx",
|
|
57
|
-
"next-env.d.ts",
|
|
58
|
-
"app",
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
interface PageResult {
|
|
62
|
-
includeWarnings: string[];
|
|
63
|
-
moved: number;
|
|
64
|
-
removed: string[];
|
|
65
|
-
skipped: string | null;
|
|
66
|
-
unsupported: string[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Rewrite a single page to idiomatic Blume MDX and move it into `docs/`. */
|
|
70
|
-
const movePage = async (
|
|
71
|
-
abs: string,
|
|
72
|
-
base: string,
|
|
73
|
-
root: string
|
|
74
|
-
): Promise<PageResult> => {
|
|
75
|
-
const rel = relative(base, abs);
|
|
76
|
-
const dest = join(root, "docs", rel);
|
|
77
|
-
if (existsSync(dest)) {
|
|
78
|
-
return {
|
|
79
|
-
includeWarnings: [],
|
|
80
|
-
moved: 0,
|
|
81
|
-
removed: [],
|
|
82
|
-
skipped: rel,
|
|
83
|
-
unsupported: [],
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const raw = await readFile(abs, "utf-8");
|
|
88
|
-
const included = await inlineFumadocsIncludes(raw, {
|
|
89
|
-
filePath: abs,
|
|
90
|
-
root: base,
|
|
91
|
-
});
|
|
92
|
-
let text = stripFumadocsImports(included.content);
|
|
93
|
-
text = rewriteFumadocsCallouts(text);
|
|
94
|
-
text = rewriteFumadocsContainers(text);
|
|
95
|
-
text = rewriteFumadocsTabs(text);
|
|
96
|
-
const unsupported = unsupportedFumadocsComponents(text);
|
|
97
|
-
|
|
98
|
-
const parsed = matter(text);
|
|
99
|
-
const { data, removed } = normalizeFumadocsPageMeta(parsed.data);
|
|
100
|
-
const content =
|
|
101
|
-
Object.keys(data).length > 0
|
|
102
|
-
? matter.stringify(parsed.content, data)
|
|
103
|
-
: parsed.content;
|
|
104
|
-
|
|
105
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
106
|
-
await writeFile(dest, content, "utf-8");
|
|
107
|
-
await rm(abs, { force: true });
|
|
108
|
-
return {
|
|
109
|
-
includeWarnings: included.warnings,
|
|
110
|
-
moved: 1,
|
|
111
|
-
removed,
|
|
112
|
-
skipped: null,
|
|
113
|
-
unsupported,
|
|
114
|
-
};
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/** Write a `FolderMeta` to `dest` unless it already exists. */
|
|
118
|
-
const writeMeta = async (
|
|
119
|
-
dest: string,
|
|
120
|
-
meta: FolderMeta,
|
|
121
|
-
rel: string,
|
|
122
|
-
warnings: string[]
|
|
123
|
-
): Promise<string[]> => {
|
|
124
|
-
if (Object.keys(meta).length === 0) {
|
|
125
|
-
return warnings;
|
|
126
|
-
}
|
|
127
|
-
if (existsSync(dest)) {
|
|
128
|
-
return [...warnings, `Skipped ${rel} (target already exists)`];
|
|
129
|
-
}
|
|
130
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
131
|
-
await writeFile(dest, renderMetaModule(meta), "utf-8");
|
|
132
|
-
return warnings;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/** Convert one `meta.json` into a typed `meta.ts`, or relocate it if unparseable. */
|
|
136
|
-
const convertMeta = async (
|
|
137
|
-
abs: string,
|
|
138
|
-
base: string,
|
|
139
|
-
root: string
|
|
140
|
-
): Promise<string[]> => {
|
|
141
|
-
const rel = relative(base, abs);
|
|
142
|
-
const raw = await readFile(abs, "utf-8");
|
|
143
|
-
let parsed: unknown;
|
|
144
|
-
try {
|
|
145
|
-
parsed = JSON.parse(raw);
|
|
146
|
-
} catch {
|
|
147
|
-
const dest = join(root, "docs", rel);
|
|
148
|
-
if (existsSync(dest)) {
|
|
149
|
-
return [`Skipped ${rel} (target already exists)`];
|
|
150
|
-
}
|
|
151
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
152
|
-
await rename(abs, dest);
|
|
153
|
-
return [
|
|
154
|
-
`Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`,
|
|
155
|
-
];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const dir = dirname(rel) === "." ? "" : dirname(rel);
|
|
159
|
-
const docsDir = join(root, "docs", dir);
|
|
160
|
-
const dest = join(docsDir, "meta.ts");
|
|
161
|
-
|
|
162
|
-
// A `pages` array with `---Section---` separators can't round-trip through a
|
|
163
|
-
// flat `meta.ts` ordering, so rebuild its sections as group folders instead.
|
|
164
|
-
const structure = parseFumadocsPages((parsed as { pages?: unknown }).pages);
|
|
165
|
-
if (structure.hasSections && !existsSync(dest)) {
|
|
166
|
-
const self = translateFumadocsSelfMeta(parsed);
|
|
167
|
-
const reshape = await reshapeFumadocsGroups(structure, docsDir);
|
|
168
|
-
const meta: FolderMeta = { ...self.meta };
|
|
169
|
-
if (reshape.order.length > 0) {
|
|
170
|
-
meta.pages = reshape.order;
|
|
171
|
-
}
|
|
172
|
-
const result = await writeMeta(dest, meta, rel, [
|
|
173
|
-
...self.warnings,
|
|
174
|
-
...reshape.warnings,
|
|
175
|
-
]);
|
|
176
|
-
await rm(abs, { force: true });
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const { meta, warnings } = translateFumadocsMeta(parsed);
|
|
181
|
-
const result = await writeMeta(dest, meta, rel, warnings);
|
|
182
|
-
await rm(abs, { force: true });
|
|
183
|
-
return result;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
interface PageSummary {
|
|
187
|
-
includeWarnings: string[];
|
|
188
|
-
moved: number;
|
|
189
|
-
removedKeys: string[];
|
|
190
|
-
skipped: string[];
|
|
191
|
-
unsupported: string[];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const summarizePages = (results: PageResult[]): PageSummary => {
|
|
195
|
-
let moved = 0;
|
|
196
|
-
const removedKeys = new Set<string>();
|
|
197
|
-
const unsupported = new Set<string>();
|
|
198
|
-
const includeWarnings: string[] = [];
|
|
199
|
-
const skipped: string[] = [];
|
|
200
|
-
for (const result of results) {
|
|
201
|
-
moved += result.moved;
|
|
202
|
-
if (result.skipped) {
|
|
203
|
-
skipped.push(result.skipped);
|
|
204
|
-
}
|
|
205
|
-
includeWarnings.push(...result.includeWarnings);
|
|
206
|
-
for (const key of result.removed) {
|
|
207
|
-
removedKeys.add(key);
|
|
208
|
-
}
|
|
209
|
-
for (const name of result.unsupported) {
|
|
210
|
-
unsupported.add(name);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
includeWarnings,
|
|
215
|
-
moved,
|
|
216
|
-
removedKeys: [...removedKeys],
|
|
217
|
-
skipped,
|
|
218
|
-
unsupported: [...unsupported],
|
|
219
|
-
};
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/** Remove `content/docs` and `content` once emptied; warn about leftovers. */
|
|
223
|
-
const cleanupSourceDirs = async (root: string): Promise<string[]> => {
|
|
224
|
-
const docs = join(root, "content", "docs");
|
|
225
|
-
if (existsSync(docs)) {
|
|
226
|
-
const remaining = await glob(["**/*"], { cwd: docs, dot: true });
|
|
227
|
-
if (remaining.length > 0) {
|
|
228
|
-
return [
|
|
229
|
-
`Kept ${remaining.length} non-page file(s) under content/docs; move them into docs/ manually.`,
|
|
230
|
-
];
|
|
231
|
-
}
|
|
232
|
-
await rm(docs, { force: true, recursive: true });
|
|
233
|
-
}
|
|
234
|
-
const content = join(root, "content");
|
|
235
|
-
if (existsSync(content)) {
|
|
236
|
-
const remaining = await glob(["**/*"], { cwd: content, dot: true });
|
|
237
|
-
if (remaining.length === 0) {
|
|
238
|
-
await rm(content, { force: true, recursive: true });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return [];
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Migrate a Fumadocs project to Blume. Moves every `content/docs` page into
|
|
246
|
-
* `docs/`, rewrites Fumadocs MDX (callouts to directives, `<Cards>`/
|
|
247
|
-
* `<Accordions>`/`<Files>` to their Blume equivalents, `<Tabs items>` to
|
|
248
|
-
* per-`<Tab>` titles, and `<include>`s inlined), and converts each `meta.json`
|
|
249
|
-
* into a typed `meta.ts`. The `loader({ baseUrl })` route prefix is preserved
|
|
250
|
-
* via a `content.sources` filesystem source.
|
|
251
|
-
*/
|
|
252
|
-
export const migrateFumadocsProject = async (
|
|
253
|
-
root: string
|
|
254
|
-
): Promise<FumadocsMigrationResult> => {
|
|
255
|
-
const { config, warnings: configWarnings } = await loadFumadocsConfig(root);
|
|
256
|
-
|
|
257
|
-
const base = join(root, SOURCE_DIR);
|
|
258
|
-
if (!existsSync(base)) {
|
|
259
|
-
await writeBlumeConfig(root, config);
|
|
260
|
-
return {
|
|
261
|
-
moved: 0,
|
|
262
|
-
warnings: [
|
|
263
|
-
`No Fumadocs content directory (${SOURCE_DIR}) found; wrote a default config.`,
|
|
264
|
-
],
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const pageFiles = await glob([PAGE_GLOB], {
|
|
269
|
-
absolute: true,
|
|
270
|
-
cwd: base,
|
|
271
|
-
ignore: IGNORE,
|
|
272
|
-
});
|
|
273
|
-
const metaFiles = await glob([META_GLOB], {
|
|
274
|
-
absolute: true,
|
|
275
|
-
cwd: base,
|
|
276
|
-
ignore: IGNORE,
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const pageResults = await Promise.all(
|
|
280
|
-
pageFiles.map((abs) => movePage(abs, base, root))
|
|
281
|
-
);
|
|
282
|
-
const pages = summarizePages(pageResults);
|
|
283
|
-
// Convert metas deepest-first and sequentially: a parent's group-folder
|
|
284
|
-
// reshape can move a child folder, so the child's own reshape must finish
|
|
285
|
-
// first (and the two can't race over the same `docs/` paths).
|
|
286
|
-
const orderedMetas = metaFiles.toSorted(
|
|
287
|
-
(a, b) => b.split("/").length - a.split("/").length
|
|
288
|
-
);
|
|
289
|
-
const metaWarnings: string[] = [];
|
|
290
|
-
for (const abs of orderedMetas) {
|
|
291
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential to avoid move races
|
|
292
|
-
metaWarnings.push(...(await convertMeta(abs, base, root)));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
await writeBlumeConfig(root, config);
|
|
296
|
-
const cleanupWarnings = await cleanupSourceDirs(root);
|
|
297
|
-
|
|
298
|
-
// Tear down the old Next/Fumadocs scaffolding so the project builds as Blume:
|
|
299
|
-
// repoint the npm scripts, ignore Blume's outputs, and list the leftover
|
|
300
|
-
// framework files to delete by hand.
|
|
301
|
-
const scriptsRewritten = await rewriteFrameworkScripts(
|
|
302
|
-
root,
|
|
303
|
-
/\bnext\b/u,
|
|
304
|
-
/fumadocs/u
|
|
305
|
-
);
|
|
306
|
-
const gitignoreAdded = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
307
|
-
const leftovers = leftoverFiles(root, FUMADOCS_LEFTOVERS);
|
|
308
|
-
|
|
309
|
-
const warnings = [
|
|
310
|
-
...configWarnings,
|
|
311
|
-
...metaWarnings,
|
|
312
|
-
...pages.includeWarnings,
|
|
313
|
-
...pages.skipped.map((rel) => `Skipped ${rel} (target already exists)`),
|
|
314
|
-
...cleanupWarnings,
|
|
315
|
-
];
|
|
316
|
-
if (pages.removedKeys.length > 0) {
|
|
317
|
-
warnings.push(
|
|
318
|
-
`Dropped unsupported page frontmatter keys: ${pages.removedKeys.join(", ")}.`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
if (pages.unsupported.length > 0) {
|
|
322
|
-
warnings.push(
|
|
323
|
-
`Components without a drop-in Blume equivalent need manual review: ${pages.unsupported.join(", ")}.`
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
if (scriptsRewritten) {
|
|
327
|
-
warnings.push("Repointed the dev/build/start scripts at the Blume CLI.");
|
|
328
|
-
}
|
|
329
|
-
if (gitignoreAdded.length > 0) {
|
|
330
|
-
warnings.push(`Added ${gitignoreAdded.join(", ")} to .gitignore.`);
|
|
331
|
-
}
|
|
332
|
-
if (leftovers.length > 0) {
|
|
333
|
-
warnings.push(
|
|
334
|
-
`Safe to delete once the migration looks right: ${leftovers.join(", ")}. Also drop the \`next\` plugin from tsconfig.json and the \`.next\`/\`.source\` lines from .gitignore.`
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
warnings.push("Review blume.config.ts and the generated meta.ts files.");
|
|
338
|
-
|
|
339
|
-
return { moved: pages.moved, warnings };
|
|
340
|
-
};
|