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 +1 -1
- package/dist/api/console.js +1 -1
- package/dist/bin/hightjs.js +3 -3
- package/dist/builder.js +60 -5
- package/dist/client/entry.client.js +155 -7
- package/dist/helpers.js +40 -1
- package/dist/hotReload.js +96 -7
- package/dist/index.js +66 -51
- package/dist/renderer.js +137 -18
- package/dist/types.d.ts +39 -0
- package/docs/checklist.md +1 -1
- package/docs/config.md +15 -0
- package/docs/middlewares.md +1 -1
- package/docs/rotas-backend.md +3 -3
- package/docs/websocket.md +1 -1
- package/example/hightjs.config.ts +6 -0
- package/example/package-lock.json +1 -1
- package/example/package.json +2 -2
- package/example/src/backend/routes/auth.ts +3 -0
- package/example/src/{web/backend → backend}/routes/version.ts +1 -1
- package/example/src/web/components/Home.tsx +140 -0
- package/example/src/web/components/LoginPage.tsx +149 -0
- package/example/src/web/layout.tsx +57 -3
- package/example/src/web/routes/index.tsx +1 -141
- package/example/src/web/routes/login.tsx +1 -146
- package/package.json +3 -1
- package/src/api/console.ts +3 -1
- package/src/bin/hightjs.js +3 -3
- package/src/builder.js +66 -5
- package/src/client/entry.client.tsx +192 -10
- package/src/helpers.ts +39 -1
- package/src/hotReload.ts +96 -7
- package/src/index.ts +79 -63
- package/src/renderer.tsx +142 -18
- package/src/router.ts +2 -0
- package/src/types.ts +56 -0
- package/example/src/web/backend/routes/auth.ts +0 -3
- /package/example/src/{auth.ts → backend/auth.ts} +0 -0
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', '
|
|
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
|
|
@@ -327,19 +327,21 @@ export class HotReloadManager {
|
|
|
327
327
|
if (typeof window !== 'undefined') {
|
|
328
328
|
let ws;
|
|
329
329
|
let reconnectAttempts = 0;
|
|
330
|
-
let maxReconnectInterval = 30000;
|
|
331
|
-
let reconnectInterval = 1000;
|
|
330
|
+
let maxReconnectInterval = 30000;
|
|
331
|
+
let reconnectInterval = 1000;
|
|
332
332
|
let reconnectTimer;
|
|
333
333
|
let isConnected = false;
|
|
334
334
|
|
|
335
335
|
function connect() {
|
|
336
|
-
//
|
|
336
|
+
const url = window.location; // Objeto com info da URL atual
|
|
337
|
+
const protocol = url.protocol === "https:" ? "wss:" : "ws:"; // Usa wss se for https
|
|
338
|
+
const wsUrl = protocol + '//' + url.host + '/hweb-hotreload/';
|
|
337
339
|
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
|
338
340
|
return;
|
|
339
341
|
}
|
|
340
342
|
|
|
341
343
|
try {
|
|
342
|
-
ws = new WebSocket(
|
|
344
|
+
ws = new WebSocket(wsUrl);
|
|
343
345
|
|
|
344
346
|
ws.onopen = function() {
|
|
345
347
|
console.log('🔌 Hot-reload connected');
|
|
@@ -355,10 +357,11 @@ export class HotReloadManager {
|
|
|
355
357
|
|
|
356
358
|
switch(message.type) {
|
|
357
359
|
case 'frontend-reload':
|
|
358
|
-
|
|
360
|
+
handleFrontendReload(message.data);
|
|
359
361
|
break;
|
|
360
362
|
case 'backend-api-reload':
|
|
361
|
-
//
|
|
363
|
+
// Backend sempre precisa recarregar
|
|
364
|
+
console.log('🔄 Backend changed, reloading...');
|
|
362
365
|
window.location.reload();
|
|
363
366
|
break;
|
|
364
367
|
case 'server-restart':
|
|
@@ -370,12 +373,91 @@ export class HotReloadManager {
|
|
|
370
373
|
case 'frontend-error':
|
|
371
374
|
console.error('❌ Frontend error:', message.data);
|
|
372
375
|
break;
|
|
376
|
+
case 'hmr-update':
|
|
377
|
+
handleHMRUpdate(message.data);
|
|
378
|
+
break;
|
|
373
379
|
}
|
|
374
380
|
} catch (e) {
|
|
375
381
|
console.error('Erro ao processar mensagem do hot-reload:', e);
|
|
376
382
|
}
|
|
377
383
|
};
|
|
378
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
|
+
|
|
379
461
|
ws.onclose = function(event) {
|
|
380
462
|
isConnected = false;
|
|
381
463
|
|
|
@@ -476,5 +558,12 @@ export class HotReloadManager {
|
|
|
476
558
|
this.buildCompleteResolve = null;
|
|
477
559
|
}
|
|
478
560
|
this.isBuilding = false;
|
|
561
|
+
|
|
562
|
+
// Notifica os clientes que o build terminou
|
|
563
|
+
if (success) {
|
|
564
|
+
this.notifyClients('build-complete', { success: true });
|
|
565
|
+
} else {
|
|
566
|
+
this.notifyClients('build-error', { success: false });
|
|
567
|
+
}
|
|
479
568
|
}
|
|
480
569
|
}
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
183
|
+
try {
|
|
184
|
+
fs.writeFileSync(entryFilePath, entryContent);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error("sdfijnsdfnijfsdijnfsdnijsdfnijfsdnijfsdnijfsdn", e)
|
|
187
|
+
}
|
|
183
188
|
|
|
184
|
-
|
|
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(
|
|
202
|
+
const userBackendRoutesDir = path.join(dir, 'src', 'backend', 'routes');
|
|
194
203
|
|
|
195
204
|
/**
|
|
196
205
|
* Executa middlewares sequencialmente e depois o handler final
|
|
@@ -289,7 +298,7 @@ export default function hweb(options: HightJSOptions) {
|
|
|
289
298
|
|
|
290
299
|
const notFound = loadNotFound(userWebDir);
|
|
291
300
|
|
|
292
|
-
const outDir = path.join(dir, '
|
|
301
|
+
const outDir = path.join(dir, '.hight');
|
|
293
302
|
fs.mkdirSync(outDir, {recursive: true});
|
|
294
303
|
|
|
295
304
|
entryPoint = createEntryFile(dir, frontendRoutes);
|
|
@@ -317,6 +326,11 @@ export default function hweb(options: HightJSOptions) {
|
|
|
317
326
|
time.update(""); // limpa a linha
|
|
318
327
|
time.end(` ${Colors.BgGreen} build ${Colors.Reset} Client build completed in ${elapsed}ms`);
|
|
319
328
|
|
|
329
|
+
// Notifica o hot reload manager que o build foi concluído
|
|
330
|
+
if (hotReloadManager) {
|
|
331
|
+
hotReloadManager.onBuildComplete(true);
|
|
332
|
+
}
|
|
333
|
+
|
|
320
334
|
} else {
|
|
321
335
|
const time = Console.dynamicLine(` ${Colors.BgYellow} watcher ${Colors.Reset} Starting client watch`);
|
|
322
336
|
watchWithChunks(entryPoint, outDir, hotReloadManager!).catch(err => {
|
|
@@ -373,7 +387,7 @@ export default function hweb(options: HightJSOptions) {
|
|
|
373
387
|
}
|
|
374
388
|
|
|
375
389
|
// 2. Primeiro verifica se é um arquivo estático da pasta public
|
|
376
|
-
if (pathname !== '/' && !pathname.startsWith('/api/') && !pathname.startsWith('
|
|
390
|
+
if (pathname !== '/' && !pathname.startsWith('/api/') && !pathname.startsWith('/.hight')) {
|
|
377
391
|
const publicDir = path.join(dir, 'public');
|
|
378
392
|
const filePath = path.join(publicDir, pathname);
|
|
379
393
|
|
|
@@ -417,12 +431,14 @@ export default function hweb(options: HightJSOptions) {
|
|
|
417
431
|
}
|
|
418
432
|
}
|
|
419
433
|
|
|
420
|
-
// 3. Verifica se é um arquivo estático do
|
|
421
|
-
if (pathname.startsWith('/
|
|
422
|
-
|
|
423
|
-
const
|
|
434
|
+
// 3. Verifica se é um arquivo estático do .hight
|
|
435
|
+
if (pathname.startsWith('/_hight/')) {
|
|
436
|
+
|
|
437
|
+
const staticPath = path.join(dir, '.hight');
|
|
438
|
+
const filePath = path.join(staticPath, pathname.replace('/_hight/', ''));
|
|
424
439
|
|
|
425
440
|
if (fs.existsSync(filePath)) {
|
|
441
|
+
|
|
426
442
|
const ext = path.extname(filePath).toLowerCase();
|
|
427
443
|
const contentTypes: Record<string, string> = {
|
|
428
444
|
'.js': 'application/javascript',
|
package/src/renderer.tsx
CHANGED
|
@@ -21,19 +21,128 @@ import type { GenericRequest } from './types/framework';
|
|
|
21
21
|
import fs from 'fs';
|
|
22
22
|
import path from 'path';
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
function
|
|
26
|
-
|
|
24
|
+
// Função para gerar todas as meta tags
|
|
25
|
+
function generateMetaTags(metadata: Metadata): string {
|
|
26
|
+
const tags: string[] = [];
|
|
27
|
+
|
|
28
|
+
// Charset
|
|
29
|
+
tags.push(`<meta charset="${metadata.charset || 'UTF-8'}">`);
|
|
30
|
+
|
|
31
|
+
// Viewport
|
|
32
|
+
tags.push(`<meta name="viewport" content="${metadata.viewport || 'width=device-width, initial-scale=1.0'}">`);
|
|
33
|
+
|
|
34
|
+
// Description
|
|
35
|
+
if (metadata.description) {
|
|
36
|
+
tags.push(`<meta name="description" content="${metadata.description}">`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Keywords
|
|
40
|
+
if (metadata.keywords) {
|
|
41
|
+
const keywordsStr = Array.isArray(metadata.keywords)
|
|
42
|
+
? metadata.keywords.join(', ')
|
|
43
|
+
: metadata.keywords;
|
|
44
|
+
tags.push(`<meta name="keywords" content="${keywordsStr}">`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Author
|
|
48
|
+
if (metadata.author) {
|
|
49
|
+
tags.push(`<meta name="author" content="${metadata.author}">`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Theme color
|
|
53
|
+
if (metadata.themeColor) {
|
|
54
|
+
tags.push(`<meta name="theme-color" content="${metadata.themeColor}">`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Robots
|
|
58
|
+
if (metadata.robots) {
|
|
59
|
+
tags.push(`<meta name="robots" content="${metadata.robots}">`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Canonical
|
|
63
|
+
if (metadata.canonical) {
|
|
64
|
+
tags.push(`<link rel="canonical" href="${metadata.canonical}">`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Favicon
|
|
68
|
+
if (metadata.favicon) {
|
|
69
|
+
tags.push(`<link rel="icon" href="${metadata.favicon}">`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Apple Touch Icon
|
|
73
|
+
if (metadata.appleTouchIcon) {
|
|
74
|
+
tags.push(`<link rel="apple-touch-icon" href="${metadata.appleTouchIcon}">`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Manifest
|
|
78
|
+
if (metadata.manifest) {
|
|
79
|
+
tags.push(`<link rel="manifest" href="${metadata.manifest}">`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Open Graph
|
|
83
|
+
if (metadata.openGraph) {
|
|
84
|
+
const og = metadata.openGraph;
|
|
85
|
+
if (og.title) tags.push(`<meta property="og:title" content="${og.title}">`);
|
|
86
|
+
if (og.description) tags.push(`<meta property="og:description" content="${og.description}">`);
|
|
87
|
+
if (og.type) tags.push(`<meta property="og:type" content="${og.type}">`);
|
|
88
|
+
if (og.url) tags.push(`<meta property="og:url" content="${og.url}">`);
|
|
89
|
+
if (og.siteName) tags.push(`<meta property="og:site_name" content="${og.siteName}">`);
|
|
90
|
+
if (og.locale) tags.push(`<meta property="og:locale" content="${og.locale}">`);
|
|
91
|
+
|
|
92
|
+
if (og.image) {
|
|
93
|
+
if (typeof og.image === 'string') {
|
|
94
|
+
tags.push(`<meta property="og:image" content="${og.image}">`);
|
|
95
|
+
} else {
|
|
96
|
+
tags.push(`<meta property="og:image" content="${og.image.url}">`);
|
|
97
|
+
if (og.image.width) tags.push(`<meta property="og:image:width" content="${og.image.width}">`);
|
|
98
|
+
if (og.image.height) tags.push(`<meta property="og:image:height" content="${og.image.height}">`);
|
|
99
|
+
if (og.image.alt) tags.push(`<meta property="og:image:alt" content="${og.image.alt}">`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Twitter Card
|
|
105
|
+
if (metadata.twitter) {
|
|
106
|
+
const tw = metadata.twitter;
|
|
107
|
+
if (tw.card) tags.push(`<meta name="twitter:card" content="${tw.card}">`);
|
|
108
|
+
if (tw.site) tags.push(`<meta name="twitter:site" content="${tw.site}">`);
|
|
109
|
+
if (tw.creator) tags.push(`<meta name="twitter:creator" content="${tw.creator}">`);
|
|
110
|
+
if (tw.title) tags.push(`<meta name="twitter:title" content="${tw.title}">`);
|
|
111
|
+
if (tw.description) tags.push(`<meta name="twitter:description" content="${tw.description}">`);
|
|
112
|
+
if (tw.image) tags.push(`<meta name="twitter:image" content="${tw.image}">`);
|
|
113
|
+
if (tw.imageAlt) tags.push(`<meta name="twitter:image:alt" content="${tw.imageAlt}">`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Custom meta tags
|
|
117
|
+
if (metadata.other) {
|
|
118
|
+
for (const [key, value] of Object.entries(metadata.other)) {
|
|
119
|
+
tags.push(`<meta name="${key}" content="${value}">`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tags.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Função para ofuscar dados (não é criptografia, apenas ofuscação)
|
|
127
|
+
function obfuscateData(data: any): string {
|
|
128
|
+
// 1. Serializa para JSON minificado
|
|
27
129
|
const jsonStr = JSON.stringify(data);
|
|
130
|
+
|
|
131
|
+
// 2. Converte para base64
|
|
28
132
|
const base64 = Buffer.from(jsonStr).toString('base64');
|
|
29
|
-
|
|
133
|
+
|
|
134
|
+
// 3. Adiciona um hash fake no início para parecer um token
|
|
135
|
+
const hash = Buffer.from(Date.now().toString()).toString('base64').substring(0, 8);
|
|
136
|
+
|
|
137
|
+
return `${hash}.${base64}`;
|
|
30
138
|
}
|
|
31
139
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
140
|
+
// Função para criar script ofuscado
|
|
141
|
+
function createInitialDataScript(data: any): string {
|
|
142
|
+
const obfuscated = obfuscateData(data);
|
|
143
|
+
|
|
144
|
+
// Usa um atributo data-* ao invés de JSON visível
|
|
145
|
+
return `<script id="__hight_data__" type="text/plain" data-h="${obfuscated}"></script>`;
|
|
37
146
|
}
|
|
38
147
|
|
|
39
148
|
// Interface para opções de renderização apenas do cliente
|
|
@@ -74,27 +183,42 @@ export async function render({ req, route, params, allRoutes }: RenderOptions):
|
|
|
74
183
|
initialParams: params,
|
|
75
184
|
};
|
|
76
185
|
|
|
77
|
-
//
|
|
78
|
-
const
|
|
186
|
+
// Cria script JSON limpo
|
|
187
|
+
const initialDataScript = createInitialDataScript(initialData);
|
|
79
188
|
|
|
80
189
|
// Script de hot reload apenas em desenvolvimento
|
|
81
190
|
const hotReloadScript = !isProduction && hotReloadManager
|
|
82
191
|
? hotReloadManager.getClientScript()
|
|
83
192
|
: '';
|
|
84
193
|
|
|
85
|
-
|
|
194
|
+
// Gera todas as meta tags
|
|
195
|
+
const metaTags = generateMetaTags(metadata);
|
|
86
196
|
|
|
87
197
|
// Determina quais arquivos JavaScript carregar
|
|
88
198
|
const jsFiles = getJavaScriptFiles(req);
|
|
89
199
|
|
|
200
|
+
const htmlLang = metadata.language || 'pt-BR';
|
|
201
|
+
|
|
90
202
|
// HTML base sem SSR - apenas o container e scripts para client-side rendering
|
|
91
|
-
return `<!DOCTYPE html
|
|
203
|
+
return `<!DOCTYPE html>
|
|
204
|
+
<html lang="${htmlLang}">
|
|
205
|
+
<head>
|
|
206
|
+
${metaTags}
|
|
207
|
+
<title>${metadata.title || 'App hweb'}</title>
|
|
208
|
+
</head>
|
|
209
|
+
<body>
|
|
210
|
+
<div id="root"></div>
|
|
211
|
+
${initialDataScript}
|
|
212
|
+
${jsFiles}
|
|
213
|
+
${hotReloadScript}
|
|
214
|
+
</body>
|
|
215
|
+
</html>`;
|
|
92
216
|
}
|
|
93
217
|
|
|
94
218
|
// Função para determinar quais arquivos JavaScript carregar
|
|
95
219
|
function getJavaScriptFiles(req: GenericRequest): string {
|
|
96
220
|
const projectDir = process.cwd();
|
|
97
|
-
const distDir = path.join(projectDir, '
|
|
221
|
+
const distDir = path.join(projectDir, '.hight');
|
|
98
222
|
|
|
99
223
|
try {
|
|
100
224
|
// Verifica se existe um manifesto de chunks (gerado pelo ESBuild com splitting)
|
|
@@ -105,7 +229,7 @@ function getJavaScriptFiles(req: GenericRequest): string {
|
|
|
105
229
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
106
230
|
const scripts = Object.values(manifest)
|
|
107
231
|
.filter((file: any) => file.endsWith('.js'))
|
|
108
|
-
.map((file: any) => `<script src="/
|
|
232
|
+
.map((file: any) => `<script src="/_hight/${file}"></script>`)
|
|
109
233
|
.join('');
|
|
110
234
|
return scripts;
|
|
111
235
|
} else {
|
|
@@ -125,15 +249,15 @@ function getJavaScriptFiles(req: GenericRequest): string {
|
|
|
125
249
|
if (jsFiles.length >= 1) {
|
|
126
250
|
// Modo chunks sem manifesto
|
|
127
251
|
return jsFiles
|
|
128
|
-
.map(file => `<script src="/
|
|
252
|
+
.map(file => `<script src="/_hight/${file}"></script>`)
|
|
129
253
|
.join('');
|
|
130
254
|
} else {
|
|
131
255
|
// Modo tradicional - único arquivo
|
|
132
|
-
return '<script src="/
|
|
256
|
+
return '<script src="/_hight/main.js"></script>';
|
|
133
257
|
}
|
|
134
258
|
}
|
|
135
259
|
} catch (error) {
|
|
136
260
|
// Fallback para o modo tradicional
|
|
137
|
-
return '<script src="/
|
|
261
|
+
return '<script src="/_hight/main.js"></script>';
|
|
138
262
|
}
|
|
139
263
|
}
|
package/src/router.ts
CHANGED
|
@@ -671,6 +671,7 @@ export function setupWebSocketUpgrade(server: any, hotReloadManager?: any) {
|
|
|
671
671
|
// Se não há listeners, ou se o hot-reload ainda não foi configurado, adiciona o nosso
|
|
672
672
|
if (existingListeners.length === 0) {
|
|
673
673
|
server.on('upgrade', (request: any, socket: any, head: Buffer) => {
|
|
674
|
+
|
|
674
675
|
handleWebSocketUpgrade(request, socket, head, hotReloadManager);
|
|
675
676
|
});
|
|
676
677
|
}
|
|
@@ -689,6 +690,7 @@ function handleWebSocketUpgrade(request: any, socket: any, head: Buffer, hotRelo
|
|
|
689
690
|
|
|
690
691
|
// Prioridade 1: Hot reload (sistema interno)
|
|
691
692
|
if (pathname === '/hweb-hotreload/') {
|
|
693
|
+
|
|
692
694
|
if (hotReloadManager) {
|
|
693
695
|
hotReloadManager.handleUpgrade(request, socket, head);
|
|
694
696
|
} else {
|
package/src/types.ts
CHANGED
|
@@ -90,6 +90,12 @@ export interface HightConfig {
|
|
|
90
90
|
* Padrão: 2048
|
|
91
91
|
*/
|
|
92
92
|
maxUrlLength?: number;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Habilita o log de acesso HTTP (ex: GET /api/users 200 15ms).
|
|
96
|
+
* Padrão: false
|
|
97
|
+
*/
|
|
98
|
+
accessLogging?: boolean;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
/**
|
|
@@ -99,11 +105,61 @@ export type HightConfigFunction = (
|
|
|
99
105
|
phase: string,
|
|
100
106
|
context: { defaultConfig: HightConfig }
|
|
101
107
|
) => HightConfig | Promise<HightConfig>;
|
|
108
|
+
|
|
102
109
|
export interface Metadata {
|
|
110
|
+
// Basic metadata
|
|
103
111
|
title?: string;
|
|
104
112
|
description?: string;
|
|
113
|
+
keywords?: string | string[];
|
|
114
|
+
author?: string;
|
|
105
115
|
favicon?: string;
|
|
116
|
+
|
|
117
|
+
// Viewport and mobile
|
|
118
|
+
viewport?: string;
|
|
119
|
+
themeColor?: string;
|
|
120
|
+
|
|
121
|
+
// SEO
|
|
122
|
+
canonical?: string;
|
|
123
|
+
robots?: string;
|
|
124
|
+
|
|
125
|
+
// Open Graph (Facebook, LinkedIn, etc.)
|
|
126
|
+
openGraph?: {
|
|
127
|
+
title?: string;
|
|
128
|
+
description?: string;
|
|
129
|
+
type?: string;
|
|
130
|
+
url?: string;
|
|
131
|
+
image?: string | {
|
|
132
|
+
url: string;
|
|
133
|
+
width?: number;
|
|
134
|
+
height?: number;
|
|
135
|
+
alt?: string;
|
|
136
|
+
};
|
|
137
|
+
siteName?: string;
|
|
138
|
+
locale?: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Twitter Card
|
|
142
|
+
twitter?: {
|
|
143
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
144
|
+
site?: string;
|
|
145
|
+
creator?: string;
|
|
146
|
+
title?: string;
|
|
147
|
+
description?: string;
|
|
148
|
+
image?: string;
|
|
149
|
+
imageAlt?: string;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Additional metadata
|
|
153
|
+
language?: string;
|
|
154
|
+
charset?: string;
|
|
155
|
+
appleTouchIcon?: string;
|
|
156
|
+
manifest?: string;
|
|
157
|
+
|
|
158
|
+
// Custom meta tags
|
|
159
|
+
other?: Record<string, string>;
|
|
106
160
|
}
|
|
161
|
+
|
|
162
|
+
|
|
107
163
|
export interface RouteConfig {
|
|
108
164
|
pattern: string;
|
|
109
165
|
component: ComponentType<any>;
|
|
File without changes
|