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.
Files changed (96) hide show
  1. package/bin/boltdocs.js +1 -1
  2. package/dist/cache-Cr8W2zgZ.cjs +6 -0
  3. package/dist/cache-DFdakSmR.mjs +6 -0
  4. package/dist/client/index.d.mts +1276 -861
  5. package/dist/client/index.d.ts +1276 -861
  6. package/dist/client/index.js +6 -1
  7. package/dist/client/index.mjs +6 -1
  8. package/dist/client/ssr.cjs +6 -0
  9. package/dist/client/ssr.d.cts +80 -0
  10. package/dist/client/ssr.d.mts +63 -61
  11. package/dist/client/ssr.mjs +6 -1
  12. package/dist/client/theme/neutral.css +388 -0
  13. package/dist/node/cli-entry.cjs +8 -0
  14. package/dist/node/cli-entry.d.cts +2 -0
  15. package/dist/node/cli-entry.d.mts +2 -1
  16. package/dist/node/cli-entry.mjs +7 -5
  17. package/dist/node/index.cjs +6 -0
  18. package/dist/node/index.d.cts +574 -0
  19. package/dist/node/index.d.mts +385 -378
  20. package/dist/node/index.mjs +6 -1
  21. package/dist/node-CWXme96p.mjs +73 -0
  22. package/dist/node-VYfhzGrh.cjs +73 -0
  23. package/dist/package-BY8Jd2j4.cjs +6 -0
  24. package/dist/package-OFZf0s2j.mjs +6 -0
  25. package/dist/search-dialog-BeNyI_KQ.mjs +6 -0
  26. package/dist/search-dialog-dYsCAk5S.js +6 -0
  27. package/dist/use-search-D25n0PrV.mjs +6 -0
  28. package/dist/use-search-WuzdH1cJ.js +6 -0
  29. package/package.json +16 -12
  30. package/src/client/app/index.tsx +15 -12
  31. package/src/client/components/default-layout.tsx +21 -19
  32. package/src/client/hooks/use-i18n.ts +1 -1
  33. package/src/client/hooks/use-routes.ts +1 -1
  34. package/src/client/hooks/use-version.ts +1 -1
  35. package/src/client/store/boltdocs-context.tsx +119 -0
  36. package/CHANGELOG.md +0 -92
  37. package/dist/cache-3FOEPC2P.mjs +0 -1
  38. package/dist/chunk-IMEKU5U3.mjs +0 -75
  39. package/dist/chunk-J2PTDWZM.mjs +0 -1
  40. package/dist/chunk-TP5KMRD3.mjs +0 -1
  41. package/dist/chunk-Y4RE5KI7.mjs +0 -1
  42. package/dist/client/ssr.d.ts +0 -78
  43. package/dist/client/ssr.js +0 -1
  44. package/dist/node/cli-entry.d.ts +0 -1
  45. package/dist/node/cli-entry.js +0 -80
  46. package/dist/node/index.d.ts +0 -567
  47. package/dist/node/index.js +0 -75
  48. package/dist/package-KCTE4HFV.mjs +0 -1
  49. package/dist/search-dialog-O6VLVSOA.mjs +0 -1
  50. package/src/client/store/use-boltdocs-store.ts +0 -44
  51. package/src/node/cache.ts +0 -408
  52. package/src/node/cli/build.ts +0 -53
  53. package/src/node/cli/dev.ts +0 -22
  54. package/src/node/cli/doctor.ts +0 -243
  55. package/src/node/cli/index.ts +0 -9
  56. package/src/node/cli/ui.ts +0 -54
  57. package/src/node/cli-entry.ts +0 -24
  58. package/src/node/config.ts +0 -382
  59. package/src/node/errors.ts +0 -44
  60. package/src/node/index.ts +0 -84
  61. package/src/node/mdx/cache.ts +0 -12
  62. package/src/node/mdx/highlighter.ts +0 -47
  63. package/src/node/mdx/index.ts +0 -122
  64. package/src/node/mdx/rehype-shiki.ts +0 -62
  65. package/src/node/mdx/remark-code-meta.ts +0 -35
  66. package/src/node/mdx/remark-shiki.ts +0 -61
  67. package/src/node/plugin/entry.ts +0 -87
  68. package/src/node/plugin/html.ts +0 -99
  69. package/src/node/plugin/index.ts +0 -464
  70. package/src/node/plugin/types.ts +0 -9
  71. package/src/node/plugins/index.ts +0 -17
  72. package/src/node/plugins/plugin-errors.ts +0 -62
  73. package/src/node/plugins/plugin-lifecycle.ts +0 -117
  74. package/src/node/plugins/plugin-sandbox.ts +0 -59
  75. package/src/node/plugins/plugin-store.ts +0 -54
  76. package/src/node/plugins/plugin-types.ts +0 -107
  77. package/src/node/plugins/plugin-validator.ts +0 -105
  78. package/src/node/routes/cache.ts +0 -28
  79. package/src/node/routes/index.ts +0 -293
  80. package/src/node/routes/parser.ts +0 -262
  81. package/src/node/routes/sorter.ts +0 -42
  82. package/src/node/routes/types.ts +0 -61
  83. package/src/node/schema/config.ts +0 -195
  84. package/src/node/schema/frontmatter.ts +0 -17
  85. package/src/node/search/index.ts +0 -55
  86. package/src/node/security/constants/index.ts +0 -10
  87. package/src/node/security/csp.ts +0 -31
  88. package/src/node/security/headers.ts +0 -27
  89. package/src/node/ssg/index.ts +0 -205
  90. package/src/node/ssg/meta.ts +0 -33
  91. package/src/node/ssg/options.ts +0 -15
  92. package/src/node/ssg/robots.ts +0 -53
  93. package/src/node/ssg/sitemap.ts +0 -55
  94. package/src/node/utils.ts +0 -349
  95. package/tsconfig.json +0 -26
  96. package/tsup.config.ts +0 -56
