boltdocs 2.5.5 → 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 -98
  37. package/dist/cache-3FOEPC2P.mjs +0 -1
  38. package/dist/chunk-5B5NKOW6.mjs +0 -77
  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 -82
  46. package/dist/node/index.d.ts +0 -567
  47. package/dist/node/index.js +0 -77
  48. package/dist/package-QFIAETHR.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 -478
  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,478 +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
- resolve: {
92
- alias: [
93
- {
94
- find: /^(.*)\/?use-sync-external-store\/shim(?:.*)$/,
95
- replacement: '\0virtual:boltdocs-usesync-shim',
96
- },
97
- {
98
- find: /^(.*)\/?use-sync-external-store$/,
99
- replacement: '\0virtual:boltdocs-usesync-shim',
100
- },
101
- ]
102
- },
103
- optimizeDeps: {
104
- include: ['react', 'react-dom'],
105
- exclude: [
106
- 'boltdocs',
107
- 'boltdocs/client',
108
- 'boltdocs/hooks',
109
- 'boltdocs/primitives',
110
- 'boltdocs/base-ui',
111
- 'boltdocs/mdx',
112
- 'boltdocs/integrations',
113
- 'boltdocs/client/hooks',
114
- 'boltdocs/client/primitives',
115
- ],
116
- },
117
- }
118
- },
119
-
120
- configResolved(resolved) {
121
- viteConfig = resolved
122
- lifecycle?.runHook('configResolved', config)
123
- },
124
-
125
- async configureServer(server) {
126
- await lifecycle?.runHook('beforeDev')
127
-
128
- // Security: Apply hardened headers and CSP
129
- server.middlewares.use((_req, res, next) => {
130
- const isProd = process.env.NODE_ENV === 'production'
131
-
132
- if (isProd) {
133
- Object.entries(SECURITY_HEADERS).forEach(([header, value]) => {
134
- res.setHeader(header, value)
135
- })
136
- }
137
-
138
- // Inject CSP if enabled in configuration
139
- if (config.security?.enableCSP) {
140
- res.setHeader('Content-Security-Policy', getCSPHeader(config))
141
- }
142
-
143
- next()
144
- })
145
-
146
- // Serve robots.txt from config
147
- server.middlewares.use((req, res, next) => {
148
- if (req.url === '/robots.txt') {
149
- const robots = generateRobotsTxt(config)
150
- res.statusCode = 200
151
- res.setHeader('Content-Type', 'text/plain')
152
- res.end(robots)
153
- return
154
- }
155
- next()
156
- })
157
-
158
- // Serve default HTML for documentation routes or if index.html is missing
159
- server.middlewares.use(async (req, res, next) => {
160
- const url = req.url?.split('?')[0] || '/'
161
- const accept = req.headers.accept || ''
162
-
163
- const isDocRoute =
164
- url === '/' ||
165
- url.startsWith('/docs') ||
166
- (config.i18n &&
167
- Object.keys(config.i18n.locales).some(
168
- (locale) =>
169
- url.startsWith(`/${locale}/docs`) || url === `/${locale}`,
170
- )) ||
171
- // Handle any HTML request that isn't a known static file or docs,
172
- // as it potentially could be an external page.
173
- // (The client-side router will handle 404s if it doesn't match anything)
174
- true
175
-
176
- // Improved check: If it's a doc route, serve HTML even if it has a dot (e.g. version 1.1)
177
- // We only skip if it has a known asset extension to prevent serving HTML for images/js/etc.
178
- const isAsset =
179
- /\.(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(
180
- url,
181
- )
182
-
183
- if (accept.includes('text/html') && !isAsset && isDocRoute) {
184
- let html = getHtmlTemplate(config)
185
- html = injectHtmlMeta(html, config)
186
- html = await server.transformIndexHtml(req.url || '/', html)
187
- res.statusCode = 200
188
- res.setHeader('Content-Type', 'text/html')
189
- res.end(html)
190
- return
191
- }
192
-
193
- next()
194
- })
195
-
196
- // Explicitly watch config files...
197
-
198
- const configPaths = CONFIG_FILES.map((c) =>
199
- path.resolve(process.cwd(), c),
200
- )
201
- const compExtensions = ['tsx', 'jsx']
202
- const layoutCompPaths = compExtensions.map((ext) =>
203
- path.resolve(docsDir, `layout.${ext}`),
204
- )
205
- const mdxCompExtensions = ['tsx', 'ts', 'jsx', 'js']
206
- const mdxCompPaths = mdxCompExtensions.map((ext) =>
207
- path.resolve(docsDir, `mdx-components.${ext}`),
208
- )
209
- const extPagesPaths = mdxCompExtensions.map((ext) =>
210
- path.resolve(docsDir, `pages-external/index.${ext}`),
211
- )
212
-
213
- server.watcher.add([
214
- ...configPaths,
215
- ...mdxCompPaths,
216
- ...layoutCompPaths,
217
- ...extPagesPaths,
218
- ])
219
-
220
- const handleFileEvent = async (
221
- file: string,
222
- type: 'add' | 'unlink' | 'change',
223
- ) => {
224
- try {
225
- const normalized = normalizePath(file)
226
-
227
- // Restart the Vite server if the Boltdocs config changes
228
- if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
229
- server.restart()
230
- return
231
- }
232
-
233
- // If mdx-components file changes, invalidate the virtual module
234
- if (
235
- mdxCompExtensions.some((ext) =>
236
- normalized.endsWith(`mdx-components.${ext}`),
237
- )
238
- ) {
239
- const mod = server.moduleGraph.getModuleById(
240
- '\0virtual:boltdocs-mdx-components',
241
- )
242
- if (mod) server.moduleGraph.invalidateModule(mod)
243
- server.ws.send({ type: 'full-reload' })
244
- return
245
- }
246
-
247
- // If layout.tsx/jsx file changes, invalidate the virtual module
248
- if (
249
- compExtensions.some((ext) => normalized.endsWith(`layout.${ext}`))
250
- ) {
251
- const mod = server.moduleGraph.getModuleById(
252
- '\0virtual:boltdocs-layout',
253
- )
254
- if (mod) server.moduleGraph.invalidateModule(mod)
255
- server.ws.send({ type: 'full-reload' })
256
- return
257
- }
258
-
259
- // If any pages-external file changes, invalidate the entry module
260
- if (
261
- normalized.includes('/pages-external/') ||
262
- normalized.includes('\\pages-external\\')
263
- ) {
264
- const mod = server.moduleGraph.getModuleById(
265
- '\0virtual:boltdocs-entry',
266
- )
267
- if (mod) server.moduleGraph.invalidateModule(mod)
268
- server.ws.send({ type: 'full-reload' })
269
- return
270
- }
271
-
272
- if (
273
- !normalized.startsWith(normalizedDocsDir) ||
274
- !isDocFile(normalized)
275
- )
276
- return
277
-
278
- // Invalidate appropriately
279
- if (type === 'add' || type === 'unlink') {
280
- invalidateRouteCache()
281
- // Re-resolve config as it might affect versions/routes
282
- config = await resolveConfig(docsDir)
283
-
284
- const configMod = server.moduleGraph.getModuleById(
285
- '\0virtual:boltdocs-config',
286
- )
287
- if (configMod) server.moduleGraph.invalidateModule(configMod)
288
-
289
- server.ws.send({
290
- type: 'custom',
291
- event: 'boltdocs:config-update',
292
- data: {
293
- theme: config?.theme,
294
- i18n: config?.i18n,
295
- versions: config?.versions,
296
- siteUrl: config?.siteUrl,
297
- },
298
- })
299
- } else {
300
- invalidateFile(file)
301
- }
302
-
303
- // Regenerate and push to client
304
- // Optimization: generateRoutes is mostly incremental thanks to docCache
305
- // We only force a full disk scan on add/unlink events
306
- const newRoutes = await generateRoutes(
307
- docsDir,
308
- config,
309
- '/docs',
310
- type !== 'change',
311
- )
312
-
313
- const routesMod = server.moduleGraph.getModuleById(
314
- '\0virtual:boltdocs-routes',
315
- )
316
- if (routesMod) server.moduleGraph.invalidateModule(routesMod)
317
-
318
- server.ws.send({
319
- type: 'custom',
320
- event: 'boltdocs:routes-update',
321
- data: newRoutes,
322
- })
323
- } catch (e) {
324
- console.error(`[boltdocs] HMR error during ${type} event:`, e)
325
- }
326
- }
327
-
328
- server.watcher.on('add', (f) => handleFileEvent(f, 'add'))
329
- server.watcher.on('unlink', (f) => handleFileEvent(f, 'unlink'))
330
- server.watcher.on('change', (f) => handleFileEvent(f, 'change'))
331
-
332
- await lifecycle?.runHook('afterDev')
333
- },
334
-
335
- resolveId(id) {
336
- if (
337
- id === 'virtual:boltdocs-routes' ||
338
- id === 'virtual:boltdocs-config' ||
339
- id === 'virtual:boltdocs-entry' ||
340
- id === 'virtual:boltdocs-mdx-components' ||
341
- id === 'virtual:boltdocs-layout' ||
342
- id === 'virtual:boltdocs-search'
343
- ) {
344
- return '\0' + id
345
- }
346
-
347
- // --- Intercept use-sync-external-store imports to fix React 19 ESM bugs ---
348
- // Exclude with-selector as it's not exported by react directly
349
- if ((id.includes('use-sync-external-store/shim') || id === 'use-sync-external-store' || id.endsWith('use-sync-external-store/index.js')) && !id.includes('with-selector')) {
350
- return '\0virtual:boltdocs-usesync-shim'
351
- }
352
- },
353
-
354
- async load(id) {
355
- if (id === '\0virtual:boltdocs-usesync-shim') {
356
- return `import * as React from 'react';\nexport const useSyncExternalStore = React.useSyncExternalStore;\nexport default React.useSyncExternalStore;`
357
- }
358
- if (id === '\0virtual:boltdocs-routes') {
359
- const routes = await generateRoutes(docsDir, config)
360
- return `export default ${JSON.stringify(routes, null, 2)};`
361
- }
362
- if (id === '\0virtual:boltdocs-config') {
363
- const clientConfig = {
364
- theme: config?.theme,
365
- i18n: config?.i18n,
366
- versions: config?.versions,
367
- siteUrl: config?.siteUrl,
368
- plugins: config?.plugins?.map((p) => ({ name: p.name })),
369
- }
370
- return `export default ${JSON.stringify(clientConfig, null, 2)};`
371
- }
372
- if (id === '\0virtual:boltdocs-entry') {
373
- const code = generateEntryCode(options, config)
374
- return code
375
- }
376
- if (id === '\0virtual:boltdocs-mdx-components') {
377
- const extensions = ['tsx', 'ts', 'jsx', 'js']
378
- let userMdxPath = null
379
-
380
- for (const ext of extensions) {
381
- const p = path.resolve(docsDir, `mdx-components.${ext}`)
382
- if (fs.existsSync(p)) {
383
- userMdxPath = p
384
- break
385
- }
386
- }
387
-
388
- if (userMdxPath) {
389
- const normalizedPath = normalizePath(userMdxPath)
390
- return `import * as components from '${normalizedPath}';
391
- const mdxComponents = components.default || components;
392
- export default mdxComponents;
393
- export * from '${normalizedPath}';`
394
- }
395
-
396
- return `export default {};`
397
- }
398
- if (id === '\0virtual:boltdocs-layout') {
399
- const extensions = ['tsx', 'jsx']
400
- let userLayoutPath = null
401
-
402
- for (const ext of extensions) {
403
- const p = path.resolve(docsDir, `layout.${ext}`)
404
- if (fs.existsSync(p)) {
405
- userLayoutPath = p
406
- break
407
- }
408
- }
409
-
410
- if (userLayoutPath) {
411
- const normalizedPath = normalizePath(userLayoutPath)
412
- return `import UserLayout from '${normalizedPath}';
413
- export default UserLayout;`
414
- }
415
-
416
- // No user layout — return the built-in default
417
- return `import { DefaultLayout } from 'boltdocs/client';
418
- export default DefaultLayout;`
419
- }
420
-
421
- if (id === '\0virtual:boltdocs-search') {
422
- const routes = await generateRoutes(docsDir, config)
423
- const searchData = generateSearchData(routes)
424
- return `export default ${JSON.stringify(searchData, null, 2)};`
425
- }
426
- },
427
-
428
- transformIndexHtml: {
429
- order: 'pre',
430
- handler(html) {
431
- return injectHtmlMeta(html, config)
432
- },
433
- },
434
-
435
- async closeBundle() {
436
- if (!isBuild) return
437
- const outDir = viteConfig?.build?.outDir
438
- ? path.resolve(viteConfig.root, viteConfig.build.outDir)
439
- : path.resolve(process.cwd(), 'dist')
440
-
441
- const docsDirName = path.basename(docsDir || 'docs')
442
- await generateStaticPages({ docsDir, docsDirName, outDir, config })
443
-
444
- const { flushCache } = await import('../cache')
445
- await flushCache()
446
-
447
- await lifecycle?.runHook('afterBuild')
448
- await lifecycle?.runHook('buildEnd')
449
- },
450
- },
451
- ViteImageOptimizer({
452
- includePublic: true,
453
- png: { quality: 80 },
454
- jpeg: { quality: 80 },
455
- jpg: { quality: 80 },
456
- webp: { quality: 80 },
457
- avif: { quality: 80 },
458
- svg: {
459
- multipass: true,
460
- plugins: [
461
- {
462
- name: 'preset-default',
463
- },
464
- ] as any,
465
- },
466
- }),
467
- // Re-bind to use the lazily populated extra plugins
468
- {
469
- name: 'vite-plugin-boltdocs-extra-plugins',
470
- async configResolved() {
471
- // This is a dummy plugin to ensure the extra plugins are integrated
472
- // Actually, Vite doesn't support changing the plugin list dynamically easily
473
- // but we can return them in the original array if we initialize them early.
474
- },
475
- },
476
- ...(() => resolvedExtraVitePlugins)(),
477
- ]
478
- }
@@ -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
- }