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.
@@ -1,154 +1,9 @@
1
1
  import {RouteConfig, router} from "hightjs/client";
2
2
  import {GuestOnly, useSession} from "hightjs/auth/react"
3
3
  import React, {useState} from "react";
4
+ import LoginPage from "../components/LoginPage";
4
5
 
5
6
 
6
- function LoginPage() {
7
- const [username, setUsername] = useState("");
8
- const [password, setPassword] = useState("");
9
- const [isLoading, setIsLoading] = useState(false);
10
- const {signIn} = useSession();
11
-
12
- const [error, setError] = useState<string | null>(null);
13
-
14
- const handleLogin = async (e: React.FormEvent) => {
15
- e.preventDefault();
16
- setIsLoading(true);
17
- setError(null);
18
-
19
- try {
20
- const result = await signIn('credentials', {
21
- redirect: false,
22
- username: username,
23
- password: password,
24
- callbackUrl: '/'
25
- });
26
- console.log(result)
27
- if (!result || result.error) {
28
- setError('Credenciais inválidas. Verifique seus dados e senha.');
29
- setIsLoading(false);
30
- return;
31
- }
32
- router.push("/")
33
-
34
- } catch (err) {
35
- setError('Ocorreu um erro inesperado. Tente novamente.');
36
- setIsLoading(false);
37
- }
38
-
39
- };
40
-
41
- return (
42
- <div className="font-sans min-h-screen flex items-center justify-center p-8 bg-gradient-to-r from-black to-gray-500 text-gray-200 relative overflow-hidden">
43
- {/* Background gradient */}
44
- <div className="absolute inset-0 -z-10 h-full w-full bg-gradient-to-r from-black to-gray-500 bg-[radial-gradient(circle_500px_at_50%_200px,#3e007555,transparent)]"></div>
45
-
46
- {/* Additional ambient lights */}
47
- <div className="absolute top-1/4 left-1/4 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl -z-10"></div>
48
- <div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl -z-10"></div>
49
-
50
- {/* Login Container */}
51
- <div className="w-full max-w-md">
52
- {/* Header */}
53
- <div className="text-center mb-8">
54
- <div className="flex items-center justify-center gap-3 mb-4">
55
- <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-purple-500">
56
- <path d="m12 3-8.9 5.06a4 4 0 0 0-1.1 5.44l1.1 1.94a4 4 0 0 0 5.44 1.1l8.9-5.06a4 4 0 0 0 1.1-5.44l-1.1-1.94a4 4 0 0 0-5.44-1.1z"></path>
57
- </svg>
58
- <h1 className="text-2xl font-bold text-purple-400 [text-shadow:_0_0_12px_theme(colors.purple.500)]">HightJS</h1>
59
- </div>
60
- <h2 className="text-xl font-medium text-gray-300 mb-2">Bem-vindo de volta</h2>
61
- <p className="text-sm text-gray-400">Faça login em sua conta</p>
62
- </div>
63
-
64
- {/* Glass Login Form */}
65
- <div className="relative">
66
- {/* Glass background */}
67
- <div className="absolute inset-0 bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl"></div>
68
-
69
- {/* error */}
70
- {error && (
71
- <div className="relative p-4 mb-4 text-sm text-red-800 bg-red-200 rounded-lg" role="alert">
72
- {error}
73
- </div>
74
- )}
75
-
76
- {/* Form content */}
77
- <form onSubmit={handleLogin} className="relative p-8 space-y-6">
78
- {/* Email field */}
79
- <div className="space-y-2">
80
- <label htmlFor="email" className="text-sm font-medium text-gray-300">
81
- Usuário
82
- </label>
83
- <input
84
- id="username"
85
- type="username"
86
- value={username}
87
- onChange={(e) => setUsername(e.target.value)}
88
- className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-gray-200 placeholder-gray-400 focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 focus:outline-none backdrop-blur-sm transition-all duration-300"
89
- required
90
- />
91
- </div>
92
-
93
- {/* Password field */}
94
- <div className="space-y-2">
95
- <label htmlFor="password" className="text-sm font-medium text-gray-300">
96
- Senha
97
- </label>
98
- <input
99
- id="password"
100
- type="password"
101
- placeholder="••••••••"
102
- value={password}
103
- onChange={(e) => setPassword(e.target.value)}
104
- className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-gray-200 placeholder-gray-400 focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 focus:outline-none backdrop-blur-sm transition-all duration-300"
105
- required
106
- />
107
- </div>
108
-
109
- {/* Remember me and forgot password */}
110
- <div className="flex items-center justify-between text-sm">
111
- <label className="flex items-center text-gray-400 cursor-pointer">
112
- <input
113
- type="checkbox"
114
- className="mr-2 rounded border-white/20 bg-white/5 text-purple-500 focus:ring-purple-500/20"
115
- />
116
- Lembrar de mim
117
- </label>
118
- <a href="#" className="text-purple-400 hover:text-purple-300 transition-colors hover:underline">
119
- Esqueceu a senha?
120
- </a>
121
- </div>
122
-
123
- {/* Login button */}
124
- <button
125
- type="submit"
126
- disabled={isLoading}
127
- className="w-full bg-purple-600 hover:bg-purple-500 text-white font-medium py-3 px-4 rounded-xl transition-all duration-300 shadow-[0_0_15px_-3px_theme(colors.purple.600)] hover:shadow-[0_0_25px_-3px_theme(colors.purple.500)] disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-purple-500/20"
128
- >
129
- {isLoading ? (
130
- <div className="flex items-center justify-center gap-2">
131
- <svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
132
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
133
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
134
- </svg>
135
- Entrando...
136
- </div>
137
- ) : (
138
- 'Entrar'
139
- )}
140
- </button>
141
-
142
-
143
-
144
-
145
- </form>
146
- </div>
147
-
148
- </div>
149
- </div>
150
- );
151
- }
152
7
 
