blume 0.5.2 → 0.5.4
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/dist/cli/index.js +2235 -5942
- package/dist/cli/index.js.map +36 -65
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +2 -2
- package/dist/types/core/schema.d.ts +58 -453
- package/dist/types/core/types.d.ts +1 -7
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/theming.mdx +3 -11
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +1 -5
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +4 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/mcp/discovery.ts +3 -1
- package/src/ai/mcp/server.ts +3 -1
- package/src/astro/component-slots.ts +10 -2
- package/src/astro/generate.ts +15 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +62 -37
- package/src/cli/coalesce.ts +43 -0
- package/src/cli/commands/dev.ts +30 -22
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +4 -2
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +8 -5
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/Prompt.astro +8 -4
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/Tabs.astro +13 -2
- package/src/components/content/TypeTable.astro +5 -5
- package/src/components/content/Update.astro +1 -1
- package/src/components/content/mermaid-element.ts +20 -2
- package/src/components/islands/ask-ai.tsx +4 -8
- package/src/components/islands/base-path.ts +30 -0
- package/src/components/islands/hooks.ts +12 -8
- package/src/components/layout/Header.astro +26 -3
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +14 -14
- package/src/components/layout/PageActions.astro +17 -11
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +31 -2
- package/src/components/layout/Search.astro +9 -6
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/layout/search/types.ts +16 -5
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/SchemaProperty.astro +1 -1
- package/src/components/openapi/SchemaTable.astro +3 -3
- package/src/components/openapi/helpers.ts +17 -8
- package/src/components/openapi/snippets.ts +17 -4
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +19 -33
- package/src/core/data.ts +2 -2
- package/src/core/graph.ts +7 -5
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/navigation.ts +5 -6
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +13 -90
- package/src/core/sources/filesystem.ts +19 -1
- package/src/core/sources/mdx-remote.ts +20 -4
- package/src/core/sources/normalize.ts +28 -6
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +45 -0
- package/src/core/types.ts +1 -8
- package/src/markdown/code-title.ts +6 -3
- package/src/markdown/package-install.ts +3 -1
- package/src/og/card.ts +16 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +54 -11
- package/src/openapi/source.ts +19 -2
- package/src/search/sync/orama-cloud.ts +2 -0
- package/src/search/sync/typesense.ts +4 -0
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +22 -104
- package/src/theme/palette.ts +35 -47
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -123
- package/src/astro/static-assets.ts +0 -117
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -204
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -378
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -340
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -958
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -372
- package/src/migrate/mintlify/snippets.ts +0 -306
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -374
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -728
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -78
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
package/src/astro/generate.ts
CHANGED
|
@@ -432,11 +432,16 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
432
432
|
if (!logo) {
|
|
433
433
|
return null;
|
|
434
434
|
}
|
|
435
|
-
const config = typeof logo === "string" ? {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const href = config
|
|
435
|
+
const config = typeof logo === "string" ? { image: logo } : logo;
|
|
436
|
+
// `text` is passed through verbatim: `undefined` lets the brand fall back to
|
|
437
|
+
// the site title, `""` renders the mark alone (a logo with the wordmark baked
|
|
438
|
+
// in).
|
|
439
|
+
const { href, image: source, text } = config;
|
|
440
|
+
const image = typeof source === "string" ? { light: source } : source;
|
|
441
|
+
const light = image?.light ?? image?.dark;
|
|
442
|
+
const dark = image?.dark ?? image?.light;
|
|
443
|
+
const alt = image?.alt ?? "";
|
|
444
|
+
const brandHref = href ?? "/";
|
|
440
445
|
|
|
441
446
|
if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
|
|
442
447
|
const rel = light.replace(/^\//u, "");
|
|
@@ -445,10 +450,10 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
445
450
|
join(project.context.root, rel),
|
|
446
451
|
].find((path) => existsSync(path));
|
|
447
452
|
if (file) {
|
|
448
|
-
return { alt, href, svg: readFileSync(file, "utf-8") };
|
|
453
|
+
return { alt, href: brandHref, svg: readFileSync(file, "utf-8"), text };
|
|
449
454
|
}
|
|
450
455
|
}
|
|
451
|
-
return { alt, dark, href, light };
|
|
456
|
+
return { alt, dark, href: brandHref, light, text };
|
|
452
457
|
};
|
|
453
458
|
|
|
454
459
|
/**
|
|
@@ -615,7 +620,6 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
615
620
|
code,
|
|
616
621
|
withReferenceTabs(
|
|
617
622
|
graph.navigationByLocale[code] ?? {
|
|
618
|
-
chromeVariants: [],
|
|
619
623
|
selectors: [],
|
|
620
624
|
sidebar: [],
|
|
621
625
|
tabs: [],
|
|
@@ -648,7 +652,6 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
648
652
|
})),
|
|
649
653
|
}
|
|
650
654
|
: null,
|
|
651
|
-
icons: config.icons,
|
|
652
655
|
imageZoom: config.markdown.imageZoom,
|
|
653
656
|
logo: resolveLogo(project),
|
|
654
657
|
mcp: config.mcp.enabled
|
|
@@ -974,9 +977,9 @@ export const generateRuntime = async (
|
|
|
974
977
|
const staged = collectStaged(project);
|
|
975
978
|
const hasStaged = staged.size > 0;
|
|
976
979
|
// Only emit a project-scanning `docs` collection when a filesystem source
|
|
977
|
-
// actually feeds it.
|
|
978
|
-
// `docs` glob would otherwise scan (and watch) the whole
|
|
979
|
-
// nothing — see contentConfigTemplate.
|
|
980
|
+
// actually feeds it. An all-staged project (openapi/notion/…) has only staged
|
|
981
|
+
// sources, so the `docs` glob would otherwise scan (and watch) the whole
|
|
982
|
+
// project root for nothing — see contentConfigTemplate.
|
|
980
983
|
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
981
984
|
|
|
982
985
|
const structural = await Promise.all([
|
package/src/astro/integration.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
|
|
4
3
|
import type { AstroIntegration } from "astro";
|
|
5
4
|
|
|
6
|
-
import type { AssetMount } from "../core/assets.ts";
|
|
7
5
|
import { enrichDiagnostic } from "../core/diagnostics.ts";
|
|
8
6
|
import type { Diagnostic } from "../core/types.ts";
|
|
9
7
|
import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
|
|
10
|
-
import { copyAssetMounts, serveAssetMounts } from "./static-assets.ts";
|
|
11
8
|
|
|
12
9
|
/** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
|
|
13
10
|
interface OverlayChannel {
|
|
@@ -77,11 +74,6 @@ export interface BlumeIntegrationOptions {
|
|
|
77
74
|
contentRoutes: string[];
|
|
78
75
|
/** Configured `deployment.base`, stripped from dev URLs before matching. */
|
|
79
76
|
base?: string;
|
|
80
|
-
/**
|
|
81
|
-
* `content.assets` mounts: top-level dirs served at the site root in dev and
|
|
82
|
-
* copied into the build output, so root-served assets need no relocation.
|
|
83
|
-
*/
|
|
84
|
-
assets?: AssetMount[];
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
/**
|
|
@@ -118,12 +110,6 @@ export const blumeIntegration = (
|
|
|
118
110
|
options: BlumeIntegrationOptions
|
|
119
111
|
): AstroIntegration => ({
|
|
120
112
|
hooks: {
|
|
121
|
-
"astro:build:done": async ({ dir }) => {
|
|
122
|
-
// Copy in-place asset mounts into the build output, mirroring publicDir.
|
|
123
|
-
if (options.assets?.length) {
|
|
124
|
-
await copyAssetMounts(options.assets, fileURLToPath(dir));
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
113
|
"astro:config:setup": ({ injectRoute }) => {
|
|
128
114
|
for (const page of options.pages) {
|
|
129
115
|
injectRoute({
|
|
@@ -143,13 +129,6 @@ export const blumeIntegration = (
|
|
|
143
129
|
handle: negotiateMarkdown(new Set(options.contentRoutes), options.base),
|
|
144
130
|
route: "",
|
|
145
131
|
});
|
|
146
|
-
// Serve `content.assets` mounts (Astro only serves publicDir in dev).
|
|
147
|
-
if (options.assets?.length) {
|
|
148
|
-
server.middlewares.stack.unshift({
|
|
149
|
-
handle: serveAssetMounts(options.assets),
|
|
150
|
-
route: "",
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
132
|
},
|
|
154
133
|
},
|
|
155
134
|
name: "blume",
|
package/src/astro/templates.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { dirname, isAbsolute, join, relative } from "pathe";
|
|
|
4
4
|
|
|
5
5
|
import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
6
6
|
import type { AskBackend } from "../ai/ask.ts";
|
|
7
|
-
import { resolveAssetMounts } from "../core/assets.ts";
|
|
8
7
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
9
8
|
import type { ProjectContext } from "../core/types.ts";
|
|
10
9
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
@@ -313,12 +312,10 @@ export const astroConfigTemplate = (options: {
|
|
|
313
312
|
if (needsSvelte) {
|
|
314
313
|
integrations.push("svelte()");
|
|
315
314
|
}
|
|
316
|
-
// Always mounted: injects user pages (a no-op when there are none)
|
|
317
|
-
//
|
|
318
|
-
// negotiation over the content routes.
|
|
319
|
-
const assets = resolveAssetMounts(context.root, config.content.assets);
|
|
315
|
+
// Always mounted: injects user pages (a no-op when there are none) and wires
|
|
316
|
+
// up dev-server `Accept: text/markdown` negotiation over the content routes.
|
|
320
317
|
integrations.push(
|
|
321
|
-
`blumeIntegration(${JSON.stringify({
|
|
318
|
+
`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`
|
|
322
319
|
);
|
|
323
320
|
|
|
324
321
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
@@ -374,6 +371,15 @@ export default defineConfig({
|
|
|
374
371
|
fs: {
|
|
375
372
|
allow: ${JSON.stringify(fsAllow)},
|
|
376
373
|
},
|
|
374
|
+
// Keep the file watcher out of Astro's own cache dir. In a migrated
|
|
375
|
+
// (root-rooted) project the docs collection is rooted at the project dir,
|
|
376
|
+
// so its glob-loader watcher would otherwise fire on every write Astro
|
|
377
|
+
// makes under .blume/.astro (data-store.json, content module manifests,
|
|
378
|
+
// self-hosted fonts) -- pure noise the loader logs as "No entry type
|
|
379
|
+
// found". Vite appends this to its default ignores.
|
|
380
|
+
watch: {
|
|
381
|
+
ignored: ${JSON.stringify([join(context.outDir, ".astro", "**")])},
|
|
382
|
+
},
|
|
377
383
|
},
|
|
378
384
|
},
|
|
379
385
|
});
|
|
@@ -394,8 +400,8 @@ export const contentConfigTemplate = (options: {
|
|
|
394
400
|
stagedBase?: string;
|
|
395
401
|
/**
|
|
396
402
|
* Whether any filesystem (non-staged) source feeds the `docs` collection.
|
|
397
|
-
* When false (e.g.
|
|
398
|
-
* collection globs nothing — see below.
|
|
403
|
+
* When false (e.g. an all-staged project where every page is materialized by
|
|
404
|
+
* a non-filesystem source), the collection globs nothing — see below.
|
|
399
405
|
*/
|
|
400
406
|
filesystem?: boolean;
|
|
401
407
|
}): string => {
|
|
@@ -414,8 +420,8 @@ export const contentConfigTemplate = (options: {
|
|
|
414
420
|
|
|
415
421
|
// With no filesystem source, no route renders through `docs`, so glob nothing.
|
|
416
422
|
// Beyond skipping wasted work, this is the only thing that keeps Astro's
|
|
417
|
-
// content-layer *watcher* out of `.blume/`:
|
|
418
|
-
// at the project dir (which contains `.blume/.astro/fonts`, rewritten on every
|
|
423
|
+
// content-layer *watcher* out of `.blume/`: an all-staged project roots the
|
|
424
|
+
// collection at the project dir (which contains `.blume/.astro/fonts`, rewritten on every
|
|
419
425
|
// request), and the watcher's match test is `picomatch.isMatch(path, pattern)`
|
|
420
426
|
// — with array-OR semantics, any `!ignored/**` negation *matches* unrelated
|
|
421
427
|
// files, so negative patterns can't exclude a subtree there. An empty pattern
|
|
@@ -503,20 +509,35 @@ export const askEndpointTemplate = (
|
|
|
503
509
|
}
|
|
504
510
|
// Validate the client-supplied body and cap its size. The endpoint is
|
|
505
511
|
// unauthenticated, so bounding message count/length limits how much a caller
|
|
506
|
-
// can spend against the model per request
|
|
507
|
-
//
|
|
512
|
+
// can spend against the model per request, and restricting roles to
|
|
513
|
+
// user/assistant keeps callers from injecting their own system prompt and
|
|
514
|
+
// repurposing the endpoint as a general LLM proxy; front it with a rate
|
|
515
|
+
// limiter (or your provider's limits) for stronger protection.
|
|
508
516
|
const validate = ` const body = await request.json().catch(() => null);
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
517
|
+
const raw = body?.messages;
|
|
518
|
+
const valid =
|
|
519
|
+
Array.isArray(raw) &&
|
|
520
|
+
raw.length > 0 &&
|
|
521
|
+
raw.length <= 40 &&
|
|
522
|
+
raw.every(
|
|
523
|
+
(m: unknown) =>
|
|
524
|
+
typeof m === "object" &&
|
|
525
|
+
m !== null &&
|
|
526
|
+
("role" in m && (m.role === "user" || m.role === "assistant")) &&
|
|
527
|
+
("content" in m && typeof m.content === "string")
|
|
528
|
+
) &&
|
|
529
|
+
JSON.stringify(raw).length <= 24_000;
|
|
530
|
+
if (!valid) {
|
|
531
|
+
return new Response(
|
|
532
|
+
"Invalid request: send 1-40 user/assistant messages with string content.",
|
|
533
|
+
{ status: 400 }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
// Re-build the array so only role/content ever reach the model.
|
|
537
|
+
const messages = raw.map((m: { role: "user" | "assistant"; content: string }) => ({
|
|
538
|
+
content: m.content,
|
|
539
|
+
role: m.role,
|
|
540
|
+
}));`;
|
|
520
541
|
const stream = grounded
|
|
521
542
|
? ` const system =
|
|
522
543
|
(await ground(messages, body.page)) ??
|
|
@@ -570,10 +591,16 @@ const SEARCH_CLIENT_HEADER = "// Generated by Blume. Do not edit.\n";
|
|
|
570
591
|
const searchClientImport = (module: string): string =>
|
|
571
592
|
`import { createSearch as create } from "blume/components/layout/search/${module}.ts";\n`;
|
|
572
593
|
|
|
594
|
+
// Joins a base-relative path onto BASE_URL, which arrives with or without a
|
|
595
|
+
// trailing slash (Astro's default trailingSlash: "ignore" passes `/docs`
|
|
596
|
+
// through bare — naive concatenation would yield `/docsblume-search.json`).
|
|
597
|
+
const SEARCH_BASE_IMPORT =
|
|
598
|
+
'import { joinBase } from "blume/components/islands/base-path.ts";\n';
|
|
599
|
+
|
|
573
600
|
/** A client that loads a static `blume-search.json` index (Orama, FlexSearch). */
|
|
574
601
|
const staticSearchClient = (module: string): string =>
|
|
575
|
-
`${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
|
|
576
|
-
const indexUrl =
|
|
602
|
+
`${SEARCH_CLIENT_HEADER}${searchClientImport(module)}${SEARCH_BASE_IMPORT}
|
|
603
|
+
const indexUrl = joinBase(import.meta.env.BASE_URL, "blume-search.json");
|
|
577
604
|
|
|
578
605
|
export const createSearch = () => create({ indexUrl });
|
|
579
606
|
`;
|
|
@@ -633,16 +660,16 @@ export const searchClientTemplate = (config: ResolvedConfig): string => {
|
|
|
633
660
|
}
|
|
634
661
|
|
|
635
662
|
if (search.provider === "mixedbread") {
|
|
636
|
-
return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}
|
|
637
|
-
const api =
|
|
663
|
+
return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
|
|
664
|
+
const api = joinBase(import.meta.env.BASE_URL, "api/search");
|
|
638
665
|
|
|
639
666
|
export const createSearch = () => create({ api });
|
|
640
667
|
`;
|
|
641
668
|
}
|
|
642
669
|
|
|
643
670
|
if (search.provider === "pagefind") {
|
|
644
|
-
return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}
|
|
645
|
-
const url =
|
|
671
|
+
return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}${SEARCH_BASE_IMPORT}
|
|
672
|
+
const url = joinBase(import.meta.env.BASE_URL, "pagefind/pagefind.js");
|
|
646
673
|
|
|
647
674
|
export const createSearch = () => create({ url });
|
|
648
675
|
`;
|
|
@@ -671,8 +698,10 @@ const client = new Mixedbread({ apiKey: process.env.MIXEDBREAD_API_KEY ?? "" });
|
|
|
671
698
|
const STORE_ID = ${JSON.stringify(storeId)};
|
|
672
699
|
|
|
673
700
|
export const POST: APIRoute = async ({ request }) => {
|
|
674
|
-
|
|
675
|
-
|
|
701
|
+
// The endpoint is public: a malformed body must 200-empty, not 500.
|
|
702
|
+
const body = await request.json().catch(() => null);
|
|
703
|
+
const query = body?.query;
|
|
704
|
+
if (!query || typeof query !== "string") {
|
|
676
705
|
return new Response("[]", {
|
|
677
706
|
headers: { "Content-Type": "application/json" },
|
|
678
707
|
});
|
|
@@ -948,10 +977,7 @@ import FileTree from "blume/components/content/FileTree.astro";
|
|
|
948
977
|
import Frame from "blume/components/content/Frame.astro";
|
|
949
978
|
import GithubInfo from "blume/components/content/GithubInfo.astro";
|
|
950
979
|
import Panel from "blume/components/content/Panel.astro";
|
|
951
|
-
import ParamField from "blume/components/content/ParamField.astro";
|
|
952
980
|
import Prompt from "blume/components/content/Prompt.astro";
|
|
953
|
-
import RequestField from "blume/components/content/RequestField.astro";
|
|
954
|
-
import ResponseField from "blume/components/content/ResponseField.astro";
|
|
955
981
|
import Step from "blume/components/content/Step.astro";
|
|
956
982
|
import Steps from "blume/components/content/Steps.astro";
|
|
957
983
|
import Tab from "blume/components/content/Tab.astro";
|
|
@@ -966,6 +992,7 @@ import Visibility from "blume/components/content/Visibility.astro";
|
|
|
966
992
|
import YouTube from "blume/components/content/YouTube.astro";
|
|
967
993
|
import Icon from "blume/components/Icon.astro";
|
|
968
994
|
import ApiOverview from "blume/components/openapi/ApiOverview.astro";
|
|
995
|
+
import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
969
996
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
970
997
|
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
971
998
|
import { islandComponents } from "../generated/islands.ts";
|
|
@@ -984,6 +1011,7 @@ const components = {
|
|
|
984
1011
|
Accordion,
|
|
985
1012
|
AccordionItem,
|
|
986
1013
|
ApiOverview,
|
|
1014
|
+
ApiTagOperations,
|
|
987
1015
|
AutoTypeTable,
|
|
988
1016
|
Badge,
|
|
989
1017
|
Callout,
|
|
@@ -1003,10 +1031,7 @@ const components = {
|
|
|
1003
1031
|
Icon,
|
|
1004
1032
|
Operation,
|
|
1005
1033
|
Panel,
|
|
1006
|
-
ParamField,
|
|
1007
1034
|
Prompt,
|
|
1008
|
-
RequestField,
|
|
1009
|
-
ResponseField,
|
|
1010
1035
|
Step,
|
|
1011
1036
|
Steps,
|
|
1012
1037
|
Tab,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an async task so it never runs concurrently with itself. Triggering the
|
|
3
|
+
* returned function while a run is in flight coalesces into a single trailing
|
|
4
|
+
* run after the current one settles.
|
|
5
|
+
*
|
|
6
|
+
* Dev regeneration (`scanProject` + `generateRuntime`) is expensive on a large
|
|
7
|
+
* project — a full content re-scan that allocates big strings. A plain debounce
|
|
8
|
+
* still lets a fast burst of watch events (or, before it was fixed, a `.blume/`
|
|
9
|
+
* watch storm) start a new scan before the previous finished, piling up
|
|
10
|
+
* overlapping scans until the heap is exhausted (observed as an OOM after
|
|
11
|
+
* minutes of looping). Single-flighting bounds it to one scan at a time while
|
|
12
|
+
* still guaranteeing a final run reflects the latest change.
|
|
13
|
+
*
|
|
14
|
+
* The task must not reject: a rejection would surface as an unhandled promise
|
|
15
|
+
* rejection, so callers handle their own errors and always resolve.
|
|
16
|
+
*/
|
|
17
|
+
export const coalescedRunner = (task: () => Promise<void>): (() => void) => {
|
|
18
|
+
let inFlight: Promise<void> | null = null;
|
|
19
|
+
let pending = false;
|
|
20
|
+
|
|
21
|
+
// Drain any run requested during the current run, then release the lock. The
|
|
22
|
+
// `inFlight` promise is assigned synchronously by the caller below, so a
|
|
23
|
+
// re-entrant trigger sees the lock immediately and only sets `pending`.
|
|
24
|
+
const cycle = async (): Promise<void> => {
|
|
25
|
+
try {
|
|
26
|
+
do {
|
|
27
|
+
pending = false;
|
|
28
|
+
// oxlint-disable-next-line no-await-in-loop -- serialized by design
|
|
29
|
+
await task();
|
|
30
|
+
} while (pending);
|
|
31
|
+
} finally {
|
|
32
|
+
inFlight = null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
if (inFlight) {
|
|
38
|
+
pending = true;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
inFlight = cycle();
|
|
42
|
+
};
|
|
43
|
+
};
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { generateRuntime } from "../../astro/generate.ts";
|
|
|
7
7
|
import { showBlumeErrorOverlay } from "../../astro/integration.ts";
|
|
8
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
9
|
import { parsePort } from "../args.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { coalescedRunner } from "../coalesce.ts";
|
|
11
|
+
import { acquireDevLock, isDevLocked } from "../dev-lock.ts";
|
|
11
12
|
import { logger } from "../log.ts";
|
|
12
13
|
import { prepareProject } from "../prepare.ts";
|
|
13
14
|
|
|
@@ -55,14 +56,16 @@ export const devCommand = defineCommand({
|
|
|
55
56
|
strict: args.strict,
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Claim the shared `.blume` dir so a concurrent build/eject/sync refuses
|
|
60
|
+
// rather than regenerating or deleting it out from under this server. A
|
|
61
|
+
// second dev server would fight over the same generated tree the same
|
|
62
|
+
// way, so it must refuse too instead of silently clobbering the lock.
|
|
63
|
+
if (isDevLocked(project.context.outDir)) {
|
|
64
|
+
logger.error(
|
|
65
|
+
"Another `blume dev` is already running in this project; two dev servers would corrupt the shared .blume dir. Stop the other one first (or delete .blume/dev.lock if it crashed)."
|
|
61
66
|
);
|
|
67
|
+
process.exit(1);
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
// Claim the shared `.blume` dir so a concurrent build/eject/sync refuses
|
|
65
|
-
// rather than regenerating or deleting it out from under this server.
|
|
66
69
|
const releaseLock = acquireDevLock(project.context.outDir);
|
|
67
70
|
process.on("exit", releaseLock);
|
|
68
71
|
|
|
@@ -82,26 +85,31 @@ export const devCommand = defineCommand({
|
|
|
82
85
|
|
|
83
86
|
// Watch user inputs and regenerate the runtime data on change. Astro/Vite
|
|
84
87
|
// hot-reloads the generated data module so nav and routes stay in sync.
|
|
88
|
+
// `coalescedRunner` single-flights the scan so a burst of watch events can
|
|
89
|
+
// never stack overlapping regenerations (a large project's scan can outlast
|
|
90
|
+
// the debounce; piled-up scans exhaust the heap).
|
|
91
|
+
const runRegenerate = coalescedRunner(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const next = await scanProject(root, {
|
|
94
|
+
devServerUrl,
|
|
95
|
+
mode: "dev",
|
|
96
|
+
overrides,
|
|
97
|
+
preview,
|
|
98
|
+
});
|
|
99
|
+
await generateRuntime(next);
|
|
100
|
+
// Surface any content/config errors in the browser overlay too.
|
|
101
|
+
showBlumeErrorOverlay(next.diagnostics);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error(`Regeneration failed: ${(error as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
85
107
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
86
108
|
const regenerate = () => {
|
|
87
109
|
if (timer) {
|
|
88
110
|
clearTimeout(timer);
|
|
89
111
|
}
|
|
90
|
-
timer = setTimeout(
|
|
91
|
-
try {
|
|
92
|
-
const next = await scanProject(root, {
|
|
93
|
-
devServerUrl,
|
|
94
|
-
mode: "dev",
|
|
95
|
-
overrides,
|
|
96
|
-
preview,
|
|
97
|
-
});
|
|
98
|
-
await generateRuntime(next);
|
|
99
|
-
// Surface any content/config errors in the browser overlay too.
|
|
100
|
-
showBlumeErrorOverlay(next.diagnostics);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
logger.error(`Regeneration failed: ${(error as Error).message}`);
|
|
103
|
-
}
|
|
104
|
-
}, 80);
|
|
112
|
+
timer = setTimeout(runRegenerate, 80);
|
|
105
113
|
};
|
|
106
114
|
|
|
107
115
|
// Content is watched per source (filesystem uses fs.watch; remote sources
|
|
@@ -3,7 +3,6 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
|
|
6
|
-
import { resolveAssetMounts } from "../../core/assets.ts";
|
|
7
6
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
8
7
|
import { validateLinks } from "../../core/links.ts";
|
|
9
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
@@ -48,7 +47,6 @@ export const validateCommand = defineCommand({
|
|
|
48
47
|
const publicDir = join(root, "public");
|
|
49
48
|
diagnostics.push(
|
|
50
49
|
...(await validateLinks(project.graph, {
|
|
51
|
-
assetMounts: resolveAssetMounts(root, project.config.content.assets),
|
|
52
50
|
checkExternal: Boolean(args.external),
|
|
53
51
|
publicDir: existsSync(publicDir) ? publicDir : null,
|
|
54
52
|
redirects: project.config.redirects,
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -38,8 +38,10 @@ export const isDevLocked = (outDir: string): boolean => {
|
|
|
38
38
|
// Signal 0 probes liveness without actually signaling the process.
|
|
39
39
|
process.kill(pid, 0);
|
|
40
40
|
return true;
|
|
41
|
-
} catch {
|
|
42
|
-
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// EPERM means the process exists but belongs to another user — still
|
|
43
|
+
// live, so the lock must hold (only ESRCH proves it's gone).
|
|
44
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
|
package/src/cli/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { devCommand } from "./commands/dev.ts";
|
|
|
8
8
|
import { doctorCommand } from "./commands/doctor.ts";
|
|
9
9
|
import { ejectCommand } from "./commands/eject.ts";
|
|
10
10
|
import { initCommand } from "./commands/init.ts";
|
|
11
|
-
import { migrateCommand } from "./commands/migrate.ts";
|
|
12
11
|
import { previewCommand } from "./commands/preview.ts";
|
|
13
12
|
import { syncCommand } from "./commands/sync.ts";
|
|
14
13
|
import { validateCommand } from "./commands/validate.ts";
|
|
@@ -29,7 +28,6 @@ const main = defineCommand({
|
|
|
29
28
|
doctor: doctorCommand,
|
|
30
29
|
eject: ejectCommand,
|
|
31
30
|
init: initCommand,
|
|
32
|
-
migrate: migrateCommand,
|
|
33
31
|
preview: previewCommand,
|
|
34
32
|
sync: syncCommand,
|
|
35
33
|
validate: validateCommand,
|
|
@@ -38,10 +38,7 @@ import FileTree from "./content/FileTree.astro";
|
|
|
38
38
|
import Frame from "./content/Frame.astro";
|
|
39
39
|
import GithubInfo from "./content/GithubInfo.astro";
|
|
40
40
|
import Panel from "./content/Panel.astro";
|
|
41
|
-
import ParamField from "./content/ParamField.astro";
|
|
42
41
|
import Prompt from "./content/Prompt.astro";
|
|
43
|
-
import RequestField from "./content/RequestField.astro";
|
|
44
|
-
import ResponseField from "./content/ResponseField.astro";
|
|
45
42
|
import Step from "./content/Step.astro";
|
|
46
43
|
import Steps from "./content/Steps.astro";
|
|
47
44
|
import Tab from "./content/Tab.astro";
|
|
@@ -90,10 +87,7 @@ const components = {
|
|
|
90
87
|
GithubInfo,
|
|
91
88
|
Icon,
|
|
92
89
|
Panel,
|
|
93
|
-
ParamField,
|
|
94
90
|
Prompt,
|
|
95
|
-
RequestField,
|
|
96
|
-
ResponseField,
|
|
97
91
|
Step,
|
|
98
92
|
Steps,
|
|
99
93
|
Tab,
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
import data from "blume:data";
|
|
3
2
|
import { resolveIcon } from "../theme/icons.ts";
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
5
|
name?: unknown;
|
|
7
6
|
icon?: unknown;
|
|
8
|
-
iconType?: string;
|
|
9
|
-
/** Icon library for a bare name; defaults to the project's `icons.library`. */
|
|
10
|
-
library?: string;
|
|
11
7
|
size?: number;
|
|
12
8
|
class?: string;
|
|
13
9
|
className?: string;
|
|
@@ -18,8 +14,6 @@ interface Props {
|
|
|
18
14
|
const {
|
|
19
15
|
color,
|
|
20
16
|
icon,
|
|
21
|
-
iconType,
|
|
22
|
-
library,
|
|
23
17
|
name,
|
|
24
18
|
size = 16,
|
|
25
19
|
class: astroClass,
|
|
@@ -41,12 +35,7 @@ const isImageIcon = (value: string): boolean =>
|
|
|
41
35
|
/\.(?:avif|gif|jpe?g|png|svg|webp)$/iu.test(value);
|
|
42
36
|
const imageSrc = iconName && !rawSvg && isImageIcon(iconName) ? iconName : null;
|
|
43
37
|
const resolvedIcon =
|
|
44
|
-
iconName && !(imageSrc || rawSvg)
|
|
45
|
-
? resolveIcon(iconName, {
|
|
46
|
-
iconType,
|
|
47
|
-
library: library ?? data.config.icons.library,
|
|
48
|
-
})
|
|
49
|
-
: null;
|
|
38
|
+
iconName && !(imageSrc || rawSvg) ? resolveIcon(iconName) : null;
|
|
50
39
|
const resolvedClass = astroClass ?? className;
|
|
51
40
|
const customStyle = `display:inline-flex;width:${size}px;height:${size}px;${
|
|
52
41
|
color ? `color:${color}` : ""
|
|
@@ -7,11 +7,9 @@ interface Props {
|
|
|
7
7
|
defaultOpen?: boolean;
|
|
8
8
|
id?: string;
|
|
9
9
|
icon?: string;
|
|
10
|
-
iconType?: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
const { defaultOpen = false, description, icon,
|
|
14
|
-
Astro.props;
|
|
12
|
+
const { defaultOpen = false, description, icon, id, title } = Astro.props;
|
|
15
13
|
const slugify = (value: string): string =>
|
|
16
14
|
value
|
|
17
15
|
.toLowerCase()
|
|
@@ -32,18 +30,17 @@ const accordionId = id ?? slugify(title);
|
|
|
32
30
|
<summary
|
|
33
31
|
class="flex cursor-pointer list-none items-center justify-between p-4 font-medium [&::-webkit-details-marker]:hidden"
|
|
34
32
|
>
|
|
35
|
-
<span class="flex
|
|
33
|
+
<span class="flex items-start gap-2">
|
|
36
34
|
{
|
|
37
35
|
icon && (
|
|
38
36
|
<Icon
|
|
39
37
|
class="mt-0.5 shrink-0 text-muted-foreground"
|
|
40
|
-
iconType={iconType}
|
|
41
38
|
name={icon}
|
|
42
39
|
size={16}
|
|
43
40
|
/>
|
|
44
41
|
)
|
|
45
42
|
}
|
|
46
|
-
<span
|
|
43
|
+
<span>
|
|
47
44
|
<span class="block">{title}</span>
|
|
48
45
|
{
|
|
49
46
|
description && (
|
|
@@ -10,7 +10,6 @@ interface Props {
|
|
|
10
10
|
color?: string;
|
|
11
11
|
disabled?: boolean | string;
|
|
12
12
|
icon?: unknown;
|
|
13
|
-
iconType?: string;
|
|
14
13
|
shape?: BadgeShape;
|
|
15
14
|
size?: BadgeSize;
|
|
16
15
|
stroke?: boolean | string;
|
|
@@ -23,7 +22,6 @@ const {
|
|
|
23
22
|
color,
|
|
24
23
|
disabled = false,
|
|
25
24
|
icon,
|
|
26
|
-
iconType,
|
|
27
25
|
shape = "rounded",
|
|
28
26
|
size = "md",
|
|
29
27
|
stroke = false,
|
|
@@ -119,6 +117,6 @@ const customStyle = customColor
|
|
|
119
117
|
style={customStyle}
|
|
120
118
|
title={tooltip}
|
|
121
119
|
>
|
|
122
|
-
{icon && <Icon icon={icon}
|
|
120
|
+
{icon && <Icon icon={icon} size={iconSize[size]} />}
|
|
123
121
|
<slot />
|
|
124
122
|
</span>
|
|
@@ -14,11 +14,10 @@ interface Props {
|
|
|
14
14
|
type?: CalloutType;
|
|
15
15
|
title?: string;
|
|
16
16
|
icon?: unknown;
|
|
17
|
-
iconType?: string;
|
|
18
17
|
color?: string;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
const { color, icon,
|
|
20
|
+
const { color, icon, type = "info", title } = Astro.props;
|
|
22
21
|
|
|
23
22
|
const iconByType: Record<CalloutType, string> = {
|
|
24
23
|
check: "circle-check",
|
|
@@ -59,14 +58,9 @@ const iconClass: Record<CalloutType, string> = {
|
|
|
59
58
|
style={color ? `border:1px solid ${color};color:${color}` : undefined}
|
|
60
59
|
>
|
|
61
60
|
<span class:list={["mt-0.5 shrink-0", color ? "" : iconClass[type]]}>
|
|
62
|
-
<Icon
|
|
63
|
-
color={color}
|
|
64
|
-
icon={icon ?? iconByType[type]}
|
|
65
|
-
iconType={iconType}
|
|
66
|
-
size={16}
|
|
67
|
-
/>
|
|
61
|
+
<Icon color={color} icon={icon ?? iconByType[type]} size={16} />
|
|
68
62
|
</span>
|
|
69
|
-
<div class="
|
|
63
|
+
<div class="flex-1 [&>:first-child]:mt-0! [&>:last-child]:mb-0!">
|
|
70
64
|
{title && <p class="mb-1 font-semibold text-foreground">{title}</p>}
|
|
71
65
|
<slot />
|
|
72
66
|
</div>
|
|
@@ -5,7 +5,6 @@ interface Props {
|
|
|
5
5
|
title?: string;
|
|
6
6
|
href?: string;
|
|
7
7
|
icon?: string;
|
|
8
|
-
iconType?: string;
|
|
9
8
|
img?: string;
|
|
10
9
|
horizontal?: boolean | string;
|
|
11
10
|
cta?: string;
|
|
@@ -14,7 +13,7 @@ interface Props {
|
|
|
14
13
|
color?: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
const { arrow, color, cta, horizontal, href, icon,
|
|
16
|
+
const { arrow, color, cta, horizontal, href, icon, img, title, type } =
|
|
18
17
|
Astro.props;
|
|
19
18
|
const Tag = href ? "a" : "div";
|
|
20
19
|
const external = href?.startsWith("http");
|
|
@@ -75,7 +74,7 @@ const variantClass = {
|
|
|
75
74
|
{
|
|
76
75
|
iconName && (
|
|
77
76
|
<div class:list={["mb-2.5", color ? "" : type ? iconClass[type] : "text-accent"]}>
|
|
78
|
-
<Icon color={color}
|
|
77
|
+
<Icon color={color} name={iconName} size={20} />
|
|
79
78
|
</div>
|
|
80
79
|
)
|
|
81
80
|
}
|