hightjs 0.3.5 → 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/src/builder.js CHANGED
@@ -18,6 +18,8 @@ const esbuild = require('esbuild');
18
18
  const path = require('path');
19
19
  const Console = require("./api/console").default
20
20
  const fs = require('fs');
21
+ const {readdir, stat} = require("node:fs/promises");
22
+ const {rm} = require("fs-extra");
21
23
  // Lista de módulos nativos do Node.js para marcar como externos (apenas os built-ins do Node)
22
24
  const nodeBuiltIns = [
23
25
  'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns',
@@ -189,6 +191,45 @@ const reactResolvePlugin = {
189
191
  }
190
192
  };
191
193
 
194
+ /**
195
+ * Plugin para adicionar suporte a HMR (Hot Module Replacement)
196
+ */
197
+ const hmrPlugin = {
198
+ name: 'hmr-plugin',
199
+ setup(build) {
200
+ // Adiciona runtime de HMR para arquivos TSX/JSX
201
+ build.onLoad({ filter: /\.(tsx|jsx)$/ }, async (args) => {
202
+ // Ignora arquivos de node_modules
203
+ if (args.path.includes('node_modules')) {
204
+ return null;
205
+ }
206
+
207
+ const fs = require('fs');
208
+ const contents = await fs.promises.readFile(args.path, 'utf8');
209
+
210
+ // Adiciona código de HMR apenas em componentes de rota
211
+ if (args.path.includes('/routes/') || args.path.includes('\\routes\\')) {
212
+ const hmrCode = `
213
+ // HMR Runtime
214
+ if (typeof window !== 'undefined' && window.__HWEB_HMR__) {
215
+ const moduleId = ${JSON.stringify(args.path)};
216
+ if (!window.__HWEB_HMR_MODULES__) {
217
+ window.__HWEB_HMR_MODULES__ = new Map();
218
+ }
219
+ window.__HWEB_HMR_MODULES__.set(moduleId, module.exports);
220
+ }
221
+ `;
222
+ return {
223
+ contents: contents + '\n' + hmrCode,
224
+ loader: 'tsx'
225
+ };
226
+ }
227
+
228
+ return null;
229
+ });
230
+ }
231
+ };
232
+
192
233
  /**
193
234
  * Builds with code splitting into multiple chunks based on module types.
194
235
  * @param {string} entryPoint - The path to the entry file.
@@ -197,8 +238,8 @@ const reactResolvePlugin = {
197
238
  * @returns {Promise<void>}
198
239
  */
