boltdocs 2.5.4 → 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/boltdocs.js +1 -1
- package/dist/cache-Cr8W2zgZ.cjs +6 -0
- package/dist/cache-DFdakSmR.mjs +6 -0
- package/dist/client/index.d.mts +1276 -861
- package/dist/client/index.d.ts +1276 -861
- package/dist/client/index.js +6 -1
- package/dist/client/index.mjs +6 -1
- package/dist/client/ssr.cjs +6 -0
- package/dist/client/ssr.d.cts +80 -0
- package/dist/client/ssr.d.mts +63 -61
- package/dist/client/ssr.mjs +6 -1
- package/dist/client/theme/neutral.css +388 -0
- package/dist/node/cli-entry.cjs +8 -0
- package/dist/node/cli-entry.d.cts +2 -0
- package/dist/node/cli-entry.d.mts +2 -1
- package/dist/node/cli-entry.mjs +7 -5
- package/dist/node/index.cjs +6 -0
- package/dist/node/index.d.cts +574 -0
- package/dist/node/index.d.mts +385 -378
- package/dist/node/index.mjs +6 -1
- package/dist/node-CWXme96p.mjs +73 -0
- package/dist/node-VYfhzGrh.cjs +73 -0
- package/dist/package-BY8Jd2j4.cjs +6 -0
- package/dist/package-OFZf0s2j.mjs +6 -0
- package/dist/search-dialog-BeNyI_KQ.mjs +6 -0
- package/dist/search-dialog-dYsCAk5S.js +6 -0
- package/dist/use-search-D25n0PrV.mjs +6 -0
- package/dist/use-search-WuzdH1cJ.js +6 -0
- package/package.json +16 -12
- package/src/client/app/index.tsx +15 -12
- package/src/client/components/default-layout.tsx +21 -19
- package/src/client/hooks/use-i18n.ts +1 -1
- package/src/client/hooks/use-routes.ts +1 -1
- package/src/client/hooks/use-version.ts +1 -1
- package/src/client/store/boltdocs-context.tsx +119 -0
- package/CHANGELOG.md +0 -92
- package/dist/cache-3FOEPC2P.mjs +0 -1
- package/dist/chunk-IMEKU5U3.mjs +0 -75
- package/dist/chunk-J2PTDWZM.mjs +0 -1
- package/dist/chunk-TP5KMRD3.mjs +0 -1
- package/dist/chunk-Y4RE5KI7.mjs +0 -1
- package/dist/client/ssr.d.ts +0 -78
- package/dist/client/ssr.js +0 -1
- package/dist/node/cli-entry.d.ts +0 -1
- package/dist/node/cli-entry.js +0 -80
- package/dist/node/index.d.ts +0 -567
- package/dist/node/index.js +0 -75
- package/dist/package-KCTE4HFV.mjs +0 -1
- package/dist/search-dialog-O6VLVSOA.mjs +0 -1
- package/src/client/store/use-boltdocs-store.ts +0 -44
- package/src/node/cache.ts +0 -408
- package/src/node/cli/build.ts +0 -53
- package/src/node/cli/dev.ts +0 -22
- package/src/node/cli/doctor.ts +0 -243
- package/src/node/cli/index.ts +0 -9
- package/src/node/cli/ui.ts +0 -54
- package/src/node/cli-entry.ts +0 -24
- package/src/node/config.ts +0 -382
- package/src/node/errors.ts +0 -44
- package/src/node/index.ts +0 -84
- package/src/node/mdx/cache.ts +0 -12
- package/src/node/mdx/highlighter.ts +0 -47
- package/src/node/mdx/index.ts +0 -122
- package/src/node/mdx/rehype-shiki.ts +0 -62
- package/src/node/mdx/remark-code-meta.ts +0 -35
- package/src/node/mdx/remark-shiki.ts +0 -61
- package/src/node/plugin/entry.ts +0 -87
- package/src/node/plugin/html.ts +0 -99
- package/src/node/plugin/index.ts +0 -464
- package/src/node/plugin/types.ts +0 -9
- package/src/node/plugins/index.ts +0 -17
- package/src/node/plugins/plugin-errors.ts +0 -62
- package/src/node/plugins/plugin-lifecycle.ts +0 -117
- package/src/node/plugins/plugin-sandbox.ts +0 -59
- package/src/node/plugins/plugin-store.ts +0 -54
- package/src/node/plugins/plugin-types.ts +0 -107
- package/src/node/plugins/plugin-validator.ts +0 -105
- package/src/node/routes/cache.ts +0 -28
- package/src/node/routes/index.ts +0 -293
- package/src/node/routes/parser.ts +0 -262
- package/src/node/routes/sorter.ts +0 -42
- package/src/node/routes/types.ts +0 -61
- package/src/node/schema/config.ts +0 -195
- package/src/node/schema/frontmatter.ts +0 -17
- package/src/node/search/index.ts +0 -55
- package/src/node/security/constants/index.ts +0 -10
- package/src/node/security/csp.ts +0 -31
- package/src/node/security/headers.ts +0 -27
- package/src/node/ssg/index.ts +0 -205
- package/src/node/ssg/meta.ts +0 -33
- package/src/node/ssg/options.ts +0 -15
- package/src/node/ssg/robots.ts +0 -53
- package/src/node/ssg/sitemap.ts +0 -55
- package/src/node/utils.ts +0 -349
- package/tsconfig.json +0 -26
- package/tsup.config.ts +0 -56
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import GithubSlugger from 'github-slugger'
|
|
3
|
-
import type { BoltdocsConfig } from '../config'
|
|
4
|
-
import type { ParsedDocFile } from './types'
|
|
5
|
-
import {
|
|
6
|
-
normalizePath,
|
|
7
|
-
parseFrontmatter,
|
|
8
|
-
fileToRoutePath,
|
|
9
|
-
capitalize,
|
|
10
|
-
stripNumberPrefix,
|
|
11
|
-
extractNumberPrefix,
|
|
12
|
-
sanitizeHtml,
|
|
13
|
-
stripHtmlTags,
|
|
14
|
-
logSecurityEvent,
|
|
15
|
-
} from '../utils'
|
|
16
|
-
import { MAX_PATH_LENGTH, ALLOWED_PATH_CHARS } from '../security/constants'
|
|
17
|
-
import { EncodingSecurityError, PathTraversalError } from '../errors'
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Parses a single Markdown/MDX file and extracts its metadata for routing.
|
|
21
|
-
* Checks frontmatter for explicit titles, descriptions, and sidebar positions.
|
|
22
|
-
*
|
|
23
|
-
* Also performs security validation to prevent path traversal and basic
|
|
24
|
-
* XSS sanitization for metadata and headings.
|
|
25
|
-
*
|
|
26
|
-
* @param file - The absolute path to the file
|
|
27
|
-
* @param docsDir - The root documentation directory (e.g., 'docs')
|
|
28
|
-
* @param basePath - The base URL path for the routes (default: '/docs')
|
|
29
|
-
* @param config - The Boltdocs configuration for versions and i18n
|
|
30
|
-
* @returns A parsed structure ready for route assembly and caching
|
|
31
|
-
*/
|
|
32
|
-
export function parseDocFile(
|
|
33
|
-
file: string,
|
|
34
|
-
docsDir: string,
|
|
35
|
-
basePath: string,
|
|
36
|
-
config?: BoltdocsConfig,
|
|
37
|
-
): ParsedDocFile {
|
|
38
|
-
// Security: Prevent path traversal and malicious encoding
|
|
39
|
-
let decodedFile: string
|
|
40
|
-
try {
|
|
41
|
-
decodedFile = decodeURIComponent(file)
|
|
42
|
-
} catch {
|
|
43
|
-
const fileName = path.basename(file)
|
|
44
|
-
logSecurityEvent('ENCODING_ERROR', 'Invalid character encoding', { file: fileName })
|
|
45
|
-
throw new EncodingSecurityError(
|
|
46
|
-
`Security breach: Invalid characters or encoding in path: ${fileName}`,
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Validation: Path length
|
|
51
|
-
if (decodedFile.length > MAX_PATH_LENGTH) {
|
|
52
|
-
const fileName = path.basename(decodedFile)
|
|
53
|
-
logSecurityEvent('PATH_TOO_LONG', 'Path length exceeds limit', {
|
|
54
|
-
length: decodedFile.length,
|
|
55
|
-
file: fileName,
|
|
56
|
-
})
|
|
57
|
-
throw new PathTraversalError(
|
|
58
|
-
`Security breach: Path length exceeds limit of ${MAX_PATH_LENGTH} characters: ${fileName}`,
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const absoluteFile = path.resolve(decodedFile)
|
|
63
|
-
const absoluteDocsDir = path.resolve(docsDir)
|
|
64
|
-
const relativePath = normalizePath(
|
|
65
|
-
path.relative(absoluteDocsDir, absoluteFile),
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
relativePath.startsWith('../') ||
|
|
70
|
-
relativePath === '..' ||
|
|
71
|
-
absoluteFile.includes('\0') ||
|
|
72
|
-
!ALLOWED_PATH_CHARS.test(relativePath)
|
|
73
|
-
) {
|
|
74
|
-
const fileName = path.basename(file)
|
|
75
|
-
logSecurityEvent('PATH_TRAVERSAL_ATTEMPT', 'Path traversal or invalid characters detected', {
|
|
76
|
-
path: relativePath,
|
|
77
|
-
})
|
|
78
|
-
throw new PathTraversalError(
|
|
79
|
-
`Security breach: File is outside of docs directory, contains null bytes, or invalid characters: ${fileName}`,
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const { data, content } = parseFrontmatter(file)
|
|
84
|
-
let parts = relativePath.split('/')
|
|
85
|
-
|
|
86
|
-
let locale: string | undefined
|
|
87
|
-
let version: string | undefined
|
|
88
|
-
|
|
89
|
-
// Level 1: Check for version
|
|
90
|
-
if (config?.versions && parts.length > 0) {
|
|
91
|
-
const potentialVersion = parts[0]
|
|
92
|
-
const prefix = config.versions.prefix || ''
|
|
93
|
-
|
|
94
|
-
const versionMatch = config.versions.versions.find((v) => {
|
|
95
|
-
const fullPath = prefix + v.path
|
|
96
|
-
return potentialVersion === fullPath || potentialVersion === v.path
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
if (versionMatch) {
|
|
100
|
-
version = versionMatch.path
|
|
101
|
-
parts = parts.slice(1)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Level 2: Check for locale
|
|
106
|
-
if (config?.i18n && parts.length > 0) {
|
|
107
|
-
const potentialLocale = parts[0]
|
|
108
|
-
if (config.i18n.locales[potentialLocale]) {
|
|
109
|
-
locale = potentialLocale
|
|
110
|
-
parts = parts.slice(1)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Level 3: Check for Tab hierarchy (name)
|
|
115
|
-
let inferredTab: string | undefined
|
|
116
|
-
if (parts.length > 0) {
|
|
117
|
-
const tabMatch = parts[0].match(/^\((.+)\)$/)
|
|
118
|
-
if (tabMatch) {
|
|
119
|
-
inferredTab = tabMatch[1].toLowerCase()
|
|
120
|
-
parts = parts.slice(1)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const cleanRelativePath = parts.join('/')
|
|
125
|
-
|
|
126
|
-
let cleanRoutePath: string
|
|
127
|
-
if (data.permalink) {
|
|
128
|
-
// If a permalink is specified, ensure it starts with a slash
|
|
129
|
-
cleanRoutePath = data.permalink.startsWith('/')
|
|
130
|
-
? data.permalink
|
|
131
|
-
: `/${data.permalink}`
|
|
132
|
-
} else {
|
|
133
|
-
cleanRoutePath = fileToRoutePath(cleanRelativePath || 'index.md')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let finalPath = basePath
|
|
137
|
-
if (version) {
|
|
138
|
-
finalPath += '/' + version
|
|
139
|
-
}
|
|
140
|
-
if (locale) {
|
|
141
|
-
finalPath += '/' + locale
|
|
142
|
-
}
|
|
143
|
-
if (inferredTab) {
|
|
144
|
-
finalPath += '/' + inferredTab
|
|
145
|
-
}
|
|
146
|
-
finalPath += cleanRoutePath === '/' ? '' : cleanRoutePath
|
|
147
|
-
|
|
148
|
-
if (!finalPath || finalPath === '') finalPath = '/'
|
|
149
|
-
|
|
150
|
-
const rawFileName = parts[parts.length - 1]
|
|
151
|
-
const cleanFileName = stripNumberPrefix(rawFileName)
|
|
152
|
-
const inferredTitle = stripNumberPrefix(
|
|
153
|
-
path.basename(file, path.extname(file)),
|
|
154
|
-
)
|
|
155
|
-
const sidebarPosition =
|
|
156
|
-
data.sidebarPosition ?? extractNumberPrefix(rawFileName)
|
|
157
|
-
|
|
158
|
-
const rawDirName = parts.length >= 2 ? parts[0] : undefined
|
|
159
|
-
const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined
|
|
160
|
-
|
|
161
|
-
const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName)
|
|
162
|
-
|
|
163
|
-
const slugger = new GithubSlugger()
|
|
164
|
-
const headings: { level: number; text: string; id: string }[] = []
|
|
165
|
-
const headingsRegex = /^(#{2,4})\s+(.+)$/gm
|
|
166
|
-
|
|
167
|
-
for (const match of content.matchAll(headingsRegex)) {
|
|
168
|
-
const level = match[1].length
|
|
169
|
-
const rawText = match[2]
|
|
170
|
-
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Strip markdown links
|
|
171
|
-
.replace(/[_*`]/g, '') // Strip markdown formatting
|
|
172
|
-
.trim()
|
|
173
|
-
|
|
174
|
-
const sanitizedText = sanitizeHtml(rawText).trim()
|
|
175
|
-
const id = slugger.slug(sanitizedText)
|
|
176
|
-
|
|
177
|
-
headings.push({ level, text: sanitizedText, id })
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const sanitizedTitle = data.title
|
|
181
|
-
? sanitizeHtml(String(data.title))
|
|
182
|
-
: inferredTitle
|
|
183
|
-
let sanitizedDescription = data.description
|
|
184
|
-
? sanitizeHtml(String(data.description))
|
|
185
|
-
: ''
|
|
186
|
-
|
|
187
|
-
// If no description is provided, extract a summary from the content
|
|
188
|
-
if (!sanitizedDescription && content) {
|
|
189
|
-
const plainExcerpt = stripHtmlTags(
|
|
190
|
-
content
|
|
191
|
-
.replace(/^#+.*$/gm, '') // Remove headers
|
|
192
|
-
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
193
|
-
.replace(/[_*`]/g, '') // Remove formatting
|
|
194
|
-
.replace(/\s+/g, ' '), // Normalize whitespace
|
|
195
|
-
)
|
|
196
|
-
.trim()
|
|
197
|
-
.slice(0, 160)
|
|
198
|
-
|
|
199
|
-
sanitizedDescription = plainExcerpt
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const sanitizedBadge = data.badge
|
|
203
|
-
? sanitizeHtml(String(data.badge))
|
|
204
|
-
: undefined
|
|
205
|
-
const icon = data.icon ? String(data.icon) : undefined
|
|
206
|
-
|
|
207
|
-
// Extract full content as plain text for search indexing
|
|
208
|
-
const plainText = parseContentToPlainText(content)
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
route: {
|
|
212
|
-
path: finalPath,
|
|
213
|
-
componentPath: file,
|
|
214
|
-
filePath: relativePath,
|
|
215
|
-
title: sanitizedTitle,
|
|
216
|
-
description: sanitizedDescription,
|
|
217
|
-
sidebarPosition,
|
|
218
|
-
headings,
|
|
219
|
-
locale,
|
|
220
|
-
version,
|
|
221
|
-
badge: sanitizedBadge,
|
|
222
|
-
icon,
|
|
223
|
-
tab: inferredTab,
|
|
224
|
-
_content: plainText,
|
|
225
|
-
_rawContent: content,
|
|
226
|
-
},
|
|
227
|
-
relativeDir: cleanDirName,
|
|
228
|
-
isGroupIndex,
|
|
229
|
-
inferredTab,
|
|
230
|
-
groupMeta: isGroupIndex
|
|
231
|
-
? {
|
|
232
|
-
title:
|
|
233
|
-
data.groupTitle ||
|
|
234
|
-
data.title ||
|
|
235
|
-
(cleanDirName ? capitalize(cleanDirName) : ''),
|
|
236
|
-
position:
|
|
237
|
-
data.groupPosition ??
|
|
238
|
-
data.sidebarPosition ??
|
|
239
|
-
(rawDirName ? extractNumberPrefix(rawDirName) : undefined),
|
|
240
|
-
icon,
|
|
241
|
-
}
|
|
242
|
-
: undefined,
|
|
243
|
-
inferredGroupPosition: rawDirName
|
|
244
|
-
? extractNumberPrefix(rawDirName)
|
|
245
|
-
: undefined,
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Converts markdown content to plain text for search indexing.
|
|
251
|
-
* Strips headers, links, tags, and formatting.
|
|
252
|
-
*/
|
|
253
|
-
function parseContentToPlainText(content: string): string {
|
|
254
|
-
const plainText = content
|
|
255
|
-
.replace(/^#+.*$/gm, '') // Remove headers
|
|
256
|
-
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
257
|
-
.replace(/\{[^\}]+\}/g, '') // Remove JS expressions/curly braces
|
|
258
|
-
.replace(/[_*`]/g, '') // Remove formatting
|
|
259
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
260
|
-
|
|
261
|
-
return stripHtmlTags(plainText).trim()
|
|
262
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { RouteMeta } from './types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sorts an array of generated routes.
|
|
5
|
-
* Ungrouped items come first. Items within the same group are sorted by position, then alphabetically.
|
|
6
|
-
* Groups are sorted relative to each other by their group position, then alphabetically.
|
|
7
|
-
*
|
|
8
|
-
* @param routes - The unsorted routes
|
|
9
|
-
* @returns A new array of sorted routes suitable for sidebar generation
|
|
10
|
-
*/
|
|
11
|
-
export function sortRoutes(routes: RouteMeta[]): RouteMeta[] {
|
|
12
|
-
return routes.sort((a, b) => {
|
|
13
|
-
// Ungrouped first
|
|
14
|
-
if (!a.group && !b.group) return compareByPosition(a, b)
|
|
15
|
-
if (!a.group) return -1
|
|
16
|
-
if (!b.group) return 1
|
|
17
|
-
|
|
18
|
-
// Different groups: sort by group position
|
|
19
|
-
if (a.group !== b.group) {
|
|
20
|
-
return compareByGroupPosition(a, b)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Same group: sort by item position
|
|
24
|
-
return compareByPosition(a, b)
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function compareByPosition(a: RouteMeta, b: RouteMeta): number {
|
|
29
|
-
if (a.sidebarPosition !== undefined && b.sidebarPosition !== undefined)
|
|
30
|
-
return a.sidebarPosition - b.sidebarPosition
|
|
31
|
-
if (a.sidebarPosition !== undefined) return -1
|
|
32
|
-
if (b.sidebarPosition !== undefined) return 1
|
|
33
|
-
return a.title.localeCompare(b.title)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function compareByGroupPosition(a: RouteMeta, b: RouteMeta): number {
|
|
37
|
-
if (a.groupPosition !== undefined && b.groupPosition !== undefined)
|
|
38
|
-
return a.groupPosition - b.groupPosition
|
|
39
|
-
if (a.groupPosition !== undefined) return -1
|
|
40
|
-
if (b.groupPosition !== undefined) return 1
|
|
41
|
-
return (a.groupTitle || a.group!).localeCompare(b.groupTitle || b.group!)
|
|
42
|
-
}
|
package/src/node/routes/types.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Metadata representing a single documentation route.
|
|
3
|
-
* This information is used to build the client-side router and the sidebar navigation.
|
|
4
|
-
*/
|
|
5
|
-
export interface RouteMeta {
|
|
6
|
-
/** The final URL path for the route (e.g., '/docs/guide/start') */
|
|
7
|
-
path: string
|
|
8
|
-
/** The absolute filesystem path to the source markdown/mdx file */
|
|
9
|
-
componentPath: string
|
|
10
|
-
/** The title of the page, usually extracted from frontmatter or the filename */
|
|
11
|
-
title: string
|
|
12
|
-
/** The relative path from the docs directory, used for edit links */
|
|
13
|
-
filePath: string
|
|
14
|
-
/** Optional description of the page (for SEO/meta tags) */
|
|
15
|
-
description?: string
|
|
16
|
-
/** Optional explicit position for ordering in the sidebar */
|
|
17
|
-
sidebarPosition?: number
|
|
18
|
-
/** The group (directory) this route belongs to */
|
|
19
|
-
group?: string
|
|
20
|
-
/** The display title for the route's group */
|
|
21
|
-
groupTitle?: string
|
|
22
|
-
/** Optional explicit position for ordering the group itself */
|
|
23
|
-
groupPosition?: number
|
|
24
|
-
/** Optional icon for the route's group */
|
|
25
|
-
groupIcon?: string
|
|
26
|
-
/** Extracted markdown headings for search indexing */
|
|
27
|
-
headings?: { level: number; text: string; id: string }[]
|
|
28
|
-
/** The locale this route belongs to, if i18n is configured */
|
|
29
|
-
locale?: string
|
|
30
|
-
/** The version this route belongs to, if versioning is configured */
|
|
31
|
-
version?: string
|
|
32
|
-
/** Optional badge to display next to the sidebar item (e.g., 'New', 'Experimental') */
|
|
33
|
-
badge?: string | { text: string; expires?: string }
|
|
34
|
-
/** Optional icon to display (Lucide icon name or raw SVG) */
|
|
35
|
-
icon?: string
|
|
36
|
-
/** The tab this route belongs to, if tabs are configured */
|
|
37
|
-
tab?: string
|
|
38
|
-
/** The extracted plain-text content of the page for search indexing */
|
|
39
|
-
_content?: string
|
|
40
|
-
/** The raw markdown content of the page */
|
|
41
|
-
_rawContent?: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Internal representation of a parsed documentation file before finalizing groups.
|
|
46
|
-
* Stored in the file cache to avoid re-parsing unchanged files.
|
|
47
|
-
*/
|
|
48
|
-
export interface ParsedDocFile {
|
|
49
|
-
/** The core route metadata without group-level details (inferred later) */
|
|
50
|
-
route: Omit<RouteMeta, 'group' | 'groupTitle' | 'groupPosition'>
|
|
51
|
-
/** The base directory of the file (used to group files together) */
|
|
52
|
-
relativeDir?: string
|
|
53
|
-
/** Whether this file is the index file for its directory group */
|
|
54
|
-
isGroupIndex: boolean
|
|
55
|
-
/** If this is a group index, any specific frontmatter metadata dictating the group's title and position */
|
|
56
|
-
groupMeta?: { title: string; position?: number; icon?: string }
|
|
57
|
-
/** Extracted group position from the directory name if it has a numeric prefix */
|
|
58
|
-
inferredGroupPosition?: number
|
|
59
|
-
/** Extracted tab name from the directory name if it follows the (tab-name) syntax */
|
|
60
|
-
inferredTab?: string
|
|
61
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Zod schema for a single social link.
|
|
5
|
-
*/
|
|
6
|
-
export const SocialLinkSchema = z.object({
|
|
7
|
-
icon: z.string().max(50),
|
|
8
|
-
link: z.string().url(),
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Zod schema for footer configuration.
|
|
13
|
-
*/
|
|
14
|
-
export const FooterConfigSchema = z.object({
|
|
15
|
-
text: z.string().max(2000).optional(),
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Zod schema for plugin permissions.
|
|
20
|
-
*/
|
|
21
|
-
export const PluginPermissionSchema = z.enum([
|
|
22
|
-
'fs:read',
|
|
23
|
-
'fs:write',
|
|
24
|
-
'vite:config',
|
|
25
|
-
'mdx:remark',
|
|
26
|
-
'mdx:rehype',
|
|
27
|
-
'components',
|
|
28
|
-
'hooks:build',
|
|
29
|
-
'hooks:dev',
|
|
30
|
-
])
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Zod schema for a Boltdocs plugin.
|
|
34
|
-
*/
|
|
35
|
-
export const BoltdocsPluginSchema = z.object({
|
|
36
|
-
name: z.string(),
|
|
37
|
-
enforce: z.enum(['pre', 'post']).optional(),
|
|
38
|
-
version: z.string().optional(),
|
|
39
|
-
boltdocsVersion: z.string().optional(),
|
|
40
|
-
permissions: z.array(PluginPermissionSchema).optional(),
|
|
41
|
-
remarkPlugins: z.array(z.any()).optional(),
|
|
42
|
-
rehypePlugins: z.array(z.any()).optional(),
|
|
43
|
-
vitePlugins: z.array(z.any()).optional(),
|
|
44
|
-
components: z.record(z.string(), z.string()).optional(),
|
|
45
|
-
hooks: z.record(z.string(), z.any()).optional(),
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Zod schema for theme configuration.
|
|
50
|
-
*/
|
|
51
|
-
export const ThemeConfigSchema = z.object({
|
|
52
|
-
title: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
|
|
53
|
-
description: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
|
|
54
|
-
logo: z
|
|
55
|
-
.union([
|
|
56
|
-
z.string(),
|
|
57
|
-
z.object({
|
|
58
|
-
dark: z.string(),
|
|
59
|
-
light: z.string(),
|
|
60
|
-
alt: z.string().optional(),
|
|
61
|
-
width: z.number().optional(),
|
|
62
|
-
height: z.number().optional(),
|
|
63
|
-
}),
|
|
64
|
-
])
|
|
65
|
-
.optional(),
|
|
66
|
-
navbar: z
|
|
67
|
-
.array(
|
|
68
|
-
z.object({
|
|
69
|
-
label: z.union([z.string(), z.record(z.string(), z.string())]),
|
|
70
|
-
href: z.string(),
|
|
71
|
-
}),
|
|
72
|
-
)
|
|
73
|
-
.optional(),
|
|
74
|
-
sidebar: z
|
|
75
|
-
.record(
|
|
76
|
-
z.string(),
|
|
77
|
-
z.array(
|
|
78
|
-
z.object({
|
|
79
|
-
text: z.string(),
|
|
80
|
-
link: z.string(),
|
|
81
|
-
}),
|
|
82
|
-
),
|
|
83
|
-
)
|
|
84
|
-
.optional(),
|
|
85
|
-
socialLinks: z.array(SocialLinkSchema).optional(),
|
|
86
|
-
footer: FooterConfigSchema.optional(),
|
|
87
|
-
breadcrumbs: z.boolean().optional(),
|
|
88
|
-
editLink: z.string().refine(val => !val || val.includes(':path'), {
|
|
89
|
-
message: "editLink must contain ':path' placeholder if specified"
|
|
90
|
-
}).optional(),
|
|
91
|
-
communityHelp: z.string().url().optional(),
|
|
92
|
-
version: z.string().max(50).optional(),
|
|
93
|
-
githubRepo: z.string().max(100).optional(),
|
|
94
|
-
favicon: z.string().optional(),
|
|
95
|
-
ogImage: z.string().optional(),
|
|
96
|
-
poweredBy: z.boolean().optional(),
|
|
97
|
-
tabs: z
|
|
98
|
-
.array(
|
|
99
|
-
z.object({
|
|
100
|
-
id: z.string(),
|
|
101
|
-
text: z.union([z.string(), z.record(z.string(), z.string())]),
|
|
102
|
-
icon: z.string().optional(),
|
|
103
|
-
}),
|
|
104
|
-
)
|
|
105
|
-
.optional(),
|
|
106
|
-
codeTheme: z
|
|
107
|
-
.union([z.string(), z.object({ light: z.string(), dark: z.string() })])
|
|
108
|
-
.optional(),
|
|
109
|
-
copyMarkdown: z
|
|
110
|
-
.union([
|
|
111
|
-
z.boolean(),
|
|
112
|
-
z.object({
|
|
113
|
-
text: z.string().optional(),
|
|
114
|
-
icon: z.string().optional(),
|
|
115
|
-
}),
|
|
116
|
-
])
|
|
117
|
-
.optional(),
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Zod schema for robots.txt configuration.
|
|
122
|
-
*/
|
|
123
|
-
export const RobotsConfigSchema = z.union([
|
|
124
|
-
z.string(),
|
|
125
|
-
z.object({
|
|
126
|
-
rules: z
|
|
127
|
-
.array(
|
|
128
|
-
z.object({
|
|
129
|
-
userAgent: z.string(),
|
|
130
|
-
allow: z.union([z.string(), z.array(z.string())]).optional(),
|
|
131
|
-
disallow: z.union([z.string(), z.array(z.string())]).optional(),
|
|
132
|
-
}),
|
|
133
|
-
)
|
|
134
|
-
.optional(),
|
|
135
|
-
sitemaps: z.array(z.string().url()).optional(),
|
|
136
|
-
}),
|
|
137
|
-
])
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Zod schema for internationalization configuration.
|
|
141
|
-
*/
|
|
142
|
-
export const I18nConfigSchema = z.object({
|
|
143
|
-
defaultLocale: z.string(),
|
|
144
|
-
locales: z.record(z.string(), z.string()),
|
|
145
|
-
localeConfigs: z
|
|
146
|
-
.record(
|
|
147
|
-
z.string(),
|
|
148
|
-
z.object({
|
|
149
|
-
label: z.string().optional(),
|
|
150
|
-
direction: z.enum(['ltr', 'rtl']).optional(),
|
|
151
|
-
htmlLang: z.string().optional(),
|
|
152
|
-
calendar: z.string().optional(),
|
|
153
|
-
}),
|
|
154
|
-
)
|
|
155
|
-
.optional(),
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Zod schema for versioning configuration.
|
|
160
|
-
*/
|
|
161
|
-
export const VersionsConfigSchema = z.object({
|
|
162
|
-
defaultVersion: z.string(),
|
|
163
|
-
prefix: z.string().optional(),
|
|
164
|
-
versions: z.array(
|
|
165
|
-
z.object({
|
|
166
|
-
label: z.string(),
|
|
167
|
-
path: z.string(),
|
|
168
|
-
}),
|
|
169
|
-
),
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Zod schema for security configuration.
|
|
174
|
-
*/
|
|
175
|
-
export const SecurityConfigSchema = z.object({
|
|
176
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
177
|
-
enableCSP: z.boolean().optional(),
|
|
178
|
-
customHeaders: z.record(z.string(), z.string()).optional(),
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Root Zod schema for Boltdocs project configuration.
|
|
183
|
-
*/
|
|
184
|
-
export const BoltdocsConfigSchema = z.object({
|
|
185
|
-
siteUrl: z.string().url().optional(),
|
|
186
|
-
docsDir: z.string().optional(),
|
|
187
|
-
homePage: z.string().optional(),
|
|
188
|
-
theme: ThemeConfigSchema.optional(),
|
|
189
|
-
i18n: I18nConfigSchema.optional(),
|
|
190
|
-
versions: VersionsConfigSchema.optional(),
|
|
191
|
-
plugins: z.array(BoltdocsPluginSchema).optional(),
|
|
192
|
-
robots: RobotsConfigSchema.optional(),
|
|
193
|
-
security: SecurityConfigSchema.optional(),
|
|
194
|
-
vite: z.record(z.string(), z.unknown()).optional(),
|
|
195
|
-
})
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Schema for strict frontmatter validation.
|
|
5
|
-
*/
|
|
6
|
-
export const FrontmatterSchema = z.object({
|
|
7
|
-
title: z.string().max(200).optional(),
|
|
8
|
-
description: z.string().max(500).optional(),
|
|
9
|
-
sidebarPosition: z.number().optional(),
|
|
10
|
-
sidebarLabel: z.string().max(100).optional(),
|
|
11
|
-
category: z.string().max(50).optional(),
|
|
12
|
-
order: z.number().optional(),
|
|
13
|
-
badge: z.string().max(50).optional(),
|
|
14
|
-
icon: z.string().max(50).optional(),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
export type FrontmatterData = z.infer<typeof FrontmatterSchema>
|
package/src/node/search/index.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security limits for file system operations.
|
|
3
|
-
*/
|
|
4
|
-
export const MAX_PATH_LENGTH = 260
|
|
5
|
-
export const ALLOWED_PATH_CHARS = /^[a-zA-Z0-9\-_\/\.\(\)]+$/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Security limits for document metadata (frontmatter).
|
|
9
|
-
*/
|
|
10
|
-
export const MAX_FRONTMATTER_SIZE = 10 * 1024 // 10KB
|
package/src/node/security/csp.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { BoltdocsConfig } from '../config'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generates a Content Security Policy (CSP) header string based on the configuration.
|
|
5
|
-
* Automatically adapts to the environment (development vs production).
|
|
6
|
-
*
|
|
7
|
-
* @param config - The Boltdocs configuration object.
|
|
8
|
-
* @returns The CSP header value.
|
|
9
|
-
*/
|
|
10
|
-
export function getCSPHeader(_config: BoltdocsConfig): string {
|
|
11
|
-
const isDev = process.env.NODE_ENV === 'development'
|
|
12
|
-
|
|
13
|
-
const directives: Record<string, string[]> = {
|
|
14
|
-
'default-src': ["'self'"],
|
|
15
|
-
'script-src': ["'self'", "'unsafe-inline'"],
|
|
16
|
-
'style-src': ["'self'", "'unsafe-inline'"],
|
|
17
|
-
'img-src': ["'self'", "data:", "https:"],
|
|
18
|
-
'font-src': ["'self'"],
|
|
19
|
-
'connect-src': ["'self'"],
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Relax policies in development to support Vite's HMR (eval-based)
|
|
23
|
-
if (isDev) {
|
|
24
|
-
directives['script-src'] = ["'self'", "'unsafe-eval'", "'unsafe-inline'"]
|
|
25
|
-
directives['style-src'] = ["'self'", "'unsafe-inline'"]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return Object.entries(directives)
|
|
29
|
-
.map(([key, values]) => `${key} ${values.join(' ')}`)
|
|
30
|
-
.join('; ')
|
|
31
|
-
}
|