blume 0.5.3 → 0.6.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/dist/cli/index.js +3349 -7024
- package/dist/cli/index.js.map +39 -69
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +379 -485
- package/dist/types/core/types.d.ts +8 -6
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +58 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +15 -18
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +6 -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/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +19 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +33 -21
- package/src/cli/commands/build.ts +15 -0
- package/src/cli/commands/dev.ts +31 -20
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +94 -21
- 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 +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +13 -8
- package/src/components/content/Update.astro +1 -1
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +27 -4
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +15 -15
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +79 -4
- package/src/components/layout/Search.astro +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +6 -2
- package/src/core/graph.ts +8 -6
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -17
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +133 -95
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +43 -11
- package/src/core/types.ts +8 -6
- package/src/deploy/robots.ts +37 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/scalar.ts +1 -1
- package/src/openapi/source.ts +19 -2
- package/src/search/documents.ts +9 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +25 -51
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- 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/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- 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 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- 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 -954
- 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 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- 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 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- 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/openapi/parse.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
3
|
|
|
3
4
|
import { normalize, upgrade } from "@scalar/openapi-parser";
|
|
4
5
|
import { isAbsolute, join } from "pathe";
|
|
5
6
|
|
|
7
|
+
import { hashText } from "../core/sources/cache.ts";
|
|
6
8
|
import type { ApiDocument } from "./model.ts";
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -10,39 +12,220 @@ import type { ApiDocument } from "./model.ts";
|
|
|
10
12
|
* (`@scalar/openapi-parser`) to read a spec (YAML or JSON), then upgrade Swagger
|
|
11
13
|
* 2.0 / OpenAPI 3.0 documents to 3.1 so the renderer only handles one shape.
|
|
12
14
|
* Internal `$ref`s are deliberately left in place (see `model.ts`).
|
|
15
|
+
*
|
|
16
|
+
* Remote (`http(s)`) specs are fetched defensively — bounded per attempt, retried
|
|
17
|
+
* on transient failures, proxy-aware, and cached on disk — mirroring the
|
|
18
|
+
* resilience the external link checker (`core/links.ts`) and the Notion source
|
|
19
|
+
* (`core/sources/notion.ts`) already have. A bare `fetch` is the classic "curl
|
|
20
|
+
* works but the build doesn't" gap: it ignores `*_PROXY`, has no timeout, and
|
|
21
|
+
* dies on a single cold-start blip.
|
|
13
22
|
*/
|
|
14
23
|
|
|
15
24
|
const URL_SPEC = /^https?:\/\//u;
|
|
16
25
|
|
|
26
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
27
|
+
const MAX_ATTEMPTS = 3;
|
|
28
|
+
const BASE_BACKOFF_MS = 500;
|
|
29
|
+
const SECOND_MS = 1000;
|
|
30
|
+
// Worth another try: request timeout, too-early, rate-limited, and the 5xx range.
|
|
31
|
+
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
32
|
+
const USER_AGENT = "blume (+https://github.com/haydenbleasel/blume)";
|
|
33
|
+
const PROXY_ENV_VARS = [
|
|
34
|
+
"HTTP_PROXY",
|
|
35
|
+
"http_proxy",
|
|
36
|
+
"HTTPS_PROXY",
|
|
37
|
+
"https_proxy",
|
|
38
|
+
"ALL_PROXY",
|
|
39
|
+
"all_proxy",
|
|
40
|
+
];
|
|
41
|
+
|
|
17
42
|
export interface ParsedSpec {
|
|
18
43
|
document: ApiDocument;
|
|
19
44
|
warnings: string[];
|
|
20
45
|
}
|
|
21
46
|
|
|
47
|
+
/** Where and whether to cache a remote spec's text between runs. */
|
|
48
|
+
export interface SpecFetchOptions {
|
|
49
|
+
/** Dir for a last-good on-disk copy of a remote spec (offline fallback). */
|
|
50
|
+
cacheDir?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Re-fetch even when a cached copy exists. Builds/sync refresh; dev is
|
|
53
|
+
* cache-first for fast, offline-tolerant restarts (see `SourceContext`).
|
|
54
|
+
*/
|
|
55
|
+
refresh?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Route Node's global `fetch` through an HTTP(S) proxy the first time a remote
|
|
60
|
+
* spec is fetched with one configured. Node's built-in `fetch` ignores `*_PROXY`
|
|
61
|
+
* on its own; undici's env proxy agent, installed on the shared global-dispatcher
|
|
62
|
+
* symbol, wires it in without replacing `fetch` (so tests can still stub it).
|
|
63
|
+
* Best-effort and lazy: no proxy env means no undici import at all, and an
|
|
64
|
+
* unavailable undici just leaves the direct connection in place.
|
|
65
|
+
*/
|
|
66
|
+
let proxyInstalled = false;
|
|
67
|
+
const ensureProxyDispatcher = async (): Promise<void> => {
|
|
68
|
+
// Only memoize a successful install: with no proxy configured we cheaply
|
|
69
|
+
// re-check each time, so a proxy set later in the process still takes effect.
|
|
70
|
+
if (proxyInstalled || !PROXY_ENV_VARS.some((name) => process.env[name])) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import("undici");
|
|
75
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
76
|
+
proxyInstalled = true;
|
|
77
|
+
} catch {
|
|
78
|
+
// No proxy support available; fall back to a direct connection.
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** `Retry-After` in ms when the server sent a sane one, else undefined. */
|
|
83
|
+
const retryAfterMs = (response: Response): number | undefined => {
|
|
84
|
+
const header = response.headers.get("retry-after");
|
|
85
|
+
const seconds = header ? Number(header) : Number.NaN;
|
|
86
|
+
return Number.isFinite(seconds) && seconds > 0
|
|
87
|
+
? seconds * SECOND_MS
|
|
88
|
+
: undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** One fetch attempt, normalized: the body text, or a (maybe-retryable) error. */
|
|
92
|
+
type Attempt =
|
|
93
|
+
| { text: string }
|
|
94
|
+
| { error: Error; retryable: boolean; retryAfter?: number };
|
|
95
|
+
|
|
96
|
+
const attemptFetch = async (spec: string): Promise<Attempt> => {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(spec, {
|
|
99
|
+
headers: {
|
|
100
|
+
accept: "application/json, application/yaml, text/yaml, */*",
|
|
101
|
+
"user-agent": USER_AGENT,
|
|
102
|
+
},
|
|
103
|
+
redirect: "follow",
|
|
104
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
105
|
+
});
|
|
106
|
+
if (response.ok) {
|
|
107
|
+
return { text: await response.text() };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
error: new Error(`${spec} -> ${response.status} ${response.statusText}`),
|
|
111
|
+
retryAfter: retryAfterMs(response),
|
|
112
|
+
retryable: RETRYABLE_STATUS.has(response.status),
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Network error, DNS/TLS failure, or an aborted (timed-out) request — all
|
|
116
|
+
// transient by nature, so worth a retry.
|
|
117
|
+
return {
|
|
118
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
119
|
+
retryable: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Fetch a remote spec's text, retrying transient failures with backoff. */
|
|
125
|
+
const fetchSpecText = async (spec: string): Promise<string> => {
|
|
126
|
+
await ensureProxyDispatcher();
|
|
127
|
+
let last: Attempt = {
|
|
128
|
+
error: new Error(`Could not fetch ${spec}`),
|
|
129
|
+
retryable: false,
|
|
130
|
+
};
|
|
131
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
|
132
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential retry attempts
|
|
133
|
+
last = await attemptFetch(spec);
|
|
134
|
+
if ("text" in last) {
|
|
135
|
+
return last.text;
|
|
136
|
+
}
|
|
137
|
+
if (!last.retryable || attempt === MAX_ATTEMPTS - 1) {
|
|
138
|
+
throw last.error;
|
|
139
|
+
}
|
|
140
|
+
// oxlint-disable-next-line no-await-in-loop -- back off before retrying
|
|
141
|
+
await sleep(last.retryAfter ?? BASE_BACKOFF_MS * 2 ** attempt);
|
|
142
|
+
}
|
|
143
|
+
throw last.error;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const cacheFileFor = (cacheDir: string, spec: string): string =>
|
|
147
|
+
join(cacheDir, `spec-${hashText(spec)}.cache`);
|
|
148
|
+
|
|
149
|
+
const readCache = async (file: string): Promise<string | undefined> => {
|
|
150
|
+
try {
|
|
151
|
+
return await readFile(file, "utf-8");
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const writeCache = async (
|
|
158
|
+
dir: string,
|
|
159
|
+
file: string,
|
|
160
|
+
text: string
|
|
161
|
+
): Promise<void> => {
|
|
162
|
+
try {
|
|
163
|
+
await mkdir(dir, { recursive: true });
|
|
164
|
+
await writeFile(file, text, "utf-8");
|
|
165
|
+
} catch {
|
|
166
|
+
// Best-effort cache; a write failure must not fail the load.
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
22
170
|
/** Read a spec's raw text from an `http(s)` URL or a local (project-relative) path. */
|
|
23
|
-
const readSpecText = async (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
171
|
+
const readSpecText = async (
|
|
172
|
+
spec: string,
|
|
173
|
+
root: string,
|
|
174
|
+
options: SpecFetchOptions
|
|
175
|
+
): Promise<{ text: string; warnings: string[] }> => {
|
|
176
|
+
if (!URL_SPEC.test(spec)) {
|
|
177
|
+
const absolute = isAbsolute(spec) ? spec : join(root, spec);
|
|
178
|
+
return { text: await readFile(absolute, "utf-8"), warnings: [] };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const cacheFile = options.cacheDir
|
|
182
|
+
? cacheFileFor(options.cacheDir, spec)
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
// Cache-first in dev: serve the last-good snapshot without touching the network.
|
|
186
|
+
if (cacheFile && options.refresh === false) {
|
|
187
|
+
const cached = await readCache(cacheFile);
|
|
188
|
+
if (cached !== undefined) {
|
|
189
|
+
return { text: cached, warnings: [] };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const text = await fetchSpecText(spec);
|
|
195
|
+
if (options.cacheDir && cacheFile) {
|
|
196
|
+
await writeCache(options.cacheDir, cacheFile, text);
|
|
197
|
+
}
|
|
198
|
+
return { text, warnings: [] };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// A transient outage falls back to the last good fetch, with a warning.
|
|
201
|
+
if (cacheFile) {
|
|
202
|
+
const cached = await readCache(cacheFile);
|
|
203
|
+
if (cached !== undefined) {
|
|
204
|
+
return {
|
|
205
|
+
text: cached,
|
|
206
|
+
warnings: [
|
|
207
|
+
`Could not fetch ${spec} (${(error as Error).message}); using the last cached copy.`,
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
28
211
|
}
|
|
29
|
-
|
|
212
|
+
throw error;
|
|
30
213
|
}
|
|
31
|
-
const absolute = isAbsolute(spec) ? spec : join(root, spec);
|
|
32
|
-
return await readFile(absolute, "utf-8");
|
|
33
214
|
};
|
|
34
215
|
|
|
35
216
|
/**
|
|
36
217
|
* Read, normalize, and upgrade a spec to an OpenAPI 3.1 document. Throws when the
|
|
37
|
-
* spec can't be read; callers turn that into a source
|
|
38
|
-
*
|
|
218
|
+
* spec can't be read and no cache is available; callers turn that into a source
|
|
219
|
+
* diagnostic (an error in build, a warning in dev) rather than a hard failure so
|
|
220
|
+
* a broken spec doesn't take down the whole build.
|
|
39
221
|
*/
|
|
40
222
|
export const parseSpec = async (
|
|
41
223
|
spec: string,
|
|
42
|
-
root: string
|
|
224
|
+
root: string,
|
|
225
|
+
options: SpecFetchOptions = {}
|
|
43
226
|
): Promise<ParsedSpec> => {
|
|
44
|
-
const text = await readSpecText(spec, root);
|
|
227
|
+
const { text, warnings } = await readSpecText(spec, root, options);
|
|
45
228
|
const normalized = normalize(text);
|
|
46
229
|
const { specification } = upgrade(normalized);
|
|
47
|
-
return { document: specification as ApiDocument, warnings
|
|
230
|
+
return { document: specification as ApiDocument, warnings };
|
|
48
231
|
};
|
|
@@ -73,13 +73,47 @@ export const operationMdx = (
|
|
|
73
73
|
};
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
export const overviewMdx = (spec: ApiSpecData): RenderedPage =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
export const overviewMdx = (spec: ApiSpecData): RenderedPage => {
|
|
77
|
+
// Tag sections: declared tags in spec order, then any tag an operation
|
|
78
|
+
// references that isn't declared under `tags`. The section headings are
|
|
79
|
+
// emitted as real markdown `##` (not markup inside a component) so the
|
|
80
|
+
// markdown pipeline gives them ids, permalink anchors, and table-of-contents
|
|
81
|
+
// entries; only the operation-link list defers to a component.
|
|
82
|
+
const operations = Object.values(spec.operations);
|
|
83
|
+
const sections = [...spec.tags];
|
|
84
|
+
const known = new Set(spec.tags.map((tag) => tag.slug));
|
|
85
|
+
for (const operation of operations) {
|
|
86
|
+
if (!known.has(operation.tagSlug)) {
|
|
87
|
+
known.add(operation.tagSlug);
|
|
88
|
+
sections.push({
|
|
89
|
+
description: "",
|
|
90
|
+
name: operation.tag,
|
|
91
|
+
slug: operation.tagSlug,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const tagSections = sections
|
|
96
|
+
.filter((tag) =>
|
|
97
|
+
operations.some((operation) => operation.tagSlug === tag.slug)
|
|
98
|
+
)
|
|
99
|
+
.map((tag) =>
|
|
100
|
+
[
|
|
101
|
+
`## ${mdxSafe(tag.name)}`,
|
|
102
|
+
...(tag.description.trim() ? [mdxSafe(tag.description.trim())] : []),
|
|
103
|
+
`<ApiTagOperations source="${spec.slug}" tag="${tag.slug}" />`,
|
|
104
|
+
].join("\n\n")
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
body: [
|
|
108
|
+
withDescription(
|
|
109
|
+
spec.description,
|
|
110
|
+
`<ApiOverview source="${spec.slug}" />`
|
|
111
|
+
),
|
|
112
|
+
...tagSections,
|
|
113
|
+
].join("\n\n"),
|
|
114
|
+
data: {
|
|
115
|
+
sidebar: { label: "Overview" },
|
|
116
|
+
title: spec.title || spec.label,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
package/src/openapi/scalar.ts
CHANGED
|
@@ -61,7 +61,7 @@ const themeConfiguration = (
|
|
|
61
61
|
const accent = resolveAccent(config.theme);
|
|
62
62
|
const radius = resolveRadius(config.theme);
|
|
63
63
|
return {
|
|
64
|
-
customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent};--scalar-radius:${radius};}`,
|
|
64
|
+
customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent.light};--scalar-radius:${radius};}.dark-mode{--scalar-color-accent:${accent.dark};}`,
|
|
65
65
|
...darkModeConfig(config.theme.mode),
|
|
66
66
|
};
|
|
67
67
|
};
|
package/src/openapi/source.ts
CHANGED
|
@@ -69,6 +69,8 @@ interface LoadedSpec {
|
|
|
69
69
|
slug: string;
|
|
70
70
|
spec: ApiSpecData;
|
|
71
71
|
entries: SourceEntry[];
|
|
72
|
+
/** Non-fatal notes from the load (e.g. an offline cache fallback). */
|
|
73
|
+
diagnostics: Diagnostic[];
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export const openApiSource = (
|
|
@@ -81,7 +83,11 @@ export const openApiSource = (
|
|
|
81
83
|
reference: ReferenceSource
|
|
82
84
|
): Promise<LoadedSpec | Diagnostic> => {
|
|
83
85
|
try {
|
|
84
|
-
const { document } = await parseSpec(
|
|
86
|
+
const { document, warnings } = await parseSpec(
|
|
87
|
+
reference.spec,
|
|
88
|
+
ctx.projectRoot,
|
|
89
|
+
{ cacheDir: ctx.cacheDir, refresh: ctx.refresh }
|
|
90
|
+
);
|
|
85
91
|
const { operations, tags } = extractOperations(document, reference.route);
|
|
86
92
|
const info = document.info ?? { title: reference.label, version: "" };
|
|
87
93
|
const spec: ApiSpecData = {
|
|
@@ -100,6 +106,11 @@ export const openApiSource = (
|
|
|
100
106
|
version: info.version ?? "",
|
|
101
107
|
};
|
|
102
108
|
return {
|
|
109
|
+
diagnostics: warnings.map((message) => ({
|
|
110
|
+
code: "BLUME_OPENAPI_STALE",
|
|
111
|
+
message,
|
|
112
|
+
severity: "warning" as const,
|
|
113
|
+
})),
|
|
103
114
|
entries: specEntries(spec, operations),
|
|
104
115
|
slug: reference.slug,
|
|
105
116
|
spec,
|
|
@@ -108,7 +119,12 @@ export const openApiSource = (
|
|
|
108
119
|
return {
|
|
109
120
|
code: "BLUME_OPENAPI_UNAVAILABLE",
|
|
110
121
|
message: `Could not load OpenAPI spec "${reference.spec}" for ${reference.route} (${(error as Error).message}); its reference pages were skipped.`,
|
|
111
|
-
|
|
122
|
+
// A configured-but-unloadable spec ships a dead nav tab (a 404 route),
|
|
123
|
+
// so fail loudly in build (blocks under --strict) while staying a warning
|
|
124
|
+
// in dev so offline work still runs.
|
|
125
|
+
severity: ctx.mode === "build" ? "error" : "warning",
|
|
126
|
+
suggestion:
|
|
127
|
+
"Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY.",
|
|
112
128
|
};
|
|
113
129
|
}
|
|
114
130
|
};
|
|
@@ -125,6 +141,7 @@ export const openApiSource = (
|
|
|
125
141
|
}
|
|
126
142
|
data[result.slug] = result.spec;
|
|
127
143
|
entries.push(...result.entries);
|
|
144
|
+
diagnostics.push(...result.diagnostics);
|
|
128
145
|
}
|
|
129
146
|
parsed = data;
|
|
130
147
|
return { diagnostics, entries };
|
package/src/search/documents.ts
CHANGED
|
@@ -114,10 +114,15 @@ const buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {
|
|
|
114
114
|
* Pass `includeWhenDisabled` to index pages on their content merits even when
|
|
115
115
|
* the search provider is `none` — used by the MCP server, which is a separate
|
|
116
116
|
* feature from on-page search.
|
|
117
|
+
*
|
|
118
|
+
* `content` selects the extraction: `"plain"` (default) strips Markdown to bare
|
|
119
|
+
* searchable text; `"markdown"` keeps the body's Markdown — code blocks, lists,
|
|
120
|
+
* headings — for Ask AI grounding, where fenced examples are often the answer
|
|
121
|
+
* and stripping them makes the model unable to cite content the docs do contain.
|
|
117
122
|
*/
|
|
118
123
|
export const buildSearchDocuments = async (
|
|
119
124
|
project: BlumeProject,
|
|
120
|
-
options?: { includeWhenDisabled?: boolean }
|
|
125
|
+
options?: { includeWhenDisabled?: boolean; content?: "markdown" | "plain" }
|
|
121
126
|
): Promise<SearchDocument[]> => {
|
|
122
127
|
const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
|
|
123
128
|
|
|
@@ -148,7 +153,9 @@ export const buildSearchDocuments = async (
|
|
|
148
153
|
indexable.map(async (route) => {
|
|
149
154
|
const page = pageById.get(route.id);
|
|
150
155
|
const raw = page ? await readEntryText(project, page) : "";
|
|
151
|
-
const
|
|
156
|
+
const source = raw ? matter(raw).content : "";
|
|
157
|
+
const body =
|
|
158
|
+
options?.content === "markdown" ? source.trim() : toPlainText(source);
|
|
152
159
|
const tags = page?.meta?.search?.tags;
|
|
153
160
|
const crumb = crumbs.get(route.path);
|
|
154
161
|
return {
|
package/src/theme/entry.ts
CHANGED
|
@@ -32,9 +32,6 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
32
32
|
|
|
33
33
|
:root {
|
|
34
34
|
--blume-background: oklch(1 0 0);
|
|
35
|
-
--blume-background-decoration: none;
|
|
36
|
-
--blume-background-decoration-repeat: no-repeat;
|
|
37
|
-
--blume-background-decoration-size: auto;
|
|
38
35
|
--blume-background-image: none;
|
|
39
36
|
--blume-background-image-repeat: no-repeat;
|
|
40
37
|
--blume-background-image-size: cover;
|
|
@@ -90,9 +87,6 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
90
87
|
|
|
91
88
|
:root[data-theme="dark"] {
|
|
92
89
|
--blume-background: oklch(0.085 0 0);
|
|
93
|
-
--blume-background-decoration: none;
|
|
94
|
-
--blume-background-decoration-repeat: no-repeat;
|
|
95
|
-
--blume-background-decoration-size: auto;
|
|
96
90
|
--blume-background-image: none;
|
|
97
91
|
--blume-background-image-repeat: no-repeat;
|
|
98
92
|
--blume-background-image-size: cover;
|
|
@@ -134,9 +128,22 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
134
128
|
}
|
|
135
129
|
|
|
136
130
|
@layer base {
|
|
131
|
+
/* Nothing refuses to shrink below its intrinsic content width. This global
|
|
132
|
+
min-width reset defuses the classic flex/grid overflow — a long or
|
|
133
|
+
truncating child forcing its container (and the page) past the viewport
|
|
134
|
+
edge — so components don't need per-element min-w-0 overrides. */
|
|
135
|
+
* {
|
|
136
|
+
min-width: 0;
|
|
137
|
+
}
|
|
138
|
+
/* Interactive controls get a pointer cursor unless disabled. */
|
|
139
|
+
button:not(:disabled),
|
|
140
|
+
[role="button"]:not(:disabled) {
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
}
|
|
137
143
|
html {
|
|
138
144
|
scroll-behavior: smooth;
|
|
139
145
|
scroll-padding-top: 4.5rem;
|
|
146
|
+
text-rendering: optimizeLegibility;
|
|
140
147
|
}
|
|
141
148
|
/* Headings use the display font (defaults to the body font when unset). */
|
|
142
149
|
h1,
|
|
@@ -153,16 +160,11 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
153
160
|
border-radius: 2px;
|
|
154
161
|
}
|
|
155
162
|
body {
|
|
156
|
-
background-attachment: fixed
|
|
157
|
-
background-image:
|
|
158
|
-
|
|
159
|
-
background-
|
|
160
|
-
background-
|
|
161
|
-
var(--blume-background-image-repeat),
|
|
162
|
-
var(--blume-background-decoration-repeat);
|
|
163
|
-
background-size:
|
|
164
|
-
var(--blume-background-image-size),
|
|
165
|
-
var(--blume-background-decoration-size);
|
|
163
|
+
background-attachment: fixed;
|
|
164
|
+
background-image: var(--blume-background-image);
|
|
165
|
+
background-position: center top;
|
|
166
|
+
background-repeat: var(--blume-background-image-repeat);
|
|
167
|
+
background-size: var(--blume-background-image-size);
|
|
166
168
|
}
|
|
167
169
|
@media (prefers-reduced-motion: reduce) {
|
|
168
170
|
html {
|
|
@@ -298,7 +300,7 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
298
300
|
line-height: 1.55;
|
|
299
301
|
margin: 1.5rem 0;
|
|
300
302
|
overflow-x: auto;
|
|
301
|
-
padding: 1rem
|
|
303
|
+
padding: 1rem 0;
|
|
302
304
|
position: relative;
|
|
303
305
|
}
|
|
304
306
|
|
|
@@ -395,6 +397,32 @@ blume-diff {
|
|
|
395
397
|
padding: 0;
|
|
396
398
|
}
|
|
397
399
|
|
|
400
|
+
/* Long lines scroll inside the code element, not the pre: the pre stays static
|
|
401
|
+
so its absolute header bar (::before) and copy button don't drift with the
|
|
402
|
+
scroll. The pre's horizontal padding lives here so content still scrolls
|
|
403
|
+
edge-to-edge past it. Two contexts opt out: twoslash blocks (popups must
|
|
404
|
+
escape any scroll container — see theme/twoslash.ts), and the API request
|
|
405
|
+
panel, which owns its code layout and keeps the copy control in the panel
|
|
406
|
+
header. Every other component that hosts a code block — Tabs, CodeGroup,
|
|
407
|
+
Steps, Callout, Card, Accordion — is real prose content and keeps the inset,
|
|
408
|
+
even though its chrome wrapper is not-prose. */
|
|
409
|
+
.prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
410
|
+
display: block;
|
|
411
|
+
overflow-x: auto;
|
|
412
|
+
padding: 0 1.25rem;
|
|
413
|
+
/* The scroller is only as tall as the code, so an overlay scrollbar would
|
|
414
|
+
draw on top of the last line; hide it (wheel/trackpad/keyboard scrolling
|
|
415
|
+
still works). */
|
|
416
|
+
scrollbar-width: none;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.prose
|
|
420
|
+
:where(
|
|
421
|
+
pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code
|
|
422
|
+
)::-webkit-scrollbar {
|
|
423
|
+
display: none;
|
|
424
|
+
}
|
|
425
|
+
|
|
398
426
|
/* Word wrap (markdown.code.wrap): long lines wrap instead of scrolling. The
|
|
399
427
|
attribute is set on <body> from config; default code keeps \`white-space: pre\`. */
|
|
400
428
|
[data-blume-code-wrap] pre,
|