hightjs 0.2.42 → 0.2.45
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/.idea/copilotDiffState.xml +67 -0
- package/README.md +26 -514
- package/dist/auth/core.js +3 -3
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/providers/google.d.ts +63 -0
- package/dist/auth/providers/google.js +186 -0
- package/dist/auth/providers.d.ts +1 -0
- package/dist/auth/providers.js +3 -1
- package/dist/auth/types.d.ts +6 -7
- package/dist/bin/hightjs.js +393 -0
- package/dist/client/entry.client.js +11 -1
- package/dist/hotReload.d.ts +8 -1
- package/dist/hotReload.js +304 -144
- package/dist/index.d.ts +2 -1
- package/dist/index.js +20 -33
- package/dist/renderer.js +1 -1
- package/dist/router.d.ts +24 -1
- package/dist/router.js +201 -2
- package/dist/types.d.ts +19 -1
- package/docs/README.md +59 -0
- package/docs/adapters.md +7 -0
- package/docs/arquivos-especiais.md +10 -0
- package/docs/autenticacao.md +212 -0
- package/docs/checklist.md +9 -0
- package/docs/cli.md +21 -0
- package/docs/estrutura.md +20 -0
- package/docs/faq.md +10 -0
- package/docs/hot-reload.md +5 -0
- package/docs/middlewares.md +73 -0
- package/docs/rotas-backend.md +45 -0
- package/docs/rotas-frontend.md +66 -0
- package/docs/seguranca.md +8 -0
- package/docs/websocket.md +45 -0
- package/package.json +1 -1
- package/src/auth/core.ts +3 -3
- package/src/auth/index.ts +2 -3
- package/src/auth/providers/google.ts +218 -0
- package/src/auth/providers.ts +1 -1
- package/src/auth/types.ts +3 -8
- package/src/bin/hightjs.js +475 -0
- package/src/client/entry.client.tsx +12 -1
- package/src/hotReload.ts +333 -147
- package/src/index.ts +58 -51
- package/src/renderer.tsx +1 -1
- package/src/router.ts +230 -3
- package/src/types.ts +24 -1
- package/dist/adapters/starters/express.d.ts +0 -0
- package/dist/adapters/starters/express.js +0 -1
- package/dist/adapters/starters/factory.d.ts +0 -0
- package/dist/adapters/starters/factory.js +0 -1
- package/dist/adapters/starters/fastify.d.ts +0 -0
- package/dist/adapters/starters/fastify.js +0 -1
- package/dist/adapters/starters/index.d.ts +0 -0
- package/dist/adapters/starters/index.js +0 -1
- package/dist/adapters/starters/native.d.ts +0 -0
- package/dist/adapters/starters/native.js +0 -1
- package/dist/auth/example.d.ts +0 -40
- package/dist/auth/example.js +0 -104
- package/dist/client/ErrorBoundary.d.ts +0 -16
- package/dist/client/ErrorBoundary.js +0 -181
- package/dist/client/routerContext.d.ts +0 -26
- package/dist/client/routerContext.js +0 -62
- package/dist/eslint/index.d.ts +0 -32
- package/dist/eslint/index.js +0 -15
- package/dist/eslint/use-client-rule.d.ts +0 -19
- package/dist/eslint/use-client-rule.js +0 -99
- package/dist/eslintSetup.d.ts +0 -0
- package/dist/eslintSetup.js +0 -1
- package/dist/example/src/web/routes/index.d.ts +0 -3
- package/dist/example/src/web/routes/index.js +0 -15
- package/dist/typescript/use-client-plugin.d.ts +0 -5
- package/dist/typescript/use-client-plugin.js +0 -113
- package/dist/validation.d.ts +0 -0
- package/dist/validation.js +0 -1
- package/src/auth/example.ts +0 -115
package/src/hotReload.ts
CHANGED
|
@@ -6,28 +6,45 @@ import { IncomingMessage } from 'http';
|
|
|
6
6
|
import * as url from 'url';
|
|
7
7
|
import { clearFileCache } from './router';
|
|
8
8
|
import Console, {Levels} from "./api/console"
|
|
9
|
+
|
|
10
|
+
interface ClientConnection {
|
|
11
|
+
ws: WebSocket;
|
|
12
|
+
pingTimer: NodeJS.Timeout;
|
|
13
|
+
lastPong: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
export class HotReloadManager {
|
|
10
17
|
private wss: WebSocketServer | null = null;
|
|
11
18
|
private watchers: chokidar.FSWatcher[] = [];
|
|
12
19
|
private projectDir: string;
|
|
13
|
-
private clients:
|
|
14
|
-
private pingInterval: NodeJS.Timeout | null = null;
|
|
20
|
+
private clients: Map<WebSocket, ClientConnection> = new Map();
|
|
15
21
|
private backendApiChangeCallback: (() => void) | null = null;
|
|
16
22
|
private frontendChangeCallback: (() => void) | null = null;
|
|
23
|
+
private isShuttingDown: boolean = false;
|
|
24
|
+
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
25
|
+
private customHotReloadListener: ((file: string) => Promise<void> | void) | null = null;
|
|
17
26
|
|
|
18
27
|
constructor(projectDir: string) {
|
|
19
28
|
this.projectDir = projectDir;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
async start() {
|
|
23
|
-
// Não cria servidor na porta separada - será integrado ao Express
|
|
24
32
|
this.setupWatchers();
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
//
|
|
35
|
+
// Método para integrar com Express
|
|
28
36
|
handleUpgrade(request: IncomingMessage, socket: any, head: Buffer) {
|
|
37
|
+
if (this.isShuttingDown) {
|
|
38
|
+
socket.destroy();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
if (!this.wss) {
|
|
30
|
-
this.wss = new WebSocketServer({
|
|
43
|
+
this.wss = new WebSocketServer({
|
|
44
|
+
noServer: true,
|
|
45
|
+
perMessageDeflate: false, // Desabilita compressão para melhor performance
|
|
46
|
+
maxPayload: 1024 * 1024 // Limite de 1MB por mensagem
|
|
47
|
+
});
|
|
31
48
|
this.setupWebSocketServer();
|
|
32
49
|
}
|
|
33
50
|
|
|
@@ -40,200 +57,350 @@ export class HotReloadManager {
|
|
|
40
57
|
if (!this.wss) return;
|
|
41
58
|
|
|
42
59
|
this.wss.on('connection', (ws: WebSocket) => {
|
|
43
|
-
this.
|
|
60
|
+
if (this.isShuttingDown) {
|
|
61
|
+
ws.close();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
44
64
|
|
|
45
|
-
// Setup ping/pong para
|
|
46
|
-
const
|
|
47
|
-
|
|
65
|
+
// Setup ping/pong para detectar conexões mortas
|
|
66
|
+
const pingTimer = setInterval(() => {
|
|
67
|
+
const client = this.clients.get(ws);
|
|
68
|
+
if (client && ws.readyState === WebSocket.OPEN) {
|
|
69
|
+
// Se não recebeu pong há mais de 60 segundos, desconecta
|
|
70
|
+
if (Date.now() - client.lastPong > 60000) {
|
|
71
|
+
ws.terminate();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
48
74
|
ws.ping();
|
|
49
75
|
}
|
|
76
|
+
}, 30000);
|
|
77
|
+
|
|
78
|
+
const clientConnection: ClientConnection = {
|
|
79
|
+
ws,
|
|
80
|
+
pingTimer,
|
|
81
|
+
lastPong: Date.now()
|
|
50
82
|
};
|
|
51
83
|
|
|
52
|
-
|
|
84
|
+
this.clients.set(ws, clientConnection);
|
|
53
85
|
|
|
54
86
|
ws.on('pong', () => {
|
|
55
|
-
|
|
87
|
+
const client = this.clients.get(ws);
|
|
88
|
+
if (client) {
|
|
89
|
+
client.lastPong = Date.now();
|
|
90
|
+
}
|
|
56
91
|
});
|
|
57
92
|
|
|
58
93
|
ws.on('close', () => {
|
|
59
|
-
this.
|
|
60
|
-
clearInterval(pingTimer);
|
|
94
|
+
this.cleanupClient(ws);
|
|
61
95
|
});
|
|
62
96
|
|
|
63
|
-
ws.on('error', () => {
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
ws.on('error', (error) => {
|
|
98
|
+
Console.logWithout(Levels.ERROR, `WebSocket error: ${error.message}`);
|
|
99
|
+
this.cleanupClient(ws);
|
|
66
100
|
});
|
|
101
|
+
|
|
102
|
+
Console.logWithout(Levels.INFO, '🔌 Hot-reload cliente conectado');
|
|
67
103
|
});
|
|
68
104
|
}
|
|
69
105
|
|
|
106
|
+
private cleanupClient(ws: WebSocket) {
|
|
107
|
+
const client = this.clients.get(ws);
|
|
108
|
+
if (client) {
|
|
109
|
+
clearInterval(client.pingTimer);
|
|
110
|
+
this.clients.delete(ws);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
70
114
|
private setupWatchers() {
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
|
|
115
|
+
// Remove watchers antigos e use apenas um watcher global para src
|
|
116
|
+
const debouncedChange = this.debounce((filePath: string) => {
|
|
117
|
+
this.handleAnySrcChange(filePath);
|
|
118
|
+
}, 100);
|
|
119
|
+
|
|
120
|
+
const watcher = chokidar.watch([
|
|
121
|
+
path.join(this.projectDir, 'src/**/*'),
|
|
74
122
|
], {
|
|
75
123
|
ignored: [
|
|
76
124
|
/(^|[\/\\])\../, // arquivos ocultos
|
|
77
|
-
|
|
125
|
+
'**/node_modules/**',
|
|
126
|
+
'**/.git/**',
|
|
127
|
+
'**/dist/**'
|
|
78
128
|
],
|
|
79
129
|
persistent: true,
|
|
80
|
-
ignoreInitial: true
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
clearFileCache(filePath);
|
|
86
|
-
// Checa build do arquivo alterado
|
|
87
|
-
const result = await this.checkFrontendBuild(filePath);
|
|
88
|
-
if (result.error) {
|
|
89
|
-
this.notifyClients('frontend-error', { file: filePath, error: result.error });
|
|
90
|
-
} else {
|
|
91
|
-
this.frontendChangeCallback?.();
|
|
92
|
-
this.notifyClients('frontend-reload');
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
frontendWatcher.on('add', async (filePath) => {
|
|
96
|
-
Console.info(`➕ Novo arquivo frontend: ${path.basename(filePath)}`);
|
|
97
|
-
const result = await this.checkFrontendBuild(filePath);
|
|
98
|
-
if (result.error) {
|
|
99
|
-
this.notifyClients('frontend-error', { file: filePath, error: result.error });
|
|
100
|
-
} else {
|
|
101
|
-
this.frontendChangeCallback?.();
|
|
102
|
-
this.notifyClients('frontend-reload');
|
|
130
|
+
ignoreInitial: true,
|
|
131
|
+
usePolling: false,
|
|
132
|
+
awaitWriteFinish: {
|
|
133
|
+
stabilityThreshold: 100,
|
|
134
|
+
pollInterval: 50
|
|
103
135
|
}
|
|
104
136
|
});
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
|
|
138
|
+
watcher.on('change', debouncedChange);
|
|
139
|
+
watcher.on('add', debouncedChange);
|
|
140
|
+
watcher.on('unlink', (filePath) => {
|
|
141
|
+
Console.info(`🗑️ Arquivo removido: ${path.basename(filePath)}`);
|
|
107
142
|
clearFileCache(filePath);
|
|
143
|
+
this.clearBackendCache(filePath);
|
|
108
144
|
this.frontendChangeCallback?.();
|
|
109
|
-
this.
|
|
145
|
+
this.backendApiChangeCallback?.();
|
|
146
|
+
this.notifyClients('src-reload', { file: filePath, event: 'unlink' });
|
|
110
147
|
});
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
path.join(this.projectDir, 'src/web/backend/routes/**/*.{ts,tsx,js,jsx}'),
|
|
115
|
-
], {
|
|
116
|
-
ignored: /(^|[\/\\])\../,
|
|
117
|
-
persistent: true,
|
|
118
|
-
ignoreInitial: true
|
|
119
|
-
});
|
|
149
|
+
this.watchers.push(watcher);
|
|
150
|
+
}
|
|
120
151
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.notifyClients('backend-api-reload');
|
|
152
|
+
private debounce(func: Function, wait: number): (...args: any[]) => void {
|
|
153
|
+
return (...args: any[]) => {
|
|
154
|
+
const key = args[0]; // usa o primeiro argumento como chave
|
|
125
155
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
const existingTimer = this.debounceTimers.get(key);
|
|
157
|
+
if (existingTimer) {
|
|
158
|
+
clearTimeout(existingTimer);
|
|
159
|
+
}
|
|
129
160
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
this.debounceTimers.delete(key);
|
|
163
|
+
func.apply(this, args);
|
|
164
|
+
}, wait);
|
|
134
165
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.notifyClients('backend-api-reload');
|
|
139
|
-
});
|
|
166
|
+
this.debounceTimers.set(key, timer);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
140
169
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
path.join(this.projectDir, 'src/server.ts'),
|
|
144
|
-
path.join(this.projectDir, 'src/**/*.ts'),
|
|
145
|
-
'!**/src/web/**', // exclui pasta web
|
|
146
|
-
], {
|
|
147
|
-
ignored: /(^|[\/\\])\../,
|
|
148
|
-
persistent: true,
|
|
149
|
-
ignoreInitial: true
|
|
150
|
-
});
|
|
170
|
+
private async handleAnySrcChange(filePath: string) {
|
|
171
|
+
Console.logWithout(Levels.INFO, `🔄 Arquivo alterado: ${path.basename(filePath)}`);
|
|
151
172
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
// Detecta se é arquivo de frontend ou backend
|
|
174
|
+
const isFrontendFile = filePath.includes(path.join('src', 'web', 'routes')) ||
|
|
175
|
+
filePath.includes(path.join('src', 'web', 'components')) ||
|
|
176
|
+
filePath.includes('layout.tsx') ||
|
|
177
|
+
filePath.includes('not-found.tsx') ||
|
|
178
|
+
filePath.endsWith('.tsx') ||
|
|
179
|
+
filePath.endsWith('.jsx');
|
|
180
|
+
|
|
181
|
+
const isBackendFile = filePath.includes(path.join('src', 'web', 'backend')) ||
|
|
182
|
+
(filePath.includes(path.join('src', 'web')) && !isFrontendFile);
|
|
183
|
+
|
|
184
|
+
// Limpa o cache do arquivo alterado
|
|
185
|
+
clearFileCache(filePath);
|
|
186
|
+
this.clearBackendCache(filePath);
|
|
187
|
+
|
|
188
|
+
// Checa build se for .ts/.tsx/.js/.jsx
|
|
189
|
+
const ext = path.extname(filePath);
|
|
190
|
+
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
191
|
+
const result = await this.checkFrontendBuild(filePath);
|
|
192
|
+
if (result.error) {
|
|
193
|
+
this.notifyClients('src-error', { file: filePath, error: result.error });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
155
197
|
|
|
156
|
-
|
|
198
|
+
// Se for arquivo de frontend, notifica o cliente para recarregar a página
|
|
199
|
+
if (isFrontendFile) {
|
|
200
|
+
Console.logWithout(Levels.INFO, `📄 Recarregando frontend...`);
|
|
201
|
+
this.frontendChangeCallback?.();
|
|
202
|
+
this.notifyClients('frontend-reload', { file: filePath, event: 'change' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Se for arquivo de backend, recarrega o módulo e notifica
|
|
206
|
+
if (isBackendFile) {
|
|
207
|
+
Console.logWithout(Levels.INFO, `⚙️ Recarregando backend...`);
|
|
208
|
+
this.backendApiChangeCallback?.();
|
|
209
|
+
this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fallback: se não for nem frontend nem backend detectado, recarrega tudo
|
|
213
|
+
if (!isFrontendFile && !isBackendFile) {
|
|
214
|
+
Console.logWithout(Levels.INFO, `🔄 Recarregando aplicação...`);
|
|
215
|
+
this.frontendChangeCallback?.();
|
|
216
|
+
this.backendApiChangeCallback?.();
|
|
217
|
+
this.notifyClients('src-reload', { file: filePath, event: 'change' });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Chama listener customizado se definido
|
|
221
|
+
if (this.customHotReloadListener) {
|
|
222
|
+
try {
|
|
223
|
+
await this.customHotReloadListener(filePath);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// @ts-ignore
|
|
226
|
+
Console.logWithout(Levels.ERROR, `Erro no listener customizado: ${error.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
157
229
|
}
|
|
158
230
|
|
|
159
231
|
private notifyClients(type: string, data?: any) {
|
|
160
|
-
|
|
232
|
+
if (this.isShuttingDown || this.clients.size === 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
161
235
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
236
|
+
const message = JSON.stringify({ type, data, timestamp: Date.now() });
|
|
237
|
+
const deadClients: WebSocket[] = [];
|
|
238
|
+
|
|
239
|
+
this.clients.forEach((client, ws) => {
|
|
240
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
241
|
+
try {
|
|
242
|
+
ws.send(message);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
Console.logWithout(Levels.ERROR, `Erro ao enviar mensagem WebSocket: ${error}`);
|
|
245
|
+
deadClients.push(ws);
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
deadClients.push(ws);
|
|
165
249
|
}
|
|
166
250
|
});
|
|
251
|
+
|
|
252
|
+
// Remove clientes mortos
|
|
253
|
+
deadClients.forEach(ws => this.cleanupClient(ws));
|
|
167
254
|
}
|
|
168
255
|
|
|
169
256
|
private restartServer() {
|
|
170
|
-
// Notifica clientes que o servidor está reiniciando
|
|
171
257
|
this.notifyClients('server-restart');
|
|
172
|
-
|
|
173
|
-
// Aguarda um pouco e tenta reconectar
|
|
174
258
|
setTimeout(() => {
|
|
175
259
|
this.notifyClients('server-ready');
|
|
176
260
|
}, 2000);
|
|
177
261
|
}
|
|
178
262
|
|
|
179
263
|
stop() {
|
|
264
|
+
this.isShuttingDown = true;
|
|
265
|
+
|
|
266
|
+
// Limpa todos os debounce timers
|
|
267
|
+
this.debounceTimers.forEach(timer => clearTimeout(timer));
|
|
268
|
+
this.debounceTimers.clear();
|
|
269
|
+
|
|
180
270
|
// Para todos os watchers
|
|
181
271
|
this.watchers.forEach(watcher => watcher.close());
|
|
182
272
|
this.watchers = [];
|
|
183
273
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
clearInterval(
|
|
187
|
-
|
|
274
|
+
// Limpa todos os clientes
|
|
275
|
+
this.clients.forEach((client, ws) => {
|
|
276
|
+
clearInterval(client.pingTimer);
|
|
277
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
278
|
+
ws.close();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
this.clients.clear();
|
|
188
282
|
|
|
189
283
|
// Fecha WebSocket server
|
|
190
284
|
if (this.wss) {
|
|
191
285
|
this.wss.close();
|
|
286
|
+
this.wss = null;
|
|
192
287
|
}
|
|
193
288
|
}
|
|
194
289
|
|
|
195
|
-
//
|
|
290
|
+
// Script do cliente otimizado com reconnection backoff
|
|
196
291
|
getClientScript(): string {
|
|
197
292
|
return `
|
|
198
293
|
<script>
|
|
199
294
|
(function() {
|
|
200
295
|
if (typeof window !== 'undefined') {
|
|
201
296
|
let ws;
|
|
202
|
-
let
|
|
297
|
+
let reconnectAttempts = 0;
|
|
298
|
+
let maxReconnectInterval = 30000; // 30 segundos max
|
|
299
|
+
let reconnectInterval = 1000; // Começa com 1 segundo
|
|
300
|
+
let reconnectTimer;
|
|
301
|
+
let isConnected = false;
|
|
203
302
|
|
|
204
303
|
function connect() {
|
|
205
|
-
|
|
304
|
+
// Evita múltiplas tentativas simultâneas
|
|
305
|
+
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
ws = new WebSocket('ws://localhost:3000/hweb-hotreload/');
|
|
311
|
+
|
|
312
|
+
ws.onopen = function() {
|
|
313
|
+
console.log('🔌 Hot-reload conectado');
|
|
314
|
+
isConnected = true;
|
|
315
|
+
reconnectAttempts = 0;
|
|
316
|
+
reconnectInterval = 1000;
|
|
317
|
+
clearTimeout(reconnectTimer);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
ws.onmessage = function(event) {
|
|
321
|
+
try {
|
|
322
|
+
const message = JSON.parse(event.data);
|
|
323
|
+
|
|
324
|
+
switch(message.type) {
|
|
325
|
+
case 'frontend-reload':
|
|
326
|
+
window.location.reload();
|
|
327
|
+
break;
|
|
328
|
+
case 'backend-api-reload':
|
|
329
|
+
// Recarrega apenas se necessário
|
|
330
|
+
window.location.reload();
|
|
331
|
+
break;
|
|
332
|
+
case 'server-restart':
|
|
333
|
+
console.log('🔄 Servidor reiniciando...');
|
|
334
|
+
break;
|
|
335
|
+
case 'server-ready':
|
|
336
|
+
setTimeout(() => window.location.reload(), 500);
|
|
337
|
+
break;
|
|
338
|
+
case 'frontend-error':
|
|
339
|
+
console.error('❌ Erro no frontend:', message.data);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.error('Erro ao processar mensagem do hot-reload:', e);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
ws.onclose = function(event) {
|
|
348
|
+
isConnected = false;
|
|
349
|
+
|
|
350
|
+
// Não tenta reconectar se foi fechamento intencional
|
|
351
|
+
if (event.code === 1000) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
scheduleReconnect();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
ws.onerror = function(error) {
|
|
359
|
+
isConnected = false;
|
|
360
|
+
// Não loga erros de conexão para evitar spam no console
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Erro ao criar WebSocket:', error);
|
|
365
|
+
scheduleReconnect();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function scheduleReconnect() {
|
|
370
|
+
if (reconnectTimer) {
|
|
371
|
+
clearTimeout(reconnectTimer);
|
|
372
|
+
}
|
|
206
373
|
|
|
207
|
-
|
|
208
|
-
clearInterval(reconnectInterval);
|
|
209
|
-
};
|
|
374
|
+
reconnectAttempts++;
|
|
210
375
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
case 'frontend-reload':
|
|
216
|
-
window.location.reload();
|
|
217
|
-
break;
|
|
218
|
-
case 'server-restart':
|
|
219
|
-
break;
|
|
220
|
-
case 'server-ready':
|
|
221
|
-
setTimeout(() => window.location.reload(), 500);
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
376
|
+
// Exponential backoff com jitter
|
|
377
|
+
const baseInterval = Math.min(reconnectInterval * Math.pow(1.5, reconnectAttempts - 1), maxReconnectInterval);
|
|
378
|
+
const jitter = Math.random() * 1000; // Adiciona até 1 segundo de variação
|
|
379
|
+
const finalInterval = baseInterval + jitter;
|
|
225
380
|
|
|
226
|
-
|
|
227
|
-
|
|
381
|
+
reconnectTimer = setTimeout(() => {
|
|
382
|
+
if (!isConnected) {
|
|
228
383
|
connect();
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
ws.onerror = function() {
|
|
233
|
-
// Silencioso - sem logs
|
|
234
|
-
};
|
|
384
|
+
}
|
|
385
|
+
}, finalInterval);
|
|
235
386
|
}
|
|
236
387
|
|
|
388
|
+
// Detecta quando a página está sendo fechada para evitar reconexões desnecessárias
|
|
389
|
+
window.addEventListener('beforeunload', function() {
|
|
390
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
391
|
+
ws.close(1000, 'Page unloading');
|
|
392
|
+
}
|
|
393
|
+
clearTimeout(reconnectTimer);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Detecta quando a aba fica visível novamente para reconectar se necessário
|
|
397
|
+
document.addEventListener('visibilitychange', function() {
|
|
398
|
+
if (!document.hidden && !isConnected) {
|
|
399
|
+
reconnectAttempts = 0; // Reset do contador quando a aba fica ativa
|
|
400
|
+
connect();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
237
404
|
connect();
|
|
238
405
|
}
|
|
239
406
|
})();
|
|
@@ -242,48 +409,67 @@ export class HotReloadManager {
|
|
|
242
409
|
}
|
|
243
410
|
|
|
244
411
|
private clearBackendCache(filePath: string) {
|
|
245
|
-
// Limpa o cache do require para forçar reload da rota de API
|
|
246
412
|
const absolutePath = path.resolve(filePath);
|
|
247
413
|
delete require.cache[absolutePath];
|
|
248
414
|
|
|
249
|
-
//
|
|
415
|
+
// Limpa dependências relacionadas de forma mais eficiente
|
|
416
|
+
const dirname = path.dirname(absolutePath);
|
|
250
417
|
Object.keys(require.cache).forEach(key => {
|
|
251
|
-
if (key.
|
|
418
|
+
if (key.startsWith(dirname)) {
|
|
252
419
|
delete require.cache[key];
|
|
253
420
|
}
|
|
254
421
|
});
|
|
255
422
|
}
|
|
256
423
|
|
|
257
|
-
// Método para registrar callback de mudança de API backend
|
|
258
424
|
onBackendApiChange(callback: () => void) {
|
|
259
425
|
this.backendApiChangeCallback = callback;
|
|
260
426
|
}
|
|
261
427
|
|
|
262
|
-
// Método para registrar callback de mudança de frontend
|
|
263
428
|
onFrontendChange(callback: () => void) {
|
|
264
429
|
this.frontendChangeCallback = callback;
|
|
265
430
|
}
|
|
266
431
|
|
|
432
|
+
setHotReloadListener(listener: (file: string) => Promise<void> | void) {
|
|
433
|
+
this.customHotReloadListener = listener;
|
|
434
|
+
Console.info('🔌 Hot reload listener customizado registrado');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
removeHotReloadListener() {
|
|
438
|
+
this.customHotReloadListener = null;
|
|
439
|
+
}
|
|
440
|
+
|
|
267
441
|
private async checkFrontendBuild(filePath: string) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
442
|
+
try {
|
|
443
|
+
const tsNodePath = require.resolve('ts-node');
|
|
444
|
+
const { spawn } = require('child_process');
|
|
445
|
+
|
|
446
|
+
return new Promise<{ error?: string }>((resolve) => {
|
|
447
|
+
const proc = spawn(process.execPath, [tsNodePath, '--transpile-only', filePath], {
|
|
448
|
+
cwd: this.projectDir,
|
|
449
|
+
env: process.env,
|
|
450
|
+
timeout: 10000 // Timeout de 10 segundos
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
let errorMsg = '';
|
|
454
|
+
|
|
455
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
456
|
+
errorMsg += data.toString();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
proc.on('close', (code: number) => {
|
|
460
|
+
if (code !== 0 && errorMsg) {
|
|
461
|
+
resolve({ error: errorMsg });
|
|
462
|
+
} else {
|
|
463
|
+
resolve({});
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
proc.on('error', (error: Error) => {
|
|
468
|
+
resolve({ error: error.message });
|
|
469
|
+
});
|
|
286
470
|
});
|
|
287
|
-
})
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return { error: `Erro ao verificar build: ${error}` };
|
|
473
|
+
}
|
|
288
474
|
}
|
|
289
475
|
}
|