vatts 1.0.1 → 1.0.2-alpha.2
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/builder.js +44 -16
- package/dist/client/clientRouter.js +0 -1
- package/dist/client/entry.client.d.ts +6 -1
- package/dist/client/entry.client.js +34 -44
- package/dist/helpers.js +18 -18
- package/dist/hotReload.d.ts +11 -2
- package/dist/hotReload.js +369 -293
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -13
- package/dist/renderer.js +1 -1
- package/dist/router.d.ts +5 -56
- package/dist/router.js +287 -473
- package/dist/types.d.ts +2 -4
- package/package.json +1 -1
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
|
-
|
|
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,
|
|
86
|
-
maxPayload: 1024 * 1024
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/(^|[\/\\])\../,
|
|
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(
|
|
227
|
+
console_1.default.info(`File removed: ${path.basename(filePath)}`);
|
|
207
228
|
(0, router_1.clearFileCache)(filePath);
|
|
208
229
|
this.clearBackendCache(filePath);
|
|
209
|
-
|
|
210
|
-
|
|
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];
|
|
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
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
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
|
-
|
|
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;
|
|
437
|
-
const protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
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]
|
|
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
|
-
|
|
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
|
-
//
|
|
538
|
-
//
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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('
|
|
634
|
+
console_1.default.info('Hot reload custom listener registered');
|
|
661
635
|
}
|
|
662
|
-
removeHotReloadListener() {
|
|
663
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
665
|
+
// Não confia no bundler. Antes de ficar "verde", roda typecheck real.
|
|
666
|
+
const tc = this.typecheckFrontend();
|
|
667
|
+
if (!tc.ok) {
|
|
668
|
+
// Se já 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;
|