@@ -1,464 +0,0 @@
1
- import { type Plugin, type ResolvedConfig, loadEnv } from 'vite'
2
- import { generateRoutes, invalidateRouteCache, invalidateFile } from '../routes'
3
- import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
4
- import { resolveConfig, type BoltdocsConfig, CONFIG_FILES } from '../config'
5
- import { generateStaticPages } from '../ssg'
6
- import { normalizePath, isDocFile } from '../utils'
7
- import path from 'path'
8
- import type { BoltdocsPluginOptions } from './types'
9
- import { generateEntryCode } from './entry'
10
- import { injectHtmlMeta, getHtmlTemplate } from './html'
11
- import { generateRobotsTxt } from '../ssg/robots'
12
- import { generateSearchData } from '../search'
13
- import { SECURITY_HEADERS } from '../security/headers'
14
- import { getCSPHeader } from '../security/csp'
15
- import fs from 'fs'
16
- import {
17
- PluginLifecycleManager,
18
- validatePlugins,
19
- PluginSandbox,
20
- type SecureBoltdocsPlugin,
21
- } from '../plugins'
22
-
23
- export * from './types'
24
-
25
- /**
26
- * The core Boltdocs Vite plugin.
27
- * Handles virtual module resolution, HMR for documentation files,
28
- * injecting HTML meta tags for SEO, and triggering the SSG process on build.
29
- *
30
- * @param options - Optional configuration for the plugin
31
- * @param passedConfig - Pre-resolved configuration (internal use)
32
- * @returns An array of Vite plugins
33
- */
34
- export function boltdocsPlugin(
35
- options: BoltdocsPluginOptions = {},
36
- passedConfig?: BoltdocsConfig,
37
- ): Plugin[] {
38
- const docsDir = path.resolve(process.cwd(), options.docsDir || 'docs')
39
- const normalizedDocsDir = normalizePath(docsDir)
40
- let config: BoltdocsConfig = passedConfig!
41
- let viteConfig: ResolvedConfig
42
- let isBuild = false
43
- let lifecycle: PluginLifecycleManager
44
-
45
- // Use a placeholder for extra plugins that will be populated once config is resolved
46
- let resolvedExtraVitePlugins: Plugin[] = []
47
-
48
- return [
49
- {
50
- name: 'vite-plugin-boltdocs',
51
- enforce: 'pre',
52
-
53
- async config(userConfig, env) {
54
- isBuild = env.command === 'build'
55
-
56
- // Load env variables and inject into process.env so they are available in boltdocs.config.js
57
- const envDir = userConfig.envDir || process.cwd()
58
- const envs = loadEnv(env.mode, envDir, '')
59
- Object.assign(process.env, envs)
60
-
61
- // Resolve config async if not already passed
62
- if (!config) {
63
- config = await resolveConfig(docsDir)
64
- }
65
-
66
- // --- NEW: Secure Plugin Initialization ---
67
- const boltdocsVersion = (await import('../../../package.json')).version
68
- const validatedPlugins = validatePlugins(
69
- (config.plugins || []) as SecureBoltdocsPlugin[],
70
- boltdocsVersion,
71
- )
72
-
73
- // Replace config plugins with validated ones
74
- config.plugins = validatedPlugins as any
75
-
76
- // Initialize lifecycle manager
77
- lifecycle = new PluginLifecycleManager(validatedPlugins, config)
78
-
79
- // Populate validated extra Vite plugins
80
- resolvedExtraVitePlugins = validatedPlugins.flatMap((p) => {
81
- const caps = PluginSandbox.getSanitizedCapabilities(p)
82
- return (caps.vitePlugins || []) as Plugin[]
83
- })
84
-
85
- // Run beforeBuild if building
86
- if (isBuild) {
87
- await lifecycle.runHook('beforeBuild')
88
- }
89
-
90
- return {
91
- optimizeDeps: {
92
- include: [
93
- 'react',
94
- 'react-dom',
95
- 'use-sync-external-store/shim',
96
- 'use-sync-external-store/shim/index.js',
97
- 'use-sync-external-store/with-selector',
98
- 'use-sync-external-store'
99
- ],
100
- exclude: [
101
- 'boltdocs',
102
- 'boltdocs/client',
103
- 'boltdocs/hooks',
104
- 'boltdocs/primitives',
105
- 'boltdocs/base-ui',
106
- 'boltdocs/mdx',
107
- 'boltdocs/integrations',
108
- 'boltdocs/client/hooks',
109
- 'boltdocs/client/primitives',
110
- ],
111
- },
112
- }
113
- },
114
-
115
- configResolved(resolved) {
116
- viteConfig = resolved
117
- lifecycle?.runHook('configResolved', config)
118
- },
119
-
120
- async configureServer(server) {
121
- await lifecycle?.runHook('beforeDev')
122
-
123
- // Security: Apply hardened headers and CSP
124
- server.middlewares.use((_req, res, next) => {
125
- const isProd = process.env.NODE_ENV === 'production'
126
-
127
- if (isProd) {
128
- Object.entries(SECURITY_HEADERS).forEach(([header, value]) => {
129
- res.setHeader(header, value)
130
- })
131
- }
132
-
133
- // Inject CSP if enabled in configuration
134
- if (config.security?.enableCSP) {
135
- res.setHeader('Content-Security-Policy', getCSPHeader(config))
136
- }
137
-
138
- next()
139
- })
140
-
141
- // Serve robots.txt from config
142
- server.middlewares.use((req, res, next) => {
143
- if (req.url === '/robots.txt') {
144
- const robots = generateRobotsTxt(config)
145
- res.statusCode = 200
146
- res.setHeader('Content-Type', 'text/plain')
147
- res.end(robots)
148
- return
149
- }
150
- next()
151
- })
152
-
153
- // Serve default HTML for documentation routes or if index.html is missing
154
- server.middlewares.use(async (req, res, next) => {
155
- const url = req.url?.split('?')[0] || '/'
156
- const accept = req.headers.accept || ''
157
-
158
- const isDocRoute =
159
- url === '/' ||
160
- url.startsWith('/docs') ||
161
- (config.i18n &&
162
- Object.keys(config.i18n.locales).some(
163
- (locale) =>
164
- url.startsWith(`/${locale}/docs`) || url === `/${locale}`,
165
- )) ||
166
- // Handle any HTML request that isn't a known static file or docs,
167
- // as it potentially could be an external page.
168
- // (The client-side router will handle 404s if it doesn't match anything)
169
- true
170
-
171
- // Improved check: If it's a doc route, serve HTML even if it has a dot (e.g. version 1.1)
172
- // We only skip if it has a known asset extension to prevent serving HTML for images/js/etc.
173
- const isAsset =
174
- /\.(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(
175
- url,
176
- )
177
-
178
- if (accept.includes('text/html') && !isAsset && isDocRoute) {
179
- let html = getHtmlTemplate(config)
180
- html = injectHtmlMeta(html, config)
181
- html = await server.transformIndexHtml(req.url || '/', html)
182
- res.statusCode = 200
183
- res.setHeader('Content-Type', 'text/html')
184
- res.end(html)
185
- return
186
- }
187
-
188
- next()
189
- })
190
-
191
- // Explicitly watch config files...
192
-
193
- const configPaths = CONFIG_FILES.map((c) =>
194
- path.resolve(process.cwd(), c),
195
- )
196
- const compExtensions = ['tsx', 'jsx']
197
- const layoutCompPaths = compExtensions.map((ext) =>
198
- path.resolve(docsDir, `layout.${ext}`),
199
- )
200
- const mdxCompExtensions = ['tsx', 'ts', 'jsx', 'js']
201
- const mdxCompPaths = mdxCompExtensions.map((ext) =>
202
- path.resolve(docsDir, `mdx-components.${ext}`),
203
- )
204
- const extPagesPaths = mdxCompExtensions.map((ext) =>
205
- path.resolve(docsDir, `pages-external/index.${ext}`),
206
- )
207
-
208
- server.watcher.add([
209
- ...configPaths,
210
- ...mdxCompPaths,
211
- ...layoutCompPaths,
212
- ...extPagesPaths,
213
- ])
214
-
215
- const handleFileEvent = async (
216
- file: string,
217
- type: 'add' | 'unlink' | 'change',
218
- ) => {
219
- try {
220
- const normalized = normalizePath(file)
221
-
222
- // Restart the Vite server if the Boltdocs config changes
223
- if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
224
- server.restart()
225
- return
226
- }
227
-
228
- // If mdx-components file changes, invalidate the virtual module
229
- if (
230
- mdxCompExtensions.some((ext) =>
231
- normalized.endsWith(`mdx-components.${ext}`),
232
- )
233
- ) {
234
- const mod = server.moduleGraph.getModuleById(
235
- '\0virtual:boltdocs-mdx-components',
236
- )
237
- if (mod) server.moduleGraph.invalidateModule(mod)
238
- server.ws.send({ type: 'full-reload' })
239
- return
240
- }
241
-
242
- // If layout.tsx/jsx file changes, invalidate the virtual module
243
- if (
244
- compExtensions.some((ext) => normalized.endsWith(`layout.${ext}`))
245
- ) {
246
- const mod = server.moduleGraph.getModuleById(
247
- '\0virtual:boltdocs-layout',
248
- )
249
- if (mod) server.moduleGraph.invalidateModule(mod)
250
- server.ws.send({ type: 'full-reload' })
251
- return
252
- }
253
-
254
- // If any pages-external file changes, invalidate the entry module
255
- if (
256
- normalized.includes('/pages-external/') ||
257
- normalized.includes('\\pages-external\\')
258
- ) {
259
- const mod = server.moduleGraph.getModuleById(
260
- '\0virtual:boltdocs-entry',
261
- )
262
- if (mod) server.moduleGraph.invalidateModule(mod)
263
- server.ws.send({ type: 'full-reload' })
264
- return
265
- }
266
-
267
- if (
268
- !normalized.startsWith(normalizedDocsDir) ||
269
- !isDocFile(normalized)
270
- )
271
- return
272
-
273
- // Invalidate appropriately
274
- if (type === 'add' || type === 'unlink') {
275
- invalidateRouteCache()
276
- // Re-resolve config as it might affect versions/routes
277
- config = await resolveConfig(docsDir)
278
-
279
- const configMod = server.moduleGraph.getModuleById(
280
- '\0virtual:boltdocs-config',
281
- )
282
- if (configMod) server.moduleGraph.invalidateModule(configMod)
283
-
284
- server.ws.send({
285
- type: 'custom',
286
- event: 'boltdocs:config-update',
287
- data: {
288
- theme: config?.theme,
289
- i18n: config?.i18n,
290
- versions: config?.versions,
291
- siteUrl: config?.siteUrl,
292
- },
293
- })
294
- } else {
295
- invalidateFile(file)
296
- }
297
-
298
- // Regenerate and push to client
299
- // Optimization: generateRoutes is mostly incremental thanks to docCache
300
- // We only force a full disk scan on add/unlink events
301
- const newRoutes = await generateRoutes(
302
- docsDir,
303
- config,
304
- '/docs',
305
- type !== 'change',
306
- )
307
-
308
- const routesMod = server.moduleGraph.getModuleById(
309
- '\0virtual:boltdocs-routes',
310
- )
311
- if (routesMod) server.moduleGraph.invalidateModule(routesMod)
312
-
313
- server.ws.send({
314
- type: 'custom',
315
- event: 'boltdocs:routes-update',
316
- data: newRoutes,
317
- })
318
- } catch (e) {
319
- console.error(`[boltdocs] HMR error during ${type} event:`, e)
320
- }
321
- }
322
-
323
- server.watcher.on('add', (f) => handleFileEvent(f, 'add'))
324
- server.watcher.on('unlink', (f) => handleFileEvent(f, 'unlink'))
325
- server.watcher.on('change', (f) => handleFileEvent(f, 'change'))
326
-
327
- await lifecycle?.runHook('afterDev')
328
- },
329
-
330
- resolveId(id) {
331
- if (
332
- id === 'virtual:boltdocs-routes' ||
333
- id === 'virtual:boltdocs-config' ||
334
- id === 'virtual:boltdocs-entry' ||
335
- id === 'virtual:boltdocs-mdx-components' ||
336
- id === 'virtual:boltdocs-layout' ||
337
- id === 'virtual:boltdocs-search'
338
- ) {
339
- return '\0' + id
340
- }
341
- },
342
-
343
- async load(id) {
344
- if (id === '\0virtual:boltdocs-routes') {
345
- const routes = await generateRoutes(docsDir, config)
346
- return `export default ${JSON.stringify(routes, null, 2)};`
347
- }
348
- if (id === '\0virtual:boltdocs-config') {
349
- const clientConfig = {
350
- theme: config?.theme,
351
- i18n: config?.i18n,
352
- versions: config?.versions,
353
- siteUrl: config?.siteUrl,
354
- plugins: config?.plugins?.map((p) => ({ name: p.name })),
355
- }
356
- return `export default ${JSON.stringify(clientConfig, null, 2)};`
357
- }
358
- if (id === '\0virtual:boltdocs-entry') {
359
- const code = generateEntryCode(options, config)
360
- return code
361
- }
362
- if (id === '\0virtual:boltdocs-mdx-components') {
363
- const extensions = ['tsx', 'ts', 'jsx', 'js']
364
- let userMdxPath = null
365
-
366
- for (const ext of extensions) {
367
- const p = path.resolve(docsDir, `mdx-components.${ext}`)
368
- if (fs.existsSync(p)) {
369
- userMdxPath = p
370
- break
371
- }
372
- }
373
-
374
- if (userMdxPath) {
375
- const normalizedPath = normalizePath(userMdxPath)
376
- return `import * as components from '${normalizedPath}';
377
- const mdxComponents = components.default || components;
378
- export default mdxComponents;
379
- export * from '${normalizedPath}';`
380
- }
381
-
382
- return `export default {};`
383
- }
384
- if (id === '\0virtual:boltdocs-layout') {
385
- const extensions = ['tsx', 'jsx']
386
- let userLayoutPath = null
387
-
388
- for (const ext of extensions) {
389
- const p = path.resolve(docsDir, `layout.${ext}`)
390
- if (fs.existsSync(p)) {
391
- userLayoutPath = p
392
- break
393
- }
394
- }
395
-
396
- if (userLayoutPath) {
397
- const normalizedPath = normalizePath(userLayoutPath)
398
- return `import UserLayout from '${normalizedPath}';
399
- export default UserLayout;`
400
- }
401
-
402
- // No user layout — return the built-in default
403
- return `import { DefaultLayout } from 'boltdocs/client';
404
- export default DefaultLayout;`
405
- }
406
-
407
- if (id === '\0virtual:boltdocs-search') {
408
- const routes = await generateRoutes(docsDir, config)
409
- const searchData = generateSearchData(routes)
410
- return `export default ${JSON.stringify(searchData, null, 2)};`
411
- }
412
- },
413
-
414
- transformIndexHtml: {
415
- order: 'pre',
416
- handler(html) {
417
- return injectHtmlMeta(html, config)
418
- },
419
- },
420
-
421
- async closeBundle() {
422
- if (!isBuild) return
423
- const outDir = viteConfig?.build?.outDir
424
- ? path.resolve(viteConfig.root, viteConfig.build.outDir)
425
- : path.resolve(process.cwd(), 'dist')
426
-
427
- const docsDirName = path.basename(docsDir || 'docs')
428
- await generateStaticPages({ docsDir, docsDirName, outDir, config })
429
-
430
- const { flushCache } = await import('../cache')
431
- await flushCache()
432
-
433
- await lifecycle?.runHook('afterBuild')
434
- await lifecycle?.runHook('buildEnd')
435
- },
436
- },
437
- ViteImageOptimizer({
438
- includePublic: true,
439
- png: { quality: 80 },
440
- jpeg: { quality: 80 },
441
- jpg: { quality: 80 },
442
- webp: { quality: 80 },
443
- avif: { quality: 80 },
444
- svg: {
445
- multipass: true,
446
- plugins: [
447
- {
448
- name: 'preset-default',
449
- },
450
- ] as any,
451
- },
452
- }),
453
- // Re-bind to use the lazily populated extra plugins
454
- {
455
- name: 'vite-plugin-boltdocs-extra-plugins',
456
- async configResolved() {
457
- // This is a dummy plugin to ensure the extra plugins are integrated
458
- // Actually, Vite doesn't support changing the plugin list dynamically easily
459
- // but we can return them in the original array if we initialize them early.
460
- },
461
- },
462
- ...(() => resolvedExtraVitePlugins)(),
463
- ]
464
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Configuration options specifically for the Boltdocs Vite plugin.
3
- */
4
- export interface BoltdocsPluginOptions {
5
- /** The root directory containing markdown files (default: 'docs') */
6
- docsDir?: string
7
- /** Path to a custom home page component (relative to project root) to render at '/' */
8
- homePage?: string
9
- }
@@ -1,17 +0,0 @@
1
- export * from './plugin-types'
2
- export * from './plugin-errors'
3
- export * from './plugin-store'
4
- export * from './plugin-validator'
5
- export * from './plugin-sandbox'
6
- export * from './plugin-lifecycle'
7
-
8
- import type { SecureBoltdocsPlugin } from './plugin-types'
9
-
10
- /**
11
- * Utility to create a Boltdocs plugin with full type safety.
12
- */
13
- export function createPlugin(
14
- plugin: SecureBoltdocsPlugin,
15
- ): SecureBoltdocsPlugin {
16
- return plugin
17
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Base class for all plugin-related errors in Boltdocs.
3
- */
4
- export class PluginError extends Error {
5
- public readonly pluginName: string
6
-
7
- constructor(pluginName: string, message: string) {
8
- super(`[plugin:${pluginName}] ${message}`)
9
- this.name = 'PluginError'
10
- this.pluginName = pluginName
11
- Object.setPrototypeOf(this, PluginError.prototype)
12
- }
13
- }
14
-
15
- /**
16
- * Specifically for schema or structure validation failures.
17
- */
18
- export class PluginValidationError extends PluginError {
19
- constructor(pluginName: string, message: string) {
20
- super(pluginName, `Validation failed: ${message}`)
21
- this.name = 'PluginValidationError'
22
- Object.setPrototypeOf(this, PluginValidationError.prototype)
23
- }
24
- }
25
-
26
- /**
27
- * Specifically for version mismatch or compatibility issues.
28
- */
29
- export class PluginCompatibilityError extends PluginError {
30
- constructor(pluginName: string, message: string) {
31
- super(pluginName, `Compatibility error: ${message}`)
32
- this.name = 'PluginCompatibilityError'
33
- Object.setPrototypeOf(this, PluginCompatibilityError.prototype)
34
- }
35
- }
36
-
37
- /**
38
- * Specifically for attempts to use capabilities without proper permissions.
39
- */
40
- export class PluginPermissionError extends PluginError {
41
- constructor(pluginName: string, permission: string) {
42
- super(pluginName, `Missing required permission: '${permission}'`)
43
- this.name = 'PluginPermissionError'
44
- Object.setPrototypeOf(this, PluginPermissionError.prototype)
45
- }
46
- }
47
-
48
- /**
49
- * Specifically for errors that occur during the execution of a lifecycle hook.
50
- */
51
- export class PluginHookError extends PluginError {
52
- public readonly hookName: string
53
-
54
- constructor(pluginName: string, hookName: string, originalError: Error) {
55
- super(pluginName, `Error in hook '${hookName}': ${originalError.message}`)
56
- this.name = 'PluginHookError'
57
- this.hookName = hookName
58
- // Prepend the hook name to the stack if possible
59
- this.stack = originalError.stack
60
- Object.setPrototypeOf(this, PluginHookError.prototype)
61
- }
62
- }
@@ -1,117 +0,0 @@
1
- import type { BoltdocsConfig } from '../config'
2
- import { PluginHookError } from './plugin-errors'
3
- import {
4
- PluginLifecycleHooks,
5
- SecureBoltdocsPlugin,
6
- PluginContext,
7
- PluginLogger,
8
- } from './plugin-types'
9
- import { BoltdocsPluginStore } from './plugin-store'
10
- import { PluginSandbox } from './plugin-sandbox'
11
-
12
- /**
13
- * Manages the lifecycle of all loaded plugins, ensuring hooks are executed
14
- * in the correct order with proper error isolation and context.
15
- */
16
- export class PluginLifecycleManager {
17
- private plugins: SecureBoltdocsPlugin[]
18
- private config: BoltdocsConfig
19
- private store: BoltdocsPluginStore
20
-
21
- constructor(plugins: SecureBoltdocsPlugin[], config: BoltdocsConfig) {
22
- this.plugins = plugins
23
- this.config = config
24
- this.store = new BoltdocsPluginStore()
25
- }
26
-
27
- /**
28
- * Runs a specific hook for all plugins that implement it.
29
- */
30
- public async runHook(
31
- hookName: keyof PluginLifecycleHooks,
32
- ...args: any[]
33
- ): Promise<void> {
34
- const sortedPlugins = this.getSortedPlugins()
35
-
36
- for (const plugin of sortedPlugins) {
37
- if (!plugin.hooks?.[hookName]) continue
38
-
39
- const context = this.createContext(plugin)
40
- const isBuildHook = hookName.toLowerCase().includes('build')
41
- const isDevHook = hookName.toLowerCase().includes('dev')
42
- const permission = isBuildHook
43
- ? 'hooks:build'
44
- : isDevHook
45
- ? 'hooks:dev'
46
- : undefined
47
-
48
- try {
49
- if (permission) {
50
- await PluginSandbox.executeWithIsolation(
51
- plugin,
52
- permission,
53
- hookName,
54
- () => (plugin.hooks![hookName] as any)(context, ...args),
55
- )
56
- } else {
57
- // Hooks like configResolved might not require specific permissions or are always allowed
58
- await (plugin.hooks![hookName] as any)(context, ...args)
59
- }
60
- } catch (error) {
61
- // Isolate error: logging it but allowing other plugins to continue
62
- const hookError = new PluginHookError(
63
- plugin.name,
64
- hookName,
65
- error instanceof Error ? error : new Error(String(error)),
66
- )
67
- context.logger.error(hookError)
68
- }
69
- }
70
- }
71
-
72
- /**
73
- * Sorts plugins based on their 'enforce' property (pre -> normal -> post).
74
- */
75
- private getSortedPlugins(): SecureBoltdocsPlugin[] {
76
- const pre = this.plugins.filter((p) => p.enforce === 'pre')
77
- const normal = this.plugins.filter((p) => !p.enforce)
78
- const post = this.plugins.filter((p) => p.enforce === 'post')
79
- return [...pre, ...normal, ...post]
80
- }
81
-
82
- /**
83
- * Creates a specialized context for a plugin.
84
- */
85
- private createContext(plugin: SecureBoltdocsPlugin): PluginContext {
86
- return {
87
- config: Object.freeze({ ...this.config }),
88
- meta: {
89
- name: plugin.name,
90
- version: plugin.version,
91
- boltdocsVersion: plugin.boltdocsVersion,
92
- },
93
- store: {
94
- get: (p, k) => this.store.get(p, k),
95
- set: (p, k, v) => this.store.set(p, k, v),
96
- has: (p, k) => this.store.has(p, k),
97
- },
98
- logger: this.createLogger(plugin.name),
99
- }
100
- }
101
-
102
- /**
103
- * Creates a namespaced logger for a plugin.
104
- */
105
- private createLogger(pluginName: string): PluginLogger {
106
- const prefix = `[plugin:${pluginName}]`
107
- return {
108
- info: (msg) => console.log(`${prefix} INFO: ${msg}`),
109
- warn: (msg) => console.warn(`${prefix} WARN: ${msg}`),
110
- error: (msg) => {
111
- const message = msg instanceof Error ? msg.message : msg
112
- console.error(`${prefix} ERROR: ${message}`)
113
- },
114
- debug: (msg) => console.debug(`${prefix} DEBUG: ${msg}`),
115
- }
116
- }
117
- }