create-fluxstack 1.12.1 → 1.14.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.
- package/LLMD/INDEX.md +8 -1
- package/LLMD/agent.md +867 -0
- package/LLMD/config/environment-vars.md +30 -0
- package/LLMD/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +465 -0
- package/LLMD/resources/live-components.md +168 -26
- package/LLMD/resources/live-logging.md +220 -0
- package/LLMD/resources/live-upload.md +59 -8
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +13 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +139 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +334 -0
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- package/app/server/auth/AuthManager.ts +213 -0
- package/app/server/auth/DevAuthProvider.ts +66 -0
- package/app/server/auth/HashManager.ts +123 -0
- package/app/server/auth/JWTAuthProvider.example.ts +101 -0
- package/app/server/auth/RateLimiter.ts +106 -0
- package/app/server/auth/contracts.ts +192 -0
- package/app/server/auth/guards/SessionGuard.ts +167 -0
- package/app/server/auth/guards/TokenGuard.ts +202 -0
- package/app/server/auth/index.ts +174 -0
- package/app/server/auth/middleware.ts +163 -0
- package/app/server/auth/providers/InMemoryProvider.ts +162 -0
- package/app/server/auth/sessions/SessionManager.ts +164 -0
- package/app/server/cache/CacheManager.ts +81 -0
- package/app/server/cache/MemoryDriver.ts +112 -0
- package/app/server/cache/contracts.ts +49 -0
- package/app/server/cache/index.ts +42 -0
- package/app/server/index.ts +14 -0
- package/app/server/live/LiveAdminPanel.ts +174 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -32
- package/app/server/live/LiveProtectedChat.ts +151 -0
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/app/server/routes/auth.routes.ts +278 -0
- package/app/server/routes/index.ts +2 -0
- package/config/index.ts +8 -0
- package/config/system/auth.config.ts +49 -0
- package/config/system/runtime.config.ts +4 -0
- package/config/system/session.config.ts +33 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +17 -10
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +58 -5
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +16 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +19 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +151 -20
- package/core/plugins/config.ts +5 -4
- package/core/plugins/discovery.ts +11 -2
- package/core/plugins/manager.ts +11 -5
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +53 -25
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +134 -50
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +144 -0
- package/core/server/live/LiveRoomManager.ts +22 -5
- package/core/server/live/StateSignature.ts +704 -643
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auth/LiveAuthContext.ts +71 -0
- package/core/server/live/auth/LiveAuthManager.ts +304 -0
- package/core/server/live/auth/index.ts +19 -0
- package/core/server/live/auth/types.ts +179 -0
- package/core/server/live/auto-generated-components.ts +8 -2
- package/core/server/live/index.ts +16 -0
- package/core/server/live/websocket-plugin.ts +323 -22
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/templates/create-project.ts +0 -3
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +278 -22
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -8
- package/plugins/crypto-auth/index.ts +6 -0
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
- package/plugins/crypto-auth/server/index.ts +24 -21
- package/rest-tests/README.md +57 -0
- package/rest-tests/auth-token.http +113 -0
- package/rest-tests/auth.http +112 -0
- package/rest-tests/rooms-token.http +69 -0
- package/rest-tests/users-token.http +62 -0
- package/.dockerignore +0 -81
- package/Dockerfile +0 -70
- package/LIVE_COMPONENTS_REVIEW.md +0 -781
- package/app/client/src/assets/react.svg +0 -1
package/core/build/optimizer.ts
CHANGED
|
@@ -1,236 +1,236 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, statSync, readdirSync } from "fs"
|
|
2
|
-
import { join, extname } from "path"
|
|
3
|
-
import { gzipSync } from "zlib"
|
|
4
|
-
import type { OptimizationConfig, OptimizationResult } from "../types/build"
|
|
5
|
-
import { buildLogger } from "../utils/build-logger"
|
|
6
|
-
|
|
7
|
-
export interface OptimizerConfig {
|
|
8
|
-
treeshake: boolean
|
|
9
|
-
compress: boolean
|
|
10
|
-
removeUnusedCSS: boolean
|
|
11
|
-
optimizeImages: boolean
|
|
12
|
-
bundleAnalysis: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class Optimizer {
|
|
16
|
-
private config: OptimizerConfig
|
|
17
|
-
|
|
18
|
-
constructor(config: OptimizerConfig) {
|
|
19
|
-
this.config = config
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async optimize(buildPath: string): Promise<OptimizationResult> {
|
|
23
|
-
buildLogger.section('Build Optimization', '🔧')
|
|
24
|
-
|
|
25
|
-
const startTime = Date.now()
|
|
26
|
-
const results: OptimizationResult = {
|
|
27
|
-
success: true,
|
|
28
|
-
duration: 0,
|
|
29
|
-
originalSize: 0,
|
|
30
|
-
optimizedSize: 0,
|
|
31
|
-
compressionRatio: 0,
|
|
32
|
-
optimizations: []
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// Get original size
|
|
37
|
-
results.originalSize = await this.calculateDirectorySize(buildPath)
|
|
38
|
-
buildLogger.step(`Original size: ${buildLogger.formatSize(results.originalSize)}`)
|
|
39
|
-
|
|
40
|
-
// Apply optimizations (minification removed for compatibility)
|
|
41
|
-
|
|
42
|
-
if (this.config.compress) {
|
|
43
|
-
await this.compressAssets(buildPath, results)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (this.config.removeUnusedCSS) {
|
|
47
|
-
await this.removeUnusedCSS(buildPath, results)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (this.config.optimizeImages) {
|
|
51
|
-
await this.optimizeImages(buildPath, results)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (this.config.bundleAnalysis) {
|
|
55
|
-
await this.analyzeBundles(buildPath, results)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Calculate final size and compression ratio
|
|
59
|
-
results.optimizedSize = await this.calculateDirectorySize(buildPath)
|
|
60
|
-
results.compressionRatio = results.originalSize > 0
|
|
61
|
-
? ((results.originalSize - results.optimizedSize) / results.originalSize) * 100
|
|
62
|
-
: 0
|
|
63
|
-
|
|
64
|
-
results.duration = Date.now() - startTime
|
|
65
|
-
|
|
66
|
-
buildLogger.success(`Optimization completed in ${buildLogger.formatDuration(results.duration)}`)
|
|
67
|
-
|
|
68
|
-
// Create optimization summary table
|
|
69
|
-
const optimizationData = results.optimizations.map(opt => ({
|
|
70
|
-
type: opt.type,
|
|
71
|
-
description: opt.description,
|
|
72
|
-
saved: buildLogger.formatSize(opt.sizeSaved)
|
|
73
|
-
}))
|
|
74
|
-
|
|
75
|
-
if (optimizationData.length > 0) {
|
|
76
|
-
buildLogger.table(
|
|
77
|
-
[
|
|
78
|
-
{ header: 'Optimization', key: 'type', width: 20, align: 'left', color: 'cyan' },
|
|
79
|
-
{ header: 'Description', key: 'description', width: 35, align: 'left' },
|
|
80
|
-
{ header: 'Size Saved', key: 'saved', width: 12, align: 'right', color: 'green' }
|
|
81
|
-
],
|
|
82
|
-
optimizationData
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return results
|
|
87
|
-
|
|
88
|
-
} catch (error) {
|
|
89
|
-
results.success = false
|
|
90
|
-
results.duration = Date.now() - startTime
|
|
91
|
-
results.error = error instanceof Error ? error.message : "Unknown optimization error"
|
|
92
|
-
|
|
93
|
-
buildLogger.error(`Optimization failed: ${results.error}`)
|
|
94
|
-
return results
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Minification methods removed for compatibility with Bun bundler
|
|
99
|
-
|
|
100
|
-
private async compressAssets(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
101
|
-
buildLogger.step("Compressing assets...")
|
|
102
|
-
|
|
103
|
-
const files = this.getFilesRecursively(buildPath)
|
|
104
|
-
let compressedCount = 0
|
|
105
|
-
|
|
106
|
-
for (const file of files) {
|
|
107
|
-
const ext = extname(file).toLowerCase()
|
|
108
|
-
|
|
109
|
-
if (['.js', '.css', '.html', '.json', '.svg'].includes(ext)) {
|
|
110
|
-
try {
|
|
111
|
-
const content = readFileSync(file)
|
|
112
|
-
const compressed = gzipSync(content)
|
|
113
|
-
|
|
114
|
-
// Only create .gz file if it's significantly smaller
|
|
115
|
-
if (compressed.length < content.length * 0.9) {
|
|
116
|
-
writeFileSync(file + '.gz', compressed)
|
|
117
|
-
compressedCount++
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
// Silently skip files that can't be compressed
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
buildLogger.success(`Compressed ${compressedCount} files`)
|
|
126
|
-
results.optimizations.push({
|
|
127
|
-
type: 'compression',
|
|
128
|
-
description: `Created gzip versions for ${compressedCount} files`,
|
|
129
|
-
sizeSaved: 0
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private async removeUnusedCSS(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
134
|
-
buildLogger.step("Analyzing CSS...")
|
|
135
|
-
|
|
136
|
-
// This is a placeholder - real implementation would use PurgeCSS or similar
|
|
137
|
-
results.optimizations.push({
|
|
138
|
-
type: 'css-purging',
|
|
139
|
-
description: 'CSS purging not implemented yet',
|
|
140
|
-
sizeSaved: 0
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private async optimizeImages(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
145
|
-
buildLogger.step("Optimizing images...")
|
|
146
|
-
|
|
147
|
-
// This is a placeholder - real implementation would use imagemin or similar
|
|
148
|
-
results.optimizations.push({
|
|
149
|
-
type: 'image-optimization',
|
|
150
|
-
description: 'Image optimization not implemented yet',
|
|
151
|
-
sizeSaved: 0
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async analyzeBundles(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
156
|
-
buildLogger.step("Analyzing bundles...")
|
|
157
|
-
|
|
158
|
-
const files = this.getFilesRecursively(buildPath)
|
|
159
|
-
const jsFiles = files.filter(f => extname(f) === '.js')
|
|
160
|
-
|
|
161
|
-
let totalJSSize = 0
|
|
162
|
-
for (const file of jsFiles) {
|
|
163
|
-
totalJSSize += statSync(file).size
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
results.optimizations.push({
|
|
167
|
-
type: 'bundle-analysis',
|
|
168
|
-
description: `Analyzed ${jsFiles.length} JS bundles (${(totalJSSize / 1024).toFixed(2)} KB total)`,
|
|
169
|
-
sizeSaved: 0
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private async calculateDirectorySize(dirPath: string): Promise<number> {
|
|
174
|
-
const files = this.getFilesRecursively(dirPath)
|
|
175
|
-
let totalSize = 0
|
|
176
|
-
|
|
177
|
-
for (const file of files) {
|
|
178
|
-
try {
|
|
179
|
-
totalSize += statSync(file).size
|
|
180
|
-
} catch (error) {
|
|
181
|
-
// Ignore files that can't be read
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return totalSize
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private getFilesRecursively(dir: string): string[] {
|
|
189
|
-
const files: string[] = []
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const items = readdirSync(dir, { withFileTypes: true })
|
|
193
|
-
|
|
194
|
-
for (const item of items) {
|
|
195
|
-
const fullPath = join(dir, item.name)
|
|
196
|
-
|
|
197
|
-
if (item.isDirectory()) {
|
|
198
|
-
files.push(...this.getFilesRecursively(fullPath))
|
|
199
|
-
} else {
|
|
200
|
-
files.push(fullPath)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
} catch (error) {
|
|
204
|
-
// Ignore directories that can't be read
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return files
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async createOptimizationReport(result: OptimizationResult): Promise<string> {
|
|
211
|
-
const report = `
|
|
212
|
-
# Build Optimization Report
|
|
213
|
-
|
|
214
|
-
## Summary
|
|
215
|
-
- **Status**: ${result.success ? '✅ Success' : '❌ Failed'}
|
|
216
|
-
- **Duration**: ${result.duration}ms
|
|
217
|
-
- **Original Size**: ${(result.originalSize / 1024).toFixed(2)} KB
|
|
218
|
-
- **Optimized Size**: ${(result.optimizedSize / 1024).toFixed(2)} KB
|
|
219
|
-
- **Size Reduction**: ${result.compressionRatio.toFixed(2)}%
|
|
220
|
-
|
|
221
|
-
## Optimizations Applied
|
|
222
|
-
|
|
223
|
-
${result.optimizations.map(opt =>
|
|
224
|
-
`### ${opt.type.charAt(0).toUpperCase() + opt.type.slice(1)}
|
|
225
|
-
- ${opt.description}
|
|
226
|
-
- Size Saved: ${(opt.sizeSaved / 1024).toFixed(2)} KB`
|
|
227
|
-
).join('\n\n')}
|
|
228
|
-
|
|
229
|
-
${result.error ? `## Error\n${result.error}` : ''}
|
|
230
|
-
|
|
231
|
-
Generated at: ${new Date().toISOString()}
|
|
232
|
-
`
|
|
233
|
-
|
|
234
|
-
return report.trim()
|
|
235
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, statSync, readdirSync } from "fs"
|
|
2
|
+
import { join, extname } from "path"
|
|
3
|
+
import { gzipSync } from "zlib"
|
|
4
|
+
import type { OptimizationConfig, OptimizationResult } from "../types/build"
|
|
5
|
+
import { buildLogger } from "../utils/build-logger"
|
|
6
|
+
|
|
7
|
+
export interface OptimizerConfig {
|
|
8
|
+
treeshake: boolean
|
|
9
|
+
compress: boolean
|
|
10
|
+
removeUnusedCSS: boolean
|
|
11
|
+
optimizeImages: boolean
|
|
12
|
+
bundleAnalysis: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Optimizer {
|
|
16
|
+
private config: OptimizerConfig
|
|
17
|
+
|
|
18
|
+
constructor(config: OptimizerConfig) {
|
|
19
|
+
this.config = config
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async optimize(buildPath: string): Promise<OptimizationResult> {
|
|
23
|
+
buildLogger.section('Build Optimization', '🔧')
|
|
24
|
+
|
|
25
|
+
const startTime = Date.now()
|
|
26
|
+
const results: OptimizationResult = {
|
|
27
|
+
success: true,
|
|
28
|
+
duration: 0,
|
|
29
|
+
originalSize: 0,
|
|
30
|
+
optimizedSize: 0,
|
|
31
|
+
compressionRatio: 0,
|
|
32
|
+
optimizations: []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Get original size
|
|
37
|
+
results.originalSize = await this.calculateDirectorySize(buildPath)
|
|
38
|
+
buildLogger.step(`Original size: ${buildLogger.formatSize(results.originalSize)}`)
|
|
39
|
+
|
|
40
|
+
// Apply optimizations (minification removed for compatibility)
|
|
41
|
+
|
|
42
|
+
if (this.config.compress) {
|
|
43
|
+
await this.compressAssets(buildPath, results)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.config.removeUnusedCSS) {
|
|
47
|
+
await this.removeUnusedCSS(buildPath, results)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.config.optimizeImages) {
|
|
51
|
+
await this.optimizeImages(buildPath, results)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.config.bundleAnalysis) {
|
|
55
|
+
await this.analyzeBundles(buildPath, results)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Calculate final size and compression ratio
|
|
59
|
+
results.optimizedSize = await this.calculateDirectorySize(buildPath)
|
|
60
|
+
results.compressionRatio = results.originalSize > 0
|
|
61
|
+
? ((results.originalSize - results.optimizedSize) / results.originalSize) * 100
|
|
62
|
+
: 0
|
|
63
|
+
|
|
64
|
+
results.duration = Date.now() - startTime
|
|
65
|
+
|
|
66
|
+
buildLogger.success(`Optimization completed in ${buildLogger.formatDuration(results.duration)}`)
|
|
67
|
+
|
|
68
|
+
// Create optimization summary table
|
|
69
|
+
const optimizationData = results.optimizations.map(opt => ({
|
|
70
|
+
type: opt.type,
|
|
71
|
+
description: opt.description,
|
|
72
|
+
saved: buildLogger.formatSize(opt.sizeSaved)
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
if (optimizationData.length > 0) {
|
|
76
|
+
buildLogger.table(
|
|
77
|
+
[
|
|
78
|
+
{ header: 'Optimization', key: 'type', width: 20, align: 'left', color: 'cyan' },
|
|
79
|
+
{ header: 'Description', key: 'description', width: 35, align: 'left' },
|
|
80
|
+
{ header: 'Size Saved', key: 'saved', width: 12, align: 'right', color: 'green' }
|
|
81
|
+
],
|
|
82
|
+
optimizationData
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return results
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
results.success = false
|
|
90
|
+
results.duration = Date.now() - startTime
|
|
91
|
+
results.error = error instanceof Error ? error.message : "Unknown optimization error"
|
|
92
|
+
|
|
93
|
+
buildLogger.error(`Optimization failed: ${results.error}`)
|
|
94
|
+
return results
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Minification methods removed for compatibility with Bun bundler
|
|
99
|
+
|
|
100
|
+
private async compressAssets(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
101
|
+
buildLogger.step("Compressing assets...")
|
|
102
|
+
|
|
103
|
+
const files = this.getFilesRecursively(buildPath)
|
|
104
|
+
let compressedCount = 0
|
|
105
|
+
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const ext = extname(file).toLowerCase()
|
|
108
|
+
|
|
109
|
+
if (['.js', '.css', '.html', '.json', '.svg'].includes(ext)) {
|
|
110
|
+
try {
|
|
111
|
+
const content = readFileSync(file)
|
|
112
|
+
const compressed = gzipSync(content)
|
|
113
|
+
|
|
114
|
+
// Only create .gz file if it's significantly smaller
|
|
115
|
+
if (compressed.length < content.length * 0.9) {
|
|
116
|
+
writeFileSync(file + '.gz', compressed)
|
|
117
|
+
compressedCount++
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Silently skip files that can't be compressed
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
buildLogger.success(`Compressed ${compressedCount} files`)
|
|
126
|
+
results.optimizations.push({
|
|
127
|
+
type: 'compression',
|
|
128
|
+
description: `Created gzip versions for ${compressedCount} files`,
|
|
129
|
+
sizeSaved: 0
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async removeUnusedCSS(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
134
|
+
buildLogger.step("Analyzing CSS...")
|
|
135
|
+
|
|
136
|
+
// This is a placeholder - real implementation would use PurgeCSS or similar
|
|
137
|
+
results.optimizations.push({
|
|
138
|
+
type: 'css-purging',
|
|
139
|
+
description: 'CSS purging not implemented yet',
|
|
140
|
+
sizeSaved: 0
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async optimizeImages(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
145
|
+
buildLogger.step("Optimizing images...")
|
|
146
|
+
|
|
147
|
+
// This is a placeholder - real implementation would use imagemin or similar
|
|
148
|
+
results.optimizations.push({
|
|
149
|
+
type: 'image-optimization',
|
|
150
|
+
description: 'Image optimization not implemented yet',
|
|
151
|
+
sizeSaved: 0
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async analyzeBundles(buildPath: string, results: OptimizationResult): Promise<void> {
|
|
156
|
+
buildLogger.step("Analyzing bundles...")
|
|
157
|
+
|
|
158
|
+
const files = this.getFilesRecursively(buildPath)
|
|
159
|
+
const jsFiles = files.filter(f => extname(f) === '.js')
|
|
160
|
+
|
|
161
|
+
let totalJSSize = 0
|
|
162
|
+
for (const file of jsFiles) {
|
|
163
|
+
totalJSSize += statSync(file).size
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
results.optimizations.push({
|
|
167
|
+
type: 'bundle-analysis',
|
|
168
|
+
description: `Analyzed ${jsFiles.length} JS bundles (${(totalJSSize / 1024).toFixed(2)} KB total)`,
|
|
169
|
+
sizeSaved: 0
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async calculateDirectorySize(dirPath: string): Promise<number> {
|
|
174
|
+
const files = this.getFilesRecursively(dirPath)
|
|
175
|
+
let totalSize = 0
|
|
176
|
+
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
try {
|
|
179
|
+
totalSize += statSync(file).size
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Ignore files that can't be read
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return totalSize
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private getFilesRecursively(dir: string): string[] {
|
|
189
|
+
const files: string[] = []
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const items = readdirSync(dir, { withFileTypes: true })
|
|
193
|
+
|
|
194
|
+
for (const item of items) {
|
|
195
|
+
const fullPath = join(dir, item.name)
|
|
196
|
+
|
|
197
|
+
if (item.isDirectory()) {
|
|
198
|
+
files.push(...this.getFilesRecursively(fullPath))
|
|
199
|
+
} else {
|
|
200
|
+
files.push(fullPath)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Ignore directories that can't be read
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return files
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async createOptimizationReport(result: OptimizationResult): Promise<string> {
|
|
211
|
+
const report = `
|
|
212
|
+
# Build Optimization Report
|
|
213
|
+
|
|
214
|
+
## Summary
|
|
215
|
+
- **Status**: ${result.success ? '✅ Success' : '❌ Failed'}
|
|
216
|
+
- **Duration**: ${result.duration}ms
|
|
217
|
+
- **Original Size**: ${(result.originalSize / 1024).toFixed(2)} KB
|
|
218
|
+
- **Optimized Size**: ${(result.optimizedSize / 1024).toFixed(2)} KB
|
|
219
|
+
- **Size Reduction**: ${result.compressionRatio.toFixed(2)}%
|
|
220
|
+
|
|
221
|
+
## Optimizations Applied
|
|
222
|
+
|
|
223
|
+
${result.optimizations.map(opt =>
|
|
224
|
+
`### ${opt.type.charAt(0).toUpperCase() + opt.type.slice(1)}
|
|
225
|
+
- ${opt.description}
|
|
226
|
+
- Size Saved: ${(opt.sizeSaved / 1024).toFixed(2)} KB`
|
|
227
|
+
).join('\n\n')}
|
|
228
|
+
|
|
229
|
+
${result.error ? `## Error\n${result.error}` : ''}
|
|
230
|
+
|
|
231
|
+
Generated at: ${new Date().toISOString()}
|
|
232
|
+
`
|
|
233
|
+
|
|
234
|
+
return report.trim()
|
|
235
|
+
}
|
|
236
236
|
}
|
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'
|
|
5
5
|
import type { WebSocketMessage, WebSocketResponse } from '../types/types'
|
|
6
6
|
|
|
7
|
+
/** Auth credentials to send during WebSocket connection */
|
|
8
|
+
export interface LiveAuthOptions {
|
|
9
|
+
/** JWT or opaque token */
|
|
10
|
+
token?: string
|
|
11
|
+
/** Provider name (if multiple auth providers configured) */
|
|
12
|
+
provider?: string
|
|
13
|
+
/** Additional credentials (publicKey, signature, etc.) */
|
|
14
|
+
[key: string]: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
export interface LiveComponentsContextValue {
|
|
8
18
|
connected: boolean
|
|
9
19
|
connecting: boolean
|
|
10
20
|
error: string | null
|
|
11
21
|
connectionId: string | null
|
|
22
|
+
/** Whether the WebSocket connection is authenticated */
|
|
23
|
+
authenticated: boolean
|
|
12
24
|
|
|
13
25
|
// Send message without waiting for response
|
|
14
26
|
sendMessage: (message: WebSocketMessage) => Promise<void>
|
|
@@ -28,6 +40,9 @@ export interface LiveComponentsContextValue {
|
|
|
28
40
|
// Manual reconnect
|
|
29
41
|
reconnect: () => void
|
|
30
42
|
|
|
43
|
+
// Authenticate (or re-authenticate) the WebSocket connection
|
|
44
|
+
authenticate: (credentials: LiveAuthOptions) => Promise<boolean>
|
|
45
|
+
|
|
31
46
|
// Get current WebSocket instance (for advanced use)
|
|
32
47
|
getWebSocket: () => WebSocket | null
|
|
33
48
|
}
|
|
@@ -37,6 +52,8 @@ const LiveComponentsContext = createContext<LiveComponentsContextValue | null>(n
|
|
|
37
52
|
export interface LiveComponentsProviderProps {
|
|
38
53
|
children: React.ReactNode
|
|
39
54
|
url?: string
|
|
55
|
+
/** Auth credentials to send on connection */
|
|
56
|
+
auth?: LiveAuthOptions
|
|
40
57
|
autoConnect?: boolean
|
|
41
58
|
reconnectInterval?: number
|
|
42
59
|
maxReconnectAttempts?: number
|
|
@@ -47,6 +64,7 @@ export interface LiveComponentsProviderProps {
|
|
|
47
64
|
export function LiveComponentsProvider({
|
|
48
65
|
children,
|
|
49
66
|
url,
|
|
67
|
+
auth,
|
|
50
68
|
autoConnect = true,
|
|
51
69
|
reconnectInterval = 1000,
|
|
52
70
|
maxReconnectAttempts = 5,
|
|
@@ -56,12 +74,23 @@ export function LiveComponentsProvider({
|
|
|
56
74
|
|
|
57
75
|
// Get WebSocket URL dynamically
|
|
58
76
|
const getWebSocketUrl = () => {
|
|
59
|
-
|
|
60
|
-
if (
|
|
77
|
+
let baseUrl: string
|
|
78
|
+
if (url) {
|
|
79
|
+
baseUrl = url
|
|
80
|
+
} else if (typeof window === 'undefined') {
|
|
81
|
+
baseUrl = 'ws://localhost:3000/api/live/ws'
|
|
82
|
+
} else {
|
|
83
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
84
|
+
baseUrl = `${protocol}//${window.location.host}/api/live/ws`
|
|
85
|
+
}
|
|
61
86
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
// Append auth token as query param if provided
|
|
88
|
+
if (auth?.token) {
|
|
89
|
+
const separator = baseUrl.includes('?') ? '&' : '?'
|
|
90
|
+
return `${baseUrl}${separator}token=${encodeURIComponent(auth.token)}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return baseUrl
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
const wsUrl = getWebSocketUrl()
|
|
@@ -71,6 +100,7 @@ export function LiveComponentsProvider({
|
|
|
71
100
|
const [connecting, setConnecting] = useState(false)
|
|
72
101
|
const [error, setError] = useState<string | null>(null)
|
|
73
102
|
const [connectionId, setConnectionId] = useState<string | null>(null)
|
|
103
|
+
const [authenticated, setAuthenticated] = useState(false)
|
|
74
104
|
|
|
75
105
|
// Refs
|
|
76
106
|
const wsRef = useRef<WebSocket | null>(null)
|
|
@@ -136,7 +166,30 @@ export function LiveComponentsProvider({
|
|
|
136
166
|
// Handle connection established
|
|
137
167
|
if (response.type === 'CONNECTION_ESTABLISHED') {
|
|
138
168
|
setConnectionId(response.connectionId || null)
|
|
169
|
+
setAuthenticated((response as any).authenticated || false)
|
|
139
170
|
log('🔗 Connection ID:', response.connectionId)
|
|
171
|
+
if ((response as any).authenticated) {
|
|
172
|
+
log('🔒 Authenticated as:', (response as any).userId)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If auth credentials provided but not yet authenticated via query,
|
|
176
|
+
// send AUTH message with full credentials
|
|
177
|
+
if (auth && !auth.token && Object.keys(auth).some(k => auth[k])) {
|
|
178
|
+
sendMessageAndWait({
|
|
179
|
+
type: 'AUTH',
|
|
180
|
+
payload: auth
|
|
181
|
+
} as any).then(authResp => {
|
|
182
|
+
if ((authResp as any).authenticated) {
|
|
183
|
+
setAuthenticated(true)
|
|
184
|
+
log('🔒 Authenticated via message')
|
|
185
|
+
}
|
|
186
|
+
}).catch(() => {})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle auth response
|
|
191
|
+
if (response.type === 'AUTH_RESPONSE') {
|
|
192
|
+
setAuthenticated((response as any).authenticated || false)
|
|
140
193
|
}
|
|
141
194
|
|
|
142
195
|
// Handle pending requests (request-response pattern)
|
|
@@ -403,6 +456,22 @@ export function LiveComponentsProvider({
|
|
|
403
456
|
log('🗑️ Component unregistered', componentId)
|
|
404
457
|
}, [log])
|
|
405
458
|
|
|
459
|
+
// Authenticate (or re-authenticate) the WebSocket connection
|
|
460
|
+
const authenticate = useCallback(async (credentials: LiveAuthOptions): Promise<boolean> => {
|
|
461
|
+
try {
|
|
462
|
+
const response = await sendMessageAndWait({
|
|
463
|
+
type: 'AUTH',
|
|
464
|
+
payload: credentials
|
|
465
|
+
} as any, 5000)
|
|
466
|
+
|
|
467
|
+
const success = (response as any).authenticated || false
|
|
468
|
+
setAuthenticated(success)
|
|
469
|
+
return success
|
|
470
|
+
} catch {
|
|
471
|
+
return false
|
|
472
|
+
}
|
|
473
|
+
}, [sendMessageAndWait])
|
|
474
|
+
|
|
406
475
|
// Get WebSocket instance
|
|
407
476
|
const getWebSocket = useCallback(() => {
|
|
408
477
|
return wsRef.current
|
|
@@ -424,12 +493,14 @@ export function LiveComponentsProvider({
|
|
|
424
493
|
connecting,
|
|
425
494
|
error,
|
|
426
495
|
connectionId,
|
|
496
|
+
authenticated,
|
|
427
497
|
sendMessage,
|
|
428
498
|
sendMessageAndWait,
|
|
429
499
|
sendBinaryAndWait,
|
|
430
500
|
registerComponent,
|
|
431
501
|
unregisterComponent,
|
|
432
502
|
reconnect,
|
|
503
|
+
authenticate,
|
|
433
504
|
getWebSocket
|
|
434
505
|
}
|
|
435
506
|
|
|
@@ -52,16 +52,23 @@ type ExtractState<T> = T extends { new(...args: any[]): { state: infer S } }
|
|
|
52
52
|
? S extends Record<string, any> ? S : Record<string, any>
|
|
53
53
|
: ExtractDefaultState<T>
|
|
54
54
|
|
|
55
|
-
// Extrai
|
|
55
|
+
// Extrai os nomes de publicActions como union type
|
|
56
|
+
type ExtractPublicActionNames<T> = T extends { publicActions: readonly (infer A)[] }
|
|
57
|
+
? A extends string ? A : never
|
|
58
|
+
: never
|
|
59
|
+
|
|
60
|
+
// Extrai as Actions respeitando publicActions (MANDATORY)
|
|
61
|
+
// - Se publicActions está definido: somente métodos listados são expostos
|
|
62
|
+
// - Se publicActions NÃO está definido: nenhuma action disponível (secure by default)
|
|
56
63
|
type ExtractActions<T> = T extends { new(...args: any[]): infer Instance }
|
|
57
|
-
? {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
? never
|
|
61
|
-
:
|
|
62
|
-
:
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
? T extends { publicActions: readonly string[] }
|
|
65
|
+
? {
|
|
66
|
+
[K in keyof Instance as K extends ExtractPublicActionNames<T>
|
|
67
|
+
? Instance[K] extends (...args: any[]) => Promise<any> ? K : never
|
|
68
|
+
: never
|
|
69
|
+
]: Instance[K]
|
|
70
|
+
}
|
|
71
|
+
: Record<string, never>
|
|
65
72
|
: Record<string, never>
|
|
66
73
|
|
|
67
74
|
// ===== Opções do Live.use() =====
|
|
@@ -74,7 +81,7 @@ interface LiveUseOptions<TState> extends UseLiveComponentOptions {
|
|
|
74
81
|
// ===== Hook Principal =====
|
|
75
82
|
|
|
76
83
|
function useLive<
|
|
77
|
-
T extends { new(...args: any[]): any; defaultState?: Record<string, any>; componentName: string },
|
|
84
|
+
T extends { new(...args: any[]): any; defaultState?: Record<string, any>; componentName: string; publicActions?: readonly string[] },
|
|
78
85
|
TBroadcasts extends Record<string, any> = Record<string, any>
|
|
79
86
|
>(
|
|
80
87
|
ComponentClass: T,
|