vatts 1.0.1 → 1.0.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hotReload.js CHANGED
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.HotReloadManager = void 0;
37
40
  /*
@@ -53,9 +56,25 @@ exports.HotReloadManager = void 0;
53
56
  const ws_1 = require("ws");
54
57
  const chokidar = __importStar(require("chokidar"));
55
58
  const path = __importStar(require("path"));
59
+ const typescript_1 = __importDefault(require("typescript"));
56
60
  const router_1 = require("./router");
57
61
  const console_1 = __importStar(require("./api/console"));
62
+ // Chaves para persistência global para sobreviver a reloads do backend
63
+ const GLOBAL_ERROR_KEY = '__VATTS_LAST_BUILD_ERROR__';
64
+ const GLOBAL_ACTIVE_MANAGER_KEY = '__VATTS_ACTIVE_HOT_RELOAD_MANAGER__';
58
65
  class HotReloadManager {
66
+ // Getter/Setter para acessar o erro persistente no escopo Global
67
+ // Agora armazena um objeto { frontend: ..., backend: ... }
68
+ get buildState() {
69
+ const state = global[GLOBAL_ERROR_KEY];
70
+ if (!state) {
71
+ return { frontend: null, backend: null };
72
+ }
73
+ return state;
74
+ }
75
+ set buildState(value) {
76
+ global[GLOBAL_ERROR_KEY] = value;
77
+ }
59
78
  constructor(projectDir) {
60
79
  this.wss = null;
61
80
  this.watchers = [];
@@ -67,13 +86,23 @@ class HotReloadManager {
67
86
  this.customHotReloadListener = null;
68
87
  this.isBuilding = false;
69
88
  this.buildCompleteResolve = null;
70
- this.lastBuildError = null;
89
+ // Impede que um "success" fora de ordem limpe um erro real de frontend.
90
+ this.lastFrontendErrorBuildId = 0;
91
+ // DEBUG: stack traces (rate-limited) para descobrir quem dispara onBuildComplete
92
+ this.lastBuildCompleteTraceAt = 0;
93
+ this.lastTypecheckAt = 0;
94
+ this.lastTypecheckResult = null;
71
95
  this.projectDir = projectDir;
96
+ // Registra esta instância como a ativa globalmente
97
+ global[GLOBAL_ACTIVE_MANAGER_KEY] = this;
98
+ // Inicializa estado se vazio
99
+ if (!global[GLOBAL_ERROR_KEY]) {
100
+ this.buildState = { frontend: null, backend: null };
101
+ }
72
102
  }
73
103
  async start() {
74
104
  this.setupWatchers();
75
105
  }
76
- // Método para integrar com Express
77
106
  handleUpgrade(request, socket, head) {
78
107
  if (this.isShuttingDown) {
79
108
  socket.destroy();
@@ -82,8 +111,8 @@ class HotReloadManager {
82
111
  if (!this.wss) {
83
112
  this.wss = new ws_1.WebSocketServer({
84
113
  noServer: true,
85
- perMessageDeflate: false, // Desabilita compressão para melhor performance
86
- maxPayload: 1024 * 1024 // Limite de 1MB por mensagem
114
+ perMessageDeflate: false,
115
+ maxPayload: 1024 * 1024
87
116
  });
88
117
  this.setupWebSocketServer();
89
118
  }
@@ -99,11 +128,9 @@ class HotReloadManager {
99
128
  ws.close();
100
129
  return;
101
130
  }
102
- // Setup ping/pong para detectar conexões mortas
103
131
  const pingTimer = setInterval(() => {
104
132
  const client = this.clients.get(ws);
105
133
  if (client && ws.readyState === ws_1.WebSocket.OPEN) {
106
- // Se não recebeu pong há mais de 60 segundos, desconecta
107
134
  if (Date.now() - client.lastPong > 60000) {
108
135
  ws.terminate();
109
136
  return;
@@ -117,39 +144,17 @@ class HotReloadManager {
117
144
  lastPong: Date.now()
118
145
  };
119
146
  this.clients.set(ws, clientConnection);
120
- // Ao conectar, envia o status atual (especialmente último erro de build),
121
- // mas faz isso no próximo tick pra evitar corrida do handshake.
147
+ // Ao conectar, envia o status atual combinado
122
148
  setTimeout(() => {
123
149
  if (ws.readyState !== ws_1.WebSocket.OPEN)
124
150
  return;
125
- if (this.lastBuildError) {
126
- try {
127
- ws.send(JSON.stringify({ type: 'build-error', data: this.lastBuildError, timestamp: Date.now() }));
128
- }
129
- catch {
130
- // ignore
131
- }
132
- }
133
- else {
134
- try {
135
- ws.send(JSON.stringify({ type: 'build-complete', data: { success: true }, timestamp: Date.now() }));
136
- }
137
- catch {
138
- // ignore
139
- }
140
- }
151
+ this.broadcastCurrentState(ws);
141
152
  }, 0);
142
153
  ws.on('message', (raw) => {
143
154
  try {
144
155
  const msg = JSON.parse(String(raw || ''));
145
156
  if (msg?.type === 'status-request') {
146
- // responde com o estado atual
147
- if (this.lastBuildError) {
148
- ws.send(JSON.stringify({ type: 'build-error', data: this.lastBuildError, timestamp: Date.now() }));
149
- }
150
- else {
151
- ws.send(JSON.stringify({ type: 'build-complete', data: { success: true }, timestamp: Date.now() }));
152
- }
157
+ this.broadcastCurrentState(ws);
153
158
  }
154
159
  }
155
160
  catch {
@@ -171,6 +176,23 @@ class HotReloadManager {
171
176
  });
172
177
  });
173
178
  }
179
+ // Helper para enviar o estado correto (Erro se Tiver Frontend OU Backend error)
180
+ broadcastCurrentState(ws) {
181
+ const state = this.buildState;
182
+ const activeError = state.backend || state.frontend;
183
+ if (activeError) {
184
+ try {
185
+ ws.send(JSON.stringify({ type: 'build-error', data: activeError, timestamp: Date.now() }));
186
+ }
187
+ catch { }
188
+ }
189
+ else {
190
+ try {
191
+ ws.send(JSON.stringify({ type: 'build-complete', data: { success: true }, timestamp: Date.now() }));
192
+ }
193
+ catch { }
194
+ }
195
+ }
174
196
  cleanupClient(ws) {
175
197
  const client = this.clients.get(ws);
176
198
  if (client) {
@@ -179,7 +201,6 @@ class HotReloadManager {
179
201
  }
180
202
  }
181
203
  setupWatchers() {
182
- // Remove watchers antigos e use apenas um watcher global para src
183
204
  const debouncedChange = this.debounce((filePath) => {
184
205
  this.handleAnySrcChange(filePath);
185
206
  }, 100);
@@ -187,7 +208,7 @@ class HotReloadManager {
187
208
  path.join(this.projectDir, 'src/**/*'),
