blume 1.5.2 → 1.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/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
|
@@ -14,7 +14,7 @@ export { normalizeRoute } from "../core/base-path.ts";
|
|
|
14
14
|
* imports so `core` can depend on it without a cycle.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
export type ReferenceKind = "openapi" | "asyncapi";
|
|
17
|
+
export type ReferenceKind = "openapi" | "asyncapi" | "graphql";
|
|
18
18
|
|
|
19
19
|
/** Who renders a reference: Blume's own UI, or the embedded Scalar SPA. */
|
|
20
20
|
export type ReferenceRenderer = "blume" | "scalar";
|
|
@@ -56,6 +56,11 @@ export interface ReferenceSource {
|
|
|
56
56
|
noindex: boolean;
|
|
57
57
|
/** Local path or `http(s)` URL, verbatim from config. */
|
|
58
58
|
spec: string;
|
|
59
|
+
/**
|
|
60
|
+
* URL of the live GraphQL endpoint the playground and code samples target
|
|
61
|
+
* (GraphQL only — a schema, unlike an OpenAPI document, names no server).
|
|
62
|
+
*/
|
|
63
|
+
endpoint?: string;
|
|
59
64
|
/** Per-block Scalar theme name override, if any (Scalar renderer only). */
|
|
60
65
|
theme?: string;
|
|
61
66
|
/**
|
|
@@ -109,19 +114,32 @@ export const slugify = (text: string): string =>
|
|
|
109
114
|
const routeSlug = (route: string): string =>
|
|
110
115
|
slugify(trimChar(route, "/")) || "reference";
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The structural shape all three reference blocks (`openapi`, `asyncapi`,
|
|
119
|
+
* `graphql`) share. `endpoint` exists only on the GraphQL block and its
|
|
120
|
+
* sources; `scalar`/`theme` only on the Scalar-capable kinds — optional here
|
|
121
|
+
* so one resolver serves every block.
|
|
122
|
+
*/
|
|
123
|
+
interface Block {
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
endpoint?: string;
|
|
126
|
+
route: string;
|
|
127
|
+
scalar?: ResolvedConfig["openapi"]["scalar"];
|
|
128
|
+
sources: {
|
|
129
|
+
endpoint?: string;
|
|
130
|
+
includeInLlms: boolean;
|
|
131
|
+
includeInSearch: boolean;
|
|
132
|
+
label?: string;
|
|
133
|
+
noindex: boolean;
|
|
134
|
+
route?: string;
|
|
135
|
+
spec: string;
|
|
136
|
+
}[];
|
|
137
|
+
spec?: string;
|
|
138
|
+
theme?: string;
|
|
139
|
+
}
|
|
113
140
|
|
|
114
141
|
/** A spec is a single source (`spec` shorthand prepended to any `sources`). */
|
|
115
|
-
const sourcesOf = (
|
|
116
|
-
block: Block
|
|
117
|
-
): {
|
|
118
|
-
includeInLlms: boolean;
|
|
119
|
-
includeInSearch: boolean;
|
|
120
|
-
label?: string;
|
|
121
|
-
noindex: boolean;
|
|
122
|
-
route?: string;
|
|
123
|
-
spec: string;
|
|
124
|
-
}[] => {
|
|
142
|
+
const sourcesOf = (block: Block): Block["sources"] => {
|
|
125
143
|
const sources = [...block.sources];
|
|
126
144
|
if (block.spec) {
|
|
127
145
|
sources.unshift({
|
|
@@ -163,7 +181,7 @@ const referencesFor = (
|
|
|
163
181
|
route = normalizeRoute(`${base}/${suffix || index + 1}`);
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
|
|
184
|
+
const reference: ReferenceSource = {
|
|
167
185
|
basePath,
|
|
168
186
|
display,
|
|
169
187
|
includeInLlms: source.includeInLlms,
|
|
@@ -178,6 +196,13 @@ const referencesFor = (
|
|
|
178
196
|
spec: source.spec,
|
|
179
197
|
theme: block.theme,
|
|
180
198
|
};
|
|
199
|
+
// Per-source endpoint wins; the block-level one is the shared default
|
|
200
|
+
// (the common single-schema case pairs it with the `spec` shorthand).
|
|
201
|
+
const endpoint = source.endpoint ?? block.endpoint;
|
|
202
|
+
if (endpoint !== undefined) {
|
|
203
|
+
reference.endpoint = endpoint;
|
|
204
|
+
}
|
|
205
|
+
return reference;
|
|
181
206
|
});
|
|
182
207
|
};
|
|
183
208
|
|
|
@@ -212,6 +237,21 @@ export const resolveReferences = (
|
|
|
212
237
|
},
|
|
213
238
|
config.basePath
|
|
214
239
|
),
|
|
240
|
+
// GraphQL is always Blume-rendered — Scalar's embedded SPA reads OpenAPI
|
|
241
|
+
// documents only, so the block declares no `renderer` opt-out (nor the
|
|
242
|
+
// schema-row `expandSchemas` toggle; GraphQL field tables have no nesting).
|
|
243
|
+
...referencesFor(
|
|
244
|
+
"graphql",
|
|
245
|
+
config.graphql,
|
|
246
|
+
"GraphQL",
|
|
247
|
+
"blume",
|
|
248
|
+
{
|
|
249
|
+
codeSamples: config.graphql.codeSamples,
|
|
250
|
+
expandSchemas: false,
|
|
251
|
+
playground: config.graphql.playground,
|
|
252
|
+
},
|
|
253
|
+
config.basePath
|
|
254
|
+
),
|
|
215
255
|
];
|
|
216
256
|
|
|
217
257
|
/**
|
|
@@ -287,3 +327,39 @@ export const blumeReferences = (config: ResolvedConfig): ReferenceSource[] => {
|
|
|
287
327
|
/** Whether any reference is Scalar-rendered (gates the `@scalar/astro` dep + pages). */
|
|
288
328
|
export const hasScalarReferences = (config: ResolvedConfig): boolean =>
|
|
289
329
|
resolveReferences(config).some((ref) => ref.renderer === "scalar");
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The reference kinds whose enabled, Blume-rendered playground opted into the
|
|
333
|
+
* built-in CORS proxy with `proxy: true`. A proxy URL string points at an
|
|
334
|
+
* external service, and `false` sends requests directly — neither routes
|
|
335
|
+
* through the endpoint. The generator's per-spec allowlist diagnostics key on
|
|
336
|
+
* this, so it shares one definition with {@link needsPlaygroundProxy}.
|
|
337
|
+
*/
|
|
338
|
+
export const builtinProxyKinds = (config: ResolvedConfig): ReferenceKind[] => {
|
|
339
|
+
const kinds: ReferenceKind[] = [];
|
|
340
|
+
if (
|
|
341
|
+
config.openapi.enabled &&
|
|
342
|
+
config.openapi.renderer === "blume" &&
|
|
343
|
+
config.openapi.playground.enabled &&
|
|
344
|
+
config.openapi.playground.proxy === true
|
|
345
|
+
) {
|
|
346
|
+
kinds.push("openapi");
|
|
347
|
+
}
|
|
348
|
+
if (
|
|
349
|
+
config.graphql.enabled &&
|
|
350
|
+
config.graphql.playground.enabled &&
|
|
351
|
+
config.graphql.playground.proxy === true
|
|
352
|
+
) {
|
|
353
|
+
kinds.push("graphql");
|
|
354
|
+
}
|
|
355
|
+
return kinds;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Whether the built-in playground CORS proxy endpoint (`/_api-proxy`) must be
|
|
360
|
+
* generated: some enabled Blume-rendered block's playground opted into it with
|
|
361
|
+
* `proxy: true`. Shared by the server feature gate and the generator so the
|
|
362
|
+
* two can never disagree.
|
|
363
|
+
*/
|
|
364
|
+
export const needsPlaygroundProxy = (config: ResolvedConfig): boolean =>
|
|
365
|
+
builtinProxyKinds(config).length > 0;
|
|
@@ -4,6 +4,8 @@ import { toString as mdastToString } from "mdast-util-to-string";
|
|
|
4
4
|
import stringWidth from "string-width";
|
|
5
5
|
|
|
6
6
|
import { columnsPrefix } from "../core/text-width.ts";
|
|
7
|
+
import type { GraphqlMember } from "./graphql.ts";
|
|
8
|
+
import { isGraphqlOperationKind } from "./graphql.ts";
|
|
7
9
|
import type { ApiOperationRef, ApiSpecData } from "./model.ts";
|
|
8
10
|
import type { ReferenceSource } from "./references.ts";
|
|
9
11
|
|
|
@@ -156,6 +158,19 @@ const clip = (text: string, max: number): string => {
|
|
|
156
158
|
|
|
157
159
|
const apiName = (spec: ApiSpecData): string => spec.title || spec.label;
|
|
158
160
|
|
|
161
|
+
/** Human phrase for each GraphQL page kind, for meta descriptions. */
|
|
162
|
+
const GRAPHQL_MEMBER_PHRASES = {
|
|
163
|
+
enum: "enum type",
|
|
164
|
+
input: "input object type",
|
|
165
|
+
interface: "interface type",
|
|
166
|
+
mutation: "mutation",
|
|
167
|
+
object: "object type",
|
|
168
|
+
query: "query",
|
|
169
|
+
scalar: "scalar type",
|
|
170
|
+
subscription: "subscription",
|
|
171
|
+
union: "union type",
|
|
172
|
+
} satisfies Record<GraphqlMember, string>;
|
|
173
|
+
|
|
159
174
|
/**
|
|
160
175
|
* The spec's own prose for the operation, followed by the endpoint it documents
|
|
161
176
|
* — so every operation page carries a distinct, self-describing meta
|
|
@@ -165,11 +180,18 @@ const operationDescription = (
|
|
|
165
180
|
spec: ApiSpecData,
|
|
166
181
|
operation: ApiOperationRef
|
|
167
182
|
): string => {
|
|
168
|
-
// AsyncAPI operations act on a channel, not an HTTP endpoint
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
// AsyncAPI operations act on a channel, not an HTTP endpoint; GraphQL pages
|
|
184
|
+
// document a root field or a named type.
|
|
185
|
+
let suffix: string;
|
|
186
|
+
if (spec.kind === "asyncapi") {
|
|
187
|
+
suffix = `Reference for the ${operation.method} operation on ${operation.path} in the ${apiName(spec)} API.`;
|
|
188
|
+
} else if (spec.kind === "graphql") {
|
|
189
|
+
// SAFETY: the GraphQL extractor only ever assigns member kinds as the
|
|
190
|
+
// method (see `extractGraphqlOperations`).
|
|
191
|
+
suffix = `Reference for the ${operation.path} ${GRAPHQL_MEMBER_PHRASES[operation.method as GraphqlMember]} in the ${apiName(spec)} API.`;
|
|
192
|
+
} else {
|
|
193
|
+
suffix = `Reference for the ${operation.method.toUpperCase()} ${operation.path} endpoint in the ${apiName(spec)} API.`;
|
|
194
|
+
}
|
|
173
195
|
const prose = clip(
|
|
174
196
|
plainProse(operation.description || operation.summary),
|
|
175
197
|
META_DESCRIPTION_MAX - stringWidth(suffix) - 1
|
|
@@ -192,7 +214,13 @@ export const operationMdx = (
|
|
|
192
214
|
>
|
|
193
215
|
): RenderedPage => {
|
|
194
216
|
const method = operation.method.toUpperCase();
|
|
195
|
-
const
|
|
217
|
+
const graphql = spec.kind === "graphql";
|
|
218
|
+
// A GraphQL page IS its field/type — `QUERY pets` would double the badge the
|
|
219
|
+
// page already renders; the other kinds title an endpoint or channel action.
|
|
220
|
+
const fallbackTitle = graphql
|
|
221
|
+
? operation.path
|
|
222
|
+
: `${method} ${operation.path}`;
|
|
223
|
+
const title = operation.summary || fallbackTitle;
|
|
196
224
|
// Skip the body description when it only repeats the summary (the `<h1>`) —
|
|
197
225
|
// common in specs that set summary and description to the same string.
|
|
198
226
|
const description =
|
|
@@ -219,6 +247,18 @@ export const operationMdx = (
|
|
|
219
247
|
if (reference?.noindex) {
|
|
220
248
|
seo.noindex = true;
|
|
221
249
|
}
|
|
250
|
+
const sidebar: RenderedPageData["sidebar"] = {
|
|
251
|
+
label: operation.summary || operation.path,
|
|
252
|
+
};
|
|
253
|
+
// GraphQL operation kinds badge like HTTP methods, but a type page's kind
|
|
254
|
+
// already heads its sidebar group ("Objects", "Enums", …) — an `OBJECT`
|
|
255
|
+
// badge on every row would only repeat it, so type pages get none. The
|
|
256
|
+
// uppercased method is likewise an internal token on GraphQL pages, so
|
|
257
|
+
// their search tags carry only the group name.
|
|
258
|
+
if (!graphql || isGraphqlOperationKind(operation.method)) {
|
|
259
|
+
sidebar.badge = method;
|
|
260
|
+
}
|
|
261
|
+
const tags = graphql ? [operation.tag] : [operation.tag, method];
|
|
222
262
|
return {
|
|
223
263
|
body: withDescription(
|
|
224
264
|
description,
|
|
@@ -226,9 +266,9 @@ export const operationMdx = (
|
|
|
226
266
|
),
|
|
227
267
|
data: {
|
|
228
268
|
...flags,
|
|
229
|
-
search: { ...searchFlags, tags
|
|
269
|
+
search: { ...searchFlags, tags },
|
|
230
270
|
seo,
|
|
231
|
-
sidebar
|
|
271
|
+
sidebar,
|
|
232
272
|
title,
|
|
233
273
|
// Signals the two-column API layout (request panel instead of the TOC).
|
|
234
274
|
type: "openapi-operation",
|
package/src/openapi/scalar.ts
CHANGED
|
@@ -32,22 +32,16 @@ const referencePagePath = (route: string): string => {
|
|
|
32
32
|
return `${segments === "" ? "index" : segments}.astro`;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const darkModeConfig = (mode: ResolvedConfig["theme"]["mode"]) => {
|
|
36
|
-
if (mode === "dark") {
|
|
37
|
-
return { darkMode: true };
|
|
38
|
-
}
|
|
39
|
-
if (mode === "light") {
|
|
40
|
-
return { darkMode: false };
|
|
41
|
-
}
|
|
42
|
-
// "system": leave Scalar to follow the OS preference.
|
|
43
|
-
return {};
|
|
44
|
-
};
|
|
45
|
-
|
|
46
35
|
/**
|
|
47
36
|
* Map Blume's theme onto Scalar's. An explicit `theme` name wins; otherwise we
|
|
48
37
|
* keep Scalar's default theme and layer Blume's accent/radius on top via
|
|
49
38
|
* `customCss`. Scalar re-injects `customCss` after its bundled theme, so these
|
|
50
39
|
* variables reliably override the defaults. Best-effort, not pixel-exact.
|
|
40
|
+
*
|
|
41
|
+
* Light/dark is not decided here: the page's `data-theme` (stored preference,
|
|
42
|
+
* else the configured mode, else the OS) is only known in the browser, so
|
|
43
|
+
* `ReferenceLayout` pins Scalar's color mode to it at mount and keeps the two
|
|
44
|
+
* in step afterwards (`SCALAR_THEME_INIT_SCRIPT`).
|
|
51
45
|
*/
|
|
52
46
|
const themeConfiguration = (config: ResolvedConfig, override?: string) => {
|
|
53
47
|
if (override) {
|
|
@@ -57,7 +51,6 @@ const themeConfiguration = (config: ResolvedConfig, override?: string) => {
|
|
|
57
51
|
const radius = resolveRadius(config.theme);
|
|
58
52
|
return {
|
|
59
53
|
customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent.light};--scalar-radius:${radius};}.dark-mode{--scalar-color-accent:${accent.dark};}`,
|
|
60
|
-
...darkModeConfig(config.theme.mode),
|
|
61
54
|
};
|
|
62
55
|
};
|
|
63
56
|
|
package/src/openapi/source.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
import type { Diagnostic } from "../core/types.ts";
|
|
12
12
|
import { extractAsyncApiOperations } from "./asyncapi.ts";
|
|
13
13
|
import type { AsyncApiDocument } from "./asyncapi.ts";
|
|
14
|
+
import { extractGraphqlOperations } from "./graphql.ts";
|
|
15
|
+
import type { GraphqlDocument } from "./graphql.ts";
|
|
14
16
|
import { extractOperations } from "./model.ts";
|
|
15
17
|
import type {
|
|
16
18
|
ApiDocument,
|
|
@@ -19,7 +21,12 @@ import type {
|
|
|
19
21
|
ApiTagRef,
|
|
20
22
|
OpenApiData,
|
|
21
23
|
} from "./model.ts";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
InvalidSpecError,
|
|
26
|
+
parseAsyncApiSpec,
|
|
27
|
+
parseGraphqlSpec,
|
|
28
|
+
parseSpec,
|
|
29
|
+
} from "./parse.ts";
|
|
23
30
|
import type { ReferenceSource } from "./references.ts";
|
|
24
31
|
import { operationMdx, overviewMdx } from "./render-mdx.ts";
|
|
25
32
|
import type { RenderedPage } from "./render-mdx.ts";
|
|
@@ -51,6 +58,62 @@ export const isOpenApiSource = (
|
|
|
51
58
|
/** Route (`/reference/pet/add-pet`) to a staged content ref, without extension. */
|
|
52
59
|
const routeToRef = (route: string): string => route.replace(/^\/+/u, "");
|
|
53
60
|
|
|
61
|
+
/** Human label per spec kind, so one kind's failure never reads as another's. */
|
|
62
|
+
const KIND_LABELS = {
|
|
63
|
+
asyncapi: "AsyncAPI",
|
|
64
|
+
graphql: "GraphQL",
|
|
65
|
+
openapi: "OpenAPI",
|
|
66
|
+
} satisfies Record<ReferenceSource["kind"], string>;
|
|
67
|
+
|
|
68
|
+
/** Diagnostic-code prefix per spec kind. */
|
|
69
|
+
const CODE_PREFIXES = {
|
|
70
|
+
asyncapi: "BLUME_ASYNCAPI",
|
|
71
|
+
graphql: "BLUME_GRAPHQL",
|
|
72
|
+
openapi: "BLUME_OPENAPI",
|
|
73
|
+
} satisfies Record<ReferenceSource["kind"], string>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse-level warning code per kind: an offline cache fallback for any kind,
|
|
77
|
+
* plus lossy 2.x→3.0 conversion notes for AsyncAPI — hence the broader codes
|
|
78
|
+
* there and for GraphQL (OpenAPI keeps its historical one).
|
|
79
|
+
*/
|
|
80
|
+
const SPEC_WARNING_CODES = {
|
|
81
|
+
asyncapi: "BLUME_ASYNCAPI_SPEC_WARNING",
|
|
82
|
+
graphql: "BLUME_GRAPHQL_SPEC_WARNING",
|
|
83
|
+
openapi: "BLUME_OPENAPI_STALE",
|
|
84
|
+
} satisfies Record<ReferenceSource["kind"], string>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract-level skip code per kind. OpenAPI keeps its historical code (the
|
|
88
|
+
* only extract warning it emits is the unresolved $ref path item); the
|
|
89
|
+
* GraphQL extractor currently skips nothing, so its code is reserved.
|
|
90
|
+
*/
|
|
91
|
+
const SKIPPED_CODES = {
|
|
92
|
+
asyncapi: "BLUME_ASYNCAPI_SKIPPED_OPERATION",
|
|
93
|
+
graphql: "BLUME_GRAPHQL_SKIPPED",
|
|
94
|
+
openapi: "BLUME_OPENAPI_REF_PATH_ITEM",
|
|
95
|
+
} satisfies Record<ReferenceSource["kind"], string>;
|
|
96
|
+
|
|
97
|
+
/** `_EMPTY` diagnostic suggestion per kind. */
|
|
98
|
+
const EMPTY_SUGGESTIONS = {
|
|
99
|
+
asyncapi:
|
|
100
|
+
"Check the spec points at an AsyncAPI document with `channels` and `operations`.",
|
|
101
|
+
graphql:
|
|
102
|
+
"Check the spec points at a GraphQL schema (SDL or introspection JSON) whose root types declare fields.",
|
|
103
|
+
openapi:
|
|
104
|
+
"Check the spec points at an OpenAPI document with operations under `paths`.",
|
|
105
|
+
} satisfies Record<ReferenceSource["kind"], string>;
|
|
106
|
+
|
|
107
|
+
/** `_UNAVAILABLE` suggestion for a readable-but-invalid spec, per kind. */
|
|
108
|
+
const INVALID_SUGGESTIONS = {
|
|
109
|
+
asyncapi:
|
|
110
|
+
"Point the spec at an AsyncAPI document (a YAML or JSON file with an object at the top level).",
|
|
111
|
+
graphql:
|
|
112
|
+
"Point the spec at a GraphQL schema — SDL text or an introspection JSON result.",
|
|
113
|
+
openapi:
|
|
114
|
+
"Point the spec at an OpenAPI document (a YAML or JSON file with an object at the top level).",
|
|
115
|
+
} satisfies Record<ReferenceSource["kind"], string>;
|
|
116
|
+
|
|
54
117
|
const toEntry = (rendered: RenderedPage, ref: string): SourceEntry => {
|
|
55
118
|
const raw = matter.stringify(`${rendered.body}\n`, rendered.data);
|
|
56
119
|
return {
|
|
@@ -122,7 +185,7 @@ interface LoadedSpec {
|
|
|
122
185
|
|
|
123
186
|
/** One parsed spec, whichever front-end read it — the kind dispatch seam. */
|
|
124
187
|
interface ParsedReference {
|
|
125
|
-
document: ApiDocument | AsyncApiDocument;
|
|
188
|
+
document: ApiDocument | AsyncApiDocument | GraphqlDocument;
|
|
126
189
|
warnings: string[];
|
|
127
190
|
operations: ApiOperationRef[];
|
|
128
191
|
tags: ApiTagRef[];
|
|
@@ -149,6 +212,21 @@ const parseReference = async (
|
|
|
149
212
|
warnings,
|
|
150
213
|
};
|
|
151
214
|
}
|
|
215
|
+
if (reference.kind === "graphql") {
|
|
216
|
+
const { document, warnings } = await parseGraphqlSpec(
|
|
217
|
+
reference.spec,
|
|
218
|
+
ctx.projectRoot,
|
|
219
|
+
options
|
|
220
|
+
);
|
|
221
|
+
const extracted = extractGraphqlOperations(document, reference.route);
|
|
222
|
+
return {
|
|
223
|
+
document,
|
|
224
|
+
extractWarnings: extracted.warnings,
|
|
225
|
+
operations: extracted.operations,
|
|
226
|
+
tags: extracted.tags,
|
|
227
|
+
warnings,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
152
230
|
const { document, warnings } = await parseSpec(
|
|
153
231
|
reference.spec,
|
|
154
232
|
ctx.projectRoot,
|
|
@@ -175,9 +253,8 @@ export const openApiSource = (
|
|
|
175
253
|
): Promise<LoadedSpec | Diagnostic> => {
|
|
176
254
|
// Human label and diagnostic-code prefix for the spec's kind, so an
|
|
177
255
|
// AsyncAPI failure never reads as an OpenAPI one.
|
|
178
|
-
const kindLabel = reference.kind
|
|
179
|
-
const codePrefix =
|
|
180
|
-
reference.kind === "asyncapi" ? "BLUME_ASYNCAPI" : "BLUME_OPENAPI";
|
|
256
|
+
const kindLabel = KIND_LABELS[reference.kind];
|
|
257
|
+
const codePrefix = CODE_PREFIXES[reference.kind];
|
|
181
258
|
try {
|
|
182
259
|
const { document, warnings, operations, tags, extractWarnings } =
|
|
183
260
|
await parseReference(reference, ctx);
|
|
@@ -221,26 +298,20 @@ export const openApiSource = (
|
|
|
221
298
|
title: info.title ?? reference.label,
|
|
222
299
|
version: info.version ?? "",
|
|
223
300
|
};
|
|
301
|
+
// Only GraphQL references carry a live endpoint (a schema names no
|
|
302
|
+
// server); assigned separately so the key stays absent otherwise.
|
|
303
|
+
if (reference.endpoint !== undefined) {
|
|
304
|
+
spec.endpoint = reference.endpoint;
|
|
305
|
+
}
|
|
224
306
|
return {
|
|
225
307
|
diagnostics: [
|
|
226
308
|
...warnings.map((message) => ({
|
|
227
|
-
|
|
228
|
-
// plus lossy 2.x→3.0 conversion notes for AsyncAPI — hence the
|
|
229
|
-
// broader code on that side (OpenAPI keeps its historical one).
|
|
230
|
-
code:
|
|
231
|
-
reference.kind === "asyncapi"
|
|
232
|
-
? "BLUME_ASYNCAPI_SPEC_WARNING"
|
|
233
|
-
: "BLUME_OPENAPI_STALE",
|
|
309
|
+
code: SPEC_WARNING_CODES[reference.kind],
|
|
234
310
|
message,
|
|
235
311
|
severity: "warning" as const,
|
|
236
312
|
})),
|
|
237
313
|
...extractWarnings.map((message) => ({
|
|
238
|
-
|
|
239
|
-
// emits is the unresolved $ref path item).
|
|
240
|
-
code:
|
|
241
|
-
reference.kind === "asyncapi"
|
|
242
|
-
? "BLUME_ASYNCAPI_SKIPPED_OPERATION"
|
|
243
|
-
: "BLUME_OPENAPI_REF_PATH_ITEM",
|
|
314
|
+
code: SKIPPED_CODES[reference.kind],
|
|
244
315
|
message: `In ${kindLabel} spec "${reference.spec}": ${message}`,
|
|
245
316
|
severity: "warning" as const,
|
|
246
317
|
})),
|
|
@@ -253,10 +324,7 @@ export const openApiSource = (
|
|
|
253
324
|
code: `${codePrefix}_EMPTY`,
|
|
254
325
|
message: `${kindLabel} spec "${reference.spec}" for ${reference.route} declares no operations; its API reference is empty.`,
|
|
255
326
|
severity: "warning" as const,
|
|
256
|
-
suggestion:
|
|
257
|
-
reference.kind === "asyncapi"
|
|
258
|
-
? "Check the spec points at an AsyncAPI document with `channels` and `operations`."
|
|
259
|
-
: "Check the spec points at an OpenAPI document with operations under `paths`.",
|
|
327
|
+
suggestion: EMPTY_SUGGESTIONS[reference.kind],
|
|
260
328
|
},
|
|
261
329
|
]
|
|
262
330
|
: []),
|
|
@@ -280,7 +348,7 @@ export const openApiSource = (
|
|
|
280
348
|
// only point at reachability for actual fetch/read failures.
|
|
281
349
|
suggestion:
|
|
282
350
|
error instanceof InvalidSpecError
|
|
283
|
-
?
|
|
351
|
+
? INVALID_SUGGESTIONS[reference.kind]
|
|
284
352
|
: "Check the spec URL/path is reachable from the build environment; behind a proxy, set HTTP(S)_PROXY.",
|
|
285
353
|
};
|
|
286
354
|
}
|
package/src/registry/eject.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
searchEndpointTemplate,
|
|
45
45
|
staticJsonEndpointTemplate,
|
|
46
46
|
} from "../astro/templates.ts";
|
|
47
|
+
import { buildIncludeGraph } from "../core/includes.ts";
|
|
47
48
|
import { packageRoot } from "../core/package-root.ts";
|
|
48
49
|
import { scanProject } from "../core/project-graph.ts";
|
|
49
50
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
@@ -383,6 +384,7 @@ export const eject = async (
|
|
|
383
384
|
content: astroConfigTemplate({
|
|
384
385
|
askPath: "./src/generated/Ask.astro",
|
|
385
386
|
config,
|
|
387
|
+
contentRoot: relContext.contentRoot,
|
|
386
388
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
387
389
|
context: relContext,
|
|
388
390
|
dataPath: "./src/generated/data.json",
|
|
@@ -485,6 +487,15 @@ export const eject = async (
|
|
|
485
487
|
content: `${JSON.stringify(rawMarkdown)}\n`,
|
|
486
488
|
path: join(genDir, "raw-markdown.json"),
|
|
487
489
|
},
|
|
490
|
+
{
|
|
491
|
+
// The partial → including-pages map behind `includeHmrPlugin`, which the
|
|
492
|
+
// ejected astro.config wires at this exact path — without the file every
|
|
493
|
+
// hot update's read would silently no-op and partial edits would serve
|
|
494
|
+
// stale pages. A snapshot like the rest of `src/generated`: the ejected
|
|
495
|
+
// app owns (and may regenerate or prune) it.
|
|
496
|
+
content: `${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`,
|
|
497
|
+
path: join(genDir, "includes.json"),
|
|
498
|
+
},
|
|
488
499
|
{
|
|
489
500
|
content: rawMarkdownEndpointTemplate("md"),
|
|
490
501
|
path: join(srcDir, "pages", "[...slug].md.ts"),
|