boltdocs 2.5.5 → 2.6.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 (166) hide show
  1. package/bin/boltdocs.js +2 -2
  2. package/dist/client/index.cjs +6 -0
  3. package/dist/client/index.d.cts +1560 -0
  4. package/dist/client/index.d.ts +1219 -922
  5. package/dist/client/index.js +6 -1
  6. package/dist/client/theme/neutral.css +428 -0
  7. package/dist/node/cli-entry.cjs +8 -0
  8. package/dist/node/cli-entry.d.cts +2 -0
  9. package/dist/node/cli-entry.d.mts +2 -1
  10. package/dist/node/cli-entry.mjs +7 -5
  11. package/dist/node/index.cjs +6 -0
  12. package/dist/node/index.d.cts +519 -0
  13. package/dist/node/index.d.mts +374 -422
  14. package/dist/node/index.mjs +6 -1
  15. package/dist/node-BgvNl2Ay.mjs +89 -0
  16. package/dist/node-vkbb0MK7.cjs +89 -0
  17. package/dist/package-CR0HF9x3.mjs +6 -0
  18. package/dist/package-Dgmsc_l5.cjs +6 -0
  19. package/dist/search-dialog-3lvKsbVG.js +6 -0
  20. package/dist/search-dialog-DMK5OpgH.cjs +6 -0
  21. package/dist/use-search-C9bxCqfF.js +6 -0
  22. package/dist/use-search-DcfZSunO.cjs +6 -0
  23. package/package.json +26 -25
  24. package/src/client/app/config-context.tsx +38 -5
  25. package/src/client/app/doc-page.tsx +34 -0
  26. package/src/client/app/mdx-component.tsx +2 -3
  27. package/src/client/app/mdx-components-context.tsx +27 -2
  28. package/src/client/app/routes-context.tsx +34 -0
  29. package/src/client/app/scroll-handler.tsx +7 -4
  30. package/src/client/app/theme-context.tsx +71 -67
  31. package/src/client/components/default-layout.tsx +34 -33
  32. package/src/client/components/docs-layout.tsx +1 -2
  33. package/src/client/components/icons-dev.tsx +36 -5
  34. package/src/client/components/mdx/admonition.tsx +11 -27
  35. package/src/client/components/mdx/badge.tsx +1 -1
  36. package/src/client/components/mdx/button.tsx +3 -3
  37. package/src/client/components/mdx/card.tsx +1 -1
  38. package/src/client/components/mdx/code-block.tsx +90 -80
  39. package/src/client/components/mdx/component-preview.tsx +1 -5
  40. package/src/client/components/mdx/component-props.tsx +1 -1
  41. package/src/client/components/mdx/field.tsx +4 -5
  42. package/src/client/components/mdx/file-tree.tsx +6 -3
  43. package/src/client/components/mdx/hooks/use-code-block.ts +2 -2
  44. package/src/client/components/mdx/image.tsx +1 -1
  45. package/src/client/components/mdx/link.tsx +2 -2
  46. package/src/client/components/mdx/list.tsx +1 -1
  47. package/src/client/components/mdx/table.tsx +1 -1
  48. package/src/client/components/mdx/tabs.tsx +1 -1
  49. package/src/client/components/primitives/breadcrumbs.tsx +1 -7
  50. package/src/client/components/primitives/button-group.tsx +1 -1
  51. package/src/client/components/primitives/button.tsx +1 -1
  52. package/src/client/components/primitives/code-block.tsx +113 -0
  53. package/src/client/components/primitives/link.tsx +23 -41
  54. package/src/client/components/primitives/menu.tsx +5 -6
  55. package/src/client/components/primitives/navbar.tsx +6 -18
  56. package/src/client/components/primitives/navigation-menu.tsx +4 -4
  57. package/src/client/components/primitives/on-this-page.tsx +6 -10
  58. package/src/client/components/primitives/page-nav.tsx +4 -9
  59. package/src/client/components/primitives/popover.tsx +1 -1
  60. package/src/client/components/primitives/search-dialog.tsx +3 -6
  61. package/src/client/components/primitives/sidebar.tsx +80 -22
  62. package/src/client/components/primitives/skeleton.tsx +1 -1
  63. package/src/client/components/primitives/tabs.tsx +4 -11
  64. package/src/client/components/primitives/tooltip.tsx +3 -3
  65. package/src/client/components/ui-base/breadcrumbs.tsx +4 -6
  66. package/src/client/components/ui-base/copy-markdown.tsx +2 -7
  67. package/src/client/components/ui-base/github-stars.tsx +2 -2
  68. package/src/client/components/ui-base/head.tsx +58 -51
  69. package/src/client/components/ui-base/loading.tsx +2 -2
  70. package/src/client/components/ui-base/navbar.tsx +12 -14
  71. package/src/client/components/ui-base/not-found.tsx +1 -1
  72. package/src/client/components/ui-base/on-this-page.tsx +6 -6
  73. package/src/client/components/ui-base/page-nav.tsx +4 -8
  74. package/src/client/components/ui-base/search-dialog.tsx +10 -8
  75. package/src/client/components/ui-base/sidebar.tsx +76 -23
  76. package/src/client/components/ui-base/tabs.tsx +9 -8
  77. package/src/client/components/ui-base/theme-toggle.tsx +2 -2
  78. package/src/client/hooks/use-i18n.ts +3 -3
  79. package/src/client/hooks/use-localized-to.ts +1 -1
  80. package/src/client/hooks/use-navbar.ts +8 -6
  81. package/src/client/hooks/use-routes.ts +19 -11
  82. package/src/client/hooks/use-search.ts +1 -1
  83. package/src/client/hooks/use-sidebar.ts +48 -2
  84. package/src/client/hooks/use-tabs.ts +6 -2
  85. package/src/client/hooks/use-version.ts +3 -3
  86. package/src/client/index.ts +22 -22
  87. package/src/client/ssg/boltdocs-shell.tsx +127 -0
  88. package/src/client/ssg/create-routes.tsx +179 -0
  89. package/src/client/ssg/index.ts +3 -0
  90. package/src/client/ssg/mdx-page.tsx +37 -0
  91. package/src/client/store/boltdocs-context.tsx +66 -0
  92. package/src/client/theme/neutral.css +90 -50
  93. package/src/client/types.ts +5 -33
  94. package/src/client/utils/react-to-text.ts +34 -0
  95. package/CHANGELOG.md +0 -98
  96. package/dist/cache-3FOEPC2P.mjs +0 -1
  97. package/dist/chunk-5B5NKOW6.mjs +0 -77
  98. package/dist/chunk-J2PTDWZM.mjs +0 -1
  99. package/dist/chunk-TP5KMRD3.mjs +0 -1
  100. package/dist/chunk-Y4RE5KI7.mjs +0 -1
  101. package/dist/client/index.d.mts +0 -1263
  102. package/dist/client/index.mjs +0 -1
  103. package/dist/client/ssr.d.mts +0 -78
  104. package/dist/client/ssr.d.ts +0 -78
  105. package/dist/client/ssr.js +0 -1
  106. package/dist/client/ssr.mjs +0 -1
  107. package/dist/node/cli-entry.d.ts +0 -1
  108. package/dist/node/cli-entry.js +0 -82
  109. package/dist/node/index.d.ts +0 -567
  110. package/dist/node/index.js +0 -77
  111. package/dist/package-QFIAETHR.mjs +0 -1
  112. package/dist/search-dialog-O6VLVSOA.mjs +0 -1
  113. package/src/client/app/index.tsx +0 -345
  114. package/src/client/app/mdx-page.tsx +0 -15
  115. package/src/client/app/preload.tsx +0 -66
  116. package/src/client/app/router.tsx +0 -30
  117. package/src/client/integrations/codesandbox.ts +0 -179
  118. package/src/client/integrations/index.ts +0 -1
  119. package/src/client/ssr.tsx +0 -65
  120. package/src/client/store/use-boltdocs-store.ts +0 -44
  121. package/src/node/cache.ts +0 -408
  122. package/src/node/cli/build.ts +0 -53
  123. package/src/node/cli/dev.ts +0 -22
  124. package/src/node/cli/doctor.ts +0 -243
  125. package/src/node/cli/index.ts +0 -9
  126. package/src/node/cli/ui.ts +0 -54
  127. package/src/node/cli-entry.ts +0 -24
  128. package/src/node/config.ts +0 -382
  129. package/src/node/errors.ts +0 -44
  130. package/src/node/index.ts +0 -84
  131. package/src/node/mdx/cache.ts +0 -12
  132. package/src/node/mdx/highlighter.ts +0 -47
  133. package/src/node/mdx/index.ts +0 -122
  134. package/src/node/mdx/rehype-shiki.ts +0 -62
  135. package/src/node/mdx/remark-code-meta.ts +0 -35
  136. package/src/node/mdx/remark-shiki.ts +0 -61
  137. package/src/node/plugin/entry.ts +0 -87
  138. package/src/node/plugin/html.ts +0 -99
  139. package/src/node/plugin/index.ts +0 -478
  140. package/src/node/plugin/types.ts +0 -9
  141. package/src/node/plugins/index.ts +0 -17
  142. package/src/node/plugins/plugin-errors.ts +0 -62
  143. package/src/node/plugins/plugin-lifecycle.ts +0 -117
  144. package/src/node/plugins/plugin-sandbox.ts +0 -59
  145. package/src/node/plugins/plugin-store.ts +0 -54
  146. package/src/node/plugins/plugin-types.ts +0 -107
  147. package/src/node/plugins/plugin-validator.ts +0 -105
  148. package/src/node/routes/cache.ts +0 -28
  149. package/src/node/routes/index.ts +0 -293
  150. package/src/node/routes/parser.ts +0 -262
  151. package/src/node/routes/sorter.ts +0 -42
  152. package/src/node/routes/types.ts +0 -61
  153. package/src/node/schema/config.ts +0 -195
  154. package/src/node/schema/frontmatter.ts +0 -17
  155. package/src/node/search/index.ts +0 -55
  156. package/src/node/security/constants/index.ts +0 -10
  157. package/src/node/security/csp.ts +0 -31
  158. package/src/node/security/headers.ts +0 -27
  159. package/src/node/ssg/index.ts +0 -205
  160. package/src/node/ssg/meta.ts +0 -33
  161. package/src/node/ssg/options.ts +0 -15
  162. package/src/node/ssg/robots.ts +0 -53
  163. package/src/node/ssg/sitemap.ts +0 -55
  164. package/src/node/utils.ts +0 -349
  165. package/tsconfig.json +0 -26
  166. package/tsup.config.ts +0 -56
