boltdocs 1.10.1 → 1.11.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 (226) hide show
  1. package/package.json +29 -7
  2. package/src/client/app/config-context.tsx +18 -0
  3. package/src/client/app/docs-layout.tsx +14 -0
  4. package/src/client/app/index.tsx +132 -260
  5. package/src/client/app/mdx-component.tsx +52 -0
  6. package/src/client/app/mdx-components-context.tsx +23 -0
  7. package/src/client/app/mdx-page.tsx +20 -0
  8. package/src/client/app/preload.tsx +38 -30
  9. package/src/client/app/router.tsx +30 -0
  10. package/src/client/app/scroll-handler.tsx +40 -0
  11. package/src/client/app/theme-context.tsx +75 -0
  12. package/src/client/components/default-layout.tsx +80 -0
  13. package/src/client/components/docs-layout.tsx +105 -0
  14. package/src/client/components/icons-dev.tsx +74 -0
  15. package/src/client/components/mdx/admonition.tsx +107 -0
  16. package/src/client/components/mdx/badge.tsx +41 -0
  17. package/src/client/components/mdx/button.tsx +35 -0
  18. package/src/client/components/mdx/card.tsx +124 -0
  19. package/src/client/components/mdx/code-block.tsx +119 -0
  20. package/src/client/components/mdx/component-preview.tsx +47 -0
  21. package/src/client/components/mdx/component-props.tsx +83 -0
  22. package/src/client/components/mdx/field.tsx +66 -0
  23. package/src/client/components/mdx/file-tree.tsx +287 -0
  24. package/src/client/components/mdx/hooks/use-code-block.ts +56 -0
  25. package/src/client/components/mdx/hooks/use-component-preview.ts +16 -0
  26. package/src/client/components/mdx/hooks/useTable.ts +74 -0
  27. package/src/client/components/mdx/hooks/useTabs.ts +68 -0
  28. package/src/client/components/mdx/image.tsx +23 -0
  29. package/src/client/components/mdx/index.ts +53 -0
  30. package/src/client/components/mdx/link.tsx +38 -0
  31. package/src/client/components/mdx/list.tsx +192 -0
  32. package/src/client/components/mdx/table.tsx +156 -0
  33. package/src/client/components/mdx/tabs.tsx +135 -0
  34. package/src/client/components/mdx/video.tsx +68 -0
  35. package/src/client/components/primitives/breadcrumbs.tsx +79 -0
  36. package/src/client/components/primitives/button-group.tsx +54 -0
  37. package/src/client/components/primitives/button.tsx +145 -0
  38. package/src/client/components/primitives/helpers/observer.ts +120 -0
  39. package/src/client/components/primitives/index.ts +17 -0
  40. package/src/client/components/primitives/link.tsx +122 -0
  41. package/src/client/components/primitives/menu.tsx +159 -0
  42. package/src/client/components/primitives/navbar.tsx +359 -0
  43. package/src/client/components/primitives/navigation-menu.tsx +116 -0
  44. package/src/client/components/primitives/on-this-page.tsx +461 -0
  45. package/src/client/components/primitives/page-nav.tsx +87 -0
  46. package/src/client/components/primitives/popover.tsx +47 -0
  47. package/src/client/components/primitives/search-dialog.tsx +183 -0
  48. package/src/client/components/primitives/sidebar.tsx +154 -0
  49. package/src/client/components/primitives/tabs.tsx +90 -0
  50. package/src/client/components/primitives/tooltip.tsx +83 -0
  51. package/src/client/components/primitives/types.ts +11 -0
  52. package/src/client/components/ui-base/breadcrumbs.tsx +42 -0
  53. package/src/client/components/ui-base/copy-markdown.tsx +112 -0
  54. package/src/client/components/ui-base/error-boundary.tsx +52 -0
  55. package/src/client/components/ui-base/github-stars.tsx +27 -0
  56. package/src/client/components/ui-base/head.tsx +69 -0
  57. package/src/client/components/ui-base/loading.tsx +87 -0
  58. package/src/client/components/ui-base/navbar.tsx +138 -0
  59. package/src/client/components/ui-base/not-found.tsx +24 -0
  60. package/src/client/components/ui-base/on-this-page.tsx +152 -0
  61. package/src/client/components/ui-base/page-nav.tsx +39 -0
  62. package/src/client/components/ui-base/powered-by.tsx +19 -0
  63. package/src/client/components/ui-base/progress-bar.tsx +67 -0
  64. package/src/client/components/ui-base/search-dialog.tsx +82 -0
  65. package/src/client/components/ui-base/sidebar.tsx +104 -0
  66. package/src/client/components/ui-base/tabs.tsx +65 -0
  67. package/src/client/components/ui-base/theme-toggle.tsx +32 -0
  68. package/src/client/hooks/index.ts +12 -0
  69. package/src/client/hooks/use-breadcrumbs.ts +22 -0
  70. package/src/client/hooks/use-i18n.ts +84 -0
  71. package/src/client/hooks/use-localized-to.ts +95 -0
  72. package/src/client/hooks/use-location.ts +5 -0
  73. package/src/client/hooks/use-navbar.ts +60 -0
  74. package/src/client/hooks/use-onthispage.ts +23 -0
  75. package/src/client/hooks/use-page-nav.ts +22 -0
  76. package/src/client/hooks/use-routes.ts +72 -0
  77. package/src/client/hooks/use-search.ts +71 -0
  78. package/src/client/hooks/use-sidebar.ts +49 -0
  79. package/src/client/hooks/use-tabs.ts +43 -0
  80. package/src/client/hooks/use-version.ts +78 -0
  81. package/src/client/index.ts +55 -18
  82. package/src/client/integrations/codesandbox.ts +179 -0
  83. package/src/client/ssr.tsx +27 -16
  84. package/src/client/theme/neutral.css +360 -0
  85. package/src/client/types.ts +131 -27
  86. package/src/client/utils/cn.ts +6 -0
  87. package/src/client/utils/copy-clipboard.ts +22 -0
  88. package/src/client/utils/get-base-file-path.ts +21 -0
  89. package/src/client/utils/github.ts +121 -0
  90. package/src/client/utils/use-on-change.ts +15 -0
  91. package/src/client/virtual.d.ts +24 -0
  92. package/src/node/cache.ts +156 -156
  93. package/src/node/config.ts +159 -103
  94. package/src/node/index.ts +13 -13
  95. package/src/node/mdx.ts +213 -61
  96. package/src/node/plugin/entry.ts +29 -18
  97. package/src/node/plugin/html.ts +11 -11
  98. package/src/node/plugin/index.ts +161 -83
  99. package/src/node/plugin/types.ts +2 -4
  100. package/src/node/routes/cache.ts +6 -6
  101. package/src/node/routes/index.ts +206 -113
  102. package/src/node/routes/parser.ts +106 -81
  103. package/src/node/routes/sorter.ts +15 -15
  104. package/src/node/routes/types.ts +24 -24
  105. package/src/node/ssg/index.ts +46 -46
  106. package/src/node/ssg/meta.ts +4 -4
  107. package/src/node/ssg/options.ts +5 -5
  108. package/src/node/ssg/sitemap.ts +14 -14
  109. package/src/node/utils.ts +31 -31
  110. package/tsconfig.json +25 -20
  111. package/tsup.config.ts +23 -14
  112. package/dist/PackageManagerTabs-NVT7G625.mjs +0 -99
  113. package/dist/SearchDialog-BEVZQ74P.css +0 -2679
  114. package/dist/SearchDialog-MEWGAONO.mjs +0 -194
  115. package/dist/Video-KNTY5BNO.mjs +0 -6
  116. package/dist/cache-KNL5B4EE.mjs +0 -12
  117. package/dist/chunk-7SFUJWTB.mjs +0 -211
  118. package/dist/chunk-FFBNU6IJ.mjs +0 -386
  119. package/dist/chunk-FMTOYQLO.mjs +0 -37
  120. package/dist/chunk-OZLYRXAD.mjs +0 -1914
  121. package/dist/chunk-Z7JHYNAS.mjs +0 -57
  122. package/dist/client/index.css +0 -2679
  123. package/dist/client/index.d.mts +0 -379
  124. package/dist/client/index.d.ts +0 -379
  125. package/dist/client/index.js +0 -3594
  126. package/dist/client/index.mjs +0 -658
  127. package/dist/client/ssr.css +0 -2679
  128. package/dist/client/ssr.d.mts +0 -27
  129. package/dist/client/ssr.d.ts +0 -27
  130. package/dist/client/ssr.js +0 -2930
  131. package/dist/client/ssr.mjs +0 -33
  132. package/dist/config-BsFQ-ErD.d.mts +0 -159
  133. package/dist/config-BsFQ-ErD.d.ts +0 -159
  134. package/dist/node/index.d.mts +0 -91
  135. package/dist/node/index.d.ts +0 -91
  136. package/dist/node/index.js +0 -1187
  137. package/dist/node/index.mjs +0 -762
  138. package/dist/types-Dj-bfnC3.d.mts +0 -74
  139. package/dist/types-Dj-bfnC3.d.ts +0 -74
  140. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -40
  141. package/src/client/theme/components/CodeBlock/index.ts +0 -1
  142. package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +0 -131
  143. package/src/client/theme/components/PackageManagerTabs/index.ts +0 -1
  144. package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +0 -64
  145. package/src/client/theme/components/Playground/Playground.tsx +0 -124
  146. package/src/client/theme/components/Playground/index.ts +0 -1
  147. package/src/client/theme/components/Playground/playground.css +0 -168
  148. package/src/client/theme/components/Video/Video.tsx +0 -84
  149. package/src/client/theme/components/Video/index.ts +0 -1
  150. package/src/client/theme/components/Video/video.css +0 -41
  151. package/src/client/theme/components/mdx/Admonition.tsx +0 -80
  152. package/src/client/theme/components/mdx/Badge.tsx +0 -31
  153. package/src/client/theme/components/mdx/Button.tsx +0 -50
  154. package/src/client/theme/components/mdx/Card.tsx +0 -80
  155. package/src/client/theme/components/mdx/Field.tsx +0 -60
  156. package/src/client/theme/components/mdx/FileTree.tsx +0 -229
  157. package/src/client/theme/components/mdx/List.tsx +0 -57
  158. package/src/client/theme/components/mdx/Table.tsx +0 -151
  159. package/src/client/theme/components/mdx/Tabs.tsx +0 -123
  160. package/src/client/theme/components/mdx/index.ts +0 -27
  161. package/src/client/theme/components/mdx/mdx-components.css +0 -764
  162. package/src/client/theme/icons/bun.tsx +0 -62
  163. package/src/client/theme/icons/deno.tsx +0 -20
  164. package/src/client/theme/icons/discord.tsx +0 -12
  165. package/src/client/theme/icons/github.tsx +0 -15
  166. package/src/client/theme/icons/npm.tsx +0 -13
  167. package/src/client/theme/icons/pnpm.tsx +0 -72
  168. package/src/client/theme/icons/twitter.tsx +0 -12
  169. package/src/client/theme/styles/markdown.css +0 -341
  170. package/src/client/theme/styles/variables.css +0 -187
  171. package/src/client/theme/styles.css +0 -38
  172. package/src/client/theme/ui/BackgroundGradient/BackgroundGradient.tsx +0 -10
  173. package/src/client/theme/ui/BackgroundGradient/index.ts +0 -1
  174. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
  175. package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
  176. package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
  177. package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -114
  178. package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
  179. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -46
  180. package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
  181. package/src/client/theme/ui/Footer/footer.css +0 -32
  182. package/src/client/theme/ui/Head/Head.tsx +0 -69
  183. package/src/client/theme/ui/Head/index.ts +0 -1
  184. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
  185. package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
  186. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
  187. package/src/client/theme/ui/Layout/Layout.tsx +0 -208
  188. package/src/client/theme/ui/Layout/base.css +0 -105
  189. package/src/client/theme/ui/Layout/index.ts +0 -2
  190. package/src/client/theme/ui/Layout/pagination.css +0 -72
  191. package/src/client/theme/ui/Layout/responsive.css +0 -36
  192. package/src/client/theme/ui/Link/Link.tsx +0 -392
  193. package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
  194. package/src/client/theme/ui/Link/index.ts +0 -2
  195. package/src/client/theme/ui/Link/link-preview.css +0 -48
  196. package/src/client/theme/ui/Loading/Loading.tsx +0 -10
  197. package/src/client/theme/ui/Loading/index.ts +0 -1
  198. package/src/client/theme/ui/Loading/loading.css +0 -30
  199. package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
  200. package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
  201. package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
  202. package/src/client/theme/ui/Navbar/index.ts +0 -2
  203. package/src/client/theme/ui/Navbar/navbar.css +0 -347
  204. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
  205. package/src/client/theme/ui/NotFound/index.ts +0 -1
  206. package/src/client/theme/ui/NotFound/not-found.css +0 -64
  207. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
  208. package/src/client/theme/ui/OnThisPage/index.ts +0 -1
  209. package/src/client/theme/ui/OnThisPage/toc.css +0 -152
  210. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
  211. package/src/client/theme/ui/PoweredBy/index.ts +0 -1
  212. package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
  213. package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
  214. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
  215. package/src/client/theme/ui/ProgressBar/index.ts +0 -1
  216. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
  217. package/src/client/theme/ui/SearchDialog/index.ts +0 -1
  218. package/src/client/theme/ui/SearchDialog/search.css +0 -152
  219. package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
  220. package/src/client/theme/ui/Sidebar/index.ts +0 -1
  221. package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
  222. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
  223. package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
  224. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
  225. package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
  226. package/src/client/utils.ts +0 -49
