blume 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +1 -1
- package/dist/cli/index.js +1473 -149
- package/dist/cli/index.js.map +47 -36
- package/dist/types/core/config-input.d.ts +18 -0
- package/dist/types/core/config.d.ts +4 -0
- package/dist/types/core/data.d.ts +3 -0
- package/dist/types/core/schema.d.ts +132 -17
- package/dist/types/core/types.d.ts +5 -3
- package/dist/types/openapi/references.d.ts +6 -0
- package/docs/advanced/api-reference.mdx +27 -0
- package/docs/advanced/changelog.mdx +10 -0
- package/docs/configuration/ai.mdx +38 -2
- package/docs/configuration/customization.mdx +27 -0
- package/docs/configuration/index.mdx +5 -0
- package/docs/content/navigation.mdx +12 -0
- package/docs/reference/cli.mdx +17 -13
- package/docs/reference/eval.mdx +106 -0
- package/docs/reference/meta.ts +1 -1
- package/package.json +1 -1
- package/src/ai/agent-readability.ts +19 -1
- package/src/ai/llms.ts +9 -4
- package/src/ai/mcp/server.ts +48 -14
- package/src/ai/mcp/stdio.ts +35 -0
- package/src/astro/generate.ts +119 -48
- package/src/astro/templates.ts +173 -37
- package/src/audit/checks/duplicates.ts +15 -6
- package/src/audit/checks/indexability.ts +11 -2
- package/src/audit/checks/network.ts +22 -8
- package/src/audit/checks/sitemap.ts +42 -16
- package/src/audit/redirects.ts +12 -1
- package/src/audit/run.ts +13 -3
- package/src/audit/url.ts +21 -2
- package/src/cli/commands/audit.ts +21 -6
- package/src/cli/commands/dev.ts +19 -2
- package/src/cli/commands/eval.ts +291 -0
- package/src/cli/commands/init.ts +9 -4
- package/src/cli/commands/mcp-stdio.ts +36 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/required-secrets.ts +1 -1
- package/src/components/content/AccordionItem.astro +2 -2
- package/src/components/content/Frame.astro +4 -1
- package/src/components/content/Prompt.astro +4 -1
- package/src/components/content/Tooltip.astro +4 -1
- package/src/components/content/TreeFolder.astro +1 -2
- package/src/components/content/Update.astro +45 -0
- package/src/components/islands/AskAI.astro +9 -2
- package/src/components/islands/ask-ai.tsx +23 -4
- package/src/components/islands/hooks.ts +48 -15
- package/src/components/layout/NavTree.astro +37 -19
- package/src/components/layout/ReferenceLayout.astro +4 -0
- package/src/components/layout/RootLayout.astro +14 -3
- package/src/components/layout/Search.astro +5 -1
- package/src/components/layout/head-scripts.ts +22 -5
- package/src/components/openapi/SchemaProperty.astro +3 -3
- package/src/core/config-input.ts +18 -0
- package/src/core/config.ts +4 -0
- package/src/core/data.ts +3 -0
- package/src/core/deployment-env.ts +7 -2
- package/src/core/graph.ts +8 -1
- package/src/core/i18n.ts +10 -2
- package/src/core/navigation.ts +16 -5
- package/src/core/schema.ts +51 -4
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/normalize.ts +69 -8
- package/src/core/sources/notion.ts +4 -2
- package/src/core/sources/sanity.ts +5 -3
- package/src/core/types.ts +5 -3
- package/src/eval/agents.ts +340 -0
- package/src/eval/findings.ts +103 -0
- package/src/eval/prompts.ts +78 -0
- package/src/eval/report.ts +214 -0
- package/src/eval/run.ts +290 -0
- package/src/eval/schema.ts +124 -0
- package/src/markdown/code-title.ts +7 -1
- package/src/openapi/model.ts +31 -2
- package/src/openapi/references.ts +23 -2
- package/src/openapi/render-mdx.ts +39 -11
- package/src/openapi/scalar.ts +1 -0
- package/src/openapi/source.ts +11 -4
- package/src/registry/eject.ts +23 -1
- package/src/search/build.ts +4 -3
package/src/astro/generate.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
3
|
import {
|
|
3
4
|
lstat,
|
|
4
5
|
mkdir,
|
|
5
6
|
readFile,
|
|
7
|
+
readlink,
|
|
6
8
|
realpath,
|
|
7
9
|
rename,
|
|
8
10
|
rm,
|
|
@@ -12,7 +14,7 @@ import {
|
|
|
12
14
|
import { createRequire } from "node:module";
|
|
13
15
|
import { pathToFileURL } from "node:url";
|
|
14
16
|
|
|
15
|
-
import { basename, dirname, join, normalize, relative } from "pathe";
|
|
17
|
+
import { basename, dirname, join, normalize, relative, resolve } from "pathe";
|
|
16
18
|
import { glob } from "tinyglobby";
|
|
17
19
|
|
|
18
20
|
import { buildAskData } from "../ai/ask-data.ts";
|
|
@@ -39,7 +41,7 @@ import type { BlumeProject } from "../core/project-graph.ts";
|
|
|
39
41
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
40
42
|
import { resolveDocsCollection } from "../core/sources/resolve.ts";
|
|
41
43
|
import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
|
|
42
|
-
import type { Navigation } from "../core/types.ts";
|
|
44
|
+
import type { Diagnostic, Navigation, ProjectContext } from "../core/types.ts";
|
|
43
45
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
44
46
|
import { resolveOgLogo } from "../og/logo.ts";
|
|
45
47
|
import { hasScalarReferences, referenceRoutes } from "../openapi/references.ts";
|
|
@@ -87,6 +89,7 @@ import {
|
|
|
87
89
|
ogEndpointTemplate,
|
|
88
90
|
rawMarkdownEndpointTemplate,
|
|
89
91
|
rssEndpointTemplate,
|
|
92
|
+
runtimeDirWithin,
|
|
90
93
|
staticJsonEndpointTemplate,
|
|
91
94
|
runtimeDependencies,
|
|
92
95
|
runtimePackageTemplate,
|
|
@@ -118,17 +121,18 @@ const canResolveFrom = (fromDir: string, spec: string): boolean => {
|
|
|
118
121
|
* that never installed the plugin directly. Resolving from `packageRoot()` binds
|
|
119
122
|
* to Blume's shipped copy regardless of the user's package manager or hoisting.
|
|
120
123
|
*/
|
|
121
|
-
const resolveReactCompiler = (
|
|
124
|
+
export const resolveReactCompiler = (
|
|
122
125
|
config: ResolvedConfig,
|
|
123
|
-
needsReact: boolean
|
|
126
|
+
needsReact: boolean,
|
|
127
|
+
pkgDir: string = packageRoot()
|
|
124
128
|
): string | null => {
|
|
125
129
|
if (!(needsReact && config.react.compiler)) {
|
|
126
130
|
return null;
|
|
127
131
|
}
|
|
128
132
|
try {
|
|
129
|
-
return createRequire(
|
|
130
|
-
|
|
131
|
-
)
|
|
133
|
+
return createRequire(pathToFileURL(join(pkgDir, "_.js")).href).resolve(
|
|
134
|
+
"babel-plugin-react-compiler"
|
|
135
|
+
);
|
|
132
136
|
} catch {
|
|
133
137
|
return null;
|
|
134
138
|
}
|
|
@@ -137,9 +141,9 @@ const resolveReactCompiler = (
|
|
|
137
141
|
/**
|
|
138
142
|
* Warning (as a spreadable list) for the case where the React Compiler was
|
|
139
143
|
* requested but its plugin couldn't be resolved — so the build silently drops
|
|
140
|
-
* to uncompiled output rather than failing.
|
|
144
|
+
* to uncompiled output rather than failing. Exported for testing.
|
|
141
145
|
*/
|
|
142
|
-
const reactCompilerWarnings = (
|
|
146
|
+
export const reactCompilerWarnings = (
|
|
143
147
|
config: ResolvedConfig,
|
|
144
148
|
needsReact: boolean,
|
|
145
149
|
compilerPath: string | null
|
|
@@ -195,8 +199,11 @@ const resolvedAstroHit = (
|
|
|
195
199
|
}
|
|
196
200
|
};
|
|
197
201
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Whether two paths name the same physical directory (realpath equality).
|
|
204
|
+
* Exported for testing.
|
|
205
|
+
*/
|
|
206
|
+
export const sameRealDir = (a: string, b: string): boolean => {
|
|
200
207
|
try {
|
|
201
208
|
return realpathSync(a) === realpathSync(b);
|
|
202
209
|
} catch {
|
|
@@ -281,6 +288,17 @@ const linkDepsJunction = async (
|
|
|
281
288
|
if (!existing.isSymbolicLink()) {
|
|
282
289
|
return;
|
|
283
290
|
}
|
|
291
|
+
// Already pointing at the right target — leave it alone. This runs on
|
|
292
|
+
// every dev regeneration, and an unconditional rm+recreate opens a window
|
|
293
|
+
// in which the Vite server's module resolution races a missing
|
|
294
|
+
// `node_modules` and 500s intermittently.
|
|
295
|
+
try {
|
|
296
|
+
if (resolve(dirname(link), await readlink(link)) === resolve(depsDir)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
// Unreadable link — replace it below.
|
|
301
|
+
}
|
|
284
302
|
await rm(link, { force: true });
|
|
285
303
|
}
|
|
286
304
|
await mkdir(dirname(link), { recursive: true });
|
|
@@ -587,6 +605,34 @@ const deploymentAdapterWarnings = (
|
|
|
587
605
|
return [];
|
|
588
606
|
};
|
|
589
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Warn when the configured search provider's SDK is missing. Provider SDKs are
|
|
610
|
+
* optional peers; warn (rather than fail opaquely in Vite) when the package
|
|
611
|
+
* isn't installed. A dep is available if the project installed it (resolves
|
|
612
|
+
* from the root) OR Blume ships it (resolves from the Blume package — the same
|
|
613
|
+
* set the `.blume` deps link exposes to the build). Resolving from the project
|
|
614
|
+
* root alone falsely flagged a shipped SDK like Orama (the default provider)
|
|
615
|
+
* as missing whenever it wasn't hoisted into the project, e.g. under isolated
|
|
616
|
+
* linkers. We resolve from each package's real location rather than through
|
|
617
|
+
* the `.blume` junction, which can't be traversed reliably for store-symlinked
|
|
618
|
+
* deps. `pkgDir` is injectable for testing.
|
|
619
|
+
*/
|
|
620
|
+
export const searchProviderWarnings = (
|
|
621
|
+
provider: ResolvedConfig["search"]["provider"],
|
|
622
|
+
root: string,
|
|
623
|
+
pkgDir: string = packageRoot()
|
|
624
|
+
): string[] => {
|
|
625
|
+
const warnings: string[] = [];
|
|
626
|
+
for (const dep of searchProviderMeta(provider).runtimeDeps) {
|
|
627
|
+
if (!(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
|
|
628
|
+
warnings.push(
|
|
629
|
+
`Search provider "${provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return warnings;
|
|
634
|
+
};
|
|
635
|
+
|
|
590
636
|
/** Absolute path to the configured `examples.css`, or null when unset. */
|
|
591
637
|
const examplesCssFile = (root: string, config: ResolvedConfig): string | null =>
|
|
592
638
|
config.examples.css ? join(root, config.examples.css) : null;
|
|
@@ -678,6 +724,22 @@ export const detectUsesMath = async (
|
|
|
678
724
|
return [...contents, ...staged].some(containsMath);
|
|
679
725
|
};
|
|
680
726
|
|
|
727
|
+
const hashConfigSource = (source: string): string =>
|
|
728
|
+
createHash("sha256").update(source).digest("hex");
|
|
729
|
+
|
|
730
|
+
const loadIntegrationBridge = async (
|
|
731
|
+
config: ResolvedConfig,
|
|
732
|
+
context: BlumeProject["context"]
|
|
733
|
+
): Promise<Parameters<typeof astroConfigTemplate>[0]["integrationBridge"]> => {
|
|
734
|
+
if (config.integrations.length === 0 || !context.configFile) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
configFile: relative(context.outDir, context.configFile),
|
|
739
|
+
sourceHash: hashConfigSource(await readOptional(context.configFile)),
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
|
|
681
743
|
const writeIfChanged = async (
|
|
682
744
|
path: string,
|
|
683
745
|
content: string
|
|
@@ -1040,12 +1102,16 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1040
1102
|
analytics: config.analytics ?? null,
|
|
1041
1103
|
appleIcon: resolveAppleIcon(project),
|
|
1042
1104
|
ask: config.ai.ask?.enabled
|
|
1043
|
-
? {
|
|
1105
|
+
? {
|
|
1106
|
+
endpoint: config.ai.ask.endpoint ?? null,
|
|
1107
|
+
suggestions: config.ai.ask.suggestions,
|
|
1108
|
+
}
|
|
1044
1109
|
: null,
|
|
1045
1110
|
banner: resolveBanner(config),
|
|
1046
1111
|
basePath: config.basePath,
|
|
1047
1112
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
1048
1113
|
codeWrap: config.markdown.code.wrap,
|
|
1114
|
+
dateFormat: config.dateFormat,
|
|
1049
1115
|
description: config.description,
|
|
1050
1116
|
favicon: resolveFavicon(project),
|
|
1051
1117
|
feedback: config.feedback,
|
|
@@ -1233,7 +1299,7 @@ const writeAskFiles = async (
|
|
|
1233
1299
|
write: (path: string, content: string) => Promise<boolean>
|
|
1234
1300
|
): Promise<void> => {
|
|
1235
1301
|
const { ask } = project.config.ai;
|
|
1236
|
-
if (!ask?.enabled) {
|
|
1302
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
1237
1303
|
return;
|
|
1238
1304
|
}
|
|
1239
1305
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -1268,6 +1334,15 @@ const writeNotFoundPage = async (
|
|
|
1268
1334
|
await write(join(srcDir, "pages", "404.astro"), notFoundPageTemplate());
|
|
1269
1335
|
};
|
|
1270
1336
|
|
|
1337
|
+
/**
|
|
1338
|
+
* Flatten a diagnostic to a single warning line, appending the suggestion when
|
|
1339
|
+
* one exists. Exported for testing.
|
|
1340
|
+
*/
|
|
1341
|
+
export const diagnosticWarning = (diagnostic: Diagnostic): string =>
|
|
1342
|
+
diagnostic.suggestion
|
|
1343
|
+
? `${diagnostic.message} ${diagnostic.suggestion}`
|
|
1344
|
+
: diagnostic.message;
|
|
1345
|
+
|
|
1271
1346
|
export interface GenerateResult {
|
|
1272
1347
|
/** Whether any structural file changed (config/page/content config). */
|
|
1273
1348
|
structuralChange: boolean;
|
|
@@ -1304,6 +1379,20 @@ const buildComponentSlots = async (
|
|
|
1304
1379
|
};
|
|
1305
1380
|
};
|
|
1306
1381
|
|
|
1382
|
+
/**
|
|
1383
|
+
* Whether the docs glob-loader's watcher observes the runtime dir: a
|
|
1384
|
+
* filesystem collection whose base contains it (a migrated, `content.root:
|
|
1385
|
+
* "."` project) — the one layout where the dev watcher must be kept out of
|
|
1386
|
+
* Astro's cache dir. See `devWatchOption` in templates.ts.
|
|
1387
|
+
*/
|
|
1388
|
+
const contentWatchesRuntimeDir = (
|
|
1389
|
+
hasFilesystemSource: boolean,
|
|
1390
|
+
collectionBase: string,
|
|
1391
|
+
context: ProjectContext
|
|
1392
|
+
): boolean =>
|
|
1393
|
+
hasFilesystemSource &&
|
|
1394
|
+
runtimeDirWithin(collectionBase, context.outDir) !== null;
|
|
1395
|
+
|
|
1307
1396
|
/**
|
|
1308
1397
|
* Write (or update) the generated `.blume/` Astro runtime for a project.
|
|
1309
1398
|
* Only files whose content changed are rewritten so Vite HMR stays fast.
|
|
@@ -1349,6 +1438,7 @@ export const generateRuntime = async (
|
|
|
1349
1438
|
usesMath,
|
|
1350
1439
|
userTheme,
|
|
1351
1440
|
userExamplesCss,
|
|
1441
|
+
integrationBridge,
|
|
1352
1442
|
islandDiscovery,
|
|
1353
1443
|
exampleDiscovery,
|
|
1354
1444
|
componentSlots,
|
|
@@ -1358,6 +1448,7 @@ export const generateRuntime = async (
|
|
|
1358
1448
|
detectUsesMath(context.root, staged.values()),
|
|
1359
1449
|
readOptional(context.themeFile),
|
|
1360
1450
|
readOptional(examplesCssFile(context.root, config)),
|
|
1451
|
+
loadIntegrationBridge(config, context),
|
|
1361
1452
|
discoverIslands(context.root),
|
|
1362
1453
|
discoverExamples(context.root, config.examples.source),
|
|
1363
1454
|
buildComponentSlots(context.componentsFile),
|
|
@@ -1407,6 +1498,7 @@ export const generateRuntime = async (
|
|
|
1407
1498
|
// sources, so the `docs` glob would otherwise scan (and watch) the whole
|
|
1408
1499
|
// project root for nothing — see contentConfigTemplate.
|
|
1409
1500
|
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
1501
|
+
const docsCollection = resolveDocsCollection(config, context);
|
|
1410
1502
|
|
|
1411
1503
|
// All of these write to distinct generated paths and never read one another's
|
|
1412
1504
|
// output, so the structural files, the per-convention hydration wrappers, and
|
|
@@ -1421,10 +1513,16 @@ export const generateRuntime = async (
|
|
|
1421
1513
|
askPath,
|
|
1422
1514
|
config,
|
|
1423
1515
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
1516
|
+
contentWatchesRuntimeDir: contentWatchesRuntimeDir(
|
|
1517
|
+
hasFilesystemSource,
|
|
1518
|
+
docsCollection.base,
|
|
1519
|
+
context
|
|
1520
|
+
),
|
|
1424
1521
|
context,
|
|
1425
1522
|
dataPath,
|
|
1426
1523
|
examplesPath,
|
|
1427
1524
|
examplesThemePath,
|
|
1525
|
+
integrationBridge,
|
|
1428
1526
|
needsReact,
|
|
1429
1527
|
needsSvelte,
|
|
1430
1528
|
needsVue,
|
|
@@ -1446,7 +1544,7 @@ export const generateRuntime = async (
|
|
|
1446
1544
|
write(
|
|
1447
1545
|
join(srcDir, "content.config.ts"),
|
|
1448
1546
|
contentConfigTemplate({
|
|
1449
|
-
collection:
|
|
1547
|
+
collection: docsCollection,
|
|
1450
1548
|
config,
|
|
1451
1549
|
context,
|
|
1452
1550
|
filesystem: hasFilesystemSource,
|
|
@@ -1666,11 +1764,7 @@ export const generateRuntime = async (
|
|
|
1666
1764
|
...[
|
|
1667
1765
|
...validateNavTargets(project.graph.navigation, navTargetRoutes),
|
|
1668
1766
|
...validateSearchPopularIcons(config.search.popular),
|
|
1669
|
-
].map(
|
|
1670
|
-
diagnostic.suggestion
|
|
1671
|
-
? `${diagnostic.message} ${diagnostic.suggestion}`
|
|
1672
|
-
: diagnostic.message
|
|
1673
|
-
)
|
|
1767
|
+
].map(diagnosticWarning)
|
|
1674
1768
|
);
|
|
1675
1769
|
|
|
1676
1770
|
// Unknown-component check: a `<Tag>` in MDX that isn't a built-in, an island,
|
|
@@ -1679,40 +1773,17 @@ export const generateRuntime = async (
|
|
|
1679
1773
|
...islandDiscovery.islands.map((island) => island.name),
|
|
1680
1774
|
...overrideTags,
|
|
1681
1775
|
]);
|
|
1776
|
+
// Missing-dependency preflights: the search provider's SDK, the deployment
|
|
1777
|
+
// adapter's package, and — since React ships with Blume while Vue/Svelte
|
|
1778
|
+
// don't — any island framework's Astro integration. Warn early rather than
|
|
1779
|
+
// let Vite fail to resolve them opaquely.
|
|
1682
1780
|
warnings.push(
|
|
1683
1781
|
...validateUsedComponents(
|
|
1684
1782
|
project.graph.pages,
|
|
1685
1783
|
knownComponentTags,
|
|
1686
1784
|
new Set(registry.map((item) => item.name))
|
|
1687
|
-
).map(
|
|
1688
|
-
|
|
1689
|
-
? `${diagnostic.message} ${diagnostic.suggestion}`
|
|
1690
|
-
: diagnostic.message
|
|
1691
|
-
)
|
|
1692
|
-
);
|
|
1693
|
-
|
|
1694
|
-
// Provider SDKs are optional peers; warn (rather than fail opaquely in Vite)
|
|
1695
|
-
// when the configured provider's package isn't installed. A dep is available
|
|
1696
|
-
// if the project installed it (resolves from the root) OR Blume ships it
|
|
1697
|
-
// (resolves from the Blume package — the same set the `.blume` deps link
|
|
1698
|
-
// exposes to the build). Resolving from the project root alone falsely flagged
|
|
1699
|
-
// a shipped SDK like Orama (the default provider) as missing whenever it
|
|
1700
|
-
// wasn't hoisted into the project, e.g. under isolated linkers. We resolve
|
|
1701
|
-
// from each package's real location rather than through the `.blume` junction,
|
|
1702
|
-
// which can't be traversed reliably for store-symlinked deps.
|
|
1703
|
-
for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
|
|
1704
|
-
if (
|
|
1705
|
-
!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))
|
|
1706
|
-
) {
|
|
1707
|
-
warnings.push(
|
|
1708
|
-
`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
// React ships with Blume; Vue/Svelte islands need their Astro integration
|
|
1714
|
-
// installed by the project. Warn early rather than let Vite fail to resolve it.
|
|
1715
|
-
warnings.push(
|
|
1785
|
+
).map(diagnosticWarning),
|
|
1786
|
+
...searchProviderWarnings(config.search.provider, context.root),
|
|
1716
1787
|
...deploymentAdapterWarnings(config.deployment, context.root),
|
|
1717
1788
|
...islandFrameworkWarnings(frameworks, context.root)
|
|
1718
1789
|
);
|
package/src/astro/templates.ts
CHANGED
|
@@ -87,7 +87,14 @@ const WRANGLER_CONFIG_FILES = [
|
|
|
87
87
|
];
|
|
88
88
|
|
|
89
89
|
const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
|
|
90
|
-
|
|
90
|
+
// Every Blume HTML route prerenders (the only server routes are API
|
|
91
|
+
// endpoints), so images are optimized at build time with sharp. The
|
|
92
|
+
// adapter's default (`cloudflare-binding`) would instead declare a runtime
|
|
93
|
+
// `IMAGES` binding in the generated wrangler config that nothing uses.
|
|
94
|
+
const args: string[] = [
|
|
95
|
+
'prerenderEnvironment: "node"',
|
|
96
|
+
'imageService: "compile"',
|
|
97
|
+
];
|
|
91
98
|
const wranglerPath = WRANGLER_CONFIG_FILES.map((file) =>
|
|
92
99
|
join(context.root, file)
|
|
93
100
|
).find((file) => existsSync(file));
|
|
@@ -104,6 +111,37 @@ const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
|
|
|
104
111
|
return `{ ${args.join(", ")} }`;
|
|
105
112
|
};
|
|
106
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Without a configured driver, `@astrojs/cloudflare` force-enables KV-backed
|
|
116
|
+
* sessions and declares a `SESSION` kv_namespaces entry in the generated
|
|
117
|
+
* wrangler config — which `wrangler deploy` then requires a real KV namespace
|
|
118
|
+
* for, even though Blume never reads `Astro.session`. An explicit in-memory
|
|
119
|
+
* driver keeps the binding out. Swap for Astro's session opt-out once
|
|
120
|
+
* withastro/astro#16871 ships in the supported range.
|
|
121
|
+
*/
|
|
122
|
+
const resolveSessionOption = (deployment: {
|
|
123
|
+
adapter: string | null;
|
|
124
|
+
output: string;
|
|
125
|
+
}): string =>
|
|
126
|
+
deployment.output === "server" && deployment.adapter === "cloudflare"
|
|
127
|
+
? "\n session: { driver: sessionDrivers.memory() },"
|
|
128
|
+
: "";
|
|
129
|
+
|
|
130
|
+
/** The named imports the generated config pulls from `astro/config`. */
|
|
131
|
+
const astroConfigImportLine = (options: {
|
|
132
|
+
hasFonts: boolean;
|
|
133
|
+
hasSession: boolean;
|
|
134
|
+
}): string => {
|
|
135
|
+
const names = ["defineConfig"];
|
|
136
|
+
if (options.hasFonts) {
|
|
137
|
+
names.push("fontProviders");
|
|
138
|
+
}
|
|
139
|
+
if (options.hasSession) {
|
|
140
|
+
names.push("sessionDrivers");
|
|
141
|
+
}
|
|
142
|
+
return `import { ${names.join(", ")} } from "astro/config";`;
|
|
143
|
+
};
|
|
144
|
+
|
|
107
145
|
/**
|
|
108
146
|
* Integration packages the generated runtime imports. Declaring them in
|
|
109
147
|
* `.blume/package.json` lets Astro's framework-package crawl discover and bundle
|
|
@@ -138,7 +176,7 @@ export const runtimeDependencies = (options: {
|
|
|
138
176
|
// (and the user installs) exactly the backend it uses — nothing more.
|
|
139
177
|
deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
|
|
140
178
|
// Ask AI's provider SDK, when its backend needs one (gateway uses core `ai`).
|
|
141
|
-
if (config.ai.ask?.enabled) {
|
|
179
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
142
180
|
const askDep = askBackendRuntimeDep(config.ai.ask);
|
|
143
181
|
if (askDep) {
|
|
144
182
|
deps.push(askDep);
|
|
@@ -261,6 +299,65 @@ const reactIntegration = (compilerPath: string | null | undefined): string =>
|
|
|
261
299
|
? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })`
|
|
262
300
|
: `react({ ${REACT_EXCLUDE} })`;
|
|
263
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The `server.watch` block for the generated dev config. Keeps the watcher out
|
|
304
|
+
* of Astro's cache dir — but ONLY when the docs collection is rooted at a
|
|
305
|
+
* directory containing the runtime dir (a migrated, `content.root: "."`
|
|
306
|
+
* project). There, the glob loader's watcher match (`picomatch.isMatch(entry,
|
|
307
|
+
* pattern)` with array-OR semantics, where any negated pattern matches
|
|
308
|
+
* unrelated files) fires on every `.blume/.astro` write — "No entry type
|
|
309
|
+
* found" noise, and a `data-store.json` event can re-ingest the store file as
|
|
310
|
+
* a JSON entry and loop the sync. Everywhere else the watcher MUST see
|
|
311
|
+
* `.astro/data-store.json`: its change events are the only trigger for
|
|
312
|
+
* Astro's dev-time content invalidation (see vite-plugin-content-virtual-mod),
|
|
313
|
+
* and `.md` bodies are rendered into the store at load time — so ignoring the
|
|
314
|
+
* file serves stale `.md` HTML on every request until the server restarts,
|
|
315
|
+
* even though the loader logs a reload.
|
|
316
|
+
*/
|
|
317
|
+
const devWatchOption = (
|
|
318
|
+
outDir: string,
|
|
319
|
+
contentWatchesRuntimeDir: boolean | undefined
|
|
320
|
+
): string =>
|
|
321
|
+
contentWatchesRuntimeDir
|
|
322
|
+
? `
|
|
323
|
+
// Astro's cache dir sits inside the docs collection, whose watcher would
|
|
324
|
+
// otherwise churn (and can loop) on Astro's own writes. Trade-off: .md
|
|
325
|
+
// body edits need a dev-server restart in this layout.
|
|
326
|
+
watch: {
|
|
327
|
+
ignored: ${JSON.stringify([join(outDir, ".astro", "**")])},
|
|
328
|
+
},`
|
|
329
|
+
: "";
|
|
330
|
+
|
|
331
|
+
interface IntegrationBridgeOptions {
|
|
332
|
+
/** Config path relative to the generated Astro config. */
|
|
333
|
+
configFile: string;
|
|
334
|
+
/** SHA-256 used to invalidate Astro's generated config. */
|
|
335
|
+
sourceHash?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const renderIntegrationBridge = (
|
|
339
|
+
bridge: IntegrationBridgeOptions | undefined
|
|
340
|
+
) => {
|
|
341
|
+
if (!bridge) {
|
|
342
|
+
return {
|
|
343
|
+
configSourceMarker: "",
|
|
344
|
+
userConfigImports: "",
|
|
345
|
+
userConfigSetup: "",
|
|
346
|
+
userIntegrationSpread: "",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
configSourceMarker: bridge.sourceHash
|
|
351
|
+
? `// Blume config source SHA-256: ${bridge.sourceHash}\n`
|
|
352
|
+
: "",
|
|
353
|
+
userConfigImports: `import { dirname, resolve } from "node:path";\nimport { fileURLToPath } from "node:url";\nimport { createModuleLoader } from "blume/core/load-module.ts";\n`,
|
|
354
|
+
userConfigSetup: `const loadBlumeConfig = createModuleLoader();\nconst blumeConfig = await loadBlumeConfig(resolve(dirname(fileURLToPath(import.meta.url)), ${JSON.stringify(
|
|
355
|
+
bridge.configFile
|
|
356
|
+
)}));\n\n`,
|
|
357
|
+
userIntegrationSpread: ", ...(blumeConfig?.integrations ?? [])",
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
|
|
264
361
|
export const astroConfigTemplate = (options: {
|
|
265
362
|
context: ProjectContext;
|
|
266
363
|
config: ResolvedConfig;
|
|
@@ -286,6 +383,15 @@ export const astroConfigTemplate = (options: {
|
|
|
286
383
|
reactCompilerPath?: string | null;
|
|
287
384
|
/** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
|
|
288
385
|
aliases?: Record<string, string>;
|
|
386
|
+
/**
|
|
387
|
+
* Whether the filesystem `docs` collection is rooted at a directory that
|
|
388
|
+
* contains the runtime dir (a migrated, `content.root: "."` project) — the
|
|
389
|
+
* only layout where the dev watcher must be kept out of Astro's cache dir.
|
|
390
|
+
* See {@link devWatchOption} for why this must stay scoped.
|
|
391
|
+
*/
|
|
392
|
+
contentWatchesRuntimeDir?: boolean;
|
|
393
|
+
/** Bridge used to load configured integrations without serializing them. */
|
|
394
|
+
integrationBridge?: IntegrationBridgeOptions;
|
|
289
395
|
}): string => {
|
|
290
396
|
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
291
397
|
const {
|
|
@@ -331,6 +437,8 @@ export const astroConfigTemplate = (options: {
|
|
|
331
437
|
const adapterOption =
|
|
332
438
|
server && deployment.adapter ? `\n adapter: ${adapterExpr},` : "";
|
|
333
439
|
|
|
440
|
+
const sessionOption = resolveSessionOption(deployment);
|
|
441
|
+
|
|
334
442
|
const siteOption = deployment.site
|
|
335
443
|
? `\n site: ${JSON.stringify(deployment.site)},`
|
|
336
444
|
: "";
|
|
@@ -388,9 +496,10 @@ export const astroConfigTemplate = (options: {
|
|
|
388
496
|
)
|
|
389
497
|
.join(", ")}],`
|
|
390
498
|
: "";
|
|
391
|
-
const defineConfigImport =
|
|
392
|
-
|
|
393
|
-
:
|
|
499
|
+
const defineConfigImport = astroConfigImportLine({
|
|
500
|
+
hasFonts: fontEntries.length > 0,
|
|
501
|
+
hasSession: sessionOption.length > 0,
|
|
502
|
+
});
|
|
394
503
|
|
|
395
504
|
// Framework renderers are only wired in when an island (or Ask AI, for React)
|
|
396
505
|
// needs them. The core theme is Astro-first and ships no client JS.
|
|
@@ -446,19 +555,30 @@ export const astroConfigTemplate = (options: {
|
|
|
446
555
|
`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`
|
|
447
556
|
);
|
|
448
557
|
|
|
558
|
+
const watchOption = devWatchOption(
|
|
559
|
+
context.outDir,
|
|
560
|
+
options.contentWatchesRuntimeDir
|
|
561
|
+
);
|
|
562
|
+
const {
|
|
563
|
+
configSourceMarker,
|
|
564
|
+
userConfigImports,
|
|
565
|
+
userConfigSetup,
|
|
566
|
+
userIntegrationSpread,
|
|
567
|
+
} = renderIntegrationBridge(options.integrationBridge);
|
|
568
|
+
|
|
449
569
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
450
|
-
${defineConfigImport}
|
|
570
|
+
${configSourceMarker}${userConfigImports}${defineConfigImport}
|
|
451
571
|
import mdx from "@astrojs/mdx";
|
|
452
572
|
import tailwindcss from "@tailwindcss/vite";
|
|
453
573
|
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
|
|
454
574
|
${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
|
|
455
|
-
export default defineConfig({
|
|
575
|
+
${userConfigSetup}export default defineConfig({
|
|
456
576
|
root: ${JSON.stringify(context.outDir)},
|
|
457
577
|
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
458
578
|
outDir: ${JSON.stringify(astroOutDir(context))},
|
|
459
579
|
publicDir: ${JSON.stringify(`${context.root}/public`)},
|
|
460
|
-
output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
461
|
-
integrations: [${integrations.join(", ")}],
|
|
580
|
+
output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
581
|
+
integrations: [${integrations.join(", ")}${userIntegrationSpread}],
|
|
462
582
|
markdown: {
|
|
463
583
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
464
584
|
basePath: config.basePath,
|
|
@@ -480,18 +600,25 @@ export default defineConfig({
|
|
|
480
600
|
devToolbar: { enabled: false },
|
|
481
601
|
vite: {
|
|
482
602
|
plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
|
|
483
|
-
//
|
|
484
|
-
// CJS (\`dayjs/dayjs.min.js\`)
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
603
|
+
// The lazy client-side imports both land on CJS/UMD files: mermaid (for
|
|
604
|
+
// diagrams) statically imports dayjs as CJS (\`dayjs/dayjs.min.js\`), and
|
|
605
|
+
// epub-gen-memory's browser bundle is a browserified UMD. In dev, an
|
|
606
|
+
// un-pre-bundled dependency is served as raw ESM, where such a file
|
|
607
|
+
// exposes no \`default\` export — mermaid throws on load and diagrams
|
|
608
|
+
// render blank, and the EPUB export throws \`epub is not a function\`
|
|
609
|
+
// (the UMD finds no \`exports\`/\`define\` and strands its callable on
|
|
610
|
+
// \`window.epubGen\` instead). Forcing them through the dep optimizer
|
|
611
|
+
// restores the CJS interop. In a standalone install these dynamic imports
|
|
612
|
+
// live inside \`node_modules/blume\`, which Vite's optimizer scan doesn't
|
|
613
|
+
// crawl, so neither is discovered on its own — hence the explicit
|
|
614
|
+
// includes. They resolve through the \`blume\` package (they aren't direct
|
|
615
|
+
// deps of the generated project), so the nested \`blume > x\` form is
|
|
616
|
+
// required, and epub-gen-memory must name the \`/bundle\` subpath that is
|
|
617
|
+
// actually imported: optimizing the package root leaves that entry out.
|
|
618
|
+
// Production (Rollup) already handles the interop, so this only affects dev.
|
|
619
|
+
optimizeDeps: {
|
|
620
|
+
include: ["blume > mermaid", "blume > epub-gen-memory/bundle"],
|
|
621
|
+
},
|
|
495
622
|
// Blume's render-time deps are forced external on both build environments so
|
|
496
623
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
497
624
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -522,16 +649,7 @@ export default defineConfig({
|
|
|
522
649
|
server: {
|
|
523
650
|
fs: {
|
|
524
651
|
allow: ${JSON.stringify(fsAllow)},
|
|
525
|
-
}
|
|
526
|
-
// Keep the file watcher out of Astro's own cache dir. In a migrated
|
|
527
|
-
// (root-rooted) project the docs collection is rooted at the project dir,
|
|
528
|
-
// so its glob-loader watcher would otherwise fire on every write Astro
|
|
529
|
-
// makes under .blume/.astro (data-store.json, content module manifests,
|
|
530
|
-
// self-hosted fonts) -- pure noise the loader logs as "No entry type
|
|
531
|
-
// found". Vite appends this to its default ignores.
|
|
532
|
-
watch: {
|
|
533
|
-
ignored: ${JSON.stringify([join(context.outDir, ".astro", "**")])},
|
|
534
|
-
},
|
|
652
|
+
},${watchOption}
|
|
535
653
|
},
|
|
536
654
|
},
|
|
537
655
|
});
|
|
@@ -542,6 +660,21 @@ export default defineConfig({
|
|
|
542
660
|
export const stagedContentDir = (outDir: string): string =>
|
|
543
661
|
join(outDir, "content");
|
|
544
662
|
|
|
663
|
+
/**
|
|
664
|
+
* The runtime dir relative to the docs collection `base` when it sits inside
|
|
665
|
+
* it (a migrated, `content.root: "."` project) — null when it lives elsewhere.
|
|
666
|
+
* Drives both the collection's negative glob (`contentConfigTemplate`) and
|
|
667
|
+
* whether the dev watcher is kept out of Astro's cache dir (the
|
|
668
|
+
* `contentWatchesRuntimeDir` option of `astroConfigTemplate`).
|
|
669
|
+
*/
|
|
670
|
+
export const runtimeDirWithin = (
|
|
671
|
+
base: string,
|
|
672
|
+
outDir: string
|
|
673
|
+
): string | null => {
|
|
674
|
+
const rel = relative(base, outDir);
|
|
675
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
|
|
676
|
+
};
|
|
677
|
+
|
|
545
678
|
/**
|
|
546
679
|
* Astro's glob loader resolves `base` with `new URL(base, config.root)`. On
|
|
547
680
|
* Windows an absolute path like `C:\\docs\\content` makes `new URL` parse the
|
|
@@ -585,11 +718,8 @@ export const contentConfigTemplate = (options: {
|
|
|
585
718
|
// collection doesn't ingest ignored trees (`node_modules`, `snippets`, the
|
|
586
719
|
// staged bodies under `.blume/content`, …) as entries. This matters when
|
|
587
720
|
// the collection base is the project root (a migrated `.`-rooted project).
|
|
588
|
-
const outDirRel =
|
|
589
|
-
const outDirIgnore =
|
|
590
|
-
outDirRel && !outDirRel.startsWith("..") && !isAbsolute(outDirRel)
|
|
591
|
-
? [`!${outDirRel}/**`]
|
|
592
|
-
: [];
|
|
721
|
+
const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
|
|
722
|
+
const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
|
|
593
723
|
|
|
594
724
|
// With no filesystem source, no route renders through `docs`, so glob nothing.
|
|
595
725
|
// Beyond skipping wasted work, this is the only thing that keeps Astro's
|
|
@@ -806,7 +936,11 @@ import data from "blume:data";
|
|
|
806
936
|
const { strings } = Astro.props;
|
|
807
937
|
---
|
|
808
938
|
|
|
809
|
-
<AskAI
|
|
939
|
+
<AskAI
|
|
940
|
+
endpoint={data.config.ask?.endpoint ?? undefined}
|
|
941
|
+
strings={strings ?? data.ui.ask}
|
|
942
|
+
suggestions={data.config.ask?.suggestions ?? []}
|
|
943
|
+
/>
|
|
810
944
|
`
|
|
811
945
|
: `---
|
|
812
946
|
// Generated by Blume. Do not edit.
|
|
@@ -1151,6 +1285,7 @@ export async function GET({ props }: { props: { title: string } }) {
|
|
|
1151
1285
|
export const scalarReferenceTemplate = (options: {
|
|
1152
1286
|
configuration: Record<string, unknown>;
|
|
1153
1287
|
dataImport: string;
|
|
1288
|
+
noindex?: boolean;
|
|
1154
1289
|
route: string;
|
|
1155
1290
|
title: string;
|
|
1156
1291
|
}): string =>
|
|
@@ -1185,6 +1320,7 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
1185
1320
|
favicon={data.config.favicon}
|
|
1186
1321
|
appleIcon={data.config.appleIcon}
|
|
1187
1322
|
navigation={data.navigation}
|
|
1323
|
+
noindex={${options.noindex === true}}
|
|
1188
1324
|
pageTitle={${JSON.stringify(options.title)}}
|
|
1189
1325
|
route={${JSON.stringify(options.route)}}
|
|
1190
1326
|
searchEnabled={data.config.search.enabled}
|