boltdocs 2.5.5 → 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/boltdocs.js +1 -1
- package/dist/cache-Cr8W2zgZ.cjs +6 -0
- package/dist/cache-DFdakSmR.mjs +6 -0
- package/dist/client/index.d.mts +1276 -861
- package/dist/client/index.d.ts +1276 -861
- package/dist/client/index.js +6 -1
- package/dist/client/index.mjs +6 -1
- package/dist/client/ssr.cjs +6 -0
- package/dist/client/ssr.d.cts +80 -0
- package/dist/client/ssr.d.mts +63 -61
- package/dist/client/ssr.mjs +6 -1
- package/dist/client/theme/neutral.css +388 -0
- package/dist/node/cli-entry.cjs +8 -0
- package/dist/node/cli-entry.d.cts +2 -0
- package/dist/node/cli-entry.d.mts +2 -1
- package/dist/node/cli-entry.mjs +7 -5
- package/dist/node/index.cjs +6 -0
- package/dist/node/index.d.cts +574 -0
- package/dist/node/index.d.mts +385 -378
- package/dist/node/index.mjs +6 -1
- package/dist/node-CWXme96p.mjs +73 -0
- package/dist/node-VYfhzGrh.cjs +73 -0
- package/dist/package-BY8Jd2j4.cjs +6 -0
- package/dist/package-OFZf0s2j.mjs +6 -0
- package/dist/search-dialog-BeNyI_KQ.mjs +6 -0
- package/dist/search-dialog-dYsCAk5S.js +6 -0
- package/dist/use-search-D25n0PrV.mjs +6 -0
- package/dist/use-search-WuzdH1cJ.js +6 -0
- package/package.json +16 -12
- package/src/client/app/index.tsx +15 -12
- package/src/client/components/default-layout.tsx +21 -19
- package/src/client/hooks/use-i18n.ts +1 -1
- package/src/client/hooks/use-routes.ts +1 -1
- package/src/client/hooks/use-version.ts +1 -1
- package/src/client/store/boltdocs-context.tsx +119 -0
- package/CHANGELOG.md +0 -98
- package/dist/cache-3FOEPC2P.mjs +0 -1
- package/dist/chunk-5B5NKOW6.mjs +0 -77
- package/dist/chunk-J2PTDWZM.mjs +0 -1
- package/dist/chunk-TP5KMRD3.mjs +0 -1
- package/dist/chunk-Y4RE5KI7.mjs +0 -1
- package/dist/client/ssr.d.ts +0 -78
- package/dist/client/ssr.js +0 -1
- package/dist/node/cli-entry.d.ts +0 -1
- package/dist/node/cli-entry.js +0 -82
- package/dist/node/index.d.ts +0 -567
- package/dist/node/index.js +0 -77
- package/dist/package-QFIAETHR.mjs +0 -1
- package/dist/search-dialog-O6VLVSOA.mjs +0 -1
- package/src/client/store/use-boltdocs-store.ts +0 -44
- package/src/node/cache.ts +0 -408
- package/src/node/cli/build.ts +0 -53
- package/src/node/cli/dev.ts +0 -22
- package/src/node/cli/doctor.ts +0 -243
- package/src/node/cli/index.ts +0 -9
- package/src/node/cli/ui.ts +0 -54
- package/src/node/cli-entry.ts +0 -24
- package/src/node/config.ts +0 -382
- package/src/node/errors.ts +0 -44
- package/src/node/index.ts +0 -84
- package/src/node/mdx/cache.ts +0 -12
- package/src/node/mdx/highlighter.ts +0 -47
- package/src/node/mdx/index.ts +0 -122
- package/src/node/mdx/rehype-shiki.ts +0 -62
- package/src/node/mdx/remark-code-meta.ts +0 -35
- package/src/node/mdx/remark-shiki.ts +0 -61
- package/src/node/plugin/entry.ts +0 -87
- package/src/node/plugin/html.ts +0 -99
- package/src/node/plugin/index.ts +0 -478
- package/src/node/plugin/types.ts +0 -9
- package/src/node/plugins/index.ts +0 -17
- package/src/node/plugins/plugin-errors.ts +0 -62
- package/src/node/plugins/plugin-lifecycle.ts +0 -117
- package/src/node/plugins/plugin-sandbox.ts +0 -59
- package/src/node/plugins/plugin-store.ts +0 -54
- package/src/node/plugins/plugin-types.ts +0 -107
- package/src/node/plugins/plugin-validator.ts +0 -105
- package/src/node/routes/cache.ts +0 -28
- package/src/node/routes/index.ts +0 -293
- package/src/node/routes/parser.ts +0 -262
- package/src/node/routes/sorter.ts +0 -42
- package/src/node/routes/types.ts +0 -61
- package/src/node/schema/config.ts +0 -195
- package/src/node/schema/frontmatter.ts +0 -17
- package/src/node/search/index.ts +0 -55
- package/src/node/security/constants/index.ts +0 -10
- package/src/node/security/csp.ts +0 -31
- package/src/node/security/headers.ts +0 -27
- package/src/node/ssg/index.ts +0 -205
- package/src/node/ssg/meta.ts +0 -33
- package/src/node/ssg/options.ts +0 -15
- package/src/node/ssg/robots.ts +0 -53
- package/src/node/ssg/sitemap.ts +0 -55
- package/src/node/utils.ts +0 -349
- package/tsconfig.json +0 -26
- 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
|
-
}
|
package/src/node/routes/cache.ts
DELETED
|
@@ -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 }
|
package/src/node/routes/index.ts
DELETED
|
@@ -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
|
-
}
|