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.
Files changed (131) hide show
  1. package/.idea/HightJS.iml +9 -0
  2. package/.idea/copilot.data.migration.agent.xml +6 -0
  3. package/.idea/copilot.data.migration.ask.xml +6 -0
  4. package/.idea/copilot.data.migration.ask2agent.xml +6 -0
  5. package/.idea/copilot.data.migration.edit.xml +6 -0
  6. package/.idea/inspectionProfiles/Project_Default.xml +13 -0
  7. package/.idea/libraries/test_package.xml +9 -0
  8. package/.idea/libraries/ts_commonjs_default_export.xml +9 -0
  9. package/.idea/misc.xml +7 -0
  10. package/.idea/modules.xml +8 -0
  11. package/.idea/vcs.xml +6 -0
  12. package/LICENSE +13 -0
  13. package/README.md +508 -0
  14. package/dist/adapters/express.d.ts +7 -0
  15. package/dist/adapters/express.js +63 -0
  16. package/dist/adapters/factory.d.ts +23 -0
  17. package/dist/adapters/factory.js +122 -0
  18. package/dist/adapters/fastify.d.ts +25 -0
  19. package/dist/adapters/fastify.js +61 -0
  20. package/dist/adapters/native.d.ts +8 -0
  21. package/dist/adapters/native.js +203 -0
  22. package/dist/adapters/starters/express.d.ts +0 -0
  23. package/dist/adapters/starters/express.js +1 -0
  24. package/dist/adapters/starters/factory.d.ts +0 -0
  25. package/dist/adapters/starters/factory.js +1 -0
  26. package/dist/adapters/starters/fastify.d.ts +0 -0
  27. package/dist/adapters/starters/fastify.js +1 -0
  28. package/dist/adapters/starters/index.d.ts +0 -0
  29. package/dist/adapters/starters/index.js +1 -0
  30. package/dist/adapters/starters/native.d.ts +0 -0
  31. package/dist/adapters/starters/native.js +1 -0
  32. package/dist/api/console.d.ts +92 -0
  33. package/dist/api/console.js +276 -0
  34. package/dist/api/http.d.ts +180 -0
  35. package/dist/api/http.js +467 -0
  36. package/dist/auth/client.d.ts +14 -0
  37. package/dist/auth/client.js +68 -0
  38. package/dist/auth/components.d.ts +29 -0
  39. package/dist/auth/components.js +84 -0
  40. package/dist/auth/core.d.ts +38 -0
  41. package/dist/auth/core.js +124 -0
  42. package/dist/auth/index.d.ts +7 -0
  43. package/dist/auth/index.js +27 -0
  44. package/dist/auth/jwt.d.ts +41 -0
  45. package/dist/auth/jwt.js +169 -0
  46. package/dist/auth/providers.d.ts +5 -0
  47. package/dist/auth/providers.js +14 -0
  48. package/dist/auth/react/index.d.ts +6 -0
  49. package/dist/auth/react/index.js +32 -0
  50. package/dist/auth/react.d.ts +22 -0
  51. package/dist/auth/react.js +175 -0
  52. package/dist/auth/routes.d.ts +16 -0
  53. package/dist/auth/routes.js +104 -0
  54. package/dist/auth/types.d.ts +62 -0
  55. package/dist/auth/types.js +2 -0
  56. package/dist/bin/hightjs.d.ts +2 -0
  57. package/dist/bin/hightjs.js +35 -0
  58. package/dist/builder.d.ts +32 -0
  59. package/dist/builder.js +341 -0
  60. package/dist/client/DefaultNotFound.d.ts +1 -0
  61. package/dist/client/DefaultNotFound.js +53 -0
  62. package/dist/client/ErrorBoundary.d.ts +16 -0
  63. package/dist/client/ErrorBoundary.js +181 -0
  64. package/dist/client/clientRouter.d.ts +58 -0
  65. package/dist/client/clientRouter.js +116 -0
  66. package/dist/client/entry.client.d.ts +1 -0
  67. package/dist/client/entry.client.js +271 -0
  68. package/dist/client/routerContext.d.ts +26 -0
  69. package/dist/client/routerContext.js +62 -0
  70. package/dist/client.d.ts +3 -0
  71. package/dist/client.js +8 -0
  72. package/dist/components/Link.d.ts +7 -0
  73. package/dist/components/Link.js +13 -0
  74. package/dist/eslint/index.d.ts +32 -0
  75. package/dist/eslint/index.js +15 -0
  76. package/dist/eslint/use-client-rule.d.ts +19 -0
  77. package/dist/eslint/use-client-rule.js +99 -0
  78. package/dist/eslintSetup.d.ts +0 -0
  79. package/dist/eslintSetup.js +1 -0
  80. package/dist/example/src/web/routes/index.d.ts +3 -0
  81. package/dist/example/src/web/routes/index.js +15 -0
  82. package/dist/helpers.d.ts +18 -0
  83. package/dist/helpers.js +318 -0
  84. package/dist/hotReload.d.ts +23 -0
  85. package/dist/hotReload.js +292 -0
  86. package/dist/index.d.ts +17 -0
  87. package/dist/index.js +480 -0
  88. package/dist/renderer.d.ts +14 -0
  89. package/dist/renderer.js +106 -0
  90. package/dist/router.d.ts +78 -0
  91. package/dist/router.js +359 -0
  92. package/dist/types/framework.d.ts +37 -0
  93. package/dist/types/framework.js +2 -0
  94. package/dist/types.d.ts +43 -0
  95. package/dist/types.js +2 -0
  96. package/dist/typescript/use-client-plugin.d.ts +5 -0
  97. package/dist/typescript/use-client-plugin.js +113 -0
  98. package/dist/validation.d.ts +0 -0
  99. package/dist/validation.js +1 -0
  100. package/package.json +72 -0
  101. package/src/adapters/express.ts +70 -0
  102. package/src/adapters/factory.ts +96 -0
  103. package/src/adapters/fastify.ts +88 -0
  104. package/src/adapters/native.ts +223 -0
  105. package/src/api/console.ts +285 -0
  106. package/src/api/http.ts +515 -0
  107. package/src/auth/client.ts +74 -0
  108. package/src/auth/components.tsx +109 -0
  109. package/src/auth/core.ts +143 -0
  110. package/src/auth/index.ts +9 -0
  111. package/src/auth/jwt.ts +194 -0
  112. package/src/auth/providers.ts +13 -0
  113. package/src/auth/react/index.ts +9 -0
  114. package/src/auth/react.tsx +209 -0
  115. package/src/auth/routes.ts +133 -0
  116. package/src/auth/types.ts +73 -0
  117. package/src/bin/hightjs.js +40 -0
  118. package/src/builder.js +362 -0
  119. package/src/client/DefaultNotFound.tsx +68 -0
  120. package/src/client/clientRouter.ts +137 -0
  121. package/src/client/entry.client.tsx +302 -0
  122. package/src/client.ts +8 -0
  123. package/src/components/Link.tsx +22 -0
  124. package/src/helpers.ts +316 -0
  125. package/src/hotReload.ts +289 -0
  126. package/src/index.ts +514 -0
  127. package/src/renderer.tsx +122 -0
  128. package/src/router.ts +400 -0
  129. package/src/types/framework.ts +42 -0
  130. package/src/types.ts +54 -0
  131. 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
+ }
@@ -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
+ }