create-fluxstack 1.18.0 → 1.19.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/app/client/src/App.tsx +7 -7
  3. package/app/client/src/components/AppLayout.tsx +60 -23
  4. package/app/client/src/components/ColorWheel.tsx +195 -0
  5. package/app/client/src/components/DemoPage.tsx +5 -3
  6. package/app/client/src/components/LiveUploadWidget.tsx +1 -1
  7. package/app/client/src/components/ThemePicker.tsx +307 -0
  8. package/app/client/src/config/theme.config.ts +127 -0
  9. package/app/client/src/hooks/useThemeClock.ts +66 -0
  10. package/app/client/src/index.css +193 -0
  11. package/app/client/src/lib/theme-clock.ts +201 -0
  12. package/app/client/src/live/AuthDemo.tsx +9 -9
  13. package/app/client/src/live/CounterDemo.tsx +10 -10
  14. package/app/client/src/live/FormDemo.tsx +8 -8
  15. package/app/client/src/live/PingPongDemo.tsx +10 -10
  16. package/app/client/src/live/RoomChatDemo.tsx +10 -10
  17. package/app/client/src/live/SharedCounterDemo.tsx +5 -5
  18. package/app/client/src/pages/ApiTestPage.tsx +5 -5
  19. package/app/client/src/pages/HomePage.tsx +12 -12
  20. package/app/server/index.ts +8 -0
  21. package/app/server/live/auto-generated-components.ts +1 -1
  22. package/app/server/live/rooms/ChatRoom.ts +13 -8
  23. package/app/server/routes/index.ts +20 -10
  24. package/core/build/index.ts +1 -1
  25. package/core/cli/command-registry.ts +1 -1
  26. package/core/cli/commands/build.ts +25 -6
  27. package/core/cli/commands/plugin-deps.ts +1 -2
  28. package/core/cli/generators/plugin.ts +433 -581
  29. package/core/framework/server.ts +34 -8
  30. package/core/index.ts +6 -5
  31. package/core/plugins/index.ts +71 -199
  32. package/core/plugins/types.ts +76 -461
  33. package/core/server/index.ts +1 -1
  34. package/core/utils/logger/startup-banner.ts +26 -4
  35. package/core/utils/version.ts +6 -6
  36. package/create-fluxstack.ts +216 -107
  37. package/package.json +108 -107
  38. package/tsconfig.json +2 -1
  39. package/app/client/.live-stubs/LiveAdminPanel.js +0 -15
  40. package/app/client/.live-stubs/LiveCounter.js +0 -9
  41. package/app/client/.live-stubs/LiveForm.js +0 -11
  42. package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
  43. package/app/client/.live-stubs/LivePingPong.js +0 -10
  44. package/app/client/.live-stubs/LiveRoomChat.js +0 -11
  45. package/app/client/.live-stubs/LiveSharedCounter.js +0 -10
  46. package/app/client/.live-stubs/LiveUpload.js +0 -15
  47. package/core/plugins/config.ts +0 -356
  48. package/core/plugins/dependency-manager.ts +0 -481
  49. package/core/plugins/discovery.ts +0 -379
  50. package/core/plugins/executor.ts +0 -353
  51. package/core/plugins/manager.ts +0 -645
  52. package/core/plugins/module-resolver.ts +0 -227
  53. package/core/plugins/registry.ts +0 -913
  54. package/vitest.config.live.ts +0 -69
