create-fluxstack 1.9.1 → 1.10.1

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 (49) hide show
  1. package/LIVE_COMPONENTS_REVIEW.md +781 -0
  2. package/app/client/src/App.tsx +39 -43
  3. package/app/client/src/lib/eden-api.ts +2 -7
  4. package/app/client/src/live/FileUploadExample.tsx +359 -0
  5. package/app/client/src/live/MinimalLiveClock.tsx +47 -0
  6. package/app/client/src/live/QuickUploadTest.tsx +193 -0
  7. package/app/client/src/main.tsx +10 -10
  8. package/app/client/src/vite-env.d.ts +1 -1
  9. package/app/client/tsconfig.app.json +45 -44
  10. package/app/client/tsconfig.node.json +25 -25
  11. package/app/server/index.ts +30 -103
  12. package/app/server/live/LiveFileUploadComponent.ts +77 -0
  13. package/app/server/live/register-components.ts +19 -19
  14. package/core/build/bundler.ts +4 -1
  15. package/core/build/index.ts +124 -4
  16. package/core/build/live-components-generator.ts +68 -1
  17. package/core/cli/index.ts +163 -35
  18. package/core/client/LiveComponentsProvider.tsx +3 -9
  19. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
  20. package/core/client/hooks/useChunkedUpload.ts +112 -61
  21. package/core/client/hooks/useHybridLiveComponent.ts +80 -26
  22. package/core/client/hooks/useTypedLiveComponent.ts +133 -0
  23. package/core/client/hooks/useWebSocket.ts +4 -16
  24. package/core/client/index.ts +20 -2
  25. package/core/framework/server.ts +181 -8
  26. package/core/live/ComponentRegistry.ts +5 -1
  27. package/core/plugins/built-in/index.ts +8 -5
  28. package/core/plugins/built-in/live-components/commands/create-live-component.ts +55 -63
  29. package/core/plugins/built-in/vite/index.ts +75 -187
  30. package/core/plugins/built-in/vite/vite-dev.ts +88 -0
  31. package/core/plugins/registry.ts +54 -2
  32. package/core/plugins/types.ts +86 -2
  33. package/core/server/index.ts +1 -2
  34. package/core/server/live/ComponentRegistry.ts +14 -5
  35. package/core/server/live/FileUploadManager.ts +22 -25
  36. package/core/server/live/auto-generated-components.ts +29 -26
  37. package/core/server/live/websocket-plugin.ts +19 -5
  38. package/core/server/plugins/static-files-plugin.ts +49 -240
  39. package/core/server/plugins/swagger.ts +33 -33
  40. package/core/types/build.ts +1 -0
  41. package/core/types/plugin.ts +9 -1
  42. package/core/types/types.ts +137 -0
  43. package/core/utils/logger/startup-banner.ts +20 -4
  44. package/core/utils/version.ts +1 -1
  45. package/eslint.config.js +23 -23
  46. package/package.json +3 -3
  47. package/plugins/crypto-auth/server/middlewares.ts +19 -19
  48. package/tsconfig.json +52 -52
  49. package/workspace.json +5 -5
@@ -6,6 +6,7 @@ import { PluginManager } from "@/core/plugins/manager"
6
6
  import { getConfigSync, getEnvironmentInfo } from "@/core/config"
7
7
  import { logger } from "@/core/utils/logger"
8
8
  import { displayStartupBanner, type StartupInfo } from "@/core/utils/logger/startup-banner"
9
+ import { componentRegistry } from "@/core/server/live/ComponentRegistry"
9
10
  import { createErrorHandler } from "@/core/utils/errors/handlers"
10
11
  import { createTimer, formatBytes, isProduction, isDevelopment } from "@/core/utils/helpers"
11
12
  import type { Plugin } from "@/core/plugins"
