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.
- package/LIVE_COMPONENTS_REVIEW.md +781 -0
- package/app/client/src/App.tsx +39 -43
- package/app/client/src/lib/eden-api.ts +2 -7
- package/app/client/src/live/FileUploadExample.tsx +359 -0
- package/app/client/src/live/MinimalLiveClock.tsx +47 -0
- package/app/client/src/live/QuickUploadTest.tsx +193 -0
- package/app/client/src/main.tsx +10 -10
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +45 -44
- package/app/client/tsconfig.node.json +25 -25
- package/app/server/index.ts +30 -103
- package/app/server/live/LiveFileUploadComponent.ts +77 -0
- package/app/server/live/register-components.ts +19 -19
- package/core/build/bundler.ts +4 -1
- package/core/build/index.ts +124 -4
- package/core/build/live-components-generator.ts +68 -1
- package/core/cli/index.ts +163 -35
- package/core/client/LiveComponentsProvider.tsx +3 -9
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
- package/core/client/hooks/useChunkedUpload.ts +112 -61
- package/core/client/hooks/useHybridLiveComponent.ts +80 -26
- package/core/client/hooks/useTypedLiveComponent.ts +133 -0
- package/core/client/hooks/useWebSocket.ts +4 -16
- package/core/client/index.ts +20 -2
- package/core/framework/server.ts +181 -8
- package/core/live/ComponentRegistry.ts +5 -1
- package/core/plugins/built-in/index.ts +8 -5
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +55 -63
- package/core/plugins/built-in/vite/index.ts +75 -187
- package/core/plugins/built-in/vite/vite-dev.ts +88 -0
- package/core/plugins/registry.ts +54 -2
- package/core/plugins/types.ts +86 -2
- package/core/server/index.ts +1 -2
- package/core/server/live/ComponentRegistry.ts +14 -5
- package/core/server/live/FileUploadManager.ts +22 -25
- package/core/server/live/auto-generated-components.ts +29 -26
- package/core/server/live/websocket-plugin.ts +19 -5
- package/core/server/plugins/static-files-plugin.ts +49 -240
- package/core/server/plugins/swagger.ts +33 -33
- package/core/types/build.ts +1 -0
- package/core/types/plugin.ts +9 -1
- package/core/types/types.ts +137 -0
- package/core/utils/logger/startup-banner.ts +20 -4
- package/core/utils/version.ts +1 -1
- package/eslint.config.js +23 -23
- package/package.json +3 -3
- package/plugins/crypto-auth/server/middlewares.ts +19 -19
- package/tsconfig.json +52 -52
- package/workspace.json +5 -5
package/core/framework/server.ts
CHANGED
|
@@ -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
|
|
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((
|
|
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:
|
|
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
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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,
|
|
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
|
|
503
|
-
import {
|
|
502
|
+
import { useTypedLiveComponent } from '@/core/client';
|
|
503
|
+
import type { InferComponentState } from '@/core/client';
|
|
504
504
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 } =
|
|
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
|
|
620
|
-
import {
|
|
618
|
+
import { useTypedLiveComponent } from '@/core/client';
|
|
619
|
+
import type { InferComponentState } from '@/core/client';
|
|
621
620
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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 } =
|
|
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 {
|
|
772
|
+
import { useTypedLiveComponent } from '@/core/client';
|
|
773
|
+
import type { InferComponentState } from '@/core/client';
|
|
776
774
|
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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 } =
|
|
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
|
|
955
|
-
import {
|
|
942
|
+
import { useTypedLiveComponent } from '@/core/client';
|
|
943
|
+
import type { InferComponentState } from '@/core/client';
|
|
956
944
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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 } =
|
|
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", "
|
|
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/
|
|
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 './
|
|
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(" #
|
|
1191
|
-
context.logger.info(" import {
|
|
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(" #
|
|
1194
|
-
context.logger.info(
|
|
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)}`);
|