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
package/src/astro/templates.ts
CHANGED
|
@@ -14,12 +14,13 @@ import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
|
14
14
|
import { trimChar } from "../core/trim.ts";
|
|
15
15
|
import type { ProjectContext } from "../core/types.ts";
|
|
16
16
|
import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
|
|
17
|
+
import type { OgCache } from "../og/cache.ts";
|
|
17
18
|
import type { OgFont, OgFontFamilies } from "../og/card.ts";
|
|
18
19
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
19
20
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
20
21
|
import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
|
|
21
22
|
import type { ExampleSpec } from "./examples.ts";
|
|
22
|
-
import type { BlumePageRoute } from "./integration.ts";
|
|
23
|
+
import type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
|
|
23
24
|
import type { IslandSpec } from "./islands.ts";
|
|
24
25
|
import type { OgCustomRoute } from "./pages.ts";
|
|
25
26
|
import { RUNTIME_MODULE_FILES } from "./runtime-modules.ts";
|
|
@@ -125,30 +126,36 @@ const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
|
|
|
125
126
|
* Without a configured driver, `@astrojs/cloudflare` force-enables KV-backed
|
|
126
127
|
* sessions and declares a `SESSION` kv_namespaces entry in the generated
|
|
127
128
|
* wrangler config — which `wrangler deploy` then requires a real KV namespace
|
|
128
|
-
* for, even though Blume never reads `Astro.session`.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
129
|
+
* for, even though Blume never reads `Astro.session`. Astro's `session: false`
|
|
130
|
+
* opts the project out, and the adapter checks for it before adding the
|
|
131
|
+
* binding. (An in-memory driver used to stand in before the opt-out existed.)
|
|
131
132
|
*/
|
|
132
133
|
const resolveSessionOption = (deployment: {
|
|
133
134
|
adapter: string | null;
|
|
134
135
|
output: string;
|
|
135
136
|
}): string =>
|
|
136
137
|
deployment.output === "server" && deployment.adapter === "cloudflare"
|
|
137
|
-
? "\n session:
|
|
138
|
+
? "\n session: false,"
|
|
138
139
|
: "";
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A font weight as Astro's Fonts API spells it: a variable range is
|
|
143
|
+
* `"100 900"` there, where Blume's config (and Takumi's Google Fonts helper,
|
|
144
|
+
* which the OG cards use) write `"100..900"`. Astro treats the dotted form as
|
|
145
|
+
* an unknown discrete weight and loads nothing for it.
|
|
146
|
+
*/
|
|
147
|
+
const astroFontWeights = (weights: (number | string)[]): string =>
|
|
148
|
+
JSON.stringify(weights).replaceAll(
|
|
149
|
+
/(?<min>\d+)\.\.(?<max>\d+)/gu,
|
|
150
|
+
"$<min> $<max>"
|
|
151
|
+
);
|
|
152
|
+
|
|
140
153
|
/** The named imports the generated config pulls from `astro/config`. */
|
|
141
|
-
const astroConfigImportLine = (options: {
|
|
142
|
-
hasFonts: boolean;
|
|
143
|
-
hasSession: boolean;
|
|
144
|
-
}): string => {
|
|
154
|
+
const astroConfigImportLine = (options: { hasFonts: boolean }): string => {
|
|
145
155
|
const names = ["defineConfig"];
|
|
146
156
|
if (options.hasFonts) {
|
|
147
157
|
names.push("fontProviders");
|
|
148
158
|
}
|
|
149
|
-
if (options.hasSession) {
|
|
150
|
-
names.push("sessionDrivers");
|
|
151
|
-
}
|
|
152
159
|
return `import { ${names.join(", ")} } from "astro/config";`;
|
|
153
160
|
};
|
|
154
161
|
|
|
@@ -395,13 +402,31 @@ interface OptimizeDepsConfig {
|
|
|
395
402
|
* absolute path (see `reactIntegration`), which that check never matches. See
|
|
396
403
|
* the optimizeDeps comment in the generated config for the failure this prevents.
|
|
397
404
|
*/
|
|
405
|
+
/**
|
|
406
|
+
* The client-side libraries a site needs, decided at generation time. A
|
|
407
|
+
* feature no page uses is left out of the module graph entirely (see
|
|
408
|
+
* {@link featuresTemplate}), so its library never enters the client bundle —
|
|
409
|
+
* Mermaid alone is over 3 MB of chunks (ELK, Cytoscape, KaTeX, every diagram
|
|
410
|
+
* type) and most of the build's client-bundle memory.
|
|
411
|
+
*/
|
|
412
|
+
export interface ClientFeatures {
|
|
413
|
+
/** `export.epub` is on: the EPUB generator's browser bundle is needed. */
|
|
414
|
+
epub: boolean;
|
|
415
|
+
/** Some page has a mermaid fence: the `<blume-mermaid>` element is needed. */
|
|
416
|
+
mermaid: boolean;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Every feature on: what a checkout that predates the detection shipped. */
|
|
420
|
+
const ALL_CLIENT_FEATURES: ClientFeatures = { epub: true, mermaid: true };
|
|
421
|
+
|
|
398
422
|
const resolveOptimizeDeps = (options: {
|
|
399
423
|
aliases: Record<string, string> | undefined;
|
|
400
424
|
context: ProjectContext;
|
|
425
|
+
features: ClientFeatures;
|
|
401
426
|
needsReact: boolean;
|
|
402
427
|
reactCompilerPath: string | null | undefined;
|
|
403
428
|
}): OptimizeDepsConfig => {
|
|
404
|
-
const { context } = options;
|
|
429
|
+
const { context, features } = options;
|
|
405
430
|
const optimizeDepsEntries = [
|
|
406
431
|
...(context.pagesRoot ? [`${context.pagesRoot}/**/*.astro`] : []),
|
|
407
432
|
`${context.root}/islands/**/*.{jsx,svelte,tsx,vue}`,
|
|
@@ -410,8 +435,10 @@ const resolveOptimizeDeps = (options: {
|
|
|
410
435
|
.map((dir) => `${dir}/**/*.{astro,jsx,svelte,tsx,vue}`),
|
|
411
436
|
];
|
|
412
437
|
const optimizeDepsInclude = [
|
|
413
|
-
|
|
414
|
-
|
|
438
|
+
// Only the libraries the site's features actually import: pre-bundling
|
|
439
|
+
// Mermaid costs the dev server seconds at startup for nothing otherwise.
|
|
440
|
+
...(features.mermaid ? ["blume > mermaid"] : []),
|
|
441
|
+
...(features.epub ? ["blume > epub-gen-memory/bundle"] : []),
|
|
415
442
|
// Astro's own client-router/prefetch virtual modules are deliberately NOT
|
|
416
443
|
// forced in here: they read Vite `define`-injected constants
|
|
417
444
|
// (__PREFETCH_PREFETCH_ALL__ and friends) that a pre-bundled copy loses,
|
|
@@ -458,6 +485,33 @@ const renderRuntimeModuleWiring = (
|
|
|
458
485
|
return { aliasLines, imports: [], pluginEntry: "" };
|
|
459
486
|
};
|
|
460
487
|
|
|
488
|
+
/**
|
|
489
|
+
* The options baked into the generated config's `blumeIntegration(...)` call.
|
|
490
|
+
* The hidden runtime gets the content routes and homepage `Link` header from
|
|
491
|
+
* the CLI in memory (`publishDevNegotiation`, republished on every
|
|
492
|
+
* regeneration), so a content-route change never rewrites the config — Astro
|
|
493
|
+
* restarts the dev server in place on a config change — and its scanned
|
|
494
|
+
* project from `blume build` for the deploy artifacts. An ejected project has
|
|
495
|
+
* no CLI, so the negotiation inputs are baked in and `astro:build:done` scans
|
|
496
|
+
* the project root (the Astro root, after eject) for the artifacts.
|
|
497
|
+
*/
|
|
498
|
+
const blumeIntegrationOptions = (options: {
|
|
499
|
+
config: ResolvedConfig;
|
|
500
|
+
contentRoutes: string[];
|
|
501
|
+
ejected: boolean;
|
|
502
|
+
pages: BlumePageRoute[];
|
|
503
|
+
}): BlumeIntegrationOptions =>
|
|
504
|
+
options.ejected
|
|
505
|
+
? {
|
|
506
|
+
buildArtifactsRoot: ".",
|
|
507
|
+
contentRoutes: options.contentRoutes,
|
|
508
|
+
homeLinkHeader:
|
|
509
|
+
buildHomeLinkHeader(options.config, options.contentRoutes) ??
|
|
510
|
+
undefined,
|
|
511
|
+
pages: options.pages,
|
|
512
|
+
}
|
|
513
|
+
: { pages: options.pages };
|
|
514
|
+
|
|
461
515
|
export const astroConfigTemplate = (options: {
|
|
462
516
|
context: ProjectContext;
|
|
463
517
|
config: ResolvedConfig;
|
|
@@ -473,6 +527,10 @@ export const astroConfigTemplate = (options: {
|
|
|
473
527
|
examplesThemePath: string;
|
|
474
528
|
themePath: string;
|
|
475
529
|
searchClientPath: string;
|
|
530
|
+
/** The generated client-feature loaders (`blume:features`). */
|
|
531
|
+
featuresPath: string;
|
|
532
|
+
/** Which client features the site uses; every feature on when omitted. */
|
|
533
|
+
features?: ClientFeatures;
|
|
476
534
|
/**
|
|
477
535
|
* Where the runtime data modules (`blume:data`, the search index, …) live as
|
|
478
536
|
* JSON files, for a project with no CLI to publish them in memory (eject):
|
|
@@ -507,6 +565,8 @@ export const astroConfigTemplate = (options: {
|
|
|
507
565
|
contentRoutes,
|
|
508
566
|
examplesPath,
|
|
509
567
|
examplesThemePath,
|
|
568
|
+
features = ALL_CLIENT_FEATURES,
|
|
569
|
+
featuresPath,
|
|
510
570
|
generatedModulesDir,
|
|
511
571
|
needsSvelte,
|
|
512
572
|
needsVue,
|
|
@@ -528,6 +588,7 @@ export const astroConfigTemplate = (options: {
|
|
|
528
588
|
const { optimizeDepsEntries, optimizeDepsInclude } = resolveOptimizeDeps({
|
|
529
589
|
aliases: options.aliases,
|
|
530
590
|
context,
|
|
591
|
+
features,
|
|
531
592
|
needsReact,
|
|
532
593
|
reactCompilerPath: options.reactCompilerPath,
|
|
533
594
|
});
|
|
@@ -567,7 +628,9 @@ export const astroConfigTemplate = (options: {
|
|
|
567
628
|
: "";
|
|
568
629
|
const imageOption = renderImageOption(config);
|
|
569
630
|
|
|
570
|
-
// Astro's native i18n
|
|
631
|
+
// Astro's native i18n resolves `Astro.currentLocale` from the URL, which the
|
|
632
|
+
// document shells fall back to for `<html lang>`/`dir` on pages the content
|
|
633
|
+
// catch-all doesn't drive (custom pages, the 404, the reference shell).
|
|
571
634
|
// Blume owns getStaticPaths and materializes fallback routes in the manifest,
|
|
572
635
|
// so we deliberately omit Astro's `fallback` to keep one source of routing.
|
|
573
636
|
const i18nOption = config.i18n
|
|
@@ -643,9 +706,7 @@ export const astroConfigTemplate = (options: {
|
|
|
643
706
|
font.name
|
|
644
707
|
)}, cssVariable: ${JSON.stringify(
|
|
645
708
|
font.cssVariable
|
|
646
|
-
)}, weights: ${JSON.stringify(
|
|
647
|
-
font.weights
|
|
648
|
-
)}, subsets: ${JSON.stringify(
|
|
709
|
+
)}, weights: ${astroFontWeights(font.weights)}, subsets: ${JSON.stringify(
|
|
649
710
|
font.subsets
|
|
650
711
|
)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`
|
|
651
712
|
)
|
|
@@ -653,7 +714,6 @@ export const astroConfigTemplate = (options: {
|
|
|
653
714
|
: "";
|
|
654
715
|
const defineConfigImport = astroConfigImportLine({
|
|
655
716
|
hasFonts: fontEntries.length > 0,
|
|
656
|
-
hasSession: sessionOption.length > 0,
|
|
657
717
|
});
|
|
658
718
|
|
|
659
719
|
// Framework renderers are only wired in when an island (or Ask AI, for React)
|
|
@@ -710,12 +770,14 @@ export const astroConfigTemplate = (options: {
|
|
|
710
770
|
// up dev-server `Accept: text/markdown` negotiation over the content routes,
|
|
711
771
|
// plus the homepage agent-discovery `Link` header.
|
|
712
772
|
integrations.push(
|
|
713
|
-
`blumeIntegration(${JSON.stringify(
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
773
|
+
`blumeIntegration(${JSON.stringify(
|
|
774
|
+
blumeIntegrationOptions({
|
|
775
|
+
config,
|
|
776
|
+
contentRoutes,
|
|
777
|
+
ejected: generatedModulesDir !== undefined,
|
|
778
|
+
pages,
|
|
779
|
+
})
|
|
780
|
+
)})`
|
|
719
781
|
);
|
|
720
782
|
|
|
721
783
|
const {
|
|
@@ -726,7 +788,8 @@ export const astroConfigTemplate = (options: {
|
|
|
726
788
|
} = renderIntegrationBridge(options.integrationBridge);
|
|
727
789
|
|
|
728
790
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
729
|
-
${configSourceMarker}${userConfigImports}
|
|
791
|
+
${configSourceMarker}${userConfigImports}import { availableParallelism } from "node:os";
|
|
792
|
+
${defineConfigImport}
|
|
730
793
|
import mdx from "@astrojs/mdx";
|
|
731
794
|
import tailwindcss from "@tailwindcss/vite";
|
|
732
795
|
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
|
|
@@ -758,11 +821,25 @@ ${userConfigSetup}export default defineConfig({
|
|
|
758
821
|
},
|
|
759
822
|
},
|
|
760
823
|
devToolbar: { enabled: false },
|
|
824
|
+
// One canonical URL per page: canonicals, the sitemap, and hreflang all use
|
|
825
|
+
// the slashless form, so the slashed spelling is not a second address. Astro
|
|
826
|
+
// applies this itself — its dev server answers a slashed URL with a 404 that
|
|
827
|
+
// names the setting, an on-demand route redirects — and the Vercel adapter
|
|
828
|
+
// turns it into the platform's 308 route, so the Build Output config needs
|
|
829
|
+
// no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
|
|
830
|
+
// serve the \`index.html\` directory layout as they always did.
|
|
831
|
+
trailingSlash: "never",
|
|
761
832
|
// The layouts render Astro's <ClientRouter />, and its in-place swaps read
|
|
762
833
|
// from the prefetch cache — fetching every link on hover/viewport hides the
|
|
763
834
|
// request latency behind the user's intent, so most navigations swap
|
|
764
835
|
// instantly.
|
|
765
836
|
prefetch: { prefetchAll: true },
|
|
837
|
+
// Prerender several pages at once. Rendering is single-threaded, but a
|
|
838
|
+
// page's OG card renders on a native thread and its HTML is written
|
|
839
|
+
// asynchronously, so the main thread would otherwise idle behind each
|
|
840
|
+
// page's off-thread work. Capped at 8: the gain flattens there and beyond it
|
|
841
|
+
// the overlap only adds memory.
|
|
842
|
+
build: { concurrency: Math.min(8, availableParallelism()) },
|
|
766
843
|
vite: {${viteCacheOption}
|
|
767
844
|
plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(
|
|
768
845
|
`${context.outDir}/src/generated/includes.json`
|
|
@@ -815,6 +892,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
815
892
|
"blume:ask": ${JSON.stringify(askPath)},
|
|
816
893
|
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
817
894
|
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
895
|
+
"blume:features": ${JSON.stringify(featuresPath)},
|
|
818
896
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
819
897
|
"blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
|
|
820
898
|
},
|
|
@@ -924,6 +1002,7 @@ const staged = defineCollection({
|
|
|
924
1002
|
base: ${JSON.stringify(astroGlobBase(stagedBase))},
|
|
925
1003
|
generateId: ({ entry }) => entry,
|
|
926
1004
|
}),
|
|
1005
|
+
schema: pageCollectionSchema,
|
|
927
1006
|
});
|
|
928
1007
|
`
|
|
929
1008
|
: "";
|
|
@@ -932,16 +1011,22 @@ const staged = defineCollection({
|
|
|
932
1011
|
import { defineCollection } from "astro:content";
|
|
933
1012
|
import { glob } from "astro/loaders";
|
|
934
1013
|
import { withIncludeRefresh } from "blume/astro";
|
|
1014
|
+
import { pageCollectionSchema } from "blume/core/schema.ts";
|
|
935
1015
|
|
|
936
1016
|
// withIncludeRefresh keeps <include>-bearing pages fresh: plain .md entries
|
|
937
1017
|
// are rendered at sync time and digest-cached on the page file alone, so a
|
|
938
1018
|
// partial edit (or a warm-cache rebuild after one) would serve stale HTML.
|
|
1019
|
+
//
|
|
1020
|
+
// pageCollectionSchema is the scan's page front-matter schema, so entry.data
|
|
1021
|
+
// is typed and normalized the same way; it passes custom keys through and
|
|
1022
|
+
// falls back to defaults for a page the scan already dropped as invalid.
|
|
939
1023
|
const docs = defineCollection({
|
|
940
1024
|
loader: withIncludeRefresh(glob({
|
|
941
1025
|
pattern: ${JSON.stringify(docsPattern)},
|
|
942
1026
|
base: ${JSON.stringify(astroGlobBase(collectionBase))},
|
|
943
1027
|
generateId: ({ entry }) => entry,
|
|
944
1028
|
}), ${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}),
|
|
1029
|
+
schema: pageCollectionSchema,
|
|
945
1030
|
});
|
|
946
1031
|
${stagedBlock}
|
|
947
1032
|
export const collections = { docs${options.staged ? ", staged" : ""} };
|
|
@@ -980,26 +1065,38 @@ export const askEndpointTemplate = (
|
|
|
980
1065
|
const fallbackPrompt = instructions
|
|
981
1066
|
? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
|
|
982
1067
|
: ASK_FALLBACK_PROMPT;
|
|
1068
|
+
// Secrets go through Astro's `getSecret` rather than `process.env`, so each
|
|
1069
|
+
// adapter supplies them its own way (Cloudflare from the Worker's bindings,
|
|
1070
|
+
// Node and Vercel from the environment).
|
|
983
1071
|
const imports = [
|
|
984
1072
|
'import type { APIRoute } from "astro";',
|
|
985
|
-
'import {
|
|
1073
|
+
'import { getSecret } from "astro:env/server";',
|
|
1074
|
+
// The gateway provider reads the key (or Vercel's OIDC token) from the
|
|
1075
|
+
// environment itself; passing the key explicitly lets a binding-backed
|
|
1076
|
+
// secret store reach it too.
|
|
1077
|
+
backend.kind === "gateway"
|
|
1078
|
+
? 'import { createGateway, streamText } from "ai";'
|
|
1079
|
+
: 'import { streamText } from "ai";',
|
|
986
1080
|
];
|
|
987
1081
|
let setup = "";
|
|
988
1082
|
let modelExpr = JSON.stringify(backend.model);
|
|
989
|
-
if (backend.kind === "
|
|
1083
|
+
if (backend.kind === "gateway") {
|
|
1084
|
+
setup = `\nconst gateway = createGateway({ apiKey: getSecret("AI_GATEWAY_API_KEY") });\n`;
|
|
1085
|
+
modelExpr = `gateway(${JSON.stringify(backend.model)})`;
|
|
1086
|
+
} else if (backend.kind === "openrouter") {
|
|
990
1087
|
imports.push(
|
|
991
1088
|
'import { createOpenRouter } from "@openrouter/ai-sdk-provider";'
|
|
992
1089
|
);
|
|
993
|
-
setup = `\nconst openrouter = createOpenRouter({ apiKey:
|
|
1090
|
+
setup = `\nconst openrouter = createOpenRouter({ apiKey: getSecret(${JSON.stringify(
|
|
994
1091
|
backend.apiKeyEnv
|
|
995
|
-
)}
|
|
1092
|
+
)}) });\n`;
|
|
996
1093
|
modelExpr = `openrouter(${JSON.stringify(backend.model)})`;
|
|
997
1094
|
} else if (backend.kind === "openai-compatible") {
|
|
998
1095
|
imports.push(
|
|
999
1096
|
'import { createOpenAICompatible } from "@ai-sdk/openai-compatible";'
|
|
1000
1097
|
);
|
|
1001
1098
|
setup = `\nconst provider = createOpenAICompatible({
|
|
1002
|
-
apiKey:
|
|
1099
|
+
apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),
|
|
1003
1100
|
baseURL: ${JSON.stringify(backend.baseUrl)},
|
|
1004
1101
|
name: ${JSON.stringify(backend.name)},
|
|
1005
1102
|
});\n`;
|
|
@@ -1062,13 +1159,13 @@ export const askEndpointTemplate = (
|
|
|
1062
1159
|
const keyCheck =
|
|
1063
1160
|
backend.kind === "gateway"
|
|
1064
1161
|
? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
|
|
1065
|
-
if (!(
|
|
1162
|
+
if (!(getSecret("AI_GATEWAY_API_KEY") || getSecret("VERCEL_OIDC_TOKEN"))) {
|
|
1066
1163
|
return new Response(
|
|
1067
1164
|
"Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
|
|
1068
1165
|
{ status: 500 }
|
|
1069
1166
|
);
|
|
1070
1167
|
}`
|
|
1071
|
-
: ` if (!
|
|
1168
|
+
: ` if (!getSecret(${JSON.stringify(backend.apiKeyEnv)})) {
|
|
1072
1169
|
return new Response(
|
|
1073
1170
|
${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
|
|
1074
1171
|
{ status: 500 }
|
|
@@ -1176,8 +1273,9 @@ const searchClientImport = (module: string): string =>
|
|
|
1176
1273
|
`import { createSearch as create } from "blume/components/layout/search/${module}.ts";\n`;
|
|
1177
1274
|
|
|
1178
1275
|
// Joins a base-relative path onto BASE_URL, which arrives with or without a
|
|
1179
|
-
// trailing slash (Astro
|
|
1180
|
-
//
|
|
1276
|
+
// trailing slash (Astro normalizes `base` by `trailingSlash`, which the
|
|
1277
|
+
// generated config pins to "never" but an owned config may set either way —
|
|
1278
|
+
// naive concatenation of a bare `/docs` would yield `/docsblume-search.json`).
|
|
1181
1279
|
const SEARCH_BASE_IMPORT =
|
|
1182
1280
|
'import { joinBase } from "blume/components/islands/base-path.ts";\n';
|
|
1183
1281
|
|
|
@@ -1237,6 +1335,34 @@ const hostedSearchOptions = (
|
|
|
1237
1335
|
}
|
|
1238
1336
|
};
|
|
1239
1337
|
|
|
1338
|
+
/**
|
|
1339
|
+
* Generate `.blume/src/generated/features.ts` — the client-feature loaders
|
|
1340
|
+
* behind the `blume:features` alias. Each loader is a dynamic import when the
|
|
1341
|
+
* site uses the feature and `null` otherwise, so an unused library is absent
|
|
1342
|
+
* from the module graph (never bundled, never pre-bundled in dev) rather than
|
|
1343
|
+
* merely lazy. Regenerated on every pass: a page that gains a mermaid fence
|
|
1344
|
+
* turns the loader on.
|
|
1345
|
+
*/
|
|
1346
|
+
export const featuresTemplate = (features: ClientFeatures): string =>
|
|
1347
|
+
`// Generated by Blume. Do not edit.
|
|
1348
|
+
//
|
|
1349
|
+
// The client features this site uses. A feature no page needs stays out of
|
|
1350
|
+
// the module graph entirely: its loader is null and its library never enters
|
|
1351
|
+
// the client bundle.
|
|
1352
|
+
|
|
1353
|
+
/** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
|
|
1354
|
+
export const loadMermaid: (() => Promise<unknown>) | null = ${
|
|
1355
|
+
features.mermaid
|
|
1356
|
+
? '() => import("blume/components/content/mermaid-element.ts")'
|
|
1357
|
+
: "null"
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
/** The EPUB generator's browser bundle; null when export.epub is off. */
|
|
1361
|
+
export const loadEpub:
|
|
1362
|
+
| (() => Promise<typeof import("epub-gen-memory/bundle")>)
|
|
1363
|
+
| null = ${features.epub ? '() => import("epub-gen-memory/bundle")' : "null"};
|
|
1364
|
+
`;
|
|
1365
|
+
|
|
1240
1366
|
/**
|
|
1241
1367
|
* Generate `.blume/src/generated/search-client.ts` — the provider-specific
|
|
1242
1368
|
* loader the `<Search>` component lazy-imports via the `blume:search-client`
|
|
@@ -1291,11 +1417,12 @@ export const createSearch = () => create({ url });
|
|
|
1291
1417
|
export const mixedbreadSearchEndpointTemplate = (storeId: string): string =>
|
|
1292
1418
|
`// Generated by Blume. Do not edit.
|
|
1293
1419
|
import type { APIRoute } from "astro";
|
|
1420
|
+
import { getSecret } from "astro:env/server";
|
|
1294
1421
|
import Mixedbread from "@mixedbread/sdk";
|
|
1295
1422
|
|
|
1296
1423
|
export const prerender = false;
|
|
1297
1424
|
|
|
1298
|
-
const client = new Mixedbread({ apiKey:
|
|
1425
|
+
const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" });
|
|
1299
1426
|
const STORE_ID = ${JSON.stringify(storeId)};
|
|
1300
1427
|
|
|
1301
1428
|
export const POST: APIRoute = async ({ request }) => {
|
|
@@ -1557,10 +1684,77 @@ export function GET({ props }: { props: { section: string } }) {
|
|
|
1557
1684
|
}
|
|
1558
1685
|
`;
|
|
1559
1686
|
|
|
1687
|
+
/**
|
|
1688
|
+
* Generate the deferred sidebar fragments page
|
|
1689
|
+
* (`.blume/src/pages/blume-nav/[version]/[locale]/[id].astro`): one
|
|
1690
|
+
* prerendered partial per collapsible or drill-in group, holding just that
|
|
1691
|
+
* group's rows, which the sidebar fetches on first open instead of carrying
|
|
1692
|
+
* every collapsed section on every page. Ids are `navGroupIds` over the full
|
|
1693
|
+
* tree, the same ids the pages' sidebars use; the `version`/`locale`
|
|
1694
|
+
* segments pick the navigation variant (`current`/`default` for the
|
|
1695
|
+
* unversioned, unlocalized tree). Rendered with no current route — a
|
|
1696
|
+
* section that isn't on the active path has no active row by definition.
|
|
1697
|
+
*/
|
|
1698
|
+
export const navFragmentTemplate = (): string =>
|
|
1699
|
+
`---
|
|
1700
|
+
// Generated by Blume. Do not edit.
|
|
1701
|
+
import NavTree from "blume/components/layout/NavTree.astro";
|
|
1702
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1703
|
+
import { navGroupIds, navVariants } from "blume/components/layout/nav-utils.ts";
|
|
1704
|
+
import type { NavNode } from "blume/core/types.ts";
|
|
1705
|
+
import data from "blume:data";
|
|
1706
|
+
|
|
1707
|
+
export const prerender = true;
|
|
1708
|
+
// A partial: no doctype, head, or script/style injection — the rows only.
|
|
1709
|
+
export const partial = true;
|
|
1710
|
+
|
|
1711
|
+
// Only imports are in scope here (Astro hoists getStaticPaths), which is why
|
|
1712
|
+
// the variant walk lives in nav-utils.
|
|
1713
|
+
export function getStaticPaths() {
|
|
1714
|
+
return navVariants(data).flatMap(({ locale, navigation, version }) =>
|
|
1715
|
+
[...navGroupIds(navigation.sidebar)]
|
|
1716
|
+
.filter(([node]) => (node.display ?? "flat") !== "flat")
|
|
1717
|
+
.map(([, id]) => ({ params: { id, locale, version } }))
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const { id, locale, version } = Astro.params;
|
|
1722
|
+
const variant = navVariants(data).find(
|
|
1723
|
+
(entry) => entry.version === version && entry.locale === locale
|
|
1724
|
+
);
|
|
1725
|
+
const ids = variant
|
|
1726
|
+
? navGroupIds(variant.navigation.sidebar)
|
|
1727
|
+
: new Map<NavNode, string>();
|
|
1728
|
+
const node = [...ids].find(([, groupId]) => groupId === id)?.[0];
|
|
1729
|
+
if (!(variant && node && node.kind === "group")) {
|
|
1730
|
+
return new Response(null, { status: 404 });
|
|
1731
|
+
}
|
|
1732
|
+
const strings =
|
|
1733
|
+
locale === "default" ? data.ui.nav : (data.uiByLocale[locale] ?? data.ui).nav;
|
|
1734
|
+
const fragmentBase = withBase(\`/blume-nav/\${version}/\${locale}\`);
|
|
1735
|
+
---
|
|
1736
|
+
|
|
1737
|
+
<NavTree
|
|
1738
|
+
currentRoute=""
|
|
1739
|
+
depth={1}
|
|
1740
|
+
fragmentBase={fragmentBase}
|
|
1741
|
+
idPrefix={id}
|
|
1742
|
+
ids={ids}
|
|
1743
|
+
items={node.children}
|
|
1744
|
+
root={false}
|
|
1745
|
+
strings={strings}
|
|
1746
|
+
/>
|
|
1747
|
+
`;
|
|
1748
|
+
|
|
1560
1749
|
/** Generate the OG image endpoint (`.blume/src/pages/og/[...slug].png.ts`). */
|
|
1561
1750
|
export const ogEndpointTemplate = (
|
|
1562
1751
|
customRoutes: OgCustomRoute[] = [],
|
|
1563
1752
|
og: {
|
|
1753
|
+
/**
|
|
1754
|
+
* The on-disk card cache (directory plus the rendering Blume version).
|
|
1755
|
+
* Absent, every card renders on every build.
|
|
1756
|
+
*/
|
|
1757
|
+
cache?: OgCache;
|
|
1564
1758
|
families?: OgFontFamilies;
|
|
1565
1759
|
fonts?: OgFont[];
|
|
1566
1760
|
/**
|
|
@@ -1573,8 +1767,8 @@ export const ogEndpointTemplate = (
|
|
|
1573
1767
|
includeChangelog = false
|
|
1574
1768
|
): string =>
|
|
1575
1769
|
`// Generated by Blume. Do not edit.
|
|
1576
|
-
import {
|
|
1577
|
-
import type { OgFont, OgFontFamilies } from "blume/og";
|
|
1770
|
+
import { cachedOgImage } from "blume/og";
|
|
1771
|
+
import type { OgCache, OgFont, OgFontFamilies } from "blume/og";
|
|
1578
1772
|
import data from "blume:data";
|
|
1579
1773
|
|
|
1580
1774
|
export const prerender = true;
|
|
@@ -1593,6 +1787,14 @@ const families: OgFontFamilies | undefined = ${
|
|
|
1593
1787
|
og.families ? JSON.stringify(og.families) : "undefined"
|
|
1594
1788
|
};
|
|
1595
1789
|
|
|
1790
|
+
// Rendered cards are kept on disk between builds, keyed by everything that
|
|
1791
|
+
// decides their pixels, so a rebuild renders only the cards whose title,
|
|
1792
|
+
// description, branding, or fonts changed. The directory is a build-machine
|
|
1793
|
+
// path, baked in for the same reason as the local font paths above.
|
|
1794
|
+
const cache: OgCache | undefined = ${
|
|
1795
|
+
og.cache ? JSON.stringify(og.cache) : "undefined"
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1596
1798
|
// A page's own description (its \`seo.description\`, else \`description\`) is
|
|
1597
1799
|
// the card subtitle, so the image says what the page's og:description says.
|
|
1598
1800
|
// Pages without one fall back to the site-wide subtitle at render time.
|
|
@@ -1652,7 +1854,7 @@ const repoSlug = data.config.github
|
|
|
1652
1854
|
: undefined;
|
|
1653
1855
|
|
|
1654
1856
|
export async function GET({ props }: { props: CardProps }) {
|
|
1655
|
-
const png = await
|
|
1857
|
+
const png = await cachedOgImage(cache, {
|
|
1656
1858
|
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1657
1859
|
brand: data.config.title,
|
|
1658
1860
|
description: props.description ?? data.config.og.description,
|
|
@@ -1735,6 +1937,12 @@ export const catchAllPageTemplate = (options: {
|
|
|
1735
1937
|
exportEpub: boolean;
|
|
1736
1938
|
exportPdf: boolean;
|
|
1737
1939
|
mathEnabled: boolean;
|
|
1940
|
+
/**
|
|
1941
|
+
* Whether the sidebar defers collapsed sections to prerendered fragments
|
|
1942
|
+
* (see `navFragmentTemplate`); true when some group renders as a
|
|
1943
|
+
* disclosure or a drill-in panel.
|
|
1944
|
+
*/
|
|
1945
|
+
navFragments?: boolean;
|
|
1738
1946
|
/** Serialize the island-hooks snapshot; only needed when React is enabled. */
|
|
1739
1947
|
needsReact: boolean;
|
|
1740
1948
|
}): string => {
|
|
@@ -1883,9 +2091,9 @@ const headings =
|
|
|
1883
2091
|
tocHidden.size > 0
|
|
1884
2092
|
? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
|
|
1885
2093
|
: allHeadings;
|
|
1886
|
-
const frontmatter = entry.data
|
|
2094
|
+
const frontmatter = entry.data;
|
|
1887
2095
|
|
|
1888
|
-
const seo = frontmatter.seo
|
|
2096
|
+
const seo = frontmatter.seo;
|
|
1889
2097
|
const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
1890
2098
|
|
|
1891
2099
|
// Percent-encode the route-derived path (the sitemap convention): a Unicode
|
|
@@ -2113,7 +2321,12 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2113
2321
|
editUrl={editUrl}
|
|
2114
2322
|
feedback={data.config.feedback}
|
|
2115
2323
|
exportPdf={${options.exportPdf}}
|
|
2116
|
-
exportEpub={${options.exportEpub}}
|
|
2324
|
+
exportEpub={${options.exportEpub}}${
|
|
2325
|
+
options.navFragments
|
|
2326
|
+
? `
|
|
2327
|
+
navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n ? locale : "default"}\`)}`
|
|
2328
|
+
: ""
|
|
2329
|
+
}
|
|
2117
2330
|
openInChat={data.config.openInChat}
|
|
2118
2331
|
feeds={data.feeds}
|
|
2119
2332
|
discovery={data.config.discovery}
|
|
@@ -3037,71 +3250,6 @@ const Example = entry.Component;
|
|
|
3037
3250
|
</html>
|
|
3038
3251
|
`;
|
|
3039
3252
|
|
|
3040
|
-
/** Generate `.blume/src/env.d.ts`. */
|
|
3041
|
-
export const envTemplate =
|
|
3042
|
-
(): string => `/// <reference path="../.astro/types.d.ts" />
|
|
3043
|
-
/// <reference types="astro/client" />
|
|
3044
|
-
|
|
3045
|
-
declare module "blume:ask" {
|
|
3046
|
-
const Ask: typeof import("blume/components/islands/AskAI.astro").default;
|
|
3047
|
-
export default Ask;
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
declare module "blume:data" {
|
|
3051
|
-
const data: import("blume").BlumeData;
|
|
3052
|
-
export default data;
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
declare module "blume:ask-data" {
|
|
3056
|
-
const askData: import("blume/ai/ask-context.ts").AskData;
|
|
3057
|
-
export default askData;
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
declare module "blume:content-assets" {
|
|
3061
|
-
const assets: Record<string, string>;
|
|
3062
|
-
export default assets;
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
declare module "blume:mcp-data" {
|
|
3066
|
-
const data: import("blume/ai/mcp/data.ts").McpData;
|
|
3067
|
-
export default data;
|
|
3068
|
-
}
|
|
3069
|
-
|
|
3070
|
-
declare module "blume:raw-markdown" {
|
|
3071
|
-
const raw: Record<string, import("blume/ai/markdown.ts").RawMarkdownEntry>;
|
|
3072
|
-
export default raw;
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
declare module "blume:rss" {
|
|
3076
|
-
const feeds: Record<string, string>;
|
|
3077
|
-
export default feeds;
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
declare module "blume:search-index" {
|
|
3081
|
-
const documents: import("blume/search/documents.ts").SearchDocument[];
|
|
3082
|
-
export default documents;
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
declare module "blume:examples" {
|
|
3086
|
-
type Examples = typeof import("./generated/examples.ts").examples;
|
|
3087
|
-
export const examples: Record<string, Examples[keyof Examples]>;
|
|
3088
|
-
export const examplesBase: string;
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
declare module "blume:examples-theme";
|
|
3092
|
-
|
|
3093
|
-
declare module "blume:openapi" {
|
|
3094
|
-
const specs: import("blume/openapi/model.ts").OpenApiData;
|
|
3095
|
-
export default specs;
|
|
3096
|
-
}
|
|
3097
|
-
|
|
3098
|
-
declare module "blume:search-client" {
|
|
3099
|
-
export const createSearch: () =>
|
|
3100
|
-
| import("blume/components/layout/search/types.ts").SearchFn
|
|
3101
|
-
| Promise<import("blume/components/layout/search/types.ts").SearchFn>;
|
|
3102
|
-
}
|
|
3103
|
-
`;
|
|
3104
|
-
|
|
3105
3253
|
/** Generate `.blume/package.json`. */
|
|
3106
3254
|
export const runtimePackageTemplate = (dependencies: string[] = []): string =>
|
|
3107
3255
|
`${JSON.stringify(
|
package/src/audit/image-size.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import sharp from "sharp";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pixel dimensions read from an image header via
|
|
5
|
-
*
|
|
6
|
-
* included
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Pixel dimensions read from an image header via sharp, which is already the
|
|
5
|
+
* build's image optimizer and so covers every format the pipeline can emit —
|
|
6
|
+
* WebP and AVIF included. libvips reads only the header and rejects malformed
|
|
7
|
+
* input with an error rather than looping on it, which is why the previous
|
|
8
|
+
* pure-JS parser was retired: it could be driven into an infinite loop by a
|
|
9
|
+
* crafted ICNS, HEIF, or JXL file and hang the build. An unrecognized or
|
|
10
|
+
* truncated buffer yields null and its checks simply don't run.
|
|
9
11
|
*/
|
|
10
12
|
export interface ImageSize {
|
|
11
13
|
width: number;
|
|
@@ -13,9 +15,9 @@ export interface ImageSize {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** The image's pixel dimensions, or null when the format isn't recognized. */
|
|
16
|
-
export const imageSize = (bytes: Buffer): ImageSize | null => {
|
|
18
|
+
export const imageSize = async (bytes: Buffer): Promise<ImageSize | null> => {
|
|
17
19
|
try {
|
|
18
|
-
const { width, height } =
|
|
20
|
+
const { width, height } = await sharp(bytes).metadata();
|
|
19
21
|
return width > 0 && height > 0 ? { height, width } : null;
|
|
20
22
|
} catch {
|
|
21
23
|
return null;
|