blume 1.4.3 → 1.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/CHANGELOG.md +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/RootLayout.astro +18 -0
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +88 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +15 -3
- package/src/core/schema.ts +213 -67
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +30 -11
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
package/src/core/schema.ts
CHANGED
|
@@ -23,6 +23,14 @@ import type { StandardSchema } from "./standard-schema.ts";
|
|
|
23
23
|
// Shared primitives
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
+
// `typeof` checks live in named predicates (the form the oxlint anti-slop
|
|
27
|
+
// config sanctions); generic so each site keeps its own union narrowing.
|
|
28
|
+
const isString = <Value>(value: Value): value is Value & string =>
|
|
29
|
+
typeof value === "string";
|
|
30
|
+
|
|
31
|
+
const isBoolean = <Value>(value: Value): value is Value & boolean =>
|
|
32
|
+
typeof value === "boolean";
|
|
33
|
+
|
|
26
34
|
/** Icon inputs in serializable contexts (frontmatter, meta files). */
|
|
27
35
|
const iconName = z.string().min(1);
|
|
28
36
|
|
|
@@ -40,12 +48,28 @@ const dateSchema = z
|
|
|
40
48
|
.union([z.string(), z.date()])
|
|
41
49
|
.transform((value) => (value instanceof Date ? value.toISOString() : value));
|
|
42
50
|
|
|
51
|
+
/**
|
|
52
|
+
* How a sidebar group renders:
|
|
53
|
+
* - `flat`: a non-collapsible header with its items listed beneath (default).
|
|
54
|
+
* - `group`: a collapsible `<details>` disclosure.
|
|
55
|
+
* - `page`: a single row that drills into a sub-panel showing only this group's
|
|
56
|
+
* items, with a back arrow at the top.
|
|
57
|
+
*/
|
|
58
|
+
const sidebarDisplaySchema = z.enum(["flat", "group", "page"]);
|
|
59
|
+
export type SidebarDisplay = z.infer<typeof sidebarDisplaySchema>;
|
|
60
|
+
|
|
43
61
|
// ---------------------------------------------------------------------------
|
|
44
62
|
// Page frontmatter
|
|
45
63
|
// ---------------------------------------------------------------------------
|
|
46
64
|
|
|
47
65
|
const sidebarMetaSchema = z.strictObject({
|
|
48
66
|
badge: z.string().optional(),
|
|
67
|
+
/**
|
|
68
|
+
* Render mode for this page's folder group. Only meaningful on a folder's
|
|
69
|
+
* `index` page — it configures the group, not the page. Overrides the
|
|
70
|
+
* folder's `meta.ts` `display` and the global `navigation.sidebar.display`.
|
|
71
|
+
*/
|
|
72
|
+
display: sidebarDisplaySchema.optional(),
|
|
49
73
|
hidden: z.boolean().default(false),
|
|
50
74
|
icon: iconName.optional(),
|
|
51
75
|
label: z.string().optional(),
|
|
@@ -141,6 +165,11 @@ export const pageMetaSchema = pageMetaBaseSchema;
|
|
|
141
165
|
export type PageMeta = z.infer<typeof pageMetaBaseSchema>;
|
|
142
166
|
export type PageMetaInput = z.input<typeof pageMetaBaseSchema>;
|
|
143
167
|
|
|
168
|
+
/** Built-in page frontmatter keys; custom keys must never redeclare one. */
|
|
169
|
+
const BUILT_IN_PAGE_META_KEYS = new Set<string>(
|
|
170
|
+
pageMetaBaseSchema.keyof().options
|
|
171
|
+
);
|
|
172
|
+
|
|
144
173
|
/**
|
|
145
174
|
* A map of custom frontmatter keys to user-supplied validation schemas,
|
|
146
175
|
* consumed through the Standard Schema `~standard` contract — never Zod's own
|
|
@@ -162,7 +191,7 @@ const customKeySchemaRecord = (where: string) =>
|
|
|
162
191
|
.default({})
|
|
163
192
|
.superRefine((value, ctx) => {
|
|
164
193
|
for (const key of Object.keys(value)) {
|
|
165
|
-
if (
|
|
194
|
+
if (BUILT_IN_PAGE_META_KEYS.has(key)) {
|
|
166
195
|
ctx.addIssue({
|
|
167
196
|
code: z.ZodIssueCode.custom,
|
|
168
197
|
message: `"${key}" is a built-in frontmatter field and cannot be redeclared via ${where}.`,
|
|
@@ -176,18 +205,10 @@ const customKeySchemaRecord = (where: string) =>
|
|
|
176
205
|
// Folder meta (meta.ts)
|
|
177
206
|
// ---------------------------------------------------------------------------
|
|
178
207
|
|
|
179
|
-
/**
|
|
180
|
-
* How a sidebar group renders:
|
|
181
|
-
* - `flat`: a non-collapsible header with its items listed beneath (default).
|
|
182
|
-
* - `group`: a collapsible `<details>` disclosure.
|
|
183
|
-
* - `page`: a single row that drills into a sub-panel showing only this group's
|
|
184
|
-
* items, with a back arrow at the top.
|
|
185
|
-
*/
|
|
186
|
-
const sidebarDisplaySchema = z.enum(["flat", "group", "page"]);
|
|
187
|
-
export type SidebarDisplay = z.infer<typeof sidebarDisplaySchema>;
|
|
188
|
-
|
|
189
208
|
export const folderMetaSchema = z.strictObject({
|
|
190
209
|
collapsed: z.boolean().optional(),
|
|
210
|
+
/** Render mode for this group; overrides `navigation.sidebar.display`. */
|
|
211
|
+
display: sidebarDisplaySchema.optional(),
|
|
191
212
|
icon: iconName.optional(),
|
|
192
213
|
order: z.number().optional(),
|
|
193
214
|
/** Explicit child ordering by slug segment (without numeric prefix). */
|
|
@@ -355,11 +376,13 @@ const githubReleasesSourceSchema = z.strictObject({
|
|
|
355
376
|
*/
|
|
356
377
|
const customSourceSchema = z.object({
|
|
357
378
|
source: z.custom<ContentSource>(
|
|
358
|
-
(val) =>
|
|
379
|
+
(val): val is ContentSource =>
|
|
359
380
|
typeof val === "object" &&
|
|
360
381
|
val !== null &&
|
|
361
|
-
|
|
362
|
-
typeof
|
|
382
|
+
"load" in val &&
|
|
383
|
+
typeof val.load === "function" &&
|
|
384
|
+
"name" in val &&
|
|
385
|
+
typeof val.name === "string",
|
|
363
386
|
{ message: "custom source must be a ContentSource (with name + load)" }
|
|
364
387
|
),
|
|
365
388
|
type: z.literal("custom"),
|
|
@@ -558,7 +581,7 @@ const localFontSchema = z.strictObject({
|
|
|
558
581
|
const fontValueSchema = z
|
|
559
582
|
.union([z.string(), remoteFontSchema, localFontSchema])
|
|
560
583
|
.superRefine((value, ctx) => {
|
|
561
|
-
if (
|
|
584
|
+
if (isString(value) && !isFontSlug(value)) {
|
|
562
585
|
ctx.addIssue({
|
|
563
586
|
code: z.ZodIssueCode.custom,
|
|
564
587
|
message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}. For any other family, use the object form: { name: "..." } (remote provider) or { name: "...", variants: [...] } (local files).`,
|
|
@@ -581,7 +604,7 @@ const perModeValueSchema = z
|
|
|
581
604
|
])
|
|
582
605
|
.optional()
|
|
583
606
|
.transform((value) =>
|
|
584
|
-
|
|
607
|
+
isString(value) ? { dark: value, light: value } : value
|
|
585
608
|
);
|
|
586
609
|
|
|
587
610
|
const themeConfigSchema = z.strictObject({
|
|
@@ -592,7 +615,7 @@ const themeConfigSchema = z.strictObject({
|
|
|
592
615
|
])
|
|
593
616
|
.default("blue")
|
|
594
617
|
.transform((value) =>
|
|
595
|
-
|
|
618
|
+
isString(value) ? { dark: value, light: value } : value
|
|
596
619
|
),
|
|
597
620
|
action: z.string().optional(),
|
|
598
621
|
background: perModeValueSchema,
|
|
@@ -682,6 +705,8 @@ const searchConfigSchema = z
|
|
|
682
705
|
.superRefine((value, ctx) => {
|
|
683
706
|
// Hosted providers can't work without their credentials; flag a missing
|
|
684
707
|
// block with a path so the diagnostic points at `search.<provider>`.
|
|
708
|
+
// SAFETY: providers without a config block (orama, pagefind, …) miss the
|
|
709
|
+
// map and read undefined, which the `field &&` guard below absorbs.
|
|
685
710
|
const field =
|
|
686
711
|
PROVIDER_CONFIG_KEY[value.provider as keyof typeof PROVIDER_CONFIG_KEY];
|
|
687
712
|
if (field && !value[field]) {
|
|
@@ -718,7 +743,7 @@ const PRIVATE_JWK_PARAMS = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
|
|
|
718
743
|
const publicJwkSchema = z
|
|
719
744
|
.record(z.string(), z.unknown())
|
|
720
745
|
.superRefine((jwk, ctx) => {
|
|
721
|
-
if (
|
|
746
|
+
if (!isString(jwk.kty) || jwk.kty.length === 0) {
|
|
722
747
|
ctx.addIssue({
|
|
723
748
|
code: z.ZodIssueCode.custom,
|
|
724
749
|
message: 'A JWK must declare its key type ("kty").',
|
|
@@ -830,7 +855,7 @@ const aiConfigSchema = z.strictObject({
|
|
|
830
855
|
])
|
|
831
856
|
.default(true)
|
|
832
857
|
.transform((value) =>
|
|
833
|
-
|
|
858
|
+
isBoolean(value) ? { enabled: value, openapi: true } : value
|
|
834
859
|
),
|
|
835
860
|
// Serializers for the agent-facing Markdown downlevel (the `.md` mirror,
|
|
836
861
|
// llms-full.txt, MCP get_page), keyed by JSX name. Functions live here —
|
|
@@ -842,9 +867,12 @@ const aiConfigSchema = z.strictObject({
|
|
|
842
867
|
markdownComponents: z
|
|
843
868
|
.record(
|
|
844
869
|
z.string(),
|
|
845
|
-
z.custom<ComponentMarkdown>(
|
|
846
|
-
|
|
847
|
-
|
|
870
|
+
z.custom<ComponentMarkdown>(
|
|
871
|
+
(value): value is ComponentMarkdown => typeof value === "function",
|
|
872
|
+
{
|
|
873
|
+
message: "Expected a serializer function.",
|
|
874
|
+
}
|
|
875
|
+
)
|
|
848
876
|
)
|
|
849
877
|
.default({}),
|
|
850
878
|
/** Expose the docs as an MCP server for connecting agents. */
|
|
@@ -933,7 +961,7 @@ const exportConfigSchema = z
|
|
|
933
961
|
}),
|
|
934
962
|
])
|
|
935
963
|
.transform((value) =>
|
|
936
|
-
|
|
964
|
+
isBoolean(value) ? { epub: value, pdf: value } : value
|
|
937
965
|
);
|
|
938
966
|
|
|
939
967
|
/** A configured locale: ISO-ish code plus display metadata for the switcher. */
|
|
@@ -990,6 +1018,82 @@ const i18nConfigSchema = z
|
|
|
990
1018
|
}
|
|
991
1019
|
});
|
|
992
1020
|
|
|
1021
|
+
/**
|
|
1022
|
+
* Version ids must start with a letter (`v1.0`, not `1.0`): the id doubles as
|
|
1023
|
+
* the snapshot directory name, and a leading digit would collide with the
|
|
1024
|
+
* numeric-prefix ordering convention (`01-intro.mdx`), which strips `1.0/` to
|
|
1025
|
+
* `0/`. The rest allows word characters, dots, and hyphens — URL-safe as-is.
|
|
1026
|
+
*/
|
|
1027
|
+
export const VERSION_ID = /^[A-Za-z][\w.-]*$/u;
|
|
1028
|
+
|
|
1029
|
+
/** A frozen documentation snapshot: a directory under the content root. */
|
|
1030
|
+
const archivedVersionSchema = z.strictObject({
|
|
1031
|
+
/**
|
|
1032
|
+
* The "you're viewing an old version" notice: `true` for the built-in
|
|
1033
|
+
* message, a string for custom copy, `false` to hide it.
|
|
1034
|
+
*/
|
|
1035
|
+
banner: z.union([z.boolean(), z.string()]).default(true),
|
|
1036
|
+
/**
|
|
1037
|
+
* Where this version's pages point their canonical URL: `latest` targets the
|
|
1038
|
+
* same page in the current docs when it still exists (self otherwise), so
|
|
1039
|
+
* search engines treat the live page as authoritative without deindexing
|
|
1040
|
+
* version-only content. `self` keeps every page authoritative.
|
|
1041
|
+
*/
|
|
1042
|
+
canonical: z.enum(["latest", "self"]).default("latest"),
|
|
1043
|
+
/** Directory name under the content root, and the URL segment. */
|
|
1044
|
+
id: z
|
|
1045
|
+
.string()
|
|
1046
|
+
.regex(
|
|
1047
|
+
VERSION_ID,
|
|
1048
|
+
'Version ids must start with a letter (e.g. "v1.0") and contain only letters, digits, dots, hyphens, and underscores.'
|
|
1049
|
+
),
|
|
1050
|
+
/** Switcher label; defaults to the id. */
|
|
1051
|
+
label: z.string().optional(),
|
|
1052
|
+
/** Emit `noindex` on every page of this version. */
|
|
1053
|
+
noindex: z.boolean().default(false),
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Docs versioning. Opt-in: the latest docs live at the content root with
|
|
1058
|
+
* unprefixed URLs, and each archived version is a frozen snapshot directory
|
|
1059
|
+
* (`content/docs/<id>/`) cut with `blume version <id>`. Archived means frozen:
|
|
1060
|
+
* snapshots carry their own translations and are never retranslated.
|
|
1061
|
+
*/
|
|
1062
|
+
const versionsConfigSchema = z
|
|
1063
|
+
.strictObject({
|
|
1064
|
+
/** Frozen snapshots, newest first — this order is the switcher order. */
|
|
1065
|
+
archived: z.array(archivedVersionSchema).default([]),
|
|
1066
|
+
/** Labels the unprefixed tree (the latest docs) in the switcher. */
|
|
1067
|
+
current: z.strictObject({
|
|
1068
|
+
/** Small tag rendered next to the label (e.g. `Latest`). */
|
|
1069
|
+
badge: z.string().optional(),
|
|
1070
|
+
label: z.string(),
|
|
1071
|
+
}),
|
|
1072
|
+
switcher: z
|
|
1073
|
+
.strictObject({
|
|
1074
|
+
/**
|
|
1075
|
+
* Where switching lands when the page has no equivalent in the target
|
|
1076
|
+
* version: `same-page` goes to the equivalent when it exists (version
|
|
1077
|
+
* root otherwise); `root` always goes to the version root.
|
|
1078
|
+
*/
|
|
1079
|
+
redirect: z.enum(["same-page", "root"]).default("same-page"),
|
|
1080
|
+
})
|
|
1081
|
+
.prefault({}),
|
|
1082
|
+
})
|
|
1083
|
+
.superRefine((value, ctx) => {
|
|
1084
|
+
const seen = new Set<string>();
|
|
1085
|
+
for (const [position, version] of value.archived.entries()) {
|
|
1086
|
+
if (seen.has(version.id)) {
|
|
1087
|
+
ctx.addIssue({
|
|
1088
|
+
code: z.ZodIssueCode.custom,
|
|
1089
|
+
message: `versions.archived declares "${version.id}" more than once.`,
|
|
1090
|
+
path: ["archived", position, "id"],
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
seen.add(version.id);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
993
1097
|
const analyticsScriptSchema = z
|
|
994
1098
|
.strictObject({
|
|
995
1099
|
// Extra attributes (e.g. `data-domain`, `id`) spread onto the <script>.
|
|
@@ -1234,11 +1338,22 @@ const githubConfigSchema = z.strictObject({
|
|
|
1234
1338
|
repo: z.string(),
|
|
1235
1339
|
});
|
|
1236
1340
|
|
|
1341
|
+
/** The theme fields the structural code-theme check inspects. */
|
|
1342
|
+
interface CodeThemeFields {
|
|
1343
|
+
colors?: unknown;
|
|
1344
|
+
settings?: unknown;
|
|
1345
|
+
tokenColors?: unknown;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** A non-null, non-array object — the floor for a theme and its `colors` map. */
|
|
1349
|
+
const isThemeObject = <Value>(value: Value): value is Value & CodeThemeFields =>
|
|
1350
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1351
|
+
|
|
1237
1352
|
const codeThemeSchema = z.custom<CodeTheme>((value) => {
|
|
1238
|
-
if (
|
|
1353
|
+
if (isString(value)) {
|
|
1239
1354
|
return true;
|
|
1240
1355
|
}
|
|
1241
|
-
if (
|
|
1356
|
+
if (!isThemeObject(value)) {
|
|
1242
1357
|
return false;
|
|
1243
1358
|
}
|
|
1244
1359
|
// Token rules live in `settings` (Shiki's canonical field, also the TextMate
|
|
@@ -1246,20 +1361,15 @@ const codeThemeSchema = z.custom<CodeTheme>((value) => {
|
|
|
1246
1361
|
// VS Code spelling Shiki falls back to). A colors-only theme (editor fg/bg,
|
|
1247
1362
|
// no token rules) is also valid — Shiki renders it from `colors` alone. Each
|
|
1248
1363
|
// field present must have the right shape, and at least one must be present.
|
|
1249
|
-
const theme = value as Record<string, unknown>;
|
|
1250
1364
|
const settingsValid =
|
|
1251
|
-
|
|
1365
|
+
value.settings === undefined || Array.isArray(value.settings);
|
|
1252
1366
|
const tokenColorsValid =
|
|
1253
|
-
|
|
1254
|
-
const colorsValid =
|
|
1255
|
-
theme.colors === undefined ||
|
|
1256
|
-
(typeof theme.colors === "object" &&
|
|
1257
|
-
theme.colors !== null &&
|
|
1258
|
-
!Array.isArray(theme.colors));
|
|
1367
|
+
value.tokenColors === undefined || Array.isArray(value.tokenColors);
|
|
1368
|
+
const colorsValid = value.colors === undefined || isThemeObject(value.colors);
|
|
1259
1369
|
const hasContent =
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1370
|
+
value.settings !== undefined ||
|
|
1371
|
+
value.tokenColors !== undefined ||
|
|
1372
|
+
value.colors !== undefined;
|
|
1263
1373
|
return settingsValid && tokenColorsValid && colorsValid && hasContent;
|
|
1264
1374
|
}, "Expected a Shiki theme name or custom theme object");
|
|
1265
1375
|
|
|
@@ -1298,7 +1408,7 @@ const examplesConfigSchema = z
|
|
|
1298
1408
|
}),
|
|
1299
1409
|
])
|
|
1300
1410
|
.transform((value): { css?: string; source: string } =>
|
|
1301
|
-
|
|
1411
|
+
isString(value) ? { source: value } : value
|
|
1302
1412
|
);
|
|
1303
1413
|
|
|
1304
1414
|
/**
|
|
@@ -1398,7 +1508,8 @@ const reactConfigSchema = z.strictObject({
|
|
|
1398
1508
|
|
|
1399
1509
|
/**
|
|
1400
1510
|
* A single spec rendered by the API reference. `spec` is a local path or an
|
|
1401
|
-
* `http(s)` URL (OpenAPI
|
|
1511
|
+
* `http(s)` URL (an OpenAPI document under `openapi`, an AsyncAPI document
|
|
1512
|
+
* under `asyncapi`).
|
|
1402
1513
|
*/
|
|
1403
1514
|
const openapiSourceSchema = z.strictObject({
|
|
1404
1515
|
/** Include generated pages from this spec in llms.txt/llms-full.txt. */
|
|
@@ -1427,6 +1538,35 @@ export type OpenApiSource = z.input<typeof openapiSourceSchema>;
|
|
|
1427
1538
|
*/
|
|
1428
1539
|
const scalarConfigSchema = z.record(z.string(), z.unknown()).optional();
|
|
1429
1540
|
|
|
1541
|
+
/**
|
|
1542
|
+
* The shared shape of both API-reference blocks — only the mount route and
|
|
1543
|
+
* code-sample defaults differ per spec kind, so each block declares just
|
|
1544
|
+
* those.
|
|
1545
|
+
*/
|
|
1546
|
+
const referenceConfigSchema = (defaults: {
|
|
1547
|
+
codeSamples: string[];
|
|
1548
|
+
route: string;
|
|
1549
|
+
}) =>
|
|
1550
|
+
z.strictObject({
|
|
1551
|
+
/** Code-sample languages/tools shown per operation (Blume renderer). */
|
|
1552
|
+
codeSamples: z.array(z.string()).default(defaults.codeSamples),
|
|
1553
|
+
enabled: z.boolean().default(false),
|
|
1554
|
+
/** Start nested schema rows expanded rather than collapsed (Blume renderer). */
|
|
1555
|
+
expandSchemas: z.boolean().default(false),
|
|
1556
|
+
/** Who renders the reference: Blume's own UI, or the embedded Scalar SPA. */
|
|
1557
|
+
renderer: z.enum(["blume", "scalar"]).default("blume"),
|
|
1558
|
+
/** Where the reference mounts. */
|
|
1559
|
+
route: z.string().default(defaults.route),
|
|
1560
|
+
/** Extra Scalar config forwarded to `<ScalarComponent>` (Scalar renderer only). */
|
|
1561
|
+
scalar: scalarConfigSchema,
|
|
1562
|
+
/** One or more specs; each renders on its own route by default. */
|
|
1563
|
+
sources: z.array(openapiSourceSchema).default([]),
|
|
1564
|
+
/** Shorthand for a single source: `sources: [{ spec }]`. */
|
|
1565
|
+
spec: z.string().optional(),
|
|
1566
|
+
/** Scalar theme name (Scalar renderer only). */
|
|
1567
|
+
theme: z.string().optional(),
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1430
1570
|
/**
|
|
1431
1571
|
* OpenAPI reference. By default (`renderer: "blume"`) Blume parses the spec with
|
|
1432
1572
|
* Scalar's parser and renders its own UI: one real page per operation, grouped
|
|
@@ -1434,38 +1574,22 @@ const scalarConfigSchema = z.record(z.string(), z.unknown()).optional();
|
|
|
1434
1574
|
* `renderer: "scalar"` to fall back to the embedded Scalar SPA (a single
|
|
1435
1575
|
* self-contained route that doesn't weave into the sidebar or search).
|
|
1436
1576
|
*/
|
|
1437
|
-
const openapiConfigSchema =
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
enabled: z.boolean().default(false),
|
|
1441
|
-
/** Start nested schema rows expanded rather than collapsed (Blume renderer). */
|
|
1442
|
-
expandSchemas: z.boolean().default(false),
|
|
1443
|
-
/** Who renders the reference: Blume's own UI, or the embedded Scalar SPA. */
|
|
1444
|
-
renderer: z.enum(["blume", "scalar"]).default("blume"),
|
|
1445
|
-
/** Where the reference mounts. */
|
|
1446
|
-
route: z.string().default("/reference"),
|
|
1447
|
-
/** Extra Scalar config forwarded to `<ScalarComponent>` (Scalar renderer only). */
|
|
1448
|
-
scalar: scalarConfigSchema,
|
|
1449
|
-
/** One or more specs; each renders on its own route by default. */
|
|
1450
|
-
sources: z.array(openapiSourceSchema).default([]),
|
|
1451
|
-
/** Shorthand for a single source: `sources: [{ spec }]`. */
|
|
1452
|
-
spec: z.string().optional(),
|
|
1453
|
-
/** Scalar theme name (Scalar renderer only). */
|
|
1454
|
-
theme: z.string().optional(),
|
|
1577
|
+
const openapiConfigSchema = referenceConfigSchema({
|
|
1578
|
+
codeSamples: ["curl", "js", "python"],
|
|
1579
|
+
route: "/reference",
|
|
1455
1580
|
});
|
|
1456
1581
|
|
|
1457
1582
|
/**
|
|
1458
|
-
* AsyncAPI reference. Same shape
|
|
1459
|
-
* (
|
|
1583
|
+
* AsyncAPI reference. Same shape as {@link openapiConfigSchema}: by default
|
|
1584
|
+
* (`renderer: "blume"`) Blume normalizes the spec to AsyncAPI 3.x and renders
|
|
1585
|
+
* its own UI — one real page per operation — with `renderer: "scalar"` as the
|
|
1586
|
+
* embedded-SPA opt-out. Only the defaults differ: the reference mounts at
|
|
1587
|
+
* `/events`, and empty `codeSamples` means every tool the operation's protocol
|
|
1588
|
+
* binding suggests.
|
|
1460
1589
|
*/
|
|
1461
|
-
const asyncapiConfigSchema =
|
|
1462
|
-
|
|
1463
|
-
route:
|
|
1464
|
-
/** Extra Scalar config forwarded to `<ScalarComponent>`. */
|
|
1465
|
-
scalar: scalarConfigSchema,
|
|
1466
|
-
sources: z.array(openapiSourceSchema).default([]),
|
|
1467
|
-
spec: z.string().optional(),
|
|
1468
|
-
theme: z.string().optional(),
|
|
1590
|
+
const asyncapiConfigSchema = referenceConfigSchema({
|
|
1591
|
+
codeSamples: [],
|
|
1592
|
+
route: "/events",
|
|
1469
1593
|
});
|
|
1470
1594
|
|
|
1471
1595
|
/**
|
|
@@ -1500,7 +1624,7 @@ const tocConfigSchema = z
|
|
|
1500
1624
|
])
|
|
1501
1625
|
.default(true)
|
|
1502
1626
|
.transform((value) => {
|
|
1503
|
-
if (
|
|
1627
|
+
if (isBoolean(value)) {
|
|
1504
1628
|
return { enabled: value, maxLevel: 3, minLevel: 2 };
|
|
1505
1629
|
}
|
|
1506
1630
|
return {
|
|
@@ -1568,8 +1692,26 @@ export const blumeConfigSchema = z
|
|
|
1568
1692
|
theme: themeConfigSchema.prefault({}),
|
|
1569
1693
|
title: z.string().default("Documentation"),
|
|
1570
1694
|
toc: tocConfigSchema,
|
|
1695
|
+
versions: versionsConfigSchema.optional(),
|
|
1571
1696
|
})
|
|
1572
1697
|
.superRefine((config, ctx) => {
|
|
1698
|
+
// A version id that is also a configured locale code would make a leading
|
|
1699
|
+
// `<id>/` directory ambiguous between the two axes — refuse it outright so
|
|
1700
|
+
// detection order (version first, then locale) never has to guess.
|
|
1701
|
+
if (config.versions && config.i18n) {
|
|
1702
|
+
const localeCodes = new Set(
|
|
1703
|
+
config.i18n.locales.map((locale) => locale.code.toLowerCase())
|
|
1704
|
+
);
|
|
1705
|
+
for (const [position, version] of config.versions.archived.entries()) {
|
|
1706
|
+
if (localeCodes.has(version.id.toLowerCase())) {
|
|
1707
|
+
ctx.addIssue({
|
|
1708
|
+
code: z.ZodIssueCode.custom,
|
|
1709
|
+
message: `Version id "${version.id}" is also a configured locale code — rename the version (e.g. "v${version.id}").`,
|
|
1710
|
+
path: ["versions", "archived", position, "id"],
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1573
1715
|
// A custom key is declared site-wide (`frontmatter.extend`) or per-type
|
|
1574
1716
|
// (`content.types`), never both — two schemas for one key would make
|
|
1575
1717
|
// precedence on pages of that type ambiguous.
|
|
@@ -1613,6 +1755,10 @@ export type FrontmatterExtend = Record<string, StandardSchema>;
|
|
|
1613
1755
|
export type ResolvedI18nConfig = z.infer<typeof i18nConfigSchema>;
|
|
1614
1756
|
/** A configured locale with display metadata. */
|
|
1615
1757
|
export type LocaleConfig = z.infer<typeof localeSchema>;
|
|
1758
|
+
/** Resolved versions block (present only when the project opts into versioning). */
|
|
1759
|
+
export type ResolvedVersionsConfig = z.infer<typeof versionsConfigSchema>;
|
|
1760
|
+
/** A configured archived (frozen) version. */
|
|
1761
|
+
export type ArchivedVersionConfig = z.infer<typeof archivedVersionSchema>;
|
|
1616
1762
|
/**
|
|
1617
1763
|
* User-authored config, straight off the schema. The public, hand-documented
|
|
1618
1764
|
* authoring type is `BlumeConfig` in `./config-input.ts`, which a compile-time
|
|
@@ -78,6 +78,8 @@ export const materializeAssets = async (
|
|
|
78
78
|
await writeFile(join(ctx.assetsDir, file), bytes);
|
|
79
79
|
rewrites.set(url, `${ctx.assetsBaseUrl}/${file}`);
|
|
80
80
|
} catch (error) {
|
|
81
|
+
// SAFETY: everything thrown in this block is an Error — the manual
|
|
82
|
+
// `!res.ok` throw above, and fetch/fs failures.
|
|
81
83
|
diagnostics.push({
|
|
82
84
|
code: "BLUME_ASSET_FETCH_FAILED",
|
|
83
85
|
message: `Failed to download asset ${url}: ${(error as Error).message}`,
|
|
@@ -85,6 +85,9 @@ export const snapshotCache = (cacheDir: string): SnapshotCache => {
|
|
|
85
85
|
return {
|
|
86
86
|
read: async () => {
|
|
87
87
|
try {
|
|
88
|
+
// SAFETY: the snapshot file is only ever written by `write` below, from
|
|
89
|
+
// a `SourceEntry[]` via JSON.stringify; a corrupt file lands in the
|
|
90
|
+
// catch and reads as empty.
|
|
88
91
|
return JSON.parse(await readFile(file, "utf-8")) as SourceEntry[];
|
|
89
92
|
} catch {
|
|
90
93
|
return [];
|
|
@@ -126,6 +129,8 @@ export const loadWithCache = async (
|
|
|
126
129
|
} catch (error) {
|
|
127
130
|
const fallback = await cache.read();
|
|
128
131
|
if (fallback.length > 0) {
|
|
132
|
+
// SAFETY: everything thrown on this path is an Error — fetch rejects
|
|
133
|
+
// with a TypeError and the source adapters throw Error instances.
|
|
129
134
|
const diagnostic: Diagnostic = {
|
|
130
135
|
code: "BLUME_SOURCE_OFFLINE",
|
|
131
136
|
message: `Source "${name}" could not be fetched (${(error as Error).message}); served ${fallback.length} cached entries.`,
|
|
@@ -133,6 +138,7 @@ export const loadWithCache = async (
|
|
|
133
138
|
};
|
|
134
139
|
return { diagnostics: [diagnostic], entries: fallback };
|
|
135
140
|
}
|
|
141
|
+
// SAFETY: same invariant as above — `fetchEntries` failures are Errors.
|
|
136
142
|
throw new BlumeError({
|
|
137
143
|
code: "BLUME_SOURCE_FETCH_FAILED",
|
|
138
144
|
message: `Source "${name}" failed to load and no cache is available: ${(error as Error).message}`,
|
|
@@ -2,8 +2,10 @@ import { fromMarkdown } from "mdast-util-from-markdown";
|
|
|
2
2
|
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
3
3
|
import { toString as mdastToString } from "mdast-util-to-string";
|
|
4
4
|
import { gfm } from "micromark-extension-gfm";
|
|
5
|
+
import stringWidth from "string-width";
|
|
5
6
|
|
|
6
7
|
import matter from "../frontmatter.ts";
|
|
8
|
+
import { columnsPrefix } from "../text-width.ts";
|
|
7
9
|
import {
|
|
8
10
|
hashText,
|
|
9
11
|
loadWithCache,
|
|
@@ -61,9 +63,10 @@ const LEADING_V = /^v/iu;
|
|
|
61
63
|
const NON_SLUG = /[^a-z0-9]+/gu;
|
|
62
64
|
const EDGE_DASHES = /^-+|-+$/gu;
|
|
63
65
|
|
|
64
|
-
// `blume audit` grades meta descriptions against the 110–160
|
|
65
|
-
// snippet range (audit/types.ts thresholds), so the derived summary
|
|
66
|
-
// the longest word-boundary cut
|
|
66
|
+
// `blume audit` grades meta descriptions against the 110–160 display-column
|
|
67
|
+
// search snippet range (audit/types.ts thresholds), so the derived summary
|
|
68
|
+
// budgets in the same columns and aims for the longest word-boundary cut
|
|
69
|
+
// under the cap.
|
|
67
70
|
const DESCRIPTION_MAX = 160;
|
|
68
71
|
const DESCRIPTION_MIN = 110;
|
|
69
72
|
|
|
@@ -85,26 +88,6 @@ const NON_PROSE = new Set(["code", "heading", "html", "thematicBreak"]);
|
|
|
85
88
|
* content kept — then cut at a word boundary to fit the search snippet cap.
|
|
86
89
|
* Undefined when the notes have no prose at all.
|
|
87
90
|
*/
|
|
88
|
-
/**
|
|
89
|
-
* The longest prefix of `text` that fits `max` UTF-16 units without cutting
|
|
90
|
-
* inside a grapheme cluster. A bare `String#slice` counts code units, so it
|
|
91
|
-
* can split a surrogate pair (emitting a lone surrogate — invalid Unicode —
|
|
92
|
-
* into a meta description) or halve an emoji sequence. Grapheme segmentation
|
|
93
|
-
* is rule-based (UAX #29), so unlike word segmentation it does not drift
|
|
94
|
-
* across ICU builds.
|
|
95
|
-
*/
|
|
96
|
-
const graphemePrefix = (text: string, max: number): string => {
|
|
97
|
-
let end = 0;
|
|
98
|
-
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
99
|
-
for (const { index, segment } of graphemes.segment(text)) {
|
|
100
|
-
if (index + segment.length > max) {
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
end = index + segment.length;
|
|
104
|
-
}
|
|
105
|
-
return text.slice(0, end);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
91
|
const releaseDescription = (body: string): string | undefined => {
|
|
109
92
|
const tree = fromMarkdown(body.replaceAll(CHANGESET_HASH, "$<mark>"), {
|
|
110
93
|
extensions: [gfm()],
|
|
@@ -123,15 +106,17 @@ const releaseDescription = (body: string): string | undefined => {
|
|
|
123
106
|
if (!text) {
|
|
124
107
|
return undefined;
|
|
125
108
|
}
|
|
126
|
-
if (text
|
|
109
|
+
if (stringWidth(text) <= DESCRIPTION_MAX) {
|
|
127
110
|
return text;
|
|
128
111
|
}
|
|
129
112
|
// Cut before the cap at a word boundary (kept only when it doesn't drop the
|
|
130
113
|
// summary under the minimum), shed any dangling punctuation, and mark the cut.
|
|
131
|
-
const slice =
|
|
114
|
+
const slice = columnsPrefix(text, DESCRIPTION_MAX - 1);
|
|
132
115
|
const boundary = slice.lastIndexOf(" ");
|
|
133
116
|
const head = (
|
|
134
|
-
boundary
|
|
117
|
+
boundary !== -1 && stringWidth(slice.slice(0, boundary)) >= DESCRIPTION_MIN
|
|
118
|
+
? slice.slice(0, boundary)
|
|
119
|
+
: slice
|
|
135
120
|
).replace(TRAILING_FRAGMENT, "");
|
|
136
121
|
return `${head}…`;
|
|
137
122
|
};
|
|
@@ -150,6 +135,19 @@ const githubHeaders = (): Headers => {
|
|
|
150
135
|
return headers;
|
|
151
136
|
};
|
|
152
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The changelog frontmatter one release lowers to. `title`/`type` are always
|
|
140
|
+
* assigned (optional only so assignment order can keep the emitted YAML key
|
|
141
|
+
* order — and so each entry's content hash — stable).
|
|
142
|
+
*/
|
|
143
|
+
interface ChangelogFrontmatter {
|
|
144
|
+
changelog: { category: string; version: string };
|
|
145
|
+
date: string;
|
|
146
|
+
seo?: { description: string };
|
|
147
|
+
title?: string;
|
|
148
|
+
type?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
153
151
|
/**
|
|
154
152
|
* Lower one release to a staged Markdown entry: the notes become the body,
|
|
155
153
|
* `type: changelog` frontmatter (title/date/version/category) drives the
|
|
@@ -166,19 +164,25 @@ const releaseToEntry = (release: GithubRelease): SourceEntry => {
|
|
|
166
164
|
// description (instead of the site-wide fallback) without also rendering the
|
|
167
165
|
// visible lede paragraph a top-level `description` would add.
|
|
168
166
|
const description = releaseDescription(body);
|
|
169
|
-
|
|
167
|
+
// Assignment order matters: js-yaml serializes keys in insertion order, so
|
|
168
|
+
// `seo` lands between `date` and `title` exactly as it always has.
|
|
169
|
+
const data: ChangelogFrontmatter = {
|
|
170
170
|
changelog: { category, version },
|
|
171
171
|
date,
|
|
172
|
-
...(description ? { seo: { description } } : {}),
|
|
173
|
-
title,
|
|
174
|
-
type: "changelog",
|
|
175
172
|
};
|
|
173
|
+
if (description) {
|
|
174
|
+
data.seo = { description };
|
|
175
|
+
}
|
|
176
|
+
data.title = title;
|
|
177
|
+
data.type = "changelog";
|
|
176
178
|
const raw = matter.stringify(`${body}\n`, data);
|
|
177
179
|
const fallbackRef = `release-${release.id}`;
|
|
178
180
|
const ref = `${slugifyTag(release.tag_name) || fallbackRef}.md`;
|
|
179
181
|
return {
|
|
180
182
|
body: { format: "md", text: body },
|
|
181
|
-
data,
|
|
183
|
+
// Spread: `SourceEntry.data` is an open dictionary, which the interface
|
|
184
|
+
// (no index signature) only satisfies as a fresh object literal.
|
|
185
|
+
data: { ...data },
|
|
182
186
|
editUrl: release.html_url,
|
|
183
187
|
hash: hashText(raw),
|
|
184
188
|
lastModified: date,
|
|
@@ -214,6 +218,8 @@ export const githubReleasesSource = (
|
|
|
214
218
|
if (!res.ok) {
|
|
215
219
|
throw new Error(`${url} -> ${res.status}`);
|
|
216
220
|
}
|
|
221
|
+
// SAFETY: GitHub's releases endpoint returns a JSON array of release
|
|
222
|
+
// objects; `GithubRelease` models only the fields the adapter reads.
|
|
217
223
|
return (await res.json()) as GithubRelease[];
|
|
218
224
|
};
|
|
219
225
|
|
|
@@ -253,6 +259,8 @@ export const githubReleasesSource = (
|
|
|
253
259
|
// repo), degrade to an empty changelog with a warning rather than failing
|
|
254
260
|
// the whole build.
|
|
255
261
|
snapshot = new Map();
|
|
262
|
+
// SAFETY: everything thrown on this path is an Error — fetch rejects
|
|
263
|
+
// with a TypeError, fetchPage and loadWithCache throw Error instances.
|
|
256
264
|
return {
|
|
257
265
|
diagnostics: [
|
|
258
266
|
{
|
|
@@ -82,6 +82,8 @@ const enumerateGithub = async (
|
|
|
82
82
|
if (!res.ok) {
|
|
83
83
|
throw new Error(`${treeUrl} -> ${res.status}`);
|
|
84
84
|
}
|
|
85
|
+
// SAFETY: GitHub's git/trees endpoint returns this envelope; a missing or
|
|
86
|
+
// differently-typed field falls through the `?? []` and blob filters below.
|
|
85
87
|
const body = (await res.json()) as {
|
|
86
88
|
tree?: GithubTreeEntry[];
|
|
87
89
|
truncated?: boolean;
|
|
@@ -198,6 +200,8 @@ export const mdxRemoteSource = (
|
|
|
198
200
|
} catch (error) {
|
|
199
201
|
skipped.push({
|
|
200
202
|
code: "BLUME_SOURCE_FETCH_FAILED",
|
|
203
|
+
// SAFETY: fetch and decode failures throw Error instances;
|
|
204
|
+
// only the message is read for the skip diagnostic.
|
|
201
205
|
message: `Source "${options.name}" skipped "${ref.ref}" (${(error as Error).message}); the rest were imported.`,
|
|
202
206
|
severity: "warning",
|
|
203
207
|
});
|