blume 0.0.0 → 0.1.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 (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,305 @@
1
+ import { readFile as readFileFromDisk } from "node:fs/promises";
2
+
3
+ import matter from "gray-matter";
4
+ import { dirname, relative, resolve } from "pathe";
5
+
6
+ const MARKDOWN_SNIPPET_IMPORT =
7
+ /^import\s+(?<name>[$A-Z_a-z][$\w]*)\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
8
+ const NAMED_SNIPPET_IMPORT =
9
+ /^import\s+\{(?<names>[^}]+)\}\s+from\s+["'](?<source>[^"']+\.mdx?)["'];?\s*$/gmu;
10
+ const EXPORTED_STRING_CONST =
11
+ /^export\s+const\s+(?<name>[$A-Z_a-z][$\w]*)\s*=\s*(?:"(?<double>(?:\\.|[^"\\])*)"|'(?<single>(?:\\.|[^'\\])*)'|`(?<template>(?:\\.|[^`\\])*)`)\s*;?\s*$/gmu;
12
+ const ATTRIBUTE =
13
+ /\s+(?<name>[$A-Z_a-z][$\w:-]*)(?:=(?:"(?<quoted>[^"]*)"|'(?<single>[^']*)'|\{(?<expression>[^}]*)\}))?/gu;
14
+ const PLACEHOLDER = /\{(?<name>[$A-Z_a-z][$\w]*)\}/gu;
15
+ const GLOBAL_VARIABLE = /\{\{\s*(?<name>[A-Za-z0-9-]+)\s*\}\}/gu;
16
+ const FRONTMATTER_BLOCK = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/u;
17
+ const USER_EXPORT =
18
+ /^(?:export\s+)?(?:const|let|var)\s+user\s*=|^import\s+\{\s*user\s*\}/mu;
19
+
20
+ interface SnippetImport {
21
+ importText: string;
22
+ name: string;
23
+ source: string;
24
+ }
25
+
26
+ interface SnippetVariableImport {
27
+ names: { imported: string; local: string }[];
28
+ source: string;
29
+ }
30
+
31
+ interface SnippetTransformOptions {
32
+ filePath: string;
33
+ root: string;
34
+ readFile?: (file: string) => Promise<string>;
35
+ seen?: Set<string>;
36
+ trail?: string[];
37
+ }
38
+
39
+ const isInsideRoot = (root: string, candidate: string): boolean => {
40
+ const rel = relative(root, candidate);
41
+ return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/"));
42
+ };
43
+
44
+ const escapeRegExp = (value: string): string =>
45
+ value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
46
+
47
+ const snippetSelfClosingTagPattern = (name: string): RegExp =>
48
+ new RegExp(`<${escapeRegExp(name)}(?<attrs>[^>]*?)\\s*/>`, "gu");
49
+
50
+ const snippetPairedTagPattern = (name: string): RegExp =>
51
+ new RegExp(
52
+ `<${escapeRegExp(name)}(?<attrs>[^>]*?)>[\\s\\S]*?</${escapeRegExp(name)}>`,
53
+ "gu"
54
+ );
55
+
56
+ const rootRelativePath = (root: string, file: string): string => {
57
+ const rel = relative(root, file);
58
+ return rel ? `/${rel}` : "/";
59
+ };
60
+
61
+ const snippetCycleMessage = (
62
+ root: string,
63
+ file: string,
64
+ trail: string[]
65
+ ): string => {
66
+ const cycleStart = trail.indexOf(file);
67
+ const cycle = [...(cycleStart === -1 ? trail : trail.slice(cycleStart)), file]
68
+ .map((entry) => rootRelativePath(root, entry))
69
+ .join(" -> ");
70
+ return `Circular Mintlify snippet import detected: ${cycle}`;
71
+ };
72
+
73
+ const resolveSnippetPath = (options: {
74
+ filePath: string;
75
+ root: string;
76
+ source: string;
77
+ }): string | null => {
78
+ const target = options.source.startsWith("/")
79
+ ? resolve(options.root, options.source.slice(1))
80
+ : resolve(dirname(options.filePath), options.source);
81
+ return isInsideRoot(options.root, target) ? target : null;
82
+ };
83
+
84
+ const collectImports = (source: string): SnippetImport[] =>
85
+ [...source.matchAll(MARKDOWN_SNIPPET_IMPORT)].flatMap((match) => {
86
+ const name = match.groups?.name;
87
+ const importSource = match.groups?.source;
88
+ if (!(name && importSource)) {
89
+ return [];
90
+ }
91
+ return [{ importText: match[0], name, source: importSource }];
92
+ });
93
+
94
+ const collectVariableImports = (source: string): SnippetVariableImport[] =>
95
+ [...source.matchAll(NAMED_SNIPPET_IMPORT)].flatMap((match) => {
96
+ const names = match.groups?.names;
97
+ const importSource = match.groups?.source;
98
+ if (!(names && importSource)) {
99
+ return [];
100
+ }
101
+ const parsedNames = names.split(",").flatMap((entry) => {
102
+ const [imported, local] = entry.trim().split(/\s+as\s+/u);
103
+ return imported ? [{ imported, local: local ?? imported }] : [];
104
+ });
105
+ return parsedNames.length
106
+ ? [{ names: parsedNames, source: importSource }]
107
+ : [];
108
+ });
109
+
110
+ const decodeStringLiteral = (value: string): string =>
111
+ value
112
+ .replaceAll("\\n", "\n")
113
+ .replaceAll("\\r", "\r")
114
+ .replaceAll("\\t", "\t")
115
+ .replaceAll(/\\(?<escaped>[\\'"`])/gu, "$<escaped>");
116
+
117
+ const collectStringExports = (source: string): Map<string, string> => {
118
+ const exports = new Map<string, string>();
119
+ for (const match of source.matchAll(EXPORTED_STRING_CONST)) {
120
+ const name = match.groups?.name;
121
+ const value =
122
+ match.groups?.double ?? match.groups?.single ?? match.groups?.template;
123
+ if (name && value !== undefined) {
124
+ exports.set(name, decodeStringLiteral(value));
125
+ }
126
+ }
127
+ return exports;
128
+ };
129
+
130
+ const parseAttributes = (source: string): Record<string, string> => {
131
+ const props: Record<string, string> = {};
132
+ for (const match of source.matchAll(ATTRIBUTE)) {
133
+ const name = match.groups?.name;
134
+ if (!name) {
135
+ continue;
136
+ }
137
+ props[name] =
138
+ match.groups?.quoted ??
139
+ match.groups?.single ??
140
+ match.groups?.expression?.trim() ??
141
+ "true";
142
+ }
143
+ return props;
144
+ };
145
+
146
+ const interpolateProps = (
147
+ source: string,
148
+ props: Record<string, string>
149
+ ): string =>
150
+ source.replaceAll(PLACEHOLDER, (value, name: string) => props[name] ?? value);
151
+
152
+ const stripImport = (source: string, importText: string): string =>
153
+ source.replace(importText, "").replaceAll(/\n{3,}/gu, "\n\n");
154
+
155
+ const replacePlaceholder = (
156
+ source: string,
157
+ name: string,
158
+ value: string
159
+ ): string =>
160
+ source.replaceAll(new RegExp(`\\{${escapeRegExp(name)}\\}`, "gu"), value);
161
+
162
+ const inlineSnippetTags = (options: {
163
+ name: string;
164
+ snippet: string;
165
+ source: string;
166
+ }): string => {
167
+ const inline = (_value: string, attrs: string): string =>
168
+ interpolateProps(options.snippet, parseAttributes(attrs));
169
+ return options.source
170
+ .replaceAll(snippetSelfClosingTagPattern(options.name), inline)
171
+ .replaceAll(snippetPairedTagPattern(options.name), inline);
172
+ };
173
+
174
+ /**
175
+ * Mintlify markdown snippets are source-level includes, not standalone pages.
176
+ * Inline markdown snippets before Astro MDX compiles so `.md` imports, nested
177
+ * markdown snippets, and `{prop}` interpolation inside prose or code fences work.
178
+ */
179
+ export const rewriteMintlifyMarkdownSnippets = async (
180
+ source: string,
181
+ options: SnippetTransformOptions
182
+ ): Promise<string> => {
183
+ const loadSnippet = async (file: string): Promise<string> => {
184
+ const seen = options.seen ?? new Set<string>();
185
+ if (seen.has(file)) {
186
+ throw new Error(
187
+ snippetCycleMessage(options.root, file, options.trail ?? [])
188
+ );
189
+ }
190
+ seen.add(file);
191
+ const readFile = options.readFile ?? readFileFromDisk;
192
+ try {
193
+ const raw = await readFile(file);
194
+ const content = matter(raw).content.trim();
195
+ const transformed = await rewriteMintlifyMarkdownSnippets(content, {
196
+ ...options,
197
+ filePath: file,
198
+ seen,
199
+ trail: [...(options.trail ?? []), file],
200
+ });
201
+ return transformed.trim();
202
+ } finally {
203
+ seen.delete(file);
204
+ }
205
+ };
206
+
207
+ const inlineImport = async (
208
+ current: string,
209
+ snippetImport: SnippetImport
210
+ ): Promise<string> => {
211
+ const file = resolveSnippetPath({
212
+ filePath: options.filePath,
213
+ root: options.root,
214
+ source: snippetImport.source,
215
+ });
216
+ if (!file) {
217
+ return current;
218
+ }
219
+
220
+ const snippet = await loadSnippet(file);
221
+ const next = inlineSnippetTags({
222
+ name: snippetImport.name,
223
+ snippet,
224
+ source: current,
225
+ });
226
+ return next === current
227
+ ? current
228
+ : stripImport(next, snippetImport.importText);
229
+ };
230
+
231
+ const imports = collectImports(source);
232
+ const inlineAt = async (index: number, current: string): Promise<string> => {
233
+ const snippetImport = imports[index];
234
+ if (!snippetImport) {
235
+ return current;
236
+ }
237
+ return inlineAt(index + 1, await inlineImport(current, snippetImport));
238
+ };
239
+
240
+ return await inlineAt(0, source);
241
+ };
242
+
243
+ /** Resolve imported string constants from Mintlify snippets for generated text. */
244
+ export const rewriteMintlifySnippetVariables = async (
245
+ source: string,
246
+ options: SnippetTransformOptions
247
+ ): Promise<string> => {
248
+ const readFile = options.readFile ?? readFileFromDisk;
249
+ const inlineImport = async (
250
+ current: string,
251
+ variableImport: SnippetVariableImport
252
+ ): Promise<string> => {
253
+ const file = resolveSnippetPath({
254
+ filePath: options.filePath,
255
+ root: options.root,
256
+ source: variableImport.source,
257
+ });
258
+ if (!file) {
259
+ return current;
260
+ }
261
+
262
+ const exports = collectStringExports(matter(await readFile(file)).content);
263
+ let next = current;
264
+ for (const name of variableImport.names) {
265
+ const value = exports.get(name.imported);
266
+ if (value !== undefined) {
267
+ next = replacePlaceholder(next, name.local, value);
268
+ }
269
+ }
270
+ return next;
271
+ };
272
+
273
+ const imports = collectVariableImports(source);
274
+ const inlineAt = async (index: number, current: string): Promise<string> => {
275
+ const variableImport = imports[index];
276
+ if (!variableImport) {
277
+ return current;
278
+ }
279
+ return inlineAt(index + 1, await inlineImport(current, variableImport));
280
+ };
281
+
282
+ return await inlineAt(0, source);
283
+ };
284
+
285
+ /** Replace docs.json Mintlify globals such as `{{product-name}}`. */
286
+ export const rewriteMintlifyGlobalVariables = (
287
+ source: string,
288
+ variables: Record<string, string>
289
+ ): string =>
290
+ source.replaceAll(GLOBAL_VARIABLE, (value, name: string) =>
291
+ Object.hasOwn(variables, name) ? (variables[name] ?? value) : value
292
+ );
293
+
294
+ /** Mintlify exposes a logged-out `user` object to MDX personalization code. */
295
+ export const rewriteMintlifyUserVariable = (source: string): string => {
296
+ if (USER_EXPORT.test(source)) {
297
+ return source;
298
+ }
299
+
300
+ const frontmatter = source.match(FRONTMATTER_BLOCK)?.[0] ?? "";
301
+ const body = (
302
+ frontmatter ? source.slice(frontmatter.length) : source
303
+ ).replace(/^\r?\n/u, "");
304
+ return `${frontmatter}export const user = {};\n\n${body}`;
305
+ };
@@ -0,0 +1,81 @@
1
+ import matter from "gray-matter";
2
+
3
+ import { stripUnknownPageMeta } from "../shared.ts";
4
+ import {
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 = rewriteMintlifyCallouts(text);
64
+
65
+ const unsupported = unsupportedMintlifyComponents(text);
66
+
67
+ const parsed = matter(text);
68
+ const mapped = normalizeMintlifyPageMeta(parsed.data);
69
+ const { data, removed } = stripUnknownPageMeta(mapped);
70
+ const content =
71
+ Object.keys(data).length > 0
72
+ ? matter.stringify(parsed.content, data)
73
+ : parsed.content;
74
+
75
+ return {
76
+ components: snippetImports.components,
77
+ content,
78
+ removed,
79
+ unsupported,
80
+ };
81
+ };
@@ -0,0 +1,46 @@
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
+ );
@@ -0,0 +1,40 @@
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
+ };