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,4064 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildMcpData,
|
|
4
|
+
buildRawMarkdown,
|
|
5
|
+
buildRssFeeds,
|
|
6
|
+
buildSearchDocuments,
|
|
7
|
+
markdownRoutePaths,
|
|
8
|
+
renderRssFeed
|
|
9
|
+
} from "./chunk-ynacq3ev.js";
|
|
10
|
+
import {
|
|
11
|
+
MCP_TOOLS
|
|
12
|
+
} from "./chunk-pxj10x8y.js";
|
|
13
|
+
import {
|
|
14
|
+
customOgRoutes,
|
|
15
|
+
discoverPages,
|
|
16
|
+
hasGeneratedChangelog,
|
|
17
|
+
routeIsTaken
|
|
18
|
+
} from "./chunk-sbdqrjbb.js";
|
|
19
|
+
import {
|
|
20
|
+
collectContentAssets,
|
|
21
|
+
rewriteRelativeImages
|
|
22
|
+
} from "./chunk-v5mm027v.js";
|
|
23
|
+
import {
|
|
24
|
+
writeTextAtomic
|
|
25
|
+
} from "./chunk-ev67ycx0.js";
|
|
26
|
+
import {
|
|
27
|
+
registry
|
|
28
|
+
} from "./chunk-3r94j3tc.js";
|
|
29
|
+
import {
|
|
30
|
+
apiNavigationTemplate,
|
|
31
|
+
apiNotFoundTemplate,
|
|
32
|
+
apiPageTemplate,
|
|
33
|
+
apiPagesIndexTemplate,
|
|
34
|
+
apiSearchTemplate,
|
|
35
|
+
askBackendRuntimeDep,
|
|
36
|
+
askComponentTemplate,
|
|
37
|
+
askEndpointTemplate,
|
|
38
|
+
astroConfigTemplate,
|
|
39
|
+
buildHomeLinkHeader,
|
|
40
|
+
catchAllPageTemplate,
|
|
41
|
+
changelogIndexTemplate,
|
|
42
|
+
contentAssetsEndpointTemplate,
|
|
43
|
+
contentConfigTemplate,
|
|
44
|
+
exampleMapTemplate,
|
|
45
|
+
exampleSlug,
|
|
46
|
+
exampleWrapperTemplate,
|
|
47
|
+
examplesPageTemplate,
|
|
48
|
+
featuresTemplate,
|
|
49
|
+
islandMapTemplate,
|
|
50
|
+
islandWrapperTemplate,
|
|
51
|
+
mcpEndpointTemplate,
|
|
52
|
+
mcpPageFile,
|
|
53
|
+
mixedbreadSearchEndpointTemplate,
|
|
54
|
+
navFragmentTemplate,
|
|
55
|
+
notFoundJsonTemplate,
|
|
56
|
+
notFoundMarkdownTemplate,
|
|
57
|
+
notFoundPageTemplate,
|
|
58
|
+
ogEndpointTemplate,
|
|
59
|
+
playgroundProxyTemplate,
|
|
60
|
+
publishRuntimeModules,
|
|
61
|
+
rawMarkdownEndpointTemplate,
|
|
62
|
+
resolveAskBackend,
|
|
63
|
+
rssEndpointTemplate,
|
|
64
|
+
runtimeDependencies,
|
|
65
|
+
runtimePackageTemplate,
|
|
66
|
+
runtimeTsconfigTemplate,
|
|
67
|
+
scalarReferenceTemplate,
|
|
68
|
+
searchClientTemplate,
|
|
69
|
+
searchEndpointTemplate,
|
|
70
|
+
stagedContentDir,
|
|
71
|
+
staticJsonEndpointTemplate
|
|
72
|
+
} from "./chunk-agy5rzxy.js";
|
|
73
|
+
import {
|
|
74
|
+
API_NAVIGATION_PATH,
|
|
75
|
+
API_PAGES_PATH,
|
|
76
|
+
API_PAGE_PATH,
|
|
77
|
+
API_SEARCH_PATH,
|
|
78
|
+
OPENAPI_PATH
|
|
79
|
+
} from "./chunk-j6pxe0dt.js";
|
|
80
|
+
import {
|
|
81
|
+
absoluteUrl,
|
|
82
|
+
siteRoot
|
|
83
|
+
} from "./chunk-vxv4x1n8.js";
|
|
84
|
+
import {
|
|
85
|
+
searchProviderMeta,
|
|
86
|
+
servesStaticIndex
|
|
87
|
+
} from "./chunk-wd27zjcz.js";
|
|
88
|
+
import {
|
|
89
|
+
apiNamePhrase,
|
|
90
|
+
buildIncludeGraph,
|
|
91
|
+
isOpenApiSource,
|
|
92
|
+
resolveDocsCollection,
|
|
93
|
+
resolveFallbackLocale,
|
|
94
|
+
validateNavTargets,
|
|
95
|
+
validateSearchPopularIcons
|
|
96
|
+
} from "./chunk-s102bysw.js";
|
|
97
|
+
import {
|
|
98
|
+
EN_UI,
|
|
99
|
+
GOOGLE_FONTS,
|
|
100
|
+
apiUrl,
|
|
101
|
+
buildFontsCss,
|
|
102
|
+
builtinProxyKinds,
|
|
103
|
+
configuredFonts,
|
|
104
|
+
editBaseUrl,
|
|
105
|
+
fontLocaleCodes,
|
|
106
|
+
hasScalarReferences,
|
|
107
|
+
isFontSlug,
|
|
108
|
+
needsPlaygroundProxy,
|
|
109
|
+
referenceRoutes,
|
|
110
|
+
repoUrl,
|
|
111
|
+
resolveReferences,
|
|
112
|
+
resolveUIStrings
|
|
113
|
+
} from "./chunk-xv91q4nm.js";
|
|
114
|
+
import {
|
|
115
|
+
normalizeBasePath,
|
|
116
|
+
trimChar,
|
|
117
|
+
withBasePath
|
|
118
|
+
} from "./chunk-27gtm2ym.js";
|
|
119
|
+
import {
|
|
120
|
+
getBlumeVersion
|
|
121
|
+
} from "./chunk-4xyggvgf.js";
|
|
122
|
+
import {
|
|
123
|
+
packageRoot
|
|
124
|
+
} from "./chunk-6kzzpsx8.js";
|
|
125
|
+
import {
|
|
126
|
+
BlumeError,
|
|
127
|
+
enrichDiagnostic
|
|
128
|
+
} from "./chunk-ey89bjj1.js";
|
|
129
|
+
|
|
130
|
+
// src/ai/web-bot-auth.ts
|
|
131
|
+
var SIGNATURES_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
|
|
132
|
+
var SIGNATURES_DIRECTORY_TYPE = "application/http-message-signatures-directory+json";
|
|
133
|
+
|
|
134
|
+
// src/astro/integration.ts
|
|
135
|
+
import { join as join3, relative, resolve as resolve2 } from "pathe";
|
|
136
|
+
|
|
137
|
+
// src/deploy/artifacts.ts
|
|
138
|
+
import { dirname, join as join2, resolve } from "pathe";
|
|
139
|
+
|
|
140
|
+
// src/search/build.ts
|
|
141
|
+
import { join } from "pathe";
|
|
142
|
+
|
|
143
|
+
// src/deploy/sitemap.ts
|
|
144
|
+
import { escape as escapeXml } from "html-escaper";
|
|
145
|
+
var ERROR_ROUTES = new Set(["/404", "/500"]);
|
|
146
|
+
|
|
147
|
+
// src/astro/integration.ts
|
|
148
|
+
var REGISTRY_KEY = Symbol.for("blume.integration");
|
|
149
|
+
var registry2 = () => {
|
|
150
|
+
const host = globalThis;
|
|
151
|
+
host[REGISTRY_KEY] ??= {
|
|
152
|
+
buildProject: null,
|
|
153
|
+
negotiation: null,
|
|
154
|
+
overlay: null,
|
|
155
|
+
refreshContent: null
|
|
156
|
+
};
|
|
157
|
+
return host[REGISTRY_KEY];
|
|
158
|
+
};
|
|
159
|
+
var publishBuildProject = (project) => {
|
|
160
|
+
registry2().buildProject = project;
|
|
161
|
+
};
|
|
162
|
+
var publishDevNegotiation = (negotiation) => {
|
|
163
|
+
registry2().negotiation = negotiation ? {
|
|
164
|
+
contentRoutes: new Set(negotiation.contentRoutes),
|
|
165
|
+
homeLinkHeader: negotiation.homeLinkHeader
|
|
166
|
+
} : null;
|
|
167
|
+
};
|
|
168
|
+
var refreshBlumeContent = async () => {
|
|
169
|
+
const { refreshContent } = registry2();
|
|
170
|
+
if (!refreshContent) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
await refreshContent({});
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
var overlayChannel = () => {
|
|
177
|
+
const { overlay } = registry2();
|
|
178
|
+
return overlay?.ws ?? overlay?.hot;
|
|
179
|
+
};
|
|
180
|
+
var showBlumeErrorOverlay = (diagnostics) => {
|
|
181
|
+
const errors = [];
|
|
182
|
+
for (const diagnostic of diagnostics) {
|
|
183
|
+
if (diagnostic.severity === "error") {
|
|
184
|
+
errors.push(enrichDiagnostic(diagnostic));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const channel = overlayChannel();
|
|
188
|
+
if (errors.length === 0 || !channel) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const body = errors.map((diagnostic) => {
|
|
192
|
+
const lineSuffix = diagnostic.line ? `:${diagnostic.line}` : "";
|
|
193
|
+
const where = diagnostic.file ? `
|
|
194
|
+
at ${diagnostic.file}${lineSuffix}` : "";
|
|
195
|
+
const fix = diagnostic.suggestion ? `
|
|
196
|
+
fix: ${diagnostic.suggestion}` : "";
|
|
197
|
+
const docs = diagnostic.docsUrl ? `
|
|
198
|
+
docs: ${diagnostic.docsUrl}` : "";
|
|
199
|
+
return `[${diagnostic.code}] ${diagnostic.message}${where}${fix}${docs}`;
|
|
200
|
+
}).join(`
|
|
201
|
+
|
|
202
|
+
`);
|
|
203
|
+
channel.send({
|
|
204
|
+
err: {
|
|
205
|
+
id: errors[0]?.file,
|
|
206
|
+
message: `Blume found ${errors.length} error(s):
|
|
207
|
+
|
|
208
|
+
${body}`,
|
|
209
|
+
plugin: "blume",
|
|
210
|
+
stack: ""
|
|
211
|
+
},
|
|
212
|
+
type: "error"
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/og/cache.ts
|
|
217
|
+
import { existsSync } from "node:fs";
|
|
218
|
+
import {
|
|
219
|
+
mkdir,
|
|
220
|
+
readdir,
|
|
221
|
+
readFile,
|
|
222
|
+
rename,
|
|
223
|
+
rm,
|
|
224
|
+
writeFile
|
|
225
|
+
} from "node:fs/promises";
|
|
226
|
+
import { dirname as dirname2, join as join4 } from "pathe";
|
|
227
|
+
var ogCacheDir = (context) => existsSync(join4(context.root, "node_modules")) ? join4(context.root, "node_modules", ".cache", "blume", "og") : join4(context.outDir, ".cache", "og");
|
|
228
|
+
var REGISTRY_KEY2 = Symbol.for("blume.og-cache");
|
|
229
|
+
var registry3 = () => {
|
|
230
|
+
const host = globalThis;
|
|
231
|
+
host[REGISTRY_KEY2] ??= { hits: 0, misses: 0, used: new Set };
|
|
232
|
+
return host[REGISTRY_KEY2];
|
|
233
|
+
};
|
|
234
|
+
var localFontDigests = new Map;
|
|
235
|
+
var inflight = new Map;
|
|
236
|
+
var pruneCardCache = async (dir) => {
|
|
237
|
+
const { used } = registry3();
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = await readdir(dir);
|
|
241
|
+
} catch {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
const stale = entries.filter((name) => name.endsWith(".tmp") || name.endsWith(".png") && !used.has(name.slice(0, -".png".length)));
|
|
245
|
+
await Promise.all(stale.map((name) => rm(join4(dir, name), { force: true })));
|
|
246
|
+
return stale.length;
|
|
247
|
+
};
|
|
248
|
+
var cardCacheTally = () => {
|
|
249
|
+
const { hits, misses } = registry3();
|
|
250
|
+
return hits + misses === 0 ? null : { hits, misses };
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/astro/generate.ts
|
|
254
|
+
import { createHash } from "node:crypto";
|
|
255
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
256
|
+
import {
|
|
257
|
+
lstat,
|
|
258
|
+
mkdir as mkdir2,
|
|
259
|
+
readFile as readFile5,
|
|
260
|
+
readlink,
|
|
261
|
+
realpath,
|
|
262
|
+
rm as rm2,
|
|
263
|
+
symlink
|
|
264
|
+
} from "node:fs/promises";
|
|
265
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
266
|
+
import { pathToFileURL } from "node:url";
|
|
267
|
+
import pMap3 from "p-map";
|
|
268
|
+
import {
|
|
269
|
+
basename as basename2,
|
|
270
|
+
dirname as dirname6,
|
|
271
|
+
isAbsolute as isAbsolute6,
|
|
272
|
+
join as join11,
|
|
273
|
+
normalize,
|
|
274
|
+
relative as relative4,
|
|
275
|
+
resolve as resolve6
|
|
276
|
+
} from "pathe";
|
|
277
|
+
import { glob as glob3 } from "tinyglobby";
|
|
278
|
+
|
|
279
|
+
// src/ai/api/problem.ts
|
|
280
|
+
var PROBLEM_TYPE = "application/problem+json";
|
|
281
|
+
|
|
282
|
+
// src/ai/api/spec.ts
|
|
283
|
+
var JSON_TYPE = "application/json";
|
|
284
|
+
var MARKDOWN_TYPE = "text/markdown";
|
|
285
|
+
var TEXT_TYPE = "text/plain";
|
|
286
|
+
var ref = (name) => ({
|
|
287
|
+
$ref: `#/components/schemas/${name}`
|
|
288
|
+
});
|
|
289
|
+
var jsonResponse = (description, schema) => ({
|
|
290
|
+
content: { [JSON_TYPE]: { schema: ref(schema) } },
|
|
291
|
+
description
|
|
292
|
+
});
|
|
293
|
+
var problemResponse = (description) => ({
|
|
294
|
+
content: { [PROBLEM_TYPE]: { schema: ref("Problem") } },
|
|
295
|
+
description
|
|
296
|
+
});
|
|
297
|
+
var textResponse = (description, type) => ({
|
|
298
|
+
content: { [type]: { schema: { type: "string" } } },
|
|
299
|
+
description
|
|
300
|
+
});
|
|
301
|
+
var string = (description) => ({ description, type: "string" });
|
|
302
|
+
var facetsSchema = {
|
|
303
|
+
additionalProperties: { type: "string" },
|
|
304
|
+
description: "Facet values the site declares for the page's content type (`content.types.<type>.facets`), key → value.",
|
|
305
|
+
type: "object"
|
|
306
|
+
};
|
|
307
|
+
var versionProperty = {
|
|
308
|
+
description: 'Docs version the page belongs to on a versioned site: `""` for the current docs, else an archived version id. Absent on unversioned sites.',
|
|
309
|
+
type: "string"
|
|
310
|
+
};
|
|
311
|
+
var searchParameters = [
|
|
312
|
+
{
|
|
313
|
+
description: "The search query.",
|
|
314
|
+
in: "query",
|
|
315
|
+
name: "q",
|
|
316
|
+
required: true,
|
|
317
|
+
schema: { minLength: 1, type: "string" }
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
description: "Maximum hits to return (default 8, at most 20).",
|
|
321
|
+
in: "query",
|
|
322
|
+
name: "limit",
|
|
323
|
+
required: false,
|
|
324
|
+
schema: { default: 8, maximum: 20, minimum: 1, type: "integer" }
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
description: "Only include pages of these content types (frontmatter `type`, e.g. `doc`, `rfc`). Comma-separated or repeated. Omit for every type.",
|
|
328
|
+
explode: false,
|
|
329
|
+
in: "query",
|
|
330
|
+
name: "contentTypes",
|
|
331
|
+
required: false,
|
|
332
|
+
schema: { items: { type: "string" }, type: "array" },
|
|
333
|
+
style: "form"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
description: "Only include pages in this locale (e.g. `fr`). Omit for every language.",
|
|
337
|
+
in: "query",
|
|
338
|
+
name: "locale",
|
|
339
|
+
required: false,
|
|
340
|
+
schema: { type: "string" }
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
description: "Docs version to scope to on a versioned site: `latest` (the default — current docs only), `all`, or an archived version id. Ignored when the site is unversioned.",
|
|
344
|
+
in: "query",
|
|
345
|
+
name: "version",
|
|
346
|
+
required: false,
|
|
347
|
+
schema: { type: "string" }
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
description: "Only include pages matching every facet, as `filters[key]=value` pairs (e.g. `filters[status]=enforced`). Facets are metadata the site declares per content type; the page index shows each page's values.",
|
|
351
|
+
explode: true,
|
|
352
|
+
in: "query",
|
|
353
|
+
name: "filters",
|
|
354
|
+
required: false,
|
|
355
|
+
schema: { additionalProperties: { type: "string" }, type: "object" },
|
|
356
|
+
style: "deepObject"
|
|
357
|
+
}
|
|
358
|
+
];
|
|
359
|
+
var schemas = {
|
|
360
|
+
JsonRpcRequest: {
|
|
361
|
+
description: "A JSON-RPC 2.0 request, as the Model Context Protocol sends.",
|
|
362
|
+
properties: {
|
|
363
|
+
id: { description: "Request id (absent on notifications)." },
|
|
364
|
+
jsonrpc: { const: "2.0", type: "string" },
|
|
365
|
+
method: string("The MCP method, e.g. `initialize` or `tools/call`."),
|
|
366
|
+
params: { description: "Method parameters.", type: "object" }
|
|
367
|
+
},
|
|
368
|
+
required: ["jsonrpc", "method"],
|
|
369
|
+
type: "object"
|
|
370
|
+
},
|
|
371
|
+
JsonRpcResponse: {
|
|
372
|
+
description: "A JSON-RPC 2.0 response: a `result` or an `error`.",
|
|
373
|
+
properties: {
|
|
374
|
+
error: {
|
|
375
|
+
properties: {
|
|
376
|
+
code: { type: "integer" },
|
|
377
|
+
data: {},
|
|
378
|
+
message: { type: "string" }
|
|
379
|
+
},
|
|
380
|
+
required: ["code", "message"],
|
|
381
|
+
type: "object"
|
|
382
|
+
},
|
|
383
|
+
id: {},
|
|
384
|
+
jsonrpc: { const: "2.0", type: "string" },
|
|
385
|
+
result: { type: "object" }
|
|
386
|
+
},
|
|
387
|
+
required: ["jsonrpc"],
|
|
388
|
+
type: "object"
|
|
389
|
+
},
|
|
390
|
+
NavLink: {
|
|
391
|
+
properties: {
|
|
392
|
+
href: string("Link target — an internal route or an external URL."),
|
|
393
|
+
icon: string("Optional icon name."),
|
|
394
|
+
label: { type: "string" }
|
|
395
|
+
},
|
|
396
|
+
required: ["href", "label"],
|
|
397
|
+
type: "object"
|
|
398
|
+
},
|
|
399
|
+
NavNode: {
|
|
400
|
+
description: "A sidebar entry: a page (linking to its route) or a group holding further nodes.",
|
|
401
|
+
oneOf: [
|
|
402
|
+
{
|
|
403
|
+
properties: {
|
|
404
|
+
badge: { type: "string" },
|
|
405
|
+
deprecated: { type: "boolean" },
|
|
406
|
+
description: { type: "string" },
|
|
407
|
+
icon: { type: "string" },
|
|
408
|
+
kind: { const: "page", type: "string" },
|
|
409
|
+
label: { type: "string" },
|
|
410
|
+
pageId: string("The page's stable content id."),
|
|
411
|
+
route: string("The page's route.")
|
|
412
|
+
},
|
|
413
|
+
required: ["kind", "label", "pageId", "route"],
|
|
414
|
+
type: "object"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
properties: {
|
|
418
|
+
badge: { type: "string" },
|
|
419
|
+
children: { items: ref("NavNode"), type: "array" },
|
|
420
|
+
collapsed: { type: "boolean" },
|
|
421
|
+
icon: { type: "string" },
|
|
422
|
+
kind: { const: "group", type: "string" },
|
|
423
|
+
label: { type: "string" },
|
|
424
|
+
path: string("The group's route prefix (not necessarily a page)."),
|
|
425
|
+
route: string("The group's index page route, when it has one.")
|
|
426
|
+
},
|
|
427
|
+
required: ["children", "kind", "label"],
|
|
428
|
+
type: "object"
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
NavSelector: {
|
|
433
|
+
description: "A top-level partition selector (products, versions, languages).",
|
|
434
|
+
properties: {
|
|
435
|
+
items: {
|
|
436
|
+
items: {
|
|
437
|
+
properties: {
|
|
438
|
+
description: { type: "string" },
|
|
439
|
+
label: { type: "string" },
|
|
440
|
+
path: { type: "string" },
|
|
441
|
+
tag: { type: "string" }
|
|
442
|
+
},
|
|
443
|
+
required: ["label", "path"],
|
|
444
|
+
type: "object"
|
|
445
|
+
},
|
|
446
|
+
type: "array"
|
|
447
|
+
},
|
|
448
|
+
kind: { type: "string" },
|
|
449
|
+
label: { type: "string" }
|
|
450
|
+
},
|
|
451
|
+
required: ["items", "kind", "label"],
|
|
452
|
+
type: "object"
|
|
453
|
+
},
|
|
454
|
+
NavTab: {
|
|
455
|
+
properties: {
|
|
456
|
+
href: string("The clickable target when it differs from `path` (the section's first page)."),
|
|
457
|
+
icon: { type: "string" },
|
|
458
|
+
label: { type: "string" },
|
|
459
|
+
path: string("The tab's section prefix.")
|
|
460
|
+
},
|
|
461
|
+
required: ["label", "path"],
|
|
462
|
+
type: "object"
|
|
463
|
+
},
|
|
464
|
+
Navigation: {
|
|
465
|
+
description: "The docs navigation model: header tabs, the sidebar tree, partition selectors, and pinned links.",
|
|
466
|
+
properties: {
|
|
467
|
+
actions: { items: ref("NavLink"), type: "array" },
|
|
468
|
+
cta: { oneOf: [ref("NavLink"), { type: "null" }] },
|
|
469
|
+
featured: { items: ref("NavLink"), type: "array" },
|
|
470
|
+
repoUrl: { type: ["string", "null"] },
|
|
471
|
+
root: string("The tree root's route (`/`, or a locale/version prefix)."),
|
|
472
|
+
selectors: { items: ref("NavSelector"), type: "array" },
|
|
473
|
+
sidebar: { items: ref("NavNode"), type: "array" },
|
|
474
|
+
tabs: { items: ref("NavTab"), type: "array" }
|
|
475
|
+
},
|
|
476
|
+
required: ["featured", "selectors", "sidebar", "tabs"],
|
|
477
|
+
type: "object"
|
|
478
|
+
},
|
|
479
|
+
Page: {
|
|
480
|
+
allOf: [
|
|
481
|
+
ref("PageSummary"),
|
|
482
|
+
{
|
|
483
|
+
properties: {
|
|
484
|
+
markdown: string("The page as agent Markdown: frontmatter included, components downleveled to plain Markdown.")
|
|
485
|
+
},
|
|
486
|
+
required: ["markdown"],
|
|
487
|
+
type: "object"
|
|
488
|
+
}
|
|
489
|
+
],
|
|
490
|
+
description: "A page's index entry plus its full Markdown body."
|
|
491
|
+
},
|
|
492
|
+
PageSummary: {
|
|
493
|
+
properties: {
|
|
494
|
+
contentType: string("The page's content type (frontmatter `type`)."),
|
|
495
|
+
description: { type: "string" },
|
|
496
|
+
facets: facetsSchema,
|
|
497
|
+
json: {
|
|
498
|
+
description: "This page's JSON representation (the `getPage` operation).",
|
|
499
|
+
format: "uri-reference",
|
|
500
|
+
type: "string"
|
|
501
|
+
},
|
|
502
|
+
lastModified: {
|
|
503
|
+
description: "ISO 8601 last-modified date, when known.",
|
|
504
|
+
type: ["string", "null"]
|
|
505
|
+
},
|
|
506
|
+
locale: string("The page's locale code."),
|
|
507
|
+
markdownUrl: {
|
|
508
|
+
description: "The page's raw-Markdown mirror (the `getPageMarkdown` operation).",
|
|
509
|
+
format: "uri-reference",
|
|
510
|
+
type: "string"
|
|
511
|
+
},
|
|
512
|
+
route: string("The page's route (`/guides/install`); the key every other operation takes."),
|
|
513
|
+
title: { type: "string" },
|
|
514
|
+
url: {
|
|
515
|
+
description: "Where the rendered page is served.",
|
|
516
|
+
format: "uri-reference",
|
|
517
|
+
type: "string"
|
|
518
|
+
},
|
|
519
|
+
version: versionProperty
|
|
520
|
+
},
|
|
521
|
+
required: [
|
|
522
|
+
"contentType",
|
|
523
|
+
"json",
|
|
524
|
+
"lastModified",
|
|
525
|
+
"locale",
|
|
526
|
+
"markdownUrl",
|
|
527
|
+
"route",
|
|
528
|
+
"title",
|
|
529
|
+
"url"
|
|
530
|
+
],
|
|
531
|
+
type: "object"
|
|
532
|
+
},
|
|
533
|
+
PagesIndex: {
|
|
534
|
+
properties: {
|
|
535
|
+
count: { type: "integer" },
|
|
536
|
+
generator: string("The Blume version that built the site."),
|
|
537
|
+
pages: { items: ref("PageSummary"), type: "array" },
|
|
538
|
+
site: { type: ["string", "null"] }
|
|
539
|
+
},
|
|
540
|
+
required: ["count", "generator", "pages", "site"],
|
|
541
|
+
type: "object"
|
|
542
|
+
},
|
|
543
|
+
Problem: {
|
|
544
|
+
description: "RFC 9457 problem details, with a stable code and a resolution hint.",
|
|
545
|
+
properties: {
|
|
546
|
+
code: string("Stable error code (e.g. `PAGE_NOT_FOUND`)."),
|
|
547
|
+
detail: string("Human-readable explanation of this occurrence."),
|
|
548
|
+
instance: string("The request path the problem occurred on."),
|
|
549
|
+
links: {
|
|
550
|
+
description: "Recovery links, when there is somewhere useful to go.",
|
|
551
|
+
items: {
|
|
552
|
+
properties: { href: { type: "string" }, label: { type: "string" } },
|
|
553
|
+
required: ["href", "label"],
|
|
554
|
+
type: "object"
|
|
555
|
+
},
|
|
556
|
+
type: "array"
|
|
557
|
+
},
|
|
558
|
+
resolution: string("What to do next."),
|
|
559
|
+
status: { type: "integer" },
|
|
560
|
+
title: { type: "string" },
|
|
561
|
+
type: string("Problem type URI; `about:blank` by default.")
|
|
562
|
+
},
|
|
563
|
+
required: ["code", "detail", "resolution", "status", "title", "type"],
|
|
564
|
+
type: "object"
|
|
565
|
+
},
|
|
566
|
+
SearchHit: {
|
|
567
|
+
properties: {
|
|
568
|
+
contentType: { type: "string" },
|
|
569
|
+
excerpt: string("The page description, else the start of its content."),
|
|
570
|
+
facets: facetsSchema,
|
|
571
|
+
route: string("The page's route; pass it to `getPage`."),
|
|
572
|
+
title: { type: "string" },
|
|
573
|
+
url: { format: "uri-reference", type: "string" },
|
|
574
|
+
version: versionProperty
|
|
575
|
+
},
|
|
576
|
+
required: ["excerpt", "route", "title", "url"],
|
|
577
|
+
type: "object"
|
|
578
|
+
},
|
|
579
|
+
SearchResponse: {
|
|
580
|
+
properties: {
|
|
581
|
+
count: { type: "integer" },
|
|
582
|
+
query: { type: "string" },
|
|
583
|
+
results: { items: ref("SearchHit"), type: "array" }
|
|
584
|
+
},
|
|
585
|
+
required: ["count", "query", "results"],
|
|
586
|
+
type: "object"
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var ROUTE_PARAM = {
|
|
590
|
+
description: "The page route without its leading slash (`guides/install`), or `index` for the home page. May contain slashes.",
|
|
591
|
+
in: "path",
|
|
592
|
+
name: "route",
|
|
593
|
+
required: true,
|
|
594
|
+
schema: { type: "string" }
|
|
595
|
+
};
|
|
596
|
+
var serverUrl = (input) => {
|
|
597
|
+
if (input.site) {
|
|
598
|
+
return input.base ? absoluteUrl(input.site, input.base) : siteRoot(input.site);
|
|
599
|
+
}
|
|
600
|
+
return input.base || "/";
|
|
601
|
+
};
|
|
602
|
+
var buildApiSpec = (input) => {
|
|
603
|
+
const pathEntries = [
|
|
604
|
+
[
|
|
605
|
+
API_PAGES_PATH,
|
|
606
|
+
{
|
|
607
|
+
get: {
|
|
608
|
+
description: "Every documentation page with its route, title, description, content type, locale, facets, and the URLs of its rendered, Markdown, and JSON forms. Unfiltered; on a versioned site every version is listed with its `version`. Use it to enumerate the docs or to find a page when search is too narrow.",
|
|
609
|
+
operationId: "listPages",
|
|
610
|
+
responses: {
|
|
611
|
+
"200": jsonResponse("The page index.", "PagesIndex"),
|
|
612
|
+
default: problemResponse("An error, as problem details.")
|
|
613
|
+
},
|
|
614
|
+
summary: "List every page",
|
|
615
|
+
tags: ["Pages"]
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
],
|
|
619
|
+
[
|
|
620
|
+
API_PAGE_PATH,
|
|
621
|
+
{
|
|
622
|
+
get: {
|
|
623
|
+
description: "A single page as JSON: its index entry plus the page's agent Markdown (frontmatter included, components downleveled to plain Markdown). Take `route` from `listPages` or `searchDocs`.",
|
|
624
|
+
operationId: "getPage",
|
|
625
|
+
parameters: [ROUTE_PARAM],
|
|
626
|
+
responses: {
|
|
627
|
+
"200": jsonResponse("The page.", "Page"),
|
|
628
|
+
"404": problemResponse("No page has that route."),
|
|
629
|
+
default: problemResponse("An error, as problem details.")
|
|
630
|
+
},
|
|
631
|
+
summary: "Get a page as JSON",
|
|
632
|
+
tags: ["Pages"]
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
],
|
|
636
|
+
[
|
|
637
|
+
API_NAVIGATION_PATH,
|
|
638
|
+
{
|
|
639
|
+
get: {
|
|
640
|
+
description: "The navigation tree (header tabs and the sidebar hierarchy) as readers see it, for the default locale and the current docs.",
|
|
641
|
+
operationId: "getNavigation",
|
|
642
|
+
responses: {
|
|
643
|
+
"200": jsonResponse("The navigation tree.", "Navigation"),
|
|
644
|
+
default: problemResponse("An error, as problem details.")
|
|
645
|
+
},
|
|
646
|
+
summary: "Get the navigation tree",
|
|
647
|
+
tags: ["Navigation"]
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
]
|
|
651
|
+
];
|
|
652
|
+
if (input.search) {
|
|
653
|
+
pathEntries.push([
|
|
654
|
+
API_SEARCH_PATH,
|
|
655
|
+
{
|
|
656
|
+
get: {
|
|
657
|
+
description: "Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; narrow by content type, locale, version, or facet. Use it first to discover relevant pages, then `getPage` to read one in full.",
|
|
658
|
+
operationId: "searchDocs",
|
|
659
|
+
parameters: searchParameters,
|
|
660
|
+
responses: {
|
|
661
|
+
"200": jsonResponse("The matching pages, best first.", "SearchResponse"),
|
|
662
|
+
"400": problemResponse("The query was missing or blank."),
|
|
663
|
+
default: problemResponse("An error, as problem details.")
|
|
664
|
+
},
|
|
665
|
+
summary: "Search the docs",
|
|
666
|
+
tags: ["Search"]
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
]);
|
|
670
|
+
}
|
|
671
|
+
pathEntries.push([
|
|
672
|
+
"/{route}.md",
|
|
673
|
+
{
|
|
674
|
+
get: {
|
|
675
|
+
description: "A page's raw-Markdown mirror: append `.md` to any page URL. Components are downleveled to plain Markdown; `.mdx` serves the source as written. The same body the `getPage` operation carries in its `markdown` field.",
|
|
676
|
+
operationId: "getPageMarkdown",
|
|
677
|
+
parameters: [ROUTE_PARAM],
|
|
678
|
+
responses: {
|
|
679
|
+
"200": textResponse("The page as Markdown.", MARKDOWN_TYPE),
|
|
680
|
+
"404": textResponse("No page has that route; the body lists where to look next.", MARKDOWN_TYPE)
|
|
681
|
+
},
|
|
682
|
+
summary: "Get a page as Markdown",
|
|
683
|
+
tags: ["Markdown"]
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
]);
|
|
687
|
+
if (input.llmsTxt) {
|
|
688
|
+
pathEntries.push([
|
|
689
|
+
"/llms.txt",
|
|
690
|
+
{
|
|
691
|
+
get: {
|
|
692
|
+
description: "The llms.txt index: the site's summary, when to use it, and every page with a one-line description, grouped by section.",
|
|
693
|
+
operationId: "getLlmsTxt",
|
|
694
|
+
responses: {
|
|
695
|
+
"200": textResponse("The index.", TEXT_TYPE)
|
|
696
|
+
},
|
|
697
|
+
summary: "Get llms.txt",
|
|
698
|
+
tags: ["Markdown"]
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
], [
|
|
702
|
+
"/llms-full.txt",
|
|
703
|
+
{
|
|
704
|
+
get: {
|
|
705
|
+
description: "The full Markdown of every current-docs page in one file.",
|
|
706
|
+
operationId: "getLlmsFullTxt",
|
|
707
|
+
responses: {
|
|
708
|
+
"200": textResponse("Every page's Markdown.", TEXT_TYPE)
|
|
709
|
+
},
|
|
710
|
+
summary: "Get llms-full.txt",
|
|
711
|
+
tags: ["Markdown"]
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
716
|
+
if (input.agentReadability) {
|
|
717
|
+
pathEntries.push([
|
|
718
|
+
"/agent-readability.json",
|
|
719
|
+
{
|
|
720
|
+
get: {
|
|
721
|
+
description: "A manifest indexing every agent-facing artifact the site publishes — this API, the Markdown mirrors, llms.txt, the MCP server, feeds, and the sitemap.",
|
|
722
|
+
operationId: "getAgentReadability",
|
|
723
|
+
responses: {
|
|
724
|
+
"200": {
|
|
725
|
+
content: { [JSON_TYPE]: { schema: { type: "object" } } },
|
|
726
|
+
description: "The manifest."
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
summary: "Get the agent-readability manifest",
|
|
730
|
+
tags: ["Discovery"]
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
]);
|
|
734
|
+
}
|
|
735
|
+
if (input.mcpRoute) {
|
|
736
|
+
pathEntries.push([
|
|
737
|
+
input.mcpRoute,
|
|
738
|
+
{
|
|
739
|
+
post: {
|
|
740
|
+
description: "The Model Context Protocol server (Streamable HTTP, stateless, JSON responses). Tools: `search_docs`, `get_page`, `list_pages`, `get_navigation` — the same operations this API exposes — plus every page as a `text/markdown` resource. Discovery document at `/.well-known/mcp.json`.",
|
|
741
|
+
operationId: "mcp",
|
|
742
|
+
requestBody: {
|
|
743
|
+
content: { [JSON_TYPE]: { schema: ref("JsonRpcRequest") } },
|
|
744
|
+
required: true
|
|
745
|
+
},
|
|
746
|
+
responses: {
|
|
747
|
+
"200": jsonResponse("The JSON-RPC response.", "JsonRpcResponse")
|
|
748
|
+
},
|
|
749
|
+
summary: "Call the MCP server",
|
|
750
|
+
tags: ["MCP"]
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
]);
|
|
754
|
+
}
|
|
755
|
+
const tags = [
|
|
756
|
+
{ description: "Enumerate and read documentation pages.", name: "Pages" },
|
|
757
|
+
...input.search ? [{ description: "Full-text search over the docs.", name: "Search" }] : [],
|
|
758
|
+
{ description: "How the docs are organized.", name: "Navigation" },
|
|
759
|
+
{ description: "Plain-text and Markdown surfaces.", name: "Markdown" },
|
|
760
|
+
...input.agentReadability ? [{ description: "Agent discovery documents.", name: "Discovery" }] : [],
|
|
761
|
+
...input.mcpRoute ? [{ description: "The Model Context Protocol endpoint.", name: "MCP" }] : []
|
|
762
|
+
];
|
|
763
|
+
const info = {
|
|
764
|
+
description: [
|
|
765
|
+
`Read-only JSON API over the ${input.name} documentation${input.description ? `: ${input.description}` : "."}`,
|
|
766
|
+
"Every operation is public and needs no authentication. Errors are RFC 9457 problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint."
|
|
767
|
+
].join(`
|
|
768
|
+
|
|
769
|
+
`),
|
|
770
|
+
title: apiNamePhrase(input.name),
|
|
771
|
+
version: input.version,
|
|
772
|
+
"x-generator": `blume@${input.version}`
|
|
773
|
+
};
|
|
774
|
+
const document = {
|
|
775
|
+
components: { schemas },
|
|
776
|
+
info,
|
|
777
|
+
openapi: "3.1.0",
|
|
778
|
+
paths: Object.fromEntries(pathEntries),
|
|
779
|
+
security: [],
|
|
780
|
+
servers: [{ description: input.name, url: serverUrl(input) }],
|
|
781
|
+
tags
|
|
782
|
+
};
|
|
783
|
+
if (input.site) {
|
|
784
|
+
document.externalDocs = {
|
|
785
|
+
description: `${input.name} documentation`,
|
|
786
|
+
url: absoluteUrl(input.site, withBasePath(input.base, "/"))
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return document;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// src/ai/ask-data.ts
|
|
793
|
+
var buildAskData = async (project) => {
|
|
794
|
+
const documents = await buildSearchDocuments(project, {
|
|
795
|
+
audience: "agents",
|
|
796
|
+
content: "markdown",
|
|
797
|
+
includeWhenDisabled: true
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
defaultLocale: project.config.i18n?.defaultLocale,
|
|
801
|
+
documents: documents.map((doc) => ({
|
|
802
|
+
content: doc.content,
|
|
803
|
+
description: doc.description,
|
|
804
|
+
locale: doc.locale,
|
|
805
|
+
route: doc.route,
|
|
806
|
+
title: doc.title
|
|
807
|
+
})),
|
|
808
|
+
site: project.config.deployment.site ?? null
|
|
809
|
+
};
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// src/ai/mcp/discovery.ts
|
|
813
|
+
var serverUrl2 = (input) => {
|
|
814
|
+
const path = withBasePath(input.base, input.route);
|
|
815
|
+
return input.site ? absoluteUrl(input.site, path) : path;
|
|
816
|
+
};
|
|
817
|
+
var buildMcpDiscovery = (input) => ({
|
|
818
|
+
servers: [
|
|
819
|
+
{
|
|
820
|
+
name: input.name,
|
|
821
|
+
transport: "streamable-http",
|
|
822
|
+
url: serverUrl2(input)
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
});
|
|
826
|
+
var SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
|
|
827
|
+
var CARD_TEXT_MAX = 100;
|
|
828
|
+
var truncate = (text) => text.length > CARD_TEXT_MAX ? `${text.slice(0, CARD_TEXT_MAX - 1)}…` : text;
|
|
829
|
+
var NON_ASCII_SLUG = /[^a-z0-9]+/gu;
|
|
830
|
+
var COMBINING_MARKS = /\p{M}+/gu;
|
|
831
|
+
var asciiSlugify = (text) => trimChar(text.normalize("NFKD").replace(COMBINING_MARKS, "").toLowerCase().replace(NON_ASCII_SLUG, "-"), "-");
|
|
832
|
+
var reverseDnsName = (input) => {
|
|
833
|
+
let namespace = "localhost";
|
|
834
|
+
if (input.site) {
|
|
835
|
+
try {
|
|
836
|
+
namespace = new URL(input.site).hostname.split(".").toReversed().join(".");
|
|
837
|
+
} catch {}
|
|
838
|
+
}
|
|
839
|
+
return `${namespace}/${asciiSlugify(input.name) || "docs"}`;
|
|
840
|
+
};
|
|
841
|
+
var HTTP_URL = /^https?:\/\//u;
|
|
842
|
+
var buildMcpServerCard = (input) => {
|
|
843
|
+
const url = serverUrl2(input);
|
|
844
|
+
const card = {
|
|
845
|
+
$schema: SERVER_CARD_SCHEMA,
|
|
846
|
+
capabilities: {
|
|
847
|
+
resources: { listChanged: false, subscribe: false },
|
|
848
|
+
tools: { listChanged: false }
|
|
849
|
+
},
|
|
850
|
+
description: truncate(`Model Context Protocol server for the ${input.name} documentation.`),
|
|
851
|
+
name: reverseDnsName(input),
|
|
852
|
+
serverInfo: { name: input.name, version: input.version },
|
|
853
|
+
title: truncate(input.name),
|
|
854
|
+
tools: MCP_TOOLS.map((tool) => ({
|
|
855
|
+
annotations: tool.annotations,
|
|
856
|
+
description: tool.description,
|
|
857
|
+
name: tool.name,
|
|
858
|
+
title: tool.title
|
|
859
|
+
})),
|
|
860
|
+
transport: "streamable-http",
|
|
861
|
+
transports: [{ endpoint: url, type: "streamable-http" }],
|
|
862
|
+
url,
|
|
863
|
+
version: input.version
|
|
864
|
+
};
|
|
865
|
+
if (HTTP_URL.test(url)) {
|
|
866
|
+
card.remotes = [{ type: "streamable-http", url }];
|
|
867
|
+
}
|
|
868
|
+
if (input.site) {
|
|
869
|
+
card.websiteUrl = siteRoot(input.site);
|
|
870
|
+
}
|
|
871
|
+
return card;
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// src/components/layout/nav-utils.ts
|
|
875
|
+
var hasDeferrableGroups = (sidebar) => sidebar.some((node) => node.kind === "group" && ((node.display ?? "flat") !== "flat" || hasDeferrableGroups(node.children)));
|
|
876
|
+
var navVariants = (data) => [
|
|
877
|
+
{ locale: "default", navigation: data.navigation, version: "current" },
|
|
878
|
+
...Object.entries(data.navigationByLocale).map(([locale, navigation]) => ({
|
|
879
|
+
locale,
|
|
880
|
+
navigation,
|
|
881
|
+
version: "current"
|
|
882
|
+
})),
|
|
883
|
+
...Object.entries(data.navigationByVersion).flatMap(([version, byLocale]) => Object.entries(byLocale).map(([locale, navigation]) => ({
|
|
884
|
+
locale: locale || "default",
|
|
885
|
+
navigation,
|
|
886
|
+
version
|
|
887
|
+
})))
|
|
888
|
+
];
|
|
889
|
+
|
|
890
|
+
// src/core/builtin-tags.ts
|
|
891
|
+
var BUILTIN_MDX_TAGS = new Set([
|
|
892
|
+
"Accordion",
|
|
893
|
+
"AccordionItem",
|
|
894
|
+
"ApiOverview",
|
|
895
|
+
"ApiTagOperations",
|
|
896
|
+
"AutoTypeTable",
|
|
897
|
+
"Badge",
|
|
898
|
+
"Callout",
|
|
899
|
+
"Card",
|
|
900
|
+
"CardGroup",
|
|
901
|
+
"CodeBlock",
|
|
902
|
+
"CodeGroup",
|
|
903
|
+
"Color",
|
|
904
|
+
"Column",
|
|
905
|
+
"Columns",
|
|
906
|
+
"Component",
|
|
907
|
+
"Diff",
|
|
908
|
+
"Expandable",
|
|
909
|
+
"FileTree",
|
|
910
|
+
"Frame",
|
|
911
|
+
"GithubInfo",
|
|
912
|
+
"Icon",
|
|
913
|
+
"Math",
|
|
914
|
+
"Operation",
|
|
915
|
+
"Panel",
|
|
916
|
+
"Prompt",
|
|
917
|
+
"Step",
|
|
918
|
+
"Steps",
|
|
919
|
+
"Tab",
|
|
920
|
+
"Tabs",
|
|
921
|
+
"Tile",
|
|
922
|
+
"Tooltip",
|
|
923
|
+
"Tree",
|
|
924
|
+
"TypeTable",
|
|
925
|
+
"Visibility",
|
|
926
|
+
"YouTube"
|
|
927
|
+
]);
|
|
928
|
+
|
|
929
|
+
// src/core/component-diagnostics.ts
|
|
930
|
+
var toKebab = (tag) => tag.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
|
|
931
|
+
var validateUsedComponents = (pages, extraTags, registryNames) => {
|
|
932
|
+
const diagnostics = [];
|
|
933
|
+
const seen = new Set;
|
|
934
|
+
for (const page of pages) {
|
|
935
|
+
for (const tag of page.componentsUsed ?? []) {
|
|
936
|
+
if (BUILTIN_MDX_TAGS.has(tag) || extraTags.has(tag) || seen.has(tag)) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
seen.add(tag);
|
|
940
|
+
const name = toKebab(tag);
|
|
941
|
+
const suggestion = registryNames.has(name) ? `Run \`blume add ${name}\` to install it, or register <${tag}> in components.ts (mdx).` : `Register <${tag}> in components.ts (mdx), or add an islands/${tag}.tsx component.`;
|
|
942
|
+
diagnostics.push({
|
|
943
|
+
code: "BLUME_UNKNOWN_COMPONENT",
|
|
944
|
+
file: page.sourcePath ?? page.id,
|
|
945
|
+
message: `<${tag}> is used in ${page.route} but isn't a known component.`,
|
|
946
|
+
severity: "warning",
|
|
947
|
+
suggestion
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return diagnostics;
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/core/component-overrides.ts
|
|
955
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
956
|
+
import { dirname as dirname3, extname, isAbsolute, resolve as resolve3 } from "pathe";
|
|
957
|
+
import ts from "typescript";
|
|
958
|
+
var GROUPS = ["mdx", "layout", "islands"];
|
|
959
|
+
var GROUP_SET = new Set(GROUPS);
|
|
960
|
+
var isGroup = (name) => GROUP_SET.has(name);
|
|
961
|
+
var FRAMEWORK_BY_EXT = new Map([
|
|
962
|
+
["jsx", "react"],
|
|
963
|
+
["svelte", "svelte"],
|
|
964
|
+
["tsx", "react"],
|
|
965
|
+
["vue", "vue"]
|
|
966
|
+
]);
|
|
967
|
+
var FRAMEWORK_LABEL = {
|
|
968
|
+
react: "React",
|
|
969
|
+
svelte: "Svelte",
|
|
970
|
+
vue: "Vue"
|
|
971
|
+
};
|
|
972
|
+
var COMPONENT_EXTS = [
|
|
973
|
+
"astro",
|
|
974
|
+
"tsx",
|
|
975
|
+
"ts",
|
|
976
|
+
"jsx",
|
|
977
|
+
"js",
|
|
978
|
+
"mjs",
|
|
979
|
+
"vue",
|
|
980
|
+
"svelte"
|
|
981
|
+
];
|
|
982
|
+
var HYDRATION_MODES = new Set([
|
|
983
|
+
"idle",
|
|
984
|
+
"load",
|
|
985
|
+
"media",
|
|
986
|
+
"only",
|
|
987
|
+
"visible"
|
|
988
|
+
]);
|
|
989
|
+
var isHydrationMode = (value) => HYDRATION_MODES.has(value);
|
|
990
|
+
var emptyAnalysis = () => ({
|
|
991
|
+
islands: [],
|
|
992
|
+
layout: [],
|
|
993
|
+
mdx: [],
|
|
994
|
+
warnings: []
|
|
995
|
+
});
|
|
996
|
+
var propName = (name) => ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
997
|
+
var addImportBindings = (map, statement) => {
|
|
998
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1002
|
+
const clause = statement.importClause;
|
|
1003
|
+
if (!clause) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (clause.name) {
|
|
1007
|
+
map.set(clause.name.text, { imported: "default", specifier });
|
|
1008
|
+
}
|
|
1009
|
+
const named = clause.namedBindings;
|
|
1010
|
+
if (named && ts.isNamedImports(named)) {
|
|
1011
|
+
for (const element of named.elements) {
|
|
1012
|
+
map.set(element.name.text, {
|
|
1013
|
+
imported: (element.propertyName ?? element.name).text,
|
|
1014
|
+
specifier
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
var collectImports = (sourceFile) => {
|
|
1020
|
+
const map = new Map;
|
|
1021
|
+
for (const statement of sourceFile.statements) {
|
|
1022
|
+
addImportBindings(map, statement);
|
|
1023
|
+
}
|
|
1024
|
+
return map;
|
|
1025
|
+
};
|
|
1026
|
+
var unwrapObject = (expression) => {
|
|
1027
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1028
|
+
return expression;
|
|
1029
|
+
}
|
|
1030
|
+
if (ts.isCallExpression(expression)) {
|
|
1031
|
+
const [arg] = expression.arguments;
|
|
1032
|
+
return arg && ts.isObjectLiteralExpression(arg) ? arg : undefined;
|
|
1033
|
+
}
|
|
1034
|
+
if (ts.isAsExpression(expression) || ts.isParenthesizedExpression(expression)) {
|
|
1035
|
+
return unwrapObject(expression.expression);
|
|
1036
|
+
}
|
|
1037
|
+
return;
|
|
1038
|
+
};
|
|
1039
|
+
var findDefaultExportObject = (sourceFile) => {
|
|
1040
|
+
for (const statement of sourceFile.statements) {
|
|
1041
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
1042
|
+
return unwrapObject(statement.expression);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
};
|
|
1047
|
+
var probeExtension = (base) => {
|
|
1048
|
+
for (const extension of COMPONENT_EXTS) {
|
|
1049
|
+
const candidate = `${base}.${extension}`;
|
|
1050
|
+
if (existsSync2(candidate)) {
|
|
1051
|
+
return candidate;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return null;
|
|
1055
|
+
};
|
|
1056
|
+
var toImport = (specifier, imported, dir) => {
|
|
1057
|
+
const relative2 = specifier.startsWith(".") || isAbsolute(specifier);
|
|
1058
|
+
let path = specifier;
|
|
1059
|
+
let extension = extname(specifier).slice(1).toLowerCase();
|
|
1060
|
+
if (relative2) {
|
|
1061
|
+
const absolute = isAbsolute(specifier) ? specifier : resolve3(dir, specifier);
|
|
1062
|
+
if (extension) {
|
|
1063
|
+
path = absolute;
|
|
1064
|
+
} else {
|
|
1065
|
+
const probed = probeExtension(absolute);
|
|
1066
|
+
path = probed ?? absolute;
|
|
1067
|
+
extension = probed ? extname(probed).slice(1).toLowerCase() : "";
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
framework: FRAMEWORK_BY_EXT.get(extension) ?? null,
|
|
1072
|
+
name: imported,
|
|
1073
|
+
path
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
1076
|
+
var resolveIdentifier = (name, imports, dir) => {
|
|
1077
|
+
const binding = imports.get(name);
|
|
1078
|
+
return binding ? toImport(binding.specifier, binding.imported, dir) : null;
|
|
1079
|
+
};
|
|
1080
|
+
var applyDescriptorProperty = (descriptor, property, imports, dir) => {
|
|
1081
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
1082
|
+
if (property.name.text === "component") {
|
|
1083
|
+
descriptor.hadComponent = true;
|
|
1084
|
+
descriptor.source = resolveIdentifier(property.name.text, imports, dir);
|
|
1085
|
+
}
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const name = propName(property.name);
|
|
1092
|
+
const init = property.initializer;
|
|
1093
|
+
if (name === "component") {
|
|
1094
|
+
descriptor.hadComponent = true;
|
|
1095
|
+
if (ts.isStringLiteral(init)) {
|
|
1096
|
+
descriptor.source = toImport(init.text, "default", dir);
|
|
1097
|
+
} else if (ts.isIdentifier(init)) {
|
|
1098
|
+
descriptor.source = resolveIdentifier(init.text, imports, dir);
|
|
1099
|
+
}
|
|
1100
|
+
} else if (name === "client" && ts.isStringLiteral(init) && isHydrationMode(init.text)) {
|
|
1101
|
+
descriptor.client = init.text;
|
|
1102
|
+
} else if (name === "media" && ts.isStringLiteral(init)) {
|
|
1103
|
+
descriptor.media = init.text;
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
var readDescriptor = (object, imports, dir) => {
|
|
1107
|
+
const descriptor = { hadComponent: false, source: null };
|
|
1108
|
+
for (const property of object.properties) {
|
|
1109
|
+
applyDescriptorProperty(descriptor, property, imports, dir);
|
|
1110
|
+
}
|
|
1111
|
+
return descriptor;
|
|
1112
|
+
};
|
|
1113
|
+
var finalize = (key, group, descriptor, label, identifier, warnings) => {
|
|
1114
|
+
const { client, media, source } = descriptor;
|
|
1115
|
+
if (group === "islands") {
|
|
1116
|
+
if (!source) {
|
|
1117
|
+
warnings.push(`Island override "${key}" couldn't be resolved to a file. Reference it by an imported component or a path string with an extension.`);
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
if (!source.framework) {
|
|
1121
|
+
warnings.push(`Island override "${key}" (${label}) is not a React, Vue, or Svelte component; only framework components can be islands.`);
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (client === "media" && !media) {
|
|
1126
|
+
warnings.push(`Override "${key}" uses client: "media" but no \`media\` query was given; it will hydrate as if \`client: "load"\`.`);
|
|
1127
|
+
}
|
|
1128
|
+
if (client === "only" && source && !source.framework) {
|
|
1129
|
+
warnings.push(`Override "${key}" uses client: "only" but its framework couldn't be inferred; reference a .tsx/.jsx/.vue/.svelte file.`);
|
|
1130
|
+
}
|
|
1131
|
+
if (!client && source?.framework) {
|
|
1132
|
+
warnings.push(`Override "${key}" points to a ${FRAMEWORK_LABEL[source.framework]} component (${label}) but has no hydration mode, so it renders as static HTML with no interactivity. Add one, e.g. \`${key}: { component: ${JSON.stringify(label)}, client: "load" }\`.`);
|
|
1133
|
+
}
|
|
1134
|
+
const normalized = { identifier, key, source };
|
|
1135
|
+
if (client) {
|
|
1136
|
+
normalized.client = client;
|
|
1137
|
+
}
|
|
1138
|
+
if (media) {
|
|
1139
|
+
normalized.media = media;
|
|
1140
|
+
}
|
|
1141
|
+
return normalized;
|
|
1142
|
+
};
|
|
1143
|
+
var normalizeEntry = (entry, group, imports, dir, warnings) => {
|
|
1144
|
+
const defaultClient = group === "islands" ? "visible" : undefined;
|
|
1145
|
+
if (ts.isShorthandPropertyAssignment(entry)) {
|
|
1146
|
+
const name = entry.name.text;
|
|
1147
|
+
return finalize(name, group, {
|
|
1148
|
+
client: defaultClient,
|
|
1149
|
+
hadComponent: true,
|
|
1150
|
+
source: resolveIdentifier(name, imports, dir)
|
|
1151
|
+
}, name, true, warnings);
|
|
1152
|
+
}
|
|
1153
|
+
if (!ts.isPropertyAssignment(entry)) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
const key = propName(entry.name);
|
|
1157
|
+
if (!key) {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
const value = entry.initializer;
|
|
1161
|
+
if (ts.isIdentifier(value)) {
|
|
1162
|
+
return finalize(key, group, {
|
|
1163
|
+
client: defaultClient,
|
|
1164
|
+
hadComponent: true,
|
|
1165
|
+
source: resolveIdentifier(value.text, imports, dir)
|
|
1166
|
+
}, value.text, true, warnings);
|
|
1167
|
+
}
|
|
1168
|
+
if (ts.isStringLiteral(value)) {
|
|
1169
|
+
return finalize(key, group, {
|
|
1170
|
+
client: defaultClient,
|
|
1171
|
+
hadComponent: true,
|
|
1172
|
+
source: toImport(value.text, "default", dir)
|
|
1173
|
+
}, value.text, false, warnings);
|
|
1174
|
+
}
|
|
1175
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
1176
|
+
const descriptor = readDescriptor(value, imports, dir);
|
|
1177
|
+
if (!descriptor.hadComponent) {
|
|
1178
|
+
warnings.push(`Override "${key}" is an object without a \`component\` field; expected \`{ component, client }\`.`);
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
if (!descriptor.source) {
|
|
1182
|
+
warnings.push(`Override "${key}"'s \`component\` couldn't be resolved to a file. Reference an imported component or a path string with an extension.`);
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
return finalize(key, group, { ...descriptor, client: descriptor.client ?? defaultClient }, key, false, warnings);
|
|
1186
|
+
}
|
|
1187
|
+
return { identifier: false, key, source: null };
|
|
1188
|
+
};
|
|
1189
|
+
var collectGroupOverrides = (property, imports, dir, result) => {
|
|
1190
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const name = propName(property.name);
|
|
1194
|
+
if (!(name && isGroup(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
for (const entry of property.initializer.properties) {
|
|
1198
|
+
const normalized = normalizeEntry(entry, name, imports, dir, result.warnings);
|
|
1199
|
+
if (normalized) {
|
|
1200
|
+
result[name].push(normalized);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
var analyzeComponentOverrides = (source, filePath) => {
|
|
1205
|
+
const result = emptyAnalysis();
|
|
1206
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, filePath.endsWith("tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
1207
|
+
const object = findDefaultExportObject(sourceFile);
|
|
1208
|
+
if (!object) {
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
const imports = collectImports(sourceFile);
|
|
1212
|
+
const dir = dirname3(filePath);
|
|
1213
|
+
for (const property of object.properties) {
|
|
1214
|
+
collectGroupOverrides(property, imports, dir, result);
|
|
1215
|
+
}
|
|
1216
|
+
return result;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// src/core/svg-dimensions.ts
|
|
1220
|
+
var ROOT = /<svg\s(?:[^>"']|"[^"]*"|'[^']*')*>/u;
|
|
1221
|
+
var WIDTH = /\swidth=(?<quote>['"])(?<value>[^%]+?)\k<quote>/u;
|
|
1222
|
+
var HEIGHT = /\sheight=(?<quote>['"])(?<value>[^%]+?)\k<quote>/u;
|
|
1223
|
+
var VIEWBOX = /\sviewBox=(?<quote>['"])(?<value>.+?)\k<quote>/iu;
|
|
1224
|
+
var INCH_CM = 2.54;
|
|
1225
|
+
var UNITS = new Map([
|
|
1226
|
+
["cm", 96 / INCH_CM],
|
|
1227
|
+
["em", 16],
|
|
1228
|
+
["ex", 8],
|
|
1229
|
+
["in", 96],
|
|
1230
|
+
["m", 96 / INCH_CM * 100],
|
|
1231
|
+
["mm", 96 / INCH_CM / 10],
|
|
1232
|
+
["pc", 96 / 72 * 12],
|
|
1233
|
+
["pt", 96 / 72],
|
|
1234
|
+
["px", 1]
|
|
1235
|
+
]);
|
|
1236
|
+
var LENGTH = /^(?<number>[0-9.]+(?:e\d+)?)(?<unit>in|cm|em|ex|m|mm|pc|pt|px)?$/u;
|
|
1237
|
+
var parseLength = (value) => {
|
|
1238
|
+
const groups = value === undefined ? undefined : LENGTH.exec(value)?.groups;
|
|
1239
|
+
if (!groups) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const pixels = Math.round(Number(groups.number) * (UNITS.get(groups.unit ?? "px") ?? 1));
|
|
1243
|
+
return pixels > 0 ? pixels : undefined;
|
|
1244
|
+
};
|
|
1245
|
+
var attribute = (root, pattern) => pattern.exec(root)?.groups?.value;
|
|
1246
|
+
var viewBoxSize = (root) => {
|
|
1247
|
+
const raw = attribute(root, VIEWBOX);
|
|
1248
|
+
if (raw === undefined) {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
const bounds = raw.split(" ");
|
|
1252
|
+
const width = parseLength(bounds[2]);
|
|
1253
|
+
const height = parseLength(bounds[3]);
|
|
1254
|
+
return width && height ? { height, width } : null;
|
|
1255
|
+
};
|
|
1256
|
+
var svgDimensions = (svg) => {
|
|
1257
|
+
const root = ROOT.exec(svg)?.[0];
|
|
1258
|
+
if (root === undefined) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
const width = parseLength(attribute(root, WIDTH));
|
|
1262
|
+
const height = parseLength(attribute(root, HEIGHT));
|
|
1263
|
+
if (width && height) {
|
|
1264
|
+
return { height, width };
|
|
1265
|
+
}
|
|
1266
|
+
const box = viewBoxSize(root);
|
|
1267
|
+
if (!box) {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
const ratio = box.width / box.height;
|
|
1271
|
+
if (width) {
|
|
1272
|
+
const scaled = Math.floor(width / ratio);
|
|
1273
|
+
return scaled > 0 ? { height: scaled, width } : null;
|
|
1274
|
+
}
|
|
1275
|
+
if (height) {
|
|
1276
|
+
const scaled = Math.floor(height * ratio);
|
|
1277
|
+
return scaled > 0 ? { height, width: scaled } : null;
|
|
1278
|
+
}
|
|
1279
|
+
return box;
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
// src/core/tsconfig-aliases.ts
|
|
1283
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1284
|
+
import { dirname as dirname4, join as join5, resolve as resolve4 } from "pathe";
|
|
1285
|
+
import ts2 from "typescript";
|
|
1286
|
+
var PARSE_HOST = {
|
|
1287
|
+
fileExists: ts2.sys.fileExists,
|
|
1288
|
+
readDirectory: () => [],
|
|
1289
|
+
readFile: ts2.sys.readFile,
|
|
1290
|
+
useCaseSensitiveFileNames: ts2.sys.useCaseSensitiveFileNames
|
|
1291
|
+
};
|
|
1292
|
+
var isPathTarget = (target) => typeof target === "string";
|
|
1293
|
+
var toAlias = (key, value, baseDir) => {
|
|
1294
|
+
const [first] = value;
|
|
1295
|
+
if (!isPathTarget(first)) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
const find = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
1299
|
+
const target = first.endsWith("/*") ? first.slice(0, -2) : first;
|
|
1300
|
+
if (find === "" || find === "*") {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
return { find, replacement: resolve4(baseDir, target) };
|
|
1304
|
+
};
|
|
1305
|
+
var resolveTsconfigAliases = (root) => {
|
|
1306
|
+
const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join5(root, name)).find((file) => existsSync3(file));
|
|
1307
|
+
if (!entry) {
|
|
1308
|
+
return {};
|
|
1309
|
+
}
|
|
1310
|
+
const { config, error } = ts2.readConfigFile(entry, ts2.sys.readFile);
|
|
1311
|
+
if (error) {
|
|
1312
|
+
return {};
|
|
1313
|
+
}
|
|
1314
|
+
const configDir = dirname4(entry);
|
|
1315
|
+
const { options } = ts2.parseJsonConfigFileContent(config, PARSE_HOST, configDir, undefined, entry);
|
|
1316
|
+
const { paths } = options;
|
|
1317
|
+
if (!paths) {
|
|
1318
|
+
return {};
|
|
1319
|
+
}
|
|
1320
|
+
const { pathsBasePath } = options;
|
|
1321
|
+
const baseDir = options.baseUrl ?? pathsBasePath ?? configDir;
|
|
1322
|
+
const entries = [];
|
|
1323
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
1324
|
+
const alias = toAlias(key, value, baseDir);
|
|
1325
|
+
if (alias) {
|
|
1326
|
+
entries.push([alias.find, alias.replacement]);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return Object.fromEntries(entries);
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
// src/markdown/language-icon.ts
|
|
1333
|
+
import {
|
|
1334
|
+
siAstro,
|
|
1335
|
+
siC,
|
|
1336
|
+
siCplusplus,
|
|
1337
|
+
siCss,
|
|
1338
|
+
siDart,
|
|
1339
|
+
siDocker,
|
|
1340
|
+
siGnubash,
|
|
1341
|
+
siGo,
|
|
1342
|
+
siGraphql,
|
|
1343
|
+
siHtml5,
|
|
1344
|
+
siJavascript,
|
|
1345
|
+
siJson,
|
|
1346
|
+
siKotlin,
|
|
1347
|
+
siLess,
|
|
1348
|
+
siLua,
|
|
1349
|
+
siMarkdown,
|
|
1350
|
+
siMdx,
|
|
1351
|
+
siMysql,
|
|
1352
|
+
siNextdotjs,
|
|
1353
|
+
siPhp,
|
|
1354
|
+
siPrisma,
|
|
1355
|
+
siPython,
|
|
1356
|
+
siReact,
|
|
1357
|
+
siRuby,
|
|
1358
|
+
siRust,
|
|
1359
|
+
siSass,
|
|
1360
|
+
siScala,
|
|
1361
|
+
siSvelte,
|
|
1362
|
+
siSvg,
|
|
1363
|
+
siSwift,
|
|
1364
|
+
siToml,
|
|
1365
|
+
siTypescript,
|
|
1366
|
+
siVuedotjs,
|
|
1367
|
+
siWebassembly,
|
|
1368
|
+
siYaml
|
|
1369
|
+
} from "simple-icons";
|
|
1370
|
+
var LANGUAGE_ICONS = {
|
|
1371
|
+
astro: siAstro,
|
|
1372
|
+
bash: siGnubash,
|
|
1373
|
+
c: siC,
|
|
1374
|
+
"c++": siCplusplus,
|
|
1375
|
+
cjs: siJavascript,
|
|
1376
|
+
cpp: siCplusplus,
|
|
1377
|
+
css: siCss,
|
|
1378
|
+
cts: siTypescript,
|
|
1379
|
+
dart: siDart,
|
|
1380
|
+
docker: siDocker,
|
|
1381
|
+
dockerfile: siDocker,
|
|
1382
|
+
go: siGo,
|
|
1383
|
+
gql: siGraphql,
|
|
1384
|
+
graphql: siGraphql,
|
|
1385
|
+
html: siHtml5,
|
|
1386
|
+
javascript: siJavascript,
|
|
1387
|
+
js: siJavascript,
|
|
1388
|
+
json: siJson,
|
|
1389
|
+
json5: siJson,
|
|
1390
|
+
jsonc: siJson,
|
|
1391
|
+
jsx: siReact,
|
|
1392
|
+
kotlin: siKotlin,
|
|
1393
|
+
kt: siKotlin,
|
|
1394
|
+
less: siLess,
|
|
1395
|
+
lua: siLua,
|
|
1396
|
+
markdown: siMarkdown,
|
|
1397
|
+
md: siMarkdown,
|
|
1398
|
+
mdx: siMdx,
|
|
1399
|
+
mjs: siJavascript,
|
|
1400
|
+
mts: siTypescript,
|
|
1401
|
+
nextjs: siNextdotjs,
|
|
1402
|
+
php: siPhp,
|
|
1403
|
+
prisma: siPrisma,
|
|
1404
|
+
py: siPython,
|
|
1405
|
+
python: siPython,
|
|
1406
|
+
rb: siRuby,
|
|
1407
|
+
react: siReact,
|
|
1408
|
+
rs: siRust,
|
|
1409
|
+
ruby: siRuby,
|
|
1410
|
+
rust: siRust,
|
|
1411
|
+
sass: siSass,
|
|
1412
|
+
scala: siScala,
|
|
1413
|
+
scss: siSass,
|
|
1414
|
+
sh: siGnubash,
|
|
1415
|
+
shell: siGnubash,
|
|
1416
|
+
sql: siMysql,
|
|
1417
|
+
svelte: siSvelte,
|
|
1418
|
+
svg: siSvg,
|
|
1419
|
+
swift: siSwift,
|
|
1420
|
+
toml: siToml,
|
|
1421
|
+
ts: siTypescript,
|
|
1422
|
+
tsx: siReact,
|
|
1423
|
+
typescript: siTypescript,
|
|
1424
|
+
vue: siVuedotjs,
|
|
1425
|
+
wasm: siWebassembly,
|
|
1426
|
+
yaml: siYaml,
|
|
1427
|
+
yml: siYaml,
|
|
1428
|
+
zsh: siGnubash
|
|
1429
|
+
};
|
|
1430
|
+
var languageIconSlug = (language) => LANGUAGE_ICONS[language.toLowerCase()]?.slug ?? null;
|
|
1431
|
+
var FENCE_LANGUAGE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*(?<lang>[\w+#.-]+)/gmu;
|
|
1432
|
+
var PROP_LANGUAGE = /\blang(?:uage)?=["'](?<lang>[\w+#.-]+)["']/gu;
|
|
1433
|
+
var languageIconSlugsIn = (markdown) => {
|
|
1434
|
+
const slugs = new Set;
|
|
1435
|
+
for (const pattern of [FENCE_LANGUAGE, PROP_LANGUAGE]) {
|
|
1436
|
+
for (const match of markdown.matchAll(pattern)) {
|
|
1437
|
+
const slug = languageIconSlug(match.groups?.lang ?? "");
|
|
1438
|
+
if (slug) {
|
|
1439
|
+
slugs.add(slug);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return [...slugs].toSorted();
|
|
1444
|
+
};
|
|
1445
|
+
var iconBySlug = (slug) => Object.values(LANGUAGE_ICONS).find((icon) => icon.slug === slug);
|
|
1446
|
+
var maskUri = (path) => `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="${path}"/></svg>`)}")`;
|
|
1447
|
+
var languageIconCss = (slugs) => slugs.map((slug) => {
|
|
1448
|
+
const icon = iconBySlug(slug);
|
|
1449
|
+
if (!icon) {
|
|
1450
|
+
return "";
|
|
1451
|
+
}
|
|
1452
|
+
const mask = maskUri(icon.path);
|
|
1453
|
+
return `.prose > :where(pre[data-language][data-icon="${slug}"])::after {
|
|
1454
|
+
background-color: var(--blume-muted-foreground);
|
|
1455
|
+
-webkit-mask-image: ${mask};
|
|
1456
|
+
mask-image: ${mask};
|
|
1457
|
+
}`;
|
|
1458
|
+
}).filter((rule) => rule !== "").join(`
|
|
1459
|
+
`);
|
|
1460
|
+
|
|
1461
|
+
// src/markdown/mermaid.ts
|
|
1462
|
+
var MERMAID_FENCE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*mermaid\b/mu;
|
|
1463
|
+
var hasMermaidFence = (text) => MERMAID_FENCE.test(text);
|
|
1464
|
+
|
|
1465
|
+
// src/og/derive.ts
|
|
1466
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1467
|
+
import { isAbsolute as isAbsolute2, join as join6 } from "pathe";
|
|
1468
|
+
var CARD_WEIGHTS = [400, 600];
|
|
1469
|
+
var absoluteSrc = (root, src) => isAbsolute2(src) ? src : join6(root, src);
|
|
1470
|
+
var isNumericWeight = (weight) => typeof weight === "number";
|
|
1471
|
+
var isRangeWeight = (weight) => typeof weight === "string";
|
|
1472
|
+
var isFontName = (value) => typeof value === "string";
|
|
1473
|
+
var isLocalOgFont = (font) => typeof font !== "string" && ("src" in font);
|
|
1474
|
+
var googleWeights = (weights) => {
|
|
1475
|
+
const numbers = weights.filter(isNumericWeight);
|
|
1476
|
+
const used = numbers.filter((weight) => CARD_WEIGHTS.includes(weight));
|
|
1477
|
+
if (used.length > 0) {
|
|
1478
|
+
return used;
|
|
1479
|
+
}
|
|
1480
|
+
if (numbers.length > 0) {
|
|
1481
|
+
return numbers;
|
|
1482
|
+
}
|
|
1483
|
+
const [first] = weights;
|
|
1484
|
+
return weights.length === 1 && isRangeWeight(first) ? first : undefined;
|
|
1485
|
+
};
|
|
1486
|
+
var googleOgFont = (name, weights) => {
|
|
1487
|
+
const weight = googleWeights(weights);
|
|
1488
|
+
return weight === undefined ? { name } : { name, weight };
|
|
1489
|
+
};
|
|
1490
|
+
var localOgFonts = (font, root) => font.variants.map((variant) => {
|
|
1491
|
+
const entry = {
|
|
1492
|
+
name: font.name,
|
|
1493
|
+
src: absoluteSrc(root, variant.src)
|
|
1494
|
+
};
|
|
1495
|
+
const withWeight = isNumericWeight(variant.weight) ? { ...entry, weight: variant.weight } : entry;
|
|
1496
|
+
return variant.style === "normal" || variant.style === "italic" ? { ...withWeight, style: variant.style } : withWeight;
|
|
1497
|
+
});
|
|
1498
|
+
var roleFonts = (value, root) => {
|
|
1499
|
+
if (isFontName(value)) {
|
|
1500
|
+
if (!isFontSlug(value)) {
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
const def = GOOGLE_FONTS[value];
|
|
1504
|
+
return [googleOgFont(def.family, def.weights)];
|
|
1505
|
+
}
|
|
1506
|
+
if ("variants" in value) {
|
|
1507
|
+
return localOgFonts(value, root);
|
|
1508
|
+
}
|
|
1509
|
+
if ((value.provider ?? "google") !== "google") {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
return [googleOgFont(value.name, value.weights ?? CARD_WEIGHTS)];
|
|
1513
|
+
};
|
|
1514
|
+
var roleFamily = (value) => {
|
|
1515
|
+
if (isFontName(value)) {
|
|
1516
|
+
return isFontSlug(value) ? GOOGLE_FONTS[value].family : null;
|
|
1517
|
+
}
|
|
1518
|
+
return value.name;
|
|
1519
|
+
};
|
|
1520
|
+
var deriveOgFonts = (fonts, root) => {
|
|
1521
|
+
const derived = [];
|
|
1522
|
+
const seen = new Set;
|
|
1523
|
+
const families = {};
|
|
1524
|
+
const roles = [
|
|
1525
|
+
["title", fonts?.display],
|
|
1526
|
+
["body", fonts?.body]
|
|
1527
|
+
];
|
|
1528
|
+
for (const [role, value] of roles) {
|
|
1529
|
+
if (value === undefined) {
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
const roleEntries = roleFonts(value, root);
|
|
1533
|
+
if (!roleEntries) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
families[role] = roleFamily(value) ?? undefined;
|
|
1537
|
+
for (const entry of roleEntries) {
|
|
1538
|
+
const key = JSON.stringify(entry);
|
|
1539
|
+
if (!seen.has(key)) {
|
|
1540
|
+
seen.add(key);
|
|
1541
|
+
derived.push(entry);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const result = { fonts: derived };
|
|
1546
|
+
if (families.title || families.body) {
|
|
1547
|
+
result.families = families;
|
|
1548
|
+
}
|
|
1549
|
+
return result;
|
|
1550
|
+
};
|
|
1551
|
+
var resolveOgFontSources = (fonts, root) => fonts.map((font) => isLocalOgFont(font) ? { ...font, src: absoluteSrc(root, font.src) } : font);
|
|
1552
|
+
var resolveOgFonts = (options, root) => {
|
|
1553
|
+
if (options.ogFonts) {
|
|
1554
|
+
return { fonts: resolveOgFontSources(options.ogFonts, root) };
|
|
1555
|
+
}
|
|
1556
|
+
return options.themeFontsConfigured ? deriveOgFonts(options.themeFonts, root) : { fonts: [] };
|
|
1557
|
+
};
|
|
1558
|
+
var missingFontFiles = (options, root) => {
|
|
1559
|
+
const sources = [];
|
|
1560
|
+
for (const value of Object.values(options.themeFonts ?? {})) {
|
|
1561
|
+
if (!isFontName(value) && "variants" in value) {
|
|
1562
|
+
sources.push(...value.variants.map((variant) => absoluteSrc(root, variant.src)));
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
for (const font of options.ogFonts) {
|
|
1566
|
+
if (isLocalOgFont(font)) {
|
|
1567
|
+
sources.push(absoluteSrc(root, font.src));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return [...new Set(sources)].filter((path) => !existsSync4(path));
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
// src/og/logo.ts
|
|
1574
|
+
import { existsSync as existsSync5, readFileSync } from "node:fs";
|
|
1575
|
+
import { join as join7 } from "pathe";
|
|
1576
|
+
var resolveOgLogo = (project, source) => {
|
|
1577
|
+
if (!source?.toLowerCase().endsWith(".svg")) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const relative2 = source.replace(/^\//u, "");
|
|
1581
|
+
const file = [
|
|
1582
|
+
join7(project.context.root, "public", relative2),
|
|
1583
|
+
join7(project.context.root, relative2)
|
|
1584
|
+
].find((path) => existsSync5(path));
|
|
1585
|
+
return file ? readFileSync(file, "utf-8") : undefined;
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
// src/openapi/scalar.ts
|
|
1589
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1590
|
+
import { isAbsolute as isAbsolute3, join as join8 } from "pathe";
|
|
1591
|
+
|
|
1592
|
+
// src/theme/palette.ts
|
|
1593
|
+
var FALLBACK_ACCENT = "oklch(0.62 0.16 250)";
|
|
1594
|
+
var ACCENTS = {
|
|
1595
|
+
blue: FALLBACK_ACCENT,
|
|
1596
|
+
green: "oklch(0.6 0.16 150)",
|
|
1597
|
+
orange: "oklch(0.68 0.17 50)",
|
|
1598
|
+
pink: "oklch(0.65 0.2 350)",
|
|
1599
|
+
purple: "oklch(0.58 0.2 290)",
|
|
1600
|
+
red: "oklch(0.58 0.22 25)",
|
|
1601
|
+
teal: "oklch(0.6 0.12 195)"
|
|
1602
|
+
};
|
|
1603
|
+
var isAccentPreset = (value) => Object.hasOwn(ACCENTS, value);
|
|
1604
|
+
var CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
|
|
1605
|
+
var safeColor = (value, fallback) => CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
|
|
1606
|
+
var presetOrColor = (value) => isAccentPreset(value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
|
|
1607
|
+
var safeColorOrNull = (value) => value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
|
|
1608
|
+
var RADII = {
|
|
1609
|
+
lg: "0.75rem",
|
|
1610
|
+
md: "0.5rem",
|
|
1611
|
+
none: "0",
|
|
1612
|
+
sm: "0.25rem"
|
|
1613
|
+
};
|
|
1614
|
+
var cssString = (value) => JSON.stringify(value);
|
|
1615
|
+
var backgroundImageCss = (image) => `url(${cssString(image)})`;
|
|
1616
|
+
var cssToken = (name, value) => value ? [` ${name}: ${value};`] : [];
|
|
1617
|
+
var themeRootCss = (theme, options) => [
|
|
1618
|
+
` --blume-accent: ${options.accent};`,
|
|
1619
|
+
...cssToken("--blume-action", options.action),
|
|
1620
|
+
...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
|
|
1621
|
+
...cssToken("--blume-background", safeColorOrNull(theme.background?.light)),
|
|
1622
|
+
...cssToken("--blume-background-image", theme.backgroundImage?.light ? backgroundImageCss(theme.backgroundImage.light) : null),
|
|
1623
|
+
` --blume-radius: ${options.radius};`
|
|
1624
|
+
].filter(Boolean).join(`
|
|
1625
|
+
`);
|
|
1626
|
+
var themeDarkCss = (theme, options) => {
|
|
1627
|
+
const tokens = [
|
|
1628
|
+
` --blume-accent: ${options.accent};`,
|
|
1629
|
+
" --blume-accent-foreground: oklch(1 0 0);",
|
|
1630
|
+
...cssToken("--blume-action", options.action),
|
|
1631
|
+
...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
|
|
1632
|
+
...cssToken("--blume-background", safeColorOrNull(theme.background?.dark)),
|
|
1633
|
+
...cssToken("--blume-background-image", theme.backgroundImage?.dark ? backgroundImageCss(theme.backgroundImage.dark) : null)
|
|
1634
|
+
].filter(Boolean);
|
|
1635
|
+
return `:root[data-theme="dark"] {
|
|
1636
|
+
${tokens.join(`
|
|
1637
|
+
`)}
|
|
1638
|
+
}
|
|
1639
|
+
`;
|
|
1640
|
+
};
|
|
1641
|
+
var resolveAccent = (theme) => ({
|
|
1642
|
+
dark: presetOrColor(theme.accent.dark),
|
|
1643
|
+
light: presetOrColor(theme.accent.light)
|
|
1644
|
+
});
|
|
1645
|
+
var resolveRadius = (theme) => RADII[theme.radius];
|
|
1646
|
+
var buildThemeCss = (theme) => {
|
|
1647
|
+
const accent = resolveAccent(theme);
|
|
1648
|
+
const action = theme.action ? presetOrColor(theme.action) : null;
|
|
1649
|
+
const radius = RADII[theme.radius];
|
|
1650
|
+
const root = themeRootCss(theme, {
|
|
1651
|
+
accent: accent.light,
|
|
1652
|
+
action,
|
|
1653
|
+
radius
|
|
1654
|
+
});
|
|
1655
|
+
const dark = themeDarkCss(theme, {
|
|
1656
|
+
accent: accent.dark,
|
|
1657
|
+
action
|
|
1658
|
+
});
|
|
1659
|
+
return `/* Generated by Blume from theme config. */
|
|
1660
|
+
:root {
|
|
1661
|
+
${root}
|
|
1662
|
+
}
|
|
1663
|
+
${dark}`;
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
// src/openapi/scalar.ts
|
|
1667
|
+
var URL_SPEC = /^https?:\/\//u;
|
|
1668
|
+
var referencePagePath = (route) => {
|
|
1669
|
+
const segments = trimChar(route, "/");
|
|
1670
|
+
return `${segments === "" ? "index" : segments}.astro`;
|
|
1671
|
+
};
|
|
1672
|
+
var themeConfiguration = (config, override) => {
|
|
1673
|
+
if (override) {
|
|
1674
|
+
return { theme: override };
|
|
1675
|
+
}
|
|
1676
|
+
const accent = resolveAccent(config.theme);
|
|
1677
|
+
const radius = resolveRadius(config.theme);
|
|
1678
|
+
return {
|
|
1679
|
+
customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent.light};--scalar-radius:${radius};}.dark-mode{--scalar-color-accent:${accent.dark};}`
|
|
1680
|
+
};
|
|
1681
|
+
};
|
|
1682
|
+
var specConfiguration = async (spec, root) => {
|
|
1683
|
+
if (URL_SPEC.test(spec)) {
|
|
1684
|
+
return { config: { url: spec } };
|
|
1685
|
+
}
|
|
1686
|
+
const absolute = isAbsolute3(spec) ? spec : join8(root, spec);
|
|
1687
|
+
try {
|
|
1688
|
+
return { config: { content: await readFile2(absolute, "utf-8") } };
|
|
1689
|
+
} catch {
|
|
1690
|
+
return {
|
|
1691
|
+
config: { url: spec },
|
|
1692
|
+
warning: `API reference spec not found: "${spec}" (looked in ${absolute}).`
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
var acceptScalarReference = (ref2, seen, contentRoutes, warnings) => {
|
|
1697
|
+
if (ref2.renderer !== "scalar") {
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1700
|
+
if (seen.has(ref2.route)) {
|
|
1701
|
+
warnings.push(`Two API reference sources resolve to ${ref2.route}; keeping the first.`);
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
if (contentRoutes.has(ref2.route)) {
|
|
1705
|
+
warnings.push(`API reference route ${ref2.route} collides with a content page; skipping the reference there.`);
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
seen.add(ref2.route);
|
|
1709
|
+
return ref2;
|
|
1710
|
+
};
|
|
1711
|
+
var buildReferenceFiles = async (options) => {
|
|
1712
|
+
const { config, root, contentRoutes } = options;
|
|
1713
|
+
const warnings = [];
|
|
1714
|
+
const seen = new Set;
|
|
1715
|
+
const accepted = [];
|
|
1716
|
+
for (const ref2 of resolveReferences(config)) {
|
|
1717
|
+
const next = acceptScalarReference(ref2, seen, contentRoutes, warnings);
|
|
1718
|
+
if (next) {
|
|
1719
|
+
accepted.push(next);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const built = await Promise.all(accepted.map(async (ref2) => ({
|
|
1723
|
+
ref: ref2,
|
|
1724
|
+
spec: await specConfiguration(ref2.spec, root)
|
|
1725
|
+
})));
|
|
1726
|
+
const files = [];
|
|
1727
|
+
for (const { ref: ref2, spec } of built) {
|
|
1728
|
+
if (spec.warning) {
|
|
1729
|
+
warnings.push(spec.warning);
|
|
1730
|
+
}
|
|
1731
|
+
const pagePath = referencePagePath(ref2.route);
|
|
1732
|
+
files.push({
|
|
1733
|
+
content: scalarReferenceTemplate({
|
|
1734
|
+
configuration: {
|
|
1735
|
+
...spec.config,
|
|
1736
|
+
...themeConfiguration(config, ref2.theme),
|
|
1737
|
+
...ref2.scalar
|
|
1738
|
+
},
|
|
1739
|
+
noindex: ref2.noindex,
|
|
1740
|
+
route: ref2.route,
|
|
1741
|
+
title: ref2.label
|
|
1742
|
+
}),
|
|
1743
|
+
pagePath
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return { files, warnings };
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
// src/search/popular.ts
|
|
1750
|
+
var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, label }) => {
|
|
1751
|
+
const page = {
|
|
1752
|
+
label,
|
|
1753
|
+
route: withBasePath(basePath, href)
|
|
1754
|
+
};
|
|
1755
|
+
if (icon) {
|
|
1756
|
+
page.icon = icon;
|
|
1757
|
+
}
|
|
1758
|
+
return page;
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
// src/theme/code-block-padding.ts
|
|
1762
|
+
var CODE_PADDING_BLOCK_REM = 1;
|
|
1763
|
+
|
|
1764
|
+
// src/theme/entry.ts
|
|
1765
|
+
var CODE_SCROLL_INSET_REM = 0.375;
|
|
1766
|
+
var DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
|
|
1767
|
+
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));`;
|
|
1768
|
+
var COLOR_SCHEME_DEFAULTS = `:root {
|
|
1769
|
+
color-scheme: light;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
:root[data-theme="dark"] {
|
|
1773
|
+
color-scheme: dark;
|
|
1774
|
+
}`;
|
|
1775
|
+
var TOKEN_DEFAULTS = `:root {
|
|
1776
|
+
--blume-background: oklch(1 0 0);
|
|
1777
|
+
--blume-background-image: none;
|
|
1778
|
+
--blume-background-image-repeat: no-repeat;
|
|
1779
|
+
--blume-background-image-size: cover;
|
|
1780
|
+
--blume-foreground: oklch(0.145 0 0);
|
|
1781
|
+
--blume-muted: oklch(0.965 0 0);
|
|
1782
|
+
/* 5.28:1 on the page background. The headroom over 4.5:1 is the point: muted
|
|
1783
|
+
text is 14px body copy routinely set on a tinted surface (callouts, badges,
|
|
1784
|
+
panels), and each tint costs a few tenths. */
|
|
1785
|
+
--blume-muted-foreground: oklch(0.53 0 0);
|
|
1786
|
+
--blume-border: oklch(0.88 0.006 260 / 0.72);
|
|
1787
|
+
--blume-accent: oklch(0.145 0 0);
|
|
1788
|
+
--blume-accent-foreground: oklch(1 0 0);
|
|
1789
|
+
--blume-action: var(--blume-accent);
|
|
1790
|
+
--blume-action-foreground: var(--blume-accent-foreground);
|
|
1791
|
+
--blume-code-background: oklch(0.99 0 0);
|
|
1792
|
+
/* Shiki notation transformers: line/word highlight, diff add/remove. */
|
|
1793
|
+
--blume-code-highlight: oklch(0.55 0.16 255 / 0.1);
|
|
1794
|
+
--blume-code-highlight-border: oklch(0.55 0.16 255 / 0.55);
|
|
1795
|
+
--blume-code-add: oklch(0.72 0.16 150 / 0.16);
|
|
1796
|
+
--blume-code-add-border: oklch(0.52 0.15 150 / 0.7);
|
|
1797
|
+
--blume-code-remove: oklch(0.66 0.21 22 / 0.16);
|
|
1798
|
+
--blume-code-remove-border: oklch(0.55 0.2 22 / 0.7);
|
|
1799
|
+
--blume-code-word: oklch(0.55 0.16 255 / 0.16);
|
|
1800
|
+
--blume-code-word-border: oklch(0.55 0.16 255 / 0.5);
|
|
1801
|
+
--blume-radius: 0.75rem;
|
|
1802
|
+
|
|
1803
|
+
/* Max width of the prose content column (article, breadcrumb, TOC, feedback,
|
|
1804
|
+
pagination, last-updated). Override to re-measure the whole column at once. */
|
|
1805
|
+
--blume-content-width: 42rem;
|
|
1806
|
+
|
|
1807
|
+
/*
|
|
1808
|
+
* Font tokens resolve through optional src variables that Astro's Fonts API
|
|
1809
|
+
* populates (via theme.fonts). When unset they fall back to the system
|
|
1810
|
+
* stacks, so the default look is unchanged. The display font defaults to the
|
|
1811
|
+
* body font.
|
|
1812
|
+
*/
|
|
1813
|
+
--blume-font-body: var(
|
|
1814
|
+
--blume-font-body-src,
|
|
1815
|
+
ui-sans-serif,
|
|
1816
|
+
system-ui,
|
|
1817
|
+
-apple-system,
|
|
1818
|
+
"Segoe UI",
|
|
1819
|
+
Roboto,
|
|
1820
|
+
Helvetica,
|
|
1821
|
+
Arial,
|
|
1822
|
+
sans-serif
|
|
1823
|
+
);
|
|
1824
|
+
--blume-font-mono: var(
|
|
1825
|
+
--blume-font-mono-src,
|
|
1826
|
+
ui-monospace,
|
|
1827
|
+
"SF Mono",
|
|
1828
|
+
"Cascadia Code",
|
|
1829
|
+
"Source Code Pro",
|
|
1830
|
+
Menlo,
|
|
1831
|
+
Consolas,
|
|
1832
|
+
monospace
|
|
1833
|
+
);
|
|
1834
|
+
--blume-font-display: var(--blume-font-display-src, var(--blume-font-body));
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
:root[data-theme="dark"] {
|
|
1838
|
+
--blume-background: oklch(0.085 0 0);
|
|
1839
|
+
--blume-background-image: none;
|
|
1840
|
+
--blume-background-image-repeat: no-repeat;
|
|
1841
|
+
--blume-background-image-size: cover;
|
|
1842
|
+
--blume-foreground: oklch(0.96 0 0);
|
|
1843
|
+
--blume-muted: oklch(0.16 0 0);
|
|
1844
|
+
--blume-muted-foreground: oklch(0.68 0 0);
|
|
1845
|
+
--blume-border: oklch(0.24 0 0 / 0.8);
|
|
1846
|
+
--blume-accent: oklch(0.96 0 0);
|
|
1847
|
+
--blume-accent-foreground: oklch(0.085 0 0);
|
|
1848
|
+
--blume-action: var(--blume-accent);
|
|
1849
|
+
--blume-action-foreground: var(--blume-accent-foreground);
|
|
1850
|
+
--blume-code-background: oklch(0.12 0 0);
|
|
1851
|
+
/* Brighter tints read better over the dark code surface. */
|
|
1852
|
+
--blume-code-highlight: oklch(0.7 0.14 255 / 0.16);
|
|
1853
|
+
--blume-code-highlight-border: oklch(0.7 0.14 255 / 0.6);
|
|
1854
|
+
--blume-code-add: oklch(0.78 0.17 150 / 0.2);
|
|
1855
|
+
--blume-code-add-border: oklch(0.72 0.16 150 / 0.7);
|
|
1856
|
+
--blume-code-remove: oklch(0.72 0.21 22 / 0.22);
|
|
1857
|
+
--blume-code-remove-border: oklch(0.7 0.2 22 / 0.7);
|
|
1858
|
+
--blume-code-word: oklch(0.7 0.14 255 / 0.22);
|
|
1859
|
+
--blume-code-word-border: oklch(0.7 0.14 255 / 0.55);
|
|
1860
|
+
}`;
|
|
1861
|
+
var THEME_MAPPING = `@theme inline {
|
|
1862
|
+
--color-background: var(--blume-background);
|
|
1863
|
+
--color-foreground: var(--blume-foreground);
|
|
1864
|
+
--color-muted: var(--blume-muted);
|
|
1865
|
+
--color-muted-foreground: var(--blume-muted-foreground);
|
|
1866
|
+
--color-border: var(--blume-border);
|
|
1867
|
+
--color-accent: var(--blume-accent);
|
|
1868
|
+
--color-accent-foreground: var(--blume-accent-foreground);
|
|
1869
|
+
--color-action: var(--blume-action);
|
|
1870
|
+
--color-action-foreground: var(--blume-action-foreground);
|
|
1871
|
+
--color-code: var(--blume-code-background);
|
|
1872
|
+
--radius-blume: var(--blume-radius);
|
|
1873
|
+
--container-content: var(--blume-content-width);
|
|
1874
|
+
--font-sans: var(--blume-font-body);
|
|
1875
|
+
--font-mono: var(--blume-font-mono);
|
|
1876
|
+
--font-display: var(--blume-font-display);
|
|
1877
|
+
}`;
|
|
1878
|
+
var tailwindEntryTemplate = (options) => `/* Generated by Blume. Do not edit. */
|
|
1879
|
+
@import "tailwindcss";
|
|
1880
|
+
@plugin "@tailwindcss/typography";
|
|
1881
|
+
|
|
1882
|
+
/* Scan Blume's components and the user's project for utility classes. */
|
|
1883
|
+
${options.sources.map((source) => `@source "${source}";`).join(`
|
|
1884
|
+
`)}
|
|
1885
|
+
|
|
1886
|
+
${DARK_VARIANT}
|
|
1887
|
+
|
|
1888
|
+
${COLOR_SCHEME_DEFAULTS}
|
|
1889
|
+
|
|
1890
|
+
${TOKEN_DEFAULTS}
|
|
1891
|
+
|
|
1892
|
+
${THEME_MAPPING}
|
|
1893
|
+
|
|
1894
|
+
@layer base {
|
|
1895
|
+
/* Nothing refuses to shrink below its intrinsic content width. This global
|
|
1896
|
+
min-width reset defuses the classic flex/grid overflow — a long or
|
|
1897
|
+
truncating child forcing its container (and the page) past the viewport
|
|
1898
|
+
edge — so components don't need per-element min-w-0 overrides. */
|
|
1899
|
+
* {
|
|
1900
|
+
min-width: 0;
|
|
1901
|
+
}
|
|
1902
|
+
/* Interactive controls get a pointer cursor unless disabled. */
|
|
1903
|
+
button:not(:disabled),
|
|
1904
|
+
[role="button"]:not(:disabled) {
|
|
1905
|
+
cursor: pointer;
|
|
1906
|
+
}
|
|
1907
|
+
html {
|
|
1908
|
+
scroll-behavior: smooth;
|
|
1909
|
+
scroll-padding-top: 4.5rem;
|
|
1910
|
+
text-rendering: optimizeLegibility;
|
|
1911
|
+
}
|
|
1912
|
+
/* Headings use the display font (defaults to the body font when unset).
|
|
1913
|
+
The tightened tracking is part of the theme, not the font: display-tuned
|
|
1914
|
+
families bake it into their metrics, but a text family promoted to
|
|
1915
|
+
headings (including the Inter default) reads loose without it. -0.05em
|
|
1916
|
+
was matched visually against Inter Tight, the previous display default. */
|
|
1917
|
+
h1,
|
|
1918
|
+
h2,
|
|
1919
|
+
h3,
|
|
1920
|
+
h4,
|
|
1921
|
+
h5,
|
|
1922
|
+
h6 {
|
|
1923
|
+
font-family: var(--font-display);
|
|
1924
|
+
letter-spacing: -0.05em;
|
|
1925
|
+
}
|
|
1926
|
+
:focus-visible {
|
|
1927
|
+
outline: 2px solid var(--blume-accent);
|
|
1928
|
+
outline-offset: 2px;
|
|
1929
|
+
border-radius: 2px;
|
|
1930
|
+
}
|
|
1931
|
+
body {
|
|
1932
|
+
background-attachment: fixed;
|
|
1933
|
+
background-image: var(--blume-background-image);
|
|
1934
|
+
background-position: center top;
|
|
1935
|
+
background-repeat: var(--blume-background-image-repeat);
|
|
1936
|
+
background-size: var(--blume-background-image-size);
|
|
1937
|
+
}
|
|
1938
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1939
|
+
html {
|
|
1940
|
+
scroll-behavior: auto;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
/* Same-origin navigations ride Astro's client router (the layouts render
|
|
1946
|
+
ClientRouter), which owns the page-transition styling — including its own
|
|
1947
|
+
prefers-reduced-motion guard — so the theme declares nothing about it. The
|
|
1948
|
+
cross-document view-transition opt-in that briefly lived here is
|
|
1949
|
+
deliberately gone: mixing it with the client router double-animates the
|
|
1950
|
+
full-load navigations the router hands back to the browser. */
|
|
1951
|
+
|
|
1952
|
+
/* Code reads left-to-right regardless of page direction; only the surrounding
|
|
1953
|
+
chrome mirrors for RTL. Inline code is isolated so LTR identifiers don't
|
|
1954
|
+
disturb the bidi flow of right-to-left prose. */
|
|
1955
|
+
[dir="rtl"] pre,
|
|
1956
|
+
[dir="rtl"] .prose pre {
|
|
1957
|
+
direction: ltr;
|
|
1958
|
+
text-align: left;
|
|
1959
|
+
}
|
|
1960
|
+
[dir="rtl"] :not(pre) > code {
|
|
1961
|
+
unicode-bidi: isolate;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/* The sidebar's rows. A docs site renders every sidebar row on every page,
|
|
1965
|
+
so a large site's HTML is mostly this markup — one utility per row kind
|
|
1966
|
+
instead of the dozen classes each expands to keeps every page (and the
|
|
1967
|
+
build's HTML output) small. Kept in sync with NavTree.astro: a page row,
|
|
1968
|
+
a page-mode group's drill-in button, a collapsible group's summary and
|
|
1969
|
+
the link inside it, and a flat group's heading and the link inside it. */
|
|
1970
|
+
@utility blume-nav-link {
|
|
1971
|
+
@apply block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground;
|
|
1972
|
+
}
|
|
1973
|
+
@utility blume-nav-drill {
|
|
1974
|
+
@apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted;
|
|
1975
|
+
}
|
|
1976
|
+
@utility blume-nav-summary {
|
|
1977
|
+
@apply flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden;
|
|
1978
|
+
}
|
|
1979
|
+
@utility blume-nav-summary-link {
|
|
1980
|
+
@apply -my-1 flex flex-1 items-center gap-1.5 rounded py-1 transition-colors aria-[current=page]:text-foreground aria-[current=page]:font-semibold;
|
|
1981
|
+
}
|
|
1982
|
+
@utility blume-nav-heading {
|
|
1983
|
+
@apply mb-2 flex items-center gap-1.5 px-2.5 font-medium text-foreground text-sm;
|
|
1984
|
+
}
|
|
1985
|
+
@utility blume-nav-heading-link {
|
|
1986
|
+
@apply -ml-1 flex flex-1 items-center gap-1.5 rounded-[0.65rem] px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/* Theme Tailwind Typography (prose) with Blume tokens. */
|
|
1990
|
+
.prose {
|
|
1991
|
+
--tw-prose-body: var(--blume-foreground);
|
|
1992
|
+
--tw-prose-headings: var(--blume-foreground);
|
|
1993
|
+
--tw-prose-lead: var(--blume-muted-foreground);
|
|
1994
|
+
--tw-prose-links: var(--blume-foreground);
|
|
1995
|
+
--tw-prose-bold: var(--blume-foreground);
|
|
1996
|
+
--tw-prose-counters: var(--blume-muted-foreground);
|
|
1997
|
+
--tw-prose-bullets: var(--blume-muted-foreground);
|
|
1998
|
+
--tw-prose-hr: var(--blume-border);
|
|
1999
|
+
--tw-prose-quotes: var(--blume-muted-foreground);
|
|
2000
|
+
--tw-prose-quote-borders: var(--blume-border);
|
|
2001
|
+
--tw-prose-captions: var(--blume-muted-foreground);
|
|
2002
|
+
--tw-prose-code: var(--blume-foreground);
|
|
2003
|
+
--tw-prose-pre-code: var(--blume-foreground);
|
|
2004
|
+
--tw-prose-pre-bg: var(--blume-code-background);
|
|
2005
|
+
--tw-prose-th-borders: var(--blume-border);
|
|
2006
|
+
--tw-prose-td-borders: var(--blume-border);
|
|
2007
|
+
color: var(--blume-muted-foreground);
|
|
2008
|
+
font-size: 0.875rem;
|
|
2009
|
+
line-height: 1.7;
|
|
2010
|
+
/* An unbreakable run — an API permission, a broker list, a bare URL used as
|
|
2011
|
+
its own link text, an OpenAPI operation title of the form
|
|
2012
|
+
\`METHOD /very/long/{path}\`, a module-qualified name in a generated
|
|
2013
|
+
reference — paints past the content column, and nothing between the
|
|
2014
|
+
paragraph and the viewport clips it, so it lands in the document's scroll
|
|
2015
|
+
width and drags the page sideways on a phone. Inherited, so headings and
|
|
2016
|
+
inline code are covered too; the only re-declarations below are the
|
|
2017
|
+
opt-in code-wrap mode and Twoslash, which want different values.
|
|
2018
|
+
\`break-word\` and not \`anywhere\`: only \`anywhere\` reduces min-content,
|
|
2019
|
+
which is what sizes \`table-layout: auto\` columns, so it would resize
|
|
2020
|
+
every table on the site. The flip side is that a token in a table cell
|
|
2021
|
+
still never breaks — the column grows to fit it — so wide tables rely on
|
|
2022
|
+
the .blume-table-scroll wrapper, not on this. */
|
|
2023
|
+
overflow-wrap: break-word;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/* No letter-spacing here: prose headings inherit the base h1-h6 rule's
|
|
2027
|
+
display tracking, same as headings outside the prose column. */
|
|
2028
|
+
.prose :where(h1, h2, h3, h4) {
|
|
2029
|
+
font-weight: 500;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
.prose :where(h1) {
|
|
2033
|
+
font-size: 3rem;
|
|
2034
|
+
line-height: 1.1;
|
|
2035
|
+
margin: 0 0 1rem;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
.prose :where(h2) {
|
|
2039
|
+
font-size: 1.875rem;
|
|
2040
|
+
line-height: 1.2;
|
|
2041
|
+
margin-top: 3rem;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
.prose :where(h3) {
|
|
2045
|
+
font-size: 1.25rem;
|
|
2046
|
+
line-height: 1.35;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
.prose :where(h4) {
|
|
2050
|
+
color: var(--blume-muted-foreground);
|
|
2051
|
+
font-size: 0.875rem;
|
|
2052
|
+
line-height: 1.4;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.prose :where(h1, h2, h3, h4, h5, h6) a {
|
|
2056
|
+
color: inherit;
|
|
2057
|
+
font-weight: inherit;
|
|
2058
|
+
text-decoration: none;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
/* Auto-generated heading permalinks (markdown.headingAnchors): the whole
|
|
2062
|
+
heading links to its own id, with a muted “#” revealed on hover or keyboard
|
|
2063
|
+
focus so the link is discoverable without cluttering the heading at rest. */
|
|
2064
|
+
.prose :where(h2, h3, h4, h5, h6) a.blume-heading-anchor::after {
|
|
2065
|
+
content: "#";
|
|
2066
|
+
color: var(--blume-muted-foreground);
|
|
2067
|
+
margin-inline-start: 0.35em;
|
|
2068
|
+
opacity: 0;
|
|
2069
|
+
transition: opacity 0.15s ease;
|
|
2070
|
+
}
|
|
2071
|
+
.prose :where(h2, h3, h4, h5, h6) a.blume-heading-anchor:hover::after,
|
|
2072
|
+
.prose :where(h2, h3, h4, h5, h6) a.blume-heading-anchor:focus-visible::after {
|
|
2073
|
+
opacity: 1;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/* A [toc]-marked heading exists only for the table of contents: it stays in
|
|
2077
|
+
the flow as a zero-height, invisible anchor target so its TOC entry (and any
|
|
2078
|
+
deep link) still has somewhere to scroll to, without rendering on the page. */
|
|
2079
|
+
.prose :where(.blume-toc-only) {
|
|
2080
|
+
border: 0;
|
|
2081
|
+
height: 0;
|
|
2082
|
+
margin: 0;
|
|
2083
|
+
overflow: hidden;
|
|
2084
|
+
padding: 0;
|
|
2085
|
+
visibility: hidden;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
.prose :where(h2:first-child) {
|
|
2089
|
+
border-top: 0;
|
|
2090
|
+
padding-top: 0;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
.prose :where(p, ul, ol) {
|
|
2094
|
+
margin-top: 1rem;
|
|
2095
|
+
margin-bottom: 1rem;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
.prose :where(strong) {
|
|
2099
|
+
font-weight: 600;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
.prose :where(a) {
|
|
2103
|
+
color: var(--blume-foreground);
|
|
2104
|
+
font-weight: 500;
|
|
2105
|
+
text-decoration-line: underline;
|
|
2106
|
+
text-decoration-style: dotted;
|
|
2107
|
+
text-decoration-thickness: 1px;
|
|
2108
|
+
text-underline-offset: 0.2em;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
.prose :where(a[data-blume-card]) {
|
|
2112
|
+
color: inherit;
|
|
2113
|
+
font-weight: inherit;
|
|
2114
|
+
text-decoration: none;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
.prose :where(hr) {
|
|
2118
|
+
margin: 2.5rem 0;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
.prose :where(pre) {
|
|
2122
|
+
background: transparent;
|
|
2123
|
+
border: 1px solid var(--blume-border);
|
|
2124
|
+
border-radius: var(--blume-radius);
|
|
2125
|
+
color: var(--blume-foreground);
|
|
2126
|
+
font-family: var(--font-mono);
|
|
2127
|
+
font-size: 0.8125rem;
|
|
2128
|
+
line-height: 1.55;
|
|
2129
|
+
margin: 1.5rem 0;
|
|
2130
|
+
overflow-x: auto;
|
|
2131
|
+
padding: ${CODE_PADDING_BLOCK_REM}rem 0;
|
|
2132
|
+
position: relative;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
.prose :where(pre.astro-code) {
|
|
2136
|
+
background: transparent !important;
|
|
2137
|
+
color: var(--shiki-light, var(--blume-foreground)) !important;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
:root[data-theme="dark"] .prose :where(pre.astro-code) {
|
|
2141
|
+
color: var(--shiki-dark, var(--blume-foreground)) !important;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
.prose :where(pre.astro-code span) {
|
|
2145
|
+
background: transparent !important;
|
|
2146
|
+
color: var(--shiki-light, inherit);
|
|
2147
|
+
font-style: var(--shiki-light-font-style, inherit);
|
|
2148
|
+
font-weight: var(--shiki-light-font-weight, inherit);
|
|
2149
|
+
text-decoration: var(--shiki-light-text-decoration, inherit);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
:root[data-theme="dark"] .prose :where(pre.astro-code span) {
|
|
2153
|
+
color: var(--shiki-dark, inherit);
|
|
2154
|
+
font-style: var(--shiki-dark-font-style, inherit);
|
|
2155
|
+
font-weight: var(--shiki-dark-font-weight, inherit);
|
|
2156
|
+
text-decoration: var(--shiki-dark-text-decoration, inherit);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/* The Diff component (@pierre/diffs) renders into a declarative shadow root and
|
|
2160
|
+
themes itself with light-dark(). Drive that from Blume's data-theme instead
|
|
2161
|
+
of the OS, scoped to the diff host: the custom property pierces the shadow
|
|
2162
|
+
boundary, where the appended host color-scheme rule consumes it. */
|
|
2163
|
+
blume-diff {
|
|
2164
|
+
--blume-diff-color-scheme: light;
|
|
2165
|
+
display: block;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
:root[data-theme="dark"] blume-diff {
|
|
2169
|
+
--blume-diff-color-scheme: dark;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
.prose :where(pre[data-language]) {
|
|
2173
|
+
padding-top: 3.75rem;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
.prose :where(pre[data-language])::before {
|
|
2177
|
+
align-items: center;
|
|
2178
|
+
border-bottom: 1px solid var(--blume-border);
|
|
2179
|
+
color: var(--blume-muted-foreground);
|
|
2180
|
+
content: attr(data-language);
|
|
2181
|
+
display: flex;
|
|
2182
|
+
font-family: var(--font-sans);
|
|
2183
|
+
font-size: 0.75rem;
|
|
2184
|
+
font-weight: 500;
|
|
2185
|
+
height: 2.75rem;
|
|
2186
|
+
left: 0;
|
|
2187
|
+
padding: 0 3.25rem 0 1rem;
|
|
2188
|
+
position: absolute;
|
|
2189
|
+
right: 0;
|
|
2190
|
+
top: 0;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
.prose :where(pre[data-title])::before {
|
|
2194
|
+
content: attr(data-title);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/* Language icon (simple-icons) sits at the header's left edge; the label shifts
|
|
2198
|
+
right to make room. Injected at build time by the language-icon transformer.
|
|
2199
|
+
The icon is gated on data-language (the header bar), not just data-icon: the
|
|
2200
|
+
transformer runs whenever icons are on, but a header-less standalone
|
|
2201
|
+
CodeBlock (no title) never gets data-language, so without this gate the
|
|
2202
|
+
absolutely-positioned icon would overlap the first code line. Fenced code
|
|
2203
|
+
and titled blocks always have a header, so the icon shows there. */
|
|
2204
|
+
.prose > :where(pre[data-language][data-icon])::after {
|
|
2205
|
+
content: "";
|
|
2206
|
+
height: 0.875rem;
|
|
2207
|
+
left: 1rem;
|
|
2208
|
+
-webkit-mask-position: center;
|
|
2209
|
+
-webkit-mask-repeat: no-repeat;
|
|
2210
|
+
-webkit-mask-size: contain;
|
|
2211
|
+
mask-position: center;
|
|
2212
|
+
mask-repeat: no-repeat;
|
|
2213
|
+
mask-size: contain;
|
|
2214
|
+
pointer-events: none;
|
|
2215
|
+
position: absolute;
|
|
2216
|
+
top: 0.875rem;
|
|
2217
|
+
width: 0.875rem;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
.prose > :where(pre[data-language][data-icon])::before {
|
|
2221
|
+
padding-left: 2.5rem;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/* One mask rule per language the site's Markdown uses (see
|
|
2225
|
+
markdown/language-icon.ts); the ::after above only gets a color and a mask
|
|
2226
|
+
from these, so a block whose language has no rule paints nothing. */
|
|
2227
|
+
${options.languageIcons ?? ""}
|
|
2228
|
+
|
|
2229
|
+
.prose :where(pre code) {
|
|
2230
|
+
background: transparent;
|
|
2231
|
+
border-radius: 0;
|
|
2232
|
+
color: inherit;
|
|
2233
|
+
font-size: inherit;
|
|
2234
|
+
font-weight: 400;
|
|
2235
|
+
padding: 0;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
/* Long lines scroll inside the code element, not the pre: the pre stays static
|
|
2239
|
+
so its absolute header bar (::before) and copy button don't drift with the
|
|
2240
|
+
scroll. The pre's horizontal padding lives here so content still scrolls
|
|
2241
|
+
edge-to-edge past it. Two contexts opt out: twoslash blocks (popups must
|
|
2242
|
+
escape any scroll container — see theme/twoslash.ts), and the API request
|
|
2243
|
+
panel, which owns its code layout and keeps the copy control in the panel
|
|
2244
|
+
header. Every other component that hosts a code block — Tabs, CodeGroup,
|
|
2245
|
+
Steps, Callout, Card, Accordion — is real prose content and keeps the inset,
|
|
2246
|
+
even though its chrome wrapper is not-prose. */
|
|
2247
|
+
.prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
2248
|
+
display: block;
|
|
2249
|
+
/* Tall blocks scroll vertically in place instead of taking the page. Both
|
|
2250
|
+
axes live on the code element for the same reason: the pre stays static
|
|
2251
|
+
so the header bar and copy button never drift with the scroll. */
|
|
2252
|
+
max-height: 24rem;
|
|
2253
|
+
overflow: auto;
|
|
2254
|
+
/* The small bottom inset keeps the horizontal thumb off the last line's
|
|
2255
|
+
descenders now that scrollbars are visible. The pre gives up the same
|
|
2256
|
+
amount below (next rule), so the inset adds no height to the block. */
|
|
2257
|
+
padding: 0 1.25rem ${CODE_SCROLL_INSET_REM}rem;
|
|
2258
|
+
/* Thin theme-colored scrollbars, matching the sidebar treatment, so a
|
|
2259
|
+
height-capped block reads as scrollable instead of simply ending.
|
|
2260
|
+
Safari before 18.2 supports neither property and falls back to the
|
|
2261
|
+
platform-default scrollbar — acceptable, since macOS overlays it. */
|
|
2262
|
+
scrollbar-color: var(--blume-border) transparent;
|
|
2263
|
+
scrollbar-width: thin;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
/* The scroller's bottom inset comes out of the pre's own block padding: the
|
|
2267
|
+
text still sits one full inset above the frame's bottom edge, the thumb sits
|
|
2268
|
+
in the gap, and a one-line command is no taller than it was before the
|
|
2269
|
+
scroller existed. */
|
|
2270
|
+
.prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> code)) {
|
|
2271
|
+
padding-bottom: calc(${CODE_PADDING_BLOCK_REM}rem - ${CODE_SCROLL_INSET_REM}rem);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/* The dark border token is too close to the page background to read as a
|
|
2275
|
+
scrollbar thumb; derive a brighter one from the muted foreground instead. */
|
|
2276
|
+
:root[data-theme="dark"]
|
|
2277
|
+
.prose
|
|
2278
|
+
:where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
2279
|
+
scrollbar-color: color-mix(in oklab, var(--blume-muted-foreground) 55%, transparent)
|
|
2280
|
+
transparent;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/* Word wrap (markdown.code.wrap): long lines wrap instead of scrolling. The
|
|
2284
|
+
attribute is set on <body> from config; default code keeps \`white-space: pre\`. */
|
|
2285
|
+
[data-blume-code-wrap] pre,
|
|
2286
|
+
[data-blume-code-wrap] pre code {
|
|
2287
|
+
overflow-wrap: break-word;
|
|
2288
|
+
white-space: pre-wrap;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
.prose :where(table) {
|
|
2292
|
+
font-size: 0.8125rem;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/* Scroll wide tables in place; the wrapper is added by the blume:table-wrap hast
|
|
2296
|
+
plugin. The border+radius frames the scroll viewport so a clipped wide table
|
|
2297
|
+
reads as scrollable rather than cut off, and carries the table's vertical
|
|
2298
|
+
rhythm (the inner table's own margin is zeroed so it fills the frame). */
|
|
2299
|
+
.blume-table-scroll {
|
|
2300
|
+
overflow-x: auto;
|
|
2301
|
+
-webkit-overflow-scrolling: touch;
|
|
2302
|
+
margin: 1.5rem 0;
|
|
2303
|
+
border: 1px solid var(--blume-border);
|
|
2304
|
+
border-radius: var(--blume-radius);
|
|
2305
|
+
}
|
|
2306
|
+
.blume-table-scroll > table {
|
|
2307
|
+
margin: 0;
|
|
2308
|
+
}
|
|
2309
|
+
/* Restore inner padding on every cell. Typography zeroes the first/last cell's
|
|
2310
|
+
inline padding so a borderless table aligns to the prose margin; inside the
|
|
2311
|
+
framed wrapper that leaves edge text touching the border. The :is() selector
|
|
2312
|
+
outweighs Typography's :where()-scoped rules so the outer columns get it too.
|
|
2313
|
+
|
|
2314
|
+
The block padding is 0.75rem against a table line-height near 1.7: at 0.5rem a
|
|
2315
|
+
cell whose content wrapped put MORE space between its own two lines than
|
|
2316
|
+
between itself and the next row, so a table of wrapping cells read as one
|
|
2317
|
+
block rather than as rows. The inline padding stays where it was, deliberately
|
|
2318
|
+
— widening it comes out of column width in a capped article, and on one corpus
|
|
2319
|
+
that pushed cells fitting on two lines onto three. */
|
|
2320
|
+
.blume-table-scroll :is(th, td) {
|
|
2321
|
+
padding: 0.75rem;
|
|
2322
|
+
}
|
|
2323
|
+
/* Keep column labels on one line so a two-word header does not wrap into a
|
|
2324
|
+
ragged stack; the table just scrolls a little wider instead. Body cells keep
|
|
2325
|
+
wrapping so a wide table stays a scroll away, not an endless horizontal run. */
|
|
2326
|
+
.blume-table-scroll th {
|
|
2327
|
+
white-space: nowrap;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/* GFM renders cells as <td><code> directly, which the descendant form alone
|
|
2331
|
+
never matches (a cell is not its own descendant). */
|
|
2332
|
+
.prose :where(td, th) > code,
|
|
2333
|
+
.prose :where(td, th) :not(pre) > code {
|
|
2334
|
+
white-space: normal;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
blume-tabs pre,
|
|
2338
|
+
.not-prose > div > pre {
|
|
2339
|
+
background: var(--blume-code-background);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/* The Component source pane fills its tab's fixed height (set inline to match the
|
|
2343
|
+
preview tab) and scrolls the inner code element, leaving the pre static so the
|
|
2344
|
+
copy button — absolute, pinned to the pre — doesn't drift while it scrolls. */
|
|
2345
|
+
pre.blume-source {
|
|
2346
|
+
display: flex;
|
|
2347
|
+
flex-direction: column;
|
|
2348
|
+
height: 100%;
|
|
2349
|
+
overflow: hidden;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
pre.blume-source > code {
|
|
2353
|
+
flex: 1;
|
|
2354
|
+
/* The pane's measured height is authoritative (it can exceed the prose
|
|
2355
|
+
24rem cap), so undo the generic prose max-height: the code must fill
|
|
2356
|
+
whatever height the tab was given. */
|
|
2357
|
+
max-height: none;
|
|
2358
|
+
min-height: 0;
|
|
2359
|
+
overflow: auto;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/* Code blocks inside tabs sit flush; the tab container owns the frame. These
|
|
2363
|
+
rules are unlayered like the base \`pre\` styles, so they win over Tailwind's
|
|
2364
|
+
layered utilities (which a class on the panel cannot). The second selector
|
|
2365
|
+
covers <CodeGroup>, where each code block IS the panel (a direct child of the
|
|
2366
|
+
tab content wrapper) rather than a <pre> nested inside a <Tab>. */
|
|
2367
|
+
blume-tabs [data-blume-tab-panel] > pre,
|
|
2368
|
+
blume-tabs [data-blume-tab-content] > pre {
|
|
2369
|
+
/* Important to beat the astro-code background rule above; the tab container
|
|
2370
|
+
owns the surface, so the code sits directly on it. */
|
|
2371
|
+
background: transparent !important;
|
|
2372
|
+
border: 0;
|
|
2373
|
+
border-radius: 0;
|
|
2374
|
+
margin: 0;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/* These contexts drop the language bar (\`::before\` is cleared below), so the
|
|
2378
|
+
bar's padding goes with it and the block keeps the plain inset. Unscoped on
|
|
2379
|
+
purpose: <CodeBlock> wraps its pre in its own \`.prose\` below the component
|
|
2380
|
+
chrome, so without this a titled block on a page with no prose column
|
|
2381
|
+
(PageLayout) would keep the 3.75rem bar padding under a cleared bar. */
|
|
2382
|
+
blume-tabs pre[data-language],
|
|
2383
|
+
.not-prose pre[data-language] {
|
|
2384
|
+
padding-top: ${CODE_PADDING_BLOCK_REM}rem;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/* Where no language bar holds the copy button — a flush block in tabs or a
|
|
2388
|
+
not-prose component, a bar-less block in prose — the docs layout still pins
|
|
2389
|
+
one over the block's top-right corner, so a first line long enough to reach
|
|
2390
|
+
it would end underneath. A code switcher (CodeGroup, ts2js) hosts the button
|
|
2391
|
+
in its tab strip instead and needs nothing here; for every block that keeps
|
|
2392
|
+
an overlay button, give the scrolling code element enough end padding that
|
|
2393
|
+
the line's tail clears the button at the end of its scroll. Keyed on the
|
|
2394
|
+
injected button itself (which the playground's client-created response pre
|
|
2395
|
+
never gets), and \`.prose\`-scoped to outrank the base \`:where(pre code)\`
|
|
2396
|
+
inset. Not a vertical inset — that reserved a strip above every one-line
|
|
2397
|
+
command and read as a rendering bug. */
|
|
2398
|
+
.prose :is(blume-tabs pre, .not-prose pre, pre:not([data-language])):not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> [data-blume-copy]) > code {
|
|
2399
|
+
padding-inline-end: 3.5rem;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
blume-tabs pre[data-language]::before,
|
|
2403
|
+
.not-prose pre[data-language]::before {
|
|
2404
|
+
content: none;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/* Pre-hydration layout-shift guard. The horizontal trigger row and
|
|
2408
|
+
single-panel visibility are built by the <blume-tabs> custom element's JS:
|
|
2409
|
+
until it upgrades, the trigger row is empty and every panel is in normal flow,
|
|
2410
|
+
so the browser paints all panels stacked vertically (e.g. all four
|
|
2411
|
+
package-manager commands at once) before JS hides them. While the element is
|
|
2412
|
+
:not(:defined), show only the first panel and reserve the trigger row's height
|
|
2413
|
+
so nothing jumps when JS takes over. The child selector covers both <Tab>
|
|
2414
|
+
panels and <CodeGroup> raw code blocks (which only gain
|
|
2415
|
+
[data-blume-tab-panel] from JS). */
|
|
2416
|
+
blume-tabs:not(:defined) [data-blume-tab-content] > *:nth-child(n + 2) {
|
|
2417
|
+
display: none;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
blume-tabs:not(:defined) [data-blume-tablist] {
|
|
2421
|
+
/* Matches a trigger's height: py-2.5 + text-xs line-height + border-b-2. */
|
|
2422
|
+
min-height: 2.375rem;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
blume-tabs:not(:defined)[data-dropdown="true"] [data-blume-tablist] {
|
|
2426
|
+
/* Dropdown variant: p-3 padding around a select (py-2 + text-sm + border). */
|
|
2427
|
+
min-height: 3.875rem;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/* Code blocks inside request/response example panels sit flush; the panel owns
|
|
2431
|
+
the frame. Unlayered (like the tab rules) so they beat the base \`pre\` styles
|
|
2432
|
+
that a class on the panel cannot. */
|
|
2433
|
+
[data-blume-code-panel] pre {
|
|
2434
|
+
background: transparent !important;
|
|
2435
|
+
border: 0;
|
|
2436
|
+
border-radius: 0;
|
|
2437
|
+
margin: 0;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/* Opt-in line numbers (\`\`\`ts file.ts lineNumbers): a counter-driven gutter that
|
|
2441
|
+
works in prose and flush (tab / API panel) code blocks alike. */
|
|
2442
|
+
pre[data-line-numbers] code {
|
|
2443
|
+
counter-reset: line;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
pre[data-line-numbers] .line::before {
|
|
2447
|
+
color: var(--blume-muted-foreground);
|
|
2448
|
+
content: counter(line);
|
|
2449
|
+
counter-increment: line;
|
|
2450
|
+
display: inline-block;
|
|
2451
|
+
margin-right: 1.25rem;
|
|
2452
|
+
text-align: right;
|
|
2453
|
+
user-select: none;
|
|
2454
|
+
width: 1.25rem;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
/* Shiki notation transformers (on by default). The notation comments are
|
|
2458
|
+
stripped from the output; these style the classes they leave behind. A styled
|
|
2459
|
+
line becomes a full-width inline-block so its background fills the row; plain
|
|
2460
|
+
lines are left untouched, so blocks without notations render as before. */
|
|
2461
|
+
.line.highlighted,
|
|
2462
|
+
.line.diff {
|
|
2463
|
+
display: inline-block;
|
|
2464
|
+
width: 100%;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/* The backgrounds need !important to beat the \`pre.astro-code span\` rule above,
|
|
2468
|
+
which forces token spans transparent; these line/word spans are exceptions. */
|
|
2469
|
+
.line.highlighted {
|
|
2470
|
+
background-color: var(--blume-code-highlight) !important;
|
|
2471
|
+
box-shadow: inset 2px 0 0 0 var(--blume-code-highlight-border);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
.line.diff.add {
|
|
2475
|
+
background-color: var(--blume-code-add) !important;
|
|
2476
|
+
box-shadow: inset 2px 0 0 0 var(--blume-code-add-border);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
.line.diff.remove {
|
|
2480
|
+
background-color: var(--blume-code-remove) !important;
|
|
2481
|
+
box-shadow: inset 2px 0 0 0 var(--blume-code-remove-border);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
/* Word highlight (\`// [!code word:x]\`) wraps matches in an inline span. The
|
|
2485
|
+
element selector keeps it ahead of the transparent token-span rule. */
|
|
2486
|
+
span.highlighted-word {
|
|
2487
|
+
background-color: var(--blume-code-word) !important;
|
|
2488
|
+
border-radius: 0.25rem;
|
|
2489
|
+
box-shadow: 0 0 0 1px var(--blume-code-word-border);
|
|
2490
|
+
padding: 0.1em 0.2em;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
/* Focus (\`// [!code focus]\`): dim and blur the rest; reveal on hover. */
|
|
2494
|
+
pre:has(.line.focused) .line:not(.focused) {
|
|
2495
|
+
filter: blur(0.085rem);
|
|
2496
|
+
opacity: 0.5;
|
|
2497
|
+
transition:
|
|
2498
|
+
filter 0.2s ease,
|
|
2499
|
+
opacity 0.2s ease;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
pre:has(.line.focused):hover .line:not(.focused) {
|
|
2503
|
+
filter: none;
|
|
2504
|
+
opacity: 1;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2508
|
+
pre:has(.line.focused) .line:not(.focused) {
|
|
2509
|
+
transition: none;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
@media (max-width: 640px) {
|
|
2514
|
+
.prose :where(h1) {
|
|
2515
|
+
font-size: 2.25rem;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
.prose :where(h2) {
|
|
2519
|
+
font-size: 1.625rem;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.prose :not(pre) > code:not(:where([class~="not-prose"] *)) {
|
|
2524
|
+
background: var(--blume-code-background);
|
|
2525
|
+
padding: 0.15em 0.35em;
|
|
2526
|
+
border-radius: 0.3rem;
|
|
2527
|
+
font-family: var(--font-mono);
|
|
2528
|
+
font-weight: 500;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
.prose :not(pre) > code::before,
|
|
2532
|
+
.prose :not(pre) > code::after {
|
|
2533
|
+
content: none;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/* Inline code highlighting: Shiki colors the tokens of a \`code\`{:lang} snippet
|
|
2537
|
+
via the same dual-theme CSS variables as fenced blocks, keeping the inline
|
|
2538
|
+
pill background. Always on — it only fires on the trailing {:lang} marker. */
|
|
2539
|
+
.prose code.blume-inline-code span {
|
|
2540
|
+
color: var(--shiki-light);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
:root[data-theme="dark"] .prose code.blume-inline-code span {
|
|
2544
|
+
color: var(--shiki-dark);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/* Print / "Save as PDF" (the page-actions Export → PDF runs window.print()).
|
|
2548
|
+
Strip the surrounding chrome so the printout is just the article. */
|
|
2549
|
+
@media print {
|
|
2550
|
+
[data-blume-banner],
|
|
2551
|
+
header,
|
|
2552
|
+
aside,
|
|
2553
|
+
[data-blume-page-actions],
|
|
2554
|
+
#blume-content > nav,
|
|
2555
|
+
#blume-content > details {
|
|
2556
|
+
display: none !important;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
/* Paper can't scroll: uncap the code scroller so long blocks print in
|
|
2560
|
+
full, same as tab panels are force-expanded below. */
|
|
2561
|
+
.prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
2562
|
+
max-height: none;
|
|
2563
|
+
overflow: visible;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
#blume-content {
|
|
2567
|
+
padding: 0 !important;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
.prose {
|
|
2571
|
+
margin: 0 !important;
|
|
2572
|
+
max-width: none !important;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
/* Reveal collapsed tab panels so their content isn't dropped from the print. */
|
|
2576
|
+
blume-tabs [data-blume-tab-panel] {
|
|
2577
|
+
display: block !important;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/* Keep code/callout backgrounds (subject to the dialog's "Background graphics"
|
|
2581
|
+
toggle). */
|
|
2582
|
+
pre,
|
|
2583
|
+
.prose :not(pre) > code {
|
|
2584
|
+
-webkit-print-color-adjust: exact;
|
|
2585
|
+
print-color-adjust: exact;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/* Twoslash rich-renderer styles (used by fences with the \`twoslash\` meta). */
|
|
2590
|
+
${options.twoslashCss ?? ""}
|
|
2591
|
+
|
|
2592
|
+
/* Token overrides: config first, then the user's theme.css (highest priority). */
|
|
2593
|
+
${options.configTokens}
|
|
2594
|
+
${options.userTheme}
|
|
2595
|
+
`;
|
|
2596
|
+
var examplesEntryTemplate = (options) => `/* Generated by Blume. Do not edit. */
|
|
2597
|
+
@import "tailwindcss";
|
|
2598
|
+
|
|
2599
|
+
/* Scan the project (and an out-of-root examples directory) for utility classes. */
|
|
2600
|
+
${options.sources.map((source) => `@source "${source}";`).join(`
|
|
2601
|
+
`)}
|
|
2602
|
+
|
|
2603
|
+
${DARK_VARIANT}
|
|
2604
|
+
|
|
2605
|
+
${COLOR_SCHEME_DEFAULTS}
|
|
2606
|
+
|
|
2607
|
+
${TOKEN_DEFAULTS}
|
|
2608
|
+
|
|
2609
|
+
${THEME_MAPPING}
|
|
2610
|
+
|
|
2611
|
+
/* Frame defaults: readable text in both modes, transparent so the docs pane's
|
|
2612
|
+
surface shows through. Base layer, so any user/example CSS wins. */
|
|
2613
|
+
@layer base {
|
|
2614
|
+
body {
|
|
2615
|
+
background: transparent;
|
|
2616
|
+
color: var(--blume-foreground);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
/* Token overrides: config first, then the configured examples css (highest
|
|
2621
|
+
priority) — the place for shadcn variables and other component tokens. */
|
|
2622
|
+
${options.configTokens}
|
|
2623
|
+
${options.userCss}
|
|
2624
|
+
`;
|
|
2625
|
+
|
|
2626
|
+
// src/theme/sources.ts
|
|
2627
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve5 } from "pathe";
|
|
2628
|
+
var SOURCE_DIRECTIVE = /@source(?<not>\s+not)?\s+(?<quote>["'])(?<path>[^"']+)\k<quote>/gu;
|
|
2629
|
+
var rebaseSourceDirectives = (css, options) => {
|
|
2630
|
+
const base = dirname5(options.from);
|
|
2631
|
+
return css.replace(SOURCE_DIRECTIVE, (directive, not, quote, path) => {
|
|
2632
|
+
if (isAbsolute4(path)) {
|
|
2633
|
+
return directive;
|
|
2634
|
+
}
|
|
2635
|
+
const rebased = relative2(options.to, resolve5(base, path));
|
|
2636
|
+
return `@source${not ?? ""} ${quote}${rebased}${quote}`;
|
|
2637
|
+
});
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
// src/theme/twoslash.ts
|
|
2641
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2642
|
+
import { createRequire } from "node:module";
|
|
2643
|
+
var require2 = createRequire(import.meta.url);
|
|
2644
|
+
var OVERRIDES = `
|
|
2645
|
+
/* Twoslash: theme the rich renderer with Blume tokens. */
|
|
2646
|
+
:root {
|
|
2647
|
+
--twoslash-border-color: var(--blume-border);
|
|
2648
|
+
--twoslash-popup-bg: var(--blume-code-background);
|
|
2649
|
+
--twoslash-popup-color: var(--blume-foreground);
|
|
2650
|
+
--twoslash-popup-shadow: 0 6px 24px oklch(0 0 0 / 0.18);
|
|
2651
|
+
--twoslash-docs-color: var(--blume-muted-foreground);
|
|
2652
|
+
--twoslash-docs-font: var(--font-sans);
|
|
2653
|
+
--twoslash-code-font: var(--font-mono);
|
|
2654
|
+
--twoslash-matched-color: var(--blume-foreground);
|
|
2655
|
+
--twoslash-unmatched-color: var(--blume-muted-foreground);
|
|
2656
|
+
--twoslash-cursor-color: var(--blume-muted-foreground);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/* Popups are absolutely positioned and must escape the pre's scroll container.
|
|
2660
|
+
The base prose pre rule (from the typography layer) wins the cascade here
|
|
2661
|
+
despite lower specificity, so !important is needed to force visibility.
|
|
2662
|
+
Regular code blocks scroll their inner code element and carry the horizontal
|
|
2663
|
+
padding there; twoslash code opts out of that scroller (popups again), so the
|
|
2664
|
+
padding is restored on the pre. */
|
|
2665
|
+
.prose pre.twoslash {
|
|
2666
|
+
overflow: visible !important;
|
|
2667
|
+
padding-left: 1.25rem;
|
|
2668
|
+
padding-right: 1.25rem;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
/* Twoslash blocks keep \`overflow: visible\` so popups can escape, which means
|
|
2672
|
+
long lines can't scroll — they'd push past the viewport on narrow screens.
|
|
2673
|
+
Wrap them on mobile instead; hover popups still position against the token. */
|
|
2674
|
+
@media (max-width: 640px) {
|
|
2675
|
+
.prose pre.twoslash code {
|
|
2676
|
+
white-space: pre-wrap;
|
|
2677
|
+
overflow-wrap: anywhere;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
/* The rich renderer renders each popup's type signature as a nested Shiki
|
|
2682
|
+
pre. Strip the code-block chrome (border, radius, padding, background) so it
|
|
2683
|
+
sits flush inside the popup, which owns the frame. */
|
|
2684
|
+
.prose pre.twoslash .twoslash-popup-container pre {
|
|
2685
|
+
background: transparent !important;
|
|
2686
|
+
border: 0 !important;
|
|
2687
|
+
border-radius: 0 !important;
|
|
2688
|
+
margin: 0 !important;
|
|
2689
|
+
padding: 0 !important;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
/* Re-assert chrome backgrounds the transparent token-span rule would clear. */
|
|
2693
|
+
.twoslash .twoslash-popup-container,
|
|
2694
|
+
.twoslash .twoslash-popup-arrow,
|
|
2695
|
+
.twoslash .twoslash-completion-cursor .twoslash-completion-list {
|
|
2696
|
+
background: var(--twoslash-popup-bg) !important;
|
|
2697
|
+
}
|
|
2698
|
+
.twoslash .twoslash-popup-error {
|
|
2699
|
+
background: var(--twoslash-error-bg) !important;
|
|
2700
|
+
}
|
|
2701
|
+
.twoslash .twoslash-highlighted {
|
|
2702
|
+
background: var(--twoslash-highlighted-bg) !important;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/* The error squiggle uses a background image the transparent rule clears, so
|
|
2706
|
+
draw it as a wavy underline instead. */
|
|
2707
|
+
.twoslash .twoslash-error {
|
|
2708
|
+
text-decoration: underline wavy var(--twoslash-error-color);
|
|
2709
|
+
text-underline-offset: 0.25em;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
.twoslash-popup-container {
|
|
2713
|
+
max-width: min(90vw, 36rem);
|
|
2714
|
+
}
|
|
2715
|
+
`;
|
|
2716
|
+
var twoslashCss = () => {
|
|
2717
|
+
const file = require2.resolve("@shikijs/twoslash/style-rich.css");
|
|
2718
|
+
return `${readFileSync2(file, "utf-8")}
|
|
2719
|
+
${OVERRIDES}`;
|
|
2720
|
+
};
|
|
2721
|
+
|
|
2722
|
+
// src/astro/component-slots.ts
|
|
2723
|
+
var EMPTY_MODULE = `// Generated by Blume. Do not edit.
|
|
2724
|
+
import type { ComponentOverride } from "blume/core/define-components.ts";
|
|
2725
|
+
export const mdxComponents: Record<string, ComponentOverride> = {};
|
|
2726
|
+
export const layoutOverrides: Record<string, ComponentOverride> = {};
|
|
2727
|
+
`;
|
|
2728
|
+
var attributeValue = (value) => value.replaceAll(/["\n\r]/gu, " ").trim();
|
|
2729
|
+
var CLIENT_LOAD = "client:load";
|
|
2730
|
+
var directiveFor = (override) => {
|
|
2731
|
+
const framework = override.source?.framework;
|
|
2732
|
+
switch (override.client) {
|
|
2733
|
+
case "idle": {
|
|
2734
|
+
return "client:idle";
|
|
2735
|
+
}
|
|
2736
|
+
case "visible": {
|
|
2737
|
+
return "client:visible";
|
|
2738
|
+
}
|
|
2739
|
+
case "media": {
|
|
2740
|
+
return override.media ? `client:media="${attributeValue(override.media)}"` : CLIENT_LOAD;
|
|
2741
|
+
}
|
|
2742
|
+
case "only": {
|
|
2743
|
+
return framework ? `client:only="${attributeValue(framework)}"` : CLIENT_LOAD;
|
|
2744
|
+
}
|
|
2745
|
+
default: {
|
|
2746
|
+
return CLIENT_LOAD;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
var importClause = (variable, name, path) => name === "default" ? `import ${variable} from ${JSON.stringify(path)};` : `import { ${name} as ${variable} } from ${JSON.stringify(path)};`;
|
|
2751
|
+
var wrapperContent = (override) => {
|
|
2752
|
+
const { name, path } = override.source;
|
|
2753
|
+
const clause = name === "default" ? `import Component from ${JSON.stringify(path)};` : `import { ${name} as Component } from ${JSON.stringify(path)};`;
|
|
2754
|
+
return `---
|
|
2755
|
+
// Generated by Blume. Do not edit.
|
|
2756
|
+
${clause}
|
|
2757
|
+
---
|
|
2758
|
+
<Component ${directiveFor(override)} {...Astro.props}><slot /></Component>
|
|
2759
|
+
`;
|
|
2760
|
+
};
|
|
2761
|
+
var sanitize = (value) => value.replaceAll(/[^A-Za-z0-9]/gu, (char) => `_${(char.codePointAt(0) ?? 0).toString(16)}_`);
|
|
2762
|
+
var planComponentSlots = (componentsFile, analysis) => {
|
|
2763
|
+
const frameworks = new Set;
|
|
2764
|
+
if (!componentsFile) {
|
|
2765
|
+
return { frameworks, module: EMPTY_MODULE, wrappers: [] };
|
|
2766
|
+
}
|
|
2767
|
+
if (!analysis) {
|
|
2768
|
+
return {
|
|
2769
|
+
frameworks,
|
|
2770
|
+
module: `// Generated by Blume. Do not edit.
|
|
2771
|
+
import overrides from ${JSON.stringify(componentsFile)};
|
|
2772
|
+
export const mdxComponents = overrides.mdx ?? {};
|
|
2773
|
+
export const layoutOverrides = overrides.layout ?? {};
|
|
2774
|
+
`,
|
|
2775
|
+
wrappers: []
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
const wrappers = [];
|
|
2779
|
+
const importLines = [];
|
|
2780
|
+
const mdxEntries = [];
|
|
2781
|
+
const layoutEntries = [];
|
|
2782
|
+
let counter = 0;
|
|
2783
|
+
const mdxOverrides = [...analysis.mdx, ...analysis.islands];
|
|
2784
|
+
const plan = (override, group, entries) => {
|
|
2785
|
+
const { source } = override;
|
|
2786
|
+
if (source?.framework) {
|
|
2787
|
+
frameworks.add(source.framework);
|
|
2788
|
+
}
|
|
2789
|
+
if (override.identifier && !override.client) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
if (!source) {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const variable = `__blumeSlot${counter}`;
|
|
2796
|
+
counter += 1;
|
|
2797
|
+
if (override.client) {
|
|
2798
|
+
const name = `${group}-${sanitize(override.key)}`;
|
|
2799
|
+
wrappers.push({ content: wrapperContent(override), name });
|
|
2800
|
+
importLines.push(importClause(variable, "default", `./component-slots/${name}.astro`));
|
|
2801
|
+
} else {
|
|
2802
|
+
importLines.push(importClause(variable, source.name, source.path));
|
|
2803
|
+
}
|
|
2804
|
+
entries.push(`${JSON.stringify(override.key)}: ${variable}`);
|
|
2805
|
+
};
|
|
2806
|
+
for (const override of mdxOverrides) {
|
|
2807
|
+
plan(override, "mdx", mdxEntries);
|
|
2808
|
+
}
|
|
2809
|
+
for (const override of analysis.layout) {
|
|
2810
|
+
plan(override, "layout", layoutEntries);
|
|
2811
|
+
}
|
|
2812
|
+
const moduleSource = `// Generated by Blume. Do not edit.
|
|
2813
|
+
import overrides from ${JSON.stringify(componentsFile)};
|
|
2814
|
+
${importLines.join(`
|
|
2815
|
+
`)}${importLines.length ? `
|
|
2816
|
+
` : ""}export const mdxComponents = { ...(overrides.mdx ?? {})${mdxEntries.length ? `, ${mdxEntries.join(", ")}` : ""} };
|
|
2817
|
+
export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.length ? `, ${layoutEntries.join(", ")}` : ""} };
|
|
2818
|
+
`;
|
|
2819
|
+
return { frameworks, module: moduleSource, wrappers };
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
// src/astro/examples.ts
|
|
2823
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2824
|
+
import pMap2 from "p-map";
|
|
2825
|
+
import { isAbsolute as isAbsolute5, join as join10, relative as relative3 } from "pathe";
|
|
2826
|
+
import { glob as glob2 } from "tinyglobby";
|
|
2827
|
+
|
|
2828
|
+
// src/astro/islands.ts
|
|
2829
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2830
|
+
import pMap from "p-map";
|
|
2831
|
+
import { basename, join as join9 } from "pathe";
|
|
2832
|
+
import { glob } from "tinyglobby";
|
|
2833
|
+
var DEFAULT_CLIENT = "visible";
|
|
2834
|
+
var READ_CONCURRENCY = 16;
|
|
2835
|
+
var VALID_MODES = new Set([
|
|
2836
|
+
"idle",
|
|
2837
|
+
"load",
|
|
2838
|
+
"only",
|
|
2839
|
+
"visible"
|
|
2840
|
+
]);
|
|
2841
|
+
var isClientMode = (mode) => VALID_MODES.has(mode);
|
|
2842
|
+
var FRAMEWORK_BY_EXT2 = new Map([
|
|
2843
|
+
["jsx", "react"],
|
|
2844
|
+
["svelte", "svelte"],
|
|
2845
|
+
["tsx", "react"],
|
|
2846
|
+
["vue", "vue"]
|
|
2847
|
+
]);
|
|
2848
|
+
var ISLAND_FILE = /\.(?<ext>jsx|svelte|tsx|vue)$/u;
|
|
2849
|
+
var CLIENT_EXPORT = /export\s+const\s+client\s*(?::[^=]+)?=\s*["'](?<mode>\w+)["']/u;
|
|
2850
|
+
var readClientMode = (source, file, warnings) => {
|
|
2851
|
+
const mode = source.match(CLIENT_EXPORT)?.groups?.mode;
|
|
2852
|
+
if (!mode) {
|
|
2853
|
+
return DEFAULT_CLIENT;
|
|
2854
|
+
}
|
|
2855
|
+
if (!isClientMode(mode)) {
|
|
2856
|
+
warnings.push(`Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`);
|
|
2857
|
+
return DEFAULT_CLIENT;
|
|
2858
|
+
}
|
|
2859
|
+
return mode;
|
|
2860
|
+
};
|
|
2861
|
+
var discoverIslands = async (root) => {
|
|
2862
|
+
const dir = join9(root, "islands");
|
|
2863
|
+
const matches = await glob(["**/*"], {
|
|
2864
|
+
absolute: true,
|
|
2865
|
+
cwd: dir,
|
|
2866
|
+
onlyFiles: true
|
|
2867
|
+
});
|
|
2868
|
+
const files = matches.toSorted();
|
|
2869
|
+
const sources = await pMap(files, (file) => readFile3(file, "utf-8"), {
|
|
2870
|
+
concurrency: READ_CONCURRENCY
|
|
2871
|
+
});
|
|
2872
|
+
const islands = [];
|
|
2873
|
+
const warnings = [];
|
|
2874
|
+
const seen = new Map;
|
|
2875
|
+
const collectIsland = (file, source) => {
|
|
2876
|
+
const base = basename(file);
|
|
2877
|
+
const ext = base.match(ISLAND_FILE)?.groups?.ext;
|
|
2878
|
+
const framework = ext ? FRAMEWORK_BY_EXT2.get(ext) : undefined;
|
|
2879
|
+
if (!framework) {
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const name = base.replace(ISLAND_FILE, "");
|
|
2883
|
+
if (!/^[A-Z][A-Za-z0-9_]*$/u.test(name)) {
|
|
2884
|
+
warnings.push(`Island "${file}" must have a PascalCase identifier filename to be used in MDX (letters, digits, and underscores only, e.g. Counter.tsx → <Counter />); skipping it.`);
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
const existing = seen.get(name);
|
|
2888
|
+
if (existing) {
|
|
2889
|
+
warnings.push(`Two islands both resolve to <${name}> ("${existing}" and "${file}"); ignoring the second. Give them distinct filenames.`);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
seen.set(name, file);
|
|
2893
|
+
islands.push({
|
|
2894
|
+
client: readClientMode(source, file, warnings),
|
|
2895
|
+
file,
|
|
2896
|
+
framework,
|
|
2897
|
+
name
|
|
2898
|
+
});
|
|
2899
|
+
};
|
|
2900
|
+
for (const [index, file] of files.entries()) {
|
|
2901
|
+
collectIsland(file, sources[index] ?? "");
|
|
2902
|
+
}
|
|
2903
|
+
return { islands, warnings };
|
|
2904
|
+
};
|
|
2905
|
+
|
|
2906
|
+
// src/astro/examples.ts
|
|
2907
|
+
var FRAMEWORK_BY_EXT3 = {
|
|
2908
|
+
astro: "astro",
|
|
2909
|
+
jsx: "react",
|
|
2910
|
+
svelte: "svelte",
|
|
2911
|
+
tsx: "react",
|
|
2912
|
+
vue: "vue"
|
|
2913
|
+
};
|
|
2914
|
+
var EXAMPLE_FILE = /\.(?<ext>astro|jsx|svelte|tsx|vue)$/u;
|
|
2915
|
+
var DEFAULT_EXAMPLE_GLOB = "**/*.{astro,jsx,svelte,tsx,vue}";
|
|
2916
|
+
var READ_CONCURRENCY2 = 16;
|
|
2917
|
+
var EXAMPLE_SCAN_GLOB = "**/*.{astro,jsx,svelte,ts,tsx,vue}";
|
|
2918
|
+
var exampleScanRoots = (root, examplesDir) => {
|
|
2919
|
+
const path = relative3(root, examplesDir);
|
|
2920
|
+
const outside = path.startsWith("..") || isAbsolute5(path);
|
|
2921
|
+
return outside ? [root, examplesDir] : [root];
|
|
2922
|
+
};
|
|
2923
|
+
var GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
2924
|
+
var splitGlobBase = (pattern) => {
|
|
2925
|
+
const segments = pattern.split("/");
|
|
2926
|
+
const firstMagic = segments.findIndex((segment) => GLOB_MAGIC.test(segment));
|
|
2927
|
+
return {
|
|
2928
|
+
base: segments.slice(0, firstMagic).join("/"),
|
|
2929
|
+
rest: segments.slice(firstMagic).join("/")
|
|
2930
|
+
};
|
|
2931
|
+
};
|
|
2932
|
+
var discoverExamples = async (root, pattern = "examples") => {
|
|
2933
|
+
const { base, rest } = GLOB_MAGIC.test(pattern) ? splitGlobBase(pattern) : { base: pattern, rest: DEFAULT_EXAMPLE_GLOB };
|
|
2934
|
+
const dir = join10(root, base);
|
|
2935
|
+
const matches = await glob2([rest], {
|
|
2936
|
+
absolute: true,
|
|
2937
|
+
cwd: dir,
|
|
2938
|
+
onlyFiles: true
|
|
2939
|
+
});
|
|
2940
|
+
const files = matches.toSorted();
|
|
2941
|
+
const sources = await pMap2(files, (file) => readFile4(file, "utf-8"), {
|
|
2942
|
+
concurrency: READ_CONCURRENCY2
|
|
2943
|
+
});
|
|
2944
|
+
const examples = [];
|
|
2945
|
+
const warnings = [];
|
|
2946
|
+
const seen = new Map;
|
|
2947
|
+
const collectExample = (file, source) => {
|
|
2948
|
+
const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
|
|
2949
|
+
const framework = ext ? FRAMEWORK_BY_EXT3[ext] : undefined;
|
|
2950
|
+
if (!(ext && framework)) {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
const path = relative3(dir, file).slice(0, -(ext.length + 1));
|
|
2954
|
+
const existing = seen.get(path);
|
|
2955
|
+
if (existing) {
|
|
2956
|
+
warnings.push(`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
seen.set(path, file);
|
|
2960
|
+
examples.push({
|
|
2961
|
+
client: framework === "astro" ? undefined : readClientMode(source, file, warnings),
|
|
2962
|
+
file,
|
|
2963
|
+
framework,
|
|
2964
|
+
lang: ext,
|
|
2965
|
+
path,
|
|
2966
|
+
source
|
|
2967
|
+
});
|
|
2968
|
+
};
|
|
2969
|
+
for (const [index, file] of files.entries()) {
|
|
2970
|
+
collectExample(file, sources[index] ?? "");
|
|
2971
|
+
}
|
|
2972
|
+
return { dir, examples, warnings };
|
|
2973
|
+
};
|
|
2974
|
+
var exampleMarkdownLookup = (examples) => Object.fromEntries(examples.map((example) => [
|
|
2975
|
+
example.path,
|
|
2976
|
+
{ lang: example.lang, source: example.source }
|
|
2977
|
+
]));
|
|
2978
|
+
|
|
2979
|
+
// src/astro/generate.ts
|
|
2980
|
+
var BLUME_SRC = join11(packageRoot(), "src");
|
|
2981
|
+
var canResolveFrom = (fromDir, spec) => {
|
|
2982
|
+
try {
|
|
2983
|
+
createRequire2(pathToFileURL(join11(fromDir, "_.js")).href).resolve(spec);
|
|
2984
|
+
return true;
|
|
2985
|
+
} catch {
|
|
2986
|
+
return false;
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
var resolveReactCompiler = (config, needsReact, pkgDir = packageRoot()) => {
|
|
2990
|
+
if (!(needsReact && config.react.compiler)) {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
try {
|
|
2994
|
+
return createRequire2(pathToFileURL(join11(pkgDir, "_.js")).href).resolve("babel-plugin-react-compiler");
|
|
2995
|
+
} catch {
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
var reactCompilerWarnings = (config, needsReact, compilerPath) => needsReact && config.react.compiler && !compilerPath ? [
|
|
3000
|
+
"React Compiler is enabled but `babel-plugin-react-compiler` could not be resolved; falling back to an uncompiled build. Reinstall Blume, or set `react: { compiler: false }` to silence this."
|
|
3001
|
+
] : [];
|
|
3002
|
+
var resolveAstroPackageJson = (modulesDir) => {
|
|
3003
|
+
try {
|
|
3004
|
+
return realpathSync(join11(modulesDir, "astro", "package.json"));
|
|
3005
|
+
} catch {
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
var resolvedAstroHit = (fromDir) => {
|
|
3010
|
+
let dir = normalize(fromDir);
|
|
3011
|
+
while (true) {
|
|
3012
|
+
const modulesDir = join11(dir, "node_modules");
|
|
3013
|
+
const pkg = resolveAstroPackageJson(modulesDir);
|
|
3014
|
+
if (pkg) {
|
|
3015
|
+
return { modulesDir, pkg };
|
|
3016
|
+
}
|
|
3017
|
+
const parent = dirname6(dir);
|
|
3018
|
+
if (parent === dir) {
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
dir = parent;
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
var sameRealDir = (a, b) => {
|
|
3025
|
+
try {
|
|
3026
|
+
return realpathSync(a) === realpathSync(b);
|
|
3027
|
+
} catch {
|
|
3028
|
+
return false;
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
var depsCandidates = (pkgDir) => [
|
|
3032
|
+
join11(pkgDir, "node_modules"),
|
|
3033
|
+
dirname6(pkgDir)
|
|
3034
|
+
];
|
|
3035
|
+
var candidateHolding = (pkgDir, ...segments) => depsCandidates(pkgDir).find((dir) => existsSync6(join11(dir, ...segments))) ?? null;
|
|
3036
|
+
var symlinkDir = async (target, link) => {
|
|
3037
|
+
try {
|
|
3038
|
+
await symlink(target, link, "dir");
|
|
3039
|
+
} catch {
|
|
3040
|
+
await symlink(target, link, "junction");
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
var linkDepsJunction = async (link, depsDir) => {
|
|
3044
|
+
let existing;
|
|
3045
|
+
try {
|
|
3046
|
+
existing = await lstat(link);
|
|
3047
|
+
} catch {
|
|
3048
|
+
existing = null;
|
|
3049
|
+
}
|
|
3050
|
+
if (existing) {
|
|
3051
|
+
if (!existing.isSymbolicLink()) {
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
try {
|
|
3055
|
+
if (resolve6(dirname6(link), await readlink(link)) === resolve6(depsDir)) {
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
} catch {}
|
|
3059
|
+
await rm2(link, { force: true });
|
|
3060
|
+
}
|
|
3061
|
+
await mkdir2(dirname6(link), { recursive: true });
|
|
3062
|
+
await symlinkDir(depsDir, link);
|
|
3063
|
+
};
|
|
3064
|
+
var readPkgVersion = (pkgJsonPath) => {
|
|
3065
|
+
if (!pkgJsonPath) {
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
3068
|
+
try {
|
|
3069
|
+
return JSON.parse(readFileSync3(pkgJsonPath, "utf-8")).version ?? null;
|
|
3070
|
+
} catch {
|
|
3071
|
+
return null;
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
var astroConflictWarning = (blumeAstroPkg, shadowAstroPkg) => {
|
|
3075
|
+
const blume = readPkgVersion(blumeAstroPkg);
|
|
3076
|
+
const shadow = readPkgVersion(shadowAstroPkg);
|
|
3077
|
+
const versions = blume && shadow ? `astro@${shadow} shadowing Blume's astro@${blume}` : "a second copy of Astro shadowing Blume's";
|
|
3078
|
+
const pin = blume ?? "<Blume's astro version>";
|
|
3079
|
+
return `Astro version conflict: another dependency hoisted ${versions} to the project root, so @astrojs/mdx binds to the wrong copy and the build fails on a missing export (e.g. "chunkToString"). A single symlink can't reconcile a split install — pin Blume's Astro by adding a package.json "overrides" (npm/bun/pnpm) or "resolutions" (yarn) entry { "astro": "${pin}" }, then reinstall. Run \`npm ls astro\` to find the dependency pulling the older copy.`;
|
|
3080
|
+
};
|
|
3081
|
+
var dropStaleDepsLink = async (link, pkgDir) => {
|
|
3082
|
+
let existing;
|
|
3083
|
+
try {
|
|
3084
|
+
existing = await lstat(link);
|
|
3085
|
+
} catch {
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
if (!existing.isSymbolicLink()) {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
let linkedBlume;
|
|
3092
|
+
let runningBlume;
|
|
3093
|
+
try {
|
|
3094
|
+
linkedBlume = await realpath(join11(link, "blume"));
|
|
3095
|
+
runningBlume = await realpath(pkgDir);
|
|
3096
|
+
} catch {
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (linkedBlume !== runningBlume) {
|
|
3100
|
+
await rm2(link, { force: true });
|
|
3101
|
+
}
|
|
3102
|
+
};
|
|
3103
|
+
var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
|
|
3104
|
+
const astroDir = candidateHolding(pkgDir, "astro");
|
|
3105
|
+
if (!astroDir) {
|
|
3106
|
+
return null;
|
|
3107
|
+
}
|
|
3108
|
+
const mdxDir = candidateHolding(pkgDir, "@astrojs", "mdx");
|
|
3109
|
+
const blumeAstro = resolveAstroPackageJson(astroDir);
|
|
3110
|
+
await dropStaleDepsLink(join11(outDir, "node_modules"), pkgDir);
|
|
3111
|
+
const outDirHit = resolvedAstroHit(outDir);
|
|
3112
|
+
const astroCorrect = blumeAstro !== null && outDirHit?.pkg === blumeAstro;
|
|
3113
|
+
const walkLandsInDeps = astroCorrect && outDirHit !== null && sameRealDir(outDirHit.modulesDir, astroDir);
|
|
3114
|
+
if (walkLandsInDeps && mdxDir === astroDir) {
|
|
3115
|
+
return null;
|
|
3116
|
+
}
|
|
3117
|
+
if (mdxDir && (mdxDir === astroDir || astroCorrect)) {
|
|
3118
|
+
await linkDepsJunction(join11(outDir, "node_modules"), mdxDir);
|
|
3119
|
+
return null;
|
|
3120
|
+
}
|
|
3121
|
+
return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
|
|
3122
|
+
};
|
|
3123
|
+
var ISLAND_FRAMEWORK_DEPS = new Map([
|
|
3124
|
+
["svelte", "@astrojs/svelte"],
|
|
3125
|
+
["vue", "@astrojs/vue"]
|
|
3126
|
+
]);
|
|
3127
|
+
var DEPLOYMENT_ADAPTER_DEPS = new Map([
|
|
3128
|
+
["cloudflare", "@astrojs/cloudflare"],
|
|
3129
|
+
["netlify", "@astrojs/netlify"]
|
|
3130
|
+
]);
|
|
3131
|
+
var islandFrameworkWarnings = (frameworks, root) => {
|
|
3132
|
+
const warnings = [];
|
|
3133
|
+
for (const framework of frameworks) {
|
|
3134
|
+
const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
|
|
3135
|
+
if (dep && !canResolveFrom(root, dep)) {
|
|
3136
|
+
warnings.push(`Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
return warnings;
|
|
3140
|
+
};
|
|
3141
|
+
var deploymentAdapterWarnings = (deployment, root, pkgDir = packageRoot()) => {
|
|
3142
|
+
const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter) : undefined;
|
|
3143
|
+
if (dep && !(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
|
|
3144
|
+
return [
|
|
3145
|
+
`Deployment adapter "${deployment.adapter}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
|
|
3146
|
+
];
|
|
3147
|
+
}
|
|
3148
|
+
return [];
|
|
3149
|
+
};
|
|
3150
|
+
var searchProviderWarnings = (provider, root, pkgDir = packageRoot()) => {
|
|
3151
|
+
const warnings = [];
|
|
3152
|
+
for (const dep of searchProviderMeta(provider).runtimeDeps) {
|
|
3153
|
+
if (!(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
|
|
3154
|
+
warnings.push(`Search provider "${provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
return warnings;
|
|
3158
|
+
};
|
|
3159
|
+
var askProviderWarnings = (ask, root, pkgDir = packageRoot()) => {
|
|
3160
|
+
if (!ask?.enabled || ask.endpoint) {
|
|
3161
|
+
return [];
|
|
3162
|
+
}
|
|
3163
|
+
const dep = askBackendRuntimeDep(ask);
|
|
3164
|
+
if (!dep || canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep)) {
|
|
3165
|
+
return [];
|
|
3166
|
+
}
|
|
3167
|
+
return [
|
|
3168
|
+
`Ask AI provider "${ask.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
|
|
3169
|
+
];
|
|
3170
|
+
};
|
|
3171
|
+
var examplesCssFile = (root, config) => config.examples.css ? join11(root, config.examples.css) : null;
|
|
3172
|
+
var writeExamplesPreview = async (options) => {
|
|
3173
|
+
const { config, hasExamples, root, srcDir, write } = options;
|
|
3174
|
+
if (hasExamples) {
|
|
3175
|
+
await write(join11(srcDir, "pages", ...config.basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro"), examplesPageTemplate());
|
|
3176
|
+
}
|
|
3177
|
+
const cssFile = examplesCssFile(root, config);
|
|
3178
|
+
return cssFile && !existsSync6(cssFile) ? [
|
|
3179
|
+
`examples.css points at "${config.examples.css}", which doesn't exist; previews render without it.`
|
|
3180
|
+
] : [];
|
|
3181
|
+
};
|
|
3182
|
+
var readOptional = async (path) => {
|
|
3183
|
+
if (!path) {
|
|
3184
|
+
return "";
|
|
3185
|
+
}
|
|
3186
|
+
try {
|
|
3187
|
+
return await readFile5(path, "utf-8");
|
|
3188
|
+
} catch {
|
|
3189
|
+
return "";
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
var readUserCss = async (file, outputDir) => {
|
|
3193
|
+
const css = await readOptional(file);
|
|
3194
|
+
return file ? rebaseSourceDirectives(css, { from: file, to: outputDir }) : css;
|
|
3195
|
+
};
|
|
3196
|
+
var detectNeedsReact = async (root) => {
|
|
3197
|
+
const matches = await glob3(["**/*.{tsx,jsx}"], {
|
|
3198
|
+
cwd: root,
|
|
3199
|
+
ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
|
|
3200
|
+
onlyFiles: true
|
|
3201
|
+
});
|
|
3202
|
+
return matches.length > 0;
|
|
3203
|
+
};
|
|
3204
|
+
var containsMath = (content) => content.includes("$$") || content.includes("<Math");
|
|
3205
|
+
var READ_CONCURRENCY3 = 16;
|
|
3206
|
+
var detectUsesMath = async (root, staged = []) => {
|
|
3207
|
+
const files = await glob3(["**/*.{md,mdx}"], {
|
|
3208
|
+
cwd: root,
|
|
3209
|
+
ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
|
|
3210
|
+
onlyFiles: true
|
|
3211
|
+
});
|
|
3212
|
+
const contents = await pMap3(files, (file) => readOptional(join11(root, file)), {
|
|
3213
|
+
concurrency: READ_CONCURRENCY3
|
|
3214
|
+
});
|
|
3215
|
+
return [...contents, ...staged].some(containsMath);
|
|
3216
|
+
};
|
|
3217
|
+
var hashConfigSource = (source) => createHash("sha256").update(source).digest("hex");
|
|
3218
|
+
var loadIntegrationBridge = async (config, context) => {
|
|
3219
|
+
if (config.integrations.length === 0 || !context.configFile) {
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
return {
|
|
3223
|
+
configFile: relative4(context.outDir, context.configFile),
|
|
3224
|
+
sourceHash: hashConfigSource(await readOptional(context.configFile))
|
|
3225
|
+
};
|
|
3226
|
+
};
|
|
3227
|
+
var writeIfChanged = async (path, content) => {
|
|
3228
|
+
let existing = null;
|
|
3229
|
+
try {
|
|
3230
|
+
existing = await readFile5(path, "utf-8");
|
|
3231
|
+
} catch {
|
|
3232
|
+
existing = null;
|
|
3233
|
+
}
|
|
3234
|
+
if (existing === content) {
|
|
3235
|
+
return false;
|
|
3236
|
+
}
|
|
3237
|
+
await writeTextAtomic(path, content);
|
|
3238
|
+
return true;
|
|
3239
|
+
};
|
|
3240
|
+
var pruneOrphans = async (srcDir, written) => {
|
|
3241
|
+
const existing = await glob3("**/*", {
|
|
3242
|
+
absolute: true,
|
|
3243
|
+
cwd: srcDir,
|
|
3244
|
+
onlyFiles: true
|
|
3245
|
+
});
|
|
3246
|
+
const removals = [];
|
|
3247
|
+
for (const path of existing) {
|
|
3248
|
+
const normalized = normalize(path);
|
|
3249
|
+
if (!written.has(normalized)) {
|
|
3250
|
+
removals.push(rm2(normalized, { force: true }));
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
await Promise.all(removals);
|
|
3254
|
+
};
|
|
3255
|
+
var collectStaged = (project) => {
|
|
3256
|
+
const staged = new Map;
|
|
3257
|
+
for (const page of project.graph.pages) {
|
|
3258
|
+
if (page.collection === "staged" && page.entryId && page.body) {
|
|
3259
|
+
const text = page.sourcePath ? rewriteRelativeImages({
|
|
3260
|
+
deployBase: project.config.deployment.base,
|
|
3261
|
+
projectRoot: project.context.root,
|
|
3262
|
+
source: page.body.text,
|
|
3263
|
+
sourcePath: page.sourcePath
|
|
3264
|
+
}) : page.body.text;
|
|
3265
|
+
staged.set(page.entryId, text);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
return staged;
|
|
3269
|
+
};
|
|
3270
|
+
var writeStagedContent = async (out, staged) => {
|
|
3271
|
+
const contentDir = stagedContentDir(out);
|
|
3272
|
+
const written = new Set;
|
|
3273
|
+
await Promise.all([...staged].map(async ([entryId, text]) => {
|
|
3274
|
+
const path = join11(contentDir, entryId);
|
|
3275
|
+
written.add(normalize(path));
|
|
3276
|
+
await writeIfChanged(path, text);
|
|
3277
|
+
}));
|
|
3278
|
+
if (existsSync6(contentDir)) {
|
|
3279
|
+
await pruneOrphans(contentDir, written);
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
var logoDimensions = (svg) => svg ? svgDimensions(svg) ?? undefined : undefined;
|
|
3283
|
+
var readLogoSvg = (project, source) => {
|
|
3284
|
+
if (!source?.toLowerCase().endsWith(".svg")) {
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
const rel = source.replace(/^\//u, "");
|
|
3288
|
+
const file = [
|
|
3289
|
+
join11(project.context.root, "public", rel),
|
|
3290
|
+
join11(project.context.root, rel)
|
|
3291
|
+
].find((path) => existsSync6(path));
|
|
3292
|
+
return file ? readFileSync3(file, "utf-8") : undefined;
|
|
3293
|
+
};
|
|
3294
|
+
var isStringShorthand = (value) => typeof value === "string";
|
|
3295
|
+
var headerRepoUrl = (repo, derived) => {
|
|
3296
|
+
if (isStringShorthand(repo)) {
|
|
3297
|
+
return repo;
|
|
3298
|
+
}
|
|
3299
|
+
return repo ? derived : null;
|
|
3300
|
+
};
|
|
3301
|
+
var resolveLogo = (project) => {
|
|
3302
|
+
const { logo } = project.config;
|
|
3303
|
+
if (!logo) {
|
|
3304
|
+
return null;
|
|
3305
|
+
}
|
|
3306
|
+
const config = isStringShorthand(logo) ? { image: logo } : logo;
|
|
3307
|
+
const { href, image: source, text } = config;
|
|
3308
|
+
const image = isStringShorthand(source) ? { light: source } : source;
|
|
3309
|
+
const light = image?.light ?? image?.dark;
|
|
3310
|
+
const dark = image?.dark ?? image?.light;
|
|
3311
|
+
const alt = image?.alt ?? "";
|
|
3312
|
+
const brandHref = href ?? "/";
|
|
3313
|
+
const lightSvg = readLogoSvg(project, light);
|
|
3314
|
+
const darkSvg = dark === light ? lightSvg : readLogoSvg(project, dark);
|
|
3315
|
+
if (light && light === dark && lightSvg) {
|
|
3316
|
+
return { alt, href: brandHref, svg: lightSvg, text };
|
|
3317
|
+
}
|
|
3318
|
+
const lightDimensions = logoDimensions(lightSvg);
|
|
3319
|
+
const darkDimensions = logoDimensions(darkSvg);
|
|
3320
|
+
const dimensions = lightDimensions || darkDimensions ? { dark: darkDimensions, light: lightDimensions } : undefined;
|
|
3321
|
+
return { alt, dark, dimensions, href: brandHref, light, text };
|
|
3322
|
+
};
|
|
3323
|
+
var FAVICON_CANDIDATES = [
|
|
3324
|
+
"icon.svg",
|
|
3325
|
+
"favicon.svg",
|
|
3326
|
+
"icon.png",
|
|
3327
|
+
"favicon.png",
|
|
3328
|
+
"favicon.ico",
|
|
3329
|
+
"icon.ico"
|
|
3330
|
+
];
|
|
3331
|
+
var darkSibling = (name) => name.replace(/\.(?=[^.]+$)/u, "-dark.");
|
|
3332
|
+
var FAVICON_TYPES = new Map([
|
|
3333
|
+
["ico", "image/x-icon"],
|
|
3334
|
+
["jpeg", "image/jpeg"],
|
|
3335
|
+
["jpg", "image/jpeg"],
|
|
3336
|
+
["png", "image/png"],
|
|
3337
|
+
["svg", "image/svg+xml"]
|
|
3338
|
+
]);
|
|
3339
|
+
var faviconType = (name) => {
|
|
3340
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
3341
|
+
return ext ? FAVICON_TYPES.get(ext) : undefined;
|
|
3342
|
+
};
|
|
3343
|
+
var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync3(file).toString("base64")}`;
|
|
3344
|
+
var defaultFavicon = () => ({
|
|
3345
|
+
dark: {
|
|
3346
|
+
href: inlineDataUri(join11(BLUME_SRC, "assets", "icon-dark.png"), "image/png"),
|
|
3347
|
+
type: "image/png"
|
|
3348
|
+
},
|
|
3349
|
+
href: inlineDataUri(join11(BLUME_SRC, "assets", "icon.png"), "image/png"),
|
|
3350
|
+
type: "image/png"
|
|
3351
|
+
});
|
|
3352
|
+
var APPLE_ICON_CANDIDATES = [
|
|
3353
|
+
"apple-icon.png",
|
|
3354
|
+
"apple-icon.jpg",
|
|
3355
|
+
"apple-icon.jpeg",
|
|
3356
|
+
"apple-touch-icon.png"
|
|
3357
|
+
];
|
|
3358
|
+
var resolveIconFile = (project, candidates) => {
|
|
3359
|
+
const { root } = project.context;
|
|
3360
|
+
for (const name of candidates) {
|
|
3361
|
+
if (existsSync6(join11(root, "public", name))) {
|
|
3362
|
+
return { href: `/${name}`, type: faviconType(name) };
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
for (const name of candidates) {
|
|
3366
|
+
const file = join11(root, name);
|
|
3367
|
+
if (existsSync6(file)) {
|
|
3368
|
+
const type = faviconType(name);
|
|
3369
|
+
return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
return null;
|
|
3373
|
+
};
|
|
3374
|
+
var resolveFavicon = (project) => {
|
|
3375
|
+
const { root } = project.context;
|
|
3376
|
+
for (const name of FAVICON_CANDIDATES) {
|
|
3377
|
+
if (existsSync6(join11(root, "public", name))) {
|
|
3378
|
+
const type = faviconType(name);
|
|
3379
|
+
const sibling = darkSibling(name);
|
|
3380
|
+
return existsSync6(join11(root, "public", sibling)) ? { dark: { href: `/${sibling}`, type }, href: `/${name}`, type } : { href: `/${name}`, type };
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
for (const name of FAVICON_CANDIDATES) {
|
|
3384
|
+
const file = join11(root, name);
|
|
3385
|
+
if (existsSync6(file)) {
|
|
3386
|
+
const type = faviconType(name);
|
|
3387
|
+
const mime = type ?? "image/x-icon";
|
|
3388
|
+
const siblingFile = join11(root, darkSibling(name));
|
|
3389
|
+
return existsSync6(siblingFile) ? {
|
|
3390
|
+
dark: { href: inlineDataUri(siblingFile, mime), type },
|
|
3391
|
+
href: inlineDataUri(file, mime),
|
|
3392
|
+
type
|
|
3393
|
+
} : { href: inlineDataUri(file, mime), type };
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
return resolveIconFile(project, FAVICON_CANDIDATES.map(darkSibling)) ?? defaultFavicon();
|
|
3397
|
+
};
|
|
3398
|
+
var resolveAppleIcon = (project) => resolveIconFile(project, APPLE_ICON_CANDIDATES);
|
|
3399
|
+
var resolveBanner = (config) => {
|
|
3400
|
+
const { banner } = config;
|
|
3401
|
+
if (!banner) {
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
if (isStringShorthand(banner)) {
|
|
3405
|
+
return { content: banner, dismissible: false, key: banner };
|
|
3406
|
+
}
|
|
3407
|
+
return {
|
|
3408
|
+
content: banner.content,
|
|
3409
|
+
dismissible: banner.dismissible,
|
|
3410
|
+
key: banner.id ?? banner.content,
|
|
3411
|
+
link: banner.link
|
|
3412
|
+
};
|
|
3413
|
+
};
|
|
3414
|
+
var resolveOgMark = (project, siteLogo) => {
|
|
3415
|
+
const configured = project.config.seo.og.logo;
|
|
3416
|
+
if (configured === false) {
|
|
3417
|
+
return false;
|
|
3418
|
+
}
|
|
3419
|
+
if (configured) {
|
|
3420
|
+
return resolveOgLogo(project, configured);
|
|
3421
|
+
}
|
|
3422
|
+
return siteLogo;
|
|
3423
|
+
};
|
|
3424
|
+
var resolveOgSite = (config) => {
|
|
3425
|
+
const configured = config.seo.og.site;
|
|
3426
|
+
if (configured === false) {
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
if (configured !== undefined) {
|
|
3430
|
+
return configured;
|
|
3431
|
+
}
|
|
3432
|
+
return config.deployment.site ? `${new URL(config.deployment.site).host}${normalizeBasePath(config.deployment.base)}` : undefined;
|
|
3433
|
+
};
|
|
3434
|
+
var resolveOgDescription = (config) => {
|
|
3435
|
+
const configured = config.seo.og.description;
|
|
3436
|
+
if (configured === false) {
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
return configured ?? config.description;
|
|
3440
|
+
};
|
|
3441
|
+
var resolveGithubData = (github) => github ? {
|
|
3442
|
+
api: apiUrl(github),
|
|
3443
|
+
host: github.host,
|
|
3444
|
+
owner: github.owner,
|
|
3445
|
+
repo: github.repo
|
|
3446
|
+
} : null;
|
|
3447
|
+
var buildRuntimeData = (project) => {
|
|
3448
|
+
const { config, context, graph, manifest } = project;
|
|
3449
|
+
const { github } = config;
|
|
3450
|
+
const repoUrl2 = github ? repoUrl(github) : null;
|
|
3451
|
+
const editBase = github ? editBaseUrl(github) : null;
|
|
3452
|
+
const logo = resolveLogo(project);
|
|
3453
|
+
const ogLogo = resolveOgMark(project, logo?.svg);
|
|
3454
|
+
const editUrlFor = (sourcePath) => {
|
|
3455
|
+
if (!(editBase && sourcePath)) {
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
const rel = relative4(context.root, sourcePath).split("\\").join("/");
|
|
3459
|
+
const editDir = trimChar(github?.dir ?? "", "/");
|
|
3460
|
+
const editPath = normalize(editDir ? join11(editDir, rel) : rel);
|
|
3461
|
+
if (editPath.startsWith("..") || isAbsolute6(editPath)) {
|
|
3462
|
+
return null;
|
|
3463
|
+
}
|
|
3464
|
+
return `${editBase}/${editPath}`;
|
|
3465
|
+
};
|
|
3466
|
+
const { i18n } = config;
|
|
3467
|
+
const markUrl = headerRepoUrl(config.navigation.repo, repoUrl2);
|
|
3468
|
+
const withRepoUrl = (nav) => ({
|
|
3469
|
+
...nav,
|
|
3470
|
+
repoUrl: markUrl
|
|
3471
|
+
});
|
|
3472
|
+
const uiByLocale = i18n ? Object.fromEntries(i18n.locales.map(({ code }) => [
|
|
3473
|
+
code,
|
|
3474
|
+
resolveUIStrings(code, {
|
|
3475
|
+
defaultLocale: i18n.defaultLocale,
|
|
3476
|
+
overrides: i18n.ui
|
|
3477
|
+
})
|
|
3478
|
+
])) : {};
|
|
3479
|
+
const defaultUi = i18n ? resolveUIStrings(i18n.defaultLocale, {
|
|
3480
|
+
defaultLocale: i18n.defaultLocale,
|
|
3481
|
+
overrides: i18n.ui
|
|
3482
|
+
}) : EN_UI;
|
|
3483
|
+
const navigationByLocale = i18n ? Object.fromEntries(i18n.locales.map(({ code }) => [
|
|
3484
|
+
code,
|
|
3485
|
+
withRepoUrl(graph.navigationByLocale[code] ?? {
|
|
3486
|
+
actions: [],
|
|
3487
|
+
cta: null,
|
|
3488
|
+
featured: [],
|
|
3489
|
+
selectors: [],
|
|
3490
|
+
sidebar: [],
|
|
3491
|
+
tabs: []
|
|
3492
|
+
})
|
|
3493
|
+
])) : {};
|
|
3494
|
+
const data = {
|
|
3495
|
+
config: {
|
|
3496
|
+
analytics: config.analytics ?? null,
|
|
3497
|
+
appleIcon: resolveAppleIcon(project),
|
|
3498
|
+
ask: config.ai.ask?.enabled ? {
|
|
3499
|
+
endpoint: config.ai.ask.endpoint ?? null,
|
|
3500
|
+
suggestions: config.ai.ask.suggestions
|
|
3501
|
+
} : null,
|
|
3502
|
+
banner: resolveBanner(config),
|
|
3503
|
+
basePath: config.basePath,
|
|
3504
|
+
codeThemes: config.markdown.codeBlocks.theme,
|
|
3505
|
+
codeWrap: config.markdown.code.wrap,
|
|
3506
|
+
dateFormat: config.dateFormat,
|
|
3507
|
+
description: config.description,
|
|
3508
|
+
discovery: {
|
|
3509
|
+
agentReadability: config.seo.agentReadability,
|
|
3510
|
+
api: config.ai.api,
|
|
3511
|
+
llmsTxt: config.ai.llmsTxt.enabled,
|
|
3512
|
+
sitemap: config.seo.sitemap && Boolean(config.deployment.site)
|
|
3513
|
+
},
|
|
3514
|
+
favicon: resolveFavicon(project),
|
|
3515
|
+
feedback: config.feedback,
|
|
3516
|
+
github: resolveGithubData(github),
|
|
3517
|
+
i18n: i18n ? {
|
|
3518
|
+
defaultLocale: i18n.defaultLocale,
|
|
3519
|
+
fallbackLocale: resolveFallbackLocale(i18n),
|
|
3520
|
+
hideDefaultLocalePrefix: i18n.hideDefaultLocalePrefix,
|
|
3521
|
+
locales: i18n.locales.map(({ code, dir, label }) => ({
|
|
3522
|
+
code,
|
|
3523
|
+
dir,
|
|
3524
|
+
label
|
|
3525
|
+
}))
|
|
3526
|
+
} : null,
|
|
3527
|
+
identity: config.seo.organization || config.seo.software ? {
|
|
3528
|
+
organization: config.seo.organization,
|
|
3529
|
+
software: config.seo.software
|
|
3530
|
+
} : null,
|
|
3531
|
+
imageZoom: config.markdown.imageZoom,
|
|
3532
|
+
logo,
|
|
3533
|
+
mcp: config.ai.mcp.enabled ? {
|
|
3534
|
+
name: config.ai.mcp.name ?? config.title,
|
|
3535
|
+
route: config.ai.mcp.route
|
|
3536
|
+
} : null,
|
|
3537
|
+
og: {
|
|
3538
|
+
description: resolveOgDescription(config),
|
|
3539
|
+
enabled: config.seo.og.enabled ?? false,
|
|
3540
|
+
logo: ogLogo,
|
|
3541
|
+
palette: config.seo.og.palette,
|
|
3542
|
+
site: resolveOgSite(config)
|
|
3543
|
+
},
|
|
3544
|
+
openInChat: config.ai.openInChat,
|
|
3545
|
+
repoUrl: repoUrl2,
|
|
3546
|
+
search: {
|
|
3547
|
+
enabled: config.search.provider !== "none",
|
|
3548
|
+
popular: resolveSearchPopular(config.search.popular, config.basePath),
|
|
3549
|
+
provider: config.search.provider
|
|
3550
|
+
},
|
|
3551
|
+
site: config.deployment.site ?? null,
|
|
3552
|
+
structuredData: config.seo.structuredData,
|
|
3553
|
+
theme: config.theme,
|
|
3554
|
+
title: config.title,
|
|
3555
|
+
toc: config.toc,
|
|
3556
|
+
versions: config.versions ?? null,
|
|
3557
|
+
webmcp: {
|
|
3558
|
+
enabled: config.ai.webmcp,
|
|
3559
|
+
llms: config.ai.llmsTxt.enabled
|
|
3560
|
+
},
|
|
3561
|
+
x: config.seo.x
|
|
3562
|
+
},
|
|
3563
|
+
feeds: buildRssFeeds(project).map((feed) => ({
|
|
3564
|
+
href: feed.path,
|
|
3565
|
+
title: feed.title
|
|
3566
|
+
})),
|
|
3567
|
+
fontCssVars: configuredFonts(config.theme.fonts, fontLocaleCodes(config.i18n)),
|
|
3568
|
+
navigation: withRepoUrl(graph.navigation),
|
|
3569
|
+
navigationByLocale,
|
|
3570
|
+
navigationByVersion: Object.fromEntries(Object.entries(graph.navigationByVersion).map(([id, byLocale]) => [
|
|
3571
|
+
id,
|
|
3572
|
+
Object.fromEntries(Object.entries(byLocale).map(([code, nav]) => [
|
|
3573
|
+
code,
|
|
3574
|
+
withRepoUrl(nav)
|
|
3575
|
+
]))
|
|
3576
|
+
])),
|
|
3577
|
+
routes: manifest.routes.map((route) => ({
|
|
3578
|
+
alternates: route.alternates,
|
|
3579
|
+
collection: route.collection,
|
|
3580
|
+
description: route.description ?? null,
|
|
3581
|
+
draft: route.draft,
|
|
3582
|
+
editUrl: route.editUrl ?? editUrlFor(route.sourcePath),
|
|
3583
|
+
entryId: route.entryId,
|
|
3584
|
+
fallback: route.fallback ?? false,
|
|
3585
|
+
hidden: route.hidden,
|
|
3586
|
+
id: route.id,
|
|
3587
|
+
indexable: route.indexable,
|
|
3588
|
+
lastModified: route.lastModified ?? null,
|
|
3589
|
+
locale: route.locale,
|
|
3590
|
+
path: route.path,
|
|
3591
|
+
title: route.title,
|
|
3592
|
+
version: route.version,
|
|
3593
|
+
versionAlternates: route.versionAlternates
|
|
3594
|
+
})),
|
|
3595
|
+
ui: defaultUi,
|
|
3596
|
+
uiByLocale
|
|
3597
|
+
};
|
|
3598
|
+
return `${JSON.stringify(data, null, 2)}
|
|
3599
|
+
`;
|
|
3600
|
+
};
|
|
3601
|
+
var planMcp = (project, srcDir, userPages) => {
|
|
3602
|
+
const { config } = project;
|
|
3603
|
+
const { route } = config.ai.mcp;
|
|
3604
|
+
const dir = join11(srcDir, "blume-mcp");
|
|
3605
|
+
const base = {
|
|
3606
|
+
dir,
|
|
3607
|
+
discoveryPages: [],
|
|
3608
|
+
enabled: false,
|
|
3609
|
+
route,
|
|
3610
|
+
srcDir,
|
|
3611
|
+
warnings: []
|
|
3612
|
+
};
|
|
3613
|
+
if (!config.ai.mcp.enabled) {
|
|
3614
|
+
return base;
|
|
3615
|
+
}
|
|
3616
|
+
if (routeIsTaken(userPages, project.graph.pages, route)) {
|
|
3617
|
+
return {
|
|
3618
|
+
...base,
|
|
3619
|
+
warnings: [
|
|
3620
|
+
`MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "ai.mcp.route" in blume.config.ts.`
|
|
3621
|
+
]
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
return {
|
|
3625
|
+
...base,
|
|
3626
|
+
discoveryPages: [
|
|
3627
|
+
{
|
|
3628
|
+
entrypoint: join11(dir, "discovery.ts"),
|
|
3629
|
+
pattern: "/.well-known/mcp.json"
|
|
3630
|
+
},
|
|
3631
|
+
{
|
|
3632
|
+
entrypoint: join11(dir, "server-card.ts"),
|
|
3633
|
+
pattern: "/.well-known/mcp/server-card.json"
|
|
3634
|
+
}
|
|
3635
|
+
],
|
|
3636
|
+
enabled: true
|
|
3637
|
+
};
|
|
3638
|
+
};
|
|
3639
|
+
var publishAgentData = async (project, plans, modules) => {
|
|
3640
|
+
if (!(plans.mcp.enabled || plans.api.enabled)) {
|
|
3641
|
+
return null;
|
|
3642
|
+
}
|
|
3643
|
+
const data = await buildMcpData(project);
|
|
3644
|
+
modules.set("blume:mcp-data", JSON.stringify(data));
|
|
3645
|
+
return data;
|
|
3646
|
+
};
|
|
3647
|
+
var writeMcpFiles = async (plan, write, data) => {
|
|
3648
|
+
if (!(plan.enabled && data)) {
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
const discoveryInput = {
|
|
3652
|
+
base: data.base,
|
|
3653
|
+
name: data.name,
|
|
3654
|
+
route: plan.route,
|
|
3655
|
+
site: data.site,
|
|
3656
|
+
version: data.version
|
|
3657
|
+
};
|
|
3658
|
+
await Promise.all([
|
|
3659
|
+
write(join11(plan.srcDir, "pages", mcpPageFile(plan.route)), mcpEndpointTemplate()),
|
|
3660
|
+
write(join11(plan.dir, "discovery.ts"), staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput))),
|
|
3661
|
+
write(join11(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
|
|
3662
|
+
]);
|
|
3663
|
+
};
|
|
3664
|
+
var ownsApiRest = (page) => page.pattern.startsWith("/api/[");
|
|
3665
|
+
var contentUnderApi = (page) => page.route === "/api" || page.route.startsWith("/api/");
|
|
3666
|
+
var planApi = (project, srcDir, userPages) => {
|
|
3667
|
+
const { config, context } = project;
|
|
3668
|
+
const server = config.deployment.output === "server";
|
|
3669
|
+
return {
|
|
3670
|
+
catchAll: server && !userPages.some(ownsApiRest) && !project.graph.pages.some(contentUnderApi),
|
|
3671
|
+
enabled: config.ai.api,
|
|
3672
|
+
server,
|
|
3673
|
+
spec: !routeIsTaken(userPages, project.graph.pages, OPENAPI_PATH) && !existsSync6(join11(context.root, "public", "openapi.json")),
|
|
3674
|
+
srcDir
|
|
3675
|
+
};
|
|
3676
|
+
};
|
|
3677
|
+
var writeApiFiles = async (project, plan, write, data, mcp) => {
|
|
3678
|
+
if (!(plan.enabled && data)) {
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
const { config } = project;
|
|
3682
|
+
const mcpRoute = mcp.enabled ? mcp.route : null;
|
|
3683
|
+
const apiDir = join11(plan.srcDir, "pages", "api");
|
|
3684
|
+
const writes = [
|
|
3685
|
+
write(join11(apiDir, "docs", "pages.json.ts"), apiPagesIndexTemplate()),
|
|
3686
|
+
write(join11(apiDir, "docs", "pages", "[...route].json.ts"), apiPageTemplate()),
|
|
3687
|
+
write(join11(apiDir, "docs", "navigation.json.ts"), apiNavigationTemplate())
|
|
3688
|
+
];
|
|
3689
|
+
if (plan.server) {
|
|
3690
|
+
writes.push(write(join11(apiDir, "docs", "search.ts"), apiSearchTemplate()));
|
|
3691
|
+
}
|
|
3692
|
+
if (plan.catchAll) {
|
|
3693
|
+
writes.push(write(join11(apiDir, "[...path].ts"), apiNotFoundTemplate({ base: data.base, site: data.site })));
|
|
3694
|
+
}
|
|
3695
|
+
if (plan.spec) {
|
|
3696
|
+
writes.push(write(join11(plan.srcDir, "pages", "openapi.json.ts"), staticJsonEndpointTemplate(buildApiSpec({
|
|
3697
|
+
agentReadability: config.seo.agentReadability,
|
|
3698
|
+
base: data.base,
|
|
3699
|
+
description: config.description,
|
|
3700
|
+
llmsTxt: config.ai.llmsTxt.enabled,
|
|
3701
|
+
mcpRoute,
|
|
3702
|
+
name: config.title,
|
|
3703
|
+
search: plan.server,
|
|
3704
|
+
site: data.site,
|
|
3705
|
+
version: data.version
|
|
3706
|
+
}))));
|
|
3707
|
+
}
|
|
3708
|
+
await Promise.all(writes);
|
|
3709
|
+
};
|
|
3710
|
+
var planPlaygroundProxy = (config, srcDir) => ({
|
|
3711
|
+
enabled: needsPlaygroundProxy(config),
|
|
3712
|
+
entrypoint: join11(srcDir, "blume-openapi", "api-proxy.ts"),
|
|
3713
|
+
pattern: "/_api-proxy"
|
|
3714
|
+
});
|
|
3715
|
+
var specOriginsOf = (spec) => {
|
|
3716
|
+
const origins = new Set;
|
|
3717
|
+
if (spec.endpoint !== undefined) {
|
|
3718
|
+
try {
|
|
3719
|
+
origins.add(new URL(spec.endpoint).origin);
|
|
3720
|
+
} catch {}
|
|
3721
|
+
}
|
|
3722
|
+
const { servers } = spec.document;
|
|
3723
|
+
for (const server of Array.isArray(servers) ? servers : []) {
|
|
3724
|
+
const url = server.url ?? "";
|
|
3725
|
+
if (url.includes("{")) {
|
|
3726
|
+
continue;
|
|
3727
|
+
}
|
|
3728
|
+
try {
|
|
3729
|
+
origins.add(new URL(url).origin);
|
|
3730
|
+
} catch {}
|
|
3731
|
+
}
|
|
3732
|
+
return [...origins];
|
|
3733
|
+
};
|
|
3734
|
+
var specOrigins = (data) => [...new Set(Object.values(data).flatMap(specOriginsOf))].toSorted();
|
|
3735
|
+
var proxyAllowlistWarnings = (config, data) => {
|
|
3736
|
+
const kinds = builtinProxyKinds(config);
|
|
3737
|
+
const warnings = [];
|
|
3738
|
+
for (const spec of Object.values(data)) {
|
|
3739
|
+
if (!kinds.includes(spec.kind) || specOriginsOf(spec).length > 0) {
|
|
3740
|
+
continue;
|
|
3741
|
+
}
|
|
3742
|
+
warnings.push(spec.kind === "graphql" ? `The "${spec.label}" GraphQL reference (${spec.route}) has playground.proxy: true, but no absolute endpoint is configured for it, so the built-in proxy has no origin to allow and will refuse every request its playground sends. Set the graphql block's (or the source's) \`endpoint\` to the live GraphQL URL, or point playground.proxy at an external proxy URL.` : `The "${spec.label}" reference (${spec.route}) has playground.proxy: true, but its spec declares no absolute servers[].url (relative and templated URLs carry no origin), so the built-in proxy has no origin to allow and will refuse every request its playground sends. Add an absolute server URL to the spec, or point playground.proxy at an external proxy URL.`);
|
|
3743
|
+
}
|
|
3744
|
+
return warnings;
|
|
3745
|
+
};
|
|
3746
|
+
var writeAskFiles = async (project, srcDir, write, modules) => {
|
|
3747
|
+
const { ask } = project.config.ai;
|
|
3748
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
const grounded = ask.provider !== "inkeep";
|
|
3752
|
+
if (grounded) {
|
|
3753
|
+
modules.set("blume:ask-data", JSON.stringify(await buildAskData(project)));
|
|
3754
|
+
}
|
|
3755
|
+
await write(join11(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded, {
|
|
3756
|
+
instructions: ask.instructions,
|
|
3757
|
+
retrieval: ask.retrieval
|
|
3758
|
+
}));
|
|
3759
|
+
};
|
|
3760
|
+
var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
|
|
3761
|
+
if (routeIsTaken(pages, contentPages, "/404")) {
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
await Promise.all([
|
|
3765
|
+
write(join11(srcDir, "pages", "404.astro"), notFoundPageTemplate()),
|
|
3766
|
+
write(join11(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate()),
|
|
3767
|
+
write(join11(srcDir, "pages", "404.json.ts"), notFoundJsonTemplate())
|
|
3768
|
+
]);
|
|
3769
|
+
};
|
|
3770
|
+
var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
|
|
3771
|
+
var buildComponentSlots = async (componentsFile) => {
|
|
3772
|
+
const analysis = componentsFile ? analyzeComponentOverrides(await readFile5(componentsFile, "utf-8"), componentsFile) : null;
|
|
3773
|
+
return {
|
|
3774
|
+
plan: planComponentSlots(componentsFile, analysis),
|
|
3775
|
+
tags: analysis ? [...analysis.mdx, ...analysis.islands].map((entry) => entry.key) : [],
|
|
3776
|
+
warnings: analysis ? analysis.warnings : []
|
|
3777
|
+
};
|
|
3778
|
+
};
|
|
3779
|
+
var projectOgFonts = (project) => resolveOgFonts({
|
|
3780
|
+
ogFonts: project.config.seo.og.fonts,
|
|
3781
|
+
themeFonts: project.config.theme.fonts,
|
|
3782
|
+
themeFontsConfigured: project.themeFontsConfigured
|
|
3783
|
+
}, project.context.root);
|
|
3784
|
+
var assertFontFilesExist = (project) => {
|
|
3785
|
+
const { config, context } = project;
|
|
3786
|
+
const missing = missingFontFiles({ ogFonts: config.seo.og.fonts ?? [], themeFonts: config.theme.fonts }, context.root);
|
|
3787
|
+
if (missing.length > 0) {
|
|
3788
|
+
throw new BlumeError({
|
|
3789
|
+
code: "BLUME_FONT_FILE_MISSING",
|
|
3790
|
+
message: `Configured font file(s) not found: ${missing.join(", ")}. Font paths resolve relative to the project root.`,
|
|
3791
|
+
severity: "error"
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
};
|
|
3795
|
+
var clientFeaturesFrom = (project, rawMarkdown) => ({
|
|
3796
|
+
epub: project.config.export.epub,
|
|
3797
|
+
mermaid: Object.values(rawMarkdown).some((entry) => hasMermaidFence(entry.mdx ?? entry.md ?? "")) || project.graph.pages.some((page) => page.body !== undefined && hasMermaidFence(page.body.text))
|
|
3798
|
+
});
|
|
3799
|
+
var languageIconCssFrom = (project, rawMarkdown) => languageIconCss(languageIconSlugsIn([
|
|
3800
|
+
...Object.values(rawMarkdown).map((entry) => entry.mdx ?? entry.md ?? ""),
|
|
3801
|
+
...project.graph.pages.map((page) => page.body?.text ?? "")
|
|
3802
|
+
].join(`
|
|
3803
|
+
`)));
|
|
3804
|
+
var languageIconCssFor = async (project) => languageIconCssFrom(project, await buildRawMarkdown(project));
|
|
3805
|
+
var writeNavFragments = (write, srcDir, navFragments) => navFragments ? write(join11(srcDir, "pages", "blume-nav", "[version]", "[locale]", "[id].astro"), navFragmentTemplate()) : Promise.resolve(false);
|
|
3806
|
+
var clientFeaturesFor = async (project) => clientFeaturesFrom(project, await buildRawMarkdown(project));
|
|
3807
|
+
var generateRuntime = async (project) => {
|
|
3808
|
+
const { context, config } = project;
|
|
3809
|
+
assertFontFilesExist(project);
|
|
3810
|
+
const out = context.outDir;
|
|
3811
|
+
const srcDir = join11(out, "src");
|
|
3812
|
+
const generatedDir = join11(srcDir, "generated");
|
|
3813
|
+
const askPath = join11(srcDir, "generated", "Ask.astro");
|
|
3814
|
+
const themePath = join11(srcDir, "generated", "app.css");
|
|
3815
|
+
const searchClientPath = join11(srcDir, "generated", "search-client.ts");
|
|
3816
|
+
const featuresPath = join11(srcDir, "generated", "features.ts");
|
|
3817
|
+
const examplesPath = join11(srcDir, "generated", "examples.ts");
|
|
3818
|
+
const examplesThemePath = join11(srcDir, "generated", "examples.css");
|
|
3819
|
+
const written = new Set;
|
|
3820
|
+
const write = (path, content) => {
|
|
3821
|
+
written.add(normalize(path));
|
|
3822
|
+
return writeIfChanged(path, content);
|
|
3823
|
+
};
|
|
3824
|
+
const modules = new Map;
|
|
3825
|
+
const depsLinkWarning = await ensureDepsLink(out);
|
|
3826
|
+
const contentRoutes = markdownRoutePaths(project);
|
|
3827
|
+
const homeLinkHeader = buildHomeLinkHeader(config, contentRoutes) ?? undefined;
|
|
3828
|
+
const askEnabled = config.ai.ask?.enabled ?? false;
|
|
3829
|
+
const exportPdf = config.export.pdf;
|
|
3830
|
+
const exportEpub = config.export.epub;
|
|
3831
|
+
const rawMarkdown = await buildRawMarkdown(project);
|
|
3832
|
+
const clientFeatures = clientFeaturesFrom(project, rawMarkdown);
|
|
3833
|
+
const navFragments = navVariants(project.graph).some(({ navigation }) => hasDeferrableGroups(navigation.sidebar));
|
|
3834
|
+
const languageIcons = languageIconCssFrom(project, rawMarkdown);
|
|
3835
|
+
const staged = collectStaged(project);
|
|
3836
|
+
const [
|
|
3837
|
+
pages,
|
|
3838
|
+
detectedReact,
|
|
3839
|
+
usesMath,
|
|
3840
|
+
userTheme,
|
|
3841
|
+
userExamplesCss,
|
|
3842
|
+
integrationBridge,
|
|
3843
|
+
islandDiscovery,
|
|
3844
|
+
exampleDiscovery,
|
|
3845
|
+
componentSlots
|
|
3846
|
+
] = await Promise.all([
|
|
3847
|
+
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
3848
|
+
detectNeedsReact(context.root),
|
|
3849
|
+
detectUsesMath(context.root, staged.values()),
|
|
3850
|
+
readUserCss(context.themeFile, generatedDir),
|
|
3851
|
+
readUserCss(examplesCssFile(context.root, config), generatedDir),
|
|
3852
|
+
loadIntegrationBridge(config, context),
|
|
3853
|
+
discoverIslands(context.root),
|
|
3854
|
+
discoverExamples(context.root, config.examples.source),
|
|
3855
|
+
buildComponentSlots(context.componentsFile)
|
|
3856
|
+
]);
|
|
3857
|
+
const {
|
|
3858
|
+
plan: slotPlan,
|
|
3859
|
+
tags: overrideTags,
|
|
3860
|
+
warnings: overrideWarnings
|
|
3861
|
+
} = componentSlots;
|
|
3862
|
+
project.examples = exampleMarkdownLookup(exampleDiscovery.examples);
|
|
3863
|
+
const frameworks = new Set([
|
|
3864
|
+
...islandDiscovery.islands.map((island) => island.framework),
|
|
3865
|
+
...exampleDiscovery.examples.map((example) => example.framework),
|
|
3866
|
+
...slotPlan.frameworks
|
|
3867
|
+
]);
|
|
3868
|
+
const needsReact = detectedReact || askEnabled || frameworks.has("react");
|
|
3869
|
+
const needsVue = frameworks.has("vue");
|
|
3870
|
+
const needsSvelte = frameworks.has("svelte");
|
|
3871
|
+
const reactCompilerPath = resolveReactCompiler(config, needsReact);
|
|
3872
|
+
const ogRoutes = customOgRoutes(pages, config.title, config.seo.og.titles);
|
|
3873
|
+
const changelogIndex = hasGeneratedChangelog(project, pages);
|
|
3874
|
+
const mcp = planMcp(project, srcDir, pages);
|
|
3875
|
+
pages.push(...mcp.discoveryPages);
|
|
3876
|
+
const api = planApi(project, srcDir, pages);
|
|
3877
|
+
const agentData = await publishAgentData(project, { api, mcp }, modules);
|
|
3878
|
+
const openApiSource = project.sources.find(isOpenApiSource);
|
|
3879
|
+
const openApiData = openApiSource ? openApiSource.openApiData() : {};
|
|
3880
|
+
const playgroundProxy = planPlaygroundProxy(config, srcDir);
|
|
3881
|
+
if (playgroundProxy.enabled) {
|
|
3882
|
+
pages.push({
|
|
3883
|
+
entrypoint: playgroundProxy.entrypoint,
|
|
3884
|
+
pattern: playgroundProxy.pattern
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
const proxyOrigins = specOrigins(openApiData);
|
|
3888
|
+
const hasStaged = staged.size > 0;
|
|
3889
|
+
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
3890
|
+
const docsCollection = resolveDocsCollection(config, context);
|
|
3891
|
+
const [structural] = await Promise.all([
|
|
3892
|
+
Promise.all([
|
|
3893
|
+
write(join11(out, "astro.config.mjs"), astroConfigTemplate({
|
|
3894
|
+
aliases: resolveTsconfigAliases(context.root),
|
|
3895
|
+
askPath,
|
|
3896
|
+
config,
|
|
3897
|
+
contentRoot: docsCollection.base,
|
|
3898
|
+
contentRoutes,
|
|
3899
|
+
context,
|
|
3900
|
+
examplesPath,
|
|
3901
|
+
examplesThemePath,
|
|
3902
|
+
features: clientFeatures,
|
|
3903
|
+
featuresPath,
|
|
3904
|
+
integrationBridge,
|
|
3905
|
+
needsReact,
|
|
3906
|
+
needsSvelte,
|
|
3907
|
+
needsVue,
|
|
3908
|
+
pages,
|
|
3909
|
+
reactCompilerPath,
|
|
3910
|
+
searchClientPath,
|
|
3911
|
+
themePath
|
|
3912
|
+
})),
|
|
3913
|
+
write(join11(out, "package.json"), runtimePackageTemplate(runtimeDependencies({ config, needsReact, needsSvelte, needsVue }))),
|
|
3914
|
+
write(join11(out, "tsconfig.json"), runtimeTsconfigTemplate()),
|
|
3915
|
+
write(join11(srcDir, "content.config.ts"), contentConfigTemplate({
|
|
3916
|
+
collection: docsCollection,
|
|
3917
|
+
config,
|
|
3918
|
+
context,
|
|
3919
|
+
filesystem: hasFilesystemSource,
|
|
3920
|
+
staged: hasStaged
|
|
3921
|
+
})),
|
|
3922
|
+
write(join11(srcDir, "pages", "[...slug].astro"), catchAllPageTemplate({
|
|
3923
|
+
exportEpub,
|
|
3924
|
+
exportPdf,
|
|
3925
|
+
mathEnabled: usesMath,
|
|
3926
|
+
navFragments,
|
|
3927
|
+
needsReact
|
|
3928
|
+
})),
|
|
3929
|
+
writeNavFragments(write, srcDir, navFragments),
|
|
3930
|
+
write(askPath, askComponentTemplate(askEnabled)),
|
|
3931
|
+
write(join11(srcDir, "generated", "components.ts"), slotPlan.module),
|
|
3932
|
+
write(join11(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
|
|
3933
|
+
write(join11(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples, config.basePath)),
|
|
3934
|
+
write(examplesThemePath, examplesEntryTemplate({
|
|
3935
|
+
configTokens: buildThemeCss(config.theme),
|
|
3936
|
+
sources: exampleScanRoots(context.root, exampleDiscovery.dir).map((dir) => `${dir}/${EXAMPLE_SCAN_GLOB}`),
|
|
3937
|
+
userCss: userExamplesCss
|
|
3938
|
+
})),
|
|
3939
|
+
write(themePath, tailwindEntryTemplate({
|
|
3940
|
+
configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
|
|
3941
|
+
languageIcons,
|
|
3942
|
+
sources: [
|
|
3943
|
+
`${BLUME_SRC}/**/*.{astro,ts,tsx}`,
|
|
3944
|
+
`${context.root}/**/*.{astro,mdx,ts,tsx}`
|
|
3945
|
+
],
|
|
3946
|
+
twoslashCss: twoslashCss(),
|
|
3947
|
+
userTheme
|
|
3948
|
+
}))
|
|
3949
|
+
]),
|
|
3950
|
+
Promise.all(islandDiscovery.islands.map((island) => write(join11(srcDir, "generated", "islands", `${island.name}.astro`), islandWrapperTemplate(island)))),
|
|
3951
|
+
Promise.all(slotPlan.wrappers.map((wrapper) => write(join11(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content))),
|
|
3952
|
+
Promise.all(exampleDiscovery.examples.map((example) => write(join11(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example)))),
|
|
3953
|
+
writeAskFiles(project, srcDir, write, modules),
|
|
3954
|
+
writeMcpFiles(mcp, write, agentData),
|
|
3955
|
+
writeApiFiles(project, api, write, agentData, mcp),
|
|
3956
|
+
playgroundProxy.enabled ? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins)) : Promise.resolve(false)
|
|
3957
|
+
]);
|
|
3958
|
+
if (config.seo.og.enabled) {
|
|
3959
|
+
await write(join11(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes, {
|
|
3960
|
+
...projectOgFonts(project),
|
|
3961
|
+
cache: {
|
|
3962
|
+
dir: ogCacheDir(project.context),
|
|
3963
|
+
version: getBlumeVersion()
|
|
3964
|
+
},
|
|
3965
|
+
pageDescriptions: config.seo.og.description !== false
|
|
3966
|
+
}, changelogIndex));
|
|
3967
|
+
}
|
|
3968
|
+
if (changelogIndex) {
|
|
3969
|
+
await write(join11(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
|
|
3970
|
+
exportEpub,
|
|
3971
|
+
exportPdf,
|
|
3972
|
+
needsReact,
|
|
3973
|
+
staged: hasStaged
|
|
3974
|
+
}));
|
|
3975
|
+
}
|
|
3976
|
+
const [examplesWarnings] = await Promise.all([
|
|
3977
|
+
writeExamplesPreview({
|
|
3978
|
+
config,
|
|
3979
|
+
hasExamples: exampleDiscovery.examples.length > 0,
|
|
3980
|
+
root: context.root,
|
|
3981
|
+
srcDir,
|
|
3982
|
+
write
|
|
3983
|
+
}),
|
|
3984
|
+
writeNotFoundPage(write, srcDir, pages, project.graph.pages),
|
|
3985
|
+
write(searchClientPath, searchClientTemplate(config)),
|
|
3986
|
+
write(featuresPath, featuresTemplate(clientFeatures))
|
|
3987
|
+
]);
|
|
3988
|
+
if (servesStaticIndex(config.search.provider)) {
|
|
3989
|
+
const documents = await buildSearchDocuments(project);
|
|
3990
|
+
modules.set("blume:search-index", JSON.stringify(documents));
|
|
3991
|
+
await write(join11(srcDir, "pages", "blume-search.json.ts"), searchEndpointTemplate());
|
|
3992
|
+
}
|
|
3993
|
+
if (config.search.provider === "mixedbread") {
|
|
3994
|
+
await write(join11(srcDir, "pages", "api", "search.ts"), mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""));
|
|
3995
|
+
}
|
|
3996
|
+
await write(join11(srcDir, "generated", "includes.json"), `${JSON.stringify(buildIncludeGraph(project.graph.pages))}
|
|
3997
|
+
`);
|
|
3998
|
+
modules.set("blume:raw-markdown", JSON.stringify(rawMarkdown));
|
|
3999
|
+
const contentAssets = await collectContentAssets(project);
|
|
4000
|
+
modules.set("blume:content-assets", JSON.stringify(contentAssets));
|
|
4001
|
+
await Promise.all([
|
|
4002
|
+
write(join11(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate("md")),
|
|
4003
|
+
write(join11(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate("mdx")),
|
|
4004
|
+
write(join11(srcDir, "pages", "blume-assets", "[...asset].ts"), contentAssetsEndpointTemplate(join11(project.context.outDir, "public", "blume-assets")))
|
|
4005
|
+
]);
|
|
4006
|
+
const feeds = buildRssFeeds(project);
|
|
4007
|
+
if (feeds.length > 0) {
|
|
4008
|
+
const feedXml = Object.fromEntries(feeds.map((feed) => [feed.type, renderRssFeed(feed)]));
|
|
4009
|
+
modules.set("blume:rss", JSON.stringify(feedXml));
|
|
4010
|
+
await write(join11(srcDir, "pages", "[section]", "rss.xml.ts"), rssEndpointTemplate());
|
|
4011
|
+
}
|
|
4012
|
+
const warnings = [
|
|
4013
|
+
...depsLinkWarning ? [depsLinkWarning] : [],
|
|
4014
|
+
...proxyAllowlistWarnings(config, openApiData),
|
|
4015
|
+
...reactCompilerWarnings(config, needsReact, reactCompilerPath),
|
|
4016
|
+
...mcp.warnings,
|
|
4017
|
+
...islandDiscovery.warnings,
|
|
4018
|
+
...exampleDiscovery.warnings,
|
|
4019
|
+
...examplesWarnings,
|
|
4020
|
+
...overrideWarnings
|
|
4021
|
+
];
|
|
4022
|
+
const navTargetRoutes = new Set([
|
|
4023
|
+
...project.graph.routes.keys(),
|
|
4024
|
+
...pages.map((page) => page.pattern),
|
|
4025
|
+
...referenceRoutes(config)
|
|
4026
|
+
]);
|
|
4027
|
+
if (changelogIndex) {
|
|
4028
|
+
navTargetRoutes.add("/changelog");
|
|
4029
|
+
}
|
|
4030
|
+
warnings.push(...[
|
|
4031
|
+
...validateNavTargets(project.graph.navigation, navTargetRoutes),
|
|
4032
|
+
...validateSearchPopularIcons(config.search.popular)
|
|
4033
|
+
].map(diagnosticWarning));
|
|
4034
|
+
const knownComponentTags = new Set([
|
|
4035
|
+
...islandDiscovery.islands.map((island) => island.name),
|
|
4036
|
+
...overrideTags
|
|
4037
|
+
]);
|
|
4038
|
+
warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map(diagnosticWarning), ...searchProviderWarnings(config.search.provider, context.root), ...askProviderWarnings(config.ai.ask, context.root), ...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
|
|
4039
|
+
if (hasScalarReferences(config)) {
|
|
4040
|
+
const references = await buildReferenceFiles({
|
|
4041
|
+
config,
|
|
4042
|
+
contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
|
|
4043
|
+
root: context.root
|
|
4044
|
+
});
|
|
4045
|
+
warnings.push(...references.warnings);
|
|
4046
|
+
await Promise.all(references.files.map((file) => write(join11(srcDir, "pages", file.pagePath), file.content)));
|
|
4047
|
+
}
|
|
4048
|
+
modules.set("blume:data", buildRuntimeData(project));
|
|
4049
|
+
modules.set("blume:openapi", JSON.stringify(openApiData));
|
|
4050
|
+
await Promise.all([
|
|
4051
|
+
write(join11(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
|
|
4052
|
+
`),
|
|
4053
|
+
writeStagedContent(out, staged)
|
|
4054
|
+
]);
|
|
4055
|
+
await pruneOrphans(srcDir, written);
|
|
4056
|
+
publishDevNegotiation({ contentRoutes, homeLinkHeader });
|
|
4057
|
+
publishRuntimeModules(modules);
|
|
4058
|
+
return { structuralChange: structural.some(Boolean), warnings };
|
|
4059
|
+
};
|
|
4060
|
+
|
|
4061
|
+
export { SIGNATURES_DIRECTORY_PATH, SIGNATURES_DIRECTORY_TYPE, publishBuildProject, refreshBlumeContent, showBlumeErrorOverlay, ogCacheDir, pruneCardCache, cardCacheTally, buildAskData, buildMcpDiscovery, buildMcpServerCard, buildThemeCss, buildReferenceFiles, tailwindEntryTemplate, examplesEntryTemplate, rebaseSourceDirectives, twoslashCss, planComponentSlots, discoverIslands, EXAMPLE_SCAN_GLOB, exampleScanRoots, discoverExamples, exampleMarkdownLookup, detectNeedsReact, detectUsesMath, collectStaged, buildRuntimeData, languageIconCssFor, clientFeaturesFor, generateRuntime };
|
|
4062
|
+
|
|
4063
|
+
//# debugId=E13637FE8F0CF5E964756E2164756E21
|
|
4064
|
+
//# sourceMappingURL=chunk-5d4q7121.js.map
|