@@ -50,6 +51,9 @@ export class FluxStackFramework {
50
51
  this.app = new Elysia()
51
52
  this.pluginRegistry = new PluginRegistry()
52
53
 
54
+ // Execute onConfigLoad hooks will be called during plugin initialization
55
+ // We defer this until plugins are loaded in initializeAutomaticPlugins()
56
+
53
57
 
54
58
 
55
59
  // Create plugin utilities
@@ -143,6 +147,50 @@ export class FluxStackFramework {
143
147
  private async initializeAutomaticPlugins() {
144
148
  try {
145
149
  await this.pluginManager.initialize()
150
+
151
+ // Sync discovered plugins from PluginManager to main registry
152
+ const discoveredPlugins = this.pluginManager.getRegistry().getAll()
153
+ for (const plugin of discoveredPlugins) {
154
+ if (!this.pluginRegistry.has(plugin.name)) {
155
+ // Register in main registry (synchronously, will call setup in start())
156
+ (this.pluginRegistry as any).plugins.set(plugin.name, plugin)
157
+ if (plugin.dependencies) {
158
+ (this.pluginRegistry as any).dependencies.set(plugin.name, plugin.dependencies)
159
+ }
160
+ }
161
+ }
162
+
163
+ // Update load order
164
+ try {
165
+ (this.pluginRegistry as any).updateLoadOrder()
166
+ } catch (error) {
167
+ // Fallback: create basic load order
168
+ const plugins = (this.pluginRegistry as any).plugins as Map<string, FluxStack.Plugin>
169
+ const loadOrder = Array.from(plugins.keys())
170
+ ;(this.pluginRegistry as any).loadOrder = loadOrder
171
+ }
172
+
173
+ // Execute onConfigLoad hooks for all plugins
174
+ const configLoadContext = {
175
+ config: this.context.config,
176
+ envVars: process.env as Record<string, string | undefined>,
177
+ configPath: undefined
178
+ }
179
+
180
+ const loadOrder = this.pluginRegistry.getLoadOrder()
181
+ for (const pluginName of loadOrder) {
182
+ const plugin = this.pluginRegistry.get(pluginName)
183
+ if (plugin && plugin.onConfigLoad) {
184
+ try {
185
+ await plugin.onConfigLoad(configLoadContext)
186
+ } catch (error) {
187
+ logger.error(`Plugin '${pluginName}' onConfigLoad hook failed`, {
188
+ error: error instanceof Error ? error.message : String(error)
189
+ })
190
+ }
191
+ }
192
+ }
193
+
146
194
  const stats = this.pluginManager.getRegistry().getStats()
147
195
  logger.debug('Automatic plugins loaded successfully', {
148
196
  pluginCount: stats.totalPlugins,
@@ -271,14 +319,46 @@ export class FluxStackFramework {
271
319
  })(),
272
320
  query: Object.fromEntries(url.searchParams.entries()),
273
321
  params: {},
322
+ body: undefined, // Will be populated if request has body
274
323
  startTime,
275
324
  handled: false,
276
325
  response: undefined
277
326
  }
278
327
 
328
+ // Try to parse body for validation
329
+ try {
330
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
331
+ const contentType = request.headers.get('content-type')
332
+ if (contentType?.includes('application/json')) {
333
+ requestContext.body = await request.clone().json().catch(() => undefined)
334
+ }
335
+ }
336
+ } catch (error) {
337
+ // Ignore body parsing errors for now
338
+ }
339
+
279
340
  // Execute onRequest hooks for all plugins first (logging, auth, etc.)
280
341
  await this.executePluginHooks('onRequest', requestContext)
281
342
 
343
+ // Execute onRequestValidation hooks (for custom validation)
344
+ const validationContext = {
345
+ ...requestContext,
346
+ errors: [] as Array<{ field: string; message: string; code: string }>,
347
+ isValid: true
348
+ }
349
+ await this.executePluginHooks('onRequestValidation', validationContext)
350
+
351
+ // If validation failed, return error response
352
+ if (!validationContext.isValid && validationContext.errors.length > 0) {
353
+ return new Response(JSON.stringify({
354
+ success: false,
355
+ errors: validationContext.errors
356
+ }), {
357
+ status: 400,
358
+ headers: { 'Content-Type': 'application/json' }
359
+ })
360
+ }
361
+
282
362
  // Execute onBeforeRoute hooks - allow plugins to handle requests before routing
283
363
  const handledResponse = await this.executePluginBeforeRouteHooks(requestContext)
284
364
 
@@ -288,8 +368,8 @@ export class FluxStackFramework {
288
368
  }
289
369
  })
290
370
 
291
- // Setup onResponse hook
292
- this.app.onAfterHandle(async ({ request, response, set }) => {
371
+ // Setup onAfterHandle hook (covers onBeforeResponse, onResponseTransform, onResponse)
372
+ this.app.onAfterHandle(async ({ request, response, set, path }) => {
293
373
  const url = this.parseRequestURL(request)
294
374
 
295
375
  // Retrieve start time using the timing key
@@ -302,6 +382,9 @@ export class FluxStackFramework {
302
382
  this.requestTimings.delete(String(requestKey))
303
383
  }
304
384
 
385
+ let currentResponse = response
386
+
387
+ // Create response context
305
388
  const responseContext = {
306
389
  request,
307
390
  path: url.pathname,
@@ -315,12 +398,39 @@ export class FluxStackFramework {
315
398
  })(),
316
399
  query: Object.fromEntries(url.searchParams.entries()),
317
400
  params: {},
318
- response,
319
- statusCode: Number((response as any)?.status || set.status || 200),
401
+ response: currentResponse,
402
+ statusCode: Number((currentResponse as any)?.status || set.status || 200),
320
403
  duration,
321
404
  startTime
322
405
  }
323
406
 
407
+ // Execute onAfterRoute hooks (route was matched, params available)
408
+ const routeContext = {
409
+ ...responseContext,
410
+ route: path || url.pathname,
411
+ handler: undefined
412
+ }
413
+ await this.executePluginHooks('onAfterRoute', routeContext)
414
+
415
+ // Execute onBeforeResponse hooks (can modify headers, response)
416
+ await this.executePluginHooks('onBeforeResponse', responseContext)
417
+ currentResponse = responseContext.response
418
+
419
+ // Execute onResponseTransform hooks (can transform response body)
420
+ const transformContext = {
421
+ ...responseContext,
422
+ response: currentResponse,
423
+ transformed: false,
424
+ originalResponse: currentResponse
425
+ }
426
+ await this.executePluginHooks('onResponseTransform', transformContext)
427
+
428
+ // Use transformed response if any plugin transformed it
429
+ if (transformContext.transformed && transformContext.response) {
430
+ currentResponse = transformContext.response
431
+ responseContext.response = currentResponse
432
+ }
433
+
324
434
  // Log the request automatically (if not disabled in config)
325
435
  if (this.context.config.server.enableRequestLogging !== false) {
326
436
  // Ensure status is always a number (HTTP status code)
@@ -331,8 +441,11 @@ export class FluxStackFramework {
331
441
  logger.request(request.method, url.pathname, status, duration)
332
442
  }
333
443
 
334
- // Execute onResponse hooks for all plugins
444
+ // Execute onResponse hooks for all plugins (final logging, metrics)
335
445
  await this.executePluginHooks('onResponse', responseContext)
446
+
447
+ // Return the potentially transformed response
448
+ return currentResponse
336
449
  })
337
450
  }
338
451
 
@@ -393,8 +506,39 @@ export class FluxStackFramework {
393
506
  try {
394
507
  await hookFn(context)
395
508
  } catch (error) {
509
+ const err = error instanceof Error ? error : new Error(String(error))
396
510
  logger.error(`Plugin '${pluginName}' ${hookName} hook failed`, {
397
- error: (error as Error).message
511
+ error: err.message
512
+ })
513
+
514
+ // Execute onPluginError hooks on all plugins (except the one that failed)
515
+ await this.executePluginErrorHook(pluginName, plugin.version, err)
516
+ }
517
+ }
518
+ }
519
+ }
520
+
521
+ private async executePluginErrorHook(pluginName: string, pluginVersion: string | undefined, error: Error): Promise<void> {
522
+ const loadOrder = this.pluginRegistry.getLoadOrder()
523
+
524
+ for (const otherPluginName of loadOrder) {
525
+ if (otherPluginName === pluginName) continue // Don't notify the plugin that failed
526
+
527
+ const otherPlugin = this.pluginRegistry.get(otherPluginName)
528
+ if (!otherPlugin) continue
529
+
530
+ const hookFn = (otherPlugin as any).onPluginError
531
+ if (typeof hookFn === 'function') {
532
+ try {
533
+ await hookFn({
534
+ pluginName,
535
+ pluginVersion,
536
+ timestamp: Date.now(),
537
+ error
538
+ })
539
+ } catch (hookError) {
540
+ logger.error(`Plugin '${otherPluginName}' onPluginError hook failed`, {
541
+ error: hookError instanceof Error ? hookError.message : String(hookError)
398
542
  })
399
543
  }
400
544
  }
@@ -566,6 +710,15 @@ export class FluxStackFramework {
566
710
  }
567
711
  }
568
712
 
713
+ // Call onBeforeServerStart hooks
714
+ for (const pluginName of loadOrder) {
715
+ const plugin = this.pluginRegistry.get(pluginName)!
716
+
717
+ if (plugin.onBeforeServerStart) {
718
+ await plugin.onBeforeServerStart(this.pluginContext)
719
+ }
720
+ }
721
+
569
722
  // Mount plugin routes if they have a plugin property
570
723
  for (const pluginName of loadOrder) {
571
724
  const plugin = this.pluginRegistry.get(pluginName)!
@@ -585,6 +738,15 @@ export class FluxStackFramework {
585
738
  }
586
739
  }
587
740
 
741
+ // Call onAfterServerStart hooks
742
+ for (const pluginName of loadOrder) {
743
+ const plugin = this.pluginRegistry.get(pluginName)!
744
+
745
+ if (plugin.onAfterServerStart) {
746
+ await plugin.onAfterServerStart(this.pluginContext)
747
+ }
748
+ }
749
+
588
750
  this.isStarted = true
589
751
  logger.debug('All plugins loaded successfully', {
590
752
  pluginCount: loadOrder.length
@@ -602,9 +764,18 @@ export class FluxStackFramework {
602
764
  }
603
765
 
604
766
  try {
605
- // Call onServerStop hooks in reverse order
767
+ // Call onBeforeServerStop hooks in reverse order
606
768
  const loadOrder = this.pluginRegistry.getLoadOrder().reverse()
607
769
 
770
+ for (const pluginName of loadOrder) {
771
+ const plugin = this.pluginRegistry.get(pluginName)!
772
+
773
+ if (plugin.onBeforeServerStop) {
774
+ await plugin.onBeforeServerStop(this.pluginContext)
775
+ }
776
+ }
777
+
778
+ // Call onServerStop hooks in reverse order
608
779
  for (const pluginName of loadOrder) {
609
780
  const plugin = this.pluginRegistry.get(pluginName)!
610
781
 
@@ -649,12 +820,14 @@ export class FluxStackFramework {
649
820
  // Prepare startup info for banner or callback
650
821
  const startupInfo: StartupInfo = {
651
822
  port,
823
+ host: this.context.config.server.host || 'localhost',
652
824
  apiPrefix,
653
825
  environment: this.context.environment,
654
826
  pluginCount: this.pluginRegistry.getAll().length,
655
827
  vitePort: this.context.config.client?.port,
656
828
  viteEmbedded: vitePluginActive, // Vite is embedded when plugin is active
657
- swaggerPath: '/swagger' // TODO: Get from swagger plugin config
829
+ swaggerPath: '/swagger', // TODO: Get from swagger plugin config
830
+ liveComponents: componentRegistry.getRegisteredComponentNames()
658
831
  }
659
832
 
660
833
  // Display banner if enabled
@@ -34,7 +34,11 @@ export class ComponentRegistry {
34
34
  const path = await import('path')
35
35
 
36
36
  if (!fs.existsSync(componentsPath)) {
37
- console.log(`⚠️ Components path not found: ${componentsPath}`)
37
+ // In production, components are already bundled - no need to auto-discover
38
+ const { appConfig } = await import('@/config/app.config')
39
+ if (appConfig.env !== 'production') {
40
+ console.log(`⚠️ Components path not found: ${componentsPath}`)
41
+ }
38
42
  return
39
43
  }
40
44
 
@@ -8,15 +8,17 @@
8
8
  // Import all built-in plugins
9
9
  import { swaggerPlugin } from './swagger'
10
10
  import { vitePlugin } from './vite'
11
- import { staticPlugin } from './static'
12
11
  import { monitoringPlugin } from './monitoring'
13
12
 
14
13
  // Export individual plugins
15
14
  export { swaggerPlugin } from './swagger'
16
15
  export { vitePlugin } from './vite'
17
- export { staticPlugin } from './static'
18
16
  export { monitoringPlugin } from './monitoring'
19
17
 
18
+ // Deprecated: staticPlugin is now merged into vitePlugin (auto-detects dev/prod)
19
+ /** @deprecated Use vitePlugin instead - it now handles both dev and prod */
20
+ export const staticPlugin = vitePlugin
21
+
20
22
  // Export as a collection
21
23
  export const builtInPlugins = {
22
24
  swagger: swaggerPlugin,
@@ -35,7 +37,7 @@ export const builtInPluginsList = [
35
37
 
36
38
  // Plugin categories
37
39
  export const pluginCategories = {
38
- core: [staticPlugin],
40
+ core: [vitePlugin], // vitePlugin now handles both dev (Vite) and prod (static)
39
41
  development: [vitePlugin],
40
42
  documentation: [swaggerPlugin],
41
43
  monitoring: [monitoringPlugin]
@@ -94,11 +96,12 @@ export const defaultPluginConfig = {
94
96
  * Get default plugins for a specific environment
95
97
  */
96
98
  export function getDefaultPlugins(environment: 'development' | 'production' | 'test' = 'development') {
97
- const basePlugins = [staticPlugin]
99
+ // vitePlugin now auto-detects dev/prod and serves appropriately
100
+ const basePlugins = [vitePlugin]
98
101
 
99
102
  switch (environment) {
100
103
  case 'development':
101
- return [...basePlugins, vitePlugin, swaggerPlugin, monitoringPlugin]
104
+ return [...basePlugins, swaggerPlugin, monitoringPlugin]
102
105
  case 'production':
103
106
  return [...basePlugins, monitoringPlugin]
104
107
  case 'test':
@@ -499,15 +499,14 @@ const getClientTemplate = (componentName: string, type: string, room?: string) =
499
499
  switch (type) {
500
500
  case 'counter':
501
501
  return `// 🔥 ${componentName} - Counter Client Component
502
- import React from 'react';
503
- import { useHybridLiveComponent } from 'fluxstack';
502
+ import { useTypedLiveComponent } from '@/core/client';
503
+ import type { InferComponentState } from '@/core/client';
504
504
 
505
- interface ${componentName}State {
506
- count: number;
507
- title: string;
508
- step: number;
509
- lastUpdated: Date;
510
- }
505
+ // Import component type DIRECTLY from backend - full type inference!
506
+ import type { ${componentName}Component } from '@/server/live/${componentName}Component';
507
+
508
+ // State type inferred from backend component
509
+ type ${componentName}State = InferComponentState<${componentName}Component>;
511
510
 
512
511
  const initialState: ${componentName}State = {
513
512
  count: 0,
@@ -517,7 +516,7 @@ const initialState: ${componentName}State = {
517
516
  };
518
517
 
519
518
  export function ${componentName}() {
520
- const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
519
+ const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
521
520
 
522
521
  if (!connected) {
523
522
  return (
@@ -550,23 +549,23 @@ export function ${componentName}() {
550
549
 
551
550
  <div className="flex gap-2 justify-center mb-4">
552
551
  <button
553
- onClick={() => call('decrement')}
552
+ onClick={() => call('decrement', {})}
554
553
  disabled={loading || state.count <= 0}
555
554
  className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
556
555
  >
557
556
  ➖ Decrement
558
557
  </button>
559
-
558
+
560
559
  <button
561
- onClick={() => call('increment')}
560
+ onClick={() => call('increment', {})}
562
561
  disabled={loading}
563
562
  className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
564
563
  >
565
564
  ➕ Increment
566
565
  </button>
567
-
566
+
568
567
  <button
569
- onClick={() => call('reset')}
568
+ onClick={() => call('reset', {})}
570
569
  disabled={loading}
571
570
  className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
572
571
  >
@@ -616,16 +615,14 @@ export function ${componentName}() {
616
615
 
617
616
  case 'form':
618
617
  return `// 🔥 ${componentName} - Form Client Component
619
- import React from 'react';
620
- import { useHybridLiveComponent } from 'fluxstack';
618
+ import { useTypedLiveComponent } from '@/core/client';
619
+ import type { InferComponentState } from '@/core/client';
621
620
 
622
- interface ${componentName}State {
623
- formData: Record<string, any>;
624
- errors: Record<string, string>;
625
- isSubmitting: boolean;
626
- isValid: boolean;
627
- lastUpdated: Date;
628
- }
621
+ // Import component type DIRECTLY from backend - full type inference!
622
+ import type { ${componentName}Component } from '@/server/live/${componentName}Component';
623
+
624
+ // State type inferred from backend component
625
+ type ${componentName}State = InferComponentState<${componentName}Component>;
629
626
 
630
627
  const initialState: ${componentName}State = {
631
628
  formData: {},
@@ -636,7 +633,7 @@ const initialState: ${componentName}State = {
636
633
  };
637
634
 
638
635
  export function ${componentName}() {
639
- const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
636
+ const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
640
637
 
641
638
  if (!connected) {
642
639
  return (
@@ -656,7 +653,7 @@ export function ${componentName}() {
656
653
  const handleSubmit = async (e: React.FormEvent) => {
657
654
  e.preventDefault();
658
655
  try {
659
- const result = await call('submitForm');
656
+ const result = await call('submitForm', {});
660
657
  if (result?.success) {
661
658
  alert('Form submitted successfully!');
662
659
  }
@@ -666,7 +663,7 @@ export function ${componentName}() {
666
663
  };
667
664
 
668
665
  const handleReset = () => {
669
- call('resetForm');
666
+ call('resetForm', {});
670
667
  };
671
668
 
672
669
  return (
@@ -772,23 +769,14 @@ export function ${componentName}() {
772
769
  case 'chat':
773
770
  return `// 🔥 ${componentName} - Chat Client Component
774
771
  import React, { useState, useEffect, useRef } from 'react';
775
- import { useHybridLiveComponent } from 'fluxstack';
772
+ import { useTypedLiveComponent } from '@/core/client';
773
+ import type { InferComponentState } from '@/core/client';
776
774
 
777
- interface Message {
778
- id: string;
779
- text: string;
780
- userId: string;
781
- username: string;
782
- timestamp: Date;
783
- }
775
+ // Import component type DIRECTLY from backend - full type inference!
776
+ import type { ${componentName}Component } from '@/server/live/${componentName}Component';
784
777
 
785
- interface ${componentName}State {
786
- messages: Message[];
787
- users: Record<string, { username: string; isOnline: boolean }>;
788
- currentMessage: string;
789
- isTyping: Record<string, boolean>;
790
- lastUpdated: Date;
791
- }
778
+ // State type inferred from backend component
779
+ type ${componentName}State = InferComponentState<${componentName}Component>;
792
780
 
793
781
  const initialState: ${componentName}State = {
794
782
  messages: [],
@@ -799,7 +787,7 @@ const initialState: ${componentName}State = {
799
787
  };
800
788
 
801
789
  export function ${componentName}() {
802
- const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
790
+ const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
803
791
  const [username, setUsername] = useState(\`User\${Math.random().toString(36).substr(2, 4)}\`);
804
792
  const [hasJoined, setHasJoined] = useState(false);
805
793
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -951,14 +939,14 @@ export function ${componentName}() {
951
939
 
952
940
  default: // basic
953
941
  return `// 🔥 ${componentName} - Client Component
954
- import React from 'react';
955
- import { useHybridLiveComponent } from 'fluxstack';
942
+ import { useTypedLiveComponent } from '@/core/client';
943
+ import type { InferComponentState } from '@/core/client';
956
944
 
957
- interface ${componentName}State {
958
- message: string;
959
- count: number;
960
- lastUpdated: Date;
961
- }
945
+ // Import component type DIRECTLY from backend - full type inference!
946
+ import type { ${componentName}Component } from '@/server/live/${componentName}Component';
947
+
948
+ // State type inferred from backend component
949
+ type ${componentName}State = InferComponentState<${componentName}Component>;
962
950
 
963
951
  const initialState: ${componentName}State = {
964
952
  message: "Loading...",
@@ -967,7 +955,7 @@ const initialState: ${componentName}State = {
967
955
  };
968
956
 
969
957
  export function ${componentName}() {
970
- const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
958
+ const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
971
959
 
972
960
  if (!connected) {
973
961
  return (
@@ -1013,24 +1001,24 @@ export function ${componentName}() {
1013
1001
  </button>
1014
1002
 
1015
1003
  <button
1016
- onClick={() => call('incrementCounter')}
1004
+ onClick={() => call('incrementCounter', {})}
1017
1005
  disabled={loading}
1018
1006
  className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
1019
1007
  >
1020
1008
  ➕ Increment
1021
1009
  </button>
1022
-
1010
+
1023
1011
  <button
1024
- onClick={() => call('resetData')}
1012
+ onClick={() => call('resetData', {})}
1025
1013
  disabled={loading}
1026
1014
  className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
1027
1015
  >
1028
1016
  🔄 Reset
1029
1017
  </button>
1030
-
1018
+
1031
1019
  <button
1032
1020
  onClick={async () => {
1033
- const result = await call('getData');
1021
+ const result = await call('getData', {});
1034
1022
  console.log('Component data:', result);
1035
1023
  alert('Data logged to console');
1036
1024
  }}
@@ -1125,7 +1113,7 @@ export const createLiveComponentCommand: CliCommand = {
1125
1113
 
1126
1114
  // File paths
1127
1115
  const serverFilePath = path.join(context.workingDir, "app", "server", "live", `${componentName}Component.ts`);
1128
- const clientFilePath = path.join(context.workingDir, "app", "client", "src", "components", `${componentName}.tsx`);
1116
+ const clientFilePath = path.join(context.workingDir, "app", "client", "src", "live", `${componentName}.tsx`);
1129
1117
 
1130
1118
  try {
1131
1119
  // Check if files exist (unless force flag is used)
@@ -1165,7 +1153,7 @@ export const createLiveComponentCommand: CliCommand = {
1165
1153
  context.logger.info("📁 Files created:");
1166
1154
  context.logger.info(` 🔥 Server: app/server/live/${componentName}Component.ts`);
1167
1155
  if (!noClient) {
1168
- context.logger.info(` ⚛️ Client: app/client/src/components/${componentName}.tsx`);
1156
+ context.logger.info(` ⚛️ Client: app/client/src/live/${componentName}.tsx`);
1169
1157
  }
1170
1158
 
1171
1159
  context.logger.info("");
@@ -1173,7 +1161,7 @@ export const createLiveComponentCommand: CliCommand = {
1173
1161
  context.logger.info(" 1. Start dev server: bun run dev");
1174
1162
  if (!noClient) {
1175
1163
  context.logger.info(` 2. Import component in your App.tsx:`);
1176
- context.logger.info(` import { ${componentName} } from './components/${componentName}'`);
1164
+ context.logger.info(` import { ${componentName} } from './live/${componentName}'`);
1177
1165
  context.logger.info(` 3. Add component to your JSX: <${componentName} />`);
1178
1166
  }
1179
1167
 
@@ -1186,12 +1174,16 @@ export const createLiveComponentCommand: CliCommand = {
1186
1174
  }
1187
1175
 
1188
1176
  context.logger.info("");
1189
- context.logger.info("📚 Import guide:");
1190
- context.logger.info(" # Use the fluxstack library (recommended):");
1191
- context.logger.info(" import { useHybridLiveComponent } from 'fluxstack';");
1177
+ context.logger.info("📚 Import guide (Type Inference):");
1178
+ context.logger.info(" # Import typed hook and type helpers:");
1179
+ context.logger.info(" import { useTypedLiveComponent } from '@/core/client';");
1180
+ context.logger.info(" import type { InferComponentState } from '@/core/client';");
1181
+ context.logger.info("");
1182
+ context.logger.info(" # Import backend component type for full inference:");
1183
+ context.logger.info(` import type { ${componentName}Component } from '@/server/live/${componentName}Component';`);
1192
1184
  context.logger.info("");
1193
- context.logger.info(" # Alternative - Direct core import:");
1194
- context.logger.info(" import { useHybridLiveComponent } from '@/core/client';");
1185
+ context.logger.info(" # Use with automatic type inference:");
1186
+ context.logger.info(` const { state, call } = useTypedLiveComponent<${componentName}Component>(...);`);
1195
1187
 
1196
1188
  } catch (error) {
1197
1189
  context.logger.error(`❌ Failed to create component files: ${error instanceof Error ? error.message : String(error)}`);