153
8
  const wrapper = () => {
154
9
  const session = useSession()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hightjs",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
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",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/express": "^4.17.21",
49
+ "@types/fs-extra": "^11.0.4",
49
50
  "@types/node": "^20.11.24",
50
51
  "@types/react": "^19.2.0",
51
52
  "@types/react-dom": "^19.2.0",
@@ -65,6 +66,7 @@
65
66
  "express": "^4.0.0",
66
67
  "fastify": "^5.6.1",
67
68
  "framer-motion": "^12.23.22",
69
+ "fs-extra": "^11.3.2",
68
70
  "ws": "^8.18.1"
69
71
  },
70
72
  "peerDependency": {
@@ -202,7 +202,9 @@ export default class Console {
202
202
  }
203
203
 
204
204
  static logWithout(level: Levels, colors?:Colors, ...args: any[]): void {
205
- 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;
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);
@@ -133,11 +133,11 @@ program
133
133
  await app.prepare();
134
134
  console.log('✅ Build complete\n');
135
135
 
136
- // 3. Copia a pasta hweb-dist para exported
137
- const distDir = path.join(projectDir, 'hweb-dist');
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, 'hweb-dist');
140
+ const exportDistDir = path.join(exportDir, '.hight');
141
141
  fs.mkdirSync(exportDistDir, { recursive: true });
142
142
 
