boltdocs 1.10.2 → 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 (225) 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 -17
  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-AGVF6JBO.mjs +0 -194
  114. package/dist/SearchDialog-YPDOM7Q6.css +0 -2847
  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-TKLQWU7H.mjs +0 -1920
  121. package/dist/chunk-Z7JHYNAS.mjs +0 -57
  122. package/dist/client/index.css +0 -2847
  123. package/dist/client/index.d.mts +0 -372
  124. package/dist/client/index.d.ts +0 -372
  125. package/dist/client/index.js +0 -3630
  126. package/dist/client/index.mjs +0 -697
  127. package/dist/client/ssr.css +0 -2847
  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 -2928
  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 -61
  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 -180
  146. package/src/client/theme/components/Playground/index.ts +0 -1
  147. package/src/client/theme/components/Playground/playground.css +0 -238
  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 -394
  170. package/src/client/theme/styles/variables.css +0 -175
  171. package/src/client/theme/styles.css +0 -39
  172. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
  173. package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
  174. package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
  175. package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -112
  176. package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
  177. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -50
  178. package/src/client/theme/ui/ErrorBoundary/error-boundary.css +0 -55
  179. package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
  180. package/src/client/theme/ui/Footer/footer.css +0 -32
  181. package/src/client/theme/ui/Head/Head.tsx +0 -69
  182. package/src/client/theme/ui/Head/index.ts +0 -1
  183. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
  184. package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
  185. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
  186. package/src/client/theme/ui/Layout/Layout.tsx +0 -203
  187. package/src/client/theme/ui/Layout/base.css +0 -106
  188. package/src/client/theme/ui/Layout/index.ts +0 -2
  189. package/src/client/theme/ui/Layout/pagination.css +0 -72
  190. package/src/client/theme/ui/Layout/responsive.css +0 -47
  191. package/src/client/theme/ui/Link/Link.tsx +0 -392
  192. package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
  193. package/src/client/theme/ui/Link/index.ts +0 -2
  194. package/src/client/theme/ui/Link/link-preview.css +0 -48
  195. package/src/client/theme/ui/Loading/Loading.tsx +0 -10
  196. package/src/client/theme/ui/Loading/index.ts +0 -1
  197. package/src/client/theme/ui/Loading/loading.css +0 -30
  198. package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
  199. package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
  200. package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
  201. package/src/client/theme/ui/Navbar/index.ts +0 -2
  202. package/src/client/theme/ui/Navbar/navbar.css +0 -347
  203. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
  204. package/src/client/theme/ui/NotFound/index.ts +0 -1
  205. package/src/client/theme/ui/NotFound/not-found.css +0 -64
  206. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
  207. package/src/client/theme/ui/OnThisPage/index.ts +0 -1
  208. package/src/client/theme/ui/OnThisPage/toc.css +0 -152
  209. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
  210. package/src/client/theme/ui/PoweredBy/index.ts +0 -1
  211. package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
  212. package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
  213. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
  214. package/src/client/theme/ui/ProgressBar/index.ts +0 -1
  215. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
  216. package/src/client/theme/ui/SearchDialog/index.ts +0 -1
  217. package/src/client/theme/ui/SearchDialog/search.css +0 -152
  218. package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
  219. package/src/client/theme/ui/Sidebar/index.ts +0 -1
  220. package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
  221. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
  222. package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
  223. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
  224. package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
  225. 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
  }