blume 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/dist/cli/index.js +3949 -1403
- package/dist/cli/index.js.map +111 -96
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +33 -2
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +122 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +29 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +26 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/07-faq.mdx +9 -9
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +13 -4
- package/docs/advanced/custom-pages.mdx +4 -2
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +25 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +30 -3
- package/docs/configuration/theming.mdx +23 -0
- package/docs/content/components.mdx +15 -1
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +66 -57
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +282 -177
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +10 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +365 -113
- package/src/cli/commands/build.ts +91 -16
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +54 -22
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +28 -3
- package/src/core/define-components.ts +2 -0
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +12 -0
- package/src/core/includes.ts +633 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/manifest.ts +10 -0
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +235 -36
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +31 -2
- package/src/core/ui-packs/ar.ts +2 -0
- package/src/core/ui-packs/bg.ts +3 -0
- package/src/core/ui-packs/bn.ts +2 -0
- package/src/core/ui-packs/ca.ts +3 -0
- package/src/core/ui-packs/cs.ts +2 -0
- package/src/core/ui-packs/da.ts +2 -0
- package/src/core/ui-packs/de.ts +3 -0
- package/src/core/ui-packs/el.ts +3 -0
- package/src/core/ui-packs/es.ts +3 -0
- package/src/core/ui-packs/fa.ts +2 -0
- package/src/core/ui-packs/fi.ts +2 -0
- package/src/core/ui-packs/fr.ts +3 -0
- package/src/core/ui-packs/he.ts +2 -0
- package/src/core/ui-packs/hi.ts +2 -0
- package/src/core/ui-packs/hr.ts +3 -0
- package/src/core/ui-packs/hu.ts +3 -0
- package/src/core/ui-packs/id.ts +3 -0
- package/src/core/ui-packs/it.ts +2 -0
- package/src/core/ui-packs/ja.ts +3 -0
- package/src/core/ui-packs/ko.ts +3 -0
- package/src/core/ui-packs/nl.ts +3 -0
- package/src/core/ui-packs/no.ts +3 -0
- package/src/core/ui-packs/pl.ts +3 -0
- package/src/core/ui-packs/pt-br.ts +3 -0
- package/src/core/ui-packs/pt.ts +3 -0
- package/src/core/ui-packs/ro.ts +3 -0
- package/src/core/ui-packs/ru.ts +3 -0
- package/src/core/ui-packs/sk.ts +2 -0
- package/src/core/ui-packs/sr.ts +2 -0
- package/src/core/ui-packs/sv.ts +3 -0
- package/src/core/ui-packs/th.ts +2 -0
- package/src/core/ui-packs/tr.ts +3 -0
- package/src/core/ui-packs/uk.ts +3 -0
- package/src/core/ui-packs/vi.ts +2 -0
- package/src/core/ui-packs/zh-tw.ts +2 -0
- package/src/core/ui-packs/zh.ts +2 -0
- package/src/core/version-cut.ts +26 -6
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/deploy/vercel-negotiation.ts +49 -6
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/og/card.ts +1 -1
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +97 -13
- package/src/openapi/render-mdx.ts +66 -12
- package/src/openapi/scalar.ts +5 -16
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +47 -17
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +67 -13
- package/src/theme/fonts.ts +189 -16
- package/src/theme/sources.ts +49 -0
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/astro/generate.ts
CHANGED
|
@@ -14,7 +14,15 @@ import { pathToFileURL } from "node:url";
|
|
|
14
14
|
|
|
15
15
|
import { imageSize } from "image-size";
|
|
16
16
|
import pMap from "p-map";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
basename,
|
|
19
|
+
dirname,
|
|
20
|
+
isAbsolute,
|
|
21
|
+
join,
|
|
22
|
+
normalize,
|
|
23
|
+
relative,
|
|
24
|
+
resolve,
|
|
25
|
+
} from "pathe";
|
|
18
26
|
import { glob } from "tinyglobby";
|
|
19
27
|
|
|
20
28
|
import { buildAskData } from "../ai/ask-data.ts";
|
|
@@ -25,7 +33,10 @@ import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
|
25
33
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
26
34
|
import { validateUsedComponents } from "../core/component-diagnostics.ts";
|
|
27
35
|
import { analyzeComponentOverrides } from "../core/component-overrides.ts";
|
|
28
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
collectContentAssets,
|
|
38
|
+
rewriteRelativeImages,
|
|
39
|
+
} from "../core/content-assets.ts";
|
|
29
40
|
import type {
|
|
30
41
|
BlumeBanner,
|
|
31
42
|
BlumeData,
|
|
@@ -34,8 +45,14 @@ import type {
|
|
|
34
45
|
} from "../core/data.ts";
|
|
35
46
|
import { BlumeError } from "../core/diagnostics.ts";
|
|
36
47
|
import { writeTextAtomic } from "../core/fs-atomic.ts";
|
|
48
|
+
import {
|
|
49
|
+
apiUrl as githubApiUrl,
|
|
50
|
+
editBaseUrl as githubEditBaseUrl,
|
|
51
|
+
repoUrl as githubRepoUrl,
|
|
52
|
+
} from "../core/github.ts";
|
|
37
53
|
import { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
|
|
38
54
|
import { resolveFallbackLocale } from "../core/i18n.ts";
|
|
55
|
+
import { buildIncludeGraph } from "../core/includes.ts";
|
|
39
56
|
import {
|
|
40
57
|
validateNavTargets,
|
|
41
58
|
validateSearchPopularIcons,
|
|
@@ -44,14 +61,20 @@ import { packageRoot } from "../core/package-root.ts";
|
|
|
44
61
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
45
62
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
46
63
|
import { resolveDocsCollection } from "../core/sources/resolve.ts";
|
|
64
|
+
import { trimChar } from "../core/trim.ts";
|
|
47
65
|
import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
|
|
48
|
-
import type { Diagnostic, Navigation
|
|
66
|
+
import type { Diagnostic, Navigation } from "../core/types.ts";
|
|
49
67
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
50
68
|
import { missingFontFiles, resolveOgFonts } from "../og/derive.ts";
|
|
51
69
|
import type { DerivedOgFonts } from "../og/derive.ts";
|
|
52
70
|
import { resolveOgLogo } from "../og/logo.ts";
|
|
53
|
-
import type { OpenApiData } from "../openapi/model.ts";
|
|
54
|
-
import {
|
|
71
|
+
import type { ApiSpecData, OpenApiData } from "../openapi/model.ts";
|
|
72
|
+
import {
|
|
73
|
+
builtinProxyKinds,
|
|
74
|
+
hasScalarReferences,
|
|
75
|
+
needsPlaygroundProxy,
|
|
76
|
+
referenceRoutes,
|
|
77
|
+
} from "../openapi/references.ts";
|
|
55
78
|
import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
56
79
|
import { isOpenApiSource } from "../openapi/source.ts";
|
|
57
80
|
import { registry } from "../registry/registry.ts";
|
|
@@ -62,12 +85,22 @@ import {
|
|
|
62
85
|
examplesEntryTemplate,
|
|
63
86
|
tailwindEntryTemplate,
|
|
64
87
|
} from "../theme/entry.ts";
|
|
65
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
buildFontsCss,
|
|
90
|
+
configuredFonts,
|
|
91
|
+
fontLocaleCodes,
|
|
92
|
+
} from "../theme/fonts.ts";
|
|
66
93
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
94
|
+
import { rebaseSourceDirectives } from "../theme/sources.ts";
|
|
67
95
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
68
96
|
import { planComponentSlots } from "./component-slots.ts";
|
|
69
97
|
import type { ComponentSlotPlan } from "./component-slots.ts";
|
|
70
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
EXAMPLE_SCAN_GLOB,
|
|
100
|
+
discoverExamples,
|
|
101
|
+
exampleMarkdownLookup,
|
|
102
|
+
exampleScanRoots,
|
|
103
|
+
} from "./examples.ts";
|
|
71
104
|
import { discoverIslands } from "./islands.ts";
|
|
72
105
|
import {
|
|
73
106
|
customOgRoutes,
|
|
@@ -75,6 +108,8 @@ import {
|
|
|
75
108
|
hasGeneratedChangelog,
|
|
76
109
|
routeIsTaken,
|
|
77
110
|
} from "./pages.ts";
|
|
111
|
+
import { publishRuntimeModules } from "./runtime-modules.ts";
|
|
112
|
+
import type { RuntimeModuleId } from "./runtime-modules.ts";
|
|
78
113
|
import {
|
|
79
114
|
askComponentTemplate,
|
|
80
115
|
askEndpointTemplate,
|
|
@@ -93,12 +128,12 @@ import {
|
|
|
93
128
|
mcpEndpointTemplate,
|
|
94
129
|
mcpPageFile,
|
|
95
130
|
mixedbreadSearchEndpointTemplate,
|
|
131
|
+
notFoundMarkdownTemplate,
|
|
96
132
|
notFoundPageTemplate,
|
|
97
133
|
ogEndpointTemplate,
|
|
98
134
|
playgroundProxyTemplate,
|
|
99
135
|
rawMarkdownEndpointTemplate,
|
|
100
136
|
rssEndpointTemplate,
|
|
101
|
-
runtimeDirWithin,
|
|
102
137
|
staticJsonEndpointTemplate,
|
|
103
138
|
runtimeDependencies,
|
|
104
139
|
runtimePackageTemplate,
|
|
@@ -512,47 +547,6 @@ export const prerenderDepsPlugin = (
|
|
|
512
547
|
},
|
|
513
548
|
});
|
|
514
549
|
|
|
515
|
-
/** The subset of Rollup's plugin context `blume:server-app-resolve` needs. */
|
|
516
|
-
interface ServerAppResolveContext {
|
|
517
|
-
resolve: (source: string) => Promise<{ id: string } | null>;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Work around an Astro + Vite dev bug that breaks content renames.
|
|
522
|
-
*
|
|
523
|
-
* Astro's dev SSR entry is the virtual module `astro:server-app`, but its
|
|
524
|
-
* resolver only matches the exact id (`/^astro:server-app$/`). Whenever the
|
|
525
|
-
* route set changes — a content add, remove, or rename — Astro triggers a full
|
|
526
|
-
* page reload, during which Vite re-requests the entry as `astro:server-app.js`.
|
|
527
|
-
* The trailing `.js` misses Astro's filter, so the load fails ("Failed to load
|
|
528
|
-
* url astro:server-app.js") and Vite's SSR module runner is left corrupted: the
|
|
529
|
-
* in-memory content store never reconnects, so `getEntry` returns undefined and
|
|
530
|
-
* the renamed page 404s until the dev server is manually restarted.
|
|
531
|
-
*
|
|
532
|
-
* Stripping the spurious `.js` and delegating back to Astro's resolver lets the
|
|
533
|
-
* reload complete cleanly, so the renamed route resolves without a restart.
|
|
534
|
-
*/
|
|
535
|
-
export interface ServerAppResolvePlugin {
|
|
536
|
-
enforce: "pre";
|
|
537
|
-
name: string;
|
|
538
|
-
resolveId: (
|
|
539
|
-
this: ServerAppResolveContext,
|
|
540
|
-
id: string
|
|
541
|
-
) => Promise<string | null>;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export const serverAppResolvePlugin = (): ServerAppResolvePlugin => ({
|
|
545
|
-
enforce: "pre",
|
|
546
|
-
name: "blume:server-app-resolve",
|
|
547
|
-
async resolveId(id) {
|
|
548
|
-
if (id === "astro:server-app.js") {
|
|
549
|
-
const resolved = await this.resolve("astro:server-app");
|
|
550
|
-
return resolved?.id ?? null;
|
|
551
|
-
}
|
|
552
|
-
return null;
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
|
|
556
550
|
/** Astro integration package each non-React island framework needs installed. */
|
|
557
551
|
const ISLAND_FRAMEWORK_DEPS = new Map([
|
|
558
552
|
["svelte", "@astrojs/svelte"],
|
|
@@ -729,6 +723,21 @@ const readOptional = async (path: string | null): Promise<string> => {
|
|
|
729
723
|
}
|
|
730
724
|
};
|
|
731
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Read a user stylesheet (`theme.css`, `examples.css`) that gets inlined into
|
|
728
|
+
* a generated Tailwind entry under `outputDir`, re-rooting its relative
|
|
729
|
+
* `@source` paths so they still resolve from where the author wrote them.
|
|
730
|
+
*/
|
|
731
|
+
const readUserCss = async (
|
|
732
|
+
file: string | null,
|
|
733
|
+
outputDir: string
|
|
734
|
+
): Promise<string> => {
|
|
735
|
+
const css = await readOptional(file);
|
|
736
|
+
return file
|
|
737
|
+
? rebaseSourceDirectives(css, { from: file, to: outputDir })
|
|
738
|
+
: css;
|
|
739
|
+
};
|
|
740
|
+
|
|
732
741
|
/** Heuristically detect whether the project uses React islands. */
|
|
733
742
|
export const detectNeedsReact = async (root: string): Promise<boolean> => {
|
|
734
743
|
const matches = await glob(["**/*.{tsx,jsx}"], {
|
|
@@ -843,7 +852,19 @@ export const collectStaged = (project: BlumeProject): Map<string, string> => {
|
|
|
843
852
|
const staged = new Map<string, string>();
|
|
844
853
|
for (const page of project.graph.pages) {
|
|
845
854
|
if (page.collection === "staged" && page.entryId && page.body) {
|
|
846
|
-
|
|
855
|
+
// A colocated `./image.png` reference resolves against `.blume/content`
|
|
856
|
+
// once the body is materialized there, where the file does not exist.
|
|
857
|
+
// Point it at the served original instead — the same rewrite the
|
|
858
|
+
// agent-facing Markdown gets.
|
|
859
|
+
const text = page.sourcePath
|
|
860
|
+
? rewriteRelativeImages({
|
|
861
|
+
deployBase: project.config.deployment.base,
|
|
862
|
+
projectRoot: project.context.root,
|
|
863
|
+
source: page.body.text,
|
|
864
|
+
sourcePath: page.sourcePath,
|
|
865
|
+
})
|
|
866
|
+
: page.body.text;
|
|
867
|
+
staged.set(page.entryId, text);
|
|
847
868
|
}
|
|
848
869
|
}
|
|
849
870
|
return staged;
|
|
@@ -916,6 +937,22 @@ const readLogoSvg = (
|
|
|
916
937
|
const isStringShorthand = <T>(value: T | string): value is string =>
|
|
917
938
|
typeof value === "string";
|
|
918
939
|
|
|
940
|
+
/**
|
|
941
|
+
* The URL behind the header's repo mark. A string is used as-is: `github`
|
|
942
|
+
* drives the per-page edit link, the header mark and the manifest's
|
|
943
|
+
* `repository` together, so a project whose docs repo is private has to unset
|
|
944
|
+
* all three, and would otherwise have no way to point the mark at anything.
|
|
945
|
+
*/
|
|
946
|
+
const headerRepoUrl = (
|
|
947
|
+
repo: boolean | string,
|
|
948
|
+
derived: string | null
|
|
949
|
+
): string | null => {
|
|
950
|
+
if (isStringShorthand(repo)) {
|
|
951
|
+
return repo;
|
|
952
|
+
}
|
|
953
|
+
return repo ? derived : null;
|
|
954
|
+
};
|
|
955
|
+
|
|
919
956
|
/**
|
|
920
957
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
921
958
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
@@ -1156,7 +1193,11 @@ const resolveOgSite = (config: ResolvedConfig): string | undefined => {
|
|
|
1156
1193
|
: undefined;
|
|
1157
1194
|
};
|
|
1158
1195
|
|
|
1159
|
-
/**
|
|
1196
|
+
/**
|
|
1197
|
+
* The OG card's site-wide subtitle: `seo.og.description` (`false` omits it)
|
|
1198
|
+
* over the site description. The generated endpoint prefers a page's own
|
|
1199
|
+
* description and falls back to this.
|
|
1200
|
+
*/
|
|
1160
1201
|
const resolveOgDescription = (config: ResolvedConfig): string | undefined => {
|
|
1161
1202
|
const configured = config.seo.og.description;
|
|
1162
1203
|
if (configured === false) {
|
|
@@ -1165,14 +1206,29 @@ const resolveOgDescription = (config: ResolvedConfig): string | undefined => {
|
|
|
1165
1206
|
return configured ?? config.description;
|
|
1166
1207
|
};
|
|
1167
1208
|
|
|
1209
|
+
/**
|
|
1210
|
+
* Repo coordinates for content components. `host` and the derived `api` ride
|
|
1211
|
+
* along so a card on an Enterprise site links and queries that instance rather
|
|
1212
|
+
* than the public one.
|
|
1213
|
+
*/
|
|
1214
|
+
const resolveGithubData = (
|
|
1215
|
+
github: ResolvedConfig["github"]
|
|
1216
|
+
): BlumeData["config"]["github"] =>
|
|
1217
|
+
github
|
|
1218
|
+
? {
|
|
1219
|
+
api: githubApiUrl(github),
|
|
1220
|
+
host: github.host,
|
|
1221
|
+
owner: github.owner,
|
|
1222
|
+
repo: github.repo,
|
|
1223
|
+
}
|
|
1224
|
+
: null;
|
|
1225
|
+
|
|
1168
1226
|
/** Serialize the content graph into the data module the runtime consumes. */
|
|
1169
1227
|
export const buildRuntimeData = (project: BlumeProject): string => {
|
|
1170
1228
|
const { config, context, graph, manifest } = project;
|
|
1171
1229
|
const { github } = config;
|
|
1172
|
-
const repoUrl = github
|
|
1173
|
-
|
|
1174
|
-
: null;
|
|
1175
|
-
const editBase = github ? `${repoUrl}/edit/${github.branch}` : null;
|
|
1230
|
+
const repoUrl = github ? githubRepoUrl(github) : null;
|
|
1231
|
+
const editBase = github ? githubEditBaseUrl(github) : null;
|
|
1176
1232
|
const logo = resolveLogo(project);
|
|
1177
1233
|
const ogLogo = resolveOgMark(project, logo?.svg);
|
|
1178
1234
|
|
|
@@ -1181,7 +1237,18 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1181
1237
|
return null;
|
|
1182
1238
|
}
|
|
1183
1239
|
const rel = relative(context.root, sourcePath).split("\\").join("/");
|
|
1184
|
-
|
|
1240
|
+
// The edit path is repo-relative: `github.dir` places the project inside
|
|
1241
|
+
// the repo, so a source above the project dir (a monorepo vault beside the
|
|
1242
|
+
// docs app) still resolves to an in-repo file. A path that escapes the
|
|
1243
|
+
// repo itself has nothing to edit — fabricating one yields a 404 link.
|
|
1244
|
+
// `dir` is a bare string in the schema, so a leading slash (`/apps/docs`)
|
|
1245
|
+
// is trimmed rather than read as an absolute path — which would drop the
|
|
1246
|
+
// link from every page of a site that has always written it that way.
|
|
1247
|
+
const editDir = trimChar(github?.dir ?? "", "/");
|
|
1248
|
+
const editPath = normalize(editDir ? join(editDir, rel) : rel);
|
|
1249
|
+
if (editPath.startsWith("..") || isAbsolute(editPath)) {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1185
1252
|
return `${editBase}/${editPath}`;
|
|
1186
1253
|
};
|
|
1187
1254
|
|
|
@@ -1190,9 +1257,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1190
1257
|
// Resolve the header repo link per locale. API references no longer add a tab
|
|
1191
1258
|
// automatically — authors point a `navigation.tabs` entry at the reference
|
|
1192
1259
|
// route to surface it (see `referenceRoutes`).
|
|
1260
|
+
const markUrl = headerRepoUrl(config.navigation.repo, repoUrl);
|
|
1193
1261
|
const withRepoUrl = (nav: Navigation): Navigation => ({
|
|
1194
1262
|
...nav,
|
|
1195
|
-
repoUrl:
|
|
1263
|
+
repoUrl: markUrl,
|
|
1196
1264
|
});
|
|
1197
1265
|
|
|
1198
1266
|
// Resolved UI dictionaries: one per locale under i18n, English baseline
|
|
@@ -1221,6 +1289,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1221
1289
|
code,
|
|
1222
1290
|
withRepoUrl(
|
|
1223
1291
|
graph.navigationByLocale[code] ?? {
|
|
1292
|
+
actions: [],
|
|
1293
|
+
cta: null,
|
|
1224
1294
|
featured: [],
|
|
1225
1295
|
selectors: [],
|
|
1226
1296
|
sidebar: [],
|
|
@@ -1250,9 +1320,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1250
1320
|
discovery: {
|
|
1251
1321
|
agentReadability: config.seo.agentReadability,
|
|
1252
1322
|
llmsTxt: config.ai.llmsTxt.enabled,
|
|
1323
|
+
// Mirrors `buildSitemapFiles`: no site, no sitemap.
|
|
1324
|
+
sitemap: config.seo.sitemap && Boolean(config.deployment.site),
|
|
1253
1325
|
},
|
|
1254
1326
|
favicon: resolveFavicon(project),
|
|
1255
1327
|
feedback: config.feedback,
|
|
1328
|
+
github: resolveGithubData(github),
|
|
1256
1329
|
i18n: i18n
|
|
1257
1330
|
? {
|
|
1258
1331
|
defaultLocale: i18n.defaultLocale,
|
|
@@ -1267,6 +1340,15 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1267
1340
|
})),
|
|
1268
1341
|
}
|
|
1269
1342
|
: null,
|
|
1343
|
+
// `undefined` members drop out of the JSON snapshot; the null keeps the
|
|
1344
|
+
// "nothing configured" case explicit for the layouts.
|
|
1345
|
+
identity:
|
|
1346
|
+
config.seo.organization || config.seo.software
|
|
1347
|
+
? {
|
|
1348
|
+
organization: config.seo.organization,
|
|
1349
|
+
software: config.seo.software,
|
|
1350
|
+
}
|
|
1351
|
+
: null,
|
|
1270
1352
|
imageZoom: config.markdown.imageZoom,
|
|
1271
1353
|
logo,
|
|
1272
1354
|
mcp: config.ai.mcp.enabled
|
|
@@ -1311,8 +1393,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1311
1393
|
title: feed.title,
|
|
1312
1394
|
})),
|
|
1313
1395
|
// CSS variables for Astro's <Font> component; matches the astro.config
|
|
1314
|
-
// `fonts:` entries derived from the same theme.fonts config
|
|
1315
|
-
|
|
1396
|
+
// `fonts:` entries derived from the same theme.fonts config (and the
|
|
1397
|
+
// same locale-derived subsets, so preloads cover the scripts pages use).
|
|
1398
|
+
fontCssVars: configuredFonts(
|
|
1399
|
+
config.theme.fonts,
|
|
1400
|
+
fontLocaleCodes(config.i18n)
|
|
1401
|
+
),
|
|
1316
1402
|
navigation: withRepoUrl(graph.navigation),
|
|
1317
1403
|
// Per-locale navigation; the catch-all selects the active locale's tree.
|
|
1318
1404
|
navigationByLocale,
|
|
@@ -1332,6 +1418,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1332
1418
|
routes: manifest.routes.map((route) => ({
|
|
1333
1419
|
alternates: route.alternates,
|
|
1334
1420
|
collection: route.collection,
|
|
1421
|
+
description: route.description ?? null,
|
|
1335
1422
|
draft: route.draft,
|
|
1336
1423
|
editUrl: route.editUrl ?? editUrlFor(route.sourcePath),
|
|
1337
1424
|
entryId: route.entryId,
|
|
@@ -1414,16 +1501,24 @@ const planMcp = (
|
|
|
1414
1501
|
};
|
|
1415
1502
|
};
|
|
1416
1503
|
|
|
1417
|
-
/**
|
|
1504
|
+
/** A pass's runtime data modules, published together once every writer ran. */
|
|
1505
|
+
type RuntimeModules = Map<RuntimeModuleId, string>;
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Publish the MCP data snapshot (`blume:mcp-data`) and write the server
|
|
1509
|
+
* endpoint and discovery documents.
|
|
1510
|
+
*/
|
|
1418
1511
|
const writeMcpFiles = async (
|
|
1419
1512
|
project: BlumeProject,
|
|
1420
1513
|
plan: McpPlan,
|
|
1421
|
-
write: (path: string, content: string) => Promise<boolean
|
|
1514
|
+
write: (path: string, content: string) => Promise<boolean>,
|
|
1515
|
+
modules: RuntimeModules
|
|
1422
1516
|
): Promise<void> => {
|
|
1423
1517
|
if (!plan.enabled) {
|
|
1424
1518
|
return;
|
|
1425
1519
|
}
|
|
1426
1520
|
const data = await buildMcpData(project);
|
|
1521
|
+
modules.set("blume:mcp-data", JSON.stringify(data));
|
|
1427
1522
|
const discoveryInput = {
|
|
1428
1523
|
base: data.base,
|
|
1429
1524
|
name: data.name,
|
|
@@ -1432,13 +1527,9 @@ const writeMcpFiles = async (
|
|
|
1432
1527
|
version: data.version,
|
|
1433
1528
|
};
|
|
1434
1529
|
await Promise.all([
|
|
1435
|
-
write(
|
|
1436
|
-
join(plan.srcDir, "generated", "mcp-data.json"),
|
|
1437
|
-
`${JSON.stringify(data)}\n`
|
|
1438
|
-
),
|
|
1439
1530
|
write(
|
|
1440
1531
|
join(plan.srcDir, "pages", mcpPageFile(plan.route)),
|
|
1441
|
-
mcpEndpointTemplate(
|
|
1532
|
+
mcpEndpointTemplate()
|
|
1442
1533
|
),
|
|
1443
1534
|
write(
|
|
1444
1535
|
join(plan.dir, "discovery.ts"),
|
|
@@ -1460,11 +1551,7 @@ const writeMcpFiles = async (
|
|
|
1460
1551
|
* `prerender = false` export wins over the injection default.
|
|
1461
1552
|
*/
|
|
1462
1553
|
const planPlaygroundProxy = (config: ResolvedConfig, srcDir: string) => ({
|
|
1463
|
-
enabled:
|
|
1464
|
-
config.openapi.enabled &&
|
|
1465
|
-
config.openapi.renderer === "blume" &&
|
|
1466
|
-
config.openapi.playground.enabled &&
|
|
1467
|
-
config.openapi.playground.proxy === true,
|
|
1554
|
+
enabled: needsPlaygroundProxy(config),
|
|
1468
1555
|
entrypoint: join(srcDir, "blume-openapi", "api-proxy.ts"),
|
|
1469
1556
|
pattern: "/_api-proxy",
|
|
1470
1557
|
});
|
|
@@ -1480,46 +1567,68 @@ const planPlaygroundProxy = (config: ResolvedConfig, srcDir: string) => ({
|
|
|
1480
1567
|
* origin to allow and are skipped; AsyncAPI documents declare `servers` as a
|
|
1481
1568
|
* map and contribute nothing (the proxy is OpenAPI-only).
|
|
1482
1569
|
*/
|
|
1483
|
-
const
|
|
1570
|
+
const specOriginsOf = (spec: ApiSpecData): string[] => {
|
|
1484
1571
|
const origins = new Set<string>();
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
// `new URL("https://{region}.api.example.com")` parses — the braces land
|
|
1493
|
-
// in the hostname — so templated URLs need an explicit check or their
|
|
1494
|
-
// junk literal becomes an allowlist entry no real request can match.
|
|
1495
|
-
if (url.includes("{")) {
|
|
1496
|
-
continue;
|
|
1497
|
-
}
|
|
1498
|
-
try {
|
|
1499
|
-
origins.add(new URL(url).origin);
|
|
1500
|
-
} catch {
|
|
1501
|
-
// Not an absolute URL: nothing to allow.
|
|
1502
|
-
}
|
|
1572
|
+
// A GraphQL schema names no servers; its configured live endpoint is the
|
|
1573
|
+
// one origin the playground targets.
|
|
1574
|
+
if (spec.endpoint !== undefined) {
|
|
1575
|
+
try {
|
|
1576
|
+
origins.add(new URL(spec.endpoint).origin);
|
|
1577
|
+
} catch {
|
|
1578
|
+
// Not an absolute URL: nothing to allow.
|
|
1503
1579
|
}
|
|
1504
1580
|
}
|
|
1505
|
-
|
|
1581
|
+
// SAFETY: `document` is arbitrary parsed JSON; the assertion only names
|
|
1582
|
+
// the optional `servers` shape, and every access below re-checks it —
|
|
1583
|
+
// `Array.isArray(servers)` guards the list and `server.url ?? ""` the url.
|
|
1584
|
+
const { servers } = spec.document as { servers?: { url?: string }[] };
|
|
1585
|
+
for (const server of Array.isArray(servers) ? servers : []) {
|
|
1586
|
+
const url = server.url ?? "";
|
|
1587
|
+
// `new URL("https://{region}.api.example.com")` parses — the braces land
|
|
1588
|
+
// in the hostname — so templated URLs need an explicit check or their
|
|
1589
|
+
// junk literal becomes an allowlist entry no real request can match.
|
|
1590
|
+
if (url.includes("{")) {
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
origins.add(new URL(url).origin);
|
|
1595
|
+
} catch {
|
|
1596
|
+
// Not an absolute URL: nothing to allow.
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return [...origins];
|
|
1506
1600
|
};
|
|
1507
1601
|
|
|
1602
|
+
const specOrigins = (data: OpenApiData): string[] =>
|
|
1603
|
+
[...new Set(Object.values(data).flatMap(specOriginsOf))].toSorted();
|
|
1604
|
+
|
|
1508
1605
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1606
|
+
* Build-time diagnostics for playground sends the built-in proxy would refuse.
|
|
1607
|
+
* The baked-in allowlist pools every documented origin, but each spec's
|
|
1608
|
+
* playground only ever targets that spec's own servers/endpoint — so a
|
|
1609
|
+
* non-empty pool can still leave one spec's Send 403ing on every request.
|
|
1610
|
+
* Hence the check is per spec: any spec routed through the built-in proxy
|
|
1611
|
+
* whose own origins came out empty (no absolute `servers[].url`, no absolute
|
|
1612
|
+
* GraphQL `endpoint`) gets a warning naming it.
|
|
1513
1613
|
*/
|
|
1514
1614
|
const proxyAllowlistWarnings = (
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
): string[] =>
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1615
|
+
config: ResolvedConfig,
|
|
1616
|
+
data: OpenApiData
|
|
1617
|
+
): string[] => {
|
|
1618
|
+
const kinds = builtinProxyKinds(config);
|
|
1619
|
+
const warnings: string[] = [];
|
|
1620
|
+
for (const spec of Object.values(data)) {
|
|
1621
|
+
if (!kinds.includes(spec.kind) || specOriginsOf(spec).length > 0) {
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
warnings.push(
|
|
1625
|
+
spec.kind === "graphql"
|
|
1626
|
+
? `The "${spec.label}" GraphQL reference (${spec.route}) has playground.proxy: true, but no absolute endpoint is configured for it, so the built-in proxy has no origin to allow and will refuse every request its playground sends. Set the graphql block's (or the source's) \`endpoint\` to the live GraphQL URL, or point playground.proxy at an external proxy URL.`
|
|
1627
|
+
: `The "${spec.label}" reference (${spec.route}) has playground.proxy: true, but its spec declares no absolute servers[].url (relative and templated URLs carry no origin), so the built-in proxy has no origin to allow and will refuse every request its playground sends. Add an absolute server URL to the spec, or point playground.proxy at an external proxy URL.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
return warnings;
|
|
1631
|
+
};
|
|
1523
1632
|
|
|
1524
1633
|
/**
|
|
1525
1634
|
* Write the Ask AI endpoint and, unless the backend runs its own retrieval
|
|
@@ -1529,7 +1638,8 @@ const proxyAllowlistWarnings = (
|
|
|
1529
1638
|
const writeAskFiles = async (
|
|
1530
1639
|
project: BlumeProject,
|
|
1531
1640
|
srcDir: string,
|
|
1532
|
-
write: (path: string, content: string) => Promise<boolean
|
|
1641
|
+
write: (path: string, content: string) => Promise<boolean>,
|
|
1642
|
+
modules: RuntimeModules
|
|
1533
1643
|
): Promise<void> => {
|
|
1534
1644
|
const { ask } = project.config.ai;
|
|
1535
1645
|
if (!(ask?.enabled && !ask.endpoint)) {
|
|
@@ -1537,10 +1647,7 @@ const writeAskFiles = async (
|
|
|
1537
1647
|
}
|
|
1538
1648
|
const grounded = ask.provider !== "inkeep";
|
|
1539
1649
|
if (grounded) {
|
|
1540
|
-
await
|
|
1541
|
-
join(srcDir, "generated", "ask-data.json"),
|
|
1542
|
-
`${JSON.stringify(await buildAskData(project))}\n`
|
|
1543
|
-
);
|
|
1650
|
+
modules.set("blume:ask-data", JSON.stringify(await buildAskData(project)));
|
|
1544
1651
|
}
|
|
1545
1652
|
await write(
|
|
1546
1653
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
@@ -1553,10 +1660,11 @@ const writeAskFiles = async (
|
|
|
1553
1660
|
|
|
1554
1661
|
/**
|
|
1555
1662
|
* Write the default 404 page at Astro's reserved `src/pages/404.astro` path so
|
|
1556
|
-
* static builds emit `dist/404.html
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1663
|
+
* static builds emit `dist/404.html`, plus its Markdown twin at `404.md.ts`
|
|
1664
|
+
* (`dist/404.md`) for agents that ask a missing URL for Markdown. Both are
|
|
1665
|
+
* skipped when the project already owns `/404` (a custom `pages/404.astro` or
|
|
1666
|
+
* a `404.md` content page), letting it be fully overridden without a route
|
|
1667
|
+
* collision; `pruneOrphans` then removes any previously-generated copies.
|
|
1560
1668
|
*/
|
|
1561
1669
|
const writeNotFoundPage = async (
|
|
1562
1670
|
write: (path: string, content: string) => Promise<boolean>,
|
|
@@ -1567,7 +1675,10 @@ const writeNotFoundPage = async (
|
|
|
1567
1675
|
if (routeIsTaken(pages, contentPages, "/404")) {
|
|
1568
1676
|
return;
|
|
1569
1677
|
}
|
|
1570
|
-
await
|
|
1678
|
+
await Promise.all([
|
|
1679
|
+
write(join(srcDir, "pages", "404.astro"), notFoundPageTemplate()),
|
|
1680
|
+
write(join(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate()),
|
|
1681
|
+
]);
|
|
1571
1682
|
};
|
|
1572
1683
|
|
|
1573
1684
|
/**
|
|
@@ -1615,20 +1726,6 @@ const buildComponentSlots = async (
|
|
|
1615
1726
|
};
|
|
1616
1727
|
};
|
|
1617
1728
|
|
|
1618
|
-
/**
|
|
1619
|
-
* Whether the docs glob-loader's watcher observes the runtime dir: a
|
|
1620
|
-
* filesystem collection whose base contains it (a migrated, `content.root:
|
|
1621
|
-
* "."` project) — the one layout where the dev watcher must be kept out of
|
|
1622
|
-
* Astro's cache dir. See `devWatchOption` in templates.ts.
|
|
1623
|
-
*/
|
|
1624
|
-
const contentWatchesRuntimeDir = (
|
|
1625
|
-
hasFilesystemSource: boolean,
|
|
1626
|
-
collectionBase: string,
|
|
1627
|
-
context: ProjectContext
|
|
1628
|
-
): boolean =>
|
|
1629
|
-
hasFilesystemSource &&
|
|
1630
|
-
runtimeDirWithin(collectionBase, context.outDir) !== null;
|
|
1631
|
-
|
|
1632
1729
|
/** The OG endpoint fonts for a scanned project (see {@link resolveOgFonts}). */
|
|
1633
1730
|
const projectOgFonts = (project: BlumeProject): DerivedOgFonts =>
|
|
1634
1731
|
resolveOgFonts(
|
|
@@ -1671,13 +1768,12 @@ export const generateRuntime = async (
|
|
|
1671
1768
|
assertFontFilesExist(project);
|
|
1672
1769
|
const out = context.outDir;
|
|
1673
1770
|
const srcDir = join(out, "src");
|
|
1771
|
+
const generatedDir = join(srcDir, "generated");
|
|
1674
1772
|
const askPath = join(srcDir, "generated", "Ask.astro");
|
|
1675
|
-
const dataPath = join(srcDir, "generated", "data.json");
|
|
1676
1773
|
const themePath = join(srcDir, "generated", "app.css");
|
|
1677
1774
|
const searchClientPath = join(srcDir, "generated", "search-client.ts");
|
|
1678
1775
|
const examplesPath = join(srcDir, "generated", "examples.ts");
|
|
1679
1776
|
const examplesThemePath = join(srcDir, "generated", "examples.css");
|
|
1680
|
-
const openapiPath = join(srcDir, "generated", "openapi.json");
|
|
1681
1777
|
|
|
1682
1778
|
// Record every file this pass writes so orphans (from a now-disabled feature)
|
|
1683
1779
|
// can be pruned afterwards. `write` wraps the atomic writer and tracks paths.
|
|
@@ -1686,6 +1782,11 @@ export const generateRuntime = async (
|
|
|
1686
1782
|
written.add(normalize(path));
|
|
1687
1783
|
return writeIfChanged(path, content);
|
|
1688
1784
|
};
|
|
1785
|
+
// The data snapshots the generated pages import (`blume:data`, the search
|
|
1786
|
+
// index, …) are collected here and published in memory at the end of the
|
|
1787
|
+
// pass — see `runtime-modules.ts`. An id a feature leaves unset is
|
|
1788
|
+
// unpublished, the in-memory counterpart of `pruneOrphans`.
|
|
1789
|
+
const modules: RuntimeModules = new Map();
|
|
1689
1790
|
|
|
1690
1791
|
const depsLinkWarning = await ensureDepsLink(out);
|
|
1691
1792
|
|
|
@@ -1714,8 +1815,8 @@ export const generateRuntime = async (
|
|
|
1714
1815
|
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
1715
1816
|
detectNeedsReact(context.root),
|
|
1716
1817
|
detectUsesMath(context.root, staged.values()),
|
|
1717
|
-
|
|
1718
|
-
|
|
1818
|
+
readUserCss(context.themeFile, generatedDir),
|
|
1819
|
+
readUserCss(examplesCssFile(context.root, config), generatedDir),
|
|
1719
1820
|
loadIntegrationBridge(config, context),
|
|
1720
1821
|
discoverIslands(context.root),
|
|
1721
1822
|
discoverExamples(context.root, config.examples.source),
|
|
@@ -1780,8 +1881,9 @@ export const generateRuntime = async (
|
|
|
1780
1881
|
pattern: playgroundProxy.pattern,
|
|
1781
1882
|
});
|
|
1782
1883
|
}
|
|
1783
|
-
// Computed once: the endpoint template bakes it in below
|
|
1784
|
-
//
|
|
1884
|
+
// Computed once: the endpoint template bakes it in below. A spec that
|
|
1885
|
+
// contributes no origin of its own gets a per-spec diagnostic
|
|
1886
|
+
// (`proxyAllowlistWarnings`) — the proxy would refuse its every send.
|
|
1785
1887
|
const proxyOrigins = specOrigins(openApiData);
|
|
1786
1888
|
|
|
1787
1889
|
const hasStaged = staged.size > 0;
|
|
@@ -1804,21 +1906,15 @@ export const generateRuntime = async (
|
|
|
1804
1906
|
aliases: resolveTsconfigAliases(context.root),
|
|
1805
1907
|
askPath,
|
|
1806
1908
|
config,
|
|
1909
|
+
contentRoot: docsCollection.base,
|
|
1807
1910
|
contentRoutes: markdownRoutePaths(project),
|
|
1808
|
-
contentWatchesRuntimeDir: contentWatchesRuntimeDir(
|
|
1809
|
-
hasFilesystemSource,
|
|
1810
|
-
docsCollection.base,
|
|
1811
|
-
context
|
|
1812
|
-
),
|
|
1813
1911
|
context,
|
|
1814
|
-
dataPath,
|
|
1815
1912
|
examplesPath,
|
|
1816
1913
|
examplesThemePath,
|
|
1817
1914
|
integrationBridge,
|
|
1818
1915
|
needsReact,
|
|
1819
1916
|
needsSvelte,
|
|
1820
1917
|
needsVue,
|
|
1821
|
-
openapiPath,
|
|
1822
1918
|
pages,
|
|
1823
1919
|
reactCompilerPath,
|
|
1824
1920
|
searchClientPath,
|
|
@@ -1866,13 +1962,16 @@ export const generateRuntime = async (
|
|
|
1866
1962
|
exampleMapTemplate(exampleDiscovery.examples, config.basePath)
|
|
1867
1963
|
),
|
|
1868
1964
|
// The isolated Tailwind entry for `<Component />` preview frames: only
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1965
|
+
// the project (and an out-of-root examples directory) is scanned, so the
|
|
1966
|
+
// docs theme never reaches a preview. Anything further afield — a sibling
|
|
1967
|
+
// package the examples import — is the user's `@source` in examples.css.
|
|
1871
1968
|
write(
|
|
1872
1969
|
examplesThemePath,
|
|
1873
1970
|
examplesEntryTemplate({
|
|
1874
1971
|
configTokens: buildThemeCss(config.theme),
|
|
1875
|
-
sources:
|
|
1972
|
+
sources: exampleScanRoots(context.root, exampleDiscovery.dir).map(
|
|
1973
|
+
(dir) => `${dir}/${EXAMPLE_SCAN_GLOB}`
|
|
1974
|
+
),
|
|
1876
1975
|
userCss: userExamplesCss,
|
|
1877
1976
|
})
|
|
1878
1977
|
),
|
|
@@ -1927,8 +2026,8 @@ export const generateRuntime = async (
|
|
|
1927
2026
|
)
|
|
1928
2027
|
)
|
|
1929
2028
|
),
|
|
1930
|
-
writeAskFiles(project, srcDir, write),
|
|
1931
|
-
writeMcpFiles(project, mcp, write),
|
|
2029
|
+
writeAskFiles(project, srcDir, write, modules),
|
|
2030
|
+
writeMcpFiles(project, mcp, write, modules),
|
|
1932
2031
|
playgroundProxy.enabled
|
|
1933
2032
|
? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins))
|
|
1934
2033
|
: Promise.resolve(false),
|
|
@@ -1937,7 +2036,14 @@ export const generateRuntime = async (
|
|
|
1937
2036
|
if (config.seo.og.enabled) {
|
|
1938
2037
|
await write(
|
|
1939
2038
|
join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
1940
|
-
ogEndpointTemplate(
|
|
2039
|
+
ogEndpointTemplate(
|
|
2040
|
+
ogRoutes,
|
|
2041
|
+
{
|
|
2042
|
+
...projectOgFonts(project),
|
|
2043
|
+
pageDescriptions: config.seo.og.description !== false,
|
|
2044
|
+
},
|
|
2045
|
+
changelogIndex
|
|
2046
|
+
)
|
|
1941
2047
|
);
|
|
1942
2048
|
}
|
|
1943
2049
|
|
|
@@ -1975,10 +2081,7 @@ export const generateRuntime = async (
|
|
|
1975
2081
|
// Client-loaded providers (orama, flexsearch) ship a static index + endpoint.
|
|
1976
2082
|
if (servesStaticIndex(config.search.provider)) {
|
|
1977
2083
|
const documents = await buildSearchDocuments(project);
|
|
1978
|
-
|
|
1979
|
-
join(srcDir, "generated", "search.json"),
|
|
1980
|
-
`${JSON.stringify(documents)}\n`
|
|
1981
|
-
);
|
|
2084
|
+
modules.set("blume:search-index", JSON.stringify(documents));
|
|
1982
2085
|
await write(
|
|
1983
2086
|
join(srcDir, "pages", "blume-search.json.ts"),
|
|
1984
2087
|
searchEndpointTemplate()
|
|
@@ -1993,16 +2096,22 @@ export const generateRuntime = async (
|
|
|
1993
2096
|
);
|
|
1994
2097
|
}
|
|
1995
2098
|
|
|
2099
|
+
// The include graph (partial → including pages) behind `includeHmrPlugin`:
|
|
2100
|
+
// editing a partial invalidates every page that splices it. Written even
|
|
2101
|
+
// when empty so the plugin's configured path always resolves.
|
|
2102
|
+
await write(
|
|
2103
|
+
join(srcDir, "generated", "includes.json"),
|
|
2104
|
+
`${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`
|
|
2105
|
+
);
|
|
2106
|
+
|
|
1996
2107
|
const rawMarkdown = await buildRawMarkdown(project);
|
|
2108
|
+
modules.set("blume:raw-markdown", JSON.stringify(rawMarkdown));
|
|
1997
2109
|
// The originals behind the rewritten `/blume-assets/content/…` references in
|
|
1998
2110
|
// the agent-facing Markdown, plus the endpoint that serves them (and the
|
|
1999
2111
|
// remote-source assets materialized under `.blume/public/blume-assets`).
|
|
2000
2112
|
const contentAssets = await collectContentAssets(project);
|
|
2113
|
+
modules.set("blume:content-assets", JSON.stringify(contentAssets));
|
|
2001
2114
|
await Promise.all([
|
|
2002
|
-
write(
|
|
2003
|
-
join(srcDir, "generated", "raw-markdown.json"),
|
|
2004
|
-
`${JSON.stringify(rawMarkdown)}\n`
|
|
2005
|
-
),
|
|
2006
2115
|
write(
|
|
2007
2116
|
join(srcDir, "pages", "[...slug].md.ts"),
|
|
2008
2117
|
rawMarkdownEndpointTemplate("md")
|
|
@@ -2011,10 +2120,6 @@ export const generateRuntime = async (
|
|
|
2011
2120
|
join(srcDir, "pages", "[...slug].mdx.ts"),
|
|
2012
2121
|
rawMarkdownEndpointTemplate("mdx")
|
|
2013
2122
|
),
|
|
2014
|
-
write(
|
|
2015
|
-
join(srcDir, "generated", "content-assets.json"),
|
|
2016
|
-
`${JSON.stringify(contentAssets)}\n`
|
|
2017
|
-
),
|
|
2018
2123
|
write(
|
|
2019
2124
|
join(srcDir, "pages", "blume-assets", "[...asset].ts"),
|
|
2020
2125
|
contentAssetsEndpointTemplate(
|
|
@@ -2030,16 +2135,11 @@ export const generateRuntime = async (
|
|
|
2030
2135
|
const feedXml = Object.fromEntries(
|
|
2031
2136
|
feeds.map((feed) => [feed.type, renderRssFeed(feed)])
|
|
2032
2137
|
);
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
write(
|
|
2039
|
-
join(srcDir, "pages", "[section]", "rss.xml.ts"),
|
|
2040
|
-
rssEndpointTemplate()
|
|
2041
|
-
),
|
|
2042
|
-
]);
|
|
2138
|
+
modules.set("blume:rss", JSON.stringify(feedXml));
|
|
2139
|
+
await write(
|
|
2140
|
+
join(srcDir, "pages", "[section]", "rss.xml.ts"),
|
|
2141
|
+
rssEndpointTemplate()
|
|
2142
|
+
);
|
|
2043
2143
|
}
|
|
2044
2144
|
|
|
2045
2145
|
// Scalar-rendered API/AsyncAPI reference pages (`renderer: "scalar"`). One
|
|
@@ -2047,7 +2147,7 @@ export const generateRuntime = async (
|
|
|
2047
2147
|
// regenerated each run.
|
|
2048
2148
|
const warnings: string[] = [
|
|
2049
2149
|
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
2050
|
-
...proxyAllowlistWarnings(
|
|
2150
|
+
...proxyAllowlistWarnings(config, openApiData),
|
|
2051
2151
|
...reactCompilerWarnings(config, needsReact, reactCompilerPath),
|
|
2052
2152
|
...mcp.warnings,
|
|
2053
2153
|
...islandDiscovery.warnings,
|
|
@@ -2114,16 +2214,16 @@ export const generateRuntime = async (
|
|
|
2114
2214
|
);
|
|
2115
2215
|
}
|
|
2116
2216
|
|
|
2117
|
-
// `
|
|
2118
|
-
//
|
|
2119
|
-
//
|
|
2217
|
+
// `blume:openapi` is always published — even as `{}` — so the import resolves
|
|
2218
|
+
// whether or not a reference is enabled; the specs were parsed during the
|
|
2219
|
+
// scan, so this is just serialization. `blume:data` is the page data every
|
|
2220
|
+
// layout reads. Neither is "structural" for Astro; publishing hot-reloads.
|
|
2221
|
+
modules.set("blume:data", buildRuntimeData(project));
|
|
2222
|
+
modules.set("blume:openapi", JSON.stringify(openApiData));
|
|
2120
2223
|
// These write to distinct trees and never read one another, so they batch.
|
|
2121
|
-
// `
|
|
2122
|
-
//
|
|
2123
|
-
// own pruning), outside `.blume/src`, so a removed remote entry doesn't linger.
|
|
2224
|
+
// `writeStagedContent` owns the `.blume/content` tree (its own pruning),
|
|
2225
|
+
// outside `.blume/src`, so a removed remote entry doesn't linger.
|
|
2124
2226
|
await Promise.all([
|
|
2125
|
-
write(join(srcDir, "generated", "data.json"), buildRuntimeData(project)),
|
|
2126
|
-
write(openapiPath, `${JSON.stringify(openApiData)}\n`),
|
|
2127
2227
|
write(
|
|
2128
2228
|
join(out, "blume.manifest.json"),
|
|
2129
2229
|
`${JSON.stringify(project.manifest, null, 2)}\n`
|
|
@@ -2135,5 +2235,10 @@ export const generateRuntime = async (
|
|
|
2135
2235
|
// endpoint left behind after the feature was switched off.
|
|
2136
2236
|
await pruneOrphans(srcDir, written);
|
|
2137
2237
|
|
|
2238
|
+
// Publish last, once every page that imports a module is on disk: a live
|
|
2239
|
+
// dev server invalidates the changed modules and reloads the browser against
|
|
2240
|
+
// the finished tree, never a half-written one.
|
|
2241
|
+
publishRuntimeModules(modules);
|
|
2242
|
+
|
|
2138
2243
|
return { structuralChange: structural.some(Boolean), warnings };
|
|
2139
2244
|
};
|