blume 0.1.0 → 0.1.1
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 +1162 -455
- package/dist/cli/index.js.map +32 -28
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3451 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/advanced/custom-pages.mdx +91 -10
- package/docs/configuration/index.mdx +19 -0
- package/docs/content/components.mdx +87 -1
- package/package.json +13 -3
- package/src/ai/llms.ts +1 -2
- package/src/astro/examples.ts +104 -0
- package/src/astro/generate.ts +197 -69
- package/src/astro/islands.ts +1 -1
- package/src/astro/pages.ts +59 -0
- package/src/astro/templates.ts +136 -9
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Tab.astro +8 -2
- package/src/components/content/diff.ts +95 -0
- package/src/components/layout/Favicon.astro +11 -4
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +9 -5
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/core/data.ts +128 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/links.ts +26 -10
- package/src/core/sources/filesystem.ts +1 -1
- package/src/core/sources/mdx-remote.ts +1 -2
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +19 -3
- package/src/core/sources/notion.ts +1 -1
- package/src/core/sources/sanity.ts +1 -1
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/index.ts +12 -0
- package/src/markdown/index.ts +69 -0
- package/src/migrate/fumadocs/config.ts +53 -4
- package/src/migrate/fumadocs/content.ts +1 -1
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +104 -19
- package/src/migrate/fumadocs/meta.ts +140 -10
- package/src/migrate/mintlify/snippets.ts +2 -1
- package/src/migrate/mintlify/transform.ts +1 -2
- package/src/migrate/nextra/index.ts +1 -1
- package/src/migrate/shared.ts +98 -1
- package/src/migrate/starlight/index.ts +1 -1
- package/src/registry/eject.ts +57 -10
- package/src/search/documents.ts +1 -2
- package/src/theme/entry.ts +29 -0
package/src/astro/generate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
|
+
lstat,
|
|
3
4
|
mkdir,
|
|
4
5
|
readFile,
|
|
5
6
|
rename,
|
|
@@ -17,11 +18,18 @@ import { resolveAskBackend } from "../ai/ask.ts";
|
|
|
17
18
|
import { buildRawMarkdown } from "../ai/markdown.ts";
|
|
18
19
|
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
19
20
|
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
21
|
+
import type {
|
|
22
|
+
BlumeBanner,
|
|
23
|
+
BlumeData,
|
|
24
|
+
BlumeFavicon,
|
|
25
|
+
BlumeLogo,
|
|
26
|
+
} from "../core/data.ts";
|
|
20
27
|
import { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
|
|
21
28
|
import { resolveFallbackLocale } from "../core/i18n.ts";
|
|
22
29
|
import { packageRoot } from "../core/package-root.ts";
|
|
23
30
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
24
31
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
32
|
+
import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
|
|
25
33
|
import type { Navigation } from "../core/types.ts";
|
|
26
34
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
27
35
|
import {
|
|
@@ -35,8 +43,9 @@ import { tailwindEntryTemplate } from "../theme/entry.ts";
|
|
|
35
43
|
import { buildFontsCss, configuredCssVars } from "../theme/fonts.ts";
|
|
36
44
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
37
45
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
46
|
+
import { discoverExamples } from "./examples.ts";
|
|
38
47
|
import { discoverIslands } from "./islands.ts";
|
|
39
|
-
import { discoverPages } from "./pages.ts";
|
|
48
|
+
import { customOgRoutes, discoverPages } from "./pages.ts";
|
|
40
49
|
import {
|
|
41
50
|
askEndpointTemplate,
|
|
42
51
|
astroConfigTemplate,
|
|
@@ -44,6 +53,9 @@ import {
|
|
|
44
53
|
changelogIndexTemplate,
|
|
45
54
|
contentConfigTemplate,
|
|
46
55
|
envTemplate,
|
|
56
|
+
exampleMapTemplate,
|
|
57
|
+
exampleWrapperTemplate,
|
|
58
|
+
exampleSlug,
|
|
47
59
|
islandMapTemplate,
|
|
48
60
|
islandWrapperTemplate,
|
|
49
61
|
mcpEndpointTemplate,
|
|
@@ -64,8 +76,6 @@ import {
|
|
|
64
76
|
|
|
65
77
|
/** Absolute path to the Blume package `src` directory. */
|
|
66
78
|
const BLUME_SRC = join(packageRoot(), "src");
|
|
67
|
-
/** The Blume package's own `node_modules` (where Astro and friends live). */
|
|
68
|
-
const BLUME_NODE_MODULES = join(packageRoot(), "node_modules");
|
|
69
79
|
|
|
70
80
|
/** Whether a module specifier resolves from a directory via node resolution. */
|
|
71
81
|
const canResolveFrom = (fromDir: string, spec: string): boolean => {
|
|
@@ -77,29 +87,97 @@ const canResolveFrom = (fromDir: string, spec: string): boolean => {
|
|
|
77
87
|
}
|
|
78
88
|
};
|
|
79
89
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Realpath of the `astro` package node resolves from a directory, or null when
|
|
92
|
+
* none resolves. Comparing this for `.blume/` against Blume's own deps tells
|
|
93
|
+
* whether the runtime would bind to the *same* astro Blume uses or a different
|
|
94
|
+
* one shadowing it (the hoisted-conflict failure mode).
|
|
95
|
+
*/
|
|
96
|
+
const resolvedAstroPath = (fromDir: string): string | null => {
|
|
97
|
+
try {
|
|
98
|
+
const pkg = createRequire(
|
|
99
|
+
pathToFileURL(join(fromDir, "_.js")).href
|
|
100
|
+
).resolve("astro/package.json");
|
|
101
|
+
return realpathSync(pkg);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Locate the directory that holds Blume's installed dependencies (Astro and its
|
|
109
|
+
* integrations).
|
|
110
|
+
*
|
|
111
|
+
* With a clean hoisted install this is moot — the deps sit in a `node_modules`
|
|
112
|
+
* the generated `.blume/` already walks up into, and {@link ensureDepsLink}
|
|
113
|
+
* short-circuits before we need it. But under isolated linkers (Bun's
|
|
114
|
+
* `isolated` mode, pnpm) Blume's deps are NOT hoisted into the project; they
|
|
115
|
+
* live beside the Blume package in a virtual store, invisible to the upward
|
|
116
|
+
* walk from `.blume/`. Two layouts are possible, so probe for `astro`:
|
|
117
|
+
* - `<blume>/node_modules` — deps nested under the package (workspace source)
|
|
118
|
+
* - `dirname(<blume>)` — deps as siblings in the store (isolated/pnpm)
|
|
119
|
+
*
|
|
120
|
+
* `packageRoot()` resolves to Blume's real on-disk path (Node follows the
|
|
121
|
+
* install symlink), so its parent is the store's package directory where the
|
|
122
|
+
* isolated linker places the siblings. The previous fixed
|
|
123
|
+
* `packageRoot()/node_modules` assumption missed the sibling layout entirely,
|
|
124
|
+
* which is why isolated-linker projects had to redeclare Blume's deps by hand.
|
|
125
|
+
*/
|
|
126
|
+
export const blumeDepsDir = (pkgDir: string = packageRoot()): string | null => {
|
|
127
|
+
const candidates = [join(pkgDir, "node_modules"), dirname(pkgDir)];
|
|
128
|
+
return candidates.find((dir) => existsSync(join(dir, "astro"))) ?? null;
|
|
129
|
+
};
|
|
83
130
|
|
|
84
131
|
/**
|
|
85
|
-
* Make the generated runtime resolve Astro and its integrations
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
132
|
+
* Make the generated runtime resolve Astro and its integrations against Blume's
|
|
133
|
+
* own dependency set. Two failure modes this repairs:
|
|
134
|
+
*
|
|
135
|
+
* - Astro is *unreachable* from `.blume/` (workspaces under isolated linkers,
|
|
136
|
+
* pnpm) — the deps live in a store the upward walk can't see.
|
|
137
|
+
* - Astro *resolves to the wrong copy* — a hoisted sibling pinned an older
|
|
138
|
+
* major (e.g. `astro@6` for a type-only import) that shadows Blume's
|
|
139
|
+
* `astro@7`, so `@astrojs/mdx@7` binds to it and crashes the build on a
|
|
140
|
+
* missing export. Resolving merely *an* astro isn't enough; it must be the
|
|
141
|
+
* same one Blume uses.
|
|
142
|
+
*
|
|
143
|
+
* In both cases we symlink Blume's dependency directory in as
|
|
144
|
+
* `.blume/node_modules` so the generated config's bare specifiers (`astro`,
|
|
145
|
+
* `@astrojs/mdx`, …) bind to the matching set. We only do this when those deps
|
|
146
|
+
* are a *co-located, consistent* set (astro beside the `@astrojs/mdx` that binds
|
|
147
|
+
* to it). A split layout — an integration hoisted away from a conflicting astro
|
|
148
|
+
* — can't be made consistent by a single symlink and needs a root `overrides`/
|
|
149
|
+
* `resolutions` pin instead, so we leave it untouched rather than half-fix it.
|
|
89
150
|
*/
|
|
90
|
-
const ensureDepsLink = async (
|
|
91
|
-
|
|
151
|
+
export const ensureDepsLink = async (
|
|
152
|
+
outDir: string,
|
|
153
|
+
pkgDir: string = packageRoot()
|
|
154
|
+
): Promise<void> => {
|
|
155
|
+
const depsDir = blumeDepsDir(pkgDir);
|
|
156
|
+
if (!depsDir || !existsSync(join(depsDir, "@astrojs", "mdx"))) {
|
|
92
157
|
return;
|
|
93
158
|
}
|
|
94
|
-
|
|
159
|
+
// Already correct when `.blume/` resolves the very same astro Blume's deps
|
|
160
|
+
// provide — the clean hoisted case. Otherwise (unreachable, or a different
|
|
161
|
+
// astro shadowing Blume's) link Blume's deps in.
|
|
162
|
+
const blumeAstro = resolvedAstroPath(depsDir);
|
|
163
|
+
if (blumeAstro && resolvedAstroPath(outDir) === blumeAstro) {
|
|
95
164
|
return;
|
|
96
165
|
}
|
|
97
166
|
const link = join(outDir, "node_modules");
|
|
98
|
-
|
|
99
|
-
|
|
167
|
+
// `lstat`, not `existsSync`, so a broken junction (target since moved) is
|
|
168
|
+
// still detected — `existsSync` follows the link and reports a dangling one
|
|
169
|
+
// as absent. Reaching here means `outDir` doesn't resolve Blume's astro, so
|
|
170
|
+
// any existing link is stale: replace a link we own (a junction/symlink) and
|
|
171
|
+
// leave a real directory untouched.
|
|
172
|
+
const existing = await lstat(link).catch(() => null);
|
|
173
|
+
if (existing) {
|
|
174
|
+
if (!existing.isSymbolicLink()) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await rm(link, { force: true });
|
|
100
178
|
}
|
|
101
179
|
await mkdir(outDir, { recursive: true });
|
|
102
|
-
await symlink(
|
|
180
|
+
await symlink(depsDir, link, "junction");
|
|
103
181
|
};
|
|
104
182
|
|
|
105
183
|
/** Astro integration package each non-React island framework needs installed. */
|
|
@@ -243,20 +321,12 @@ const writeStagedContent = async (
|
|
|
243
321
|
};
|
|
244
322
|
|
|
245
323
|
/** The logo shape the runtime consumes: an inline SVG or image URL(s). */
|
|
246
|
-
interface ResolvedLogo {
|
|
247
|
-
svg?: string;
|
|
248
|
-
light?: string;
|
|
249
|
-
dark?: string;
|
|
250
|
-
alt: string;
|
|
251
|
-
href: string;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
324
|
/**
|
|
255
325
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
256
326
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
257
327
|
* `<img>`. The file is looked up under `public/` and the project root.
|
|
258
328
|
*/
|
|
259
|
-
const resolveLogo = (project: BlumeProject):
|
|
329
|
+
const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
260
330
|
const { logo } = project.config;
|
|
261
331
|
if (!logo) {
|
|
262
332
|
return null;
|
|
@@ -280,12 +350,6 @@ const resolveLogo = (project: BlumeProject): ResolvedLogo | null => {
|
|
|
280
350
|
return { alt, dark, href, light };
|
|
281
351
|
};
|
|
282
352
|
|
|
283
|
-
/** The favicon shape the runtime consumes: a link href plus optional MIME type. */
|
|
284
|
-
interface ResolvedFavicon {
|
|
285
|
-
href: string;
|
|
286
|
-
type?: string;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
353
|
/**
|
|
290
354
|
* Favicon filenames Blume auto-detects, in priority order. Mirrors the Next.js
|
|
291
355
|
* convention: an `icon.*` or `favicon.*` file in `public/` or the project root
|
|
@@ -303,6 +367,8 @@ const FAVICON_CANDIDATES = [
|
|
|
303
367
|
/** `<link type>` MIME for the favicon extensions we recognize. */
|
|
304
368
|
const FAVICON_TYPES: Record<string, string> = {
|
|
305
369
|
ico: "image/x-icon",
|
|
370
|
+
jpeg: "image/jpeg",
|
|
371
|
+
jpg: "image/jpeg",
|
|
306
372
|
png: "image/png",
|
|
307
373
|
svg: "image/svg+xml",
|
|
308
374
|
};
|
|
@@ -318,45 +384,67 @@ const inlineDataUri = (file: string, type: string): string =>
|
|
|
318
384
|
`data:${type};base64,${readFileSync(file).toString("base64")}`;
|
|
319
385
|
|
|
320
386
|
/** The bundled Blume favicon, inlined as a data URI so it needs no public file. */
|
|
321
|
-
const defaultFavicon = ():
|
|
387
|
+
const defaultFavicon = (): BlumeFavicon => ({
|
|
322
388
|
href: inlineDataUri(join(BLUME_SRC, "assets", "icon.png"), "image/png"),
|
|
323
389
|
type: "image/png",
|
|
324
390
|
});
|
|
325
391
|
|
|
326
392
|
/**
|
|
327
|
-
*
|
|
328
|
-
* `
|
|
329
|
-
*
|
|
330
|
-
*
|
|
393
|
+
* Apple touch icon filenames Blume auto-detects, in priority order. Mirrors the
|
|
394
|
+
* Next.js `apple-icon.*` convention (plus the `apple-touch-icon.png` most favicon
|
|
395
|
+
* generators emit): a match in `public/` or the project root becomes the iOS
|
|
396
|
+
* home-screen icon, no config required.
|
|
331
397
|
*/
|
|
332
|
-
const
|
|
398
|
+
const APPLE_ICON_CANDIDATES = [
|
|
399
|
+
"apple-icon.png",
|
|
400
|
+
"apple-icon.jpg",
|
|
401
|
+
"apple-icon.jpeg",
|
|
402
|
+
"apple-touch-icon.png",
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Resolve an icon file by convention. A candidate in `public/` is served as-is
|
|
407
|
+
* and referenced by URL; one at the project root is inlined as a data URI (the
|
|
408
|
+
* root isn't a served directory). Returns null when the project ships none.
|
|
409
|
+
*/
|
|
410
|
+
const resolveIconFile = (
|
|
411
|
+
project: BlumeProject,
|
|
412
|
+
candidates: string[]
|
|
413
|
+
): BlumeFavicon | null => {
|
|
333
414
|
const { root } = project.context;
|
|
334
|
-
for (const name of
|
|
415
|
+
for (const name of candidates) {
|
|
335
416
|
if (existsSync(join(root, "public", name))) {
|
|
336
417
|
return { href: `/${name}`, type: faviconType(name) };
|
|
337
418
|
}
|
|
338
419
|
}
|
|
339
|
-
for (const name of
|
|
420
|
+
for (const name of candidates) {
|
|
340
421
|
const file = join(root, name);
|
|
341
422
|
if (existsSync(file)) {
|
|
342
423
|
const type = faviconType(name);
|
|
343
424
|
return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
|
|
344
425
|
}
|
|
345
426
|
}
|
|
346
|
-
return
|
|
427
|
+
return null;
|
|
347
428
|
};
|
|
348
429
|
|
|
349
|
-
/**
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Resolve the site favicon by convention, falling back to the bundled Blume mark
|
|
432
|
+
* when the project ships no `icon.*`/`favicon.*` file.
|
|
433
|
+
*/
|
|
434
|
+
const resolveFavicon = (project: BlumeProject): BlumeFavicon =>
|
|
435
|
+
resolveIconFile(project, FAVICON_CANDIDATES) ?? defaultFavicon();
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Resolve the Apple touch icon by convention, or null when the project ships
|
|
439
|
+
* none (unlike the favicon, there's no bundled default). Note: iOS ignores
|
|
440
|
+
* `data:`-URI apple-touch-icons, so a `public/` file (served by URL) is the
|
|
441
|
+
* reliable path; a root-level file is still inlined for symmetry with favicons.
|
|
442
|
+
*/
|
|
443
|
+
const resolveAppleIcon = (project: BlumeProject): BlumeFavicon | null =>
|
|
444
|
+
resolveIconFile(project, APPLE_ICON_CANDIDATES);
|
|
357
445
|
|
|
358
446
|
/** Normalize the banner config (string shorthand or object) for the runtime. */
|
|
359
|
-
const resolveBanner = (config: ResolvedConfig):
|
|
447
|
+
const resolveBanner = (config: ResolvedConfig): BlumeBanner | null => {
|
|
360
448
|
const { banner } = config;
|
|
361
449
|
if (!banner) {
|
|
362
450
|
return null;
|
|
@@ -436,9 +524,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
436
524
|
)
|
|
437
525
|
: {};
|
|
438
526
|
|
|
439
|
-
const data = {
|
|
527
|
+
const data: BlumeData = {
|
|
440
528
|
config: {
|
|
441
529
|
analytics: config.analytics ?? null,
|
|
530
|
+
appleIcon: resolveAppleIcon(project),
|
|
442
531
|
banner: resolveBanner(config),
|
|
443
532
|
codeWrap: config.markdown.code.wrap,
|
|
444
533
|
description: config.description,
|
|
@@ -463,7 +552,9 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
463
552
|
mcp: config.mcp.enabled
|
|
464
553
|
? { name: config.mcp.name ?? config.title, route: config.mcp.route }
|
|
465
554
|
: null,
|
|
466
|
-
|
|
555
|
+
// `og.enabled` is resolved to a definite boolean in `loadConfig`; coerce
|
|
556
|
+
// the optional schema type so the serialized shape stays `boolean`.
|
|
557
|
+
og: { enabled: config.seo.og.enabled ?? false },
|
|
467
558
|
repoUrl,
|
|
468
559
|
search: {
|
|
469
560
|
enabled: config.search.provider !== "none",
|
|
@@ -619,6 +710,7 @@ export const generateRuntime = async (
|
|
|
619
710
|
const dataPath = join(srcDir, "generated", "data.json");
|
|
620
711
|
const themePath = join(srcDir, "generated", "app.css");
|
|
621
712
|
const searchClientPath = join(srcDir, "generated", "search-client.ts");
|
|
713
|
+
const examplesPath = join(srcDir, "generated", "examples.ts");
|
|
622
714
|
|
|
623
715
|
// Record every file this pass writes so orphans (from a now-disabled feature)
|
|
624
716
|
// can be pruned afterwards. `write` wraps the atomic writer and tracks paths.
|
|
@@ -633,21 +725,31 @@ export const generateRuntime = async (
|
|
|
633
725
|
const askEnabled = config.ai.ask?.enabled ?? false;
|
|
634
726
|
const exportPdf = config.export.pdf;
|
|
635
727
|
const exportEpub = config.export.epub;
|
|
636
|
-
const [pages, detectedReact, userTheme, islandDiscovery] =
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
728
|
+
const [pages, detectedReact, userTheme, islandDiscovery, exampleDiscovery] =
|
|
729
|
+
await Promise.all([
|
|
730
|
+
context.pagesRoot
|
|
731
|
+
? discoverPages(context.pagesRoot)
|
|
732
|
+
: Promise.resolve([]),
|
|
733
|
+
detectNeedsReact(context.root),
|
|
734
|
+
readOptional(context.themeFile),
|
|
735
|
+
discoverIslands(context.root),
|
|
736
|
+
discoverExamples(context.root),
|
|
737
|
+
]);
|
|
738
|
+
// Each island/example framework enables its Astro renderer. React also
|
|
739
|
+
// switches on for any project `.tsx`/`.jsx` and for Ask AI; Vue/Svelte are
|
|
740
|
+
// island/example-driven. `.astro` examples need no renderer.
|
|
741
|
+
const frameworks = new Set<string>([
|
|
742
|
+
...islandDiscovery.islands.map((island) => island.framework),
|
|
743
|
+
...exampleDiscovery.examples.map((example) => example.framework),
|
|
641
744
|
]);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
const needsSvelte = islandFrameworks.has("svelte");
|
|
745
|
+
const needsReact = detectedReact || askEnabled || frameworks.has("react");
|
|
746
|
+
const needsVue = frameworks.has("vue");
|
|
747
|
+
const needsSvelte = frameworks.has("svelte");
|
|
748
|
+
|
|
749
|
+
// Custom pages that should get a generated OG card (the home most of all).
|
|
750
|
+
// Computed before the MCP `.well-known` routes are appended below — those are
|
|
751
|
+
// private and filtered out anyway, but the intent is the user's pages.
|
|
752
|
+
const ogRoutes = customOgRoutes(pages, config.title, config.description);
|
|
651
753
|
|
|
652
754
|
// The hosted MCP server. The `.well-known` discovery docs are injected as
|
|
653
755
|
// prerendered routes alongside user pages; the server endpoint itself is a
|
|
@@ -664,10 +766,12 @@ export const generateRuntime = async (
|
|
|
664
766
|
write(
|
|
665
767
|
join(out, "astro.config.mjs"),
|
|
666
768
|
astroConfigTemplate({
|
|
769
|
+
aliases: resolveTsconfigAliases(context.root),
|
|
667
770
|
config,
|
|
668
771
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
669
772
|
context,
|
|
670
773
|
dataPath,
|
|
774
|
+
examplesPath,
|
|
671
775
|
needsReact,
|
|
672
776
|
needsSvelte,
|
|
673
777
|
needsVue,
|
|
@@ -705,6 +809,10 @@ export const generateRuntime = async (
|
|
|
705
809
|
join(srcDir, "generated", "islands.ts"),
|
|
706
810
|
islandMapTemplate(islandDiscovery.islands)
|
|
707
811
|
),
|
|
812
|
+
write(
|
|
813
|
+
join(srcDir, "generated", "examples.ts"),
|
|
814
|
+
exampleMapTemplate(exampleDiscovery.examples)
|
|
815
|
+
),
|
|
708
816
|
write(
|
|
709
817
|
themePath,
|
|
710
818
|
tailwindEntryTemplate({
|
|
@@ -731,6 +839,22 @@ export const generateRuntime = async (
|
|
|
731
839
|
)
|
|
732
840
|
);
|
|
733
841
|
|
|
842
|
+
// Per-example live wrappers for the `examples/` convention, resolved by
|
|
843
|
+
// `<Component path>` through the `examples.ts` map (written above, always).
|
|
844
|
+
await Promise.all(
|
|
845
|
+
exampleDiscovery.examples.map((example) =>
|
|
846
|
+
write(
|
|
847
|
+
join(
|
|
848
|
+
srcDir,
|
|
849
|
+
"generated",
|
|
850
|
+
"examples",
|
|
851
|
+
`${exampleSlug(example.path)}.astro`
|
|
852
|
+
),
|
|
853
|
+
exampleWrapperTemplate(example)
|
|
854
|
+
)
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
|
|
734
858
|
if (askEnabled) {
|
|
735
859
|
await write(
|
|
736
860
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
@@ -743,7 +867,7 @@ export const generateRuntime = async (
|
|
|
743
867
|
if (config.seo.og.enabled) {
|
|
744
868
|
await write(
|
|
745
869
|
join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
746
|
-
ogEndpointTemplate()
|
|
870
|
+
ogEndpointTemplate(ogRoutes)
|
|
747
871
|
);
|
|
748
872
|
}
|
|
749
873
|
|
|
@@ -827,7 +951,11 @@ export const generateRuntime = async (
|
|
|
827
951
|
|
|
828
952
|
// API/AsyncAPI reference pages (Scalar). One self-contained page per source,
|
|
829
953
|
// mounted on its configured route and regenerated each run.
|
|
830
|
-
const warnings: string[] = [
|
|
954
|
+
const warnings: string[] = [
|
|
955
|
+
...mcp.warnings,
|
|
956
|
+
...islandDiscovery.warnings,
|
|
957
|
+
...exampleDiscovery.warnings,
|
|
958
|
+
];
|
|
831
959
|
|
|
832
960
|
// The new provider SDKs are optional peers; warn (rather than fail opaquely in
|
|
833
961
|
// Vite) when the configured provider's package isn't installed.
|
|
@@ -841,7 +969,7 @@ export const generateRuntime = async (
|
|
|
841
969
|
|
|
842
970
|
// React ships with Blume; Vue/Svelte islands need their Astro integration
|
|
843
971
|
// installed by the project. Warn early rather than let Vite fail to resolve it.
|
|
844
|
-
warnings.push(...islandFrameworkWarnings(
|
|
972
|
+
warnings.push(...islandFrameworkWarnings(frameworks, context.root));
|
|
845
973
|
if (hasReferences(config)) {
|
|
846
974
|
const references = await buildReferenceFiles({
|
|
847
975
|
config,
|
package/src/astro/islands.ts
CHANGED
|
@@ -53,7 +53,7 @@ const ISLAND_FILE = /\.(?<ext>jsx|svelte|tsx|vue)$/u;
|
|
|
53
53
|
const CLIENT_EXPORT =
|
|
54
54
|
/export\s+const\s+client\s*(?::[^=]+)?=\s*["'](?<mode>\w+)["']/u;
|
|
55
55
|
|
|
56
|
-
const readClientMode = (
|
|
56
|
+
export const readClientMode = (
|
|
57
57
|
source: string,
|
|
58
58
|
file: string,
|
|
59
59
|
warnings: string[]
|
package/src/astro/pages.ts
CHANGED
|
@@ -26,3 +26,62 @@ export const discoverPages = async (
|
|
|
26
26
|
return { entrypoint: file, pattern };
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
|
+
|
|
30
|
+
/** A custom-page route that should get a generated OG card. */
|
|
31
|
+
export interface OgCustomRoute {
|
|
32
|
+
/** `og/<slug>.png` path segment; `index` for the site root. */
|
|
33
|
+
slug: string;
|
|
34
|
+
/** Card title. */
|
|
35
|
+
title: string;
|
|
36
|
+
/** Small eyebrow line above the title. */
|
|
37
|
+
eyebrow?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Skip private (`_partial`, `.well-known`) and Astro dynamic (`[param]`) parts. */
|
|
41
|
+
const PRIVATE_SEGMENT = /^[._]/u;
|
|
42
|
+
|
|
43
|
+
const humanizeSegment = (segment: string): string =>
|
|
44
|
+
segment
|
|
45
|
+
.split(/[-_]/u)
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
48
|
+
.join(" ");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pick the custom-page routes that should get a generated Open Graph card, with
|
|
52
|
+
* the card's slug and text. OG is otherwise content-route only, so a custom page
|
|
53
|
+
* — most importantly the landing `/`, the most-shared URL — would have no card.
|
|
54
|
+
*
|
|
55
|
+
* Dynamic (`[param]`) routes and private segments (`_partials`, `.well-known`)
|
|
56
|
+
* are skipped: they aren't shareable pages. The home is titled with the site
|
|
57
|
+
* title (eyebrow: the site description); a deeper page is titled from its last
|
|
58
|
+
* path segment.
|
|
59
|
+
*/
|
|
60
|
+
export const customOgRoutes = (
|
|
61
|
+
pages: BlumePageRoute[],
|
|
62
|
+
siteTitle: string,
|
|
63
|
+
siteDescription?: string
|
|
64
|
+
): OgCustomRoute[] => {
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
const routes: OgCustomRoute[] = [];
|
|
67
|
+
for (const { pattern } of pages) {
|
|
68
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
69
|
+
if (
|
|
70
|
+
segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("["))
|
|
71
|
+
) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const slug = segments.length === 0 ? "index" : segments.join("/");
|
|
75
|
+
if (seen.has(slug)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
seen.add(slug);
|
|
79
|
+
const last = segments.at(-1);
|
|
80
|
+
routes.push(
|
|
81
|
+
last
|
|
82
|
+
? { eyebrow: siteTitle, slug, title: humanizeSegment(last) }
|
|
83
|
+
: { eyebrow: siteDescription, slug, title: siteTitle }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return routes;
|
|
87
|
+
};
|