blume 0.4.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 +1137 -722
- package/dist/cli/index.js.map +28 -23
- 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 +154 -15
- package/dist/types/core/types.d.ts +7 -0
- package/docs/advanced/api-reference.mdx +33 -23
- package/docs/advanced/bridge.mdx +74 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +119 -0
- package/docs/configuration/index.mdx +1 -1
- package/docs/content/components.mdx +55 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/syntax.mdx +2 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +29 -1
- package/docs/reference/frontmatter.mdx +5 -0
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -8
- package/src/astro/templates.ts +28 -4
- package/src/cli/commands/build.ts +107 -63
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/dev-lock.ts +13 -5
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +6 -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/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/core/builtin-tags.ts +5 -0
- package/src/core/data.ts +2 -0
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +47 -6
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/types.ts +7 -0
- package/src/migrate/mintlify/config.ts +153 -1
- package/src/migrate/mintlify/content.ts +8 -2
- package/src/migrate/mintlify/index.ts +58 -1
- 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 +15 -2
- package/src/theme/chrome-icons.ts +22 -0
- package/src/theme/icons.ts +151 -161
|
@@ -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.`
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import matter from "../core/frontmatter.ts";
|
|
2
|
+
import { hashText } from "../core/sources/cache.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ContentSource,
|
|
5
|
+
SourceContext,
|
|
6
|
+
SourceEntry,
|
|
7
|
+
SourceLoadResult,
|
|
8
|
+
} from "../core/sources/types.ts";
|
|
9
|
+
import type { Diagnostic } from "../core/types.ts";
|
|
10
|
+
import { extractOperations } from "./model.ts";
|
|
11
|
+
import type { ApiOperationRef, ApiSpecData, OpenApiData } from "./model.ts";
|
|
12
|
+
import { parseSpec } from "./parse.ts";
|
|
13
|
+
import type { ReferenceSource } from "./references.ts";
|
|
14
|
+
import { operationMdx, overviewMdx } from "./render-mdx.ts";
|
|
15
|
+
import type { RenderedPage } from "./render-mdx.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The staged content source behind Blume's own OpenAPI renderer. Each configured
|
|
19
|
+
* spec is parsed once here, then lowered into one MDX page per operation plus an
|
|
20
|
+
* overview page — so operations become first-class Blume pages (real routes,
|
|
21
|
+
* sidebar, search, i18n, OG) and the parsed documents are handed to the
|
|
22
|
+
* generated `blume:openapi` module for the UI components to render.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A content source that also exposes the specs it parsed during `load()`. */
|
|
26
|
+
export interface OpenApiContentSource extends ContentSource {
|
|
27
|
+
readonly kind: "openapi-source";
|
|
28
|
+
/** Parsed spec data, populated by `load()`; `{}` before the first load. */
|
|
29
|
+
openApiData: () => OpenApiData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Narrow a content source to the OpenAPI source (to read its parsed specs). */
|
|
33
|
+
export const isOpenApiSource = (
|
|
34
|
+
source: ContentSource
|
|
35
|
+
): source is OpenApiContentSource =>
|
|
36
|
+
(source as Partial<OpenApiContentSource>).kind === "openapi-source";
|
|
37
|
+
|
|
38
|
+
/** Route (`/reference/pet/add-pet`) to a staged content ref, without extension. */
|
|
39
|
+
const routeToRef = (route: string): string => route.replace(/^\/+/u, "");
|
|
40
|
+
|
|
41
|
+
const toEntry = (rendered: RenderedPage, ref: string): SourceEntry => {
|
|
42
|
+
const raw = matter.stringify(`${rendered.body}\n`, rendered.data);
|
|
43
|
+
return {
|
|
44
|
+
body: { format: "mdx", text: rendered.body },
|
|
45
|
+
data: rendered.data,
|
|
46
|
+
hash: hashText(raw),
|
|
47
|
+
raw,
|
|
48
|
+
ref,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** All staged entries for one spec: operations first, overview last. */
|
|
53
|
+
const specEntries = (
|
|
54
|
+
spec: ApiSpecData,
|
|
55
|
+
operations: ApiOperationRef[]
|
|
56
|
+
): SourceEntry[] => {
|
|
57
|
+
const entries = operations.map((operation) =>
|
|
58
|
+
toEntry(operationMdx(spec, operation), `${routeToRef(operation.route)}.mdx`)
|
|
59
|
+
);
|
|
60
|
+
// Overview last so an operation sets the section's routePath before the index
|
|
61
|
+
// page is inserted (the group's routePath is derived from its first child).
|
|
62
|
+
entries.push(
|
|
63
|
+
toEntry(overviewMdx(spec), `${routeToRef(spec.route)}/index.mdx`)
|
|
64
|
+
);
|
|
65
|
+
return entries;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
interface LoadedSpec {
|
|
69
|
+
slug: string;
|
|
70
|
+
spec: ApiSpecData;
|
|
71
|
+
entries: SourceEntry[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const openApiSource = (
|
|
75
|
+
references: ReferenceSource[],
|
|
76
|
+
ctx: SourceContext
|
|
77
|
+
): OpenApiContentSource => {
|
|
78
|
+
let parsed: OpenApiData = {};
|
|
79
|
+
|
|
80
|
+
const loadReference = async (
|
|
81
|
+
reference: ReferenceSource
|
|
82
|
+
): Promise<LoadedSpec | Diagnostic> => {
|
|
83
|
+
try {
|
|
84
|
+
const { document } = await parseSpec(reference.spec, ctx.projectRoot);
|
|
85
|
+
const { operations, tags } = extractOperations(document, reference.route);
|
|
86
|
+
const info = document.info ?? { title: reference.label, version: "" };
|
|
87
|
+
const spec: ApiSpecData = {
|
|
88
|
+
codeSamples: reference.display.codeSamples,
|
|
89
|
+
description: info.description ?? "",
|
|
90
|
+
document,
|
|
91
|
+
expandSchemas: reference.display.expandSchemas,
|
|
92
|
+
label: reference.label,
|
|
93
|
+
operations: Object.fromEntries(
|
|
94
|
+
operations.map((operation) => [operation.key, operation])
|
|
95
|
+
),
|
|
96
|
+
route: reference.route,
|
|
97
|
+
slug: reference.slug,
|
|
98
|
+
tags,
|
|
99
|
+
title: info.title ?? reference.label,
|
|
100
|
+
version: info.version ?? "",
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
entries: specEntries(spec, operations),
|
|
104
|
+
slug: reference.slug,
|
|
105
|
+
spec,
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
code: "BLUME_OPENAPI_UNAVAILABLE",
|
|
110
|
+
message: `Could not load OpenAPI spec "${reference.spec}" for ${reference.route} (${(error as Error).message}); its reference pages were skipped.`,
|
|
111
|
+
severity: "warning",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const load = async (): Promise<SourceLoadResult> => {
|
|
117
|
+
const results = await Promise.all(references.map(loadReference));
|
|
118
|
+
const entries: SourceEntry[] = [];
|
|
119
|
+
const diagnostics: Diagnostic[] = [];
|
|
120
|
+
const data: OpenApiData = {};
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
if ("severity" in result) {
|
|
123
|
+
diagnostics.push(result);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
data[result.slug] = result.spec;
|
|
127
|
+
entries.push(...result.entries);
|
|
128
|
+
}
|
|
129
|
+
parsed = data;
|
|
130
|
+
return { diagnostics, entries };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
kind: "openapi-source",
|
|
135
|
+
load,
|
|
136
|
+
name: "openapi",
|
|
137
|
+
openApiData: () => parsed,
|
|
138
|
+
staged: true,
|
|
139
|
+
};
|
|
140
|
+
};
|
package/src/registry/eject.ts
CHANGED
|
@@ -39,7 +39,9 @@ import { scanProject } from "../core/project-graph.ts";
|
|
|
39
39
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
40
40
|
import type { ProjectContext } from "../core/types.ts";
|
|
41
41
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
42
|
-
import {
|
|
42
|
+
import { hasScalarReferences } from "../openapi/references.ts";
|
|
43
|
+
import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
44
|
+
import { isOpenApiSource } from "../openapi/source.ts";
|
|
43
45
|
import { buildSearchDocuments } from "../search/documents.ts";
|
|
44
46
|
import { servesStaticIndex } from "../search/providers.ts";
|
|
45
47
|
import { tailwindEntryTemplate } from "../theme/entry.ts";
|
|
@@ -48,6 +50,12 @@ import { twoslashCss } from "../theme/twoslash.ts";
|
|
|
48
50
|
|
|
49
51
|
const POSIX = (path: string): string => path.split("\\").join("/");
|
|
50
52
|
|
|
53
|
+
/** The `blume:openapi` payload for the ejected app (`{}` when none). */
|
|
54
|
+
const ejectOpenApiData = (project: BlumeProject): unknown => {
|
|
55
|
+
const source = project.sources.find(isOpenApiSource);
|
|
56
|
+
return source ? source.openApiData() : {};
|
|
57
|
+
};
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
60
|
* The Ask AI endpoint plus, unless the backend runs its own retrieval (Inkeep),
|
|
53
61
|
* its grounding snapshot. Empty when Ask AI is disabled.
|
|
@@ -156,6 +164,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
156
164
|
needsReact,
|
|
157
165
|
needsSvelte,
|
|
158
166
|
needsVue,
|
|
167
|
+
openapiPath: "./src/generated/openapi.json",
|
|
159
168
|
pages: relPages,
|
|
160
169
|
searchClientPath: "./src/generated/search-client.ts",
|
|
161
170
|
themePath: "./src/generated/app.css",
|
|
@@ -219,6 +228,10 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
219
228
|
path: join(genDir, "app.css"),
|
|
220
229
|
},
|
|
221
230
|
{ content: buildRuntimeData(project), path: join(genDir, "data.json") },
|
|
231
|
+
{
|
|
232
|
+
content: `${JSON.stringify(ejectOpenApiData(project))}\n`,
|
|
233
|
+
path: join(genDir, "openapi.json"),
|
|
234
|
+
},
|
|
222
235
|
{
|
|
223
236
|
content: `${JSON.stringify(rawMarkdown)}\n`,
|
|
224
237
|
path: join(genDir, "raw-markdown.json"),
|
|
@@ -302,7 +315,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
302
315
|
|
|
303
316
|
// Scalar API/AsyncAPI reference pages, mirrored from the generated runtime so
|
|
304
317
|
// the ejected app keeps its reference routes.
|
|
305
|
-
if (
|
|
318
|
+
if (hasScalarReferences(config)) {
|
|
306
319
|
const references = await buildReferenceFiles({
|
|
307
320
|
config,
|
|
308
321
|
contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handful of built-in glyphs Blume's own chrome renders from **client-side**
|
|
3
|
+
* scripts (copy/check buttons, search result rows). These stay hand-inlined and
|
|
4
|
+
* dependency-free so they can be bundled into client JS — the full icon
|
|
5
|
+
* resolver (`./icons.ts`) pulls in library data far too large to ship to the
|
|
6
|
+
* browser, so it must never be imported from a client script.
|
|
7
|
+
*
|
|
8
|
+
* Author-facing content icons (Cards, Steps, sidebar, `icon:` frontmatter) do
|
|
9
|
+
* NOT come from here — they resolve from the bundled icon libraries at build
|
|
10
|
+
* time via `resolveIcon` and inline as zero-JS SVG.
|
|
11
|
+
*
|
|
12
|
+
* Values are Lucide inner-SVG markup; the client `svg()` helpers wrap them in an
|
|
13
|
+
* `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" …>`.
|
|
14
|
+
*/
|
|
15
|
+
export const chromeIcons: Record<string, string> = {
|
|
16
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
17
|
+
copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
|
18
|
+
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
19
|
+
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
20
|
+
sparkles:
|
|
21
|
+
'<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/><path d="M5 3v4"/><path d="M3 5h4"/><path d="M19 17v4"/><path d="M17 19h4"/>',
|
|
22
|
+
};
|