boltdocs 1.10.2 → 2.0.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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/dist/cache-7G6D532T.mjs +1 -0
- package/dist/chunk-A4HQPEPU.mjs +1 -0
- package/dist/chunk-BA5NH5HU.mjs +1 -0
- package/dist/chunk-BQCD3DWG.mjs +1 -0
- package/dist/chunk-H63UMKYF.mjs +1 -0
- package/dist/chunk-IWHRQHS7.mjs +1 -0
- package/dist/chunk-JZXLCA2E.mjs +1 -0
- package/dist/chunk-MFU7Q6WF.mjs +1 -0
- package/dist/chunk-QYPNX5UN.mjs +1 -0
- package/dist/chunk-XEAPSFMB.mjs +1 -0
- package/dist/client/components/mdx/index.d.mts +209 -0
- package/dist/client/components/mdx/index.d.ts +209 -0
- package/dist/client/components/mdx/index.js +1 -0
- package/dist/client/components/mdx/index.mjs +1 -0
- package/dist/client/hooks/index.d.mts +133 -0
- package/dist/client/hooks/index.d.ts +133 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/hooks/index.mjs +1 -0
- package/dist/client/index.d.mts +138 -298
- package/dist/client/index.d.ts +138 -298
- package/dist/client/index.js +1 -3630
- package/dist/client/index.mjs +1 -697
- package/dist/client/ssr.d.mts +7 -3
- package/dist/client/ssr.d.ts +7 -3
- package/dist/client/ssr.js +1 -2928
- package/dist/client/ssr.mjs +1 -33
- package/dist/{config-BsFQ-ErD.d.ts → config-CX4l-ZNp.d.mts} +42 -35
- package/dist/{config-BsFQ-ErD.d.mts → config-CX4l-ZNp.d.ts} +42 -35
- package/dist/node/index.d.mts +2 -4
- package/dist/node/index.d.ts +2 -4
- package/dist/node/index.js +31 -1161
- package/dist/node/index.mjs +31 -736
- package/dist/search-dialog-EB3N4TYM.mjs +1 -0
- package/dist/types-BuZWFT7r.d.ts +159 -0
- package/dist/types-CvT-SGbK.d.mts +159 -0
- package/dist/use-routes-5bAtAAYX.d.mts +30 -0
- package/dist/use-routes-BefRXY3v.d.ts +30 -0
- package/package.json +34 -12
- package/src/client/app/config-context.tsx +18 -0
- package/src/client/app/docs-layout.tsx +14 -0
- package/src/client/app/index.tsx +137 -262
- package/src/client/app/mdx-component.tsx +52 -0
- package/src/client/app/mdx-components-context.tsx +23 -0
- package/src/client/app/mdx-page.tsx +20 -0
- package/src/client/app/preload.tsx +38 -30
- package/src/client/app/router.tsx +30 -0
- package/src/client/app/scroll-handler.tsx +40 -0
- package/src/client/app/theme-context.tsx +75 -0
- package/src/client/components/default-layout.tsx +80 -0
- package/src/client/components/docs-layout.tsx +105 -0
- package/src/client/components/icons-dev.tsx +74 -0
- package/src/client/components/mdx/admonition.tsx +107 -0
- package/src/client/components/mdx/badge.tsx +41 -0
- package/src/client/components/mdx/button.tsx +35 -0
- package/src/client/components/mdx/card.tsx +124 -0
- package/src/client/components/mdx/code-block.tsx +119 -0
- package/src/client/components/mdx/component-preview.tsx +47 -0
- package/src/client/components/mdx/component-props.tsx +83 -0
- package/src/client/components/mdx/field.tsx +66 -0
- package/src/client/components/mdx/file-tree.tsx +287 -0
- package/src/client/components/mdx/hooks/use-code-block.ts +56 -0
- package/src/client/components/mdx/hooks/use-component-preview.ts +16 -0
- package/src/client/components/mdx/hooks/useTable.ts +74 -0
- package/src/client/components/mdx/hooks/useTabs.ts +68 -0
- package/src/client/components/mdx/image.tsx +23 -0
- package/src/client/components/mdx/index.ts +53 -0
- package/src/client/components/mdx/link.tsx +38 -0
- package/src/client/components/mdx/list.tsx +192 -0
- package/src/client/components/mdx/table.tsx +156 -0
- package/src/client/components/mdx/tabs.tsx +135 -0
- package/src/client/components/mdx/video.tsx +68 -0
- package/src/client/components/primitives/breadcrumbs.tsx +79 -0
- package/src/client/components/primitives/button-group.tsx +54 -0
- package/src/client/components/primitives/button.tsx +145 -0
- package/src/client/components/primitives/helpers/observer.ts +120 -0
- package/src/client/components/primitives/index.ts +17 -0
- package/src/client/components/primitives/link.tsx +122 -0
- package/src/client/components/primitives/menu.tsx +159 -0
- package/src/client/components/primitives/navbar.tsx +359 -0
- package/src/client/components/primitives/navigation-menu.tsx +116 -0
- package/src/client/components/primitives/on-this-page.tsx +461 -0
- package/src/client/components/primitives/page-nav.tsx +87 -0
- package/src/client/components/primitives/popover.tsx +47 -0
- package/src/client/components/primitives/search-dialog.tsx +183 -0
- package/src/client/components/primitives/sidebar.tsx +154 -0
- package/src/client/components/primitives/tabs.tsx +90 -0
- package/src/client/components/primitives/tooltip.tsx +83 -0
- package/src/client/components/primitives/types.ts +11 -0
- package/src/client/components/ui-base/breadcrumbs.tsx +42 -0
- package/src/client/components/ui-base/copy-markdown.tsx +112 -0
- package/src/client/components/ui-base/error-boundary.tsx +52 -0
- package/src/client/components/ui-base/github-stars.tsx +27 -0
- package/src/client/components/ui-base/head.tsx +69 -0
- package/src/client/components/ui-base/loading.tsx +87 -0
- package/src/client/components/ui-base/navbar.tsx +138 -0
- package/src/client/components/ui-base/not-found.tsx +24 -0
- package/src/client/components/ui-base/on-this-page.tsx +152 -0
- package/src/client/components/ui-base/page-nav.tsx +39 -0
- package/src/client/components/ui-base/powered-by.tsx +19 -0
- package/src/client/components/ui-base/progress-bar.tsx +67 -0
- package/src/client/components/ui-base/search-dialog.tsx +82 -0
- package/src/client/components/ui-base/sidebar.tsx +104 -0
- package/src/client/components/ui-base/tabs.tsx +65 -0
- package/src/client/components/ui-base/theme-toggle.tsx +32 -0
- package/src/client/hooks/index.ts +12 -0
- package/src/client/hooks/use-breadcrumbs.ts +22 -0
- package/src/client/hooks/use-i18n.ts +84 -0
- package/src/client/hooks/use-localized-to.ts +95 -0
- package/src/client/hooks/use-location.ts +5 -0
- package/src/client/hooks/use-navbar.ts +60 -0
- package/src/client/hooks/use-onthispage.ts +23 -0
- package/src/client/hooks/use-page-nav.ts +22 -0
- package/src/client/hooks/use-routes.ts +72 -0
- package/src/client/hooks/use-search.ts +71 -0
- package/src/client/hooks/use-sidebar.ts +49 -0
- package/src/client/hooks/use-tabs.ts +43 -0
- package/src/client/hooks/use-version.ts +78 -0
- package/src/client/index.ts +55 -17
- package/src/client/integrations/codesandbox.ts +179 -0
- package/src/client/ssr.tsx +27 -16
- package/src/client/theme/neutral.css +360 -0
- package/src/client/types.ts +131 -27
- package/src/client/utils/cn.ts +6 -0
- package/src/client/utils/copy-clipboard.ts +22 -0
- package/src/client/utils/get-base-file-path.ts +21 -0
- package/src/client/utils/github.ts +121 -0
- package/src/client/utils/use-on-change.ts +15 -0
- package/src/client/virtual.d.ts +24 -0
- package/src/node/cache.ts +156 -156
- package/src/node/config.ts +159 -103
- package/src/node/index.ts +13 -13
- package/src/node/mdx.ts +213 -61
- package/src/node/plugin/entry.ts +29 -18
- package/src/node/plugin/html.ts +11 -11
- package/src/node/plugin/index.ts +161 -84
- package/src/node/plugin/types.ts +2 -4
- package/src/node/routes/cache.ts +6 -6
- package/src/node/routes/index.ts +206 -113
- package/src/node/routes/parser.ts +102 -82
- package/src/node/routes/sorter.ts +15 -15
- package/src/node/routes/types.ts +24 -24
- package/src/node/ssg/index.ts +73 -47
- package/src/node/ssg/meta.ts +4 -4
- package/src/node/ssg/options.ts +5 -5
- package/src/node/ssg/sitemap.ts +14 -14
- package/src/node/utils.ts +54 -31
- package/tsconfig.json +25 -20
- package/tsup.config.ts +23 -14
- package/dist/PackageManagerTabs-NVT7G625.mjs +0 -99
- package/dist/SearchDialog-AGVF6JBO.mjs +0 -194
- package/dist/SearchDialog-YPDOM7Q6.css +0 -2847
- package/dist/Video-KNTY5BNO.mjs +0 -6
- package/dist/cache-KNL5B4EE.mjs +0 -12
- package/dist/chunk-7SFUJWTB.mjs +0 -211
- package/dist/chunk-FFBNU6IJ.mjs +0 -386
- package/dist/chunk-FMTOYQLO.mjs +0 -37
- package/dist/chunk-TKLQWU7H.mjs +0 -1920
- package/dist/chunk-Z7JHYNAS.mjs +0 -57
- package/dist/client/index.css +0 -2847
- package/dist/client/ssr.css +0 -2847
- package/dist/types-Dj-bfnC3.d.mts +0 -74
- package/dist/types-Dj-bfnC3.d.ts +0 -74
- package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -61
- package/src/client/theme/components/CodeBlock/index.ts +0 -1
- package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +0 -131
- package/src/client/theme/components/PackageManagerTabs/index.ts +0 -1
- package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +0 -64
- package/src/client/theme/components/Playground/Playground.tsx +0 -180
- package/src/client/theme/components/Playground/index.ts +0 -1
- package/src/client/theme/components/Playground/playground.css +0 -238
- package/src/client/theme/components/Video/Video.tsx +0 -84
- package/src/client/theme/components/Video/index.ts +0 -1
- package/src/client/theme/components/Video/video.css +0 -41
- package/src/client/theme/components/mdx/Admonition.tsx +0 -80
- package/src/client/theme/components/mdx/Badge.tsx +0 -31
- package/src/client/theme/components/mdx/Button.tsx +0 -50
- package/src/client/theme/components/mdx/Card.tsx +0 -80
- package/src/client/theme/components/mdx/Field.tsx +0 -60
- package/src/client/theme/components/mdx/FileTree.tsx +0 -229
- package/src/client/theme/components/mdx/List.tsx +0 -57
- package/src/client/theme/components/mdx/Table.tsx +0 -151
- package/src/client/theme/components/mdx/Tabs.tsx +0 -123
- package/src/client/theme/components/mdx/index.ts +0 -27
- package/src/client/theme/components/mdx/mdx-components.css +0 -764
- package/src/client/theme/icons/bun.tsx +0 -62
- package/src/client/theme/icons/deno.tsx +0 -20
- package/src/client/theme/icons/discord.tsx +0 -12
- package/src/client/theme/icons/github.tsx +0 -15
- package/src/client/theme/icons/npm.tsx +0 -13
- package/src/client/theme/icons/pnpm.tsx +0 -72
- package/src/client/theme/icons/twitter.tsx +0 -12
- package/src/client/theme/styles/markdown.css +0 -394
- package/src/client/theme/styles/variables.css +0 -175
- package/src/client/theme/styles.css +0 -39
- package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
- package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
- package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
- package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -112
- package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
- package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -50
- package/src/client/theme/ui/ErrorBoundary/error-boundary.css +0 -55
- package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
- package/src/client/theme/ui/Footer/footer.css +0 -32
- package/src/client/theme/ui/Head/Head.tsx +0 -69
- package/src/client/theme/ui/Head/index.ts +0 -1
- package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
- package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
- package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
- package/src/client/theme/ui/Layout/Layout.tsx +0 -203
- package/src/client/theme/ui/Layout/base.css +0 -106
- package/src/client/theme/ui/Layout/index.ts +0 -2
- package/src/client/theme/ui/Layout/pagination.css +0 -72
- package/src/client/theme/ui/Layout/responsive.css +0 -47
- package/src/client/theme/ui/Link/Link.tsx +0 -392
- package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
- package/src/client/theme/ui/Link/index.ts +0 -2
- package/src/client/theme/ui/Link/link-preview.css +0 -48
- package/src/client/theme/ui/Loading/Loading.tsx +0 -10
- package/src/client/theme/ui/Loading/index.ts +0 -1
- package/src/client/theme/ui/Loading/loading.css +0 -30
- package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
- package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
- package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
- package/src/client/theme/ui/Navbar/index.ts +0 -2
- package/src/client/theme/ui/Navbar/navbar.css +0 -347
- package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
- package/src/client/theme/ui/NotFound/index.ts +0 -1
- package/src/client/theme/ui/NotFound/not-found.css +0 -64
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
- package/src/client/theme/ui/OnThisPage/index.ts +0 -1
- package/src/client/theme/ui/OnThisPage/toc.css +0 -152
- package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
- package/src/client/theme/ui/PoweredBy/index.ts +0 -1
- package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
- package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
- package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
- package/src/client/theme/ui/ProgressBar/index.ts +0 -1
- package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
- package/src/client/theme/ui/SearchDialog/index.ts +0 -1
- package/src/client/theme/ui/SearchDialog/search.css +0 -152
- package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
- package/src/client/theme/ui/Sidebar/index.ts +0 -1
- package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
- package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
- package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
- package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
- package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
- package/src/client/utils.ts +0 -49
package/src/node/routes/index.ts
CHANGED
|
@@ -1,179 +1,272 @@
|
|
|
1
|
-
import fastGlob from
|
|
2
|
-
import { BoltdocsConfig } from
|
|
3
|
-
import { capitalize } from
|
|
1
|
+
import fastGlob from 'fast-glob'
|
|
2
|
+
import type { BoltdocsConfig } from '../config'
|
|
3
|
+
import { capitalize } from '../utils'
|
|
4
4
|
|
|
5
|
-
import { RouteMeta, ParsedDocFile } from
|
|
6
|
-
import { docCache, invalidateRouteCache, invalidateFile } from
|
|
7
|
-
import { parseDocFile } from
|
|
8
|
-
import { sortRoutes } from
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
-
* @param config -
|
|
22
|
-
* @param basePath - The base URL path
|
|
23
|
-
* @returns A promise
|
|
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 =
|
|
32
|
+
basePath: string = '/docs',
|
|
29
33
|
): Promise<RouteMeta[]> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
docCache.set(file, result);
|
|
59
|
-
return result;
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
80
|
+
parsed.push(...chunkResults)
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
70
|
-
docCache.save()
|
|
88
|
+
// Save cache after processing
|
|
89
|
+
docCache.save()
|
|
71
90
|
|
|
72
|
-
//
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
//
|
|
112
|
-
const routes: RouteMeta[] = parsed.
|
|
113
|
-
|
|
114
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
153
|
+
// 5. OPTIMIZED I18N FALLBACKS
|
|
154
|
+
let finalRoutes = routes
|
|
126
155
|
if (config?.i18n) {
|
|
127
|
-
const
|
|
128
|
-
|
|
156
|
+
const fallbacks = generateI18nFallbacks(routes, config, basePath)
|
|
157
|
+
finalRoutes = [...routes, ...fallbacks]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const sorted = sortRoutes(finalRoutes)
|
|
129
161
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
167
|
+
return sorted
|
|
168
|
+
}
|
|
137
169
|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import GithubSlugger from
|
|
3
|
-
import { BoltdocsConfig } from
|
|
4
|
-
import { ParsedDocFile } from
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import GithubSlugger from 'github-slugger'
|
|
3
|
+
import type { BoltdocsConfig } from '../config'
|
|
4
|
+
import type { ParsedDocFile } from './types'
|
|
5
5
|
import {
|
|
6
6
|
normalizePath,
|
|
7
7
|
parseFrontmatter,
|
|
@@ -9,16 +9,21 @@ import {
|
|
|
9
9
|
capitalize,
|
|
10
10
|
stripNumberPrefix,
|
|
11
11
|
extractNumberPrefix,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
sanitizeHtml,
|
|
13
|
+
stripHtmlTags,
|
|
14
|
+
} from '../utils'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Parses a single Markdown/MDX file and extracts its metadata for routing.
|
|
17
18
|
* Checks frontmatter for explicit titles, descriptions, and sidebar positions.
|
|
18
19
|
*
|
|
20
|
+
* Also performs security validation to prevent path traversal and basic
|
|
21
|
+
* XSS sanitization for metadata and headings.
|
|
22
|
+
*
|
|
19
23
|
* @param file - The absolute path to the file
|
|
20
24
|
* @param docsDir - The root documentation directory (e.g., 'docs')
|
|
21
25
|
* @param basePath - The base URL path for the routes (default: '/docs')
|
|
26
|
+
* @param config - The Boltdocs configuration for versions and i18n
|
|
22
27
|
* @returns A parsed structure ready for route assembly and caching
|
|
23
28
|
*/
|
|
24
29
|
export function parseDocFile(
|
|
@@ -28,138 +33,138 @@ export function parseDocFile(
|
|
|
28
33
|
config?: BoltdocsConfig,
|
|
29
34
|
): ParsedDocFile {
|
|
30
35
|
// Security: Prevent path traversal
|
|
31
|
-
const decodedFile = decodeURIComponent(file)
|
|
32
|
-
const absoluteFile = path.resolve(decodedFile)
|
|
33
|
-
const absoluteDocsDir = path.resolve(docsDir)
|
|
36
|
+
const decodedFile = decodeURIComponent(file)
|
|
37
|
+
const absoluteFile = path.resolve(decodedFile)
|
|
38
|
+
const absoluteDocsDir = path.resolve(docsDir)
|
|
34
39
|
const relativePath = normalizePath(
|
|
35
40
|
path.relative(absoluteDocsDir, absoluteFile),
|
|
36
|
-
)
|
|
41
|
+
)
|
|
37
42
|
|
|
38
43
|
if (
|
|
39
|
-
relativePath.startsWith(
|
|
40
|
-
relativePath ===
|
|
41
|
-
absoluteFile.includes(
|
|
44
|
+
relativePath.startsWith('../') ||
|
|
45
|
+
relativePath === '..' ||
|
|
46
|
+
absoluteFile.includes('\0')
|
|
42
47
|
) {
|
|
43
48
|
throw new Error(
|
|
44
49
|
`Security breach: File is outside of docs directory or contains null bytes: ${file}`,
|
|
45
|
-
)
|
|
50
|
+
)
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
const { data, content } = parseFrontmatter(file)
|
|
49
|
-
let parts = relativePath.split(
|
|
53
|
+
const { data, content } = parseFrontmatter(file)
|
|
54
|
+
let parts = relativePath.split('/')
|
|
50
55
|
|
|
51
|
-
let locale: string | undefined
|
|
52
|
-
let version: string | undefined
|
|
56
|
+
let locale: string | undefined
|
|
57
|
+
let version: string | undefined
|
|
53
58
|
|
|
54
59
|
// Level 1: Check for version
|
|
55
60
|
if (config?.versions && parts.length > 0) {
|
|
56
|
-
const potentialVersion = parts[0]
|
|
61
|
+
const potentialVersion = parts[0]
|
|
57
62
|
if (config.versions.versions[potentialVersion]) {
|
|
58
|
-
version = potentialVersion
|
|
59
|
-
parts = parts.slice(1)
|
|
63
|
+
version = potentialVersion
|
|
64
|
+
parts = parts.slice(1)
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
// Level 2: Check for locale
|
|
64
69
|
if (config?.i18n && parts.length > 0) {
|
|
65
|
-
const potentialLocale = parts[0]
|
|
70
|
+
const potentialLocale = parts[0]
|
|
66
71
|
if (config.i18n.locales[potentialLocale]) {
|
|
67
|
-
locale = potentialLocale
|
|
68
|
-
parts = parts.slice(1)
|
|
72
|
+
locale = potentialLocale
|
|
73
|
+
parts = parts.slice(1)
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
// Level 3: Check for Tab hierarchy (name)
|
|
73
|
-
let inferredTab: string | undefined
|
|
78
|
+
let inferredTab: string | undefined
|
|
74
79
|
if (parts.length > 0) {
|
|
75
|
-
const tabMatch = parts[0].match(/^\((.+)\)$/)
|
|
80
|
+
const tabMatch = parts[0].match(/^\((.+)\)$/)
|
|
76
81
|
if (tabMatch) {
|
|
77
|
-
inferredTab = tabMatch[1].toLowerCase()
|
|
78
|
-
parts = parts.slice(1)
|
|
82
|
+
inferredTab = tabMatch[1].toLowerCase()
|
|
83
|
+
parts = parts.slice(1)
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
const cleanRelativePath = parts.join(
|
|
87
|
+
const cleanRelativePath = parts.join('/')
|
|
83
88
|
|
|
84
|
-
let cleanRoutePath: string
|
|
89
|
+
let cleanRoutePath: string
|
|
85
90
|
if (data.permalink) {
|
|
86
91
|
// If a permalink is specified, ensure it starts with a slash
|
|
87
|
-
cleanRoutePath = data.permalink.startsWith(
|
|
92
|
+
cleanRoutePath = data.permalink.startsWith('/')
|
|
88
93
|
? data.permalink
|
|
89
|
-
: `/${data.permalink}
|
|
94
|
+
: `/${data.permalink}`
|
|
90
95
|
} else {
|
|
91
|
-
cleanRoutePath = fileToRoutePath(cleanRelativePath ||
|
|
96
|
+
cleanRoutePath = fileToRoutePath(cleanRelativePath || 'index.md')
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
let finalPath = basePath
|
|
99
|
+
let finalPath = basePath
|
|
95
100
|
if (version) {
|
|
96
|
-
finalPath +=
|
|
101
|
+
finalPath += '/' + version
|
|
97
102
|
}
|
|
98
103
|
if (locale) {
|
|
99
|
-
finalPath +=
|
|
104
|
+
finalPath += '/' + locale
|
|
100
105
|
}
|
|
101
|
-
finalPath += cleanRoutePath ===
|
|
106
|
+
finalPath += cleanRoutePath === '/' ? '' : cleanRoutePath
|
|
102
107
|
|
|
103
|
-
if (!finalPath || finalPath ===
|
|
108
|
+
if (!finalPath || finalPath === '') finalPath = '/'
|
|
104
109
|
|
|
105
|
-
const rawFileName = parts[parts.length - 1]
|
|
106
|
-
const cleanFileName = stripNumberPrefix(rawFileName)
|
|
110
|
+
const rawFileName = parts[parts.length - 1]
|
|
111
|
+
const cleanFileName = stripNumberPrefix(rawFileName)
|
|
107
112
|
const inferredTitle = stripNumberPrefix(
|
|
108
113
|
path.basename(file, path.extname(file)),
|
|
109
|
-
)
|
|
114
|
+
)
|
|
110
115
|
const sidebarPosition =
|
|
111
|
-
data.sidebarPosition ?? extractNumberPrefix(rawFileName)
|
|
116
|
+
data.sidebarPosition ?? extractNumberPrefix(rawFileName)
|
|
117
|
+
|
|
118
|
+
const rawDirName = parts.length >= 2 ? parts[0] : undefined
|
|
119
|
+
const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined
|
|
112
120
|
|
|
113
|
-
const
|
|
114
|
-
const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined;
|
|
121
|
+
const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName)
|
|
115
122
|
|
|
116
|
-
const
|
|
123
|
+
const slugger = new GithubSlugger()
|
|
124
|
+
const headings: { level: number; text: string; id: string }[] = []
|
|
125
|
+
const headingsRegex = /^(#{2,4})\s+(.+)$/gm
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
const slugger = new GithubSlugger();
|
|
120
|
-
const headingsRegex = /^(#{2,4})\s+(.+)$/gm;
|
|
121
|
-
let match;
|
|
127
|
+
let match
|
|
122
128
|
while ((match = headingsRegex.exec(content)) !== null) {
|
|
123
|
-
const level = match[1].length
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.replace(
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
const level = match[1].length
|
|
130
|
+
const rawText = match[2]
|
|
131
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Strip markdown links
|
|
132
|
+
.replace(/[_*`]/g, '') // Strip markdown formatting
|
|
133
|
+
.trim()
|
|
134
|
+
|
|
135
|
+
const sanitizedText = sanitizeHtml(rawText).trim()
|
|
136
|
+
const id = slugger.slug(sanitizedText)
|
|
137
|
+
|
|
138
|
+
headings.push({ level, text: sanitizedText, id })
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
const sanitizedTitle = data.title
|
|
141
|
+
const sanitizedTitle = data.title
|
|
142
|
+
? sanitizeHtml(String(data.title))
|
|
143
|
+
: inferredTitle
|
|
135
144
|
let sanitizedDescription = data.description
|
|
136
|
-
? data.description
|
|
137
|
-
:
|
|
145
|
+
? sanitizeHtml(String(data.description))
|
|
146
|
+
: ''
|
|
138
147
|
|
|
139
148
|
// If no description is provided, extract a summary from the content
|
|
140
149
|
if (!sanitizedDescription && content) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
const plainExcerpt = stripHtmlTags(
|
|
151
|
+
content
|
|
152
|
+
.replace(/^#+.*$/gm, '') // Remove headers
|
|
153
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
154
|
+
.replace(/[_*`]/g, '') // Remove formatting
|
|
155
|
+
.replace(/\s+/g, ' '), // Normalize whitespace
|
|
156
|
+
)
|
|
146
157
|
.trim()
|
|
147
|
-
.slice(0, 160)
|
|
148
|
-
|
|
158
|
+
.slice(0, 160)
|
|
159
|
+
|
|
160
|
+
sanitizedDescription = plainExcerpt
|
|
149
161
|
}
|
|
150
162
|
|
|
151
|
-
const sanitizedBadge = data.badge ? data.badge : undefined
|
|
152
|
-
const icon = data.icon ? String(data.icon) : undefined
|
|
163
|
+
const sanitizedBadge = data.badge ? sanitizeHtml(String(data.badge)) : undefined
|
|
164
|
+
const icon = data.icon ? String(data.icon) : undefined
|
|
153
165
|
|
|
154
166
|
// Extract full content as plain text for search indexing
|
|
155
|
-
const plainText = content
|
|
156
|
-
.replace(/^#+.*$/gm, "") // Remove headers
|
|
157
|
-
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") // Simplify links
|
|
158
|
-
.replace(/<[^>]+>/g, "") // Remove HTML/JSX tags
|
|
159
|
-
.replace(/\{[^\}]+\}/g, "") // Remove JS expressions/curly braces
|
|
160
|
-
.replace(/[_*`]/g, "") // Remove formatting
|
|
161
|
-
.replace(/\n+/g, " ") // Normalize whitespace
|
|
162
|
-
.trim();
|
|
167
|
+
const plainText = parseContentToPlainText(content)
|
|
163
168
|
|
|
164
169
|
return {
|
|
165
170
|
route: {
|
|
@@ -183,10 +188,10 @@ export function parseDocFile(
|
|
|
183
188
|
inferredTab,
|
|
184
189
|
groupMeta: isGroupIndex
|
|
185
190
|
? {
|
|
186
|
-
title:
|
|
191
|
+
title:
|
|
187
192
|
data.groupTitle ||
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
data.title ||
|
|
194
|
+
(cleanDirName ? capitalize(cleanDirName) : ''),
|
|
190
195
|
position:
|
|
191
196
|
data.groupPosition ??
|
|
192
197
|
data.sidebarPosition ??
|
|
@@ -197,5 +202,20 @@ export function parseDocFile(
|
|
|
197
202
|
inferredGroupPosition: rawDirName
|
|
198
203
|
? extractNumberPrefix(rawDirName)
|
|
199
204
|
: undefined,
|
|
200
|
-
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Converts markdown content to plain text for search indexing.
|
|
210
|
+
* Strips headers, links, tags, and formatting.
|
|
211
|
+
*/
|
|
212
|
+
function parseContentToPlainText(content: string): string {
|
|
213
|
+
const plainText = content
|
|
214
|
+
.replace(/^#+.*$/gm, '') // Remove headers
|
|
215
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
216
|
+
.replace(/\{[^\}]+\}/g, '') // Remove JS expressions/curly braces
|
|
217
|
+
.replace(/[_*`]/g, '') // Remove formatting
|
|
218
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
219
|
+
|
|
220
|
+
return stripHtmlTags(plainText).trim()
|
|
201
221
|
}
|