blume 1.4.2 → 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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -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 +116 -15
- package/dist/types/core/sources/types.d.ts +11 -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 +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- 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/sources.mdx +1 -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 +13 -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 +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- 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 +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- 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/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- 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/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- 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/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- 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 +8 -5
- package/src/components/layout/search/types.ts +45 -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/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -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 +74 -4
- 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/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- 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 +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- 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 +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- 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 +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* Markdown text that flows through Blume's normal pipeline.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A field value on a Portable Text node: the arbitrary JSON the CMS query
|
|
10
|
+
* returned (custom blocks carry whatever fields the studio schema defines).
|
|
11
|
+
* Spans and mark defs appear as members so the block's typed fields conform
|
|
12
|
+
* to its index signature.
|
|
13
|
+
*/
|
|
14
|
+
export type PortableTextValue =
|
|
15
|
+
| string
|
|
16
|
+
| number
|
|
17
|
+
| boolean
|
|
18
|
+
| null
|
|
19
|
+
| undefined
|
|
20
|
+
| PortableTextSpan
|
|
21
|
+
| PortableTextMarkDef
|
|
22
|
+
| PortableTextValue[]
|
|
23
|
+
| { [key: string]: PortableTextValue };
|
|
24
|
+
|
|
8
25
|
/** A single Portable Text node (block, image, or a custom type). */
|
|
9
26
|
export interface PortableTextBlock {
|
|
10
27
|
_type: string;
|
|
@@ -14,7 +31,7 @@ export interface PortableTextBlock {
|
|
|
14
31
|
level?: number;
|
|
15
32
|
children?: PortableTextSpan[];
|
|
16
33
|
markDefs?: PortableTextMarkDef[];
|
|
17
|
-
[key: string]:
|
|
34
|
+
[key: string]: PortableTextValue;
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
interface PortableTextSpan {
|
|
@@ -36,21 +53,36 @@ export interface PortableTextOptions {
|
|
|
36
53
|
serializers?: Record<string, (block: PortableTextBlock) => string>;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
const HEADING_STYLES
|
|
40
|
-
h1
|
|
41
|
-
h2
|
|
42
|
-
h3
|
|
43
|
-
h4
|
|
44
|
-
h5
|
|
45
|
-
h6
|
|
46
|
-
|
|
56
|
+
const HEADING_STYLES = new Map([
|
|
57
|
+
["h1", "# "],
|
|
58
|
+
["h2", "## "],
|
|
59
|
+
["h3", "### "],
|
|
60
|
+
["h4", "#### "],
|
|
61
|
+
["h5", "##### "],
|
|
62
|
+
["h6", "###### "],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
// Markdown/raw-HTML structure characters. Portable Text spans are *plain
|
|
66
|
+
// text* — formatting arrives as marks, never as syntax in the text — so a
|
|
67
|
+
// literal `*`, `_`, `[`, backtick, `~`, or `<` typed in the CMS must render
|
|
68
|
+
// as itself. Unescaped, it opened emphasis or a code span mid-paragraph, and
|
|
69
|
+
// `<` let CMS prose inject raw HTML into the rendered page. CommonMark
|
|
70
|
+
// backslash-escapes every ASCII punctuation character, so `\*` is always the
|
|
71
|
+
// literal asterisk.
|
|
72
|
+
const MARKDOWN_SPECIALS = /[\\`*_[\]~<]/gu;
|
|
73
|
+
|
|
74
|
+
const escapeText = (text: string): string =>
|
|
75
|
+
text.replaceAll(MARKDOWN_SPECIALS, String.raw`\$&`);
|
|
47
76
|
|
|
48
77
|
/** Wrap a span's text in Markdown for its marks (decorators + link defs). */
|
|
49
78
|
const renderSpan = (
|
|
50
79
|
span: PortableTextSpan,
|
|
51
80
|
defs: Map<string, PortableTextMarkDef>
|
|
52
81
|
): string => {
|
|
53
|
-
|
|
82
|
+
// Code spans stay verbatim: their text is literal inside the backticks,
|
|
83
|
+
// and backslash escapes would render as backslashes.
|
|
84
|
+
const isCode = span.marks?.includes("code") ?? false;
|
|
85
|
+
let text = isCode ? (span.text ?? "") : escapeText(span.text ?? "");
|
|
54
86
|
if (!span.marks || span.marks.length === 0) {
|
|
55
87
|
return text;
|
|
56
88
|
}
|
|
@@ -93,6 +125,10 @@ const renderChildren = (block: PortableTextBlock): string => {
|
|
|
93
125
|
return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
|
|
94
126
|
};
|
|
95
127
|
|
|
128
|
+
/** Whether an image block's `alt` field is usable alt text (CMS JSON may hold anything). */
|
|
129
|
+
const isAltText = (value: PortableTextValue): value is string =>
|
|
130
|
+
typeof value === "string";
|
|
131
|
+
|
|
96
132
|
const renderBlock = (
|
|
97
133
|
block: PortableTextBlock,
|
|
98
134
|
options: PortableTextOptions
|
|
@@ -103,7 +139,7 @@ const renderBlock = (
|
|
|
103
139
|
}
|
|
104
140
|
if (block._type === "image") {
|
|
105
141
|
const url = options.imageUrl?.(block);
|
|
106
|
-
const alt =
|
|
142
|
+
const alt = isAltText(block.alt) ? block.alt : "";
|
|
107
143
|
return url ? `` : "";
|
|
108
144
|
}
|
|
109
145
|
if (block._type !== "block") {
|
|
@@ -119,7 +155,7 @@ const renderBlock = (
|
|
|
119
155
|
if (block.style === "blockquote") {
|
|
120
156
|
return `> ${inline}`;
|
|
121
157
|
}
|
|
122
|
-
return `${HEADING_STYLES
|
|
158
|
+
return `${HEADING_STYLES.get(block.style ?? "normal") ?? ""}${inline}`;
|
|
123
159
|
};
|
|
124
160
|
|
|
125
161
|
/** Serialize a Portable Text array into a Markdown string. */
|
|
@@ -59,12 +59,44 @@ export interface SanitySourceOptions {
|
|
|
59
59
|
|
|
60
60
|
const IMAGE_REF = /^image-(?<id>[a-f0-9]+)-(?<dims>\d+x\d+)-(?<ext>\w+)$/u;
|
|
61
61
|
|
|
62
|
+
/** A value in a fetched document: arbitrary JSON the GROQ query returned. */
|
|
63
|
+
type SanityValue =
|
|
64
|
+
| string
|
|
65
|
+
| number
|
|
66
|
+
| boolean
|
|
67
|
+
| null
|
|
68
|
+
| SanityValue[]
|
|
69
|
+
| { [key: string]: SanityValue };
|
|
70
|
+
|
|
71
|
+
/** A document as the GROQ query returns it. */
|
|
72
|
+
interface SanityDocument {
|
|
73
|
+
[key: string]: SanityValue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The frontmatter fields this adapter maps from a document. */
|
|
77
|
+
interface SanityFrontmatter {
|
|
78
|
+
description?: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A node a dot path can descend into. Arrays pass too (matching their runtime
|
|
84
|
+
* string-key indexing, e.g. `items.0`), so the predicate types them as the
|
|
85
|
+
* keyed form both traverse through.
|
|
86
|
+
*/
|
|
87
|
+
const isTraversable = (
|
|
88
|
+
value: SanityValue | undefined
|
|
89
|
+
): value is SanityDocument => typeof value === "object" && value !== null;
|
|
90
|
+
|
|
62
91
|
/** Resolve a dot path (`slug.current`) against a document. */
|
|
63
|
-
const getPath = (
|
|
64
|
-
|
|
92
|
+
const getPath = (
|
|
93
|
+
doc: SanityDocument,
|
|
94
|
+
path: string
|
|
95
|
+
): SanityValue | undefined => {
|
|
96
|
+
let current: SanityValue | undefined = doc;
|
|
65
97
|
for (const key of path.split(".")) {
|
|
66
|
-
if (current
|
|
67
|
-
current =
|
|
98
|
+
if (isTraversable(current)) {
|
|
99
|
+
current = current[key];
|
|
68
100
|
} else {
|
|
69
101
|
return;
|
|
70
102
|
}
|
|
@@ -72,8 +104,11 @@ const getPath = (doc: Record<string, unknown>, path: string): unknown => {
|
|
|
72
104
|
return current;
|
|
73
105
|
};
|
|
74
106
|
|
|
75
|
-
const
|
|
76
|
-
typeof value === "string"
|
|
107
|
+
const isStringValue = (value: SanityValue | undefined): value is string =>
|
|
108
|
+
typeof value === "string";
|
|
109
|
+
|
|
110
|
+
const asString = (value: SanityValue | undefined): string | undefined =>
|
|
111
|
+
isStringValue(value) ? value : undefined;
|
|
77
112
|
|
|
78
113
|
/** Build a Sanity CDN URL from an image asset `_ref`. */
|
|
79
114
|
const imageUrlFromRef = (
|
|
@@ -89,6 +124,16 @@ const imageUrlFromRef = (
|
|
|
89
124
|
return `https://cdn.sanity.io/images/${projectId}/${dataset}/${id}-${dims}.${ext}`;
|
|
90
125
|
};
|
|
91
126
|
|
|
127
|
+
/** The `createClient` config slice this adapter passes. */
|
|
128
|
+
interface SanityClientConfig {
|
|
129
|
+
apiVersion: string;
|
|
130
|
+
dataset: string;
|
|
131
|
+
perspective: "previewDrafts" | "published";
|
|
132
|
+
projectId: string;
|
|
133
|
+
token: string | undefined;
|
|
134
|
+
useCdn: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
92
137
|
const resolveClient = async (
|
|
93
138
|
options: SanitySourceOptions,
|
|
94
139
|
preview: boolean
|
|
@@ -96,10 +141,13 @@ const resolveClient = async (
|
|
|
96
141
|
if (options.client) {
|
|
97
142
|
return options.client;
|
|
98
143
|
}
|
|
99
|
-
let createClient: (config:
|
|
144
|
+
let createClient: (config: SanityClientConfig) => SanityClientLike;
|
|
100
145
|
try {
|
|
146
|
+
// SAFETY: `@sanity/client` is an optional dependency; its `createClient`
|
|
147
|
+
// accepts a superset of this config slice and returns a client exposing
|
|
148
|
+
// the `fetch` method this adapter uses.
|
|
101
149
|
({ createClient } = (await import("@sanity/client")) as {
|
|
102
|
-
createClient: (config:
|
|
150
|
+
createClient: (config: SanityClientConfig) => SanityClientLike;
|
|
103
151
|
});
|
|
104
152
|
} catch {
|
|
105
153
|
throw new BlumeError({
|
|
@@ -135,7 +183,7 @@ export const sanitySource = (
|
|
|
135
183
|
);
|
|
136
184
|
let snapshot = new Map<string, SourceEntry>();
|
|
137
185
|
|
|
138
|
-
const toEntry = (doc:
|
|
186
|
+
const toEntry = (doc: SanityDocument): SourceEntry => {
|
|
139
187
|
const slugValue =
|
|
140
188
|
asString(getPath(doc, fields.slug ?? "slug.current")) ??
|
|
141
189
|
asString(doc._id) ??
|
|
@@ -148,7 +196,7 @@ export const sanitySource = (
|
|
|
148
196
|
const slug =
|
|
149
197
|
slugifyPath(slugValue) || slugify(asString(doc._id) ?? "") || "untitled";
|
|
150
198
|
|
|
151
|
-
const data:
|
|
199
|
+
const data: SanityFrontmatter = {};
|
|
152
200
|
const title = asString(getPath(doc, fields.title ?? "title"));
|
|
153
201
|
const description = asString(
|
|
154
202
|
getPath(doc, fields.description ?? "description")
|
|
@@ -160,11 +208,17 @@ export const sanitySource = (
|
|
|
160
208
|
data.description = description;
|
|
161
209
|
}
|
|
162
210
|
|
|
211
|
+
// SAFETY: the configured body field holds Portable Text blocks; a value of
|
|
212
|
+
// any other shape fails the `Array.isArray` check below and yields an
|
|
213
|
+
// empty body, and the serializer tolerates malformed blocks.
|
|
163
214
|
const blocks = (getPath(doc, fields.body ?? "body") ??
|
|
164
215
|
[]) as PortableTextBlock[];
|
|
165
216
|
const markdown = Array.isArray(blocks)
|
|
166
217
|
? portableTextToMarkdown(blocks, {
|
|
167
218
|
imageUrl: (block) => {
|
|
219
|
+
// SAFETY: `asset` on an image block is Sanity's asset reference
|
|
220
|
+
// object; any other shape yields no `_ref` and the image is
|
|
221
|
+
// skipped.
|
|
168
222
|
const ref = (block.asset as { _ref?: string } | undefined)?._ref;
|
|
169
223
|
return ref
|
|
170
224
|
? imageUrlFromRef(ref, options.projectId, options.dataset)
|
|
@@ -177,7 +231,9 @@ export const sanitySource = (
|
|
|
177
231
|
const raw = matter.stringify(markdown, data);
|
|
178
232
|
return {
|
|
179
233
|
body: { format: "md", text: markdown },
|
|
180
|
-
data
|
|
234
|
+
// Spread into a fresh literal: `SourceEntry.data` wants an
|
|
235
|
+
// index-signature type, which the named interface lacks.
|
|
236
|
+
data: { ...data },
|
|
181
237
|
hash: hashText(raw),
|
|
182
238
|
lastModified: asString(getPath(doc, fields.lastModified ?? "_updatedAt")),
|
|
183
239
|
raw,
|
|
@@ -193,9 +249,7 @@ export const sanitySource = (
|
|
|
193
249
|
cache,
|
|
194
250
|
async () => {
|
|
195
251
|
const client = await resolveClient(options, ctx?.preview ?? false);
|
|
196
|
-
const docs = await client.fetch<
|
|
197
|
-
options.query
|
|
198
|
-
);
|
|
252
|
+
const docs = await client.fetch<SanityDocument[]>(options.query);
|
|
199
253
|
return docs.map(toEntry);
|
|
200
254
|
},
|
|
201
255
|
refresh
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FolderMeta,
|
|
3
|
+
FrontmatterExtend,
|
|
4
|
+
ResolvedI18nConfig,
|
|
5
|
+
ResolvedVersionsConfig,
|
|
6
|
+
} from "../schema.ts";
|
|
2
7
|
import type { Diagnostic } from "../types.ts";
|
|
3
8
|
|
|
4
9
|
/**
|
|
@@ -12,6 +17,7 @@ export interface SourceEntry {
|
|
|
12
17
|
/** Logical route input; defaults to `ref` if omitted. May include slashes. */
|
|
13
18
|
slug?: string;
|
|
14
19
|
/** Frontmatter-equivalent metadata, validated against the Blume meta schema. */
|
|
20
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- pre-validation frontmatter from YAML/CMS payloads; the meta schema parses it downstream
|
|
15
21
|
data: Record<string, unknown>;
|
|
16
22
|
/** The renderable body as Markdown/MDX source text (frontmatter stripped). */
|
|
17
23
|
body: { format: "md" | "mdx"; text: string };
|
|
@@ -40,6 +46,14 @@ export interface SourceLoadResult {
|
|
|
40
46
|
entries: SourceEntry[];
|
|
41
47
|
/** Source-level diagnostics (e.g. an offline cache fallback warning). */
|
|
42
48
|
diagnostics: Diagnostic[];
|
|
49
|
+
/**
|
|
50
|
+
* Folder meta the source derives for the sidebar groups its entries create,
|
|
51
|
+
* keyed by locale-stripped group path (the `meta.ts` key space). The OpenAPI
|
|
52
|
+
* source labels each tag directory with the spec's own tag name, so the
|
|
53
|
+
* sidebar shows `OAuth2`/`Größe` instead of a re-humanized slug. Merged
|
|
54
|
+
* beneath user-authored meta files, which always win.
|
|
55
|
+
*/
|
|
56
|
+
folderMeta?: Record<string, FolderMeta>;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
@@ -117,4 +131,6 @@ export interface NormalizeContext {
|
|
|
117
131
|
* applied to a page only when its resolved `type` matches.
|
|
118
132
|
*/
|
|
119
133
|
typeFrontmatter?: Record<string, FrontmatterExtend>;
|
|
134
|
+
/** Docs versioning config; a leading archived-version dir becomes the page's version. */
|
|
135
|
+
versions?: ResolvedVersionsConfig;
|
|
120
136
|
}
|
|
@@ -71,7 +71,7 @@ export const ignoringWatchListener = (
|
|
|
71
71
|
const ignore = new Set(ignoreDirs);
|
|
72
72
|
return (_event, filename) => {
|
|
73
73
|
if (
|
|
74
|
-
|
|
74
|
+
filename !== null &&
|
|
75
75
|
filename.split(/[/\\]/u).some((segment) => ignore.has(segment))
|
|
76
76
|
) {
|
|
77
77
|
return;
|
|
@@ -38,7 +38,7 @@ export interface StandardSchema<Input = unknown, Output = Input> {
|
|
|
38
38
|
readonly version: 1;
|
|
39
39
|
readonly vendor: string;
|
|
40
40
|
readonly validate: (
|
|
41
|
-
value:
|
|
41
|
+
value: Input
|
|
42
42
|
) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
43
43
|
readonly types?:
|
|
44
44
|
| { readonly input: Input; readonly output: Output }
|
|
@@ -46,9 +46,15 @@ export interface StandardSchema<Input = unknown, Output = Input> {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Whether a config-supplied value implements the `~standard` contract. Generic
|
|
51
|
+
* so it can decode any input at the config boundary (`z.custom` hands it the
|
|
52
|
+
* raw config value) while narrowing whatever type the caller holds.
|
|
53
|
+
*/
|
|
54
|
+
export const isStandardSchema = <T>(value: T): value is T & StandardSchema =>
|
|
51
55
|
typeof value === "object" &&
|
|
52
56
|
value !== null &&
|
|
57
|
+
// SAFETY: the assertion only widens the checked object for property probing;
|
|
58
|
+
// the trailing typeof check is what verifies `~standard.validate` exists.
|
|
53
59
|
typeof (value as { "~standard"?: { validate?: unknown } })["~standard"]
|
|
54
60
|
?.validate === "function";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The longest prefix of `text` that renders within `max` display columns
|
|
5
|
+
* without cutting inside a grapheme cluster. Columns are `string-width`'s: a
|
|
6
|
+
* fullwidth or wide character counts 2, a combining mark 0, everything else 1
|
|
7
|
+
* — the same measure `blume audit` grades titles and meta descriptions with,
|
|
8
|
+
* so text cut here stays inside the audit's thresholds for every script, not
|
|
9
|
+
* just Latin. Grapheme segmentation is rule-based (UAX #29), so unlike word
|
|
10
|
+
* segmentation it does not drift across ICU builds, and it can never split a
|
|
11
|
+
* surrogate pair or halve an emoji sequence.
|
|
12
|
+
*/
|
|
13
|
+
export const columnsPrefix = (text: string, max: number): string => {
|
|
14
|
+
let end = 0;
|
|
15
|
+
let used = 0;
|
|
16
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
17
|
+
for (const { index, segment } of graphemes.segment(text)) {
|
|
18
|
+
const width = stringWidth(segment);
|
|
19
|
+
if (used + width > max) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
used += width;
|
|
23
|
+
end = index + segment.length;
|
|
24
|
+
}
|
|
25
|
+
return text.slice(0, end);
|
|
26
|
+
};
|
|
@@ -30,16 +30,20 @@ const substituteConfigDir = (value: string, configDir: string): string =>
|
|
|
30
30
|
? join(configDir, value.slice(CONFIG_DIR_TEMPLATE.length))
|
|
31
31
|
: value;
|
|
32
32
|
|
|
33
|
+
/** Whether a `paths` fallback entry is a usable target (raw JSONC may lie). */
|
|
34
|
+
const isPathTarget = (target: string | undefined): target is string =>
|
|
35
|
+
typeof target === "string";
|
|
36
|
+
|
|
33
37
|
/** Convert one tsconfig `paths` mapping to a Vite alias, or null to skip. */
|
|
34
38
|
const toAlias = (
|
|
35
39
|
key: string,
|
|
36
|
-
value:
|
|
40
|
+
value: string | string[],
|
|
37
41
|
baseDir: string,
|
|
38
42
|
configDir: string
|
|
39
43
|
): { find: string; replacement: string } | null => {
|
|
40
44
|
// tsconfig allows a fallback array; Vite aliases are 1:1, so take the first.
|
|
41
45
|
const first = Array.isArray(value) ? value[0] : value;
|
|
42
|
-
if (
|
|
46
|
+
if (!isPathTarget(first)) {
|
|
43
47
|
return null;
|
|
44
48
|
}
|
|
45
49
|
const find = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
@@ -86,12 +90,12 @@ export const resolveTsconfigAliases = (
|
|
|
86
90
|
configDir,
|
|
87
91
|
substituteConfigDir(options?.baseUrl ?? ".", configDir)
|
|
88
92
|
);
|
|
89
|
-
const
|
|
93
|
+
const entries: [string, string][] = [];
|
|
90
94
|
for (const [key, value] of Object.entries(paths)) {
|
|
91
95
|
const alias = toAlias(key, value, baseDir, configDir);
|
|
92
96
|
if (alias) {
|
|
93
|
-
|
|
97
|
+
entries.push([alias.find, alias.replacement]);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
|
-
return
|
|
100
|
+
return Object.fromEntries(entries);
|
|
97
101
|
};
|
package/src/core/types.ts
CHANGED
|
@@ -121,9 +121,24 @@ export interface PageRecord {
|
|
|
121
121
|
locale: string;
|
|
122
122
|
/**
|
|
123
123
|
* Locale-agnostic logical route shared by every translation (e.g.
|
|
124
|
-
* `/guides/x`
|
|
124
|
+
* `/guides/x`, or `/v1.0/guides/x` under versioning — the key is
|
|
125
|
+
* version-specific, so translations group within their version).
|
|
126
|
+
* Pages with the same key are translations of each other.
|
|
125
127
|
*/
|
|
126
128
|
translationKey: string;
|
|
129
|
+
/**
|
|
130
|
+
* Resolved docs version: an archived id (`v1.0`) for snapshot pages, `""`
|
|
131
|
+
* for the current (unprefixed) docs — including every page of an
|
|
132
|
+
* unversioned project.
|
|
133
|
+
*/
|
|
134
|
+
version: string;
|
|
135
|
+
/**
|
|
136
|
+
* Version- and locale-agnostic logical route (e.g. `/guides/x` for
|
|
137
|
+
* `/v1.0/guides/x`). Pages with the same key and locale are the same
|
|
138
|
+
* logical page across versions — this drives the switcher's same-page
|
|
139
|
+
* navigation and the canonical-to-latest lookup.
|
|
140
|
+
*/
|
|
141
|
+
versionKey: string;
|
|
127
142
|
/**
|
|
128
143
|
* True for entries filled in from the fallback locale to pad a locale's
|
|
129
144
|
* navigation for pages it hasn't translated yet. The record's content —
|
|
@@ -151,6 +166,7 @@ export interface PageRecord {
|
|
|
151
166
|
* schemas (schema output, so transforms apply). Present only when the
|
|
152
167
|
* project opts in and the page carries at least one value.
|
|
153
168
|
*/
|
|
169
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- values are outputs of arbitrary user-supplied zod schemas (transforms included), so no narrower type exists
|
|
154
170
|
custom?: Record<string, unknown>;
|
|
155
171
|
headings: Heading[];
|
|
156
172
|
/** Whether the file is `.md`/`.mdx`. */
|
|
@@ -180,7 +196,12 @@ export type NavNode =
|
|
|
180
196
|
label: string;
|
|
181
197
|
badge?: string;
|
|
182
198
|
directory?: DirectoryMode;
|
|
183
|
-
|
|
199
|
+
/**
|
|
200
|
+
* The group's resolved render mode. Always stamped by both builders
|
|
201
|
+
* (generated and explicit-config sidebars), so the renderer can rely
|
|
202
|
+
* on it per node.
|
|
203
|
+
*/
|
|
204
|
+
display: SidebarDisplay;
|
|
184
205
|
icon?: string;
|
|
185
206
|
route?: string;
|
|
186
207
|
/**
|
|
@@ -266,6 +287,12 @@ export interface ContentGraph {
|
|
|
266
287
|
navigation: Navigation;
|
|
267
288
|
/** Navigation per locale; one entry per configured locale under i18n. */
|
|
268
289
|
navigationByLocale: Record<string, Navigation>;
|
|
290
|
+
/**
|
|
291
|
+
* Navigation per archived version, keyed by version id and then locale code
|
|
292
|
+
* (`""` on a single-locale site). The current version's trees are
|
|
293
|
+
* `navigation`/`navigationByLocale`; empty when versioning is off.
|
|
294
|
+
*/
|
|
295
|
+
navigationByVersion: Record<string, Record<string, Navigation>>;
|
|
269
296
|
/** Map of route -> pageId for fast lookup and duplicate detection. */
|
|
270
297
|
routes: Map<string, string>;
|
|
271
298
|
diagnostics: Diagnostic[];
|
|
@@ -289,6 +316,13 @@ export interface LocaleSwitchOption {
|
|
|
289
316
|
untranslated: boolean;
|
|
290
317
|
}
|
|
291
318
|
|
|
319
|
+
/** A docs version a logical page exists in, for the switcher and canonicals. */
|
|
320
|
+
export interface VersionAlternate {
|
|
321
|
+
/** Version id; `""` for the current (unprefixed) docs. */
|
|
322
|
+
version: string;
|
|
323
|
+
path: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
/** A route entry written to `blume.manifest.json`. */
|
|
293
327
|
export interface RouteManifestEntry {
|
|
294
328
|
id: string;
|
|
@@ -313,6 +347,15 @@ export interface RouteManifestEntry {
|
|
|
313
347
|
locale: string;
|
|
314
348
|
/** Locales this logical page is genuinely translated into (excludes fallbacks). */
|
|
315
349
|
alternates: RouteAlternate[];
|
|
350
|
+
/** Resolved docs version (`""` for the current docs; see `PageRecord.version`). */
|
|
351
|
+
version: string;
|
|
352
|
+
/**
|
|
353
|
+
* Versions this logical page exists in within this route's locale — the
|
|
354
|
+
* current version first, then archived versions in configured order. Drives
|
|
355
|
+
* the switcher's same-page navigation and the canonical-to-latest lookup.
|
|
356
|
+
* Empty when versioning is off.
|
|
357
|
+
*/
|
|
358
|
+
versionAlternates: VersionAlternate[];
|
|
316
359
|
/** True when this route renders fallback content for a missing translation. */
|
|
317
360
|
fallback?: boolean;
|
|
318
361
|
/** Resolved "last updated" ISO date, when the feature is enabled. */
|
|
@@ -47,7 +47,15 @@ import { zh } from "./zh.ts";
|
|
|
47
47
|
* `zh-TW`). A locale's pack is merged onto English by {@link resolveUIStrings},
|
|
48
48
|
* so a pack only needs the keys it translates.
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Starter packs keyed by locale code. Open-keyed on purpose: resolvers look
|
|
52
|
+
* packs up by whatever locale a project configures, not only the codes here.
|
|
53
|
+
*/
|
|
54
|
+
interface UiPackRegistry {
|
|
55
|
+
[locale: string]: UIStringsOverride;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const UI_PACKS: UiPackRegistry = {
|
|
51
59
|
ar,
|
|
52
60
|
bg,
|
|
53
61
|
bn,
|