199
240
  async function buildWithChunks(entryPoint, outdir, isProduction = false) {
200
- // limpar diretorio
201
- fs.rmSync(outdir, { recursive: true, force: true });
241
+ // limpar diretorio, menos a pasta temp
242
+ await cleanDirectoryExcept(outdir, 'temp');
202
243
 
203
244
  try {
204
245
  await esbuild.build({
@@ -242,7 +283,7 @@ async function buildWithChunks(entryPoint, outdir, isProduction = false) {
242
283
  */
243
284
  async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
244
285
  // limpar diretorio
245
- fs.rmSync(outdir, { recursive: true, force: true });
286
+ await cleanDirectoryExcept(outdir, 'temp');
246
287
  try {
247
288
  // Plugin para notificar quando o build termina
248
289
  const buildCompletePlugin = {
@@ -272,7 +313,7 @@ async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
272
313
  outdir: outdir,
273
314
  loader: { '.js': 'jsx', '.ts': 'tsx' },
274
315
  external: nodeBuiltIns,
275
- plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, buildCompletePlugin],
316
+ plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin],
276
317
  format: 'esm',
277
318
  jsx: 'automatic',
278
319
  define: {
@@ -306,7 +347,7 @@ async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
306
347
  async function build(entryPoint, outfile, isProduction = false) {
307
348
  // limpar diretorio do outfile
308
349
  const outdir = path.dirname(outfile);
309
- fs.rmSync(outdir, { recursive: true, force: true });
350
+ await cleanDirectoryExcept(outdir, 'temp');
310
351
 
311
352
  try {
312
353
  await esbuild.build({
@@ -407,5 +448,25 @@ async function watch(entryPoint, outfile, hotReloadManager = null) {
407
448
  throw error;
408
449
  }
409
450
  }
451
+ /**
452
+ * Remove todo o conteúdo de um diretório,
453
+ * exceto a pasta (ou pastas) especificada(s) em excludeFolder.
454
+ *
455
+ * @param {string} dirPath - Caminho do diretório a limpar
456
+ * @param {string|string[]} excludeFolder - Nome ou nomes das pastas a manter
457
+ */
458
+ async function cleanDirectoryExcept(dirPath, excludeFolder) {
459
+ const excludes = Array.isArray(excludeFolder) ? excludeFolder : [excludeFolder];
460
+
461
+ const items = await readdir(dirPath);
462
+
463
+ for (const item of items) {
464
+ if (excludes.includes(item)) continue; // pula as pastas excluídas
410
465
 
466
+ const itemPath = path.join(dirPath, item);
467
+ const info = await stat(itemPath);
468
+
469
+ await rm(itemPath, { recursive: info.isDirectory(), force: true });
470
+ }
471
+ }
411
472
  module.exports = { build, watch, buildWithChunks, watchWithChunks };
@@ -30,7 +30,7 @@ interface AppProps {
30
30
 
31
31
  function App({ componentMap, routes, initialComponentPath, initialParams, layoutComponent }: AppProps) {
32
32
  // Estado que guarda o componente a ser renderizado atualmente
33
-
33
+ const [hmrTimestamp, setHmrTimestamp] = useState(Date.now());
34
34
  const [CurrentPageComponent, setCurrentPageComponent] = useState(() => {
35
35
  // Se for a rota especial __404__, não busca no componentMap
36
36
  if (initialComponentPath === '__404__') {
@@ -40,6 +40,72 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
40
40
  });
41
41
  const [params, setParams] = useState(initialParams);
42
42
 
43
+ // HMR: Escuta eventos de hot reload
44
+ useEffect(() => {
45
+ // Ativa o sistema de HMR
46
+ (window as any).__HWEB_HMR__ = true;
47
+
48
+ const handleHMRUpdate = async (event: CustomEvent) => {
49
+ const { file, timestamp } = event.detail;
50
+ const fileName = file ? file.split('/').pop()?.split('\\').pop() : 'unknown';
51
+ console.log('🔥 HMR: Hot reloading...', fileName);
52
+
53
+ try {
54
+ // Aguarda um pouco para o esbuild terminar de recompilar
55
+ await new Promise(resolve => setTimeout(resolve, 300));
56
+
57
+ // Re-importa o módulo principal com cache busting
58
+ const mainScript = document.querySelector('script[src*="main.js"]') as HTMLScriptElement;
59
+ if (mainScript) {
60
+ const mainSrc = mainScript.src.split('?')[0];
61
+ const cacheBustedSrc = `${mainSrc}?t=${timestamp}`;
62
+
63
+ // Cria novo script
64
+ const newScript = document.createElement('script');
65
+ newScript.type = 'module';
66
+ newScript.src = cacheBustedSrc;
67
+
68
+ // Quando o novo script carregar, força re-render
69
+ newScript.onload = () => {
70
+ console.log('✅ HMR: Modules reloaded');
71
+
72
+ // Força re-render do componente
73
+ setHmrTimestamp(timestamp);
74
+
75
+ // Marca sucesso
76
+ (window as any).__HMR_SUCCESS__ = true;
77
+ setTimeout(() => {
78
+ (window as any).__HMR_SUCCESS__ = false;
79
+ }, 3000);
80
+ };
81
+
82
+ newScript.onerror = () => {
83
+ console.error('❌ HMR: Failed to reload modules');
84
+ (window as any).__HMR_SUCCESS__ = false;
85
+ };
86
+
87
+ // Remove o script antigo e adiciona o novo
88
+ // (não remove para não quebrar o app)
89
+ document.head.appendChild(newScript);
90
+ } else {
91
+ // Se não encontrou o script, apenas força re-render
92
+ console.log('⚡ HMR: Forcing re-render');
93
+ setHmrTimestamp(timestamp);
94
+ (window as any).__HMR_SUCCESS__ = true;
95
+ }
96
+ } catch (error) {
97
+ console.error('❌ HMR Error:', error);
98
+ (window as any).__HMR_SUCCESS__ = false;
99
+ }
100
+ };
101
+
102
+ window.addEventListener('hmr:component-update' as any, handleHMRUpdate);
103
+
104
+ return () => {
105
+ window.removeEventListener('hmr:component-update' as any, handleHMRUpdate);
106
+ };
107
+ }, []);
108
+
43
109
  const findRouteForPath = useCallback((path: string) => {
44
110
  for (const route of routes) {
45
111
  const regexPattern = route.pattern
@@ -123,7 +189,8 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
123
189
  }
124
190
 
125
191
  // Renderiza o componente atual (sem Context, usa o router diretamente)
126
- const PageContent = <CurrentPageComponent params={params} />;
192
+ // Usa key com timestamp para forçar re-mount durante HMR
193
+ const PageContent = <CurrentPageComponent key={`page-${hmrTimestamp}`} params={params} />;
127
194
 
128
195
  // SEMPRE usa o layout - se não existir, cria um wrapper padrão
129
196
  const content = layoutComponent
package/src/hotReload.ts CHANGED
@@ -195,7 +195,7 @@ export class HotReloadManager {
195
195
  filePath.endsWith('.tsx') ||
196
196
  filePath.endsWith('.jsx');
197
197
 
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
 
201
201
  // Limpa o cache do arquivo alterado
@@ -357,10 +357,11 @@ export class HotReloadManager {
357
357
 
358
358
  switch(message.type) {
359
359
  case 'frontend-reload':
360
- window.location.reload();
360
+ handleFrontendReload(message.data);
361
361
  break;
362
362
  case 'backend-api-reload':
363
- // Recarrega apenas se necessário
363
+ // Backend sempre precisa recarregar
364
+ console.log('🔄 Backend changed, reloading...');
364
365
  window.location.reload();
365
366
  break;
366
367
  case 'server-restart':
@@ -372,12 +373,91 @@ export class HotReloadManager {
372
373
  case 'frontend-error':
373
374
  console.error('❌ Frontend error:', message.data);
374
375
  break;
376
+ case 'hmr-update':
377
+ handleHMRUpdate(message.data);
378
+ break;
375
379
  }
376
380
  } catch (e) {
377
381
  console.error('Erro ao processar mensagem do hot-reload:', e);
378
382
  }
379
383
  };
380
384
 
385
+ function handleFrontendReload(data) {
386
+ if (!data || !data.file) {
387
+ window.location.reload();
388
+ return;
389
+ }
390
+
391
+ const file = data.file.toLowerCase();
392
+
393
+ // Mudanças que exigem reload completo
394
+ const needsFullReload =
395
+ file.includes('layout.tsx') ||
396
+ file.includes('layout.jsx') ||
397
+ file.includes('not-found.tsx') ||
398
+ file.includes('not-found.jsx') ||
399
+ file.endsWith('.css');
400
+
401
+ if (needsFullReload) {
402
+ console.log('⚡ Layout/CSS changed, full reload...');
403
+ window.location.reload();
404
+ return;
405
+ }
406
+
407
+ // Mudanças em rotas: tenta HMR
408
+ if (file.includes('/routes/') || file.includes('\\\\routes\\\\')) {
409
+ console.log('⚡ Route component changed, hot reloading...');
410
+
411
+ // Dispara evento para forçar re-render
412
+ const event = new CustomEvent('hmr:component-update', {
413
+ detail: { file: data.file, timestamp: Date.now() }
414
+ });
415
+ window.dispatchEvent(event);
416
+
417
+ // Aguarda 500ms para ver se o HMR foi bem-sucedido
418
+ setTimeout(() => {
419
+ const hmrSuccess = window.__HMR_SUCCESS__;
420
+ if (!hmrSuccess) {
421
+ console.log('⚠️ HMR failed, falling back to full reload');
422
+ window.location.reload();
423
+ } else {
424
+ console.log('✅ HMR successful!');
425
+ }
426
+ }, 500);
427
+ } else {
428
+ // Outros arquivos: reload completo por segurança
429
+ window.location.reload();
430
+ }
431
+ }
432
+
433
+ function handleHMRUpdate(data) {
434
+ console.log('🔥 HMR Update:', data);
435
+
436
+ // Dispara evento customizado para o React capturar
437
+ const event = new CustomEvent('hmr:update', {
438
+ detail: data
439
+ });
440
+ window.dispatchEvent(event);
441
+ }
442
+
443
+ function attemptHMR(changedFile) {
444
+ // Tenta fazer Hot Module Replacement
445
+ // Dispara evento para o React App capturar
446
+ const event = new CustomEvent('hmr:component-update', {
447
+ detail: { file: changedFile, timestamp: Date.now() }
448
+ });
449
+ window.dispatchEvent(event);
450
+
451
+ // Fallback: se após 2s não houve sucesso, reload
452
+ setTimeout(() => {
453
+ const hmrSuccess = window.__HMR_SUCCESS__;
454
+ if (!hmrSuccess) {
455
+ console.log('⚠️ HMR failed, falling back to full reload');
456
+ window.location.reload();
457
+ }
458
+ }, 2000);
459
+ }
460
+
381
461
  ws.onclose = function(event) {
382
462
  isConnected = false;
383
463
 
package/src/index.ts CHANGED
@@ -102,60 +102,61 @@ function isLargeProject(projectDir: string): boolean {
102
102
 
103
103
  // Função para gerar o arquivo de entrada para o esbuild
104
104
  function createEntryFile(projectDir: string, routes: (RouteConfig & { componentPath: string })[]): string {
105
- const tempDir = path.join(projectDir, '.hweb');
106
- fs.mkdirSync(tempDir, { recursive: true });
107
-
108
- const entryFilePath = path.join(tempDir, 'entry.client.js');
109
-
110
- // Verifica se layout
111
- const layout = getLayout();
112
-
113
- // Verifica se há notFound personalizado
114
- const notFound = getNotFound();
115
-
116
- // Gera imports dinâmicos para cada componente
117
- const imports = routes
118
- .map((route, index) => {
119
- const relativePath = path.relative(tempDir, route.componentPath).replace(/\\/g, '/');
120
- return `import route${index} from '${relativePath}';`;
121
- })
122
- .join('\n');
123
-
124
- // Import do layout se existir
125
- const layoutImport = layout
126
- ? `import LayoutComponent from '${path.relative(tempDir, layout.componentPath).replace(/\\/g, '/')}';`
127
- : '';
128
-
129
- // Import do notFound se existir
130
- const notFoundImport = notFound
131
- ? `import NotFoundComponent from '${path.relative(tempDir, notFound.componentPath).replace(/\\/g, '/')}';`
132
- : '';
133
-
134
- // Registra os componentes no window para o cliente acessar
135
- const componentRegistration = routes
136
- .map((route, index) => ` '${route.componentPath}': route${index}.component || route${index}.default?.component,`)
137
- .join('\n');
138
-
139
- // Registra o layout se existir
140
- const layoutRegistration = layout
141
- ? `window.__HWEB_LAYOUT__ = LayoutComponent.default || LayoutComponent;`
142
- : `window.__HWEB_LAYOUT__ = null;`;
143
-
144
- // Registra o notFound se existir
145
- const notFoundRegistration = notFound
146
- ? `window.__HWEB_NOT_FOUND__ = NotFoundComponent.default || NotFoundComponent;`
147
- : `window.__HWEB_NOT_FOUND__ = null;`;
148
-
149
- // Caminho correto para o entry.client.tsx
150
- const sdkDir = path.dirname(__dirname); // Vai para a pasta pai de src (onde está o hweb-sdk)
151
- const entryClientPath = path.join(sdkDir, 'src', 'client', 'entry.client.tsx');
152
- const relativeEntryPath = path.relative(tempDir, entryClientPath).replace(/\\/g, '/');
153
-
154
- // Import do DefaultNotFound do SDK
155
- const defaultNotFoundPath = path.join(sdkDir, 'src', 'client', 'DefaultNotFound.tsx');
156
- const relativeDefaultNotFoundPath = path.relative(tempDir, defaultNotFoundPath).replace(/\\/g, '/');
157
-
158
- const entryContent = `// Arquivo gerado automaticamente pelo hweb
105
+ try {
106
+ const tempDir = path.join(projectDir, '.hight', 'temp');
107
+
108
+ fs.mkdirSync(tempDir, { recursive: true });
109
+
110
+ const entryFilePath = path.join(tempDir, 'entry.client.js');
111
+ // Verifica se há layout
112
+ const layout = getLayout();
113
+
114
+ // Verifica se há notFound personalizado
115
+ const notFound = getNotFound();
116
+
117
+ // Gera imports dinâmicos para cada componente
118
+ const imports = routes
119
+ .map((route, index) => {
120
+ const relativePath = path.relative(tempDir, route.componentPath).replace(/\\/g, '/');
121
+ return `import route${index} from '${relativePath}';`;
122
+ })
123
+ .join('\n');
124
+
125
+ // Import do layout se existir
126
+ const layoutImport = layout
127
+ ? `import LayoutComponent from '${path.relative(tempDir, layout.componentPath).replace(/\\/g, '/')}';`
128
+ : '';
129
+
130
+ // Import do notFound se existir
131
+ const notFoundImport = notFound
132
+ ? `import NotFoundComponent from '${path.relative(tempDir, notFound.componentPath).replace(/\\/g, '/')}';`
133
+ : '';
134
+
135
+ // Registra os componentes no window para o cliente acessar
136
+ const componentRegistration = routes
137
+ .map((route, index) => ` '${route.componentPath}': route${index}.component || route${index}.default?.component,`)
138
+ .join('\n');
139
+
140
+ // Registra o layout se existir
141
+ const layoutRegistration = layout
142
+ ? `window.__HWEB_LAYOUT__ = LayoutComponent.default || LayoutComponent;`
143
+ : `window.__HWEB_LAYOUT__ = null;`;
144
+
145
+ // Registra o notFound se existir
146
+ const notFoundRegistration = notFound
147
+ ? `window.__HWEB_NOT_FOUND__ = NotFoundComponent.default || NotFoundComponent;`
148
+ : `window.__HWEB_NOT_FOUND__ = null;`;
149
+
150
+ // Caminho correto para o entry.client.tsx
151
+ const sdkDir = path.dirname(__dirname); // Vai para a pasta pai de src (onde está o hweb-sdk)
152
+ const entryClientPath = path.join(sdkDir, 'src', 'client', 'entry.client.tsx');
153
+ const relativeEntryPath = path.relative(tempDir, entryClientPath).replace(/\\/g, '/');
154
+
155
+ // Import do DefaultNotFound do SDK
156
+ const defaultNotFoundPath = path.join(sdkDir, 'src', 'client', 'DefaultNotFound.tsx');
157
+ const relativeDefaultNotFoundPath = path.relative(tempDir, defaultNotFoundPath).replace(/\\/g, '/');
158
+
159
+ const entryContent = `// Arquivo gerado automaticamente pelo hweb
159
160
  ${imports}
160
161
  ${layoutImport}
161
162
  ${notFoundImport}
@@ -179,9 +180,17 @@ window.__HWEB_DEFAULT_NOT_FOUND__ = DefaultNotFound;
179
180
  import '${relativeEntryPath}';
180
181
  `;
181
182
 
182
- fs.writeFileSync(entryFilePath, entryContent);
183
+ try {
184
+ fs.writeFileSync(entryFilePath, entryContent);
185
+ } catch (e) {
186
+ console.error("sdfijnsdfnijfsdijnfsdnijsdfnijfsdnijfsdnijfsdn", e)
187
+ }
183
188
 
184
- return entryFilePath;
189
+ return entryFilePath;
190
+ }catch (e){
191
+ Console.error("Error creating entry file:", e);
192
+ throw e;
193
+ }
185
194
  }
186
195
 
187
196
  export default function hweb(options: HightJSOptions) {
@@ -190,7 +199,7 @@ export default function hweb(options: HightJSOptions) {
190
199
  process.hight = options;
191
200
  const userWebDir = path.join(dir, 'src', 'web');
192
201
  const userWebRoutesDir = path.join(userWebDir, 'routes');
193
- const userBackendRoutesDir = path.join(userWebDir, 'backend', 'routes');
202
+ const userBackendRoutesDir = path.join(dir, 'src', 'backend', 'routes');
194
203
 
195
204
  /**
196
205
  * Executa middlewares sequencialmente e depois o handler final
@@ -1,3 +0,0 @@
1
- import {authRoutes} from "../../../auth";
2
-
3
- export default authRoutes
File without changes