hightjs 0.1.1
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/.idea/HightJS.iml +9 -0
- package/.idea/copilot.data.migration.agent.xml +6 -0
- package/.idea/copilot.data.migration.ask.xml +6 -0
- package/.idea/copilot.data.migration.ask2agent.xml +6 -0
- package/.idea/copilot.data.migration.edit.xml +6 -0
- package/.idea/inspectionProfiles/Project_Default.xml +13 -0
- package/.idea/libraries/test_package.xml +9 -0
- package/.idea/libraries/ts_commonjs_default_export.xml +9 -0
- package/.idea/misc.xml +7 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/LICENSE +13 -0
- package/README.md +508 -0
- package/dist/adapters/express.d.ts +7 -0
- package/dist/adapters/express.js +63 -0
- package/dist/adapters/factory.d.ts +23 -0
- package/dist/adapters/factory.js +122 -0
- package/dist/adapters/fastify.d.ts +25 -0
- package/dist/adapters/fastify.js +61 -0
- package/dist/adapters/native.d.ts +8 -0
- package/dist/adapters/native.js +203 -0
- package/dist/adapters/starters/express.d.ts +0 -0
- package/dist/adapters/starters/express.js +1 -0
- package/dist/adapters/starters/factory.d.ts +0 -0
- package/dist/adapters/starters/factory.js +1 -0
- package/dist/adapters/starters/fastify.d.ts +0 -0
- package/dist/adapters/starters/fastify.js +1 -0
- package/dist/adapters/starters/index.d.ts +0 -0
- package/dist/adapters/starters/index.js +1 -0
- package/dist/adapters/starters/native.d.ts +0 -0
- package/dist/adapters/starters/native.js +1 -0
- package/dist/api/console.d.ts +92 -0
- package/dist/api/console.js +276 -0
- package/dist/api/http.d.ts +180 -0
- package/dist/api/http.js +467 -0
- package/dist/auth/client.d.ts +14 -0
- package/dist/auth/client.js +68 -0
- package/dist/auth/components.d.ts +29 -0
- package/dist/auth/components.js +84 -0
- package/dist/auth/core.d.ts +38 -0
- package/dist/auth/core.js +124 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.js +27 -0
- package/dist/auth/jwt.d.ts +41 -0
- package/dist/auth/jwt.js +169 -0
- package/dist/auth/providers.d.ts +5 -0
- package/dist/auth/providers.js +14 -0
- package/dist/auth/react/index.d.ts +6 -0
- package/dist/auth/react/index.js +32 -0
- package/dist/auth/react.d.ts +22 -0
- package/dist/auth/react.js +175 -0
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/routes.js +104 -0
- package/dist/auth/types.d.ts +62 -0
- package/dist/auth/types.js +2 -0
- package/dist/bin/hightjs.d.ts +2 -0
- package/dist/bin/hightjs.js +35 -0
- package/dist/builder.d.ts +32 -0
- package/dist/builder.js +341 -0
- package/dist/client/DefaultNotFound.d.ts +1 -0
- package/dist/client/DefaultNotFound.js +53 -0
- package/dist/client/ErrorBoundary.d.ts +16 -0
- package/dist/client/ErrorBoundary.js +181 -0
- package/dist/client/clientRouter.d.ts +58 -0
- package/dist/client/clientRouter.js +116 -0
- package/dist/client/entry.client.d.ts +1 -0
- package/dist/client/entry.client.js +271 -0
- package/dist/client/routerContext.d.ts +26 -0
- package/dist/client/routerContext.js +62 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +8 -0
- package/dist/components/Link.d.ts +7 -0
- package/dist/components/Link.js +13 -0
- package/dist/eslint/index.d.ts +32 -0
- package/dist/eslint/index.js +15 -0
- package/dist/eslint/use-client-rule.d.ts +19 -0
- package/dist/eslint/use-client-rule.js +99 -0
- package/dist/eslintSetup.d.ts +0 -0
- package/dist/eslintSetup.js +1 -0
- package/dist/example/src/web/routes/index.d.ts +3 -0
- package/dist/example/src/web/routes/index.js +15 -0
- package/dist/helpers.d.ts +18 -0
- package/dist/helpers.js +318 -0
- package/dist/hotReload.d.ts +23 -0
- package/dist/hotReload.js +292 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +480 -0
- package/dist/renderer.d.ts +14 -0
- package/dist/renderer.js +106 -0
- package/dist/router.d.ts +78 -0
- package/dist/router.js +359 -0
- package/dist/types/framework.d.ts +37 -0
- package/dist/types/framework.js +2 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +2 -0
- package/dist/typescript/use-client-plugin.d.ts +5 -0
- package/dist/typescript/use-client-plugin.js +113 -0
- package/dist/validation.d.ts +0 -0
- package/dist/validation.js +1 -0
- package/package.json +72 -0
- package/src/adapters/express.ts +70 -0
- package/src/adapters/factory.ts +96 -0
- package/src/adapters/fastify.ts +88 -0
- package/src/adapters/native.ts +223 -0
- package/src/api/console.ts +285 -0
- package/src/api/http.ts +515 -0
- package/src/auth/client.ts +74 -0
- package/src/auth/components.tsx +109 -0
- package/src/auth/core.ts +143 -0
- package/src/auth/index.ts +9 -0
- package/src/auth/jwt.ts +194 -0
- package/src/auth/providers.ts +13 -0
- package/src/auth/react/index.ts +9 -0
- package/src/auth/react.tsx +209 -0
- package/src/auth/routes.ts +133 -0
- package/src/auth/types.ts +73 -0
- package/src/bin/hightjs.js +40 -0
- package/src/builder.js +362 -0
- package/src/client/DefaultNotFound.tsx +68 -0
- package/src/client/clientRouter.ts +137 -0
- package/src/client/entry.client.tsx +302 -0
- package/src/client.ts +8 -0
- package/src/components/Link.tsx +22 -0
- package/src/helpers.ts +316 -0
- package/src/hotReload.ts +289 -0
- package/src/index.ts +514 -0
- package/src/renderer.tsx +122 -0
- package/src/router.ts +400 -0
- package/src/types/framework.ts +42 -0
- package/src/types.ts +54 -0
- package/tsconfig.json +17 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Helpers para integração com diferentes frameworks
|
|
2
|
+
import hweb, { FrameworkAdapterFactory } from './index';
|
|
3
|
+
import type { HightJSOptions } from './types';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import Console, {Colors} from "./api/console";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function getLocalExternalIp() {
|
|
11
|
+
|
|
12
|
+
const interfaces = os.networkInterfaces();
|
|
13
|
+
for (const name of Object.keys(interfaces)) {
|
|
14
|
+
for (const iface of interfaces[name]!) {
|
|
15
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
16
|
+
return iface.address;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return 'localhost';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sendBox = (options:HightJSOptions) => {
|
|
24
|
+
const isDev = options.dev ? "Rodando em modo de desenvolvimento" : null;
|
|
25
|
+
const messages = [
|
|
26
|
+
` ${Colors.FgMagenta}● ${Colors.Reset}Local: ${Colors.FgGreen}http://localhost:${options.port}${Colors.Reset}`,
|
|
27
|
+
` ${Colors.FgMagenta}● ${Colors.Reset}Rede: ${Colors.FgGreen}http://${getLocalExternalIp()}:${options.port}${Colors.Reset}`,
|
|
28
|
+
]
|
|
29
|
+
if(isDev) {
|
|
30
|
+
messages.push(` ${Colors.FgMagenta}● ${Colors.Reset}${isDev}`)
|
|
31
|
+
}
|
|
32
|
+
Console.box(messages.join("\n"), {title: "Acesse o HightJS em:"})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export default app
|
|
37
|
+
export function app(options: HightJSOptions = {}) {
|
|
38
|
+
const framework = options.framework || 'native'; // Mudando o padrão para 'native'
|
|
39
|
+
FrameworkAdapterFactory.setFramework(framework)
|
|
40
|
+
|
|
41
|
+
const hwebApp = hweb(options);
|
|
42
|
+
return {
|
|
43
|
+
...hwebApp,
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Integra com uma aplicação de qualquer framework (Express, Fastify, etc)
|
|
47
|
+
*/
|
|
48
|
+
integrate: async (serverApp: any) => {
|
|
49
|
+
await hwebApp.prepare();
|
|
50
|
+
const handler = hwebApp.getRequestHandler();
|
|
51
|
+
|
|
52
|
+
if (framework === 'express') {
|
|
53
|
+
// Express integration
|
|
54
|
+
serverApp.use(handler);
|
|
55
|
+
hwebApp.setupWebSocket(serverApp);
|
|
56
|
+
} else if (framework === 'fastify') {
|
|
57
|
+
// Fastify integration
|
|
58
|
+
await serverApp.register(async (fastify: any) => {
|
|
59
|
+
fastify.all('*', handler);
|
|
60
|
+
});
|
|
61
|
+
hwebApp.setupWebSocket(serverApp);
|
|
62
|
+
} else {
|
|
63
|
+
// Generic integration - assume Express-like
|
|
64
|
+
serverApp.use(handler);
|
|
65
|
+
hwebApp.setupWebSocket(serverApp);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
hwebApp.executeInstrumentation();
|
|
69
|
+
return serverApp;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Inicia um servidor HightJS fechado (o usuário não tem acesso ao framework)
|
|
74
|
+
*/
|
|
75
|
+
init: async () => {
|
|
76
|
+
console.log(`${Colors.FgMagenta}
|
|
77
|
+
_ _ _ _ _ _ _____
|
|
78
|
+
| | | (_) | | | | | |/ ____|
|
|
79
|
+
| |__| |_ __ _| |__ | |_ | | (___
|
|
80
|
+
| __ | |/ _\` | '_ \\| __| _ | |\\___ \\
|
|
81
|
+
| | | | | (_| | | | | |_ | |__| |____) |
|
|
82
|
+
|_| |_|_|\\__, |_| |_|\\__| \\____/|_____/
|
|
83
|
+
__/ |
|
|
84
|
+
|___/ ${Colors.Reset}`)
|
|
85
|
+
const actualPort = options.port || 3000;
|
|
86
|
+
const actualHostname = options.hostname || "0.0.0.0";
|
|
87
|
+
|
|
88
|
+
if (framework === 'express') {
|
|
89
|
+
return await initExpressServer(hwebApp, options, actualPort, actualHostname);
|
|
90
|
+
} else if (framework === 'fastify') {
|
|
91
|
+
return await initFastifyServer(hwebApp, options, actualPort, actualHostname);
|
|
92
|
+
} else {
|
|
93
|
+
// Default to Native
|
|
94
|
+
return await initNativeServer(hwebApp, options, actualPort, actualHostname);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Inicializa servidor Express fechado
|
|
102
|
+
*/
|
|
103
|
+
async function initExpressServer(hwebApp: any, options: HightJSOptions, port: number, hostname: string) {
|
|
104
|
+
const msg = Console.dynamicLine(` ${Colors.FgCyan}● ${Colors.Reset}Iniciando HightJS com Express...`);
|
|
105
|
+
const express = require('express');
|
|
106
|
+
const app = express();
|
|
107
|
+
|
|
108
|
+
// Middlewares básicos para Express
|
|
109
|
+
app.use(express.json());
|
|
110
|
+
app.use(express.urlencoded({ extended: true }));
|
|
111
|
+
|
|
112
|
+
// Cookie parser se disponível
|
|
113
|
+
try {
|
|
114
|
+
const cookieParser = require('cookie-parser');
|
|
115
|
+
app.use(cookieParser());
|
|
116
|
+
} catch (e) {
|
|
117
|
+
Console.error("Não foi possivel achar cookie-parser")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await hwebApp.prepare();
|
|
121
|
+
const handler = hwebApp.getRequestHandler();
|
|
122
|
+
|
|
123
|
+
app.use(handler);
|
|
124
|
+
|
|
125
|
+
const server = app.listen(port, hostname, () => {
|
|
126
|
+
sendBox({ ...options, port });
|
|
127
|
+
msg.end(` ${Colors.FgCyan}● ${Colors.Reset}Servidor Express iniciado (compatibilidade)`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Configura WebSocket para hot reload
|
|
131
|
+
hwebApp.setupWebSocket(server);
|
|
132
|
+
hwebApp.executeInstrumentation();
|
|
133
|
+
return server;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Inicializa servidor Fastify fechado
|
|
138
|
+
*/
|
|
139
|
+
async function initFastifyServer(hwebApp: any, options: HightJSOptions, port: number, hostname: string) {
|
|
140
|
+
const msg = Console.dynamicLine(` ${Colors.FgCyan}● ${Colors.Reset}Iniciando HightJS com Fastify...`);
|
|
141
|
+
const fastify = require('fastify')({ logger: false });
|
|
142
|
+
|
|
143
|
+
// Registra plugins básicos para Fastify
|
|
144
|
+
try {
|
|
145
|
+
await fastify.register(require('@fastify/cookie'));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
Console.error("Não foi possivel achar @fastify/cookie")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await fastify.register(require('@fastify/formbody'));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
Console.error("Não foi possivel achar @fastify/formbody")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await hwebApp.prepare();
|
|
157
|
+
const handler = hwebApp.getRequestHandler();
|
|
158
|
+
|
|
159
|
+
// Registra o handler do hweb
|
|
160
|
+
await fastify.register(async (fastify: any) => {
|
|
161
|
+
fastify.all('*', handler);
|
|
162
|
+
});
|
|
163
|
+
hwebApp.setupWebSocket(fastify);
|
|
164
|
+
|
|
165
|
+
const address = await fastify.listen({ port, host: hostname });
|
|
166
|
+
sendBox({ ...options, port });
|
|
167
|
+
msg.end(` ${Colors.FgCyan}● ${Colors.Reset}Servidor Fastify iniciado (compatibilidade)`);
|
|
168
|
+
hwebApp.executeInstrumentation();
|
|
169
|
+
return fastify;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Inicializa servidor nativo do HightJS usando HTTP puro
|
|
174
|
+
*/
|
|
175
|
+
async function initNativeServer(hwebApp: any, options: HightJSOptions, port: number, hostname: string) {
|
|
176
|
+
const msg = Console.dynamicLine(` ${Colors.FgMagenta}⚡ ${Colors.Reset}${Colors.Bright}Iniciando HightJS em modo NATIVO${Colors.Reset}`);
|
|
177
|
+
|
|
178
|
+
const http = require('http');
|
|
179
|
+
const { parse: parseUrl } = require('url');
|
|
180
|
+
const { parse: parseQuery } = require('querystring');
|
|
181
|
+
|
|
182
|
+
await hwebApp.prepare();
|
|
183
|
+
const handler = hwebApp.getRequestHandler();
|
|
184
|
+
|
|
185
|
+
// Middleware para parsing do body com proteções de segurança
|
|
186
|
+
const parseBody = (req: any): Promise<any> => {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
189
|
+
resolve(null);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let body = '';
|
|
194
|
+
let totalSize = 0;
|
|
195
|
+
const maxBodySize = 10 * 1024 * 1024; // 10MB limite
|
|
196
|
+
|
|
197
|
+
// Timeout para requisições que demoram muito
|
|
198
|
+
const timeout = setTimeout(() => {
|
|
199
|
+
req.destroy();
|
|
200
|
+
reject(new Error('Request timeout'));
|
|
201
|
+
}, 30000); // 30 segundos
|
|
202
|
+
|
|
203
|
+
req.on('data', (chunk: Buffer) => {
|
|
204
|
+
totalSize += chunk.length;
|
|
205
|
+
|
|
206
|
+
// Proteção contra ataques de DoS por body muito grande
|
|
207
|
+
if (totalSize > maxBodySize) {
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
req.destroy();
|
|
210
|
+
reject(new Error('Request body too large'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
body += chunk.toString();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
req.on('end', () => {
|
|
218
|
+
clearTimeout(timeout);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const contentType = req.headers['content-type'] || '';
|
|
222
|
+
if (contentType.includes('application/json')) {
|
|
223
|
+
// Validação adicional para JSON
|
|
224
|
+
if (body.length > 1024 * 1024) { // 1MB limite para JSON
|
|
225
|
+
reject(new Error('JSON body too large'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
resolve(JSON.parse(body));
|
|
229
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
230
|
+
resolve(parseQuery(body));
|
|
231
|
+
} else {
|
|
232
|
+
resolve(body);
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
resolve(body); // Fallback para string se parsing falhar
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
req.on('error', (error: Error) => {
|
|
240
|
+
clearTimeout(timeout);
|
|
241
|
+
reject(error);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Cria o servidor HTTP nativo com configurações de segurança
|
|
247
|
+
const server = http.createServer(async (req: any, res: any) => {
|
|
248
|
+
// Configurações de segurança básicas
|
|
249
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
250
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
251
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
252
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
253
|
+
|
|
254
|
+
// Timeout para requisições
|
|
255
|
+
req.setTimeout(30000, () => {
|
|
256
|
+
res.statusCode = 408; // Request Timeout
|
|
257
|
+
res.end('Request timeout');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Validação básica de URL para prevenir ataques
|
|
262
|
+
const url = req.url || '/';
|
|
263
|
+
if (url.length > 2048) {
|
|
264
|
+
res.statusCode = 414; // URI Too Long
|
|
265
|
+
res.end('URL too long');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Parse do body com proteções
|
|
270
|
+
req.body = await parseBody(req);
|
|
271
|
+
|
|
272
|
+
// Adiciona host se não existir
|
|
273
|
+
req.headers.host = req.headers.host || `localhost:${port}`;
|
|
274
|
+
|
|
275
|
+
// Chama o handler do HightJS
|
|
276
|
+
await handler(req, res);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
Console.error('Erro no servidor nativo:', error);
|
|
279
|
+
|
|
280
|
+
if (!res.headersSent) {
|
|
281
|
+
res.statusCode = 500;
|
|
282
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
283
|
+
|
|
284
|
+
if (error instanceof Error) {
|
|
285
|
+
if (error.message.includes('too large')) {
|
|
286
|
+
res.statusCode = 413; // Payload Too Large
|
|
287
|
+
res.end('Request too large');
|
|
288
|
+
} else if (error.message.includes('timeout')) {
|
|
289
|
+
res.statusCode = 408; // Request Timeout
|
|
290
|
+
res.end('Request timeout');
|
|
291
|
+
} else {
|
|
292
|
+
res.end('Internal server error');
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
res.end('Internal server error');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Configurações de segurança do servidor
|
|
302
|
+
server.setTimeout(35000); // Timeout geral do servidor
|
|
303
|
+
server.maxHeadersCount = 100; // Limita número de headers
|
|
304
|
+
server.headersTimeout = 60000; // Timeout para headers
|
|
305
|
+
server.requestTimeout = 30000; // Timeout para requisições
|
|
306
|
+
|
|
307
|
+
server.listen(port, hostname, () => {
|
|
308
|
+
sendBox({ ...options, port });
|
|
309
|
+
msg.end(` ${Colors.FgGreen}⚡ ${Colors.Reset}${Colors.Bright}Servidor HightJS NATIVO ativo!${Colors.Reset}`);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Configura WebSocket para hot reload
|
|
313
|
+
hwebApp.setupWebSocket(server);
|
|
314
|
+
hwebApp.executeInstrumentation();
|
|
315
|
+
return server;
|
|
316
|
+
}
|
package/src/hotReload.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
import * as chokidar from 'chokidar';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { IncomingMessage } from 'http';
|
|
6
|
+
import * as url from 'url';
|
|
7
|
+
import { clearFileCache } from './router';
|
|
8
|
+
import Console, {Levels} from "./api/console"
|
|
9
|
+
export class HotReloadManager {
|
|
10
|
+
private wss: WebSocketServer | null = null;
|
|
11
|
+
private watchers: chokidar.FSWatcher[] = [];
|
|
12
|
+
private projectDir: string;
|
|
13
|
+
private clients: Set<WebSocket> = new Set();
|
|
14
|
+
private pingInterval: NodeJS.Timeout | null = null;
|
|
15
|
+
private backendApiChangeCallback: (() => void) | null = null;
|
|
16
|
+
private frontendChangeCallback: (() => void) | null = null;
|
|
17
|
+
|
|
18
|
+
constructor(projectDir: string) {
|
|
19
|
+
this.projectDir = projectDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start() {
|
|
23
|
+
// Não cria servidor na porta separada - será integrado ao Express
|
|
24
|
+
this.setupWatchers();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Novo método para integrar com Express
|
|
28
|
+
handleUpgrade(request: IncomingMessage, socket: any, head: Buffer) {
|
|
29
|
+
if (!this.wss) {
|
|
30
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
31
|
+
this.setupWebSocketServer();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
35
|
+
this.wss!.emit('connection', ws, request);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private setupWebSocketServer() {
|
|
40
|
+
if (!this.wss) return;
|
|
41
|
+
|
|
42
|
+
this.wss.on('connection', (ws: WebSocket) => {
|
|
43
|
+
this.clients.add(ws);
|
|
44
|
+
|
|
45
|
+
// Setup ping/pong para manter conexão viva
|
|
46
|
+
const ping = () => {
|
|
47
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
48
|
+
ws.ping();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const pingTimer = setInterval(ping, 30000); // Ping a cada 30 segundos
|
|
53
|
+
|
|
54
|
+
ws.on('pong', () => {
|
|
55
|
+
// Cliente respondeu ao ping - conexão ainda ativa
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ws.on('close', () => {
|
|
59
|
+
this.clients.delete(ws);
|
|
60
|
+
clearInterval(pingTimer);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ws.on('error', () => {
|
|
64
|
+
this.clients.delete(ws);
|
|
65
|
+
clearInterval(pingTimer);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private setupWatchers() {
|
|
71
|
+
// 1. Watcher para arquivos frontend (rotas, componentes) - EXCLUINDO backend
|
|
72
|
+
const frontendWatcher = chokidar.watch([
|
|
73
|
+
path.join(this.projectDir, 'src/web/**/*.{tsx,ts,jsx,js}'),
|
|
74
|
+
], {
|
|
75
|
+
ignored: [
|
|
76
|
+
/(^|[\/\\])\../, // arquivos ocultos
|
|
77
|
+
path.join(this.projectDir, 'src/web/backend/**/*') // exclui toda a pasta backend
|
|
78
|
+
],
|
|
79
|
+
persistent: true,
|
|
80
|
+
ignoreInitial: true
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
frontendWatcher.on('change', async (filePath) => {
|
|
84
|
+
Console.logWithout(Levels.INFO, `🔄 Frontend alterado: ${filePath}`);
|
|
85
|
+
clearFileCache(filePath);
|
|
86
|
+
// Checa build do arquivo alterado
|
|
87
|
+
const result = await this.checkFrontendBuild(filePath);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
this.notifyClients('frontend-error', { file: filePath, error: result.error });
|
|
90
|
+
} else {
|
|
91
|
+
this.frontendChangeCallback?.();
|
|
92
|
+
this.notifyClients('frontend-reload');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
frontendWatcher.on('add', async (filePath) => {
|
|
96
|
+
Console.info(`➕ Novo arquivo frontend: ${path.basename(filePath)}`);
|
|
97
|
+
const result = await this.checkFrontendBuild(filePath);
|
|
98
|
+
if (result.error) {
|
|
99
|
+
this.notifyClients('frontend-error', { file: filePath, error: result.error });
|
|
100
|
+
} else {
|
|
101
|
+
this.frontendChangeCallback?.();
|
|
102
|
+
this.notifyClients('frontend-reload');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
frontendWatcher.on('unlink', (filePath) => {
|
|
106
|
+
Console.info(`🗑️ Arquivo frontend removido: ${path.basename(filePath)}`);
|
|
107
|
+
clearFileCache(filePath);
|
|
108
|
+
this.frontendChangeCallback?.();
|
|
109
|
+
this.notifyClients('frontend-reload');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 2. Watcher específico para rotas de API backend
|
|
113
|
+
const backendApiWatcher = chokidar.watch([
|
|
114
|
+
path.join(this.projectDir, 'src/web/backend/routes/**/*.{ts,tsx,js,jsx}'),
|
|
115
|
+
], {
|
|
116
|
+
ignored: /(^|[\/\\])\../,
|
|
117
|
+
persistent: true,
|
|
118
|
+
ignoreInitial: true
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
backendApiWatcher.on('change', (filePath) => {
|
|
122
|
+
Console.info(`🔄 API backend alterada: ${path.basename(filePath)}`);
|
|
123
|
+
this.clearBackendCache(filePath);
|
|
124
|
+
this.notifyClients('backend-api-reload');
|
|
125
|
+
|
|
126
|
+
// Chama o callback, se definido
|
|
127
|
+
this.backendApiChangeCallback?.();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
backendApiWatcher.on('add', (filePath) => {
|
|
131
|
+
Console.info(`➕ Nova API backend: ${path.basename(filePath)}`);
|
|
132
|
+
this.notifyClients('backend-api-reload');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
backendApiWatcher.on('unlink', (filePath) => {
|
|
136
|
+
Console.info(`🗑️ API backend removida: ${path.basename(filePath)}`);
|
|
137
|
+
this.clearBackendCache(filePath);
|
|
138
|
+
this.notifyClients('backend-api-reload');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 3. Watcher para arquivos backend (server.ts, configs)
|
|
142
|
+
const backendWatcher = chokidar.watch([
|
|
143
|
+
path.join(this.projectDir, 'src/server.ts'),
|
|
144
|
+
path.join(this.projectDir, 'src/**/*.ts'),
|
|
145
|
+
'!**/src/web/**', // exclui pasta web
|
|
146
|
+
], {
|
|
147
|
+
ignored: /(^|[\/\\])\../,
|
|
148
|
+
persistent: true,
|
|
149
|
+
ignoreInitial: true
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
backendWatcher.on('change', () => {
|
|
153
|
+
this.restartServer();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.watchers.push(frontendWatcher, backendApiWatcher, backendWatcher);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private notifyClients(type: string, data?: any) {
|
|
160
|
+
const message = JSON.stringify({ type, data, timestamp: Date.now() });
|
|
161
|
+
|
|
162
|
+
this.clients.forEach((client) => {
|
|
163
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
164
|
+
client.send(message);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private restartServer() {
|
|
170
|
+
// Notifica clientes que o servidor está reiniciando
|
|
171
|
+
this.notifyClients('server-restart');
|
|
172
|
+
|
|
173
|
+
// Aguarda um pouco e tenta reconectar
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
this.notifyClients('server-ready');
|
|
176
|
+
}, 2000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
stop() {
|
|
180
|
+
// Para todos os watchers
|
|
181
|
+
this.watchers.forEach(watcher => watcher.close());
|
|
182
|
+
this.watchers = [];
|
|
183
|
+
|
|
184
|
+
// Para ping interval
|
|
185
|
+
if (this.pingInterval) {
|
|
186
|
+
clearInterval(this.pingInterval);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Fecha WebSocket server
|
|
190
|
+
if (this.wss) {
|
|
191
|
+
this.wss.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Retorna o script do cliente para injetar no HTML
|
|
196
|
+
getClientScript(): string {
|
|
197
|
+
return `
|
|
198
|
+
<script>
|
|
199
|
+
(function() {
|
|
200
|
+
if (typeof window !== 'undefined') {
|
|
201
|
+
let ws;
|
|
202
|
+
let reconnectInterval;
|
|
203
|
+
|
|
204
|
+
function connect() {
|
|
205
|
+
ws = new WebSocket('ws://localhost:3000/hweb-hotreload/');
|
|
206
|
+
|
|
207
|
+
ws.onopen = function() {
|
|
208
|
+
clearInterval(reconnectInterval);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
ws.onmessage = function(event) {
|
|
212
|
+
const message = JSON.parse(event.data);
|
|
213
|
+
|
|
214
|
+
switch(message.type) {
|
|
215
|
+
case 'frontend-reload':
|
|
216
|
+
window.location.reload();
|
|
217
|
+
break;
|
|
218
|
+
case 'server-restart':
|
|
219
|
+
break;
|
|
220
|
+
case 'server-ready':
|
|
221
|
+
setTimeout(() => window.location.reload(), 500);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
ws.onclose = function() {
|
|
227
|
+
reconnectInterval = setInterval(() => {
|
|
228
|
+
connect();
|
|
229
|
+
}, 1000);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
ws.onerror = function() {
|
|
233
|
+
// Silencioso - sem logs
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
connect();
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
240
|
+
</script>
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private clearBackendCache(filePath: string) {
|
|
245
|
+
// Limpa o cache do require para forçar reload da rota de API
|
|
246
|
+
const absolutePath = path.resolve(filePath);
|
|
247
|
+
delete require.cache[absolutePath];
|
|
248
|
+
|
|
249
|
+
// Também limpa dependências relacionadas
|
|
250
|
+
Object.keys(require.cache).forEach(key => {
|
|
251
|
+
if (key.includes(path.dirname(absolutePath))) {
|
|
252
|
+
delete require.cache[key];
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Método para registrar callback de mudança de API backend
|
|
258
|
+
onBackendApiChange(callback: () => void) {
|
|
259
|
+
this.backendApiChangeCallback = callback;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Método para registrar callback de mudança de frontend
|
|
263
|
+
onFrontendChange(callback: () => void) {
|
|
264
|
+
this.frontendChangeCallback = callback;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async checkFrontendBuild(filePath: string) {
|
|
268
|
+
// Usa ts-node para checar erros de compilação do arquivo alterado
|
|
269
|
+
const tsNodePath = require.resolve('ts-node');
|
|
270
|
+
const { spawn } = require('child_process');
|
|
271
|
+
return new Promise<{ error?: string }>((resolve) => {
|
|
272
|
+
const proc = spawn(process.execPath, [tsNodePath, '--transpile-only', filePath], {
|
|
273
|
+
cwd: this.projectDir,
|
|
274
|
+
env: process.env,
|
|
275
|
+
});
|
|
276
|
+
let errorMsg = '';
|
|
277
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
278
|
+
errorMsg += data.toString();
|
|
279
|
+
});
|
|
280
|
+
proc.on('close', (code: number) => {
|
|
281
|
+
if (code !== 0 && errorMsg) {
|
|
282
|
+
resolve({ error: errorMsg });
|
|
283
|
+
} else {
|
|
284
|
+
resolve({});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|