blume 0.5.3 → 0.6.0

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.
Files changed (132) hide show
  1. package/dist/cli/index.js +3349 -7024
  2. package/dist/cli/index.js.map +39 -69
  3. package/dist/types/core/config.d.ts +0 -8
  4. package/dist/types/core/data.d.ts +6 -2
  5. package/dist/types/core/i18n-ui.d.ts +50 -0
  6. package/dist/types/core/schema.d.ts +379 -485
  7. package/dist/types/core/types.d.ts +8 -6
  8. package/docs/advanced/meta.ts +1 -8
  9. package/docs/advanced/skills.mdx +28 -0
  10. package/docs/configuration/ai.mdx +58 -0
  11. package/docs/configuration/index.mdx +13 -17
  12. package/docs/configuration/seo.mdx +59 -1
  13. package/docs/configuration/theming.mdx +15 -18
  14. package/docs/content/components.mdx +2 -53
  15. package/docs/content/i18n.mdx +0 -4
  16. package/docs/content/meta.mdx +3 -17
  17. package/docs/content/navigation.mdx +41 -4
  18. package/docs/content/syntax.mdx +1 -1
  19. package/docs/index.mdx +0 -1
  20. package/docs/reference/cli.mdx +12 -13
  21. package/package.json +6 -6
  22. package/skills/blume/SKILL.md +71 -0
  23. package/skills/blume-update-docs/SKILL.md +52 -0
  24. package/skills/blume-update-docs/references/audit-checklist.md +46 -0
  25. package/src/ai/agent-readability.ts +97 -0
  26. package/src/ai/ask-context.ts +131 -8
  27. package/src/ai/ask-data.ts +4 -1
  28. package/src/astro/generate.ts +19 -12
  29. package/src/astro/integration.ts +0 -21
  30. package/src/astro/templates.ts +33 -21
  31. package/src/cli/commands/build.ts +15 -0
  32. package/src/cli/commands/dev.ts +31 -20
  33. package/src/cli/commands/validate.ts +0 -2
  34. package/src/cli/dev-lock.ts +94 -21
  35. package/src/cli/index.ts +0 -2
  36. package/src/components/BlumePage.astro +0 -6
  37. package/src/components/Icon.astro +1 -12
  38. package/src/components/content/AccordionItem.astro +3 -6
  39. package/src/components/content/Badge.astro +1 -3
  40. package/src/components/content/Callout.astro +3 -9
  41. package/src/components/content/Card.astro +2 -3
  42. package/src/components/content/ColorItem.astro +2 -2
  43. package/src/components/content/Column.astro +1 -1
  44. package/src/components/content/GithubInfo.astro +11 -10
  45. package/src/components/content/Prompt.astro +1 -1
  46. package/src/components/content/Step.astro +3 -4
  47. package/src/components/content/Tab.astro +2 -3
  48. package/src/components/content/TypeTable.astro +13 -8
  49. package/src/components/content/Update.astro +1 -1
  50. package/src/components/islands/AskAI.astro +66 -2
  51. package/src/components/islands/ask-ai.tsx +289 -53
  52. package/src/components/layout/Header.astro +27 -4
  53. package/src/components/layout/Logo.astro +5 -1
  54. package/src/components/layout/NavSelector.astro +1 -1
  55. package/src/components/layout/NavTree.astro +15 -15
  56. package/src/components/layout/PageActions.astro +73 -30
  57. package/src/components/layout/PageLayout.astro +42 -0
  58. package/src/components/layout/ReferenceLayout.astro +1 -0
  59. package/src/components/layout/RootLayout.astro +79 -4
  60. package/src/components/layout/Search.astro +5 -5
  61. package/src/components/layout/nav-utils.ts +9 -4
  62. package/src/components/openapi/ApiOverview.astro +4 -50
  63. package/src/components/openapi/ApiTagOperations.astro +42 -0
  64. package/src/core/builtin-tags.ts +1 -3
  65. package/src/core/config.ts +5 -28
  66. package/src/core/data.ts +6 -2
  67. package/src/core/graph.ts +8 -6
  68. package/src/core/i18n-ui.ts +5 -0
  69. package/src/core/links.ts +5 -19
  70. package/src/core/meta.ts +1 -1
  71. package/src/core/nav-diagnostics.ts +7 -0
  72. package/src/core/navigation.ts +38 -17
  73. package/src/core/project-graph.ts +0 -5
  74. package/src/core/schema.ts +133 -95
  75. package/src/core/sources/filesystem.ts +5 -1
  76. package/src/core/sources/resolve.ts +0 -13
  77. package/src/core/sources/watch.ts +43 -11
  78. package/src/core/types.ts +8 -6
  79. package/src/deploy/robots.ts +37 -4
  80. package/src/openapi/parse.ts +197 -14
  81. package/src/openapi/render-mdx.ts +44 -10
  82. package/src/openapi/scalar.ts +1 -1
  83. package/src/openapi/source.ts +19 -2
  84. package/src/search/documents.ts +9 -2
  85. package/src/theme/entry.ts +45 -17
  86. package/src/theme/icons.ts +18 -109
  87. package/src/theme/palette.ts +25 -51
  88. package/src/theme/twoslash.ts +6 -1
  89. package/dist/types/core/bridge.d.ts +0 -24
  90. package/dist/types/core/package-json.d.ts +0 -12
  91. package/dist/types/migrate/mintlify/assets.d.ts +0 -8
  92. package/dist/types/migrate/mintlify/config.d.ts +0 -16
  93. package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
  94. package/dist/types/migrate/shared.d.ts +0 -153
  95. package/docs/advanced/bridge.mdx +0 -76
  96. package/docs/advanced/migrate.mdx +0 -124
  97. package/src/astro/static-assets.ts +0 -124
  98. package/src/cli/commands/migrate.ts +0 -39
  99. package/src/components/content/ApiField.astro +0 -75
  100. package/src/components/content/ParamField.astro +0 -39
  101. package/src/components/content/RequestField.astro +0 -23
  102. package/src/components/content/ResponseField.astro +0 -23
  103. package/src/components/content/Warning.astro +0 -9
  104. package/src/core/assets.ts +0 -31
  105. package/src/core/bridge.ts +0 -102
  106. package/src/core/sources/mintlify.ts +0 -190
  107. package/src/migrate/fumadocs/config.ts +0 -155
  108. package/src/migrate/fumadocs/content.ts +0 -376
  109. package/src/migrate/fumadocs/frontmatter.ts +0 -18
  110. package/src/migrate/fumadocs/groups.ts +0 -237
  111. package/src/migrate/fumadocs/index.ts +0 -355
  112. package/src/migrate/fumadocs/meta.ts +0 -244
  113. package/src/migrate/migrate.ts +0 -53
  114. package/src/migrate/mintlify/assets.ts +0 -46
  115. package/src/migrate/mintlify/config.ts +0 -954
  116. package/src/migrate/mintlify/content.ts +0 -120
  117. package/src/migrate/mintlify/frontmatter.ts +0 -126
  118. package/src/migrate/mintlify/i18n.ts +0 -51
  119. package/src/migrate/mintlify/icons.ts +0 -128
  120. package/src/migrate/mintlify/index.ts +0 -459
  121. package/src/migrate/mintlify/snippets.ts +0 -315
  122. package/src/migrate/mintlify/transform.ts +0 -82
  123. package/src/migrate/nextra/content.ts +0 -46
  124. package/src/migrate/nextra/frontmatter.ts +0 -40
  125. package/src/migrate/nextra/index.ts +0 -389
  126. package/src/migrate/nextra/meta.ts +0 -266
  127. package/src/migrate/shared.ts +0 -801
  128. package/src/migrate/starlight/config.ts +0 -455
  129. package/src/migrate/starlight/content.ts +0 -75
  130. package/src/migrate/starlight/frontmatter.ts +0 -111
  131. package/src/migrate/starlight/i18n.ts +0 -54
  132. package/src/migrate/starlight/index.ts +0 -131
