blume 1.5.3 → 1.6.1
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 +94 -0
- package/dist/cli/index.js +3949 -1403
- package/dist/cli/index.js.map +111 -96
- 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 +33 -2
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +122 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +29 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +26 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/07-faq.mdx +9 -9
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +13 -4
- package/docs/advanced/custom-pages.mdx +4 -2
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +25 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +30 -3
- package/docs/configuration/theming.mdx +23 -0
- package/docs/content/components.mdx +15 -1
- 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 +66 -57
- 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/examples.ts +29 -2
- package/src/astro/generate.ts +282 -177
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +10 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +365 -113
- package/src/cli/commands/build.ts +91 -16
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- 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/copy-feedback.ts +93 -9
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- 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 +54 -22
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -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 +28 -3
- package/src/core/define-components.ts +2 -0
- 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 +12 -0
- package/src/core/includes.ts +633 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/manifest.ts +10 -0
- 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 +235 -36
- 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 +31 -2
- package/src/core/ui-packs/ar.ts +2 -0
- package/src/core/ui-packs/bg.ts +3 -0
- package/src/core/ui-packs/bn.ts +2 -0
- package/src/core/ui-packs/ca.ts +3 -0
- package/src/core/ui-packs/cs.ts +2 -0
- package/src/core/ui-packs/da.ts +2 -0
- package/src/core/ui-packs/de.ts +3 -0
- package/src/core/ui-packs/el.ts +3 -0
- package/src/core/ui-packs/es.ts +3 -0
- package/src/core/ui-packs/fa.ts +2 -0
- package/src/core/ui-packs/fi.ts +2 -0
- package/src/core/ui-packs/fr.ts +3 -0
- package/src/core/ui-packs/he.ts +2 -0
- package/src/core/ui-packs/hi.ts +2 -0
- package/src/core/ui-packs/hr.ts +3 -0
- package/src/core/ui-packs/hu.ts +3 -0
- package/src/core/ui-packs/id.ts +3 -0
- package/src/core/ui-packs/it.ts +2 -0
- package/src/core/ui-packs/ja.ts +3 -0
- package/src/core/ui-packs/ko.ts +3 -0
- package/src/core/ui-packs/nl.ts +3 -0
- package/src/core/ui-packs/no.ts +3 -0
- package/src/core/ui-packs/pl.ts +3 -0
- package/src/core/ui-packs/pt-br.ts +3 -0
- package/src/core/ui-packs/pt.ts +3 -0
- package/src/core/ui-packs/ro.ts +3 -0
- package/src/core/ui-packs/ru.ts +3 -0
- package/src/core/ui-packs/sk.ts +2 -0
- package/src/core/ui-packs/sr.ts +2 -0
- package/src/core/ui-packs/sv.ts +3 -0
- package/src/core/ui-packs/th.ts +2 -0
- package/src/core/ui-packs/tr.ts +3 -0
- package/src/core/ui-packs/uk.ts +3 -0
- package/src/core/ui-packs/vi.ts +2 -0
- package/src/core/ui-packs/zh-tw.ts +2 -0
- package/src/core/ui-packs/zh.ts +2 -0
- package/src/core/version-cut.ts +26 -6
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/deploy/vercel-negotiation.ts +49 -6
- 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/og/card.ts +1 -1
- 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 +97 -13
- package/src/openapi/render-mdx.ts +66 -12
- package/src/openapi/scalar.ts +5 -16
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +47 -17
- 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 +67 -13
- package/src/theme/fonts.ts +189 -16
- package/src/theme/sources.ts +49 -0
- 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";
|
|
@@ -54,8 +54,18 @@ export interface ReferenceSource {
|
|
|
54
54
|
includeInSearch: boolean;
|
|
55
55
|
/** Whether generated pages emit noindex metadata and stay out of the sitemap. */
|
|
56
56
|
noindex: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether operation meta descriptions end with the generated English
|
|
59
|
+
* "Reference for …" sentence, or carry the spec's own prose alone.
|
|
60
|
+
*/
|
|
61
|
+
seoDescriptionSuffix: boolean;
|
|
57
62
|
/** Local path or `http(s)` URL, verbatim from config. */
|
|
58
63
|
spec: string;
|
|
64
|
+
/**
|
|
65
|
+
* URL of the live GraphQL endpoint the playground and code samples target
|
|
66
|
+
* (GraphQL only — a schema, unlike an OpenAPI document, names no server).
|
|
67
|
+
*/
|
|
68
|
+
endpoint?: string;
|
|
59
69
|
/** Per-block Scalar theme name override, if any (Scalar renderer only). */
|
|
60
70
|
theme?: string;
|
|
61
71
|
/**
|
|
@@ -109,25 +119,40 @@ export const slugify = (text: string): string =>
|
|
|
109
119
|
const routeSlug = (route: string): string =>
|
|
110
120
|
slugify(trimChar(route, "/")) || "reference";
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The structural shape all three reference blocks (`openapi`, `asyncapi`,
|
|
124
|
+
* `graphql`) share. `endpoint` exists only on the GraphQL block and its
|
|
125
|
+
* sources; `scalar`/`theme` only on the Scalar-capable kinds — optional here
|
|
126
|
+
* so one resolver serves every block.
|
|
127
|
+
*/
|
|
128
|
+
interface Block {
|
|
129
|
+
enabled: boolean;
|
|
130
|
+
endpoint?: string;
|
|
131
|
+
route: string;
|
|
132
|
+
scalar?: ResolvedConfig["openapi"]["scalar"];
|
|
133
|
+
sources: {
|
|
134
|
+
endpoint?: string;
|
|
135
|
+
includeInLlms: boolean;
|
|
136
|
+
includeInSearch: boolean;
|
|
137
|
+
label?: string;
|
|
138
|
+
noindex: boolean;
|
|
139
|
+
route?: string;
|
|
140
|
+
seoDescriptionSuffix: boolean;
|
|
141
|
+
spec: string;
|
|
142
|
+
}[];
|
|
143
|
+
spec?: string;
|
|
144
|
+
theme?: string;
|
|
145
|
+
}
|
|
113
146
|
|
|
114
147
|
/** 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
|
-
}[] => {
|
|
148
|
+
const sourcesOf = (block: Block): Block["sources"] => {
|
|
125
149
|
const sources = [...block.sources];
|
|
126
150
|
if (block.spec) {
|
|
127
151
|
sources.unshift({
|
|
128
152
|
includeInLlms: true,
|
|
129
153
|
includeInSearch: true,
|
|
130
154
|
noindex: false,
|
|
155
|
+
seoDescriptionSuffix: true,
|
|
131
156
|
spec: block.spec,
|
|
132
157
|
});
|
|
133
158
|
}
|
|
@@ -163,7 +188,7 @@ const referencesFor = (
|
|
|
163
188
|
route = normalizeRoute(`${base}/${suffix || index + 1}`);
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
|
|
191
|
+
const reference: ReferenceSource = {
|
|
167
192
|
basePath,
|
|
168
193
|
display,
|
|
169
194
|
includeInLlms: source.includeInLlms,
|
|
@@ -174,10 +199,18 @@ const referencesFor = (
|
|
|
174
199
|
renderer,
|
|
175
200
|
route,
|
|
176
201
|
scalar: block.scalar,
|
|
202
|
+
seoDescriptionSuffix: source.seoDescriptionSuffix,
|
|
177
203
|
slug: routeSlug(route),
|
|
178
204
|
spec: source.spec,
|
|
179
205
|
theme: block.theme,
|
|
180
206
|
};
|
|
207
|
+
// Per-source endpoint wins; the block-level one is the shared default
|
|
208
|
+
// (the common single-schema case pairs it with the `spec` shorthand).
|
|
209
|
+
const endpoint = source.endpoint ?? block.endpoint;
|
|
210
|
+
if (endpoint !== undefined) {
|
|
211
|
+
reference.endpoint = endpoint;
|
|
212
|
+
}
|
|
213
|
+
return reference;
|
|
181
214
|
});
|
|
182
215
|
};
|
|
183
216
|
|
|
@@ -212,6 +245,21 @@ export const resolveReferences = (
|
|
|
212
245
|
},
|
|
213
246
|
config.basePath
|
|
214
247
|
),
|
|
248
|
+
// GraphQL is always Blume-rendered — Scalar's embedded SPA reads OpenAPI
|
|
249
|
+
// documents only, so the block declares no `renderer` opt-out (nor the
|
|
250
|
+
// schema-row `expandSchemas` toggle; GraphQL field tables have no nesting).
|
|
251
|
+
...referencesFor(
|
|
252
|
+
"graphql",
|
|
253
|
+
config.graphql,
|
|
254
|
+
"GraphQL",
|
|
255
|
+
"blume",
|
|
256
|
+
{
|
|
257
|
+
codeSamples: config.graphql.codeSamples,
|
|
258
|
+
expandSchemas: false,
|
|
259
|
+
playground: config.graphql.playground,
|
|
260
|
+
},
|
|
261
|
+
config.basePath
|
|
262
|
+
),
|
|
215
263
|
];
|
|
216
264
|
|
|
217
265
|
/**
|
|
@@ -287,3 +335,39 @@ export const blumeReferences = (config: ResolvedConfig): ReferenceSource[] => {
|
|
|
287
335
|
/** Whether any reference is Scalar-rendered (gates the `@scalar/astro` dep + pages). */
|
|
288
336
|
export const hasScalarReferences = (config: ResolvedConfig): boolean =>
|
|
289
337
|
resolveReferences(config).some((ref) => ref.renderer === "scalar");
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The reference kinds whose enabled, Blume-rendered playground opted into the
|
|
341
|
+
* built-in CORS proxy with `proxy: true`. A proxy URL string points at an
|
|
342
|
+
* external service, and `false` sends requests directly — neither routes
|
|
343
|
+
* through the endpoint. The generator's per-spec allowlist diagnostics key on
|
|
344
|
+
* this, so it shares one definition with {@link needsPlaygroundProxy}.
|
|
345
|
+
*/
|
|
346
|
+
export const builtinProxyKinds = (config: ResolvedConfig): ReferenceKind[] => {
|
|
347
|
+
const kinds: ReferenceKind[] = [];
|
|
348
|
+
if (
|
|
349
|
+
config.openapi.enabled &&
|
|
350
|
+
config.openapi.renderer === "blume" &&
|
|
351
|
+
config.openapi.playground.enabled &&
|
|
352
|
+
config.openapi.playground.proxy === true
|
|
353
|
+
) {
|
|
354
|
+
kinds.push("openapi");
|
|
355
|
+
}
|
|
356
|
+
if (
|
|
357
|
+
config.graphql.enabled &&
|
|
358
|
+
config.graphql.playground.enabled &&
|
|
359
|
+
config.graphql.playground.proxy === true
|
|
360
|
+
) {
|
|
361
|
+
kinds.push("graphql");
|
|
362
|
+
}
|
|
363
|
+
return kinds;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Whether the built-in playground CORS proxy endpoint (`/_api-proxy`) must be
|
|
368
|
+
* generated: some enabled Blume-rendered block's playground opted into it with
|
|
369
|
+
* `proxy: true`. Shared by the server feature gate and the generator so the
|
|
370
|
+
* two can never disagree.
|
|
371
|
+
*/
|
|
372
|
+
export const needsPlaygroundProxy = (config: ResolvedConfig): boolean =>
|
|
373
|
+
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,20 +158,51 @@ 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
|
|
162
|
-
* description even when the spec's summaries are terse.
|
|
177
|
+
* description even when the spec's summaries are terse. With the suffix
|
|
178
|
+
* switched off (`seoDescriptionSuffix: false`, for sites whose prose isn't
|
|
179
|
+
* English) the description is the prose alone, or the page `title` — a
|
|
180
|
+
* language-neutral `GET /pets`, channel, or field name — when the operation
|
|
181
|
+
* has no prose at all, so no page ships an empty description.
|
|
163
182
|
*/
|
|
164
183
|
const operationDescription = (
|
|
165
184
|
spec: ApiSpecData,
|
|
166
|
-
operation: ApiOperationRef
|
|
185
|
+
operation: ApiOperationRef,
|
|
186
|
+
options: { suffix: boolean; title: string }
|
|
167
187
|
): string => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
if (!options.suffix) {
|
|
189
|
+
return clip(
|
|
190
|
+
plainProse(operation.description || operation.summary) || options.title,
|
|
191
|
+
META_DESCRIPTION_MAX
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
// AsyncAPI operations act on a channel, not an HTTP endpoint; GraphQL pages
|
|
195
|
+
// document a root field or a named type.
|
|
196
|
+
let suffix: string;
|
|
197
|
+
if (spec.kind === "asyncapi") {
|
|
198
|
+
suffix = `Reference for the ${operation.method} operation on ${operation.path} in the ${apiName(spec)} API.`;
|
|
199
|
+
} else if (spec.kind === "graphql") {
|
|
200
|
+
// SAFETY: the GraphQL extractor only ever assigns member kinds as the
|
|
201
|
+
// method (see `extractGraphqlOperations`).
|
|
202
|
+
suffix = `Reference for the ${operation.path} ${GRAPHQL_MEMBER_PHRASES[operation.method as GraphqlMember]} in the ${apiName(spec)} API.`;
|
|
203
|
+
} else {
|
|
204
|
+
suffix = `Reference for the ${operation.method.toUpperCase()} ${operation.path} endpoint in the ${apiName(spec)} API.`;
|
|
205
|
+
}
|
|
173
206
|
const prose = clip(
|
|
174
207
|
plainProse(operation.description || operation.summary),
|
|
175
208
|
META_DESCRIPTION_MAX - stringWidth(suffix) - 1
|
|
@@ -188,11 +221,17 @@ export const operationMdx = (
|
|
|
188
221
|
operation: ApiOperationRef,
|
|
189
222
|
reference?: Pick<
|
|
190
223
|
ReferenceSource,
|
|
191
|
-
"includeInLlms" | "includeInSearch" | "noindex"
|
|
224
|
+
"includeInLlms" | "includeInSearch" | "noindex" | "seoDescriptionSuffix"
|
|
192
225
|
>
|
|
193
226
|
): RenderedPage => {
|
|
194
227
|
const method = operation.method.toUpperCase();
|
|
195
|
-
const
|
|
228
|
+
const graphql = spec.kind === "graphql";
|
|
229
|
+
// A GraphQL page IS its field/type — `QUERY pets` would double the badge the
|
|
230
|
+
// page already renders; the other kinds title an endpoint or channel action.
|
|
231
|
+
const fallbackTitle = graphql
|
|
232
|
+
? operation.path
|
|
233
|
+
: `${method} ${operation.path}`;
|
|
234
|
+
const title = operation.summary || fallbackTitle;
|
|
196
235
|
// Skip the body description when it only repeats the summary (the `<h1>`) —
|
|
197
236
|
// common in specs that set summary and description to the same string.
|
|
198
237
|
const description =
|
|
@@ -214,11 +253,26 @@ export const operationMdx = (
|
|
|
214
253
|
searchFlags.exclude = true;
|
|
215
254
|
}
|
|
216
255
|
const seo: RenderedPageData["seo"] = {
|
|
217
|
-
description: operationDescription(spec, operation
|
|
256
|
+
description: operationDescription(spec, operation, {
|
|
257
|
+
suffix: reference?.seoDescriptionSuffix !== false,
|
|
258
|
+
title,
|
|
259
|
+
}),
|
|
218
260
|
};
|
|
219
261
|
if (reference?.noindex) {
|
|
220
262
|
seo.noindex = true;
|
|
221
263
|
}
|
|
264
|
+
const sidebar: RenderedPageData["sidebar"] = {
|
|
265
|
+
label: operation.summary || operation.path,
|
|
266
|
+
};
|
|
267
|
+
// GraphQL operation kinds badge like HTTP methods, but a type page's kind
|
|
268
|
+
// already heads its sidebar group ("Objects", "Enums", …) — an `OBJECT`
|
|
269
|
+
// badge on every row would only repeat it, so type pages get none. The
|
|
270
|
+
// uppercased method is likewise an internal token on GraphQL pages, so
|
|
271
|
+
// their search tags carry only the group name.
|
|
272
|
+
if (!graphql || isGraphqlOperationKind(operation.method)) {
|
|
273
|
+
sidebar.badge = method;
|
|
274
|
+
}
|
|
275
|
+
const tags = graphql ? [operation.tag] : [operation.tag, method];
|
|
222
276
|
return {
|
|
223
277
|
body: withDescription(
|
|
224
278
|
description,
|
|
@@ -226,9 +280,9 @@ export const operationMdx = (
|
|
|
226
280
|
),
|
|
227
281
|
data: {
|
|
228
282
|
...flags,
|
|
229
|
-
search: { ...searchFlags, tags
|
|
283
|
+
search: { ...searchFlags, tags },
|
|
230
284
|
seo,
|
|
231
|
-
sidebar
|
|
285
|
+
sidebar,
|
|
232
286
|
title,
|
|
233
287
|
// Signals the two-column API layout (request panel instead of the TOC).
|
|
234
288
|
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
|
|
|
@@ -151,9 +144,6 @@ export const buildReferenceFiles = async (options: {
|
|
|
151
144
|
warnings.push(spec.warning);
|
|
152
145
|
}
|
|
153
146
|
const pagePath = referencePagePath(ref.route);
|
|
154
|
-
// Relative path from the page back to src/generated/data.json: a page one
|
|
155
|
-
// directory deep (api/events.astro) needs an extra "../".
|
|
156
|
-
const depth = pagePath.split("/").length - 1;
|
|
157
147
|
files.push({
|
|
158
148
|
content: scalarReferenceTemplate({
|
|
159
149
|
configuration: {
|
|
@@ -164,7 +154,6 @@ export const buildReferenceFiles = async (options: {
|
|
|
164
154
|
// hideTestRequestButton, orderSchemaPropertiesBy, and the rest).
|
|
165
155
|
...ref.scalar,
|
|
166
156
|
},
|
|
167
|
-
dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
|
|
168
157
|
noindex: ref.noindex,
|
|
169
158
|
route: ref.route,
|
|
170
159
|
title: ref.label,
|
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
|
@@ -9,7 +9,12 @@ import { buildRawMarkdown } from "../ai/markdown.ts";
|
|
|
9
9
|
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
10
10
|
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
11
11
|
import { planComponentSlots } from "../astro/component-slots.ts";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
EXAMPLE_SCAN_GLOB,
|
|
14
|
+
discoverExamples,
|
|
15
|
+
exampleMarkdownLookup,
|
|
16
|
+
exampleScanRoots,
|
|
17
|
+
} from "../astro/examples.ts";
|
|
13
18
|
import {
|
|
14
19
|
buildRuntimeData,
|
|
15
20
|
collectStaged,
|
|
@@ -44,6 +49,7 @@ import {
|
|
|
44
49
|
searchEndpointTemplate,
|
|
45
50
|
staticJsonEndpointTemplate,
|
|
46
51
|
} from "../astro/templates.ts";
|
|
52
|
+
import { buildIncludeGraph } from "../core/includes.ts";
|
|
47
53
|
import { packageRoot } from "../core/package-root.ts";
|
|
48
54
|
import { scanProject } from "../core/project-graph.ts";
|
|
49
55
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
@@ -60,6 +66,7 @@ import {
|
|
|
60
66
|
tailwindEntryTemplate,
|
|
61
67
|
} from "../theme/entry.ts";
|
|
62
68
|
import { buildThemeCss } from "../theme/palette.ts";
|
|
69
|
+
import { rebaseSourceDirectives } from "../theme/sources.ts";
|
|
63
70
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
64
71
|
|
|
65
72
|
const toPosix = (path: string): string => path.split("\\").join("/");
|
|
@@ -202,7 +209,7 @@ const mcpFiles = async (
|
|
|
202
209
|
path: join(genDir, "mcp-data.json"),
|
|
203
210
|
},
|
|
204
211
|
{
|
|
205
|
-
content: mcpEndpointTemplate(
|
|
212
|
+
content: mcpEndpointTemplate(),
|
|
206
213
|
path: join(srcDir, "pages", mcpPageFile(route)),
|
|
207
214
|
},
|
|
208
215
|
{
|
|
@@ -250,13 +257,21 @@ const changelogFiles = (
|
|
|
250
257
|
};
|
|
251
258
|
|
|
252
259
|
/** Contents of the configured `examples.css`, or `""` when unset/absent. */
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Read a user stylesheet that eject inlines into a generated entry under
|
|
262
|
+
* `genDir`, re-rooting its relative `@source` paths from the user's file.
|
|
263
|
+
* Resolves to an empty string when the file is unset or absent.
|
|
264
|
+
*/
|
|
265
|
+
const readUserCss = async (
|
|
266
|
+
file: string | null,
|
|
267
|
+
genDir: string
|
|
268
|
+
): Promise<string> => {
|
|
269
|
+
if (!(file && existsSync(file))) {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
const css = await readFile(file, "utf-8");
|
|
273
|
+
return rebaseSourceDirectives(css, { from: file, to: genDir });
|
|
274
|
+
};
|
|
260
275
|
|
|
261
276
|
/**
|
|
262
277
|
* The per-example preview route `<Component />` iframes embed, nested under
|
|
@@ -330,10 +345,11 @@ export const eject = async (
|
|
|
330
345
|
context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
|
|
331
346
|
detectNeedsReact(root),
|
|
332
347
|
detectUsesMath(root),
|
|
333
|
-
context.themeFile
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
348
|
+
readUserCss(context.themeFile, genDir),
|
|
349
|
+
readUserCss(
|
|
350
|
+
config.examples.css ? join(root, config.examples.css) : null,
|
|
351
|
+
genDir
|
|
352
|
+
),
|
|
337
353
|
buildRawMarkdown(project),
|
|
338
354
|
discoverIslands(root),
|
|
339
355
|
]);
|
|
@@ -383,11 +399,14 @@ export const eject = async (
|
|
|
383
399
|
content: astroConfigTemplate({
|
|
384
400
|
askPath: "./src/generated/Ask.astro",
|
|
385
401
|
config,
|
|
402
|
+
contentRoot: relContext.contentRoot,
|
|
386
403
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
387
404
|
context: relContext,
|
|
388
|
-
dataPath: "./src/generated/data.json",
|
|
389
405
|
examplesPath: "./src/generated/examples.ts",
|
|
390
406
|
examplesThemePath: "./src/generated/examples.css",
|
|
407
|
+
// No CLI publishes the runtime data modules in memory after eject, so
|
|
408
|
+
// the config aliases each to the JSON snapshot written below.
|
|
409
|
+
generatedModulesDir: "./src/generated",
|
|
391
410
|
integrationBridge: ejectIntegrationBridge(
|
|
392
411
|
config,
|
|
393
412
|
root,
|
|
@@ -396,7 +415,6 @@ export const eject = async (
|
|
|
396
415
|
needsReact,
|
|
397
416
|
needsSvelte,
|
|
398
417
|
needsVue,
|
|
399
|
-
openapiPath: "./src/generated/openapi.json",
|
|
400
418
|
pages: relPages,
|
|
401
419
|
searchClientPath: "./src/generated/search-client.ts",
|
|
402
420
|
themePath: "./src/generated/app.css",
|
|
@@ -450,7 +468,9 @@ export const eject = async (
|
|
|
450
468
|
// Relative sources keep the ejected app portable.
|
|
451
469
|
content: examplesEntryTemplate({
|
|
452
470
|
configTokens: buildThemeCss(config.theme),
|
|
453
|
-
sources:
|
|
471
|
+
sources: exampleScanRoots(root, examples.dir).map(
|
|
472
|
+
(dir) => `${relative(genDir, dir)}/${EXAMPLE_SCAN_GLOB}`
|
|
473
|
+
),
|
|
454
474
|
userCss: userExamplesCss,
|
|
455
475
|
}),
|
|
456
476
|
path: join(genDir, "examples.css"),
|
|
@@ -485,6 +505,15 @@ export const eject = async (
|
|
|
485
505
|
content: `${JSON.stringify(rawMarkdown)}\n`,
|
|
486
506
|
path: join(genDir, "raw-markdown.json"),
|
|
487
507
|
},
|
|
508
|
+
{
|
|
509
|
+
// The partial → including-pages map behind `includeHmrPlugin`, which the
|
|
510
|
+
// ejected astro.config wires at this exact path — without the file every
|
|
511
|
+
// hot update's read would silently no-op and partial edits would serve
|
|
512
|
+
// stale pages. A snapshot like the rest of `src/generated`: the ejected
|
|
513
|
+
// app owns (and may regenerate or prune) it.
|
|
514
|
+
content: `${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`,
|
|
515
|
+
path: join(genDir, "includes.json"),
|
|
516
|
+
},
|
|
488
517
|
{
|
|
489
518
|
content: rawMarkdownEndpointTemplate("md"),
|
|
490
519
|
path: join(srcDir, "pages", "[...slug].md.ts"),
|
|
@@ -502,7 +531,8 @@ export const eject = async (
|
|
|
502
531
|
if (config.seo.og.enabled) {
|
|
503
532
|
files.push({
|
|
504
533
|
content: ogEndpointTemplate(
|
|
505
|
-
customOgRoutes(pages, config.title, config.seo.og.titles)
|
|
534
|
+
customOgRoutes(pages, config.title, config.seo.og.titles),
|
|
535
|
+
{ pageDescriptions: config.seo.og.description !== false }
|
|
506
536
|
),
|
|
507
537
|
path: join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
508
538
|
});
|