blume 0.1.0 → 0.1.2
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 +1213 -473
- package/dist/cli/index.js.map +33 -29
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3461 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/advanced/custom-pages.mdx +91 -10
- package/docs/configuration/index.mdx +19 -0
- package/docs/content/components.mdx +103 -1
- package/package.json +13 -3
- package/src/ai/llms.ts +1 -2
- package/src/astro/examples.ts +106 -0
- package/src/astro/generate.ts +300 -75
- package/src/astro/index.ts +1 -1
- package/src/astro/islands.ts +1 -1
- package/src/astro/pages.ts +59 -0
- package/src/astro/templates.ts +180 -22
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Tab.astro +8 -2
- package/src/components/content/diff.ts +95 -0
- package/src/components/layout/Favicon.astro +11 -4
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +9 -5
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/core/data.ts +128 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/links.ts +26 -10
- package/src/core/schema.ts +8 -0
- package/src/core/sources/filesystem.ts +1 -1
- package/src/core/sources/mdx-remote.ts +1 -2
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +19 -3
- package/src/core/sources/notion.ts +1 -1
- package/src/core/sources/sanity.ts +1 -1
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/index.ts +12 -0
- package/src/markdown/index.ts +69 -0
- package/src/migrate/fumadocs/config.ts +53 -4
- package/src/migrate/fumadocs/content.ts +1 -1
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +104 -19
- package/src/migrate/fumadocs/meta.ts +140 -10
- package/src/migrate/mintlify/snippets.ts +2 -1
- package/src/migrate/mintlify/transform.ts +1 -2
- package/src/migrate/nextra/index.ts +1 -1
- package/src/migrate/shared.ts +98 -1
- package/src/migrate/starlight/index.ts +1 -1
- package/src/registry/eject.ts +57 -10
- package/src/search/documents.ts +1 -2
- package/src/theme/entry.ts +29 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import matter from "gray-matter";
|
|
5
4
|
import { dirname, join, relative } from "pathe";
|
|
6
5
|
import { glob } from "tinyglobby";
|
|
7
6
|
|
|
8
|
-
import
|
|
7
|
+
import matter from "../../core/frontmatter.ts";
|
|
8
|
+
import type { FolderMeta } from "../../core/schema.ts";
|
|
9
|
+
import {
|
|
10
|
+
ensureGitignore,
|
|
11
|
+
leftoverFiles,
|
|
12
|
+
rewriteFrameworkScripts,
|
|
13
|
+
writeBlumeConfig,
|
|
14
|
+
} from "../shared.ts";
|
|
9
15
|
import { loadFumadocsConfig } from "./config.ts";
|
|
10
16
|
import {
|
|
11
17
|
inlineFumadocsIncludes,
|
|
@@ -16,7 +22,13 @@ import {
|
|
|
16
22
|
unsupportedFumadocsComponents,
|
|
17
23
|
} from "./content.ts";
|
|
18
24
|
import { normalizeFumadocsPageMeta } from "./frontmatter.ts";
|
|
19
|
-
import {
|
|
25
|
+
import { reshapeFumadocsGroups } from "./groups.ts";
|
|
26
|
+
import {
|
|
27
|
+
parseFumadocsPages,
|
|
28
|
+
renderMetaModule,
|
|
29
|
+
translateFumadocsMeta,
|
|
30
|
+
translateFumadocsSelfMeta,
|
|
31
|
+
} from "./meta.ts";
|
|
20
32
|
|
|
21
33
|
export interface FumadocsMigrationResult {
|
|
22
34
|
moved: number;
|
|
@@ -29,6 +41,23 @@ const PAGE_GLOB = "**/*.{md,mdx}";
|
|
|
29
41
|
const META_GLOB = "**/meta.json";
|
|
30
42
|
const IGNORE = ["**/node_modules/**"];
|
|
31
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
|
+
|
|
32
61
|
interface PageResult {
|
|
33
62
|
includeWarnings: string[];
|
|
34
63
|
moved: number;
|
|
@@ -82,6 +111,24 @@ const movePage = async (
|
|
|
82
111
|
};
|
|
83
112
|
};
|
|
84
113
|
|
|
114
|
+
/** Write a `FolderMeta` to `dest` unless it already exists. */
|
|
115
|
+
const writeMeta = async (
|
|
116
|
+
dest: string,
|
|
117
|
+
meta: FolderMeta,
|
|
118
|
+
rel: string,
|
|
119
|
+
warnings: string[]
|
|
120
|
+
): Promise<string[]> => {
|
|
121
|
+
if (Object.keys(meta).length === 0) {
|
|
122
|
+
return warnings;
|
|
123
|
+
}
|
|
124
|
+
if (existsSync(dest)) {
|
|
125
|
+
return [...warnings, `Skipped ${rel} (target already exists)`];
|
|
126
|
+
}
|
|
127
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
128
|
+
await writeFile(dest, renderMetaModule(meta), "utf-8");
|
|
129
|
+
return warnings;
|
|
130
|
+
};
|
|
131
|
+
|
|
85
132
|
/** Convert one `meta.json` into a typed `meta.ts`, or relocate it if unparseable. */
|
|
86
133
|
const convertMeta = async (
|
|
87
134
|
abs: string,
|
|
@@ -105,23 +152,32 @@ const convertMeta = async (
|
|
|
105
152
|
];
|
|
106
153
|
}
|
|
107
154
|
|
|
108
|
-
const { meta, warnings } = translateFumadocsMeta(parsed);
|
|
109
155
|
const dir = dirname(rel) === "." ? "" : dirname(rel);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
156
|
+
const docsDir = join(root, "docs", dir);
|
|
157
|
+
const dest = join(docsDir, "meta.ts");
|
|
158
|
+
|
|
159
|
+
// A `pages` array with `---Section---` separators can't round-trip through a
|
|
160
|
+
// flat `meta.ts` ordering, so rebuild its sections as group folders instead.
|
|
161
|
+
const structure = parseFumadocsPages((parsed as { pages?: unknown }).pages);
|
|
162
|
+
if (structure.hasSections && !existsSync(dest)) {
|
|
163
|
+
const self = translateFumadocsSelfMeta(parsed);
|
|
164
|
+
const reshape = await reshapeFumadocsGroups(structure, docsDir);
|
|
165
|
+
const meta: FolderMeta = { ...self.meta };
|
|
166
|
+
if (reshape.order.length > 0) {
|
|
167
|
+
meta.pages = reshape.order;
|
|
115
168
|
}
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
169
|
+
const result = await writeMeta(dest, meta, rel, [
|
|
170
|
+
...self.warnings,
|
|
171
|
+
...reshape.warnings,
|
|
172
|
+
]);
|
|
173
|
+
await rm(abs, { force: true });
|
|
174
|
+
return result;
|
|
122
175
|
}
|
|
176
|
+
|
|
177
|
+
const { meta, warnings } = translateFumadocsMeta(parsed);
|
|
178
|
+
const result = await writeMeta(dest, meta, rel, warnings);
|
|
123
179
|
await rm(abs, { force: true });
|
|
124
|
-
return
|
|
180
|
+
return result;
|
|
125
181
|
};
|
|
126
182
|
|
|
127
183
|
interface PageSummary {
|
|
@@ -221,14 +277,32 @@ export const migrateFumadocsProject = async (
|
|
|
221
277
|
pageFiles.map((abs) => movePage(abs, base, root))
|
|
222
278
|
);
|
|
223
279
|
const pages = summarizePages(pageResults);
|
|
224
|
-
|
|
225
|
-
|
|
280
|
+
// Convert metas deepest-first and sequentially: a parent's group-folder
|
|
281
|
+
// reshape can move a child folder, so the child's own reshape must finish
|
|
282
|
+
// first (and the two can't race over the same `docs/` paths).
|
|
283
|
+
const orderedMetas = metaFiles.toSorted(
|
|
284
|
+
(a, b) => b.split("/").length - a.split("/").length
|
|
226
285
|
);
|
|
227
|
-
const metaWarnings =
|
|
286
|
+
const metaWarnings: string[] = [];
|
|
287
|
+
for (const abs of orderedMetas) {
|
|
288
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential to avoid move races
|
|
289
|
+
metaWarnings.push(...(await convertMeta(abs, base, root)));
|
|
290
|
+
}
|
|
228
291
|
|
|
229
292
|
await writeBlumeConfig(root, config);
|
|
230
293
|
const cleanupWarnings = await cleanupSourceDirs(root);
|
|
231
294
|
|
|
295
|
+
// Tear down the old Next/Fumadocs scaffolding so the project builds as Blume:
|
|
296
|
+
// repoint the npm scripts, ignore Blume's outputs, and list the leftover
|
|
297
|
+
// framework files to delete by hand.
|
|
298
|
+
const scriptsRewritten = await rewriteFrameworkScripts(
|
|
299
|
+
root,
|
|
300
|
+
/\bnext\b/u,
|
|
301
|
+
/fumadocs/u
|
|
302
|
+
);
|
|
303
|
+
const gitignoreAdded = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
304
|
+
const leftovers = leftoverFiles(root, FUMADOCS_LEFTOVERS);
|
|
305
|
+
|
|
232
306
|
const warnings = [
|
|
233
307
|
...configWarnings,
|
|
234
308
|
...metaWarnings,
|
|
@@ -246,6 +320,17 @@ export const migrateFumadocsProject = async (
|
|
|
246
320
|
`Components without a drop-in Blume equivalent need manual review: ${pages.unsupported.join(", ")}.`
|
|
247
321
|
);
|
|
248
322
|
}
|
|
323
|
+
if (scriptsRewritten) {
|
|
324
|
+
warnings.push("Repointed the dev/build/start scripts at the Blume CLI.");
|
|
325
|
+
}
|
|
326
|
+
if (gitignoreAdded.length > 0) {
|
|
327
|
+
warnings.push(`Added ${gitignoreAdded.join(", ")} to .gitignore.`);
|
|
328
|
+
}
|
|
329
|
+
if (leftovers.length > 0) {
|
|
330
|
+
warnings.push(
|
|
331
|
+
`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.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
249
334
|
warnings.push("Review blume.config.ts and the generated meta.ts files.");
|
|
250
335
|
|
|
251
336
|
return { moved: pages.moved, warnings };
|
|
@@ -10,9 +10,16 @@ import type { FolderMeta } from "../../core/schema.ts";
|
|
|
10
10
|
* - `root: true` -> `display: "page"` (the closest analogue to a page-tree root).
|
|
11
11
|
* - `description` -> dropped (folders carry no description in Blume).
|
|
12
12
|
* - `pages` entries: plain slugs are kept as ordering; `"..."` (the rest marker,
|
|
13
|
-
* already Blume's default) is dropped silently; `"
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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`.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
interface FumadocsMeta {
|
|
@@ -27,6 +34,8 @@ interface FumadocsMeta {
|
|
|
27
34
|
const SEPARATOR = /^---(?<label>.*)---$/u;
|
|
28
35
|
const LINK = /^(?:\[[^\]]*\])?\[(?<text>[^\]]*)\]\((?<href>[^)]*)\)$/u;
|
|
29
36
|
const REST = "...";
|
|
37
|
+
/** Fumadocs *extract* operator: `...folder` inlines that folder's children. */
|
|
38
|
+
const EXTRACT = /^\.\.\.(?<folder>.+)$/u;
|
|
30
39
|
|
|
31
40
|
interface PagesResult {
|
|
32
41
|
pages: string[];
|
|
@@ -48,6 +57,20 @@ const filterPages = (raw: unknown): PagesResult => {
|
|
|
48
57
|
if (value === REST || value === "") {
|
|
49
58
|
continue;
|
|
50
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
|
+
}
|
|
51
74
|
const separator = SEPARATOR.exec(value);
|
|
52
75
|
if (separator) {
|
|
53
76
|
const label = separator.groups?.label?.trim();
|
|
@@ -74,8 +97,17 @@ export interface FumadocsMetaConversion {
|
|
|
74
97
|
warnings: string[];
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
/**
|
|
78
|
-
export const
|
|
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 = (
|
|
79
111
|
value: unknown
|
|
80
112
|
): FumadocsMetaConversion => {
|
|
81
113
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -104,11 +136,109 @@ export const translateFumadocsMeta = (
|
|
|
104
136
|
);
|
|
105
137
|
}
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
meta.pages = pages;
|
|
110
|
-
}
|
|
111
|
-
warnings.push(...pageWarnings);
|
|
139
|
+
return { meta, warnings };
|
|
140
|
+
};
|
|
112
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
|
+
}
|
|
113
156
|
return { meta, warnings };
|
|
114
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
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFile as readFileFromDisk } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
import matter from "gray-matter";
|
|
4
3
|
import { dirname, relative, resolve } from "pathe";
|
|
5
4
|
|
|
5
|
+
import matter from "../../core/frontmatter.ts";
|
|
6
|
+
|
|
6
7
|
const MARKDOWN_SNIPPET_IMPORT =
|
|
7
8
|
/^import\s+(?<name>[$A-Z_a-z][$\w]*)\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
|
|
8
9
|
const NAMED_SNIPPET_IMPORT =
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import matter from "gray-matter";
|
|
5
4
|
import { basename, dirname, extname, join, relative } from "pathe";
|
|
6
5
|
import { glob } from "tinyglobby";
|
|
7
6
|
|
|
7
|
+
import matter from "../../core/frontmatter.ts";
|
|
8
8
|
import type { BlumeConfig, FolderMeta } from "../../core/schema.ts";
|
|
9
9
|
import { writeBlumeConfig } from "../shared.ts";
|
|
10
10
|
import {
|
package/src/migrate/shared.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
|
|
3
4
|
import { join } from "pathe";
|
|
4
5
|
|
|
@@ -20,6 +21,102 @@ export const writeBlumeConfig = async (
|
|
|
20
21
|
await writeFile(join(root, "blume.config.ts"), body, "utf-8");
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Old-framework teardown
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** The Blume command each standard npm script maps to after a migration. */
|
|
29
|
+
const BLUME_SCRIPTS: Record<string, string> = {
|
|
30
|
+
build: "blume build",
|
|
31
|
+
dev: "blume dev",
|
|
32
|
+
start: "blume preview",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Rewrite a migrated project's npm scripts off the old framework's CLI. A
|
|
37
|
+
* `dev`/`build`/`start` script whose command invokes `cli` (e.g. `/\bnext\b/`)
|
|
38
|
+
* is repointed at the matching Blume command (`start` -> `blume preview`); a
|
|
39
|
+
* script whose command matches `remove` (e.g. a `fumadocs-mdx` postinstall) is
|
|
40
|
+
* dropped. Scripts that don't match either are left untouched, so custom tasks
|
|
41
|
+
* survive. Returns true when `package.json` changed.
|
|
42
|
+
*/
|
|
43
|
+
export const rewriteFrameworkScripts = async (
|
|
44
|
+
root: string,
|
|
45
|
+
cli: RegExp,
|
|
46
|
+
remove?: RegExp
|
|
47
|
+
): Promise<boolean> => {
|
|
48
|
+
const pkgPath = join(root, "package.json");
|
|
49
|
+
if (!existsSync(pkgPath)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
let pkg: { scripts?: Record<string, unknown> };
|
|
53
|
+
try {
|
|
54
|
+
pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const { scripts } = pkg;
|
|
59
|
+
if (!scripts || typeof scripts !== "object") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const next: Record<string, unknown> = {};
|
|
64
|
+
let changed = false;
|
|
65
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
66
|
+
const blume = BLUME_SCRIPTS[name];
|
|
67
|
+
if (typeof command === "string" && remove?.test(command)) {
|
|
68
|
+
changed = true;
|
|
69
|
+
} else if (
|
|
70
|
+
typeof command === "string" &&
|
|
71
|
+
blume &&
|
|
72
|
+
cli.test(command) &&
|
|
73
|
+
command !== blume
|
|
74
|
+
) {
|
|
75
|
+
next[name] = blume;
|
|
76
|
+
changed = true;
|
|
77
|
+
} else {
|
|
78
|
+
next[name] = command;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (changed) {
|
|
83
|
+
pkg.scripts = next;
|
|
84
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
return changed;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A `.gitignore` line, normalized for comparison (trailing slashes dropped). */
|
|
90
|
+
const gitignoreKey = (line: string): string => line.trim().replace(/\/+$/u, "");
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Ensure `.gitignore` ignores each of `entries`, appending any that are missing
|
|
94
|
+
* (creating the file when absent). Trailing-slash differences (`dist` vs
|
|
95
|
+
* `dist/`) count as already present. Returns the entries actually added.
|
|
96
|
+
*/
|
|
97
|
+
export const ensureGitignore = async (
|
|
98
|
+
root: string,
|
|
99
|
+
entries: string[]
|
|
100
|
+
): Promise<string[]> => {
|
|
101
|
+
const path = join(root, ".gitignore");
|
|
102
|
+
const existing = existsSync(path) ? await readFile(path, "utf-8") : "";
|
|
103
|
+
const present = new Set(
|
|
104
|
+
existing.split("\n").map(gitignoreKey).filter(Boolean)
|
|
105
|
+
);
|
|
106
|
+
const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
|
|
107
|
+
if (added.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const gap = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
111
|
+
await writeFile(path, `${existing}${gap}${added.join("\n")}\n`, "utf-8");
|
|
112
|
+
return added;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Of the candidate project-relative paths, the ones that still exist — the old
|
|
116
|
+
* framework files a migration leaves behind for the user to remove by hand. */
|
|
117
|
+
export const leftoverFiles = (root: string, candidates: string[]): string[] =>
|
|
118
|
+
candidates.filter((candidate) => existsSync(join(root, candidate)));
|
|
119
|
+
|
|
23
120
|
// ---------------------------------------------------------------------------
|
|
24
121
|
// Callout components -> Blume `:::` directives
|
|
25
122
|
// ---------------------------------------------------------------------------
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import matter from "gray-matter";
|
|
5
4
|
import { join } from "pathe";
|
|
6
5
|
import { glob } from "tinyglobby";
|
|
7
6
|
|
|
7
|
+
import matter from "../../core/frontmatter.ts";
|
|
8
8
|
import { writeBlumeConfig } from "../shared.ts";
|
|
9
9
|
import { loadStarlightConfig, mapStarlightConfig } from "./config.ts";
|
|
10
10
|
import {
|
package/src/registry/eject.ts
CHANGED
|
@@ -5,18 +5,25 @@ import { join, relative } from "pathe";
|
|
|
5
5
|
|
|
6
6
|
import { resolveAskBackend } from "../ai/ask.ts";
|
|
7
7
|
import { buildRawMarkdown } from "../ai/markdown.ts";
|
|
8
|
+
import { discoverExamples } from "../astro/examples.ts";
|
|
8
9
|
import {
|
|
9
10
|
buildRuntimeData,
|
|
10
11
|
collectStaged,
|
|
11
12
|
detectNeedsReact,
|
|
12
13
|
} from "../astro/generate.ts";
|
|
13
|
-
import {
|
|
14
|
+
import { discoverIslands } from "../astro/islands.ts";
|
|
15
|
+
import { customOgRoutes, discoverPages } from "../astro/pages.ts";
|
|
14
16
|
import {
|
|
15
17
|
askEndpointTemplate,
|
|
16
18
|
astroConfigTemplate,
|
|
17
19
|
catchAllPageTemplate,
|
|
18
20
|
contentConfigTemplate,
|
|
19
21
|
envTemplate,
|
|
22
|
+
exampleMapTemplate,
|
|
23
|
+
exampleWrapperTemplate,
|
|
24
|
+
exampleSlug,
|
|
25
|
+
islandMapTemplate,
|
|
26
|
+
islandWrapperTemplate,
|
|
20
27
|
mixedbreadSearchEndpointTemplate,
|
|
21
28
|
ogEndpointTemplate,
|
|
22
29
|
rawMarkdownEndpointTemplate,
|
|
@@ -55,15 +62,28 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
55
62
|
const exportPdf = config.export.pdf;
|
|
56
63
|
const exportEpub = config.export.epub;
|
|
57
64
|
|
|
58
|
-
const [pages, needsReactRaw, userTheme, rawMarkdown] =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const [pages, needsReactRaw, userTheme, rawMarkdown, islands, examples] =
|
|
66
|
+
await Promise.all([
|
|
67
|
+
context.pagesRoot
|
|
68
|
+
? discoverPages(context.pagesRoot)
|
|
69
|
+
: Promise.resolve([]),
|
|
70
|
+
detectNeedsReact(root),
|
|
71
|
+
context.themeFile
|
|
72
|
+
? readFile(context.themeFile, "utf-8")
|
|
73
|
+
: Promise.resolve(""),
|
|
74
|
+
buildRawMarkdown(project),
|
|
75
|
+
discoverIslands(root),
|
|
76
|
+
discoverExamples(root),
|
|
77
|
+
]);
|
|
78
|
+
// Island/example frameworks drive which Astro renderers the ejected config
|
|
79
|
+
// wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
|
|
80
|
+
const frameworks = new Set<string>([
|
|
81
|
+
...islands.islands.map((island) => island.framework),
|
|
82
|
+
...examples.examples.map((example) => example.framework),
|
|
65
83
|
]);
|
|
66
|
-
const needsReact = needsReactRaw || askEnabled;
|
|
84
|
+
const needsReact = needsReactRaw || askEnabled || frameworks.has("react");
|
|
85
|
+
const needsVue = frameworks.has("vue");
|
|
86
|
+
const needsSvelte = frameworks.has("svelte");
|
|
67
87
|
|
|
68
88
|
// A project-relative context so generated files use portable paths.
|
|
69
89
|
const relContext: ProjectContext = {
|
|
@@ -95,7 +115,10 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
95
115
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
96
116
|
context: relContext,
|
|
97
117
|
dataPath: "./src/generated/data.json",
|
|
118
|
+
examplesPath: "./src/generated/examples.ts",
|
|
98
119
|
needsReact,
|
|
120
|
+
needsSvelte,
|
|
121
|
+
needsVue,
|
|
99
122
|
pages: relPages,
|
|
100
123
|
searchClientPath: "./src/generated/search-client.ts",
|
|
101
124
|
themePath: "./src/generated/app.css",
|
|
@@ -129,6 +152,16 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
129
152
|
content: userComponentsTemplate(componentsImport),
|
|
130
153
|
path: join(genDir, "components.ts"),
|
|
131
154
|
},
|
|
155
|
+
// Island/example maps the catch-all imports; written even when empty so the
|
|
156
|
+
// relative import and the `blume:examples` alias always resolve.
|
|
157
|
+
{
|
|
158
|
+
content: islandMapTemplate(islands.islands),
|
|
159
|
+
path: join(genDir, "islands.ts"),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
content: exampleMapTemplate(examples.examples),
|
|
163
|
+
path: join(genDir, "examples.ts"),
|
|
164
|
+
},
|
|
132
165
|
{
|
|
133
166
|
content: tailwindEntryTemplate({
|
|
134
167
|
configTokens: buildThemeCss(config.theme),
|
|
@@ -166,7 +199,9 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
166
199
|
|
|
167
200
|
if (config.seo.og.enabled) {
|
|
168
201
|
files.push({
|
|
169
|
-
content: ogEndpointTemplate(
|
|
202
|
+
content: ogEndpointTemplate(
|
|
203
|
+
customOgRoutes(pages, config.title, config.description)
|
|
204
|
+
),
|
|
170
205
|
path: join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
171
206
|
});
|
|
172
207
|
}
|
|
@@ -233,6 +268,18 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
233
268
|
}
|
|
234
269
|
}
|
|
235
270
|
|
|
271
|
+
// Per-island and per-example live wrappers referenced by the maps above.
|
|
272
|
+
files.push(
|
|
273
|
+
...islands.islands.map((island) => ({
|
|
274
|
+
content: islandWrapperTemplate(island),
|
|
275
|
+
path: join(genDir, "islands", `${island.name}.astro`),
|
|
276
|
+
})),
|
|
277
|
+
...examples.examples.map((example) => ({
|
|
278
|
+
content: exampleWrapperTemplate(example),
|
|
279
|
+
path: join(genDir, "examples", `${exampleSlug(example.path)}.astro`),
|
|
280
|
+
}))
|
|
281
|
+
);
|
|
282
|
+
|
|
236
283
|
// Materialize staged source bodies under `<root>/blume-staged/<source>/<ref>`,
|
|
237
284
|
// matching the relative `staged` collection base in the ejected config.
|
|
238
285
|
for (const [entryId, content] of staged) {
|
package/src/search/documents.ts
CHANGED