@@ -1,315 +0,0 @@
1
- import { readFile as readFileFromDisk } from "node:fs/promises";
2
-
3
- import { dirname, relative, resolve } from "pathe";
4
-
5
- import matter from "../../core/frontmatter.ts";
6
- import { isInsideRoot, stripImports } from "../shared.ts";
7
-
8
- const MARKDOWN_SNIPPET_IMPORT =
9
- /^import\s+(?<name>[$A-Z_a-z][$\w]*)\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
10
- const NAMED_SNIPPET_IMPORT =
11
- /^import\s+\{(?<names>[^}]+)\}\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
12
- const EXPORTED_STRING_CONST =
13
- /^export\s+const\s+(?<name>[$A-Z_a-z][$\w]*)\s*=\s*(?:"(?<double>(?:\\.|[^"\\])*)"|'(?<single>(?:\\.|[^'\\])*)'|`(?<template>(?:\\.|[^`\\])*)`)\s*;?\s*$/gmu;
14
- const ATTRIBUTE =
15
- /\s+(?<name>[$A-Z_a-z][$\w:-]*)(?:=(?:"(?<quoted>[^"]*)"|'(?<single>[^']*)'|\{(?<expression>[^}]*)\}))?/gu;
16
- const PLACEHOLDER = /\{(?<name>[$A-Z_a-z][$\w]*)\}/gu;
17
- const GLOBAL_VARIABLE = /\{\{\s*(?<name>[A-Za-z0-9-]+)\s*\}\}/gu;
18
- const FRONTMATTER_BLOCK = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/u;
19
- const USER_EXPORT =
20
- /^(?:export\s+)?(?:const|let|var)\s+user\s*=|^import\s+\{\s*user\s*\}/mu;
21
-
22
- interface SnippetImport {
23
- importText: string;
24
- name: string;
25
- source: string;
26
- }
27
-
28
- interface SnippetVariableImport {
29
- names: { imported: string; local: string }[];
30
- source: string;
31
- }
32
-
33
- interface SnippetTransformOptions {
34
- filePath: string;
35
- root: string;
36
- readFile?: (file: string) => Promise<string>;
37
- seen?: Set<string>;
38
- trail?: string[];
39
- }
40
-
41
- const escapeRegExp = (value: string): string =>
42
- value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
43
-
44
- const snippetSelfClosingTagPattern = (name: string): RegExp =>
45
- new RegExp(`<${escapeRegExp(name)}(?<attrs>[^>]*?)\\s*/>`, "gu");
46
-
47
- const snippetPairedTagPattern = (name: string): RegExp =>
48
- new RegExp(
49
- `<${escapeRegExp(name)}(?<attrs>[^>]*?)>[\\s\\S]*?</${escapeRegExp(name)}>`,
50
- "gu"
51
- );
52
-
53
- const rootRelativePath = (root: string, file: string): string => {
54
- const rel = relative(root, file);
55
- return rel ? `/${rel}` : "/";
56
- };
57
-
58
- const snippetCycleMessage = (
59
- root: string,
60
- file: string,
61
- trail: string[]
62
- ): string => {
63
- const cycleStart = trail.indexOf(file);
64
- const cycle = [...(cycleStart === -1 ? trail : trail.slice(cycleStart)), file]
65
- .map((entry) => rootRelativePath(root, entry))
66
- .join(" -> ");
67
- return `Circular Mintlify snippet import detected: ${cycle}`;
68
- };
69
-
70
- const resolveSnippetPath = (options: {
71
- filePath: string;
72
- root: string;
73
- source: string;
74
- }): string | null => {
75
- const target = options.source.startsWith("/")
76
- ? resolve(options.root, options.source.slice(1))
77
- : resolve(dirname(options.filePath), options.source);
78
- return isInsideRoot(options.root, target) ? target : null;
79
- };
80
-
81
- const collectImports = (source: string): SnippetImport[] =>
82
- [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
83
- const name = match.groups?.name;
84
- const importSource = match.groups?.source;
85
- if (!(name && importSource)) {
86
- return [];
87
- }
88
- return [{ importText: match[0], name, source: importSource }];
89
- });
90
-
91
- const collectVariableImports = (source: string): SnippetVariableImport[] =>
92
- [...source.matchAll(NAMED_SNIPPET_IMPORT)].flatMap((match) => {
93
- const names = match.groups?.names;
94
- const importSource = match.groups?.source;
95
- if (!(names && importSource)) {
96
- return [];
97
- }
98
- const parsedNames = names.split(",").flatMap((entry) => {
99
- const [imported, local] = entry.trim().split(/\s+as\s+/u);
100
- return imported ? [{ imported, local: local ?? imported }] : [];
101
- });
102
- return parsedNames.length
103
- ? [{ names: parsedNames, source: importSource }]
104
- : [];
105
- });
106
-
107
- const decodeStringLiteral = (value: string): string =>
108
- value
109
- .replaceAll("\\n", "\n")
110
- .replaceAll("\\r", "\r")
111
- .replaceAll("\\t", "\t")
112
- .replaceAll(/\\(?<escaped>[\\'"`])/gu, "$<escaped>");
113
-
114
- const collectStringExports = (source: string): Map<string, string> => {
115
- const exports = new Map<string, string>();
116
- for (const match of source.matchAll(EXPORTED_STRING_CONST)) {
117
- const name = match.groups?.name;
118
- const value =
119
- match.groups?.double ?? match.groups?.single ?? match.groups?.template;
120
- if (name && value !== undefined) {
121
- exports.set(name, decodeStringLiteral(value));
122
- }
123
- }
124
- return exports;
125
- };
126
-
127
- const parseAttributes = (source: string): Record<string, string> => {
128
- const props: Record<string, string> = {};
129
- for (const match of source.matchAll(ATTRIBUTE)) {
130
- const name = match.groups?.name;
131
- if (!name) {
132
- continue;
133
- }
134
- props[name] =
135
- match.groups?.quoted ??
136
- match.groups?.single ??
137
- match.groups?.expression?.trim() ??
138
- "true";
139
- }
140
- return props;
141
- };
142
-
143
- const interpolateProps = (
144
- source: string,
145
- props: Record<string, string>
146
- ): string =>
147
- source.replaceAll(PLACEHOLDER, (value, name: string) => props[name] ?? value);
148
-
149
- const stripImport = (source: string, importText: string): string =>
150
- // Seam-targeted removal: a whole-document `\n{3,}` collapse would rewrite
151
- // real double blank lines inside the page's code fences.
152
- stripImports(source, new RegExp(escapeRegExp(importText), "gu"));
153
-
154
- const replacePlaceholder = (
155
- source: string,
156
- name: string,
157
- value: string
158
- ): string =>
159
- source.replaceAll(new RegExp(`\\{${escapeRegExp(name)}\\}`, "gu"), value);
160
-
161
- const inlineSnippetTags = (options: {
162
- name: string;
163
- snippet: string;
164
- source: string;
165
- }): string => {
166
- const inline = (_value: string, attrs: string): string =>
167
- interpolateProps(options.snippet, parseAttributes(attrs));
168
- return options.source
169
- .replaceAll(snippetSelfClosingTagPattern(options.name), inline)
170
- .replaceAll(snippetPairedTagPattern(options.name), inline);
171
- };
172
-
173
- /**
174
- * Mintlify markdown snippets are source-level includes, not standalone pages.
175
- * Inline markdown snippets before Astro MDX compiles so `.md` imports, nested
176
- * markdown snippets, and `{prop}` interpolation inside prose or code fences work.
177
- */
178
- export const rewriteMintlifyMarkdownSnippets = async (
179
- source: string,
180
- options: SnippetTransformOptions
181
- ): Promise<string> => {
182
- const loadSnippet = async (file: string): Promise<string> => {
183
- const seen = options.seen ?? new Set<string>();
184
- if (seen.has(file)) {
185
- throw new Error(
186
- snippetCycleMessage(options.root, file, options.trail ?? [])
187
- );
188
- }
189
- seen.add(file);
190
- const readFile =
191
- options.readFile ??
192
- ((path: string): Promise<string> => readFileFromDisk(path, "utf-8"));
193
- try {
194
- let raw: string;
195
- try {
196
- raw = await readFile(file);
197
- } catch {
198
- // A dangling import must fail with a message that names both ends —
199
- // the caller downgrades it to a per-page warning and moves on.
200
- throw new Error(
201
- `snippet ${rootRelativePath(options.root, file)} (imported by ${rootRelativePath(options.root, options.filePath)}) does not exist`
202
- );
203
- }
204
- const content = matter(raw).content.trim();
205
- const transformed = await rewriteMintlifyMarkdownSnippets(content, {
206
- ...options,
207
- filePath: file,
208
- seen,
209
- trail: [...(options.trail ?? []), file],
210
- });
211
- return transformed.trim();
212
- } finally {
213
- seen.delete(file);
214
- }
215
- };
216
-
217
- const inlineImport = async (
218
- current: string,
219
- snippetImport: SnippetImport
220
- ): Promise<string> => {
221
- const file = resolveSnippetPath({
222
- filePath: options.filePath,
223
- root: options.root,
224
- source: snippetImport.source,
225
- });
226
- if (!file) {
227
- return current;
228
- }
229
-
230
- const snippet = await loadSnippet(file);
231
- const next = inlineSnippetTags({
232
- name: snippetImport.name,
233
- snippet,
234
- source: current,
235
- });
236
- return next === current
237
- ? current
238
- : stripImport(next, snippetImport.importText);
239
- };
240
-
241
- const imports = collectImports(source);
242
- const inlineAt = async (index: number, current: string): Promise<string> => {
243
- const snippetImport = imports[index];
244
- if (!snippetImport) {
245
- return current;
246
- }
247
- return inlineAt(index + 1, await inlineImport(current, snippetImport));
248
- };
249
-
250
- return await inlineAt(0, source);
251
- };
252
-
253
- /** Resolve imported string constants from Mintlify snippets for generated text. */
254
- export const rewriteMintlifySnippetVariables = async (
255
- source: string,
256
- options: SnippetTransformOptions
257
- ): Promise<string> => {
258
- const readFile = options.readFile ?? readFileFromDisk;
259
- const inlineImport = async (
260
- current: string,
261
- variableImport: SnippetVariableImport
262
- ): Promise<string> => {
263
- const file = resolveSnippetPath({
264
- filePath: options.filePath,
265
- root: options.root,
266
- source: variableImport.source,
267
- });
268
- if (!file) {
269
- return current;
270
- }
271
-
272
- const exports = collectStringExports(matter(await readFile(file)).content);
273
- let next = current;
274
- for (const name of variableImport.names) {
275
- const value = exports.get(name.imported);
276
- if (value !== undefined) {
277
- next = replacePlaceholder(next, name.local, value);
278
- }
279
- }
280
- return next;
281
- };
282
-
283
- const imports = collectVariableImports(source);
284
- const inlineAt = async (index: number, current: string): Promise<string> => {
285
- const variableImport = imports[index];
286
- if (!variableImport) {
287
- return current;
288
- }
289
- return inlineAt(index + 1, await inlineImport(current, variableImport));
290
- };
291
-
292
- return await inlineAt(0, source);
293
- };
294
-
295
- /** Replace docs.json Mintlify globals such as `{{product-name}}`. */
296
- export const rewriteMintlifyGlobalVariables = (
297
- source: string,
298
- variables: Record<string, string>
299
- ): string =>
300
- source.replaceAll(GLOBAL_VARIABLE, (value, name: string) =>
301
- Object.hasOwn(variables, name) ? (variables[name] ?? value) : value
302
- );
303
-
304
- /** Mintlify exposes a logged-out `user` object to MDX personalization code. */
305
- export const rewriteMintlifyUserVariable = (source: string): string => {
306
- if (USER_EXPORT.test(source)) {
307
- return source;
308
- }
309
-
310
- const frontmatter = source.match(FRONTMATTER_BLOCK)?.[0] ?? "";
311
- const body = (
312
- frontmatter ? source.slice(frontmatter.length) : source
313
- ).replace(/^\r?\n/u, "");
314
- return `${frontmatter}export const user = {};\n\n${body}`;
315
- };
@@ -1,82 +0,0 @@
1
- import matter from "../../core/frontmatter.ts";
2
- import { stripUnknownPageMeta } from "../shared.ts";
3
- import {
4
- rewriteMintlifyAccordions,
5
- rewriteMintlifyCallouts,
6
- rewriteMintlifyExampleBlocks,
7
- rewriteSnippetImports,
8
- unsupportedMintlifyComponents,
9
- } from "./content.ts";
10
- import { normalizeMintlifyPageMeta } from "./frontmatter.ts";
11
- import { rewriteMintlifySvgIconProps } from "./icons.ts";
12
- import {
13
- rewriteMintlifyGlobalVariables,
14
- rewriteMintlifyMarkdownSnippets,
15
- rewriteMintlifySnippetVariables,
16
- rewriteMintlifyUserVariable,
17
- } from "./snippets.ts";
18
-
19
- const USER_REFERENCE = /\{[^{}]*\buser\b/u;
20
-
21
- /** The outcome of transforming a single Mintlify page to Blume MDX. */
22
- export interface MintlifyTransformResult {
23
- /** Component snippet imports kept (rewritten to relative paths). */
24
- components: string[];
25
- /** The rewritten file text, including normalized frontmatter. */
26
- content: string;
27
- /** Frontmatter keys dropped because Blume has no equivalent. */
28
- removed: string[];
29
- /** Components encountered that have no Blume equivalent. */
30
- unsupported: string[];
31
- }
32
-
33
- /**
34
- * Apply the per-file source transforms that turn Mintlify MDX into Blume MDX:
35
- * inline snippets and variables, rewrite callouts (`<Note>` → `:::note`),
36
- * example blocks, and inline-SVG icon props, then normalize frontmatter. Pure
37
- * text-in/text-out (snippets read from disk relative to `root`), so both the
38
- * one-shot migrator and the live bridge source share identical behavior.
39
- */
40
- export const transformMintlifyContent = async (
41
- raw: string,
42
- options: { filePath: string; root: string; variables: Record<string, string> }
43
- ): Promise<MintlifyTransformResult> => {
44
- let text = await rewriteMintlifyMarkdownSnippets(raw, {
45
- filePath: options.filePath,
46
- root: options.root,
47
- });
48
- text = await rewriteMintlifySnippetVariables(text, {
49
- filePath: options.filePath,
50
- root: options.root,
51
- });
52
- text = rewriteMintlifyGlobalVariables(text, options.variables);
53
- if (USER_REFERENCE.test(text)) {
54
- text = rewriteMintlifyUserVariable(text);
55
- }
56
- const snippetImports = rewriteSnippetImports(text, {
57
- filePath: options.filePath,
58
- root: options.root,
59
- });
60
- text = snippetImports.source;
61
- text = rewriteMintlifySvgIconProps(text);
62
- text = rewriteMintlifyExampleBlocks(text);
63
- text = rewriteMintlifyAccordions(text);
64
- text = rewriteMintlifyCallouts(text);
65
-
66
- const unsupported = unsupportedMintlifyComponents(text);
67
-
68
- const parsed = matter(text);
69
- const mapped = normalizeMintlifyPageMeta(parsed.data);
70
- const { data, removed } = stripUnknownPageMeta(mapped);
71
- const content =
72
- Object.keys(data).length > 0
73
- ? matter.stringify(parsed.content, data)
74
- : parsed.content;
75
-
76
- return {
77
- components: snippetImports.components,
78
- content,
79
- removed,
80
- unsupported,
81
- };
82
- };
@@ -1,46 +0,0 @@
1
- import { rewriteCallouts } from "../shared.ts";
2
-
3
- /**
4
- * Source-to-source rewrites that turn Nextra-only MDX into idiomatic Blume
5
- * markup. Runs once at migration time.
6
- */
7
-
8
- /** Nextra `<Callout type="X">` values mapped to Blume directive names. */
9
- const CALLOUT_TYPE_DIRECTIVES: Record<string, string> = {
10
- default: "note",
11
- error: "danger",
12
- info: "info",
13
- warning: "warning",
14
- };
15
-
16
- /**
17
- * Convert Nextra `<Callout>` components (with an optional `type`) into Blume
18
- * `:::` directives. A bare `<Callout>` becomes `:::note`; the `emoji` prop is
19
- * dropped.
20
- */
21
- export const rewriteNextraCallouts = (source: string): string =>
22
- rewriteCallouts(source, {
23
- defaultDirective: "note",
24
- tagDirectives: {},
25
- tags: ["Callout"],
26
- typeDirectives: CALLOUT_TYPE_DIRECTIVES,
27
- });
28
-
29
- const NEXTRA_IMPORT =
30
- /^import\s+[\s\S]*?\s+from\s+["']nextra(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
31
-
32
- /**
33
- * Drop `import … from "nextra/…"` statements. Blume injects its components
34
- * globally, so these imports would fail to resolve once `nextra` is gone.
35
- */
36
- export const stripNextraImports = (source: string): string =>
37
- source.replace(NEXTRA_IMPORT, "");
38
-
39
- /** Nextra components with no drop-in Blume equivalent — flagged for review. */
40
- const UNSUPPORTED_COMPONENTS = ["Bleed", "Cards", "FileTree", "Steps", "Tabs"];
41
-
42
- /** Names of Nextra components in `source` that need manual attention. */
43
- export const unsupportedNextraComponents = (source: string): string[] =>
44
- UNSUPPORTED_COMPONENTS.filter((name) =>
45
- new RegExp(`<${name}\\b`, "u").test(source)
46
- );
@@ -1,40 +0,0 @@
1
- import { stripUnknownPageMeta } from "../shared.ts";
2
-
3
- export interface NextraPageOverride {
4
- hidden?: boolean;
5
- label?: string;
6
- }
7
-
8
- /**
9
- * Fold Nextra `_meta` overrides (a sidebar label, `display: "hidden"`) into a
10
- * page's frontmatter, then drop any keys Blume's strict page schema rejects.
11
- * Nextra's own `title`/`description`/`sidebarTitle` already validate and pass
12
- * through untouched. Explicit page frontmatter wins over `_meta` overrides.
13
- */
14
- export const normalizeNextraPageMeta = (
15
- value: unknown,
16
- override: NextraPageOverride = {}
17
- ): { data: Record<string, unknown>; removed: string[] } => {
18
- const source =
19
- value && typeof value === "object"
20
- ? (value as Record<string, unknown>)
21
- : {};
22
- const data: Record<string, unknown> = { ...source };
23
-
24
- const sidebar: Record<string, unknown> = {
25
- ...(typeof source.sidebar === "object" && source.sidebar !== null
26
- ? (source.sidebar as Record<string, unknown>)
27
- : {}),
28
- };
29
- if (override.label !== undefined && sidebar.label === undefined) {
30
- sidebar.label = override.label;
31
- }
32
- if (override.hidden && sidebar.hidden === undefined) {
33
- sidebar.hidden = true;
34
- }
35
- if (Object.keys(sidebar).length > 0) {
36
- data.sidebar = sidebar;
37
- }
38
-
39
- return stripUnknownPageMeta(data);
40
- };