blume 0.6.7 → 0.7.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/dist/cli/index.js +1179 -738
- package/dist/cli/index.js.map +52 -51
- package/dist/types/core/base-path.d.ts +38 -0
- package/dist/types/core/config-input.d.ts +74 -10
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +1 -3
- package/dist/types/core/schema.d.ts +95 -52
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/docs/02-deployment.mdx +16 -1
- package/docs/03-faq.mdx +8 -8
- package/docs/configuration/index.mdx +6 -0
- package/docs/content/components.mdx +29 -2
- package/docs/content/islands.mdx +8 -0
- package/docs/content/syntax.mdx +13 -0
- package/package.json +2 -1
- package/src/ai/agent-readability.ts +7 -2
- package/src/ai/ask.ts +12 -7
- package/src/ai/llms.ts +15 -4
- package/src/ai/mcp/data.ts +8 -4
- package/src/ai/mcp/server.ts +3 -0
- package/src/astro/component-slots.ts +5 -3
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +317 -144
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +8 -4
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +8 -3
- package/src/astro/templates.ts +166 -19
- package/src/cli/commands/build.ts +32 -19
- package/src/cli/commands/dev.ts +48 -15
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/validate.ts +1 -0
- package/src/cli/dev-lock.ts +26 -15
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/auto-type-table.ts +18 -8
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +3 -0
- package/src/components/index.ts +23 -1
- package/src/components/islands/ask-ai.tsx +12 -6
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +16 -1
- package/src/components/layout/Banner.astro +2 -1
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +2 -1
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +2 -1
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +5 -4
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +9 -4
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/RootLayout.astro +7 -4
- package/src/components/layout/Search.astro +13 -5
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/toc-element.ts +7 -1
- package/src/components/openapi/RequestPanel.astro +7 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +70 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +81 -15
- package/src/core/config.ts +5 -3
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +156 -88
- package/src/core/i18n-ui.ts +18 -3
- package/src/core/last-modified.ts +2 -0
- package/src/core/links.ts +38 -18
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +116 -55
- package/src/core/project-graph.ts +10 -9
- package/src/core/schema.ts +572 -621
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +58 -54
- package/src/core/sources/normalize.ts +116 -73
- package/src/core/sources/notion.ts +19 -10
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/deploy/redirects.ts +18 -0
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +14 -10
- package/src/markdown/base-links.ts +58 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +34 -9
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +3 -1
- package/src/openapi/references.ts +41 -17
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/registry/eject.ts +64 -8
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +2 -2
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +16 -6
- package/src/theme/entry.ts +85 -20
package/src/astro/generate.ts
CHANGED
|
@@ -43,7 +43,10 @@ import { isOpenApiSource } from "../openapi/source.ts";
|
|
|
43
43
|
import { registry } from "../registry/registry.ts";
|
|
44
44
|
import { buildSearchDocuments } from "../search/documents.ts";
|
|
45
45
|
import { searchProviderMeta, servesStaticIndex } from "../search/providers.ts";
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
examplesEntryTemplate,
|
|
48
|
+
tailwindEntryTemplate,
|
|
49
|
+
} from "../theme/entry.ts";
|
|
47
50
|
import { buildFontsCss, configuredCssVars } from "../theme/fonts.ts";
|
|
48
51
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
49
52
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
@@ -61,6 +64,7 @@ import {
|
|
|
61
64
|
envTemplate,
|
|
62
65
|
exampleMapTemplate,
|
|
63
66
|
exampleWrapperTemplate,
|
|
67
|
+
examplesPageTemplate,
|
|
64
68
|
exampleSlug,
|
|
65
69
|
islandMapTemplate,
|
|
66
70
|
islandWrapperTemplate,
|
|
@@ -93,6 +97,47 @@ const canResolveFrom = (fromDir: string, spec: string): boolean => {
|
|
|
93
97
|
}
|
|
94
98
|
};
|
|
95
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Absolute path to `babel-plugin-react-compiler`, resolved from Blume's own
|
|
102
|
+
* package root (Blume ships it). Returns null when React or the compiler is off.
|
|
103
|
+
*
|
|
104
|
+
* The path must be absolute: @vitejs/plugin-react resolves babel plugins from
|
|
105
|
+
* the *project* root, not `.blume/`, so a bare specifier fails in a user project
|
|
106
|
+
* that never installed the plugin directly. Resolving from `packageRoot()` binds
|
|
107
|
+
* to Blume's shipped copy regardless of the user's package manager or hoisting.
|
|
108
|
+
*/
|
|
109
|
+
const resolveReactCompiler = (
|
|
110
|
+
config: ResolvedConfig,
|
|
111
|
+
needsReact: boolean
|
|
112
|
+
): string | null => {
|
|
113
|
+
if (!(needsReact && config.react.compiler)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return createRequire(
|
|
118
|
+
pathToFileURL(join(packageRoot(), "_.js")).href
|
|
119
|
+
).resolve("babel-plugin-react-compiler");
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Warning (as a spreadable list) for the case where the React Compiler was
|
|
127
|
+
* requested but its plugin couldn't be resolved — so the build silently drops
|
|
128
|
+
* to uncompiled output rather than failing.
|
|
129
|
+
*/
|
|
130
|
+
const reactCompilerWarnings = (
|
|
131
|
+
config: ResolvedConfig,
|
|
132
|
+
needsReact: boolean,
|
|
133
|
+
compilerPath: string | null
|
|
134
|
+
): string[] =>
|
|
135
|
+
needsReact && config.react.compiler && !compilerPath
|
|
136
|
+
? [
|
|
137
|
+
"React Compiler is enabled but `babel-plugin-react-compiler` could not be resolved; falling back to an uncompiled build. Reinstall Blume, or set `react: { compiler: false }` to silence this.",
|
|
138
|
+
]
|
|
139
|
+
: [];
|
|
140
|
+
|
|
96
141
|
/**
|
|
97
142
|
* Realpath of the `astro` package node resolves from a directory, or null when
|
|
98
143
|
* none resolves. Comparing this for `.blume/` against Blume's own deps tells
|
|
@@ -145,7 +190,12 @@ const linkDepsJunction = async (
|
|
|
145
190
|
link: string,
|
|
146
191
|
depsDir: string
|
|
147
192
|
): Promise<void> => {
|
|
148
|
-
|
|
193
|
+
let existing: Awaited<ReturnType<typeof lstat>> | null;
|
|
194
|
+
try {
|
|
195
|
+
existing = await lstat(link);
|
|
196
|
+
} catch {
|
|
197
|
+
existing = null;
|
|
198
|
+
}
|
|
149
199
|
if (existing) {
|
|
150
200
|
if (!existing.isSymbolicLink()) {
|
|
151
201
|
return;
|
|
@@ -282,6 +332,45 @@ export const prerenderDepsPlugin = (
|
|
|
282
332
|
},
|
|
283
333
|
});
|
|
284
334
|
|
|
335
|
+
/** The subset of Rollup's plugin context `blume:server-app-resolve` needs. */
|
|
336
|
+
interface ServerAppResolveContext {
|
|
337
|
+
resolve: (source: string) => Promise<{ id: string } | null>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Work around an Astro + Vite dev bug that breaks content renames.
|
|
342
|
+
*
|
|
343
|
+
* Astro's dev SSR entry is the virtual module `astro:server-app`, but its
|
|
344
|
+
* resolver only matches the exact id (`/^astro:server-app$/`). Whenever the
|
|
345
|
+
* route set changes — a content add, remove, or rename — Astro triggers a full
|
|
346
|
+
* page reload, during which Vite re-requests the entry as `astro:server-app.js`.
|
|
347
|
+
* The trailing `.js` misses Astro's filter, so the load fails ("Failed to load
|
|
348
|
+
* url astro:server-app.js") and Vite's SSR module runner is left corrupted: the
|
|
349
|
+
* in-memory content store never reconnects, so `getEntry` returns undefined and
|
|
350
|
+
* the renamed page 404s until the dev server is manually restarted.
|
|
351
|
+
*
|
|
352
|
+
* Stripping the spurious `.js` and delegating back to Astro's resolver lets the
|
|
353
|
+
* reload complete cleanly, so the renamed route resolves without a restart.
|
|
354
|
+
*/
|
|
355
|
+
export const serverAppResolvePlugin = (): {
|
|
356
|
+
enforce: "pre";
|
|
357
|
+
name: string;
|
|
358
|
+
resolveId: (
|
|
359
|
+
this: ServerAppResolveContext,
|
|
360
|
+
id: string
|
|
361
|
+
) => Promise<string | null>;
|
|
362
|
+
} => ({
|
|
363
|
+
enforce: "pre",
|
|
364
|
+
name: "blume:server-app-resolve",
|
|
365
|
+
async resolveId(id) {
|
|
366
|
+
if (id === "astro:server-app.js") {
|
|
367
|
+
const resolved = await this.resolve("astro:server-app");
|
|
368
|
+
return resolved?.id ?? null;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
285
374
|
/** Astro integration package each non-React island framework needs installed. */
|
|
286
375
|
const ISLAND_FRAMEWORK_DEPS: Record<string, string> = {
|
|
287
376
|
svelte: "@astrojs/svelte",
|
|
@@ -309,6 +398,47 @@ const islandFrameworkWarnings = (
|
|
|
309
398
|
return warnings;
|
|
310
399
|
};
|
|
311
400
|
|
|
401
|
+
/** Absolute path to the configured `examples.css`, or null when unset. */
|
|
402
|
+
const examplesCssFile = (root: string, config: ResolvedConfig): string | null =>
|
|
403
|
+
config.examples.css ? join(root, config.examples.css) : null;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Write the per-example preview route (`{basePath}/blume-examples/<path>`)
|
|
407
|
+
* that `<Component />` iframes embed — the iframe boundary is what isolates
|
|
408
|
+
* previews from the docs CSS. Nested under `basePath` in the filesystem so
|
|
409
|
+
* the routes stay reachable behind a proxy that only forwards the base;
|
|
410
|
+
* pruneOrphans clears a stale copy when `basePath` changes or the last
|
|
411
|
+
* example is removed. Returns (as a spreadable list) a warning when the
|
|
412
|
+
* configured `examples.css` doesn't exist.
|
|
413
|
+
*/
|
|
414
|
+
const writeExamplesPreview = async (options: {
|
|
415
|
+
config: ResolvedConfig;
|
|
416
|
+
hasExamples: boolean;
|
|
417
|
+
root: string;
|
|
418
|
+
srcDir: string;
|
|
419
|
+
write: (path: string, content: string) => Promise<boolean>;
|
|
420
|
+
}): Promise<string[]> => {
|
|
421
|
+
const { config, hasExamples, root, srcDir, write } = options;
|
|
422
|
+
if (hasExamples) {
|
|
423
|
+
await write(
|
|
424
|
+
join(
|
|
425
|
+
srcDir,
|
|
426
|
+
"pages",
|
|
427
|
+
...config.basePath.split("/").filter(Boolean),
|
|
428
|
+
"blume-examples",
|
|
429
|
+
"[...path].astro"
|
|
430
|
+
),
|
|
431
|
+
examplesPageTemplate()
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const cssFile = examplesCssFile(root, config);
|
|
435
|
+
return cssFile && !existsSync(cssFile)
|
|
436
|
+
? [
|
|
437
|
+
`examples.css points at "${config.examples.css}", which doesn't exist; previews render without it.`,
|
|
438
|
+
]
|
|
439
|
+
: [];
|
|
440
|
+
};
|
|
441
|
+
|
|
312
442
|
/** Read a file's contents, or return an empty string if it is absent. */
|
|
313
443
|
const readOptional = async (path: string | null): Promise<string> => {
|
|
314
444
|
if (!path) {
|
|
@@ -397,12 +527,14 @@ export const pruneOrphans = async (
|
|
|
397
527
|
cwd: srcDir,
|
|
398
528
|
onlyFiles: true,
|
|
399
529
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.
|
|
405
|
-
|
|
530
|
+
const removals: Promise<void>[] = [];
|
|
531
|
+
for (const path of existing) {
|
|
532
|
+
const normalized = normalize(path);
|
|
533
|
+
if (!written.has(normalized)) {
|
|
534
|
+
removals.push(rm(normalized, { force: true }));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
await Promise.all(removals);
|
|
406
538
|
};
|
|
407
539
|
|
|
408
540
|
/**
|
|
@@ -601,7 +733,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
601
733
|
return null;
|
|
602
734
|
}
|
|
603
735
|
const rel = relative(context.root, sourcePath).split("\\").join("/");
|
|
604
|
-
|
|
736
|
+
const editPath = github?.dir ? `${github.dir}/${rel}` : rel;
|
|
737
|
+
return `${editBase}/${editPath}`;
|
|
605
738
|
};
|
|
606
739
|
|
|
607
740
|
const { i18n } = config;
|
|
@@ -659,6 +792,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
659
792
|
? { suggestions: config.ai.ask.suggestions }
|
|
660
793
|
: null,
|
|
661
794
|
banner: resolveBanner(config),
|
|
795
|
+
codeThemes: config.markdown.codeBlocks.theme,
|
|
662
796
|
codeWrap: config.markdown.code.wrap,
|
|
663
797
|
description: config.description,
|
|
664
798
|
favicon: resolveFavicon(project),
|
|
@@ -947,6 +1081,7 @@ export const generateRuntime = async (
|
|
|
947
1081
|
const themePath = join(srcDir, "generated", "app.css");
|
|
948
1082
|
const searchClientPath = join(srcDir, "generated", "search-client.ts");
|
|
949
1083
|
const examplesPath = join(srcDir, "generated", "examples.ts");
|
|
1084
|
+
const examplesThemePath = join(srcDir, "generated", "examples.css");
|
|
950
1085
|
const openapiPath = join(srcDir, "generated", "openapi.json");
|
|
951
1086
|
|
|
952
1087
|
// Record every file this pass writes so orphans (from a now-disabled feature)
|
|
@@ -962,29 +1097,34 @@ export const generateRuntime = async (
|
|
|
962
1097
|
const askEnabled = config.ai.ask?.enabled ?? false;
|
|
963
1098
|
const exportPdf = config.export.pdf;
|
|
964
1099
|
const exportEpub = config.export.epub;
|
|
1100
|
+
// Statically analyze `components.ts` overrides (never executed): drives the
|
|
1101
|
+
// `islands` group, hydration on layout/mdx overrides, string-path resolution,
|
|
1102
|
+
// and the "framework component with no client mode" diagnostic. Independent of
|
|
1103
|
+
// the discovery reads, so it joins the same parallel batch.
|
|
965
1104
|
const [
|
|
966
1105
|
pages,
|
|
967
1106
|
detectedReact,
|
|
968
1107
|
usesMath,
|
|
969
1108
|
userTheme,
|
|
1109
|
+
userExamplesCss,
|
|
970
1110
|
islandDiscovery,
|
|
971
1111
|
exampleDiscovery,
|
|
1112
|
+
componentSlots,
|
|
972
1113
|
] = await Promise.all([
|
|
973
1114
|
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
974
1115
|
detectNeedsReact(context.root),
|
|
975
1116
|
detectUsesMath(context.root),
|
|
976
1117
|
readOptional(context.themeFile),
|
|
1118
|
+
readOptional(examplesCssFile(context.root, config)),
|
|
977
1119
|
discoverIslands(context.root),
|
|
978
|
-
discoverExamples(context.root, config.examples),
|
|
1120
|
+
discoverExamples(context.root, config.examples.source),
|
|
1121
|
+
buildComponentSlots(context.componentsFile),
|
|
979
1122
|
]);
|
|
980
|
-
// Statically analyze `components.ts` overrides (never executed): drives the
|
|
981
|
-
// `islands` group, hydration on layout/mdx overrides, string-path resolution,
|
|
982
|
-
// and the "framework component with no client mode" diagnostic.
|
|
983
1123
|
const {
|
|
984
1124
|
plan: slotPlan,
|
|
985
1125
|
tags: overrideTags,
|
|
986
1126
|
warnings: overrideWarnings,
|
|
987
|
-
} =
|
|
1127
|
+
} = componentSlots;
|
|
988
1128
|
|
|
989
1129
|
// Each island/example framework enables its Astro renderer. React also
|
|
990
1130
|
// switches on for any project `.tsx`/`.jsx` and for Ask AI; Vue/Svelte are
|
|
@@ -999,6 +1139,12 @@ export const generateRuntime = async (
|
|
|
999
1139
|
const needsVue = frameworks.has("vue");
|
|
1000
1140
|
const needsSvelte = frameworks.has("svelte");
|
|
1001
1141
|
|
|
1142
|
+
// Absolute path to the React Compiler babel plugin (null when off). Resolved
|
|
1143
|
+
// here, Node-side, so the generated config points babel straight at Blume's
|
|
1144
|
+
// shipped copy — see resolveReactCompiler. Any unresolved-but-requested
|
|
1145
|
+
// warning is folded into `warnings` below (declared later).
|
|
1146
|
+
const reactCompilerPath = resolveReactCompiler(config, needsReact);
|
|
1147
|
+
|
|
1002
1148
|
// Custom pages that should get a generated OG card (the home most of all).
|
|
1003
1149
|
// Computed before the MCP `.well-known` routes are appended below — those are
|
|
1004
1150
|
// private and filtered out anyway, but the intent is the user's pages.
|
|
@@ -1020,120 +1166,135 @@ export const generateRuntime = async (
|
|
|
1020
1166
|
// project root for nothing — see contentConfigTemplate.
|
|
1021
1167
|
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
1022
1168
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1169
|
+
// All of these write to distinct generated paths and never read one another's
|
|
1170
|
+
// output, so the structural files, the per-convention hydration wrappers, and
|
|
1171
|
+
// the Ask/MCP writers all run in one parallel batch. Only the structural
|
|
1172
|
+
// writes' change flags feed `structuralChange`, so they stay a nested group.
|
|
1173
|
+
const [structural] = await Promise.all([
|
|
1174
|
+
Promise.all([
|
|
1175
|
+
write(
|
|
1176
|
+
join(out, "astro.config.mjs"),
|
|
1177
|
+
astroConfigTemplate({
|
|
1178
|
+
aliases: resolveTsconfigAliases(context.root),
|
|
1179
|
+
config,
|
|
1180
|
+
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
1181
|
+
context,
|
|
1182
|
+
dataPath,
|
|
1183
|
+
examplesPath,
|
|
1184
|
+
examplesThemePath,
|
|
1185
|
+
needsReact,
|
|
1186
|
+
needsSvelte,
|
|
1187
|
+
needsVue,
|
|
1188
|
+
openapiPath,
|
|
1189
|
+
pages,
|
|
1190
|
+
reactCompilerPath,
|
|
1191
|
+
searchClientPath,
|
|
1192
|
+
themePath,
|
|
1193
|
+
})
|
|
1194
|
+
),
|
|
1195
|
+
write(
|
|
1196
|
+
join(out, "package.json"),
|
|
1197
|
+
runtimePackageTemplate(
|
|
1198
|
+
runtimeDependencies({ config, needsReact, needsSvelte, needsVue })
|
|
1199
|
+
)
|
|
1200
|
+
),
|
|
1201
|
+
write(join(out, "tsconfig.json"), runtimeTsconfigTemplate()),
|
|
1202
|
+
write(join(srcDir, "env.d.ts"), envTemplate()),
|
|
1203
|
+
write(
|
|
1204
|
+
join(srcDir, "content.config.ts"),
|
|
1205
|
+
contentConfigTemplate({
|
|
1206
|
+
collection: resolveDocsCollection(config, context),
|
|
1207
|
+
config,
|
|
1208
|
+
context,
|
|
1209
|
+
filesystem: hasFilesystemSource,
|
|
1210
|
+
staged: hasStaged,
|
|
1211
|
+
})
|
|
1212
|
+
),
|
|
1213
|
+
write(
|
|
1214
|
+
join(srcDir, "pages", "[...slug].astro"),
|
|
1215
|
+
catchAllPageTemplate({
|
|
1216
|
+
askEnabled,
|
|
1217
|
+
exportEpub,
|
|
1218
|
+
exportPdf,
|
|
1219
|
+
mathEnabled: usesMath,
|
|
1220
|
+
needsReact,
|
|
1221
|
+
})
|
|
1222
|
+
),
|
|
1223
|
+
write(join(srcDir, "generated", "components.ts"), slotPlan.module),
|
|
1224
|
+
write(
|
|
1225
|
+
join(srcDir, "generated", "islands.ts"),
|
|
1226
|
+
islandMapTemplate(islandDiscovery.islands)
|
|
1227
|
+
),
|
|
1228
|
+
write(
|
|
1229
|
+
join(srcDir, "generated", "examples.ts"),
|
|
1230
|
+
exampleMapTemplate(exampleDiscovery.examples, config.basePath)
|
|
1231
|
+
),
|
|
1232
|
+
// The isolated Tailwind entry for `<Component />` preview frames: only
|
|
1233
|
+
// example files (and the project sources they import) are scanned, so
|
|
1234
|
+
// the docs theme never reaches a preview.
|
|
1235
|
+
write(
|
|
1236
|
+
examplesThemePath,
|
|
1237
|
+
examplesEntryTemplate({
|
|
1238
|
+
configTokens: buildThemeCss(config.theme),
|
|
1239
|
+
sources: [`${context.root}/**/*.{astro,jsx,svelte,ts,tsx,vue}`],
|
|
1240
|
+
userCss: userExamplesCss,
|
|
1241
|
+
})
|
|
1242
|
+
),
|
|
1243
|
+
write(
|
|
1039
1244
|
themePath,
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1245
|
+
tailwindEntryTemplate({
|
|
1246
|
+
configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
|
|
1247
|
+
sources: [
|
|
1248
|
+
`${BLUME_SRC}/**/*.{astro,ts,tsx}`,
|
|
1249
|
+
`${context.root}/**/*.{astro,mdx,ts,tsx}`,
|
|
1250
|
+
],
|
|
1251
|
+
twoslashCss: twoslashCss(),
|
|
1252
|
+
userTheme,
|
|
1253
|
+
})
|
|
1254
|
+
),
|
|
1255
|
+
]),
|
|
1256
|
+
// Per-island hydration wrappers for the `islands/` convention. The map
|
|
1257
|
+
// module (written above, always) imports these; orphans from removed
|
|
1258
|
+
// islands are pruned at the end of the pass.
|
|
1259
|
+
Promise.all(
|
|
1260
|
+
islandDiscovery.islands.map((island) =>
|
|
1261
|
+
write(
|
|
1262
|
+
join(srcDir, "generated", "islands", `${island.name}.astro`),
|
|
1263
|
+
islandWrapperTemplate(island)
|
|
1264
|
+
)
|
|
1046
1265
|
)
|
|
1047
1266
|
),
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
),
|
|
1060
|
-
write(
|
|
1061
|
-
join(srcDir, "pages", "[...slug].astro"),
|
|
1062
|
-
catchAllPageTemplate({
|
|
1063
|
-
askEnabled,
|
|
1064
|
-
exportEpub,
|
|
1065
|
-
exportPdf,
|
|
1066
|
-
mathEnabled: usesMath,
|
|
1067
|
-
needsReact,
|
|
1068
|
-
})
|
|
1069
|
-
),
|
|
1070
|
-
write(join(srcDir, "generated", "components.ts"), slotPlan.module),
|
|
1071
|
-
write(
|
|
1072
|
-
join(srcDir, "generated", "islands.ts"),
|
|
1073
|
-
islandMapTemplate(islandDiscovery.islands)
|
|
1074
|
-
),
|
|
1075
|
-
write(
|
|
1076
|
-
join(srcDir, "generated", "examples.ts"),
|
|
1077
|
-
exampleMapTemplate(exampleDiscovery.examples)
|
|
1267
|
+
// Per-override hydration wrappers for `defineComponents` islands and
|
|
1268
|
+
// `client:*` layout/mdx overrides. The generated `components.ts` (written
|
|
1269
|
+
// above) imports these; orphans from removed overrides are pruned at the
|
|
1270
|
+
// end of the pass.
|
|
1271
|
+
Promise.all(
|
|
1272
|
+
slotPlan.wrappers.map((wrapper) =>
|
|
1273
|
+
write(
|
|
1274
|
+
join(srcDir, "generated", "component-slots", `${wrapper.name}.astro`),
|
|
1275
|
+
wrapper.content
|
|
1276
|
+
)
|
|
1277
|
+
)
|
|
1078
1278
|
),
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1279
|
+
// Per-example live wrappers for the `examples/` convention, resolved by
|
|
1280
|
+
// `<Component path>` through the `examples.ts` map (written above, always).
|
|
1281
|
+
Promise.all(
|
|
1282
|
+
exampleDiscovery.examples.map((example) =>
|
|
1283
|
+
write(
|
|
1284
|
+
join(
|
|
1285
|
+
srcDir,
|
|
1286
|
+
"generated",
|
|
1287
|
+
"examples",
|
|
1288
|
+
`${exampleSlug(example.path)}.astro`
|
|
1289
|
+
),
|
|
1290
|
+
exampleWrapperTemplate(example)
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1090
1293
|
),
|
|
1294
|
+
writeAskFiles(project, srcDir, write),
|
|
1295
|
+
writeMcpFiles(project, mcp, write),
|
|
1091
1296
|
]);
|
|
1092
1297
|
|
|
1093
|
-
// Per-island hydration wrappers for the `islands/` convention. The map module
|
|
1094
|
-
// (written above, always) imports these; orphans from removed islands are
|
|
1095
|
-
// pruned at the end of the pass.
|
|
1096
|
-
await Promise.all(
|
|
1097
|
-
islandDiscovery.islands.map((island) =>
|
|
1098
|
-
write(
|
|
1099
|
-
join(srcDir, "generated", "islands", `${island.name}.astro`),
|
|
1100
|
-
islandWrapperTemplate(island)
|
|
1101
|
-
)
|
|
1102
|
-
)
|
|
1103
|
-
);
|
|
1104
|
-
|
|
1105
|
-
// Per-override hydration wrappers for `defineComponents` islands and `client:*`
|
|
1106
|
-
// layout/mdx overrides. The generated `components.ts` (written above) imports
|
|
1107
|
-
// these; orphans from removed overrides are pruned at the end of the pass.
|
|
1108
|
-
await Promise.all(
|
|
1109
|
-
slotPlan.wrappers.map((wrapper) =>
|
|
1110
|
-
write(
|
|
1111
|
-
join(srcDir, "generated", "component-slots", `${wrapper.name}.astro`),
|
|
1112
|
-
wrapper.content
|
|
1113
|
-
)
|
|
1114
|
-
)
|
|
1115
|
-
);
|
|
1116
|
-
|
|
1117
|
-
// Per-example live wrappers for the `examples/` convention, resolved by
|
|
1118
|
-
// `<Component path>` through the `examples.ts` map (written above, always).
|
|
1119
|
-
await Promise.all(
|
|
1120
|
-
exampleDiscovery.examples.map((example) =>
|
|
1121
|
-
write(
|
|
1122
|
-
join(
|
|
1123
|
-
srcDir,
|
|
1124
|
-
"generated",
|
|
1125
|
-
"examples",
|
|
1126
|
-
`${exampleSlug(example.path)}.astro`
|
|
1127
|
-
),
|
|
1128
|
-
exampleWrapperTemplate(example)
|
|
1129
|
-
)
|
|
1130
|
-
)
|
|
1131
|
-
);
|
|
1132
|
-
|
|
1133
|
-
await writeAskFiles(project, srcDir, write);
|
|
1134
|
-
|
|
1135
|
-
await writeMcpFiles(project, mcp, write);
|
|
1136
|
-
|
|
1137
1298
|
if (config.seo.og.enabled) {
|
|
1138
1299
|
await write(
|
|
1139
1300
|
join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
@@ -1155,12 +1316,23 @@ export const generateRuntime = async (
|
|
|
1155
1316
|
);
|
|
1156
1317
|
}
|
|
1157
1318
|
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1319
|
+
// Three independent writes: the per-example preview routes that
|
|
1320
|
+
// `<Component />` iframes embed (returning a warning when the configured
|
|
1321
|
+
// examples.css is missing), the default 404 page (`/404`, unless the project
|
|
1322
|
+
// already owns the route), and the provider-specific client loader behind
|
|
1323
|
+
// the `blume:search-client` alias — always (re)generated so the alias
|
|
1324
|
+
// resolves even when search is disabled.
|
|
1325
|
+
const [examplesWarnings] = await Promise.all([
|
|
1326
|
+
writeExamplesPreview({
|
|
1327
|
+
config,
|
|
1328
|
+
hasExamples: exampleDiscovery.examples.length > 0,
|
|
1329
|
+
root: context.root,
|
|
1330
|
+
srcDir,
|
|
1331
|
+
write,
|
|
1332
|
+
}),
|
|
1333
|
+
writeNotFoundPage(write, srcDir, pages, project.graph.pages),
|
|
1334
|
+
write(searchClientPath, searchClientTemplate(config)),
|
|
1335
|
+
]);
|
|
1164
1336
|
|
|
1165
1337
|
// Client-loaded providers (orama, flexsearch) ship a static index + endpoint.
|
|
1166
1338
|
if (servesStaticIndex(config.search.provider)) {
|
|
@@ -1222,9 +1394,11 @@ export const generateRuntime = async (
|
|
|
1222
1394
|
// mounted on its configured route and regenerated each run.
|
|
1223
1395
|
const warnings: string[] = [
|
|
1224
1396
|
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
1397
|
+
...reactCompilerWarnings(config, needsReact, reactCompilerPath),
|
|
1225
1398
|
...mcp.warnings,
|
|
1226
1399
|
...islandDiscovery.warnings,
|
|
1227
1400
|
...exampleDiscovery.warnings,
|
|
1401
|
+
...examplesWarnings,
|
|
1228
1402
|
...overrideWarnings,
|
|
1229
1403
|
];
|
|
1230
1404
|
|
|
@@ -1302,27 +1476,26 @@ export const generateRuntime = async (
|
|
|
1302
1476
|
);
|
|
1303
1477
|
}
|
|
1304
1478
|
|
|
1305
|
-
// Data and manifest are not "structural" for Astro; they hot-reload.
|
|
1306
|
-
await write(
|
|
1307
|
-
join(srcDir, "generated", "data.json"),
|
|
1308
|
-
buildRuntimeData(project)
|
|
1309
|
-
);
|
|
1310
1479
|
// The parsed OpenAPI specs behind the `blume:openapi` alias. Always written
|
|
1311
1480
|
// (even as `{}`) so the alias resolves whether or not a reference is enabled;
|
|
1312
1481
|
// the source parsed the specs during the scan, so this is just serialization.
|
|
1313
1482
|
const openApiSource = project.sources.find(isOpenApiSource);
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
)
|
|
1318
|
-
await
|
|
1319
|
-
join(
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1483
|
+
// These write to distinct trees and never read one another, so they batch.
|
|
1484
|
+
// `data.json`/`openapi.json` and the manifest are not "structural" for Astro;
|
|
1485
|
+
// they hot-reload. `writeStagedContent` owns the `.blume/content` tree (its
|
|
1486
|
+
// own pruning), outside `.blume/src`, so a removed remote entry doesn't linger.
|
|
1487
|
+
await Promise.all([
|
|
1488
|
+
write(join(srcDir, "generated", "data.json"), buildRuntimeData(project)),
|
|
1489
|
+
write(
|
|
1490
|
+
openapiPath,
|
|
1491
|
+
`${JSON.stringify(openApiSource ? openApiSource.openApiData() : {})}\n`
|
|
1492
|
+
),
|
|
1493
|
+
write(
|
|
1494
|
+
join(out, "blume.manifest.json"),
|
|
1495
|
+
`${JSON.stringify(project.manifest, null, 2)}\n`
|
|
1496
|
+
),
|
|
1497
|
+
writeStagedContent(out, staged),
|
|
1498
|
+
]);
|
|
1326
1499
|
|
|
1327
1500
|
// Remove anything under `.blume/src` this pass didn't write — e.g. an Ask AI
|
|
1328
1501
|
// endpoint left behind after the feature was switched off.
|
package/src/astro/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
generateRuntime,
|
|
3
|
+
prerenderDepsPlugin,
|
|
4
|
+
serverAppResolvePlugin,
|
|
5
|
+
} from "./generate.ts";
|
|
2
6
|
export type { GenerateResult } from "./generate.ts";
|
|
3
7
|
export { blumeIntegration } from "./integration.ts";
|
|
4
8
|
export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
|
package/src/astro/integration.ts
CHANGED
|
@@ -30,17 +30,21 @@ const overlayChannel = (): OverlayChannel | undefined =>
|
|
|
30
30
|
* overlay clears itself on the next successful HMR update.
|
|
31
31
|
*/
|
|
32
32
|
export const showBlumeErrorOverlay = (diagnostics: Diagnostic[]): void => {
|
|
33
|
-
const errors =
|
|
34
|
-
|
|
35
|
-
.
|
|
33
|
+
const errors: Diagnostic[] = [];
|
|
34
|
+
for (const diagnostic of diagnostics) {
|
|
35
|
+
if (diagnostic.severity === "error") {
|
|
36
|
+
errors.push(enrichDiagnostic(diagnostic));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
36
39
|
const channel = overlayChannel();
|
|
37
40
|
if (errors.length === 0 || !channel) {
|
|
38
41
|
return;
|
|
39
42
|
}
|
|
40
43
|
const body = errors
|
|
41
44
|
.map((diagnostic) => {
|
|
45
|
+
const lineSuffix = diagnostic.line ? `:${diagnostic.line}` : "";
|
|
42
46
|
const where = diagnostic.file
|
|
43
|
-
? `\n at ${diagnostic.file}${
|
|
47
|
+
? `\n at ${diagnostic.file}${lineSuffix}`
|
|
44
48
|
: "";
|
|
45
49
|
const fix = diagnostic.suggestion
|
|
46
50
|
? `\n fix: ${diagnostic.suggestion}`
|
package/src/astro/islands.ts
CHANGED
|
@@ -97,12 +97,14 @@ export const discoverIslands = async (
|
|
|
97
97
|
const warnings: string[] = [];
|
|
98
98
|
const seen = new Map<string, string>();
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// Extracted so the skip paths become early `return`s (one `continue` budget
|
|
101
|
+
// per loop under the lint rule) instead of `continue` statements.
|
|
102
|
+
const collectIsland = (file: string, source: string): void => {
|
|
101
103
|
const base = basename(file);
|
|
102
104
|
const ext = base.match(ISLAND_FILE)?.groups?.ext;
|
|
103
105
|
const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
|
|
104
106
|
if (!framework) {
|
|
105
|
-
|
|
107
|
+
return;
|
|
106
108
|
}
|
|
107
109
|
const name = base.replace(ISLAND_FILE, "");
|
|
108
110
|
// The name is used verbatim as both an MDX tag and an unquoted object key
|
|
@@ -113,22 +115,26 @@ export const discoverIslands = async (
|
|
|
113
115
|
warnings.push(
|
|
114
116
|
`Island "${file}" must have a PascalCase identifier filename to be used in MDX (letters, digits, and underscores only, e.g. Counter.tsx → <Counter />); skipping it.`
|
|
115
117
|
);
|
|
116
|
-
|
|
118
|
+
return;
|
|
117
119
|
}
|
|
118
120
|
const existing = seen.get(name);
|
|
119
121
|
if (existing) {
|
|
120
122
|
warnings.push(
|
|
121
123
|
`Two islands both resolve to <${name}> ("${existing}" and "${file}"); ignoring the second. Give them distinct filenames.`
|
|
122
124
|
);
|
|
123
|
-
|
|
125
|
+
return;
|
|
124
126
|
}
|
|
125
127
|
seen.set(name, file);
|
|
126
128
|
islands.push({
|
|
127
|
-
client: readClientMode(
|
|
129
|
+
client: readClientMode(source, file, warnings),
|
|
128
130
|
file,
|
|
129
131
|
framework,
|
|
130
132
|
name,
|
|
131
133
|
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
for (const [index, file] of files.entries()) {
|
|
137
|
+
collectIsland(file, sources[index] ?? "");
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
return { islands, warnings };
|