boltdocs 2.1.1 → 2.2.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 +9 -0
- package/dist/base-ui/index.d.mts +25 -0
- package/dist/base-ui/index.d.ts +25 -0
- package/dist/base-ui/index.js +1 -0
- package/dist/base-ui/index.mjs +1 -0
- package/dist/{cache-Q4T6VAUL.mjs → cache-CRAZ55X7.mjs} +1 -1
- package/dist/chunk-5D6XPYQ3.mjs +74 -0
- package/dist/chunk-6QXCKZAT.mjs +1 -0
- package/dist/chunk-H4M6P3DM.mjs +1 -0
- package/dist/chunk-JD3RSDE4.mjs +1 -0
- package/dist/chunk-JXHNX2WN.mjs +1 -0
- package/dist/chunk-JZXLCA2E.mjs +1 -0
- package/dist/chunk-MZBG4N4W.mjs +1 -0
- package/dist/chunk-NBCYHLAA.mjs +1 -0
- package/dist/chunk-Q3MLYTIQ.mjs +1 -0
- package/dist/chunk-RSII2UPE.mjs +1 -0
- package/dist/chunk-T3W44KWY.mjs +1 -0
- package/dist/chunk-ZK2266IZ.mjs +1 -0
- package/dist/chunk-ZRJ55GGF.mjs +1 -0
- package/dist/client/index.d.mts +13 -115
- package/dist/client/index.d.ts +13 -115
- package/dist/client/index.js +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/client/ssr.js +1 -1
- package/dist/client/ssr.mjs +1 -1
- package/dist/client/types.d.mts +3 -0
- package/dist/client/types.d.ts +3 -0
- package/dist/client/types.js +1 -0
- package/dist/client/types.mjs +0 -0
- package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
- package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
- package/dist/{client/hooks → hooks}/index.d.mts +25 -12
- package/dist/{client/hooks → hooks}/index.d.ts +25 -12
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.mjs +1 -0
- package/dist/integrations/index.d.mts +48 -0
- package/dist/integrations/index.d.ts +48 -0
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/link-DfBwCeZc.d.mts +68 -0
- package/dist/link-DfBwCeZc.d.ts +68 -0
- package/dist/loading-BqGrFWO5.d.mts +68 -0
- package/dist/loading-chS3pm9W.d.ts +68 -0
- package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
- package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
- package/dist/mdx/index.js +1 -0
- package/dist/mdx/index.mjs +1 -0
- package/dist/node/cli-entry.js +27 -27
- package/dist/node/cli-entry.mjs +1 -1
- package/dist/node/index.d.mts +44 -14
- package/dist/node/index.d.ts +44 -14
- package/dist/node/index.js +23 -23
- package/dist/node/index.mjs +1 -1
- package/dist/primitives/index.d.mts +301 -0
- package/dist/primitives/index.d.ts +301 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/index.mjs +1 -0
- package/dist/search-dialog-MA5AISC7.mjs +1 -0
- package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
- package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
- package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
- package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
- package/package.json +34 -8
- package/src/client/app/index.tsx +155 -35
- package/src/client/app/mdx-component.tsx +7 -3
- package/src/client/app/theme-context.tsx +40 -23
- package/src/client/components/default-layout.tsx +12 -6
- package/src/client/components/primitives/breadcrumbs.tsx +1 -1
- package/src/client/components/primitives/navbar.tsx +5 -2
- package/src/client/components/primitives/search-dialog.tsx +12 -3
- package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
- package/src/client/components/ui-base/index.ts +17 -0
- package/src/client/components/ui-base/navbar.tsx +66 -33
- package/src/client/components/ui-base/powered-by.tsx +8 -5
- package/src/client/components/ui-base/sidebar.tsx +31 -22
- package/src/client/components/ui-base/tabs.tsx +4 -1
- package/src/client/components/ui-base/theme-toggle.tsx +35 -15
- package/src/client/hooks/use-i18n.ts +37 -7
- package/src/client/hooks/use-localized-to.ts +45 -68
- package/src/client/hooks/use-navbar.ts +10 -3
- package/src/client/hooks/use-routes.ts +61 -17
- package/src/client/hooks/use-search.ts +21 -5
- package/src/client/hooks/use-sidebar.ts +5 -2
- package/src/client/hooks/use-version.ts +5 -0
- package/src/client/integrations/index.ts +1 -0
- package/src/client/store/use-boltdocs-store.ts +43 -0
- package/src/client/types.ts +4 -2
- package/src/client/utils/i18n.ts +23 -0
- package/src/node/config.ts +54 -17
- package/src/node/index.ts +1 -1
- package/src/node/mdx/cache.ts +12 -0
- package/src/node/mdx/highlighter.ts +47 -0
- package/src/node/mdx/index.ts +114 -0
- package/src/node/mdx/rehype-shiki.ts +53 -0
- package/src/node/mdx/remark-shiki.ts +61 -0
- package/src/node/plugin/html.ts +8 -4
- package/src/node/plugin/index.ts +117 -68
- package/src/node/routes/index.ts +34 -13
- package/src/node/routes/parser.ts +12 -4
- package/src/node/ssg/index.ts +3 -3
- package/src/node/utils.ts +32 -2
- package/tsup.config.ts +7 -2
- package/dist/chunk-52MVMZWS.mjs +0 -1
- package/dist/chunk-BVWWKXJH.mjs +0 -1
- package/dist/chunk-DVY3RDXD.mjs +0 -1
- package/dist/chunk-FUVYCYWC.mjs +0 -1
- package/dist/chunk-GBLMDJ2B.mjs +0 -1
- package/dist/chunk-ISPX45DF.mjs +0 -1
- package/dist/chunk-PNXZMUCO.mjs +0 -1
- package/dist/chunk-V2ZHKQSP.mjs +0 -74
- package/dist/client/components/mdx/index.js +0 -1
- package/dist/client/components/mdx/index.mjs +0 -1
- package/dist/client/hooks/index.js +0 -1
- package/dist/client/hooks/index.mjs +0 -1
- package/dist/search-dialog-TWGYKF2D.mjs +0 -1
- package/src/node/mdx.ts +0 -279
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHighlighter } from 'shiki'
|
|
2
|
+
import type { Highlighter } from 'shiki'
|
|
3
|
+
|
|
4
|
+
let shikiHighlighter: Highlighter | null = null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves or initializes the Shiki highlighter instance.
|
|
8
|
+
* Supports dual-theme configurations (light/dark).
|
|
9
|
+
*
|
|
10
|
+
* @param codeTheme - Theme configuration (string for single, object for dual).
|
|
11
|
+
* @returns A promise resolving to the highlighter instance.
|
|
12
|
+
*/
|
|
13
|
+
export async function getShikiHighlighter(codeTheme: any) {
|
|
14
|
+
if (shikiHighlighter) return shikiHighlighter
|
|
15
|
+
|
|
16
|
+
const themes =
|
|
17
|
+
typeof codeTheme === 'object'
|
|
18
|
+
? [codeTheme.light, codeTheme.dark]
|
|
19
|
+
: [codeTheme ?? 'github-dark']
|
|
20
|
+
|
|
21
|
+
// Fallbacks for standard themes
|
|
22
|
+
;['github-light', 'github-dark'].forEach((t) => {
|
|
23
|
+
if (!themes.includes(t)) themes.push(t)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Initialize with a core set of languages first to speed up boot
|
|
27
|
+
shikiHighlighter = await createHighlighter({
|
|
28
|
+
themes,
|
|
29
|
+
langs: [
|
|
30
|
+
'tsx',
|
|
31
|
+
'jsx',
|
|
32
|
+
'ts',
|
|
33
|
+
'js',
|
|
34
|
+
'json',
|
|
35
|
+
'md',
|
|
36
|
+
'mdx',
|
|
37
|
+
'css',
|
|
38
|
+
'html',
|
|
39
|
+
'bash',
|
|
40
|
+
'sh',
|
|
41
|
+
'yaml',
|
|
42
|
+
'yml',
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return shikiHighlighter
|
|
47
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import mdxPlugin from '@mdx-js/rollup'
|
|
2
|
+
import remarkGfm from 'remark-gfm'
|
|
3
|
+
import remarkFrontmatter from 'remark-frontmatter'
|
|
4
|
+
import rehypeSlug from 'rehype-slug'
|
|
5
|
+
import type { Plugin } from 'vite'
|
|
6
|
+
import crypto from 'crypto'
|
|
7
|
+
|
|
8
|
+
import type { BoltdocsConfig } from '../config'
|
|
9
|
+
import { mdxCache, MDX_PLUGIN_VERSION } from './cache'
|
|
10
|
+
import { remarkShiki } from './remark-shiki'
|
|
11
|
+
import { rehypeShiki } from './rehype-shiki'
|
|
12
|
+
|
|
13
|
+
let mdxCacheLoaded = false
|
|
14
|
+
let hits = 0
|
|
15
|
+
let total = 0
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configures the MDX compiler for Vite using `@mdx-js/rollup`.
|
|
19
|
+
* Includes standard remark and rehype plugins for GitHub Flavored Markdown (GFM),
|
|
20
|
+
* frontmatter extraction, and auto-linking headers.
|
|
21
|
+
*
|
|
22
|
+
* Also wraps the plugin with a persistent cache to avoid re-compiling unchanged MDX files.
|
|
23
|
+
*
|
|
24
|
+
* @param config - The Boltdocs configuration containing custom plugins
|
|
25
|
+
* @param compiler - The MDX compiler plugin (for testing)
|
|
26
|
+
* @returns A Vite plugin configured for MDX parsing with caching
|
|
27
|
+
*/
|
|
28
|
+
export function boltdocsMdxPlugin(
|
|
29
|
+
config?: BoltdocsConfig,
|
|
30
|
+
compiler = mdxPlugin,
|
|
31
|
+
): Plugin {
|
|
32
|
+
const extraRemarkPlugins =
|
|
33
|
+
config?.plugins?.flatMap((p) => p.remarkPlugins || []) || []
|
|
34
|
+
const extraRehypePlugins =
|
|
35
|
+
config?.plugins?.flatMap((p) => p.rehypePlugins || []) || []
|
|
36
|
+
|
|
37
|
+
const baseMdxPlugin = compiler({
|
|
38
|
+
remarkPlugins: [
|
|
39
|
+
remarkGfm,
|
|
40
|
+
remarkFrontmatter,
|
|
41
|
+
[remarkShiki, config],
|
|
42
|
+
...(extraRemarkPlugins as any[]),
|
|
43
|
+
],
|
|
44
|
+
rehypePlugins: [
|
|
45
|
+
rehypeSlug,
|
|
46
|
+
[rehypeShiki, config],
|
|
47
|
+
...(extraRehypePlugins as any[]),
|
|
48
|
+
],
|
|
49
|
+
jsxRuntime: 'automatic',
|
|
50
|
+
providerImportSource: '@mdx-js/react',
|
|
51
|
+
}) as Plugin
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...baseMdxPlugin,
|
|
55
|
+
name: 'vite-plugin-boltdocs-mdx',
|
|
56
|
+
|
|
57
|
+
async buildStart() {
|
|
58
|
+
hits = 0
|
|
59
|
+
total = 0
|
|
60
|
+
if (!mdxCacheLoaded) {
|
|
61
|
+
mdxCache.load()
|
|
62
|
+
mdxCacheLoaded = true
|
|
63
|
+
}
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
if (baseMdxPlugin.buildStart) {
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
await baseMdxPlugin.buildStart.call(this)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async transform(code, id, options) {
|
|
72
|
+
if (!id.endsWith('.md') && !id.endsWith('.mdx')) {
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
return baseMdxPlugin.transform?.call(this, code, id, options)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`[boltdocs] Transforming MDX: ${id}`)
|
|
78
|
+
total++
|
|
79
|
+
// Create a cache key based on path, content, and plugin version
|
|
80
|
+
const contentHash = crypto.createHash('md5').update(code).digest('hex')
|
|
81
|
+
const cacheKey = `${id}:${contentHash}:${MDX_PLUGIN_VERSION}`
|
|
82
|
+
|
|
83
|
+
const cached = mdxCache.get(cacheKey)
|
|
84
|
+
if (cached) {
|
|
85
|
+
hits++
|
|
86
|
+
return { code: cached, map: null }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
const result = await baseMdxPlugin.transform.call(this, code, id, options)
|
|
91
|
+
|
|
92
|
+
if (result && typeof result === 'object' && result.code) {
|
|
93
|
+
mdxCache.set(cacheKey, result.code)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async buildEnd() {
|
|
100
|
+
if (total > 0) {
|
|
101
|
+
console.log(
|
|
102
|
+
`[boltdocs] MDX Cache Performance: ${hits}/${total} hits (${Math.round((hits / total) * 100) || 0}%)`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
mdxCache.save()
|
|
106
|
+
await mdxCache.flush()
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
if (baseMdxPlugin.buildEnd) {
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
await baseMdxPlugin.buildEnd.call(this)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit'
|
|
2
|
+
import type { BoltdocsConfig } from '../config'
|
|
3
|
+
import { getShikiHighlighter } from './highlighter'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom rehype plugin to perform syntax highlighting at build time for
|
|
7
|
+
* standard Markdown code blocks.
|
|
8
|
+
*
|
|
9
|
+
* Injects 'data-highlighted="true"' and 'highlightedHtml' into the 'pre' tag properties,
|
|
10
|
+
* which are then consumed by the client-side CodeBlock component.
|
|
11
|
+
*
|
|
12
|
+
* @param config - The Boltdocs configuration
|
|
13
|
+
* @returns A rehype plugin function
|
|
14
|
+
*/
|
|
15
|
+
export function rehypeShiki(config?: BoltdocsConfig) {
|
|
16
|
+
return async (tree: any) => {
|
|
17
|
+
const codeTheme = config?.theme?.codeTheme || {
|
|
18
|
+
light: 'github-light',
|
|
19
|
+
dark: 'github-dark',
|
|
20
|
+
}
|
|
21
|
+
const highlighter = await getShikiHighlighter(codeTheme)
|
|
22
|
+
|
|
23
|
+
visit(tree, 'element', (node: any) => {
|
|
24
|
+
// Handle standard Markdown code blocks
|
|
25
|
+
if (node.tagName === 'pre' && node.children?.[0]?.tagName === 'code') {
|
|
26
|
+
const codeNode = node.children[0]
|
|
27
|
+
const className = codeNode.properties?.className || []
|
|
28
|
+
const langMatch = className.find((c: string) =>
|
|
29
|
+
c.startsWith('language-'),
|
|
30
|
+
)
|
|
31
|
+
const lang = langMatch ? langMatch.slice(9) : 'text'
|
|
32
|
+
const code = codeNode.children[0]?.value || ''
|
|
33
|
+
|
|
34
|
+
const options: any = { lang }
|
|
35
|
+
if (typeof codeTheme === 'object') {
|
|
36
|
+
options.themes = {
|
|
37
|
+
light: codeTheme.light,
|
|
38
|
+
dark: codeTheme.dark,
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
options.theme = codeTheme
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const html = highlighter.codeToHtml(code, options)
|
|
45
|
+
|
|
46
|
+
// Inject highlighted HTML and mark as highlighted for CodeBlock component
|
|
47
|
+
node.properties.dataHighlighted = 'true'
|
|
48
|
+
node.properties.highlightedHtml = html
|
|
49
|
+
node.children = []
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit'
|
|
2
|
+
import type { BoltdocsConfig } from '../config'
|
|
3
|
+
import { getShikiHighlighter } from './highlighter'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom remark plugin to highlight code in ComponentPreview components.
|
|
7
|
+
* This runs before rehype, ensuring that the 'highlightedHtml' prop is correctly
|
|
8
|
+
* attached to the MDX component as a JSX attribute.
|
|
9
|
+
*
|
|
10
|
+
* Supports both string literals and MDX expression values (template literals)
|
|
11
|
+
* for the 'code' attribute.
|
|
12
|
+
*
|
|
13
|
+
* @param config - The Boltdocs configuration
|
|
14
|
+
* @returns A remark plugin function
|
|
15
|
+
*/
|
|
16
|
+
export function remarkShiki(config?: BoltdocsConfig) {
|
|
17
|
+
return async (tree: any) => {
|
|
18
|
+
const codeTheme = config?.theme?.codeTheme ?? {
|
|
19
|
+
light: 'github-light',
|
|
20
|
+
dark: 'github-dark',
|
|
21
|
+
}
|
|
22
|
+
const highlighter = await getShikiHighlighter(codeTheme)
|
|
23
|
+
|
|
24
|
+
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node: any) => {
|
|
25
|
+
if (node.name !== 'ComponentPreview') return
|
|
26
|
+
|
|
27
|
+
const codeAttr = node.attributes?.find((a: any) => a.name === 'code')
|
|
28
|
+
let code = ''
|
|
29
|
+
|
|
30
|
+
if (codeAttr) {
|
|
31
|
+
if (typeof codeAttr.value === 'string') {
|
|
32
|
+
code = codeAttr.value
|
|
33
|
+
} else if (codeAttr.value?.type === 'mdxJsxAttributeValueExpression') {
|
|
34
|
+
const expr = codeAttr.value.value ?? ''
|
|
35
|
+
code = expr.match(/^[`'"](.+)[`'"]$/)?.[1] ?? expr
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!code) return
|
|
40
|
+
|
|
41
|
+
const options: any =
|
|
42
|
+
typeof codeTheme === 'object'
|
|
43
|
+
? {
|
|
44
|
+
themes: { light: codeTheme.light, dark: codeTheme.dark },
|
|
45
|
+
lang: 'tsx',
|
|
46
|
+
}
|
|
47
|
+
: { theme: codeTheme, lang: 'tsx' }
|
|
48
|
+
|
|
49
|
+
const html = highlighter.codeToHtml(code, options)
|
|
50
|
+
|
|
51
|
+
node.attributes = (node.attributes ?? []).filter(
|
|
52
|
+
(a: any) => a.name !== 'highlightedHtml',
|
|
53
|
+
)
|
|
54
|
+
node.attributes.push({
|
|
55
|
+
type: 'mdxJsxAttribute',
|
|
56
|
+
name: 'highlightedHtml',
|
|
57
|
+
value: html,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/node/plugin/html.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { BoltdocsConfig } from '../config'
|
|
|
4
4
|
* Provides a default HTML template if none is found in the project root.
|
|
5
5
|
*/
|
|
6
6
|
export function getHtmlTemplate(config: BoltdocsConfig): string {
|
|
7
|
-
const title = config.theme?.title ||
|
|
7
|
+
const title = config.theme?.title || 'Boltdocs'
|
|
8
8
|
return `<!doctype html>
|
|
9
9
|
<html lang="en">
|
|
10
10
|
<head>
|
|
@@ -27,7 +27,7 @@ export function getHtmlTemplate(config: BoltdocsConfig): string {
|
|
|
27
27
|
* @returns {string} The modified HTML string with injected tags
|
|
28
28
|
*/
|
|
29
29
|
export function injectHtmlMeta(html: string, config: BoltdocsConfig): string {
|
|
30
|
-
const theme = config.theme
|
|
30
|
+
const theme = config.theme
|
|
31
31
|
const title = theme?.title || 'Boltdocs'
|
|
32
32
|
const description = theme?.description || ''
|
|
33
33
|
|
|
@@ -46,12 +46,16 @@ export function injectHtmlMeta(html: string, config: BoltdocsConfig): string {
|
|
|
46
46
|
`<meta name="description" content="${description}">`,
|
|
47
47
|
`<meta property="og:title" content="${title}">`,
|
|
48
48
|
`<meta property="og:description" content="${description}">`,
|
|
49
|
-
theme?.ogImage
|
|
49
|
+
theme?.ogImage
|
|
50
|
+
? `<meta property="og:image" content="${theme.ogImage}">`
|
|
51
|
+
: '',
|
|
50
52
|
`<meta property="og:type" content="website">`,
|
|
51
53
|
`<meta name="twitter:card" content="summary_large_image">`,
|
|
52
54
|
`<meta name="twitter:title" content="${title}">`,
|
|
53
55
|
`<meta name="twitter:description" content="${description}">`,
|
|
54
|
-
theme?.ogImage
|
|
56
|
+
theme?.ogImage
|
|
57
|
+
? `<meta name="twitter:image" content="${theme.ogImage}">`
|
|
58
|
+
: '',
|
|
55
59
|
`<meta name="generator" content="Boltdocs">`,
|
|
56
60
|
]
|
|
57
61
|
.filter(Boolean)
|
package/src/node/plugin/index.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { injectHtmlMeta, getHtmlTemplate } from './html'
|
|
|
11
11
|
import { generateRobotsTxt } from '../ssg/robots'
|
|
12
12
|
import fs from 'fs'
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
export * from './types'
|
|
16
15
|
|
|
17
16
|
/**
|
|
@@ -55,7 +54,20 @@ export function boltdocsPlugin(
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
return {
|
|
58
|
-
optimizeDeps: {
|
|
57
|
+
optimizeDeps: {
|
|
58
|
+
include: ['react', 'react-dom'],
|
|
59
|
+
exclude: [
|
|
60
|
+
'boltdocs',
|
|
61
|
+
'boltdocs/client',
|
|
62
|
+
'boltdocs/hooks',
|
|
63
|
+
'boltdocs/primitives',
|
|
64
|
+
'boltdocs/base-ui',
|
|
65
|
+
'boltdocs/mdx',
|
|
66
|
+
'boltdocs/integrations',
|
|
67
|
+
'boltdocs/client/hooks',
|
|
68
|
+
'boltdocs/client/primitives',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
59
71
|
}
|
|
60
72
|
},
|
|
61
73
|
|
|
@@ -76,25 +88,40 @@ export function boltdocsPlugin(
|
|
|
76
88
|
next()
|
|
77
89
|
})
|
|
78
90
|
|
|
79
|
-
// Serve default HTML if index.html is missing
|
|
91
|
+
// Serve default HTML for documentation routes or if index.html is missing
|
|
80
92
|
server.middlewares.use(async (req, res, next) => {
|
|
81
93
|
const url = req.url?.split('?')[0] || '/'
|
|
82
94
|
const accept = req.headers.accept || ''
|
|
83
95
|
|
|
96
|
+
const isDocRoute =
|
|
97
|
+
url === '/' ||
|
|
98
|
+
url.startsWith('/docs') ||
|
|
99
|
+
(config.i18n &&
|
|
100
|
+
Object.keys(config.i18n.locales).some(
|
|
101
|
+
(locale) =>
|
|
102
|
+
url.startsWith(`/${locale}/docs`) || url === `/${locale}`,
|
|
103
|
+
)) ||
|
|
104
|
+
(config.external &&
|
|
105
|
+
Object.keys(config.external).some((extPath) =>
|
|
106
|
+
url.startsWith(extPath),
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
// Improved check: If it's a doc route, serve HTML even if it has a dot (e.g. version 1.1)
|
|
110
|
+
// We only skip if it has a known asset extension to prevent serving HTML for images/js/etc.
|
|
111
|
+
const isAsset = /\.(js|css|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|otf|mp4|webm|ogg|mp3|wav|flac|aac|pdf|zip|gz|map|json)$/i.test(url)
|
|
112
|
+
|
|
84
113
|
if (
|
|
85
114
|
accept.includes('text/html') &&
|
|
86
|
-
!
|
|
115
|
+
!isAsset &&
|
|
116
|
+
isDocRoute
|
|
87
117
|
) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
res.end(html)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
118
|
+
let html = getHtmlTemplate(config)
|
|
119
|
+
html = injectHtmlMeta(html, config)
|
|
120
|
+
html = await server.transformIndexHtml(req.url || '/', html)
|
|
121
|
+
res.statusCode = 200
|
|
122
|
+
res.setHeader('Content-Type', 'text/html')
|
|
123
|
+
res.end(html)
|
|
124
|
+
return
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
next()
|
|
@@ -124,66 +151,89 @@ export function boltdocsPlugin(
|
|
|
124
151
|
file: string,
|
|
125
152
|
type: 'add' | 'unlink' | 'change',
|
|
126
153
|
) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Restart the Vite server if the Boltdocs config changes
|
|
130
|
-
if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
|
|
131
|
-
server.restart()
|
|
132
|
-
return
|
|
133
|
-
}
|
|
154
|
+
try {
|
|
155
|
+
const normalized = normalizePath(file)
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
) {
|
|
141
|
-
const mod = server.moduleGraph.getModuleById(
|
|
142
|
-
'\0virtual:boltdocs-mdx-components',
|
|
143
|
-
)
|
|
144
|
-
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
145
|
-
server.ws.send({ type: 'full-reload' })
|
|
146
|
-
return
|
|
147
|
-
}
|
|
157
|
+
// Restart the Vite server if the Boltdocs config changes
|
|
158
|
+
if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
|
|
159
|
+
server.restart()
|
|
160
|
+
return
|
|
161
|
+
}
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
// If mdx-components file changes, invalidate the virtual module
|
|
164
|
+
if (
|
|
165
|
+
mdxCompExtensions.some((ext) =>
|
|
166
|
+
normalized.endsWith(`mdx-components.${ext}`),
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
const mod = server.moduleGraph.getModuleById(
|
|
170
|
+
'\0virtual:boltdocs-mdx-components',
|
|
171
|
+
)
|
|
172
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
173
|
+
server.ws.send({ type: 'full-reload' })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
160
176
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
// If layout.tsx/jsx file changes, invalidate the virtual module
|
|
178
|
+
if (
|
|
179
|
+
compExtensions.some((ext) => normalized.endsWith(`layout.${ext}`))
|
|
180
|
+
) {
|
|
181
|
+
const mod = server.moduleGraph.getModuleById(
|
|
182
|
+
'\0virtual:boltdocs-layout',
|
|
183
|
+
)
|
|
184
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
185
|
+
server.ws.send({ type: 'full-reload' })
|
|
186
|
+
return
|
|
187
|
+
}
|
|
166
188
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
189
|
+
if (
|
|
190
|
+
!normalized.startsWith(normalizedDocsDir) ||
|
|
191
|
+
!isDocFile(normalized)
|
|
192
|
+
)
|
|
193
|
+
return
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
195
|
+
// Invalidate appropriately
|
|
196
|
+
if (type === 'add' || type === 'unlink') {
|
|
197
|
+
invalidateRouteCache()
|
|
198
|
+
// Re-resolve config as it might affect versions/routes
|
|
199
|
+
config = await resolveConfig(docsDir)
|
|
200
|
+
|
|
201
|
+
const configMod = server.moduleGraph.getModuleById('\0virtual:boltdocs-config')
|
|
202
|
+
if (configMod) server.moduleGraph.invalidateModule(configMod)
|
|
203
|
+
|
|
204
|
+
server.ws.send({
|
|
205
|
+
type: 'custom',
|
|
206
|
+
event: 'boltdocs:config-update',
|
|
207
|
+
data: {
|
|
208
|
+
theme: config?.theme,
|
|
209
|
+
integrations: config?.integrations,
|
|
210
|
+
i18n: config?.i18n,
|
|
211
|
+
versions: config?.versions,
|
|
212
|
+
siteUrl: config?.siteUrl,
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
invalidateFile(file)
|
|
217
|
+
}
|
|
176
218
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
// Regenerate and push to client
|
|
220
|
+
// Optimization: generateRoutes is mostly incremental thanks to docCache
|
|
221
|
+
// We only force a full disk scan on add/unlink events
|
|
222
|
+
const newRoutes = await generateRoutes(docsDir, config, '/docs', type !== 'change')
|
|
181
223
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
224
|
+
const routesMod = server.moduleGraph.getModuleById(
|
|
225
|
+
'\0virtual:boltdocs-routes',
|
|
226
|
+
)
|
|
227
|
+
if (routesMod) server.moduleGraph.invalidateModule(routesMod)
|
|
228
|
+
|
|
229
|
+
server.ws.send({
|
|
230
|
+
type: 'custom',
|
|
231
|
+
event: 'boltdocs:routes-update',
|
|
232
|
+
data: newRoutes,
|
|
233
|
+
})
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error(`[boltdocs] HMR error during ${type} event:`, e)
|
|
236
|
+
}
|
|
187
237
|
}
|
|
188
238
|
|
|
189
239
|
server.watcher.on('add', (f) => handleFileEvent(f, 'add'))
|
|
@@ -211,7 +261,6 @@ export function boltdocsPlugin(
|
|
|
211
261
|
if (id === '\0virtual:boltdocs-config') {
|
|
212
262
|
const clientConfig = {
|
|
213
263
|
theme: config?.theme,
|
|
214
|
-
themeConfig: config?.themeConfig,
|
|
215
264
|
integrations: config?.integrations,
|
|
216
265
|
i18n: config?.i18n,
|
|
217
266
|
versions: config?.versions,
|
package/src/node/routes/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { sortRoutes } from './sorter'
|
|
|
11
11
|
export type { RouteMeta }
|
|
12
12
|
export { invalidateRouteCache, invalidateFile }
|
|
13
13
|
|
|
14
|
-
// Cache for localized path computations
|
|
14
|
+
// Cache for file list and localized path computations
|
|
15
|
+
let cachedFileList: string[] | null = null
|
|
15
16
|
const localizedPathCache = new Map<string, string>()
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -30,6 +31,7 @@ export async function generateRoutes(
|
|
|
30
31
|
docsDir: string,
|
|
31
32
|
config?: BoltdocsConfig,
|
|
32
33
|
basePath: string = '/docs',
|
|
34
|
+
forceScan: boolean = true,
|
|
33
35
|
): Promise<RouteMeta[]> {
|
|
34
36
|
const start = performance.now()
|
|
35
37
|
|
|
@@ -44,13 +46,19 @@ export async function generateRoutes(
|
|
|
44
46
|
docCache.invalidateAll()
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
// 1. FAST SCAN
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
62
|
|
|
55
63
|
// Prune cache entries for deleted files
|
|
56
64
|
docCache.pruneStale(new Set(files))
|
|
@@ -197,8 +205,6 @@ function generateI18nFallbacks(
|
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
for (const locale of allLocales) {
|
|
200
|
-
if (locale === defaultLocale) continue
|
|
201
|
-
|
|
202
208
|
const localePaths = routesByLocale.get(locale) || new Set<string>()
|
|
203
209
|
|
|
204
210
|
for (const defRoute of defaultRoutes) {
|
|
@@ -207,8 +213,12 @@ function generateI18nFallbacks(
|
|
|
207
213
|
defaultLocale,
|
|
208
214
|
locale,
|
|
209
215
|
basePath,
|
|
216
|
+
config,
|
|
210
217
|
)
|
|
211
218
|
|
|
219
|
+
// Skip if the path is already the same (e.g. for default locale unprefixed)
|
|
220
|
+
if (targetPath === defRoute.path) continue
|
|
221
|
+
|
|
212
222
|
if (!localePaths.has(targetPath)) {
|
|
213
223
|
fallbackRoutes.push({
|
|
214
224
|
...defRoute,
|
|
@@ -231,15 +241,26 @@ function computeLocalizedPath(
|
|
|
231
241
|
defaultLocale: string,
|
|
232
242
|
targetLocale: string,
|
|
233
243
|
basePath: string,
|
|
244
|
+
config?: BoltdocsConfig,
|
|
234
245
|
): string {
|
|
235
246
|
const cacheKey = `${path}:${targetLocale}`
|
|
236
247
|
const cached = localizedPathCache.get(cacheKey)
|
|
237
248
|
if (cached) return cached
|
|
238
249
|
|
|
239
250
|
let prefix = basePath
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
}
|
|
243
264
|
}
|
|
244
265
|
|
|
245
266
|
let pathAfterVersion = path.substring(prefix.length)
|
|
@@ -59,8 +59,17 @@ export function parseDocFile(
|
|
|
59
59
|
// Level 1: Check for version
|
|
60
60
|
if (config?.versions && parts.length > 0) {
|
|
61
61
|
const potentialVersion = parts[0]
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const prefix = config.versions.prefix || ''
|
|
63
|
+
|
|
64
|
+
const versionMatch = config.versions.versions.find(
|
|
65
|
+
(v) => {
|
|
66
|
+
const fullPath = prefix + v.path
|
|
67
|
+
return potentialVersion === fullPath || potentialVersion === v.path
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (versionMatch) {
|
|
72
|
+
version = versionMatch.path
|
|
64
73
|
parts = parts.slice(1)
|
|
65
74
|
}
|
|
66
75
|
}
|
|
@@ -124,8 +133,7 @@ export function parseDocFile(
|
|
|
124
133
|
const headings: { level: number; text: string; id: string }[] = []
|
|
125
134
|
const headingsRegex = /^(#{2,4})\s+(.+)$/gm
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
while ((match = headingsRegex.exec(content)) !== null) {
|
|
136
|
+
for (const match of content.matchAll(headingsRegex)) {
|
|
129
137
|
const level = match[1].length
|
|
130
138
|
const rawText = match[2]
|
|
131
139
|
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Strip markdown links
|