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,98 @@
1
+ import { dirname, join, relative } from "pathe";
2
+
3
+ import { rewriteCallouts } from "../shared.ts";
4
+
5
+ /**
6
+ * Source-to-source rewrites that turn Mintlify-only MDX component syntax into
7
+ * idiomatic Blume markup. Runs once at migration time — no Mintlify-aware
8
+ * plugins remain in the Blume runtime.
9
+ */
10
+
11
+ /** Mintlify callout components mapped to Blume directive names. */
12
+ const CALLOUT_DIRECTIVES: Record<string, string> = {
13
+ Check: "success",
14
+ Danger: "danger",
15
+ Error: "danger",
16
+ Info: "info",
17
+ Note: "note",
18
+ Success: "success",
19
+ Tip: "tip",
20
+ Warning: "warning",
21
+ };
22
+
23
+ /** `<Callout type="X">` values mapped to Blume directive names. */
24
+ const CALLOUT_TYPE_DIRECTIVES: Record<string, string> = {
25
+ caution: "warning",
26
+ check: "success",
27
+ danger: "danger",
28
+ error: "danger",
29
+ info: "info",
30
+ note: "note",
31
+ success: "success",
32
+ tip: "tip",
33
+ warning: "warning",
34
+ };
35
+
36
+ /**
37
+ * Convert Mintlify callout components (`<Note>`, `<Warning>`, `<Callout
38
+ * type="…">`, …) into Blume `:::` directives.
39
+ */
40
+ export const rewriteMintlifyCallouts = (source: string): string =>
41
+ rewriteCallouts(source, {
42
+ defaultDirective: "note",
43
+ tagDirectives: CALLOUT_DIRECTIVES,
44
+ tags: ["Callout", ...Object.keys(CALLOUT_DIRECTIVES)],
45
+ typeDirectives: CALLOUT_TYPE_DIRECTIVES,
46
+ });
47
+
48
+ /**
49
+ * Mintlify's `<RequestExample>`/`<ResponseExample>` are tab-style code wrappers
50
+ * with no Blume equivalent; rename them to `<CodeGroup>`, which renders the
51
+ * same titled-fence tabs.
52
+ */
53
+ export const rewriteMintlifyExampleBlocks = (source: string): string =>
54
+ source.replaceAll(
55
+ /<(?<close>\/?)(?:Request|Response)Example\b/gu,
56
+ "<$<close>CodeGroup"
57
+ );
58
+
59
+ const SNIPPET_IMPORT =
60
+ /^import\s+[\s\S]*?\s+from\s+["'](?<source>\/snippets\/[^"']+)["'];?[ \t]*\n?/gmu;
61
+
62
+ /**
63
+ * After snippets are inlined, clean up leftover `/snippets/*` imports: drop the
64
+ * now-dead markdown imports, and rewrite component imports (`.jsx`/`.tsx`/…) to
65
+ * a path relative to the page so they still resolve once `/snippets` content is
66
+ * gone. Returns the rewritten source and the component files still referenced.
67
+ */
68
+ export const rewriteSnippetImports = (
69
+ source: string,
70
+ options: { filePath: string; root: string }
71
+ ): { components: string[]; source: string } => {
72
+ const components: string[] = [];
73
+ const next = source.replaceAll(
74
+ SNIPPET_IMPORT,
75
+ (match, importSource: string) => {
76
+ if (/\.mdx?$/u.test(importSource)) {
77
+ return "";
78
+ }
79
+ const target = join(options.root, importSource.replace(/^\/+/u, ""));
80
+ components.push(importSource.replace(/^\/+/u, ""));
81
+ let rel = relative(dirname(options.filePath), target);
82
+ if (!rel.startsWith(".")) {
83
+ rel = `./${rel}`;
84
+ }
85
+ return match.replace(importSource, rel);
86
+ }
87
+ );
88
+ return { components, source: next };
89
+ };
90
+
91
+ /** Component tags Blume has no equivalent for — reported for manual review. */
92
+ const UNSUPPORTED_COMPONENTS = ["ParamField", "ResponseField"];
93
+
94
+ /** Names of Mintlify components in `source` that need manual attention. */
95
+ export const unsupportedMintlifyComponents = (source: string): string[] =>
96
+ UNSUPPORTED_COMPONENTS.filter((name) =>
97
+ new RegExp(`<${name}\\b`, "u").test(source)
98
+ );
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * One-time translation of Mintlify page frontmatter into Blume's shape. This
5
+ * runs at migration time only — Blume's runtime page schema stays
6
+ * Mintlify-free and strict. `sidebarTitle`/`icon`/`tag`/`hidden` fold into
7
+ * `sidebar`, `hidden` implies `noindex`, `canonical`/`og:image` move under
8
+ * `seo`, and OpenAPI/AsyncAPI pages become `type: "api"`.
9
+ */
10
+
11
+ const mintlifySidebarMetaSchema = z
12
+ .object({
13
+ badge: z.string().optional(),
14
+ hidden: z.boolean().optional(),
15
+ icon: z.string().optional(),
16
+ label: z.string().optional(),
17
+ order: z.number().optional(),
18
+ })
19
+ .passthrough();
20
+
21
+ const mintlifyPageMetaInputSchema = z
22
+ .object({
23
+ api: z.unknown().optional(),
24
+ asyncapi: z.string().optional(),
25
+ canonical: z.string().optional(),
26
+ hidden: z.boolean().optional(),
27
+ icon: z.string().optional(),
28
+ noindex: z.boolean().optional(),
29
+ "og:image": z.string().optional(),
30
+ openapi: z.string().optional(),
31
+ sidebar: mintlifySidebarMetaSchema.optional(),
32
+ sidebarTitle: z.string().optional(),
33
+ tag: z.string().optional(),
34
+ type: z.string().optional(),
35
+ })
36
+ .passthrough();
37
+
38
+ type MintlifyPageMetaInput = z.infer<typeof mintlifyPageMetaInputSchema>;
39
+
40
+ const normalizedMintlifySidebar = (meta: MintlifyPageMetaInput) => ({
41
+ ...meta.sidebar,
42
+ ...(meta.sidebarTitle !== undefined && meta.sidebar?.label === undefined
43
+ ? { label: meta.sidebarTitle }
44
+ : {}),
45
+ ...(meta.icon !== undefined && meta.sidebar?.icon === undefined
46
+ ? { icon: meta.icon }
47
+ : {}),
48
+ ...(meta.tag !== undefined && meta.sidebar?.badge === undefined
49
+ ? { badge: meta.tag }
50
+ : {}),
51
+ ...(meta.hidden === true && meta.sidebar?.hidden === undefined
52
+ ? { hidden: true }
53
+ : {}),
54
+ });
55
+
56
+ const mintlifyPageType = (meta: MintlifyPageMetaInput): string | undefined =>
57
+ meta.type ??
58
+ (meta.openapi !== undefined ||
59
+ meta.asyncapi !== undefined ||
60
+ meta.api !== undefined
61
+ ? "api"
62
+ : undefined);
63
+
64
+ const mintlifyNoindex = (meta: MintlifyPageMetaInput): boolean | undefined =>
65
+ meta.hidden === true && meta.noindex === undefined ? true : meta.noindex;
66
+
67
+ /** Top-level Mintlify keys folded elsewhere — removed from the output. */
68
+ const CONSUMED_KEYS = new Set([
69
+ "canonical",
70
+ "hidden",
71
+ "icon",
72
+ "og:image",
73
+ "sidebarTitle",
74
+ "tag",
75
+ ]);
76
+
77
+ /** Map Mintlify page frontmatter onto Blume's frontmatter shape. */
78
+ export const normalizeMintlifyPageMeta = (
79
+ value: unknown
80
+ ): Record<string, unknown> => {
81
+ const parsed = mintlifyPageMetaInputSchema.safeParse(value);
82
+ if (!parsed.success || typeof value !== "object" || value === null) {
83
+ return (value ?? {}) as Record<string, unknown>;
84
+ }
85
+
86
+ const meta = parsed.data;
87
+ const source = value as Record<string, unknown>;
88
+ const data: Record<string, unknown> = { ...source };
89
+
90
+ const sidebar = normalizedMintlifySidebar(meta);
91
+ if (Object.keys(sidebar).length > 0) {
92
+ data.sidebar = sidebar;
93
+ }
94
+
95
+ const seo: Record<string, unknown> = {
96
+ ...(typeof source.seo === "object" && source.seo !== null
97
+ ? (source.seo as Record<string, unknown>)
98
+ : {}),
99
+ };
100
+ if (meta.canonical !== undefined && seo.canonical === undefined) {
101
+ seo.canonical = meta.canonical;
102
+ }
103
+ if (meta["og:image"] !== undefined && seo.image === undefined) {
104
+ seo.image = meta["og:image"];
105
+ }
106
+ if (Object.keys(seo).length > 0) {
107
+ data.seo = seo;
108
+ }
109
+
110
+ const noindex = mintlifyNoindex(meta);
111
+ if (noindex !== undefined) {
112
+ data.noindex = noindex;
113
+ }
114
+ const type = mintlifyPageType(meta);
115
+ if (type !== undefined) {
116
+ data.type = type;
117
+ }
118
+
119
+ const cleaned: Record<string, unknown> = {};
120
+ for (const [key, raw] of Object.entries(data)) {
121
+ if (!CONSUMED_KEYS.has(key)) {
122
+ cleaned[key] = raw;
123
+ }
124
+ }
125
+ return cleaned;
126
+ };
@@ -0,0 +1,51 @@
1
+ import type { BlumeConfig } from "../../core/schema.ts";
2
+
3
+ /** A Mintlify `navigation.languages[]` entry. */
4
+ interface MintlifyLanguage {
5
+ language: string;
6
+ default?: boolean;
7
+ }
8
+
9
+ /**
10
+ * A display label for a locale code — the language's native name when the
11
+ * runtime knows it (`fr` -> `Français`), else the code itself.
12
+ */
13
+ const localeLabel = (code: string): string => {
14
+ try {
15
+ const native = new Intl.DisplayNames([code], { type: "language" }).of(code);
16
+ if (native && native !== code) {
17
+ return native.charAt(0).toUpperCase() + native.slice(1);
18
+ }
19
+ } catch {
20
+ // Unknown code or no ICU data; fall through to the code.
21
+ }
22
+ return code;
23
+ };
24
+
25
+ /**
26
+ * Map a Mintlify `navigation.languages[]` array to a Blume `i18n` config. The
27
+ * entry marked `default: true` becomes `defaultLocale`; translated content
28
+ * already lives in ISO-code directories, which match Blume's `dir` parser.
29
+ */
30
+ export const mintlifyI18n = (
31
+ spec: Record<string, unknown>
32
+ ): BlumeConfig["i18n"] | null => {
33
+ const navigation = spec.navigation as
34
+ | { languages?: MintlifyLanguage[] }
35
+ | undefined;
36
+ const languages = navigation?.languages;
37
+ if (!Array.isArray(languages) || languages.length < 2) {
38
+ return null;
39
+ }
40
+ const defaultLocale =
41
+ languages.find((entry) => entry.default)?.language ??
42
+ languages[0]?.language ??
43
+ "en";
44
+ return {
45
+ defaultLocale,
46
+ locales: languages.map((entry) => ({
47
+ code: entry.language,
48
+ label: localeLabel(entry.language),
49
+ })),
50
+ };
51
+ };
@@ -0,0 +1,128 @@
1
+ const JSX_ATTRIBUTE_ALIASES: Record<string, string> = {
2
+ className: "class",
3
+ clipPath: "clip-path",
4
+ clipRule: "clip-rule",
5
+ fillOpacity: "fill-opacity",
6
+ fillRule: "fill-rule",
7
+ strokeDasharray: "stroke-dasharray",
8
+ strokeDashoffset: "stroke-dashoffset",
9
+ strokeLinecap: "stroke-linecap",
10
+ strokeLinejoin: "stroke-linejoin",
11
+ strokeMiterlimit: "stroke-miterlimit",
12
+ strokeOpacity: "stroke-opacity",
13
+ strokeWidth: "stroke-width",
14
+ };
15
+
16
+ const SVG_EXPRESSION = /^\s*\(?\s*<svg[\s\S]*<\/svg>\s*\)?\s*;?\s*$/u;
17
+
18
+ const escapeAttribute = (value: string): string =>
19
+ value
20
+ .replaceAll("&", "&amp;")
21
+ .replaceAll('"', "&quot;")
22
+ .replaceAll("<", "&lt;")
23
+ .replaceAll(">", "&gt;");
24
+
25
+ const stripExpressionWrapper = (value: string): string => {
26
+ let next = value.trim().replaceAll(/;$/gu, "").trim();
27
+ if (next.startsWith("(") && next.endsWith(")")) {
28
+ next = next.slice(1, -1).trim();
29
+ }
30
+ return next;
31
+ };
32
+
33
+ const jsxExpressionAttribute = (
34
+ value: string,
35
+ quoted: string | undefined,
36
+ single: string | undefined,
37
+ template: string | undefined,
38
+ scalar: string | undefined
39
+ ): string => {
40
+ const literal = quoted ?? single ?? template ?? scalar;
41
+ return literal === undefined ? value : `="${escapeAttribute(literal)}"`;
42
+ };
43
+
44
+ const normalizeJsxSvg = (value: string): string | null => {
45
+ const stripped = stripExpressionWrapper(value);
46
+ if (!SVG_EXPRESSION.test(stripped)) {
47
+ return null;
48
+ }
49
+
50
+ let svg = stripped.replaceAll(
51
+ /[=]\{\s*(?:"(?<quoted>[^"]*)"|'(?<single>[^']*)'|`(?<template>[^`]*)`|(?<scalar>-?\d+(?:\.\d+)?|true|false))\s*\}/gu,
52
+ jsxExpressionAttribute
53
+ );
54
+ for (const [jsxName, htmlName] of Object.entries(JSX_ATTRIBUTE_ALIASES)) {
55
+ svg = svg.replaceAll(new RegExp(`\\b${jsxName}=`, "gu"), `${htmlName}=`);
56
+ }
57
+ return svg;
58
+ };
59
+
60
+ const findExpressionEnd = (source: string, start: number): number => {
61
+ let depth = 1;
62
+ let quote: '"' | "'" | "`" | null = null;
63
+ for (let index = start; index < source.length; index += 1) {
64
+ const char = source[index];
65
+ const previous = source[index - 1];
66
+ if (quote) {
67
+ if (char === quote && previous !== "\\") {
68
+ quote = null;
69
+ }
70
+ continue;
71
+ }
72
+ if (char === '"' || char === "'" || char === "`") {
73
+ quote = char;
74
+ continue;
75
+ }
76
+ if (char === "{") {
77
+ depth += 1;
78
+ continue;
79
+ }
80
+ if (char === "}") {
81
+ depth -= 1;
82
+ if (depth === 0) {
83
+ return index;
84
+ }
85
+ }
86
+ }
87
+ return -1;
88
+ };
89
+
90
+ const startsWithSvgExpression = (value: string): boolean => {
91
+ const trimmed = value.trimStart();
92
+ return trimmed.startsWith("<svg") || trimmed.startsWith("(<svg");
93
+ };
94
+
95
+ /**
96
+ * Rewrite Mintlify inline-SVG icon JSX props (`icon={<svg .../>}`) to plain
97
+ * string props so the migrated MDX compiles under Astro. Runs once at
98
+ * migration time; non-SVG `icon={...}` expressions are left untouched.
99
+ */
100
+ export const rewriteMintlifySvgIconProps = (source: string): string => {
101
+ let output = "";
102
+ let cursor = 0;
103
+ while (cursor < source.length) {
104
+ const start = source.indexOf("icon={", cursor);
105
+ if (start === -1) {
106
+ output += source.slice(cursor);
107
+ break;
108
+ }
109
+
110
+ const expressionStart = start + "icon={".length;
111
+ const end = findExpressionEnd(source, expressionStart);
112
+ if (end === -1) {
113
+ output += source.slice(cursor);
114
+ break;
115
+ }
116
+
117
+ const expression = source.slice(expressionStart, end);
118
+ const svg = startsWithSvgExpression(expression)
119
+ ? normalizeJsxSvg(expression)
120
+ : null;
121
+ output += source.slice(cursor, start);
122
+ output += svg
123
+ ? `icon={${JSON.stringify(svg)}}`
124
+ : source.slice(start, end + 1);
125
+ cursor = end + 1;
126
+ }
127
+ return output;
128
+ };
@@ -0,0 +1,266 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+
4
+ import { dirname, join } from "pathe";
5
+ import { glob } from "tinyglobby";
6
+
7
+ import type { BlumeConfig } from "../../core/schema.ts";
8
+ import { loadMintlifyConfig } from "./config.ts";
9
+ import { mintlifyI18n } from "./i18n.ts";
10
+ import { transformMintlifyContent } from "./transform.ts";
11
+
12
+ export interface MintlifyMigrationResult {
13
+ moved: number;
14
+ warnings: string[];
15
+ }
16
+
17
+ /** Recursively drop `undefined`, empty arrays, and empty objects. */
18
+ const prune = (value: unknown): unknown => {
19
+ if (Array.isArray(value)) {
20
+ return value.map(prune);
21
+ }
22
+ if (value && typeof value === "object") {
23
+ const out: Record<string, unknown> = {};
24
+ for (const [key, raw] of Object.entries(value)) {
25
+ const pruned = prune(raw);
26
+ if (pruned === undefined) {
27
+ continue;
28
+ }
29
+ if (Array.isArray(pruned) && pruned.length === 0) {
30
+ continue;
31
+ }
32
+ if (
33
+ pruned &&
34
+ typeof pruned === "object" &&
35
+ !Array.isArray(pruned) &&
36
+ Object.keys(pruned).length === 0
37
+ ) {
38
+ continue;
39
+ }
40
+ out[key] = pruned;
41
+ }
42
+ return out;
43
+ }
44
+ return value;
45
+ };
46
+
47
+ const writeBlumeConfig = async (
48
+ root: string,
49
+ config: BlumeConfig
50
+ ): Promise<void> => {
51
+ const body = `import { defineConfig } from "blume";\n\nexport default defineConfig(${JSON.stringify(prune(config), null, 2)});\n`;
52
+ await writeFile(join(root, "blume.config.ts"), body, "utf-8");
53
+ };
54
+
55
+ /** Move a referenced top-level asset path (file or dir) under `public/`. */
56
+ const relocateAssets = async (
57
+ root: string,
58
+ refs: unknown[]
59
+ ): Promise<string[]> => {
60
+ const segments = new Set<string>();
61
+ for (const ref of refs) {
62
+ if (typeof ref !== "string" || !ref.startsWith("/")) {
63
+ continue;
64
+ }
65
+ const [segment] = ref.replace(/^\/+/u, "").split("/");
66
+ if (segment) {
67
+ segments.add(segment);
68
+ }
69
+ }
70
+
71
+ const moved: string[] = [];
72
+ for (const segment of segments) {
73
+ const source = join(root, segment);
74
+ if (!existsSync(source) || segment === "public") {
75
+ continue;
76
+ }
77
+ const dest = join(root, "public", segment);
78
+ if (existsSync(dest)) {
79
+ continue;
80
+ }
81
+ // oxlint-disable-next-line no-await-in-loop -- sequential fs moves
82
+ await mkdir(join(root, "public"), { recursive: true });
83
+ // oxlint-disable-next-line no-await-in-loop -- sequential fs moves
84
+ await rename(source, dest);
85
+ moved.push(segment);
86
+ }
87
+ return moved;
88
+ };
89
+
90
+ /**
91
+ * Delete the inlined markdown snippets. Component files (e.g. `.jsx`) are kept
92
+ * because their imports were rewritten to resolve against `/snippets`.
93
+ */
94
+ const cleanupSnippets = async (
95
+ root: string,
96
+ kept: Set<string>,
97
+ warnings: string[]
98
+ ): Promise<void> => {
99
+ const dir = join(root, "snippets");
100
+ if (!existsSync(dir)) {
101
+ return;
102
+ }
103
+ const markdown = await glob(["**/*.{md,mdx}"], { absolute: true, cwd: dir });
104
+ for (const file of markdown) {
105
+ // oxlint-disable-next-line no-await-in-loop -- sequential fs removes
106
+ await rm(file, { force: true });
107
+ }
108
+ const remaining = await glob(["**/*"], { cwd: dir, dot: true });
109
+ if (remaining.length === 0) {
110
+ await rm(dir, { force: true, recursive: true });
111
+ warnings.push("Inlined and removed the /snippets directory.");
112
+ } else {
113
+ warnings.push(
114
+ `Inlined markdown snippets; kept ${remaining.length} component file(s) under /snippets.`
115
+ );
116
+ }
117
+ if (kept.size > 0) {
118
+ warnings.push(
119
+ `Rewrote ${kept.size} component snippet import(s) to relative paths; verify they resolve.`
120
+ );
121
+ }
122
+ };
123
+
124
+ /** Asset paths referenced by the resolved config (logo, favicon, backgrounds). */
125
+ const assetRefs = (config: BlumeConfig): unknown[] => {
126
+ const refs: unknown[] = ["/images"];
127
+ const logo = config.logo as
128
+ | string
129
+ | { dark?: string; light?: string }
130
+ | undefined;
131
+ if (typeof logo === "string") {
132
+ refs.push(logo);
133
+ } else if (logo) {
134
+ refs.push(logo.light, logo.dark);
135
+ }
136
+ const favicon = config.favicon as
137
+ | string
138
+ | { dark?: string; light?: string }
139
+ | undefined;
140
+ if (typeof favicon === "string") {
141
+ refs.push(favicon);
142
+ } else if (favicon) {
143
+ refs.push(favicon.light, favicon.dark);
144
+ }
145
+ refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
146
+ return refs;
147
+ };
148
+
149
+ /**
150
+ * Migrate a Mintlify project to Blume: translate `docs.json`/`mint.json` into
151
+ * `blume.config.ts`, rewrite every page to idiomatic Blume MDX in place, and
152
+ * relocate static assets. Content stays at the project root (`content.root`
153
+ * is `"."`).
154
+ */
155
+ export const migrateMintlifyProject = async (
156
+ root: string
157
+ ): Promise<MintlifyMigrationResult> => {
158
+ const warnings: string[] = [];
159
+ const configFile = existsSync(join(root, "docs.json"))
160
+ ? join(root, "docs.json")
161
+ : join(root, "mint.json");
162
+
163
+ let config: BlumeConfig;
164
+ if (existsSync(configFile)) {
165
+ config = await loadMintlifyConfig(root, configFile);
166
+ const spec = JSON.parse(await readFile(configFile, "utf-8")) as Record<
167
+ string,
168
+ unknown
169
+ >;
170
+ const i18n = mintlifyI18n(spec);
171
+ if (i18n) {
172
+ config.i18n = i18n;
173
+ // Language switching is handled by Blume i18n, not a nav selector.
174
+ if (config.navigation?.selectors) {
175
+ config.navigation.selectors = config.navigation.selectors.filter(
176
+ (selector) => selector.kind !== "language"
177
+ );
178
+ }
179
+ warnings.push(
180
+ `Mapped ${i18n.locales.length} languages to i18n.locales (default: ${i18n.defaultLocale}); review the locale labels.`
181
+ );
182
+ }
183
+ } else {
184
+ warnings.push("No docs.json or mint.json found; writing a default config.");
185
+ config = { content: { root: "." }, title: "Documentation" };
186
+ }
187
+
188
+ const variables = (config.variables as Record<string, string>) ?? {};
189
+ // Globals are inlined into content below; Blume has no runtime substitution.
190
+ config.variables = undefined;
191
+
192
+ const files = await glob(["**/*.{md,mdx}"], {
193
+ absolute: true,
194
+ cwd: root,
195
+ ignore: [
196
+ "node_modules/**",
197
+ ".blume/**",
198
+ "dist/**",
199
+ "public/**",
200
+ "snippets/**",
201
+ ],
202
+ });
203
+
204
+ let moved = 0;
205
+ const removedKeys = new Set<string>();
206
+ const unsupported = new Set<string>();
207
+ const keptComponents = new Set<string>();
208
+ for (const file of files) {
209
+ // oxlint-disable-next-line no-await-in-loop -- sequential fs writes
210
+ const raw = await readFile(file, "utf-8");
211
+ // oxlint-disable-next-line no-await-in-loop -- sequential transforms
212
+ const result = await transformMintlifyContent(raw, {
213
+ filePath: file,
214
+ root,
215
+ variables,
216
+ });
217
+ if (result.content !== raw) {
218
+ // oxlint-disable-next-line no-await-in-loop -- sequential fs writes
219
+ await mkdir(dirname(file), { recursive: true });
220
+ // oxlint-disable-next-line no-await-in-loop -- sequential fs writes
221
+ await writeFile(file, result.content, "utf-8");
222
+ }
223
+ for (const key of result.removed) {
224
+ removedKeys.add(key);
225
+ }
226
+ for (const name of result.unsupported) {
227
+ unsupported.add(name);
228
+ }
229
+ for (const name of result.components) {
230
+ keptComponents.add(name);
231
+ }
232
+ moved += 1;
233
+ }
234
+
235
+ const movedAssets = await relocateAssets(root, assetRefs(config));
236
+ await cleanupSnippets(root, keptComponents, warnings);
237
+
238
+ if (config.content?.exclude) {
239
+ config.content.exclude = [...new Set(config.content.exclude)];
240
+ }
241
+ await writeBlumeConfig(root, config);
242
+
243
+ if (Object.keys(variables).length > 0) {
244
+ warnings.push(
245
+ `Inlined ${Object.keys(variables).length} docs.json variable(s) into content; Blume has no runtime variable substitution.`
246
+ );
247
+ }
248
+ if (movedAssets.length > 0) {
249
+ warnings.push(`Moved assets into public/: ${movedAssets.join(", ")}.`);
250
+ }
251
+ if (removedKeys.size > 0) {
252
+ warnings.push(
253
+ `Dropped unsupported page frontmatter keys: ${[...removedKeys].join(", ")}.`
254
+ );
255
+ }
256
+ if (unsupported.size > 0) {
257
+ warnings.push(
258
+ `Components without a Blume equivalent need manual review (use the OpenAPI reference instead): ${[...unsupported].join(", ")}.`
259
+ );
260
+ }
261
+ warnings.push(
262
+ "Review blume.config.ts; navigation, theme, and chrome were mapped from docs.json."
263
+ );
264
+
265
+ return { moved, warnings };
266
+ };