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.
Files changed (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/index.js +2260 -1100
  3. package/dist/cli/index.js.map +123 -117
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/base-path.d.ts +8 -0
  6. package/dist/types/core/config-input.d.ts +87 -27
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +16 -1
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +116 -15
  12. package/dist/types/core/sources/types.d.ts +11 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +14 -7
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/docs/advanced/api-reference.mdx +8 -6
  19. package/docs/configuration/ai.mdx +26 -8
  20. package/docs/configuration/search.mdx +2 -0
  21. package/docs/configuration/seo.mdx +1 -1
  22. package/docs/content/i18n.mdx +1 -1
  23. package/docs/content/meta.mdx +2 -1
  24. package/docs/content/meta.ts +1 -0
  25. package/docs/content/navigation.mdx +35 -1
  26. package/docs/content/sources.mdx +1 -1
  27. package/docs/content/versioning.mdx +106 -0
  28. package/docs/reference/cli.mdx +1 -0
  29. package/docs/reference/frontmatter.mdx +3 -0
  30. package/package.json +13 -1
  31. package/skills/blume-migrate/SKILL.md +2 -2
  32. package/skills/blume-migrate/references/docusaurus.md +1 -1
  33. package/skills/blume-migrate/references/fumadocs.md +1 -1
  34. package/skills/blume-migrate/references/mintlify.md +1 -1
  35. package/src/ai/agent-readability.ts +40 -12
  36. package/src/ai/api-catalog.ts +2 -2
  37. package/src/ai/ask-context.ts +49 -12
  38. package/src/ai/ask.ts +10 -1
  39. package/src/ai/component-markdown.ts +80 -43
  40. package/src/ai/llms.ts +42 -17
  41. package/src/ai/mcp/data.ts +48 -12
  42. package/src/ai/mcp/discovery.ts +52 -16
  43. package/src/ai/mcp/server.ts +280 -125
  44. package/src/ai/mcp/tools.ts +3 -3
  45. package/src/ai/skills.ts +32 -9
  46. package/src/ai/tar.ts +29 -70
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +13 -5
  50. package/src/astro/generate.ts +113 -63
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +23 -12
  53. package/src/astro/templates.ts +185 -41
  54. package/src/audit/agent.ts +16 -31
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +78 -25
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +15 -5
  64. package/src/audit/snapshot.ts +29 -6
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +21 -21
  68. package/src/cli/commands/build.ts +30 -16
  69. package/src/cli/commands/dev.ts +15 -15
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eject.ts +4 -4
  72. package/src/cli/commands/eval.ts +24 -30
  73. package/src/cli/commands/init.ts +9 -9
  74. package/src/cli/commands/mcp-stdio.ts +3 -0
  75. package/src/cli/commands/translate.ts +14 -3
  76. package/src/cli/commands/version.ts +85 -0
  77. package/src/cli/dev-lock.ts +31 -10
  78. package/src/cli/eject-scripts.ts +17 -2
  79. package/src/cli/env.ts +13 -30
  80. package/src/cli/index.ts +2 -0
  81. package/src/cli/init/questions.ts +1 -1
  82. package/src/cli/init/scaffold.ts +41 -13
  83. package/src/cli/internal-error.ts +1 -0
  84. package/src/cli/report-format.ts +22 -0
  85. package/src/components/content/AccordionItem.astro +2 -9
  86. package/src/components/content/ColorItem.astro +5 -13
  87. package/src/components/content/Component.astro +12 -8
  88. package/src/components/content/Frame.astro +2 -12
  89. package/src/components/content/Prompt.astro +12 -31
  90. package/src/components/content/Tab.astro +2 -9
  91. package/src/components/content/Tooltip.astro +1 -9
  92. package/src/components/content/Update.astro +2 -9
  93. package/src/components/content/auto-type-table.ts +3 -0
  94. package/src/components/content/diff.ts +9 -5
  95. package/src/components/content/github-info.ts +2 -0
  96. package/src/components/content/inline-markdown.ts +28 -0
  97. package/src/components/copy-feedback.ts +96 -0
  98. package/src/components/islands/ask-ai.tsx +111 -34
  99. package/src/components/islands/hooks.ts +5 -1
  100. package/src/components/islands/webmcp.ts +49 -12
  101. package/src/components/layout/Header.astro +25 -1
  102. package/src/components/layout/NavSelector.astro +11 -2
  103. package/src/components/layout/NavTree.astro +4 -2
  104. package/src/components/layout/PageActions.astro +20 -32
  105. package/src/components/layout/PageLayout.astro +8 -28
  106. package/src/components/layout/RootLayout.astro +24 -48
  107. package/src/components/layout/Search.astro +133 -22
  108. package/src/components/layout/VersionBanner.astro +39 -0
  109. package/src/components/layout/analytics-client.ts +8 -5
  110. package/src/components/layout/drawer-inert.ts +31 -0
  111. package/src/components/layout/hydration-hint.ts +1 -1
  112. package/src/components/layout/nav-utils.ts +1 -4
  113. package/src/components/layout/overrides.ts +25 -12
  114. package/src/components/layout/search/algolia.ts +18 -5
  115. package/src/components/layout/search/endpoint.ts +3 -0
  116. package/src/components/layout/search/flexsearch.ts +23 -7
  117. package/src/components/layout/search/orama-cloud.ts +1 -1
  118. package/src/components/layout/search/orama.ts +4 -1
  119. package/src/components/layout/search/pagefind.ts +8 -5
  120. package/src/components/layout/search/types.ts +45 -1
  121. package/src/components/layout/search/typesense.ts +19 -3
  122. package/src/components/openapi/ApiOverview.astro +32 -6
  123. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  124. package/src/components/openapi/Bindings.astro +89 -0
  125. package/src/components/openapi/MethodBadge.astro +3 -0
  126. package/src/components/openapi/Operation.astro +7 -2
  127. package/src/components/openapi/PanelTabs.astro +131 -0
  128. package/src/components/openapi/ParametersTable.astro +2 -0
  129. package/src/components/openapi/RequestPanel.astro +12 -119
  130. package/src/components/openapi/async-snippets.ts +174 -0
  131. package/src/components/openapi/async.ts +348 -0
  132. package/src/components/openapi/helpers.ts +52 -20
  133. package/src/components/openapi/panel.ts +11 -8
  134. package/src/components/openapi/security.ts +102 -29
  135. package/src/components/openapi/snippets.ts +11 -11
  136. package/src/components/raf-throttle.ts +21 -0
  137. package/src/components/slug.ts +14 -0
  138. package/src/core/base-path.ts +18 -1
  139. package/src/core/component-overrides.ts +28 -23
  140. package/src/core/config-input.ts +96 -27
  141. package/src/core/config.ts +20 -7
  142. package/src/core/content.ts +3 -1
  143. package/src/core/data.ts +16 -1
  144. package/src/core/define-components.ts +5 -0
  145. package/src/core/diagnostics.ts +46 -38
  146. package/src/core/frontmatter.ts +74 -4
  147. package/src/core/graph.ts +137 -53
  148. package/src/core/i18n-ui.ts +15 -0
  149. package/src/core/i18n.ts +16 -8
  150. package/src/core/load-module.ts +1 -0
  151. package/src/core/manifest.ts +92 -3
  152. package/src/core/meta.ts +44 -14
  153. package/src/core/nav-diagnostics.ts +3 -3
  154. package/src/core/navigation.ts +247 -67
  155. package/src/core/probe.ts +7 -19
  156. package/src/core/project-graph.ts +27 -4
  157. package/src/core/schema.ts +219 -67
  158. package/src/core/site-url.ts +27 -0
  159. package/src/core/sources/assets.ts +2 -0
  160. package/src/core/sources/cache.ts +16 -8
  161. package/src/core/sources/github-releases.ts +39 -11
  162. package/src/core/sources/mdx-remote.ts +4 -0
  163. package/src/core/sources/normalize.ts +93 -22
  164. package/src/core/sources/notion.ts +76 -22
  165. package/src/core/sources/portable-text.ts +48 -12
  166. package/src/core/sources/resolve.ts +1 -0
  167. package/src/core/sources/sanity.ts +68 -14
  168. package/src/core/sources/types.ts +17 -1
  169. package/src/core/sources/watch.ts +1 -1
  170. package/src/core/standard-schema.ts +9 -3
  171. package/src/core/text-width.ts +26 -0
  172. package/src/core/tsconfig-aliases.ts +9 -5
  173. package/src/core/types.ts +45 -2
  174. package/src/core/ui-packs/index.ts +9 -1
  175. package/src/core/version-cut.ts +301 -0
  176. package/src/core/version.ts +2 -0
  177. package/src/core/versions.ts +170 -0
  178. package/src/deploy/adapter-output.ts +5 -2
  179. package/src/deploy/cloudflare-negotiation.ts +40 -11
  180. package/src/deploy/robots.ts +2 -1
  181. package/src/deploy/rss.ts +2 -1
  182. package/src/deploy/sitemap.ts +89 -8
  183. package/src/deploy/vercel-negotiation.ts +11 -4
  184. package/src/eval/agents.ts +13 -10
  185. package/src/eval/report.ts +5 -18
  186. package/src/eval/run.ts +2 -2
  187. package/src/eval/schema.ts +1 -1
  188. package/src/markdown/base-links.ts +6 -6
  189. package/src/markdown/directives.ts +7 -1
  190. package/src/markdown/heading-anchors.ts +17 -6
  191. package/src/markdown/index.ts +73 -24
  192. package/src/markdown/inline-code.ts +14 -2
  193. package/src/markdown/language-icon.ts +6 -2
  194. package/src/markdown/mdast.ts +18 -4
  195. package/src/markdown/package-commands.ts +63 -58
  196. package/src/markdown/table-wrap.ts +4 -1
  197. package/src/markdown/twoslash.ts +2 -0
  198. package/src/og/card.ts +50 -33
  199. package/src/og/derive.ts +43 -27
  200. package/src/openapi/asyncapi.ts +366 -0
  201. package/src/openapi/model.ts +135 -66
  202. package/src/openapi/parse.ts +166 -33
  203. package/src/openapi/references.ts +47 -22
  204. package/src/openapi/render-mdx.ts +137 -59
  205. package/src/openapi/scalar.ts +8 -10
  206. package/src/openapi/source.ts +126 -29
  207. package/src/registry/eject.ts +7 -2
  208. package/src/search/documents.ts +103 -39
  209. package/src/search/facets.ts +7 -5
  210. package/src/search/orama-index.ts +117 -32
  211. package/src/search/popular.ts +10 -5
  212. package/src/search/providers.ts +2 -2
  213. package/src/search/sync/index.ts +2 -0
  214. package/src/search/sync/typesense.ts +4 -2
  215. package/src/seo/jsonld.ts +24 -6
  216. package/src/seo/x-handle.ts +8 -3
  217. package/src/theme/chrome-icons.ts +7 -2
  218. package/src/theme/fonts.ts +8 -4
  219. package/src/theme/icons.ts +4 -2
  220. package/src/theme/palette.ts +27 -15
  221. package/src/translate/ledger.ts +4 -2
  222. package/src/translate/meta.ts +15 -6
  223. package/src/translate/report.ts +10 -19
  224. package/src/translate/run.ts +29 -38
  225. package/src/translate/validate.ts +52 -17
  226. package/src/translate/work-list.ts +0 -0
  227. package/src/cli/coalesce.ts +0 -43
package/src/core/i18n.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { ResolvedConfig, ResolvedI18nConfig } from "./schema.ts";
1
+ import type {
2
+ ResolvedConfig,
3
+ ResolvedI18nConfig,
4
+ ResolvedVersionsConfig,
5
+ } from "./schema.ts";
2
6
  import type { Diagnostic, PageRecord } from "./types.ts";
3
7
  import { UI_PACKS } from "./ui-packs/index.ts";
4
8
 
@@ -70,10 +74,7 @@ export const localizeRoute = (
70
74
  * matched as a leading segment. Returns the resolved locale and the remaining
71
75
  * (locale-stripped) segments.
72
76
  */
73
- export const detectLocale = (
74
- parts: string[],
75
- i18n: ResolvedI18nConfig
76
- ): { locale: string; rest: string[] } => {
77
+ export const detectLocale = (parts: string[], i18n: ResolvedI18nConfig) => {
77
78
  // BCP 47 codes are case-insensitive: a conventional lowercase folder
78
79
  // (`pt-br/`) must match a configured `pt-BR`. The configured casing is what
79
80
  // flows into routes and labels.
@@ -102,7 +103,7 @@ export const localePlacement = (
102
103
  rel: string,
103
104
  ext: string,
104
105
  i18n: ResolvedI18nConfig
105
- ): { navPath: string; locales: string[] } => {
106
+ ) => {
106
107
  const base = rel.slice(0, rel.length - ext.length);
107
108
 
108
109
  // Shared `$` file: the same content in every locale. A shared file placed
@@ -187,17 +188,24 @@ export const localeTargetPath = (
187
188
  */
188
189
  export const i18nDiagnostics = (
189
190
  pages: PageRecord[],
190
- i18n: ResolvedI18nConfig
191
+ i18n: ResolvedI18nConfig,
192
+ versions?: ResolvedVersionsConfig
191
193
  ): Diagnostic[] => {
192
194
  const configured = new Set(
193
195
  i18n.locales.map((locale) => locale.code.toLowerCase())
194
196
  );
197
+ const versionDirs = new Set(versions?.archived.map((version) => version.id));
195
198
  const seen = new Set<string>();
196
199
  const diagnostics: Diagnostic[] = [];
197
200
  for (const page of pages) {
198
201
  // The locale-looking folder is the first segment of the source-local ref
199
202
  // (e.g. `fr/guide.md`), not the namespaced id (`filesystem:fr/guide.md`).
200
- const first = page.source.ref.split("/")[0]?.toLowerCase();
203
+ // Inside a version snapshot the locale dir sits one level deeper
204
+ // (`v1.0/fr/guide.md`), so a configured version segment is skipped first.
205
+ const parts = page.source.ref.split("/");
206
+ const first = (
207
+ parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0]
208
+ )?.toLowerCase();
201
209
  if (
202
210
  first &&
203
211
  !seen.has(first) &&
@@ -6,6 +6,7 @@ import { createJiti } from "jiti";
6
6
  * is called with. `moduleCache: false` ensures edits are picked up on each load,
7
7
  * which is what makes dev-server regeneration reflect config/meta changes.
8
8
  */
9
+ // oxlint-disable-next-line anti-slop/no-unknown-returns -- user-authored modules can export anything; callers validate the loaded value at their own boundary
9
10
  export const createModuleLoader = (): ((file: string) => Promise<unknown>) => {
10
11
  const jiti = createJiti(import.meta.url, { moduleCache: false });
11
12
  return async (file: string) => {
@@ -8,9 +8,39 @@ import type {
8
8
  ProjectContext,
9
9
  RouteAlternate,
10
10
  RouteManifestEntry,
11
+ VersionAlternate,
11
12
  } from "./types.ts";
12
13
  import { getBlumeVersion } from "./version.ts";
13
14
 
15
+ /**
16
+ * Key for the same logical page across versions within one locale: the
17
+ * version- and locale-agnostic route plus the locale code. NUL never appears
18
+ * in either part, so the join is unambiguous.
19
+ */
20
+ const versionAlternateKey = (versionKey: string, locale: string): string =>
21
+ `${versionKey}\u0000${locale}`;
22
+
23
+ /**
24
+ * Get-or-create the shared alternate list for a (versionKey, locale) pair.
25
+ * Lists are attached to route entries by reference and mutated as fallback
26
+ * routes materialize, then sorted once at the end — every holder sees the
27
+ * final list.
28
+ */
29
+ const versionAlternatesFor = (
30
+ byKey: Map<string, VersionAlternate[]>,
31
+ versionKey: string,
32
+ locale: string
33
+ ): VersionAlternate[] => {
34
+ const key = versionAlternateKey(versionKey, locale);
35
+ const existing = byKey.get(key);
36
+ if (existing) {
37
+ return existing;
38
+ }
39
+ const list: VersionAlternate[] = [];
40
+ byKey.set(key, list);
41
+ return list;
42
+ };
43
+
14
44
  /** The current manifest schema version. */
15
45
  export const MANIFEST_VERSION = 1;
16
46
 
@@ -37,7 +67,8 @@ const buildFallbackRoutes = (
37
67
  graph: ContentGraph,
38
68
  i18n: NonNullable<ResolvedConfig["i18n"]>,
39
69
  alternatesByKey: Map<string, RouteAlternate[]>,
40
- basePath: string
70
+ basePath: string,
71
+ versionAlternatesByKey: Map<string, VersionAlternate[]> | undefined
41
72
  ): RouteManifestEntry[] => {
42
73
  const fallback = resolveFallbackLocale(i18n);
43
74
  if (!fallback) {
@@ -62,6 +93,17 @@ const buildFallbackRoutes = (
62
93
  if (present.has(key)) {
63
94
  continue;
64
95
  }
96
+ const path = withBasePath(basePath, localizeRoute(key, code, i18n));
97
+ // A fallback route is a real prerendered page, so it registers as a
98
+ // version alternate too — the switcher on a sibling version's page
99
+ // lands here instead of bouncing to the version root. Its own path is
100
+ // recorded (not the fallback source's), keeping the target in-locale.
101
+ const versionAlternates: VersionAlternate[] = versionAlternatesByKey
102
+ ? versionAlternatesFor(versionAlternatesByKey, source.versionKey, code)
103
+ : [];
104
+ if (versionAlternatesByKey) {
105
+ versionAlternates.push({ path, version: source.version });
106
+ }
65
107
  routes.push({
66
108
  alternates: alternatesByKey.get(key) ?? [],
67
109
  collection: source.collection ?? "docs",
@@ -75,10 +117,12 @@ const buildFallbackRoutes = (
75
117
  indexable: false,
76
118
  lastModified: source.lastModified,
77
119
  locale: code,
78
- path: withBasePath(basePath, localizeRoute(key, code, i18n)),
120
+ path,
79
121
  source: source.source,
80
122
  sourcePath: source.sourcePath,
81
123
  title: source.title,
124
+ version: source.version,
125
+ versionAlternates,
82
126
  });
83
127
  }
84
128
  }
@@ -106,6 +150,23 @@ export const buildManifest = (options: {
106
150
  }
107
151
  }
108
152
 
153
+ // The same logical page across versions, within each locale — for the
154
+ // version switcher and the canonical-to-latest lookup. Built only under
155
+ // versioning; lists are shared by reference and finalized (fallback routes
156
+ // appended, then sorted) before the manifest is returned.
157
+ const versionAlternatesByKey = config.versions
158
+ ? new Map<string, VersionAlternate[]>()
159
+ : undefined;
160
+ if (versionAlternatesByKey) {
161
+ for (const page of graph.pages) {
162
+ versionAlternatesFor(
163
+ versionAlternatesByKey,
164
+ page.versionKey,
165
+ page.locale
166
+ ).push({ path: page.route, version: page.version });
167
+ }
168
+ }
169
+
109
170
  const routes: RouteManifestEntry[] = graph.pages.map((page) => ({
110
171
  alternates: alternatesByKey.get(page.translationKey) ?? [],
111
172
  collection: page.collection ?? "docs",
@@ -122,14 +183,42 @@ export const buildManifest = (options: {
122
183
  source: page.source,
123
184
  sourcePath: page.sourcePath,
124
185
  title: page.title,
186
+ version: page.version,
187
+ versionAlternates: versionAlternatesByKey
188
+ ? versionAlternatesFor(
189
+ versionAlternatesByKey,
190
+ page.versionKey,
191
+ page.locale
192
+ )
193
+ : [],
125
194
  }));
126
195
 
127
196
  if (i18n) {
128
197
  routes.push(
129
- ...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath)
198
+ ...buildFallbackRoutes(
199
+ graph,
200
+ i18n,
201
+ alternatesByKey,
202
+ config.basePath,
203
+ versionAlternatesByKey
204
+ )
130
205
  );
131
206
  }
132
207
 
208
+ // Alternate lists read current-first, then archived versions in configured
209
+ // (switcher) order. Sorted once here — every route holding a list by
210
+ // reference sees the final ordering.
211
+ if (versionAlternatesByKey && config.versions) {
212
+ const rank = new Map<string, number>(
213
+ config.versions.archived.map((version, index) => [version.id, index + 1])
214
+ );
215
+ for (const list of versionAlternatesByKey.values()) {
216
+ list.sort(
217
+ (a, b) => (rank.get(a.version) ?? 0) - (rank.get(b.version) ?? 0)
218
+ );
219
+ }
220
+ }
221
+
133
222
  routes.sort((a, b) => a.path.localeCompare(b.path));
134
223
 
135
224
  return {
package/src/core/meta.ts CHANGED
@@ -17,9 +17,23 @@ const META_FILES = [
17
17
  "**/meta.$.mjs",
18
18
  ];
19
19
 
20
+ /**
21
+ * A meta module's default export before validation: `folderMetaSchema` parses
22
+ * it only after any factory is resolved, so it carries the loader's raw type.
23
+ */
24
+ type MetaModuleExport = Awaited<
25
+ ReturnType<ReturnType<typeof createModuleLoader>>
26
+ >;
27
+
28
+ /** A factory-style meta module default-exports a function returning the meta. */
29
+ const isMetaFactory = (
30
+ mod: MetaModuleExport
31
+ ): mod is () => MetaModuleExport | Promise<MetaModuleExport> =>
32
+ typeof mod === "function";
33
+
20
34
  /** Resolve a meta module's default export, calling it if it is a factory. */
21
- const resolveMeta = async (mod: unknown): Promise<unknown> =>
22
- typeof mod === "function" ? await (mod as () => unknown)() : mod;
35
+ const resolveMeta = async (mod: MetaModuleExport) =>
36
+ isMetaFactory(mod) ? await mod() : mod;
23
37
 
24
38
  /** A filesystem content source to scan for folder meta: its on-disk root and
25
39
  * optional route prefix. The prefix is folded into every key so meta lines up
@@ -58,18 +72,28 @@ const metaKeyFor = (prefix: string | undefined, dir: string): string => {
58
72
  * group path starts with the source prefix, so a locale directory found at a
59
73
  * source root is hoisted in front of the prefix (`docs/fr/guides/meta.ts` keys
60
74
  * to `fr/docs/guides`, not `docs/fr/guides`).
75
+ *
76
+ * `versionDirs` names the archived-version snapshot directories, which sit
77
+ * outermost on disk — a locale directory inside a snapshot is one level deeper.
78
+ * Both are hoisted, version first (`v1.0/fr/guides/meta.ts` keys to
79
+ * `v1.0/fr/<prefix>/guides`), matching navigation's version-aware meta prefix.
61
80
  */
62
81
  export const discoverFolderMeta = async (
63
82
  sources: string | FolderMetaSource[],
64
- options: { localeDirs?: readonly string[] } = {}
83
+ options: {
84
+ localeDirs?: readonly string[];
85
+ versionDirs?: readonly string[];
86
+ } = {}
65
87
  ): Promise<{
66
88
  meta: Map<string, FolderMeta>;
67
89
  shared: Map<string, FolderMeta>;
68
90
  diagnostics: Diagnostic[];
69
91
  }> => {
70
- const list: FolderMetaSource[] =
71
- typeof sources === "string" ? [{ root: sources }] : sources;
92
+ const list: FolderMetaSource[] = Array.isArray(sources)
93
+ ? sources
94
+ : [{ root: sources }];
72
95
  const localeDirs = new Set(options.localeDirs);
96
+ const versionDirs = new Set(options.versionDirs);
73
97
 
74
98
  const load = createModuleLoader();
75
99
  const meta = new Map<string, FolderMeta>();
@@ -103,6 +127,8 @@ export const discoverFolderMeta = async (
103
127
  value: await resolveMeta(await load(file)),
104
128
  };
105
129
  } catch (error) {
130
+ // SAFETY: jiti surfaces load/evaluate failures as Error
131
+ // instances, and only `message` is read downstream.
106
132
  return { error: error as Error, file, ok: false };
107
133
  }
108
134
  }
@@ -115,16 +141,20 @@ export const discoverFolderMeta = async (
115
141
  for (const { loaded, source } of perSource) {
116
142
  for (const entry of loaded) {
117
143
  const dir = relative(source.root, dirname(entry.file));
144
+ // A version snapshot dir is outermost, with a locale dir one level
145
+ // deeper; both are hoisted in front of the (prefixed) group path, in
146
+ // that order — the lookup key reads `version/locale/prefix/dir`.
118
147
  const [head, ...tail] = dir.split("/");
119
- // A locale directory sits between the source root and the folder, but the
120
- // lookup key carries the locale in front of the (prefixed) group path.
121
- const key =
122
- head && localeDirs.has(head)
123
- ? `${head}/${metaKeyFor(source.prefix, tail.join("/"))}`.replace(
124
- /\/$/u,
125
- ""
126
- )
127
- : metaKeyFor(source.prefix, dir);
148
+ const version = head && versionDirs.has(head) ? head : "";
149
+ const afterVersion = version ? tail : [head ?? "", ...tail];
150
+ const [localeHead, ...localeTail] = afterVersion;
151
+ const locale = localeHead && localeDirs.has(localeHead) ? localeHead : "";
152
+ const rest = (locale ? localeTail : afterVersion)
153
+ .filter(Boolean)
154
+ .join("/");
155
+ const key = [version, locale, metaKeyFor(source.prefix, rest)]
156
+ .filter(Boolean)
157
+ .join("/");
128
158
 
129
159
  if (!entry.ok) {
130
160
  diagnostics.push({
@@ -9,7 +9,7 @@ import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
9
9
  * covers every source (config, folder meta, frontmatter) at once.
10
10
  */
11
11
 
12
- const ICON_SHAPE_HINT =
12
+ const ICON_FORMAT_HINT =
13
13
  "Use a built-in icon name, an image path/URL, or inline SVG markup.";
14
14
 
15
15
  /** Flatten a sidebar tree to every node, descending into groups. */
@@ -80,7 +80,7 @@ const unknownIconDiagnostics = (
80
80
 
81
81
  /** Warn about icon names that aren't in Blume's set (skipping image/SVG icons). */
82
82
  export const validateNavIcons = (navigation: Navigation): Diagnostic[] =>
83
- unknownIconDiagnostics(collectIcons(navigation), ICON_SHAPE_HINT);
83
+ unknownIconDiagnostics(collectIcons(navigation), ICON_FORMAT_HINT);
84
84
 
85
85
  /**
86
86
  * Warn about unknown icons on curated `search.popular` links. Same accepted
@@ -94,7 +94,7 @@ export const validateSearchPopularIcons = (
94
94
  ? [{ icon: link.icon, where: `popular link "${link.label}"` }]
95
95
  : []
96
96
  );
97
- return unknownIconDiagnostics(icons, ICON_SHAPE_HINT);
97
+ return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
98
98
  };
99
99
 
100
100
  /** Whether an internal path resolves to a page or a section that has pages. */