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
|
@@ -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
|
+
"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": {
|
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);
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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',
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
451
|
+
// Lê 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]
|
|
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
|