blume 0.1.0 → 0.1.2
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 +1213 -473
- package/dist/cli/index.js.map +33 -29
- 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 +3461 -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 +103 -1
- package/package.json +13 -3
- package/src/ai/llms.ts +1 -2
- package/src/astro/examples.ts +106 -0
- package/src/astro/generate.ts +300 -75
- package/src/astro/index.ts +1 -1
- package/src/astro/islands.ts +1 -1
- package/src/astro/pages.ts +59 -0
- package/src/astro/templates.ts +180 -22
- 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/schema.ts +8 -0
- 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,
|
|
@@ -10,18 +11,25 @@ import {
|
|
|
10
11
|
import { createRequire } from "node:module";
|
|
11
12
|
import { pathToFileURL } from "node:url";
|
|
12
13
|
|
|
13
|
-
import { dirname, join, normalize, relative } from "pathe";
|
|
14
|
+
import { basename, dirname, join, normalize, relative } from "pathe";
|
|
14
15
|
import { glob } from "tinyglobby";
|
|
15
16
|
|
|
16
17
|
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,31 +87,195 @@ 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
|
+
};
|
|
83
106
|
|
|
84
107
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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.
|
|
89
125
|
*/
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Point `link` at Blume's dependency directory via a `node_modules` junction,
|
|
133
|
+
* replacing a stale junction we own and leaving a real directory untouched.
|
|
134
|
+
*
|
|
135
|
+
* `lstat`, not `existsSync`, so a broken junction (target since moved) is still
|
|
136
|
+
* detected — `existsSync` follows the link and reports a dangling one as absent.
|
|
137
|
+
*/
|
|
138
|
+
const linkDepsJunction = async (
|
|
139
|
+
link: string,
|
|
140
|
+
depsDir: string
|
|
141
|
+
): Promise<void> => {
|
|
142
|
+
const existing = await lstat(link).catch(() => null);
|
|
143
|
+
if (existing) {
|
|
144
|
+
if (!existing.isSymbolicLink()) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await rm(link, { force: true });
|
|
93
148
|
}
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
await mkdir(dirname(link), { recursive: true });
|
|
150
|
+
await symlink(depsDir, link, "junction");
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Read the `version` field of a `package.json`, or null when unreadable. */
|
|
154
|
+
const readPkgVersion = (pkgJsonPath: string | null): string | null => {
|
|
155
|
+
if (!pkgJsonPath) {
|
|
156
|
+
return null;
|
|
96
157
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(readFileSync(pkgJsonPath, "utf-8")).version ?? null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
100
162
|
}
|
|
101
|
-
await mkdir(outDir, { recursive: true });
|
|
102
|
-
await symlink(BLUME_NODE_MODULES, link, "junction");
|
|
103
163
|
};
|
|
104
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Build the diagnostic for a split-layout Astro conflict that a symlink can't
|
|
167
|
+
* repair: a different Astro is hoisted to the project root, shadowing Blume's,
|
|
168
|
+
* and `@astrojs/mdx` binds to the wrong copy. `blumeAstroPkg`/`shadowAstroPkg`
|
|
169
|
+
* are the resolved `astro/package.json` paths for Blume's set and the one the
|
|
170
|
+
* runtime actually resolves.
|
|
171
|
+
*/
|
|
172
|
+
const astroConflictWarning = (
|
|
173
|
+
blumeAstroPkg: string | null,
|
|
174
|
+
shadowAstroPkg: string | null
|
|
175
|
+
): string => {
|
|
176
|
+
const blume = readPkgVersion(blumeAstroPkg);
|
|
177
|
+
const shadow = readPkgVersion(shadowAstroPkg);
|
|
178
|
+
const versions =
|
|
179
|
+
blume && shadow
|
|
180
|
+
? `astro@${shadow} shadowing Blume's astro@${blume}`
|
|
181
|
+
: "a second copy of Astro shadowing Blume's";
|
|
182
|
+
const pin = blume ?? "<Blume's astro version>";
|
|
183
|
+
return `Astro version conflict: another dependency hoisted ${versions} to the project root, so @astrojs/mdx binds to the wrong copy and the build fails on a missing export (e.g. "chunkToString"). A single symlink can't reconcile a split install — pin Blume's Astro by adding a package.json "overrides" (npm/bun/pnpm) or "resolutions" (yarn) entry { "astro": "${pin}" }, then reinstall. Run \`npm ls astro\` to find the dependency pulling the older copy.`;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Make the generated runtime resolve Astro and its integrations against Blume's
|
|
188
|
+
* own dependency set. Two failure modes this repairs:
|
|
189
|
+
*
|
|
190
|
+
* - Astro is *unreachable* from `.blume/` (workspaces under isolated linkers,
|
|
191
|
+
* pnpm) — the deps live in a store the upward walk can't see.
|
|
192
|
+
* - Astro *resolves to the wrong copy* — a hoisted sibling pinned an older
|
|
193
|
+
* major (e.g. `astro@6` for a type-only import) that shadows Blume's
|
|
194
|
+
* `astro@7`, so `@astrojs/mdx@7` binds to it and crashes the build on a
|
|
195
|
+
* missing export. Resolving merely *an* astro isn't enough; it must be the
|
|
196
|
+
* same one Blume uses.
|
|
197
|
+
*
|
|
198
|
+
* In both cases we symlink Blume's dependency directory in as
|
|
199
|
+
* `.blume/node_modules` so the generated config's bare specifiers (`astro`,
|
|
200
|
+
* `@astrojs/mdx`, …) bind to the matching set. We only do this when those deps
|
|
201
|
+
* are a *co-located, consistent* set (astro beside the `@astrojs/mdx` that binds
|
|
202
|
+
* to it). A split layout — an integration hoisted away from a conflicting astro
|
|
203
|
+
* — can't be made consistent by a single symlink and needs a root `overrides`/
|
|
204
|
+
* `resolutions` pin instead. We can't fix that from `.blume/`, so we return a
|
|
205
|
+
* diagnostic naming the conflict rather than silently shipping a runtime that
|
|
206
|
+
* crashes downstream. Returns the warning, or null when nothing needs saying.
|
|
207
|
+
*/
|
|
208
|
+
export const ensureDepsLink = async (
|
|
209
|
+
outDir: string,
|
|
210
|
+
pkgDir: string = packageRoot()
|
|
211
|
+
): Promise<string | null> => {
|
|
212
|
+
const depsDir = blumeDepsDir(pkgDir);
|
|
213
|
+
if (!depsDir) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
// Already correct when `.blume/` resolves the very same astro Blume's deps
|
|
217
|
+
// provide — the clean hoisted case, nothing to do.
|
|
218
|
+
const blumeAstro = resolvedAstroPath(depsDir);
|
|
219
|
+
const outDirAstro = resolvedAstroPath(outDir);
|
|
220
|
+
if (blumeAstro && outDirAstro === blumeAstro) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
// A co-located, consistent set (astro beside the @astrojs/mdx that binds to
|
|
224
|
+
// it) can be linked in wholesale; this repairs the unreachable and the
|
|
225
|
+
// repairable-conflict cases. Any existing link here is stale and gets
|
|
226
|
+
// replaced.
|
|
227
|
+
if (existsSync(join(depsDir, "@astrojs", "mdx"))) {
|
|
228
|
+
await linkDepsJunction(join(outDir, "node_modules"), depsDir);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
// Split layout: Blume's astro is nested (a conflicting astro took the root
|
|
232
|
+
// spot) but @astrojs/mdx hoisted away from it, binding to the shadow. Only a
|
|
233
|
+
// root pin fixes this — surface it.
|
|
234
|
+
return astroConflictWarning(blumeAstro, outDirAstro);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Vite plugin that makes Blume's externalized runtime deps (zod, shiki, sharp,
|
|
239
|
+
* `@takumi-rs/core`, …) resolvable when Astro executes the static prerender
|
|
240
|
+
* bundle under an isolated linker (Bun's `isolated` mode, pnpm).
|
|
241
|
+
*
|
|
242
|
+
* Astro's static build emits a self-contained SSR bundle to
|
|
243
|
+
* `<outDir>/.prerender/` and `import()`s it in-process to generate the HTML.
|
|
244
|
+
* That bundle externalizes Blume's render-time deps, so Node resolves them at
|
|
245
|
+
* prerender time by walking up from `.prerender/chunks/*.mjs`. {@link
|
|
246
|
+
* ensureDepsLink} only repairs resolution rooted at `.blume/`; `.prerender/`
|
|
247
|
+
* lives under `dist/`, a separate tree an isolated linker never hoists Blume's
|
|
248
|
+
* deps into — so the import dies with `Cannot find package 'zod'`. We drop the
|
|
249
|
+
* same `node_modules` junction into the prerender root, mirroring
|
|
250
|
+
* `.blume/node_modules`, so every externalized specifier — native bindings
|
|
251
|
+
* included, which can't be bundled — resolves. Astro deletes `.prerender/` once
|
|
252
|
+
* generation finishes (and the junction with it: `fs.rm` unlinks symlinks, it
|
|
253
|
+
* never follows them), so nothing leaks into the published `dist/`.
|
|
254
|
+
*
|
|
255
|
+
* Keyed off the output dir's basename (`.prerender`) — the name Astro 7 gives
|
|
256
|
+
* the prerender build for both static (`<outDir>/.prerender/`) and server
|
|
257
|
+
* (`<build.server>/.prerender/`) output — so it fires for exactly that build.
|
|
258
|
+
* Inert in dev, where there is no build and `writeBundle` never runs.
|
|
259
|
+
*/
|
|
260
|
+
export const prerenderDepsPlugin = (
|
|
261
|
+
pkgDir: string = packageRoot()
|
|
262
|
+
): {
|
|
263
|
+
name: string;
|
|
264
|
+
writeBundle: (options: { dir?: string }) => Promise<void>;
|
|
265
|
+
} => ({
|
|
266
|
+
name: "blume:prerender-deps",
|
|
267
|
+
writeBundle: async (options) => {
|
|
268
|
+
if (!options.dir || basename(options.dir) !== ".prerender") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const depsDir = blumeDepsDir(pkgDir);
|
|
272
|
+
if (!depsDir) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
await linkDepsJunction(join(options.dir, "node_modules"), depsDir);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
105
279
|
/** Astro integration package each non-React island framework needs installed. */
|
|
106
280
|
const ISLAND_FRAMEWORK_DEPS: Record<string, string> = {
|
|
107
281
|
svelte: "@astrojs/svelte",
|
|
@@ -243,20 +417,12 @@ const writeStagedContent = async (
|
|
|
243
417
|
};
|
|
244
418
|
|
|
245
419
|
/** 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
420
|
/**
|
|
255
421
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
256
422
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
257
423
|
* `<img>`. The file is looked up under `public/` and the project root.
|
|
258
424
|
*/
|
|
259
|
-
const resolveLogo = (project: BlumeProject):
|
|
425
|
+
const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
260
426
|
const { logo } = project.config;
|
|
261
427
|
if (!logo) {
|
|
262
428
|
return null;
|
|
@@ -280,12 +446,6 @@ const resolveLogo = (project: BlumeProject): ResolvedLogo | null => {
|
|
|
280
446
|
return { alt, dark, href, light };
|
|
281
447
|
};
|
|
282
448
|
|
|
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
449
|
/**
|
|
290
450
|
* Favicon filenames Blume auto-detects, in priority order. Mirrors the Next.js
|
|
291
451
|
* convention: an `icon.*` or `favicon.*` file in `public/` or the project root
|
|
@@ -303,6 +463,8 @@ const FAVICON_CANDIDATES = [
|
|
|
303
463
|
/** `<link type>` MIME for the favicon extensions we recognize. */
|
|
304
464
|
const FAVICON_TYPES: Record<string, string> = {
|
|
305
465
|
ico: "image/x-icon",
|
|
466
|
+
jpeg: "image/jpeg",
|
|
467
|
+
jpg: "image/jpeg",
|
|
306
468
|
png: "image/png",
|
|
307
469
|
svg: "image/svg+xml",
|
|
308
470
|
};
|
|
@@ -318,45 +480,67 @@ const inlineDataUri = (file: string, type: string): string =>
|
|
|
318
480
|
`data:${type};base64,${readFileSync(file).toString("base64")}`;
|
|
319
481
|
|
|
320
482
|
/** The bundled Blume favicon, inlined as a data URI so it needs no public file. */
|
|
321
|
-
const defaultFavicon = ():
|
|
483
|
+
const defaultFavicon = (): BlumeFavicon => ({
|
|
322
484
|
href: inlineDataUri(join(BLUME_SRC, "assets", "icon.png"), "image/png"),
|
|
323
485
|
type: "image/png",
|
|
324
486
|
});
|
|
325
487
|
|
|
326
488
|
/**
|
|
327
|
-
*
|
|
328
|
-
* `
|
|
329
|
-
*
|
|
330
|
-
*
|
|
489
|
+
* Apple touch icon filenames Blume auto-detects, in priority order. Mirrors the
|
|
490
|
+
* Next.js `apple-icon.*` convention (plus the `apple-touch-icon.png` most favicon
|
|
491
|
+
* generators emit): a match in `public/` or the project root becomes the iOS
|
|
492
|
+
* home-screen icon, no config required.
|
|
493
|
+
*/
|
|
494
|
+
const APPLE_ICON_CANDIDATES = [
|
|
495
|
+
"apple-icon.png",
|
|
496
|
+
"apple-icon.jpg",
|
|
497
|
+
"apple-icon.jpeg",
|
|
498
|
+
"apple-touch-icon.png",
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Resolve an icon file by convention. A candidate in `public/` is served as-is
|
|
503
|
+
* and referenced by URL; one at the project root is inlined as a data URI (the
|
|
504
|
+
* root isn't a served directory). Returns null when the project ships none.
|
|
331
505
|
*/
|
|
332
|
-
const
|
|
506
|
+
const resolveIconFile = (
|
|
507
|
+
project: BlumeProject,
|
|
508
|
+
candidates: string[]
|
|
509
|
+
): BlumeFavicon | null => {
|
|
333
510
|
const { root } = project.context;
|
|
334
|
-
for (const name of
|
|
511
|
+
for (const name of candidates) {
|
|
335
512
|
if (existsSync(join(root, "public", name))) {
|
|
336
513
|
return { href: `/${name}`, type: faviconType(name) };
|
|
337
514
|
}
|
|
338
515
|
}
|
|
339
|
-
for (const name of
|
|
516
|
+
for (const name of candidates) {
|
|
340
517
|
const file = join(root, name);
|
|
341
518
|
if (existsSync(file)) {
|
|
342
519
|
const type = faviconType(name);
|
|
343
520
|
return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
|
|
344
521
|
}
|
|
345
522
|
}
|
|
346
|
-
return
|
|
523
|
+
return null;
|
|
347
524
|
};
|
|
348
525
|
|
|
349
|
-
/**
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
526
|
+
/**
|
|
527
|
+
* Resolve the site favicon by convention, falling back to the bundled Blume mark
|
|
528
|
+
* when the project ships no `icon.*`/`favicon.*` file.
|
|
529
|
+
*/
|
|
530
|
+
const resolveFavicon = (project: BlumeProject): BlumeFavicon =>
|
|
531
|
+
resolveIconFile(project, FAVICON_CANDIDATES) ?? defaultFavicon();
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Resolve the Apple touch icon by convention, or null when the project ships
|
|
535
|
+
* none (unlike the favicon, there's no bundled default). Note: iOS ignores
|
|
536
|
+
* `data:`-URI apple-touch-icons, so a `public/` file (served by URL) is the
|
|
537
|
+
* reliable path; a root-level file is still inlined for symmetry with favicons.
|
|
538
|
+
*/
|
|
539
|
+
const resolveAppleIcon = (project: BlumeProject): BlumeFavicon | null =>
|
|
540
|
+
resolveIconFile(project, APPLE_ICON_CANDIDATES);
|
|
357
541
|
|
|
358
542
|
/** Normalize the banner config (string shorthand or object) for the runtime. */
|
|
359
|
-
const resolveBanner = (config: ResolvedConfig):
|
|
543
|
+
const resolveBanner = (config: ResolvedConfig): BlumeBanner | null => {
|
|
360
544
|
const { banner } = config;
|
|
361
545
|
if (!banner) {
|
|
362
546
|
return null;
|
|
@@ -436,9 +620,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
436
620
|
)
|
|
437
621
|
: {};
|
|
438
622
|
|
|
439
|
-
const data = {
|
|
623
|
+
const data: BlumeData = {
|
|
440
624
|
config: {
|
|
441
625
|
analytics: config.analytics ?? null,
|
|
626
|
+
appleIcon: resolveAppleIcon(project),
|
|
442
627
|
banner: resolveBanner(config),
|
|
443
628
|
codeWrap: config.markdown.code.wrap,
|
|
444
629
|
description: config.description,
|
|
@@ -463,7 +648,9 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
463
648
|
mcp: config.mcp.enabled
|
|
464
649
|
? { name: config.mcp.name ?? config.title, route: config.mcp.route }
|
|
465
650
|
: null,
|
|
466
|
-
|
|
651
|
+
// `og.enabled` is resolved to a definite boolean in `loadConfig`; coerce
|
|
652
|
+
// the optional schema type so the serialized shape stays `boolean`.
|
|
653
|
+
og: { enabled: config.seo.og.enabled ?? false },
|
|
467
654
|
repoUrl,
|
|
468
655
|
search: {
|
|
469
656
|
enabled: config.search.provider !== "none",
|
|
@@ -619,6 +806,7 @@ export const generateRuntime = async (
|
|
|
619
806
|
const dataPath = join(srcDir, "generated", "data.json");
|
|
620
807
|
const themePath = join(srcDir, "generated", "app.css");
|
|
621
808
|
const searchClientPath = join(srcDir, "generated", "search-client.ts");
|
|
809
|
+
const examplesPath = join(srcDir, "generated", "examples.ts");
|
|
622
810
|
|
|
623
811
|
// Record every file this pass writes so orphans (from a now-disabled feature)
|
|
624
812
|
// can be pruned afterwards. `write` wraps the atomic writer and tracks paths.
|
|
@@ -628,26 +816,36 @@ export const generateRuntime = async (
|
|
|
628
816
|
return writeIfChanged(path, content);
|
|
629
817
|
};
|
|
630
818
|
|
|
631
|
-
await ensureDepsLink(out);
|
|
819
|
+
const depsLinkWarning = await ensureDepsLink(out);
|
|
632
820
|
|
|
633
821
|
const askEnabled = config.ai.ask?.enabled ?? false;
|
|
634
822
|
const exportPdf = config.export.pdf;
|
|
635
823
|
const exportEpub = config.export.epub;
|
|
636
|
-
const [pages, detectedReact, userTheme, islandDiscovery] =
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
824
|
+
const [pages, detectedReact, userTheme, islandDiscovery, exampleDiscovery] =
|
|
825
|
+
await Promise.all([
|
|
826
|
+
context.pagesRoot
|
|
827
|
+
? discoverPages(context.pagesRoot)
|
|
828
|
+
: Promise.resolve([]),
|
|
829
|
+
detectNeedsReact(context.root),
|
|
830
|
+
readOptional(context.themeFile),
|
|
831
|
+
discoverIslands(context.root),
|
|
832
|
+
discoverExamples(context.root, config.examples),
|
|
833
|
+
]);
|
|
834
|
+
// Each island/example framework enables its Astro renderer. React also
|
|
835
|
+
// switches on for any project `.tsx`/`.jsx` and for Ask AI; Vue/Svelte are
|
|
836
|
+
// island/example-driven. `.astro` examples need no renderer.
|
|
837
|
+
const frameworks = new Set<string>([
|
|
838
|
+
...islandDiscovery.islands.map((island) => island.framework),
|
|
839
|
+
...exampleDiscovery.examples.map((example) => example.framework),
|
|
641
840
|
]);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
const needsSvelte = islandFrameworks.has("svelte");
|
|
841
|
+
const needsReact = detectedReact || askEnabled || frameworks.has("react");
|
|
842
|
+
const needsVue = frameworks.has("vue");
|
|
843
|
+
const needsSvelte = frameworks.has("svelte");
|
|
844
|
+
|
|
845
|
+
// Custom pages that should get a generated OG card (the home most of all).
|
|
846
|
+
// Computed before the MCP `.well-known` routes are appended below — those are
|
|
847
|
+
// private and filtered out anyway, but the intent is the user's pages.
|
|
848
|
+
const ogRoutes = customOgRoutes(pages, config.title, config.description);
|
|
651
849
|
|
|
652
850
|
// The hosted MCP server. The `.well-known` discovery docs are injected as
|
|
653
851
|
// prerendered routes alongside user pages; the server endpoint itself is a
|
|
@@ -664,10 +862,12 @@ export const generateRuntime = async (
|
|
|
664
862
|
write(
|
|
665
863
|
join(out, "astro.config.mjs"),
|
|
666
864
|
astroConfigTemplate({
|
|
865
|
+
aliases: resolveTsconfigAliases(context.root),
|
|
667
866
|
config,
|
|
668
867
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
669
868
|
context,
|
|
670
869
|
dataPath,
|
|
870
|
+
examplesPath,
|
|
671
871
|
needsReact,
|
|
672
872
|
needsSvelte,
|
|
673
873
|
needsVue,
|
|
@@ -705,6 +905,10 @@ export const generateRuntime = async (
|
|
|
705
905
|
join(srcDir, "generated", "islands.ts"),
|
|
706
906
|
islandMapTemplate(islandDiscovery.islands)
|
|
707
907
|
),
|
|
908
|
+
write(
|
|
909
|
+
join(srcDir, "generated", "examples.ts"),
|
|
910
|
+
exampleMapTemplate(exampleDiscovery.examples)
|
|
911
|
+
),
|
|
708
912
|
write(
|
|
709
913
|
themePath,
|
|
710
914
|
tailwindEntryTemplate({
|
|
@@ -731,6 +935,22 @@ export const generateRuntime = async (
|
|
|
731
935
|
)
|
|
732
936
|
);
|
|
733
937
|
|
|
938
|
+
// Per-example live wrappers for the `examples/` convention, resolved by
|
|
939
|
+
// `<Component path>` through the `examples.ts` map (written above, always).
|
|
940
|
+
await Promise.all(
|
|
941
|
+
exampleDiscovery.examples.map((example) =>
|
|
942
|
+
write(
|
|
943
|
+
join(
|
|
944
|
+
srcDir,
|
|
945
|
+
"generated",
|
|
946
|
+
"examples",
|
|
947
|
+
`${exampleSlug(example.path)}.astro`
|
|
948
|
+
),
|
|
949
|
+
exampleWrapperTemplate(example)
|
|
950
|
+
)
|
|
951
|
+
)
|
|
952
|
+
);
|
|
953
|
+
|
|
734
954
|
if (askEnabled) {
|
|
735
955
|
await write(
|
|
736
956
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
@@ -743,7 +963,7 @@ export const generateRuntime = async (
|
|
|
743
963
|
if (config.seo.og.enabled) {
|
|
744
964
|
await write(
|
|
745
965
|
join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
746
|
-
ogEndpointTemplate()
|
|
966
|
+
ogEndpointTemplate(ogRoutes)
|
|
747
967
|
);
|
|
748
968
|
}
|
|
749
969
|
|
|
@@ -827,7 +1047,12 @@ export const generateRuntime = async (
|
|
|
827
1047
|
|
|
828
1048
|
// API/AsyncAPI reference pages (Scalar). One self-contained page per source,
|
|
829
1049
|
// mounted on its configured route and regenerated each run.
|
|
830
|
-
const warnings: string[] = [
|
|
1050
|
+
const warnings: string[] = [
|
|
1051
|
+
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
1052
|
+
...mcp.warnings,
|
|
1053
|
+
...islandDiscovery.warnings,
|
|
1054
|
+
...exampleDiscovery.warnings,
|
|
1055
|
+
];
|
|
831
1056
|
|
|
832
1057
|
// The new provider SDKs are optional peers; warn (rather than fail opaquely in
|
|
833
1058
|
// Vite) when the configured provider's package isn't installed.
|
|
@@ -841,7 +1066,7 @@ export const generateRuntime = async (
|
|
|
841
1066
|
|
|
842
1067
|
// React ships with Blume; Vue/Svelte islands need their Astro integration
|
|
843
1068
|
// installed by the project. Warn early rather than let Vite fail to resolve it.
|
|
844
|
-
warnings.push(...islandFrameworkWarnings(
|
|
1069
|
+
warnings.push(...islandFrameworkWarnings(frameworks, context.root));
|
|
845
1070
|
if (hasReferences(config)) {
|
|
846
1071
|
const references = await buildReferenceFiles({
|
|
847
1072
|
config,
|
package/src/astro/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { generateRuntime } from "./generate.ts";
|
|
1
|
+
export { generateRuntime, prerenderDepsPlugin } from "./generate.ts";
|
|
2
2
|
export type { GenerateResult } from "./generate.ts";
|
|
3
3
|
export { blumeIntegration } from "./integration.ts";
|
|
4
4
|
export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
|
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
|
+
};
|