hightjs 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ Caso tenha alguma dúvida, entre em contato por uma das redes abaixo:
34
34
 
35
35
  ## ✨ Principais Recursos
36
36
 
37
- - **Roteamento automático** de páginas [`src/web/routes`] e APIs [`src/web/backend/routes`]
37
+ - **Roteamento automático** de páginas [`src/web/routes`] e APIs [`src/backend/routes`]
38
38
  - **React 19** com client-side hydration
39
39
  - **TypeScript** first (totalmente tipado)
40
40
  - **WebSockets** nativo nas rotas backend
@@ -185,7 +185,7 @@ class Console {
185
185
  }
186
186
  }
187
187
  static logWithout(level, colors, ...args) {
188
- const color = colors || level === Levels.ERROR ? Colors.BgRed : level === Levels.WARN ? Colors.BgYellow : level === Levels.INFO ? Colors.BgMagenta : level === Levels.SUCCESS ? Colors.BgGreen : Colors.BgCyan;
188
+ const color = colors ? colors : level === Levels.ERROR ? Colors.BgRed : level === Levels.WARN ? Colors.BgYellow : level === Levels.INFO ? Colors.BgMagenta : level === Levels.SUCCESS ? Colors.BgGreen : Colors.BgCyan;
189
189
  let output = "";
190
190
  for (const arg of args) {
191
191
  let msg = (arg instanceof Error) ? arg.stack : (typeof arg === 'string') ? arg : JSON.stringify(arg, null, 2);
@@ -113,11 +113,11 @@ program
113
113
  const app = teste.default({ dev: false, port: 3000, hostname: '0.0.0.0', framework: 'native' });
114
114
  await app.prepare();
115
115
  console.log('✅ Build complete\n');
116
- // 3. Copia a pasta hweb-dist para exported
117
- const distDir = path.join(projectDir, 'hweb-dist');
116
+ // 3. Copia a pasta .hight para exported
117
+ const distDir = path.join(projectDir, '.hight');
118
118
  if (fs.existsSync(distDir)) {
119
119
  console.log('📦 Copying JavaScript files...');
120
- const exportDistDir = path.join(exportDir, 'hweb-dist');
120
+ const exportDistDir = path.join(exportDir, '.hight');
121
121
  fs.mkdirSync(exportDistDir, { recursive: true });
122
122
  const files = fs.readdirSync(distDir);
123
123
  files.forEach(file => {
package/dist/builder.js CHANGED
@@ -19,6 +19,8 @@ const esbuild = require('esbuild');
19
19
  const path = require('path');
20
20
  const Console = require("./api/console").default;
21
21
  const fs = require('fs');
22
+ const { readdir, stat } = require("node:fs/promises");
23
+ const { rm } = require("fs-extra");
22
24
  // Lista de módulos nativos do Node.js para marcar como externos (apenas os built-ins do Node)
23
25
  const nodeBuiltIns = [
24
26
  'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns',
@@ -175,6 +177,41 @@ const reactResolvePlugin = {
175
177
  });
176
178
  }
177
179
  };
180
+ /**
181
+ * Plugin para adicionar suporte a HMR (Hot Module Replacement)
182
+ */
183
+ const hmrPlugin = {
184
+ name: 'hmr-plugin',
185
+ setup(build) {
186
+ // Adiciona runtime de HMR para arquivos TSX/JSX
187
+ build.onLoad({ filter: /\.(tsx|jsx)$/ }, async (args) => {
188
+ // Ignora arquivos de node_modules
189
+ if (args.path.includes('node_modules')) {
190
+ return null;
191
+ }
192
+ const fs = require('fs');
193
+ const contents = await fs.promises.readFile(args.path, 'utf8');
194
+ // Adiciona código de HMR apenas em componentes de rota
195
+ if (args.path.includes('/routes/') || args.path.includes('\\routes\\')) {
196
+ const hmrCode = `
197
+ // HMR Runtime
198
+ if (typeof window !== 'undefined' && window.__HWEB_HMR__) {
199
+ const moduleId = ${JSON.stringify(args.path)};
200
+ if (!window.__HWEB_HMR_MODULES__) {
201
+ window.__HWEB_HMR_MODULES__ = new Map();
202
+ }
203
+ window.__HWEB_HMR_MODULES__.set(moduleId, module.exports);
204
+ }
205
+ `;
206
+ return {
207
+ contents: contents + '\n' + hmrCode,
208
+ loader: 'tsx'
209
+ };
210
+ }
211
+ return null;
212
+ });
213
+ }
214
+ };
178
215
  /**
179
216
  * Builds with code splitting into multiple chunks based on module types.
180
217
  * @param {string} entryPoint - The path to the entry file.
@@ -183,8 +220,8 @@ const reactResolvePlugin = {
183
220
  * @returns {Promise<void>}
184
221
  */
185
222
  async function buildWithChunks(entryPoint, outdir, isProduction = false) {
186
- // limpar diretorio
187
- fs.rmSync(outdir, { recursive: true, force: true });
223
+ // limpar diretorio, menos a pasta temp
224
+ await cleanDirectoryExcept(outdir, 'temp');
188
225
  try {
189
226
  await esbuild.build({
190
227
  entryPoints: [entryPoint],
@@ -226,7 +263,7 @@ async function buildWithChunks(entryPoint, outdir, isProduction = false) {
226
263
  */
227
264
  async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
228
265
  // limpar diretorio
229
- fs.rmSync(outdir, { recursive: true, force: true });
266
+ await cleanDirectoryExcept(outdir, 'temp');
230
267
  try {
231
268
  // Plugin para notificar quando o build termina
232
269
  const buildCompletePlugin = {
@@ -256,7 +293,7 @@ async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
256
293
  outdir: outdir,
257
294
  loader: { '.js': 'jsx', '.ts': 'tsx' },
258
295
  external: nodeBuiltIns,
259
- plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, buildCompletePlugin],
296
+ plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin],
260
297
  format: 'esm',
261
298
  jsx: 'automatic',
262
299
  define: {
@@ -289,7 +326,7 @@ async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
289
326
  async function build(entryPoint, outfile, isProduction = false) {
290
327
  // limpar diretorio do outfile
291
328
  const outdir = path.dirname(outfile);
292
- fs.rmSync(outdir, { recursive: true, force: true });
329
+ await cleanDirectoryExcept(outdir, 'temp');
293
330
  try {
294
331
  await esbuild.build({
295
332
  entryPoints: [entryPoint],
@@ -388,4 +425,22 @@ async function watch(entryPoint, outfile, hotReloadManager = null) {
388
425
  throw error;
389
426
  }
390
427
  }
428
+ /**
429
+ * Remove todo o conteúdo de um diretório,
430
+ * exceto a pasta (ou pastas) especificada(s) em excludeFolder.
431
+ *
432
+ * @param {string} dirPath - Caminho do diretório a limpar
433
+ * @param {string|string[]} excludeFolder - Nome ou nomes das pastas a manter
434
+ */
435
+ async function cleanDirectoryExcept(dirPath, excludeFolder) {
436
+ const excludes = Array.isArray(excludeFolder) ? excludeFolder : [excludeFolder];
437
+ const items = await readdir(dirPath);
438
+ for (const item of items) {
439
+ if (excludes.includes(item))
440
+ continue; // pula as pastas excluídas
441
+ const itemPath = path.join(dirPath, item);
442
+ const info = await stat(itemPath);
443
+ await rm(itemPath, { recursive: info.isDirectory(), force: true });
444
+ }
445
+ }
391
446
  module.exports = { build, watch, buildWithChunks, watchWithChunks };
@@ -55,6 +55,7 @@ const client_1 = require("react-dom/client");
55
55
  const clientRouter_1 = require("./clientRouter");
56
56
  function App({ componentMap, routes, initialComponentPath, initialParams, layoutComponent }) {
57
57
  // Estado que guarda o componente a ser renderizado atualmente
58
+ const [hmrTimestamp, setHmrTimestamp] = (0, react_1.useState)(Date.now());
58
59
  const [CurrentPageComponent, setCurrentPageComponent] = (0, react_1.useState)(() => {
59
60
  // Se for a rota especial __404__, não busca no componentMap
60
61
  if (initialComponentPath === '__404__') {
@@ -63,6 +64,62 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
63
64
  return componentMap[initialComponentPath];
64
65
  });
65
66
  const [params, setParams] = (0, react_1.useState)(initialParams);
67
+ // HMR: Escuta eventos de hot reload
68
+ (0, react_1.useEffect)(() => {
69
+ // Ativa o sistema de HMR
70
+ window.__HWEB_HMR__ = true;
71
+ const handleHMRUpdate = async (event) => {
72
+ const { file, timestamp } = event.detail;
73
+ const fileName = file ? file.split('/').pop()?.split('\\').pop() : 'unknown';
74
+ console.log('🔥 HMR: Hot reloading...', fileName);
75
+ try {
76
+ // Aguarda um pouco para o esbuild terminar de recompilar
77
+ await new Promise(resolve => setTimeout(resolve, 300));
78
+ // Re-importa o módulo principal com cache busting
79
+ const mainScript = document.querySelector('script[src*="main.js"]');
80
+ if (mainScript) {
81
+ const mainSrc = mainScript.src.split('?')[0];
82
+ const cacheBustedSrc = `${mainSrc}?t=${timestamp}`;
83
+ // Cria novo script
84
+ const newScript = document.createElement('script');
85
+ newScript.type = 'module';
86
+ newScript.src = cacheBustedSrc;
87
+ // Quando o novo script carregar, força re-render
88
+ newScript.onload = () => {
89
+ console.log('✅ HMR: Modules reloaded');
90
+ // Força re-render do componente
91
+ setHmrTimestamp(timestamp);
92
+ // Marca sucesso
93
+ window.__HMR_SUCCESS__ = true;
94
+ setTimeout(() => {
95
+ window.__HMR_SUCCESS__ = false;
96
+ }, 3000);
97
+ };
98
+ newScript.onerror = () => {
99
+ console.error('❌ HMR: Failed to reload modules');
100
+ window.__HMR_SUCCESS__ = false;
101
+ };
102
+ // Remove o script antigo e adiciona o novo
103
+ // (não remove para não quebrar o app)
104
+ document.head.appendChild(newScript);
105
+ }
106
+ else {
107
+ // Se não encontrou o script, apenas força re-render
108
+ console.log('⚡ HMR: Forcing re-render');
109
+ setHmrTimestamp(timestamp);
110
+ window.__HMR_SUCCESS__ = true;
111
+ }
112
+ }
113
+ catch (error) {
114
+ console.error('❌ HMR Error:', error);
115
+ window.__HMR_SUCCESS__ = false;
116
+ }
117
+ };
118
+ window.addEventListener('hmr:component-update', handleHMRUpdate);
119
+ return () => {
120
+ window.removeEventListener('hmr:component-update', handleHMRUpdate);
121
+ };
122
+ }, []);
66
123
  const findRouteForPath = (0, react_1.useCallback)((path) => {
67
124
  for (const route of routes) {
68
125
  const regexPattern = route.pattern
@@ -138,7 +195,8 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
138
195
  }
139
196
  }
140
197
  // Renderiza o componente atual (sem Context, usa o router diretamente)
141
- const PageContent = (0, jsx_runtime_1.jsx)(CurrentPageComponent, { params: params });
198
+ // Usa key com timestamp para forçar re-mount durante HMR
199
+ const PageContent = (0, jsx_runtime_1.jsx)(CurrentPageComponent, { params: params }, `page-${hmrTimestamp}`);
142
200
  // SEMPRE usa o layout - se não existir, cria um wrapper padrão
143
201
  const content = layoutComponent
144
202
  ? react_1.default.createElement(layoutComponent, { children: PageContent })
@@ -157,10 +215,49 @@ const DEV_INDICATOR_CORNERS = [
157
215
  function DevIndicator() {
158
216
  const [corner, setCorner] = (0, react_1.useState)(3); // Canto atual (0-3)
159
217
  const [isDragging, setIsDragging] = (0, react_1.useState)(false); // Estado de arrastar
218
+ const [isBuilding, setIsBuilding] = (0, react_1.useState)(false); // Estado de build
160
219
  // Posição visual do indicador durante o arraste
161
220
  const [position, setPosition] = (0, react_1.useState)({ top: 0, left: 0 });
162
221
  const indicatorRef = (0, react_1.useRef)(null);
163
222
  const dragStartRef = (0, react_1.useRef)(null);
223
+ // Escuta eventos de hot reload para mostrar estado de build
224
+ (0, react_1.useEffect)(() => {
225
+ if (typeof window === 'undefined')
226
+ return;
227
+ const handleHotReloadMessage = (event) => {
228
+ try {
229
+ const message = JSON.parse(event.data);
230
+ // Quando detecta mudança em arquivo, ativa loading
231
+ if (message.type === 'frontend-reload' ||
232
+ message.type === 'backend-api-reload' ||
233
+ message.type === 'src-reload') {
234
+ setIsBuilding(true);
235
+ }
236
+ // Quando o build termina ou servidor fica pronto, desativa loading
237
+ if (message.type === 'server-ready' || message.type === 'build-complete') {
238
+ setIsBuilding(false);
239
+ }
240
+ }
241
+ catch (e) {
242
+ // Ignora mensagens que não são JSON
243
+ }
244
+ };
245
+ // Intercepta mensagens WebSocket
246
+ const originalWebSocket = window.WebSocket;
247
+ window.WebSocket = class extends originalWebSocket {
248
+ constructor(url, protocols) {
249
+ super(url, protocols);
250
+ this.addEventListener('message', (event) => {
251
+ if (url.toString().includes('hweb-hotreload')) {
252
+ handleHotReloadMessage(event);
253
+ }
254
+ });
255
+ }
256
+ };
257
+ return () => {
258
+ window.WebSocket = originalWebSocket;
259
+ };
260
+ }, []);
164
261
  // --- Estilos Dinâmicos ---
165
262
  const getIndicatorStyle = () => {
166
263
  const baseStyle = {
@@ -169,17 +266,22 @@ function DevIndicator() {
169
266
  width: DEV_INDICATOR_SIZE,
170
267
  height: DEV_INDICATOR_SIZE,
171
268
  borderRadius: '50%',
172
- background: 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo
269
+ background: isBuilding
270
+ ? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
271
+ : 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
173
272
  color: 'white',
174
273
  fontWeight: 'bold',
175
274
  fontSize: 28,
176
- boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
275
+ boxShadow: isBuilding
276
+ ? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
277
+ : '0 4px 15px rgba(0,0,0,0.2)',
177
278
  display: 'flex',
178
279
  alignItems: 'center',
179
280
  justifyContent: 'center',
180
281
  cursor: isDragging ? 'grabbing' : 'grab',
181
282
  userSelect: 'none',
182
- transition: isDragging ? 'none' : 'all 0.3s ease-out', // Animação suave ao soltar
283
+ transition: isDragging ? 'none' : 'all 0.3s ease-out',
284
+ animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
183
285
  };
184
286
  if (isDragging) {
185
287
  return {
@@ -259,13 +361,59 @@ function DevIndicator() {
259
361
  window.removeEventListener('mouseup', handleMouseUp);
260
362
  };
261
363
  }, [isDragging, handleMouseMove, handleMouseUp]);
262
- return ((0, jsx_runtime_1.jsx)("div", { ref: indicatorRef, style: getIndicatorStyle(), onMouseDown: handleMouseDown, title: "Modo Dev HightJS", children: "H" }));
364
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { children: `
365
+ @keyframes hweb-pulse {
366
+ 0%, 100% {
367
+ transform: scale(1);
368
+ opacity: 1;
369
+ }
370
+ 50% {
371
+ transform: scale(1.1);
372
+ opacity: 0.8;
373
+ }
374
+ }
375
+
376
+ @keyframes hweb-spin {
377
+ from {
378
+ transform: rotate(0deg);
379
+ }
380
+ to {
381
+ transform: rotate(360deg);
382
+ }
383
+ }
384
+ ` }), (0, jsx_runtime_1.jsx)("div", { ref: indicatorRef, style: getIndicatorStyle(), onMouseDown: handleMouseDown, title: isBuilding ? "Building..." : "Modo Dev HightJS", children: isBuilding ? ((0, jsx_runtime_1.jsx)("span", { style: { animation: 'hweb-spin 1s linear infinite' }, children: "\u27F3" })) : ('H') })] }));
263
385
  }
264
386
  // --- Inicialização do Cliente (CSR - Client-Side Rendering) ---
387
+ function deobfuscateData(obfuscated) {
388
+ try {
389
+ // Remove o hash fake
390
+ const parts = obfuscated.split('.');
391
+ const base64 = parts.length > 1 ? parts[1] : parts[0];
392
+ // Decodifica base64
393
+ const jsonStr = atob(base64);
394
+ // Parse JSON
395
+ return JSON.parse(jsonStr);
396
+ }
397
+ catch (error) {
398
+ console.error('[hweb] Failed to decode data:', error);
399
+ return null;
400
+ }
401
+ }
265
402
  function initializeClient() {
266
- const initialData = window.__HWEB_INITIAL_DATA__;
403
+ // os dados do atributo data-h
404
+ const dataElement = document.getElementById('__hight_data__');
405
+ if (!dataElement) {
406
+ console.error('[hweb] Initial data script not found.');
407
+ return;
408
+ }
409
+ const obfuscated = dataElement.getAttribute('data-h');
410
+ if (!obfuscated) {
411
+ console.error('[hweb] Data attribute not found.');
412
+ return;
413
+ }
414
+ const initialData = deobfuscateData(obfuscated);
267
415
  if (!initialData) {
268
- console.error('[hweb] Initial data not found on page.');
416
+ console.error('[hweb] Failed to parse initial data.');
269
417
  return;
270
418
  }
271
419
  // Cria o mapa de componentes dinamicamente a partir dos módulos carregados
package/dist/helpers.js CHANGED
@@ -119,6 +119,7 @@ async function loadHightConfig(projectDir, phase) {
119
119
  serverTimeout: 35000,
120
120
  individualRequestTimeout: 30000,
121
121
  maxUrlLength: 2048,
122
+ accessLogging: true,
122
123
  };
123
124
  try {
124
125
  // Tenta primeiro .ts, depois .js
@@ -250,6 +251,9 @@ async function initNativeServer(hwebApp, options, port, hostname) {
250
251
  // Extraímos a lógica principal para uma variável
251
252
  // para que possa ser usada tanto pelo servidor HTTP quanto HTTPS.
252
253
  const requestListener = async (req, res) => {
254
+ const requestStartTime = Date.now();
255
+ const method = req.method || 'GET';
256
+ const url = req.url || '/';
253
257
  // Configurações de segurança básicas
254
258
  res.setHeader('X-Content-Type-Options', 'nosniff');
255
259
  res.setHeader('X-Frame-Options', 'DENY');
@@ -264,10 +268,45 @@ async function initNativeServer(hwebApp, options, port, hostname) {
264
268
  req.setTimeout(hightConfig.individualRequestTimeout || 30000, () => {
265
269
  res.statusCode = 408; // Request Timeout
266
270
  res.end('Request timeout');
271
+ // Log de timeout
272
+ if (hightConfig.accessLogging) {
273
+ const duration = Date.now() - requestStartTime;
274
+ console_1.default.info(`${console_1.Colors.FgYellow}${method}${console_1.Colors.Reset} ${url} ${console_1.Colors.FgRed}408${console_1.Colors.Reset} ${console_1.Colors.FgGray}${duration}ms${console_1.Colors.Reset}`);
275
+ }
267
276
  });
277
+ // Intercepta o método end() para logar quando a resposta for enviada
278
+ const originalEnd = res.end.bind(res);
279
+ let hasEnded = false;
280
+ res.end = function (...args) {
281
+ if (!hasEnded && hightConfig.accessLogging) {
282
+ hasEnded = true;
283
+ const duration = Date.now() - requestStartTime;
284
+ const statusCode = res.statusCode || 200;
285
+ // Define cor baseada no status code
286
+ let statusColor = console_1.Colors.FgGreen; // 2xx
287
+ if (statusCode >= 500)
288
+ statusColor = console_1.Colors.FgRed; // 5xx
289
+ else if (statusCode >= 400)
290
+ statusColor = console_1.Colors.FgYellow; // 4xx
291
+ else if (statusCode >= 300)
292
+ statusColor = console_1.Colors.FgCyan; // 3xx
293
+ // Formata o método com cor
294
+ let methodColor = console_1.Colors.BgCyan;
295
+ if (method === 'POST')
296
+ methodColor = console_1.Colors.BgGreen;
297
+ else if (method === 'PUT')
298
+ methodColor = console_1.Colors.BgYellow;
299
+ else if (method === 'DELETE')
300
+ methodColor = console_1.Colors.BgRed;
301
+ else if (method === 'PATCH')
302
+ methodColor = console_1.Colors.BgMagenta;
303
+ console_1.default.logCustomLevel(method, true, methodColor, `${url} ${statusColor}${statusCode}${console_1.Colors.Reset} ${console_1.Colors.FgGray}${duration}ms${console_1.Colors.Reset}`);
304
+ }
305
+ // @ts-ignore
306
+ return originalEnd.apply(this, args);
307
+ };
268
308
  try {
269
309
  // Validação básica de URL (usa configuração personalizada)
270
- const url = req.url || '/';
271
310
  const maxUrlLength = hightConfig.maxUrlLength || 2048;
272
311
  if (url.length > maxUrlLength) {
273
312
  res.statusCode = 414; // URI Too Long
package/dist/hotReload.js CHANGED
@@ -195,7 +195,7 @@ class HotReloadManager {
195
195
  filePath.includes('not-found.tsx') ||
196
196
  filePath.endsWith('.tsx') ||
197
197
  filePath.endsWith('.jsx');
198
- const isBackendFile = filePath.includes(path.join('src', 'web', 'backend')) ||
198
+ const isBackendFile = filePath.includes(path.join('src', 'backend')) ||
199
199
  (filePath.includes(path.join('src', 'web')) && !isFrontendFile);
200
200
  // Limpa o cache do arquivo alterado
201
201
  (0, router_1.clearFileCache)(filePath);
@@ -312,19 +312,21 @@ class HotReloadManager {
312
312
  if (typeof window !== 'undefined') {
313
313
  let ws;
314
314
  let reconnectAttempts = 0;
315
- let maxReconnectInterval = 30000; // 30 segundos max
316
- let reconnectInterval = 1000; // Começa com 1 segundo
315
+ let maxReconnectInterval = 30000;
316
+ let reconnectInterval = 1000;
317
317
  let reconnectTimer;
318
318
  let isConnected = false;
319
319
 
320
320
  function connect() {
321
- // Evita múltiplas tentativas simultâneas
321
+ const url = window.location; // Objeto com info da URL atual
322
+ const protocol = url.protocol === "https:" ? "wss:" : "ws:"; // Usa wss se for https
323
+ const wsUrl = protocol + '//' + url.host + '/hweb-hotreload/';
322
324
  if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
323
325
  return;
324
326
  }
325
327
 
326
328
  try {
327
- ws = new WebSocket('ws://localhost:3000/hweb-hotreload/');
329
+ ws = new WebSocket(wsUrl);
328
330
 
329
331
  ws.onopen = function() {
330
332
  console.log('🔌 Hot-reload connected');
@@ -340,10 +342,11 @@ class HotReloadManager {
340
342
 
341
343
  switch(message.type) {
342
344
  case 'frontend-reload':
343
- window.location.reload();
345
+ handleFrontendReload(message.data);
344
346
  break;
345
347
  case 'backend-api-reload':
346
- // Recarrega apenas se necessário
348
+ // Backend sempre precisa recarregar
349
+ console.log('🔄 Backend changed, reloading...');
347
350
  window.location.reload();
348
351
  break;
349
352
  case 'server-restart':
@@ -355,12 +358,91 @@ class HotReloadManager {
355
358
  case 'frontend-error':
356
359
  console.error('❌ Frontend error:', message.data);
357
360
  break;
361
+ case 'hmr-update':
362
+ handleHMRUpdate(message.data);
363
+ break;
358
364
  }
359
365
  } catch (e) {
360
366
  console.error('Erro ao processar mensagem do hot-reload:', e);
361
367
  }
362
368
  };
363
369
 
370
+ function handleFrontendReload(data) {
371
+ if (!data || !data.file) {
372
+ window.location.reload();
373
+ return;
374
+ }
375
+
376
+ const file = data.file.toLowerCase();
377
+
378
+ // Mudanças que exigem reload completo
379
+ const needsFullReload =
380
+ file.includes('layout.tsx') ||
381
+ file.includes('layout.jsx') ||
382
+ file.includes('not-found.tsx') ||
383
+ file.includes('not-found.jsx') ||
384
+ file.endsWith('.css');
385
+
386
+ if (needsFullReload) {
387
+ console.log('⚡ Layout/CSS changed, full reload...');
388
+ window.location.reload();
389
+ return;
390
+ }
391
+
392
+ // Mudanças em rotas: tenta HMR
393
+ if (file.includes('/routes/') || file.includes('\\\\routes\\\\')) {
394
+ console.log('⚡ Route component changed, hot reloading...');
395
+
396
+ // Dispara evento para forçar re-render
397
+ const event = new CustomEvent('hmr:component-update', {
398
+ detail: { file: data.file, timestamp: Date.now() }
399
+ });
400
+ window.dispatchEvent(event);
401
+
402
+ // Aguarda 500ms para ver se o HMR foi bem-sucedido
403
+ setTimeout(() => {
404
+ const hmrSuccess = window.__HMR_SUCCESS__;
405
+ if (!hmrSuccess) {
406
+ console.log('⚠️ HMR failed, falling back to full reload');
407
+ window.location.reload();
408
+ } else {
409
+ console.log('✅ HMR successful!');
410
+ }
411
+ }, 500);
412
+ } else {
413
+ // Outros arquivos: reload completo por segurança
414
+ window.location.reload();
415
+ }
416
+ }
417
+
418
+ function handleHMRUpdate(data) {
419
+ console.log('🔥 HMR Update:', data);
420
+
421
+ // Dispara evento customizado para o React capturar
422
+ const event = new CustomEvent('hmr:update', {
423
+ detail: data
424
+ });
425
+ window.dispatchEvent(event);
426
+ }
427
+
428
+ function attemptHMR(changedFile) {
429
+ // Tenta fazer Hot Module Replacement
430
+ // Dispara evento para o React App capturar
431
+ const event = new CustomEvent('hmr:component-update', {
432
+ detail: { file: changedFile, timestamp: Date.now() }
433
+ });
434
+ window.dispatchEvent(event);
435
+
436
+ // Fallback: se após 2s não houve sucesso, reload
437
+ setTimeout(() => {
438
+ const hmrSuccess = window.__HMR_SUCCESS__;
439
+ if (!hmrSuccess) {
440
+ console.log('⚠️ HMR failed, falling back to full reload');
441
+ window.location.reload();
442
+ }
443
+ }, 2000);
444
+ }
445
+
364
446
  ws.onclose = function(event) {
365
447
  isConnected = false;
366
448
 
@@ -454,6 +536,13 @@ class HotReloadManager {
454
536
  this.buildCompleteResolve = null;
455
537
  }
456
538
  this.isBuilding = false;
539
+ // Notifica os clientes que o build terminou
540
+ if (success) {
541
+ this.notifyClients('build-complete', { success: true });
542
+ }
543
+ else {
544
+ this.notifyClients('build-error', { success: false });
545
+ }
457
546
  }
458
547
  }
459
548
  exports.HotReloadManager = HotReloadManager;