@@ -1,179 +1,272 @@
1
- import fastGlob from "fast-glob";
2
- import { BoltdocsConfig } from "../config";
3
- import { capitalize } from "../utils";
1
+ import fastGlob from 'fast-glob'
2
+ import type { BoltdocsConfig } from '../config'
3
+ import { capitalize } from '../utils'
4
4
 
5
- import { RouteMeta, ParsedDocFile } from "./types";
6
- import { docCache, invalidateRouteCache, invalidateFile } from "./cache";
7
- import { parseDocFile } from "./parser";
8
- import { sortRoutes } from "./sorter";
5
+ import type { RouteMeta, ParsedDocFile } from './types'
6
+ import { docCache, invalidateRouteCache, invalidateFile } from './cache'
7
+ import { parseDocFile } from './parser'
8
+ import { sortRoutes } from './sorter'
9
9
 
10
10
  // Re-export public API
11
- export type { RouteMeta };
12
- export { invalidateRouteCache, invalidateFile };
11
+ export type { RouteMeta }
12
+ export { invalidateRouteCache, invalidateFile }
13
+
14
+ // Cache for localized path computations
15
+ const localizedPathCache = new Map<string, string>()
13
16
 
14
17
  /**
15
18
  * Generates the entire route map for the documentation site.
16
- * This reads all `.md` and `.mdx` files in the `docsDir`, parses them (using cache),
17
- * infers group hierarchies based on directory structure and `index.md` files,
18
- * and returns a sorted array of RouteMeta objects intended for the client.
19
+ * OPTIMIZED: Uses Map-based i18n lookups, chunked processing, and path caching.
20
+ *
21
+ * Automatically handles versioning and i18n routing, including fallback
22
+ * generation for missing translations.
19
23
  *
20
- * @param docsDir - The root directory containing markdown files
21
- * @param config - Optional configuration for i18n and versioning
22
- * @param basePath - The base URL path to prefix to generated routes (e.g., '/docs')
23
- * @returns A promise that resolves to the final list of RouteMeta objects
24
+ * @param docsDir - The root documentation directory
25
+ * @param config - The Boltdocs configuration
26
+ * @param basePath - The base URL path for the routes (default: '/docs')
27
+ * @returns A promise resolving to an array of RouteMeta objects
24
28
  */
