blume 1.6.5 → 1.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/CHANGELOG.md +39 -0
- package/bin/blume.mjs +3 -2
- package/dist/cli/chunk-0qhq7b8q.js +111 -0
- package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
- package/dist/cli/chunk-18tjv4f7.js +96 -0
- package/dist/cli/chunk-18tjv4f7.js.map +10 -0
- package/dist/cli/chunk-27gtm2ym.js +69 -0
- package/dist/cli/chunk-27gtm2ym.js.map +11 -0
- package/dist/cli/chunk-2aj8ddew.js +72 -0
- package/dist/cli/chunk-2aj8ddew.js.map +10 -0
- package/dist/cli/chunk-3r94j3tc.js +221 -0
- package/dist/cli/chunk-3r94j3tc.js.map +10 -0
- package/dist/cli/chunk-4trphnvy.js +102 -0
- package/dist/cli/chunk-4trphnvy.js.map +11 -0
- package/dist/cli/chunk-4xyggvgf.js +21 -0
- package/dist/cli/chunk-4xyggvgf.js.map +10 -0
- package/dist/cli/chunk-5d4q7121.js +4064 -0
- package/dist/cli/chunk-5d4q7121.js.map +40 -0
- package/dist/cli/chunk-5hs6gb7n.js +32 -0
- package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
- package/dist/cli/chunk-6kzzpsx8.js +26 -0
- package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
- package/dist/cli/chunk-8gnpdsn1.js +952 -0
- package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
- package/dist/cli/chunk-9qs6acpw.js +176 -0
- package/dist/cli/chunk-9qs6acpw.js.map +10 -0
- package/dist/cli/chunk-agy5rzxy.js +2453 -0
- package/dist/cli/chunk-agy5rzxy.js.map +15 -0
- package/dist/cli/chunk-bcy492zc.js +16 -0
- package/dist/cli/chunk-bcy492zc.js.map +10 -0
- package/dist/cli/chunk-btfr9yvw.js +41 -0
- package/dist/cli/chunk-btfr9yvw.js.map +10 -0
- package/dist/cli/chunk-cbjnx4s8.js +73 -0
- package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
- package/dist/cli/chunk-cfw6x4rm.js +1967 -0
- package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
- package/dist/cli/chunk-ckh3a410.js +277 -0
- package/dist/cli/chunk-ckh3a410.js.map +11 -0
- package/dist/cli/chunk-drke6t0h.js +259 -0
- package/dist/cli/chunk-drke6t0h.js.map +11 -0
- package/dist/cli/chunk-ev67ycx0.js +15 -0
- package/dist/cli/chunk-ev67ycx0.js.map +10 -0
- package/dist/cli/chunk-ey89bjj1.js +209 -0
- package/dist/cli/chunk-ey89bjj1.js.map +11 -0
- package/dist/cli/chunk-j6pxe0dt.js +69 -0
- package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
- package/dist/cli/chunk-jk1zwka1.js +387 -0
- package/dist/cli/chunk-jk1zwka1.js.map +12 -0
- package/dist/cli/chunk-jtb45atp.js +467 -0
- package/dist/cli/chunk-jtb45atp.js.map +14 -0
- package/dist/cli/chunk-jxkxjsc1.js +76 -0
- package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
- package/dist/cli/chunk-kwx90v78.js +81 -0
- package/dist/cli/chunk-kwx90v78.js.map +10 -0
- package/dist/cli/chunk-n0nyat6g.js +30 -0
- package/dist/cli/chunk-n0nyat6g.js.map +10 -0
- package/dist/cli/chunk-pxj10x8y.js +35 -0
- package/dist/cli/chunk-pxj10x8y.js.map +10 -0
- package/dist/cli/chunk-qq9nm3qd.js +1141 -0
- package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
- package/dist/cli/chunk-s102bysw.js +5170 -0
- package/dist/cli/chunk-s102bysw.js.map +47 -0
- package/dist/cli/chunk-s5dsk8bj.js +769 -0
- package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
- package/dist/cli/chunk-s5e5jt53.js +227 -0
- package/dist/cli/chunk-s5e5jt53.js.map +11 -0
- package/dist/cli/chunk-sbdqrjbb.js +81 -0
- package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
- package/dist/cli/chunk-tnskyrej.js +117 -0
- package/dist/cli/chunk-tnskyrej.js.map +10 -0
- package/dist/cli/chunk-v2ymm99c.js +1016 -0
- package/dist/cli/chunk-v2ymm99c.js.map +13 -0
- package/dist/cli/chunk-v5mm027v.js +185 -0
- package/dist/cli/chunk-v5mm027v.js.map +11 -0
- package/dist/cli/chunk-vt8fgygt.js +23 -0
- package/dist/cli/chunk-vt8fgygt.js.map +10 -0
- package/dist/cli/chunk-vxv4x1n8.js +17 -0
- package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
- package/dist/cli/chunk-wd27zjcz.js +60 -0
- package/dist/cli/chunk-wd27zjcz.js.map +10 -0
- package/dist/cli/chunk-x66c5yjn.js +23 -0
- package/dist/cli/chunk-x66c5yjn.js.map +10 -0
- package/dist/cli/chunk-xv91q4nm.js +5314 -0
- package/dist/cli/chunk-xv91q4nm.js.map +58 -0
- package/dist/cli/chunk-y3g15rvv.js +679 -0
- package/dist/cli/chunk-y3g15rvv.js.map +15 -0
- package/dist/cli/chunk-ye9zdkgv.js +136 -0
- package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
- package/dist/cli/chunk-ynacq3ev.js +1062 -0
- package/dist/cli/chunk-ynacq3ev.js.map +25 -0
- package/dist/cli/chunk-zr3ygrq3.js +54 -0
- package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
- package/dist/cli/index.js +55 -27597
- package/dist/cli/index.js.map +5 -243
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/components/layout/nav-utils.d.ts +33 -1
- package/dist/types/core/code-fences.d.ts +11 -0
- package/dist/types/core/package-root.d.ts +1 -1
- package/dist/types/core/schema.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +22 -22
- package/docs/02-deployment.mdx +22 -1
- package/docs/configuration/ask-ai.mdx +1 -1
- package/docs/configuration/customization.mdx +2 -9
- package/docs/content/navigation.mdx +2 -0
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +4 -0
- package/docs/reference/cli.mdx +1 -1
- package/package.json +4 -2
- package/src/ai/api/handlers.ts +4 -7
- package/src/ai/api/paths.ts +8 -0
- package/src/ai/api/spec.ts +2 -1
- package/src/ai/ask-context.ts +378 -22
- package/src/astro/generate.ts +161 -28
- package/src/astro/include-hmr.ts +10 -13
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +6 -1
- package/src/astro/integration.ts +280 -53
- package/src/astro/module-types.ts +83 -0
- package/src/astro/templates.ts +256 -108
- package/src/audit/image-size.ts +10 -8
- package/src/cli/command-meta.ts +77 -0
- package/src/cli/commands/add.ts +2 -4
- package/src/cli/commands/audit.ts +2 -4
- package/src/cli/commands/build.ts +70 -346
- package/src/cli/commands/check.ts +2 -4
- package/src/cli/commands/dev.ts +31 -42
- package/src/cli/commands/doctor.ts +2 -4
- package/src/cli/commands/eject.ts +3 -41
- package/src/cli/commands/eval.ts +2 -5
- package/src/cli/commands/init.ts +2 -4
- package/src/cli/commands/mcp-stdio.ts +2 -5
- package/src/cli/commands/preview.ts +3 -5
- package/src/cli/commands/sync.ts +2 -4
- package/src/cli/commands/translate.ts +2 -5
- package/src/cli/commands/validate.ts +2 -4
- package/src/cli/commands/version.ts +2 -4
- package/src/cli/eject-scripts.ts +0 -45
- package/src/cli/host-args.ts +16 -0
- package/src/cli/index.ts +84 -35
- package/src/cli/lazy-command.ts +47 -0
- package/src/components/Icon.astro +24 -0
- package/src/components/content/GithubInfo.astro +4 -1
- package/src/components/icon-sprite-middleware.ts +41 -0
- package/src/components/icon-sprite.ts +93 -0
- package/src/components/layout/IconSprite.astro +11 -0
- package/src/components/layout/NavTree.astro +156 -188
- package/src/components/layout/NavTreeCache.astro +45 -0
- package/src/components/layout/NavTreeScript.astro +256 -0
- package/src/components/layout/PageActions.astro +11 -5
- package/src/components/layout/PageLayout.astro +21 -3
- package/src/components/layout/ReferenceLayout.astro +21 -4
- package/src/components/layout/RootLayout.astro +44 -6
- package/src/components/layout/nav-cache.ts +49 -0
- package/src/components/layout/nav-utils.ts +69 -1
- package/src/components/layout/page-locale.ts +29 -0
- package/src/core/api-name.ts +18 -0
- package/src/core/code-fences.ts +48 -0
- package/src/core/content-assets.ts +3 -7
- package/src/core/includes.ts +3 -7
- package/src/core/package-root.ts +1 -1
- package/src/core/schema.ts +19 -0
- package/src/core/sources/normalize.ts +2 -37
- package/src/core/sources/obsidian.ts +3 -2
- package/src/core/svg-dimensions.ts +97 -0
- package/src/core/version-cut.ts +2 -2
- package/src/deploy/artifacts.ts +370 -0
- package/src/deploy/cloudflare-negotiation.ts +97 -32
- package/src/deploy/function-bundle.ts +66 -20
- package/src/deploy/sitemap.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +8 -30
- package/src/markdown/language-icon.ts +64 -20
- package/src/markdown/mermaid.ts +11 -0
- package/src/og/cache.ts +236 -0
- package/src/og/card.ts +18 -16
- package/src/og/index.ts +8 -1
- package/src/openapi/render-mdx.ts +9 -5
- package/src/registry/eject.ts +23 -10
- package/src/theme/entry.ts +41 -7
- package/src/theme/fonts.ts +30 -23
|
@@ -0,0 +1,2453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
API_CATALOG_PATH,
|
|
4
|
+
OPENAPI_PATH,
|
|
5
|
+
hasApiCatalog
|
|
6
|
+
} from "./chunk-j6pxe0dt.js";
|
|
7
|
+
import {
|
|
8
|
+
searchProviderMeta
|
|
9
|
+
} from "./chunk-wd27zjcz.js";
|
|
10
|
+
import {
|
|
11
|
+
BLUME_IGNORE_DIRS,
|
|
12
|
+
TOC_HIDDEN_KEY
|
|
13
|
+
} from "./chunk-s102bysw.js";
|
|
14
|
+
import {
|
|
15
|
+
buildFontEntries,
|
|
16
|
+
fontLocaleCodes,
|
|
17
|
+
hasScalarReferences
|
|
18
|
+
} from "./chunk-xv91q4nm.js";
|
|
19
|
+
import {
|
|
20
|
+
normalizeBasePath,
|
|
21
|
+
trimChar,
|
|
22
|
+
withBasePath,
|
|
23
|
+
withComposedBasePath
|
|
24
|
+
} from "./chunk-27gtm2ym.js";
|
|
25
|
+
|
|
26
|
+
// src/deploy/adapter-output.ts
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
29
|
+
import { dirname, join } from "pathe";
|
|
30
|
+
var ADAPTER_IGNORE_DIRS = {
|
|
31
|
+
netlify: ".netlify/",
|
|
32
|
+
vercel: ".vercel/"
|
|
33
|
+
};
|
|
34
|
+
var ADAPTER_OUTPUT_PATHS = {
|
|
35
|
+
netlify: ".netlify/v1"
|
|
36
|
+
};
|
|
37
|
+
var servesClientSubdir = (deployment) => deployment.output === "server" && (deployment.adapter === "node" || deployment.adapter === "cloudflare");
|
|
38
|
+
var deployStaticDir = (config, context) => {
|
|
39
|
+
const { adapter, output } = config.deployment;
|
|
40
|
+
if (output === "server" && adapter === "vercel") {
|
|
41
|
+
return join(context.root, ".vercel", "output", "static");
|
|
42
|
+
}
|
|
43
|
+
const dist = context.distDir ?? join(context.root, "dist");
|
|
44
|
+
if (servesClientSubdir(config.deployment)) {
|
|
45
|
+
return join(dist, "client");
|
|
46
|
+
}
|
|
47
|
+
return dist;
|
|
48
|
+
};
|
|
49
|
+
var surfaceAdapterOutput = async (config, context) => {
|
|
50
|
+
const { adapter, output } = config.deployment;
|
|
51
|
+
if (output !== "server" || !adapter) {
|
|
52
|
+
return { moved: false };
|
|
53
|
+
}
|
|
54
|
+
const rel = ADAPTER_OUTPUT_PATHS[adapter];
|
|
55
|
+
if (!rel) {
|
|
56
|
+
return { moved: false };
|
|
57
|
+
}
|
|
58
|
+
const from = join(context.outDir, rel);
|
|
59
|
+
const to = join(context.root, rel);
|
|
60
|
+
if (!existsSync(from)) {
|
|
61
|
+
return { moved: false };
|
|
62
|
+
}
|
|
63
|
+
await mkdir(dirname(to), { recursive: true });
|
|
64
|
+
await rm(to, { force: true, recursive: true });
|
|
65
|
+
await cp(from, to, { recursive: true, verbatimSymlinks: true });
|
|
66
|
+
await rm(from, { force: true, recursive: true });
|
|
67
|
+
return { from, moved: true, to };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/ai/link-headers.ts
|
|
71
|
+
var buildHomeLinkHeader = (config, routePaths) => {
|
|
72
|
+
const deployBase = normalizeBasePath(config.deployment.base);
|
|
73
|
+
const links = [];
|
|
74
|
+
if (hasApiCatalog(config)) {
|
|
75
|
+
links.push(`<${deployBase}${API_CATALOG_PATH}>; rel="api-catalog"; type="application/linkset+json"`);
|
|
76
|
+
}
|
|
77
|
+
if (config.ai.api) {
|
|
78
|
+
links.push(`<${deployBase}${OPENAPI_PATH}>; rel="service-desc"; type="application/json"`);
|
|
79
|
+
}
|
|
80
|
+
if (config.seo.agentReadability) {
|
|
81
|
+
links.push(`<${deployBase}/agent-readability.json>; rel="describedby"; type="application/json"`);
|
|
82
|
+
}
|
|
83
|
+
if (config.ai.llmsTxt.enabled) {
|
|
84
|
+
links.push(`<${deployBase}/llms.txt>; rel="describedby"; type="text/plain"`);
|
|
85
|
+
}
|
|
86
|
+
if (routePaths.includes("/")) {
|
|
87
|
+
links.push(`<${deployBase}/index.md>; rel="alternate"; type="text/markdown"`);
|
|
88
|
+
}
|
|
89
|
+
return links.length > 0 ? links.join(", ") : null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/deploy/redirects.ts
|
|
93
|
+
var applyBaseToAstroRedirects = (redirects, basePath, deployBase) => {
|
|
94
|
+
const base = normalizeBasePath(deployBase);
|
|
95
|
+
return basePath || base ? redirects.map((redirect) => ({
|
|
96
|
+
...redirect,
|
|
97
|
+
from: withBasePath(basePath, redirect.from),
|
|
98
|
+
to: withComposedBasePath(base, basePath, redirect.to)
|
|
99
|
+
})) : redirects;
|
|
100
|
+
};
|
|
101
|
+
var applyBaseToPlatformRedirects = (redirects, basePath, deployBase) => {
|
|
102
|
+
const base = normalizeBasePath(deployBase);
|
|
103
|
+
return basePath || base ? redirects.map((redirect) => ({
|
|
104
|
+
...redirect,
|
|
105
|
+
from: withComposedBasePath(base, basePath, redirect.from),
|
|
106
|
+
to: withComposedBasePath(base, basePath, redirect.to)
|
|
107
|
+
})) : redirects;
|
|
108
|
+
};
|
|
109
|
+
var platformRedirects = (config) => applyBaseToPlatformRedirects(config.redirects, config.basePath, config.deployment.base ?? "");
|
|
110
|
+
|
|
111
|
+
// src/ai/ask.ts
|
|
112
|
+
var OPENAI_COMPATIBLE = "openai-compatible";
|
|
113
|
+
var OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
|
|
114
|
+
var ASK_PRESETS = {
|
|
115
|
+
inkeep: {
|
|
116
|
+
apiKeyEnv: "INKEEP_API_KEY",
|
|
117
|
+
baseUrl: "https://api.inkeep.com/v1",
|
|
118
|
+
kind: OPENAI_COMPATIBLE,
|
|
119
|
+
name: "inkeep",
|
|
120
|
+
runtimeDep: OPENAI_COMPATIBLE_DEP
|
|
121
|
+
},
|
|
122
|
+
llmgateway: {
|
|
123
|
+
apiKeyEnv: "LLMGATEWAY_API_KEY",
|
|
124
|
+
baseUrl: "https://api.llmgateway.io/v1",
|
|
125
|
+
kind: OPENAI_COMPATIBLE,
|
|
126
|
+
name: "llmgateway",
|
|
127
|
+
runtimeDep: OPENAI_COMPATIBLE_DEP
|
|
128
|
+
},
|
|
129
|
+
openrouter: {
|
|
130
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
131
|
+
kind: "openrouter",
|
|
132
|
+
name: "openrouter",
|
|
133
|
+
runtimeDep: "@openrouter/ai-sdk-provider"
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var DEFAULT_MODEL = "openai/gpt-5.5";
|
|
137
|
+
var resolveAskBackend = (ask) => {
|
|
138
|
+
const provider = ask?.provider ?? "gateway";
|
|
139
|
+
const model = ask?.model ?? DEFAULT_MODEL;
|
|
140
|
+
if (provider === "gateway") {
|
|
141
|
+
return { kind: "gateway", model };
|
|
142
|
+
}
|
|
143
|
+
const preset = ASK_PRESETS[provider];
|
|
144
|
+
const apiKeyEnv = ask?.apiKeyEnv ?? preset?.apiKeyEnv ?? "API_KEY";
|
|
145
|
+
if (provider === "openrouter") {
|
|
146
|
+
return { apiKeyEnv, kind: "openrouter", model };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
apiKeyEnv,
|
|
150
|
+
baseUrl: ask?.baseUrl ?? preset?.baseUrl ?? "",
|
|
151
|
+
kind: OPENAI_COMPATIBLE,
|
|
152
|
+
model,
|
|
153
|
+
name: preset?.name ?? OPENAI_COMPATIBLE
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
var askBackendRuntimeDep = (ask) => {
|
|
157
|
+
const provider = ask?.provider ?? "gateway";
|
|
158
|
+
if (provider === "gateway") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
return ASK_PRESETS[provider]?.runtimeDep ?? OPENAI_COMPATIBLE_DEP;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/astro/runtime-modules.ts
|
|
165
|
+
var RUNTIME_MODULE_FILES = new Map([
|
|
166
|
+
["blume:ask-data", "ask-data.json"],
|
|
167
|
+
["blume:content-assets", "content-assets.json"],
|
|
168
|
+
["blume:data", "data.json"],
|
|
169
|
+
["blume:mcp-data", "mcp-data.json"],
|
|
170
|
+
["blume:openapi", "openapi.json"],
|
|
171
|
+
["blume:raw-markdown", "raw-markdown.json"],
|
|
172
|
+
["blume:rss", "rss.json"],
|
|
173
|
+
["blume:search-index", "search.json"]
|
|
174
|
+
]);
|
|
175
|
+
var RUNTIME_MODULE_IDS = new Set(RUNTIME_MODULE_FILES.keys());
|
|
176
|
+
var RESOLVED_PREFIX = "\x00";
|
|
177
|
+
var REGISTRY_KEY = Symbol.for("blume.runtime-modules");
|
|
178
|
+
var registry = () => {
|
|
179
|
+
const host = globalThis;
|
|
180
|
+
host[REGISTRY_KEY] ??= { modules: new Map, servers: new Set };
|
|
181
|
+
return host[REGISTRY_KEY];
|
|
182
|
+
};
|
|
183
|
+
var publishRuntimeModules = (modules) => {
|
|
184
|
+
const { modules: current, servers } = registry();
|
|
185
|
+
const changed = [];
|
|
186
|
+
for (const id of RUNTIME_MODULE_FILES.keys()) {
|
|
187
|
+
const next = modules.get(id);
|
|
188
|
+
if (current.get(id) === next) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (next === undefined) {
|
|
192
|
+
current.delete(id);
|
|
193
|
+
} else {
|
|
194
|
+
current.set(id, next);
|
|
195
|
+
}
|
|
196
|
+
changed.push(id);
|
|
197
|
+
}
|
|
198
|
+
if (changed.length === 0) {
|
|
199
|
+
return changed;
|
|
200
|
+
}
|
|
201
|
+
for (const server of servers) {
|
|
202
|
+
for (const id of changed) {
|
|
203
|
+
const mod = server.moduleGraph.getModuleById(`${RESOLVED_PREFIX}${id}`);
|
|
204
|
+
if (mod) {
|
|
205
|
+
server.moduleGraph.invalidateModule(mod);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
server.ws.send({ type: "full-reload" });
|
|
209
|
+
}
|
|
210
|
+
return changed;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// src/astro/templates.ts
|
|
214
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
215
|
+
import { pathToFileURL } from "node:url";
|
|
216
|
+
import { dirname as dirname2, isAbsolute, join as join2, relative } from "pathe";
|
|
217
|
+
var WORKSPACE_MARKERS = [
|
|
218
|
+
".git",
|
|
219
|
+
"bun.lock",
|
|
220
|
+
"bun.lockb",
|
|
221
|
+
"package-lock.json",
|
|
222
|
+
"pnpm-lock.yaml",
|
|
223
|
+
"pnpm-workspace.yaml",
|
|
224
|
+
"yarn.lock"
|
|
225
|
+
];
|
|
226
|
+
var hasWorkspacesField = (pkgPath) => {
|
|
227
|
+
if (!existsSync2(pkgPath)) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
232
|
+
return pkg.workspaces !== undefined;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var hasWorkspaceMarker = (dir) => hasWorkspacesField(join2(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync2(join2(dir, marker)));
|
|
238
|
+
var findWorkspaceRoot = (start) => {
|
|
239
|
+
let dir = start;
|
|
240
|
+
for (;; ) {
|
|
241
|
+
if (hasWorkspaceMarker(dir)) {
|
|
242
|
+
return dir;
|
|
243
|
+
}
|
|
244
|
+
const parent = dirname2(dir);
|
|
245
|
+
if (parent === dir) {
|
|
246
|
+
return start;
|
|
247
|
+
}
|
|
248
|
+
dir = parent;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var ADAPTER_IMPORTS = {
|
|
252
|
+
cloudflare: "@astrojs/cloudflare",
|
|
253
|
+
netlify: "@astrojs/netlify",
|
|
254
|
+
node: "@astrojs/node",
|
|
255
|
+
vercel: "@astrojs/vercel"
|
|
256
|
+
};
|
|
257
|
+
var ADAPTER_OPTIONS = new Map([
|
|
258
|
+
["node", '{ mode: "standalone" }']
|
|
259
|
+
]);
|
|
260
|
+
var WRANGLER_CONFIG_FILES = [
|
|
261
|
+
"wrangler.jsonc",
|
|
262
|
+
"wrangler.json",
|
|
263
|
+
"wrangler.toml"
|
|
264
|
+
];
|
|
265
|
+
var resolveCloudflareAdapterArgs = (context) => {
|
|
266
|
+
const args = [
|
|
267
|
+
'prerenderEnvironment: "node"',
|
|
268
|
+
'imageService: "compile"'
|
|
269
|
+
];
|
|
270
|
+
const wranglerPath = WRANGLER_CONFIG_FILES.map((file) => join2(context.root, file)).find((file) => existsSync2(file));
|
|
271
|
+
if (wranglerPath) {
|
|
272
|
+
let configPath = relative(context.outDir, wranglerPath);
|
|
273
|
+
if (!configPath.startsWith(".") && !configPath.startsWith("/")) {
|
|
274
|
+
configPath = `./${configPath}`;
|
|
275
|
+
}
|
|
276
|
+
args.push(`configPath: ${JSON.stringify(configPath)}`);
|
|
277
|
+
}
|
|
278
|
+
return `{ ${args.join(", ")} }`;
|
|
279
|
+
};
|
|
280
|
+
var resolveSessionOption = (deployment) => deployment.output === "server" && deployment.adapter === "cloudflare" ? `
|
|
281
|
+
session: false,` : "";
|
|
282
|
+
var astroFontWeights = (weights) => JSON.stringify(weights).replaceAll(/(?<min>\d+)\.\.(?<max>\d+)/gu, "$<min> $<max>");
|
|
283
|
+
var astroConfigImportLine = (options) => {
|
|
284
|
+
const names = ["defineConfig"];
|
|
285
|
+
if (options.hasFonts) {
|
|
286
|
+
names.push("fontProviders");
|
|
287
|
+
}
|
|
288
|
+
return `import { ${names.join(", ")} } from "astro/config";`;
|
|
289
|
+
};
|
|
290
|
+
var runtimeDependencies = (options) => {
|
|
291
|
+
const { config, needsReact, needsSvelte, needsVue } = options;
|
|
292
|
+
const deps = ["@astrojs/mdx"];
|
|
293
|
+
if (needsReact) {
|
|
294
|
+
deps.push("@astrojs/react");
|
|
295
|
+
}
|
|
296
|
+
if (needsVue) {
|
|
297
|
+
deps.push("@astrojs/vue");
|
|
298
|
+
}
|
|
299
|
+
if (needsSvelte) {
|
|
300
|
+
deps.push("@astrojs/svelte");
|
|
301
|
+
}
|
|
302
|
+
if (hasScalarReferences(config)) {
|
|
303
|
+
deps.push("@scalar/astro");
|
|
304
|
+
}
|
|
305
|
+
deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
|
|
306
|
+
if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
|
|
307
|
+
const askDep = askBackendRuntimeDep(config.ai.ask);
|
|
308
|
+
if (askDep) {
|
|
309
|
+
deps.push(askDep);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const { deployment } = config;
|
|
313
|
+
if (deployment.output === "server" && deployment.adapter) {
|
|
314
|
+
const adapter = ADAPTER_IMPORTS[deployment.adapter];
|
|
315
|
+
if (adapter) {
|
|
316
|
+
deps.push(adapter);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return deps;
|
|
320
|
+
};
|
|
321
|
+
var RENDER_EXTERNAL_DEPS = [
|
|
322
|
+
"@astrojs/markdown-satteri",
|
|
323
|
+
"@pierre/diffs",
|
|
324
|
+
"@shikijs/transformers",
|
|
325
|
+
"@takumi-rs/core",
|
|
326
|
+
"@takumi-rs/helpers",
|
|
327
|
+
"@takumi-rs/wasm",
|
|
328
|
+
"github-slugger",
|
|
329
|
+
"katex",
|
|
330
|
+
"shiki",
|
|
331
|
+
"simple-icons",
|
|
332
|
+
"takumi-js",
|
|
333
|
+
"takumi-js/helpers",
|
|
334
|
+
"zod"
|
|
335
|
+
];
|
|
336
|
+
var renderUserAliases = (aliases) => Object.entries(aliases ?? {}).toSorted(([a], [b]) => b.length - a.length).map(([find, replacement]) => `
|
|
337
|
+
${JSON.stringify(find)}: ${JSON.stringify(replacement)},`).join("");
|
|
338
|
+
var astroOutDir = (context) => context.distDir ?? `${context.root}/dist`;
|
|
339
|
+
var adapterRoot = (context) => dirname2(astroOutDir(context));
|
|
340
|
+
var REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//, /\/\.cache\/vite\//]`;
|
|
341
|
+
var runtimeCacheOptions = (context, generatedModulesDir) => {
|
|
342
|
+
if (generatedModulesDir !== undefined) {
|
|
343
|
+
return { astro: "", vite: "" };
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
astro: `
|
|
347
|
+
cacheDir: ${JSON.stringify(`${context.outDir}/.cache/astro`)},`,
|
|
348
|
+
vite: `
|
|
349
|
+
cacheDir: ${JSON.stringify(`${context.outDir}/.cache/vite`)},`
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
var reactIntegration = (compilerPath) => compilerPath ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })` : `react({ ${REACT_EXCLUDE} })`;
|
|
353
|
+
var renderIntegrationBridge = (bridge) => {
|
|
354
|
+
if (!bridge) {
|
|
355
|
+
return {
|
|
356
|
+
configSourceMarker: "",
|
|
357
|
+
userConfigImports: "",
|
|
358
|
+
userConfigSetup: "",
|
|
359
|
+
userIntegrationSpread: ""
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
configSourceMarker: bridge.sourceHash ? `// Blume config source SHA-256: ${bridge.sourceHash}
|
|
364
|
+
` : "",
|
|
365
|
+
userConfigImports: `import { dirname, resolve } from "node:path";
|
|
366
|
+
import { fileURLToPath } from "node:url";
|
|
367
|
+
import { createModuleLoader } from "blume/core/load-module.ts";
|
|
368
|
+
`,
|
|
369
|
+
userConfigSetup: `const loadBlumeConfig = createModuleLoader();
|
|
370
|
+
const blumeConfig = await loadBlumeConfig(resolve(dirname(fileURLToPath(import.meta.url)), ${JSON.stringify(bridge.configFile)}));
|
|
371
|
+
|
|
372
|
+
`,
|
|
373
|
+
userIntegrationSpread: ", ...(blumeConfig?.integrations ?? [])"
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
var renderImageOption = (config) => config.image.domains.length > 0 || config.image.remotePatterns.length > 0 ? `
|
|
377
|
+
image: ${JSON.stringify(config.image)},` : "";
|
|
378
|
+
var ALL_CLIENT_FEATURES = { epub: true, mermaid: true };
|
|
379
|
+
var resolveOptimizeDeps = (options) => {
|
|
380
|
+
const { context, features } = options;
|
|
381
|
+
const optimizeDepsEntries = [
|
|
382
|
+
...context.pagesRoot ? [`${context.pagesRoot}/**/*.astro`] : [],
|
|
383
|
+
`${context.root}/islands/**/*.{jsx,svelte,tsx,vue}`,
|
|
384
|
+
...[...new Set(Object.values(options.aliases ?? {}))].toSorted().map((dir) => `${dir}/**/*.{astro,jsx,svelte,tsx,vue}`)
|
|
385
|
+
];
|
|
386
|
+
const optimizeDepsInclude = [
|
|
387
|
+
...features.mermaid ? ["blume > mermaid"] : [],
|
|
388
|
+
...features.epub ? ["blume > epub-gen-memory/bundle"] : [],
|
|
389
|
+
...options.needsReact && options.reactCompilerPath ? ["react/compiler-runtime"] : []
|
|
390
|
+
];
|
|
391
|
+
return { optimizeDepsEntries, optimizeDepsInclude };
|
|
392
|
+
};
|
|
393
|
+
var renderRuntimeModuleWiring = (generatedModulesDir) => {
|
|
394
|
+
if (generatedModulesDir === undefined) {
|
|
395
|
+
return {
|
|
396
|
+
aliasLines: "",
|
|
397
|
+
imports: ["runtimeModulesPlugin"],
|
|
398
|
+
pluginEntry: "runtimeModulesPlugin(), "
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const aliasLines = [...RUNTIME_MODULE_FILES].map(([id, file]) => `
|
|
402
|
+
${JSON.stringify(id)}: ${JSON.stringify(`${generatedModulesDir}/${file}`)},`).join("");
|
|
403
|
+
return { aliasLines, imports: [], pluginEntry: "" };
|
|
404
|
+
};
|
|
405
|
+
var blumeIntegrationOptions = (options) => options.ejected ? {
|
|
406
|
+
buildArtifactsRoot: ".",
|
|
407
|
+
contentRoutes: options.contentRoutes,
|
|
408
|
+
homeLinkHeader: buildHomeLinkHeader(options.config, options.contentRoutes) ?? undefined,
|
|
409
|
+
pages: options.pages
|
|
410
|
+
} : { pages: options.pages };
|
|
411
|
+
var astroConfigTemplate = (options) => {
|
|
412
|
+
const { context, config, needsReact, pages, themePath } = options;
|
|
413
|
+
const { astro: cacheOptions, vite: viteCacheOption } = runtimeCacheOptions(context, options.generatedModulesDir);
|
|
414
|
+
const {
|
|
415
|
+
askPath,
|
|
416
|
+
contentRoutes,
|
|
417
|
+
examplesPath,
|
|
418
|
+
examplesThemePath,
|
|
419
|
+
features = ALL_CLIENT_FEATURES,
|
|
420
|
+
featuresPath,
|
|
421
|
+
generatedModulesDir,
|
|
422
|
+
needsSvelte,
|
|
423
|
+
needsVue,
|
|
424
|
+
searchClientPath
|
|
425
|
+
} = options;
|
|
426
|
+
const {
|
|
427
|
+
aliasLines: runtimeModuleAliasLines,
|
|
428
|
+
imports: runtimeModuleImports,
|
|
429
|
+
pluginEntry: runtimeModulesPluginEntry
|
|
430
|
+
} = renderRuntimeModuleWiring(generatedModulesDir);
|
|
431
|
+
const { deployment } = config;
|
|
432
|
+
const userAliasLines = renderUserAliases(options.aliases);
|
|
433
|
+
const server = deployment.output === "server";
|
|
434
|
+
const fsAllow = [...new Set([findWorkspaceRoot(context.root), context.root])];
|
|
435
|
+
const { optimizeDepsEntries, optimizeDepsInclude } = resolveOptimizeDeps({
|
|
436
|
+
aliases: options.aliases,
|
|
437
|
+
context,
|
|
438
|
+
features,
|
|
439
|
+
needsReact,
|
|
440
|
+
reactCompilerPath: options.reactCompilerPath
|
|
441
|
+
});
|
|
442
|
+
const adapterImport = server && deployment.adapter ? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";
|
|
443
|
+
` : "";
|
|
444
|
+
const adapterArgs = (() => {
|
|
445
|
+
if (!server || !deployment.adapter) {
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
448
|
+
if (deployment.adapter === "cloudflare") {
|
|
449
|
+
return resolveCloudflareAdapterArgs(context);
|
|
450
|
+
}
|
|
451
|
+
return ADAPTER_OPTIONS.get(deployment.adapter) ?? "";
|
|
452
|
+
})();
|
|
453
|
+
const adapterExpr = deployment.adapter === "vercel" ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})` : `adapter(${adapterArgs})`;
|
|
454
|
+
const adapterOption = server && deployment.adapter ? `
|
|
455
|
+
adapter: ${adapterExpr},` : "";
|
|
456
|
+
const sessionOption = resolveSessionOption(deployment);
|
|
457
|
+
const siteOption = deployment.site ? `
|
|
458
|
+
site: ${JSON.stringify(deployment.site)},` : "";
|
|
459
|
+
const baseOption = deployment.base ? `
|
|
460
|
+
base: ${JSON.stringify(deployment.base)},` : "";
|
|
461
|
+
const imageOption = renderImageOption(config);
|
|
462
|
+
const i18nOption = config.i18n ? `
|
|
463
|
+
i18n: ${JSON.stringify({
|
|
464
|
+
defaultLocale: config.i18n.defaultLocale,
|
|
465
|
+
locales: config.i18n.locales.map((locale) => locale.code),
|
|
466
|
+
routing: {
|
|
467
|
+
prefixDefaultLocale: !config.i18n.hideDefaultLocalePrefix
|
|
468
|
+
}
|
|
469
|
+
})},` : "";
|
|
470
|
+
const basedRedirects = applyBaseToAstroRedirects(config.redirects, config.basePath, deployment.base ?? "");
|
|
471
|
+
const redirectsOption = basedRedirects.length > 0 ? `
|
|
472
|
+
redirects: ${JSON.stringify(Object.fromEntries(basedRedirects.map((redirect) => [
|
|
473
|
+
redirect.from,
|
|
474
|
+
{ destination: redirect.to, status: redirect.status }
|
|
475
|
+
])))},` : "";
|
|
476
|
+
const fontEntries = buildFontEntries(config.theme.fonts, fontLocaleCodes(config.i18n));
|
|
477
|
+
const fontsOption = fontEntries.length ? `
|
|
478
|
+
fonts: [${fontEntries.map((font) => font.kind === "local" ? `{ provider: fontProviders.local(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, fallbacks: ${JSON.stringify(font.fallbacks)}, options: { variants: ${JSON.stringify(font.variants.map((variant) => {
|
|
479
|
+
const face = {};
|
|
480
|
+
if (variant.weight !== undefined) {
|
|
481
|
+
face.weight = variant.weight;
|
|
482
|
+
}
|
|
483
|
+
if (variant.style !== undefined) {
|
|
484
|
+
face.style = variant.style;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
...face,
|
|
488
|
+
src: [
|
|
489
|
+
isAbsolute(variant.src) ? variant.src : join2(context.root, variant.src)
|
|
490
|
+
]
|
|
491
|
+
};
|
|
492
|
+
}))} } }` : `{ provider: fontProviders.${font.provider}(), name: ${JSON.stringify(font.name)}, cssVariable: ${JSON.stringify(font.cssVariable)}, weights: ${astroFontWeights(font.weights)}, subsets: ${JSON.stringify(font.subsets)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`).join(", ")}],` : "";
|
|
493
|
+
const defineConfigImport = astroConfigImportLine({
|
|
494
|
+
hasFonts: fontEntries.length > 0
|
|
495
|
+
});
|
|
496
|
+
const reactImport = needsReact ? `import react from "@astrojs/react";
|
|
497
|
+
` : "";
|
|
498
|
+
const vueImport = needsVue ? `import vue from "@astrojs/vue";
|
|
499
|
+
` : "";
|
|
500
|
+
const svelteImport = needsSvelte ? `import svelte from "@astrojs/svelte";
|
|
501
|
+
` : "";
|
|
502
|
+
const blumeImports = [
|
|
503
|
+
"blumeIntegration",
|
|
504
|
+
"includeHmrPlugin",
|
|
505
|
+
"prerenderDepsPlugin",
|
|
506
|
+
...runtimeModuleImports,
|
|
507
|
+
...adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []
|
|
508
|
+
];
|
|
509
|
+
const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";
|
|
510
|
+
`;
|
|
511
|
+
const twoslashTransformer = "blumeTwoslashTransformer(), ";
|
|
512
|
+
const deployBase = normalizeBasePath(deployment.base);
|
|
513
|
+
const integrations = [
|
|
514
|
+
`mdx({ processor: blumeMdxProcessor(${JSON.stringify({
|
|
515
|
+
basePath: config.basePath,
|
|
516
|
+
codeThemes: config.markdown.codeBlocks.theme,
|
|
517
|
+
contentRoot: options.contentRoot,
|
|
518
|
+
deployBase,
|
|
519
|
+
headingAnchors: config.markdown.headingAnchors
|
|
520
|
+
})}) })`
|
|
521
|
+
];
|
|
522
|
+
if (needsReact) {
|
|
523
|
+
integrations.push(reactIntegration(options.reactCompilerPath));
|
|
524
|
+
}
|
|
525
|
+
if (needsVue) {
|
|
526
|
+
integrations.push("vue()");
|
|
527
|
+
}
|
|
528
|
+
if (needsSvelte) {
|
|
529
|
+
integrations.push("svelte()");
|
|
530
|
+
}
|
|
531
|
+
integrations.push(`blumeIntegration(${JSON.stringify(blumeIntegrationOptions({
|
|
532
|
+
config,
|
|
533
|
+
contentRoutes,
|
|
534
|
+
ejected: generatedModulesDir !== undefined,
|
|
535
|
+
pages
|
|
536
|
+
}))})`);
|
|
537
|
+
const {
|
|
538
|
+
configSourceMarker,
|
|
539
|
+
userConfigImports,
|
|
540
|
+
userConfigSetup,
|
|
541
|
+
userIntegrationSpread
|
|
542
|
+
} = renderIntegrationBridge(options.integrationBridge);
|
|
543
|
+
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
544
|
+
${configSourceMarker}${userConfigImports}import { availableParallelism } from "node:os";
|
|
545
|
+
${defineConfigImport}
|
|
546
|
+
import mdx from "@astrojs/mdx";
|
|
547
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
548
|
+
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
|
|
549
|
+
${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
|
|
550
|
+
${userConfigSetup}export default defineConfig({
|
|
551
|
+
root: ${JSON.stringify(context.outDir)},
|
|
552
|
+
srcDir: ${JSON.stringify(`${context.outDir}/src`)},
|
|
553
|
+
outDir: ${JSON.stringify(astroOutDir(context))},
|
|
554
|
+
publicDir: ${JSON.stringify(`${context.root}/public`)},${cacheOptions}
|
|
555
|
+
output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${imageOption}${redirectsOption}${i18nOption}${fontsOption}
|
|
556
|
+
integrations: [${integrations.join(", ")}${userIntegrationSpread}],
|
|
557
|
+
markdown: {
|
|
558
|
+
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
559
|
+
basePath: config.basePath,
|
|
560
|
+
codeThemes: config.markdown.codeBlocks.theme,
|
|
561
|
+
contentRoot: options.contentRoot,
|
|
562
|
+
deployBase,
|
|
563
|
+
headingAnchors: config.markdown.headingAnchors
|
|
564
|
+
})}),
|
|
565
|
+
shikiConfig: {
|
|
566
|
+
themes: {
|
|
567
|
+
light: ${JSON.stringify(config.markdown.codeBlocks.theme.light)},
|
|
568
|
+
dark: ${JSON.stringify(config.markdown.codeBlocks.theme.dark)},
|
|
569
|
+
},
|
|
570
|
+
defaultColor: false,
|
|
571
|
+
transformers: [${twoslashTransformer}...blumeShikiTransformers(${JSON.stringify({ icons: config.markdown.code.icons })})],
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
devToolbar: { enabled: false },
|
|
575
|
+
// One canonical URL per page: canonicals, the sitemap, and hreflang all use
|
|
576
|
+
// the slashless form, so the slashed spelling is not a second address. Astro
|
|
577
|
+
// applies this itself — its dev server answers a slashed URL with a 404 that
|
|
578
|
+
// names the setting, an on-demand route redirects — and the Vercel adapter
|
|
579
|
+
// turns it into the platform's 308 route, so the Build Output config needs
|
|
580
|
+
// no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
|
|
581
|
+
// serve the \`index.html\` directory layout as they always did.
|
|
582
|
+
trailingSlash: "never",
|
|
583
|
+
// The layouts render Astro's <ClientRouter />, and its in-place swaps read
|
|
584
|
+
// from the prefetch cache — fetching every link on hover/viewport hides the
|
|
585
|
+
// request latency behind the user's intent, so most navigations swap
|
|
586
|
+
// instantly.
|
|
587
|
+
prefetch: { prefetchAll: true },
|
|
588
|
+
// Prerender several pages at once. Rendering is single-threaded, but a
|
|
589
|
+
// page's OG card renders on a native thread and its HTML is written
|
|
590
|
+
// asynchronously, so the main thread would otherwise idle behind each
|
|
591
|
+
// page's off-thread work. Capped at 8: the gain flattens there and beyond it
|
|
592
|
+
// the overlap only adds memory.
|
|
593
|
+
build: { concurrency: Math.min(8, availableParallelism()) },
|
|
594
|
+
vite: {${viteCacheOption}
|
|
595
|
+
plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}), prerenderDepsPlugin()],
|
|
596
|
+
// Everything hydration can reach must be part of the dev dep optimizer's
|
|
597
|
+
// FIRST run. The Vite root is the generated runtime, so user pages,
|
|
598
|
+
// islands, and aliased components live outside it and are only crawled
|
|
599
|
+
// when first requested — and \`react/compiler-runtime\` is Babel-injected,
|
|
600
|
+
// so no source scan can ever see it. A dependency discovered after
|
|
601
|
+
// hydration begins triggers a mid-session re-optimization whose new
|
|
602
|
+
// generation imports React through new \`?v=\` URLs; the browser then
|
|
603
|
+
// evaluates a second React copy and every island tears down with
|
|
604
|
+
// "Invalid hook call" (#157). \`entries\` points the startup scanner at
|
|
605
|
+
// the user's files (the scanner follows their imports, so their deps land
|
|
606
|
+
// in the initial optimization); the compiler runtime rides the include
|
|
607
|
+
// list because only the transform pipeline knows it exists.
|
|
608
|
+
//
|
|
609
|
+
// The mermaid/epub includes fix CJS interop instead: both lazy client-side
|
|
610
|
+
// imports land on CJS/UMD files (mermaid statically imports dayjs as CJS,
|
|
611
|
+
// epub-gen-memory's browser bundle is a browserified UMD) that break when
|
|
612
|
+
// served as raw ESM — mermaid throws on load and the EPUB export throws
|
|
613
|
+
// \`epub is not a function\`. They resolve through the \`blume\` package
|
|
614
|
+
// (they aren't direct deps of the generated project), so the nested
|
|
615
|
+
// \`blume > x\` form is required, and epub-gen-memory must name the
|
|
616
|
+
// \`/bundle\` subpath that is actually imported: optimizing the package
|
|
617
|
+
// root leaves that entry out. Production (Rollup) already handles the
|
|
618
|
+
// interop, so all of this only affects dev.
|
|
619
|
+
optimizeDeps: {
|
|
620
|
+
entries: ${JSON.stringify(optimizeDepsEntries)},
|
|
621
|
+
include: ${JSON.stringify(optimizeDepsInclude)},
|
|
622
|
+
},
|
|
623
|
+
// Blume's render-time deps are forced external on both build environments so
|
|
624
|
+
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
625
|
+
// symlinked store copies (which would surface their children as unresolvable
|
|
626
|
+
// imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
|
|
627
|
+
//
|
|
628
|
+
// The SSR externals go through the legacy \`ssr.external\` key rather than
|
|
629
|
+
// \`environments.ssr\`: defining a user-owned \`environments.ssr\` block
|
|
630
|
+
// collides with the internal environment Astro 7 builds the server under and
|
|
631
|
+
// detaches the adapter's server entrypoint from the rolldown input, so the
|
|
632
|
+
// SSR entry is emitted as \`index.mjs\` instead of the \`entry.mjs\` the
|
|
633
|
+
// Vercel adapter's \`astro:build:done\` hook then fails to find. \`prerender\`
|
|
634
|
+
// is Astro-only and has no legacy equivalent, so it stays under \`environments\`.
|
|
635
|
+
ssr: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} },
|
|
636
|
+
environments: {
|
|
637
|
+
prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
638
|
+
},
|
|
639
|
+
resolve: {
|
|
640
|
+
alias: {
|
|
641
|
+
"blume:ask": ${JSON.stringify(askPath)},
|
|
642
|
+
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
643
|
+
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
644
|
+
"blume:features": ${JSON.stringify(featuresPath)},
|
|
645
|
+
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
646
|
+
"blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
server: {
|
|
650
|
+
fs: {
|
|
651
|
+
allow: ${JSON.stringify(fsAllow)},
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
`;
|
|
657
|
+
};
|
|
658
|
+
var stagedContentDir = (outDir) => join2(outDir, "content");
|
|
659
|
+
var runtimeDirWithin = (base, outDir) => {
|
|
660
|
+
const rel = relative(base, outDir);
|
|
661
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
|
|
662
|
+
};
|
|
663
|
+
var astroGlobBase = (base) => isAbsolute(base) ? pathToFileURL(base).href : base;
|
|
664
|
+
var contentConfigTemplate = (options) => {
|
|
665
|
+
const { context, config } = options;
|
|
666
|
+
const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
|
|
667
|
+
const collectionBase = options.collection?.base ?? context.contentRoot;
|
|
668
|
+
const includeGlobs = options.collection?.include ?? config.content.include;
|
|
669
|
+
const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
|
|
670
|
+
const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
|
|
671
|
+
const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
|
|
672
|
+
const filesystem = options.filesystem ?? true;
|
|
673
|
+
const docsPattern = filesystem ? [
|
|
674
|
+
...includeGlobs,
|
|
675
|
+
...(excludeGlobs ?? []).map((pattern) => `!${pattern}`),
|
|
676
|
+
...BLUME_IGNORE_DIRS.flatMap((dir) => dir === ".blume" ? [] : [`!**/${dir}/**`]),
|
|
677
|
+
...outDirIgnore
|
|
678
|
+
] : [];
|
|
679
|
+
const stagedBlock = options.staged ? `
|
|
680
|
+
const staged = defineCollection({
|
|
681
|
+
loader: glob({
|
|
682
|
+
pattern: ["**/*.{md,mdx}"],
|
|
683
|
+
base: ${JSON.stringify(astroGlobBase(stagedBase))},
|
|
684
|
+
generateId: ({ entry }) => entry,
|
|
685
|
+
}),
|
|
686
|
+
schema: pageCollectionSchema,
|
|
687
|
+
});
|
|
688
|
+
` : "";
|
|
689
|
+
return `// Generated by Blume. Do not edit.
|
|
690
|
+
import { defineCollection } from "astro:content";
|
|
691
|
+
import { glob } from "astro/loaders";
|
|
692
|
+
import { withIncludeRefresh } from "blume/astro";
|
|
693
|
+
import { pageCollectionSchema } from "blume/core/schema.ts";
|
|
694
|
+
|
|
695
|
+
// withIncludeRefresh keeps <include>-bearing pages fresh: plain .md entries
|
|
696
|
+
// are rendered at sync time and digest-cached on the page file alone, so a
|
|
697
|
+
// partial edit (or a warm-cache rebuild after one) would serve stale HTML.
|
|
698
|
+
//
|
|
699
|
+
// pageCollectionSchema is the scan's page front-matter schema, so entry.data
|
|
700
|
+
// is typed and normalized the same way; it passes custom keys through and
|
|
701
|
+
// falls back to defaults for a page the scan already dropped as invalid.
|
|
702
|
+
const docs = defineCollection({
|
|
703
|
+
loader: withIncludeRefresh(glob({
|
|
704
|
+
pattern: ${JSON.stringify(docsPattern)},
|
|
705
|
+
base: ${JSON.stringify(astroGlobBase(collectionBase))},
|
|
706
|
+
generateId: ({ entry }) => entry,
|
|
707
|
+
}), ${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}),
|
|
708
|
+
schema: pageCollectionSchema,
|
|
709
|
+
});
|
|
710
|
+
${stagedBlock}
|
|
711
|
+
export const collections = { docs${options.staged ? ", staged" : ""} };
|
|
712
|
+
`;
|
|
713
|
+
};
|
|
714
|
+
var ASK_FALLBACK_PROMPT = "You are a helpful documentation assistant. Answer using the project's documentation.";
|
|
715
|
+
var askEndpointTemplate = (backend, grounded, options) => {
|
|
716
|
+
const instructions = options?.instructions;
|
|
717
|
+
const fallbackPrompt = instructions ? `${ASK_FALLBACK_PROMPT}
|
|
718
|
+
|
|
719
|
+
${instructions}` : ASK_FALLBACK_PROMPT;
|
|
720
|
+
const imports = [
|
|
721
|
+
'import type { APIRoute } from "astro";',
|
|
722
|
+
'import { getSecret } from "astro:env/server";',
|
|
723
|
+
backend.kind === "gateway" ? 'import { createGateway, streamText } from "ai";' : 'import { streamText } from "ai";'
|
|
724
|
+
];
|
|
725
|
+
let setup = "";
|
|
726
|
+
let modelExpr = JSON.stringify(backend.model);
|
|
727
|
+
if (backend.kind === "gateway") {
|
|
728
|
+
setup = `
|
|
729
|
+
const gateway = createGateway({ apiKey: getSecret("AI_GATEWAY_API_KEY") });
|
|
730
|
+
`;
|
|
731
|
+
modelExpr = `gateway(${JSON.stringify(backend.model)})`;
|
|
732
|
+
} else if (backend.kind === "openrouter") {
|
|
733
|
+
imports.push('import { createOpenRouter } from "@openrouter/ai-sdk-provider";');
|
|
734
|
+
setup = `
|
|
735
|
+
const openrouter = createOpenRouter({ apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}) });
|
|
736
|
+
`;
|
|
737
|
+
modelExpr = `openrouter(${JSON.stringify(backend.model)})`;
|
|
738
|
+
} else if (backend.kind === "openai-compatible") {
|
|
739
|
+
imports.push('import { createOpenAICompatible } from "@ai-sdk/openai-compatible";');
|
|
740
|
+
setup = `
|
|
741
|
+
const provider = createOpenAICompatible({
|
|
742
|
+
apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),
|
|
743
|
+
baseURL: ${JSON.stringify(backend.baseUrl)},
|
|
744
|
+
name: ${JSON.stringify(backend.name)},
|
|
745
|
+
});
|
|
746
|
+
`;
|
|
747
|
+
modelExpr = `provider(${JSON.stringify(backend.model)})`;
|
|
748
|
+
}
|
|
749
|
+
if (grounded) {
|
|
750
|
+
imports.push('import { createAskContext } from "blume/ai/ask-context.ts";', 'import askData from "blume:ask-data";');
|
|
751
|
+
const groundFields = [];
|
|
752
|
+
if (instructions) {
|
|
753
|
+
groundFields.push(`instructions: ${JSON.stringify(instructions)}`);
|
|
754
|
+
}
|
|
755
|
+
if (options?.retrieval) {
|
|
756
|
+
groundFields.push(`retrieval: ${JSON.stringify(options.retrieval)}`);
|
|
757
|
+
}
|
|
758
|
+
const groundOptions = groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
|
|
759
|
+
setup += `
|
|
760
|
+
const ground = createAskContext(askData${groundOptions});
|
|
761
|
+
`;
|
|
762
|
+
}
|
|
763
|
+
const validate = ` const body = await request.json().catch(() => null);
|
|
764
|
+
const raw = body?.messages;
|
|
765
|
+
const valid =
|
|
766
|
+
Array.isArray(raw) &&
|
|
767
|
+
raw.length > 0 &&
|
|
768
|
+
raw.length <= 40 &&
|
|
769
|
+
raw.every(
|
|
770
|
+
(m: unknown) =>
|
|
771
|
+
typeof m === "object" &&
|
|
772
|
+
m !== null &&
|
|
773
|
+
("role" in m && (m.role === "user" || m.role === "assistant")) &&
|
|
774
|
+
("content" in m && typeof m.content === "string")
|
|
775
|
+
) &&
|
|
776
|
+
JSON.stringify(raw).length <= 24_000;
|
|
777
|
+
if (!valid) {
|
|
778
|
+
return new Response(
|
|
779
|
+
"Invalid request: send 1-40 user/assistant messages with string content.",
|
|
780
|
+
{ status: 400 }
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
// Re-build the array so only role/content ever reach the model.
|
|
784
|
+
const messages = raw.map((m: { role: "user" | "assistant"; content: string }) => ({
|
|
785
|
+
content: m.content,
|
|
786
|
+
role: m.role,
|
|
787
|
+
}));`;
|
|
788
|
+
const keyCheck = backend.kind === "gateway" ? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
|
|
789
|
+
if (!(getSecret("AI_GATEWAY_API_KEY") || getSecret("VERCEL_OIDC_TOKEN"))) {
|
|
790
|
+
return new Response(
|
|
791
|
+
"Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
|
|
792
|
+
{ status: 500 }
|
|
793
|
+
);
|
|
794
|
+
}` : ` if (!getSecret(${JSON.stringify(backend.apiKeyEnv)})) {
|
|
795
|
+
return new Response(
|
|
796
|
+
${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
|
|
797
|
+
{ status: 500 }
|
|
798
|
+
);
|
|
799
|
+
}`;
|
|
800
|
+
const onError = ` onError({ error }) {
|
|
801
|
+
console.error("Ask AI provider error:", error);
|
|
802
|
+
},`;
|
|
803
|
+
const stream = grounded ? ` const instructions =
|
|
804
|
+
(await ground(messages, body.page)) ??
|
|
805
|
+
${JSON.stringify(fallbackPrompt)};
|
|
806
|
+
const result = streamText({
|
|
807
|
+
model: ${modelExpr},
|
|
808
|
+
instructions,
|
|
809
|
+
messages,
|
|
810
|
+
${onError}
|
|
811
|
+
});` : ` const result = streamText({
|
|
812
|
+
model: ${modelExpr},
|
|
813
|
+
instructions:
|
|
814
|
+
${JSON.stringify(fallbackPrompt)},
|
|
815
|
+
messages,
|
|
816
|
+
${onError}
|
|
817
|
+
});`;
|
|
818
|
+
const handler = `export const POST: APIRoute = async ({ request }) => {
|
|
819
|
+
${validate}
|
|
820
|
+
${keyCheck}
|
|
821
|
+
try {
|
|
822
|
+
${stream}
|
|
823
|
+
return result.toTextStreamResponse();
|
|
824
|
+
} catch {
|
|
825
|
+
return new Response("Failed to generate a response.", { status: 500 });
|
|
826
|
+
}
|
|
827
|
+
};`;
|
|
828
|
+
return `// Generated by Blume. Do not edit.
|
|
829
|
+
${imports.join(`
|
|
830
|
+
`)}
|
|
831
|
+
|
|
832
|
+
export const prerender = false;
|
|
833
|
+
${setup}
|
|
834
|
+
${handler}
|
|
835
|
+
`;
|
|
836
|
+
};
|
|
837
|
+
var askComponentTemplate = (askEnabled) => askEnabled ? `---
|
|
838
|
+
// Generated by Blume. Do not edit.
|
|
839
|
+
import AskAI from "blume/components/islands/AskAI.astro";
|
|
840
|
+
import data from "blume:data";
|
|
841
|
+
|
|
842
|
+
const { strings } = Astro.props;
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
<AskAI
|
|
846
|
+
endpoint={data.config.ask?.endpoint ?? undefined}
|
|
847
|
+
strings={strings ?? data.ui.ask}
|
|
848
|
+
suggestions={data.config.ask?.suggestions ?? []}
|
|
849
|
+
/>
|
|
850
|
+
` : `---
|
|
851
|
+
// Generated by Blume. Do not edit.
|
|
852
|
+
// Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
|
|
853
|
+
// Deliberately imports no React island, keeping the JSX renderer out of projects
|
|
854
|
+
// that don't need it.
|
|
855
|
+
---
|
|
856
|
+
`;
|
|
857
|
+
var searchEndpointTemplate = () => `// Generated by Blume. Do not edit.
|
|
858
|
+
import documents from "blume:search-index";
|
|
859
|
+
|
|
860
|
+
export const prerender = true;
|
|
861
|
+
|
|
862
|
+
export function GET() {
|
|
863
|
+
return new Response(JSON.stringify(documents), {
|
|
864
|
+
headers: { "Content-Type": "application/json" },
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
`;
|
|
868
|
+
var SEARCH_CLIENT_HEADER = `// Generated by Blume. Do not edit.
|
|
869
|
+
`;
|
|
870
|
+
var searchClientImport = (module) => `import { createSearch as create } from "blume/components/layout/search/${module}.ts";
|
|
871
|
+
`;
|
|
872
|
+
var SEARCH_BASE_IMPORT = `import { joinBase } from "blume/components/islands/base-path.ts";
|
|
873
|
+
`;
|
|
874
|
+
var staticSearchClient = (module, locale) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}${SEARCH_BASE_IMPORT}
|
|
875
|
+
const indexUrl = joinBase(import.meta.env.BASE_URL, "blume-search.json");
|
|
876
|
+
|
|
877
|
+
export const createSearch = () => create({ indexUrl${locale ? `, locale: ${JSON.stringify(locale)}` : ""} });
|
|
878
|
+
`;
|
|
879
|
+
var hostedSearchClient = (module, options) => `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
|
|
880
|
+
export const createSearch = () => create(${JSON.stringify(options)});
|
|
881
|
+
`;
|
|
882
|
+
var hostedSearchOptions = (search) => {
|
|
883
|
+
switch (search.provider) {
|
|
884
|
+
case "algolia": {
|
|
885
|
+
return { module: "algolia", options: { ...search.algolia } };
|
|
886
|
+
}
|
|
887
|
+
case "orama-cloud": {
|
|
888
|
+
return {
|
|
889
|
+
module: "orama-cloud",
|
|
890
|
+
options: {
|
|
891
|
+
apiKey: search.oramaCloud?.apiKey,
|
|
892
|
+
endpoint: search.oramaCloud?.endpoint
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
case "typesense": {
|
|
897
|
+
return { module: "typesense", options: { ...search.typesense } };
|
|
898
|
+
}
|
|
899
|
+
default: {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
var featuresTemplate = (features) => `// Generated by Blume. Do not edit.
|
|
905
|
+
//
|
|
906
|
+
// The client features this site uses. A feature no page needs stays out of
|
|
907
|
+
// the module graph entirely: its loader is null and its library never enters
|
|
908
|
+
// the client bundle.
|
|
909
|
+
|
|
910
|
+
/** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
|
|
911
|
+
export const loadMermaid: (() => Promise<unknown>) | null = ${features.mermaid ? '() => import("blume/components/content/mermaid-element.ts")' : "null"};
|
|
912
|
+
|
|
913
|
+
/** The EPUB generator's browser bundle; null when export.epub is off. */
|
|
914
|
+
export const loadEpub:
|
|
915
|
+
| (() => Promise<typeof import("epub-gen-memory/bundle")>)
|
|
916
|
+
| null = ${features.epub ? '() => import("epub-gen-memory/bundle")' : "null"};
|
|
917
|
+
`;
|
|
918
|
+
var searchClientTemplate = (config) => {
|
|
919
|
+
const { search } = config;
|
|
920
|
+
if (search.provider === "orama" || search.provider === "flexsearch") {
|
|
921
|
+
const locale = search.provider === "orama" ? config.i18n?.defaultLocale : undefined;
|
|
922
|
+
return staticSearchClient(search.provider, locale);
|
|
923
|
+
}
|
|
924
|
+
const hosted = hostedSearchOptions(search);
|
|
925
|
+
if (hosted) {
|
|
926
|
+
return hostedSearchClient(hosted.module, hosted.options);
|
|
927
|
+
}
|
|
928
|
+
if (search.provider === "mixedbread") {
|
|
929
|
+
return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
|
|
930
|
+
const api = joinBase(import.meta.env.BASE_URL, "api/search");
|
|
931
|
+
|
|
932
|
+
export const createSearch = () => create({ api });
|
|
933
|
+
`;
|
|
934
|
+
}
|
|
935
|
+
if (search.provider === "pagefind") {
|
|
936
|
+
return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}${SEARCH_BASE_IMPORT}
|
|
937
|
+
const url = joinBase(import.meta.env.BASE_URL, "pagefind/pagefind.js");
|
|
938
|
+
|
|
939
|
+
export const createSearch = () => create({ url });
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
return `${SEARCH_CLIENT_HEADER}export const createSearch = () => () =>
|
|
943
|
+
Promise.resolve({ hits: [], sections: [] });
|
|
944
|
+
`;
|
|
945
|
+
};
|
|
946
|
+
var mixedbreadSearchEndpointTemplate = (storeId) => `// Generated by Blume. Do not edit.
|
|
947
|
+
import type { APIRoute } from "astro";
|
|
948
|
+
import { getSecret } from "astro:env/server";
|
|
949
|
+
import Mixedbread from "@mixedbread/sdk";
|
|
950
|
+
|
|
951
|
+
export const prerender = false;
|
|
952
|
+
|
|
953
|
+
const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" });
|
|
954
|
+
const STORE_ID = ${JSON.stringify(storeId)};
|
|
955
|
+
|
|
956
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
957
|
+
// The endpoint is public: a malformed body must 200-empty, not 500.
|
|
958
|
+
const body = await request.json().catch(() => null);
|
|
959
|
+
const query = body?.query;
|
|
960
|
+
if (!query || typeof query !== "string") {
|
|
961
|
+
return new Response("[]", {
|
|
962
|
+
headers: { "Content-Type": "application/json" },
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const response = await client.stores.search({
|
|
966
|
+
query,
|
|
967
|
+
store_identifiers: [STORE_ID],
|
|
968
|
+
top_k: 8,
|
|
969
|
+
});
|
|
970
|
+
const hits = (response.data ?? []).map((chunk) => {
|
|
971
|
+
const meta = chunk.generated_metadata ?? {};
|
|
972
|
+
return {
|
|
973
|
+
excerpt: chunk.text ?? meta.excerpt ?? "",
|
|
974
|
+
title: meta.title ?? chunk.filename ?? "",
|
|
975
|
+
url: meta.url ?? "",
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
return new Response(JSON.stringify(hits), {
|
|
979
|
+
headers: { "Content-Type": "application/json" },
|
|
980
|
+
});
|
|
981
|
+
};
|
|
982
|
+
`;
|
|
983
|
+
var rawMarkdownEndpointTemplate = (kind) => `// Generated by Blume. Do not edit.
|
|
984
|
+
import raw from "blume:raw-markdown";
|
|
985
|
+
|
|
986
|
+
export const prerender = true;
|
|
987
|
+
|
|
988
|
+
export function getStaticPaths() {
|
|
989
|
+
return Object.keys(raw).map((route) => ({
|
|
990
|
+
params: { slug: route === "/" ? "index" : route.slice(1) },
|
|
991
|
+
props: { route },
|
|
992
|
+
}));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export function GET({ props }: { props: { route: string } }) {
|
|
996
|
+
const entries = raw as Record<string, { md?: string; mdx?: string }>;
|
|
997
|
+
const entry = entries[props.route];
|
|
998
|
+
const body = entry ? ${kind === "md" ? '(entry.md ?? entry.mdx ?? "")' : '(entry.mdx ?? "")'} : "";
|
|
999
|
+
return new Response(body, {
|
|
1000
|
+
headers: {
|
|
1001
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1002
|
+
// ~4 characters per token; keep in sync with markdownTokenCount.
|
|
1003
|
+
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
`;
|
|
1008
|
+
var contentAssetsEndpointTemplate = (stagedAssetsDir) => `// Generated by Blume. Do not edit.
|
|
1009
|
+
import { existsSync } from "node:fs";
|
|
1010
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1011
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
1012
|
+
import type { APIRoute } from "astro";
|
|
1013
|
+
import assets from "blume:content-assets";
|
|
1014
|
+
|
|
1015
|
+
export const prerender = true;
|
|
1016
|
+
|
|
1017
|
+
const files = assets as Record<string, string>;
|
|
1018
|
+
const STAGED_DIR = ${JSON.stringify(stagedAssetsDir)};
|
|
1019
|
+
|
|
1020
|
+
const TYPES: Record<string, string> = {
|
|
1021
|
+
".apng": "image/apng",
|
|
1022
|
+
".avif": "image/avif",
|
|
1023
|
+
".bmp": "image/bmp",
|
|
1024
|
+
".gif": "image/gif",
|
|
1025
|
+
".ico": "image/x-icon",
|
|
1026
|
+
".jpeg": "image/jpeg",
|
|
1027
|
+
".jpg": "image/jpeg",
|
|
1028
|
+
".png": "image/png",
|
|
1029
|
+
".svg": "image/svg+xml",
|
|
1030
|
+
".tiff": "image/tiff",
|
|
1031
|
+
".webp": "image/webp",
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const contentType = (path: string): string =>
|
|
1035
|
+
TYPES[path.slice(path.lastIndexOf(".")).toLowerCase()] ??
|
|
1036
|
+
"application/octet-stream";
|
|
1037
|
+
|
|
1038
|
+
const stagedParams = async (): Promise<string[]> => {
|
|
1039
|
+
if (!existsSync(STAGED_DIR)) {
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
const entries = await readdir(STAGED_DIR, {
|
|
1043
|
+
recursive: true,
|
|
1044
|
+
withFileTypes: true,
|
|
1045
|
+
});
|
|
1046
|
+
return entries
|
|
1047
|
+
.filter((entry) => entry.isFile())
|
|
1048
|
+
.map((entry) =>
|
|
1049
|
+
relative(STAGED_DIR, join(entry.parentPath, entry.name)).replaceAll(
|
|
1050
|
+
"\\\\",
|
|
1051
|
+
"/"
|
|
1052
|
+
)
|
|
1053
|
+
);
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
export async function getStaticPaths() {
|
|
1057
|
+
const params = [
|
|
1058
|
+
...Object.keys(files).map((key) => "content/" + key),
|
|
1059
|
+
...(await stagedParams()),
|
|
1060
|
+
];
|
|
1061
|
+
return params.map((asset) => ({ params: { asset } }));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const resolveAsset = (asset: string): string | null => {
|
|
1065
|
+
if (asset.startsWith("content/")) {
|
|
1066
|
+
return files[asset.slice("content/".length)] ?? null;
|
|
1067
|
+
}
|
|
1068
|
+
const abs = resolve(STAGED_DIR, asset);
|
|
1069
|
+
// Traversal guard: the dev server renders on demand, so the param is
|
|
1070
|
+
// attacker-controlled there — never step outside the staged directory.
|
|
1071
|
+
// path.relative rather than a string-prefix test: STAGED_DIR is baked in
|
|
1072
|
+
// with forward slashes while resolve() answers in the platform's
|
|
1073
|
+
// separators, so on Windows the prefix test 404'd every legitimate staged
|
|
1074
|
+
// asset — and a bare prefix also admits a sibling directory that merely
|
|
1075
|
+
// shares the name.
|
|
1076
|
+
const rel = relative(STAGED_DIR, abs);
|
|
1077
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
return existsSync(abs) ? abs : null;
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
export const GET: APIRoute = async ({ params }) => {
|
|
1084
|
+
const path = resolveAsset(String(params.asset ?? ""));
|
|
1085
|
+
if (!path) {
|
|
1086
|
+
return new Response(null, { status: 404 });
|
|
1087
|
+
}
|
|
1088
|
+
const body = await readFile(path);
|
|
1089
|
+
return new Response(new Uint8Array(body), {
|
|
1090
|
+
headers: { "Content-Type": contentType(path) },
|
|
1091
|
+
});
|
|
1092
|
+
};
|
|
1093
|
+
`;
|
|
1094
|
+
var mcpPageFile = (route) => `${trimChar(route, "/")}.ts`;
|
|
1095
|
+
var mcpEndpointTemplate = () => `// Generated by Blume. Do not edit.
|
|
1096
|
+
import type { APIRoute } from "astro";
|
|
1097
|
+
import { createMcpFetchHandler } from "blume/ai/mcp/server.ts";
|
|
1098
|
+
import data from "blume:mcp-data";
|
|
1099
|
+
|
|
1100
|
+
export const prerender = false;
|
|
1101
|
+
|
|
1102
|
+
const handler = createMcpFetchHandler(data);
|
|
1103
|
+
|
|
1104
|
+
export const ALL: APIRoute = ({ request }) => handler(request);
|
|
1105
|
+
`;
|
|
1106
|
+
var playgroundProxyTemplate = (origins) => `// Generated by Blume. Do not edit.
|
|
1107
|
+
import type { APIRoute } from "astro";
|
|
1108
|
+
import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";
|
|
1109
|
+
|
|
1110
|
+
export const prerender = false;
|
|
1111
|
+
|
|
1112
|
+
const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
|
|
1113
|
+
|
|
1114
|
+
export const ALL: APIRoute = ({ request }) => handler(request);
|
|
1115
|
+
`;
|
|
1116
|
+
var staticJsonEndpointTemplate = (payload) => `// Generated by Blume. Do not edit.
|
|
1117
|
+
export const prerender = true;
|
|
1118
|
+
|
|
1119
|
+
const payload = ${JSON.stringify(payload, null, 2)};
|
|
1120
|
+
|
|
1121
|
+
export function GET() {
|
|
1122
|
+
return new Response(JSON.stringify(payload), {
|
|
1123
|
+
headers: { "Content-Type": "application/json" },
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
`;
|
|
1127
|
+
var rssEndpointTemplate = () => `// Generated by Blume. Do not edit.
|
|
1128
|
+
import feeds from "blume:rss";
|
|
1129
|
+
|
|
1130
|
+
export const prerender = true;
|
|
1131
|
+
|
|
1132
|
+
export function getStaticPaths() {
|
|
1133
|
+
return Object.keys(feeds).map((section) => ({
|
|
1134
|
+
params: { section },
|
|
1135
|
+
props: { section },
|
|
1136
|
+
}));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function GET({ props }: { props: { section: string } }) {
|
|
1140
|
+
const bySection = feeds as Record<string, string>;
|
|
1141
|
+
return new Response(bySection[props.section] ?? "", {
|
|
1142
|
+
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
`;
|
|
1146
|
+
var navFragmentTemplate = () => `---
|
|
1147
|
+
// Generated by Blume. Do not edit.
|
|
1148
|
+
import NavTree from "blume/components/layout/NavTree.astro";
|
|
1149
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1150
|
+
import { navGroupIds, navVariants } from "blume/components/layout/nav-utils.ts";
|
|
1151
|
+
import type { NavNode } from "blume/core/types.ts";
|
|
1152
|
+
import data from "blume:data";
|
|
1153
|
+
|
|
1154
|
+
export const prerender = true;
|
|
1155
|
+
// A partial: no doctype, head, or script/style injection — the rows only.
|
|
1156
|
+
export const partial = true;
|
|
1157
|
+
|
|
1158
|
+
// Only imports are in scope here (Astro hoists getStaticPaths), which is why
|
|
1159
|
+
// the variant walk lives in nav-utils.
|
|
1160
|
+
export function getStaticPaths() {
|
|
1161
|
+
return navVariants(data).flatMap(({ locale, navigation, version }) =>
|
|
1162
|
+
[...navGroupIds(navigation.sidebar)]
|
|
1163
|
+
.filter(([node]) => (node.display ?? "flat") !== "flat")
|
|
1164
|
+
.map(([, id]) => ({ params: { id, locale, version } }))
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const { id, locale, version } = Astro.params;
|
|
1169
|
+
const variant = navVariants(data).find(
|
|
1170
|
+
(entry) => entry.version === version && entry.locale === locale
|
|
1171
|
+
);
|
|
1172
|
+
const ids = variant
|
|
1173
|
+
? navGroupIds(variant.navigation.sidebar)
|
|
1174
|
+
: new Map<NavNode, string>();
|
|
1175
|
+
const node = [...ids].find(([, groupId]) => groupId === id)?.[0];
|
|
1176
|
+
if (!(variant && node && node.kind === "group")) {
|
|
1177
|
+
return new Response(null, { status: 404 });
|
|
1178
|
+
}
|
|
1179
|
+
const strings =
|
|
1180
|
+
locale === "default" ? data.ui.nav : (data.uiByLocale[locale] ?? data.ui).nav;
|
|
1181
|
+
const fragmentBase = withBase(\`/blume-nav/\${version}/\${locale}\`);
|
|
1182
|
+
---
|
|
1183
|
+
|
|
1184
|
+
<NavTree
|
|
1185
|
+
currentRoute=""
|
|
1186
|
+
depth={1}
|
|
1187
|
+
fragmentBase={fragmentBase}
|
|
1188
|
+
idPrefix={id}
|
|
1189
|
+
ids={ids}
|
|
1190
|
+
items={node.children}
|
|
1191
|
+
root={false}
|
|
1192
|
+
strings={strings}
|
|
1193
|
+
/>
|
|
1194
|
+
`;
|
|
1195
|
+
var ogEndpointTemplate = (customRoutes = [], og = {}, includeChangelog = false) => `// Generated by Blume. Do not edit.
|
|
1196
|
+
import { cachedOgImage } from "blume/og";
|
|
1197
|
+
import type { OgCache, OgFont, OgFontFamilies } from "blume/og";
|
|
1198
|
+
import data from "blume:data";
|
|
1199
|
+
|
|
1200
|
+
export const prerender = true;
|
|
1201
|
+
|
|
1202
|
+
// Custom (non-content) pages opted into a generated card, baked in at build.
|
|
1203
|
+
// The annotation keeps the empty-array case from being an implicit any[]
|
|
1204
|
+
// (ts(7034)) under a strict tsconfig.
|
|
1205
|
+
const customRoutes: { slug: string; title: string }[] = ${JSON.stringify(customRoutes)};
|
|
1206
|
+
|
|
1207
|
+
// Card fonts, resolved at generation time (explicit seo.og.fonts, or derived
|
|
1208
|
+
// from theme.fonts). Local entries carry absolute build-machine paths, which
|
|
1209
|
+
// is why they are baked into this build-only endpoint instead of the runtime
|
|
1210
|
+
// data that pages serialize into HTML.
|
|
1211
|
+
const fonts: OgFont[] = ${JSON.stringify(og.fonts ?? [])};
|
|
1212
|
+
const families: OgFontFamilies | undefined = ${og.families ? JSON.stringify(og.families) : "undefined"};
|
|
1213
|
+
|
|
1214
|
+
// Rendered cards are kept on disk between builds, keyed by everything that
|
|
1215
|
+
// decides their pixels, so a rebuild renders only the cards whose title,
|
|
1216
|
+
// description, branding, or fonts changed. The directory is a build-machine
|
|
1217
|
+
// path, baked in for the same reason as the local font paths above.
|
|
1218
|
+
const cache: OgCache | undefined = ${og.cache ? JSON.stringify(og.cache) : "undefined"};
|
|
1219
|
+
|
|
1220
|
+
// A page's own description (its \`seo.description\`, else \`description\`) is
|
|
1221
|
+
// the card subtitle, so the image says what the page's og:description says.
|
|
1222
|
+
// Pages without one fall back to the site-wide subtitle at render time.
|
|
1223
|
+
// \`seo.og.description: false\` hides the subtitle on every card, page text
|
|
1224
|
+
// included, which is what switches this off.
|
|
1225
|
+
const pageDescriptions = ${og.pageDescriptions !== false};
|
|
1226
|
+
|
|
1227
|
+
interface CardProps {
|
|
1228
|
+
title: string;
|
|
1229
|
+
description: string | null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export function getStaticPaths() {
|
|
1233
|
+
const seen = new Set<string>();
|
|
1234
|
+
const paths: { params: { slug: string }; props: CardProps }[] = [];
|
|
1235
|
+
const add = (slug: string, title: string, description: string | null) => {
|
|
1236
|
+
if (seen.has(slug)) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
seen.add(slug);
|
|
1240
|
+
paths.push({
|
|
1241
|
+
params: { slug },
|
|
1242
|
+
props: { title, description: pageDescriptions ? description : null },
|
|
1243
|
+
});
|
|
1244
|
+
};
|
|
1245
|
+
// A custom page wins over a content route sharing its path, so add it first.
|
|
1246
|
+
// Its description is unknown at generate time, so it takes the site subtitle.
|
|
1247
|
+
for (const route of customRoutes) {
|
|
1248
|
+
add(route.slug, route.title, null);
|
|
1249
|
+
}
|
|
1250
|
+
for (const route of data.routes) {
|
|
1251
|
+
add(
|
|
1252
|
+
route.path === "/" ? "index" : route.path.slice(1),
|
|
1253
|
+
route.title,
|
|
1254
|
+
route.description
|
|
1255
|
+
);
|
|
1256
|
+
}${includeChangelog ? `
|
|
1257
|
+
// The generated changelog index is not a content route, so it needs its own
|
|
1258
|
+
// card. Added last: a custom page or content route owning /changelog wins.
|
|
1259
|
+
add(
|
|
1260
|
+
"changelog",
|
|
1261
|
+
data.ui.changelog?.title ?? "Changelog",
|
|
1262
|
+
data.ui.changelog?.description ?? null
|
|
1263
|
+
);` : ""}
|
|
1264
|
+
return paths;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Footer branding shared by every card. The slug comes from the configured
|
|
1268
|
+
// repo rather than the URL, so an Enterprise host reads the same as github.com;
|
|
1269
|
+
// the site text (host plus deployment base) is resolved at generate time.
|
|
1270
|
+
const repoSlug = data.config.github
|
|
1271
|
+
? \`\${data.config.github.owner}/\${data.config.github.repo}\`
|
|
1272
|
+
: undefined;
|
|
1273
|
+
|
|
1274
|
+
export async function GET({ props }: { props: CardProps }) {
|
|
1275
|
+
const png = await cachedOgImage(cache, {
|
|
1276
|
+
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1277
|
+
brand: data.config.title,
|
|
1278
|
+
description: props.description ?? data.config.og.description,
|
|
1279
|
+
families,
|
|
1280
|
+
fonts,
|
|
1281
|
+
logo: data.config.og.logo,
|
|
1282
|
+
palette: data.config.og.palette,
|
|
1283
|
+
repo: repoSlug,
|
|
1284
|
+
site: data.config.og.site,
|
|
1285
|
+
title: props.title,
|
|
1286
|
+
});
|
|
1287
|
+
return new Response(new Uint8Array(png), {
|
|
1288
|
+
headers: {
|
|
1289
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
1290
|
+
"Content-Type": "image/png",
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
`;
|
|
1295
|
+
var scalarReferenceTemplate = (options) => `---
|
|
1296
|
+
// Generated by Blume. Do not edit.
|
|
1297
|
+
import { ScalarComponent } from "@scalar/astro";
|
|
1298
|
+
import ReferenceLayout from "blume/components/layout/ReferenceLayout.astro";
|
|
1299
|
+
import data from "blume:data";
|
|
1300
|
+
|
|
1301
|
+
export const prerender = true;
|
|
1302
|
+
|
|
1303
|
+
const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
1304
|
+
|
|
1305
|
+
// The reference is an unlocalized route, so its chrome renders in the default
|
|
1306
|
+
// locale's language and direction (\`data.ui\` is the default locale's resolved
|
|
1307
|
+
// dictionary), mirroring the changelog index's locale wiring.
|
|
1308
|
+
const i18n = data.config.i18n;
|
|
1309
|
+
const localeMeta = i18n
|
|
1310
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1311
|
+
: null;
|
|
1312
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1313
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1316
|
+
<ReferenceLayout
|
|
1317
|
+
analytics={data.config.analytics}
|
|
1318
|
+
banner={data.config.banner}
|
|
1319
|
+
dir={dir}
|
|
1320
|
+
fontCssVars={data.fontCssVars}
|
|
1321
|
+
locale={htmlLang}
|
|
1322
|
+
logo={data.config.logo}
|
|
1323
|
+
favicon={data.config.favicon}
|
|
1324
|
+
appleIcon={data.config.appleIcon}
|
|
1325
|
+
navigation={data.navigation}
|
|
1326
|
+
noindex={${options.noindex === true}}
|
|
1327
|
+
pageTitle={${JSON.stringify(options.title)}}
|
|
1328
|
+
route={${JSON.stringify(options.route)}}
|
|
1329
|
+
searchEnabled={data.config.search.enabled}
|
|
1330
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
1331
|
+
themeMode={data.config.theme.mode}
|
|
1332
|
+
ui={data.ui}
|
|
1333
|
+
>
|
|
1334
|
+
<ScalarComponent configuration={configuration} renderMode="client" />
|
|
1335
|
+
</ReferenceLayout>
|
|
1336
|
+
`;
|
|
1337
|
+
var catchAllPageTemplate = (options) => {
|
|
1338
|
+
const mathImport = options.mathEnabled ? `import Math from "blume/components/content/Math.astro";
|
|
1339
|
+
` : "";
|
|
1340
|
+
const mathEntry = options.mathEnabled ? `Math,
|
|
1341
|
+
` : "";
|
|
1342
|
+
const clientData = options.needsReact ? `
|
|
1343
|
+
clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}` : "";
|
|
1344
|
+
return `---
|
|
1345
|
+
// Generated by Blume. Do not edit.
|
|
1346
|
+
import { getEntry, render } from "astro:content";
|
|
1347
|
+
import type { CollectionKey } from "astro:content";
|
|
1348
|
+
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1349
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1350
|
+
import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
|
|
1351
|
+
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1352
|
+
import Accordion from "blume/components/content/Accordion.astro";
|
|
1353
|
+
import AccordionItem from "blume/components/content/AccordionItem.astro";
|
|
1354
|
+
import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
|
|
1355
|
+
import Badge from "blume/components/content/Badge.astro";
|
|
1356
|
+
import Callout from "blume/components/content/Callout.astro";
|
|
1357
|
+
import Card from "blume/components/content/Card.astro";
|
|
1358
|
+
import CardGroup from "blume/components/content/CardGroup.astro";
|
|
1359
|
+
import CodeBlock from "blume/components/content/CodeBlock.astro";
|
|
1360
|
+
import CodeGroup from "blume/components/content/CodeGroup.astro";
|
|
1361
|
+
import ColorRoot from "blume/components/content/Color.astro";
|
|
1362
|
+
import ColorItem from "blume/components/content/ColorItem.astro";
|
|
1363
|
+
import ColorRow from "blume/components/content/ColorRow.astro";
|
|
1364
|
+
import Column from "blume/components/content/Column.astro";
|
|
1365
|
+
import Columns from "blume/components/content/Columns.astro";
|
|
1366
|
+
import Component from "blume/components/content/Component.astro";
|
|
1367
|
+
import Diff from "blume/components/content/Diff.astro";
|
|
1368
|
+
import Expandable from "blume/components/content/Expandable.astro";
|
|
1369
|
+
import FileTree from "blume/components/content/FileTree.astro";
|
|
1370
|
+
import Frame from "blume/components/content/Frame.astro";
|
|
1371
|
+
import GithubInfo from "blume/components/content/GithubInfo.astro";
|
|
1372
|
+
import Panel from "blume/components/content/Panel.astro";
|
|
1373
|
+
import Prompt from "blume/components/content/Prompt.astro";
|
|
1374
|
+
import Step from "blume/components/content/Step.astro";
|
|
1375
|
+
import Steps from "blume/components/content/Steps.astro";
|
|
1376
|
+
import Tab from "blume/components/content/Tab.astro";
|
|
1377
|
+
import Tabs from "blume/components/content/Tabs.astro";
|
|
1378
|
+
import Tile from "blume/components/content/Tile.astro";
|
|
1379
|
+
import Tooltip from "blume/components/content/Tooltip.astro";
|
|
1380
|
+
import TreeRoot from "blume/components/content/Tree.astro";
|
|
1381
|
+
import TreeFile from "blume/components/content/TreeFile.astro";
|
|
1382
|
+
import TreeFolder from "blume/components/content/TreeFolder.astro";
|
|
1383
|
+
import TypeTable from "blume/components/content/TypeTable.astro";
|
|
1384
|
+
import Visibility from "blume/components/content/Visibility.astro";
|
|
1385
|
+
import YouTube from "blume/components/content/YouTube.astro";
|
|
1386
|
+
import Icon from "blume/components/Icon.astro";
|
|
1387
|
+
import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
|
|
1388
|
+
import ApiOverview from "blume/components/openapi/ApiOverview.astro";
|
|
1389
|
+
import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
1390
|
+
import Operation from "blume/components/openapi/Operation.astro";
|
|
1391
|
+
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
1392
|
+
import { islandComponents } from "../generated/islands.ts";
|
|
1393
|
+
import data from "blume:data";
|
|
1394
|
+
|
|
1395
|
+
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
1396
|
+
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
1397
|
+
|
|
1398
|
+
// Docs content is file-based and always prerendered, even in server output
|
|
1399
|
+
// (where only endpoints like /api/ask render on demand). Without this, server
|
|
1400
|
+
// builds would render this route on demand and ignore getStaticPaths, leaving
|
|
1401
|
+
// the entry id undefined.
|
|
1402
|
+
export const prerender = true;
|
|
1403
|
+
|
|
1404
|
+
const components = {
|
|
1405
|
+
Accordion,
|
|
1406
|
+
AccordionItem,
|
|
1407
|
+
ApiOverview,
|
|
1408
|
+
ApiTagOperations,
|
|
1409
|
+
AutoTypeTable,
|
|
1410
|
+
Badge,
|
|
1411
|
+
Callout,
|
|
1412
|
+
Card,
|
|
1413
|
+
CardGroup,
|
|
1414
|
+
CodeBlock,
|
|
1415
|
+
CodeGroup,
|
|
1416
|
+
Color,
|
|
1417
|
+
Column,
|
|
1418
|
+
Columns,
|
|
1419
|
+
Component,
|
|
1420
|
+
Diff,
|
|
1421
|
+
Expandable,
|
|
1422
|
+
FileTree,
|
|
1423
|
+
Frame,
|
|
1424
|
+
GithubInfo,
|
|
1425
|
+
Icon,
|
|
1426
|
+
Operation,
|
|
1427
|
+
Panel,
|
|
1428
|
+
Prompt,
|
|
1429
|
+
Step,
|
|
1430
|
+
Steps,
|
|
1431
|
+
Tab,
|
|
1432
|
+
Tabs,
|
|
1433
|
+
Tile,
|
|
1434
|
+
Tooltip,
|
|
1435
|
+
Tree,
|
|
1436
|
+
TypeTable,
|
|
1437
|
+
Visibility,
|
|
1438
|
+
YouTube,
|
|
1439
|
+
${mathEntry}...islandComponents,
|
|
1440
|
+
...userMdx,
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
export function getStaticPaths() {
|
|
1444
|
+
return data.routes.map((route) => ({
|
|
1445
|
+
params: { slug: route.path === "/" ? undefined : route.path.slice(1) },
|
|
1446
|
+
props: {
|
|
1447
|
+
alternates: route.alternates,
|
|
1448
|
+
collection: route.collection,
|
|
1449
|
+
editUrl: route.editUrl,
|
|
1450
|
+
entryId: route.entryId,
|
|
1451
|
+
fallback: route.fallback,
|
|
1452
|
+
indexable: route.indexable,
|
|
1453
|
+
lastModified: route.lastModified,
|
|
1454
|
+
locale: route.locale,
|
|
1455
|
+
route: route.path,
|
|
1456
|
+
title: route.title,
|
|
1457
|
+
version: route.version,
|
|
1458
|
+
versionAlternates: route.versionAlternates,
|
|
1459
|
+
},
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, version, versionAlternates } = Astro.props;
|
|
1464
|
+
const entry = await getEntry(collection as CollectionKey, entryId);
|
|
1465
|
+
if (!entry) {
|
|
1466
|
+
return new Response(null, { status: 404 });
|
|
1467
|
+
}
|
|
1468
|
+
const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render(entry);
|
|
1469
|
+
// \`[!toc]\`-marked headings render on the page but stay out of the table of
|
|
1470
|
+
// contents; the heading plugin reports their slugs through the render's
|
|
1471
|
+
// frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
|
|
1472
|
+
// counts: \`frontmatter.extend\` can declare the same key, and on a page with
|
|
1473
|
+
// no headings that user-supplied value would pass straight through.
|
|
1474
|
+
const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
|
|
1475
|
+
const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
|
|
1476
|
+
const headings =
|
|
1477
|
+
tocHidden.size > 0
|
|
1478
|
+
? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
|
|
1479
|
+
: allHeadings;
|
|
1480
|
+
const frontmatter = entry.data;
|
|
1481
|
+
|
|
1482
|
+
const seo = frontmatter.seo;
|
|
1483
|
+
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
1484
|
+
|
|
1485
|
+
// Percent-encode the route-derived path (the sitemap convention): a Unicode
|
|
1486
|
+
// slug (\`/api/größe\`) is not legal in a raw URI, and crawlers compare
|
|
1487
|
+
// canonical against the sitemap's encoded <loc> byte-for-byte.
|
|
1488
|
+
const ogPath = data.config.og.enabled
|
|
1489
|
+
? encodeURI(\`/og/\${route === "/" ? "index" : route.slice(1)}.png\`)
|
|
1490
|
+
: null;
|
|
1491
|
+
const ogRel = seo.image ?? ogPath;
|
|
1492
|
+
// Absolute URLs also carry the deployment base (the page is served under it):
|
|
1493
|
+
// \`site + base + path\`. Only absolutize root-relative paths: \`seo.image\` may be
|
|
1494
|
+
// an external URL, which passes through verbatim (mirrors PageLayout).
|
|
1495
|
+
const ogImage =
|
|
1496
|
+
ogRel && base && ogRel.startsWith("/") ? \`\${base}\${withBase(ogRel)}\` : ogRel;
|
|
1497
|
+
// Blume's generated card has known dimensions the layout can declare; a user's
|
|
1498
|
+
// \`seo.image\` could be any size or format, so it gets none.
|
|
1499
|
+
const ogGenerated = !seo.image && Boolean(ogPath);
|
|
1500
|
+
|
|
1501
|
+
// X attribution: the site's account, plus a creator the page can claim for
|
|
1502
|
+
// itself (a guest post crediting its own author) over the configured default.
|
|
1503
|
+
const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
|
|
1504
|
+
|
|
1505
|
+
const basedRoute = withBase(route);
|
|
1506
|
+
|
|
1507
|
+
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
|
|
1508
|
+
// build hreflang alternates, and derive the language-switcher targets.
|
|
1509
|
+
const i18n = data.config.i18n;
|
|
1510
|
+
const localePrefix = (codeArg: string) =>
|
|
1511
|
+
i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
|
|
1512
|
+
? ""
|
|
1513
|
+
: \`/\${codeArg}\`;
|
|
1514
|
+
const localizeRoute = (logical: string, codeArg: string) => {
|
|
1515
|
+
const prefix = localePrefix(codeArg);
|
|
1516
|
+
if (!prefix) {
|
|
1517
|
+
return logical;
|
|
1518
|
+
}
|
|
1519
|
+
return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
|
|
1520
|
+
};
|
|
1521
|
+
const stripLocale = (path: string, codeArg: string) => {
|
|
1522
|
+
const prefix = localePrefix(codeArg);
|
|
1523
|
+
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// Version resolution. An archived page renders its snapshot's navigation tree,
|
|
1527
|
+
// points its canonical at the latest equivalent (unless configured otherwise),
|
|
1528
|
+
// and shows the old-version notice.
|
|
1529
|
+
const versionsConfig = data.config.versions;
|
|
1530
|
+
const archived = versionsConfig && version
|
|
1531
|
+
? (versionsConfig.archived.find((v) => v.id === version) ?? null)
|
|
1532
|
+
: null;
|
|
1533
|
+
const latestVersionAlt = (versionAlternates ?? []).find((alt) => alt.version === "");
|
|
1534
|
+
|
|
1535
|
+
const navigation = version
|
|
1536
|
+
? (data.navigationByVersion[version]?.[i18n ? locale : ""] ?? data.navigation)
|
|
1537
|
+
: i18n
|
|
1538
|
+
? (data.navigationByLocale[locale] ?? data.navigation)
|
|
1539
|
+
: data.navigation;
|
|
1540
|
+
const ui = i18n ? (data.uiByLocale[locale] ?? data.ui) : data.ui;
|
|
1541
|
+
const localeMeta = i18n ? i18n.locales.find((l) => l.code === locale) : null;
|
|
1542
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1543
|
+
const htmlLang = i18n ? locale : "en";
|
|
1544
|
+
// A fallback page renders the fallback locale's content, so its text direction
|
|
1545
|
+
// follows that language — not the (mirrored) page locale.
|
|
1546
|
+
const contentLocale =
|
|
1547
|
+
fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
|
|
1548
|
+
const contentDir = i18n
|
|
1549
|
+
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
1550
|
+
: "ltr";
|
|
1551
|
+
// The root route keeps its trailing slash (\`https://site/\`) so canonical and
|
|
1552
|
+
// hreflang URLs byte-match the sitemap's <loc> for the home page.
|
|
1553
|
+
const absolute = (path: string) => base + withBase(path);
|
|
1554
|
+
|
|
1555
|
+
// An archived page defaults its canonical to the same page in the latest docs
|
|
1556
|
+
// when that page still exists — search engines treat the live page as
|
|
1557
|
+
// authoritative without deindexing version-only content. A page's own
|
|
1558
|
+
// \`seo.canonical\` always wins, and \`canonical: "self"\` keeps the default.
|
|
1559
|
+
const canonical =
|
|
1560
|
+
seo.canonical ??
|
|
1561
|
+
(archived && archived.canonical === "latest" && latestVersionAlt && base
|
|
1562
|
+
? absolute(latestVersionAlt.path)
|
|
1563
|
+
: base
|
|
1564
|
+
? \`\${base}\${basedRoute === "/" ? "/" : encodeURI(basedRoute)}\`
|
|
1565
|
+
: null);
|
|
1566
|
+
const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
|
|
1567
|
+
|
|
1568
|
+
const localeAlternates =
|
|
1569
|
+
i18n && base
|
|
1570
|
+
? (alternates ?? []).map((alt) => ({ hreflang: alt.locale, href: absolute(alt.path) }))
|
|
1571
|
+
: [];
|
|
1572
|
+
const defaultAlt = i18n ? (alternates ?? []).find((alt) => alt.locale === i18n.defaultLocale) : null;
|
|
1573
|
+
const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
|
|
1574
|
+
|
|
1575
|
+
// \`route\` arrives with \`basePath\` already applied, so the locale segment it
|
|
1576
|
+
// carries sits *after* the base (\`/docs/ja/guide\`), while \`localePrefix\` is
|
|
1577
|
+
// base-less (\`/ja\`). Stripping and re-adding a locale therefore happen in
|
|
1578
|
+
// base-less space, with the base re-applied at the end — the same
|
|
1579
|
+
// \`withBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
|
|
1580
|
+
// build every real route. Done in based space, \`stripLocale\` matches nothing
|
|
1581
|
+
// and \`localizeRoute\` prepends a second prefix (\`/ja/docs/ja/guide\`). Only a
|
|
1582
|
+
// switcher entry for a locale with no real translation reaches this fallback:
|
|
1583
|
+
// a partially translated hand-written page for its missing locales, or a
|
|
1584
|
+
// generated reference (which has no \`alternates\` at all) for every locale
|
|
1585
|
+
// but its own.
|
|
1586
|
+
const mountLocalized = (logical: string, codeArg: string) =>
|
|
1587
|
+
withBasePath(data.config.basePath, localizeRoute(logical, codeArg));
|
|
1588
|
+
const logicalRoute = i18n
|
|
1589
|
+
? stripLocale(stripBasePath(data.config.basePath, route), locale)
|
|
1590
|
+
: route;
|
|
1591
|
+
const localeSwitch = i18n
|
|
1592
|
+
? i18n.locales.map((l) => {
|
|
1593
|
+
const alt = (alternates ?? []).find((x) => x.locale === l.code);
|
|
1594
|
+
return {
|
|
1595
|
+
code: l.code,
|
|
1596
|
+
current: l.code === locale,
|
|
1597
|
+
dir: l.dir,
|
|
1598
|
+
href: alt ? alt.path : mountLocalized(logicalRoute, l.code),
|
|
1599
|
+
label: l.label,
|
|
1600
|
+
untranslated: !alt,
|
|
1601
|
+
};
|
|
1602
|
+
})
|
|
1603
|
+
: [];
|
|
1604
|
+
|
|
1605
|
+
// Version switcher + old-version notice. The switcher auto-populates from the
|
|
1606
|
+
// versions config as a \`kind: "version"\` selector; a user-declared version
|
|
1607
|
+
// selector in \`navigation.selectors\` suppresses it (theirs renders instead).
|
|
1608
|
+
// Fallback version roots compose like real routes — \`{basePath}/{locale?}/{id}\`
|
|
1609
|
+
// (manifest \`versionAlternates\` paths arrive with the base already applied).
|
|
1610
|
+
const versionRootFor = (id: string) => {
|
|
1611
|
+
const logical = id ? \`/\${id}\` : "/";
|
|
1612
|
+
return i18n
|
|
1613
|
+
? mountLocalized(logical, locale)
|
|
1614
|
+
: withBasePath(data.config.basePath, logical);
|
|
1615
|
+
};
|
|
1616
|
+
const samePageSwitch = versionsConfig
|
|
1617
|
+
? versionsConfig.switcher.redirect === "same-page"
|
|
1618
|
+
: true;
|
|
1619
|
+
const userHasVersionSelector = navigation.selectors.some(
|
|
1620
|
+
(selector) => selector.kind === "version"
|
|
1621
|
+
);
|
|
1622
|
+
const versionSelector =
|
|
1623
|
+
versionsConfig && !userHasVersionSelector
|
|
1624
|
+
? {
|
|
1625
|
+
items: [
|
|
1626
|
+
{
|
|
1627
|
+
id: "",
|
|
1628
|
+
label: versionsConfig.current.label,
|
|
1629
|
+
tag: versionsConfig.current.badge,
|
|
1630
|
+
},
|
|
1631
|
+
...versionsConfig.archived.map((v) => ({
|
|
1632
|
+
id: v.id,
|
|
1633
|
+
label: v.label ?? v.id,
|
|
1634
|
+
tag: undefined,
|
|
1635
|
+
})),
|
|
1636
|
+
].map((entry) => {
|
|
1637
|
+
const alt = (versionAlternates ?? []).find(
|
|
1638
|
+
(a) => a.version === entry.id
|
|
1639
|
+
);
|
|
1640
|
+
return {
|
|
1641
|
+
label: entry.label,
|
|
1642
|
+
path: samePageSwitch && alt ? alt.path : versionRootFor(entry.id),
|
|
1643
|
+
...(entry.tag ? { tag: entry.tag } : {}),
|
|
1644
|
+
};
|
|
1645
|
+
}),
|
|
1646
|
+
kind: "version" as const,
|
|
1647
|
+
label: ui.versions.switcher,
|
|
1648
|
+
}
|
|
1649
|
+
: null;
|
|
1650
|
+
|
|
1651
|
+
const versionNotice =
|
|
1652
|
+
archived && archived.banner !== false
|
|
1653
|
+
? {
|
|
1654
|
+
latestHref: latestVersionAlt
|
|
1655
|
+
? latestVersionAlt.path
|
|
1656
|
+
: versionRootFor(""),
|
|
1657
|
+
latestLabel: ui.versions.latest,
|
|
1658
|
+
message:
|
|
1659
|
+
typeof archived.banner === "string"
|
|
1660
|
+
? archived.banner
|
|
1661
|
+
: ui.versions.notice.replace(
|
|
1662
|
+
"{version}",
|
|
1663
|
+
archived.label ?? archived.id
|
|
1664
|
+
),
|
|
1665
|
+
}
|
|
1666
|
+
: null;
|
|
1667
|
+
|
|
1668
|
+
// The whole page shell is overridable via \`layout.Layout\`; it receives the same
|
|
1669
|
+
// props as the built-in RootLayout, plus the \`layout\` map for its inner slots.
|
|
1670
|
+
const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
1671
|
+
---
|
|
1672
|
+
|
|
1673
|
+
<LayoutComponent
|
|
1674
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
1675
|
+
layout={layoutOverrides}${clientData}
|
|
1676
|
+
logo={data.config.logo}
|
|
1677
|
+
mcp={data.config.mcp}
|
|
1678
|
+
favicon={data.config.favicon}
|
|
1679
|
+
appleIcon={data.config.appleIcon}
|
|
1680
|
+
banner={data.config.banner}
|
|
1681
|
+
analytics={data.config.analytics}
|
|
1682
|
+
imageZoom={data.config.imageZoom}
|
|
1683
|
+
codeWrap={data.config.codeWrap}
|
|
1684
|
+
navigation={navigation}
|
|
1685
|
+
locale={htmlLang}
|
|
1686
|
+
dir={dir}
|
|
1687
|
+
contentDir={contentDir}
|
|
1688
|
+
ui={ui}
|
|
1689
|
+
localeAlternates={localeAlternates}
|
|
1690
|
+
xDefault={xDefault}
|
|
1691
|
+
localeSwitch={localeSwitch}
|
|
1692
|
+
versionSelector={versionSelector}
|
|
1693
|
+
versionNotice={versionNotice}
|
|
1694
|
+
searchVersion={versionsConfig ? version : null}
|
|
1695
|
+
page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
|
|
1696
|
+
headings={headings}
|
|
1697
|
+
toc={data.config.toc}
|
|
1698
|
+
dateFormat={data.config.dateFormat}
|
|
1699
|
+
themeMode={data.config.theme.mode}
|
|
1700
|
+
fontCssVars={data.fontCssVars}
|
|
1701
|
+
searchEnabled={data.config.search.enabled}
|
|
1702
|
+
indexable={indexable}
|
|
1703
|
+
ogImage={ogImage}
|
|
1704
|
+
ogGenerated={ogGenerated}
|
|
1705
|
+
x={x}
|
|
1706
|
+
canonical={canonical}
|
|
1707
|
+
editUrl={editUrl}
|
|
1708
|
+
feedback={data.config.feedback}
|
|
1709
|
+
exportPdf={${options.exportPdf}}
|
|
1710
|
+
exportEpub={${options.exportEpub}}${options.navFragments ? `
|
|
1711
|
+
navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n ? locale : "default"}\`)}` : ""}
|
|
1712
|
+
openInChat={data.config.openInChat}
|
|
1713
|
+
feeds={data.feeds}
|
|
1714
|
+
discovery={data.config.discovery}
|
|
1715
|
+
siteUrl={data.config.site}
|
|
1716
|
+
pageType={frontmatter.type}
|
|
1717
|
+
published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
|
|
1718
|
+
lastModified={lastModified}
|
|
1719
|
+
noindex={effectiveNoindex}
|
|
1720
|
+
structuredDataEnabled={data.config.structuredData}
|
|
1721
|
+
>
|
|
1722
|
+
<h1>{title}</h1>
|
|
1723
|
+
{frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
|
|
1724
|
+
<LocaleLinks locale={locale}>
|
|
1725
|
+
<Content components={components} />
|
|
1726
|
+
</LocaleLinks>
|
|
1727
|
+
</LayoutComponent>
|
|
1728
|
+
`;
|
|
1729
|
+
};
|
|
1730
|
+
var changelogIndexTemplate = (options) => {
|
|
1731
|
+
const clientData = options.needsReact ? `
|
|
1732
|
+
clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: pageTitle } }}` : "";
|
|
1733
|
+
const stagedSpread = options.staged ? `
|
|
1734
|
+
...(await getCollection("staged")),` : "";
|
|
1735
|
+
return `---
|
|
1736
|
+
// Generated by Blume. Do not edit.
|
|
1737
|
+
import { getCollection, render } from "astro:content";
|
|
1738
|
+
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1739
|
+
import Update from "blume/components/content/Update.astro";
|
|
1740
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1741
|
+
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1742
|
+
import { resolveDateFormatOptions } from "blume/core/date-format.ts";
|
|
1743
|
+
import { layoutOverrides } from "../generated/components.ts";
|
|
1744
|
+
import data from "blume:data";
|
|
1745
|
+
|
|
1746
|
+
export const prerender = true;
|
|
1747
|
+
|
|
1748
|
+
const entryDate = (entry: {
|
|
1749
|
+
data: { date?: string | null; changelog?: { date?: string | null } | null };
|
|
1750
|
+
}) => entry.data.date ?? entry.data.changelog?.date ?? null;
|
|
1751
|
+
|
|
1752
|
+
const toTime = (value: string | null | undefined) => {
|
|
1753
|
+
if (!value) {
|
|
1754
|
+
return 0;
|
|
1755
|
+
}
|
|
1756
|
+
const date = new Date(value);
|
|
1757
|
+
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
// The changelog is an unlocalized route, so its chrome renders in the default
|
|
1761
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1762
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
1763
|
+
const i18n = data.config.i18n;
|
|
1764
|
+
const localeMeta = i18n
|
|
1765
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1766
|
+
: null;
|
|
1767
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1768
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1769
|
+
|
|
1770
|
+
// Formatted in the same locale as the chrome, and with the configured
|
|
1771
|
+
// \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
|
|
1772
|
+
const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
|
|
1773
|
+
const formatDate = (value: string | null | undefined) => {
|
|
1774
|
+
if (!value) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const date = new Date(value);
|
|
1778
|
+
return Number.isNaN(date.getTime())
|
|
1779
|
+
? undefined
|
|
1780
|
+
: new Intl.DateTimeFormat(htmlLang, dateFormatOptions).format(date);
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
const slugify = (text: string) =>
|
|
1784
|
+
text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
|
|
1785
|
+
"update";
|
|
1786
|
+
|
|
1787
|
+
// The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
|
|
1788
|
+
// null when there is no full major.minor.patch to key on. Drives the changelog's
|
|
1789
|
+
// group-by-major pagination, so it tolerates the scoped tags monorepos publish.
|
|
1790
|
+
const majorVersion = (version: string | null | undefined) => {
|
|
1791
|
+
const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
|
|
1792
|
+
return match ? Number(match[1]) : null;
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// Map each entry to its own generated page so the timeline heading can deep-link
|
|
1796
|
+
// to it. The collection entry id matches the route manifest's \`entryId\`.
|
|
1797
|
+
const routeByEntry = new Map(
|
|
1798
|
+
data.routes.map((route) => [route.entryId, route.path])
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
const changelogEntries = [
|
|
1802
|
+
...(await getCollection("docs")),${stagedSpread}
|
|
1803
|
+
]
|
|
1804
|
+
.filter(
|
|
1805
|
+
(entry) =>
|
|
1806
|
+
entry.data.type === "changelog" &&
|
|
1807
|
+
!entry.data.draft &&
|
|
1808
|
+
!entry.data.sidebar?.hidden
|
|
1809
|
+
)
|
|
1810
|
+
.toSorted((a, b) => toTime(entryDate(b)) - toTime(entryDate(a)));
|
|
1811
|
+
|
|
1812
|
+
const items = await Promise.all(
|
|
1813
|
+
changelogEntries.map(async (entry) => {
|
|
1814
|
+
const label =
|
|
1815
|
+
entry.data.title ??
|
|
1816
|
+
(entry.data.changelog?.version
|
|
1817
|
+
? "v" + entry.data.changelog.version
|
|
1818
|
+
: "Update");
|
|
1819
|
+
return {
|
|
1820
|
+
Content: (await render(entry)).Content,
|
|
1821
|
+
date: formatDate(entryDate(entry)),
|
|
1822
|
+
href: routeByEntry.get(entry.id) ?? undefined,
|
|
1823
|
+
id: slugify(label),
|
|
1824
|
+
label,
|
|
1825
|
+
major: majorVersion(entry.data.changelog?.version),
|
|
1826
|
+
tags: entry.data.changelog?.category
|
|
1827
|
+
? [entry.data.changelog.category]
|
|
1828
|
+
: [],
|
|
1829
|
+
};
|
|
1830
|
+
})
|
|
1831
|
+
);
|
|
1832
|
+
|
|
1833
|
+
// Repeated labels slug to the same id (e.g. two entries with neither a title
|
|
1834
|
+
// nor a version both falling back to "update"); suffix the later ones -2, -3,
|
|
1835
|
+
// ... so every heading deep-links to its own entry. The first keeps the plain
|
|
1836
|
+
// slug, and the rendered ids stay in lockstep with the \`headings\` list below.
|
|
1837
|
+
const seenIds = new Set();
|
|
1838
|
+
for (const item of items) {
|
|
1839
|
+
let uniqueId = item.id;
|
|
1840
|
+
for (let n = 2; seenIds.has(uniqueId); n += 1) {
|
|
1841
|
+
uniqueId = item.id + "-" + n;
|
|
1842
|
+
}
|
|
1843
|
+
seenIds.add(uniqueId);
|
|
1844
|
+
item.id = uniqueId;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// A changelog is semver-paginated only when every visible release parses as
|
|
1848
|
+
// semver and they span more than one major line. Older majors then collapse
|
|
1849
|
+
// into groups the reader reveals one at a time; otherwise the timeline is flat.
|
|
1850
|
+
const majors = items.every((item) => item.major !== null)
|
|
1851
|
+
? [...new Set(items.map((item) => item.major))]
|
|
1852
|
+
.filter((major): major is number => major !== null)
|
|
1853
|
+
.toSorted((a, b) => b - a)
|
|
1854
|
+
: [];
|
|
1855
|
+
const paginate = majors.length > 1;
|
|
1856
|
+
const majorGroups = majors.map((major) => ({
|
|
1857
|
+
items: items.filter((item) => item.major === major),
|
|
1858
|
+
label: major + ".x",
|
|
1859
|
+
major,
|
|
1860
|
+
}));
|
|
1861
|
+
|
|
1862
|
+
const headings = items.map((item) => ({
|
|
1863
|
+
depth: 2,
|
|
1864
|
+
slug: item.id,
|
|
1865
|
+
text: item.label,
|
|
1866
|
+
}));
|
|
1867
|
+
|
|
1868
|
+
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
1869
|
+
// The canonical URL carries the deployment base (the page is served under it),
|
|
1870
|
+
// matching how the catch-all canonicalizes via \`withBase(route)\`.
|
|
1871
|
+
const basedRoute = withBase("/changelog");
|
|
1872
|
+
const canonical = base ? base + basedRoute : null;
|
|
1873
|
+
|
|
1874
|
+
// The generated OG card for this route (the /og endpoint emits it alongside
|
|
1875
|
+
// the content-route cards), absolutized like the catch-all's so crawlers get
|
|
1876
|
+
// a full URL when the site is known.
|
|
1877
|
+
const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
|
|
1878
|
+
const ogImage = ogPath && base ? base + ogPath : ogPath;
|
|
1879
|
+
|
|
1880
|
+
// The page chrome (h1, title, description) comes from the same translatable
|
|
1881
|
+
// \`changelog\` group as the reveal button; optional chaining tolerates a
|
|
1882
|
+
// not-yet-regenerated data snapshot from before these keys existed.
|
|
1883
|
+
const changelogTitle = data.ui.changelog?.title ?? "Changelog";
|
|
1884
|
+
const changelogDescription =
|
|
1885
|
+
data.ui.changelog?.description ??
|
|
1886
|
+
"Product updates, new features, and fixes from every release.";
|
|
1887
|
+
// The layout suffixes "- {site title}" itself, so the page title is just the
|
|
1888
|
+
// changelog's own name — prefixing the site title too would double it
|
|
1889
|
+
// ("Acme Changelog - Acme").
|
|
1890
|
+
const pageTitle = changelogTitle;
|
|
1891
|
+
|
|
1892
|
+
const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
1893
|
+
---
|
|
1894
|
+
|
|
1895
|
+
<LayoutComponent
|
|
1896
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
1897
|
+
layout={layoutOverrides}${clientData}
|
|
1898
|
+
logo={data.config.logo}
|
|
1899
|
+
mcp={data.config.mcp}
|
|
1900
|
+
favicon={data.config.favicon}
|
|
1901
|
+
appleIcon={data.config.appleIcon}
|
|
1902
|
+
banner={data.config.banner}
|
|
1903
|
+
analytics={data.config.analytics}
|
|
1904
|
+
imageZoom={data.config.imageZoom}
|
|
1905
|
+
codeWrap={data.config.codeWrap}
|
|
1906
|
+
navigation={data.navigation}
|
|
1907
|
+
locale={htmlLang}
|
|
1908
|
+
dir={dir}
|
|
1909
|
+
ui={data.ui}
|
|
1910
|
+
page={{
|
|
1911
|
+
title: pageTitle,
|
|
1912
|
+
description: changelogDescription,
|
|
1913
|
+
route: "/changelog",
|
|
1914
|
+
}}
|
|
1915
|
+
headings={headings}
|
|
1916
|
+
toc={data.config.toc}
|
|
1917
|
+
contentLayout="bare"
|
|
1918
|
+
themeMode={data.config.theme.mode}
|
|
1919
|
+
fontCssVars={data.fontCssVars}
|
|
1920
|
+
searchEnabled={data.config.search.enabled}
|
|
1921
|
+
indexable={true}
|
|
1922
|
+
ogImage={ogImage}
|
|
1923
|
+
ogGenerated={Boolean(ogImage)}
|
|
1924
|
+
x={data.config.x}
|
|
1925
|
+
canonical={canonical}
|
|
1926
|
+
exportPdf={${options.exportPdf}}
|
|
1927
|
+
exportEpub={${options.exportEpub}}
|
|
1928
|
+
openInChat={data.config.openInChat}
|
|
1929
|
+
feeds={data.feeds}
|
|
1930
|
+
discovery={data.config.discovery}
|
|
1931
|
+
siteUrl={data.config.site}
|
|
1932
|
+
noindex={false}
|
|
1933
|
+
structuredDataEnabled={data.config.structuredData}
|
|
1934
|
+
>
|
|
1935
|
+
<h1>{changelogTitle}</h1>
|
|
1936
|
+
{
|
|
1937
|
+
items.length === 0 ? (
|
|
1938
|
+
<p>No changelog entries yet.</p>
|
|
1939
|
+
) : paginate ? (
|
|
1940
|
+
<blume-changelog
|
|
1941
|
+
class="not-prose mt-8 block"
|
|
1942
|
+
data-i18n-more={data.ui.changelog?.showReleases}
|
|
1943
|
+
>
|
|
1944
|
+
{majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
|
|
1945
|
+
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
1946
|
+
<Content />
|
|
1947
|
+
</Update>
|
|
1948
|
+
))}
|
|
1949
|
+
{majorGroups.slice(1).map((group) => (
|
|
1950
|
+
<section
|
|
1951
|
+
aria-label={group.label + " releases"}
|
|
1952
|
+
data-changelog-label={group.label}
|
|
1953
|
+
data-changelog-major={group.major}
|
|
1954
|
+
>
|
|
1955
|
+
{group.items.map(({ Content, href, id, label, date, tags }) => (
|
|
1956
|
+
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
1957
|
+
<Content />
|
|
1958
|
+
</Update>
|
|
1959
|
+
))}
|
|
1960
|
+
</section>
|
|
1961
|
+
))}
|
|
1962
|
+
<div class="mt-10 flex justify-center">
|
|
1963
|
+
<button
|
|
1964
|
+
class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
|
|
1965
|
+
data-changelog-more
|
|
1966
|
+
hidden
|
|
1967
|
+
type="button"
|
|
1968
|
+
>
|
|
1969
|
+
Show older releases
|
|
1970
|
+
</button>
|
|
1971
|
+
</div>
|
|
1972
|
+
</blume-changelog>
|
|
1973
|
+
) : (
|
|
1974
|
+
<div class="not-prose mt-8">
|
|
1975
|
+
{items.map(({ Content, href, id, label, date, tags }) => (
|
|
1976
|
+
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
1977
|
+
<Content />
|
|
1978
|
+
</Update>
|
|
1979
|
+
))}
|
|
1980
|
+
</div>
|
|
1981
|
+
)
|
|
1982
|
+
}
|
|
1983
|
+
<script>
|
|
1984
|
+
import "blume/components/content/changelog-element.ts";
|
|
1985
|
+
</script>
|
|
1986
|
+
</LayoutComponent>
|
|
1987
|
+
`;
|
|
1988
|
+
};
|
|
1989
|
+
var notFoundPageTemplate = () => `---
|
|
1990
|
+
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1991
|
+
import Icon from "blume/components/Icon.astro";
|
|
1992
|
+
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1993
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1994
|
+
import data from "blume:data";
|
|
1995
|
+
|
|
1996
|
+
export const prerender = true;
|
|
1997
|
+
|
|
1998
|
+
const nf = data.ui.notFound;
|
|
1999
|
+
|
|
2000
|
+
// The 404 page is an unlocalized route, so its chrome renders in the default
|
|
2001
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
2002
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
2003
|
+
const i18n = data.config.i18n;
|
|
2004
|
+
const localeMeta = i18n
|
|
2005
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
2006
|
+
: null;
|
|
2007
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
2008
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
2009
|
+
|
|
2010
|
+
// Recovery links, so a reader — or an agent that followed a stale URL — can
|
|
2011
|
+
// get back on track without guessing: every top-level section, then the
|
|
2012
|
+
// machine-readable indexes the build emits (the sitemap only exists with a
|
|
2013
|
+
// \`deployment.site\`; llms.txt only when \`ai.llmsTxt\` is on). Tabs link to
|
|
2014
|
+
// their resolved target when the section has no index page of its own.
|
|
2015
|
+
const suggestions = [
|
|
2016
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2017
|
+
href: withBase(tab.href ?? tab.path),
|
|
2018
|
+
label: tab.label,
|
|
2019
|
+
})),
|
|
2020
|
+
...(data.config.discovery.sitemap
|
|
2021
|
+
? [{ href: withBase("/sitemap.xml"), label: nf.sitemap }]
|
|
2022
|
+
: []),
|
|
2023
|
+
...(data.config.discovery.llmsTxt
|
|
2024
|
+
? [{ href: withBase("/llms.txt"), label: nf.llms }]
|
|
2025
|
+
: []),
|
|
2026
|
+
];
|
|
2027
|
+
---
|
|
2028
|
+
|
|
2029
|
+
<PageLayout
|
|
2030
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
2031
|
+
logo={data.config.logo}
|
|
2032
|
+
favicon={data.config.favicon}
|
|
2033
|
+
appleIcon={data.config.appleIcon}
|
|
2034
|
+
banner={data.config.banner}
|
|
2035
|
+
analytics={data.config.analytics}
|
|
2036
|
+
navigation={data.navigation}
|
|
2037
|
+
page={{ title: nf.title, route: "/404" }}
|
|
2038
|
+
themeMode={data.config.theme.mode}
|
|
2039
|
+
fontCssVars={data.fontCssVars}
|
|
2040
|
+
searchEnabled={data.config.search.enabled}
|
|
2041
|
+
locale={htmlLang}
|
|
2042
|
+
dir={dir}
|
|
2043
|
+
ui={data.ui}
|
|
2044
|
+
noindex={true}
|
|
2045
|
+
>
|
|
2046
|
+
<div
|
|
2047
|
+
class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
|
|
2048
|
+
>
|
|
2049
|
+
<div class="flex flex-col items-start">
|
|
2050
|
+
<p
|
|
2051
|
+
class="font-mono text-xs font-medium tracking-widest text-muted-foreground"
|
|
2052
|
+
>
|
|
2053
|
+
404
|
|
2054
|
+
</p>
|
|
2055
|
+
<h1
|
|
2056
|
+
class="mt-4 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl"
|
|
2057
|
+
>
|
|
2058
|
+
{nf.title}
|
|
2059
|
+
</h1>
|
|
2060
|
+
<p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground">
|
|
2061
|
+
{nf.description}
|
|
2062
|
+
</p>
|
|
2063
|
+
<a
|
|
2064
|
+
class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
|
|
2065
|
+
href={withBase("/")}
|
|
2066
|
+
>
|
|
2067
|
+
<Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
|
|
2068
|
+
{nf.home}
|
|
2069
|
+
</a>
|
|
2070
|
+
</div>
|
|
2071
|
+
{
|
|
2072
|
+
suggestions.length > 0 && (
|
|
2073
|
+
<nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16">
|
|
2074
|
+
<h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground">
|
|
2075
|
+
{nf.suggestions}
|
|
2076
|
+
</h2>
|
|
2077
|
+
<ul class="mt-4 divide-y divide-border border-y border-border">
|
|
2078
|
+
{suggestions.map((link) => (
|
|
2079
|
+
<li>
|
|
2080
|
+
<a
|
|
2081
|
+
class="group flex items-center justify-between gap-4 py-3 text-sm font-medium text-foreground transition-colors hover:text-accent"
|
|
2082
|
+
href={link.href}
|
|
2083
|
+
>
|
|
2084
|
+
<span>{link.label}</span>
|
|
2085
|
+
<Icon
|
|
2086
|
+
class="shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100 rtl:group-hover:-translate-x-0.5"
|
|
2087
|
+
name="arrow-right"
|
|
2088
|
+
size={14}
|
|
2089
|
+
/>
|
|
2090
|
+
</a>
|
|
2091
|
+
</li>
|
|
2092
|
+
))}
|
|
2093
|
+
</ul>
|
|
2094
|
+
</nav>
|
|
2095
|
+
)
|
|
2096
|
+
}
|
|
2097
|
+
</div>
|
|
2098
|
+
</PageLayout>
|
|
2099
|
+
`;
|
|
2100
|
+
var notFoundMarkdownTemplate = () => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
2101
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2102
|
+
import { absoluteUrl } from "blume/core/site-url.ts";
|
|
2103
|
+
import data from "blume:data";
|
|
2104
|
+
|
|
2105
|
+
export const prerender = true;
|
|
2106
|
+
|
|
2107
|
+
const nf = data.ui.notFound;
|
|
2108
|
+
|
|
2109
|
+
// Absolute for internal routes when the site is known; an external tab href
|
|
2110
|
+
// passes through untouched.
|
|
2111
|
+
const href = (path: string): string => {
|
|
2112
|
+
const based = withBase(path);
|
|
2113
|
+
return data.config.site && based.startsWith("/") && !based.startsWith("//")
|
|
2114
|
+
? absoluteUrl(data.config.site, based)
|
|
2115
|
+
: based;
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
// The recovery set of 404.astro: home, every top-level section (a tab links to
|
|
2119
|
+
// its resolved target), then the machine-readable indexes that exist.
|
|
2120
|
+
const links = [
|
|
2121
|
+
{ href: href("/"), label: nf.home },
|
|
2122
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2123
|
+
href: href(tab.href ?? tab.path),
|
|
2124
|
+
label: tab.label,
|
|
2125
|
+
})),
|
|
2126
|
+
...(data.config.discovery.sitemap
|
|
2127
|
+
? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
|
|
2128
|
+
: []),
|
|
2129
|
+
...(data.config.discovery.llmsTxt
|
|
2130
|
+
? [{ href: href("/llms.txt"), label: nf.llms }]
|
|
2131
|
+
: []),
|
|
2132
|
+
...(data.config.discovery.api
|
|
2133
|
+
? [{ href: href("/openapi.json"), label: nf.api }]
|
|
2134
|
+
: []),
|
|
2135
|
+
];
|
|
2136
|
+
|
|
2137
|
+
const body = [
|
|
2138
|
+
"# " + nf.title,
|
|
2139
|
+
"",
|
|
2140
|
+
nf.description,
|
|
2141
|
+
"",
|
|
2142
|
+
"## " + nf.suggestions,
|
|
2143
|
+
"",
|
|
2144
|
+
...links.map((link) => "- [" + link.label + "](" + link.href + ")"),
|
|
2145
|
+
"",
|
|
2146
|
+
].join("\\n");
|
|
2147
|
+
|
|
2148
|
+
export function GET() {
|
|
2149
|
+
return new Response(body, {
|
|
2150
|
+
headers: {
|
|
2151
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
2152
|
+
// ~4 characters per token; keep in sync with markdownTokenCount.
|
|
2153
|
+
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
|
|
2154
|
+
},
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
`;
|
|
2158
|
+
var notFoundJsonTemplate = () => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
2159
|
+
import { problem } from "blume/ai/api/problem.ts";
|
|
2160
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2161
|
+
import { absoluteUrl } from "blume/core/site-url.ts";
|
|
2162
|
+
import data from "blume:data";
|
|
2163
|
+
|
|
2164
|
+
export const prerender = true;
|
|
2165
|
+
|
|
2166
|
+
const nf = data.ui.notFound;
|
|
2167
|
+
|
|
2168
|
+
// Absolute for internal routes when the site is known; an external tab href
|
|
2169
|
+
// passes through untouched.
|
|
2170
|
+
const href = (path: string): string => {
|
|
2171
|
+
const based = withBase(path);
|
|
2172
|
+
return data.config.site && based.startsWith("/") && !based.startsWith("//")
|
|
2173
|
+
? absoluteUrl(data.config.site, based)
|
|
2174
|
+
: based;
|
|
2175
|
+
};
|
|
2176
|
+
|
|
2177
|
+
// The recovery set of 404.astro: home, every top-level section (a tab links to
|
|
2178
|
+
// its resolved target), then the machine-readable indexes that exist.
|
|
2179
|
+
const links = [
|
|
2180
|
+
{ href: href("/"), label: nf.home },
|
|
2181
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2182
|
+
href: href(tab.href ?? tab.path),
|
|
2183
|
+
label: tab.label,
|
|
2184
|
+
})),
|
|
2185
|
+
...(data.config.discovery.sitemap
|
|
2186
|
+
? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
|
|
2187
|
+
: []),
|
|
2188
|
+
...(data.config.discovery.llmsTxt
|
|
2189
|
+
? [{ href: href("/llms.txt"), label: nf.llms }]
|
|
2190
|
+
: []),
|
|
2191
|
+
...(data.config.discovery.api
|
|
2192
|
+
? [{ href: href("/openapi.json"), label: nf.api }]
|
|
2193
|
+
: []),
|
|
2194
|
+
];
|
|
2195
|
+
|
|
2196
|
+
const body = problem({
|
|
2197
|
+
code: "PAGE_NOT_FOUND",
|
|
2198
|
+
detail: nf.description,
|
|
2199
|
+
links,
|
|
2200
|
+
resolution: nf.suggestions + ": " + links.map((link) => link.href).join(", "),
|
|
2201
|
+
status: 404,
|
|
2202
|
+
title: nf.title,
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
export function GET() {
|
|
2206
|
+
return new Response(JSON.stringify(body, null, 2) + "\\n", {
|
|
2207
|
+
headers: { "Content-Type": "application/problem+json; charset=utf-8" },
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
`;
|
|
2211
|
+
var apiPagesIndexTemplate = () => `// Generated by Blume. Do not edit.
|
|
2212
|
+
import { pagesIndexResponse } from "blume/ai/api/handlers.ts";
|
|
2213
|
+
import data from "blume:mcp-data";
|
|
2214
|
+
|
|
2215
|
+
export const prerender = true;
|
|
2216
|
+
|
|
2217
|
+
export function GET() {
|
|
2218
|
+
return pagesIndexResponse(data);
|
|
2219
|
+
}
|
|
2220
|
+
`;
|
|
2221
|
+
var apiPageTemplate = () => `// Generated by Blume. Do not edit.
|
|
2222
|
+
import { pageParams, pageResponse } from "blume/ai/api/handlers.ts";
|
|
2223
|
+
import data from "blume:mcp-data";
|
|
2224
|
+
|
|
2225
|
+
export const prerender = true;
|
|
2226
|
+
|
|
2227
|
+
export function getStaticPaths() {
|
|
2228
|
+
return pageParams(data);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
export function GET({ props }: { props: { route: string } }) {
|
|
2232
|
+
return pageResponse(data, props.route);
|
|
2233
|
+
}
|
|
2234
|
+
`;
|
|
2235
|
+
var apiNavigationTemplate = () => `// Generated by Blume. Do not edit.
|
|
2236
|
+
import { navigationResponse } from "blume/ai/api/handlers.ts";
|
|
2237
|
+
import data from "blume:mcp-data";
|
|
2238
|
+
|
|
2239
|
+
export const prerender = true;
|
|
2240
|
+
|
|
2241
|
+
export function GET() {
|
|
2242
|
+
return navigationResponse(data);
|
|
2243
|
+
}
|
|
2244
|
+
`;
|
|
2245
|
+
var apiSearchTemplate = () => `// Generated by Blume. Do not edit.
|
|
2246
|
+
import type { APIRoute } from "astro";
|
|
2247
|
+
import { createSearchHandler } from "blume/ai/api/handlers.ts";
|
|
2248
|
+
import data from "blume:mcp-data";
|
|
2249
|
+
|
|
2250
|
+
export const prerender = false;
|
|
2251
|
+
|
|
2252
|
+
const handler = createSearchHandler(data);
|
|
2253
|
+
|
|
2254
|
+
export const GET: APIRoute = ({ request }) => handler(request);
|
|
2255
|
+
`;
|
|
2256
|
+
var apiNotFoundTemplate = (context) => `// Generated by Blume. Do not edit.
|
|
2257
|
+
import type { APIRoute } from "astro";
|
|
2258
|
+
import { apiNotFoundResponse } from "blume/ai/api/handlers.ts";
|
|
2259
|
+
|
|
2260
|
+
export const prerender = false;
|
|
2261
|
+
|
|
2262
|
+
const context = ${JSON.stringify(context)};
|
|
2263
|
+
|
|
2264
|
+
export const ALL: APIRoute = ({ request }) => apiNotFoundResponse(request, context);
|
|
2265
|
+
`;
|
|
2266
|
+
var islandDirective = (spec) => spec.client === "only" ? `client:only="${spec.framework}"` : `client:${spec.client}`;
|
|
2267
|
+
var wrapperPropsType = (name) => `type Props = typeof ${name} extends (
|
|
2268
|
+
props: infer P extends object,
|
|
2269
|
+
...rest: never[]
|
|
2270
|
+
) => unknown
|
|
2271
|
+
? P
|
|
2272
|
+
: Record<string, unknown>;`;
|
|
2273
|
+
var islandWrapperTemplate = (spec) => `---
|
|
2274
|
+
// Generated by Blume. Do not edit.
|
|
2275
|
+
import Island from ${JSON.stringify(spec.file)};
|
|
2276
|
+
${wrapperPropsType("Island")}
|
|
2277
|
+
---
|
|
2278
|
+
<Island ${islandDirective(spec)} {...Astro.props}><slot /></Island>
|
|
2279
|
+
`;
|
|
2280
|
+
var islandMapTemplate = (specs) => {
|
|
2281
|
+
if (specs.length === 0) {
|
|
2282
|
+
return `// Generated by Blume. Do not edit.
|
|
2283
|
+
export const islandComponents = {};
|
|
2284
|
+
`;
|
|
2285
|
+
}
|
|
2286
|
+
const imports = specs.map((spec, index) => `import I${index} from "./islands/${spec.name}.astro";`).join(`
|
|
2287
|
+
`);
|
|
2288
|
+
const entries = specs.map((spec, index) => ` ${spec.name}: I${index},`).join(`
|
|
2289
|
+
`);
|
|
2290
|
+
return `// Generated by Blume. Do not edit.
|
|
2291
|
+
${imports}
|
|
2292
|
+
export const islandComponents = {
|
|
2293
|
+
${entries}
|
|
2294
|
+
};
|
|
2295
|
+
`;
|
|
2296
|
+
};
|
|
2297
|
+
var exampleDirective = (spec) => {
|
|
2298
|
+
if (spec.framework === "astro" || !spec.client) {
|
|
2299
|
+
return "";
|
|
2300
|
+
}
|
|
2301
|
+
return spec.client === "only" ? `client:only="${spec.framework}" ` : `client:${spec.client} `;
|
|
2302
|
+
};
|
|
2303
|
+
var exampleSlug = (path) => path.replaceAll(/[^a-zA-Z0-9]/gu, (char) => `_${(char.codePointAt(0) ?? 0).toString(16)}_`);
|
|
2304
|
+
var exampleWrapperTemplate = (spec) => `---
|
|
2305
|
+
// Generated by Blume. Do not edit.
|
|
2306
|
+
import Example from ${JSON.stringify(spec.file)};
|
|
2307
|
+
${wrapperPropsType("Example")}
|
|
2308
|
+
---
|
|
2309
|
+
<Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
|
|
2310
|
+
`;
|
|
2311
|
+
var examplesRouteBase = (basePath) => `${basePath}/blume-examples`;
|
|
2312
|
+
var exampleMapTemplate = (specs, basePath) => {
|
|
2313
|
+
const base = `export const examplesBase = ${JSON.stringify(examplesRouteBase(basePath))};`;
|
|
2314
|
+
if (specs.length === 0) {
|
|
2315
|
+
return `// Generated by Blume. Do not edit.
|
|
2316
|
+
${base}
|
|
2317
|
+
export const examples = {};
|
|
2318
|
+
`;
|
|
2319
|
+
}
|
|
2320
|
+
const imports = specs.map((spec, index) => `import E${index} from "./examples/${exampleSlug(spec.path)}.astro";`).join(`
|
|
2321
|
+
`);
|
|
2322
|
+
const entries = specs.map((spec, index) => ` ${JSON.stringify(spec.path)}: { Component: E${index}, code: ${JSON.stringify(spec.source)}, lang: ${JSON.stringify(spec.lang)} },`).join(`
|
|
2323
|
+
`);
|
|
2324
|
+
return `// Generated by Blume. Do not edit.
|
|
2325
|
+
${imports}
|
|
2326
|
+
${base}
|
|
2327
|
+
export const examples = {
|
|
2328
|
+
${entries}
|
|
2329
|
+
};
|
|
2330
|
+
`;
|
|
2331
|
+
};
|
|
2332
|
+
var examplesPageTemplate = () => `---
|
|
2333
|
+
// Generated by Blume. Do not edit.
|
|
2334
|
+
import { examples } from "blume:examples";
|
|
2335
|
+
import "blume:examples-theme";
|
|
2336
|
+
|
|
2337
|
+
// Prerendered even in server output, like docs content.
|
|
2338
|
+
export const prerender = true;
|
|
2339
|
+
|
|
2340
|
+
export const getStaticPaths = () =>
|
|
2341
|
+
Object.keys(examples).map((path) => ({ params: { path } }));
|
|
2342
|
+
|
|
2343
|
+
const { path } = Astro.params;
|
|
2344
|
+
const entry = path ? examples[path] : undefined;
|
|
2345
|
+
if (!entry) {
|
|
2346
|
+
return new Response(null, { status: 404 });
|
|
2347
|
+
}
|
|
2348
|
+
const Example = entry.Component;
|
|
2349
|
+
---
|
|
2350
|
+
|
|
2351
|
+
<html lang="en">
|
|
2352
|
+
<head>
|
|
2353
|
+
<meta charset="utf-8" />
|
|
2354
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2355
|
+
<meta name="robots" content="noindex" />
|
|
2356
|
+
<title>{path}</title>
|
|
2357
|
+
<script is:inline>
|
|
2358
|
+
(() => {
|
|
2359
|
+
const root = document.documentElement;
|
|
2360
|
+
const apply = (theme) => {
|
|
2361
|
+
root.dataset.theme = theme;
|
|
2362
|
+
root.classList.toggle("dark", theme === "dark");
|
|
2363
|
+
};
|
|
2364
|
+
const stored = () =>
|
|
2365
|
+
localStorage.getItem("blume-theme") ??
|
|
2366
|
+
(matchMedia("(prefers-color-scheme: dark)").matches
|
|
2367
|
+
? "dark"
|
|
2368
|
+
: "light");
|
|
2369
|
+
try {
|
|
2370
|
+
const host = window.parent.document.documentElement;
|
|
2371
|
+
apply(host.dataset.theme ?? stored());
|
|
2372
|
+
new MutationObserver(() => {
|
|
2373
|
+
apply(host.dataset.theme ?? stored());
|
|
2374
|
+
}).observe(host, { attributeFilter: ["data-theme"] });
|
|
2375
|
+
} catch {
|
|
2376
|
+
apply(stored());
|
|
2377
|
+
}
|
|
2378
|
+
})();
|
|
2379
|
+
</script>
|
|
2380
|
+
</head>
|
|
2381
|
+
<!-- Flex + margin:auto centers the example and, unlike place-items, keeps
|
|
2382
|
+
the top edge reachable when the example outgrows the frame. -->
|
|
2383
|
+
<body style="display:flex;min-height:100svh;padding:1.5rem">
|
|
2384
|
+
<div data-blume-example style="margin:auto"><Example /></div>
|
|
2385
|
+
<script is:inline>
|
|
2386
|
+
(() => {
|
|
2387
|
+
// Report the example's rendered height so the embedding docs page can
|
|
2388
|
+
// size the preview pane to the content. The wrapper is observed rather
|
|
2389
|
+
// than the body: the body stretches to the frame's own height, so it
|
|
2390
|
+
// would only echo the pane back. Direct opens have no distinct parent
|
|
2391
|
+
// and skip out; the frame is same-origin with the docs page (see the
|
|
2392
|
+
// theme sync above), so the origin is pinned on both ends.
|
|
2393
|
+
if (window.parent === window) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const wrapper = document.querySelector("[data-blume-example]");
|
|
2397
|
+
if (!wrapper) {
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
// The body's padding frames the example; fold it into the report so
|
|
2401
|
+
// the parent can apply the number as-is. Read from the live value —
|
|
2402
|
+
// the user's examples.css is injected after Blume's defaults precisely
|
|
2403
|
+
// so their tokens win, so a root font-size override must be honored
|
|
2404
|
+
// rather than assuming 1.5rem is 48px.
|
|
2405
|
+
const bodyStyle = getComputedStyle(document.body);
|
|
2406
|
+
const paddingPx =
|
|
2407
|
+
parseFloat(bodyStyle.paddingTop) + parseFloat(bodyStyle.paddingBottom);
|
|
2408
|
+
const report = () => {
|
|
2409
|
+
window.parent.postMessage(
|
|
2410
|
+
{
|
|
2411
|
+
height:
|
|
2412
|
+
Math.ceil(wrapper.getBoundingClientRect().height) + paddingPx,
|
|
2413
|
+
type: "blume:example-height",
|
|
2414
|
+
},
|
|
2415
|
+
window.location.origin
|
|
2416
|
+
);
|
|
2417
|
+
};
|
|
2418
|
+
new ResizeObserver(report).observe(wrapper);
|
|
2419
|
+
// The parent asks for a fresh report when its listener comes up, in
|
|
2420
|
+
// case the first one above was posted before anyone was listening.
|
|
2421
|
+
window.addEventListener("message", (event) => {
|
|
2422
|
+
if (
|
|
2423
|
+
event.source === window.parent &&
|
|
2424
|
+
event.origin === window.location.origin &&
|
|
2425
|
+
event.data?.type === "blume:example-height-request"
|
|
2426
|
+
) {
|
|
2427
|
+
report();
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
})();
|
|
2431
|
+
</script>
|
|
2432
|
+
</body>
|
|
2433
|
+
</html>
|
|
2434
|
+
`;
|
|
2435
|
+
var runtimePackageTemplate = (dependencies = []) => `${JSON.stringify({
|
|
2436
|
+
dependencies: Object.fromEntries([...dependencies].toSorted().map((name) => [name, "*"])),
|
|
2437
|
+
name: "blume-runtime",
|
|
2438
|
+
private: true,
|
|
2439
|
+
type: "module",
|
|
2440
|
+
version: "0.0.0"
|
|
2441
|
+
}, null, 2)}
|
|
2442
|
+
`;
|
|
2443
|
+
var runtimeTsconfigTemplate = () => `${JSON.stringify({
|
|
2444
|
+
exclude: ["dist"],
|
|
2445
|
+
extends: "astro/tsconfigs/strict",
|
|
2446
|
+
include: [".astro/types.d.ts", "**/*"]
|
|
2447
|
+
}, null, 2)}
|
|
2448
|
+
`;
|
|
2449
|
+
|
|
2450
|
+
export { ADAPTER_IGNORE_DIRS, servesClientSubdir, deployStaticDir, surfaceAdapterOutput, resolveAskBackend, askBackendRuntimeDep, buildHomeLinkHeader, platformRedirects, publishRuntimeModules, runtimeDependencies, astroConfigTemplate, stagedContentDir, contentConfigTemplate, askEndpointTemplate, askComponentTemplate, searchEndpointTemplate, featuresTemplate, searchClientTemplate, mixedbreadSearchEndpointTemplate, rawMarkdownEndpointTemplate, contentAssetsEndpointTemplate, mcpPageFile, mcpEndpointTemplate, playgroundProxyTemplate, staticJsonEndpointTemplate, rssEndpointTemplate, navFragmentTemplate, ogEndpointTemplate, scalarReferenceTemplate, catchAllPageTemplate, changelogIndexTemplate, notFoundPageTemplate, notFoundMarkdownTemplate, notFoundJsonTemplate, apiPagesIndexTemplate, apiPageTemplate, apiNavigationTemplate, apiSearchTemplate, apiNotFoundTemplate, islandWrapperTemplate, islandMapTemplate, exampleSlug, exampleWrapperTemplate, examplesRouteBase, exampleMapTemplate, examplesPageTemplate, runtimePackageTemplate, runtimeTsconfigTemplate };
|
|
2451
|
+
|
|
2452
|
+
//# debugId=539E38867287181364756E2164756E21
|
|
2453
|
+
//# sourceMappingURL=chunk-agy5rzxy.js.map
|