@@ -1,59 +0,0 @@
1
- import { PluginPermissionError } from './plugin-errors'
2
- import { hasPermission } from './plugin-validator'
3
- import type { SecureBoltdocsPlugin, PluginPermission } from './plugin-types'
4
-
5
- /**
6
- * The Sandbox provides a protective layer that ensures plugins only use
7
- * the capabilities they have explicitly requested permissions for.
8
- */
9
- export class PluginSandbox {
10
- /**
11
- * Validates if a plugin has the required permission for a capability.
12
- * Throws a PluginPermissionError if not granted.
13
- */
14
- public static checkPermission(
15
- plugin: SecureBoltdocsPlugin,
16
- permission: PluginPermission
17
- ): void {
18
- if (!hasPermission(plugin, permission)) {
19
- throw new PluginPermissionError(plugin.name, permission)
20
- }
21
- }
22
-
23
- /**
24
- * Filters a plugin's capabilities based on its declared permissions.
25
- * This is used when aggregating plugins (remark, rehype, vite, components).
26
- */
27
- public static getSanitizedCapabilities(plugin: SecureBoltdocsPlugin) {
28
- return {
29
- remarkPlugins: hasPermission(plugin, 'mdx:remark') ? plugin.remarkPlugins : [],
30
- rehypePlugins: hasPermission(plugin, 'mdx:rehype') ? plugin.rehypePlugins : [],
31
- vitePlugins: hasPermission(plugin, 'vite:config') ? plugin.vitePlugins : [],
32
- components: hasPermission(plugin, 'components') ? plugin.components : {},
33
- }
34
- }
35
-
36
- /**
37
- * Wraps a hook execution with permission checks and error isolation.
38
- */
39
- public static async executeWithIsolation<T>(
40
- plugin: SecureBoltdocsPlugin,
41
- requiredPermission: PluginPermission,
42
- hookName: string,
43
- action: () => Promise<T> | T
44
- ): Promise<T | undefined> {
45
- try {
46
- this.checkPermission(plugin, requiredPermission)
47
- return await action()
48
- } catch (error) {
49
- if (error instanceof PluginPermissionError) {
50
- // Log skip instead of failing hard for permissions in some contexts
51
- console.warn(`[boltdocs] Skipping hook '${hookName}' for plugin '${plugin.name}': ${error.message}`)
52
- return undefined
53
- }
54
-
55
- // Re-throw other errors to be caught by the LifecycleManager
56
- throw error
57
- }
58
- }
59
- }
@@ -1,54 +0,0 @@
1
- import type { PluginStore } from './plugin-types'
2
-
3
- /**
4
- * Implementation of the shared plugin store.
5
- * Uses a namespaced approach to prevent key collisions between plugins.
6
- */
7
- export class BoltdocsPluginStore implements PluginStore {
8
- private data = new Map<string, unknown>()
9
-
10
- /**
11
- * Internal helper to create a namespaced key.
12
- */
13
- private getNamespaceKey(pluginName: string, key: string): string {
14
- return `${pluginName}:${key}`
15
- }
16
-
17
- /**
18
- * Retrieves a value from the store. Returns a deep clone to prevent mutations (side-effects).
19
- */
20
- public get<T = unknown>(pluginName: string, key: string): T | undefined {
21
- const nsKey = this.getNamespaceKey(pluginName, key)
22
- const value = this.data.get(nsKey)
23
-
24
- if (value === undefined) return undefined
25
-
26
- // For safety, return a deep clone if it's an object
27
- if (typeof value === 'object' && value !== null) {
28
- return JSON.parse(JSON.stringify(value)) as T
29
- }
30
-
31
- return value as T
32
- }
33
-
34
- /**
35
- * Stores a value in the store. Key is automatically namespaced.
36
- */
37
- public set(pluginName: string, key: string, value: unknown): void {
38
- const nsKey = this.getNamespaceKey(pluginName, key)
39
- // We also store a clone to ensure the store state is immutable from the outside
40
- const storedValue = typeof value === 'object' && value !== null
41
- ? JSON.parse(JSON.stringify(value))
42
- : value
43
-
44
- this.data.set(nsKey, storedValue)
45
- }
46
-
47
- /**
48
- * Checks for the existence of a key in the store.
49
- */
50
- public has(pluginName: string, key: string): boolean {
51
- const nsKey = this.getNamespaceKey(pluginName, key)
52
- return this.data.has(nsKey)
53
- }
54
- }
@@ -1,107 +0,0 @@
1
- import type { Plugin as VitePlugin } from 'vite'
2
- import type { BoltdocsConfig } from '../config'
3
-
4
- /**
5
- * Permissions that a plugin can request to access specific Boltdocs capabilities.
6
- */
7
- export type PluginPermission =
8
- | 'fs:read' // Read filesystem
9
- | 'fs:write' // Write filesystem
10
- | 'vite:config' // Modify Vite config
11
- | 'mdx:remark' // Add remark plugins
12
- | 'mdx:rehype' // Add rehype plugins
13
- | 'components' // Register MDX components
14
- | 'hooks:build' // Access build lifecycle hooks
15
- | 'hooks:dev' // Access dev lifecycle hooks
16
-
17
- /**
18
- * Shared context injected into every plugin lifecycle hook.
19
- */
20
- export interface PluginContext {
21
- /** The full, resolved Boltdocs configuration (Readonly) */
22
- readonly config: BoltdocsConfig
23
- /** A plugin-specific logger */
24
- readonly logger: PluginLogger
25
- /** A shared store for dependency injection and state sharing between plugins */
26
- readonly store: PluginStore
27
- /** Metadata about the current plugin */
28
- readonly meta: PluginMeta
29
- }
30
-
31
- /**
32
- * Simple logger interface for plugins.
33
- */
34
- export interface PluginLogger {
35
- info(message: string): void
36
- warn(message: string): void
37
- error(message: string | Error): void
38
- debug(message: string): void
39
- }
40
-
41
- /**
42
- * A shared key-value store that allows plugins to share state and configuration.
43
- */
44
- export interface PluginStore {
45
- /** Get a value from the store. Keys are namespaced by plugin internally. */
46
- get<T = unknown>(pluginName: string, key: string): T | undefined
47
- /** Set a value in the store. */
48
- set(pluginName: string, key: string, value: unknown): void
49
- /** Check if a key exists in the store. */
50
- has(pluginName: string, key: string): boolean
51
- }
52
-
53
- /**
54
- * Metadata for a plugin, used for identification and compatibility checks.
55
- */
56
- export interface PluginMeta {
57
- /** Unique identifier for the plugin */
58
- name: string
59
- /** Version of the plugin itself (semver) */
60
- version?: string
61
- /** Minimum required version of Boltdocs (semver range) */
62
- boltdocsVersion?: string
63
- }
64
-
65
- /**
66
- * Lifecycle hooks that a plugin can implement to hook into the build and dev processes.
67
- */
68
- export interface PluginLifecycleHooks {
69
- /** Called before the build process starts */
70
- beforeBuild?: (ctx: PluginContext) => Promise<void> | void
71
- /** Called after the build process finishes successfully */
72
- afterBuild?: (ctx: PluginContext) => Promise<void> | void
73
- /** Called before the dev server starts */
74
- beforeDev?: (ctx: PluginContext) => Promise<void> | void
75
- /** Called after the dev server is ready (configureServer) */
76
- afterDev?: (ctx: PluginContext) => Promise<void> | void
77
- /** Called when the final Boltdocs config is resolved */
78
- configResolved?: (ctx: PluginContext, config: BoltdocsConfig) => void
79
- /** Called when the build is closing */
80
- buildEnd?: (ctx: PluginContext) => Promise<void> | void
81
- }
82
-
83
- /**
84
- * The extended, secure Boltdocs plugin interface.
85
- */
86
- export interface SecureBoltdocsPlugin {
87
- /** A unique name for the plugin (e.g., 'boltdocs-plugin-mermaid') */
88
- name: string
89
- /** Whether to run this plugin before or after default ones */
90
- enforce?: 'pre' | 'post'
91
- /** Version of the plugin (optional, but recommended for security) */
92
- version?: string
93
- /** Minimum compatible Boltdocs version (optional, semver range) */
94
- boltdocsVersion?: string
95
- /** List of permissions this plugin requires to operate */
96
- permissions?: PluginPermission[]
97
- /** Optional remark plugins to add to the MDX pipeline (requires 'mdx:remark' permission) */
98
- remarkPlugins?: unknown[]
99
- /** Optional rehype plugins to add to the MDX pipeline (requires 'mdx:rehype' permission) */
100
- rehypePlugins?: unknown[]
101
- /** Optional Vite plugins to inject into the build process (requires 'vite:config' permission) */
102
- vitePlugins?: VitePlugin[]
103
- /** Optional custom React components to register in MDX. Map of Name -> Module Path. (requires 'components' permission) */
104
- components?: Record<string, string>
105
- /** Implementation of lifecycle hooks (requires 'hooks:build' or 'hooks:dev' permissions) */
106
- hooks?: PluginLifecycleHooks
107
- }
@@ -1,105 +0,0 @@
1
- import { z } from 'zod'
2
- import semver from 'semver'
3
- import path from 'path'
4
- import { BoltdocsPluginSchema } from '../schema/config'
5
- import {
6
- PluginValidationError,
7
- PluginCompatibilityError,
8
- } from './plugin-errors'
9
- import type { SecureBoltdocsPlugin, PluginPermission } from './plugin-types'
10
-
11
- /**
12
- * Enhanced Zod schema for secure plugins.
13
- */
14
- export const SecurePluginSchema = BoltdocsPluginSchema.extend({
15
- version: z.string().optional(),
16
- boltdocsVersion: z.string().optional(),
17
- permissions: z.array(z.string()).optional(),
18
- hooks: z
19
- .object({
20
- beforeBuild: z.function().optional(),
21
- afterBuild: z.function().optional(),
22
- beforeDev: z.function().optional(),
23
- afterDev: z.function().optional(),
24
- configResolved: z.function().optional(),
25
- buildEnd: z.function().optional(),
26
- })
27
- .optional(),
28
- })
29
-
30
- /**
31
- * Validates a list of plugins for correctness, security, and compatibility.
32
- */
33
- export function validatePlugins(
34
- plugins: any[],
35
- boltdocsVersion: string,
36
- ): SecureBoltdocsPlugin[] {
37
- const validatedPlugins: SecureBoltdocsPlugin[] = []
38
- const pluginNames = new Set<string>()
39
-
40
- for (const rawPlugin of plugins) {
41
- // 1. Basic Structure Validation
42
- const result = SecurePluginSchema.safeParse(rawPlugin)
43
- if (!result.success) {
44
- throw new PluginValidationError(
45
- rawPlugin.name || 'unknown',
46
- result.error.issues
47
- .map((i) => `${i.path.join('.')}: ${i.message}`)
48
- .join(', '),
49
- )
50
- }
51
-
52
- const plugin = result.data as SecureBoltdocsPlugin
53
-
54
- // 2. Name Uniqueness
55
- if (pluginNames.has(plugin.name)) {
56
- throw new PluginValidationError(
57
- plugin.name,
58
- 'Duplicate plugin name detected',
59
- )
60
- }
61
- pluginNames.add(plugin.name)
62
-
63
- // 3. Semver Compatibility
64
- if (
65
- plugin.boltdocsVersion &&
66
- !semver.satisfies(boltdocsVersion, plugin.boltdocsVersion)
67
- ) {
68
- throw new PluginCompatibilityError(
69
- plugin.name,
70
- `Plugin expects Boltdocs version ${plugin.boltdocsVersion}, but current is ${boltdocsVersion}`,
71
- )
72
- }
73
-
74
- // 4. Component Path Security (Path Traversal Prevention)
75
- if (plugin.components) {
76
- for (const [compName, compPath] of Object.entries(plugin.components)) {
77
- if (compPath.includes('..') || path.isAbsolute(compPath)) {
78
- // Absolute paths are technically okay but we restrict them for consistency/security
79
- // if they point outside the workspace. For now, we block '..' explicitly.
80
- if (compPath.includes('..')) {
81
- throw new PluginValidationError(
82
- plugin.name,
83
- `Component '${compName}' has an invalid path: traversal sequences are not allowed.`,
84
- )
85
- }
86
- }
87
- }
88
- }
89
-
90
- validatedPlugins.push(plugin)
91
- }
92
-
93
- return validatedPlugins
94
- }
95
-
96
- /**
97
- * Helper to check if a specific permission is granted to a plugin.
98
- */
99
- export function hasPermission(
100
- plugin: SecureBoltdocsPlugin,
101
- permission: PluginPermission,
102
- ): boolean {
103
- if (!plugin.permissions) return false
104
- return plugin.permissions.includes(permission)
105
- }
@@ -1,28 +0,0 @@
1
- import { FileCache } from '../cache'
2
- import type { ParsedDocFile } from './types'
3
-
4
- /**
5
- * Persistent cache for parsed documentation files.
6
- * Saves data to `.boltdocs/routes.json`.
7
- */
8
- const docCache = new FileCache<ParsedDocFile>({ name: 'routes' })
9
-
10
- /**
11
- * Invalidate all cached routes.
12
- * Typically called when a file is added or deleted, requiring a complete route rebuild.
13
- */
14
- export function invalidateRouteCache(): void {
15
- docCache.invalidateAll()
16
- }
17
-
18
- /**
19
- * Invalidate a specific file from cache.
20
- * Called when a specific file is modified (changed).
21
- *
22
- * @param filePath - The absolute path of the file to invalidate
23
- */
24
- export function invalidateFile(filePath: string): void {
25
- docCache.invalidate(filePath)
26
- }
27
-
28
- export { docCache }
@@ -1,293 +0,0 @@
1
- import fastGlob from 'fast-glob'
2
- import type { BoltdocsConfig } from '../config'
3
- import { capitalize } from '../utils'
4
-
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
-
10
- // Re-export public API
11
- export type { RouteMeta }
12
- export { invalidateRouteCache, invalidateFile }
13
-
14
- // Cache for file list and localized path computations
15
- let cachedFileList: string[] | null = null
16
- const localizedPathCache = new Map<string, string>()
17
-
18
- /**
19
- * Generates the entire route map for the documentation site.
20
- * OPTIMIZED: Uses Map-based i18n lookups, chunked processing, and path caching.
21
- *
22
- * Automatically handles versioning and i18n routing, including fallback
23
- * generation for missing translations.
24
- *
25
- * @param docsDir - The root documentation directory
26
- * @param config - The Boltdocs configuration
27
- * @param basePath - The base URL path for the routes (default: '/docs')
28
- * @returns A promise resolving to an array of RouteMeta objects
29
- */
30
- export async function generateRoutes(
31
- docsDir: string,
32
- config?: BoltdocsConfig,
33
- basePath: string = '/docs',
34
- forceScan: boolean = true,
35
- ): Promise<RouteMeta[]> {
36
- const start = performance.now()
37
-
38
- // Load persistent cache
39
- docCache.load()
40
-
41
- // Clear path computation cache between generations
42
- localizedPathCache.clear()
43
-
44
- // Force re-parse if specifically requested (e.g. for content/config changes)
45
- if (process.env.BOLTDOCS_FORCE_REPARSE === 'true' || config?.i18n) {
46
- docCache.invalidateAll()
47
- }
48
-
49
- // 1. FAST SCAN (Skip if incremental and we have a cache)
50
- let files: string[]
51
- if (!forceScan && cachedFileList) {
52
- files = cachedFileList
53
- } else {
54
- files = await fastGlob(['**/*.md', '**/*.mdx'], {
55
- cwd: docsDir,
56
- absolute: true,
57
- suppressErrors: true,
58
- followSymbolicLinks: false,
59
- })
60
- cachedFileList = files
61
- }
62
-
63
- // Prune cache entries for deleted files
64
- docCache.pruneStale(new Set(files))
65
-
66
- // 2. CHUNKED PROCESSING (prevents blocking event loop)
67
- const CHUNK_SIZE = 50
68
- const parsed: ParsedDocFile[] = []
69
- let cacheHits = 0
70
-
71
- for (let i = 0; i < files.length; i += CHUNK_SIZE) {
72
- const chunk = files.slice(i, i + CHUNK_SIZE)
73
-
74
- const chunkResults = await Promise.all(
75
- chunk.map(async (file) => {
76
- const cached = docCache.get(file)
77
- if (cached) {
78
- cacheHits++
79
- return cached
80
- }
81
-
82
- const result = parseDocFile(file, docsDir, basePath, config)
83
- docCache.set(file, result)
84
- return result
85
- }),
86
- )
87
-
88
- parsed.push(...chunkResults)
89
-
90
- // Yield to event loop between chunks if there's more to process
91
- if (i + CHUNK_SIZE < files.length) {
92
- await new Promise((resolve) => setImmediate(resolve))
93
- }
94
- }
95
-
96
- // Save cache after processing
97
- docCache.save()
98
-
99
- // 3. OPTIMIZED METADATA COLLECTION
100
- const groupMeta = new Map<
101
- string,
102
- { title: string; position?: number; icon?: string }
103
- >()
104
- const groupIndexFiles: ParsedDocFile[] = []
105
-
106
- for (const p of parsed) {
107
- if (p.isGroupIndex && p.relativeDir) {
108
- groupIndexFiles.push(p)
109
- }
110
-
111
- if (p.relativeDir) {
112
- let entry = groupMeta.get(p.relativeDir)
113
- if (!entry) {
114
- entry = {
115
- title: capitalize(p.relativeDir),
116
- position: p.inferredGroupPosition,
117
- icon: p.route.icon,
118
- }
119
- groupMeta.set(p.relativeDir, entry)
120
- } else {
121
- if (
122
- entry.position === undefined &&
123
- p.inferredGroupPosition !== undefined
124
- ) {
125
- entry.position = p.inferredGroupPosition
126
- }
127
- if (!entry.icon && p.route.icon) {
128
- entry.icon = p.route.icon
129
- }
130
- }
131
- }
132
- }
133
-
134
- // Override with explicit group index metadata
135
- for (const p of groupIndexFiles) {
136
- const entry = groupMeta.get(p.relativeDir!)!
137
- if (p.groupMeta) {
138
- entry.title = p.groupMeta.title
139
- if (p.groupMeta.position !== undefined)
140
- entry.position = p.groupMeta.position
141
- if (p.groupMeta.icon) entry.icon = p.groupMeta.icon
142
- }
143
- }
144
-
145
- // 4. BUILD BASE ROUTES
146
- const routes: RouteMeta[] = new Array(parsed.length)
147
- for (let i = 0; i < parsed.length; i++) {
148
- const p = parsed[i]
149
- const dir = p.relativeDir
150
- const meta = dir ? groupMeta.get(dir) : undefined
151
-
152
- routes[i] = {
153
- ...p.route,
154
- group: dir,
155
- groupTitle: meta?.title || (dir ? capitalize(dir) : undefined),
156
- groupPosition: meta?.position,
157
- groupIcon: meta?.icon,
158
- }
159
- }
160
-
161
- // 5. OPTIMIZED I18N FALLBACKS
162
- let finalRoutes = routes
163
- if (config?.i18n) {
164
- const fallbacks = generateI18nFallbacks(routes, config, basePath)
165
- finalRoutes = [...routes, ...fallbacks]
166
- }
167
-
168
- const sorted = sortRoutes(finalRoutes)
169
-
170
- const duration = performance.now() - start
171
- console.log(
172
- `[boltdocs] Route generation: ${duration.toFixed(2)}ms (${files.length} files, ${cacheHits} cache hits)`,
173
- )
174
-
175
- return sorted
176
- }
177
-
178
- /**
179
- * Generates fallback routes for missing translations.
180
- * Optimization: Uses Map for O(1) existence checks instead of nested filters.
181
- */
182
- function generateI18nFallbacks(
183
- routes: RouteMeta[],
184
- config: BoltdocsConfig,
185
- basePath: string,
186
- ): RouteMeta[] {
187
- const defaultLocale = config.i18n!.defaultLocale
188
- const allLocales = Object.keys(config.i18n!.locales)
189
- const fallbackRoutes: RouteMeta[] = []
190
-
191
- // Index existing routes by locale for O(1) lookup
192
- const routesByLocale = new Map<string, Set<string>>()
193
- const defaultRoutes: RouteMeta[] = []
194
-
195
- for (const r of routes) {
196
- const locale = r.locale || defaultLocale
197
- if (!routesByLocale.has(locale)) {
198
- routesByLocale.set(locale, new Set())
199
- }
200
- routesByLocale.get(locale)!.add(r.path)
201
-
202
- if (locale === defaultLocale) {
203
- defaultRoutes.push(r)
204
- }
205
- }
206
-
207
- for (const locale of allLocales) {
208
- const localePaths = routesByLocale.get(locale) || new Set<string>()
209
-
210
- for (const defRoute of defaultRoutes) {
211
- const targetPath = computeLocalizedPath(
212
- defRoute.path,
213
- defaultLocale,
214
- locale,
215
- basePath,
216
- config,
217
- )
218
-
219
- // Skip if the path is already the same (e.g. for default locale unprefixed)
220
- if (targetPath === defRoute.path) continue
221
-
222
- if (!localePaths.has(targetPath)) {
223
- fallbackRoutes.push({
224
- ...defRoute,
225
- path: targetPath,
226
- locale,
227
- })
228
- }
229
- }
230
- }
231
-
232
- return fallbackRoutes
233
- }
234
-
235
- /**
236
- * Computes a localized path based on the default locale and target locale.
237
- * Uses a cache to avoid redundant string manipulation.
238
- */
239
- function computeLocalizedPath(
240
- path: string,
241
- defaultLocale: string,
242
- targetLocale: string,
243
- basePath: string,
244
- config?: BoltdocsConfig,
245
- ): string {
246
- const cacheKey = `${path}:${targetLocale}`
247
- const cached = localizedPathCache.get(cacheKey)
248
- if (cached) return cached
249
-
250
- let prefix = basePath
251
- if (config?.versions) {
252
- const vPrefix = config.versions.prefix || ''
253
- for (const vConfig of config.versions.versions) {
254
- const fullVPath = vPrefix + vConfig.path
255
- if (path.startsWith(`${basePath}/${fullVPath}`)) {
256
- prefix += '/' + fullVPath
257
- break
258
- }
259
- if (path.startsWith(`${basePath}/${vConfig.path}`)) {
260
- prefix += '/' + vConfig.path
261
- break
262
- }
263
- }
264
- }
265
-
266
- let pathAfterVersion = path.substring(prefix.length)
267
-
268
- // Handle case where path already has default locale
269
- const defaultLocaleSegment = `/${defaultLocale}`
270
- if (pathAfterVersion.startsWith(defaultLocaleSegment + '/')) {
271
- pathAfterVersion =
272
- '/' +
273
- targetLocale +
274
- '/' +
275
- pathAfterVersion.substring(defaultLocaleSegment.length + 1)
276
- } else if (pathAfterVersion === defaultLocaleSegment) {
277
- pathAfterVersion = '/' + targetLocale
278
- } else if (pathAfterVersion === '/' || pathAfterVersion === '') {
279
- pathAfterVersion = '/' + targetLocale
280
- } else {
281
- // Ensure pathAfterVersion starts with a slash if not already
282
- const pathPrefix = pathAfterVersion.startsWith('/') ? '' : '/'
283
- pathAfterVersion = '/' + targetLocale + pathPrefix + pathAfterVersion
284
- }
285
-
286
- const result = prefix + pathAfterVersion
287
-
288
- // Simple cache eviction to prevent memory leaks in extreme cases
289
- if (localizedPathCache.size > 2000) localizedPathCache.clear()
290
- localizedPathCache.set(cacheKey, result)
291
-
292
- return result
293
- }