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/navigation.ts
CHANGED
|
@@ -47,6 +47,19 @@ const segmentKey = (raw: string): string => {
|
|
|
47
47
|
const isIndexStem = (stem: string): boolean =>
|
|
48
48
|
stem.replace(NUMERIC_PREFIX, "") === "index";
|
|
49
49
|
|
|
50
|
+
/** A filename's stem: the name with its extension stripped. */
|
|
51
|
+
const stemOf = (filename: string): string =>
|
|
52
|
+
filename.replace(extname(filename), "");
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The stem of a nav path's last segment. The single index-detection input for
|
|
56
|
+
* the warn path (`sidebarDisplayIgnoredDiagnostics`) and the apply path
|
|
57
|
+
* (`buildFileSystemSidebar`), so the two can't drift apart on what counts as
|
|
58
|
+
* a folder's index page.
|
|
59
|
+
*/
|
|
60
|
+
const navStem = (navPath: string): string =>
|
|
61
|
+
stemOf(navPath.split("/").at(-1) ?? navPath);
|
|
62
|
+
|
|
50
63
|
interface MutablePage {
|
|
51
64
|
kind: "page";
|
|
52
65
|
key: string;
|
|
@@ -79,6 +92,7 @@ interface MutableGroup {
|
|
|
79
92
|
label: string;
|
|
80
93
|
icon?: string;
|
|
81
94
|
collapsed?: boolean;
|
|
95
|
+
display?: SidebarDisplay;
|
|
82
96
|
order: number;
|
|
83
97
|
children: MutableNode[];
|
|
84
98
|
index: Map<string, MutableGroup>;
|
|
@@ -121,14 +135,17 @@ const ensureGroup = (
|
|
|
121
135
|
return group;
|
|
122
136
|
};
|
|
123
137
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
interface PageOrder {
|
|
139
|
+
order: number;
|
|
140
|
+
/** Whether the order came from the author (frontmatter or a rank prefix). */
|
|
141
|
+
orderIsAuthored: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pageOrder = (page: PageRecord, filename: string): PageOrder => {
|
|
128
145
|
if (page.meta.sidebar.order !== undefined) {
|
|
129
146
|
return { order: page.meta.sidebar.order, orderIsAuthored: true };
|
|
130
147
|
}
|
|
131
|
-
if (isIndexStem(
|
|
148
|
+
if (isIndexStem(stemOf(filename))) {
|
|
132
149
|
return { order: Number.NEGATIVE_INFINITY, orderIsAuthored: false };
|
|
133
150
|
}
|
|
134
151
|
// Changelog entries read newest-first, matching the generated timeline. Sort
|
|
@@ -164,18 +181,28 @@ const metaKey = (path: string, metaPrefix: string): string => {
|
|
|
164
181
|
return path ? `${metaPrefix}/${path}` : metaPrefix;
|
|
165
182
|
};
|
|
166
183
|
|
|
167
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Apply folder meta (title/order/icon/collapsed/display and explicit page
|
|
186
|
+
* order), plus the index-frontmatter display sugar collected per folder path.
|
|
187
|
+
*/
|
|
168
188
|
const applyFolderMeta = (
|
|
169
189
|
group: MutableGroup,
|
|
170
190
|
folderMeta: Map<string, FolderMeta>,
|
|
171
191
|
sharedMeta: Map<string, FolderMeta>,
|
|
172
|
-
metaPrefix: string
|
|
192
|
+
metaPrefix: string,
|
|
193
|
+
sharedMetaPrefix: string,
|
|
194
|
+
indexDisplay: Map<string, SidebarDisplay>
|
|
173
195
|
): void => {
|
|
174
196
|
// Locale-specific meta wins; a shared `meta.$.*` (keyed by the locale-stripped
|
|
175
|
-
// group path) applies to every locale
|
|
197
|
+
// group path — version-prefixed inside a snapshot) applies to every locale
|
|
198
|
+
// otherwise.
|
|
176
199
|
const meta =
|
|
177
200
|
folderMeta.get(metaKey(group.path, metaPrefix)) ??
|
|
178
|
-
sharedMeta.get(group.path);
|
|
201
|
+
sharedMeta.get(metaKey(group.path, sharedMetaPrefix));
|
|
202
|
+
// The group's own render mode, resolved index frontmatter first, then folder
|
|
203
|
+
// meta; `toNavNode` falls back to the global mode. Applies to this group
|
|
204
|
+
// only — nested subgroups resolve their own value through the same chain.
|
|
205
|
+
group.display = indexDisplay.get(group.path) ?? meta?.display;
|
|
179
206
|
if (meta) {
|
|
180
207
|
group.label = meta.title ?? group.label;
|
|
181
208
|
group.icon = meta.icon ?? group.icon;
|
|
@@ -198,7 +225,14 @@ const applyFolderMeta = (
|
|
|
198
225
|
|
|
199
226
|
for (const child of group.children) {
|
|
200
227
|
if (child.kind === "group") {
|
|
201
|
-
applyFolderMeta(
|
|
228
|
+
applyFolderMeta(
|
|
229
|
+
child,
|
|
230
|
+
folderMeta,
|
|
231
|
+
sharedMeta,
|
|
232
|
+
metaPrefix,
|
|
233
|
+
sharedMetaPrefix,
|
|
234
|
+
indexDisplay
|
|
235
|
+
);
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
238
|
};
|
|
@@ -229,14 +263,15 @@ const indexTitleMismatchDiagnostic = (
|
|
|
229
263
|
folderPath: string,
|
|
230
264
|
folderMeta: Map<string, FolderMeta>,
|
|
231
265
|
sharedMeta: Map<string, FolderMeta>,
|
|
232
|
-
metaPrefix: string
|
|
266
|
+
metaPrefix: string,
|
|
267
|
+
sharedMetaPrefix: string
|
|
233
268
|
): Diagnostic | undefined => {
|
|
234
269
|
if (!page.meta.title || page.fallback || folderPath === "") {
|
|
235
270
|
return undefined;
|
|
236
271
|
}
|
|
237
272
|
const meta =
|
|
238
273
|
folderMeta.get(metaKey(folderPath, metaPrefix)) ??
|
|
239
|
-
sharedMeta.get(folderPath);
|
|
274
|
+
sharedMeta.get(metaKey(folderPath, sharedMetaPrefix));
|
|
240
275
|
if (!meta?.title || meta.title === page.title) {
|
|
241
276
|
return undefined;
|
|
242
277
|
}
|
|
@@ -249,6 +284,71 @@ const indexTitleMismatchDiagnostic = (
|
|
|
249
284
|
};
|
|
250
285
|
};
|
|
251
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Warn wherever a page's `sidebar.display` is dead weight, instead of
|
|
289
|
+
* silently dropping it:
|
|
290
|
+
*
|
|
291
|
+
* - On any page under an explicit `navigation.sidebar` config: the config
|
|
292
|
+
* items own each group's display mode, and neither index frontmatter nor
|
|
293
|
+
* folder meta is read — index pages included.
|
|
294
|
+
* - On the content root's own index page: the root is not a sidebar group,
|
|
295
|
+
* so there is nothing for the value to configure; the sidebar-wide mode
|
|
296
|
+
* lives in `navigation.sidebar.display`.
|
|
297
|
+
* - On any other non-index page: only a folder's index page can configure
|
|
298
|
+
* its group's display mode.
|
|
299
|
+
*
|
|
300
|
+
* A non-root folder index under the generated sidebar is the one placement
|
|
301
|
+
* that IS honored, so it alone is exempt. Under i18n fallback fill the same
|
|
302
|
+
* source file is checked once per locale with an identical message, and the
|
|
303
|
+
* graph-level code+file+message dedupe collapses the copies.
|
|
304
|
+
*/
|
|
305
|
+
const sidebarDisplayIgnoredDiagnostics = (
|
|
306
|
+
pages: PageRecord[],
|
|
307
|
+
explicitSidebar: boolean
|
|
308
|
+
): Diagnostic[] => {
|
|
309
|
+
const diagnostics: Diagnostic[] = [];
|
|
310
|
+
for (const page of pages) {
|
|
311
|
+
if (!page.meta.sidebar.display) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const isIndex = isIndexStem(navStem(page.navPath));
|
|
315
|
+
if (explicitSidebar) {
|
|
316
|
+
diagnostics.push({
|
|
317
|
+
code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
|
|
318
|
+
file: page.sourcePath ?? page.id,
|
|
319
|
+
message: `"${page.navPath}" sets sidebar.display, but the explicit navigation.sidebar config owns each group's display mode — the value is ignored.`,
|
|
320
|
+
severity: "warning",
|
|
321
|
+
suggestion:
|
|
322
|
+
"Set display on the matching group in navigation.sidebar, or remove the frontmatter key.",
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (isIndex && !page.navPath.includes("/")) {
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
|
|
329
|
+
file: page.sourcePath ?? page.id,
|
|
330
|
+
message: `"${page.navPath}" sets sidebar.display, but the content root is not a sidebar group — the value is ignored.`,
|
|
331
|
+
severity: "warning",
|
|
332
|
+
suggestion:
|
|
333
|
+
"Set navigation.sidebar.display in blume.config to change the sidebar-wide mode.",
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (isIndex) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
diagnostics.push({
|
|
341
|
+
code: "BLUME_SIDEBAR_DISPLAY_IGNORED",
|
|
342
|
+
file: page.sourcePath ?? page.id,
|
|
343
|
+
message: `"${page.navPath}" sets sidebar.display, but only a folder's index page can set its group's display mode — the value is ignored.`,
|
|
344
|
+
severity: "warning",
|
|
345
|
+
suggestion:
|
|
346
|
+
"Move display to the folder's index page frontmatter, or set it in the folder's meta.ts.",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return diagnostics;
|
|
350
|
+
};
|
|
351
|
+
|
|
252
352
|
/** Whether a node's `order` reflects a deliberate authoring choice. */
|
|
253
353
|
const isAuthoredOrder = (node: MutableNode): boolean =>
|
|
254
354
|
node.kind === "group" || node.orderIsAuthored;
|
|
@@ -319,20 +419,28 @@ const sortNodes = (nodes: MutableNode[], diagnostics: Diagnostic[]): void => {
|
|
|
319
419
|
};
|
|
320
420
|
|
|
321
421
|
/**
|
|
322
|
-
* Hoist loose pages above groups so
|
|
323
|
-
* rather than a group's trailing children (relative order otherwise
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
422
|
+
* Hoist loose pages above groups so they read as their own level's entries
|
|
423
|
+
* rather than a preceding group's trailing children (relative order otherwise
|
|
424
|
+
* preserved). The root level always hoists, in every display mode. A deeper
|
|
425
|
+
* level hoists only when a sibling group renders flat — a flat group is a
|
|
426
|
+
* plain section header, so a loose page sorted after it would visually read
|
|
427
|
+
* as its last child — while `group`/`page` groups are self-delimiting
|
|
428
|
+
* disclosure/drill-in rows, so authored interleaving is kept. Each group's
|
|
429
|
+
* render mode is its own resolved `display` falling back to the global mode,
|
|
430
|
+
* mirroring `toNavNode`.
|
|
327
431
|
*/
|
|
328
|
-
const hoistPages = (
|
|
329
|
-
|
|
432
|
+
const hoistPages = (
|
|
433
|
+
nodes: MutableNode[],
|
|
434
|
+
display: SidebarDisplay,
|
|
435
|
+
hoist: boolean
|
|
436
|
+
): void => {
|
|
330
437
|
const groups = nodes.filter((node) => node.kind === "group");
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
438
|
+
if (hoist || groups.some((group) => (group.display ?? display) === "flat")) {
|
|
439
|
+
const pages = nodes.filter((node) => node.kind === "page");
|
|
440
|
+
nodes.splice(0, nodes.length, ...pages, ...groups);
|
|
441
|
+
}
|
|
442
|
+
for (const group of groups) {
|
|
443
|
+
hoistPages(group.children, display, false);
|
|
336
444
|
}
|
|
337
445
|
};
|
|
338
446
|
|
|
@@ -345,16 +453,16 @@ const hoistPages = (nodes: MutableNode[], recurse: boolean): void => {
|
|
|
345
453
|
const hoistTabSections = (
|
|
346
454
|
nodes: MutableNode[],
|
|
347
455
|
tabPaths: Set<string>,
|
|
348
|
-
|
|
456
|
+
display: SidebarDisplay
|
|
349
457
|
): void => {
|
|
350
458
|
for (const node of nodes) {
|
|
351
459
|
if (node.kind !== "group") {
|
|
352
460
|
continue;
|
|
353
461
|
}
|
|
354
462
|
if (node.routePath !== undefined && tabPaths.has(node.routePath)) {
|
|
355
|
-
hoistPages(node.children,
|
|
463
|
+
hoistPages(node.children, display, true);
|
|
356
464
|
}
|
|
357
|
-
hoistTabSections(node.children, tabPaths,
|
|
465
|
+
hoistTabSections(node.children, tabPaths, display);
|
|
358
466
|
}
|
|
359
467
|
};
|
|
360
468
|
|
|
@@ -374,7 +482,7 @@ const toNavNode = (node: MutableNode, display: SidebarDisplay): NavNode => {
|
|
|
374
482
|
return {
|
|
375
483
|
children: node.children.map((child) => toNavNode(child, display)),
|
|
376
484
|
collapsed: node.collapsed,
|
|
377
|
-
display,
|
|
485
|
+
display: node.display ?? display,
|
|
378
486
|
icon: node.icon,
|
|
379
487
|
kind: "group",
|
|
380
488
|
label: node.label,
|
|
@@ -388,17 +496,22 @@ const buildFileSystemSidebar = (
|
|
|
388
496
|
folderMeta: Map<string, FolderMeta>,
|
|
389
497
|
sharedMeta: Map<string, FolderMeta>,
|
|
390
498
|
metaPrefix: string,
|
|
499
|
+
sharedMetaPrefix: string,
|
|
391
500
|
display: SidebarDisplay,
|
|
392
501
|
tabPaths: Set<string>,
|
|
393
502
|
diagnostics: Diagnostic[] = []
|
|
394
503
|
): NavNode[] => {
|
|
395
504
|
const root = createGroup("", "", "", 0);
|
|
505
|
+
// Folder path -> `sidebar.display` from that folder's index page frontmatter.
|
|
506
|
+
// Collected before the hidden filter (like the title check): hiding the index
|
|
507
|
+
// row from the panel shouldn't stop it configuring its group.
|
|
508
|
+
const indexDisplay = new Map<string, SidebarDisplay>();
|
|
396
509
|
|
|
397
510
|
for (const page of pages) {
|
|
398
511
|
// Group by the locale-stripped path so the locale dir is not a nav group.
|
|
399
512
|
const parts = page.navPath.split("/");
|
|
400
513
|
const filename = parts.at(-1) ?? page.navPath;
|
|
401
|
-
const stem =
|
|
514
|
+
const stem = stemOf(filename);
|
|
402
515
|
const dirs = parts.slice(0, -1);
|
|
403
516
|
|
|
404
517
|
// Checked before the hidden filter: a sidebar-hidden index page still
|
|
@@ -409,11 +522,19 @@ const buildFileSystemSidebar = (
|
|
|
409
522
|
dirs.join("/"),
|
|
410
523
|
folderMeta,
|
|
411
524
|
sharedMeta,
|
|
412
|
-
metaPrefix
|
|
525
|
+
metaPrefix,
|
|
526
|
+
sharedMetaPrefix
|
|
413
527
|
);
|
|
414
528
|
if (diagnostic) {
|
|
415
529
|
diagnostics.push(diagnostic);
|
|
416
530
|
}
|
|
531
|
+
// Fallback-filled pages are exempt (like the title check above): their
|
|
532
|
+
// frontmatter belongs to the fallback locale, and letting it through
|
|
533
|
+
// would override this locale's own authored `meta.ts` display — then
|
|
534
|
+
// silently flip again the day the index gets translated.
|
|
535
|
+
if (page.meta.sidebar.display && !page.fallback) {
|
|
536
|
+
indexDisplay.set(dirs.join("/"), page.meta.sidebar.display);
|
|
537
|
+
}
|
|
417
538
|
}
|
|
418
539
|
|
|
419
540
|
if (page.meta.sidebar.hidden) {
|
|
@@ -461,10 +582,17 @@ const buildFileSystemSidebar = (
|
|
|
461
582
|
});
|
|
462
583
|
}
|
|
463
584
|
|
|
464
|
-
applyFolderMeta(
|
|
585
|
+
applyFolderMeta(
|
|
586
|
+
root,
|
|
587
|
+
folderMeta,
|
|
588
|
+
sharedMeta,
|
|
589
|
+
metaPrefix,
|
|
590
|
+
sharedMetaPrefix,
|
|
591
|
+
indexDisplay
|
|
592
|
+
);
|
|
465
593
|
sortNodes(root.children, diagnostics);
|
|
466
|
-
hoistPages(root.children, display
|
|
467
|
-
hoistTabSections(root.children, tabPaths, display
|
|
594
|
+
hoistPages(root.children, display, true);
|
|
595
|
+
hoistTabSections(root.children, tabPaths, display);
|
|
468
596
|
return root.children.map((child) => toNavNode(child, display));
|
|
469
597
|
};
|
|
470
598
|
|
|
@@ -498,6 +626,10 @@ const routeForRef = (
|
|
|
498
626
|
return byRoute.get(normalized)?.route ?? withBasePath(basePath, normalized);
|
|
499
627
|
};
|
|
500
628
|
|
|
629
|
+
/** An explicit-config sidebar item written as a bare page-ref string. */
|
|
630
|
+
const isPageRef = (item: SidebarItemConfig): item is string =>
|
|
631
|
+
typeof item === "string";
|
|
632
|
+
|
|
501
633
|
/**
|
|
502
634
|
* Convert one non-group explicit-config sidebar item (string ref, `root`, or
|
|
503
635
|
* `href`) to a nav node, or null to skip. Group items (`item.items`) are handled
|
|
@@ -508,7 +640,7 @@ const configItemToNode = (
|
|
|
508
640
|
byRoute: Map<string, PageRecord>,
|
|
509
641
|
basePath: string
|
|
510
642
|
): NavNode | null => {
|
|
511
|
-
if (
|
|
643
|
+
if (isPageRef(item)) {
|
|
512
644
|
const page = byRoute.get(normalizeRef(item));
|
|
513
645
|
if (!page) {
|
|
514
646
|
return null;
|
|
@@ -561,7 +693,7 @@ const buildConfigSidebar = (
|
|
|
561
693
|
): NavNode[] => {
|
|
562
694
|
const nodes: NavNode[] = [];
|
|
563
695
|
for (const item of items) {
|
|
564
|
-
if (
|
|
696
|
+
if (!isPageRef(item) && item.items) {
|
|
565
697
|
nodes.push({
|
|
566
698
|
badge: item.badge,
|
|
567
699
|
children: buildConfigSidebar(item.items, byRoute, display, basePath),
|
|
@@ -626,6 +758,60 @@ const withTabHrefs = (tabs: NavTab[], sidebar: NavNode[]): NavTab[] =>
|
|
|
626
758
|
return href === tab.path ? tab : { ...tab, href };
|
|
627
759
|
});
|
|
628
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Rebase config-provided nav chrome (featured links, selectors, tabs) under the
|
|
763
|
+
* site base path. Config paths are authored as if mounted at root, so the base
|
|
764
|
+
* is applied here (idempotently, and only to internal paths — external URLs
|
|
765
|
+
* pass through). With no base this is a pure pass-through — the arrays keep
|
|
766
|
+
* their exact authored shape.
|
|
767
|
+
*/
|
|
768
|
+
interface NavChrome {
|
|
769
|
+
featured: FeaturedLink[];
|
|
770
|
+
selectors: NavSelector[];
|
|
771
|
+
tabs: NavTab[];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const rebaseNavChrome = (
|
|
775
|
+
basePath: string,
|
|
776
|
+
options: {
|
|
777
|
+
featured?: FeaturedLink[];
|
|
778
|
+
selectors?: NavSelector[];
|
|
779
|
+
tabs?: NavTab[];
|
|
780
|
+
}
|
|
781
|
+
): NavChrome => {
|
|
782
|
+
const featured = options.featured ?? [];
|
|
783
|
+
const selectors = options.selectors ?? [];
|
|
784
|
+
const tabs = options.tabs ?? [];
|
|
785
|
+
if (!basePath) {
|
|
786
|
+
return { featured, selectors, tabs };
|
|
787
|
+
}
|
|
788
|
+
const rebasePath = <T extends { path: string }>(item: T): T => ({
|
|
789
|
+
...item,
|
|
790
|
+
path: withBasePath(basePath, item.path),
|
|
791
|
+
});
|
|
792
|
+
return {
|
|
793
|
+
featured: featured.map((link) => ({
|
|
794
|
+
...link,
|
|
795
|
+
href: withBasePath(basePath, link.href),
|
|
796
|
+
})),
|
|
797
|
+
selectors: selectors.map((selector) => ({
|
|
798
|
+
...selector,
|
|
799
|
+
items: selector.items.map(rebasePath),
|
|
800
|
+
})),
|
|
801
|
+
tabs: tabs.map((tab) => {
|
|
802
|
+
const rebased: NavTab = {
|
|
803
|
+
...tab,
|
|
804
|
+
items: tab.items?.map(rebasePath),
|
|
805
|
+
path: withBasePath(basePath, tab.path),
|
|
806
|
+
};
|
|
807
|
+
if (tab.href) {
|
|
808
|
+
rebased.href = withBasePath(basePath, tab.href);
|
|
809
|
+
}
|
|
810
|
+
return rebased;
|
|
811
|
+
}),
|
|
812
|
+
};
|
|
813
|
+
};
|
|
814
|
+
|
|
629
815
|
/** Build the complete navigation model from pages, meta, and config. */
|
|
630
816
|
export const buildNavigation = (
|
|
631
817
|
pages: PageRecord[],
|
|
@@ -639,8 +825,18 @@ export const buildNavigation = (
|
|
|
639
825
|
selectors?: NavSelector[];
|
|
640
826
|
tabs?: NavTab[];
|
|
641
827
|
sidebar?: SidebarItemConfig[];
|
|
642
|
-
/**
|
|
828
|
+
/**
|
|
829
|
+
* Folder-meta lookup prefix (`""` for the default locale of the current
|
|
830
|
+
* version): the version dir and/or locale dir hoisted in front of the
|
|
831
|
+
* group path, e.g. `fr`, `v1.0`, or `v1.0/fr`.
|
|
832
|
+
*/
|
|
643
833
|
metaPrefix?: string;
|
|
834
|
+
/**
|
|
835
|
+
* Prefix for shared `meta.$.*` lookups — the version dir inside a
|
|
836
|
+
* snapshot (`v1.0`), since shared meta is locale-agnostic but still
|
|
837
|
+
* version-specific. `""` for the current version.
|
|
838
|
+
*/
|
|
839
|
+
sharedMetaPrefix?: string;
|
|
644
840
|
/**
|
|
645
841
|
* Resolve explicit-sidebar references against each page's locale-agnostic
|
|
646
842
|
* `translationKey` instead of its localized `route`. Used under i18n so a
|
|
@@ -667,38 +863,13 @@ export const buildNavigation = (
|
|
|
667
863
|
const basePath = options.basePath ?? "";
|
|
668
864
|
const display = options.display ?? "flat";
|
|
669
865
|
const metaPrefix = options.metaPrefix ?? "";
|
|
866
|
+
const sharedMetaPrefix = options.sharedMetaPrefix ?? "";
|
|
670
867
|
const sharedFolderMeta = options.sharedFolderMeta ?? new Map();
|
|
671
868
|
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
// agree with the based content routes. With no base, this is a pure pass-
|
|
677
|
-
// through — the arrays keep their exact authored shape.
|
|
678
|
-
const rebasePath = <T extends { path: string }>(item: T): T => ({
|
|
679
|
-
...item,
|
|
680
|
-
path: withBasePath(basePath, item.path),
|
|
681
|
-
});
|
|
682
|
-
const featured = basePath
|
|
683
|
-
? (options.featured ?? []).map((link) => ({
|
|
684
|
-
...link,
|
|
685
|
-
href: withBasePath(basePath, link.href),
|
|
686
|
-
}))
|
|
687
|
-
: (options.featured ?? []);
|
|
688
|
-
const selectors = basePath
|
|
689
|
-
? (options.selectors ?? []).map((selector) => ({
|
|
690
|
-
...selector,
|
|
691
|
-
items: selector.items.map(rebasePath),
|
|
692
|
-
}))
|
|
693
|
-
: (options.selectors ?? []);
|
|
694
|
-
const tabs = basePath
|
|
695
|
-
? (options.tabs ?? []).map((tab) => ({
|
|
696
|
-
...tab,
|
|
697
|
-
...(tab.href ? { href: withBasePath(basePath, tab.href) } : {}),
|
|
698
|
-
items: tab.items?.map(rebasePath),
|
|
699
|
-
path: withBasePath(basePath, tab.path),
|
|
700
|
-
}))
|
|
701
|
-
: (options.tabs ?? []);
|
|
869
|
+
// Content-derived sidebar routes are already based via `page.route`; the
|
|
870
|
+
// based tab paths also feed tab-scoping below, so they must agree with the
|
|
871
|
+
// based content routes.
|
|
872
|
+
const { featured, selectors, tabs } = rebaseNavChrome(basePath, options);
|
|
702
873
|
const byRoute = new Map(
|
|
703
874
|
pages.map((page) => [
|
|
704
875
|
options.refByLogical ? page.translationKey : page.route,
|
|
@@ -727,6 +898,14 @@ export const buildNavigation = (
|
|
|
727
898
|
// returned navigation so render-time scoping compares in the same space too.
|
|
728
899
|
const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
|
|
729
900
|
|
|
901
|
+
// Emitted here, before the sidebar-mode branch: an explicit config sidebar
|
|
902
|
+
// ignores a stray `sidebar.display` just like the filesystem sidebar does
|
|
903
|
+
// (and ignores it on index pages too), so both paths must warn.
|
|
904
|
+
const diagnostics = options.diagnostics ?? [];
|
|
905
|
+
diagnostics.push(
|
|
906
|
+
...sidebarDisplayIgnoredDiagnostics(pages, Boolean(options.sidebar))
|
|
907
|
+
);
|
|
908
|
+
|
|
730
909
|
if (options.sidebar) {
|
|
731
910
|
const sidebar = buildConfigSidebar(
|
|
732
911
|
options.sidebar,
|
|
@@ -748,11 +927,12 @@ export const buildNavigation = (
|
|
|
748
927
|
options.folderMeta,
|
|
749
928
|
sharedFolderMeta,
|
|
750
929
|
metaPrefix,
|
|
930
|
+
sharedMetaPrefix,
|
|
751
931
|
display,
|
|
752
932
|
new Set(
|
|
753
933
|
tabs.flatMap((tab) => (tab.path === rootTabPath ? [] : [tab.path]))
|
|
754
934
|
),
|
|
755
|
-
|
|
935
|
+
diagnostics
|
|
756
936
|
);
|
|
757
937
|
return {
|
|
758
938
|
featured,
|
package/src/core/probe.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pMap from "p-map";
|
|
2
|
+
|
|
1
3
|
import type { DiagnosticSeverity } from "./types.ts";
|
|
2
4
|
|
|
3
5
|
export const PROBE_CONCURRENCY = 8;
|
|
@@ -113,24 +115,10 @@ export const probeAll = async (
|
|
|
113
115
|
options: { concurrency?: number; timeoutMs?: number } = {}
|
|
114
116
|
): Promise<Map<string, ProbeResult>> => {
|
|
115
117
|
const unique = [...new Set(urls)];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
options
|
|
119
|
-
|
|
118
|
+
const entries = await pMap(
|
|
119
|
+
unique,
|
|
120
|
+
async (url) => [url, await probe(url, options)] as const,
|
|
121
|
+
{ concurrency: Math.max(1, options.concurrency ?? PROBE_CONCURRENCY) }
|
|
120
122
|
);
|
|
121
|
-
|
|
122
|
-
let cursor = 0;
|
|
123
|
-
const worker = async (): Promise<void> => {
|
|
124
|
-
while (cursor < unique.length) {
|
|
125
|
-
const url = unique[cursor];
|
|
126
|
-
cursor += 1;
|
|
127
|
-
if (url !== undefined) {
|
|
128
|
-
// oxlint-disable-next-line no-await-in-loop -- bounded-concurrency pool
|
|
129
|
-
results.set(url, await probe(url, options));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
await Promise.all(Array.from({ length: limit }, worker));
|
|
134
|
-
|
|
135
|
-
return results;
|
|
123
|
+
return new Map(entries);
|
|
136
124
|
};
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
PageRecord,
|
|
25
25
|
ProjectContext,
|
|
26
26
|
} from "./types.ts";
|
|
27
|
+
import { versionsDiagnostics } from "./versions.ts";
|
|
27
28
|
|
|
28
29
|
/** Build mode: drafts are kept in `dev` and dropped in `build`. */
|
|
29
30
|
export type BuildMode = "dev" | "build";
|
|
@@ -142,7 +143,7 @@ const entryIdDiagnostics = (
|
|
|
142
143
|
const normalizeLoadedEntries = (
|
|
143
144
|
loaded: ({ source: ContentSource } & SourceLoadResult)[],
|
|
144
145
|
config: ResolvedConfig
|
|
145
|
-
)
|
|
146
|
+
) => {
|
|
146
147
|
// Only thread `frontmatter.extend` / `content.types` through when a project
|
|
147
148
|
// opts in, so the known-key split in `normalizeEntry` stays off the default
|
|
148
149
|
// path.
|
|
@@ -177,6 +178,7 @@ const normalizeLoadedEntries = (
|
|
|
177
178
|
staged: source.staged,
|
|
178
179
|
},
|
|
179
180
|
typeFrontmatter,
|
|
181
|
+
versions: config.versions,
|
|
180
182
|
});
|
|
181
183
|
if (normalized.pages.length === 0 && normalized.diagnostics.length > 0) {
|
|
182
184
|
droppedPages += 1;
|
|
@@ -259,7 +261,21 @@ export const scanProject = async (
|
|
|
259
261
|
Promise.all(
|
|
260
262
|
sources.map(async (source) => ({ source, ...(await source.load()) }))
|
|
261
263
|
),
|
|
262
|
-
discoverFolderMeta(metaSources, {
|
|
264
|
+
discoverFolderMeta(metaSources, {
|
|
265
|
+
localeDirs,
|
|
266
|
+
versionDirs: config.versions?.archived.map((version) => version.id),
|
|
267
|
+
}),
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
// Folder meta contributed by the sources themselves (the OpenAPI source
|
|
271
|
+
// labels each tag directory with the spec's own tag name). It applies to
|
|
272
|
+
// every locale, so it merges into the shared map — beneath user-authored
|
|
273
|
+
// entries, which are spread last and win.
|
|
274
|
+
const sharedFolderMeta = new Map([
|
|
275
|
+
...loaded.flatMap(({ folderMeta: sourceMeta }) =>
|
|
276
|
+
Object.entries(sourceMeta ?? {})
|
|
277
|
+
),
|
|
278
|
+
...folderMeta.shared,
|
|
263
279
|
]);
|
|
264
280
|
|
|
265
281
|
const {
|
|
@@ -301,11 +317,17 @@ export const scanProject = async (
|
|
|
301
317
|
folderMeta: folderMeta.meta,
|
|
302
318
|
i18n: config.i18n,
|
|
303
319
|
navigation: config.navigation,
|
|
304
|
-
sharedFolderMeta
|
|
320
|
+
sharedFolderMeta,
|
|
321
|
+
versions: config.versions,
|
|
305
322
|
});
|
|
306
323
|
const manifest = buildManifest({ config, context, graph });
|
|
307
324
|
|
|
308
|
-
const i18nWarnings = config.i18n
|
|
325
|
+
const i18nWarnings = config.i18n
|
|
326
|
+
? i18nDiagnostics(pages, config.i18n, config.versions)
|
|
327
|
+
: [];
|
|
328
|
+
const versionWarnings = config.versions
|
|
329
|
+
? versionsDiagnostics(pages, config.versions)
|
|
330
|
+
: [];
|
|
309
331
|
|
|
310
332
|
return {
|
|
311
333
|
config,
|
|
@@ -316,6 +338,7 @@ export const scanProject = async (
|
|
|
316
338
|
...entryIdDiagnostics(pages, resolveDocsCollection(config, context).base),
|
|
317
339
|
...graph.diagnostics,
|
|
318
340
|
...i18nWarnings,
|
|
341
|
+
...versionWarnings,
|
|
319
342
|
],
|
|
320
343
|
droppedPages,
|
|
321
344
|
graph,
|