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,190 +0,0 @@
1
- import { existsSync, watch as fsWatch } from "node:fs";
2
- import type { WatchListener } from "node:fs";
3
- import { readFile } from "node:fs/promises";
4
-
5
- import { isAbsolute, join, relative, resolve } from "pathe";
6
- import { glob } from "tinyglobby";
7
-
8
- import { transformMintlifyContent } from "../../migrate/mintlify/transform.ts";
9
- import { BlumeError } from "../diagnostics.ts";
10
- import matter from "../frontmatter.ts";
11
- import type { Diagnostic } from "../types.ts";
12
- import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
13
- import { BLUME_WATCH_IGNORE_DIRS, ignoringWatchListener } from "./watch.ts";
14
-
15
- /** Options for the Mintlify bridge content source. */
16
- export interface MintlifySourceOptions {
17
- /** Stable source name; namespaces ids and diagnostics. */
18
- name: string;
19
- /** Optional route prefix. */
20
- prefix?: string;
21
- /** Content root, absolute or relative to `projectRoot` (Mintlify: `.`). */
22
- root: string;
23
- include: string[];
24
- exclude: string[];
25
- /** `docs.json` variables, inlined into content (`{{name}}`) at scan time. */
26
- variables: Record<string, string>;
27
- /** Absolute path of the `docs.json`/`mint.json`, watched for changes in dev. */
28
- configFile?: string;
29
- /** Absolute project root, used to resolve a relative `root`. */
30
- projectRoot: string;
31
- }
32
-
33
- /**
34
- * Folders Mintlify projects keep alongside content that are never pages:
35
- * snippets are inlined as includes, and build/tooling dirs are noise. Merged
36
- * with the user's `exclude` so bridge mode behaves like the one-shot migrator.
37
- */
38
- const MINTLIFY_SOURCE_IGNORES = [
39
- "node_modules/**",
40
- ".blume/**",
41
- "dist/**",
42
- "build/**",
43
- "public/**",
44
- "snippets/**",
45
- ];
46
-
47
- /**
48
- * Directory names the recursive dev watcher must ignore, on top of the shared
49
- * {@link BLUME_WATCH_IGNORE_DIRS} (Blume's own `.blume/` output, VCS,
50
- * dependencies). Derived from {@link MINTLIFY_SOURCE_IGNORES} so bridge mode's
51
- * watcher stays in sync with what its scan skips (snippets, build output, …).
52
- */
53
- const WATCH_IGNORE_DIRS = [
54
- ...BLUME_WATCH_IGNORE_DIRS,
55
- ...MINTLIFY_SOURCE_IGNORES.map((pattern) => pattern.replace(/\/\*\*$/u, "")),
56
- ];
57
-
58
- /**
59
- * Build the recursive-watch listener for the bridge source: ignore events under
60
- * {@link WATCH_IGNORE_DIRS} so the dev server's `.blume/` writes don't feed a
61
- * regeneration loop. Exported for testing.
62
- */
63
- export const mintlifyWatchListener = (
64
- onChange: () => void
65
- ): WatchListener<string> => ignoringWatchListener(onChange, WATCH_IGNORE_DIRS);
66
-
67
- /**
68
- * The Mintlify bridge content source. Reads an unconverted Mintlify project in
69
- * place and transforms each page to Blume MDX at scan time (callouts → `:::`
70
- * directives, snippet/variable inlining, etc.) via `transformMintlifyContent`.
71
- * Staged: the transformed bodies are materialized under `.blume/content` and
72
- * rendered through Astro's `staged` collection, so the rewrites actually reach
73
- * the output. Components Blume already ships (Card, Tabs, Steps, …) render as-is.
74
- */
75
- export const mintlifySource = (
76
- options: MintlifySourceOptions
77
- ): ContentSource & { readonly contentRoot: string } => {
78
- const contentRoot = isAbsolute(options.root)
79
- ? options.root
80
- : join(resolve(options.projectRoot), options.root);
81
- const ignore = [...new Set([...options.exclude, ...MINTLIFY_SOURCE_IGNORES])];
82
-
83
- const transform = (
84
- raw: string,
85
- file: string
86
- ): ReturnType<typeof transformMintlifyContent> =>
87
- transformMintlifyContent(raw, {
88
- filePath: file,
89
- root: resolve(options.projectRoot),
90
- variables: options.variables,
91
- });
92
-
93
- const load = async (): Promise<SourceLoadResult> => {
94
- const files = await glob(options.include, {
95
- absolute: true,
96
- cwd: contentRoot,
97
- ignore,
98
- onlyFiles: true,
99
- });
100
- files.sort();
101
-
102
- const unsupported = new Set<string>();
103
- const entries = await Promise.all(
104
- files.map(async (file): Promise<SourceEntry> => {
105
- const result = await transform(await readFile(file, "utf-8"), file);
106
- for (const name of result.unsupported) {
107
- unsupported.add(name);
108
- }
109
- const parsed = matter(result.content);
110
- // Force MDX: Mintlify pages are MDX-authored and the rewrites emit `:::`
111
- // directives + JSX, neither of which the plain `.md` processor expands.
112
- return {
113
- body: { format: "mdx", text: parsed.content },
114
- data: parsed.data,
115
- raw: result.content,
116
- ref: relative(contentRoot, file),
117
- sourcePath: file,
118
- };
119
- })
120
- );
121
-
122
- const diagnostics: Diagnostic[] =
123
- unsupported.size > 0
124
- ? [
125
- {
126
- code: "BLUME_MINTLIFY_UNSUPPORTED",
127
- message: `Mintlify components without a Blume equivalent were left as-is: ${[...unsupported].toSorted().join(", ")}. Replace them by hand or provide a matching component.`,
128
- severity: "warning",
129
- },
130
- ]
131
- : [];
132
-
133
- return { diagnostics, entries };
134
- };
135
-
136
- const validate = (): void => {
137
- if (!existsSync(contentRoot)) {
138
- throw new BlumeError({
139
- code: "BLUME_CONTENT_ROOT_MISSING",
140
- file: contentRoot,
141
- message: `Content root not found: ${options.root}`,
142
- severity: "error",
143
- suggestion: `Run "blume dev" from the directory that contains docs.json.`,
144
- });
145
- }
146
- };
147
-
148
- const watch = (onChange: () => void): (() => void) => {
149
- const disposers: (() => void)[] = [];
150
- if (existsSync(contentRoot)) {
151
- // Recursively watch the content root, but skip Blume's own output and
152
- // other non-content trees so the dev server's `.blume/` writes don't feed
153
- // a regeneration loop (`fs.watch` has no ignore option, so filter here).
154
- const watcher = fsWatch(
155
- contentRoot,
156
- { recursive: true },
157
- mintlifyWatchListener(onChange)
158
- );
159
- disposers.push(() => watcher.close());
160
- }
161
- // Watch docs.json directly: it lives at the content root but a non-recursive
162
- // single-file watch fires reliably on edits that recursive dir-watch can miss.
163
- if (options.configFile && existsSync(options.configFile)) {
164
- const watcher = fsWatch(options.configFile, onChange);
165
- disposers.push(() => watcher.close());
166
- }
167
- return () => {
168
- for (const dispose of disposers) {
169
- dispose();
170
- }
171
- };
172
- };
173
-
174
- const read = async (ref: string): Promise<string> => {
175
- const file = join(contentRoot, ref);
176
- const result = await transform(await readFile(file, "utf-8"), file);
177
- return result.content;
178
- };
179
-
180
- return {
181
- contentRoot,
182
- load,
183
- name: options.name,
184
- prefix: options.prefix,
185
- read,
186
- staged: true,
187
- validate,
188
- watch,
189
- };
190
- };
@@ -1,155 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
-
4
- import { basename, dirname, join } from "pathe";
5
-
6
- import type { BlumeConfig } from "../../core/schema.ts";
7
-
8
- /**
9
- * Build a `BlumeConfig` for a Fumadocs project. Fumadocs is code-first — its
10
- * navigation comes from the folder tree plus `meta.json` files, not a JSON
11
- * config — so there is little to translate. We take the title from
12
- * `package.json` and scrape the `loader({ baseUrl })` route prefix from the
13
- * source loader so migrated routes keep serving under `/<baseUrl>`. The source
14
- * files are TypeScript, so we read by pattern rather than executing them.
15
- */
16
-
17
- /** Loader files that may hold `loader({ baseUrl: "/docs" })`. */
18
- const SOURCE_FILES = [
19
- "lib/source.ts",
20
- "app/source.ts",
21
- "src/lib/source.ts",
22
- "src/app/source.ts",
23
- "source.ts",
24
- ];
25
-
26
- const BASE_URL = /baseUrl\s*:\s*['"`](?<base>[^'"`]+)['"`]/u;
27
-
28
- /** Title-case a package name (drop any scope) into a readable doc title. */
29
- const prettifyTitle = (name: string): string => {
30
- const base = name.includes("/")
31
- ? name.slice(name.lastIndexOf("/") + 1)
32
- : name;
33
- const words = base.split(/[-_\s]+/u).filter(Boolean);
34
- if (words.length === 0) {
35
- return "Documentation";
36
- }
37
- return words
38
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
39
- .join(" ");
40
- };
41
-
42
- /**
43
- * Generic monorepo app-shell package names. When the migrated project is named
44
- * one of these, its name makes a poor doc title ("Web"), so we fall back to the
45
- * repo name — but only in a monorepo, where a better name is actually available.
46
- */
47
- const GENERIC_NAMES = new Set([
48
- "api",
49
- "app",
50
- "client",
51
- "frontend",
52
- "server",
53
- "site",
54
- "web",
55
- "www",
56
- ]);
57
-
58
- /** The nearest ancestor that is a git repository root, or null. */
59
- const gitRepoRoot = (start: string): string | null => {
60
- let dir = start;
61
- for (;;) {
62
- if (existsSync(join(dir, ".git"))) {
63
- return dir;
64
- }
65
- const parent = dirname(dir);
66
- if (parent === dir) {
67
- return null;
68
- }
69
- dir = parent;
70
- }
71
- };
72
-
73
- /** The unscoped package name (`@acme/web` -> `web`). */
74
- const bareName = (name: string): string =>
75
- name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
76
-
77
- const readTitle = async (root: string): Promise<string> => {
78
- const packageJson = join(root, "package.json");
79
- if (!existsSync(packageJson)) {
80
- return "Documentation";
81
- }
82
- try {
83
- const parsed = JSON.parse(await readFile(packageJson, "utf-8")) as {
84
- name?: unknown;
85
- };
86
- const { name } = parsed;
87
- if (typeof name !== "string" || !name.trim()) {
88
- return "Documentation";
89
- }
90
- // A generic name (`apps/web` -> "Web") is a weak title. In a monorepo the
91
- // repo's own directory name is usually better, so prefer it when this isn't
92
- // already the repo root.
93
- if (GENERIC_NAMES.has(bareName(name).toLowerCase())) {
94
- const repoRoot = gitRepoRoot(root);
95
- if (repoRoot && repoRoot !== root) {
96
- const repoTitle = prettifyTitle(basename(repoRoot));
97
- if (repoTitle !== "Documentation") {
98
- return repoTitle;
99
- }
100
- }
101
- }
102
- return prettifyTitle(name);
103
- } catch {
104
- return "Documentation";
105
- }
106
- };
107
-
108
- const scrapeBaseUrl = async (root: string): Promise<string | null> => {
109
- for (const candidate of SOURCE_FILES) {
110
- const file = join(root, candidate);
111
- if (!existsSync(file)) {
112
- continue;
113
- }
114
- // oxlint-disable-next-line no-await-in-loop -- sequential probing of candidates
115
- const base = BASE_URL.exec(await readFile(file, "utf-8"))?.groups?.base;
116
- if (base) {
117
- return base;
118
- }
119
- }
120
- return null;
121
- };
122
-
123
- export interface FumadocsConfigResult {
124
- config: BlumeConfig;
125
- warnings: string[];
126
- }
127
-
128
- /** Resolve the Blume config and route prefix for a Fumadocs project. */
129
- export const loadFumadocsConfig = async (
130
- root: string
131
- ): Promise<FumadocsConfigResult> => {
132
- const title = await readTitle(root);
133
- const baseUrl = await scrapeBaseUrl(root);
134
- // Default to the conventional Fumadocs `/docs` base when none is declared.
135
- const prefix = (baseUrl ?? "docs").replaceAll(/^\/+|\/+$/gu, "");
136
- const warnings: string[] = [];
137
-
138
- if (prefix) {
139
- warnings.push(
140
- `Docs are served under /${prefix} (set content.sources prefix); change it to "" to serve from the site root.`
141
- );
142
- return {
143
- config: {
144
- content: {
145
- sources: [{ prefix, root: "docs", type: "filesystem" }],
146
- },
147
- title,
148
- },
149
- warnings,
150
- };
151
- }
152
-
153
- warnings.push("Docs are served from the site root.");
154
- return { config: { title }, warnings };
155
- };