blume 0.3.0 → 0.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/dist/cli/index.js +1631 -940
- package/dist/cli/index.js.map +62 -50
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/project.d.ts +12 -2
- package/dist/types/core/schema.d.ts +442 -292
- package/dist/types/core/types.d.ts +7 -0
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +21 -54
- package/docs/advanced/api-reference.mdx +34 -51
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/bridge.mdx +74 -0
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +21 -78
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +119 -0
- package/docs/configuration/ai.mdx +42 -103
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +40 -73
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +67 -87
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +17 -48
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +95 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +25 -52
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +23 -62
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +12 -41
- package/docs/reference/cli.mdx +47 -30
- package/docs/reference/frontmatter.mdx +7 -5
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -8
- package/src/astro/integration.ts +26 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +6 -1
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +76 -30
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +129 -62
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/commands/dev.ts +11 -2
- package/src/cli/commands/doctor.ts +10 -1
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +21 -1
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +12 -1
- package/src/cli/dev-lock.ts +92 -0
- package/src/cli/log.ts +11 -0
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +8 -0
- package/src/components/Icon.astro +13 -10
- package/src/components/content/ApiField.astro +75 -0
- package/src/components/content/ParamField.astro +39 -0
- package/src/components/content/RequestField.astro +23 -0
- package/src/components/content/ResponseField.astro +23 -0
- package/src/components/content/Step.astro +1 -1
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/islands/ask-ai.tsx +14 -14
- package/src/components/layout/Breadcrumbs.astro +7 -2
- package/src/components/layout/NavTree.astro +24 -8
- package/src/components/layout/RootLayout.astro +56 -34
- package/src/components/layout/Search.astro +1 -1
- package/src/components/openapi/ApiOverview.astro +84 -0
- package/src/components/openapi/MethodBadge.astro +28 -0
- package/src/components/openapi/Operation.astro +140 -0
- package/src/components/openapi/ParametersTable.astro +97 -0
- package/src/components/openapi/RequestBody.astro +58 -0
- package/src/components/openapi/RequestPanel.astro +169 -0
- package/src/components/openapi/Responses.astro +91 -0
- package/src/components/openapi/SchemaProperty.astro +118 -0
- package/src/components/openapi/SchemaTable.astro +86 -0
- package/src/components/openapi/helpers.ts +238 -0
- package/src/components/openapi/panel.ts +59 -0
- package/src/components/openapi/snippets.ts +201 -0
- package/src/components/props.ts +3 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +6 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +6 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/links.ts +60 -19
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +54 -6
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +6 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +7 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +153 -1
- package/src/migrate/mintlify/content.ts +8 -2
- package/src/migrate/mintlify/index.ts +111 -46
- package/src/migrate/shared.ts +12 -27
- package/src/og/card.ts +14 -2
- package/src/openapi/model.ts +174 -0
- package/src/openapi/parse.ts +48 -0
- package/src/openapi/references.ts +164 -0
- package/src/openapi/render-mdx.ts +76 -0
- package/src/openapi/scalar.ts +15 -103
- package/src/openapi/source.ts +140 -0
- package/src/registry/eject.ts +28 -5
- package/src/registry/registry.ts +6 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/chrome-icons.ts +22 -0
- package/src/theme/icons.ts +151 -161
- package/src/theme/palette.ts +26 -7
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Document,
|
|
3
|
+
OperationObject,
|
|
4
|
+
PathItemObject,
|
|
5
|
+
} from "@scalar/openapi-types/3.1";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Blume's own OpenAPI model. Specs are parsed and upgraded to 3.1 (see
|
|
9
|
+
* `parse.ts`) with internal `$ref`s left intact — the document stays
|
|
10
|
+
* JSON-serializable (a fully dereferenced graph can be circular), and the schema
|
|
11
|
+
* components resolve refs against `document.components.schemas` at render time.
|
|
12
|
+
* Each operation is flattened into an {@link ApiOperationRef} with a real,
|
|
13
|
+
* per-operation route so it becomes a first-class Blume page.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A normalized OpenAPI 3.1 document, internal `$ref`s intact. */
|
|
17
|
+
export type ApiDocument = Document;
|
|
18
|
+
|
|
19
|
+
const NON_SLUG = /[^a-z0-9]+/gu;
|
|
20
|
+
const SLUG_EDGES = /^-+|-+$/gu;
|
|
21
|
+
|
|
22
|
+
/** Lowercase, URL-safe slug: `Add a Pet!` -> `add-a-pet`. */
|
|
23
|
+
export const slugify = (text: string): string =>
|
|
24
|
+
text.toLowerCase().replace(NON_SLUG, "-").replace(SLUG_EDGES, "");
|
|
25
|
+
|
|
26
|
+
/** The HTTP methods an OpenAPI path item may declare, in spec order. */
|
|
27
|
+
export const HTTP_METHODS = [
|
|
28
|
+
"get",
|
|
29
|
+
"put",
|
|
30
|
+
"post",
|
|
31
|
+
"delete",
|
|
32
|
+
"options",
|
|
33
|
+
"head",
|
|
34
|
+
"patch",
|
|
35
|
+
"trace",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
39
|
+
|
|
40
|
+
/** Group used for operations that declare no tag. */
|
|
41
|
+
const UNTAGGED = "Operations";
|
|
42
|
+
|
|
43
|
+
/** A stable, URL-safe key for an operation: its `operationId`, else method+path. */
|
|
44
|
+
export const operationKey = (
|
|
45
|
+
method: string,
|
|
46
|
+
path: string,
|
|
47
|
+
operationId?: string
|
|
48
|
+
): string => {
|
|
49
|
+
const fromId = operationId ? slugify(operationId) : "";
|
|
50
|
+
return fromId || slugify(`${method}-${path}`);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** One operation, flattened out of the paths object and mapped to a route. */
|
|
54
|
+
export interface ApiOperationRef {
|
|
55
|
+
/** Stable key, unique within a spec; matches the MDX `<Operation id>`. */
|
|
56
|
+
key: string;
|
|
57
|
+
method: HttpMethod;
|
|
58
|
+
/** Templated path, e.g. `/pets/{id}`. */
|
|
59
|
+
path: string;
|
|
60
|
+
/** Full site route for this operation's page, e.g. `/reference/pet/add-pet`. */
|
|
61
|
+
route: string;
|
|
62
|
+
/** Display tag name (first tag, or `Operations` when untagged). */
|
|
63
|
+
tag: string;
|
|
64
|
+
tagSlug: string;
|
|
65
|
+
summary: string;
|
|
66
|
+
description: string;
|
|
67
|
+
operationId?: string;
|
|
68
|
+
deprecated: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A tag/section, in first-seen order. */
|
|
72
|
+
export interface ApiTagRef {
|
|
73
|
+
slug: string;
|
|
74
|
+
name: string;
|
|
75
|
+
description: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Everything the runtime needs for one spec, serialized into `blume:openapi`. */
|
|
79
|
+
export interface ApiSpecData {
|
|
80
|
+
/** Unique token used as the `<Operation source>` and the data-module key. */
|
|
81
|
+
slug: string;
|
|
82
|
+
/** Base route the spec's operations hang off, e.g. `/reference`. */
|
|
83
|
+
route: string;
|
|
84
|
+
label: string;
|
|
85
|
+
title: string;
|
|
86
|
+
version: string;
|
|
87
|
+
description: string;
|
|
88
|
+
document: ApiDocument;
|
|
89
|
+
/** Operations keyed by {@link ApiOperationRef.key}. */
|
|
90
|
+
operations: Record<string, ApiOperationRef>;
|
|
91
|
+
tags: ApiTagRef[];
|
|
92
|
+
/** Code-sample languages to render per operation. */
|
|
93
|
+
codeSamples: string[];
|
|
94
|
+
/** Whether nested schema rows start expanded. */
|
|
95
|
+
expandSchemas: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The generated `blume:openapi` module: specs keyed by {@link ApiSpecData.slug}. */
|
|
99
|
+
export type OpenApiData = Record<string, ApiSpecData>;
|
|
100
|
+
|
|
101
|
+
const isOperation = (value: unknown): value is OperationObject =>
|
|
102
|
+
typeof value === "object" && value !== null;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Flatten a 3.1 document into a route-mapped operation list and its ordered
|
|
106
|
+
* tags. Operations inherit the first tag they declare; keys are de-duplicated so
|
|
107
|
+
* a repeated `operationId` still yields distinct routes.
|
|
108
|
+
*/
|
|
109
|
+
export const extractOperations = (
|
|
110
|
+
document: ApiDocument,
|
|
111
|
+
baseRoute: string
|
|
112
|
+
): { operations: ApiOperationRef[]; tags: ApiTagRef[] } => {
|
|
113
|
+
const operations: ApiOperationRef[] = [];
|
|
114
|
+
const tagOrder: string[] = [];
|
|
115
|
+
const tagMeta = new Map(
|
|
116
|
+
(document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""])
|
|
117
|
+
);
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
|
|
120
|
+
for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
|
|
121
|
+
const item = rawItem as PathItemObject | undefined;
|
|
122
|
+
if (!item || "$ref" in item) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const method of HTTP_METHODS) {
|
|
126
|
+
const operation = item[method];
|
|
127
|
+
if (!isOperation(operation)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const tag = operation.tags?.[0] ?? UNTAGGED;
|
|
131
|
+
const tagSlug = slugify(tag) || "operations";
|
|
132
|
+
if (!tagOrder.includes(tag)) {
|
|
133
|
+
tagOrder.push(tag);
|
|
134
|
+
}
|
|
135
|
+
let key = operationKey(method, path, operation.operationId);
|
|
136
|
+
while (seen.has(key)) {
|
|
137
|
+
key = `${key}-${method}`;
|
|
138
|
+
}
|
|
139
|
+
seen.add(key);
|
|
140
|
+
operations.push({
|
|
141
|
+
deprecated: operation.deprecated ?? false,
|
|
142
|
+
description: operation.description ?? "",
|
|
143
|
+
key,
|
|
144
|
+
method,
|
|
145
|
+
operationId: operation.operationId,
|
|
146
|
+
path,
|
|
147
|
+
route: `${baseRoute}/${tagSlug}/${key}`,
|
|
148
|
+
summary: operation.summary ?? "",
|
|
149
|
+
tag,
|
|
150
|
+
tagSlug,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const tags: ApiTagRef[] = tagOrder.map((name) => ({
|
|
156
|
+
description: tagMeta.get(name) ?? "",
|
|
157
|
+
name,
|
|
158
|
+
slug: slugify(name) || "operations",
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
return { operations, tags };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Resolve the operation object for a ref out of its document. */
|
|
165
|
+
export const operationObject = (
|
|
166
|
+
spec: ApiSpecData,
|
|
167
|
+
ref: ApiOperationRef
|
|
168
|
+
): OperationObject | undefined => {
|
|
169
|
+
const item = (spec.document.paths?.[ref.path] ?? undefined) as
|
|
170
|
+
| PathItemObject
|
|
171
|
+
| undefined;
|
|
172
|
+
const operation = item?.[ref.method];
|
|
173
|
+
return isOperation(operation) ? operation : undefined;
|
|
174
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { normalize, upgrade } from "@scalar/openapi-parser";
|
|
4
|
+
import { isAbsolute, join } from "pathe";
|
|
5
|
+
|
|
6
|
+
import type { ApiDocument } from "./model.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Spec loading and normalization. Blume reuses Scalar's parser
|
|
10
|
+
* (`@scalar/openapi-parser`) to read a spec (YAML or JSON), then upgrade Swagger
|
|
11
|
+
* 2.0 / OpenAPI 3.0 documents to 3.1 so the renderer only handles one shape.
|
|
12
|
+
* Internal `$ref`s are deliberately left in place (see `model.ts`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const URL_SPEC = /^https?:\/\//u;
|
|
16
|
+
|
|
17
|
+
export interface ParsedSpec {
|
|
18
|
+
document: ApiDocument;
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Read a spec's raw text from an `http(s)` URL or a local (project-relative) path. */
|
|
23
|
+
const readSpecText = async (spec: string, root: string): Promise<string> => {
|
|
24
|
+
if (URL_SPEC.test(spec)) {
|
|
25
|
+
const response = await fetch(spec);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`${spec} -> ${response.status} ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
return await response.text();
|
|
30
|
+
}
|
|
31
|
+
const absolute = isAbsolute(spec) ? spec : join(root, spec);
|
|
32
|
+
return await readFile(absolute, "utf-8");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 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 diagnostic rather than a
|
|
38
|
+
* hard failure so a broken spec doesn't take down the whole build.
|
|
39
|
+
*/
|
|
40
|
+
export const parseSpec = async (
|
|
41
|
+
spec: string,
|
|
42
|
+
root: string
|
|
43
|
+
): Promise<ParsedSpec> => {
|
|
44
|
+
const text = await readSpecText(spec, root);
|
|
45
|
+
const normalized = normalize(text);
|
|
46
|
+
const { specification } = upgrade(normalized);
|
|
47
|
+
return { document: specification as ApiDocument, warnings: [] };
|
|
48
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
2
|
+
import type { NavTab } from "../core/types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure resolution of the configured API reference blocks into concrete routes,
|
|
6
|
+
* labels, and a renderer choice — no file IO, so the content source, the nav
|
|
7
|
+
* tabs, the Scalar page generator, and the `blume:openapi` data module all share
|
|
8
|
+
* one source of truth. Kept free of any Astro/template imports so `core` can
|
|
9
|
+
* depend on it without a cycle.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ReferenceKind = "openapi" | "asyncapi";
|
|
13
|
+
|
|
14
|
+
/** Who renders a reference: Blume's own UI, or the embedded Scalar SPA. */
|
|
15
|
+
export type ReferenceRenderer = "blume" | "scalar";
|
|
16
|
+
|
|
17
|
+
/** Per-block display options for the Blume renderer. */
|
|
18
|
+
export interface ReferenceDisplay {
|
|
19
|
+
/** Code-sample languages shown per operation. */
|
|
20
|
+
codeSamples: string[];
|
|
21
|
+
/** Whether nested schema rows start expanded. */
|
|
22
|
+
expandSchemas: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A spec source resolved to a concrete route, label, and renderer. */
|
|
26
|
+
export interface ReferenceSource {
|
|
27
|
+
kind: ReferenceKind;
|
|
28
|
+
renderer: ReferenceRenderer;
|
|
29
|
+
/** Unique token derived from the route; the `<Operation source>` / data key. */
|
|
30
|
+
slug: string;
|
|
31
|
+
/** Normalized route the reference mounts at, e.g. `/reference`. */
|
|
32
|
+
route: string;
|
|
33
|
+
label: string;
|
|
34
|
+
/** Local path or `http(s)` URL, verbatim from config. */
|
|
35
|
+
spec: string;
|
|
36
|
+
/** Per-block Scalar theme name override, if any (Scalar renderer only). */
|
|
37
|
+
theme?: string;
|
|
38
|
+
/** Display options carried through to the Blume renderer. */
|
|
39
|
+
display: ReferenceDisplay;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const NON_SLUG = /[^a-z0-9]+/gu;
|
|
43
|
+
const SLUG_EDGES = /^-+|-+$/gu;
|
|
44
|
+
const ROUTE_EDGES = /^\/+|\/+$/gu;
|
|
45
|
+
const TRAILING_SLASH = /\/+$/u;
|
|
46
|
+
|
|
47
|
+
export const slugify = (text: string): string =>
|
|
48
|
+
text.toLowerCase().replace(NON_SLUG, "-").replace(SLUG_EDGES, "");
|
|
49
|
+
|
|
50
|
+
/** Normalize a configured route to a single leading slash, no trailing slash. */
|
|
51
|
+
export const normalizeRoute = (route: string): string => {
|
|
52
|
+
const trimmed = route.trim();
|
|
53
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
54
|
+
const noTrailing = withSlash.replace(TRAILING_SLASH, "");
|
|
55
|
+
return noTrailing === "" ? "/" : noTrailing;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** A stable per-reference token from its route: `/api/events` -> `api-events`. */
|
|
59
|
+
const routeSlug = (route: string): string =>
|
|
60
|
+
slugify(route.replace(ROUTE_EDGES, "")) || "reference";
|
|
61
|
+
|
|
62
|
+
type Block = ResolvedConfig["openapi"] | ResolvedConfig["asyncapi"];
|
|
63
|
+
|
|
64
|
+
/** A spec is a single source (`spec` shorthand prepended to any `sources`). */
|
|
65
|
+
const sourcesOf = (
|
|
66
|
+
block: Block
|
|
67
|
+
): { label?: string; route?: string; spec: string }[] => {
|
|
68
|
+
const sources = [...block.sources];
|
|
69
|
+
if (block.spec) {
|
|
70
|
+
sources.unshift({ spec: block.spec });
|
|
71
|
+
}
|
|
72
|
+
return sources;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const referencesFor = (
|
|
76
|
+
kind: ReferenceKind,
|
|
77
|
+
block: Block,
|
|
78
|
+
defaultLabel: string,
|
|
79
|
+
renderer: ReferenceRenderer,
|
|
80
|
+
display: ReferenceDisplay
|
|
81
|
+
): ReferenceSource[] => {
|
|
82
|
+
if (!block.enabled) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const sources = sourcesOf(block);
|
|
86
|
+
const base = normalizeRoute(block.route);
|
|
87
|
+
|
|
88
|
+
return sources.map((source, index) => {
|
|
89
|
+
const label =
|
|
90
|
+
source.label ??
|
|
91
|
+
(sources.length > 1 ? `${defaultLabel} ${index + 1}` : defaultLabel);
|
|
92
|
+
|
|
93
|
+
let route: string;
|
|
94
|
+
if (source.route) {
|
|
95
|
+
route = normalizeRoute(source.route);
|
|
96
|
+
} else if (sources.length === 1) {
|
|
97
|
+
route = base;
|
|
98
|
+
} else {
|
|
99
|
+
const suffix = source.label ? slugify(source.label) : "";
|
|
100
|
+
route = normalizeRoute(`${base}/${suffix || index + 1}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
display,
|
|
105
|
+
kind,
|
|
106
|
+
label,
|
|
107
|
+
renderer,
|
|
108
|
+
route,
|
|
109
|
+
slug: routeSlug(route),
|
|
110
|
+
spec: source.spec,
|
|
111
|
+
theme: block.theme,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const NO_DISPLAY: ReferenceDisplay = { codeSamples: [], expandSchemas: false };
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve every enabled reference. OpenAPI honors its `renderer` (Blume's own UI
|
|
120
|
+
* by default); AsyncAPI is always rendered by Scalar for now.
|
|
121
|
+
*/
|
|
122
|
+
export const resolveReferences = (
|
|
123
|
+
config: ResolvedConfig
|
|
124
|
+
): ReferenceSource[] => [
|
|
125
|
+
...referencesFor(
|
|
126
|
+
"openapi",
|
|
127
|
+
config.openapi,
|
|
128
|
+
"API Reference",
|
|
129
|
+
config.openapi.renderer,
|
|
130
|
+
{
|
|
131
|
+
codeSamples: config.openapi.codeSamples,
|
|
132
|
+
expandSchemas: config.openapi.expandSchemas,
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
...referencesFor("asyncapi", config.asyncapi, "Events", "scalar", NO_DISPLAY),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
/** Nav tabs (header links) for every reference, regardless of renderer. */
|
|
139
|
+
export const referenceTabs = (config: ResolvedConfig): NavTab[] =>
|
|
140
|
+
resolveReferences(config).map((ref) => ({
|
|
141
|
+
label: ref.label,
|
|
142
|
+
path: ref.route,
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
/** Blume-rendered OpenAPI references, deduped by route (first wins). */
|
|
146
|
+
export const blumeReferences = (config: ResolvedConfig): ReferenceSource[] => {
|
|
147
|
+
const seen = new Set<string>();
|
|
148
|
+
const result: ReferenceSource[] = [];
|
|
149
|
+
for (const ref of resolveReferences(config)) {
|
|
150
|
+
if (ref.kind !== "openapi" || ref.renderer !== "blume") {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (seen.has(ref.route)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
seen.add(ref.route);
|
|
157
|
+
result.push(ref);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Whether any reference is Scalar-rendered (gates the `@scalar/astro` dep + pages). */
|
|
163
|
+
export const hasScalarReferences = (config: ResolvedConfig): boolean =>
|
|
164
|
+
resolveReferences(config).some((ref) => ref.renderer === "scalar");
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ApiOperationRef, ApiSpecData } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lower a parsed spec into MDX for the staged content source. Each operation and
|
|
5
|
+
* the spec overview become a thin MDX page: the frontmatter carries the
|
|
6
|
+
* searchable `title` (so operations flow into Blume's search, OG, and llms.txt),
|
|
7
|
+
* the operation/overview **description is emitted as markdown in the body** so it
|
|
8
|
+
* renders parsed (links, formatting) and is indexed, and the structured UI is
|
|
9
|
+
* deferred to a Blume-owned component (`<Operation>` / `<ApiOverview>`). The
|
|
10
|
+
* catch-all renders the frontmatter title as the page `<h1>`, so the components
|
|
11
|
+
* omit their own top heading.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Neutralize the few characters MDX treats specially (`{` expressions, `<` JSX)
|
|
15
|
+
// so an arbitrary spec description can be embedded in the body verbatim without
|
|
16
|
+
// breaking compilation. They render as their literal selves.
|
|
17
|
+
const MDX_UNSAFE = /[<>{}]/gu;
|
|
18
|
+
const ENTITIES: Record<string, string> = {
|
|
19
|
+
"<": "<",
|
|
20
|
+
">": ">",
|
|
21
|
+
"{": "{",
|
|
22
|
+
"}": "}",
|
|
23
|
+
};
|
|
24
|
+
const mdxSafe = (text: string): string =>
|
|
25
|
+
text.replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char);
|
|
26
|
+
|
|
27
|
+
/** Frontmatter + body for one operation or overview page. */
|
|
28
|
+
export interface RenderedPage {
|
|
29
|
+
data: Record<string, unknown>;
|
|
30
|
+
body: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Prepend a markdown description (if any) above a component invocation. */
|
|
34
|
+
const withDescription = (description: string, component: string): string =>
|
|
35
|
+
description.trim()
|
|
36
|
+
? `${mdxSafe(description.trim())}\n\n${component}`
|
|
37
|
+
: component;
|
|
38
|
+
|
|
39
|
+
export const operationMdx = (
|
|
40
|
+
spec: ApiSpecData,
|
|
41
|
+
operation: ApiOperationRef
|
|
42
|
+
): RenderedPage => {
|
|
43
|
+
const method = operation.method.toUpperCase();
|
|
44
|
+
const title = operation.summary || `${method} ${operation.path}`;
|
|
45
|
+
// Skip the body description when it only repeats the summary (the `<h1>`) —
|
|
46
|
+
// common in specs that set summary and description to the same string.
|
|
47
|
+
const description =
|
|
48
|
+
operation.description.trim() === operation.summary.trim()
|
|
49
|
+
? ""
|
|
50
|
+
: operation.description;
|
|
51
|
+
return {
|
|
52
|
+
body: withDescription(
|
|
53
|
+
description,
|
|
54
|
+
`<Operation source="${spec.slug}" id="${operation.key}" />`
|
|
55
|
+
),
|
|
56
|
+
data: {
|
|
57
|
+
...(operation.deprecated ? { deprecated: true } : {}),
|
|
58
|
+
search: { tags: [operation.tag, method] },
|
|
59
|
+
sidebar: { badge: method, label: operation.summary || operation.path },
|
|
60
|
+
title,
|
|
61
|
+
// Signals the two-column API layout (request panel instead of the TOC).
|
|
62
|
+
type: "openapi-operation",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const overviewMdx = (spec: ApiSpecData): RenderedPage => ({
|
|
68
|
+
body: withDescription(
|
|
69
|
+
spec.description,
|
|
70
|
+
`<ApiOverview source="${spec.slug}" />`
|
|
71
|
+
),
|
|
72
|
+
data: {
|
|
73
|
+
sidebar: { label: "Overview" },
|
|
74
|
+
title: spec.title || spec.label,
|
|
75
|
+
},
|
|
76
|
+
});
|
package/src/openapi/scalar.ts
CHANGED
|
@@ -4,32 +4,19 @@ 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 type { NavTab } from "../core/types.ts";
|
|
8
7
|
import { resolveAccent, resolveRadius } from "../theme/palette.ts";
|
|
8
|
+
import { resolveReferences } from "./references.ts";
|
|
9
|
+
import type { ReferenceSource } from "./references.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The Scalar renderer: an escape hatch (`openapi.renderer: "scalar"`) and the
|
|
13
|
+
* path AsyncAPI still uses. Each Scalar-rendered spec becomes one self-contained
|
|
14
|
+
* `@scalar/astro` page loaded client-side from Scalar's CDN. Blume's own OpenAPI
|
|
15
|
+
* renderer (the default) lives in `source.ts` / the `components/openapi` set and
|
|
16
|
+
* does not pass through here.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** A spec source resolved to a concrete route and nav label. */
|
|
21
|
-
export interface ReferenceSource {
|
|
22
|
-
kind: ReferenceKind;
|
|
23
|
-
/** Normalized route the reference mounts at, e.g. `/reference`. */
|
|
24
|
-
route: string;
|
|
25
|
-
label: string;
|
|
26
|
-
/** Local path or `http(s)` URL, verbatim from config. */
|
|
27
|
-
spec: string;
|
|
28
|
-
/** Per-block Scalar theme name override, if any. */
|
|
29
|
-
theme?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A generated reference page, ready to write under `src/pages`. */
|
|
19
|
+
/** A generated Scalar reference page, ready to write under `src/pages`. */
|
|
33
20
|
export interface ReferenceFile {
|
|
34
21
|
/** Path relative to `src/pages`, e.g. `reference.astro`, `api/events.astro`. */
|
|
35
22
|
pagePath: string;
|
|
@@ -37,21 +24,7 @@ export interface ReferenceFile {
|
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
const URL_SPEC = /^https?:\/\//u;
|
|
40
|
-
const NON_SLUG = /[^a-z0-9]+/gu;
|
|
41
|
-
const SLUG_EDGES = /^-+|-+$/gu;
|
|
42
27
|
const ROUTE_EDGES = /^\/+|\/+$/gu;
|
|
43
|
-
const TRAILING_SLASH = /\/+$/u;
|
|
44
|
-
|
|
45
|
-
const slugify = (text: string): string =>
|
|
46
|
-
text.toLowerCase().replace(NON_SLUG, "-").replace(SLUG_EDGES, "");
|
|
47
|
-
|
|
48
|
-
/** Normalize a configured route to a single leading slash, no trailing slash. */
|
|
49
|
-
const normalizeRoute = (route: string): string => {
|
|
50
|
-
const trimmed = route.trim();
|
|
51
|
-
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
52
|
-
const noTrailing = withSlash.replace(TRAILING_SLASH, "");
|
|
53
|
-
return noTrailing === "" ? "/" : noTrailing;
|
|
54
|
-
};
|
|
55
28
|
|
|
56
29
|
/** The `src/pages`-relative file path for a reference route. */
|
|
57
30
|
const referencePagePath = (route: string): string => {
|
|
@@ -59,71 +32,6 @@ const referencePagePath = (route: string): string => {
|
|
|
59
32
|
return `${segments === "" ? "index" : segments}.astro`;
|
|
60
33
|
};
|
|
61
34
|
|
|
62
|
-
/** A spec is a single source (`spec` shorthand prepended to any `sources`). */
|
|
63
|
-
type Block = ResolvedConfig["openapi"] | ResolvedConfig["asyncapi"];
|
|
64
|
-
|
|
65
|
-
const sourcesOf = (
|
|
66
|
-
block: Block
|
|
67
|
-
): { label?: string; route?: string; spec: string }[] => {
|
|
68
|
-
const sources = [...block.sources];
|
|
69
|
-
if (block.spec) {
|
|
70
|
-
sources.unshift({ spec: block.spec });
|
|
71
|
-
}
|
|
72
|
-
return sources;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const referencesFor = (
|
|
76
|
-
kind: ReferenceKind,
|
|
77
|
-
block: Block,
|
|
78
|
-
defaultLabel: string
|
|
79
|
-
): ReferenceSource[] => {
|
|
80
|
-
if (!block.enabled) {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
const sources = sourcesOf(block);
|
|
84
|
-
const base = normalizeRoute(block.route);
|
|
85
|
-
|
|
86
|
-
return sources.map((source, index) => {
|
|
87
|
-
const label =
|
|
88
|
-
source.label ??
|
|
89
|
-
(sources.length > 1 ? `${defaultLabel} ${index + 1}` : defaultLabel);
|
|
90
|
-
|
|
91
|
-
let route: string;
|
|
92
|
-
if (source.route) {
|
|
93
|
-
route = normalizeRoute(source.route);
|
|
94
|
-
} else if (sources.length === 1) {
|
|
95
|
-
route = base;
|
|
96
|
-
} else {
|
|
97
|
-
const suffix = source.label ? slugify(source.label) : "";
|
|
98
|
-
route = normalizeRoute(`${base}/${suffix || index + 1}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { kind, label, route, spec: source.spec, theme: block.theme };
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Resolve every enabled reference source into its route and label. Pure (no file
|
|
107
|
-
* IO), so the nav and the page generator stay in sync from one source of truth.
|
|
108
|
-
*/
|
|
109
|
-
export const resolveReferences = (
|
|
110
|
-
config: ResolvedConfig
|
|
111
|
-
): ReferenceSource[] => [
|
|
112
|
-
...referencesFor("openapi", config.openapi, "API Reference"),
|
|
113
|
-
...referencesFor("asyncapi", config.asyncapi, "Events"),
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
/** Nav tabs (header links) for the configured references. */
|
|
117
|
-
export const referenceTabs = (config: ResolvedConfig): NavTab[] =>
|
|
118
|
-
resolveReferences(config).map((ref) => ({
|
|
119
|
-
label: ref.label,
|
|
120
|
-
path: ref.route,
|
|
121
|
-
}));
|
|
122
|
-
|
|
123
|
-
/** Whether any reference block is enabled (gates dependency + page wiring). */
|
|
124
|
-
export const hasReferences = (config: ResolvedConfig): boolean =>
|
|
125
|
-
config.openapi.enabled || config.asyncapi.enabled;
|
|
126
|
-
|
|
127
35
|
const darkModeConfig = (
|
|
128
36
|
mode: ResolvedConfig["theme"]["mode"]
|
|
129
37
|
): Record<string, boolean> => {
|
|
@@ -180,9 +88,10 @@ const specConfiguration = async (
|
|
|
180
88
|
};
|
|
181
89
|
|
|
182
90
|
/**
|
|
183
|
-
* Build the Scalar reference page(s) for the project.
|
|
184
|
-
*
|
|
185
|
-
*
|
|
91
|
+
* Build the Scalar reference page(s) for the project. Only Scalar-rendered
|
|
92
|
+
* references are emitted here (Blume-rendered OpenAPI is staged content). Reads
|
|
93
|
+
* local specs, maps the theme, and skips routes that collide with a content page
|
|
94
|
+
* or another source. Returns the files to write under `src/pages` plus warnings.
|
|
186
95
|
*/
|
|
187
96
|
export const buildReferenceFiles = async (options: {
|
|
188
97
|
config: ResolvedConfig;
|
|
@@ -196,6 +105,9 @@ export const buildReferenceFiles = async (options: {
|
|
|
196
105
|
const seen = new Set<string>();
|
|
197
106
|
const accepted: ReferenceSource[] = [];
|
|
198
107
|
for (const ref of resolveReferences(config)) {
|
|
108
|
+
if (ref.renderer !== "scalar") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
199
111
|
if (seen.has(ref.route)) {
|
|
200
112
|
warnings.push(
|
|
201
113
|
`Two API reference sources resolve to ${ref.route}; keeping the first.`
|