blume 1.0.3 → 1.1.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 +94 -0
- package/dist/cli/index.js +13784 -10579
- package/dist/cli/index.js.map +93 -61
- package/dist/types/core/config-input.d.ts +87 -8
- package/dist/types/core/data.d.ts +21 -0
- package/dist/types/core/deployment-env.d.ts +6 -0
- package/dist/types/core/diagnostics.d.ts +23 -0
- package/dist/types/core/i18n-ui.d.ts +140 -140
- package/dist/types/core/schema.d.ts +549 -370
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +41 -0
- package/dist/types/core/types.d.ts +23 -0
- package/dist/types/og/card.d.ts +63 -0
- package/dist/types/og/dimensions.d.ts +12 -0
- package/dist/types/openapi/references.d.ts +12 -7
- package/docs/01-quickstart.mdx +1 -1
- package/docs/02-deployment.mdx +9 -1
- package/docs/advanced/api-reference.mdx +22 -3
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +1 -1
- package/docs/configuration/customization.mdx +1 -1
- package/docs/configuration/export.mdx +1 -1
- package/docs/configuration/index.mdx +21 -1
- package/docs/configuration/search.mdx +28 -1
- package/docs/configuration/seo.mdx +40 -2
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/components.mdx +15 -2
- package/docs/content/index.mdx +1 -1
- package/docs/content/meta.mdx +1 -1
- package/docs/content/navigation.mdx +11 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +116 -4
- package/docs/reference/cli.mdx +79 -1
- package/docs/reference/frontmatter.mdx +29 -1
- package/package.json +3 -3
- package/skills/blume-migrate/SKILL.md +170 -0
- package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
- package/skills/blume-migrate/references/docusaurus.md +95 -0
- package/skills/blume-migrate/references/fumadocs.md +95 -0
- package/skills/blume-migrate/references/mintlify.md +156 -0
- package/skills/blume-migrate/references/monorepo.md +224 -0
- package/skills/blume-migrate/references/nextra.md +76 -0
- package/skills/blume-migrate/references/starlight.md +116 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +478 -0
- package/src/ai/llms.ts +15 -0
- package/src/astro/adapter-root.ts +70 -0
- package/src/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +132 -42
- package/src/astro/index.ts +1 -0
- package/src/astro/pages.ts +18 -3
- package/src/astro/templates.ts +158 -56
- package/src/audit/agent.ts +114 -0
- package/src/audit/catalog.ts +826 -0
- package/src/audit/checks/assets.ts +177 -0
- package/src/audit/checks/content.ts +231 -0
- package/src/audit/checks/duplicates.ts +131 -0
- package/src/audit/checks/i18n.ts +246 -0
- package/src/audit/checks/indexability.ts +213 -0
- package/src/audit/checks/links.ts +223 -0
- package/src/audit/checks/llms.ts +135 -0
- package/src/audit/checks/network.ts +272 -0
- package/src/audit/checks/og-image.ts +113 -0
- package/src/audit/checks/redirects.ts +87 -0
- package/src/audit/checks/robots.ts +114 -0
- package/src/audit/checks/sitemap.ts +229 -0
- package/src/audit/checks/social.ts +238 -0
- package/src/audit/crawl.ts +259 -0
- package/src/audit/graph.ts +74 -0
- package/src/audit/html.ts +54 -0
- package/src/audit/image-size.ts +63 -0
- package/src/audit/locate.ts +33 -0
- package/src/audit/redirects.ts +74 -0
- package/src/audit/report.ts +278 -0
- package/src/audit/run.ts +198 -0
- package/src/audit/snapshot.ts +189 -0
- package/src/audit/types.ts +214 -0
- package/src/audit/url.ts +103 -0
- package/src/cli/commands/audit.ts +205 -0
- package/src/cli/commands/build.ts +51 -12
- package/src/cli/index.ts +2 -0
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/content/Tabs.astro +98 -15
- package/src/components/layout/Breadcrumbs.astro +1 -1
- package/src/components/layout/Header.astro +5 -8
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +2 -2
- package/src/components/layout/PageLayout.astro +9 -9
- package/src/components/layout/Pagination.astro +7 -7
- package/src/components/layout/RootLayout.astro +9 -11
- package/src/components/layout/Search.astro +36 -7
- package/src/components/layout/TableOfContents.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/components/openapi/Authorization.astro +80 -0
- package/src/components/openapi/Operation.astro +19 -1
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/security.ts +201 -0
- package/src/components/openapi/snippets.ts +42 -13
- package/src/core/config-input.ts +94 -8
- package/src/core/data.ts +18 -2
- package/src/core/deployment-env.ts +9 -0
- package/src/core/diagnostics.ts +59 -12
- package/src/core/links.ts +2 -91
- package/src/core/nav-diagnostics.ts +48 -4
- package/src/core/navigation.ts +55 -13
- package/src/core/probe.ts +136 -0
- package/src/core/project-graph.ts +8 -0
- package/src/core/schema.ts +100 -1
- package/src/core/sources/normalize.ts +198 -25
- package/src/core/sources/types.ts +3 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/standard-schema.ts +54 -0
- package/src/core/types.ts +23 -0
- package/src/deploy/adapter-output.ts +27 -15
- package/src/deploy/headers.ts +66 -0
- package/src/deploy/redirects.ts +49 -9
- package/src/markdown/index.ts +2 -0
- package/src/markdown/language-icon.ts +2 -1
- package/src/markdown/table-wrap.ts +43 -0
- package/src/og/card.ts +128 -36
- package/src/og/index.ts +1 -1
- package/src/og/logo.ts +21 -0
- package/src/openapi/references.ts +19 -16
- package/src/search/popular.ts +33 -0
- package/src/theme/entry.ts +56 -6
package/src/astro/templates.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { ResolvedConfig } from "../core/schema.ts";
|
|
|
10
10
|
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
11
11
|
import { trimChar } from "../core/trim.ts";
|
|
12
12
|
import type { ProjectContext } from "../core/types.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
|
|
14
14
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
15
15
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
16
16
|
import { buildFontEntries } from "../theme/fonts.ts";
|
|
@@ -80,6 +80,30 @@ const ADAPTER_OPTIONS: Record<string, string> = {
|
|
|
80
80
|
node: '{ mode: "standalone" }',
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const WRANGLER_CONFIG_FILES = [
|
|
84
|
+
"wrangler.jsonc",
|
|
85
|
+
"wrangler.json",
|
|
86
|
+
"wrangler.toml",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
|
|
90
|
+
const args: string[] = ['prerenderEnvironment: "node"'];
|
|
91
|
+
const wranglerPath = WRANGLER_CONFIG_FILES.map((file) =>
|
|
92
|
+
join(context.root, file)
|
|
93
|
+
).find((file) => existsSync(file));
|
|
94
|
+
if (wranglerPath) {
|
|
95
|
+
let configPath = relative(context.outDir, wranglerPath);
|
|
96
|
+
// The wrangler config always lives at the project root, above the `.blume`
|
|
97
|
+
// runtime, so `relative` yields a `../…` path; normalize the theoretical
|
|
98
|
+
// sibling case to an explicit `./` so it reads as a relative import.
|
|
99
|
+
if (!configPath.startsWith(".") && !configPath.startsWith("/")) {
|
|
100
|
+
configPath = `./${configPath}`;
|
|
101
|
+
}
|
|
102
|
+
args.push(`configPath: ${JSON.stringify(configPath)}`);
|
|
103
|
+
}
|
|
104
|
+
return `{ ${args.join(", ")} }`;
|
|
105
|
+
};
|
|
106
|
+
|
|
83
107
|
/**
|
|
84
108
|
* Integration packages the generated runtime imports. Declaring them in
|
|
85
109
|
* `.blume/package.json` lets Astro's framework-package crawl discover and bundle
|
|
@@ -142,11 +166,16 @@ export const runtimeDependencies = (options: {
|
|
|
142
166
|
* static-prerender Vite environments.
|
|
143
167
|
*
|
|
144
168
|
* Two reasons a dep lands here:
|
|
145
|
-
* -
|
|
146
|
-
* platform-specific `.node` binding via
|
|
147
|
-
* Bundling it relocates `import.meta.url`
|
|
148
|
-
* ("Cannot find native binding") on other
|
|
149
|
-
* runner), so it must resolve from
|
|
169
|
+
* - `takumi-js` (OG image rendering) loads `@takumi-rs/core`, a native NAPI
|
|
170
|
+
* addon that finds its platform-specific `.node` binding via
|
|
171
|
+
* `createRequire(import.meta.url)`. Bundling it relocates `import.meta.url`
|
|
172
|
+
* and breaks the binding lookup ("Cannot find native binding") on other
|
|
173
|
+
* platforms (e.g. the Linux CI runner), so it must resolve from
|
|
174
|
+
* `node_modules` at runtime instead. The prerender env matches these by
|
|
175
|
+
* exact specifier, so every entry point Blume imports has to be listed:
|
|
176
|
+
* the bare `takumi-js` (render) plus `takumi-js/helpers` (the `googleFonts`
|
|
177
|
+
* OG-font loader). The `@takumi-rs/*` packages are listed too so the native
|
|
178
|
+
* backend is never pulled into a chunk down any transitive path.
|
|
150
179
|
* - The rest are pure-JS packages kept external so an isolated linker (Bun's
|
|
151
180
|
* `isolated` mode, pnpm) doesn't bundle their symlinked store copies. When
|
|
152
181
|
* Vite bundles such a package but leaves its own `node_modules` child
|
|
@@ -166,10 +195,13 @@ const RENDER_EXTERNAL_DEPS = [
|
|
|
166
195
|
"@shikijs/transformers",
|
|
167
196
|
"@takumi-rs/core",
|
|
168
197
|
"@takumi-rs/helpers",
|
|
198
|
+
"@takumi-rs/wasm",
|
|
169
199
|
"github-slugger",
|
|
170
200
|
"katex",
|
|
171
201
|
"shiki",
|
|
172
202
|
"simple-icons",
|
|
203
|
+
"takumi-js",
|
|
204
|
+
"takumi-js/helpers",
|
|
173
205
|
"zod",
|
|
174
206
|
];
|
|
175
207
|
|
|
@@ -188,6 +220,21 @@ const renderUserAliases = (
|
|
|
188
220
|
const astroOutDir = (context: ProjectContext): string =>
|
|
189
221
|
context.distDir ?? `${context.root}/dist`;
|
|
190
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The root a deploy adapter is shown, in place of the `.blume` runtime Astro
|
|
225
|
+
* actually roots at. Adapters assume `outDir` is `<root>/dist` and resolve their
|
|
226
|
+
* own output (and Vercel's dependency trace) against `root`, so the root implied
|
|
227
|
+
* by Blume's `outDir` is the one that keeps that assumption true. See
|
|
228
|
+
* {@link withAdapterRoot}.
|
|
229
|
+
*
|
|
230
|
+
* For a normal build that is the project root (`<project>/dist` -> `<project>`).
|
|
231
|
+
* For a relocated runtime (`blume build --isolated`) it is the runtime dir
|
|
232
|
+
* itself (`<runtime>/dist` -> `<runtime>`), keeping a verify build's adapter
|
|
233
|
+
* output self-contained instead of overwriting the real `.vercel/output`.
|
|
234
|
+
*/
|
|
235
|
+
const adapterRoot = (context: ProjectContext): string =>
|
|
236
|
+
dirname(astroOutDir(context));
|
|
237
|
+
|
|
191
238
|
/**
|
|
192
239
|
* Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
|
|
193
240
|
* react() replaces the plugin's default `/node_modules/` exclude with just
|
|
@@ -263,12 +310,26 @@ export const astroConfigTemplate = (options: {
|
|
|
263
310
|
server && deployment.adapter
|
|
264
311
|
? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";\n`
|
|
265
312
|
: "";
|
|
266
|
-
const adapterArgs =
|
|
267
|
-
server
|
|
268
|
-
|
|
269
|
-
|
|
313
|
+
const adapterArgs = (() => {
|
|
314
|
+
if (!server || !deployment.adapter) {
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
if (deployment.adapter === "cloudflare") {
|
|
318
|
+
return resolveCloudflareAdapterArgs(context);
|
|
319
|
+
}
|
|
320
|
+
return ADAPTER_OPTIONS[deployment.adapter] ?? "";
|
|
321
|
+
})();
|
|
322
|
+
// Vercel resolves its Build Output tree and its `@vercel/nft` dependency
|
|
323
|
+
// trace against the Astro root, which for Blume is the hidden `.blume`
|
|
324
|
+
// runtime — leaving the traced function without its chunks or node_modules.
|
|
325
|
+
// The other adapters emit into `outDir` (cloudflare, node) or are surfaced
|
|
326
|
+
// afterwards (netlify), so none of them read `root` this way.
|
|
327
|
+
const adapterExpr =
|
|
328
|
+
deployment.adapter === "vercel"
|
|
329
|
+
? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})`
|
|
330
|
+
: `adapter(${adapterArgs})`;
|
|
270
331
|
const adapterOption =
|
|
271
|
-
server && deployment.adapter ? `\n adapter:
|
|
332
|
+
server && deployment.adapter ? `\n adapter: ${adapterExpr},` : "";
|
|
272
333
|
|
|
273
334
|
const siteOption = deployment.site
|
|
274
335
|
? `\n site: ${JSON.stringify(deployment.site)},`
|
|
@@ -291,10 +352,12 @@ export const astroConfigTemplate = (options: {
|
|
|
291
352
|
: "";
|
|
292
353
|
|
|
293
354
|
// Base the redirect paths the same way routes are based, so a redirect lands
|
|
294
|
-
// under `basePath` too. Astro layers its own `base` (deployment.base)
|
|
295
|
-
|
|
355
|
+
// under `basePath` too. Astro layers its own `base` (deployment.base) onto
|
|
356
|
+
// `from` when matching, but never onto `to` — see applyBaseToAstroRedirects.
|
|
357
|
+
const basedRedirects = applyBaseToAstroRedirects(
|
|
296
358
|
config.redirects,
|
|
297
|
-
config.basePath
|
|
359
|
+
config.basePath,
|
|
360
|
+
deployment.base ?? ""
|
|
298
361
|
);
|
|
299
362
|
const redirectsOption =
|
|
300
363
|
basedRedirects.length > 0
|
|
@@ -336,7 +399,13 @@ export const astroConfigTemplate = (options: {
|
|
|
336
399
|
const svelteImport = needsSvelte
|
|
337
400
|
? `import svelte from "@astrojs/svelte";\n`
|
|
338
401
|
: "";
|
|
339
|
-
const
|
|
402
|
+
const blumeImports = [
|
|
403
|
+
"blumeIntegration",
|
|
404
|
+
"prerenderDepsPlugin",
|
|
405
|
+
"serverAppResolvePlugin",
|
|
406
|
+
...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
|
|
407
|
+
];
|
|
408
|
+
const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
|
|
340
409
|
|
|
341
410
|
// Twoslash runs first, before the always-on transformers, but only on fences
|
|
342
411
|
// with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
|
|
@@ -410,6 +479,18 @@ export default defineConfig({
|
|
|
410
479
|
devToolbar: { enabled: false },
|
|
411
480
|
vite: {
|
|
412
481
|
plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
|
|
482
|
+
// Mermaid (lazy-loaded client-side for diagrams) statically imports dayjs as
|
|
483
|
+
// CJS (\`dayjs/dayjs.min.js\`). In dev, an un-pre-bundled dependency is served
|
|
484
|
+
// as raw ESM, and that UMD file exposes no \`default\` export, so mermaid
|
|
485
|
+
// throws on load and diagrams render blank. Forcing mermaid through the dep
|
|
486
|
+
// optimizer bundles dayjs with correct CJS interop. In a standalone install
|
|
487
|
+
// Blume's dynamic \`import("mermaid")\` lives inside \`node_modules/blume\`,
|
|
488
|
+
// which Vite's optimizer scan doesn't crawl, so mermaid is never discovered
|
|
489
|
+
// on its own — hence the explicit include. mermaid resolves through the
|
|
490
|
+
// \`blume\` package (it isn't a direct dep of the generated project), so the
|
|
491
|
+
// nested \`blume > mermaid\` form is required. Production (Rollup) already
|
|
492
|
+
// handles the interop, so this only affects dev.
|
|
493
|
+
optimizeDeps: { include: ["blume > mermaid"] },
|
|
413
494
|
// Blume's render-time deps are forced external on both build environments so
|
|
414
495
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
415
496
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -909,10 +990,11 @@ export function getStaticPaths() {
|
|
|
909
990
|
}));
|
|
910
991
|
}
|
|
911
992
|
|
|
912
|
-
export function GET({ props }) {
|
|
913
|
-
const
|
|
993
|
+
export function GET({ props }: { props: { route: string } }) {
|
|
994
|
+
const entries = raw as Record<string, { md?: string; mdx?: string }>;
|
|
995
|
+
const entry = entries[props.route];
|
|
914
996
|
return new Response(entry ? ${
|
|
915
|
-
kind === "md" ?
|
|
997
|
+
kind === "md" ? '(entry.md ?? entry.mdx ?? "")' : '(entry.mdx ?? "")'
|
|
916
998
|
} : "", {
|
|
917
999
|
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
918
1000
|
});
|
|
@@ -976,8 +1058,9 @@ export function getStaticPaths() {
|
|
|
976
1058
|
}));
|
|
977
1059
|
}
|
|
978
1060
|
|
|
979
|
-
export function GET({ props }) {
|
|
980
|
-
|
|
1061
|
+
export function GET({ props }: { props: { section: string } }) {
|
|
1062
|
+
const bySection = feeds as Record<string, string>;
|
|
1063
|
+
return new Response(bySection[props.section] ?? "", {
|
|
981
1064
|
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
|
|
982
1065
|
});
|
|
983
1066
|
}
|
|
@@ -989,7 +1072,7 @@ export const ogEndpointTemplate = (
|
|
|
989
1072
|
): string =>
|
|
990
1073
|
`// Generated by Blume. Do not edit.
|
|
991
1074
|
import { renderOgImage } from "blume/og";
|
|
992
|
-
import data from "
|
|
1075
|
+
import data from "blume:data";
|
|
993
1076
|
|
|
994
1077
|
export const prerender = true;
|
|
995
1078
|
|
|
@@ -997,9 +1080,9 @@ export const prerender = true;
|
|
|
997
1080
|
const customRoutes = ${JSON.stringify(customRoutes)};
|
|
998
1081
|
|
|
999
1082
|
export function getStaticPaths() {
|
|
1000
|
-
const seen = new Set();
|
|
1001
|
-
const paths = [];
|
|
1002
|
-
const add = (slug, title) => {
|
|
1083
|
+
const seen = new Set<string>();
|
|
1084
|
+
const paths: { params: { slug: string }; props: { title: string } }[] = [];
|
|
1085
|
+
const add = (slug: string, title: string) => {
|
|
1003
1086
|
if (seen.has(slug)) {
|
|
1004
1087
|
return;
|
|
1005
1088
|
}
|
|
@@ -1032,17 +1115,19 @@ const siteHost = (() => {
|
|
|
1032
1115
|
}
|
|
1033
1116
|
})();
|
|
1034
1117
|
|
|
1035
|
-
export async function GET({ props }) {
|
|
1118
|
+
export async function GET({ props }: { props: { title: string } }) {
|
|
1036
1119
|
const png = await renderOgImage({
|
|
1037
|
-
accent: data.config.theme.accent.light,
|
|
1120
|
+
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1038
1121
|
brand: data.config.title,
|
|
1039
1122
|
description: data.config.description,
|
|
1040
|
-
|
|
1123
|
+
fonts: data.config.og.fonts,
|
|
1124
|
+
logo: data.config.og.logo,
|
|
1125
|
+
palette: data.config.og.palette,
|
|
1041
1126
|
repo: repoSlug,
|
|
1042
1127
|
site: siteHost,
|
|
1043
1128
|
title: props.title,
|
|
1044
1129
|
});
|
|
1045
|
-
return new Response(png, {
|
|
1130
|
+
return new Response(new Uint8Array(png), {
|
|
1046
1131
|
headers: {
|
|
1047
1132
|
"Cache-Control": "public, max-age=31536000, immutable",
|
|
1048
1133
|
"Content-Type": "image/png",
|
|
@@ -1127,6 +1212,7 @@ export const catchAllPageTemplate = (options: {
|
|
|
1127
1212
|
return `---
|
|
1128
1213
|
// Generated by Blume. Do not edit.
|
|
1129
1214
|
import { getEntry, render } from "astro:content";
|
|
1215
|
+
import type { CollectionKey } from "astro:content";
|
|
1130
1216
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1131
1217
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1132
1218
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
@@ -1170,7 +1256,7 @@ import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
|
1170
1256
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
1171
1257
|
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
1172
1258
|
import { islandComponents } from "../generated/islands.ts";
|
|
1173
|
-
import data from "
|
|
1259
|
+
import data from "blume:data";
|
|
1174
1260
|
|
|
1175
1261
|
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
1176
1262
|
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
@@ -1239,7 +1325,7 @@ export function getStaticPaths() {
|
|
|
1239
1325
|
}
|
|
1240
1326
|
|
|
1241
1327
|
const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
|
|
1242
|
-
const entry = await getEntry(collection, entryId);
|
|
1328
|
+
const entry = await getEntry(collection as CollectionKey, entryId);
|
|
1243
1329
|
if (!entry) {
|
|
1244
1330
|
return new Response(null, { status: 404 });
|
|
1245
1331
|
}
|
|
@@ -1274,18 +1360,18 @@ const canonical =
|
|
|
1274
1360
|
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
|
|
1275
1361
|
// build hreflang alternates, and derive the language-switcher targets.
|
|
1276
1362
|
const i18n = data.config.i18n;
|
|
1277
|
-
const localePrefix = (codeArg) =>
|
|
1363
|
+
const localePrefix = (codeArg: string) =>
|
|
1278
1364
|
i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
|
|
1279
1365
|
? ""
|
|
1280
1366
|
: \`/\${codeArg}\`;
|
|
1281
|
-
const localizeRoute = (logical, codeArg) => {
|
|
1367
|
+
const localizeRoute = (logical: string, codeArg: string) => {
|
|
1282
1368
|
const prefix = localePrefix(codeArg);
|
|
1283
1369
|
if (!prefix) {
|
|
1284
1370
|
return logical;
|
|
1285
1371
|
}
|
|
1286
1372
|
return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
|
|
1287
1373
|
};
|
|
1288
|
-
const stripLocale = (path, codeArg) => {
|
|
1374
|
+
const stripLocale = (path: string, codeArg: string) => {
|
|
1289
1375
|
const prefix = localePrefix(codeArg);
|
|
1290
1376
|
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
1291
1377
|
};
|
|
@@ -1302,7 +1388,7 @@ const contentLocale =
|
|
|
1302
1388
|
const contentDir = i18n
|
|
1303
1389
|
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
1304
1390
|
: "ltr";
|
|
1305
|
-
const absolute = (path) => {
|
|
1391
|
+
const absolute = (path: string) => {
|
|
1306
1392
|
const p = withBase(path);
|
|
1307
1393
|
return base + (p === "/" ? "" : p);
|
|
1308
1394
|
};
|
|
@@ -1414,14 +1500,15 @@ import Update from "blume/components/content/Update.astro";
|
|
|
1414
1500
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1415
1501
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1416
1502
|
import { layoutOverrides } from "../generated/components.ts";
|
|
1417
|
-
import data from "
|
|
1503
|
+
import data from "blume:data";
|
|
1418
1504
|
|
|
1419
1505
|
export const prerender = true;
|
|
1420
1506
|
|
|
1421
|
-
const entryDate = (entry
|
|
1422
|
-
|
|
1507
|
+
const entryDate = (entry: {
|
|
1508
|
+
data: { date?: string | null; changelog?: { date?: string | null } | null };
|
|
1509
|
+
}) => entry.data.date ?? entry.data.changelog?.date ?? null;
|
|
1423
1510
|
|
|
1424
|
-
const toTime = (value) => {
|
|
1511
|
+
const toTime = (value: string | null | undefined) => {
|
|
1425
1512
|
if (!value) {
|
|
1426
1513
|
return 0;
|
|
1427
1514
|
}
|
|
@@ -1429,27 +1516,39 @@ const toTime = (value) => {
|
|
|
1429
1516
|
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
1430
1517
|
};
|
|
1431
1518
|
|
|
1432
|
-
|
|
1519
|
+
// The changelog is an unlocalized route, so its chrome renders in the default
|
|
1520
|
+
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1521
|
+
// dictionary), mirroring the catch-all's locale wiring.
|
|
1522
|
+
const i18n = data.config.i18n;
|
|
1523
|
+
const localeMeta = i18n
|
|
1524
|
+
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1525
|
+
: null;
|
|
1526
|
+
const dir = localeMeta?.dir ?? "ltr";
|
|
1527
|
+
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1528
|
+
|
|
1529
|
+
// Formatted in the same locale as the chrome, and in UTC, to match the
|
|
1530
|
+
// per-page "last updated" stamp.
|
|
1531
|
+
const formatDate = (value: string | null | undefined) => {
|
|
1433
1532
|
if (!value) {
|
|
1434
1533
|
return;
|
|
1435
1534
|
}
|
|
1436
1535
|
const date = new Date(value);
|
|
1437
1536
|
return Number.isNaN(date.getTime())
|
|
1438
1537
|
? undefined
|
|
1439
|
-
: new Intl.DateTimeFormat(
|
|
1538
|
+
: new Intl.DateTimeFormat(htmlLang, {
|
|
1440
1539
|
dateStyle: "long",
|
|
1441
1540
|
timeZone: "UTC",
|
|
1442
1541
|
}).format(date);
|
|
1443
1542
|
};
|
|
1444
1543
|
|
|
1445
|
-
const slugify = (text) =>
|
|
1544
|
+
const slugify = (text: string) =>
|
|
1446
1545
|
text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
|
|
1447
1546
|
"update";
|
|
1448
1547
|
|
|
1449
1548
|
// The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
|
|
1450
1549
|
// null when there is no full major.minor.patch to key on. Drives the changelog's
|
|
1451
1550
|
// group-by-major pagination, so it tolerates the scoped tags monorepos publish.
|
|
1452
|
-
const majorVersion = (version) => {
|
|
1551
|
+
const majorVersion = (version: string | null | undefined) => {
|
|
1453
1552
|
const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
|
|
1454
1553
|
return match ? Number(match[1]) : null;
|
|
1455
1554
|
};
|
|
@@ -1481,7 +1580,7 @@ const items = await Promise.all(
|
|
|
1481
1580
|
return {
|
|
1482
1581
|
Content: (await render(entry)).Content,
|
|
1483
1582
|
date: formatDate(entryDate(entry)),
|
|
1484
|
-
href: routeByEntry.get(entry.id) ??
|
|
1583
|
+
href: routeByEntry.get(entry.id) ?? undefined,
|
|
1485
1584
|
id: slugify(label),
|
|
1486
1585
|
label,
|
|
1487
1586
|
major: majorVersion(entry.data.changelog?.version),
|
|
@@ -1510,7 +1609,9 @@ for (const item of items) {
|
|
|
1510
1609
|
// semver and they span more than one major line. Older majors then collapse
|
|
1511
1610
|
// into groups the reader reveals one at a time; otherwise the timeline is flat.
|
|
1512
1611
|
const majors = items.every((item) => item.major !== null)
|
|
1513
|
-
? [...new Set(items.map((item) => item.major))]
|
|
1612
|
+
? [...new Set(items.map((item) => item.major))]
|
|
1613
|
+
.filter((major): major is number => major !== null)
|
|
1614
|
+
.toSorted((a, b) => b - a)
|
|
1514
1615
|
: [];
|
|
1515
1616
|
const paginate = majors.length > 1;
|
|
1516
1617
|
const majorGroups = majors.map((major) => ({
|
|
@@ -1531,16 +1632,6 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
|
1531
1632
|
const basedRoute = withBase("/changelog");
|
|
1532
1633
|
const canonical = base ? base + basedRoute : null;
|
|
1533
1634
|
|
|
1534
|
-
// The changelog is an unlocalized route, so its chrome renders in the default
|
|
1535
|
-
// locale's dictionary and direction (\`data.ui\` is the default locale's resolved
|
|
1536
|
-
// dictionary), mirroring the catch-all's locale wiring.
|
|
1537
|
-
const i18n = data.config.i18n;
|
|
1538
|
-
const localeMeta = i18n
|
|
1539
|
-
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
|
|
1540
|
-
: null;
|
|
1541
|
-
const dir = localeMeta?.dir ?? "ltr";
|
|
1542
|
-
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
1543
|
-
|
|
1544
1635
|
// The page chrome (h1, title, description) comes from the same translatable
|
|
1545
1636
|
// \`changelog\` group as the reveal button; optional chaining tolerates a
|
|
1546
1637
|
// not-yet-regenerated data snapshot from before these keys existed.
|
|
@@ -1658,7 +1749,7 @@ export const notFoundPageTemplate = (): string => `---
|
|
|
1658
1749
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1659
1750
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1660
1751
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1661
|
-
import data from "
|
|
1752
|
+
import data from "blume:data";
|
|
1662
1753
|
|
|
1663
1754
|
export const prerender = true;
|
|
1664
1755
|
|
|
@@ -1867,7 +1958,10 @@ export const getStaticPaths = () =>
|
|
|
1867
1958
|
Object.keys(examples).map((path) => ({ params: { path } }));
|
|
1868
1959
|
|
|
1869
1960
|
const { path } = Astro.params;
|
|
1870
|
-
const entry = examples[path];
|
|
1961
|
+
const entry = path ? examples[path] : undefined;
|
|
1962
|
+
if (!entry) {
|
|
1963
|
+
return new Response(null, { status: 404 });
|
|
1964
|
+
}
|
|
1871
1965
|
const Example = entry.Component;
|
|
1872
1966
|
---
|
|
1873
1967
|
|
|
@@ -1924,6 +2018,14 @@ declare module "blume:data" {
|
|
|
1924
2018
|
export default data;
|
|
1925
2019
|
}
|
|
1926
2020
|
|
|
2021
|
+
declare module "blume:examples" {
|
|
2022
|
+
type Examples = typeof import("./generated/examples.ts").examples;
|
|
2023
|
+
export const examples: Record<string, Examples[keyof Examples]>;
|
|
2024
|
+
export const examplesBase: string;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
declare module "blume:examples-theme";
|
|
2028
|
+
|
|
1927
2029
|
declare module "blume:openapi" {
|
|
1928
2030
|
const specs: import("blume/openapi/model.ts").OpenApiData;
|
|
1929
2031
|
export default specs;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { join } from "pathe";
|
|
6
|
+
|
|
7
|
+
import { reportJson } from "./report.ts";
|
|
8
|
+
import type { AuditResult } from "./run.ts";
|
|
9
|
+
|
|
10
|
+
/** A coding agent CLI the audit can hand its findings to (`--claude`, `--codex`). */
|
|
11
|
+
export interface AgentCli {
|
|
12
|
+
/** The executable to look up on PATH. */
|
|
13
|
+
bin: string;
|
|
14
|
+
/** How to install it, shown when the executable is missing. */
|
|
15
|
+
install: string;
|
|
16
|
+
/** Display name for messages. */
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AgentKind = "claude" | "codex";
|
|
21
|
+
|
|
22
|
+
export const AGENTS: Record<AgentKind, AgentCli> = {
|
|
23
|
+
claude: {
|
|
24
|
+
bin: "claude",
|
|
25
|
+
install: "npm install -g @anthropic-ai/claude-code",
|
|
26
|
+
name: "Claude Code",
|
|
27
|
+
},
|
|
28
|
+
codex: {
|
|
29
|
+
bin: "codex",
|
|
30
|
+
install: "npm install -g @openai/codex",
|
|
31
|
+
name: "Codex",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write the full JSON report where the agent can read it. A file rather than
|
|
37
|
+
* inline prompt text: a large site's report can exceed the platform's argv
|
|
38
|
+
* limit, and the JSON already carries every finding untruncated — the terminal
|
|
39
|
+
* report previews three pages per check, the file never does.
|
|
40
|
+
*/
|
|
41
|
+
export const writeAgentReport = async (
|
|
42
|
+
result: AuditResult,
|
|
43
|
+
root: string
|
|
44
|
+
): Promise<string> => {
|
|
45
|
+
const dir = await mkdtemp(join(tmpdir(), "blume-audit-"));
|
|
46
|
+
const path = join(dir, "report.json");
|
|
47
|
+
await writeFile(path, reportJson(result, root));
|
|
48
|
+
return path;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The handoff prompt: where the report is, how to read it, and the ground rules. */
|
|
52
|
+
export const fixPrompt = (reportPath: string): string =>
|
|
53
|
+
`Fix the issues found by \`blume audit\` in this project.
|
|
54
|
+
|
|
55
|
+
The full audit report is at ${reportPath}. It is JSON: each entry in \`diagnostics\` is one finding, with the check \`code\`, a \`message\` explaining what is wrong, the affected page \`url\`, the source \`file\` to edit (relative to the current directory, with a \`line\` when the finding points at a specific front matter key), and a \`suggestion\` describing the fix.
|
|
56
|
+
|
|
57
|
+
Work through every finding:
|
|
58
|
+
1. Read the report and group the findings by \`file\`.
|
|
59
|
+
2. Apply each finding's \`suggestion\` by editing the named source file — most fixes are front matter edits at the cited line.
|
|
60
|
+
3. Never fix a finding by deleting a page, removing content, or hiding it from the audit; if a finding genuinely needs a human decision, leave it and say so in your summary.
|
|
61
|
+
|
|
62
|
+
When you are done, run \`blume build\` and then \`blume audit\` to verify, and repeat until the audit reports no issues.`;
|
|
63
|
+
|
|
64
|
+
const spawnAgent = (
|
|
65
|
+
command: string,
|
|
66
|
+
args: string[],
|
|
67
|
+
shell: boolean
|
|
68
|
+
): Promise<number> =>
|
|
69
|
+
// oxlint-disable-next-line promise/avoid-new -- adapt spawn's event callbacks
|
|
70
|
+
new Promise((resolve, reject) => {
|
|
71
|
+
const child = spawn(command, args, { shell, stdio: "inherit" });
|
|
72
|
+
child.once("error", reject);
|
|
73
|
+
child.once("close", (code) => resolve(code ?? 1));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* cmd.exe reports a missing executable through this exit code instead of a
|
|
78
|
+
* spawn error, so a shell launch can't rely on the `error` event for the
|
|
79
|
+
* "not installed" diagnosis.
|
|
80
|
+
*/
|
|
81
|
+
export const WINDOWS_COMMAND_NOT_FOUND = 9009;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run the agent CLI interactively with the handoff prompt, inheriting the
|
|
85
|
+
* terminal so the user watches and steers the fixes rather than granting a
|
|
86
|
+
* headless process blanket write access. Resolves with the agent's exit code;
|
|
87
|
+
* rejects when the executable isn't on PATH.
|
|
88
|
+
*
|
|
89
|
+
* On Windows, npm installs agent CLIs as `.cmd` shims, which Node refuses to
|
|
90
|
+
* spawn without a shell — and cmd.exe cannot carry the multi-line prompt as an
|
|
91
|
+
* argument (a newline ends the command). So there the prompt is written to a
|
|
92
|
+
* file next to the report and handed over via a one-line pointer that survives
|
|
93
|
+
* cmd.exe quoting; a missing executable surfaces as
|
|
94
|
+
* {@link WINDOWS_COMMAND_NOT_FOUND} rather than a rejection.
|
|
95
|
+
*/
|
|
96
|
+
export const launchAgent = async (
|
|
97
|
+
bin: string,
|
|
98
|
+
prompt: string,
|
|
99
|
+
platform: NodeJS.Platform = process.platform
|
|
100
|
+
): Promise<number> => {
|
|
101
|
+
if (platform !== "win32") {
|
|
102
|
+
return await spawnAgent(bin, [prompt], false);
|
|
103
|
+
}
|
|
104
|
+
const dir = await mkdtemp(join(tmpdir(), "blume-audit-"));
|
|
105
|
+
const promptPath = join(dir, "prompt.md");
|
|
106
|
+
await writeFile(promptPath, prompt);
|
|
107
|
+
// Double quotes are the one grouping cmd.exe respects; neither the temp
|
|
108
|
+
// path nor the fixed pointer text can contain one.
|
|
109
|
+
return await spawnAgent(
|
|
110
|
+
`"${bin}" "Read ${promptPath} and follow its instructions exactly."`,
|
|
111
|
+
[],
|
|
112
|
+
true
|
|
113
|
+
);
|
|
114
|
+
};
|