hightjs 0.3.4 → 0.3.5
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/api/console.js +1 -1
- package/dist/bin/hightjs.js +3 -3
- package/dist/client/entry.client.js +96 -6
- package/dist/helpers.js +40 -1
- package/dist/hotReload.js +13 -4
- package/dist/index.js +10 -6
- package/dist/renderer.js +137 -18
- package/dist/types.d.ts +39 -0
- package/docs/config.md +15 -0
- package/example/hightjs.config.ts +6 -0
- package/example/package-lock.json +1 -1
- package/example/package.json +2 -2
- package/example/src/web/layout.tsx +57 -3
- package/example/src/web/routes/index.tsx +1 -1
- package/package.json +1 -1
- package/src/api/console.ts +3 -1
- package/src/bin/hightjs.js +3 -3
- package/src/client/entry.client.tsx +123 -8
- package/src/helpers.ts +39 -1
- package/src/hotReload.ts +13 -4
- package/src/index.ts +13 -6
- package/src/renderer.tsx +142 -18
- package/src/router.ts +2 -0
- package/src/types.ts +56 -0
|
@@ -8,11 +8,65 @@ interface LayoutProps {
|
|
|
8
8
|
children: React.ReactNode;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
export const metadata: Metadata = {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// --- Informações Básicas ---
|
|
14
|
+
// Título otimizado para abas do navegador e resultados de busca
|
|
15
|
+
title: "Hight JS | O Framework Web Rápido e Simples para React",
|
|
16
|
+
description: "O framework web mais rápido e simples para React! Comece a construir aplicações web performáticas hoje mesmo com Hight JS.",
|
|
17
|
+
keywords: ["Hight JS", "framework web", "React", "JavaScript", "TypeScript", "desenvolvimento web", "rápido", "simples", "SSR", "frontend"],
|
|
18
|
+
author: "Hight JS Team",
|
|
15
19
|
favicon: "/favicon.ico",
|
|
20
|
+
|
|
21
|
+
// --- Mobile e Viewport ---
|
|
22
|
+
// Essencial para design responsivo e boa pontuação no mobile
|
|
23
|
+
viewport: "width=device-width, initial-scale=1.0",
|
|
24
|
+
// Cor da barra de navegação em browsers mobile (ex: Chrome no Android)
|
|
25
|
+
themeColor: "#0A0A0A", // Sugestão de cor escura, ajuste conforme sua marca
|
|
26
|
+
|
|
27
|
+
// --- SEO ---
|
|
28
|
+
// URL principal do site para evitar conteúdo duplicado
|
|
29
|
+
canonical: "https://hightjs.com", // Substitua pelo seu domínio real
|
|
30
|
+
// Instruções para robôs de busca (Google, Bing, etc.)
|
|
31
|
+
robots: "index, follow",
|
|
32
|
+
|
|
33
|
+
// --- Open Graph (para Redes Sociais como Facebook, LinkedIn, Discord) ---
|
|
34
|
+
openGraph: {
|
|
35
|
+
title: "Hight JS | O Framework Web Rápido e Simples para React",
|
|
36
|
+
description: "Descubra o Hight JS, o framework web focado em performance e simplicidade para suas aplicações React.",
|
|
37
|
+
type: "website",
|
|
38
|
+
url: "https://hightjs.com", // Seu domínio real
|
|
39
|
+
// URL de uma imagem de preview (idealmente 1200x630px)
|
|
40
|
+
image: "https://hightjs.com/og-image.png",
|
|
41
|
+
siteName: "Hight JS",
|
|
42
|
+
locale: "pt_BR",
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// --- Twitter Card (para compartilhamento no Twitter) ---
|
|
46
|
+
twitter: {
|
|
47
|
+
card: "summary_large_image", // Mostra uma imagem grande no tweet
|
|
48
|
+
site: "@hightjs_team", // Handle do Twitter do projeto (exemplo)
|
|
49
|
+
creator: "@seu_criador", // Handle do Twitter do criador (exemplo)
|
|
50
|
+
title: "Hight JS | O Framework Web Rápido e Simples para React",
|
|
51
|
+
description: "Cansado de complexidade? Conheça o Hight JS e construa sites React mais rápidos e leves.",
|
|
52
|
+
// Imagem específica para Twitter (ou pode reusar a 'og-image')
|
|
53
|
+
image: "https://hightjs.com/twitter-image.png",
|
|
54
|
+
imageAlt: "Logo e slogan do framework Hight JS",
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// --- Ícones e Manifest (PWA) ---
|
|
58
|
+
// Ícone para dispositivos Apple (quando salvo na tela inicial)
|
|
59
|
+
appleTouchIcon: "/apple-touch-icon.png", // (ex: 180x180px)
|
|
60
|
+
// Link para o manifest de PWA (Progressive Web App)
|
|
61
|
+
manifest: "/site.webmanifest",
|
|
62
|
+
|
|
63
|
+
// --- Outros ---
|
|
64
|
+
language: "pt-BR", // Define o idioma principal da página
|
|
65
|
+
charset: "UTF-8", // Define o charset (embora geralmente setado no HTML)
|
|
66
|
+
other: {
|
|
67
|
+
// Tag útil para compatibilidade com navegadores legados (Edge/IE)
|
|
68
|
+
"X-UA-Compatible": "IE=edge"
|
|
69
|
+
}
|
|
16
70
|
};
|
|
17
71
|
|
|
18
72
|
export default function Layout({ children }: LayoutProps) {
|
|
@@ -21,7 +21,7 @@ function Home() {
|
|
|
21
21
|
{status === 'loading' && <p>Carregando...</p>}
|
|
22
22
|
{status === 'authenticated' && session?.user && (
|
|
23
23
|
<>
|
|
24
|
-
<p className="text-lg">Você está logado como <span className="font-bold text-purple-400">{session.user.name}</span
|
|
24
|
+
<p className="text-lg">Você está logado como <span className="font-bold text-purple-400">{session.user.name}</span>!</p>
|
|
25
25
|
<button
|
|
26
26
|
onClick={() => signOut({callbackUrl: '/'})}
|
|
27
27
|
className="rounded-full border border-solid border-transparent transition-all duration-300 flex items-center justify-center bg-purple-600 text-white gap-2 hover:bg-purple-500 font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-40 shadow-[0_0_15px_-3px_theme(colors.purple.600)] hover:shadow-[0_0_25px_-3px_theme(colors.purple.500)]"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hightjs",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "HightJS is a high-level framework for building web applications with ease and speed. It provides a robust set of tools and features to streamline development and enhance productivity.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/api/console.ts
CHANGED
|
@@ -202,7 +202,9 @@ export default class Console {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
static logWithout(level: Levels, colors?:Colors, ...args: any[]): void {
|
|
205
|
-
|
|
205
|
+
|
|
206
|
+
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;
|
|
207
|
+
|
|
206
208
|
let output = "";
|
|
207
209
|
for (const arg of args) {
|
|
208
210
|
let msg = (arg instanceof Error) ? arg.stack : (typeof arg === 'string') ? arg : JSON.stringify(arg, null, 2);
|
package/src/bin/hightjs.js
CHANGED
|
@@ -133,11 +133,11 @@ program
|
|
|
133
133
|
await app.prepare();
|
|
134
134
|
console.log('✅ Build complete\n');
|
|
135
135
|
|
|
136
|
-
// 3. Copia a pasta
|
|
137
|
-
const distDir = path.join(projectDir, '
|
|
136
|
+
// 3. Copia a pasta .hight para exported
|
|
137
|
+
const distDir = path.join(projectDir, '.hight');
|
|
138
138
|
if (fs.existsSync(distDir)) {
|
|
139
139
|
console.log('📦 Copying JavaScript files...');
|
|
140
|
-
const exportDistDir = path.join(exportDir, '
|
|
140
|
+
const exportDistDir = path.join(exportDir, '.hight');
|
|
141
141
|
fs.mkdirSync(exportDistDir, { recursive: true });
|
|
142
142
|
|
|
143
143
|
const files = fs.readdirSync(distDir);
|
|
@@ -153,6 +153,7 @@ const DEV_INDICATOR_CORNERS = [
|
|
|
153
153
|
function DevIndicator() {
|
|
154
154
|
const [corner, setCorner] = useState(3); // Canto atual (0-3)
|
|
155
155
|
const [isDragging, setIsDragging] = useState(false); // Estado de arrastar
|
|
156
|
+
const [isBuilding, setIsBuilding] = useState(false); // Estado de build
|
|
156
157
|
|
|
157
158
|
// Posição visual do indicador durante o arraste
|
|
158
159
|
const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
|
@@ -160,6 +161,49 @@ function DevIndicator() {
|
|
|
160
161
|
const indicatorRef = useRef<HTMLDivElement>(null);
|
|
161
162
|
const dragStartRef = useRef<{ x: number; y: number; moved: boolean } | null>(null);
|
|
162
163
|
|
|
164
|
+
// Escuta eventos de hot reload para mostrar estado de build
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (typeof window === 'undefined') return;
|
|
167
|
+
|
|
168
|
+
const handleHotReloadMessage = (event: MessageEvent) => {
|
|
169
|
+
try {
|
|
170
|
+
const message = JSON.parse(event.data);
|
|
171
|
+
|
|
172
|
+
// Quando detecta mudança em arquivo, ativa loading
|
|
173
|
+
if (message.type === 'frontend-reload' ||
|
|
174
|
+
message.type === 'backend-api-reload' ||
|
|
175
|
+
message.type === 'src-reload') {
|
|
176
|
+
setIsBuilding(true);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Quando o build termina ou servidor fica pronto, desativa loading
|
|
180
|
+
if (message.type === 'server-ready' || message.type === 'build-complete') {
|
|
181
|
+
setIsBuilding(false);
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// Ignora mensagens que não são JSON
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Intercepta mensagens WebSocket
|
|
189
|
+
const originalWebSocket = window.WebSocket;
|
|
190
|
+
window.WebSocket = class extends originalWebSocket {
|
|
191
|
+
constructor(url: string | URL, protocols?: string | string[]) {
|
|
192
|
+
super(url, protocols);
|
|
193
|
+
|
|
194
|
+
this.addEventListener('message', (event) => {
|
|
195
|
+
if (url.toString().includes('hweb-hotreload')) {
|
|
196
|
+
handleHotReloadMessage(event);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
} as any;
|
|
201
|
+
|
|
202
|
+
return () => {
|
|
203
|
+
window.WebSocket = originalWebSocket;
|
|
204
|
+
};
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
163
207
|
// --- Estilos Dinâmicos ---
|
|
164
208
|
const getIndicatorStyle = (): React.CSSProperties => {
|
|
165
209
|
const baseStyle: React.CSSProperties = {
|
|
@@ -168,17 +212,22 @@ function DevIndicator() {
|
|
|
168
212
|
width: DEV_INDICATOR_SIZE,
|
|
169
213
|
height: DEV_INDICATOR_SIZE,
|
|
170
214
|
borderRadius: '50%',
|
|
171
|
-
background:
|
|
215
|
+
background: isBuilding
|
|
216
|
+
? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
|
|
217
|
+
: 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
|
|
172
218
|
color: 'white',
|
|
173
219
|
fontWeight: 'bold',
|
|
174
220
|
fontSize: 28,
|
|
175
|
-
boxShadow:
|
|
221
|
+
boxShadow: isBuilding
|
|
222
|
+
? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
|
|
223
|
+
: '0 4px 15px rgba(0,0,0,0.2)',
|
|
176
224
|
display: 'flex',
|
|
177
225
|
alignItems: 'center',
|
|
178
226
|
justifyContent: 'center',
|
|
179
227
|
cursor: isDragging ? 'grabbing' : 'grab',
|
|
180
228
|
userSelect: 'none',
|
|
181
|
-
transition: isDragging ? 'none' : 'all 0.3s ease-out',
|
|
229
|
+
transition: isDragging ? 'none' : 'all 0.3s ease-out',
|
|
230
|
+
animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
|
|
182
231
|
};
|
|
183
232
|
|
|
184
233
|
if (isDragging) {
|
|
@@ -272,19 +321,85 @@ function DevIndicator() {
|
|
|
272
321
|
|
|
273
322
|
|
|
274
323
|
return (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
324
|
+
<>
|
|
325
|
+
<style>
|
|
326
|
+
{`
|
|
327
|
+
@keyframes hweb-pulse {
|
|
328
|
+
0%, 100% {
|
|
329
|
+
transform: scale(1);
|
|
330
|
+
opacity: 1;
|
|
331
|
+
}
|
|
332
|
+
50% {
|
|
333
|
+
transform: scale(1.1);
|
|
334
|
+
opacity: 0.8;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@keyframes hweb-spin {
|
|
339
|
+
from {
|
|
340
|
+
transform: rotate(0deg);
|
|
341
|
+
}
|
|
342
|
+
to {
|
|
343
|
+
transform: rotate(360deg);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
`}
|
|
347
|
+
</style>
|
|
348
|
+
<div
|
|
349
|
+
ref={indicatorRef}
|
|
350
|
+
style={getIndicatorStyle()}
|
|
351
|
+
onMouseDown={handleMouseDown}
|
|
352
|
+
title={isBuilding ? "Building..." : "Modo Dev HightJS"}
|
|
353
|
+
>
|
|
354
|
+
{isBuilding ? (
|
|
355
|
+
<span style={{ animation: 'hweb-spin 1s linear infinite' }}>⟳</span>
|
|
356
|
+
) : (
|
|
357
|
+
'H'
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
278
361
|
);
|
|
279
362
|
}
|
|
280
363
|
|
|
281
364
|
// --- Inicialização do Cliente (CSR - Client-Side Rendering) ---
|
|
282
365
|
|
|
366
|
+
function deobfuscateData(obfuscated: string): any {
|
|
367
|
+
try {
|
|
368
|
+
// Remove o hash fake
|
|
369
|
+
const parts = obfuscated.split('.');
|
|
370
|
+
const base64 = parts.length > 1 ? parts[1] : parts[0];
|
|
371
|
+
|
|
372
|
+
// Decodifica base64
|
|
373
|
+
const jsonStr = atob(base64);
|
|
374
|
+
|
|
375
|
+
// Parse JSON
|
|
376
|
+
return JSON.parse(jsonStr);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('[hweb] Failed to decode data:', error);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
283
383
|
function initializeClient() {
|
|
284
|
-
|
|
384
|
+
// Lê os dados do atributo data-h
|
|
385
|
+
const dataElement = document.getElementById('__hight_data__');
|
|
386
|
+
|
|
387
|
+
if (!dataElement) {
|
|
388
|
+
console.error('[hweb] Initial data script not found.');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const obfuscated = dataElement.getAttribute('data-h');
|
|
393
|
+
|
|
394
|
+
if (!obfuscated) {
|
|
395
|
+
console.error('[hweb] Data attribute not found.');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const initialData = deobfuscateData(obfuscated);
|
|
285
400
|
|
|
286
401
|
if (!initialData) {
|
|
287
|
-
console.error('[hweb]
|
|
402
|
+
console.error('[hweb] Failed to parse initial data.');
|
|
288
403
|
return;
|
|
289
404
|
}
|
|
290
405
|
|
package/src/helpers.ts
CHANGED
|
@@ -112,6 +112,7 @@ async function loadHightConfig(projectDir: string, phase: string): Promise<Hight
|
|
|
112
112
|
serverTimeout: 35000,
|
|
113
113
|
individualRequestTimeout: 30000,
|
|
114
114
|
maxUrlLength: 2048,
|
|
115
|
+
accessLogging: true,
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
try {
|
|
@@ -265,6 +266,10 @@ async function initNativeServer(hwebApp: HWebApp, options: HightJSOptions, port:
|
|
|
265
266
|
// Extraímos a lógica principal para uma variável
|
|
266
267
|
// para que possa ser usada tanto pelo servidor HTTP quanto HTTPS.
|
|
267
268
|
const requestListener = async (req: HWebIncomingMessage, res: ServerResponse) => {
|
|
269
|
+
const requestStartTime = Date.now();
|
|
270
|
+
const method = req.method || 'GET';
|
|
271
|
+
const url = req.url || '/';
|
|
272
|
+
|
|
268
273
|
// Configurações de segurança básicas
|
|
269
274
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
270
275
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
@@ -281,11 +286,44 @@ async function initNativeServer(hwebApp: HWebApp, options: HightJSOptions, port:
|
|
|
281
286
|
req.setTimeout(hightConfig.individualRequestTimeout || 30000, () => {
|
|
282
287
|
res.statusCode = 408; // Request Timeout
|
|
283
288
|
res.end('Request timeout');
|
|
289
|
+
|
|
290
|
+
// Log de timeout
|
|
291
|
+
if (hightConfig.accessLogging) {
|
|
292
|
+
const duration = Date.now() - requestStartTime;
|
|
293
|
+
Console.info(`${Colors.FgYellow}${method}${Colors.Reset} ${url} ${Colors.FgRed}408${Colors.Reset} ${Colors.FgGray}${duration}ms${Colors.Reset}`);
|
|
294
|
+
}
|
|
284
295
|
});
|
|
285
296
|
|
|
297
|
+
// Intercepta o método end() para logar quando a resposta for enviada
|
|
298
|
+
const originalEnd = res.end.bind(res);
|
|
299
|
+
let hasEnded = false;
|
|
300
|
+
|
|
301
|
+
res.end = function(this: ServerResponse, ...args: any[]): any {
|
|
302
|
+
if (!hasEnded && hightConfig.accessLogging) {
|
|
303
|
+
hasEnded = true;
|
|
304
|
+
const duration = Date.now() - requestStartTime;
|
|
305
|
+
const statusCode = res.statusCode || 200;
|
|
306
|
+
|
|
307
|
+
// Define cor baseada no status code
|
|
308
|
+
let statusColor = Colors.FgGreen; // 2xx
|
|
309
|
+
if (statusCode >= 500) statusColor = Colors.FgRed; // 5xx
|
|
310
|
+
else if (statusCode >= 400) statusColor = Colors.FgYellow; // 4xx
|
|
311
|
+
else if (statusCode >= 300) statusColor = Colors.FgCyan; // 3xx
|
|
312
|
+
|
|
313
|
+
// Formata o método com cor
|
|
314
|
+
let methodColor = Colors.BgCyan;
|
|
315
|
+
if (method === 'POST') methodColor = Colors.BgGreen;
|
|
316
|
+
else if (method === 'PUT') methodColor = Colors.BgYellow;
|
|
317
|
+
else if (method === 'DELETE') methodColor = Colors.BgRed;
|
|
318
|
+
else if (method === 'PATCH') methodColor = Colors.BgMagenta;
|
|
319
|
+
Console.logCustomLevel(method, true, methodColor, `${url} ${statusColor}${statusCode}${Colors.Reset} ${Colors.FgGray}${duration}ms${Colors.Reset}`);
|
|
320
|
+
}
|
|
321
|
+
// @ts-ignore
|
|
322
|
+
return originalEnd.apply(this, args);
|
|
323
|
+
} as any;
|
|
324
|
+
|
|
286
325
|
try {
|
|
287
326
|
// Validação básica de URL (usa configuração personalizada)
|
|
288
|
-
const url = req.url || '/';
|
|
289
327
|
const maxUrlLength = hightConfig.maxUrlLength || 2048;
|
|
290
328
|
if (url.length > maxUrlLength) {
|
|
291
329
|
res.statusCode = 414; // URI Too Long
|
package/src/hotReload.ts
CHANGED
|
@@ -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');
|
|
@@ -476,5 +478,12 @@ export class HotReloadManager {
|
|
|
476
478
|
this.buildCompleteResolve = null;
|
|
477
479
|
}
|
|
478
480
|
this.isBuilding = false;
|
|
481
|
+
|
|
482
|
+
// Notifica os clientes que o build terminou
|
|
483
|
+
if (success) {
|
|
484
|
+
this.notifyClients('build-complete', { success: true });
|
|
485
|
+
} else {
|
|
486
|
+
this.notifyClients('build-error', { success: false });
|
|
487
|
+
}
|
|
479
488
|
}
|
|
480
489
|
}
|
package/src/index.ts
CHANGED
|
@@ -289,7 +289,7 @@ export default function hweb(options: HightJSOptions) {
|
|
|
289
289
|
|
|
290
290
|
const notFound = loadNotFound(userWebDir);
|
|
291
291
|
|
|
292
|
-
const outDir = path.join(dir, '
|
|
292
|
+
const outDir = path.join(dir, '.hight');
|
|
293
293
|
fs.mkdirSync(outDir, {recursive: true});
|
|
294
294
|
|
|
295
295
|
entryPoint = createEntryFile(dir, frontendRoutes);
|
|
@@ -317,6 +317,11 @@ export default function hweb(options: HightJSOptions) {
|
|
|
317
317
|
time.update(""); // limpa a linha
|
|
318
318
|
time.end(` ${Colors.BgGreen} build ${Colors.Reset} Client build completed in ${elapsed}ms`);
|
|
319
319
|
|
|
320
|
+
// Notifica o hot reload manager que o build foi concluído
|
|
321
|
+
if (hotReloadManager) {
|
|
322
|
+
hotReloadManager.onBuildComplete(true);
|
|
323
|
+
}
|
|
324
|
+
|
|
320
325
|
} else {
|
|
321
326
|
const time = Console.dynamicLine(` ${Colors.BgYellow} watcher ${Colors.Reset} Starting client watch`);
|
|
322
327
|
watchWithChunks(entryPoint, outDir, hotReloadManager!).catch(err => {
|
|
@@ -373,7 +378,7 @@ export default function hweb(options: HightJSOptions) {
|
|
|
373
378
|
}
|
|
374
379
|
|
|
375
380
|
// 2. Primeiro verifica se é um arquivo estático da pasta public
|
|
376
|
-
if (pathname !== '/' && !pathname.startsWith('/api/') && !pathname.startsWith('
|
|
381
|
+
if (pathname !== '/' && !pathname.startsWith('/api/') && !pathname.startsWith('/.hight')) {
|
|
377
382
|
const publicDir = path.join(dir, 'public');
|
|
378
383
|
const filePath = path.join(publicDir, pathname);
|
|
379
384
|
|
|
@@ -417,12 +422,14 @@ export default function hweb(options: HightJSOptions) {
|
|
|
417
422
|
}
|
|
418
423
|
}
|
|
419
424
|
|
|
420
|
-
// 3. Verifica se é um arquivo estático do
|
|
421
|
-
if (pathname.startsWith('/
|
|
422
|
-
|
|
423
|
-
const
|
|
425
|
+
// 3. Verifica se é um arquivo estático do .hight
|
|
426
|
+
if (pathname.startsWith('/_hight/')) {
|
|
427
|
+
|
|
428
|
+
const staticPath = path.join(dir, '.hight');
|
|
429
|
+
const filePath = path.join(staticPath, pathname.replace('/_hight/', ''));
|
|
424
430
|
|
|
425
431
|
if (fs.existsSync(filePath)) {
|
|
432
|
+
|
|
426
433
|
const ext = path.extname(filePath).toLowerCase();
|
|
427
434
|
const contentTypes: Record<string, string> = {
|
|
428
435
|
'.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
|
}
|