blume 0.5.3 → 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 +2985 -6883
- package/dist/cli/index.js.map +27 -58
- 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 +2 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/theming.mdx +2 -10
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- 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/astro/generate.ts +15 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +9 -16
- package/src/cli/commands/dev.ts +0 -6
- package/src/cli/commands/validate.ts +0 -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 +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +5 -5
- package/src/components/content/Update.astro +1 -1
- 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/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 +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +2 -2
- package/src/core/graph.ts +1 -4
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/navigation.ts +0 -5
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +13 -90
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +3 -2
- package/src/core/types.ts +1 -8
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/source.ts +19 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +4 -37
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- 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/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- 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 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- 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 -954
- 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 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- 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 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- 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,355 +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
|
-
/**
|
|
118
|
-
* Write a `FolderMeta` to `dest` unless it already exists. `handled` tells the
|
|
119
|
-
* caller whether the source `meta.json` is safe to delete — a skipped write
|
|
120
|
-
* (target exists) must keep the source, or its title/ordering is lost with
|
|
121
|
-
* nowhere to recover it from.
|
|
122
|
-
*/
|
|
123
|
-
const writeMeta = async (
|
|
124
|
-
dest: string,
|
|
125
|
-
meta: FolderMeta,
|
|
126
|
-
rel: string,
|
|
127
|
-
warnings: string[]
|
|
128
|
-
): Promise<{ handled: boolean; warnings: string[] }> => {
|
|
129
|
-
if (Object.keys(meta).length === 0) {
|
|
130
|
-
return { handled: true, warnings };
|
|
131
|
-
}
|
|
132
|
-
if (existsSync(dest)) {
|
|
133
|
-
return {
|
|
134
|
-
handled: false,
|
|
135
|
-
warnings: [
|
|
136
|
-
...warnings,
|
|
137
|
-
`Skipped ${rel} (target already exists); the source file was kept — merge it into the existing meta.ts by hand.`,
|
|
138
|
-
],
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
142
|
-
await writeFile(dest, renderMetaModule(meta), "utf-8");
|
|
143
|
-
return { handled: true, warnings };
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/** Convert one `meta.json` into a typed `meta.ts`, or relocate it if unparseable. */
|
|
147
|
-
const convertMeta = async (
|
|
148
|
-
abs: string,
|
|
149
|
-
base: string,
|
|
150
|
-
root: string
|
|
151
|
-
): Promise<string[]> => {
|
|
152
|
-
const rel = relative(base, abs);
|
|
153
|
-
const raw = await readFile(abs, "utf-8");
|
|
154
|
-
let parsed: unknown;
|
|
155
|
-
try {
|
|
156
|
-
parsed = JSON.parse(raw);
|
|
157
|
-
} catch {
|
|
158
|
-
const dest = join(root, "docs", rel);
|
|
159
|
-
if (existsSync(dest)) {
|
|
160
|
-
return [`Skipped ${rel} (target already exists)`];
|
|
161
|
-
}
|
|
162
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
163
|
-
await rename(abs, dest);
|
|
164
|
-
return [
|
|
165
|
-
`Could not parse ${rel}; moved as-is — convert it to meta.ts by hand.`,
|
|
166
|
-
];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const dir = dirname(rel) === "." ? "" : dirname(rel);
|
|
170
|
-
const docsDir = join(root, "docs", dir);
|
|
171
|
-
const dest = join(docsDir, "meta.ts");
|
|
172
|
-
|
|
173
|
-
// A `pages` array with `---Section---` separators can't round-trip through a
|
|
174
|
-
// flat `meta.ts` ordering, so rebuild its sections as group folders instead.
|
|
175
|
-
const structure = parseFumadocsPages((parsed as { pages?: unknown }).pages);
|
|
176
|
-
if (structure.hasSections && !existsSync(dest)) {
|
|
177
|
-
const self = translateFumadocsSelfMeta(parsed);
|
|
178
|
-
const reshape = await reshapeFumadocsGroups(structure, docsDir);
|
|
179
|
-
const meta: FolderMeta = { ...self.meta };
|
|
180
|
-
if (reshape.order.length > 0) {
|
|
181
|
-
meta.pages = reshape.order;
|
|
182
|
-
}
|
|
183
|
-
const result = await writeMeta(dest, meta, rel, [
|
|
184
|
-
...self.warnings,
|
|
185
|
-
...reshape.warnings,
|
|
186
|
-
]);
|
|
187
|
-
if (result.handled) {
|
|
188
|
-
await rm(abs, { force: true });
|
|
189
|
-
}
|
|
190
|
-
return result.warnings;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const { meta, warnings } = translateFumadocsMeta(parsed);
|
|
194
|
-
const result = await writeMeta(dest, meta, rel, warnings);
|
|
195
|
-
if (result.handled) {
|
|
196
|
-
await rm(abs, { force: true });
|
|
197
|
-
}
|
|
198
|
-
return result.warnings;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
interface PageSummary {
|
|
202
|
-
includeWarnings: string[];
|
|
203
|
-
moved: number;
|
|
204
|
-
removedKeys: string[];
|
|
205
|
-
skipped: string[];
|
|
206
|
-
unsupported: string[];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const summarizePages = (results: PageResult[]): PageSummary => {
|
|
210
|
-
let moved = 0;
|
|
211
|
-
const removedKeys = new Set<string>();
|
|
212
|
-
const unsupported = new Set<string>();
|
|
213
|
-
const includeWarnings: string[] = [];
|
|
214
|
-
const skipped: string[] = [];
|
|
215
|
-
for (const result of results) {
|
|
216
|
-
moved += result.moved;
|
|
217
|
-
if (result.skipped) {
|
|
218
|
-
skipped.push(result.skipped);
|
|
219
|
-
}
|
|
220
|
-
includeWarnings.push(...result.includeWarnings);
|
|
221
|
-
for (const key of result.removed) {
|
|
222
|
-
removedKeys.add(key);
|
|
223
|
-
}
|
|
224
|
-
for (const name of result.unsupported) {
|
|
225
|
-
unsupported.add(name);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
includeWarnings,
|
|
230
|
-
moved,
|
|
231
|
-
removedKeys: [...removedKeys],
|
|
232
|
-
skipped,
|
|
233
|
-
unsupported: [...unsupported],
|
|
234
|
-
};
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
/** Remove `content/docs` and `content` once emptied; warn about leftovers. */
|
|
238
|
-
const cleanupSourceDirs = async (root: string): Promise<string[]> => {
|
|
239
|
-
const docs = join(root, "content", "docs");
|
|
240
|
-
if (existsSync(docs)) {
|
|
241
|
-
const remaining = await glob(["**/*"], { cwd: docs, dot: true });
|
|
242
|
-
if (remaining.length > 0) {
|
|
243
|
-
return [
|
|
244
|
-
`Kept ${remaining.length} non-page file(s) under content/docs; move them into docs/ manually.`,
|
|
245
|
-
];
|
|
246
|
-
}
|
|
247
|
-
await rm(docs, { force: true, recursive: true });
|
|
248
|
-
}
|
|
249
|
-
const content = join(root, "content");
|
|
250
|
-
if (existsSync(content)) {
|
|
251
|
-
const remaining = await glob(["**/*"], { cwd: content, dot: true });
|
|
252
|
-
if (remaining.length === 0) {
|
|
253
|
-
await rm(content, { force: true, recursive: true });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return [];
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Migrate a Fumadocs project to Blume. Moves every `content/docs` page into
|
|
261
|
-
* `docs/`, rewrites Fumadocs MDX (callouts to directives, `<Cards>`/
|
|
262
|
-
* `<Accordions>`/`<Files>` to their Blume equivalents, `<Tabs items>` to
|
|
263
|
-
* per-`<Tab>` titles, and `<include>`s inlined), and converts each `meta.json`
|
|
264
|
-
* into a typed `meta.ts`. The `loader({ baseUrl })` route prefix is preserved
|
|
265
|
-
* via a `content.sources` filesystem source.
|
|
266
|
-
*/
|
|
267
|
-
export const migrateFumadocsProject = async (
|
|
268
|
-
root: string
|
|
269
|
-
): Promise<FumadocsMigrationResult> => {
|
|
270
|
-
const { config, warnings: configWarnings } = await loadFumadocsConfig(root);
|
|
271
|
-
|
|
272
|
-
const base = join(root, SOURCE_DIR);
|
|
273
|
-
if (!existsSync(base)) {
|
|
274
|
-
await writeBlumeConfig(root, config);
|
|
275
|
-
return {
|
|
276
|
-
moved: 0,
|
|
277
|
-
warnings: [
|
|
278
|
-
`No Fumadocs content directory (${SOURCE_DIR}) found; wrote a default config.`,
|
|
279
|
-
],
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const pageFiles = await glob([PAGE_GLOB], {
|
|
284
|
-
absolute: true,
|
|
285
|
-
cwd: base,
|
|
286
|
-
ignore: IGNORE,
|
|
287
|
-
});
|
|
288
|
-
const metaFiles = await glob([META_GLOB], {
|
|
289
|
-
absolute: true,
|
|
290
|
-
cwd: base,
|
|
291
|
-
ignore: IGNORE,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const pageResults = await Promise.all(
|
|
295
|
-
pageFiles.map((abs) => movePage(abs, base, root))
|
|
296
|
-
);
|
|
297
|
-
const pages = summarizePages(pageResults);
|
|
298
|
-
// Convert metas deepest-first and sequentially: a parent's group-folder
|
|
299
|
-
// reshape can move a child folder, so the child's own reshape must finish
|
|
300
|
-
// first (and the two can't race over the same `docs/` paths).
|
|
301
|
-
const orderedMetas = metaFiles.toSorted(
|
|
302
|
-
(a, b) => b.split("/").length - a.split("/").length
|
|
303
|
-
);
|
|
304
|
-
const metaWarnings: string[] = [];
|
|
305
|
-
for (const abs of orderedMetas) {
|
|
306
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential to avoid move races
|
|
307
|
-
metaWarnings.push(...(await convertMeta(abs, base, root)));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
await writeBlumeConfig(root, config);
|
|
311
|
-
const cleanupWarnings = await cleanupSourceDirs(root);
|
|
312
|
-
|
|
313
|
-
// Tear down the old Next/Fumadocs scaffolding so the project builds as Blume:
|
|
314
|
-
// repoint the npm scripts, ignore Blume's outputs, and list the leftover
|
|
315
|
-
// framework files to delete by hand.
|
|
316
|
-
const scriptsRewritten = await rewriteFrameworkScripts(
|
|
317
|
-
root,
|
|
318
|
-
/\bnext\b/u,
|
|
319
|
-
/fumadocs/u
|
|
320
|
-
);
|
|
321
|
-
const gitignoreAdded = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
322
|
-
const leftovers = leftoverFiles(root, FUMADOCS_LEFTOVERS);
|
|
323
|
-
|
|
324
|
-
const warnings = [
|
|
325
|
-
...configWarnings,
|
|
326
|
-
...metaWarnings,
|
|
327
|
-
...pages.includeWarnings,
|
|
328
|
-
...pages.skipped.map((rel) => `Skipped ${rel} (target already exists)`),
|
|
329
|
-
...cleanupWarnings,
|
|
330
|
-
];
|
|
331
|
-
if (pages.removedKeys.length > 0) {
|
|
332
|
-
warnings.push(
|
|
333
|
-
`Dropped unsupported page frontmatter keys: ${pages.removedKeys.join(", ")}.`
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
if (pages.unsupported.length > 0) {
|
|
337
|
-
warnings.push(
|
|
338
|
-
`Components without a drop-in Blume equivalent need manual review: ${pages.unsupported.join(", ")}.`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
if (scriptsRewritten) {
|
|
342
|
-
warnings.push("Repointed the dev/build/start scripts at the Blume CLI.");
|
|
343
|
-
}
|
|
344
|
-
if (gitignoreAdded.length > 0) {
|
|
345
|
-
warnings.push(`Added ${gitignoreAdded.join(", ")} to .gitignore.`);
|
|
346
|
-
}
|
|
347
|
-
if (leftovers.length > 0) {
|
|
348
|
-
warnings.push(
|
|
349
|
-
`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.`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
warnings.push("Review blume.config.ts and the generated meta.ts files.");
|
|
353
|
-
|
|
354
|
-
return { moved: pages.moved, warnings };
|
|
355
|
-
};
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import type { FolderMeta } from "../../core/schema.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Translate a Fumadocs folder `meta.json` into a Blume `FolderMeta` (`meta.ts`).
|
|
5
|
-
* Unlike Nextra, Fumadocs declares a folder's own title/icon/ordering in *that
|
|
6
|
-
* folder's* `meta.json`, so the mapping is self-contained. Fumadocs-only
|
|
7
|
-
* concepts with no Blume equivalent are dropped and reported:
|
|
8
|
-
*
|
|
9
|
-
* - `defaultOpen` -> `collapsed` (inverted) + `display: "group"` (collapsible).
|
|
10
|
-
* - `root: true` -> `display: "page"` (the closest analogue to a page-tree root).
|
|
11
|
-
* - `description` -> dropped (folders carry no description in Blume).
|
|
12
|
-
* - `pages` entries: plain slugs are kept as ordering; `"..."` (the rest marker,
|
|
13
|
-
* already Blume's default) is dropped silently; `"...folder"` (the *extract*
|
|
14
|
-
* operator, which inlines a folder's children) keeps the folder's place in the
|
|
15
|
-
* ordering as a plain `"folder"` key and warns that it renders as a normal
|
|
16
|
-
* group rather than flattened; `"---Section---"` separators and `"[Text](url)"`
|
|
17
|
-
* links have no folder-meta home and are dropped with a warning.
|
|
18
|
-
*
|
|
19
|
-
* When a `pages` array carries `---Section---` separators, the migrator instead
|
|
20
|
-
* takes the *structural* path ({@link parseFumadocsPages} + the group-folder
|
|
21
|
-
* reshape in `index.ts`), which rebuilds each section as a Blume group folder
|
|
22
|
-
* rather than flattening it through `filterPages`.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
interface FumadocsMeta {
|
|
26
|
-
defaultOpen?: unknown;
|
|
27
|
-
description?: unknown;
|
|
28
|
-
icon?: unknown;
|
|
29
|
-
pages?: unknown;
|
|
30
|
-
root?: unknown;
|
|
31
|
-
title?: unknown;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const SEPARATOR = /^---(?<label>.*)---$/u;
|
|
35
|
-
const LINK = /^(?:\[[^\]]*\])?\[(?<text>[^\]]*)\]\((?<href>[^)]*)\)$/u;
|
|
36
|
-
const REST = "...";
|
|
37
|
-
/** Fumadocs *extract* operator: `...folder` inlines that folder's children. */
|
|
38
|
-
const EXTRACT = /^\.\.\.(?<folder>.+)$/u;
|
|
39
|
-
|
|
40
|
-
interface PagesResult {
|
|
41
|
-
pages: string[];
|
|
42
|
-
warnings: string[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const filterPages = (raw: unknown): PagesResult => {
|
|
46
|
-
if (!Array.isArray(raw)) {
|
|
47
|
-
return { pages: [], warnings: [] };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const pages: string[] = [];
|
|
51
|
-
const warnings: string[] = [];
|
|
52
|
-
for (const entry of raw) {
|
|
53
|
-
if (typeof entry !== "string") {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const value = entry.trim();
|
|
57
|
-
if (value === REST || value === "") {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const extract = EXTRACT.exec(value);
|
|
61
|
-
if (extract) {
|
|
62
|
-
// Blume has no "flatten a folder's children inline" sidebar primitive, but
|
|
63
|
-
// the folder is a sibling here, so keep its position in the ordering and
|
|
64
|
-
// let it render as a normal group.
|
|
65
|
-
const folder = extract.groups?.folder?.trim() ?? "";
|
|
66
|
-
if (folder) {
|
|
67
|
-
pages.push(folder);
|
|
68
|
-
warnings.push(
|
|
69
|
-
`Sidebar extract "...${folder}" became a normal "${folder}" group (its pages are not flattened into the parent).`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const separator = SEPARATOR.exec(value);
|
|
75
|
-
if (separator) {
|
|
76
|
-
const label = separator.groups?.label?.trim();
|
|
77
|
-
warnings.push(
|
|
78
|
-
`Dropped sidebar separator "${label || value}" — recreate it with a sidebar group if needed.`
|
|
79
|
-
);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const link = LINK.exec(value);
|
|
83
|
-
if (link) {
|
|
84
|
-
warnings.push(
|
|
85
|
-
`Dropped sidebar link "${link.groups?.text}" (${link.groups?.href}) — add it to navbar.links manually.`
|
|
86
|
-
);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
pages.push(value);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { pages, warnings };
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
export interface FumadocsMetaConversion {
|
|
96
|
-
meta: FolderMeta;
|
|
97
|
-
warnings: string[];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Render a Blume `meta.ts` module source for a `FolderMeta`. */
|
|
101
|
-
export const renderMetaModule = (meta: FolderMeta): string =>
|
|
102
|
-
`import { defineMeta } from "blume";\n\nexport default defineMeta(${JSON.stringify(meta, null, 2)});\n`;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Translate a folder's own title/icon/display fields — everything in a Fumadocs
|
|
106
|
-
* `meta.json` *except* the `pages` ordering. Split out from
|
|
107
|
-
* {@link translateFumadocsMeta} so the structural path (which reshapes `pages`
|
|
108
|
-
* into group folders) can reuse the self fields without the flat `pages` array.
|
|
109
|
-
*/
|
|
110
|
-
export const translateFumadocsSelfMeta = (
|
|
111
|
-
value: unknown
|
|
112
|
-
): FumadocsMetaConversion => {
|
|
113
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
114
|
-
return { meta: {}, warnings: [] };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const source = value as FumadocsMeta;
|
|
118
|
-
const meta: FolderMeta = {};
|
|
119
|
-
const warnings: string[] = [];
|
|
120
|
-
|
|
121
|
-
if (typeof source.title === "string") {
|
|
122
|
-
meta.title = source.title;
|
|
123
|
-
}
|
|
124
|
-
if (typeof source.icon === "string") {
|
|
125
|
-
meta.icon = source.icon;
|
|
126
|
-
}
|
|
127
|
-
if (source.root === true) {
|
|
128
|
-
meta.display = "page";
|
|
129
|
-
} else if (typeof source.defaultOpen === "boolean") {
|
|
130
|
-
meta.display = "group";
|
|
131
|
-
meta.collapsed = !source.defaultOpen;
|
|
132
|
-
}
|
|
133
|
-
if (typeof source.description === "string") {
|
|
134
|
-
warnings.push(
|
|
135
|
-
"Dropped folder `description` — Blume folders have no description field."
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { meta, warnings };
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/** Map a parsed Fumadocs `meta.json` object onto a Blume `FolderMeta`. */
|
|
143
|
-
export const translateFumadocsMeta = (
|
|
144
|
-
value: unknown
|
|
145
|
-
): FumadocsMetaConversion => {
|
|
146
|
-
const { meta, warnings } = translateFumadocsSelfMeta(value);
|
|
147
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
148
|
-
const { pages, warnings: pageWarnings } = filterPages(
|
|
149
|
-
(value as FumadocsMeta).pages
|
|
150
|
-
);
|
|
151
|
-
if (pages.length > 0) {
|
|
152
|
-
meta.pages = pages;
|
|
153
|
-
}
|
|
154
|
-
warnings.push(...pageWarnings);
|
|
155
|
-
}
|
|
156
|
-
return { meta, warnings };
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Structural pages (separators -> sections)
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* One entry in a Fumadocs `pages` array, after classification: a `ref` (a plain
|
|
165
|
-
* page or folder name), an `extract` (`...folder`, whose children Fumadocs
|
|
166
|
-
* inlines), or a `link` (`[Text](url)`).
|
|
167
|
-
*/
|
|
168
|
-
export type FumadocsPageItem =
|
|
169
|
-
| { kind: "extract"; name: string }
|
|
170
|
-
| { kind: "link"; href: string; text: string }
|
|
171
|
-
| { kind: "ref"; name: string };
|
|
172
|
-
|
|
173
|
-
/** A run of items under one `---Label---` separator. */
|
|
174
|
-
export interface FumadocsSection {
|
|
175
|
-
items: FumadocsPageItem[];
|
|
176
|
-
label: string;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export interface FumadocsPagesStructure {
|
|
180
|
-
/** Whether any `---Section---` separator was present. */
|
|
181
|
-
hasSections: boolean;
|
|
182
|
-
/** Items before the first separator; they stay at the folder's top level. */
|
|
183
|
-
lead: FumadocsPageItem[];
|
|
184
|
-
sections: FumadocsSection[];
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Parse a Fumadocs `pages` array into its structural shape: the lead items
|
|
189
|
-
* (before any separator) and the sections each `---Label---` introduces. Unlike
|
|
190
|
-
* {@link filterPages}, separators are preserved (as section boundaries) so the
|
|
191
|
-
* migrator can rebuild them as Blume group folders. The rest marker (`"..."`) is
|
|
192
|
-
* dropped — Blume appends unlisted pages by default.
|
|
193
|
-
*/
|
|
194
|
-
export const parseFumadocsPages = (raw: unknown): FumadocsPagesStructure => {
|
|
195
|
-
const lead: FumadocsPageItem[] = [];
|
|
196
|
-
const sections: FumadocsSection[] = [];
|
|
197
|
-
if (!Array.isArray(raw)) {
|
|
198
|
-
return { hasSections: false, lead, sections };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let current: FumadocsSection | null = null;
|
|
202
|
-
const push = (item: FumadocsPageItem): void => {
|
|
203
|
-
(current ? current.items : lead).push(item);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
for (const entry of raw) {
|
|
207
|
-
if (typeof entry !== "string") {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
const value = entry.trim();
|
|
211
|
-
if (value === REST || value === "") {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const separator = SEPARATOR.exec(value);
|
|
215
|
-
if (separator) {
|
|
216
|
-
current = {
|
|
217
|
-
items: [],
|
|
218
|
-
label: separator.groups?.label?.trim() || "Section",
|
|
219
|
-
};
|
|
220
|
-
sections.push(current);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
const extract = EXTRACT.exec(value);
|
|
224
|
-
if (extract) {
|
|
225
|
-
const name = extract.groups?.folder?.trim();
|
|
226
|
-
if (name) {
|
|
227
|
-
push({ kind: "extract", name });
|
|
228
|
-
}
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const link = LINK.exec(value);
|
|
232
|
-
if (link) {
|
|
233
|
-
push({
|
|
234
|
-
href: link.groups?.href ?? "",
|
|
235
|
-
kind: "link",
|
|
236
|
-
text: link.groups?.text ?? "",
|
|
237
|
-
});
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
push({ kind: "ref", name: value });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { hasSections: sections.length > 0, lead, sections };
|
|
244
|
-
};
|
package/src/migrate/migrate.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { migrateFumadocsProject } from "./fumadocs/index.ts";
|
|
2
|
-
import { migrateMintlifyProject } from "./mintlify/index.ts";
|
|
3
|
-
import { migrateNextraProject } from "./nextra/index.ts";
|
|
4
|
-
import { migrateStarlightProject } from "./starlight/index.ts";
|
|
5
|
-
|
|
6
|
-
export interface MigrationResult {
|
|
7
|
-
moved: number;
|
|
8
|
-
warnings: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Migrate a Mintlify project (`docs.json`/`mint.json` + MDX). Translates the
|
|
13
|
-
* config, rewrites pages to idiomatic Blume MDX in place, and relocates assets.
|
|
14
|
-
* Unlike the other migrators, content stays at the project root.
|
|
15
|
-
*/
|
|
16
|
-
export const migrateMintlify = (root: string): Promise<MigrationResult> =>
|
|
17
|
-
migrateMintlifyProject(root);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Migrate a Nextra project (`content/` or `pages/` + `_meta` files). Moves pages
|
|
21
|
-
* into `docs/`, rewrites `<Callout>`s to directives, and converts every `_meta`
|
|
22
|
-
* into a typed `meta.ts`, preserving navigation order and titles.
|
|
23
|
-
*/
|
|
24
|
-
export const migrateNextra = (root: string): Promise<MigrationResult> =>
|
|
25
|
-
migrateNextraProject(root);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Migrate a Fumadocs project (`content/docs` + `meta.json`). Moves pages into
|
|
29
|
-
* `docs/`, rewrites Fumadocs MDX (callouts, `<Cards>`/`<Accordions>`/`<Files>`,
|
|
30
|
-
* `<Tabs items>`, `<include>`) to idiomatic Blume markup, converts every
|
|
31
|
-
* `meta.json` into a typed `meta.ts`, and preserves the `/docs` route prefix.
|
|
32
|
-
*/
|
|
33
|
-
export const migrateFumadocs = (root: string): Promise<MigrationResult> =>
|
|
34
|
-
migrateFumadocsProject(root);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Migrate a Starlight project (`src/content/docs` + `astro.config.*`). Translates
|
|
38
|
-
* the `starlight({...})` options into `blume.config.ts` and rewrites each page to
|
|
39
|
-
* idiomatic Blume MDX in place (asides → directives, component renames,
|
|
40
|
-
* frontmatter mapping). Content stays under `src/content/docs`.
|
|
41
|
-
*/
|
|
42
|
-
export const migrateStarlight = (root: string): Promise<MigrationResult> =>
|
|
43
|
-
migrateStarlightProject(root);
|
|
44
|
-
|
|
45
|
-
export const migrators: Record<
|
|
46
|
-
string,
|
|
47
|
-
(root: string) => Promise<MigrationResult>
|
|
48
|
-
> = {
|
|
49
|
-
fumadocs: migrateFumadocs,
|
|
50
|
-
mintlify: migrateMintlify,
|
|
51
|
-
nextra: migrateNextra,
|
|
52
|
-
starlight: migrateStarlight,
|
|
53
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
};
|