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,459 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
import { dirname, join, relative } from "pathe";
|
|
5
|
-
import { glob } from "tinyglobby";
|
|
6
|
-
|
|
7
|
-
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
8
|
-
import type { BlumeConfig } from "../../core/schema.ts";
|
|
9
|
-
import { ensurePackageJson } from "../shared.ts";
|
|
10
|
-
import { assetSegments } from "./assets.ts";
|
|
11
|
-
import { loadMintlifyConfig, partitionMintlifyRedirects } from "./config.ts";
|
|
12
|
-
import { mintlifyI18n } from "./i18n.ts";
|
|
13
|
-
import { transformMintlifyContent } from "./transform.ts";
|
|
14
|
-
|
|
15
|
-
export interface MintlifyMigrationResult {
|
|
16
|
-
moved: number;
|
|
17
|
-
warnings: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
21
|
-
value && typeof value === "object" && !Array.isArray(value)
|
|
22
|
-
? (value as Record<string, unknown>)
|
|
23
|
-
: undefined;
|
|
24
|
-
|
|
25
|
-
const hasFontFamily = (value: unknown): boolean => {
|
|
26
|
-
const object = asRecord(value);
|
|
27
|
-
if (!object) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const named = (child: unknown): boolean =>
|
|
31
|
-
typeof asRecord(child)?.family === "string";
|
|
32
|
-
return (
|
|
33
|
-
typeof object.family === "string" ||
|
|
34
|
-
named(object.heading) ||
|
|
35
|
-
named(object.body)
|
|
36
|
-
);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Warn about Mintlify site chrome that Blume's config doesn't model, so it isn't
|
|
41
|
-
* dropped silently: header links (`navbar.links`/`navbar.primary`), footer
|
|
42
|
-
* socials (`footer.socials`), and fonts outside Blume's curated Google set. The
|
|
43
|
-
* contextual page menu and last-updated timestamp are covered by Blume defaults
|
|
44
|
-
* (page actions, git-derived dates), so they need no warning.
|
|
45
|
-
*/
|
|
46
|
-
const droppedChromeWarnings = (
|
|
47
|
-
spec: Record<string, unknown>,
|
|
48
|
-
config: BlumeConfig
|
|
49
|
-
): string[] => {
|
|
50
|
-
const warnings: string[] = [];
|
|
51
|
-
const navbar = asRecord(spec.navbar);
|
|
52
|
-
if (navbar && (navbar.links || navbar.primary)) {
|
|
53
|
-
warnings.push(
|
|
54
|
-
"Header links (navbar.links/navbar.primary) have no blume.config equivalent and were dropped; re-add them with navigation.tabs or a Header layout override."
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
if (asRecord(spec.footer)?.socials) {
|
|
58
|
-
warnings.push(
|
|
59
|
-
"Footer social links (footer.socials) have no blume.config equivalent and were dropped; add them with a Footer layout override."
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
if (hasFontFamily(spec.fonts ?? spec.font) && !config.theme?.fonts) {
|
|
63
|
-
warnings.push(
|
|
64
|
-
"docs.json font family isn't in Blume's curated Google Fonts set; set theme.fonts to a supported slug or add @font-face rules in theme.css."
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return warnings;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Warn about dynamic (wildcard/param) redirects the migrator dropped. Blume
|
|
72
|
-
* redirects are static path-to-path, so a `:slug*`/`:id` source becomes an
|
|
73
|
-
* unroutable Astro destination — kept ones crash the build. Point the user at
|
|
74
|
-
* host-level rules that do support wildcards.
|
|
75
|
-
*/
|
|
76
|
-
const droppedRedirectWarnings = (spec: Record<string, unknown>): string[] => {
|
|
77
|
-
const { dropped } = partitionMintlifyRedirects(spec);
|
|
78
|
-
if (dropped.length === 0) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
return [
|
|
82
|
-
`Dropped ${dropped.length} dynamic redirect(s) Blume can't model as static path-to-path (${dropped.join(", ")}); re-add them as host-level rules (e.g. _redirects or vercel.json).`,
|
|
83
|
-
];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/** Recursively drop `undefined`, empty arrays, and empty objects. */
|
|
87
|
-
const prune = (value: unknown): unknown => {
|
|
88
|
-
if (Array.isArray(value)) {
|
|
89
|
-
return value.map(prune);
|
|
90
|
-
}
|
|
91
|
-
if (value && typeof value === "object") {
|
|
92
|
-
const out: Record<string, unknown> = {};
|
|
93
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
94
|
-
const pruned = prune(raw);
|
|
95
|
-
if (pruned === undefined) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (Array.isArray(pruned) && pruned.length === 0) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
pruned &&
|
|
103
|
-
typeof pruned === "object" &&
|
|
104
|
-
!Array.isArray(pruned) &&
|
|
105
|
-
Object.keys(pruned).length === 0
|
|
106
|
-
) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
out[key] = pruned;
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
return value;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const writeBlumeConfig = async (
|
|
117
|
-
root: string,
|
|
118
|
-
config: BlumeConfig
|
|
119
|
-
): Promise<void> => {
|
|
120
|
-
const body = `import { defineConfig } from "blume";\n\nexport default defineConfig(${JSON.stringify(prune(config), null, 2)});\n`;
|
|
121
|
-
await writeFile(join(root, "blume.config.ts"), body, "utf-8");
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
interface RelocatedAssets {
|
|
125
|
-
/** Top-level dirs served in place via `content.assets` (no files moved). */
|
|
126
|
-
served: string[];
|
|
127
|
-
/** Top-level files moved under `public/`. */
|
|
128
|
-
moved: string[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Make referenced top-level assets resolvable in Blume. Directories (e.g.
|
|
133
|
-
* Mintlify's `images/`) are left in place and served via `content.assets`, so
|
|
134
|
-
* the migration doesn't churn every file under them; loose top-level files
|
|
135
|
-
* (a root `favicon.png`, `logo.png`) are moved under `public/` since a mount
|
|
136
|
-
* points at a directory.
|
|
137
|
-
*/
|
|
138
|
-
const relocateAssets = async (
|
|
139
|
-
root: string,
|
|
140
|
-
segments: string[]
|
|
141
|
-
): Promise<RelocatedAssets> => {
|
|
142
|
-
const served: string[] = [];
|
|
143
|
-
const moved: string[] = [];
|
|
144
|
-
for (const segment of segments) {
|
|
145
|
-
const source = join(root, segment);
|
|
146
|
-
if (!existsSync(source) || segment === "public") {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs stats
|
|
150
|
-
const stats = await stat(source);
|
|
151
|
-
if (stats.isDirectory()) {
|
|
152
|
-
served.push(segment);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const dest = join(root, "public", segment);
|
|
156
|
-
if (existsSync(dest)) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs moves
|
|
160
|
-
await mkdir(join(root, "public"), { recursive: true });
|
|
161
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs moves
|
|
162
|
-
await rename(source, dest);
|
|
163
|
-
moved.push(segment);
|
|
164
|
-
}
|
|
165
|
-
return { moved, served };
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Fold relocated assets into the config (served dirs become `content.assets`)
|
|
170
|
-
* and record what happened. Served dirs stay in place; only loose files moved.
|
|
171
|
-
*/
|
|
172
|
-
const applyRelocatedAssets = (
|
|
173
|
-
config: BlumeConfig,
|
|
174
|
-
assets: RelocatedAssets,
|
|
175
|
-
warnings: string[]
|
|
176
|
-
): void => {
|
|
177
|
-
if (assets.served.length > 0) {
|
|
178
|
-
config.content = {
|
|
179
|
-
...config.content,
|
|
180
|
-
assets: [
|
|
181
|
-
...new Set([...(config.content?.assets ?? []), ...assets.served]),
|
|
182
|
-
],
|
|
183
|
-
};
|
|
184
|
-
warnings.push(
|
|
185
|
-
`Kept asset dir(s) in place, served via content.assets: ${assets.served.join(", ")}.`
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
if (assets.moved.length > 0) {
|
|
189
|
-
warnings.push(`Moved assets into public/: ${assets.moved.join(", ")}.`);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Remove the foreign Mintlify config now that it's been translated into
|
|
195
|
-
* `blume.config.ts`. Leaving `docs.json`/`mint.json` on disk keeps the project a
|
|
196
|
-
* bridge-mode candidate: `detectMintlifyBridge` fires on any later run where a
|
|
197
|
-
* `blume.config.*` is absent (e.g. the config is deleted to re-run the
|
|
198
|
-
* migration), silently serving the *un-migrated* Mintlify project instead of the
|
|
199
|
-
* converted one. Removing it makes the conversion permanent — matching the
|
|
200
|
-
* migrator's promise and how it already deletes inlined snippets.
|
|
201
|
-
*/
|
|
202
|
-
const removeForeignConfig = async (
|
|
203
|
-
root: string,
|
|
204
|
-
warnings: string[]
|
|
205
|
-
): Promise<void> => {
|
|
206
|
-
const removed: string[] = [];
|
|
207
|
-
for (const name of ["docs.json", "mint.json"]) {
|
|
208
|
-
const file = join(root, name);
|
|
209
|
-
if (existsSync(file)) {
|
|
210
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs removes
|
|
211
|
-
await rm(file, { force: true });
|
|
212
|
-
removed.push(name);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (removed.length > 0) {
|
|
216
|
-
warnings.push(
|
|
217
|
-
`Removed ${removed.join(", ")} (translated to blume.config.ts) so "blume dev" no longer falls back to Mintlify bridge mode.`
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Scaffold the project files a config-only Mintlify repo lacks: a runnable
|
|
224
|
-
* `package.json` (it ships no npm manifest) and a `.gitignore` for Blume's
|
|
225
|
-
* generated `.blume/` runtime and `dist/` build output. Both are idempotent —
|
|
226
|
-
* an existing file is extended, not overwritten — and noted in the warnings.
|
|
227
|
-
*/
|
|
228
|
-
const scaffoldProjectFiles = async (
|
|
229
|
-
root: string,
|
|
230
|
-
warnings: string[]
|
|
231
|
-
): Promise<void> => {
|
|
232
|
-
if (await ensurePackageJson(root)) {
|
|
233
|
-
warnings.push(
|
|
234
|
-
"Created a package.json with blume as a dependency; run `npm install`, then `npm run dev`."
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
238
|
-
if (ignored.length > 0) {
|
|
239
|
-
warnings.push(`Added ${ignored.join(", ")} to .gitignore.`);
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Delete the inlined markdown snippets. Component files (e.g. `.jsx`) are kept
|
|
245
|
-
* because their imports were rewritten to resolve against `/snippets`.
|
|
246
|
-
*/
|
|
247
|
-
const cleanupSnippets = async (
|
|
248
|
-
root: string,
|
|
249
|
-
kept: Set<string>,
|
|
250
|
-
warnings: string[]
|
|
251
|
-
): Promise<void> => {
|
|
252
|
-
const dir = join(root, "snippets");
|
|
253
|
-
if (!existsSync(dir)) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const markdown = await glob(["**/*.{md,mdx}"], { absolute: true, cwd: dir });
|
|
257
|
-
for (const file of markdown) {
|
|
258
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs removes
|
|
259
|
-
await rm(file, { force: true });
|
|
260
|
-
}
|
|
261
|
-
const remaining = await glob(["**/*"], { cwd: dir, dot: true });
|
|
262
|
-
if (remaining.length === 0) {
|
|
263
|
-
await rm(dir, { force: true, recursive: true });
|
|
264
|
-
warnings.push("Inlined and removed the /snippets directory.");
|
|
265
|
-
} else {
|
|
266
|
-
warnings.push(
|
|
267
|
-
`Inlined markdown snippets; kept ${remaining.length} component file(s) under /snippets.`
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
if (kept.size > 0) {
|
|
271
|
-
warnings.push(
|
|
272
|
-
`Rewrote ${kept.size} component snippet import(s) to relative paths; verify they resolve.`
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// Root-absolute asset references in page content (``,
|
|
278
|
-
// `src="/img/x.svg"`). Mintlify serves any top-level dir at the site root, so
|
|
279
|
-
// dirs referenced only by content — not the config — must also be mounted.
|
|
280
|
-
const CONTENT_ASSET_REF =
|
|
281
|
-
/(?:\]\(|src=["'])(?<path>\/[^\s"')]+\.(?:avif|bmp|gif|ico|jpe?g|mov|mp4|pdf|png|svg|webm|webp|zip))/giu;
|
|
282
|
-
|
|
283
|
-
const contentAssetSegments = (content: string): string[] =>
|
|
284
|
-
[...content.matchAll(CONTENT_ASSET_REF)].flatMap((match) => {
|
|
285
|
-
const segment = match.groups?.path?.split("/")[1];
|
|
286
|
-
return segment ? [segment] : [];
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Rewrite every page to idiomatic Blume MDX in place, collecting what was
|
|
291
|
-
* dropped or kept for the migration summary. A page whose transform throws
|
|
292
|
-
* (a dangling snippet import, a snippet cycle) must not abort the whole
|
|
293
|
-
* migration mid-rewrite; it is left as-is with a warning.
|
|
294
|
-
*/
|
|
295
|
-
const rewritePagesInPlace = async (
|
|
296
|
-
files: string[],
|
|
297
|
-
options: {
|
|
298
|
-
root: string;
|
|
299
|
-
variables: Record<string, string>;
|
|
300
|
-
warnings: string[];
|
|
301
|
-
}
|
|
302
|
-
): Promise<{
|
|
303
|
-
moved: number;
|
|
304
|
-
removedKeys: Set<string>;
|
|
305
|
-
unsupported: Set<string>;
|
|
306
|
-
keptComponents: Set<string>;
|
|
307
|
-
assetDirs: Set<string>;
|
|
308
|
-
}> => {
|
|
309
|
-
const { root, variables, warnings } = options;
|
|
310
|
-
let moved = 0;
|
|
311
|
-
const removedKeys = new Set<string>();
|
|
312
|
-
const unsupported = new Set<string>();
|
|
313
|
-
const keptComponents = new Set<string>();
|
|
314
|
-
const assetDirs = new Set<string>();
|
|
315
|
-
for (const file of files) {
|
|
316
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs writes
|
|
317
|
-
const raw = await readFile(file, "utf-8");
|
|
318
|
-
let result: Awaited<ReturnType<typeof transformMintlifyContent>>;
|
|
319
|
-
try {
|
|
320
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential transforms
|
|
321
|
-
result = await transformMintlifyContent(raw, {
|
|
322
|
-
filePath: file,
|
|
323
|
-
root,
|
|
324
|
-
variables,
|
|
325
|
-
});
|
|
326
|
-
} catch (error) {
|
|
327
|
-
warnings.push(
|
|
328
|
-
`Skipped rewriting ${relative(root, file)}: ${(error as Error).message}. The page was left unconverted — fix it by hand.`
|
|
329
|
-
);
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
if (result.content !== raw) {
|
|
333
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs writes
|
|
334
|
-
await mkdir(dirname(file), { recursive: true });
|
|
335
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs writes
|
|
336
|
-
await writeFile(file, result.content, "utf-8");
|
|
337
|
-
}
|
|
338
|
-
for (const key of result.removed) {
|
|
339
|
-
removedKeys.add(key);
|
|
340
|
-
}
|
|
341
|
-
for (const name of result.unsupported) {
|
|
342
|
-
unsupported.add(name);
|
|
343
|
-
}
|
|
344
|
-
for (const name of result.components) {
|
|
345
|
-
keptComponents.add(name);
|
|
346
|
-
}
|
|
347
|
-
for (const segment of contentAssetSegments(result.content)) {
|
|
348
|
-
assetDirs.add(segment);
|
|
349
|
-
}
|
|
350
|
-
moved += 1;
|
|
351
|
-
}
|
|
352
|
-
return { assetDirs, keptComponents, moved, removedKeys, unsupported };
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Migrate a Mintlify project to Blume: translate `docs.json`/`mint.json` into
|
|
357
|
-
* `blume.config.ts`, rewrite every page to idiomatic Blume MDX in place, and
|
|
358
|
-
* relocate static assets. Content stays at the project root (`content.root`
|
|
359
|
-
* is `"."`).
|
|
360
|
-
*/
|
|
361
|
-
export const migrateMintlifyProject = async (
|
|
362
|
-
root: string
|
|
363
|
-
): Promise<MintlifyMigrationResult> => {
|
|
364
|
-
const warnings: string[] = [];
|
|
365
|
-
const configFile = existsSync(join(root, "docs.json"))
|
|
366
|
-
? join(root, "docs.json")
|
|
367
|
-
: join(root, "mint.json");
|
|
368
|
-
|
|
369
|
-
let config: BlumeConfig;
|
|
370
|
-
if (existsSync(configFile)) {
|
|
371
|
-
config = await loadMintlifyConfig(root, configFile);
|
|
372
|
-
const spec = JSON.parse(await readFile(configFile, "utf-8")) as Record<
|
|
373
|
-
string,
|
|
374
|
-
unknown
|
|
375
|
-
>;
|
|
376
|
-
const i18n = mintlifyI18n(spec);
|
|
377
|
-
if (i18n) {
|
|
378
|
-
config.i18n = i18n;
|
|
379
|
-
// Language switching is handled by Blume i18n, not a nav selector.
|
|
380
|
-
if (config.navigation?.selectors) {
|
|
381
|
-
config.navigation.selectors = config.navigation.selectors.filter(
|
|
382
|
-
(selector) => selector.kind !== "language"
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
warnings.push(
|
|
386
|
-
`Mapped ${i18n.locales.length} languages to i18n.locales (default: ${i18n.defaultLocale}); review the locale labels.`
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const openapiSources = config.openapi?.sources ?? [];
|
|
390
|
-
if (openapiSources.length > 0) {
|
|
391
|
-
warnings.push(
|
|
392
|
-
`Mapped ${openapiSources.length} OpenAPI spec source(s) to openapi.sources (native reference renderer); verify each spec path or URL resolves.`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
warnings.push(...droppedChromeWarnings(spec, config));
|
|
396
|
-
warnings.push(...droppedRedirectWarnings(spec));
|
|
397
|
-
} else {
|
|
398
|
-
warnings.push("No docs.json or mint.json found; writing a default config.");
|
|
399
|
-
config = { content: { root: "." }, title: "Documentation" };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const variables = (config.variables as Record<string, string>) ?? {};
|
|
403
|
-
// Globals are inlined into content below; Blume has no runtime substitution.
|
|
404
|
-
config.variables = undefined;
|
|
405
|
-
|
|
406
|
-
const files = await glob(["**/*.{md,mdx}"], {
|
|
407
|
-
absolute: true,
|
|
408
|
-
cwd: root,
|
|
409
|
-
ignore: [
|
|
410
|
-
"node_modules/**",
|
|
411
|
-
".blume/**",
|
|
412
|
-
"dist/**",
|
|
413
|
-
"public/**",
|
|
414
|
-
"snippets/**",
|
|
415
|
-
],
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
const { moved, removedKeys, unsupported, keptComponents, assetDirs } =
|
|
419
|
-
await rewritePagesInPlace(files, { root, variables, warnings });
|
|
420
|
-
|
|
421
|
-
// Config-referenced segments (logo/favicon/backgrounds + /images) plus dirs
|
|
422
|
-
// referenced only by page content — Mintlify serves every top-level dir at
|
|
423
|
-
// the site root, so both kinds must stay resolvable after migration.
|
|
424
|
-
const assets = await relocateAssets(root, [
|
|
425
|
-
...new Set([...assetSegments(config), ...assetDirs]),
|
|
426
|
-
]);
|
|
427
|
-
await cleanupSnippets(root, keptComponents, warnings);
|
|
428
|
-
|
|
429
|
-
if (config.content?.exclude) {
|
|
430
|
-
config.content.exclude = [...new Set(config.content.exclude)];
|
|
431
|
-
}
|
|
432
|
-
applyRelocatedAssets(config, assets, warnings);
|
|
433
|
-
await writeBlumeConfig(root, config);
|
|
434
|
-
// Drop the source config only after the Blume config is safely on disk, so a
|
|
435
|
-
// mid-migration failure never leaves the project with neither.
|
|
436
|
-
await removeForeignConfig(root, warnings);
|
|
437
|
-
await scaffoldProjectFiles(root, warnings);
|
|
438
|
-
|
|
439
|
-
if (Object.keys(variables).length > 0) {
|
|
440
|
-
warnings.push(
|
|
441
|
-
`Inlined ${Object.keys(variables).length} docs.json variable(s) into content; Blume has no runtime variable substitution.`
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
if (removedKeys.size > 0) {
|
|
445
|
-
warnings.push(
|
|
446
|
-
`Dropped unsupported page frontmatter keys: ${[...removedKeys].join(", ")}.`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
if (unsupported.size > 0) {
|
|
450
|
-
warnings.push(
|
|
451
|
-
`Components without a Blume equivalent need manual review: ${[...unsupported].join(", ")}.`
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
warnings.push(
|
|
455
|
-
"Review blume.config.ts; navigation, theme, and chrome were mapped from docs.json."
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
return { moved, warnings };
|
|
459
|
-
};
|