create-fluxstack 1.10.1 → 1.12.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 (257) hide show
  1. package/.dockerignore +1 -2
  2. package/Dockerfile +8 -8
  3. package/LLMD/INDEX.md +64 -0
  4. package/LLMD/MAINTENANCE.md +197 -0
  5. package/LLMD/MIGRATION.md +156 -0
  6. package/LLMD/config/.gitkeep +1 -0
  7. package/LLMD/config/declarative-system.md +268 -0
  8. package/LLMD/config/environment-vars.md +327 -0
  9. package/LLMD/config/runtime-reload.md +401 -0
  10. package/LLMD/core/.gitkeep +1 -0
  11. package/LLMD/core/build-system.md +599 -0
  12. package/LLMD/core/framework-lifecycle.md +229 -0
  13. package/LLMD/core/plugin-system.md +451 -0
  14. package/LLMD/patterns/.gitkeep +1 -0
  15. package/LLMD/patterns/anti-patterns.md +297 -0
  16. package/LLMD/patterns/project-structure.md +264 -0
  17. package/LLMD/patterns/type-safety.md +440 -0
  18. package/LLMD/reference/.gitkeep +1 -0
  19. package/LLMD/reference/cli-commands.md +250 -0
  20. package/LLMD/reference/plugin-hooks.md +357 -0
  21. package/LLMD/reference/routing.md +39 -0
  22. package/LLMD/reference/troubleshooting.md +364 -0
  23. package/LLMD/resources/.gitkeep +1 -0
  24. package/LLMD/resources/controllers.md +465 -0
  25. package/LLMD/resources/live-components.md +703 -0
  26. package/LLMD/resources/live-rooms.md +482 -0
  27. package/LLMD/resources/live-upload.md +130 -0
  28. package/LLMD/resources/plugins-external.md +617 -0
  29. package/LLMD/resources/routes-eden.md +254 -0
  30. package/README.md +37 -17
  31. package/app/client/index.html +0 -1
  32. package/app/client/src/App.tsx +107 -150
  33. package/app/client/src/components/AppLayout.tsx +68 -0
  34. package/app/client/src/components/BackButton.tsx +13 -0
  35. package/app/client/src/components/DemoPage.tsx +20 -0
  36. package/app/client/src/components/LiveUploadWidget.tsx +204 -0
  37. package/app/client/src/lib/eden-api.ts +85 -60
  38. package/app/client/src/live/ChatDemo.tsx +107 -0
  39. package/app/client/src/live/CounterDemo.tsx +206 -0
  40. package/app/client/src/live/FormDemo.tsx +119 -0
  41. package/app/client/src/live/RoomChatDemo.tsx +242 -0
  42. package/app/client/src/live/UploadDemo.tsx +21 -0
  43. package/app/client/src/main.tsx +4 -1
  44. package/app/client/src/pages/ApiTestPage.tsx +108 -0
  45. package/app/client/src/pages/HomePage.tsx +76 -0
  46. package/app/server/app.ts +1 -4
  47. package/app/server/controllers/users.controller.ts +36 -44
  48. package/app/server/index.ts +25 -35
  49. package/app/server/live/LiveChat.ts +77 -0
  50. package/app/server/live/LiveCounter.ts +67 -0
  51. package/app/server/live/LiveForm.ts +63 -0
  52. package/app/server/live/LiveLocalCounter.ts +32 -0
  53. package/app/server/live/LiveRoomChat.ts +285 -0
  54. package/app/server/live/LiveUpload.ts +81 -0
  55. package/app/server/routes/index.ts +3 -1
  56. package/app/server/routes/room.routes.ts +117 -0
  57. package/app/server/routes/users.routes.ts +35 -27
  58. package/app/shared/types/index.ts +14 -2
  59. package/config/app.config.ts +2 -62
  60. package/config/client.config.ts +2 -95
  61. package/config/database.config.ts +2 -99
  62. package/config/fluxstack.config.ts +25 -45
  63. package/config/index.ts +57 -38
  64. package/config/monitoring.config.ts +2 -114
  65. package/config/plugins.config.ts +2 -80
  66. package/config/server.config.ts +2 -68
  67. package/config/services.config.ts +2 -130
  68. package/config/system/app.config.ts +29 -0
  69. package/config/system/build.config.ts +49 -0
  70. package/config/system/client.config.ts +68 -0
  71. package/config/system/database.config.ts +17 -0
  72. package/config/system/fluxstack.config.ts +114 -0
  73. package/config/{logger.config.ts → system/logger.config.ts} +3 -1
  74. package/config/system/monitoring.config.ts +114 -0
  75. package/config/system/plugins.config.ts +84 -0
  76. package/config/{runtime.config.ts → system/runtime.config.ts} +1 -1
  77. package/config/system/server.config.ts +68 -0
  78. package/config/system/services.config.ts +46 -0
  79. package/config/{system.config.ts → system/system.config.ts} +1 -1
  80. package/core/build/flux-plugins-generator.ts +325 -325
  81. package/core/build/index.ts +39 -27
  82. package/core/build/live-components-generator.ts +3 -3
  83. package/core/build/optimizer.ts +235 -235
  84. package/core/cli/command-registry.ts +6 -4
  85. package/core/cli/commands/build.ts +79 -0
  86. package/core/cli/commands/create.ts +54 -0
  87. package/core/cli/commands/dev.ts +101 -0
  88. package/core/cli/commands/help.ts +34 -0
  89. package/core/cli/commands/index.ts +34 -0
  90. package/core/cli/commands/make-plugin.ts +90 -0
  91. package/core/cli/commands/plugin-add.ts +197 -0
  92. package/core/cli/commands/plugin-deps.ts +2 -2
  93. package/core/cli/commands/plugin-list.ts +208 -0
  94. package/core/cli/commands/plugin-remove.ts +170 -0
  95. package/core/cli/generators/component.ts +769 -769
  96. package/core/cli/generators/controller.ts +1 -1
  97. package/core/cli/generators/index.ts +146 -146
  98. package/core/cli/generators/interactive.ts +227 -227
  99. package/core/cli/generators/plugin.ts +2 -2
  100. package/core/cli/generators/prompts.ts +82 -82
  101. package/core/cli/generators/route.ts +6 -6
  102. package/core/cli/generators/service.ts +2 -2
  103. package/core/cli/generators/template-engine.ts +4 -3
  104. package/core/cli/generators/types.ts +2 -2
  105. package/core/cli/generators/utils.ts +191 -191
  106. package/core/cli/index.ts +115 -686
  107. package/core/cli/plugin-discovery.ts +2 -2
  108. package/core/client/LiveComponentsProvider.tsx +60 -8
  109. package/core/client/api/eden.ts +183 -0
  110. package/core/client/api/index.ts +11 -0
  111. package/core/client/components/Live.tsx +104 -0
  112. package/core/client/fluxstack.ts +1 -9
  113. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  114. package/core/client/hooks/state-validator.ts +1 -1
  115. package/core/client/hooks/useAuth.ts +48 -48
  116. package/core/client/hooks/useChunkedUpload.ts +85 -35
  117. package/core/client/hooks/useLiveChunkedUpload.ts +87 -0
  118. package/core/client/hooks/useLiveComponent.ts +800 -0
  119. package/core/client/hooks/useLiveUpload.ts +71 -0
  120. package/core/client/hooks/useRoom.ts +409 -0
  121. package/core/client/hooks/useRoomProxy.ts +382 -0
  122. package/core/client/index.ts +17 -68
  123. package/core/client/standalone-entry.ts +8 -0
  124. package/core/client/standalone.ts +74 -53
  125. package/core/client/state/createStore.ts +192 -192
  126. package/core/client/state/index.ts +14 -14
  127. package/core/config/index.ts +70 -291
  128. package/core/config/schema.ts +42 -723
  129. package/core/framework/client.ts +131 -131
  130. package/core/framework/index.ts +7 -7
  131. package/core/framework/server.ts +47 -40
  132. package/core/framework/types.ts +2 -2
  133. package/core/index.ts +23 -4
  134. package/core/live/ComponentRegistry.ts +3 -3
  135. package/core/live/types.ts +77 -0
  136. package/core/plugins/built-in/index.ts +134 -134
  137. package/core/plugins/built-in/live-components/commands/create-live-component.ts +242 -1066
  138. package/core/plugins/built-in/live-components/index.ts +1 -1
  139. package/core/plugins/built-in/monitoring/index.ts +111 -47
  140. package/core/plugins/built-in/static/index.ts +1 -1
  141. package/core/plugins/built-in/swagger/index.ts +68 -265
  142. package/core/plugins/built-in/vite/index.ts +85 -185
  143. package/core/plugins/built-in/vite/vite-dev.ts +10 -16
  144. package/core/plugins/config.ts +9 -7
  145. package/core/plugins/dependency-manager.ts +31 -1
  146. package/core/plugins/discovery.ts +19 -7
  147. package/core/plugins/executor.ts +2 -2
  148. package/core/plugins/index.ts +203 -203
  149. package/core/plugins/manager.ts +27 -39
  150. package/core/plugins/module-resolver.ts +19 -8
  151. package/core/plugins/registry.ts +255 -19
  152. package/core/plugins/types.ts +20 -53
  153. package/core/server/framework.ts +66 -43
  154. package/core/server/index.ts +15 -15
  155. package/core/server/live/ComponentRegistry.ts +78 -71
  156. package/core/server/live/FileUploadManager.ts +23 -10
  157. package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
  158. package/core/server/live/LiveRoomManager.ts +261 -0
  159. package/core/server/live/RoomEventBus.ts +234 -0
  160. package/core/server/live/RoomStateManager.ts +172 -0
  161. package/core/server/live/StateSignature.ts +643 -643
  162. package/core/server/live/WebSocketConnectionManager.ts +30 -19
  163. package/core/server/live/auto-generated-components.ts +21 -9
  164. package/core/server/live/index.ts +14 -0
  165. package/core/server/live/websocket-plugin.ts +214 -67
  166. package/core/server/middleware/elysia-helpers.ts +7 -2
  167. package/core/server/middleware/errorHandling.ts +1 -1
  168. package/core/server/middleware/index.ts +31 -31
  169. package/core/server/plugins/database.ts +180 -180
  170. package/core/server/plugins/static-files-plugin.ts +69 -69
  171. package/core/server/plugins/swagger.ts +1 -1
  172. package/core/server/rooms/RoomBroadcaster.ts +357 -0
  173. package/core/server/rooms/RoomSystem.ts +463 -0
  174. package/core/server/rooms/index.ts +13 -0
  175. package/core/server/services/BaseService.ts +1 -1
  176. package/core/server/services/ServiceContainer.ts +1 -1
  177. package/core/server/services/index.ts +8 -8
  178. package/core/templates/create-project.ts +12 -12
  179. package/core/testing/index.ts +9 -9
  180. package/core/testing/setup.ts +73 -73
  181. package/core/types/api.ts +168 -168
  182. package/core/types/build.ts +219 -219
  183. package/core/types/config.ts +56 -26
  184. package/core/types/index.ts +4 -4
  185. package/core/types/plugin.ts +107 -107
  186. package/core/types/types.ts +353 -14
  187. package/core/utils/build-logger.ts +324 -324
  188. package/core/utils/config-schema.ts +480 -480
  189. package/core/utils/env.ts +2 -8
  190. package/core/utils/errors/codes.ts +114 -114
  191. package/core/utils/errors/handlers.ts +36 -1
  192. package/core/utils/errors/index.ts +49 -5
  193. package/core/utils/errors/middleware.ts +113 -113
  194. package/core/utils/helpers.ts +6 -16
  195. package/core/utils/index.ts +17 -17
  196. package/core/utils/logger/colors.ts +114 -114
  197. package/core/utils/logger/config.ts +13 -9
  198. package/core/utils/logger/formatter.ts +82 -82
  199. package/core/utils/logger/group-logger.ts +101 -101
  200. package/core/utils/logger/index.ts +6 -1
  201. package/core/utils/logger/stack-trace.ts +3 -1
  202. package/core/utils/logger/startup-banner.ts +82 -82
  203. package/core/utils/logger/winston-logger.ts +152 -152
  204. package/core/utils/monitoring/index.ts +211 -211
  205. package/core/utils/sync-version.ts +66 -66
  206. package/core/utils/version.ts +1 -1
  207. package/create-fluxstack.ts +8 -7
  208. package/package.json +12 -13
  209. package/plugins/crypto-auth/cli/make-protected-route.command.ts +1 -1
  210. package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
  211. package/plugins/crypto-auth/client/components/index.ts +11 -11
  212. package/plugins/crypto-auth/client/index.ts +11 -11
  213. package/plugins/crypto-auth/config/index.ts +1 -1
  214. package/plugins/crypto-auth/index.ts +4 -4
  215. package/plugins/crypto-auth/package.json +65 -65
  216. package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
  217. package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
  218. package/plugins/crypto-auth/server/index.ts +21 -21
  219. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +3 -3
  220. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
  221. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +2 -2
  222. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +2 -2
  223. package/plugins/crypto-auth/server/middlewares/helpers.ts +1 -1
  224. package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
  225. package/tsconfig.api-strict.json +16 -0
  226. package/tsconfig.json +48 -52
  227. package/{app/client/tsconfig.node.json → tsconfig.node.json} +25 -25
  228. package/types/global.d.ts +29 -29
  229. package/types/vitest.d.ts +8 -8
  230. package/vite.config.ts +38 -62
  231. package/vitest.config.live.ts +10 -9
  232. package/vitest.config.ts +29 -17
  233. package/app/client/README.md +0 -69
  234. package/app/client/SIMPLIFICATION.md +0 -140
  235. package/app/client/frontend-only.ts +0 -12
  236. package/app/client/src/live/FileUploadExample.tsx +0 -359
  237. package/app/client/src/live/MinimalLiveClock.tsx +0 -47
  238. package/app/client/src/live/QuickUploadTest.tsx +0 -193
  239. package/app/client/tsconfig.app.json +0 -45
  240. package/app/client/tsconfig.json +0 -7
  241. package/app/client/zustand-setup.md +0 -65
  242. package/app/server/backend-only.ts +0 -18
  243. package/app/server/live/LiveClockComponent.ts +0 -215
  244. package/app/server/live/LiveFileUploadComponent.ts +0 -77
  245. package/app/server/routes/env-test.ts +0 -110
  246. package/core/client/hooks/index.ts +0 -7
  247. package/core/client/hooks/useHybridLiveComponent.ts +0 -685
  248. package/core/client/hooks/useTypedLiveComponent.ts +0 -133
  249. package/core/client/hooks/useWebSocket.ts +0 -361
  250. package/core/config/env.ts +0 -546
  251. package/core/config/loader.ts +0 -522
  252. package/core/config/runtime-config.ts +0 -327
  253. package/core/config/validator.ts +0 -540
  254. package/core/server/backend-entry.ts +0 -51
  255. package/core/server/standalone.ts +0 -106
  256. package/core/utils/regenerate-files.ts +0 -69
  257. package/fluxstack.config.ts +0 -354
