boltdocs 2.2.0 → 2.3.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 +10 -0
- package/bin/boltdocs.js +2 -2
- package/dist/base-ui/index.d.mts +3 -3
- package/dist/base-ui/index.d.ts +3 -3
- package/dist/base-ui/index.js +1 -1
- package/dist/base-ui/index.mjs +1 -1
- package/dist/{cache-CRAZ55X7.mjs → cache-P6WK424C.mjs} +1 -1
- package/dist/chunk-22NXDNP4.mjs +74 -0
- package/dist/chunk-2HUVMMJU.mjs +1 -0
- package/dist/chunk-2Z5T6EAU.mjs +1 -0
- package/dist/chunk-CRZGOE32.mjs +1 -0
- package/dist/chunk-HA6543SL.mjs +1 -0
- package/dist/{chunk-ZK2266IZ.mjs → chunk-RPUERTVC.mjs} +1 -1
- package/dist/{chunk-MZBG4N4W.mjs → chunk-URTD6E6S.mjs} +1 -1
- package/dist/chunk-W2NB4T6V.mjs +1 -0
- package/dist/chunk-Y4RRHPXC.mjs +1 -0
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.ts +1 -1
- 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/hooks/index.d.mts +7 -15
- package/dist/hooks/index.d.ts +7 -15
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/{loading-chS3pm9W.d.ts → loading-B7X5Wchs.d.ts} +3 -5
- package/dist/{loading-BqGrFWO5.d.mts → loading-WuaQbsKb.d.mts} +3 -5
- package/dist/mdx/index.js +1 -1
- package/dist/mdx/index.mjs +1 -1
- package/dist/node/cli-entry.js +27 -23
- package/dist/node/cli-entry.mjs +5 -1
- package/dist/node/index.js +10 -10
- package/dist/node/index.mjs +1 -1
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.d.ts +2 -2
- package/dist/primitives/index.js +1 -1
- package/dist/primitives/index.mjs +1 -1
- package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
- package/package.json +2 -1
- package/src/client/app/theme-context.tsx +14 -7
- package/src/client/components/default-layout.tsx +6 -2
- package/src/client/components/primitives/navbar.tsx +3 -3
- package/src/client/components/primitives/search-dialog.tsx +4 -4
- package/src/client/components/primitives/sidebar.tsx +3 -2
- package/src/client/components/primitives/skeleton.tsx +26 -0
- package/src/client/components/ui-base/loading.tsx +43 -73
- package/src/client/components/ui-base/navbar.tsx +8 -6
- package/src/client/components/ui-base/page-nav.tsx +2 -1
- package/src/client/components/ui-base/powered-by.tsx +4 -1
- package/src/client/components/ui-base/search-dialog.tsx +16 -5
- package/src/client/components/ui-base/sidebar.tsx +4 -2
- package/src/client/hooks/use-i18n.ts +3 -2
- package/src/client/hooks/use-localized-to.ts +6 -5
- package/src/client/hooks/use-page-nav.ts +27 -6
- package/src/client/hooks/use-routes.ts +2 -1
- package/src/client/hooks/use-search.ts +81 -59
- package/src/client/hooks/use-sidebar.ts +2 -1
- package/src/client/store/use-boltdocs-store.ts +6 -5
- package/src/client/theme/neutral.css +29 -0
- package/src/node/{cli.ts → cli/build.ts} +17 -23
- package/src/node/cli/dev.ts +22 -0
- package/src/node/cli/doctor.ts +243 -0
- package/src/node/cli/index.ts +9 -0
- package/src/node/cli/ui.ts +54 -0
- package/src/node/cli-entry.ts +16 -16
- package/src/node/config.ts +1 -1
- package/src/node/plugin/entry.ts +1 -1
- package/src/node/plugin/index.ts +24 -10
- package/src/node/routes/parser.ts +9 -9
- package/src/node/search/index.ts +55 -0
- package/src/node/ssg/index.ts +14 -6
- package/src/node/ssg/robots.ts +7 -4
- package/dist/chunk-5D6XPYQ3.mjs +0 -74
- package/dist/chunk-6QXCKZAT.mjs +0 -1
- package/dist/chunk-H4M6P3DM.mjs +0 -1
- package/dist/chunk-JXHNX2WN.mjs +0 -1
- package/dist/chunk-Q3MLYTIQ.mjs +0 -1
- package/dist/chunk-RSII2UPE.mjs +0 -1
- package/dist/chunk-ZRJ55GGF.mjs +0 -1
- package/dist/search-dialog-MA5AISC7.mjs +0 -1
|
@@ -1,27 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createViteConfig, resolveConfig } from '
|
|
3
|
-
import { getHtmlTemplate } from '
|
|
1
|
+
import { build, preview } from 'vite'
|
|
2
|
+
import { createViteConfig, resolveConfig } from '../index'
|
|
3
|
+
import { getHtmlTemplate } from '../plugin/html'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import fs from 'fs'
|
|
6
|
+
import * as ui from './ui'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Logic for the `boltdocs build` command.
|
|
10
|
+
* Prepares the production bundle and handles dynamic index.html generation.
|
|
11
|
+
*
|
|
12
|
+
* @param root - The project root directory
|
|
10
13
|
*/
|
|
11
|
-
|
|
12
|
-
export async function devAction(root: string = process.cwd()) {
|
|
13
|
-
try {
|
|
14
|
-
const viteConfig = await createViteConfig(root, 'development')
|
|
15
|
-
const server = await createServer(viteConfig)
|
|
16
|
-
await server.listen()
|
|
17
|
-
server.printUrls()
|
|
18
|
-
server.bindCLIShortcuts({ print: true })
|
|
19
|
-
} catch (e) {
|
|
20
|
-
console.error('[boltdocs] Failed to start dev server:', e)
|
|
21
|
-
process.exit(1)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
14
|
export async function buildAction(root: string = process.cwd()) {
|
|
26
15
|
let createdIndexHtml = false
|
|
27
16
|
const indexPath = path.resolve(root, 'index.html')
|
|
@@ -35,9 +24,9 @@ export async function buildAction(root: string = process.cwd()) {
|
|
|
35
24
|
|
|
36
25
|
const viteConfig = await createViteConfig(root, 'production')
|
|
37
26
|
await build(viteConfig)
|
|
38
|
-
|
|
27
|
+
ui.success('Build completed successfully.')
|
|
39
28
|
} catch (e) {
|
|
40
|
-
|
|
29
|
+
ui.error('Build failed:', e)
|
|
41
30
|
process.exit(1)
|
|
42
31
|
} finally {
|
|
43
32
|
if (createdIndexHtml && fs.existsSync(indexPath)) {
|
|
@@ -46,14 +35,19 @@ export async function buildAction(root: string = process.cwd()) {
|
|
|
46
35
|
}
|
|
47
36
|
}
|
|
48
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Logic for the `boltdocs preview` command.
|
|
40
|
+
* Serves the production build from the disk.
|
|
41
|
+
*
|
|
42
|
+
* @param root - The project root directory
|
|
43
|
+
*/
|
|
49
44
|
export async function previewAction(root: string = process.cwd()) {
|
|
50
45
|
try {
|
|
51
46
|
const viteConfig = await createViteConfig(root, 'production')
|
|
52
47
|
const previewServer = await preview(viteConfig)
|
|
53
48
|
previewServer.printUrls()
|
|
54
49
|
} catch (e) {
|
|
55
|
-
|
|
50
|
+
ui.error('Failed to start preview server:', e)
|
|
56
51
|
process.exit(1)
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
|
-
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createServer } from 'vite'
|
|
2
|
+
import { createViteConfig } from '../index'
|
|
3
|
+
import * as ui from './ui'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Logic for the `boltdocs dev` command.
|
|
7
|
+
* Starts a Vite development server and sets up HMR.
|
|
8
|
+
*
|
|
9
|
+
* @param root - The project root directory
|
|
10
|
+
*/
|
|
11
|
+
export async function devAction(root: string = process.cwd()) {
|
|
12
|
+
try {
|
|
13
|
+
const viteConfig = await createViteConfig(root, 'development')
|
|
14
|
+
const server = await createServer(viteConfig)
|
|
15
|
+
await server.listen()
|
|
16
|
+
server.printUrls()
|
|
17
|
+
server.bindCLIShortcuts({ print: true })
|
|
18
|
+
} catch (e) {
|
|
19
|
+
ui.error('Failed to start dev server:', e)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import fastGlob from 'fast-glob'
|
|
4
|
+
import { resolveConfig } from '../config'
|
|
5
|
+
import { parseFrontmatter, normalizePath } from '../utils'
|
|
6
|
+
import * as ui from './ui'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface representing a documentation hygiene issue.
|
|
10
|
+
*/
|
|
11
|
+
interface Issue {
|
|
12
|
+
level: 'high' | 'warning' | 'low'
|
|
13
|
+
message: string
|
|
14
|
+
suggestion?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Logic for the `boltdocs doctor` command.
|
|
19
|
+
* Scans the documentation directory for broken links, missing frontmatter,
|
|
20
|
+
* and orphaned translations.
|
|
21
|
+
*
|
|
22
|
+
* @param root - The project root directory
|
|
23
|
+
*/
|
|
24
|
+
export async function doctorAction(root: string = process.cwd()) {
|
|
25
|
+
const { colors } = ui
|
|
26
|
+
ui.info(
|
|
27
|
+
`${colors.bold}Running documentation health check...${colors.reset}\n`,
|
|
28
|
+
)
|
|
29
|
+
const start = performance.now()
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const config = await resolveConfig('docs', root)
|
|
33
|
+
const docsDir = path.resolve(root, 'docs')
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(docsDir)) {
|
|
36
|
+
ui.error(`Documentation directory not found at ${docsDir}`)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = await fastGlob(['**/*.md', '**/*.mdx'], {
|
|
41
|
+
cwd: docsDir,
|
|
42
|
+
absolute: true,
|
|
43
|
+
suppressErrors: true,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
let highCount = 0
|
|
47
|
+
let warningCount = 0
|
|
48
|
+
let lowCount = 0
|
|
49
|
+
const issuesMap = new Map<string, Issue[]>()
|
|
50
|
+
|
|
51
|
+
const addIssue = (file: string, issue: Issue) => {
|
|
52
|
+
const relPath = path.relative(docsDir, file)
|
|
53
|
+
let issues = issuesMap.get(relPath)
|
|
54
|
+
if (!issues) {
|
|
55
|
+
issues = []
|
|
56
|
+
issuesMap.set(relPath, issues)
|
|
57
|
+
}
|
|
58
|
+
issues.push(issue)
|
|
59
|
+
if (issue.level === 'high') highCount++
|
|
60
|
+
else if (issue.level === 'warning') warningCount++
|
|
61
|
+
else if (issue.level === 'low') lowCount++
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const basePath = '/docs'
|
|
65
|
+
|
|
66
|
+
// 1. Scan for Frontmatter, Links, and Content Issues
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const { data, content } = parseFrontmatter(file)
|
|
69
|
+
|
|
70
|
+
// Frontmatter Validation
|
|
71
|
+
if (!data.title) {
|
|
72
|
+
addIssue(file, {
|
|
73
|
+
level: 'warning',
|
|
74
|
+
message: 'Missing "title" in frontmatter.',
|
|
75
|
+
suggestion:
|
|
76
|
+
'Add `title: Your Title` to the YAML frontmatter at the top of the file.',
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!data.description) {
|
|
81
|
+
addIssue(file, {
|
|
82
|
+
level: 'low',
|
|
83
|
+
message: 'Missing "description" in frontmatter.',
|
|
84
|
+
suggestion:
|
|
85
|
+
'Adding a description helps with SEO and search previews.',
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Link Validation
|
|
90
|
+
const linkRegex = /\[.*?\]\((.*?)\)/g
|
|
91
|
+
const htmlLinkRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>/g
|
|
92
|
+
const links = [
|
|
93
|
+
...content.matchAll(linkRegex),
|
|
94
|
+
...content.matchAll(htmlLinkRegex),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for (const match of links) {
|
|
98
|
+
let link = match[1]
|
|
99
|
+
if (
|
|
100
|
+
!link ||
|
|
101
|
+
link.startsWith('http') ||
|
|
102
|
+
link.startsWith('https') ||
|
|
103
|
+
link.startsWith('#') ||
|
|
104
|
+
link.startsWith('mailto:') ||
|
|
105
|
+
link.startsWith('tel:')
|
|
106
|
+
) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
link = link.split('#')[0]
|
|
111
|
+
if (!link) continue
|
|
112
|
+
|
|
113
|
+
let targetPath: string
|
|
114
|
+
if (link.startsWith('/')) {
|
|
115
|
+
let pathAfterBase = link
|
|
116
|
+
if (link.startsWith(basePath + '/') || link === basePath) {
|
|
117
|
+
pathAfterBase = link.substring(basePath.length)
|
|
118
|
+
}
|
|
119
|
+
targetPath = path.join(docsDir, pathAfterBase)
|
|
120
|
+
} else {
|
|
121
|
+
targetPath = path.resolve(path.dirname(file), link)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const extensions = ['', '.md', '.mdx', '/index.md', '/index.mdx']
|
|
125
|
+
let exists = false
|
|
126
|
+
for (const ext of extensions) {
|
|
127
|
+
const finalPath = targetPath + ext
|
|
128
|
+
if (fs.existsSync(finalPath) && fs.statSync(finalPath).isFile()) {
|
|
129
|
+
exists = true
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!exists) {
|
|
135
|
+
addIssue(file, {
|
|
136
|
+
level: 'high',
|
|
137
|
+
message: `Broken internal link: "${link}"`,
|
|
138
|
+
suggestion: `Ensure the file exists at "${targetPath}". If it's a directory, ensure it has an "index.md" or "index.mdx".`,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Scan for Orphaned Translations
|
|
145
|
+
if (config.i18n) {
|
|
146
|
+
const { defaultLocale, locales } = config.i18n
|
|
147
|
+
const otherLocales = Object.keys(locales).filter(
|
|
148
|
+
(l) => l !== defaultLocale,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const relPath = normalizePath(path.relative(docsDir, file))
|
|
153
|
+
const parts = relPath.split('/')
|
|
154
|
+
|
|
155
|
+
if (parts[0] === defaultLocale) {
|
|
156
|
+
const pathAfterLocale = parts.slice(1).join('/')
|
|
157
|
+
for (const locale of otherLocales) {
|
|
158
|
+
const localeParts = [locale, ...parts.slice(1)]
|
|
159
|
+
const targetLocaleFile = path.join(docsDir, ...localeParts)
|
|
160
|
+
|
|
161
|
+
if (!fs.existsSync(targetLocaleFile)) {
|
|
162
|
+
addIssue(file, {
|
|
163
|
+
level: 'warning',
|
|
164
|
+
message: `Missing translation for locale "${locale}"`,
|
|
165
|
+
suggestion: `Create a translated version of this file at "${locale}/${pathAfterLocale}".`,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Final Reporting
|
|
174
|
+
if (issuesMap.size === 0) {
|
|
175
|
+
ui.success('All documentation files are healthy!\n')
|
|
176
|
+
} else {
|
|
177
|
+
for (const [file, issues] of issuesMap.entries()) {
|
|
178
|
+
console.log(`📄 ${colors.bold}${file}${colors.reset}`)
|
|
179
|
+
|
|
180
|
+
const sortedIssues = issues.sort((a, b) => {
|
|
181
|
+
const order = { high: 1, warning: 2, low: 3 }
|
|
182
|
+
return order[a.level] - order[b.level]
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
for (const issue of sortedIssues) {
|
|
186
|
+
let prefix = ''
|
|
187
|
+
let color = ''
|
|
188
|
+
if (issue.level === 'high') {
|
|
189
|
+
prefix = '❌'
|
|
190
|
+
color = colors.red
|
|
191
|
+
} else if (issue.level === 'warning') {
|
|
192
|
+
prefix = '⚠️'
|
|
193
|
+
color = colors.yellow
|
|
194
|
+
} else {
|
|
195
|
+
prefix = 'ℹ️'
|
|
196
|
+
color = colors.cyan
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(
|
|
200
|
+
` ${color}${prefix} ${issue.level.toUpperCase()}:${colors.reset} ${issue.message}`,
|
|
201
|
+
)
|
|
202
|
+
if (issue.suggestion) {
|
|
203
|
+
console.log(
|
|
204
|
+
` ${colors.gray}💡 Suggestion: ${issue.suggestion}${colors.reset}`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log('')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`${colors.bold}Summary:${colors.reset}`)
|
|
212
|
+
console.log(
|
|
213
|
+
` ${colors.red}${highCount} high-level errors${colors.reset}`,
|
|
214
|
+
)
|
|
215
|
+
console.log(` ${colors.yellow}${warningCount} warnings${colors.reset}`)
|
|
216
|
+
console.log(
|
|
217
|
+
` ${colors.cyan}${lowCount} minor improvements${colors.reset}\n`,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (highCount > 0) {
|
|
221
|
+
ui.error(
|
|
222
|
+
'HIGH ERROR: Fix these to ensure your documentation builds correctly.',
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
if (warningCount > 0 || lowCount > 0) {
|
|
226
|
+
ui.info(
|
|
227
|
+
'TIP: Address warnings and suggestions for premium quality docs.',
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
console.log('')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const duration = performance.now() - start
|
|
234
|
+
ui.info(`Finished in ${duration.toFixed(2)}ms\n`)
|
|
235
|
+
|
|
236
|
+
if (highCount > 0) {
|
|
237
|
+
process.exit(1)
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {
|
|
240
|
+
ui.error('Failed to run doctor check:', e)
|
|
241
|
+
process.exit(1)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI Escape sequences for terminal coloring and styling.
|
|
3
|
+
* Used to provide a premium and consistent CLI experience.
|
|
4
|
+
*/
|
|
5
|
+
export const colors = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
bold: '\x1b[1m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
yellow: '\x1b[33m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
cyan: '\x1b[36m',
|
|
13
|
+
gray: '\x1b[90m',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Formats a message with the boltdocs prefix and provided styling.
|
|
18
|
+
*
|
|
19
|
+
* @param message - The content to log
|
|
20
|
+
* @param style - Optional ANSI style prefix
|
|
21
|
+
* @returns The formatted string
|
|
22
|
+
*/
|
|
23
|
+
export function formatLog(message: string, style: string = ''): string {
|
|
24
|
+
return `${style}${colors.bold}[boltdocs]${colors.reset} ${message}${colors.reset}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Logs a standard informational message to the console.
|
|
29
|
+
*
|
|
30
|
+
* @param message - The message to display
|
|
31
|
+
*/
|
|
32
|
+
export function info(message: string) {
|
|
33
|
+
console.log(formatLog(message))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Logs an error message to the console with red styling.
|
|
38
|
+
*
|
|
39
|
+
* @param message - The error description
|
|
40
|
+
* @param error - Optional error object for stack tracing
|
|
41
|
+
*/
|
|
42
|
+
export function error(message: string, error?: any) {
|
|
43
|
+
console.error(formatLog(message, colors.red))
|
|
44
|
+
if (error) console.error(error)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Logs a success message to the console with green styling.
|
|
49
|
+
*
|
|
50
|
+
* @param message - The success description
|
|
51
|
+
*/
|
|
52
|
+
export function success(message: string) {
|
|
53
|
+
console.log(formatLog(message, colors.green))
|
|
54
|
+
}
|
package/src/node/cli-entry.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import cac from
|
|
3
|
-
import {
|
|
2
|
+
import cac from 'cac'
|
|
3
|
+
import {
|
|
4
|
+
devAction,
|
|
5
|
+
buildAction,
|
|
6
|
+
previewAction,
|
|
7
|
+
doctorAction,
|
|
8
|
+
} from './cli/index'
|
|
4
9
|
|
|
5
|
-
const cli = cac(
|
|
10
|
+
const cli = cac('boltdocs')
|
|
6
11
|
|
|
7
|
-
cli
|
|
8
|
-
.command("[root]", "Start development server")
|
|
9
|
-
.alias("dev")
|
|
10
|
-
.action(devAction);
|
|
12
|
+
cli.command('[root]', 'Start development server').alias('dev').action(devAction)
|
|
11
13
|
|
|
12
|
-
cli
|
|
13
|
-
.command("build [root]", "Build for production")
|
|
14
|
-
.action(buildAction);
|
|
14
|
+
cli.command('build [root]', 'Build for production').action(buildAction)
|
|
15
15
|
|
|
16
|
-
cli
|
|
17
|
-
.command("preview [root]", "Preview production build")
|
|
18
|
-
.action(previewAction);
|
|
16
|
+
cli.command('preview [root]', 'Preview production build').action(previewAction)
|
|
19
17
|
|
|
20
|
-
cli.
|
|
18
|
+
cli.command('doctor [root]', 'Check documentation health').action(doctorAction)
|
|
19
|
+
|
|
20
|
+
cli.help()
|
|
21
21
|
// This will be replaced at build time or package publishing, but hardcoded to 2.0.0 for now
|
|
22
|
-
cli.version(
|
|
22
|
+
cli.version('2.0.0')
|
|
23
23
|
|
|
24
|
-
cli.parse()
|
|
24
|
+
cli.parse()
|
package/src/node/config.ts
CHANGED
|
@@ -164,7 +164,7 @@ export interface BoltdocsVersionConfig {
|
|
|
164
164
|
export interface BoltdocsVersionsConfig {
|
|
165
165
|
/** The default version path (e.g., 'v2') */
|
|
166
166
|
defaultVersion: string
|
|
167
|
-
/**
|
|
167
|
+
/**
|
|
168
168
|
* Optional prefix for all version paths (e.g., 'v').
|
|
169
169
|
* If set to 'v', version '1.1' will be available at '/docs/v1.1'.
|
|
170
170
|
*/
|
package/src/node/plugin/entry.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function generateEntryCode(
|
|
|
19
19
|
const homeImport = options.homePage
|
|
20
20
|
? `import HomePage from '${normalizePath(options.homePage)}';`
|
|
21
21
|
: ''
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
// Auto-import index.css if it exists
|
|
24
24
|
const cssPath = path.resolve(process.cwd(), 'index.css')
|
|
25
25
|
const cssImport = fs.existsSync(cssPath) ? "import './index.css';" : ''
|
package/src/node/plugin/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { BoltdocsPluginOptions } from './types'
|
|
|
9
9
|
import { generateEntryCode } from './entry'
|
|
10
10
|
import { injectHtmlMeta, getHtmlTemplate } from './html'
|
|
11
11
|
import { generateRobotsTxt } from '../ssg/robots'
|
|
12
|
+
import { generateSearchData } from '../search'
|
|
12
13
|
import fs from 'fs'
|
|
13
14
|
|
|
14
15
|
export * from './types'
|
|
@@ -108,13 +109,12 @@ export function boltdocsPlugin(
|
|
|
108
109
|
|
|
109
110
|
// Improved check: If it's a doc route, serve HTML even if it has a dot (e.g. version 1.1)
|
|
110
111
|
// We only skip if it has a known asset extension to prevent serving HTML for images/js/etc.
|
|
111
|
-
const isAsset =
|
|
112
|
+
const isAsset =
|
|
113
|
+
/\.(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(
|
|
114
|
+
url,
|
|
115
|
+
)
|
|
112
116
|
|
|
113
|
-
if (
|
|
114
|
-
accept.includes('text/html') &&
|
|
115
|
-
!isAsset &&
|
|
116
|
-
isDocRoute
|
|
117
|
-
) {
|
|
117
|
+
if (accept.includes('text/html') && !isAsset && isDocRoute) {
|
|
118
118
|
let html = getHtmlTemplate(config)
|
|
119
119
|
html = injectHtmlMeta(html, config)
|
|
120
120
|
html = await server.transformIndexHtml(req.url || '/', html)
|
|
@@ -197,8 +197,10 @@ export function boltdocsPlugin(
|
|
|
197
197
|
invalidateRouteCache()
|
|
198
198
|
// Re-resolve config as it might affect versions/routes
|
|
199
199
|
config = await resolveConfig(docsDir)
|
|
200
|
-
|
|
201
|
-
const configMod = server.moduleGraph.getModuleById(
|
|
200
|
+
|
|
201
|
+
const configMod = server.moduleGraph.getModuleById(
|
|
202
|
+
'\0virtual:boltdocs-config',
|
|
203
|
+
)
|
|
202
204
|
if (configMod) server.moduleGraph.invalidateModule(configMod)
|
|
203
205
|
|
|
204
206
|
server.ws.send({
|
|
@@ -219,7 +221,12 @@ export function boltdocsPlugin(
|
|
|
219
221
|
// Regenerate and push to client
|
|
220
222
|
// Optimization: generateRoutes is mostly incremental thanks to docCache
|
|
221
223
|
// We only force a full disk scan on add/unlink events
|
|
222
|
-
const newRoutes = await generateRoutes(
|
|
224
|
+
const newRoutes = await generateRoutes(
|
|
225
|
+
docsDir,
|
|
226
|
+
config,
|
|
227
|
+
'/docs',
|
|
228
|
+
type !== 'change',
|
|
229
|
+
)
|
|
223
230
|
|
|
224
231
|
const routesMod = server.moduleGraph.getModuleById(
|
|
225
232
|
'\0virtual:boltdocs-routes',
|
|
@@ -247,7 +254,8 @@ export function boltdocsPlugin(
|
|
|
247
254
|
id === 'virtual:boltdocs-config' ||
|
|
248
255
|
id === 'virtual:boltdocs-entry' ||
|
|
249
256
|
id === 'virtual:boltdocs-mdx-components' ||
|
|
250
|
-
id === 'virtual:boltdocs-layout'
|
|
257
|
+
id === 'virtual:boltdocs-layout' ||
|
|
258
|
+
id === 'virtual:boltdocs-search'
|
|
251
259
|
) {
|
|
252
260
|
return '\0' + id
|
|
253
261
|
}
|
|
@@ -316,6 +324,12 @@ export default UserLayout;`
|
|
|
316
324
|
return `import { DefaultLayout } from 'boltdocs/client';
|
|
317
325
|
export default DefaultLayout;`
|
|
318
326
|
}
|
|
327
|
+
|
|
328
|
+
if (id === '\0virtual:boltdocs-search') {
|
|
329
|
+
const routes = await generateRoutes(docsDir, config)
|
|
330
|
+
const searchData = generateSearchData(routes)
|
|
331
|
+
return `export default ${JSON.stringify(searchData, null, 2)};`
|
|
332
|
+
}
|
|
319
333
|
},
|
|
320
334
|
|
|
321
335
|
transformIndexHtml: {
|
|
@@ -60,14 +60,12 @@ export function parseDocFile(
|
|
|
60
60
|
if (config?.versions && parts.length > 0) {
|
|
61
61
|
const potentialVersion = parts[0]
|
|
62
62
|
const prefix = config.versions.prefix || ''
|
|
63
|
-
|
|
64
|
-
const versionMatch = config.versions.versions.find(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
63
|
+
|
|
64
|
+
const versionMatch = config.versions.versions.find((v) => {
|
|
65
|
+
const fullPath = prefix + v.path
|
|
66
|
+
return potentialVersion === fullPath || potentialVersion === v.path
|
|
67
|
+
})
|
|
68
|
+
|
|
71
69
|
if (versionMatch) {
|
|
72
70
|
version = versionMatch.path
|
|
73
71
|
parts = parts.slice(1)
|
|
@@ -168,7 +166,9 @@ export function parseDocFile(
|
|
|
168
166
|
sanitizedDescription = plainExcerpt
|
|
169
167
|
}
|
|
170
168
|
|
|
171
|
-
const sanitizedBadge = data.badge
|
|
169
|
+
const sanitizedBadge = data.badge
|
|
170
|
+
? sanitizeHtml(String(data.badge))
|
|
171
|
+
: undefined
|
|
172
172
|
const icon = data.icon ? String(data.icon) : undefined
|
|
173
173
|
|
|
174
174
|
// Extract full content as plain text for search indexing
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RouteMeta } from '../routes/types'
|
|
2
|
+
|
|
3
|
+
export interface SearchDocument {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
content: string
|
|
7
|
+
url: string
|
|
8
|
+
display: string
|
|
9
|
+
locale?: string
|
|
10
|
+
version?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates a flat list of searchable documents from the route metadata.
|
|
15
|
+
* Each page is indexed as a primary document, and its sections (headings)
|
|
16
|
+
* are indexed as secondary documents to provide granular search results.
|
|
17
|
+
*/
|
|
18
|
+
export function generateSearchData(routes: RouteMeta[]): SearchDocument[] {
|
|
19
|
+
const documents: SearchDocument[] = []
|
|
20
|
+
|
|
21
|
+
for (const route of routes) {
|
|
22
|
+
// 1. Index the main page
|
|
23
|
+
documents.push({
|
|
24
|
+
id: route.path,
|
|
25
|
+
title: route.title,
|
|
26
|
+
content: route._content || '',
|
|
27
|
+
url: route.path,
|
|
28
|
+
display: route.groupTitle
|
|
29
|
+
? `${route.groupTitle} > ${route.title}`
|
|
30
|
+
: route.title,
|
|
31
|
+
locale: route.locale,
|
|
32
|
+
version: route.version,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// 2. Index headings as sub-documents for deep linking
|
|
36
|
+
if (route.headings) {
|
|
37
|
+
for (const heading of route.headings) {
|
|
38
|
+
// We find the content belonging to this heading?
|
|
39
|
+
// For now, indexing just the heading text and a bit of context is standard.
|
|
40
|
+
// Deep full-text mapping to specific headings is more complex.
|
|
41
|
+
documents.push({
|
|
42
|
+
id: `${route.path}#${heading.id}`,
|
|
43
|
+
title: heading.text,
|
|
44
|
+
content: `${heading.text} in ${route.title}`,
|
|
45
|
+
url: `${route.path}#${heading.id}`,
|
|
46
|
+
display: `${route.title} > ${heading.text}`,
|
|
47
|
+
locale: route.locale,
|
|
48
|
+
version: route.version,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return documents
|
|
55
|
+
}
|
package/src/node/ssg/index.ts
CHANGED
|
@@ -48,14 +48,17 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
48
48
|
__esModule: true,
|
|
49
49
|
default: function SSG_Virtual_Layout(props: any) {
|
|
50
50
|
try {
|
|
51
|
-
const client = originalRequire.apply(this, [
|
|
52
|
-
|
|
51
|
+
const client = originalRequire.apply(this, [
|
|
52
|
+
path.resolve(_dirname, '../client/index.js'),
|
|
53
|
+
])
|
|
54
|
+
const Comp =
|
|
55
|
+
client.DefaultLayout || (({ children }: any) => children)
|
|
53
56
|
const React = originalRequire.apply(this, ['react'])
|
|
54
57
|
return React.createElement(Comp, props)
|
|
55
58
|
} catch (e) {
|
|
56
59
|
return props.children
|
|
57
60
|
}
|
|
58
|
-
}
|
|
61
|
+
},
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
return originalRequire.apply(this, [id, ...args])
|
|
@@ -77,8 +80,10 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
77
80
|
// Generate an HTML file for each route concurrently
|
|
78
81
|
await Promise.all(
|
|
79
82
|
routes.map(async (route) => {
|
|
80
|
-
const siteTitle =
|
|
81
|
-
|
|
83
|
+
const siteTitle =
|
|
84
|
+
getTranslated(config?.theme?.title, route.locale) || 'Boltdocs'
|
|
85
|
+
const siteDescription =
|
|
86
|
+
getTranslated(config?.theme?.description, route.locale) || ''
|
|
82
87
|
const pageTitle = `${route.title} | ${siteTitle}`
|
|
83
88
|
const pageDescription = route.description || siteDescription
|
|
84
89
|
|
|
@@ -111,7 +116,10 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
111
116
|
'utf-8',
|
|
112
117
|
)
|
|
113
118
|
} catch (e: any) {
|
|
114
|
-
console.error(
|
|
119
|
+
console.error(
|
|
120
|
+
`[boltdocs] Error SSR rendering route ${route.path}:`,
|
|
121
|
+
e ? e.stack || e : e,
|
|
122
|
+
)
|
|
115
123
|
}
|
|
116
124
|
}),
|
|
117
125
|
)
|