143
143
  const files = fs.readdirSync(distDir);
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
@@ -153,6 +220,7 @@ const DEV_INDICATOR_CORNERS = [
153
220
  function DevIndicator() {
154
221
  const [corner, setCorner] = useState(3); // Canto atual (0-3)
155
222
  const [isDragging, setIsDragging] = useState(false); // Estado de arrastar
223
+ const [isBuilding, setIsBuilding] = useState(false); // Estado de build
156
224
 
157
225
  // Posição visual do indicador durante o arraste
158
226
  const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
@@ -160,6 +228,49 @@ function DevIndicator() {
160
228
  const indicatorRef = useRef<HTMLDivElement>(null);
161
229
  const dragStartRef = useRef<{ x: number; y: number; moved: boolean } | null>(null);
162
230
 
231
+ // Escuta eventos de hot reload para mostrar estado de build
232
+ useEffect(() => {
233
+ if (typeof window === 'undefined') return;
234
+
235
+ const handleHotReloadMessage = (event: MessageEvent) => {
236
+ try {
237
+ const message = JSON.parse(event.data);
238
+
239
+ // Quando detecta mudança em arquivo, ativa loading
240
+ if (message.type === 'frontend-reload' ||
241
+ message.type === 'backend-api-reload' ||
242
+ message.type === 'src-reload') {
243
+ setIsBuilding(true);
244
+ }
245
+
246
+ // Quando o build termina ou servidor fica pronto, desativa loading
247
+ if (message.type === 'server-ready' || message.type === 'build-complete') {
248
+ setIsBuilding(false);
249
+ }
250
+ } catch (e) {
251
+ // Ignora mensagens que não são JSON
252
+ }
253
+ };
254
+
255
+ // Intercepta mensagens WebSocket
256
+ const originalWebSocket = window.WebSocket;
257
+ window.WebSocket = class extends originalWebSocket {
258
+ constructor(url: string | URL, protocols?: string | string[]) {
259
+ super(url, protocols);
260
+
261
+ this.addEventListener('message', (event) => {
262
+ if (url.toString().includes('hweb-hotreload')) {
263
+ handleHotReloadMessage(event);
264
+ }
265
+ });
266
+ }
267
+ } as any;
268
+
269
+ return () => {
270
+ window.WebSocket = originalWebSocket;
271
+ };
272
+ }, []);
273
+
163
274
  // --- Estilos Dinâmicos ---
164
275
  const getIndicatorStyle = (): React.CSSProperties => {
165
276
  const baseStyle: React.CSSProperties = {
@@ -168,17 +279,22 @@ function DevIndicator() {
168
279
  width: DEV_INDICATOR_SIZE,
169
280
  height: DEV_INDICATOR_SIZE,
170
281
  borderRadius: '50%',
171
- background: 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo
282
+ background: isBuilding
283
+ ? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
284
+ : 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
172
285
  color: 'white',
173
286
  fontWeight: 'bold',
174
287
  fontSize: 28,
175
- boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
288
+ boxShadow: isBuilding
289
+ ? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
290
+ : '0 4px 15px rgba(0,0,0,0.2)',
176
291
  display: 'flex',
177
292
  alignItems: 'center',
178
293
  justifyContent: 'center',
179
294
  cursor: isDragging ? 'grabbing' : 'grab',
180
295
  userSelect: 'none',
181
- transition: isDragging ? 'none' : 'all 0.3s ease-out', // Animação suave ao soltar
296
+ transition: isDragging ? 'none' : 'all 0.3s ease-out',
297
+ animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
182
298
  };
183
299
 
184
300
  if (isDragging) {
@@ -272,19 +388,85 @@ function DevIndicator() {
272
388
 
273
389
 
274
390
  return (
275
- <div ref={indicatorRef} style={getIndicatorStyle()} onMouseDown={handleMouseDown} title="Modo Dev HightJS">
276
- H
277
- </div>
391
+ <>
392
+ <style>
393
+ {`
394
+ @keyframes hweb-pulse {
395
+ 0%, 100% {
396
+ transform: scale(1);
397
+ opacity: 1;
398
+ }
399
+ 50% {
400
+ transform: scale(1.1);
401
+ opacity: 0.8;
402
+ }
403
+ }
404
+
405
+ @keyframes hweb-spin {
406
+ from {
407
+ transform: rotate(0deg);
408
+ }
409
+ to {
410
+ transform: rotate(360deg);
411
+ }
412
+ }
413
+ `}
414
+ </style>
415
+ <div
416
+ ref={indicatorRef}
417
+ style={getIndicatorStyle()}
418
+ onMouseDown={handleMouseDown}
419
+ title={isBuilding ? "Building..." : "Modo Dev HightJS"}
420
+ >
421
+ {isBuilding ? (
422
+ <span style={{ animation: 'hweb-spin 1s linear infinite' }}>⟳</span>
423
+ ) : (
424
+ 'H'
425
+ )}
426
+ </div>
427
+ </>
278
428
  );
279
429
  }
280
430
 
281
431
  // --- Inicialização do Cliente (CSR - Client-Side Rendering) ---
282
432
 
433
+ function deobfuscateData(obfuscated: string): any {
434
+ try {
435
+ // Remove o hash fake
436
+ const parts = obfuscated.split('.');
437
+ const base64 = parts.length > 1 ? parts[1] : parts[0];
438
+
439
+ // Decodifica base64
440
+ const jsonStr = atob(base64);
441
+
442
+ // Parse JSON
443
+ return JSON.parse(jsonStr);
444
+ } catch (error) {
445
+ console.error('[hweb] Failed to decode data:', error);
446
+ return null;
447
+ }
448
+ }
449
+
283
450
  function initializeClient() {
284
- const initialData = (window as any).__HWEB_INITIAL_DATA__;
451
+ // os dados do atributo data-h
452
+ const dataElement = document.getElementById('__hight_data__');
453
+
454
+ if (!dataElement) {
455
+ console.error('[hweb] Initial data script not found.');
456
+ return;
457
+ }
458
+
459
+ const obfuscated = dataElement.getAttribute('data-h');
460
+
461
+ if (!obfuscated) {
462
+ console.error('[hweb] Data attribute not found.');
463
+ return;
464
+ }
465
+
466
+ const initialData = deobfuscateData(obfuscated);
285
467
 
286
468
  if (!initialData) {
287
- console.error('[hweb] Initial data not found on page.');
469
+ console.error('[hweb] Failed to parse initial data.');
288
470
  return;
289
471
  }
290
472
 
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