blume 0.5.2 → 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.
- package/dist/cli/index.js +2235 -5942
- package/dist/cli/index.js.map +36 -65
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +2 -2
- package/dist/types/core/schema.d.ts +58 -453
- package/dist/types/core/types.d.ts +1 -7
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/theming.mdx +3 -11
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +1 -5
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +4 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/mcp/discovery.ts +3 -1
- package/src/ai/mcp/server.ts +3 -1
- package/src/astro/component-slots.ts +10 -2
- package/src/astro/generate.ts +15 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +62 -37
- package/src/cli/coalesce.ts +43 -0
- package/src/cli/commands/dev.ts +30 -22
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +4 -2
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +8 -5
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/Prompt.astro +8 -4
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/Tabs.astro +13 -2
- package/src/components/content/TypeTable.astro +5 -5
- package/src/components/content/Update.astro +1 -1
- package/src/components/content/mermaid-element.ts +20 -2
- package/src/components/islands/ask-ai.tsx +4 -8
- package/src/components/islands/base-path.ts +30 -0
- package/src/components/islands/hooks.ts +12 -8
- package/src/components/layout/Header.astro +26 -3
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +14 -14
- package/src/components/layout/PageActions.astro +17 -11
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +31 -2
- package/src/components/layout/Search.astro +9 -6
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/layout/search/types.ts +16 -5
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/SchemaProperty.astro +1 -1
- package/src/components/openapi/SchemaTable.astro +3 -3
- package/src/components/openapi/helpers.ts +17 -8
- package/src/components/openapi/snippets.ts +17 -4
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +19 -33
- package/src/core/data.ts +2 -2
- package/src/core/graph.ts +7 -5
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/navigation.ts +5 -6
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +13 -90
- package/src/core/sources/filesystem.ts +19 -1
- package/src/core/sources/mdx-remote.ts +20 -4
- package/src/core/sources/normalize.ts +28 -6
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +45 -0
- package/src/core/types.ts +1 -8
- package/src/markdown/code-title.ts +6 -3
- package/src/markdown/package-install.ts +3 -1
- package/src/og/card.ts +16 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +54 -11
- package/src/openapi/source.ts +19 -2
- package/src/search/sync/orama-cloud.ts +2 -0
- package/src/search/sync/typesense.ts +4 -0
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +22 -104
- package/src/theme/palette.ts +35 -47
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -123
- package/src/astro/static-assets.ts +0 -117
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -204
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -378
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -340
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -958
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -372
- package/src/migrate/mintlify/snippets.ts +0 -306
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -374
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -728
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -78
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
import { join } from "pathe";
|
|
5
|
-
|
|
6
|
-
import type { BlumeConfig, SidebarItemConfig } from "../../core/schema.ts";
|
|
7
|
-
import type { LiteralValue } from "../shared.ts";
|
|
8
|
-
import {
|
|
9
|
-
asLiteralArray as asArray,
|
|
10
|
-
asLiteralString as asString,
|
|
11
|
-
isLiteralObject as isObject,
|
|
12
|
-
parseLiteral,
|
|
13
|
-
scanObject,
|
|
14
|
-
stripJsComments,
|
|
15
|
-
} from "../shared.ts";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Translate a Starlight `astro.config.*` into a `BlumeConfig`. The
|
|
19
|
-
* `starlight({...})` options object is read statically out of the source text —
|
|
20
|
-
* never executed — mirroring the Nextra `_meta` reader. Values that aren't pure
|
|
21
|
-
* literals are skipped and reported, so a config that references imported assets
|
|
22
|
-
* or plugins still yields a usable result instead of failing wholesale.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const CONFIG_FILES = [
|
|
26
|
-
"astro.config.mjs",
|
|
27
|
-
"astro.config.mts",
|
|
28
|
-
"astro.config.ts",
|
|
29
|
-
"astro.config.js",
|
|
30
|
-
"astro.config.cjs",
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const STARLIGHT_IMPORT =
|
|
34
|
-
/import\s+(?<name>\w+)\s+from\s+["']@astrojs\/starlight["']/u;
|
|
35
|
-
|
|
36
|
-
type StarlightOptions = Record<string, LiteralValue>;
|
|
37
|
-
|
|
38
|
-
/** Extract the object literal passed to `starlight(…)` from config source. */
|
|
39
|
-
const extractStarlightOptions = (
|
|
40
|
-
source: string
|
|
41
|
-
): StarlightOptions | "unparseable" | "missing" => {
|
|
42
|
-
const clean = stripJsComments(source);
|
|
43
|
-
const importName = STARLIGHT_IMPORT.exec(clean)?.groups?.name ?? "starlight";
|
|
44
|
-
const call = new RegExp(`\\b${importName}\\s*\\(`, "u").exec(clean);
|
|
45
|
-
if (!call) {
|
|
46
|
-
return "missing";
|
|
47
|
-
}
|
|
48
|
-
let index = call.index + call[0].length;
|
|
49
|
-
while (index < clean.length && /\s/u.test(clean[index] ?? "")) {
|
|
50
|
-
index += 1;
|
|
51
|
-
}
|
|
52
|
-
if (clean[index] !== "{") {
|
|
53
|
-
// `starlight()` with no/non-literal options — nothing to translate.
|
|
54
|
-
return clean[index] === ")" ? {} : "unparseable";
|
|
55
|
-
}
|
|
56
|
-
const scan = scanObject(clean, index);
|
|
57
|
-
if (!scan) {
|
|
58
|
-
return "unparseable";
|
|
59
|
-
}
|
|
60
|
-
const parsed = parseLiteral(clean.slice(index, scan.end + 1));
|
|
61
|
-
return isObject(parsed) ? parsed : "unparseable";
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export interface LoadedStarlightConfig {
|
|
65
|
-
options: StarlightOptions;
|
|
66
|
-
warnings: string[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Locate and read the Starlight options out of the project's astro config. */
|
|
70
|
-
export const loadStarlightConfig = async (
|
|
71
|
-
root: string
|
|
72
|
-
): Promise<LoadedStarlightConfig> => {
|
|
73
|
-
const file = CONFIG_FILES.map((name) => join(root, name)).find((path) =>
|
|
74
|
-
existsSync(path)
|
|
75
|
-
);
|
|
76
|
-
if (!file) {
|
|
77
|
-
return {
|
|
78
|
-
options: {},
|
|
79
|
-
warnings: [
|
|
80
|
-
"No astro.config.* found; wrote a default config — port your Starlight options by hand.",
|
|
81
|
-
],
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const source = await readFile(file, "utf-8");
|
|
86
|
-
const result = extractStarlightOptions(source);
|
|
87
|
-
if (result === "missing") {
|
|
88
|
-
return {
|
|
89
|
-
options: {},
|
|
90
|
-
warnings: [
|
|
91
|
-
"Could not find a starlight() call in astro.config.*; port your options by hand.",
|
|
92
|
-
],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
if (result === "unparseable") {
|
|
96
|
-
return {
|
|
97
|
-
options: {},
|
|
98
|
-
warnings: [
|
|
99
|
-
"Could not statically read the starlight() options; port your config by hand.",
|
|
100
|
-
],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return { options: result, warnings: [] };
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// Field mappers
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
type LogoConfig = string | { alt?: string; dark?: string; light?: string };
|
|
111
|
-
|
|
112
|
-
const mapLogo = (
|
|
113
|
-
value: LiteralValue | undefined,
|
|
114
|
-
warnings: string[]
|
|
115
|
-
): LogoConfig | undefined => {
|
|
116
|
-
const logo = isObject(value) ? value : undefined;
|
|
117
|
-
if (!logo) {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
const alt = asString(logo.alt);
|
|
121
|
-
const src = asString(logo.src);
|
|
122
|
-
const light = asString(logo.light);
|
|
123
|
-
const dark = asString(logo.dark);
|
|
124
|
-
|
|
125
|
-
let result: LogoConfig | undefined;
|
|
126
|
-
if (src) {
|
|
127
|
-
result = alt ? { alt, dark: src, light: src } : src;
|
|
128
|
-
} else if (light || dark) {
|
|
129
|
-
result = {
|
|
130
|
-
...(alt ? { alt } : {}),
|
|
131
|
-
...(dark ? { dark } : {}),
|
|
132
|
-
...(light ? { light } : {}),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
if (result) {
|
|
136
|
-
warnings.push(
|
|
137
|
-
"Mapped the Starlight logo; move the referenced file under public/ (Blume serves static assets from there)."
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
return result;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const GITHUB_REPO = /github\.com\/(?<owner>[^/]+)\/(?<repo>[^/?#]+)/u;
|
|
144
|
-
const EDIT_LINK =
|
|
145
|
-
/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/edit\/(?<branch>[^/?#]+)/u;
|
|
146
|
-
|
|
147
|
-
interface GithubConfig {
|
|
148
|
-
branch?: string;
|
|
149
|
-
owner: string;
|
|
150
|
-
repo: string;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const githubFromUrl = (url: string): GithubConfig | undefined => {
|
|
154
|
-
const groups = GITHUB_REPO.exec(url)?.groups;
|
|
155
|
-
if (!groups?.owner || !groups.repo) {
|
|
156
|
-
return undefined;
|
|
157
|
-
}
|
|
158
|
-
return { owner: groups.owner, repo: groups.repo.replace(/\.git$/u, "") };
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
interface SocialMapping {
|
|
162
|
-
github?: GithubConfig;
|
|
163
|
-
socials: Record<string, string>;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Normalize a Starlight social icon key into a Blume `footer.socials` key. */
|
|
167
|
-
const socialKey = (icon: string): string =>
|
|
168
|
-
icon.toLowerCase().replace(/\.com$/u, "");
|
|
169
|
-
|
|
170
|
-
const mapSocial = (value: LiteralValue | undefined): SocialMapping => {
|
|
171
|
-
const result: SocialMapping = { socials: {} };
|
|
172
|
-
const record = (icon: string, href: string): void => {
|
|
173
|
-
const key = socialKey(icon);
|
|
174
|
-
result.socials[key] = href;
|
|
175
|
-
if (key === "github" && !result.github) {
|
|
176
|
-
result.github = githubFromUrl(href);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const array = asArray(value);
|
|
181
|
-
if (array) {
|
|
182
|
-
for (const entry of array) {
|
|
183
|
-
if (!isObject(entry)) {
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
const icon = asString(entry.icon);
|
|
187
|
-
const href = asString(entry.href);
|
|
188
|
-
if (icon && href) {
|
|
189
|
-
record(icon, href);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
// Older Starlight: `social: { github: "https://…", discord: "https://…" }`.
|
|
195
|
-
if (isObject(value)) {
|
|
196
|
-
for (const [icon, href] of Object.entries(value)) {
|
|
197
|
-
const url = asString(href);
|
|
198
|
-
if (url) {
|
|
199
|
-
record(icon, url);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return result;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const mapEditLink = (
|
|
207
|
-
value: LiteralValue | undefined
|
|
208
|
-
): GithubConfig | undefined => {
|
|
209
|
-
const editLink = isObject(value) ? value : undefined;
|
|
210
|
-
const baseUrl = asString(editLink?.baseUrl);
|
|
211
|
-
if (!baseUrl) {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
const groups = EDIT_LINK.exec(baseUrl)?.groups;
|
|
215
|
-
if (!groups?.owner || !groups.repo || !groups.branch) {
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
branch: groups.branch,
|
|
220
|
-
owner: groups.owner,
|
|
221
|
-
repo: groups.repo.replace(/\.git$/u, ""),
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
/** Flatten a Starlight badge (`string` or `{ text, variant }`) to a string. */
|
|
226
|
-
const badgeText = (value: LiteralValue | undefined): string | undefined =>
|
|
227
|
-
asString(value) ?? asString(isObject(value) ? value.text : undefined);
|
|
228
|
-
|
|
229
|
-
const mapSidebarItem = (
|
|
230
|
-
value: LiteralValue,
|
|
231
|
-
warnings: string[]
|
|
232
|
-
): SidebarItemConfig | null => {
|
|
233
|
-
if (typeof value === "string") {
|
|
234
|
-
return `/${value.replace(/^\/+/u, "")}`;
|
|
235
|
-
}
|
|
236
|
-
if (!isObject(value)) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
const label = asString(value.label);
|
|
240
|
-
const badge = badgeText(value.badge);
|
|
241
|
-
const collapsed =
|
|
242
|
-
typeof value.collapsed === "boolean" ? value.collapsed : undefined;
|
|
243
|
-
|
|
244
|
-
const autogenerate = isObject(value.autogenerate)
|
|
245
|
-
? asString(value.autogenerate.directory)
|
|
246
|
-
: undefined;
|
|
247
|
-
if (autogenerate && label) {
|
|
248
|
-
return {
|
|
249
|
-
label,
|
|
250
|
-
...(badge ? { badge } : {}),
|
|
251
|
-
...(collapsed === undefined ? {} : { collapsed }),
|
|
252
|
-
root: autogenerate,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const items = asArray(value.items);
|
|
257
|
-
if (items && label) {
|
|
258
|
-
const mapped = items
|
|
259
|
-
.map((item) => mapSidebarItem(item, warnings))
|
|
260
|
-
.filter((item): item is SidebarItemConfig => item !== null);
|
|
261
|
-
return {
|
|
262
|
-
...(badge ? { badge } : {}),
|
|
263
|
-
...(collapsed === undefined ? {} : { collapsed }),
|
|
264
|
-
items: mapped,
|
|
265
|
-
label,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const link = asString(value.link);
|
|
270
|
-
if (link) {
|
|
271
|
-
return label
|
|
272
|
-
? { ...(badge ? { badge } : {}), href: link, label }
|
|
273
|
-
: { href: link, label: link };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const slug = asString(value.slug);
|
|
277
|
-
if (slug) {
|
|
278
|
-
const href = `/${slug.replace(/^\/+/u, "")}`;
|
|
279
|
-
return label ? { ...(badge ? { badge } : {}), href, label } : href;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
warnings.push("Skipped a sidebar entry that uses an unsupported shape.");
|
|
283
|
-
return null;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const mapSidebar = (
|
|
287
|
-
value: LiteralValue | undefined,
|
|
288
|
-
warnings: string[]
|
|
289
|
-
): SidebarItemConfig[] | undefined => {
|
|
290
|
-
const array = asArray(value);
|
|
291
|
-
if (!array) {
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
const items = array
|
|
295
|
-
.map((item) => mapSidebarItem(item, warnings))
|
|
296
|
-
.filter((item): item is SidebarItemConfig => item !== null);
|
|
297
|
-
return items.length > 0 ? items : undefined;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const mapCodeTheme = (
|
|
301
|
-
value: LiteralValue | undefined,
|
|
302
|
-
warnings: string[]
|
|
303
|
-
): { dark: string; light: string } | undefined => {
|
|
304
|
-
const expressive = isObject(value) ? value : undefined;
|
|
305
|
-
if (value === false) {
|
|
306
|
-
warnings.push(
|
|
307
|
-
"expressiveCode is disabled in Starlight; Blume always renders code blocks."
|
|
308
|
-
);
|
|
309
|
-
return undefined;
|
|
310
|
-
}
|
|
311
|
-
if (!expressive) {
|
|
312
|
-
return undefined;
|
|
313
|
-
}
|
|
314
|
-
if (expressive.styleOverrides !== undefined) {
|
|
315
|
-
warnings.push(
|
|
316
|
-
"Dropped expressiveCode.styleOverrides; restyle code blocks via theme.css."
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
const themes = asArray(expressive.themes)?.filter(
|
|
320
|
-
(theme): theme is string => typeof theme === "string"
|
|
321
|
-
);
|
|
322
|
-
const [first, second] = themes ?? [];
|
|
323
|
-
if (!first) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
const dark = themes?.find((theme) => /dark/u.test(theme)) ?? first;
|
|
327
|
-
const light =
|
|
328
|
-
themes?.find((theme) => /light/u.test(theme)) ?? second ?? first;
|
|
329
|
-
return { dark, light };
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const mapHead = (
|
|
333
|
-
value: LiteralValue | undefined,
|
|
334
|
-
warnings: string[]
|
|
335
|
-
): Record<string, string> | undefined => {
|
|
336
|
-
const array = asArray(value);
|
|
337
|
-
if (!array) {
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
const metatags: Record<string, string> = {};
|
|
341
|
-
for (const entry of array) {
|
|
342
|
-
if (!isObject(entry)) {
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
const tag = asString(entry.tag);
|
|
346
|
-
const attrs = isObject(entry.attrs) ? entry.attrs : undefined;
|
|
347
|
-
const content = asString(attrs?.content);
|
|
348
|
-
const key = asString(attrs?.name) ?? asString(attrs?.property);
|
|
349
|
-
if (tag === "meta" && key && content) {
|
|
350
|
-
metatags[key] = content;
|
|
351
|
-
} else {
|
|
352
|
-
warnings.push(
|
|
353
|
-
`Dropped a <${tag ?? "?"}> head entry; re-add scripts/links via a custom layout.`
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return Object.keys(metatags).length > 0 ? metatags : undefined;
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
/** Warn about Starlight options that carry over to Blume only by hand. */
|
|
361
|
-
const warnDroppedOptions = (
|
|
362
|
-
options: StarlightOptions,
|
|
363
|
-
warnings: string[]
|
|
364
|
-
): void => {
|
|
365
|
-
if (options.customCss !== undefined) {
|
|
366
|
-
warnings.push(
|
|
367
|
-
"Dropped customCss; move your custom styles into a top-level theme.css."
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
if (isObject(options.components)) {
|
|
371
|
-
warnings.push(
|
|
372
|
-
"Dropped Starlight component overrides; re-implement them via `blume eject`."
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
if (asArray(options.plugins)?.length) {
|
|
376
|
-
warnings.push(
|
|
377
|
-
"Dropped Starlight plugins; they have no Blume equivalent and need manual review."
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
if (options.routeMiddleware !== undefined) {
|
|
381
|
-
warnings.push(
|
|
382
|
-
"Dropped routeMiddleware; re-implement any route data in Blume directly."
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Map the statically-read Starlight options onto a `BlumeConfig`. Content stays
|
|
389
|
-
* under `src/content/docs`, so the content root is pinned there. Unsupported or
|
|
390
|
-
* non-literal options are dropped with a warning rather than failing.
|
|
391
|
-
*/
|
|
392
|
-
export const mapStarlightConfig = (
|
|
393
|
-
options: StarlightOptions,
|
|
394
|
-
warnings: string[]
|
|
395
|
-
): BlumeConfig => {
|
|
396
|
-
const config: BlumeConfig = {
|
|
397
|
-
content: { root: "src/content/docs" },
|
|
398
|
-
title: asString(options.title) ?? "Documentation",
|
|
399
|
-
};
|
|
400
|
-
if (options.title !== undefined && asString(options.title) === undefined) {
|
|
401
|
-
warnings.push(
|
|
402
|
-
"Starlight title is per-locale or computed; set config.title manually."
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const description =
|
|
407
|
-
asString(options.description) ?? asString(options.tagline);
|
|
408
|
-
if (description) {
|
|
409
|
-
config.description = description;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const logo = mapLogo(options.logo, warnings);
|
|
413
|
-
if (logo) {
|
|
414
|
-
config.logo = logo;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const favicon = asString(options.favicon);
|
|
418
|
-
if (favicon) {
|
|
419
|
-
config.favicon = favicon;
|
|
420
|
-
if (!favicon.startsWith("/") && !/^https?:/u.test(favicon)) {
|
|
421
|
-
warnings.push(
|
|
422
|
-
`favicon "${favicon}" should live under public/ and be referenced from the site root.`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const social = mapSocial(options.social);
|
|
428
|
-
const github = mapEditLink(options.editLink) ?? social.github;
|
|
429
|
-
if (github) {
|
|
430
|
-
config.github = github;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const sidebar = mapSidebar(options.sidebar, warnings);
|
|
434
|
-
if (sidebar) {
|
|
435
|
-
config.navigation = { sidebar };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const theme = mapCodeTheme(options.expressiveCode, warnings);
|
|
439
|
-
if (theme) {
|
|
440
|
-
config.markdown = { codeBlocks: { theme } };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const metatags = mapHead(options.head, warnings);
|
|
444
|
-
if (metatags) {
|
|
445
|
-
config.seo = { metatags };
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (options.lastUpdated === true) {
|
|
449
|
-
config.lastModified = true;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
warnDroppedOptions(options, warnings);
|
|
453
|
-
|
|
454
|
-
return config;
|
|
455
|
-
};
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { renameTag, rewriteCallouts } from "../shared.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Source-to-source rewrites that turn Starlight-only MDX into idiomatic Blume
|
|
5
|
-
* markup. Runs once at migration time — no Starlight-aware plugins remain in the
|
|
6
|
-
* Blume runtime.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const STARLIGHT_IMPORT =
|
|
10
|
-
/^import\s+[\s\S]*?\s+from\s+["']@astrojs\/starlight(?:\/[^"']*)?["'];?[ \t]*\n?/gmu;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Drop `import … from "@astrojs/starlight/components"` (and other subpaths).
|
|
14
|
-
* Blume injects its components globally, so these imports would fail to resolve
|
|
15
|
-
* once the Starlight packages are gone.
|
|
16
|
-
*/
|
|
17
|
-
export const stripStarlightImports = (source: string): string => {
|
|
18
|
-
const stripped = source.replace(STARLIGHT_IMPORT, "");
|
|
19
|
-
// Collapse the blank gap a removed import block leaves behind.
|
|
20
|
-
return stripped === source ? source : stripped.replaceAll(/\n{3,}/gu, "\n\n");
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** Starlight `<Aside type="X">` values mapped to Blume directive names. */
|
|
24
|
-
const ASIDE_TYPE_DIRECTIVES: Record<string, string> = {
|
|
25
|
-
caution: "caution",
|
|
26
|
-
danger: "danger",
|
|
27
|
-
note: "note",
|
|
28
|
-
tip: "tip",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Convert Starlight `<Aside type="note|tip|caution|danger" title="…">`
|
|
33
|
-
* components into Blume `:::` directives. A bare `<Aside>` becomes `:::note`.
|
|
34
|
-
* (Plain `:::`-syntax asides already pass through unchanged — Blume's directive
|
|
35
|
-
* set covers all four types, with `caution` aliased to `warning`.)
|
|
36
|
-
*/
|
|
37
|
-
export const rewriteStarlightAsides = (source: string): string =>
|
|
38
|
-
rewriteCallouts(source, {
|
|
39
|
-
defaultDirective: "note",
|
|
40
|
-
tagDirectives: {},
|
|
41
|
-
tags: ["Aside"],
|
|
42
|
-
typeDirectives: ASIDE_TYPE_DIRECTIVES,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Rename Starlight container components to their Blume equivalents, then move
|
|
47
|
-
* each `<Tab>`'s `label` onto the `title` prop Blume reads. `LinkCard` folds into
|
|
48
|
-
* `Card` (which renders an `href`); its `description` has no Card prop and is
|
|
49
|
-
* dropped. `CardGrid`'s `stagger` layout flag has no equivalent and is left on
|
|
50
|
-
* the tag harmlessly (Blume ignores unknown attributes).
|
|
51
|
-
*/
|
|
52
|
-
export const rewriteStarlightComponents = (source: string): string => {
|
|
53
|
-
let out = renameTag(source, "CardGrid", "CardGroup");
|
|
54
|
-
out = renameTag(out, "LinkCard", "Card");
|
|
55
|
-
out = renameTag(out, "TabItem", "Tab");
|
|
56
|
-
// Starlight selects a tab label with `label=`; Blume's `<Tab>` uses `title=`.
|
|
57
|
-
out = out.replaceAll(
|
|
58
|
-
/(?<tab><Tab\b[^>]*?)\blabel(?<eq>\s*=)/gu,
|
|
59
|
-
"$<tab>title$<eq>"
|
|
60
|
-
);
|
|
61
|
-
return out;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** Starlight components with no drop-in Blume equivalent — flagged for review. */
|
|
65
|
-
const UNSUPPORTED_COMPONENTS = ["Code", "FileTree", "LinkButton", "Steps"];
|
|
66
|
-
|
|
67
|
-
/** Names of Starlight components in `source` that need manual attention. */
|
|
68
|
-
export const unsupportedStarlightComponents = (source: string): string[] =>
|
|
69
|
-
UNSUPPORTED_COMPONENTS.filter((name) =>
|
|
70
|
-
new RegExp(`<${name}\\b`, "u").test(source)
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const ASSET_ALIAS =
|
|
74
|
-
/(?:!\[[^\]]*\]\(|\bsrc\s*=\s*["'])(?<path>[~@]\/[^"')\s]+)/u;
|
|
75
|
-
|
|
76
|
-
/** True when the source references images via Starlight's `~/`/`@/` aliases. */
|
|
77
|
-
export const hasAliasedAssets = (source: string): boolean =>
|
|
78
|
-
ASSET_ALIAS.test(source);
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
import { stripUnknownPageMeta } from "../shared.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* One-time translation of Starlight page frontmatter into Blume's shape. Runs at
|
|
7
|
-
* migration time only — Blume's runtime page schema stays Starlight-free and
|
|
8
|
-
* strict. `sidebar.badge` flattens to a string, `pagefind:false` becomes
|
|
9
|
-
* `search.exclude`, `lastUpdated` (a date) becomes `lastModified`, and
|
|
10
|
-
* `prev`/`next:false` become `hideFooterPagination`. Starlight-only keys
|
|
11
|
-
* (`template`, `hero`, `banner`, `tableOfContents`, `editUrl`, `head`) are then
|
|
12
|
-
* dropped by the strict-schema pass and reported.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const starlightBadgeSchema = z.union([
|
|
16
|
-
z.string(),
|
|
17
|
-
z.object({ text: z.string() }).passthrough(),
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
const starlightSidebarSchema = z
|
|
21
|
-
.object({
|
|
22
|
-
badge: starlightBadgeSchema.optional(),
|
|
23
|
-
hidden: z.boolean().optional(),
|
|
24
|
-
label: z.string().optional(),
|
|
25
|
-
order: z.number().optional(),
|
|
26
|
-
})
|
|
27
|
-
.passthrough();
|
|
28
|
-
|
|
29
|
-
const starlightPageMetaInputSchema = z
|
|
30
|
-
.object({
|
|
31
|
-
lastUpdated: z.union([z.date(), z.string(), z.boolean()]).optional(),
|
|
32
|
-
next: z.unknown().optional(),
|
|
33
|
-
pagefind: z.boolean().optional(),
|
|
34
|
-
prev: z.unknown().optional(),
|
|
35
|
-
sidebar: starlightSidebarSchema.optional(),
|
|
36
|
-
})
|
|
37
|
-
.passthrough();
|
|
38
|
-
|
|
39
|
-
type StarlightPageMeta = z.infer<typeof starlightPageMetaInputSchema>;
|
|
40
|
-
|
|
41
|
-
const normalizedSidebar = (
|
|
42
|
-
sidebar: NonNullable<StarlightPageMeta["sidebar"]>
|
|
43
|
-
): Record<string, unknown> => {
|
|
44
|
-
const out: Record<string, unknown> = {};
|
|
45
|
-
if (sidebar.label !== undefined) {
|
|
46
|
-
out.label = sidebar.label;
|
|
47
|
-
}
|
|
48
|
-
if (sidebar.order !== undefined) {
|
|
49
|
-
out.order = sidebar.order;
|
|
50
|
-
}
|
|
51
|
-
if (sidebar.hidden !== undefined) {
|
|
52
|
-
out.hidden = sidebar.hidden;
|
|
53
|
-
}
|
|
54
|
-
const badge =
|
|
55
|
-
typeof sidebar.badge === "string" ? sidebar.badge : sidebar.badge?.text;
|
|
56
|
-
if (badge !== undefined) {
|
|
57
|
-
out.badge = badge;
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Map Starlight page frontmatter onto Blume's frontmatter shape, returning the
|
|
64
|
-
* cleaned data plus any unknown keys dropped to satisfy the strict page schema.
|
|
65
|
-
*/
|
|
66
|
-
export const normalizeStarlightPageMeta = (
|
|
67
|
-
value: unknown
|
|
68
|
-
): { data: Record<string, unknown>; removed: string[] } => {
|
|
69
|
-
const parsed = starlightPageMetaInputSchema.safeParse(value);
|
|
70
|
-
if (!parsed.success || typeof value !== "object" || value === null) {
|
|
71
|
-
return stripUnknownPageMeta((value ?? {}) as Record<string, unknown>);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const meta = parsed.data;
|
|
75
|
-
const data: Record<string, unknown> = {
|
|
76
|
-
...(value as Record<string, unknown>),
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
if (meta.sidebar) {
|
|
80
|
-
const sidebar = normalizedSidebar(meta.sidebar);
|
|
81
|
-
if (Object.keys(sidebar).length > 0) {
|
|
82
|
-
data.sidebar = sidebar;
|
|
83
|
-
} else {
|
|
84
|
-
delete data.sidebar;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (meta.pagefind === false) {
|
|
89
|
-
const existing =
|
|
90
|
-
typeof data.search === "object" && data.search !== null
|
|
91
|
-
? (data.search as Record<string, unknown>)
|
|
92
|
-
: {};
|
|
93
|
-
data.search = { ...existing, exclude: true };
|
|
94
|
-
}
|
|
95
|
-
delete data.pagefind;
|
|
96
|
-
|
|
97
|
-
if (meta.lastUpdated instanceof Date) {
|
|
98
|
-
data.lastModified = meta.lastUpdated.toISOString();
|
|
99
|
-
} else if (typeof meta.lastUpdated === "string") {
|
|
100
|
-
data.lastModified = meta.lastUpdated;
|
|
101
|
-
}
|
|
102
|
-
delete data.lastUpdated;
|
|
103
|
-
|
|
104
|
-
if (meta.prev === false || meta.next === false) {
|
|
105
|
-
data.hideFooterPagination = true;
|
|
106
|
-
}
|
|
107
|
-
delete data.prev;
|
|
108
|
-
delete data.next;
|
|
109
|
-
|
|
110
|
-
return stripUnknownPageMeta(data);
|
|
111
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { BlumeConfig } from "../../core/schema.ts";
|
|
2
|
-
import type { LiteralValue } from "../shared.ts";
|
|
3
|
-
import { asLiteralString, isLiteralObject } from "../shared.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Map Starlight's `defaultLocale` + `locales` onto Blume's `i18n` config.
|
|
7
|
-
* Starlight keys locales by URL segment (`root` for the un-prefixed default) and
|
|
8
|
-
* carries the BCP-47 tag in `lang`; Blume keys by `code`. The `root` locale maps
|
|
9
|
-
* to the default with `hideDefaultLocalePrefix`, and locale directories under
|
|
10
|
-
* `src/content/docs` line up with Blume's `"dir"` parser.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
type I18nConfig = NonNullable<BlumeConfig["i18n"]>;
|
|
14
|
-
|
|
15
|
-
export const starlightI18n = (
|
|
16
|
-
options: Record<string, LiteralValue>
|
|
17
|
-
): I18nConfig | undefined => {
|
|
18
|
-
const locales = isLiteralObject(options.locales)
|
|
19
|
-
? options.locales
|
|
20
|
-
: undefined;
|
|
21
|
-
if (!locales) {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const entries: { code: string; label: string }[] = [];
|
|
26
|
-
let rootCode: string | undefined;
|
|
27
|
-
for (const [key, value] of Object.entries(locales)) {
|
|
28
|
-
const entry = isLiteralObject(value) ? value : undefined;
|
|
29
|
-
const lang = asLiteralString(entry?.lang);
|
|
30
|
-
const label = asLiteralString(entry?.label) ?? key;
|
|
31
|
-
const code = key === "root" ? (lang ?? "en") : (lang ?? key);
|
|
32
|
-
if (key === "root") {
|
|
33
|
-
rootCode = code;
|
|
34
|
-
}
|
|
35
|
-
entries.push({ code, label });
|
|
36
|
-
}
|
|
37
|
-
if (entries.length === 0) {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const requested = asLiteralString(options.defaultLocale);
|
|
42
|
-
const fallback = rootCode ?? entries[0]?.code ?? "en";
|
|
43
|
-
const preferred =
|
|
44
|
-
requested === undefined || requested === "root" ? fallback : requested;
|
|
45
|
-
const codes = new Set(entries.map((locale) => locale.code));
|
|
46
|
-
const defaultLocale = codes.has(preferred) ? preferred : fallback;
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
defaultLocale,
|
|
50
|
-
hideDefaultLocalePrefix: true,
|
|
51
|
-
locales: entries,
|
|
52
|
-
parser: "dir",
|
|
53
|
-
};
|
|
54
|
-
};
|