blume 0.5.3 → 0.5.4

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 (113) hide show
  1. package/dist/cli/index.js +2985 -6883
  2. package/dist/cli/index.js.map +27 -58
  3. package/dist/types/core/config.d.ts +0 -8
  4. package/dist/types/core/data.d.ts +2 -2
  5. package/dist/types/core/schema.d.ts +58 -453
  6. package/dist/types/core/types.d.ts +1 -7
  7. package/docs/advanced/meta.ts +1 -8
  8. package/docs/advanced/skills.mdx +28 -0
  9. package/docs/configuration/ai.mdx +2 -0
  10. package/docs/configuration/index.mdx +13 -17
  11. package/docs/configuration/theming.mdx +2 -10
  12. package/docs/content/components.mdx +2 -53
  13. package/docs/content/i18n.mdx +0 -4
  14. package/docs/content/syntax.mdx +1 -1
  15. package/docs/index.mdx +0 -1
  16. package/docs/reference/cli.mdx +12 -13
  17. package/package.json +4 -6
  18. package/skills/blume/SKILL.md +71 -0
  19. package/skills/blume-update-docs/SKILL.md +52 -0
  20. package/skills/blume-update-docs/references/audit-checklist.md +46 -0
  21. package/src/astro/generate.ts +15 -12
  22. package/src/astro/integration.ts +0 -21
  23. package/src/astro/templates.ts +9 -16
  24. package/src/cli/commands/dev.ts +0 -6
  25. package/src/cli/commands/validate.ts +0 -2
  26. package/src/cli/index.ts +0 -2
  27. package/src/components/BlumePage.astro +0 -6
  28. package/src/components/Icon.astro +1 -12
  29. package/src/components/content/AccordionItem.astro +3 -6
  30. package/src/components/content/Badge.astro +1 -3
  31. package/src/components/content/Callout.astro +3 -9
  32. package/src/components/content/Card.astro +2 -3
  33. package/src/components/content/ColorItem.astro +2 -2
  34. package/src/components/content/Column.astro +1 -1
  35. package/src/components/content/Prompt.astro +1 -1
  36. package/src/components/content/Step.astro +3 -4
  37. package/src/components/content/Tab.astro +2 -3
  38. package/src/components/content/TypeTable.astro +5 -5
  39. package/src/components/content/Update.astro +1 -1
  40. package/src/components/layout/Header.astro +26 -3
  41. package/src/components/layout/Logo.astro +5 -1
  42. package/src/components/layout/NavSelector.astro +1 -1
  43. package/src/components/layout/NavTree.astro +14 -14
  44. package/src/components/layout/PageLayout.astro +42 -0
  45. package/src/components/layout/ReferenceLayout.astro +1 -0
  46. package/src/components/layout/RootLayout.astro +31 -2
  47. package/src/components/layout/Search.astro +5 -5
  48. package/src/components/layout/nav-utils.ts +9 -4
  49. package/src/components/openapi/ApiOverview.astro +4 -50
  50. package/src/components/openapi/ApiTagOperations.astro +42 -0
  51. package/src/core/builtin-tags.ts +1 -3
  52. package/src/core/config.ts +5 -28
  53. package/src/core/data.ts +2 -2
  54. package/src/core/graph.ts +1 -4
  55. package/src/core/links.ts +5 -19
  56. package/src/core/meta.ts +1 -1
  57. package/src/core/navigation.ts +0 -5
  58. package/src/core/project-graph.ts +0 -5
  59. package/src/core/schema.ts +13 -90
  60. package/src/core/sources/resolve.ts +0 -13
  61. package/src/core/sources/watch.ts +3 -2
  62. package/src/core/types.ts +1 -8
  63. package/src/openapi/parse.ts +197 -14
  64. package/src/openapi/render-mdx.ts +44 -10
  65. package/src/openapi/source.ts +19 -2
  66. package/src/theme/entry.ts +45 -17
  67. package/src/theme/icons.ts +18 -109
  68. package/src/theme/palette.ts +4 -37
  69. package/src/theme/twoslash.ts +6 -1
  70. package/dist/types/core/bridge.d.ts +0 -24
  71. package/dist/types/core/package-json.d.ts +0 -12
  72. package/dist/types/migrate/mintlify/assets.d.ts +0 -8
  73. package/dist/types/migrate/mintlify/config.d.ts +0 -16
  74. package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
  75. package/dist/types/migrate/shared.d.ts +0 -153
  76. package/docs/advanced/bridge.mdx +0 -76
  77. package/docs/advanced/migrate.mdx +0 -124
  78. package/src/astro/static-assets.ts +0 -124
  79. package/src/cli/commands/migrate.ts +0 -39
  80. package/src/components/content/ApiField.astro +0 -75
  81. package/src/components/content/ParamField.astro +0 -39
  82. package/src/components/content/RequestField.astro +0 -23
  83. package/src/components/content/ResponseField.astro +0 -23
  84. package/src/components/content/Warning.astro +0 -9
  85. package/src/core/assets.ts +0 -31
  86. package/src/core/bridge.ts +0 -102
  87. package/src/core/sources/mintlify.ts +0 -190
  88. package/src/migrate/fumadocs/config.ts +0 -155
  89. package/src/migrate/fumadocs/content.ts +0 -376
  90. package/src/migrate/fumadocs/frontmatter.ts +0 -18
  91. package/src/migrate/fumadocs/groups.ts +0 -237
  92. package/src/migrate/fumadocs/index.ts +0 -355
  93. package/src/migrate/fumadocs/meta.ts +0 -244
  94. package/src/migrate/migrate.ts +0 -53
  95. package/src/migrate/mintlify/assets.ts +0 -46
  96. package/src/migrate/mintlify/config.ts +0 -954
  97. package/src/migrate/mintlify/content.ts +0 -120
  98. package/src/migrate/mintlify/frontmatter.ts +0 -126
  99. package/src/migrate/mintlify/i18n.ts +0 -51
  100. package/src/migrate/mintlify/icons.ts +0 -128
  101. package/src/migrate/mintlify/index.ts +0 -459
  102. package/src/migrate/mintlify/snippets.ts +0 -315
  103. package/src/migrate/mintlify/transform.ts +0 -82
  104. package/src/migrate/nextra/content.ts +0 -46
  105. package/src/migrate/nextra/frontmatter.ts +0 -40
  106. package/src/migrate/nextra/index.ts +0 -389
  107. package/src/migrate/nextra/meta.ts +0 -266
  108. package/src/migrate/shared.ts +0 -801
  109. package/src/migrate/starlight/config.ts +0 -455
  110. package/src/migrate/starlight/content.ts +0 -75
  111. package/src/migrate/starlight/frontmatter.ts +0 -111
  112. package/src/migrate/starlight/i18n.ts +0 -54
  113. package/src/migrate/starlight/index.ts +0 -131