@@ -1,645 +0,0 @@
1
- /**
2
- * Plugin Manager
3
- * Handles plugin lifecycle, execution, and context management
4
- */
5
-
6
- import type {
7
- FluxStack,
8
- PluginContext,
9
- PluginHook,
10
- PluginHookResult,
11
- PluginLoadResult,
12
- PluginMetrics,
13
- PluginExecutionContext,
14
- HookExecutionOptions,
15
- RequestContext,
16
- ResponseContext,
17
- ErrorContext,
18
- BuildContext
19
- } from "./types"
20
-
21
- type Plugin = FluxStack.Plugin
22
- import type { FluxStackConfig } from "@config"
23
- import type { Logger } from "@core/utils/logger"
24
- import { PluginRegistry } from "./registry"
25
- import { createPluginUtils } from "./config"
26
- import { FluxStackError } from "@core/utils/errors"
27
- import { pluginClientHooks } from "@core/server/plugin-client-hooks"
28
- import { EventEmitter } from "events"
29
-
30
- /**
31
- * Helper to safely parse request.url which might be relative or absolute
32
- */
33
- function parseRequestURL(request: Request): URL {
34
- try {
35
- // Try parsing as absolute URL first
36
- return new URL(request.url)
37
- } catch {
38
- // If relative, use host from headers or default to localhost
39
- const host = request.headers.get('host') || 'localhost'
40
- const protocol = request.headers.get('x-forwarded-proto') || 'http'
41
- return new URL(request.url, `${protocol}://${host}`)
42
- }
43
- }
44
-
45
- export interface PluginManagerConfig {
46
- config: FluxStackConfig
47
- logger: Logger
48
- app?: unknown
49
- }
50
-
51
- export class PluginManager extends EventEmitter {
52
- private registry: PluginRegistry
53
- private config: FluxStackConfig
54
- private logger: Logger
55
- private app?: unknown
56
- private metrics: Map<string, PluginMetrics> = new Map()
57
- private contexts: Map<string, PluginContext> = new Map()
58
- private initialized = false
59
-
60
- constructor(options: PluginManagerConfig) {
61
- super()
62
- this.config = options.config
63
- this.logger = options.logger
64
- this.app = options.app
65
-
66
- this.registry = new PluginRegistry({
67
- logger: this.logger,
68
- config: this.config
69
- })
70
- }
71
-
72
- /**
73
- * Initialize the plugin manager
74
- */
75
- async initialize(): Promise<void> {
76
- if (this.initialized) {
77
- return
78
- }
79
-
80
- this.logger.debug('Initializing plugin manager')
81
-
82
- try {
83
- // Discover and load plugins
84
- this.logger.debug('Starting plugin discovery...')
85
- await this.discoverPlugins()
86
- this.logger.debug('Plugin discovery completed')
87
-
88
- // Setup plugin contexts
89
- this.logger.debug('Setting up plugin contexts...')
90
- this.setupPluginContexts()
91
- this.logger.debug('Plugin contexts setup completed')
92
-
93
- // Execute setup hooks
94
- this.logger.debug('Executing setup hooks...')
95
- await this.executeHook('setup')
96
- this.logger.debug('Setup hooks execution completed')
97
-
98
- this.initialized = true
99
- const stats = this.registry.getStats()
100
- this.logger.debug('Plugin manager initialized successfully', {
101
- totalPlugins: stats.totalPlugins,
102
- enabledPlugins: stats.enabledPlugins,
103
- loadOrder: stats.loadOrder
104
- })
105
- } catch (error) {
106
- this.logger.error('Failed to initialize plugin manager', {
107
- error: error instanceof Error ? {
108
- message: error.message,
109
- stack: error.stack,
110
- name: error.name
111
- } : error
112
- })
113
- throw error
114
- }
115
- }
116
-
117
- /**
118
- * Shutdown the plugin manager
119
- */
120
- async shutdown(): Promise<void> {
121
- if (!this.initialized) {
122
- return
123
- }
124
-
125
- this.logger.info('Shutting down plugin manager')
126
-
127
- try {
128
- await this.executeHook('onServerStop')
129
- this.initialized = false
130
- this.logger.info('Plugin manager shut down successfully')
131
- } catch (error) {
132
- this.logger.error('Error during plugin manager shutdown', { error })
133
- }
134
- }
135
-
136
- /**
137
- * Get the plugin registry
138
- */
139
- getRegistry(): PluginRegistry {
140
- return this.registry
141
- }
142
-
143
- /**
144
- * Register a plugin
145
- */
146
- async registerPlugin(plugin: Plugin): Promise<void> {
147
- await this.registry.register(plugin)
148
- this.setupPluginContext(plugin)
149
-
150
- if (this.initialized && plugin.setup) {
151
- await this.executePluginHook(plugin, 'setup')
152
- }
153
- }
154
-
155
- /**
156
- * Unregister a plugin
157
- */
158
- unregisterPlugin(name: string): void {
159
- this.registry.unregister(name)
160
- this.contexts.delete(name)
161
- this.metrics.delete(name)
162
- }
163
-
164
- /**
165
- * Execute a hook on all plugins
166
- */
167
- async executeHook(
168
- hook: PluginHook,
169
- context?: unknown,
170
- options: HookExecutionOptions = {}
171
- ): Promise<PluginHookResult[]> {
172
- const {
173
- timeout = 30000,
174
- parallel = false,
175
- stopOnError = false,
176
- retries = 0
177
- } = options
178
-
179
- const results: PluginHookResult[] = []
180
- const loadOrder = this.registry.getLoadOrder()
181
- const enabledPlugins = this.getEnabledPlugins()
182
- const enabledSet = new Set(enabledPlugins.map(p => p.name))
183
-
184
- this.logger.debug(`Executing hook '${hook}' on ${enabledPlugins.length} plugins`, {
185
- hook,
186
- plugins: enabledPlugins.map(p => p.name),
187
- parallel,
188
- timeout
189
- })
190
-
191
- const executePlugin = async (plugin: Plugin): Promise<PluginHookResult> => {
192
- if (!enabledSet.has(plugin.name)) {
193
- return {
194
- success: true,
195
- duration: 0,
196
- plugin: plugin.name,
197
- hook
198
- }
199
- }
200
-
201
- return this.executePluginHook(plugin, hook, context, { timeout, retries })
202
- }
203
-
204
- try {
205
- if (parallel) {
206
- // Execute all plugins in parallel
207
- const promises = loadOrder
208
- .map(name => this.registry.get(name))
209
- .filter(Boolean)
210
- .map(plugin => executePlugin(plugin!))
211
-
212
- const settled = await Promise.allSettled(promises)
213
-
214
- for (const result of settled) {
215
- if (result.status === 'fulfilled') {
216
- results.push(result.value)
217
- } else {
218
- results.push({
219
- success: false,
220
- error: result.reason,
221
- duration: 0,
222
- plugin: 'unknown',
223
- hook
224
- })
225
- }
226
- }
227
- } else {
228
- // Execute plugins sequentially
229
- for (const pluginName of loadOrder) {
230
- const plugin = this.registry.get(pluginName)
231
- if (!plugin) continue
232
-
233
- const result = await executePlugin(plugin)
234
- results.push(result)
235
-
236
- if (!result.success && stopOnError) {
237
- this.logger.error(`Hook execution stopped due to error in plugin '${plugin.name}'`, {
238
- hook,
239
- plugin: plugin.name,
240
- error: result.error
241
- })
242
- break
243
- }
244
- }
245
- }
246
-
247
- // Emit hook completion event
248
- this.emit('hook:after', { hook, results, context })
249
-
250
- return results
251
- } catch (error) {
252
- this.logger.error(`Hook '${hook}' execution failed`, { error })
253
- this.emit('hook:error', { hook, error, context })
254
- throw error
255
- }
256
- }
257
-
258
- /**
259
- * Execute a specific hook on a specific plugin
260
- */
261
- async executePluginHook(
262
- plugin: Plugin,
263
- hook: PluginHook,
264
- context?: unknown,
265
- options: { timeout?: number; retries?: number } = {}
266
- ): Promise<PluginHookResult> {
267
- const { timeout = 30000, retries = 0 } = options
268
- const startTime = Date.now()
269
-
270
- // Check if plugin implements this hook
271
- const hookFunction = plugin[hook]
272
- if (!hookFunction || typeof hookFunction !== 'function') {
273
- return {
274
- success: true,
275
- duration: 0,
276
- plugin: plugin.name,
277
- hook
278
- }
279
- }
280
-
281
- this.emit('hook:before', { plugin: plugin.name, hook, context })
282
-
283
- let attempt = 0
284
- let lastError: Error | undefined
285
-
286
- while (attempt <= retries) {
287
- try {
288
- const pluginContext = this.getPluginContext(plugin.name)
289
- const executionContext: PluginExecutionContext = {
290
- plugin,
291
- hook,
292
- startTime: Date.now(),
293
- timeout,
294
- retries
295
- }
296
-
297
- // Create timeout promise with cleanup
298
- let timeoutId: ReturnType<typeof setTimeout> | undefined
299
- const timeoutPromise = new Promise<never>((_, reject) => {
300
- timeoutId = setTimeout(() => {
301
- reject(new FluxStackError(
302
- `Plugin '${plugin.name}' hook '${hook}' timed out after ${timeout}ms`,
303
- 'PLUGIN_TIMEOUT',
304
- 408
305
- ))
306
- }, timeout)
307
- })
308
-
309
- // Execute the hook with appropriate context
310
- let hookPromise: Promise<unknown>
311
-
312
- switch (hook) {
313
- case 'setup':
314
- case 'onServerStart':
315
- case 'onServerStop':
316
- hookPromise = Promise.resolve((hookFunction as Function)(pluginContext))
317
- break
318
- case 'onRequest':
319
- case 'onResponse':
320
- case 'onError':
321
- hookPromise = Promise.resolve((hookFunction as Function)(context))
322
- break
323
- case 'onBuild':
324
- case 'onBuildComplete':
325
- hookPromise = Promise.resolve((hookFunction as Function)(context))
326
- break
327
- default:
328
- hookPromise = Promise.resolve((hookFunction as Function)(context || pluginContext))
329
- }
330
-
331
- // Race between hook execution and timeout
332
- try {
333
- await Promise.race([hookPromise, timeoutPromise])
334
- } finally {
335
- clearTimeout(timeoutId)
336
- }
337
-
338
- const duration = Date.now() - startTime
339
-
340
- // Update metrics
341
- this.updatePluginMetrics(plugin.name, hook, duration, true)
342
-
343
- this.logger.debug(`Plugin '${plugin.name}' hook '${hook}' completed successfully`, {
344
- plugin: plugin.name,
345
- hook,
346
- duration,
347
- attempt: attempt + 1
348
- })
349
-
350
- return {
351
- success: true,
352
- duration,
353
- plugin: plugin.name,
354
- hook,
355
- context: executionContext
356
- }
357
-
358
- } catch (error) {
359
- lastError = error instanceof Error ? error : new Error(String(error))
360
- attempt++
361
-
362
- this.logger.warn(`Plugin '${plugin.name}' hook '${hook}' failed (attempt ${attempt}/${retries + 1})`, {
363
- plugin: plugin.name,
364
- hook,
365
- error: lastError.message,
366
- attempt
367
- })
368
-
369
- if (attempt <= retries) {
370
- // Wait before retry (exponential backoff)
371
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt - 1) * 1000))
372
- }
373
- }
374
- }
375
-
376
- const duration = Date.now() - startTime
377
-
378
- // Update metrics
379
- this.updatePluginMetrics(plugin.name, hook, duration, false)
380
-
381
- this.emit('plugin:error', { plugin: plugin.name, hook, error: lastError })
382
-
383
- return {
384
- success: false,
385
- error: lastError,
386
- duration,
387
- plugin: plugin.name,
388
- hook
389
- }
390
- }
391
-
392
- /**
393
- * Get plugin metrics
394
- */
395
- getPluginMetrics(pluginName?: string): PluginMetrics | Map<string, PluginMetrics> {
396
- if (pluginName) {
397
- return this.metrics.get(pluginName) || {
398
- loadTime: 0,
399
- setupTime: 0,
400
- hookExecutions: new Map(),
401
- errors: 0,
402
- warnings: 0
403
- }
404
- }
405
- return this.metrics
406
- }
407
-
408
- /**
409
- * Get enabled plugins
410
- */
411
- private getEnabledPlugins(): Plugin[] {
412
- const allPlugins = this.registry.getAll()
413
- const enabledNames = (this.config.plugins.enabled ?? []) as string[]
414
- const disabledNames = (this.config.plugins.disabled ?? []) as string[]
415
-
416
- return allPlugins.filter(plugin => {
417
- // If explicitly disabled, exclude
418
- if (disabledNames.includes(plugin.name)) {
419
- return false
420
- }
421
-
422
- // If enabled list is empty, include all non-disabled
423
- if (enabledNames.length === 0) {
424
- return true
425
- }
426
-
427
- // Otherwise, only include if explicitly enabled
428
- return enabledNames.includes(plugin.name)
429
- })
430
- }
431
-
432
- /**
433
- * Discover and load plugins
434
- */
435
- private async discoverPlugins(): Promise<void> {
436
- try {
437
- // ⚠️ Built-in plugins are now registered manually via .use()
438
- // No auto-discovery for core/plugins/built-in - developer chooses which to use
439
-
440
- const results: PluginLoadResult[] = []
441
-
442
- // 1. Discover project plugins (plugins/ directory)
443
- this.logger.debug('Discovering project plugins in directory: plugins')
444
- const projectResults = await this.registry.discoverPlugins({
445
- directories: ['plugins'],
446
- includeBuiltIn: false,
447
- includeExternal: true
448
- })
449
- results.push(...projectResults)
450
-
451
- // 2. Discover npm plugins (node_modules/)
452
- if (this.config.plugins.discoverNpmPlugins) {
453
- this.logger.debug('Discovering npm plugins in node_modules...')
454
- const npmResults = await this.registry.discoverNpmPlugins()
455
- results.push(...npmResults)
456
- } else {
457
- this.logger.debug('🔒 NPM plugin discovery disabled for security (PLUGINS_DISCOVER_NPM=false)')
458
- this.logger.debug(' To enable: Set PLUGINS_DISCOVER_NPM=true and add plugins to PLUGINS_ALLOWED')
459
- }
460
-
461
- let loaded = 0
462
- let failed = 0
463
-
464
- for (const result of results) {
465
- if (result.success) {
466
- loaded++
467
- if (result.warnings && result.warnings.length > 0) {
468
- this.logger.warn(`Plugin '${result.plugin?.name}' loaded with warnings`, {
469
- warnings: result.warnings
470
- })
471
- }
472
- } else {
473
- failed++
474
- this.logger.error(`Failed to load plugin: ${result.error}`)
475
- }
476
- }
477
-
478
- this.logger.debug('Plugin discovery completed', { loaded, failed })
479
- } catch (error) {
480
- this.logger.error('Plugin discovery failed', { error })
481
- throw error
482
- }
483
- }
484
-
485
- /**
486
- * Setup plugin contexts for all plugins
487
- */
488
- private setupPluginContexts(): void {
489
- const plugins = this.registry.getAll()
490
-
491
- for (const plugin of plugins) {
492
- this.setupPluginContext(plugin)
493
- }
494
- }
495
-
496
- /**
497
- * Setup context for a specific plugin
498
- */
499
- private setupPluginContext(plugin: Plugin): void {
500
- // Plugin config available but not used in current implementation
501
- // const pluginConfig = this.config.plugins.config[plugin.name] || {}
502
- // const mergedConfig = { ...plugin.defaultConfig, ...pluginConfig }
503
-
504
- const context: PluginContext = {
505
- config: this.config,
506
- logger: this.logger.child ? this.logger.child({ plugin: plugin.name }) : this.logger,
507
- app: this.app,
508
- utils: createPluginUtils(this.logger),
509
- registry: this.registry,
510
- clientHooks: {
511
- register: (hookName: string, jsCode: string) => pluginClientHooks.register(hookName, jsCode)
512
- }
513
- }
514
-
515
- this.contexts.set(plugin.name, context)
516
-
517
- // Initialize metrics
518
- this.metrics.set(plugin.name, {
519
- loadTime: 0,
520
- setupTime: 0,
521
- hookExecutions: new Map(),
522
- errors: 0,
523
- warnings: 0
524
- })
525
- }
526
-
527
- /**
528
- * Get plugin context
529
- */
530
- private getPluginContext(pluginName: string): PluginContext {
531
- const context = this.contexts.get(pluginName)
532
- if (!context) {
533
- throw new FluxStackError(
534
- `Plugin context not found for '${pluginName}'`,
535
- 'PLUGIN_CONTEXT_NOT_FOUND',
536
- 500
537
- )
538
- }
539
- return context
540
- }
541
-
542
- /**
543
- * Update plugin metrics
544
- */
545
- private updatePluginMetrics(
546
- pluginName: string,
547
- hook: PluginHook,
548
- duration: number,
549
- success: boolean
550
- ): void {
551
- const metrics = this.metrics.get(pluginName)
552
- if (!metrics) return
553
-
554
- // Update hook execution count
555
- const currentCount = metrics.hookExecutions.get(hook) || 0
556
- metrics.hookExecutions.set(hook, currentCount + 1)
557
-
558
- // Update error/success counts
559
- if (success) {
560
- if (hook === 'setup') {
561
- metrics.setupTime = duration
562
- }
563
- } else {
564
- metrics.errors++
565
- }
566
-
567
- metrics.lastExecution = new Date()
568
- }
569
- }
570
-
571
- /**
572
- * Create request context from HTTP request
573
- */
574
- export function createRequestContext(request: Request, additionalData: Record<string, unknown> = {}): RequestContext {
575
- const url = parseRequestURL(request)
576
-
577
- return {
578
- request,
579
- path: url.pathname,
580
- method: request.method,
581
- headers: (() => {
582
- const headers: Record<string, string> = {}
583
- request.headers.forEach((value, key) => {
584
- headers[key] = value
585
- })
586
- return headers
587
- })(),
588
- query: Object.fromEntries(url.searchParams.entries()),
589
- params: {},
590
- startTime: Date.now(),
591
- ...additionalData
592
- }
593
- }
594
-
595
- /**
596
- * Create response context from request context and response
597
- */
598
- export function createResponseContext(
599
- requestContext: RequestContext,
600
- response: Response,
601
- additionalData: Record<string, unknown> = {}
602
- ): ResponseContext {
603
- return {
604
- ...requestContext,
605
- response,
606
- statusCode: response.status,
607
- duration: Date.now() - requestContext.startTime,
608
- size: parseInt(response.headers.get('content-length') || '0'),
609
- ...additionalData
610
- }
611
- }
612
-
613
- /**
614
- * Create error context from request context and error
615
- */
616
- export function createErrorContext(
617
- requestContext: RequestContext,
618
- error: Error,
619
- additionalData: Record<string, unknown> = {}
620
- ): ErrorContext {
621
- return {
622
- ...requestContext,
623
- error,
624
- duration: Date.now() - requestContext.startTime,
625
- handled: false,
626
- ...additionalData
627
- }
628
- }
629
-
630
- /**
631
- * Create build context
632
- */
633
- export function createBuildContext(
634
- target: string,
635
- outDir: string,
636
- mode: 'development' | 'production',
637
- config: FluxStackConfig
638
- ): BuildContext {
639
- return {
640
- target,
641
- outDir,
642
- mode,
643
- config
644
- }
645
- }