blume 1.5.2 → 1.6.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 +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- 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 +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- 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 +23 -14
- 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/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- 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/dropdown-dismiss.ts +122 -0
- 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 +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -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 +18 -2
- 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 +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- 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 +226 -35
- 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 +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- 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/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 +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- 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 +65 -11
- package/src/theme/fonts.ts +189 -16
- 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,7 +85,11 @@ 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";
|
|
67
94
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
68
95
|
import { planComponentSlots } from "./component-slots.ts";
|
|
@@ -98,7 +125,6 @@ import {
|
|
|
98
125
|
playgroundProxyTemplate,
|
|
99
126
|
rawMarkdownEndpointTemplate,
|
|
100
127
|
rssEndpointTemplate,
|
|
101
|
-
runtimeDirWithin,
|
|
102
128
|
staticJsonEndpointTemplate,
|
|
103
129
|
runtimeDependencies,
|
|
104
130
|
runtimePackageTemplate,
|
|
@@ -512,47 +538,6 @@ export const prerenderDepsPlugin = (
|
|
|
512
538
|
},
|
|
513
539
|
});
|
|
514
540
|
|
|
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
541
|
/** Astro integration package each non-React island framework needs installed. */
|
|
557
542
|
const ISLAND_FRAMEWORK_DEPS = new Map([
|
|
558
543
|
["svelte", "@astrojs/svelte"],
|
|
@@ -843,7 +828,19 @@ export const collectStaged = (project: BlumeProject): Map<string, string> => {
|
|
|
843
828
|
const staged = new Map<string, string>();
|
|
844
829
|
for (const page of project.graph.pages) {
|
|
845
830
|
if (page.collection === "staged" && page.entryId && page.body) {
|
|
846
|
-
|
|
831
|
+
// A colocated `./image.png` reference resolves against `.blume/content`
|
|
832
|
+
// once the body is materialized there, where the file does not exist.
|
|
833
|
+
// Point it at the served original instead — the same rewrite the
|
|
834
|
+
// agent-facing Markdown gets.
|
|
835
|
+
const text = page.sourcePath
|
|
836
|
+
? rewriteRelativeImages({
|
|
837
|
+
deployBase: project.config.deployment.base,
|
|
838
|
+
projectRoot: project.context.root,
|
|
839
|
+
source: page.body.text,
|
|
840
|
+
sourcePath: page.sourcePath,
|
|
841
|
+
})
|
|
842
|
+
: page.body.text;
|
|
843
|
+
staged.set(page.entryId, text);
|
|
847
844
|
}
|
|
848
845
|
}
|
|
849
846
|
return staged;
|
|
@@ -916,6 +913,22 @@ const readLogoSvg = (
|
|
|
916
913
|
const isStringShorthand = <T>(value: T | string): value is string =>
|
|
917
914
|
typeof value === "string";
|
|
918
915
|
|
|
916
|
+
/**
|
|
917
|
+
* The URL behind the header's repo mark. A string is used as-is: `github`
|
|
918
|
+
* drives the per-page edit link, the header mark and the manifest's
|
|
919
|
+
* `repository` together, so a project whose docs repo is private has to unset
|
|
920
|
+
* all three, and would otherwise have no way to point the mark at anything.
|
|
921
|
+
*/
|
|
922
|
+
const headerRepoUrl = (
|
|
923
|
+
repo: boolean | string,
|
|
924
|
+
derived: string | null
|
|
925
|
+
): string | null => {
|
|
926
|
+
if (isStringShorthand(repo)) {
|
|
927
|
+
return repo;
|
|
928
|
+
}
|
|
929
|
+
return repo ? derived : null;
|
|
930
|
+
};
|
|
931
|
+
|
|
919
932
|
/**
|
|
920
933
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
921
934
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
@@ -1165,14 +1178,29 @@ const resolveOgDescription = (config: ResolvedConfig): string | undefined => {
|
|
|
1165
1178
|
return configured ?? config.description;
|
|
1166
1179
|
};
|
|
1167
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* Repo coordinates for content components. `host` and the derived `api` ride
|
|
1183
|
+
* along so a card on an Enterprise site links and queries that instance rather
|
|
1184
|
+
* than the public one.
|
|
1185
|
+
*/
|
|
1186
|
+
const resolveGithubData = (
|
|
1187
|
+
github: ResolvedConfig["github"]
|
|
1188
|
+
): BlumeData["config"]["github"] =>
|
|
1189
|
+
github
|
|
1190
|
+
? {
|
|
1191
|
+
api: githubApiUrl(github),
|
|
1192
|
+
host: github.host,
|
|
1193
|
+
owner: github.owner,
|
|
1194
|
+
repo: github.repo,
|
|
1195
|
+
}
|
|
1196
|
+
: null;
|
|
1197
|
+
|
|
1168
1198
|
/** Serialize the content graph into the data module the runtime consumes. */
|
|
1169
1199
|
export const buildRuntimeData = (project: BlumeProject): string => {
|
|
1170
1200
|
const { config, context, graph, manifest } = project;
|
|
1171
1201
|
const { github } = config;
|
|
1172
|
-
const repoUrl = github
|
|
1173
|
-
|
|
1174
|
-
: null;
|
|
1175
|
-
const editBase = github ? `${repoUrl}/edit/${github.branch}` : null;
|
|
1202
|
+
const repoUrl = github ? githubRepoUrl(github) : null;
|
|
1203
|
+
const editBase = github ? githubEditBaseUrl(github) : null;
|
|
1176
1204
|
const logo = resolveLogo(project);
|
|
1177
1205
|
const ogLogo = resolveOgMark(project, logo?.svg);
|
|
1178
1206
|
|
|
@@ -1181,7 +1209,18 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1181
1209
|
return null;
|
|
1182
1210
|
}
|
|
1183
1211
|
const rel = relative(context.root, sourcePath).split("\\").join("/");
|
|
1184
|
-
|
|
1212
|
+
// The edit path is repo-relative: `github.dir` places the project inside
|
|
1213
|
+
// the repo, so a source above the project dir (a monorepo vault beside the
|
|
1214
|
+
// docs app) still resolves to an in-repo file. A path that escapes the
|
|
1215
|
+
// repo itself has nothing to edit — fabricating one yields a 404 link.
|
|
1216
|
+
// `dir` is a bare string in the schema, so a leading slash (`/apps/docs`)
|
|
1217
|
+
// is trimmed rather than read as an absolute path — which would drop the
|
|
1218
|
+
// link from every page of a site that has always written it that way.
|
|
1219
|
+
const editDir = trimChar(github?.dir ?? "", "/");
|
|
1220
|
+
const editPath = normalize(editDir ? join(editDir, rel) : rel);
|
|
1221
|
+
if (editPath.startsWith("..") || isAbsolute(editPath)) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1185
1224
|
return `${editBase}/${editPath}`;
|
|
1186
1225
|
};
|
|
1187
1226
|
|
|
@@ -1190,9 +1229,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1190
1229
|
// Resolve the header repo link per locale. API references no longer add a tab
|
|
1191
1230
|
// automatically — authors point a `navigation.tabs` entry at the reference
|
|
1192
1231
|
// route to surface it (see `referenceRoutes`).
|
|
1232
|
+
const markUrl = headerRepoUrl(config.navigation.repo, repoUrl);
|
|
1193
1233
|
const withRepoUrl = (nav: Navigation): Navigation => ({
|
|
1194
1234
|
...nav,
|
|
1195
|
-
repoUrl:
|
|
1235
|
+
repoUrl: markUrl,
|
|
1196
1236
|
});
|
|
1197
1237
|
|
|
1198
1238
|
// Resolved UI dictionaries: one per locale under i18n, English baseline
|
|
@@ -1221,6 +1261,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1221
1261
|
code,
|
|
1222
1262
|
withRepoUrl(
|
|
1223
1263
|
graph.navigationByLocale[code] ?? {
|
|
1264
|
+
actions: [],
|
|
1265
|
+
cta: null,
|
|
1224
1266
|
featured: [],
|
|
1225
1267
|
selectors: [],
|
|
1226
1268
|
sidebar: [],
|
|
@@ -1250,9 +1292,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1250
1292
|
discovery: {
|
|
1251
1293
|
agentReadability: config.seo.agentReadability,
|
|
1252
1294
|
llmsTxt: config.ai.llmsTxt.enabled,
|
|
1295
|
+
// Mirrors `buildSitemapFiles`: no site, no sitemap.
|
|
1296
|
+
sitemap: config.seo.sitemap && Boolean(config.deployment.site),
|
|
1253
1297
|
},
|
|
1254
1298
|
favicon: resolveFavicon(project),
|
|
1255
1299
|
feedback: config.feedback,
|
|
1300
|
+
github: resolveGithubData(github),
|
|
1256
1301
|
i18n: i18n
|
|
1257
1302
|
? {
|
|
1258
1303
|
defaultLocale: i18n.defaultLocale,
|
|
@@ -1267,6 +1312,15 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1267
1312
|
})),
|
|
1268
1313
|
}
|
|
1269
1314
|
: null,
|
|
1315
|
+
// `undefined` members drop out of the JSON snapshot; the null keeps the
|
|
1316
|
+
// "nothing configured" case explicit for the layouts.
|
|
1317
|
+
identity:
|
|
1318
|
+
config.seo.organization || config.seo.software
|
|
1319
|
+
? {
|
|
1320
|
+
organization: config.seo.organization,
|
|
1321
|
+
software: config.seo.software,
|
|
1322
|
+
}
|
|
1323
|
+
: null,
|
|
1270
1324
|
imageZoom: config.markdown.imageZoom,
|
|
1271
1325
|
logo,
|
|
1272
1326
|
mcp: config.ai.mcp.enabled
|
|
@@ -1311,8 +1365,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1311
1365
|
title: feed.title,
|
|
1312
1366
|
})),
|
|
1313
1367
|
// CSS variables for Astro's <Font> component; matches the astro.config
|
|
1314
|
-
// `fonts:` entries derived from the same theme.fonts config
|
|
1315
|
-
|
|
1368
|
+
// `fonts:` entries derived from the same theme.fonts config (and the
|
|
1369
|
+
// same locale-derived subsets, so preloads cover the scripts pages use).
|
|
1370
|
+
fontCssVars: configuredFonts(
|
|
1371
|
+
config.theme.fonts,
|
|
1372
|
+
fontLocaleCodes(config.i18n)
|
|
1373
|
+
),
|
|
1316
1374
|
navigation: withRepoUrl(graph.navigation),
|
|
1317
1375
|
// Per-locale navigation; the catch-all selects the active locale's tree.
|
|
1318
1376
|
navigationByLocale,
|
|
@@ -1460,11 +1518,7 @@ const writeMcpFiles = async (
|
|
|
1460
1518
|
* `prerender = false` export wins over the injection default.
|
|
1461
1519
|
*/
|
|
1462
1520
|
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,
|
|
1521
|
+
enabled: needsPlaygroundProxy(config),
|
|
1468
1522
|
entrypoint: join(srcDir, "blume-openapi", "api-proxy.ts"),
|
|
1469
1523
|
pattern: "/_api-proxy",
|
|
1470
1524
|
});
|
|
@@ -1480,46 +1534,68 @@ const planPlaygroundProxy = (config: ResolvedConfig, srcDir: string) => ({
|
|
|
1480
1534
|
* origin to allow and are skipped; AsyncAPI documents declare `servers` as a
|
|
1481
1535
|
* map and contribute nothing (the proxy is OpenAPI-only).
|
|
1482
1536
|
*/
|
|
1483
|
-
const
|
|
1537
|
+
const specOriginsOf = (spec: ApiSpecData): string[] => {
|
|
1484
1538
|
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
|
-
}
|
|
1539
|
+
// A GraphQL schema names no servers; its configured live endpoint is the
|
|
1540
|
+
// one origin the playground targets.
|
|
1541
|
+
if (spec.endpoint !== undefined) {
|
|
1542
|
+
try {
|
|
1543
|
+
origins.add(new URL(spec.endpoint).origin);
|
|
1544
|
+
} catch {
|
|
1545
|
+
// Not an absolute URL: nothing to allow.
|
|
1503
1546
|
}
|
|
1504
1547
|
}
|
|
1505
|
-
|
|
1548
|
+
// SAFETY: `document` is arbitrary parsed JSON; the assertion only names
|
|
1549
|
+
// the optional `servers` shape, and every access below re-checks it —
|
|
1550
|
+
// `Array.isArray(servers)` guards the list and `server.url ?? ""` the url.
|
|
1551
|
+
const { servers } = spec.document as { servers?: { url?: string }[] };
|
|
1552
|
+
for (const server of Array.isArray(servers) ? servers : []) {
|
|
1553
|
+
const url = server.url ?? "";
|
|
1554
|
+
// `new URL("https://{region}.api.example.com")` parses — the braces land
|
|
1555
|
+
// in the hostname — so templated URLs need an explicit check or their
|
|
1556
|
+
// junk literal becomes an allowlist entry no real request can match.
|
|
1557
|
+
if (url.includes("{")) {
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
try {
|
|
1561
|
+
origins.add(new URL(url).origin);
|
|
1562
|
+
} catch {
|
|
1563
|
+
// Not an absolute URL: nothing to allow.
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return [...origins];
|
|
1506
1567
|
};
|
|
1507
1568
|
|
|
1569
|
+
const specOrigins = (data: OpenApiData): string[] =>
|
|
1570
|
+
[...new Set(Object.values(data).flatMap(specOriginsOf))].toSorted();
|
|
1571
|
+
|
|
1508
1572
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1573
|
+
* Build-time diagnostics for playground sends the built-in proxy would refuse.
|
|
1574
|
+
* The baked-in allowlist pools every documented origin, but each spec's
|
|
1575
|
+
* playground only ever targets that spec's own servers/endpoint — so a
|
|
1576
|
+
* non-empty pool can still leave one spec's Send 403ing on every request.
|
|
1577
|
+
* Hence the check is per spec: any spec routed through the built-in proxy
|
|
1578
|
+
* whose own origins came out empty (no absolute `servers[].url`, no absolute
|
|
1579
|
+
* GraphQL `endpoint`) gets a warning naming it.
|
|
1513
1580
|
*/
|
|
1514
1581
|
const proxyAllowlistWarnings = (
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
): string[] =>
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1582
|
+
config: ResolvedConfig,
|
|
1583
|
+
data: OpenApiData
|
|
1584
|
+
): string[] => {
|
|
1585
|
+
const kinds = builtinProxyKinds(config);
|
|
1586
|
+
const warnings: string[] = [];
|
|
1587
|
+
for (const spec of Object.values(data)) {
|
|
1588
|
+
if (!kinds.includes(spec.kind) || specOriginsOf(spec).length > 0) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
warnings.push(
|
|
1592
|
+
spec.kind === "graphql"
|
|
1593
|
+
? `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.`
|
|
1594
|
+
: `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.`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
return warnings;
|
|
1598
|
+
};
|
|
1523
1599
|
|
|
1524
1600
|
/**
|
|
1525
1601
|
* Write the Ask AI endpoint and, unless the backend runs its own retrieval
|
|
@@ -1615,20 +1691,6 @@ const buildComponentSlots = async (
|
|
|
1615
1691
|
};
|
|
1616
1692
|
};
|
|
1617
1693
|
|
|
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
1694
|
/** The OG endpoint fonts for a scanned project (see {@link resolveOgFonts}). */
|
|
1633
1695
|
const projectOgFonts = (project: BlumeProject): DerivedOgFonts =>
|
|
1634
1696
|
resolveOgFonts(
|
|
@@ -1780,8 +1842,9 @@ export const generateRuntime = async (
|
|
|
1780
1842
|
pattern: playgroundProxy.pattern,
|
|
1781
1843
|
});
|
|
1782
1844
|
}
|
|
1783
|
-
// Computed once: the endpoint template bakes it in below
|
|
1784
|
-
//
|
|
1845
|
+
// Computed once: the endpoint template bakes it in below. A spec that
|
|
1846
|
+
// contributes no origin of its own gets a per-spec diagnostic
|
|
1847
|
+
// (`proxyAllowlistWarnings`) — the proxy would refuse its every send.
|
|
1785
1848
|
const proxyOrigins = specOrigins(openApiData);
|
|
1786
1849
|
|
|
1787
1850
|
const hasStaged = staged.size > 0;
|
|
@@ -1804,12 +1867,8 @@ export const generateRuntime = async (
|
|
|
1804
1867
|
aliases: resolveTsconfigAliases(context.root),
|
|
1805
1868
|
askPath,
|
|
1806
1869
|
config,
|
|
1870
|
+
contentRoot: docsCollection.base,
|
|
1807
1871
|
contentRoutes: markdownRoutePaths(project),
|
|
1808
|
-
contentWatchesRuntimeDir: contentWatchesRuntimeDir(
|
|
1809
|
-
hasFilesystemSource,
|
|
1810
|
-
docsCollection.base,
|
|
1811
|
-
context
|
|
1812
|
-
),
|
|
1813
1872
|
context,
|
|
1814
1873
|
dataPath,
|
|
1815
1874
|
examplesPath,
|
|
@@ -1993,6 +2052,14 @@ export const generateRuntime = async (
|
|
|
1993
2052
|
);
|
|
1994
2053
|
}
|
|
1995
2054
|
|
|
2055
|
+
// The include graph (partial → including pages) behind `includeHmrPlugin`:
|
|
2056
|
+
// editing a partial invalidates every page that splices it. Written even
|
|
2057
|
+
// when empty so the plugin's configured path always resolves.
|
|
2058
|
+
await write(
|
|
2059
|
+
join(srcDir, "generated", "includes.json"),
|
|
2060
|
+
`${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`
|
|
2061
|
+
);
|
|
2062
|
+
|
|
1996
2063
|
const rawMarkdown = await buildRawMarkdown(project);
|
|
1997
2064
|
// The originals behind the rewritten `/blume-assets/content/…` references in
|
|
1998
2065
|
// the agent-facing Markdown, plus the endpoint that serves them (and the
|
|
@@ -2047,7 +2114,7 @@ export const generateRuntime = async (
|
|
|
2047
2114
|
// regenerated each run.
|
|
2048
2115
|
const warnings: string[] = [
|
|
2049
2116
|
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
2050
|
-
...proxyAllowlistWarnings(
|
|
2117
|
+
...proxyAllowlistWarnings(config, openApiData),
|
|
2051
2118
|
...reactCompilerWarnings(config, needsReact, reactCompilerPath),
|
|
2052
2119
|
...mcp.warnings,
|
|
2053
2120
|
...islandDiscovery.warnings,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFile, utimes } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dev-server invalidation for `<include>` partials. A partial is not an Astro
|
|
5
|
+
* content-collection entry (the default underscore-prefix exclude keeps it
|
|
6
|
+
* out of the `docs` glob), so Vite has no edge from an including page to it —
|
|
7
|
+
* editing the partial would keep serving the page's stale compiled module.
|
|
8
|
+
* The scan records each page's transitive includes; `generateRuntime` writes
|
|
9
|
+
* them to `generated/includes.json` as partial → including pages, and this
|
|
10
|
+
* plugin turns a partial edit into an invalidation of those page modules plus
|
|
11
|
+
* a full reload.
|
|
12
|
+
*
|
|
13
|
+
* The graph file is re-read on every hot update: `blume dev`'s regenerate
|
|
14
|
+
* loop rewrites it after each content change, so the mapping tracks include
|
|
15
|
+
* edits without restarting the server.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The Vite module-graph slice the plugin touches (structurally typed, like
|
|
19
|
+
* every Blume-authored Vite plugin — see `serverAppResolvePlugin`). */
|
|
20
|
+
interface IncludeHmrServer {
|
|
21
|
+
moduleGraph: {
|
|
22
|
+
getModulesByFile: (file: string) => Set<unknown> | undefined;
|
|
23
|
+
invalidateModule: (mod: never) => void;
|
|
24
|
+
};
|
|
25
|
+
ws: { send: (payload: { type: "full-reload" }) => void };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IncludeHmrContext {
|
|
29
|
+
file: string;
|
|
30
|
+
server: IncludeHmrServer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IncludeHmrPlugin {
|
|
34
|
+
name: string;
|
|
35
|
+
handleHotUpdate: (ctx: IncludeHmrContext) => Promise<never[] | undefined>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const includeHmrPlugin = (graphPath: string): IncludeHmrPlugin => ({
|
|
39
|
+
async handleHotUpdate(ctx) {
|
|
40
|
+
let graph: Record<string, string[]>;
|
|
41
|
+
try {
|
|
42
|
+
// SAFETY: `generateRuntime` is the file's only writer and serializes
|
|
43
|
+
// exactly this shape; a malformed hand-edit throws into the catch below.
|
|
44
|
+
graph = JSON.parse(await readFile(graphPath, "utf-8")) as Record<
|
|
45
|
+
string,
|
|
46
|
+
string[]
|
|
47
|
+
>;
|
|
48
|
+
} catch {
|
|
49
|
+
// No graph yet (first run) — nothing to invalidate.
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const includers = graph[ctx.file];
|
|
53
|
+
if (!includers || includers.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { moduleGraph, ws } = ctx.server;
|
|
57
|
+
const now = new Date();
|
|
58
|
+
for (const includer of includers) {
|
|
59
|
+
for (const mod of moduleGraph.getModulesByFile(includer) ?? []) {
|
|
60
|
+
// SAFETY: the module came out of this module graph; `never` only
|
|
61
|
+
// reflects that the structural slice doesn't model the node type.
|
|
62
|
+
moduleGraph.invalidateModule(mod as never);
|
|
63
|
+
}
|
|
64
|
+
// Plain `.md` pages have no Vite module: their HTML lives in the
|
|
65
|
+
// content-layer store, rendered at sync time. Bump the page's mtime so
|
|
66
|
+
// Astro's content watcher re-syncs it — the include-aware digest
|
|
67
|
+
// (`withIncludeRefresh`) then forces a fresh render that re-reads the
|
|
68
|
+
// edited partial.
|
|
69
|
+
try {
|
|
70
|
+
// oxlint-disable-next-line no-await-in-loop -- ordered per-page touch
|
|
71
|
+
await utimes(includer, now, now);
|
|
72
|
+
} catch {
|
|
73
|
+
// The page may have been deleted since the graph was written.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ws.send({ type: "full-reload" });
|
|
77
|
+
// The partial itself is not a module; suppress Vite's default handling.
|
|
78
|
+
return [];
|
|
79
|
+
},
|
|
80
|
+
name: "blume:include-hmr",
|
|
81
|
+
});
|
|
Binary file
|
package/src/astro/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export { withAdapterRoot } from "./adapter-root.ts";
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
serverAppResolvePlugin,
|
|
6
|
-
} from "./generate.ts";
|
|
2
|
+
export { generateRuntime, prerenderDepsPlugin } from "./generate.ts";
|
|
3
|
+
export { includeHmrPlugin } from "./include-hmr.ts";
|
|
4
|
+
export { withIncludeRefresh } from "./include-refresh.ts";
|
|
7
5
|
export type { GenerateResult } from "./generate.ts";
|
|
8
6
|
export { blumeIntegration } from "./integration.ts";
|
|
9
7
|
export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
|