188
209
  ], {
189
210
  ignored: [
190
- /(^|[\/\\])\../, // arquivos ocultos
211
+ /(^|[\/\\])\../,
191
212
  '**/node_modules/**',
192
213
  '**/.git/**',
193
214
  '**/dist/**'
@@ -203,22 +224,32 @@ class HotReloadManager {
203
224
  watcher.on('change', debouncedChange);
204
225
  watcher.on('add', debouncedChange);
205
226
  watcher.on('unlink', (filePath) => {
206
- console_1.default.info(`🗑️ Arquivo removido: ${path.basename(filePath)}`);
227
+ console_1.default.info(`File removed: ${path.basename(filePath)}`);
207
228
  (0, router_1.clearFileCache)(filePath);
208
229
  this.clearBackendCache(filePath);
209
- this.frontendChangeCallback?.();
210
- this.backendApiChangeCallback?.();
230
+ // Unlink também precisa de try-catch se for chamar callbacks
231
+ try {
232
+ this.frontendChangeCallback?.();
233
+ }
234
+ catch (e) {
235
+ this.setBackendError(e);
236
+ }
237
+ try {
238
+ this.backendApiChangeCallback?.();
239
+ }
240
+ catch (e) {
241
+ this.setBackendError(e);
242
+ }
211
243
  this.notifyClients('src-reload', { file: filePath, event: 'unlink' });
212
244
  });
213
245
  this.watchers.push(watcher);
214
246
  }
215
247
  debounce(func, wait) {
216
248
  return (...args) => {
217
- const key = args[0]; // usa o primeiro argumento como chave
249
+ const key = args[0];
218
250
  const existingTimer = this.debounceTimers.get(key);
219
- if (existingTimer) {
251
+ if (existingTimer)
220
252
  clearTimeout(existingTimer);
221
- }
222
253
  const timer = setTimeout(() => {
223
254
  this.debounceTimers.delete(key);
224
255
  func.apply(this, args);
@@ -226,44 +257,108 @@ class HotReloadManager {
226
257
  this.debounceTimers.set(key, timer);
227
258
  };
228
259
  }
260
+ // Método para capturar erro do Backend explicitamente
261
+ setBackendError(error) {
262
+ const currentState = this.buildState;
263
+ // Formata o erro para ser amigável ao JSON
264
+ const errorData = {
265
+ message: error?.message || 'Unknown Backend Error',
266
+ stack: error?.stack,
267
+ type: 'BackendError',
268
+ ts: Date.now()
269
+ };
270
+ // Se for erro de TypeScript/ts-node compilando TSX, trata como erro de FRONT também
271
+ // (é a mesma causa raiz que impede o app de rodar).
272
+ const isTypeScriptCompileError = typeof errorData.message === 'string' &&
273
+ (errorData.message.includes('Unable to compile TypeScript') ||
274
+ errorData.message.includes('TSError') ||
275
+ errorData.message.includes('TS2304') ||
276
+ errorData.message.includes('TS1005') ||
277
+ errorData.message.includes('TS17002'));
278
+ console_1.default.error("Captured Backend Error:", errorData.message);
279
+ this.buildState = {
280
+ ...currentState,
281
+ backend: errorData,
282
+ frontend: isTypeScriptCompileError ? (currentState.frontend || errorData) : currentState.frontend
283
+ };
284
+ // Notifica clientes imediatamente
285
+ this.notifyStatusChange();
286
+ }
229
287
  async handleAnySrcChange(filePath) {
230
288
  const dm = console_1.default.dynamicLine(`File change detected ${path.basename(filePath)}, processing...`);
231
- // Detecta se é arquivo de frontend ou backend
289
+ let hasBackendError = false;
232
290
  const isFrontendFile = filePath.includes(path.join('src', 'web', 'routes')) ||
233
291
  filePath.includes(path.join('src', 'web', 'components')) ||
234
292
  filePath.includes('layout.tsx') ||
235
293
  filePath.includes('not-found.tsx') ||
236
294
  filePath.endsWith('.tsx');
237
295
  const isBackendFile = filePath.includes(path.join('src', 'backend')) && !isFrontendFile;
238
- // Limpa o cache do arquivo alterado
239
296
  (0, router_1.clearFileCache)(filePath);
240
297
  this.clearBackendCache(filePath);
241
- // Se for arquivo de frontend, NÃO aguarda build. Apenas avisa o frontend para tentar um soft reload.
242
- // Em modo dev com Vite, o bundler já recompila/serve as mudanças.
243
- if (isFrontendFile) {
244
- console_1.default.logWithout(console_1.Levels.INFO, undefined, `Frontend change detected: ${path.basename(filePath)}`);
245
- // Mantém a regeneração do entry file/rotas no backend (necessário para novas rotas/layout/not-found)
246
- this.frontendChangeCallback?.();
247
- dm.end(`Frontend change detected for ${path.basename(filePath)}, notifying clients.`);
248
- // O client tenta HMR/soft-reload (sem recarregar a página inteira)
249
- this.notifyClients('frontend-reload', { file: filePath, event: 'change' });
250
- return;
298
+ // Tenta executar os callbacks e captura erros do Backend (Router/SSR)
299
+ try {
300
+ if (isFrontendFile) {
301
+ console_1.default.logWithout(console_1.Levels.INFO, undefined, `Frontend change detected: ${path.basename(filePath)}`);
302
+ // CRÍTICO: Executa e captura erro
303
+ try {
304
+ this.frontendChangeCallback?.();
305
+ // Se passou aqui, limpa erro de backend anterior
306
+ const s = this.buildState;
307
+ if (s.backend) {
308
+ this.buildState = { ...s, backend: null };
309
+ this.notifyStatusChange();
310
+ }
311
+ }
312
+ catch (e) {
313
+ this.setBackendError(e);
314
+ hasBackendError = true;
315
+ }
316
+ dm.end(`Frontend change detected for ${path.basename(filePath)}, notifying clients.`);
317
+ this.notifyClients('frontend-reload', { file: filePath, event: 'change' });
318
+ }
319
+ else if (isBackendFile) {
320
+ console_1.default.logWithout(console_1.Levels.INFO, undefined, `Backend change detected: ${path.basename(filePath)}. Reloading backend...`);
321
+ try {
322
+ this.backendApiChangeCallback?.();
323
+ // Se passou aqui, limpa erro de backend anterior
324
+ const s = this.buildState;
325
+ if (s.backend) {
326
+ this.buildState = { ...s, backend: null };
327
+ this.notifyStatusChange();
328
+ }
329
+ }
330
+ catch (e) {
331
+ this.setBackendError(e);
332
+ hasBackendError = true;
333
+ }
334
+ this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
335
+ dm.end(`Backend reloaded for ${path.basename(filePath)}.`);
336
+ }
337
+ else {
338
+ // Fallback
339
+ try {
340
+ this.frontendChangeCallback?.();
341
+ }
342
+ catch (e) {
343
+ this.setBackendError(e);
344
+ hasBackendError = true;
345
+ }
346
+ try {
347
+ this.backendApiChangeCallback?.();
348
+ }
349
+ catch (e) {
350
+ if (!hasBackendError)
351
+ this.setBackendError(e);
352
+ hasBackendError = true;
353
+ }
354
+ this.notifyClients('src-reload', { file: filePath, event: 'change' });
355
+ dm.end(`Application reload triggered by ${path.basename(filePath)}.`);
356
+ }
251
357
  }
252
- // Se for arquivo de backend, recarrega o módulo e notifica
253
- if (isBackendFile) {
254
- console_1.default.logWithout(console_1.Levels.INFO, undefined, `Backend change detected: ${path.basename(filePath)}. Reloading backend...`);
255
- this.backendApiChangeCallback?.();
256
- this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
257
- dm.end(`Backend reloaded for ${path.basename(filePath)}.`);
258
- return;
358
+ catch (e) {
359
+ // Catch-all de segurança
360
+ this.setBackendError(e);
259
361
  }
260
- // Fallback: se não for nem frontend nem backend detectado, recarrega tudo
261
- console_1.default.logWithout(console_1.Levels.INFO, undefined, `File change detected (misc): ${path.basename(filePath)}. Reloading application...`);
262
- this.frontendChangeCallback?.();
263
- this.backendApiChangeCallback?.();
264
- this.notifyClients('src-reload', { file: filePath, event: 'change' });
265
- dm.end(`Application reload triggered by ${path.basename(filePath)}.`);
266
- // Chama listener customizado se definido
267
362
  if (this.customHotReloadListener) {
268
363
  try {
269
364
  await this.customHotReloadListener(filePath);
@@ -274,9 +369,26 @@ class HotReloadManager {
274
369
  }
275
370
  }
276
371
  }
372
+ notifyStatusChange() {
373
+ const state = this.buildState;
374
+ const activeError = state.backend || state.frontend; // Backend tem prioridade ou unificamos?
375
+ if (activeError) {
376
+ this.notifyClients('build-error', activeError);
377
+ }
378
+ else {
379
+ this.notifyClients('build-complete', { success: true });
380
+ }
381
+ }
277
382
  notifyClients(type, data) {
278
- if (this.isShuttingDown || this.clients.size === 0) {
383
+ if (this.isShuttingDown)
279
384
  return;
385
+ if (this.clients.size === 0) {
386
+ const activeManager = global[GLOBAL_ACTIVE_MANAGER_KEY];
387
+ if (activeManager && activeManager !== this) {
388
+ // @ts-ignore
389
+ activeManager.notifyClients(type, data);
390
+ return;
391
+ }
280
392
  }
281
393
  const message = JSON.stringify({ type, data, timestamp: Date.now() });
282
394
  const deadClients = [];
@@ -286,7 +398,6 @@ class HotReloadManager {
286
398
  ws.send(message);
287
399
  }
288
400
  catch (error) {
289
- console_1.default.logWithout(console_1.Levels.ERROR, console_1.Colors.BgRed, `Error sending WebSocket message: ${error}`);
290
401
  deadClients.push(ws);
291
402
  }
292
403
  }
@@ -294,15 +405,9 @@ class HotReloadManager {
294
405
  deadClients.push(ws);
295
406
  }
296
407
  });
297
- // Remove clientes mortos
298
408
  deadClients.forEach(ws => this.cleanupClient(ws));
299
409
  }
300
- restartServer() {
301
- this.notifyClients('server-restart');
302
- setTimeout(() => {
303
- this.notifyClients('server-ready');
304
- }, 2000);
305
- }
410
+ // ... (rest of methods like getClientScript, clearBackendCache keep unchanged or minimally adjusted)
306
411
  // Script do cliente otimizado com reconnection backoff
307
412
  getClientScript() {
308
413
  return `
@@ -315,165 +420,89 @@ class HotReloadManager {
315
420
  let reconnectInterval = 1000;
316
421
  let reconnectTimer;
317
422
  let isConnected = false;
318
-
319
- // Estado de erro de build (o app pode usar para UI)
320
423
  let lastBuildError = null;
321
424
 
322
425
  function emitBuildError(error) {
323
426
  try {
324
427
  lastBuildError = error;
325
- window.__VATTS_BUILD_ERROR__ = error;
326
- try {
327
- sessionStorage.setItem('__VATTS_BUILD_ERROR__', JSON.stringify(error));
328
- } catch {}
329
-
428
+ // Não persistimos erro no client. Ele sempre vem do servidor via WebSocket.
330
429
  window.dispatchEvent(new CustomEvent('vatts:build-error', { detail: error }));
331
- } catch {
332
- // noop
333
- }
430
+ } catch {}
334
431
  }
335
432
 
336
433
  function clearBuildError() {
337
434
  try {
338
435
  lastBuildError = null;
339
- window.__VATTS_BUILD_ERROR__ = null;
340
- try {
341
- sessionStorage.removeItem('__VATTS_BUILD_ERROR__');
342
- } catch {}
343
-
344
436
  window.dispatchEvent(new CustomEvent('vatts:build-ok', { detail: { ts: Date.now() } }));
345
- } catch {
346
- // noop
347
- }
437
+ } catch {}
348
438
  }
349
439
 
350
- // Se a página recarregou e já existe erro salvo, re-dispara o evento
351
- // para garantir que o React pegue (mesmo se o WS ainda não mandou).
352
- (function bootstrapStoredBuildError() {
353
- try {
354
- const existing = window.__VATTS_BUILD_ERROR__;
355
- if (existing) {
356
- setTimeout(() => {
357
- try { window.dispatchEvent(new CustomEvent('vatts:build-error', { detail: existing })); } catch {}
358
- }, 0);
359
- return;
360
- }
361
-
362
- const fromStorage = sessionStorage.getItem('__VATTS_BUILD_ERROR__');
363
- if (fromStorage) {
364
- const parsed = JSON.parse(fromStorage);
365
- window.__VATTS_BUILD_ERROR__ = parsed;
366
- setTimeout(() => {
367
- try { window.dispatchEvent(new CustomEvent('vatts:build-error', { detail: parsed })); } catch {}
368
- }, 0);
369
- }
370
- } catch {
371
- // ignore
372
- }
373
- })();
440
+ // Não faz bootstrap de erro salvo (sem sessionStorage/localStorage).
374
441
 
375
442
  function notifyHotReloadState(state, payload) {
376
443
  try {
377
- // estado global simples pra debug/telemetria local
378
444
  window.__VATTS_HOT_RELOAD__ = { state: state, payload: payload || null, ts: Date.now() };
379
445
  const ev = new CustomEvent('vatts:hotreload', { detail: window.__VATTS_HOT_RELOAD__ });
380
446
  window.dispatchEvent(ev);
381
- } catch {
382
- // noop
383
- }
447
+ } catch {}
384
448
  }
385
449
 
386
450
  function requestBuildStatus(reason) {
387
451
  try {
388
452
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
389
453
  ws.send(JSON.stringify({ type: 'status-request', reason: reason || 'unknown', ts: Date.now() }));
390
- } catch {
391
- // ignore
392
- }
454
+ } catch {}
393
455
  }
394
456
 
395
- // Aguarda o main.js ficar "atual" antes de disparar o soft reload.
396
- // Isso evita o problema de o evento chegar antes do bundler (Vite/dev server) terminar de servir o novo conteúdo.
397
457
  async function waitForMainToBeUpdated(timeoutMs, pollMs) {
398
458
  try {
399
459
  const script = document.querySelector('script[src*="main.js"]');
400
- if (!script || !script.src) return true; // se não achou, não bloqueia
401
-
460
+ if (!script || !script.src) return true;
402
461
  const mainUrl = script.src.split('?')[0];
403
462
  const start = Date.now();
404
-
405
- // conteúdo atual (baseline)
406
463
  let baseline = '';
407
464
  try {
408
465
  const r0 = await fetch(mainUrl + '?t=' + Date.now(), { cache: 'no-store' });
409
466
  baseline = await r0.text();
410
- } catch {
411
- // se falhar o fetch baseline, segue sem bloquear
412
- return true;
413
- }
467
+ } catch { return true; }
414
468
 
415
469
  while (Date.now() - start < timeoutMs) {
416
470
  await new Promise(r => setTimeout(r, pollMs));
417
471
  try {
418
472
  const r = await fetch(mainUrl + '?t=' + Date.now(), { cache: 'no-store' });
419
473
  const txt = await r.text();
420
- if (txt && txt !== baseline) {
421
- return true;
422
- }
423
- } catch {
424
- // ignora e continua tentando
425
- }
474
+ if (txt && txt !== baseline) return true;
475
+ } catch {}
426
476
  }
427
-
428
- // timeout: deixa o app seguir e usar fallback (reload) se precisar
429
477
  return false;
430
- } catch {
431
- return true;
432
- }
478
+ } catch { return true; }
433
479
  }
434
480
 
435
481
  function connect() {
436
- const url = window.location; // Objeto com info da URL atual
437
- const protocol = url.protocol === "https:" ? "wss:" : "ws:"; // Usa wss se for https
482
+ const url = window.location;
483
+ const protocol = url.protocol === "https:" ? "wss:" : "ws:";
438
484
  const wsUrl = protocol + '//' + url.host + '/hweb-hotreload/';
439
- if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
440
- return;
441
- }
485
+ if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) return;
442
486
 
443
487
  try {
444
488
  ws = new WebSocket(wsUrl);
445
-
446
489
  ws.onopen = function() {
447
490
  console.log('\u001b[32m[vatts]\u001b[0m Hot-reload connected');
448
491
  isConnected = true;
449
492
  reconnectAttempts = 0;
450
- reconnectInterval = 1000;
451
493
  clearTimeout(reconnectTimer);
452
-
453
- // Pede o status atual (erro de build / ok) sempre que reconectar.
494
+ // Ao conectar, sempre pede o status atual ao servidor.
454
495
  requestBuildStatus('ws-open');
455
-
456
- // Também tenta sincronizar UI com algum erro já persistido
457
- try {
458
- if (window.__VATTS_BUILD_ERROR__) {
459
- setTimeout(() => {
460
- try { window.dispatchEvent(new CustomEvent('vatts:build-error', { detail: window.__VATTS_BUILD_ERROR__ })); } catch {}
461
- }, 0);
462
- }
463
- } catch {}
464
496
  };
465
497
 
466
498
  ws.onmessage = function(event) {
467
499
  try {
468
500
  const message = JSON.parse(event.data);
469
-
470
501
  switch(message.type) {
471
502
  case 'frontend-reload':
472
- // Agora pode recarregar mesmo com erro.
473
503
  handleFrontendReload(message.data);
474
504
  break;
475
505
  case 'backend-api-reload':
476
- // Backend ainda precisa do callback (server-side). No client, fazemos reload por segurança.
477
506
  console.log('[vatts] Backend changed, reloading page...');
478
507
  window.location.reload();
479
508
  break;
@@ -484,8 +513,8 @@ class HotReloadManager {
484
513
  setTimeout(() => window.location.reload(), 500);
485
514
  break;
486
515
  case 'build-error':
516
+ console.log('Erro detectado')
487
517
  notifyHotReloadState('build-error', message.data);
488
- // Em vez de só console.log, manda pro app renderizar UI.
489
518
  emitBuildError(message.data);
490
519
  break;
491
520
  case 'build-complete':
@@ -493,216 +522,263 @@ class HotReloadManager {
493
522
  clearBuildError();
494
523
  console.log('[vatts] Build complete');
495
524
  break;
496
- case 'frontend-error':
497
- console.error('[vatts] Frontend error:', message.data);
498
- break;
499
- case 'hmr-update':
500
- handleHMRUpdate(message.data);
501
- break;
502
525
  }
503
526
  } catch (e) {
504
- console.error('[vatts] Erro ao processar mensagem do hot-reload:', e);
527
+ console.error('[vatts] Error processing msg:', e);
505
528
  }
506
529
  };
507
530
 
508
531
  async function handleFrontendReload(data) {
509
- // sinaliza no DevIndicator que estamos processando reload
510
532
  notifyHotReloadState('reloading', { type: 'frontend', data: data || null });
511
-
512
533
  if (!data || !data.file) {
513
- // Sem info suficiente, tenta soft reload mesmo
514
- const event = new CustomEvent('hmr:component-update', {
515
- detail: { file: null, timestamp: Date.now() }
516
- });
517
- window.dispatchEvent(event);
534
+ window.dispatchEvent(new CustomEvent('hmr:component-update', { detail: { file: null, timestamp: Date.now() } }));
518
535
  return;
519
536
  }
520
-
521
537
  const file = (data.file || '').toLowerCase();
522
538
  console.log('[vatts] Frontend changed:', data.file);
523
-
524
- // Mudanças que exigem reload completo
525
- const needsFullReload =
526
- file.includes('layout.tsx') ||
527
- file.includes('not-found.tsx') ||
528
- file.endsWith('.css');
529
-
539
+ const needsFullReload = file.includes('layout.tsx') || file.includes('not-found.tsx') || file.endsWith('.css');
530
540
  if (needsFullReload) {
531
- console.log('[vatts] Layout/CSS changed, full reload...');
532
541
  notifyHotReloadState('full-reload', { reason: 'layout/css', file: data.file });
533
542
  window.location.reload();
534
543
  return;
535
544
  }
536
545
 
537
- // Espera o bundle (main.js) ser atualizado/servido antes de disparar HMR.
538
- // Timeout curtinho pra não travar o dev flow.
546
+ // Recarrega main.js (o bundle do app) com cache-busting.
547
+ // Em DEV, React/ReactDOM agora são externos, então isso não duplica hooks.
539
548
  await waitForMainToBeUpdated(5000, 120);
540
549
 
541
- // Dispara um soft reload (HMR via evento capturado pelo React)
542
- const ts = Date.now();
543
- const event = new CustomEvent('hmr:component-update', {
544
- detail: { file: data.file, timestamp: ts }
545
- });
546
- window.dispatchEvent(event);
550
+ const script = document.querySelector('script[src*="main.js"]');
551
+ if (script) {
552
+ const mainUrl = (script.getAttribute('src') || script.src || '');
553
+ const base = String(mainUrl).split('?')[0];
554
+
555
+ // Remove scripts antigos de main.js para evitar múltiplas execuções do bundle
556
+ try {
557
+ document.querySelectorAll('script[src*="main.js"]').forEach(s => {
558
+ if (s && s.parentNode) s.parentNode.removeChild(s);
559
+ });
560
+ } catch {}
561
+
562
+ const newScript = document.createElement('script');
563
+ newScript.type = 'module';
564
+ newScript.src = base + '?t=' + Date.now();
565
+
566
+ newScript.onload = () => {
567
+ // depois de carregar o novo bundle, pede render/update
568
+ const ts = Date.now();
569
+ window.dispatchEvent(new CustomEvent('hmr:component-update', { detail: { file: data.file, timestamp: ts } }));
570
+ setTimeout(() => requestBuildStatus('after-frontend-reload'), 50);
571
+ };
547
572
 
548
- // Após disparar o HMR, pede ao backend o status atual do build.
549
- // Isso garante que, se o código ainda estiver com erro, o modal reapareça/atualize.
550
- setTimeout(() => requestBuildStatus('after-frontend-reload'), 50);
573
+ newScript.onerror = () => {
574
+ window.location.reload();
575
+ };
576
+
577
+ document.head.appendChild(newScript);
578
+ } else {
579
+ // fallback
580
+ const ts = Date.now();
581
+ window.dispatchEvent(new CustomEvent('hmr:component-update', { detail: { file: data.file, timestamp: ts } }));
582
+ setTimeout(() => requestBuildStatus('after-frontend-reload'), 50);
583
+ }
551
584
 
552
- // Se após um tempo o app não marcou sucesso, cai pro reload completo
553
585
  setTimeout(() => {
554
- const hmrSuccess = window.__HMR_SUCCESS__;
555
- if (hmrSuccess) {
586
+ if (window.__HMR_SUCCESS__) {
556
587
  notifyHotReloadState('idle', { success: true, file: data.file });
557
- // Garante sincronização final do status
558
588
  requestBuildStatus('after-hmr-success');
559
589
  return;
560
590
  }
561
-
562
- console.log('[vatts] Soft reload failed, falling back to full reload');
563
- notifyHotReloadState('full-reload', { reason: 'hmr-failed', file: data.file });
591
+
564
592
  window.location.reload();
565
- }, 1200);
566
- }
567
-
568
- function handleHMRUpdate(data) {
569
- console.log('[vatts] HMR Update:', data);
570
-
571
- // Dispara evento customizado para o React capturar
572
- const event = new CustomEvent('hmr:update', {
573
- detail: data
574
- });
575
- window.dispatchEvent(event);
593
+ }, 1600);
576
594
  }
577
595
 
578
596
  ws.onclose = function(event) {
579
597
  isConnected = false;
580
-
581
- // Não tenta reconectar se foi fechamento intencional
582
- if (event.code === 1000) {
583
- return;
584
- }
585
-
598
+ if (event.code === 1000) return;
586
599
  scheduleReconnect();
587
600
  };
588
-
589
- ws.onerror = function(error) {
590
- isConnected = false;
591
- // Não loga erros de conexão para evitar spam no console
592
- };
593
-
594
- } catch (error) {
595
- console.error('[vatts] Error creating WebSocket:', error);
596
- scheduleReconnect();
597
- }
601
+ ws.onerror = function() {};
602
+ } catch (error) { scheduleReconnect(); }
598
603
  }
599
604
 
600
605
  function scheduleReconnect() {
601
- if (reconnectTimer) {
602
- clearTimeout(reconnectTimer);
603
- }
604
-
606
+ if (reconnectTimer) clearTimeout(reconnectTimer);
605
607
  reconnectAttempts++;
606
-
607
- // Exponential backoff com jitter
608
608
  const baseInterval = Math.min(reconnectInterval * Math.pow(1.5, reconnectAttempts - 1), maxReconnectInterval);
609
- const jitter = Math.random() * 1000; // Adiciona até 1 segundo de variação
610
- const finalInterval = baseInterval + jitter;
611
-
612
- reconnectTimer = setTimeout(() => {
613
- if (!isConnected) {
614
- connect();
615
- }
616
- }, finalInterval);
609
+ reconnectTimer = setTimeout(() => { if (!isConnected) connect(); }, baseInterval + Math.random() * 1000);
617
610
  }
618
611
 
619
- // Detecta quando a página está sendo fechada para evitar reconexões desnecessárias
620
- window.addEventListener('beforeunload', function() {
621
- if (ws && ws.readyState === WebSocket.OPEN) {
622
- ws.close(1000, 'Page unloading');
623
- }
624
- clearTimeout(reconnectTimer);
625
- });
626
-
627
- // Detecta quando a aba fica visível novamente para reconectar se necessário
628
- document.addEventListener('visibilitychange', function() {
629
- if (!document.hidden && !isConnected) {
630
- reconnectAttempts = 0; // Reset do contador quando a aba fica ativa
631
- connect();
632
- }
633
- });
634
-
612
+ window.addEventListener('beforeunload', function() { if (ws && ws.readyState === WebSocket.OPEN) ws.close(1000, 'Page unloading'); });
613
+ document.addEventListener('visibilitychange', function() { if (!document.hidden && !isConnected) { reconnectAttempts = 0; connect(); } });
635
614
  connect();
636
615
  }
637
616
  })();
638
617
  </script>
639
618
  `;
640
619
  }
620
+ // Mantendo métodos de cache iguais
641
621
  clearBackendCache(filePath) {
642
622
  const absolutePath = path.resolve(filePath);
643
623
  delete require.cache[absolutePath];
644
- // Limpa dependências relacionadas de forma mais eficiente
645
624
  const dirname = path.dirname(absolutePath);
646
625
  Object.keys(require.cache).forEach(key => {
647
- if (key.startsWith(dirname)) {
626
+ if (key.startsWith(dirname))
648
627
  delete require.cache[key];
649
- }
650
628
  });
651
629
  }
652
- onBackendApiChange(callback) {
653
- this.backendApiChangeCallback = callback;
654
- }
655
- onFrontendChange(callback) {
656
- this.frontendChangeCallback = callback;
657
- }
630
+ onBackendApiChange(callback) { this.backendApiChangeCallback = callback; }
631
+ onFrontendChange(callback) { this.frontendChangeCallback = callback; }
658
632
  setHotReloadListener(listener) {
659
633
  this.customHotReloadListener = listener;
660
- console_1.default.info('🔌 Hot reload custom listener registered');
634
+ console_1.default.info('Hot reload custom listener registered');
661
635
  }
662
- removeHotReloadListener() {
663
- this.customHotReloadListener = null;
636
+ removeHotReloadListener() { this.customHotReloadListener = null; }
637
+ traceBuildComplete(success, error) {
638
+ try {
639
+ const now = Date.now();
640
+ // Rate limit pra não floodar o console
641
+ if (now - this.lastBuildCompleteTraceAt < 500)
642
+ return;
643
+ this.lastBuildCompleteTraceAt = now;
644
+ }
645
+ catch {
646
+ // noop
647
+ }
664
648
  }
665
649
  onBuildComplete(success, error) {
650
+ // DEBUG stack
651
+ this.traceBuildComplete(success, error);
652
+ const activeManager = global[GLOBAL_ACTIVE_MANAGER_KEY];
653
+ if (activeManager && activeManager !== this) {
654
+ activeManager.onBuildComplete(success, error);
655
+ return;
656
+ }
666
657
  if (this.buildCompleteResolve) {
667
658
  this.buildCompleteResolve();
668
659
  this.buildCompleteResolve = null;
669
660
  }
670
661
  this.isBuilding = false;
662
+ const currentState = this.buildState;
663
+ const buildId = typeof error?.buildId === 'number' ? error.buildId : undefined;
671
664
  if (success) {
672
- this.lastBuildError = null;
673
- }
674
- else {
675
- this.lastBuildError = error || { message: 'Build failed', ts: Date.now() };
676
- }
677
- // Notifica os clientes que o build terminou
678
- if (success) {
679
- this.notifyClients('build-complete', { success: true });
680
- }
681
- else {
682
- this.notifyClients('build-error', this.lastBuildError);
665
+ // Não confia no bundler. Antes de ficar "verde", roda typecheck real.
666
+ const tc = this.typecheckFrontend();
667
+ if (!tc.ok) {
668
+ // Se existe um erro de bundler (esbuild/rollup) mais específico, preserva ele.
669
+ // O typecheck serve como "gate" para não ficar verde, mas não deve piorar a mensagem.
670
+ const existing = currentState.frontend;
671
+ const shouldKeepExistingBundlerError = existing &&
672
+ typeof existing?.message === 'string' &&
673
+ (existing.message.includes('Transform failed') || existing.message.includes('Expected') || existing.message.includes('esbuild'));
674
+ this.buildState = {
675
+ ...currentState,
676
+ frontend: shouldKeepExistingBundlerError ? existing : (tc.error ?? existing)
677
+ };
678
+ this.notifyStatusChange();
679
+ return;
680
+ }
681
+ // Proteção: não deixa um success antigo limpar um erro mais novo
682
+ if (buildId !== undefined && buildId < this.lastFrontendErrorBuildId) {
683
+ this.buildState = { ...currentState };
684
+ this.notifyStatusChange();
685
+ return;
686
+ }
687
+ this.buildState = { ...currentState, frontend: null, backend: null };
688
+ this.notifyStatusChange();
689
+ return;
683
690
  }
691
+ // error
692
+ const errData = error || { message: 'Build failed', ts: Date.now() };
693
+ if (buildId !== undefined)
694
+ this.lastFrontendErrorBuildId = Math.max(this.lastFrontendErrorBuildId, buildId);
695
+ this.buildState = { ...currentState, frontend: errData };
696
+ // Notifica baseado no estado combinado (Frontend + Backend)
697
+ this.notifyStatusChange();
684
698
  }
685
699
  stop() {
686
700
  this.isShuttingDown = true;
687
- // Limpa todos os debounce timers
688
701
  this.debounceTimers.forEach(timer => clearTimeout(timer));
689
702
  this.debounceTimers.clear();
690
- // Para todos os watchers
691
703
  this.watchers.forEach(watcher => watcher.close());
692
704
  this.watchers = [];
693
- // Limpa todos os clientes
694
705
  this.clients.forEach((client, ws) => {
695
706
  clearInterval(client.pingTimer);
696
- if (ws.readyState === ws_1.WebSocket.OPEN) {
707
+ if (ws.readyState === ws_1.WebSocket.OPEN)
697
708
  ws.close();
698
- }
699
709
  });
700
710
  this.clients.clear();
701
- // Fecha WebSocket server
702
711
  if (this.wss) {
703
712
  this.wss.close();
704
713
  this.wss = null;
705
714
  }
706
715
  }
716
+ typecheckFrontend() {
717
+ try {
718
+ const now = Date.now();
719
+ if (this.lastTypecheckResult && now - this.lastTypecheckAt < 750)
720
+ return this.lastTypecheckResult;
721
+ const projectDir = this.projectDir;
722
+ const configPath = typescript_1.default.findConfigFile(projectDir, typescript_1.default.sys.fileExists, 'tsconfig.json');
723
+ if (!configPath) {
724
+ this.lastTypecheckAt = now;
725
+ this.lastTypecheckResult = { ok: true };
726
+ return this.lastTypecheckResult;
727
+ }
728
+ const configFile = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
729
+ if (configFile.error) {
730
+ const msg = typescript_1.default.flattenDiagnosticMessageText(configFile.error.messageText, '\n');
731
+ const err = { message: `tsconfig read error: ${msg}`, type: 'TypeScriptConfigError', ts: Date.now() };
732
+ this.lastTypecheckAt = now;
733
+ this.lastTypecheckResult = { ok: false, error: err };
734
+ return this.lastTypecheckResult;
735
+ }
736
+ const parsed = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, path.dirname(configPath));
737
+ const options = { ...parsed.options, noEmit: true };
738
+ const rootNames = parsed.fileNames;
739
+ const webRoot = path.resolve(projectDir, 'src', 'web') + path.sep;
740
+ const filteredRoots = rootNames.filter(f => f.startsWith(webRoot));
741
+ const program = typescript_1.default.createProgram(filteredRoots.length ? filteredRoots : rootNames, options);
742
+ const diagnostics = typescript_1.default.getPreEmitDiagnostics(program);
743
+ if (!diagnostics.length) {
744
+ this.lastTypecheckAt = now;
745
+ this.lastTypecheckResult = { ok: true };
746
+ return this.lastTypecheckResult;
747
+ }
748
+ const first = diagnostics[0];
749
+ const message = typescript_1.default.flattenDiagnosticMessageText(first.messageText, '\n');
750
+ const code = typeof first.code === 'number' ? `TS${first.code}` : undefined;
751
+ const loc = first.file && typeof first.start === 'number'
752
+ ? (() => {
753
+ const pos = first.file.getLineAndCharacterOfPosition(first.start);
754
+ return { file: first.file.fileName, line: pos.line + 1, column: pos.character + 1 };
755
+ })()
756
+ : undefined;
757
+ const lines = [];
758
+ lines.push(`TypeScript check failed${code ? ` (${code})` : ''}: ${message}`);
759
+ if (loc?.file)
760
+ lines.push(`at ${loc.file}:${loc.line}:${loc.column}`);
761
+ // stack sintético só pra debug (sem poluir com stack interna do TS)
762
+ const syntheticStack = ['Error: ' + lines[0], ...(loc?.file ? [lines[1]] : [])].join('\n');
763
+ const err = {
764
+ message: lines[0],
765
+ type: 'TypeScriptError',
766
+ code,
767
+ loc,
768
+ // compat: alguns overlays procuram stack
769
+ stack: syntheticStack,
770
+ ts: Date.now()
771
+ };
772
+ this.lastTypecheckAt = now;
773
+ this.lastTypecheckResult = { ok: false, error: err };
774
+ return this.lastTypecheckResult;
775
+ }
776
+ catch (e) {
777
+ const err = { message: e?.message || 'TypeScript check failed', stack: e?.stack, type: 'TypeScriptError', ts: Date.now() };
778
+ this.lastTypecheckAt = Date.now();
779
+ this.lastTypecheckResult = { ok: false, error: err };
780
+ return this.lastTypecheckResult;
781
+ }
782
+ }
707
783
  }
708
784
  exports.HotReloadManager = HotReloadManager;