@@ -1,1193 +1,369 @@
1
- import type { CliCommand } from "@/core/plugins/types";
1
+ import type { CliCommand } from "@core/plugins/types";
2
2
  import { promises as fs } from "fs";
3
3
  import path from "path";
4
4
 
5
- // Component templates for different types
6
- const getServerTemplate = (componentName: string, type: string, room?: string) => {
7
- const roomComment = room ? `\n // Default room: ${room}` : '';
8
- const roomInit = room ? `\n this.room = '${room}';` : '';
5
+ // ===== SERVER TEMPLATES =====
6
+
7
+ const getServerTemplate = (name: string, type: string, room?: string, hasClient = true) => {
8
+ // Se room for especificado, precisa de constructor para definir this.room
9
+ const needsConstructor = !!room;
10
+ const constructorBlock = needsConstructor ? `
11
+ constructor(initialState: Partial<typeof ${name}.defaultState> = {}, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
12
+ super(initialState, ws, options)
13
+ this.room = '${room}'
14
+ }
15
+ ` : '';
16
+
17
+ const clientLink = hasClient ? `
18
+ // Componente Cliente (Ctrl+Click para navegar)
19
+ import type { ${name}Demo as _Client } from '@client/src/live/${name}'
20
+ ` : '';
9
21
 
10
22
  switch (type) {
11
23
  case 'counter':
12
- return `// 🔥 ${componentName} - Counter Live Component
13
- import { LiveComponent } from "@/core/types/types";
24
+ return `// ${name} - Contador
25
+ import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
26
+ ${clientLink}
14
27
 
15
- interface ${componentName}State {
16
- count: number;
17
- title: string;
18
- step: number;
19
- lastUpdated: Date;
20
- }
21
-
22
- export class ${componentName}Component extends LiveComponent<${componentName}State> {
23
- constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
24
- super({
25
- count: 0,
26
- title: "${componentName} Counter",
27
- step: 1,
28
- lastUpdated: new Date(),
29
- ...initialState
30
- }, ws, options);${roomComment}${roomInit}
31
-
32
- console.log(\`🔢 \${this.constructor.name} component created: \${this.id}\`);
28
+ export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
29
+ static componentName = '${name}'
30
+ static defaultState = {
31
+ count: 0
33
32
  }
34
-
35
- async increment(amount: number = this.state.step) {
36
- const newCount = this.state.count + amount;
37
-
38
- this.setState({
39
- count: newCount,
40
- lastUpdated: new Date()
41
- });
42
-
43
- // Broadcast to room for multi-user sync
44
- if (this.room) {
45
- this.broadcast('COUNTER_INCREMENTED', {
46
- count: newCount,
47
- amount,
48
- userId: this.userId
49
- });
50
- }
51
-
52
- console.log(\`🔢 Counter incremented to \${newCount} (step: \${amount})\`);
53
- return { success: true, count: newCount };
33
+ ${constructorBlock}
34
+ async increment() {
35
+ this.state.count++
36
+ return { success: true, count: this.state.count }
54
37
  }
55
38
 
56
- async decrement(amount: number = this.state.step) {
57
- const newCount = Math.max(0, this.state.count - amount);
58
-
59
- this.setState({
60
- count: newCount,
61
- lastUpdated: new Date()
62
- });
63
-
64
- if (this.room) {
65
- this.broadcast('COUNTER_DECREMENTED', {
66
- count: newCount,
67
- amount,
68
- userId: this.userId
69
- });
70
- }
71
-
72
- console.log(\`🔢 Counter decremented to \${newCount} (step: \${amount})\`);
73
- return { success: true, count: newCount };
39
+ async decrement() {
40
+ this.state.count--
41
+ return { success: true, count: this.state.count }
74
42
  }
75
43
 
76
44
  async reset() {
77
- this.setState({
78
- count: 0,
79
- lastUpdated: new Date()
80
- });
81
-
82
- if (this.room) {
83
- this.broadcast('COUNTER_RESET', { userId: this.userId });
84
- }
85
-
86
- console.log(\`🔢 Counter reset\`);
87
- return { success: true, count: 0 };
88
- }
89
-
90
- async setStep(step: number) {
91
- this.setState({
92
- step: Math.max(1, step),
93
- lastUpdated: new Date()
94
- });
95
-
96
- return { success: true, step };
45
+ this.state.count = 0
46
+ return { success: true }
97
47
  }
98
-
99
- async updateTitle(data: { title: string }) {
100
- const newTitle = data.title.trim();
101
-
102
- if (!newTitle || newTitle.length > 50) {
103
- throw new Error('Title must be 1-50 characters');
104
- }
105
-
106
- this.setState({
107
- title: newTitle,
108
- lastUpdated: new Date()
109
- });
110
-
111
- return { success: true, title: newTitle };
112
- }
113
- }`;
114
-
115
- case 'form':
116
- return `// 🔥 ${componentName} - Form Live Component
117
- import { LiveComponent } from "@/core/types/types";
118
-
119
- interface ${componentName}State {
120
- formData: Record<string, any>;
121
- errors: Record<string, string>;
122
- isSubmitting: boolean;
123
- isValid: boolean;
124
- lastUpdated: Date;
125
48
  }
49
+ `;
126
50
 
127
- export class ${componentName}Component extends LiveComponent<${componentName}State> {
128
- constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
129
- super({
130
- formData: {},
131
- errors: {},
132
- isSubmitting: false,
133
- isValid: false,
134
- lastUpdated: new Date(),
135
- ...initialState
136
- }, ws, options);${roomComment}${roomInit}
137
-
138
- console.log(\`📝 \${this.constructor.name} component created: \${this.id}\`);
139
- }
140
-
141
- async updateField(data: { field: string; value: any }) {
142
- const { field, value } = data;
143
- const newFormData = { ...this.state.formData, [field]: value };
144
- const newErrors = { ...this.state.errors };
145
-
146
- // Clear error for this field
147
- delete newErrors[field];
148
-
149
- // Basic validation example
150
- if (field === 'email' && value && !this.isValidEmail(value)) {
151
- newErrors[field] = 'Invalid email format';
152
- }
153
-
154
- this.setState({
155
- formData: newFormData,
156
- errors: newErrors,
157
- isValid: Object.keys(newErrors).length === 0,
158
- lastUpdated: new Date()
159
- });
160
-
161
- return { success: true, field, value };
162
- }
163
-
164
- async submitForm() {
165
- this.setState({ isSubmitting: true });
166
-
167
- try {
168
- // Simulate form submission
169
- await this.sleep(1000);
170
-
171
- // Validate all fields
172
- const errors = this.validateForm(this.state.formData);
173
-
174
- if (Object.keys(errors).length > 0) {
175
- this.setState({
176
- errors,
177
- isSubmitting: false,
178
- isValid: false
179
- });
180
- return { success: false, errors };
181
- }
182
-
183
- // Success
184
- this.setState({
185
- isSubmitting: false,
186
- errors: {},
187
- isValid: true,
188
- lastUpdated: new Date()
189
- });
190
-
191
- if (this.room) {
192
- this.broadcast('FORM_SUBMITTED', {
193
- formData: this.state.formData,
194
- userId: this.userId
195
- });
196
- }
197
-
198
- console.log(\`📝 Form submitted successfully\`);
199
- return { success: true, data: this.state.formData };
200
-
201
- } catch (error: any) {
202
- this.setState({ isSubmitting: false });
203
- throw error;
204
- }
205
- }
206
-
207
- async resetForm() {
208
- this.setState({
209
- formData: {},
210
- errors: {},
211
- isSubmitting: false,
212
- isValid: false,
213
- lastUpdated: new Date()
214
- });
215
-
216
- return { success: true };
217
- }
218
-
219
- private validateForm(data: Record<string, any>): Record<string, string> {
220
- const errors: Record<string, string> = {};
221
-
222
- // Add your validation rules here
223
- if (!data.name || data.name.trim().length < 2) {
224
- errors.name = 'Name must be at least 2 characters';
225
- }
226
-
227
- if (!data.email || !this.isValidEmail(data.email)) {
228
- errors.email = 'Valid email is required';
51
+ case 'form':
52
+ return `// ${name} - Formulário
53
+ import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
54
+ ${clientLink}
55
+
56
+ export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
57
+ static componentName = '${name}'
58
+ static defaultState = {
59
+ name: '',
60
+ email: '',
61
+ message: '',
62
+ submitted: false,
63
+ submittedAt: null as string | null
64
+ }
65
+ ${constructorBlock}
66
+ async submit() {
67
+ if (!this.state.name?.trim() || !this.state.email?.trim()) {
68
+ throw new Error('Nome e email são obrigatórios')
229
69
  }
230
-
231
- return errors;
232
- }
233
-
234
- private isValidEmail(email: string): boolean {
235
- return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
70
+ this.setState({ submitted: true, submittedAt: new Date().toISOString() })
71
+ console.log(\`📝 Form submitted:\`, { name: this.state.name, email: this.state.email })
72
+ return { success: true, data: this.state }
236
73
  }
237
74
 
238
- private sleep(ms: number): Promise<void> {
239
- return new Promise(resolve => setTimeout(resolve, ms));
75
+ async reset() {
76
+ this.setState({ ...${name}.defaultState })
77
+ return { success: true }
240
78
  }
241
- }`;
242
-
243
- case 'chat':
244
- return `// 🔥 ${componentName} - Chat Live Component
245
- import { LiveComponent } from "@/core/types/types";
246
-
247
- interface Message {
248
- id: string;
249
- text: string;
250
- userId: string;
251
- username: string;
252
- timestamp: Date;
253
79
  }
80
+ `;
254
81
 
255
- interface ${componentName}State {
256
- messages: Message[];
257
- users: Record<string, { username: string; isOnline: boolean }>;
258
- currentMessage: string;
259
- isTyping: Record<string, boolean>;
260
- lastUpdated: Date;
261
- }
262
-
263
- export class ${componentName}Component extends LiveComponent<${componentName}State> {
264
- constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
265
- super({
266
- messages: [],
267
- users: {},
268
- currentMessage: "",
269
- isTyping: {},
270
- lastUpdated: new Date(),
271
- ...initialState
272
- }, ws, options);${roomComment}${roomInit}
273
-
274
- console.log(\`💬 \${this.constructor.name} component created: \${this.id}\`);
275
-
276
- // Add user to online users
277
- if (this.userId) {
278
- this.addUser(this.userId, \`User \${this.userId.slice(-4)}\`);
279
- }
280
- }
281
-
282
- async sendMessage(data: { text: string; username?: string }) {
283
- const { text, username = \`User \${this.userId?.slice(-4) || 'Anonymous'}\` } = data;
284
-
285
- if (!text.trim()) {
286
- throw new Error('Message cannot be empty');
287
- }
288
-
289
- const message: Message = {
290
- id: \`msg-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`,
291
- text: text.trim(),
292
- userId: this.userId || 'anonymous',
293
- username,
294
- timestamp: new Date()
295
- };
296
-
297
- const newMessages = [...this.state.messages, message];
298
-
299
- this.setState({
300
- messages: newMessages.slice(-50), // Keep last 50 messages
301
- currentMessage: "",
302
- lastUpdated: new Date()
303
- });
304
-
305
- // Broadcast to all users in the room
306
- if (this.room) {
307
- this.broadcast('NEW_MESSAGE', {
308
- message,
309
- totalMessages: newMessages.length
310
- });
311
- }
312
-
313
- console.log(\`💬 Message sent: "\${text}" by \${username}\`);
314
- return { success: true, message };
315
- }
316
-
317
- async updateTyping(data: { isTyping: boolean; username?: string }) {
318
- const { isTyping, username = \`User \${this.userId?.slice(-4)}\` } = data;
319
- const userId = this.userId || 'anonymous';
320
-
321
- const newTyping = { ...this.state.isTyping };
322
-
323
- if (isTyping) {
324
- newTyping[userId] = true;
325
- } else {
326
- delete newTyping[userId];
327
- }
328
-
329
- this.setState({
330
- isTyping: newTyping,
331
- lastUpdated: new Date()
332
- });
333
-
334
- // Broadcast typing status
335
- if (this.room) {
336
- this.broadcast('USER_TYPING', {
337
- userId,
338
- username,
339
- isTyping
340
- });
341
- }
342
-
343
- // Auto-clear typing after 3 seconds
344
- if (isTyping) {
345
- setTimeout(() => {
346
- this.updateTyping({ isTyping: false, username });
347
- }, 3000);
348
- }
349
-
350
- return { success: true };
351
- }
352
-
353
- async joinRoom(data: { username: string }) {
354
- const { username } = data;
355
-
356
- this.addUser(this.userId || 'anonymous', username);
357
-
358
- if (this.room) {
359
- this.broadcast('USER_JOINED', {
360
- userId: this.userId,
361
- username,
362
- timestamp: new Date()
363
- });
82
+ case 'chat':
83
+ return `// ${name} - Chat
84
+ import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
85
+ ${clientLink}
86
+
87
+ export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
88
+ static componentName = '${name}'
89
+ static defaultState = {
90
+ messages: [] as Array<{ id: string; text: string; username: string; timestamp: string }>,
91
+ username: '',
92
+ currentMessage: ''
93
+ }
94
+ ${constructorBlock}
95
+ async sendMessage(payload: { text: string }) {
96
+ if (!payload.text?.trim()) throw new Error('Message cannot be empty')
97
+ const message = {
98
+ id: \`msg-\${Date.now()}-\${Math.random().toString(36).slice(2, 9)}\`,
99
+ text: payload.text.trim(),
100
+ username: this.state.username || 'Anonymous',
101
+ timestamp: new Date().toISOString()
364
102
  }
365
-
366
- return { success: true, username };
103
+ this.setState({ messages: [...this.state.messages.slice(-49), message], currentMessage: '' })
104
+ if (this.room) this.broadcast('NEW_MESSAGE', { message })
105
+ return { success: true, message }
367
106
  }
368
107
 
369
- async leaveRoom() {
370
- const userId = this.userId || 'anonymous';
371
- const users = { ...this.state.users };
372
- delete users[userId];
373
-
374
- this.setState({
375
- users,
376
- lastUpdated: new Date()
377
- });
378
-
379
- if (this.room) {
380
- this.broadcast('USER_LEFT', {
381
- userId,
382
- timestamp: new Date()
383
- });
108
+ async setUsername(payload: { username: string }) {
109
+ if (!payload.username?.trim() || payload.username.length > 20) {
110
+ throw new Error('Username must be 1-20 characters')
384
111
  }
385
-
386
- return { success: true };
387
- }
388
-
389
- private addUser(userId: string, username: string) {
390
- const users = {
391
- ...this.state.users,
392
- [userId]: { username, isOnline: true }
393
- };
394
-
395
- this.setState({
396
- users,
397
- lastUpdated: new Date()
398
- });
399
- }
400
-
401
- public destroy() {
402
- this.leaveRoom();
403
- super.destroy();
112
+ this.setState({ username: payload.username.trim() })
113
+ return { success: true }
404
114
  }
405
- }`;
115
+ }
116
+ `;
406
117
 
407
118
  default: // basic
408
- return `// 🔥 ${componentName} - Live Component
409
- import { LiveComponent } from "@/core/types/types";
410
-
411
- interface ${componentName}State {
412
- message: string;
413
- count: number;
414
- lastUpdated: Date;
415
- }
119
+ return `// ${name} - Live Component
120
+ import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
121
+ ${clientLink}
416
122
 
417
- export class ${componentName}Component extends LiveComponent<${componentName}State> {
418
- constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
419
- super({
420
- message: "Hello from ${componentName}!",
421
- count: 0,
422
- lastUpdated: new Date(),
423
- ...initialState
424
- }, ws, options);${roomComment}${roomInit}
425
-
426
- console.log(\`🔥 \${this.constructor.name} component created: \${this.id}\`);
123
+ export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
124
+ static componentName = '${name}'
125
+ static defaultState = {
126
+ message: 'Hello from ${name}!',
127
+ count: 0
427
128
  }
428
-
129
+ ${constructorBlock}
429
130
  async updateMessage(payload: { message: string }) {
430
- const { message } = payload;
431
-
432
- if (!message || message.trim().length === 0) {
433
- throw new Error('Message cannot be empty');
434
- }
435
-
436
- this.setState({
437
- message: message.trim(),
438
- lastUpdated: new Date()
439
- });
440
-
441
- // Broadcast to room if in multi-user mode
442
- if (this.room) {
443
- this.broadcast('MESSAGE_UPDATED', {
444
- message: message.trim(),
445
- userId: this.userId
446
- });
447
- }
448
-
449
- console.log(\`📝 Message updated: "\${message}"\`);
450
- return { success: true, message: message.trim() };
131
+ if (!payload.message?.trim()) throw new Error('Message cannot be empty')
132
+ this.state.message = payload.message.trim()
133
+ return { success: true }
451
134
  }
452
135
 
453
- async incrementCounter() {
454
- const newCount = this.state.count + 1;
455
-
456
- this.setState({
457
- count: newCount,
458
- lastUpdated: new Date()
459
- });
460
-
461
- if (this.room) {
462
- this.broadcast('COUNTER_INCREMENTED', {
463
- count: newCount,
464
- userId: this.userId
465
- });
466
- }
467
-
468
- return { success: true, count: newCount };
136
+ async increment() {
137
+ this.state.count++
138
+ return { success: true, count: this.state.count }
469
139
  }
470
140
 
471
- async resetData() {
472
- this.setState({
473
- message: "Hello from ${componentName}!",
474
- count: 0,
475
- lastUpdated: new Date()
476
- });
477
-
478
- return { success: true };
479
- }
480
-
481
- async getData() {
482
- return {
483
- success: true,
484
- data: {
485
- ...this.state,
486
- componentId: this.id,
487
- room: this.room,
488
- userId: this.userId
489
- }
490
- };
141
+ async reset() {
142
+ this.setState({ ...${name}.defaultState })
143
+ return { success: true }
491
144
  }
492
- }`;
145
+ }
146
+ `;
493
147
  }
494
148
  };
495
149
 
496
- const getClientTemplate = (componentName: string, type: string, room?: string) => {
497
- const roomProps = room ? `, { room: '${room}' }` : '';
150
+ // ===== CLIENT TEMPLATES =====
498
151
 
152
+ const getClientTemplate = (name: string, type: string) => {
499
153
  switch (type) {
500
154
  case 'counter':
501
- return `// 🔥 ${componentName} - Counter Client Component
502
- import { useTypedLiveComponent } from '@/core/client';
503
- import type { InferComponentState } from '@/core/client';
504
-
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>;
510
-
511
- const initialState: ${componentName}State = {
512
- count: 0,
513
- title: "${componentName} Counter",
514
- step: 1,
515
- lastUpdated: new Date(),
516
- };
155
+ return `// 🔥 ${name}
156
+ import { Live } from '@/core/client'
157
+ import { ${name} } from '@server/live/${name}'
517
158
 
518
- export function ${componentName}() {
519
- const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
159
+ export function ${name}Demo() {
160
+ const counter = Live.use(${name}, {
161
+ initialState: ${name}.defaultState
162
+ })
520
163
 
521
- if (!connected) {
522
- return (
523
- <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
524
- <div className="text-center">
525
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
526
- <p className="text-gray-600">Connecting to ${componentName}...</p>
527
- </div>
528
- </div>
529
- );
530
- }
164
+ if (!counter.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
531
165
 
532
166
  return (
533
- <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 relative">
534
- <div className="flex items-center justify-between mb-4">
535
- <h2 className="text-2xl font-bold text-gray-800">{state.title}</h2>
536
- <span className={
537
- \`px-2 py-1 rounded-full text-xs font-medium \${
538
- connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
539
- }\`
540
- }>
541
- {connected ? '🟢 Connected' : '🔴 Disconnected'}
542
- </span>
543
- </div>
544
-
545
- <div className="text-center mb-6">
546
- <div className="text-6xl font-bold text-blue-600 mb-2">{state.count}</div>
547
- <p className="text-gray-600">Current Count</p>
548
- </div>
549
-
550
- <div className="flex gap-2 justify-center mb-4">
551
- <button
552
- onClick={() => call('decrement', {})}
553
- disabled={loading || state.count <= 0}
554
- className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
555
- >
556
- ➖ Decrement
557
- </button>
558
-
559
- <button
560
- onClick={() => call('increment', {})}
561
- disabled={loading}
562
- className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
563
- >
564
- ➕ Increment
565
- </button>
566
-
567
- <button
568
- onClick={() => call('reset', {})}
569
- disabled={loading}
570
- className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
571
- >
572
- 🔄 Reset
573
- </button>
574
- </div>
575
-
576
- <div className="border-t pt-4">
577
- <div className="flex gap-2 items-center mb-2">
578
- <label className="text-sm font-medium text-gray-700">Step Size:</label>
579
- <input
580
- type="number"
581
- min="1"
582
- max="10"
583
- value={state.step}
584
- onChange={(e) => call('setStep', parseInt(e.target.value) || 1)}
585
- className="w-20 px-2 py-1 border rounded"
586
- disabled={loading}
587
- />
588
- </div>
589
-
590
- <div className="flex gap-2 items-center">
591
- <label className="text-sm font-medium text-gray-700">Title:</label>
592
- <input
593
- type="text"
594
- value={state.title}
595
- onChange={(e) => call('updateTitle', { title: e.target.value })}
596
- className="flex-1 px-2 py-1 border rounded"
597
- disabled={loading}
598
- maxLength={50}
599
- />
600
- </div>
601
- </div>
602
-
603
- <div className="mt-4 text-xs text-gray-500 text-center">
604
- Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
167
+ <div className="p-6 bg-white rounded-xl shadow-sm border">
168
+ <h2 className="text-xl font-bold mb-4">${name}</h2>
169
+ <div className="text-5xl font-bold text-blue-600 text-center mb-6">{counter.count}</div>
170
+ <div className="flex gap-2 justify-center">
171
+ <button onClick={() => counter.decrement()} disabled={counter.count <= 0} className="px-4 py-2 bg-red-500 text-white rounded-lg disabled:opacity-50">-</button>
172
+ <button onClick={() => counter.increment()} className="px-4 py-2 bg-blue-500 text-white rounded-lg">+</button>
173
+ <button onClick={() => counter.reset()} className="px-4 py-2 bg-gray-500 text-white rounded-lg">Reset</button>
605
174
  </div>
606
-
607
- {loading && (
608
- <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
609
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
610
- </div>
611
- )}
612
175
  </div>
613
- );
614
- }`;
176
+ )
177
+ }
178
+ `;
615
179
 
616
180
  case 'form':
617
- return `// 🔥 ${componentName} - Form Client Component
618
- import { useTypedLiveComponent } from '@/core/client';
619
- import type { InferComponentState } from '@/core/client';
181
+ return `// 🔥 ${name}
182
+ import { Live } from '@/core/client'
183
+ import { ${name} } from '@server/live/${name}'
620
184
 
621
- // Import component type DIRECTLY from backend - full type inference!
622
- import type { ${componentName}Component } from '@/server/live/${componentName}Component';
185
+ export function ${name}Demo() {
186
+ const form = Live.use(${name}, {
187
+ initialState: ${name}.defaultState
188
+ })
623
189
 
624
- // State type inferred from backend component
625
- type ${componentName}State = InferComponentState<${componentName}Component>;
190
+ if (!form.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
626
191
 
627
- const initialState: ${componentName}State = {
628
- formData: {},
629
- errors: {},
630
- isSubmitting: false,
631
- isValid: false,
632
- lastUpdated: new Date(),
633
- };
634
-
635
- export function ${componentName}() {
636
- const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
637
-
638
- if (!connected) {
192
+ if (form.submitted) {
639
193
  return (
640
- <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
641
- <div className="text-center">
642
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
643
- <p className="text-gray-600">Connecting to ${componentName}...</p>
644
- </div>
194
+ <div className="p-6 bg-green-50 rounded-xl text-center">
195
+ <h2 className="text-xl font-bold text-green-700 mb-2">Enviado!</h2>
196
+ <p className="text-gray-600">Obrigado, {form.name}!</p>
197
+ <button onClick={() => form.reset()} className="mt-4 px-4 py-2 bg-green-500 text-white rounded-lg">Novo</button>
645
198
  </div>
646
- );
199
+ )
647
200
  }
648
201
 
649
- const handleFieldChange = (field: string, value: any) => {
650
- call('updateField', { field, value });
651
- };
652
-
653
- const handleSubmit = async (e: React.FormEvent) => {
654
- e.preventDefault();
655
- try {
656
- const result = await call('submitForm', {});
657
- if (result?.success) {
658
- alert('Form submitted successfully!');
659
- }
660
- } catch (error) {
661
- console.error('Submit error:', error);
662
- }
663
- };
664
-
665
- const handleReset = () => {
666
- call('resetForm', {});
667
- };
668
-
669
202
  return (
670
- <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 max-w-md mx-auto">
671
- <div className="flex items-center justify-between mb-4">
672
- <h2 className="text-2xl font-bold text-gray-800">${componentName} Form</h2>
673
- <span className={
674
- \`px-2 py-1 rounded-full text-xs font-medium \${
675
- connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
676
- }\`
677
- }>
678
- {connected ? '🟢 Connected' : '🔴 Disconnected'}
679
- </span>
680
- </div>
681
-
682
- <form onSubmit={handleSubmit} className="space-y-4">
683
- <div>
684
- <label className="block text-sm font-medium text-gray-700 mb-1">
685
- Name *
686
- </label>
687
- <input
688
- type="text"
689
- value={state.formData.name || ''}
690
- onChange={(e) => handleFieldChange('name', e.target.value)}
691
- className={\`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 \${
692
- state.errors.name ? 'border-red-500' : 'border-gray-300'
693
- }\`}
694
- disabled={loading || state.isSubmitting}
695
- placeholder="Enter your name"
696
- />
697
- {state.errors.name && (
698
- <p className="text-red-500 text-xs mt-1">{state.errors.name}</p>
699
- )}
700
- </div>
701
-
702
- <div>
703
- <label className="block text-sm font-medium text-gray-700 mb-1">
704
- Email *
705
- </label>
706
- <input
707
- type="email"
708
- value={state.formData.email || ''}
709
- onChange={(e) => handleFieldChange('email', e.target.value)}
710
- className={\`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 \${
711
- state.errors.email ? 'border-red-500' : 'border-gray-300'
712
- }\`}
713
- disabled={loading || state.isSubmitting}
714
- placeholder="Enter your email"
715
- />
716
- {state.errors.email && (
717
- <p className="text-red-500 text-xs mt-1">{state.errors.email}</p>
718
- )}
719
- </div>
720
-
721
- <div>
722
- <label className="block text-sm font-medium text-gray-700 mb-1">
723
- Message
724
- </label>
725
- <textarea
726
- value={state.formData.message || ''}
727
- onChange={(e) => handleFieldChange('message', e.target.value)}
728
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
729
- disabled={loading || state.isSubmitting}
730
- placeholder="Enter your message"
731
- rows={3}
732
- />
733
- </div>
734
-
203
+ <div className="p-6 bg-white rounded-xl shadow-sm border">
204
+ <h2 className="text-xl font-bold mb-4">${name}</h2>
205
+ <div className="space-y-4">
206
+ <input {...form.$field('name', { syncOn: 'blur' })} placeholder="Nome" className="w-full px-3 py-2 border rounded-lg" />
207
+ <input {...form.$field('email', { syncOn: 'change', debounce: 500 })} type="email" placeholder="Email" className="w-full px-3 py-2 border rounded-lg" />
208
+ <textarea {...form.$field('message', { syncOn: 'blur' })} placeholder="Mensagem" rows={3} className="w-full px-3 py-2 border rounded-lg" />
735
209
  <div className="flex gap-2">
736
- <button
737
- type="submit"
738
- disabled={loading || state.isSubmitting || !state.isValid}
739
- className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
740
- >
741
- {state.isSubmitting ? (
742
- <>
743
- <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
744
- Submitting...
745
- </>
746
- ) : (
747
- '📤 Submit'
748
- )}
749
- </button>
750
-
751
- <button
752
- type="button"
753
- onClick={handleReset}
754
- disabled={loading || state.isSubmitting}
755
- className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
756
- >
757
- 🔄 Reset
758
- </button>
210
+ <button onClick={async () => { try { await form.$sync(); await form.submit() } catch (e: any) { alert(e.message) }}} className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg">Enviar</button>
211
+ <button onClick={() => form.reset()} className="px-4 py-2 bg-gray-500 text-white rounded-lg">Limpar</button>
759
212
  </div>
760
- </form>
761
-
762
- <div className="mt-4 text-xs text-gray-500 text-center">
763
- Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
764
213
  </div>
765
214
  </div>
766
- );
767
- }`;
215
+ )
216
+ }
217
+ `;
768
218
 
769
219
  case 'chat':
770
- return `// 🔥 ${componentName} - Chat Client Component
771
- import React, { useState, useEffect, useRef } from 'react';
772
- import { useTypedLiveComponent } from '@/core/client';
773
- import type { InferComponentState } from '@/core/client';
220
+ return `// 🔥 ${name}
221
+ import { useRef, useEffect } from 'react'
222
+ import { Live } from '@/core/client'
223
+ import { ${name} } from '@server/live/${name}'
774
224
 
775
- // Import component type DIRECTLY from backend - full type inference!
776
- import type { ${componentName}Component } from '@/server/live/${componentName}Component';
225
+ export function ${name}Demo() {
226
+ const chat = Live.use(${name}, {
227
+ initialState: ${name}.defaultState
228
+ })
229
+ const messagesEndRef = useRef<HTMLDivElement>(null)
777
230
 
778
- // State type inferred from backend component
779
- type ${componentName}State = InferComponentState<${componentName}Component>;
780
-
781
- const initialState: ${componentName}State = {
782
- messages: [],
783
- users: {},
784
- currentMessage: "",
785
- isTyping: {},
786
- lastUpdated: new Date(),
787
- };
788
-
789
- export function ${componentName}() {
790
- const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
791
- const [username, setUsername] = useState(\`User\${Math.random().toString(36).substr(2, 4)}\`);
792
- const [hasJoined, setHasJoined] = useState(false);
793
- const messagesEndRef = useRef<HTMLDivElement>(null);
794
-
795
- useEffect(() => {
796
- if (messagesEndRef.current) {
797
- messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
798
- }
799
- }, [state.messages]);
231
+ useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [chat.messages])
800
232
 
801
- if (!connected) {
802
- return (
803
- <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
804
- <div className="text-center">
805
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
806
- <p className="text-gray-600">Connecting to ${componentName}...</p>
807
- </div>
808
- </div>
809
- );
810
- }
233
+ if (!chat.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
811
234
 
812
- if (!hasJoined) {
235
+ if (!chat.username) {
813
236
  return (
814
- <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 max-w-md mx-auto">
815
- <h2 className="text-2xl font-bold text-gray-800 mb-4">Join ${componentName}</h2>
816
- <div className="space-y-4">
817
- <div>
818
- <label className="block text-sm font-medium text-gray-700 mb-1">
819
- Username
820
- </label>
821
- <input
822
- type="text"
823
- value={username}
824
- onChange={(e) => setUsername(e.target.value)}
825
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
826
- placeholder="Enter your username"
827
- maxLength={20}
828
- />
829
- </div>
830
- <button
831
- onClick={async () => {
832
- if (username.trim()) {
833
- await call('joinRoom', { username: username.trim() });
834
- setHasJoined(true);
835
- }
836
- }}
837
- disabled={!username.trim() || loading}
838
- className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
839
- >
840
- 💬 Join Chat
841
- </button>
842
- </div>
237
+ <div className="p-6 bg-white rounded-xl shadow-sm border max-w-md mx-auto">
238
+ <h2 className="text-xl font-bold mb-4">Entrar no ${name}</h2>
239
+ <input {...chat.$field('username', { syncOn: 'blur' })} placeholder="Seu nome" className="w-full px-3 py-2 border rounded-lg mb-4" maxLength={20} />
240
+ <button onClick={async () => { await chat.$sync(); if (chat.username.trim()) await chat.setUsername({ username: chat.username }) }} className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg">Entrar</button>
843
241
  </div>
844
- );
242
+ )
845
243
  }
846
244
 
847
- const handleSendMessage = async (e: React.FormEvent) => {
848
- e.preventDefault();
849
- if (state.currentMessage.trim()) {
850
- await call('sendMessage', { text: state.currentMessage, username });
851
- }
852
- };
853
-
854
- const typingUsers = Object.keys(state.isTyping).filter(userId => state.isTyping[userId]);
855
-
856
245
  return (
857
- <div className="bg-white border border-gray-200 rounded-lg shadow-sm m-4 max-w-2xl mx-auto flex flex-col h-96">
858
- {/* Header */}
859
- <div className="flex items-center justify-between p-4 border-b border-gray-200">
860
- <h2 className="text-xl font-bold text-gray-800">${componentName}</h2>
861
- <div className="flex items-center gap-2">
862
- <span className="text-sm text-gray-600">
863
- {Object.keys(state.users).length} online
864
- </span>
865
- <span className={
866
- \`px-2 py-1 rounded-full text-xs font-medium \${
867
- connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
868
- }\`
869
- }>
870
- {connected ? '🟢 Connected' : '🔴 Disconnected'}
871
- </span>
872
- </div>
873
- </div>
874
-
875
- {/* Messages */}
246
+ <div className="flex flex-col h-96 bg-white rounded-xl shadow-sm border">
247
+ <div className="p-4 border-b flex justify-between"><h2 className="font-bold">${name}</h2><span className="text-sm text-gray-500">{chat.username}</span></div>
876
248
  <div className="flex-1 overflow-y-auto p-4 space-y-2">
877
- {state.messages.map((message) => (
878
- <div
879
- key={message.id}
880
- className={\`flex \${message.username === username ? 'justify-end' : 'justify-start'}\`}
881
- >
882
- <div
883
- className={\`max-w-xs px-3 py-2 rounded-lg \${
884
- message.username === username
885
- ? 'bg-blue-500 text-white'
886
- : 'bg-gray-100 text-gray-800'
887
- }\`}
888
- >
889
- {message.username !== username && (
890
- <div className="text-xs font-medium mb-1">{message.username}</div>
891
- )}
892
- <div className="text-sm">{message.text}</div>
893
- <div className={\`text-xs mt-1 \${
894
- message.username === username ? 'text-blue-100' : 'text-gray-500'
895
- }\`}>
896
- {new Date(message.timestamp).toLocaleTimeString()}
897
- </div>
249
+ {chat.messages.map((msg) => (
250
+ <div key={msg.id} className={\`flex \${msg.username === chat.username ? 'justify-end' : 'justify-start'}\`}>
251
+ <div className={\`max-w-xs px-3 py-2 rounded-lg \${msg.username === chat.username ? 'bg-blue-500 text-white' : 'bg-gray-100'}\`}>
252
+ {msg.username !== chat.username && <div className="text-xs font-medium mb-1">{msg.username}</div>}
253
+ <div>{msg.text}</div>
898
254
  </div>
899
255
  </div>
900
256
  ))}
901
-
902
- {typingUsers.length > 0 && (
903
- <div className="text-xs text-gray-500 italic">
904
- {typingUsers.map(userId => state.users[userId]?.username || userId).join(', ')}
905
- {typingUsers.length === 1 ? ' is' : ' are'} typing...
906
- </div>
907
- )}
908
-
909
257
  <div ref={messagesEndRef} />
910
258
  </div>
911
-
912
- {/* Input */}
913
- <form onSubmit={handleSendMessage} className="p-4 border-t border-gray-200">
914
- <div className="flex gap-2">
915
- <input
916
- type="text"
917
- value={state.currentMessage}
918
- onChange={(e) => {
919
- call('updateField', { field: 'currentMessage', value: e.target.value });
920
- call('updateTyping', { isTyping: e.target.value.length > 0, username });
921
- }}
922
- className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
923
- placeholder="Type a message..."
924
- disabled={loading}
925
- maxLength={500}
926
- />
927
- <button
928
- type="submit"
929
- disabled={!state.currentMessage.trim() || loading}
930
- className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
931
- >
932
- 📤
933
- </button>
934
- </div>
259
+ <form onSubmit={async (e) => { e.preventDefault(); if (chat.currentMessage.trim()) await chat.sendMessage({ text: chat.currentMessage }) }} className="p-4 border-t flex gap-2">
260
+ <input {...chat.$field('currentMessage', { syncOn: 'change', debounce: 100 })} placeholder="Mensagem..." className="flex-1 px-3 py-2 border rounded-lg" />
261
+ <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg">Enviar</button>
935
262
  </form>
936
263
  </div>
937
- );
938
- }`;
264
+ )
265
+ }
266
+ `;
939
267
 
940
268
  default: // basic
941
- return `// 🔥 ${componentName} - Client Component
942
- import { useTypedLiveComponent } from '@/core/client';
943
- import type { InferComponentState } from '@/core/client';
944
-
945
- // Import component type DIRECTLY from backend - full type inference!
946
- import type { ${componentName}Component } from '@/server/live/${componentName}Component';
269
+ return `// 🔥 ${name}
270
+ import { Live } from '@/core/client'
271
+ import { ${name} } from '@server/live/${name}'
947
272
 
948
- // State type inferred from backend component
949
- type ${componentName}State = InferComponentState<${componentName}Component>;
273
+ export function ${name}Demo() {
274
+ const component = Live.use(${name}, {
275
+ initialState: ${name}.defaultState
276
+ })
950
277
 
951
- const initialState: ${componentName}State = {
952
- message: "Loading...",
953
- count: 0,
954
- lastUpdated: new Date(),
955
- };
956
-
957
- export function ${componentName}() {
958
- const { state, call, connected, loading } = useTypedLiveComponent<${componentName}Component>('${componentName}', initialState${roomProps});
959
-
960
- if (!connected) {
961
- return (
962
- <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
963
- <div className="text-center">
964
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
965
- <p className="text-gray-600">Connecting to ${componentName}...</p>
966
- </div>
967
- </div>
968
- );
969
- }
278
+ if (!component.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
970
279
 
971
280
  return (
972
- <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 relative">
973
- <div className="flex items-center justify-between mb-4">
974
- <h2 className="text-2xl font-bold text-gray-800">${componentName} Live Component</h2>
975
- <span className={
976
- \`px-2 py-1 rounded-full text-xs font-medium \${
977
- connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
978
- }\`
979
- }>
980
- {connected ? '🟢 Connected' : '🔴 Disconnected'}
981
- </span>
982
- </div>
983
-
281
+ <div className="p-6 bg-white rounded-xl shadow-sm border">
282
+ <h2 className="text-xl font-bold mb-4">${name}</h2>
984
283
  <div className="space-y-4">
985
- <div>
986
- <p className="text-gray-600 mb-2">Server message:</p>
987
- <p className="text-lg font-semibold text-blue-600">{state.message}</p>
988
- </div>
989
-
990
- <div>
991
- <p className="text-gray-600 mb-2">Counter: <span className="font-bold text-2xl">{state.count}</span></p>
992
- </div>
993
-
994
- <div className="flex gap-2 flex-wrap">
995
- <button
996
- onClick={() => call('updateMessage', { message: 'Hello from the client!' })}
997
- disabled={loading}
998
- className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
999
- >
1000
- 📝 Update Message
1001
- </button>
1002
-
1003
- <button
1004
- onClick={() => call('incrementCounter', {})}
1005
- disabled={loading}
1006
- className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
1007
- >
1008
- ➕ Increment
1009
- </button>
1010
-
1011
- <button
1012
- onClick={() => call('resetData', {})}
1013
- disabled={loading}
1014
- className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
1015
- >
1016
- 🔄 Reset
1017
- </button>
1018
-
1019
- <button
1020
- onClick={async () => {
1021
- const result = await call('getData', {});
1022
- console.log('Component data:', result);
1023
- alert('Data logged to console');
1024
- }}
1025
- disabled={loading}
1026
- className="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed"
1027
- >
1028
- 📊 Get Data
1029
- </button>
284
+ <p className="text-lg text-blue-600">{component.message}</p>
285
+ <p className="text-gray-600">Count: <span className="font-bold text-2xl">{component.count}</span></p>
286
+ <div className="flex gap-2">
287
+ <button onClick={() => component.updateMessage({ message: 'Hello!' })} className="px-4 py-2 bg-blue-500 text-white rounded-lg">Update</button>
288
+ <button onClick={() => component.increment()} className="px-4 py-2 bg-green-500 text-white rounded-lg">+1</button>
289
+ <button onClick={() => component.reset()} className="px-4 py-2 bg-gray-500 text-white rounded-lg">Reset</button>
1030
290
  </div>
1031
291
  </div>
1032
-
1033
- <div className="mt-4 text-xs text-gray-500 text-center">
1034
- Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
1035
- </div>
1036
-
1037
- {loading && (
1038
- <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
1039
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
1040
- </div>
1041
- )}
1042
292
  </div>
1043
- );
1044
- }`;
293
+ )
294
+ }
295
+ `;
1045
296
  }
1046
297
  };
1047
298
 
1048
299
  export const createLiveComponentCommand: CliCommand = {
1049
- name: "make:component",
1050
- description: "Create a new Live Component with server and client files",
300
+ name: "make:live",
301
+ description: "Create a new Live Component",
1051
302
  category: "Live Components",
1052
- aliases: ["make:live", "create:component", "create:live-component"],
1053
- usage: "flux make:component <ComponentName> [options]",
303
+ aliases: ["make:component", "create:live"],
304
+ usage: "flux make:live <Name> [options]",
1054
305
  examples: [
1055
- "flux make:component UserProfile # Basic component",
1056
- "flux make:component TodoCounter --type=counter # Counter component",
1057
- "flux make:component ContactForm --type=form # Form component",
1058
- "flux make:component LiveChat --type=chat # Chat component",
1059
- "flux make:component ServerOnly --no-client # Server-only component",
1060
- "flux make:component MultiUser --room=lobby # Component with room support"
1061
- ],
1062
- arguments: [
1063
- {
1064
- name: "ComponentName",
1065
- description: "The name of the component in PascalCase (e.g., UserProfile, TodoCounter)",
1066
- required: true,
1067
- type: "string"
1068
- },
306
+ "flux make:live Counter --type=counter",
307
+ "flux make:live ContactForm --type=form",
308
+ "flux make:live Chat --type=chat",
309
+ "flux make:live MyComponent"
1069
310
  ],
311
+ arguments: [{ name: "Name", description: "Component name in PascalCase", required: true, type: "string" }],
1070
312
  options: [
1071
- {
1072
- name: "type",
1073
- short: "t",
1074
- description: "Type of component template to generate",
1075
- type: "string",
1076
- default: "basic",
1077
- choices: ["basic", "counter", "form", "chat"]
1078
- },
1079
- {
1080
- name: "no-client",
1081
- description: "Generate only server component (no client file)",
1082
- type: "boolean"
1083
- },
1084
- {
1085
- name: "room",
1086
- short: "r",
1087
- description: "Default room name for multi-user features",
1088
- type: "string"
1089
- },
1090
- {
1091
- name: "force",
1092
- short: "f",
1093
- description: "Overwrite existing files if they exist",
1094
- type: "boolean"
1095
- }
313
+ { name: "type", short: "t", description: "Template type", type: "string", default: "basic", choices: ["basic", "counter", "form", "chat"] },
314
+ { name: "no-client", description: "Server only", type: "boolean" },
315
+ { name: "room", short: "r", description: "Default room", type: "string" },
316
+ { name: "force", short: "f", description: "Overwrite", type: "boolean" }
1096
317
  ],
1097
318
  handler: async (args, options, context) => {
1098
- const [componentName] = args;
319
+ const [name] = args;
1099
320
  const { type = 'basic', 'no-client': noClient, room, force } = options;
1100
321
 
1101
- // Validation
1102
- if (!componentName || !/^[A-Z][a-zA-Z0-9]*$/.test(componentName)) {
1103
- context.logger.error("❌ Invalid component name. It must be in PascalCase (e.g., UserProfile, TodoCounter).");
1104
- context.logger.info("Examples: UserProfile, TodoCounter, ContactForm, LiveChat");
322
+ if (!name || !/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
323
+ context.logger.error("❌ Nome inválido. Use PascalCase (ex: MeuComponente)");
1105
324
  return;
1106
325
  }
1107
326
 
1108
- if (!['basic', 'counter', 'form', 'chat'].includes(type)) {
1109
- context.logger.error(`❌ Invalid component type: ${type}`);
1110
- context.logger.info("Available types: basic, counter, form, chat");
1111
- return;
1112
- }
1113
-
1114
- // File paths
1115
- const serverFilePath = path.join(context.workingDir, "app", "server", "live", `${componentName}Component.ts`);
1116
- const clientFilePath = path.join(context.workingDir, "app", "client", "src", "live", `${componentName}.tsx`);
327
+ const serverPath = path.join(context.workingDir, "app", "server", "live", `${name}.ts`);
328
+ const clientPath = path.join(context.workingDir, "app", "client", "src", "live", `${name}.tsx`);
1117
329
 
1118
330
  try {
1119
- // Check if files exist (unless force flag is used)
1120
331
  if (!force) {
1121
- const serverExists = await fs.access(serverFilePath).then(() => true).catch(() => false);
1122
- const clientExists = !noClient && await fs.access(clientFilePath).then(() => true).catch(() => false);
1123
-
332
+ const serverExists = await fs.access(serverPath).then(() => true).catch(() => false);
333
+ const clientExists = !noClient && await fs.access(clientPath).then(() => true).catch(() => false);
1124
334
  if (serverExists || clientExists) {
1125
- context.logger.error(`❌ Component files already exist. Use --force to overwrite.`);
1126
- if (serverExists) context.logger.info(` Server: ${serverFilePath}`);
1127
- if (clientExists) context.logger.info(` Client: ${clientFilePath}`);
335
+ context.logger.error("❌ Arquivos existem. Use --force para sobrescrever.");
1128
336
  return;
1129
337
  }
1130
338
  }
1131
339
 
1132
- // Ensure directories exist
1133
- await fs.mkdir(path.dirname(serverFilePath), { recursive: true });
1134
- if (!noClient) {
1135
- await fs.mkdir(path.dirname(clientFilePath), { recursive: true });
1136
- }
340
+ await fs.mkdir(path.dirname(serverPath), { recursive: true });
341
+ if (!noClient) await fs.mkdir(path.dirname(clientPath), { recursive: true });
1137
342
 
1138
- // Generate server component
1139
- context.logger.info(`🔥 Creating server component: ${componentName}Component.ts`);
1140
- const serverTemplate = getServerTemplate(componentName, type, room);
1141
- await fs.writeFile(serverFilePath, serverTemplate);
343
+ context.logger.info(`🔥 Criando ${name}...`);
1142
344
 
1143
- // Generate client component (unless --no-client)
1144
- if (!noClient) {
1145
- context.logger.info(`⚛️ Creating client component: ${componentName}.tsx`);
1146
- const clientTemplate = getClientTemplate(componentName, type, room);
1147
- await fs.writeFile(clientFilePath, clientTemplate);
1148
- }
345
+ await fs.writeFile(serverPath, getServerTemplate(name, type, room, !noClient));
346
+ context.logger.info(` ✅ Server: app/server/live/${name}.ts`);
1149
347
 
1150
- // Success message
1151
- context.logger.info(`✅ Successfully created '${componentName}' live component!`);
1152
- context.logger.info("");
1153
- context.logger.info("📁 Files created:");
1154
- context.logger.info(` 🔥 Server: app/server/live/${componentName}Component.ts`);
1155
- if (!noClient) {
1156
- context.logger.info(` ⚛️ Client: app/client/src/live/${componentName}.tsx`);
1157
- }
1158
-
1159
- context.logger.info("");
1160
- context.logger.info("🚀 Next steps:");
1161
- context.logger.info(" 1. Start dev server: bun run dev");
1162
348
  if (!noClient) {
1163
- context.logger.info(` 2. Import component in your App.tsx:`);
1164
- context.logger.info(` import { ${componentName} } from './live/${componentName}'`);
1165
- context.logger.info(` 3. Add component to your JSX: <${componentName} />`);
1166
- }
1167
-
1168
- if (room) {
1169
- context.logger.info(` 4. Component supports multi-user features with room: ${room}`);
1170
- }
1171
-
1172
- if (type !== 'basic') {
1173
- context.logger.info(` 5. Template type '${type}' includes specialized functionality`);
349
+ await fs.writeFile(clientPath, getClientTemplate(name, type));
350
+ context.logger.info(` Client: app/client/src/live/${name}.tsx`);
1174
351
  }
1175
352
 
1176
353
  context.logger.info("");
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';");
354
+ context.logger.info("🚀 Uso:");
355
+ context.logger.info(` import { ${name}Demo } from './live/${name}'`);
356
+ context.logger.info(` <${name}Demo />`);
1181
357
  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';`);
1184
- context.logger.info("");
1185
- context.logger.info(" # Use with automatic type inference:");
1186
- context.logger.info(` const { state, call } = useTypedLiveComponent<${componentName}Component>(...);`);
358
+ context.logger.info("📖 API:");
359
+ context.logger.info(` const x = Live.use(${name}) // usa defaultState do backend`);
360
+ context.logger.info(` const x = Live.use(${name}, { ... }) // com override parcial`);
361
+ context.logger.info(` x.property // estado`);
362
+ context.logger.info(` x.action() // ação`);
363
+ context.logger.info(` x.$connected // status`);
1187
364
 
1188
365
  } catch (error) {
1189
- context.logger.error(`❌ Failed to create component files: ${error instanceof Error ? error.message : String(error)}`);
1190
- throw error;
366
+ context.logger.error(`❌ Erro: ${error instanceof Error ? error.message : String(error)}`);
1191
367
  }
1192
368
  },
1193
- };
369
+ };