@@ -1,23 +0,0 @@
1
- ---
2
- // Mintlify-compat <ResponseField>: a named field (`name`, `type`, `required`,
3
- // `default`, `deprecated`) with its description in the slot.
4
- import ApiField from "./ApiField.astro";
5
-
6
- const {
7
- default: defaultValue,
8
- deprecated,
9
- name,
10
- required,
11
- type,
12
- } = Astro.props;
13
- ---
14
-
15
- <ApiField
16
- default={defaultValue}
17
- deprecated={deprecated}
18
- name={name}
19
- required={required}
20
- type={type}
21
- >
22
- <slot />
23
- </ApiField>
@@ -1,9 +0,0 @@
1
- ---
2
- import Callout from "./Callout.astro";
3
-
4
- const { title } = Astro.props;
5
- ---
6
-
7
- <Callout title={title} type="warning">
8
- <slot />
9
- </Callout>
@@ -1,31 +0,0 @@
1
- import { join } from "pathe";
2
-
3
- /** A static directory served at a URL prefix, in addition to `public/`. */
4
- export interface AssetMount {
5
- /** Absolute filesystem path to the source directory (or file). */
6
- dir: string;
7
- /** URL path prefix the source is served at, e.g. `/images`. */
8
- url: string;
9
- }
10
-
11
- /**
12
- * Resolve `content.assets` entries (top-level dirs served at the site root,
13
- * alongside `public/`) to `{ dir, url }` mounts. Shared by the generated Astro
14
- * runtime (dev middleware + build copy) and by link validation, so all three
15
- * agree on where a `/images/foo.png` reference resolves on disk.
16
- *
17
- * Each entry is normalized to a leading-slash URL and joined to the project
18
- * root; leading `./` or `/` and any `..` segments are stripped so a mount can't
19
- * escape the root or collide with the site's own routing prefix.
20
- */
21
- export const resolveAssetMounts = (
22
- root: string,
23
- assets: string[]
24
- ): AssetMount[] =>
25
- assets.map((entry) => {
26
- const rel = entry
27
- .replace(/^[./]+/u, "")
28
- .replaceAll(/\.\.\/?/gu, "")
29
- .replace(/\/+$/u, "");
30
- return { dir: join(root, rel), url: `/${rel}` };
31
- });
@@ -1,102 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
-
4
- import { join } from "pathe";
5
-
6
- import { assetSegments } from "../migrate/mintlify/assets.ts";
7
- import { loadMintlifyConfig } from "../migrate/mintlify/config.ts";
8
- import { mintlifyI18n } from "../migrate/mintlify/i18n.ts";
9
- import type { BlumeConfig } from "./schema.ts";
10
-
11
- /** A detected docs-tool config that Blume can serve without a migration. */
12
- export interface BridgeDetection {
13
- /** Which foreign docs tool was detected. */
14
- tool: "mintlify";
15
- /** Absolute path of the detected config file (`docs.json`/`mint.json`). */
16
- configFile: string;
17
- /** A Blume config synthesized from the foreign config, ready to validate. */
18
- raw: BlumeConfig;
19
- }
20
-
21
- const MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
22
-
23
- /**
24
- * Detect a Mintlify project at `root` and synthesize an equivalent Blume config.
25
- *
26
- * "Bridge mode" lets a team swap `mintlify dev` for `blume dev` with no file
27
- * changes: `docs.json` is translated to Blume config (`loadMintlifyConfig`) and
28
- * its content block is rewired to a single `mintlify` content source, which
29
- * transforms each MDX page to Blume idiom at scan time. The original
30
- * `content.root`/`exclude` and `variables` move onto the source (Blume has no
31
- * runtime variable substitution, so globals are inlined into content there).
32
- *
33
- * Returns `null` when no Mintlify config is present. Only called when no
34
- * `blume.config.*` exists, so an explicit Blume config always takes precedence.
35
- */
36
- export const detectMintlifyBridge = async (
37
- root: string
38
- ): Promise<BridgeDetection | null> => {
39
- const configFile = MINTLIFY_CONFIG_FILES.map((name) => join(root, name)).find(
40
- (candidate) => existsSync(candidate)
41
- );
42
- if (!configFile) {
43
- return null;
44
- }
45
-
46
- const config = await loadMintlifyConfig(root, configFile);
47
-
48
- // i18n is derived from the raw spec (language dirs), mirroring the migrator.
49
- const spec = JSON.parse(await readFile(configFile, "utf-8")) as Record<
50
- string,
51
- unknown
52
- >;
53
- const i18n = mintlifyI18n(spec);
54
- if (i18n) {
55
- config.i18n = i18n;
56
- // Language switching is handled by Blume i18n, not a nav selector.
57
- if (config.navigation?.selectors) {
58
- config.navigation.selectors = config.navigation.selectors.filter(
59
- (selector) => selector.kind !== "language"
60
- );
61
- }
62
- }
63
-
64
- const variables = (config.variables as Record<string, string>) ?? {};
65
- const root_ = config.content?.root ?? ".";
66
- const exclude = config.content?.exclude ?? [];
67
-
68
- // Mintlify serves assets from the project root; the bridge never moves files,
69
- // so referenced root-level asset folders (e.g. `images/`) are served in place
70
- // via `content.assets` instead. This is the read-only twin of the migrator's
71
- // relocation — same referenced segments, just no `public/` move.
72
- const assets = assetSegments(config).filter(
73
- (segment) => segment !== "public" && existsSync(join(root, segment))
74
- );
75
-
76
- return {
77
- configFile,
78
- raw: {
79
- ...config,
80
- content: {
81
- assets,
82
- // Mirror the excludes onto `content.exclude` too: the generated Astro
83
- // `docs` collection globs `content.root` (here the project root) and
84
- // must skip node_modules/snippets just like the source does.
85
- exclude,
86
- root: root_,
87
- sources: [
88
- {
89
- configFile,
90
- exclude,
91
- root: root_,
92
- type: "mintlify",
93
- variables,
94
- },
95
- ],
96
- },
97
- // Globals are inlined into content by the source; drop the top-level copy.
98
- variables: undefined,
99
- },
100
- tool: "mintlify",
101
- };
102
- };
@@ -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
- };