25
29
  export async function generateRoutes(
26
30
  docsDir: string,
27
31
  config?: BoltdocsConfig,
28
- basePath: string = "/docs",
32
+ basePath: string = '/docs',
29
33
  ): Promise<RouteMeta[]> {
30
- // Load persistent cache on first call
31
- docCache.load();
32
- docCache.invalidateAll(); // FORCE RE-PARSE to pick up new _content field
34
+ const start = performance.now()
35
+
36
+ // Load persistent cache
37
+ docCache.load()
38
+
39
+ // Clear path computation cache between generations
40
+ localizedPathCache.clear()
41
+
42
+ // Force re-parse if specifically requested (e.g. for content/config changes)
43
+ if (process.env.BOLTDOCS_FORCE_REPARSE === 'true' || config?.i18n) {
44
+ docCache.invalidateAll()
45
+ }
33
46
 
34
- const files = await fastGlob(["**/*.md", "**/*.mdx"], {
47
+ // 1. FAST SCAN
48
+ const files = await fastGlob(['**/*.md', '**/*.mdx'], {
35
49
  cwd: docsDir,
36
50
  absolute: true,
37
- });
51
+ suppressErrors: true,
52
+ followSymbolicLinks: false,
53
+ })
38
54
 
39
55
  // Prune cache entries for deleted files
40
- docCache.pruneStale(new Set(files));
56
+ docCache.pruneStale(new Set(files))
41
57
 
42
- // Invalidate all caches if config changes drastically (e.g. i18n enabled)
43
- if (config?.i18n) {
44
- docCache.invalidateAll();
45
- }
58
+ // 2. CHUNKED PROCESSING (prevents blocking event loop)
59
+ const CHUNK_SIZE = 50
60
+ const parsed: ParsedDocFile[] = []
61
+ let cacheHits = 0
46
62
 
47
- // Parse files in parallel using Promise.all for increased efficiency
48
- let cacheHits = 0;
49
- const parsed: ParsedDocFile[] = await Promise.all(
50
- files.map(async (file) => {
51
- const cached = docCache.get(file);
52
- if (cached) {
53
- cacheHits++;
54
- return cached;
55
- }
63
+ for (let i = 0; i < files.length; i += CHUNK_SIZE) {
64
+ const chunk = files.slice(i, i + CHUNK_SIZE)
65
+
66
+ const chunkResults = await Promise.all(
67
+ chunk.map(async (file) => {
68
+ const cached = docCache.get(file)
69
+ if (cached) {
70
+ cacheHits++
71
+ return cached
72
+ }
73
+
74
+ const result = parseDocFile(file, docsDir, basePath, config)
75
+ docCache.set(file, result)
76
+ return result
77
+ }),
78
+ )
56
79
 
57
- const result = parseDocFile(file, docsDir, basePath, config);
58
- docCache.set(file, result);
59
- return result;
60
- }),
61
- );
80
+ parsed.push(...chunkResults)
62
81
 
63
- if (files.length > 0) {
64
- console.log(
65
- `[boltdocs] Routes generated: ${files.length} files (${cacheHits} from cache, ${files.length - cacheHits} parsed)`,
66
- );
82
+ // Yield to event loop between chunks if there's more to process
83
+ if (i + CHUNK_SIZE < files.length) {
84
+ await new Promise((resolve) => setImmediate(resolve))
85
+ }
67
86
  }
68
87
 
69
- // Save cache after batch processing
70
- docCache.save();
88
+ // Save cache after processing
89
+ docCache.save()
71
90
 
72
- // Collect group metadata from directory names and index files
91
+ // 3. OPTIMIZED METADATA COLLECTION
73
92
  const groupMeta = new Map<
74
93
  string,
75
94
  { title: string; position?: number; icon?: string }
76
- >();
95
+ >()
96
+ const groupIndexFiles: ParsedDocFile[] = []
97
+
77
98
  for (const p of parsed) {
99
+ if (p.isGroupIndex && p.relativeDir) {
100
+ groupIndexFiles.push(p)
101
+ }
102
+
78
103
  if (p.relativeDir) {
79
- if (!groupMeta.has(p.relativeDir)) {
80
- groupMeta.set(p.relativeDir, {
104
+ let entry = groupMeta.get(p.relativeDir)
105
+ if (!entry) {
106
+ entry = {
81
107
  title: capitalize(p.relativeDir),
82
108
  position: p.inferredGroupPosition,
83
109
  icon: p.route.icon,
84
- });
110
+ }
111
+ groupMeta.set(p.relativeDir, entry)
85
112
  } else {
86
- const entry = groupMeta.get(p.relativeDir)!;
87
113
  if (
88
114
  entry.position === undefined &&
89
115
  p.inferredGroupPosition !== undefined
90
116
  ) {
91
- entry.position = p.inferredGroupPosition;
117
+ entry.position = p.inferredGroupPosition
92
118
  }
93
119
  if (!entry.icon && p.route.icon) {
94
- entry.icon = p.route.icon;
120
+ entry.icon = p.route.icon
95
121
  }
96
122
  }
97
123
  }
124
+ }
98
125
 
99
- if (p.isGroupIndex && p.relativeDir && p.groupMeta) {
100
- const entry = groupMeta.get(p.relativeDir)!;
101
- entry.title = p.groupMeta.title;
102
- if (p.groupMeta.position !== undefined) {
103
- entry.position = p.groupMeta.position;
104
- }
105
- if (p.groupMeta.icon) {
106
- entry.icon = p.groupMeta.icon;
107
- }
126
+ // Override with explicit group index metadata
127
+ for (const p of groupIndexFiles) {
128
+ const entry = groupMeta.get(p.relativeDir!)!
129
+ if (p.groupMeta) {
130
+ entry.title = p.groupMeta.title
131
+ if (p.groupMeta.position !== undefined)
132
+ entry.position = p.groupMeta.position
133
+ if (p.groupMeta.icon) entry.icon = p.groupMeta.icon
108
134
  }
109
135
  }
110
136
 
111
- // Build final routes with group info
112
- const routes: RouteMeta[] = parsed.map((p) => {
113
- const dir = p.relativeDir;
114
- const meta = dir ? groupMeta.get(dir) : undefined;
137
+ // 4. BUILD BASE ROUTES
138
+ const routes: RouteMeta[] = new Array(parsed.length)
139
+ for (let i = 0; i < parsed.length; i++) {
140
+ const p = parsed[i]
141
+ const dir = p.relativeDir
142
+ const meta = dir ? groupMeta.get(dir) : undefined
115
143
 
116
- return {
144
+ routes[i] = {
117
145
  ...p.route,
118
146
  group: dir,
119
147
  groupTitle: meta?.title || (dir ? capitalize(dir) : undefined),
120
148
  groupPosition: meta?.position,
121
149
  groupIcon: meta?.icon,
122
- };
123
- });
150
+ }
151
+ }
124
152
 
125
- // Add fallbacks if i18n is enabled
153
+ // 5. OPTIMIZED I18N FALLBACKS
154
+ let finalRoutes = routes
126
155
  if (config?.i18n) {
127
- const defaultLocale = config.i18n.defaultLocale;
128
- const allLocales = Object.keys(config.i18n.locales);
156
+ const fallbacks = generateI18nFallbacks(routes, config, basePath)
157
+ finalRoutes = [...routes, ...fallbacks]
158
+ }
159
+
160
+ const sorted = sortRoutes(finalRoutes)
129
161
 
130
- const fallbackRoutes: RouteMeta[] = [];
131
- const defaultRoutes = routes.filter(
132
- (r) => (r.locale || defaultLocale) === defaultLocale,
133
- );
162
+ const duration = performance.now() - start
163
+ console.log(
164
+ `[boltdocs] Route generation: ${duration.toFixed(2)}ms (${files.length} files, ${cacheHits} cache hits)`,
165
+ )
134
166
 
135
- for (const locale of allLocales) {
136
- if (locale === defaultLocale) continue;
167
+ return sorted
168
+ }
137
169
 
138
- const localeRoutePaths = new Set(
139
- routes.filter((r) => r.locale === locale).map((r) => r.path),
140
- );
170
+ /**
171
+ * Generates fallback routes for missing translations.
172
+ * Optimization: Uses Map for O(1) existence checks instead of nested filters.
173
+ */
174
+ function generateI18nFallbacks(
175
+ routes: RouteMeta[],
176
+ config: BoltdocsConfig,
177
+ basePath: string,
178
+ ): RouteMeta[] {
179
+ const defaultLocale = config.i18n!.defaultLocale
180
+ const allLocales = Object.keys(config.i18n!.locales)
181
+ const fallbackRoutes: RouteMeta[] = []
141
182
 
142
- for (const defRoute of defaultRoutes) {
143
- let prefix = basePath;
144
- if (defRoute.version) {
145
- prefix += "/" + defRoute.version;
146
- }
183
+ // Index existing routes by locale for O(1) lookup
184
+ const routesByLocale = new Map<string, Set<string>>()
185
+ const defaultRoutes: RouteMeta[] = []
147
186
 
148
- let pathAfterVersion = defRoute.path.substring(prefix.length);
187
+ for (const r of routes) {
188
+ const locale = r.locale || defaultLocale
189
+ if (!routesByLocale.has(locale)) {
190
+ routesByLocale.set(locale, new Set())
191
+ }
192
+ routesByLocale.get(locale)!.add(r.path)
149
193
 
150
- if (pathAfterVersion.startsWith("/" + defaultLocale + "/")) {
151
- pathAfterVersion = pathAfterVersion.substring(
152
- defaultLocale.length + 1,
153
- );
154
- } else if (pathAfterVersion === "/" + defaultLocale) {
155
- pathAfterVersion = "/";
156
- }
157
- const targetPath =
158
- prefix +
159
- "/" +
160
- locale +
161
- (pathAfterVersion === "/" || pathAfterVersion === ""
162
- ? ""
163
- : pathAfterVersion);
164
-
165
- if (!localeRoutePaths.has(targetPath)) {
166
- fallbackRoutes.push({
167
- ...defRoute,
168
- path: targetPath,
169
- locale: locale,
170
- });
171
- }
194
+ if (locale === defaultLocale) {
195
+ defaultRoutes.push(r)
196
+ }
197
+ }
198
+
199
+ for (const locale of allLocales) {
200
+ if (locale === defaultLocale) continue
201
+
202
+ const localePaths = routesByLocale.get(locale) || new Set<string>()
203
+
204
+ for (const defRoute of defaultRoutes) {
205
+ const targetPath = computeLocalizedPath(
206
+ defRoute.path,
207
+ defaultLocale,
208
+ locale,
209
+ basePath,
210
+ )
211
+
212
+ if (!localePaths.has(targetPath)) {
213
+ fallbackRoutes.push({
214
+ ...defRoute,
215
+ path: targetPath,
216
+ locale,
217
+ })
172
218
  }
173
219
  }
220
+ }
221
+
222
+ return fallbackRoutes
223
+ }
224
+
225
+ /**
226
+ * Computes a localized path based on the default locale and target locale.
227
+ * Uses a cache to avoid redundant string manipulation.
228
+ */
229
+ function computeLocalizedPath(
230
+ path: string,
231
+ defaultLocale: string,
232
+ targetLocale: string,
233
+ basePath: string,
234
+ ): string {
235
+ const cacheKey = `${path}:${targetLocale}`
236
+ const cached = localizedPathCache.get(cacheKey)
237
+ if (cached) return cached
238
+
239
+ let prefix = basePath
240
+ const versionMatch = path.match(new RegExp(`^${basePath}/(v[0-9]+)`))
241
+ if (versionMatch) {
242
+ prefix += '/' + versionMatch[1]
243
+ }
174
244
 
175
- return sortRoutes([...routes, ...fallbackRoutes]);
245
+ let pathAfterVersion = path.substring(prefix.length)
246
+
247
+ // Handle case where path already has default locale
248
+ const defaultLocaleSegment = `/${defaultLocale}`
249
+ if (pathAfterVersion.startsWith(defaultLocaleSegment + '/')) {
250
+ pathAfterVersion =
251
+ '/' +
252
+ targetLocale +
253
+ '/' +
254
+ pathAfterVersion.substring(defaultLocaleSegment.length + 1)
255
+ } else if (pathAfterVersion === defaultLocaleSegment) {
256
+ pathAfterVersion = '/' + targetLocale
257
+ } else if (pathAfterVersion === '/' || pathAfterVersion === '') {
258
+ pathAfterVersion = '/' + targetLocale
259
+ } else {
260
+ // Ensure pathAfterVersion starts with a slash if not already
261
+ const pathPrefix = pathAfterVersion.startsWith('/') ? '' : '/'
262
+ pathAfterVersion = '/' + targetLocale + pathPrefix + pathAfterVersion
176
263
  }
177
264
 
178
- return sortRoutes(routes);
265
+ const result = prefix + pathAfterVersion
266
+
267
+ // Simple cache eviction to prevent memory leaks in extreme cases
268
+ if (localizedPathCache.size > 2000) localizedPathCache.clear()
269
+ localizedPathCache.set(cacheKey, result)
270
+
271
+ return result
179
272
  }