blume 1.4.2 → 1.5.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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/openapi/scalar.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isAbsolute, join } from "pathe";
|
|
|
4
4
|
|
|
5
5
|
import { scalarReferenceTemplate } from "../astro/templates.ts";
|
|
6
6
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
7
|
+
import { trimChar } from "../core/trim.ts";
|
|
7
8
|
import { resolveAccent, resolveRadius } from "../theme/palette.ts";
|
|
8
9
|
import { resolveReferences } from "./references.ts";
|
|
9
10
|
import type { ReferenceSource } from "./references.ts";
|
|
@@ -24,17 +25,14 @@ export interface ReferenceFile {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const URL_SPEC = /^https?:\/\//u;
|
|
27
|
-
const ROUTE_EDGES = /^\/+|\/+$/gu;
|
|
28
28
|
|
|
29
29
|
/** The `src/pages`-relative file path for a reference route. */
|
|
30
30
|
const referencePagePath = (route: string): string => {
|
|
31
|
-
const segments = route
|
|
31
|
+
const segments = trimChar(route, "/");
|
|
32
32
|
return `${segments === "" ? "index" : segments}.astro`;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const darkModeConfig = (
|
|
36
|
-
mode: ResolvedConfig["theme"]["mode"]
|
|
37
|
-
): Record<string, boolean> => {
|
|
35
|
+
const darkModeConfig = (mode: ResolvedConfig["theme"]["mode"]) => {
|
|
38
36
|
if (mode === "dark") {
|
|
39
37
|
return { darkMode: true };
|
|
40
38
|
}
|
|
@@ -51,10 +49,7 @@ const darkModeConfig = (
|
|
|
51
49
|
* `customCss`. Scalar re-injects `customCss` after its bundled theme, so these
|
|
52
50
|
* variables reliably override the defaults. Best-effort, not pixel-exact.
|
|
53
51
|
*/
|
|
54
|
-
const themeConfiguration = (
|
|
55
|
-
config: ResolvedConfig,
|
|
56
|
-
override?: string
|
|
57
|
-
): Record<string, unknown> => {
|
|
52
|
+
const themeConfiguration = (config: ResolvedConfig, override?: string) => {
|
|
58
53
|
if (override) {
|
|
59
54
|
return { theme: override };
|
|
60
55
|
}
|
|
@@ -70,7 +65,10 @@ const themeConfiguration = (
|
|
|
70
65
|
const specConfiguration = async (
|
|
71
66
|
spec: string,
|
|
72
67
|
root: string
|
|
73
|
-
): Promise<{
|
|
68
|
+
): Promise<{
|
|
69
|
+
config: { content: string } | { url: string };
|
|
70
|
+
warning?: string;
|
|
71
|
+
}> => {
|
|
74
72
|
if (URL_SPEC.test(spec)) {
|
|
75
73
|
return { config: { url: spec } };
|
|
76
74
|
}
|
package/src/openapi/source.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withBasePath } from "../core/base-path.ts";
|
|
2
2
|
import matter from "../core/frontmatter.ts";
|
|
3
|
+
import type { FolderMeta } from "../core/schema.ts";
|
|
3
4
|
import { hashText } from "../core/sources/cache.ts";
|
|
4
5
|
import type {
|
|
5
6
|
ContentSource,
|
|
@@ -8,19 +9,30 @@ import type {
|
|
|
8
9
|
SourceLoadResult,
|
|
9
10
|
} from "../core/sources/types.ts";
|
|
10
11
|
import type { Diagnostic } from "../core/types.ts";
|
|
12
|
+
import { extractAsyncApiOperations } from "./asyncapi.ts";
|
|
13
|
+
import type { AsyncApiDocument } from "./asyncapi.ts";
|
|
11
14
|
import { extractOperations } from "./model.ts";
|
|
12
|
-
import type {
|
|
13
|
-
|
|
15
|
+
import type {
|
|
16
|
+
ApiDocument,
|
|
17
|
+
ApiOperationRef,
|
|
18
|
+
ApiSpecData,
|
|
19
|
+
ApiTagRef,
|
|
20
|
+
OpenApiData,
|
|
21
|
+
} from "./model.ts";
|
|
22
|
+
import { InvalidSpecError, parseAsyncApiSpec, parseSpec } from "./parse.ts";
|
|
14
23
|
import type { ReferenceSource } from "./references.ts";
|
|
15
24
|
import { operationMdx, overviewMdx } from "./render-mdx.ts";
|
|
16
25
|
import type { RenderedPage } from "./render-mdx.ts";
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
|
-
* The staged content source behind Blume's own
|
|
20
|
-
* spec is parsed once here, then lowered
|
|
21
|
-
* overview page — so operations become
|
|
22
|
-
* sidebar, search, i18n, OG) and the
|
|
23
|
-
* generated `blume:openapi` module for the
|
|
28
|
+
* The staged content source behind Blume's own API reference renderer (OpenAPI
|
|
29
|
+
* and AsyncAPI alike). Each configured spec is parsed once here, then lowered
|
|
30
|
+
* into one MDX page per operation plus an overview page — so operations become
|
|
31
|
+
* first-class Blume pages (real routes, sidebar, search, i18n, OG) and the
|
|
32
|
+
* parsed documents are handed to the generated `blume:openapi` module for the
|
|
33
|
+
* UI components to render. The source keeps its historical `openapi` name for
|
|
34
|
+
* both kinds — downstream consumers (`ai.llmsTxt.openapi`, the llms noindex
|
|
35
|
+
* exemption) key on it as "the generated API reference source".
|
|
24
36
|
*/
|
|
25
37
|
|
|
26
38
|
/** A content source that also exposes the specs it parsed during `load()`. */
|
|
@@ -34,7 +46,7 @@ export interface OpenApiContentSource extends ContentSource {
|
|
|
34
46
|
export const isOpenApiSource = (
|
|
35
47
|
source: ContentSource
|
|
36
48
|
): source is OpenApiContentSource =>
|
|
37
|
-
|
|
49
|
+
"kind" in source && source.kind === "openapi-source";
|
|
38
50
|
|
|
39
51
|
/** Route (`/reference/pet/add-pet`) to a staged content ref, without extension. */
|
|
40
52
|
const routeToRef = (route: string): string => route.replace(/^\/+/u, "");
|
|
@@ -43,7 +55,9 @@ const toEntry = (rendered: RenderedPage, ref: string): SourceEntry => {
|
|
|
43
55
|
const raw = matter.stringify(`${rendered.body}\n`, rendered.data);
|
|
44
56
|
return {
|
|
45
57
|
body: { format: "mdx", text: rendered.body },
|
|
46
|
-
|
|
58
|
+
// Spread so the named frontmatter shape satisfies the open metadata
|
|
59
|
+
// dictionary every source entry carries.
|
|
60
|
+
data: { ...rendered.data },
|
|
47
61
|
hash: hashText(raw),
|
|
48
62
|
raw,
|
|
49
63
|
ref,
|
|
@@ -75,14 +89,81 @@ const specEntries = (
|
|
|
75
89
|
return entries;
|
|
76
90
|
};
|
|
77
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Label each tag's sidebar group with the spec's own tag name. The group label
|
|
94
|
+
* is otherwise re-humanized from the tag's route slug (split on hyphens,
|
|
95
|
+
* title-cased), which mangles authored casing and symbols — `OAuth2` →
|
|
96
|
+
* "Oauth2", `Größe` → "Größe" only by luck of the slug. Keys are the tag
|
|
97
|
+
* directories under the reference route, the same group paths `meta.ts` files
|
|
98
|
+
* use, so user-authored meta still overrides these.
|
|
99
|
+
*/
|
|
100
|
+
const tagFolderMeta = (
|
|
101
|
+
spec: ApiSpecData,
|
|
102
|
+
tags: { slug: string; name: string }[]
|
|
103
|
+
): Record<string, FolderMeta> => {
|
|
104
|
+
const base = routeToRef(spec.route);
|
|
105
|
+
return Object.fromEntries(
|
|
106
|
+
tags.map((tag) => [
|
|
107
|
+
base ? `${base}/${tag.slug}` : tag.slug,
|
|
108
|
+
{ title: tag.name },
|
|
109
|
+
])
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
78
113
|
interface LoadedSpec {
|
|
79
114
|
slug: string;
|
|
80
115
|
spec: ApiSpecData;
|
|
81
116
|
entries: SourceEntry[];
|
|
117
|
+
/** Sidebar-group labels for the spec's tag directories. */
|
|
118
|
+
folderMeta: Record<string, FolderMeta>;
|
|
82
119
|
/** Non-fatal notes from the load (e.g. an offline cache fallback). */
|
|
83
120
|
diagnostics: Diagnostic[];
|
|
84
121
|
}
|
|
85
122
|
|
|
123
|
+
/** One parsed spec, whichever front-end read it — the kind dispatch seam. */
|
|
124
|
+
interface ParsedReference {
|
|
125
|
+
document: ApiDocument | AsyncApiDocument;
|
|
126
|
+
warnings: string[];
|
|
127
|
+
operations: ApiOperationRef[];
|
|
128
|
+
tags: ApiTagRef[];
|
|
129
|
+
extractWarnings: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parseReference = async (
|
|
133
|
+
reference: ReferenceSource,
|
|
134
|
+
ctx: SourceContext
|
|
135
|
+
): Promise<ParsedReference> => {
|
|
136
|
+
const options = { cacheDir: ctx.cacheDir, refresh: ctx.refresh };
|
|
137
|
+
if (reference.kind === "asyncapi") {
|
|
138
|
+
const { document, warnings } = await parseAsyncApiSpec(
|
|
139
|
+
reference.spec,
|
|
140
|
+
ctx.projectRoot,
|
|
141
|
+
options
|
|
142
|
+
);
|
|
143
|
+
const extracted = extractAsyncApiOperations(document, reference.route);
|
|
144
|
+
return {
|
|
145
|
+
document,
|
|
146
|
+
extractWarnings: extracted.warnings,
|
|
147
|
+
operations: extracted.operations,
|
|
148
|
+
tags: extracted.tags,
|
|
149
|
+
warnings,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const { document, warnings } = await parseSpec(
|
|
153
|
+
reference.spec,
|
|
154
|
+
ctx.projectRoot,
|
|
155
|
+
options
|
|
156
|
+
);
|
|
157
|
+
const extracted = extractOperations(document, reference.route);
|
|
158
|
+
return {
|
|
159
|
+
document,
|
|
160
|
+
extractWarnings: extracted.warnings,
|
|
161
|
+
operations: extracted.operations,
|
|
162
|
+
tags: extracted.tags,
|
|
163
|
+
warnings,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
86
167
|
export const openApiSource = (
|
|
87
168
|
references: ReferenceSource[],
|
|
88
169
|
ctx: SourceContext
|
|
@@ -92,23 +173,21 @@ export const openApiSource = (
|
|
|
92
173
|
const loadReference = async (
|
|
93
174
|
reference: ReferenceSource
|
|
94
175
|
): Promise<LoadedSpec | Diagnostic> => {
|
|
176
|
+
// Human label and diagnostic-code prefix for the spec's kind, so an
|
|
177
|
+
// AsyncAPI failure never reads as an OpenAPI one.
|
|
178
|
+
const kindLabel = reference.kind === "asyncapi" ? "AsyncAPI" : "OpenAPI";
|
|
179
|
+
const codePrefix =
|
|
180
|
+
reference.kind === "asyncapi" ? "BLUME_ASYNCAPI" : "BLUME_OPENAPI";
|
|
95
181
|
try {
|
|
96
|
-
const { document, warnings } =
|
|
97
|
-
reference
|
|
98
|
-
ctx.projectRoot,
|
|
99
|
-
{ cacheDir: ctx.cacheDir, refresh: ctx.refresh }
|
|
100
|
-
);
|
|
101
|
-
const {
|
|
102
|
-
operations,
|
|
103
|
-
tags,
|
|
104
|
-
warnings: extractWarnings,
|
|
105
|
-
} = extractOperations(document, reference.route);
|
|
182
|
+
const { document, warnings, operations, tags, extractWarnings } =
|
|
183
|
+
await parseReference(reference, ctx);
|
|
106
184
|
const info = document.info ?? { title: reference.label, version: "" };
|
|
107
185
|
const spec: ApiSpecData = {
|
|
108
186
|
codeSamples: reference.display.codeSamples,
|
|
109
187
|
description: info.description ?? "",
|
|
110
188
|
document,
|
|
111
189
|
expandSchemas: reference.display.expandSchemas,
|
|
190
|
+
kind: reference.kind,
|
|
112
191
|
label: reference.label,
|
|
113
192
|
// Operation pages flow through the content pipeline, which mounts them
|
|
114
193
|
// under the site-wide `basePath` (staged entry refs below stay
|
|
@@ -132,13 +211,24 @@ export const openApiSource = (
|
|
|
132
211
|
return {
|
|
133
212
|
diagnostics: [
|
|
134
213
|
...warnings.map((message) => ({
|
|
135
|
-
|
|
214
|
+
// Parse-level notes: an offline cache fallback for either kind,
|
|
215
|
+
// plus lossy 2.x→3.0 conversion notes for AsyncAPI — hence the
|
|
216
|
+
// broader code on that side (OpenAPI keeps its historical one).
|
|
217
|
+
code:
|
|
218
|
+
reference.kind === "asyncapi"
|
|
219
|
+
? "BLUME_ASYNCAPI_SPEC_WARNING"
|
|
220
|
+
: "BLUME_OPENAPI_STALE",
|
|
136
221
|
message,
|
|
137
222
|
severity: "warning" as const,
|
|
138
223
|
})),
|
|
139
224
|
...extractWarnings.map((message) => ({
|
|
140
|
-
code
|
|
141
|
-
|
|
225
|
+
// OpenAPI keeps its historical code (the only extract warning it
|
|
226
|
+
// emits is the unresolved $ref path item).
|
|
227
|
+
code:
|
|
228
|
+
reference.kind === "asyncapi"
|
|
229
|
+
? "BLUME_ASYNCAPI_SKIPPED_OPERATION"
|
|
230
|
+
: "BLUME_OPENAPI_REF_PATH_ITEM",
|
|
231
|
+
message: `In ${kindLabel} spec "${reference.spec}": ${message}`,
|
|
142
232
|
severity: "warning" as const,
|
|
143
233
|
})),
|
|
144
234
|
// A document with no operations (say, a config file that happens to
|
|
@@ -147,23 +237,28 @@ export const openApiSource = (
|
|
|
147
237
|
...(operations.length === 0
|
|
148
238
|
? [
|
|
149
239
|
{
|
|
150
|
-
code:
|
|
151
|
-
message:
|
|
240
|
+
code: `${codePrefix}_EMPTY`,
|
|
241
|
+
message: `${kindLabel} spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
|
|
152
242
|
severity: "warning" as const,
|
|
153
243
|
suggestion:
|
|
154
|
-
|
|
244
|
+
reference.kind === "asyncapi"
|
|
245
|
+
? "Check the spec points at an AsyncAPI document with `channels` and `operations`."
|
|
246
|
+
: "Check the spec points at an OpenAPI document with operations under `paths`.",
|
|
155
247
|
},
|
|
156
248
|
]
|
|
157
249
|
: []),
|
|
158
250
|
],
|
|
159
251
|
entries: specEntries(spec, operations, reference),
|
|
252
|
+
folderMeta: tagFolderMeta(spec, tags),
|
|
160
253
|
slug: reference.slug,
|
|
161
254
|
spec,
|
|
162
255
|
};
|
|
163
256
|
} catch (error) {
|
|
164
257
|
return {
|
|
165
|
-
code:
|
|
166
|
-
|
|
258
|
+
code: `${codePrefix}_UNAVAILABLE`,
|
|
259
|
+
// SAFETY: spec loading fails with Error instances (fetch, read, and
|
|
260
|
+
// parse errors alike); only the message is read for the diagnostic.
|
|
261
|
+
message: `Could not load ${kindLabel} spec "${reference.spec}" for ${reference.route} (${(error as Error).message}); its reference pages were skipped.`,
|
|
167
262
|
// A configured-but-unloadable spec ships a dead nav tab (a 404 route),
|
|
168
263
|
// so fail loudly in build (blocks under --strict) while staying a warning
|
|
169
264
|
// in dev so offline work still runs.
|
|
@@ -172,7 +267,7 @@ export const openApiSource = (
|
|
|
172
267
|
// only point at reachability for actual fetch/read failures.
|
|
173
268
|
suggestion:
|
|
174
269
|
error instanceof InvalidSpecError
|
|
175
|
-
?
|
|
270
|
+
? `Point the spec at ${reference.kind === "asyncapi" ? "an AsyncAPI" : "an OpenAPI"} document (a YAML or JSON file with an object at the top level).`
|
|
176
271
|
: "Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY.",
|
|
177
272
|
};
|
|
178
273
|
}
|
|
@@ -192,6 +287,7 @@ export const openApiSource = (
|
|
|
192
287
|
}))
|
|
193
288
|
);
|
|
194
289
|
const data: OpenApiData = {};
|
|
290
|
+
const folderMeta: Record<string, FolderMeta> = {};
|
|
195
291
|
for (const result of results) {
|
|
196
292
|
if ("severity" in result) {
|
|
197
293
|
diagnostics.push(result);
|
|
@@ -199,10 +295,11 @@ export const openApiSource = (
|
|
|
199
295
|
}
|
|
200
296
|
data[result.slug] = result.spec;
|
|
201
297
|
entries.push(...result.entries);
|
|
298
|
+
Object.assign(folderMeta, result.folderMeta);
|
|
202
299
|
diagnostics.push(...result.diagnostics);
|
|
203
300
|
}
|
|
204
301
|
parsed = data;
|
|
205
|
-
return { diagnostics, entries };
|
|
302
|
+
return { diagnostics, entries, folderMeta };
|
|
206
303
|
};
|
|
207
304
|
|
|
208
305
|
return {
|
package/src/registry/eject.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { scanProject } from "../core/project-graph.ts";
|
|
|
49
49
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
50
50
|
import type { ProjectContext } from "../core/types.ts";
|
|
51
51
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
52
|
+
import type { OpenApiData } from "../openapi/model.ts";
|
|
52
53
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
53
54
|
import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
54
55
|
import { isOpenApiSource } from "../openapi/source.ts";
|
|
@@ -97,7 +98,7 @@ export const blumeSourceGlob = (
|
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
/** The `blume:openapi` payload for the ejected app (`{}` when none). */
|
|
100
|
-
const ejectOpenApiData = (project: BlumeProject):
|
|
101
|
+
const ejectOpenApiData = (project: BlumeProject): OpenApiData => {
|
|
101
102
|
const source = project.sources.find(isOpenApiSource);
|
|
102
103
|
return source ? source.openApiData() : {};
|
|
103
104
|
};
|
|
@@ -126,7 +127,11 @@ const askFiles = async (
|
|
|
126
127
|
const grounded = ask.provider !== "inkeep";
|
|
127
128
|
const files = [
|
|
128
129
|
{
|
|
129
|
-
content: askEndpointTemplate(
|
|
130
|
+
content: askEndpointTemplate(
|
|
131
|
+
resolveAskBackend(ask),
|
|
132
|
+
grounded,
|
|
133
|
+
ask.instructions
|
|
134
|
+
),
|
|
130
135
|
path: join(srcDir, "pages", "api", "ask.ts"),
|
|
131
136
|
},
|
|
132
137
|
];
|
package/src/search/documents.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { Nodes } from "mdast";
|
|
2
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
3
|
+
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
4
|
+
import { gfm } from "micromark-extension-gfm";
|
|
5
|
+
|
|
1
6
|
import { applyAudienceVisibility } from "../ai/visibility.ts";
|
|
2
7
|
import type { VisibilityAudience } from "../ai/visibility.ts";
|
|
3
8
|
import matter from "../core/frontmatter.ts";
|
|
@@ -21,6 +26,8 @@ export interface SearchDocument {
|
|
|
21
26
|
locale: string;
|
|
22
27
|
/** Resolved page `type` (`doc`, `blog`, a custom `rfc`…), for type filters. */
|
|
23
28
|
contentType: string;
|
|
29
|
+
/** Docs version (`""` for the current docs), so results scope to the viewed version. */
|
|
30
|
+
version: string;
|
|
24
31
|
/** Frontmatter `search.tags`, surfaced for hosted-provider faceting. */
|
|
25
32
|
tags?: string[];
|
|
26
33
|
/**
|
|
@@ -43,51 +50,96 @@ export interface SearchRecord {
|
|
|
43
50
|
content: string;
|
|
44
51
|
/** Locale code, carried as a facet for per-language filtering. */
|
|
45
52
|
locale: string;
|
|
53
|
+
/**
|
|
54
|
+
* Docs version, carried as a facet for per-version filtering. The current
|
|
55
|
+
* docs upload as `"current"` — hosted backends treat an empty facet value
|
|
56
|
+
* unreliably, so the sentinel stands in for the empty version id.
|
|
57
|
+
*/
|
|
58
|
+
version: string;
|
|
46
59
|
/** Single faceting tag (the first frontmatter tag, when present). */
|
|
47
60
|
tag?: string;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
const CODE_FENCE = /```[\s\S]*?```/gu;
|
|
51
|
-
const INLINE_CODE = /`(?<code>[^`]+)`/gu;
|
|
52
63
|
// Tag-shaped only: a name (or closing slash/fragment) right after `<`, and no
|
|
53
|
-
// newline inside.
|
|
54
|
-
//
|
|
64
|
+
// newline inside. Applied *within* html/JSX nodes so their inner prose is
|
|
65
|
+
// kept; the surrounding Markdown is walked as a tree, so a bare `<` in prose
|
|
66
|
+
// ("costs < 5 credits") is ordinary text and never at risk.
|
|
55
67
|
const HTML_OR_JSX = /<\/?[a-zA-Z][^\n<>]*>|<\/?>/gu;
|
|
56
|
-
const IMAGE = /!\[[^\]]*\]\([^)]*\)/gu;
|
|
57
|
-
const LINK = /\[(?<text>[^\]]*)\]\([^)]*\)/gu;
|
|
58
|
-
const HEADING_MARK = /^#{1,6}\s+/gmu;
|
|
59
|
-
const MARKDOWN_PUNCT = /[*_~>]+/gu;
|
|
60
68
|
const WHITESPACE = /\s+/gu;
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
// Parents whose children are inline: no separator is inserted after them, or
|
|
71
|
+
// `re*ally*` would index as `re ally`. Every other parent is block-shaped and
|
|
72
|
+
// ends with a space so adjacent paragraphs/headings/cells don't fuse.
|
|
73
|
+
const INLINE_PARENTS = new Set([
|
|
74
|
+
"delete",
|
|
75
|
+
"emphasis",
|
|
76
|
+
"footnoteReference",
|
|
77
|
+
"link",
|
|
78
|
+
"linkReference",
|
|
79
|
+
"strong",
|
|
80
|
+
]);
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
/** Fold one mdast node into the plain-text accumulator. */
|
|
83
|
+
const collectText = (node: Nodes, out: string[]): void => {
|
|
84
|
+
switch (node.type) {
|
|
85
|
+
// Fenced code is excluded from the plain index (ranking noise) — the
|
|
86
|
+
// "markdown" extraction keeps it for Ask AI grounding — and image alt
|
|
87
|
+
// text was never indexed.
|
|
88
|
+
case "code":
|
|
89
|
+
case "image":
|
|
90
|
+
case "imageReference": {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Inline code is kept verbatim — `Array<T>` is a type parameter, not a
|
|
94
|
+
// tag, and its tokens must stay searchable.
|
|
95
|
+
case "inlineCode": {
|
|
96
|
+
out.push(node.value);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// A raw-HTML/JSX run. CommonMark parses a block-level `<Callout>` with no
|
|
100
|
+
// blank lines as ONE html node holding all its inner prose, so the node
|
|
101
|
+
// can't just be dropped — strip the tag-shaped runs and keep the text.
|
|
102
|
+
case "html": {
|
|
103
|
+
out.push(node.value.replaceAll(HTML_OR_JSX, " "));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case "break": {
|
|
107
|
+
out.push(" ");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if ("value" in node) {
|
|
115
|
+
out.push(node.value);
|
|
116
|
+
return;
|
|
82
117
|
}
|
|
83
|
-
|
|
118
|
+
if ("children" in node) {
|
|
119
|
+
for (const child of node.children) {
|
|
120
|
+
collectText(child, out);
|
|
121
|
+
}
|
|
122
|
+
if (!INLINE_PARENTS.has(node.type)) {
|
|
123
|
+
out.push(" ");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
84
127
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Reduce Markdown/MDX to plain, searchable text: parse (GFM included) and walk
|
|
130
|
+
* the tree instead of regex-stripping the source, so reference-style links,
|
|
131
|
+
* autolinks, setext headings, tables, and literal `*`/`~`/`>` in prose all
|
|
132
|
+
* reduce correctly. This feeds the client index *and* every hosted-provider
|
|
133
|
+
* record, so anything lost here is a permanent search-quality loss.
|
|
134
|
+
*/
|
|
135
|
+
const toPlainText = (markdown: string): string => {
|
|
136
|
+
const tree = fromMarkdown(markdown, {
|
|
137
|
+
extensions: [gfm()],
|
|
138
|
+
mdastExtensions: [gfmFromMarkdown()],
|
|
139
|
+
});
|
|
140
|
+
const out: string[] = [];
|
|
141
|
+
collectText(tree, out);
|
|
142
|
+
return out.join("").replaceAll(WHITESPACE, " ").trim();
|
|
91
143
|
};
|
|
92
144
|
|
|
93
145
|
interface Crumbs {
|
|
@@ -169,10 +221,17 @@ export const buildSearchDocuments = async (
|
|
|
169
221
|
// locale-prefixed routes), so localized pages get the right section/breadcrumb.
|
|
170
222
|
// Falls back to the single default-locale nav when i18n is off.
|
|
171
223
|
const byLocale = Object.values(project.graph.navigationByLocale ?? {});
|
|
172
|
-
|
|
173
|
-
|
|
224
|
+
// Archived versions' trees contribute too, so snapshot pages get their own
|
|
225
|
+
// section/breadcrumb instead of falling through to the "Docs" default.
|
|
226
|
+
const byVersion = Object.values(project.graph.navigationByVersion ?? {})
|
|
227
|
+
.flatMap((locales) => Object.values(locales))
|
|
228
|
+
.map((nav) => nav.sidebar);
|
|
229
|
+
const sidebars = [
|
|
230
|
+
...(byLocale.length > 0
|
|
174
231
|
? byLocale.map((nav) => nav.sidebar)
|
|
175
|
-
: [project.graph.navigation?.sidebar ?? []]
|
|
232
|
+
: [project.graph.navigation?.sidebar ?? []]),
|
|
233
|
+
...byVersion,
|
|
234
|
+
];
|
|
176
235
|
const crumbs = new Map<string, Crumbs>();
|
|
177
236
|
for (const sidebar of sidebars) {
|
|
178
237
|
for (const [route, crumb] of buildCrumbIndex(sidebar)) {
|
|
@@ -202,18 +261,22 @@ export const buildSearchDocuments = async (
|
|
|
202
261
|
const tags = page?.meta?.search?.tags;
|
|
203
262
|
const crumb = crumbs.get(route.path);
|
|
204
263
|
const facets = page ? pageFacets(page, project.config) : undefined;
|
|
205
|
-
|
|
264
|
+
const document: SearchDocument = {
|
|
206
265
|
breadcrumb: crumb?.breadcrumb ?? [],
|
|
207
266
|
content: body,
|
|
208
267
|
contentType: route.contentType,
|
|
209
268
|
description: page?.description ?? "",
|
|
210
|
-
...(facets ? { facets } : {}),
|
|
211
269
|
locale: route.locale,
|
|
212
270
|
route: route.path,
|
|
213
271
|
section: crumb?.section || "Docs",
|
|
214
272
|
tags: tags && tags.length > 0 ? tags : undefined,
|
|
215
273
|
title: route.title,
|
|
274
|
+
version: route.version,
|
|
216
275
|
};
|
|
276
|
+
if (facets) {
|
|
277
|
+
document.facets = facets;
|
|
278
|
+
}
|
|
279
|
+
return document;
|
|
217
280
|
})
|
|
218
281
|
);
|
|
219
282
|
};
|
|
@@ -232,4 +295,5 @@ export const toSearchRecords = (documents: SearchDocument[]): SearchRecord[] =>
|
|
|
232
295
|
tag: doc.tags?.[0],
|
|
233
296
|
title: doc.title,
|
|
234
297
|
url: doc.route,
|
|
298
|
+
version: doc.version || "current",
|
|
235
299
|
}));
|
package/src/search/facets.ts
CHANGED
|
@@ -10,6 +10,12 @@ import type { PageRecord } from "../core/types.ts";
|
|
|
10
10
|
* when nothing facets, so the field stays absent from serialized documents
|
|
11
11
|
* rather than shipping as `{}` on every page.
|
|
12
12
|
*/
|
|
13
|
+
/** Whether a custom frontmatter value stringifies into a usable facet value. */
|
|
14
|
+
const isFacetValue = <T>(value: T): value is T & (string | number | boolean) =>
|
|
15
|
+
typeof value === "string" ||
|
|
16
|
+
typeof value === "number" ||
|
|
17
|
+
typeof value === "boolean";
|
|
18
|
+
|
|
13
19
|
export const pageFacets = (
|
|
14
20
|
page: Pick<PageRecord, "contentType" | "custom">,
|
|
15
21
|
config: ResolvedConfig
|
|
@@ -21,11 +27,7 @@ export const pageFacets = (
|
|
|
21
27
|
const facets: Record<string, string> = {};
|
|
22
28
|
for (const key of declared) {
|
|
23
29
|
const value = page.custom[key];
|
|
24
|
-
if (
|
|
25
|
-
typeof value === "string" ||
|
|
26
|
-
typeof value === "number" ||
|
|
27
|
-
typeof value === "boolean"
|
|
28
|
-
) {
|
|
30
|
+
if (isFacetValue(value)) {
|
|
29
31
|
facets[key] = String(value);
|
|
30
32
|
}
|
|
31
33
|
}
|