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
package/src/core/data.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { UIStrings } from "./i18n-ui.ts";
|
|
2
2
|
import type { ResolvedConfig, SearchProvider } from "./schema.ts";
|
|
3
|
-
import type { Navigation, RouteAlternate } from "./types.ts";
|
|
3
|
+
import type { Navigation, RouteAlternate, VersionAlternate } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The shape of the `blume:data` virtual module — the resolved, serializable
|
|
@@ -85,6 +85,14 @@ export interface BlumeRoute {
|
|
|
85
85
|
locale: string;
|
|
86
86
|
path: string;
|
|
87
87
|
title: string;
|
|
88
|
+
/** Resolved docs version (`""` for the current docs). */
|
|
89
|
+
version: string;
|
|
90
|
+
/**
|
|
91
|
+
* Versions this logical page exists in within this route's locale — the
|
|
92
|
+
* current version first, then archived versions in configured order. Empty
|
|
93
|
+
* when versioning is off.
|
|
94
|
+
*/
|
|
95
|
+
versionAlternates: VersionAlternate[];
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
/** Site-wide settings derived from `blume.config` — the `config` field of {@link BlumeData}. */
|
|
@@ -161,6 +169,8 @@ export interface BlumeDataConfig {
|
|
|
161
169
|
* WebMCP in-page tools (`ai.webmcp`), plus whether llms.txt exists for the
|
|
162
170
|
* list tool to fetch (`ai.llmsTxt.enabled`).
|
|
163
171
|
*/
|
|
172
|
+
/** Docs versioning config; `null` when the site is unversioned. */
|
|
173
|
+
versions: NonNullable<ResolvedConfig["versions"]> | null;
|
|
164
174
|
webmcp: { enabled: boolean; llms: boolean };
|
|
165
175
|
/** X (Twitter) attribution: the site's account, and a default creator. */
|
|
166
176
|
x: { creator?: string; handle?: string };
|
|
@@ -188,6 +198,11 @@ export interface BlumeData {
|
|
|
188
198
|
navigation: Navigation;
|
|
189
199
|
/** Per-locale navigation trees, keyed by locale code (empty without i18n). */
|
|
190
200
|
navigationByLocale: Record<string, Navigation>;
|
|
201
|
+
/**
|
|
202
|
+
* Per-archived-version navigation trees, keyed by version id and then locale
|
|
203
|
+
* code (`""` on a single-locale site). Empty when versioning is off.
|
|
204
|
+
*/
|
|
205
|
+
navigationByVersion: Record<string, Record<string, Navigation>>;
|
|
191
206
|
routes: BlumeRoute[];
|
|
192
207
|
/** Resolved UI strings for the default locale. */
|
|
193
208
|
ui: UIStrings;
|
|
@@ -18,6 +18,10 @@ export interface IslandDescriptor {
|
|
|
18
18
|
export type ComponentOverride = ComponentReference | IslandDescriptor;
|
|
19
19
|
|
|
20
20
|
/** User-authored component overrides, grouped by surface. */
|
|
21
|
+
// oxlint-disable anti-slop/no-unsafe-dictionary-type -- `ComponentOverride` is untyped by
|
|
22
|
+
// design: user configs pass imported components from any framework (React
|
|
23
|
+
// functions, Svelte classes, Vue SFC objects), which share no structural type.
|
|
24
|
+
// `resolveSlot` and the generated components map are the runtime boundary.
|
|
21
25
|
export interface ComponentOverrides {
|
|
22
26
|
/**
|
|
23
27
|
* Interactive framework components made available in every `.mdx` page. Like
|
|
@@ -31,6 +35,7 @@ export interface ComponentOverrides {
|
|
|
31
35
|
/** MDX component map overrides (`Callout`, `Card`, ...). */
|
|
32
36
|
mdx?: Record<string, ComponentOverride>;
|
|
33
37
|
}
|
|
38
|
+
// oxlint-enable anti-slop/no-unsafe-dictionary-type
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* Identity helper for authoring `components.ts`. Provides type inference and a
|
package/src/core/diagnostics.ts
CHANGED
|
@@ -27,41 +27,44 @@ const DOCS_CONTENT_SOURCES = "/docs/content/sources";
|
|
|
27
27
|
const DOCS_CONTENT_NAVIGATION = "/docs/content/navigation";
|
|
28
28
|
|
|
29
29
|
/** Diagnostic code → the docs page that explains it. */
|
|
30
|
-
const DOCS_PATHS
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
30
|
+
const DOCS_PATHS = new Map(
|
|
31
|
+
Object.entries({
|
|
32
|
+
BLUME_ADAPTER_REQUIRED: DOCS_DEPLOYMENT,
|
|
33
|
+
BLUME_ASSETS_UNCHECKED: DOCS_REFERENCE_CLI,
|
|
34
|
+
BLUME_ASSET_FETCH_FAILED: DOCS_CONTENT_SOURCES,
|
|
35
|
+
BLUME_BROKEN_ANCHOR: DOCS_REFERENCE_CLI,
|
|
36
|
+
BLUME_BROKEN_ASSET: DOCS_REFERENCE_CLI,
|
|
37
|
+
BLUME_BROKEN_LINK: DOCS_REFERENCE_CLI,
|
|
38
|
+
BLUME_CONFIG_INVALID: "/docs/configuration",
|
|
39
|
+
BLUME_CONFIG_LOAD_FAILED: "/docs/configuration",
|
|
40
|
+
BLUME_CONTENT_ROOT_MISSING: DOCS_CONTENT_SOURCES,
|
|
41
|
+
BLUME_DEAD_LINK: DOCS_REFERENCE_CLI,
|
|
42
|
+
BLUME_DUPLICATE_ROUTE: DOCS_CONTENT_NAVIGATION,
|
|
43
|
+
BLUME_DUPLICATE_SIDEBAR_ORDER: DOCS_CONTENT_NAVIGATION,
|
|
44
|
+
BLUME_FRONTMATTER_INVALID: "/docs/reference/frontmatter",
|
|
45
|
+
BLUME_META_INVALID: "/docs/content/meta",
|
|
46
|
+
BLUME_META_LOAD_FAILED: "/docs/content/meta",
|
|
47
|
+
BLUME_MISSING_SECRET: DOCS_DEPLOYMENT,
|
|
48
|
+
BLUME_NAV_DUPLICATE_LABEL: DOCS_CONTENT_NAVIGATION,
|
|
49
|
+
BLUME_NAV_HIDDEN_IN_SIDEBAR: DOCS_CONTENT_NAVIGATION,
|
|
50
|
+
BLUME_NAV_INDEX_TITLE_MISMATCH: DOCS_CONTENT_NAVIGATION,
|
|
51
|
+
BLUME_NAV_MISSING_PAGE: DOCS_CONTENT_NAVIGATION,
|
|
52
|
+
BLUME_NODE_VERSION: "/docs/quickstart",
|
|
53
|
+
BLUME_SERVER_FEATURE_REQUIRED: DOCS_DEPLOYMENT,
|
|
54
|
+
BLUME_SIDEBAR_DISPLAY_IGNORED: DOCS_CONTENT_NAVIGATION,
|
|
55
|
+
BLUME_SOURCE_FETCH_FAILED: DOCS_CONTENT_SOURCES,
|
|
56
|
+
BLUME_SOURCE_MISCONFIGURED: DOCS_CONTENT_SOURCES,
|
|
57
|
+
BLUME_SOURCE_OFFLINE: DOCS_CONTENT_SOURCES,
|
|
58
|
+
BLUME_SOURCE_SDK_MISSING: DOCS_CONTENT_SOURCES,
|
|
59
|
+
BLUME_SOURCE_UNAVAILABLE: DOCS_CONTENT_SOURCES,
|
|
60
|
+
BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
|
|
61
|
+
BLUME_UNKNOWN_ICON: DOCS_CONTENT_NAVIGATION,
|
|
62
|
+
})
|
|
63
|
+
);
|
|
61
64
|
|
|
62
65
|
/** The docs URL that explains a diagnostic code, if one is mapped. */
|
|
63
66
|
export const resolveDocsUrl = (code: string): string | undefined => {
|
|
64
|
-
const path = DOCS_PATHS
|
|
67
|
+
const path = DOCS_PATHS.get(code);
|
|
65
68
|
return path ? `${DOCS_BASE}${path}` : undefined;
|
|
66
69
|
};
|
|
67
70
|
|
|
@@ -75,6 +78,10 @@ const REGEXP_SPECIAL = /[$()*+.?[\\\]^{|}]/gu;
|
|
|
75
78
|
const escapeRegExp = (value: string): string =>
|
|
76
79
|
value.replaceAll(REGEXP_SPECIAL, String.raw`\$&`);
|
|
77
80
|
|
|
81
|
+
/** A key segment scans the source text; an array index has no key to find. */
|
|
82
|
+
const isKeySegment = (segment: string | number): segment is string =>
|
|
83
|
+
typeof segment === "string";
|
|
84
|
+
|
|
78
85
|
/**
|
|
79
86
|
* Best-effort source position for a Zod issue path (e.g. `["seo", "title"]`) in
|
|
80
87
|
* the raw config / frontmatter text. Narrows key-by-key — finding each string
|
|
@@ -86,9 +93,9 @@ const stepSegment = (
|
|
|
86
93
|
source: string,
|
|
87
94
|
segment: string | number,
|
|
88
95
|
cursor: number
|
|
89
|
-
)
|
|
96
|
+
) => {
|
|
90
97
|
// A non-string path segment (array index) is skipped without moving on.
|
|
91
|
-
if (
|
|
98
|
+
if (!isKeySegment(segment)) {
|
|
92
99
|
return { index: -1, next: cursor, stop: false };
|
|
93
100
|
}
|
|
94
101
|
// The negative lookbehind keeps a segment like `title` from matching the
|
|
@@ -247,10 +254,11 @@ export const formatDiagnostic = (
|
|
|
247
254
|
export const hasErrors = (diagnostics: Diagnostic[]): boolean =>
|
|
248
255
|
diagnostics.some((d) => d.severity === "error");
|
|
249
256
|
|
|
250
|
-
export const countBySeverity = (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
export const countBySeverity = (diagnostics: Diagnostic[]) => {
|
|
258
|
+
const counts = { error: 0, info: 0, warning: 0 } satisfies Record<
|
|
259
|
+
Diagnostic["severity"],
|
|
260
|
+
number
|
|
261
|
+
>;
|
|
254
262
|
for (const diagnostic of diagnostics) {
|
|
255
263
|
counts[diagnostic.severity] += 1;
|
|
256
264
|
}
|
package/src/core/frontmatter.ts
CHANGED
|
@@ -14,27 +14,97 @@ type MatterOptions = Parameters<typeof baseMatter>[1];
|
|
|
14
14
|
type ReadArgs = Parameters<typeof baseMatter.read>;
|
|
15
15
|
type StringifyArgs = Parameters<typeof baseMatter.stringify>;
|
|
16
16
|
|
|
17
|
+
/** Front matter data as gray-matter types it (`GrayMatterFile["data"]`). */
|
|
18
|
+
type FrontMatterData = ReturnType<typeof baseMatter>["data"];
|
|
19
|
+
|
|
17
20
|
const yamlEngine = {
|
|
21
|
+
// SAFETY: gray-matter's engine contract expects an object; a front matter
|
|
22
|
+
// block is a YAML mapping, and js-yaml returns a scalar only for degenerate
|
|
23
|
+
// input, which gray-matter treats the same way its bundled engine's output
|
|
24
|
+
// is treated.
|
|
18
25
|
parse: (input: string): object => (load(input) ?? {}) as object,
|
|
19
|
-
stringify: (data:
|
|
26
|
+
stringify: (data: FrontMatterData): string => dump(data),
|
|
20
27
|
};
|
|
21
28
|
|
|
22
|
-
const withYamlEngine = <O
|
|
29
|
+
const withYamlEngine = <O extends { engines?: object } | undefined>(
|
|
30
|
+
options: O
|
|
31
|
+
): O =>
|
|
32
|
+
// SAFETY: the spread keeps every field of `options`; adding a default yaml
|
|
33
|
+
// engine (overridden by any caller-supplied `engines`) stays within O's
|
|
34
|
+
// shape — TS just can't prove a spread of a generic re-satisfies O.
|
|
23
35
|
({
|
|
24
36
|
...options,
|
|
25
37
|
engines: {
|
|
26
38
|
yaml: yamlEngine,
|
|
27
|
-
...
|
|
39
|
+
...options?.engines,
|
|
28
40
|
},
|
|
29
41
|
}) as O;
|
|
30
42
|
|
|
43
|
+
/**
|
|
44
|
+
* True when a document's leading `---` line is a CommonMark thematic break,
|
|
45
|
+
* not a front matter fence. Two shapes qualify (mirroring
|
|
46
|
+
* `linesWithoutFrontMatter` in `sources/normalize.ts`):
|
|
47
|
+
* - the next line is blank (or absent) — YAML metadata starts on the very
|
|
48
|
+
* next line, so a gap means the body *opens* with a divider (e.g. a
|
|
49
|
+
* Notion page whose first block is one);
|
|
50
|
+
* - no closing `---` line follows — gray-matter would swallow the whole
|
|
51
|
+
* document as one unclosed YAML block and hand it to js-yaml, which
|
|
52
|
+
* crashes on ordinary Markdown (`> quote` → "a line break is expected").
|
|
53
|
+
*/
|
|
54
|
+
const opensWithThematicBreak = (input: string): boolean => {
|
|
55
|
+
const [first = "", second] = input.split(/\r?\n/u, 2);
|
|
56
|
+
if (!/^-{3}\s*$/u.test(first)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (second === undefined || second.trim() === "") {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// gray-matter closes the block at the next line-leading `---`; matching its
|
|
63
|
+
// search exactly keeps this guard from firing on any document it parses.
|
|
64
|
+
return !input.includes("\n---", 1);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* gray-matter's runtime result carries `isEmpty`, which its declared
|
|
69
|
+
* GrayMatterFile type omits.
|
|
70
|
+
*/
|
|
71
|
+
interface MatterResult extends ReturnType<typeof baseMatter> {
|
|
72
|
+
isEmpty: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The parse result for a document with no front matter: the input passes
|
|
77
|
+
* through as content, untouched. Shaped like gray-matter's own no-matter
|
|
78
|
+
* result (every Blume call site reads only `content` and `data`).
|
|
79
|
+
*/
|
|
80
|
+
const passthrough = (input: string): ReturnType<typeof baseMatter> => {
|
|
81
|
+
const file: MatterResult = {
|
|
82
|
+
content: input,
|
|
83
|
+
data: {},
|
|
84
|
+
excerpt: "",
|
|
85
|
+
isEmpty: false,
|
|
86
|
+
language: "",
|
|
87
|
+
matter: "",
|
|
88
|
+
orig: input,
|
|
89
|
+
// Recomposing a file with no matter and empty data is the content itself.
|
|
90
|
+
stringify: (): string => input,
|
|
91
|
+
};
|
|
92
|
+
return file;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Narrows gray-matter's input union to the raw-string form. */
|
|
96
|
+
const isStringInput = (input: MatterInput): input is string =>
|
|
97
|
+
typeof input === "string";
|
|
98
|
+
|
|
31
99
|
// Every helper that parses or emits YAML (`read`, `stringify`) must be
|
|
32
100
|
// re-wrapped here — Object.assign copies gray-matter's own helpers, which use
|
|
33
101
|
// its default `safeLoad` engine and would reintroduce the crash. `test` only
|
|
34
102
|
// checks for a delimiter, so the copied original is safe.
|
|
35
103
|
const matter = Object.assign(
|
|
36
104
|
(input: MatterInput, options?: MatterOptions) =>
|
|
37
|
-
|
|
105
|
+
isStringInput(input) && opensWithThematicBreak(input)
|
|
106
|
+
? passthrough(input)
|
|
107
|
+
: baseMatter(input, withYamlEngine(options)),
|
|
38
108
|
baseMatter,
|
|
39
109
|
{
|
|
40
110
|
read: (filepath: ReadArgs[0], options?: ReadArgs[1]) =>
|
package/src/core/graph.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
LocalizableLabel,
|
|
8
8
|
ResolvedConfig,
|
|
9
9
|
ResolvedI18nConfig,
|
|
10
|
+
ResolvedVersionsConfig,
|
|
10
11
|
} from "./schema.ts";
|
|
11
12
|
import type {
|
|
12
13
|
ContentGraph,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
NavTab,
|
|
16
17
|
PageRecord,
|
|
17
18
|
} from "./types.ts";
|
|
19
|
+
import { versionizeRoute } from "./versions.ts";
|
|
18
20
|
|
|
19
21
|
interface BuildContentGraphOptions {
|
|
20
22
|
/** Site-wide route mount point (`""` or `/seg`); invisible to the nav tree. */
|
|
@@ -23,14 +25,13 @@ interface BuildContentGraphOptions {
|
|
|
23
25
|
sharedFolderMeta?: Map<string, FolderMeta>;
|
|
24
26
|
navigation: ResolvedConfig["navigation"];
|
|
25
27
|
i18n?: ResolvedI18nConfig;
|
|
28
|
+
versions?: ResolvedVersionsConfig;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
type FallbackLocale = ReturnType<typeof resolveFallbackLocale>;
|
|
29
32
|
|
|
30
33
|
/** Build the route → page-id map, flagging any duplicate-route collisions. */
|
|
31
|
-
const collectRoutes = (
|
|
32
|
-
pages: PageRecord[]
|
|
33
|
-
): { diagnostics: Diagnostic[]; routes: Map<string, string> } => {
|
|
34
|
+
const collectRoutes = (pages: PageRecord[]) => {
|
|
34
35
|
const routes = new Map<string, string>();
|
|
35
36
|
const diagnostics: Diagnostic[] = [];
|
|
36
37
|
for (const page of pages) {
|
|
@@ -86,12 +87,16 @@ const localePagesFor = (
|
|
|
86
87
|
* the active locale's entry, else the default locale's, else the map's first
|
|
87
88
|
* entry (which is also what a single-locale site gets).
|
|
88
89
|
*/
|
|
90
|
+
/** A label is either one string for every locale or a per-locale map. */
|
|
91
|
+
const isSingleLabel = (label: LocalizableLabel): label is string =>
|
|
92
|
+
typeof label === "string";
|
|
93
|
+
|
|
89
94
|
const resolveLabel = (
|
|
90
95
|
label: LocalizableLabel,
|
|
91
96
|
locale: string,
|
|
92
97
|
defaultLocale?: string
|
|
93
98
|
): string => {
|
|
94
|
-
if (
|
|
99
|
+
if (isSingleLabel(label)) {
|
|
95
100
|
return label;
|
|
96
101
|
}
|
|
97
102
|
return (
|
|
@@ -117,7 +122,12 @@ const resolveTabLabels = (
|
|
|
117
122
|
label: resolveLabel(tab.label, locale, defaultLocale),
|
|
118
123
|
}));
|
|
119
124
|
|
|
120
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Build one locale's navigation tree from its own pages and folder meta.
|
|
127
|
+
* `version` is the archived version id when building a snapshot's tree
|
|
128
|
+
* (`""` for the current docs): it shifts the folder-meta lookups into the
|
|
129
|
+
* snapshot's key space and roots the tree at the localized version root.
|
|
130
|
+
*/
|
|
121
131
|
const buildLocaleNavigation = (
|
|
122
132
|
code: string,
|
|
123
133
|
pages: PageRecord[],
|
|
@@ -125,7 +135,8 @@ const buildLocaleNavigation = (
|
|
|
125
135
|
fallbackByKey: Map<string, PageRecord>,
|
|
126
136
|
options: BuildContentGraphOptions,
|
|
127
137
|
i18n: ResolvedI18nConfig,
|
|
128
|
-
diagnostics: Diagnostic[]
|
|
138
|
+
diagnostics: Diagnostic[],
|
|
139
|
+
version = ""
|
|
129
140
|
): Navigation => {
|
|
130
141
|
// Localize internal tab paths — the tab's own and its dropdown items' — so a
|
|
131
142
|
// header tab points to its in-locale route (e.g. `/docs` -> `/fr/docs`);
|
|
@@ -137,15 +148,20 @@ const buildLocaleNavigation = (
|
|
|
137
148
|
options.navigation.tabs,
|
|
138
149
|
code,
|
|
139
150
|
i18n.defaultLocale
|
|
140
|
-
).map((tab) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
).map((tab) => {
|
|
152
|
+
const localized = {
|
|
153
|
+
...tab,
|
|
154
|
+
items: tab.items?.map((item) => ({
|
|
155
|
+
...item,
|
|
156
|
+
path: localizePath(item.path),
|
|
157
|
+
})),
|
|
158
|
+
path: localizePath(tab.path),
|
|
159
|
+
};
|
|
160
|
+
if (localized.href) {
|
|
161
|
+
localized.href = localizePath(localized.href);
|
|
162
|
+
}
|
|
163
|
+
return localized;
|
|
164
|
+
});
|
|
149
165
|
const real = pages.filter((page) => page.locale === code);
|
|
150
166
|
const localePages = localePagesFor(
|
|
151
167
|
code,
|
|
@@ -155,6 +171,13 @@ const buildLocaleNavigation = (
|
|
|
155
171
|
i18n,
|
|
156
172
|
options.basePath ?? ""
|
|
157
173
|
);
|
|
174
|
+
// Meta files live in locale directories only under the `dir` parser
|
|
175
|
+
// (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations sit
|
|
176
|
+
// next to the originals and `guides/meta.ts` applies to every locale —
|
|
177
|
+
// prefixing would look up keys that can never exist. Inside a snapshot the
|
|
178
|
+
// version dir is hoisted in front (`v1.0/fr`), matching `discoverFolderMeta`.
|
|
179
|
+
const localeDir =
|
|
180
|
+
i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "";
|
|
158
181
|
return buildNavigation(localePages, {
|
|
159
182
|
basePath: options.basePath ?? "",
|
|
160
183
|
diagnostics,
|
|
@@ -167,34 +190,37 @@ const buildLocaleNavigation = (
|
|
|
167
190
|
href: localizePath(link.href),
|
|
168
191
|
})),
|
|
169
192
|
folderMeta: options.folderMeta,
|
|
170
|
-
// The localized tree root ("/" for the hidden default, "/fr" otherwise
|
|
171
|
-
// the tab pointing here spans the whole
|
|
172
|
-
// tab section.
|
|
173
|
-
localizedRoot: localizeRoute("/", code, i18n),
|
|
174
|
-
|
|
175
|
-
// (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations sit
|
|
176
|
-
// next to the originals and `guides/meta.ts` applies to every locale —
|
|
177
|
-
// prefixing would look up keys that can never exist.
|
|
178
|
-
metaPrefix:
|
|
179
|
-
i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
|
|
193
|
+
// The localized tree root ("/" for the hidden default, "/fr" otherwise;
|
|
194
|
+
// "/fr/v1.0" inside a snapshot): the tab pointing here spans the whole
|
|
195
|
+
// tree and must not be treated as a tab section.
|
|
196
|
+
localizedRoot: localizeRoute(versionizeRoute("/", version), code, i18n),
|
|
197
|
+
metaPrefix: [version, localeDir].filter(Boolean).join("/"),
|
|
180
198
|
refByLogical: true,
|
|
181
199
|
selectors: options.navigation.selectors,
|
|
182
200
|
sharedFolderMeta: options.sharedFolderMeta,
|
|
183
|
-
|
|
201
|
+
// Shared `meta.$.*` files are locale-agnostic but version-specific: a
|
|
202
|
+
// snapshot's shared meta keys under its version dir.
|
|
203
|
+
sharedMetaPrefix: version,
|
|
204
|
+
// A configured explicit sidebar describes the current docs; a frozen
|
|
205
|
+
// snapshot's structure comes from the snapshot itself, so archived trees
|
|
206
|
+
// always build from the filesystem.
|
|
207
|
+
sidebar: version ? undefined : options.navigation.sidebar.items,
|
|
184
208
|
tabs,
|
|
185
209
|
});
|
|
186
210
|
};
|
|
187
211
|
|
|
188
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Per-locale navigation trees plus the default-locale tree for i18n sites.
|
|
214
|
+
* Called once for the current docs and once per archived version (with that
|
|
215
|
+
* version's pages and its id as `version`).
|
|
216
|
+
*/
|
|
189
217
|
const buildI18nNavigation = (
|
|
190
218
|
pages: PageRecord[],
|
|
191
219
|
options: BuildContentGraphOptions,
|
|
192
220
|
i18n: ResolvedI18nConfig,
|
|
193
|
-
diagnostics: Diagnostic[]
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
navigationByLocale: Record<string, Navigation>;
|
|
197
|
-
} => {
|
|
221
|
+
diagnostics: Diagnostic[],
|
|
222
|
+
version = ""
|
|
223
|
+
) => {
|
|
198
224
|
// Pages of the fallback locale, by translation key — used to fill in a
|
|
199
225
|
// locale's sidebar for pages it hasn't translated yet.
|
|
200
226
|
const fallback = resolveFallbackLocale(i18n);
|
|
@@ -223,7 +249,8 @@ const buildI18nNavigation = (
|
|
|
223
249
|
fallbackByKey,
|
|
224
250
|
options,
|
|
225
251
|
i18n,
|
|
226
|
-
localeDiagnostics
|
|
252
|
+
localeDiagnostics,
|
|
253
|
+
version
|
|
227
254
|
);
|
|
228
255
|
for (const diagnostic of localeDiagnostics) {
|
|
229
256
|
const key = `${diagnostic.code}\n${diagnostic.file ?? ""}\n${diagnostic.message}`;
|
|
@@ -233,7 +260,7 @@ const buildI18nNavigation = (
|
|
|
233
260
|
}
|
|
234
261
|
}
|
|
235
262
|
}
|
|
236
|
-
const navigation = navigationByLocale[i18n.defaultLocale] ?? {
|
|
263
|
+
const navigation: Navigation = navigationByLocale[i18n.defaultLocale] ?? {
|
|
237
264
|
featured: [],
|
|
238
265
|
selectors: [],
|
|
239
266
|
sidebar: [],
|
|
@@ -242,6 +269,37 @@ const buildI18nNavigation = (
|
|
|
242
269
|
return { navigation, navigationByLocale };
|
|
243
270
|
};
|
|
244
271
|
|
|
272
|
+
/** One archived version's navigation trees, keyed by locale (`""` sans i18n). */
|
|
273
|
+
const buildVersionNavigation = (
|
|
274
|
+
id: string,
|
|
275
|
+
versionPages: PageRecord[],
|
|
276
|
+
options: BuildContentGraphOptions,
|
|
277
|
+
diagnostics: Diagnostic[]
|
|
278
|
+
) => {
|
|
279
|
+
const { i18n } = options;
|
|
280
|
+
if (i18n) {
|
|
281
|
+
return buildI18nNavigation(versionPages, options, i18n, diagnostics, id)
|
|
282
|
+
.navigationByLocale;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
"": buildNavigation(versionPages, {
|
|
286
|
+
basePath: options.basePath ?? "",
|
|
287
|
+
diagnostics,
|
|
288
|
+
display: options.navigation.sidebar.display,
|
|
289
|
+
featured: options.navigation.featured,
|
|
290
|
+
folderMeta: options.folderMeta,
|
|
291
|
+
localizedRoot: versionizeRoute("/", id),
|
|
292
|
+
metaPrefix: id,
|
|
293
|
+
selectors: options.navigation.selectors,
|
|
294
|
+
sharedFolderMeta: options.sharedFolderMeta,
|
|
295
|
+
sharedMetaPrefix: id,
|
|
296
|
+
// A configured explicit sidebar describes the current docs; a snapshot's
|
|
297
|
+
// structure comes from the snapshot itself.
|
|
298
|
+
tabs: resolveTabLabels(options.navigation.tabs, ""),
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
245
303
|
/** Assemble the content graph: routes map, nav, and duplicate diagnostics. */
|
|
246
304
|
export const buildContentGraph = (
|
|
247
305
|
pages: PageRecord[],
|
|
@@ -250,37 +308,63 @@ export const buildContentGraph = (
|
|
|
250
308
|
const { diagnostics, routes } = collectRoutes(pages);
|
|
251
309
|
const { i18n } = options;
|
|
252
310
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
311
|
+
// Under versioning, each version gets independent trees: navPath is
|
|
312
|
+
// version-stripped, so mixing versions into one build would collide the
|
|
313
|
+
// same logical page once per version.
|
|
314
|
+
const currentPages = options.versions
|
|
315
|
+
? pages.filter((page) => page.version === "")
|
|
316
|
+
: pages;
|
|
317
|
+
|
|
318
|
+
// A single-locale site has no per-locale trees; the empty map is its
|
|
319
|
+
// navigationByLocale.
|
|
320
|
+
let navigationByLocale: Record<string, Navigation> = {};
|
|
321
|
+
let navigation: Navigation;
|
|
322
|
+
if (i18n) {
|
|
323
|
+
({ navigation, navigationByLocale } = buildI18nNavigation(
|
|
324
|
+
currentPages,
|
|
325
|
+
options,
|
|
326
|
+
i18n,
|
|
327
|
+
diagnostics
|
|
328
|
+
));
|
|
329
|
+
} else {
|
|
330
|
+
navigation = buildNavigation(currentPages, {
|
|
331
|
+
basePath: options.basePath ?? "",
|
|
332
|
+
diagnostics,
|
|
333
|
+
display: options.navigation.sidebar.display,
|
|
334
|
+
featured: options.navigation.featured,
|
|
335
|
+
folderMeta: options.folderMeta,
|
|
336
|
+
selectors: options.navigation.selectors,
|
|
337
|
+
sharedFolderMeta: options.sharedFolderMeta,
|
|
338
|
+
sidebar: options.navigation.sidebar.items,
|
|
339
|
+
// No locale to prefer: a per-locale label map resolves to its first
|
|
340
|
+
// entry on a single-locale site.
|
|
341
|
+
tabs: resolveTabLabels(options.navigation.tabs, ""),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const navigationByVersion: Record<string, Record<string, Navigation>> = {};
|
|
346
|
+
for (const { id } of options.versions?.archived ?? []) {
|
|
347
|
+
navigationByVersion[id] = buildVersionNavigation(
|
|
348
|
+
id,
|
|
349
|
+
pages.filter((page) => page.version === id),
|
|
350
|
+
options,
|
|
351
|
+
diagnostics
|
|
352
|
+
);
|
|
353
|
+
}
|
|
271
354
|
|
|
272
355
|
// Icon typos, duplicate labels, and hidden-page-in-sidebar are validated on
|
|
273
356
|
// the built navigation. Missing-target detection needs the full route set
|
|
274
357
|
// (incl. custom + generated pages), so it runs later in generateRuntime.
|
|
275
358
|
diagnostics.push(
|
|
276
359
|
...validateNavIcons(navigation),
|
|
277
|
-
...validateNavStructure(navigation,
|
|
360
|
+
...validateNavStructure(navigation, currentPages)
|
|
278
361
|
);
|
|
279
362
|
|
|
280
363
|
return {
|
|
281
364
|
diagnostics,
|
|
282
365
|
navigation,
|
|
283
366
|
navigationByLocale,
|
|
367
|
+
navigationByVersion,
|
|
284
368
|
pages,
|
|
285
369
|
routes,
|
|
286
370
|
};
|
package/src/core/i18n-ui.ts
CHANGED
|
@@ -123,6 +123,7 @@ const uiStringsObject = z.object({
|
|
|
123
123
|
.object({
|
|
124
124
|
all: z.string().default("All"),
|
|
125
125
|
allLanguages: z.string().default("All languages"),
|
|
126
|
+
allVersions: z.string().default("All versions"),
|
|
126
127
|
askAi: z.string().default("Ask AI"),
|
|
127
128
|
askAiHint: z.string().default("Get an instant answer from AI"),
|
|
128
129
|
button: z.string().default("Search"),
|
|
@@ -145,6 +146,18 @@ const uiStringsObject = z.object({
|
|
|
145
146
|
title: z.string().default("On this page"),
|
|
146
147
|
})
|
|
147
148
|
.prefault({}),
|
|
149
|
+
versions: z
|
|
150
|
+
.object({
|
|
151
|
+
latest: z.string().default("Go to latest"),
|
|
152
|
+
// `{version}` is replaced with the archived version's label at render time.
|
|
153
|
+
notice: z
|
|
154
|
+
.string()
|
|
155
|
+
.default(
|
|
156
|
+
"You're viewing documentation for {version}. It may be out of date."
|
|
157
|
+
),
|
|
158
|
+
switcher: z.string().default("Version"),
|
|
159
|
+
})
|
|
160
|
+
.prefault({}),
|
|
148
161
|
});
|
|
149
162
|
|
|
150
163
|
export const uiStringsSchema = uiStringsObject.prefault({});
|
|
@@ -186,6 +199,8 @@ const mergeUI = (base: UIStrings, override?: UIStringsOverride): UIStrings => {
|
|
|
186
199
|
}
|
|
187
200
|
const out: UIStrings = structuredClone(base);
|
|
188
201
|
for (const [group, values] of Object.entries(override)) {
|
|
202
|
+
// SAFETY: UIStrings is exactly two levels of string leaves, and the
|
|
203
|
+
// override schema mirrors its groups, so `group` indexes a string map.
|
|
189
204
|
const target = (out as Record<string, Record<string, string>>)[group];
|
|
190
205
|
if (target && values) {
|
|
191
206
|
Object.assign(target, values);
|