hightjs 0.5.0 → 0.5.2

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.
@@ -0,0 +1,526 @@
1
+ /*
2
+ * This file is part of the HightJS Project.
3
+ * Copyright (c) 2025 itsmuzin
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import React, {useState, useEffect, useCallback, useRef} from 'react';
18
+ import { createRoot } from 'react-dom/client';
19
+ import { router } from './clientRouter';
20
+
21
+ // --- O Componente Principal do Cliente (Roteador) ---
22
+
23
+ interface AppProps {
24
+ componentMap: Record<string, any>;
25
+ routes: { pattern: string; componentPath: string }[];
26
+ initialComponentPath: string;
27
+ initialParams: any;
28
+ layoutComponent?: any;
29
+ }
30
+
31
+ function App({ componentMap, routes, initialComponentPath, initialParams, layoutComponent }: AppProps) {
32
+ // Estado que guarda o componente a ser renderizado atualmente
33
+ const [hmrTimestamp, setHmrTimestamp] = useState(Date.now());
34
+
35
+ // Helper para encontrar rota baseado no path
36
+ const findRouteForPath = useCallback((path: string) => {
37
+ for (const route of routes) {
38
+ const regexPattern = route.pattern
39
+ // [[...param]] → opcional catch-all
40
+ .replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
41
+ // [...param] → obrigatório catch-all
42
+ .replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
43
+ // /[[param]] → opcional com barra também opcional
44
+ .replace(/\/\[\[(\w+)\]\]/g, '(?:/(?<$1>[^/]+))?')
45
+ // [[param]] → segmento opcional (sem barra anterior)
46
+ .replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
47
+ // [param] → segmento obrigatório
48
+ .replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
49
+ const regex = new RegExp(`^${regexPattern}/?$`);
50
+ const match = path.match(regex);
51
+ if (match) {
52
+ return {
53
+ componentPath: route.componentPath,
54
+ params: match.groups || {}
55
+ };
56
+ }
57
+ }
58
+ return null;
59
+ }, [routes]);
60
+
61
+ // Inicializa o componente e params baseado na URL ATUAL (não no initialComponentPath)
62
+ const [CurrentPageComponent, setCurrentPageComponent] = useState(() => {
63
+ // Pega a rota atual da URL
64
+ const currentPath = window.location.pathname;
65
+ const match = findRouteForPath(currentPath);
66
+
67
+ if (match) {
68
+ return componentMap[match.componentPath];
69
+ }
70
+
71
+ // Se não encontrou rota, retorna null para mostrar 404
72
+ return null;
73
+ });
74
+
75
+ const [params, setParams] = useState(() => {
76
+ // Pega os params da URL atual
77
+ const currentPath = window.location.pathname;
78
+ const match = findRouteForPath(currentPath);
79
+ return match ? match.params : {};
80
+ });
81
+
82
+ // HMR: Escuta eventos de hot reload
83
+ useEffect(() => {
84
+ // Ativa o sistema de HMR
85
+ (window as any).__HWEB_HMR__ = true;
86
+
87
+ const handleHMRUpdate = async (event: CustomEvent) => {
88
+ const { file, timestamp } = event.detail;
89
+ const fileName = file ? file.split('/').pop()?.split('\\').pop() : 'unknown';
90
+ console.log('🔥 HMR: Hot reloading...', fileName);
91
+
92
+ try {
93
+ // Aguarda um pouco para o esbuild terminar de recompilar
94
+ await new Promise(resolve => setTimeout(resolve, 300));
95
+
96
+ // Re-importa o módulo principal com cache busting
97
+ const mainScript = document.querySelector('script[src*="main.js"]') as HTMLScriptElement;
98
+ if (mainScript) {
99
+ const mainSrc = mainScript.src.split('?')[0];
100
+ const cacheBustedSrc = `${mainSrc}?t=${timestamp}`;
101
+
102
+ // Cria novo script
103
+ const newScript = document.createElement('script');
104
+ newScript.type = 'module';
105
+ newScript.src = cacheBustedSrc;
106
+
107
+ // Quando o novo script carregar, força re-render
108
+ newScript.onload = () => {
109
+ console.log('✅ HMR: Modules reloaded');
110
+
111
+ // Força re-render do componente
112
+ setHmrTimestamp(timestamp);
113
+
114
+ // Marca sucesso
115
+ (window as any).__HMR_SUCCESS__ = true;
116
+ setTimeout(() => {
117
+ (window as any).__HMR_SUCCESS__ = false;
118
+ }, 3000);
119
+ };
120
+
121
+ newScript.onerror = () => {
122
+ console.error('❌ HMR: Failed to reload modules');
123
+ (window as any).__HMR_SUCCESS__ = false;
124
+ };
125
+
126
+ // Remove o script antigo e adiciona o novo
127
+ // (não remove para não quebrar o app)
128
+ document.head.appendChild(newScript);
129
+ } else {
130
+ // Se não encontrou o script, apenas força re-render
131
+ console.log('⚡ HMR: Forcing re-render');
132
+ setHmrTimestamp(timestamp);
133
+ (window as any).__HMR_SUCCESS__ = true;
134
+ }
135
+ } catch (error) {
136
+ console.error('❌ HMR Error:', error);
137
+ (window as any).__HMR_SUCCESS__ = false;
138
+ }
139
+ };
140
+
141
+ window.addEventListener('hmr:component-update' as any, handleHMRUpdate);
142
+
143
+ return () => {
144
+ window.removeEventListener('hmr:component-update' as any, handleHMRUpdate);
145
+ };
146
+ }, []);
147
+
148
+
149
+ const updateRoute = useCallback(() => {
150
+ const currentPath = router.pathname;
151
+ const match = findRouteForPath(currentPath);
152
+ if (match) {
153
+ setCurrentPageComponent(() => componentMap[match.componentPath]);
154
+ setParams(match.params);
155
+ } else {
156
+ // Se não encontrou rota, define como null para mostrar 404
157
+ setCurrentPageComponent(null);
158
+ setParams({});
159
+ }
160
+ }, [router.pathname, findRouteForPath, componentMap]);
161
+
162
+ // Ouve os eventos de "voltar" e "avançar" do navegador
163
+ useEffect(() => {
164
+ const handlePopState = () => {
165
+ updateRoute();
166
+ };
167
+
168
+ window.addEventListener('popstate', handlePopState);
169
+
170
+ // Também se inscreve no router para mudanças de rota
171
+ const unsubscribe = router.subscribe(updateRoute);
172
+
173
+ return () => {
174
+ window.removeEventListener('popstate', handlePopState);
175
+ unsubscribe();
176
+ };
177
+ }, [updateRoute]);
178
+
179
+ // Se não há componente ou é a rota __404__, mostra página 404
180
+ if (!CurrentPageComponent || initialComponentPath === '__404__') {
181
+ // Usa o componente 404 personalizado se existir, senão usa o padrão do hweb
182
+ const NotFoundComponent = (window as any).__HWEB_NOT_FOUND__;
183
+
184
+ if (NotFoundComponent) {
185
+ // Usa o notFound.tsx personalizado do usuário
186
+ const NotFoundContent = <NotFoundComponent />;
187
+
188
+ // Aplica o layout se existir
189
+ if (layoutComponent) {
190
+ return React.createElement(layoutComponent, { children: NotFoundContent });
191
+ }
192
+ return NotFoundContent;
193
+ } else {
194
+ // Usa o 404 padrão do hweb que foi incluído no build
195
+ const DefaultNotFound = (window as any).__HWEB_DEFAULT_NOT_FOUND__;
196
+ const NotFoundContent = <DefaultNotFound />;
197
+
198
+ // Aplica o layout se existir
199
+ if (layoutComponent) {
200
+ return React.createElement(layoutComponent, { children: NotFoundContent });
201
+ }
202
+ return NotFoundContent;
203
+ }
204
+ }
205
+
206
+ // Renderiza o componente atual (sem Context, usa o router diretamente)
207
+ // Usa key com timestamp para forçar re-mount durante HMR
208
+ const PageContent = <CurrentPageComponent key={`page-${hmrTimestamp}`} params={params} />;
209
+
210
+ // SEMPRE usa o layout - se não existir, cria um wrapper padrão
211
+ const content = layoutComponent
212
+ ? React.createElement(layoutComponent, { children: PageContent })
213
+ : <div>{PageContent}</div>;
214
+
215
+ // Adiciona o indicador de dev se não for produção
216
+ return (
217
+ <>
218
+ {content}
219
+ {process.env.NODE_ENV !== 'production' && <DevIndicator />}
220
+ </>
221
+ );
222
+ }
223
+
224
+
225
+
226
+ // --- Constantes de Configuração ---
227
+ const DEV_INDICATOR_SIZE = 48;
228
+ const DEV_INDICATOR_CORNERS = [
229
+ { top: 16, left: 16 }, // 0: topo-esquerda
230
+ { top: 16, right: 16 }, // 1: topo-direita
231
+ { bottom: 16, left: 16 }, // 2: baixo-esquerda
232
+ { bottom: 16, right: 16 },// 3: baixo-direita
233
+ ];
234
+
235
+ function DevIndicator() {
236
+ const [corner, setCorner] = useState(3); // Canto atual (0-3)
237
+ const [isDragging, setIsDragging] = useState(false); // Estado de arrastar
238
+ const [isBuilding, setIsBuilding] = useState(false); // Estado de build
239
+
240
+ // Posição visual do indicador durante o arraste
241
+ const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
242
+
243
+ const indicatorRef = useRef<HTMLDivElement>(null);
244
+ const dragStartRef = useRef<{ x: number; y: number; moved: boolean } | null>(null);
245
+
246
+ // Escuta eventos de hot reload para mostrar estado de build
247
+ useEffect(() => {
248
+ if (typeof window === 'undefined') return;
249
+
250
+ const handleHotReloadMessage = (event: MessageEvent) => {
251
+ try {
252
+ const message = JSON.parse(event.data);
253
+
254
+ // Quando detecta mudança em arquivo, ativa loading
255
+ if (message.type === 'frontend-reload' ||
256
+ message.type === 'backend-api-reload' ||
257
+ message.type === 'src-reload') {
258
+ setIsBuilding(true);
259
+ }
260
+
261
+ // Quando o build termina ou servidor fica pronto, desativa loading
262
+ if (message.type === 'server-ready' || message.type === 'build-complete') {
263
+ setIsBuilding(false);
264
+ }
265
+ } catch (e) {
266
+ // Ignora mensagens que não são JSON
267
+ }
268
+ };
269
+
270
+ // Intercepta mensagens WebSocket
271
+ const originalWebSocket = window.WebSocket;
272
+ window.WebSocket = class extends originalWebSocket {
273
+ constructor(url: string | URL, protocols?: string | string[]) {
274
+ super(url, protocols);
275
+
276
+ this.addEventListener('message', (event) => {
277
+ if (url.toString().includes('hweb-hotreload')) {
278
+ handleHotReloadMessage(event);
279
+ }
280
+ });
281
+ }
282
+ } as any;
283
+
284
+ return () => {
285
+ window.WebSocket = originalWebSocket;
286
+ };
287
+ }, []);
288
+
289
+ // --- Estilos Dinâmicos ---
290
+ const getIndicatorStyle = (): React.CSSProperties => {
291
+ const baseStyle: React.CSSProperties = {
292
+ position: 'fixed',
293
+ zIndex: 9999,
294
+ width: DEV_INDICATOR_SIZE,
295
+ height: DEV_INDICATOR_SIZE,
296
+ borderRadius: '50%',
297
+ background: isBuilding
298
+ ? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
299
+ : 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
300
+ color: 'white',
301
+ fontWeight: 'bold',
302
+ fontSize: 28,
303
+ boxShadow: isBuilding
304
+ ? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
305
+ : '0 4px 15px rgba(0,0,0,0.2)',
306
+ display: 'flex',
307
+ alignItems: 'center',
308
+ justifyContent: 'center',
309
+ cursor: isDragging ? 'grabbing' : 'grab',
310
+ userSelect: 'none',
311
+ transition: isDragging ? 'none' : 'all 0.3s ease-out',
312
+ animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
313
+ };
314
+
315
+ if (isDragging) {
316
+ return {
317
+ ...baseStyle,
318
+ top: position.top,
319
+ left: position.left,
320
+ };
321
+ }
322
+
323
+ return { ...baseStyle, ...DEV_INDICATOR_CORNERS[corner] };
324
+ };
325
+
326
+ const getMenuPositionStyle = (): React.CSSProperties => {
327
+ // Posiciona o menu dependendo do canto
328
+ switch (corner) {
329
+ case 0: return { top: '110%', left: '0' }; // Top-Left
330
+ case 1: return { top: '110%', right: '0' }; // Top-Right
331
+ case 2: return { bottom: '110%', left: '0' }; // Bottom-Left
332
+ case 3: return { bottom: '110%', right: '0' }; // Bottom-Right
333
+ default: return {};
334
+ }
335
+ };
336
+
337
+ // --- Lógica de Eventos ---
338
+ const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
339
+ e.preventDefault();
340
+ dragStartRef.current = { x: e.clientX, y: e.clientY, moved: false };
341
+ if (indicatorRef.current) {
342
+ const rect = indicatorRef.current.getBoundingClientRect();
343
+ setPosition({ top: rect.top, left: rect.left });
344
+ }
345
+ setIsDragging(true);
346
+ };
347
+
348
+ const handleMouseMove = useCallback((e: MouseEvent) => {
349
+ if (!isDragging || !dragStartRef.current) return;
350
+
351
+ const deltaX = e.clientX - dragStartRef.current.x;
352
+ const deltaY = e.clientY - dragStartRef.current.y;
353
+
354
+ // Diferencia clique de arrastar (threshold de 5px)
355
+ if (!dragStartRef.current.moved && Math.hypot(deltaX, deltaY) > 5) {
356
+ dragStartRef.current.moved = true;
357
+ }
358
+
359
+ if (dragStartRef.current.moved) {
360
+ setPosition(prevPos => ({
361
+ top: prevPos.top + deltaY,
362
+ left: prevPos.left + deltaX,
363
+ }));
364
+ // Atualiza a referência para o próximo movimento
365
+ dragStartRef.current.x = e.clientX;
366
+ dragStartRef.current.y = e.clientY;
367
+ }
368
+ }, [isDragging]);
369
+
370
+ const handleMouseUp = useCallback((e: MouseEvent) => {
371
+ if (!isDragging) return;
372
+ setIsDragging(false);
373
+
374
+ // Se moveu, calcula o canto mais próximo
375
+ if (dragStartRef.current?.moved) {
376
+ const { clientX, clientY } = e;
377
+ const w = window.innerWidth;
378
+ const h = window.innerHeight;
379
+
380
+ const dists = [
381
+ Math.hypot(clientX, clientY), // TL
382
+ Math.hypot(w - clientX, clientY), // TR
383
+ Math.hypot(clientX, h - clientY), // BL
384
+ Math.hypot(w - clientX, h - clientY), // BR
385
+ ];
386
+ setCorner(dists.indexOf(Math.min(...dists)));
387
+ }
388
+
389
+ dragStartRef.current = null;
390
+ }, [isDragging]);
391
+
392
+ // Adiciona e remove listeners globais
393
+ useEffect(() => {
394
+ if (isDragging) {
395
+ window.addEventListener('mousemove', handleMouseMove);
396
+ window.addEventListener('mouseup', handleMouseUp);
397
+ }
398
+ return () => {
399
+ window.removeEventListener('mousemove', handleMouseMove);
400
+ window.removeEventListener('mouseup', handleMouseUp);
401
+ };
402
+ }, [isDragging, handleMouseMove, handleMouseUp]);
403
+
404
+
405
+ return (
406
+ <>
407
+ <style>
408
+ {`
409
+ @keyframes hweb-pulse {
410
+ 0%, 100% {
411
+ transform: scale(1);
412
+ opacity: 1;
413
+ }
414
+ 50% {
415
+ transform: scale(1.1);
416
+ opacity: 0.8;
417
+ }
418
+ }
419
+
420
+ @keyframes hweb-spin {
421
+ from {
422
+ transform: rotate(0deg);
423
+ }
424
+ to {
425
+ transform: rotate(360deg);
426
+ }
427
+ }
428
+ `}
429
+ </style>
430
+ <div
431
+ ref={indicatorRef}
432
+ style={getIndicatorStyle()}
433
+ onMouseDown={handleMouseDown}
434
+ title={isBuilding ? "Building..." : "Modo Dev HightJS"}
435
+ >
436
+ {isBuilding ? (
437
+ <span style={{ animation: 'hweb-spin 1s linear infinite' }}>⟳</span>
438
+ ) : (
439
+ 'H'
440
+ )}
441
+ </div>
442
+ </>
443
+ );
444
+ }
445
+
446
+ // --- Inicialização do Cliente (CSR - Client-Side Rendering) ---
447
+
448
+ function deobfuscateData(obfuscated: string): any {
449
+ try {
450
+ // Remove o hash fake
451
+ const parts = obfuscated.split('.');
452
+ const base64 = parts.length > 1 ? parts[1] : parts[0];
453
+
454
+ // Decodifica base64
455
+ const jsonStr = atob(base64);
456
+
457
+ // Parse JSON
458
+ return JSON.parse(jsonStr);
459
+ } catch (error) {
460
+ console.error('[hweb] Failed to decode data:', error);
461
+ return null;
462
+ }
463
+ }
464
+
465
+ function initializeClient() {
466
+ // Lê os dados do atributo data-h
467
+ const dataElement = document.getElementById('__hight_data__');
468
+
469
+ if (!dataElement) {
470
+ console.error('[hweb] Initial data script not found.');
471
+ return;
472
+ }
473
+
474
+ const obfuscated = dataElement.getAttribute('data-h');
475
+
476
+ if (!obfuscated) {
477
+ console.error('[hweb] Data attribute not found.');
478
+ return;
479
+ }
480
+
481
+ const initialData = deobfuscateData(obfuscated);
482
+
483
+ if (!initialData) {
484
+ console.error('[hweb] Failed to parse initial data.');
485
+ return;
486
+ }
487
+
488
+ // Cria o mapa de componentes dinamicamente a partir dos módulos carregados
489
+ const componentMap: Record<string, any> = {};
490
+
491
+ // Registra todos os componentes que foram importados
492
+ if ((window as any).__HWEB_COMPONENTS__) {
493
+ Object.assign(componentMap, (window as any).__HWEB_COMPONENTS__);
494
+ }
495
+
496
+ const container = document.getElementById('root');
497
+ if (!container) {
498
+ console.error('[hweb] Container #root not found.');
499
+ return;
500
+ }
501
+
502
+ try {
503
+ // Usar createRoot para render inicial (CSR)
504
+ const root = createRoot(container);
505
+
506
+ root.render(
507
+ <App
508
+ componentMap={componentMap}
509
+ routes={initialData.routes}
510
+ initialComponentPath={initialData.initialComponentPath}
511
+ initialParams={initialData.initialParams}
512
+ layoutComponent={(window as any).__HWEB_LAYOUT__}
513
+ />
514
+ );
515
+ } catch (error) {
516
+ console.error('[hweb] Error rendering application:', error);
517
+ }
518
+ }
519
+
520
+ // Executa quando o DOM estiver pronto
521
+ if (document.readyState === 'loading') {
522
+ document.addEventListener('DOMContentLoaded', initializeClient);
523
+ } else {
524
+ initializeClient();
525
+ }
526
+
@@ -0,0 +1,38 @@
1
+ /*
2
+ * This file is part of the HightJS Project.
3
+ * Copyright (c) 2025 itsmuzin
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import React, { type AnchorHTMLAttributes, type ReactNode } from 'react';
18
+ import { router } from '../client/clientRouter';
19
+
20
+ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
21
+ href: string;
22
+ children: ReactNode;
23
+ }
24
+
25
+ export function Link({ href, children, ...props }: LinkProps) {
26
+ const handleClick = async (e: React.MouseEvent<HTMLAnchorElement>) => {
27
+ e.preventDefault();
28
+
29
+ // Usa o novo sistema de router
30
+ await router.push(href);
31
+ };
32
+
33
+ return (
34
+ <a href={href} {...props} onClick={handleClick}>
35
+ {children}
36
+ </a>
37
+ );
38
+ }