vatts 1.0.1 → 1.0.2-alpha.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.
- package/dist/builder.js +44 -16
- package/dist/client/clientRouter.js +0 -1
- package/dist/client/entry.client.d.ts +6 -1
- package/dist/client/entry.client.js +34 -44
- package/dist/helpers.js +18 -18
- package/dist/hotReload.d.ts +11 -2
- package/dist/hotReload.js +369 -293
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -13
- package/dist/renderer.js +1 -1
- package/dist/router.d.ts +5 -56
- package/dist/router.js +287 -473
- package/dist/types.d.ts +2 -4
- package/package.json +1 -1
package/dist/router.js
CHANGED
|
@@ -39,113 +39,116 @@ const url_1 = require("url");
|
|
|
39
39
|
const console_1 = __importDefault(require("./api/console"));
|
|
40
40
|
const factory_1 = require("./adapters/factory");
|
|
41
41
|
const http_1 = require("./api/http");
|
|
42
|
-
// ---
|
|
43
|
-
// Guarda todas as rotas de PÁGINA (React) encontradas
|
|
44
|
-
// A rota agora também armazena o caminho do arquivo para ser usado como um ID único no cliente
|
|
42
|
+
// --- Estado Global ---
|
|
45
43
|
let allRoutes = [];
|
|
46
|
-
|
|
44
|
+
let allBackendRoutes = [];
|
|
45
|
+
let allWebSocketRoutes = [];
|
|
46
|
+
// Cache de arquivos para Hot Reload
|
|
47
|
+
const loadedFiles = new Set();
|
|
48
|
+
// Componentes Especiais
|
|
47
49
|
let layoutComponent = null;
|
|
48
|
-
// Guarda o componente 404 personalizado se existir
|
|
49
50
|
let notFoundComponent = null;
|
|
50
|
-
//
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
// Conexões ativas
|
|
52
|
+
let wsConnections = new Set();
|
|
53
|
+
// --- Helpers de Regex ---
|
|
54
|
+
// Re-implementação da compilação usando Named Groups para segurança e facilidade (como no original, mas cached)
|
|
55
|
+
function compileRoutePatternWithGroups(pattern) {
|
|
56
|
+
const regexPattern = pattern
|
|
57
|
+
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
|
|
58
|
+
.replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
|
|
59
|
+
.replace(/\/\[\[(\w+)\]\]/g, '(?:/(?<$1>[^/]+))?')
|
|
60
|
+
.replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
|
|
61
|
+
.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
|
|
62
|
+
return new RegExp(`^${regexPattern}/?$`);
|
|
63
|
+
}
|
|
64
|
+
// --- Gerenciamento de Cache ---
|
|
65
|
+
function safeClearCache(filePath) {
|
|
59
66
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// Tenta deletar direto pela chave do caminho absoluto (mais rápido)
|
|
68
|
+
if (require.cache[filePath]) {
|
|
69
|
+
delete require.cache[filePath];
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Fallback: resolve o caminho (tem custo de I/O)
|
|
73
|
+
const resolved = require.resolve(filePath);
|
|
74
|
+
if (require.cache[resolved]) {
|
|
75
|
+
delete require.cache[resolved];
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
|
-
catch {
|
|
70
|
-
//
|
|
78
|
+
catch (e) {
|
|
79
|
+
// Ignora erro se arquivo não for resolvível
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
|
-
// Nota: Suporte apenas para TypeScript (.ts, .tsx). Não tratamos .jsx aqui.
|
|
74
|
-
/**
|
|
75
|
-
* Limpa todo o cache de rotas carregadas
|
|
76
|
-
*/
|
|
77
82
|
function clearAllRouteCache() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
clearRequireCache(filePath);
|
|
81
|
-
});
|
|
82
|
-
loadedRouteFiles.clear();
|
|
83
|
-
// Limpa cache do layout
|
|
84
|
-
loadedLayoutFiles.forEach(filePath => {
|
|
85
|
-
clearRequireCache(filePath);
|
|
86
|
-
});
|
|
87
|
-
loadedLayoutFiles.clear();
|
|
88
|
-
// Limpa cache do notFound
|
|
89
|
-
loadedNotFoundFiles.forEach(filePath => {
|
|
90
|
-
clearRequireCache(filePath);
|
|
91
|
-
});
|
|
92
|
-
loadedNotFoundFiles.clear();
|
|
83
|
+
loadedFiles.forEach(file => safeClearCache(file));
|
|
84
|
+
loadedFiles.clear();
|
|
93
85
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Limpa o cache de um arquivo específico e recarrega as rotas se necessário
|
|
96
|
-
* @param changedFilePath Caminho do arquivo que foi alterado
|
|
97
|
-
*/
|
|
98
86
|
function clearFileCache(changedFilePath) {
|
|
99
|
-
const absolutePath = path_1.default.
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
loadedNotFoundFiles.delete(absolutePath);
|
|
87
|
+
const absolutePath = path_1.default.resolve(changedFilePath);
|
|
88
|
+
// Só tenta limpar se realmente já foi carregado (evita I/O desnecessário)
|
|
89
|
+
if (loadedFiles.has(absolutePath)) {
|
|
90
|
+
safeClearCache(absolutePath);
|
|
91
|
+
loadedFiles.delete(absolutePath);
|
|
92
|
+
}
|
|
106
93
|
}
|
|
107
94
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* @returns O layout carregado ou null se não existir
|
|
95
|
+
* Hook para ignorar importações de CSS/SCSS/SASS durante o require do servidor.
|
|
96
|
+
* Isso evita a necessidade de processar arquivos pesados que não afetam a rota.
|
|
111
97
|
*/
|
|
98
|
+
function requireWithoutStyles(modulePath) {
|
|
99
|
+
const extensions = ['.css', '.scss', '.sass', '.less', '.png', '.jpg', '.jpeg', '.gif', '.svg'];
|
|
100
|
+
const originalHandlers = {};
|
|
101
|
+
// Salva handlers originais e substitui por no-op
|
|
102
|
+
extensions.forEach(ext => {
|
|
103
|
+
originalHandlers[ext] = require.extensions[ext];
|
|
104
|
+
require.extensions[ext] = (m, filename) => {
|
|
105
|
+
// Retorna módulo vazio para imports de estilo/assets
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
m.exports = {};
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
// Não chamamos safeClearCache aqui explicitamente; quem chama loadRoutes já deve ter gerenciado o ciclo
|
|
112
|
+
return require(modulePath);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
// Restaura handlers originais
|
|
116
|
+
extensions.forEach(ext => {
|
|
117
|
+
if (originalHandlers[ext]) {
|
|
118
|
+
require.extensions[ext] = originalHandlers[ext];
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
delete require.extensions[ext];
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// --- Carregamento de Layout (Otimizado - Sem I/O de Disco) ---
|
|
112
127
|
function loadLayout(webDir) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
128
|
+
const extensions = ['layout.tsx', 'layout.ts'];
|
|
129
|
+
let layoutFile = null;
|
|
130
|
+
for (const ext of extensions) {
|
|
131
|
+
const fullPath = path_1.default.join(webDir, ext);
|
|
132
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
133
|
+
layoutFile = fullPath;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
117
137
|
if (layoutFile) {
|
|
118
138
|
const absolutePath = path_1.default.resolve(layoutFile);
|
|
119
139
|
const componentPath = path_1.default.relative(process.cwd(), layoutFile).replace(/\\/g, '/');
|
|
120
140
|
try {
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.replace(/import\s+['"][^'"]*\.css['"];?/g, '// CSS import removido para servidor')
|
|
125
|
-
.replace(/import\s+['"][^'"]*\.scss['"];?/g, '// SCSS import removido para servidor')
|
|
126
|
-
.replace(/import\s+['"][^'"]*\.sass['"];?/g, '// SASS import removido para servidor');
|
|
127
|
-
// Escreve um arquivo temporário .temp.tsx ou .temp.ts sem imports de CSS
|
|
128
|
-
const ext = path_1.default.extname(layoutFile).toLowerCase();
|
|
129
|
-
const tempFile = layoutFile.replace(/\.(tsx|ts)$/i, '.temp.$1');
|
|
130
|
-
fs_1.default.writeFileSync(tempFile, tempContent, 'utf8');
|
|
131
|
-
// Otimização: limpa cache apenas se existir
|
|
132
|
-
try {
|
|
133
|
-
const resolvedPath = require.resolve(tempFile);
|
|
134
|
-
if (require.cache[resolvedPath]) {
|
|
135
|
-
delete require.cache[resolvedPath];
|
|
136
|
-
}
|
|
141
|
+
// Se já carregamos antes, limpa o cache para garantir reload
|
|
142
|
+
if (loadedFiles.has(absolutePath)) {
|
|
143
|
+
safeClearCache(absolutePath);
|
|
137
144
|
}
|
|
138
|
-
|
|
139
|
-
const layoutModule =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const metadata = layoutModule.metadata || null;
|
|
146
|
-
// Registra o arquivo como carregado
|
|
147
|
-
loadedLayoutFiles.add(absolutePath);
|
|
148
|
-
layoutComponent = { componentPath, metadata };
|
|
145
|
+
// OTIMIZAÇÃO: Carrega em memória ignorando CSS
|
|
146
|
+
const layoutModule = requireWithoutStyles(layoutFile);
|
|
147
|
+
loadedFiles.add(absolutePath);
|
|
148
|
+
layoutComponent = {
|
|
149
|
+
componentPath,
|
|
150
|
+
metadata: layoutModule.metadata || null
|
|
151
|
+
};
|
|
149
152
|
return layoutComponent;
|
|
150
153
|
}
|
|
151
154
|
catch (error) {
|
|
@@ -157,322 +160,208 @@ function loadLayout(webDir) {
|
|
|
157
160
|
layoutComponent = null;
|
|
158
161
|
return null;
|
|
159
162
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*/
|
|
163
|
-
function getLayout() {
|
|
164
|
-
return layoutComponent;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Carrega dinamicamente todas as rotas de frontend do diretório do usuário.
|
|
168
|
-
* @param routesDir O diretório onde as rotas de página estão localizadas.
|
|
169
|
-
* @returns A lista de rotas de página que foram carregadas.
|
|
170
|
-
*/
|
|
163
|
+
function getLayout() { return layoutComponent; }
|
|
164
|
+
// --- Carregamento de Rotas Frontend ---
|
|
171
165
|
function loadRoutes(routesDir) {
|
|
172
166
|
if (!fs_1.default.existsSync(routesDir)) {
|
|
173
|
-
console_1.default.warn(`Frontend routes directory not found at ${routesDir}
|
|
167
|
+
console_1.default.warn(`Frontend routes directory not found at ${routesDir}.`);
|
|
174
168
|
allRoutes = [];
|
|
175
|
-
return
|
|
169
|
+
return [];
|
|
176
170
|
}
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
171
|
+
const loaded = [];
|
|
172
|
+
const cwdPath = process.cwd();
|
|
173
|
+
// Função recursiva otimizada
|
|
174
|
+
const scanAndLoad = (dir) => {
|
|
180
175
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
181
176
|
for (const entry of entries) {
|
|
182
|
-
const
|
|
177
|
+
const name = entry.name;
|
|
178
|
+
// Skip arquivos ocultos ou de sistema para acelerar scan
|
|
179
|
+
if (name.startsWith('.') || name.startsWith('_'))
|
|
180
|
+
continue;
|
|
181
|
+
const fullPath = path_1.default.join(dir, name);
|
|
183
182
|
if (entry.isDirectory()) {
|
|
184
|
-
|
|
185
|
-
if (entry.name === 'backend')
|
|
183
|
+
if (name === 'backend')
|
|
186
184
|
continue;
|
|
187
|
-
|
|
185
|
+
scanAndLoad(fullPath);
|
|
188
186
|
}
|
|
189
|
-
else if (entry.isFile()) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
else if (entry.isFile() && (name.endsWith('.tsx') || name.endsWith('.ts'))) {
|
|
188
|
+
try {
|
|
189
|
+
const absolutePath = path_1.default.resolve(fullPath);
|
|
190
|
+
// OTIMIZAÇÃO CRÍTICA: Só limpa cache se o arquivo já foi carregado antes.
|
|
191
|
+
// Isso evita chamadas caras de require.resolve() na inicialização do servidor.
|
|
192
|
+
if (loadedFiles.has(absolutePath)) {
|
|
193
|
+
safeClearCache(absolutePath);
|
|
194
|
+
}
|
|
195
|
+
// OTIMIZAÇÃO: Usa requireWithoutStyles também para rotas!
|
|
196
|
+
// Isso evita que o Node tente processar imports de CSS/Assets dentro das páginas durante o roteamento.
|
|
197
|
+
const routeModule = requireWithoutStyles(absolutePath);
|
|
198
|
+
const defaultConfig = routeModule.default;
|
|
199
|
+
if (defaultConfig?.pattern && defaultConfig?.component) {
|
|
200
|
+
const componentPath = path_1.default.relative(cwdPath, fullPath).replace(/\\/g, '/');
|
|
201
|
+
// OTIMIZAÇÃO: Pré-compila a regex aqui
|
|
202
|
+
const regex = compileRoutePatternWithGroups(defaultConfig.pattern);
|
|
203
|
+
loaded.push({
|
|
204
|
+
config: defaultConfig,
|
|
205
|
+
componentPath,
|
|
206
|
+
regex,
|
|
207
|
+
paramNames: [] // Usando named groups na regex, não precisamos mapear manual
|
|
208
|
+
});
|
|
209
|
+
loadedFiles.add(absolutePath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console_1.default.error(`Error loading route ${fullPath}:`, error);
|
|
193
214
|
}
|
|
194
215
|
}
|
|
195
216
|
}
|
|
196
217
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const cwdPath = process.cwd();
|
|
200
|
-
// Otimização: processa arquivos em lote
|
|
201
|
-
for (const file of routeFiles) {
|
|
202
|
-
const filePath = path_1.default.join(routesDir, file);
|
|
203
|
-
const absolutePath = path_1.default.resolve(filePath);
|
|
204
|
-
try {
|
|
205
|
-
// Otimização: limpa cache apenas se já existir
|
|
206
|
-
const resolvedPath = require.resolve(filePath);
|
|
207
|
-
if (require.cache[resolvedPath]) {
|
|
208
|
-
delete require.cache[resolvedPath];
|
|
209
|
-
}
|
|
210
|
-
const routeModule = require(filePath);
|
|
211
|
-
if (routeModule.default?.pattern && routeModule.default?.component) {
|
|
212
|
-
// Otimização: calcula componentPath apenas uma vez
|
|
213
|
-
const componentPath = path_1.default.relative(cwdPath, filePath).replace(/\\/g, '/');
|
|
214
|
-
loaded.push({ ...routeModule.default, componentPath });
|
|
215
|
-
loadedRouteFiles.add(absolutePath);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
catch (error) {
|
|
219
|
-
console_1.default.error(`Error loading page route ${filePath}:`, error);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
218
|
+
scanAndLoad(routesDir);
|
|
219
|
+
// Atualiza variável global e retorna formato esperado pelo sistema (sem a regex exposta para não quebrar tipagem antiga se for estrita)
|
|
222
220
|
allRoutes = loaded;
|
|
223
|
-
return allRoutes;
|
|
221
|
+
return allRoutes.map(r => ({ ...r.config, componentPath: r.componentPath }));
|
|
224
222
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Encontra a rota de página correspondente para uma URL.
|
|
227
|
-
* @param pathname O caminho da URL (ex: "/users/123").
|
|
228
|
-
* @returns Um objeto com a rota e os parâmetros, ou null se não encontrar.
|
|
229
|
-
*/
|
|
230
223
|
function findMatchingRoute(pathname) {
|
|
224
|
+
// OTIMIZAÇÃO: Loop usando regex pré-compilada. Muito mais rápido.
|
|
231
225
|
for (const route of allRoutes) {
|
|
232
|
-
|
|
233
|
-
continue;
|
|
234
|
-
const regexPattern = route.pattern
|
|
235
|
-
// [[...param]] → opcional catch-all
|
|
236
|
-
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
|
|
237
|
-
// [...param] → obrigatório catch-all
|
|
238
|
-
.replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
|
|
239
|
-
// /[[param]] → opcional com barra também opcional
|
|
240
|
-
.replace(/\/\[\[(\w+)\]\]/g, '(?:/(?<$1>[^/]+))?')
|
|
241
|
-
// [[param]] → segmento opcional (sem barra anterior)
|
|
242
|
-
.replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
|
|
243
|
-
// [param] → segmento obrigatório
|
|
244
|
-
.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
|
|
245
|
-
// permite / opcional no final
|
|
246
|
-
const regex = new RegExp(`^${regexPattern}/?$`);
|
|
247
|
-
const match = pathname.match(regex);
|
|
226
|
+
const match = pathname.match(route.regex);
|
|
248
227
|
if (match) {
|
|
249
228
|
return {
|
|
250
|
-
route,
|
|
229
|
+
route: { ...route.config, componentPath: route.componentPath },
|
|
251
230
|
params: match.groups || {}
|
|
252
231
|
};
|
|
253
232
|
}
|
|
254
233
|
}
|
|
255
234
|
return null;
|
|
256
235
|
}
|
|
257
|
-
// ---
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
// Procura por exports nomeados que sejam funções
|
|
288
|
-
Object.keys(middlewareModule).forEach(key => {
|
|
289
|
-
if (key !== 'default' && typeof middlewareModule[key] === 'function') {
|
|
290
|
-
middlewares.push(middlewareModule[key]);
|
|
236
|
+
// --- Carregamento de Rotas Backend ---
|
|
237
|
+
// Cache de middlewares simples
|
|
238
|
+
const middlewareCache = new Map();
|
|
239
|
+
function getMiddlewaresForDir(dir) {
|
|
240
|
+
if (middlewareCache.has(dir))
|
|
241
|
+
return middlewareCache.get(dir);
|
|
242
|
+
const files = ['middleware.ts', 'middleware.tsx'];
|
|
243
|
+
let middlewares = [];
|
|
244
|
+
for (const file of files) {
|
|
245
|
+
const fullPath = path_1.default.join(dir, file);
|
|
246
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
247
|
+
try {
|
|
248
|
+
const absolutePath = path_1.default.resolve(fullPath);
|
|
249
|
+
if (loadedFiles.has(absolutePath)) {
|
|
250
|
+
safeClearCache(absolutePath);
|
|
251
|
+
}
|
|
252
|
+
// Middlewares backend não usam requireWithoutStyles pois não devem ter CSS,
|
|
253
|
+
// e podem precisar de outros módulos backend.
|
|
254
|
+
const mod = require(fullPath);
|
|
255
|
+
loadedFiles.add(absolutePath);
|
|
256
|
+
// Extração robusta de exports
|
|
257
|
+
if (typeof mod.default === 'function')
|
|
258
|
+
middlewares.push(mod.default);
|
|
259
|
+
else if (Array.isArray(mod.default))
|
|
260
|
+
middlewares.push(...mod.default);
|
|
261
|
+
Object.keys(mod).forEach(key => {
|
|
262
|
+
if (key !== 'default' && typeof mod[key] === 'function') {
|
|
263
|
+
middlewares.push(mod[key]);
|
|
291
264
|
}
|
|
292
265
|
});
|
|
266
|
+
// Só processa o primeiro arquivo de middleware encontrado na pasta
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
console_1.default.error(`Error loading middleware ${fullPath}`, e);
|
|
293
271
|
}
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
console_1.default.error(`Error loading middleware ${middlewareFile}:`, error);
|
|
297
272
|
}
|
|
298
273
|
}
|
|
274
|
+
middlewareCache.set(dir, middlewares);
|
|
299
275
|
return middlewares;
|
|
300
276
|
}
|
|
301
|
-
/**
|
|
302
|
-
* Coleta middlewares do diretório específico da rota (não herda dos pais)
|
|
303
|
-
* @param routeFilePath Caminho completo do arquivo de rota
|
|
304
|
-
* @param backendRoutesDir Diretório raiz das rotas de backend
|
|
305
|
-
* @returns Array com middlewares apenas do diretório da rota
|
|
306
|
-
*/
|
|
307
|
-
function collectMiddlewaresForRoute(routeFilePath, backendRoutesDir) {
|
|
308
|
-
const relativePath = path_1.default.relative(backendRoutesDir, routeFilePath);
|
|
309
|
-
const routeDir = path_1.default.dirname(path_1.default.join(backendRoutesDir, relativePath));
|
|
310
|
-
// Carrega middlewares APENAS do diretório específico da rota (não herda dos pais)
|
|
311
|
-
if (!loadedMiddlewares.has(routeDir)) {
|
|
312
|
-
const middlewares = loadMiddlewareFromDirectory(routeDir);
|
|
313
|
-
loadedMiddlewares.set(routeDir, middlewares);
|
|
314
|
-
}
|
|
315
|
-
return loadedMiddlewares.get(routeDir) || [];
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Carrega dinamicamente todas as rotas de API do diretório de backend.
|
|
319
|
-
* @param backendRoutesDir O diretório onde as rotas de API estão localizadas.
|
|
320
|
-
*/
|
|
321
277
|
function loadBackendRoutes(backendRoutesDir) {
|
|
322
278
|
if (!fs_1.default.existsSync(backendRoutesDir)) {
|
|
323
|
-
// É opcional ter uma API, então não mostramos um aviso se a pasta não existir.
|
|
324
279
|
allBackendRoutes = [];
|
|
325
280
|
return;
|
|
326
281
|
}
|
|
327
|
-
// Limpa cache de
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const routeFiles = [];
|
|
331
|
-
const middlewareFiles = new Map(); // dir -> filepath
|
|
332
|
-
const scanDirectory = (dir, baseDir = '') => {
|
|
282
|
+
middlewareCache.clear(); // Limpa cache de middleware ao recarregar rotas
|
|
283
|
+
const loaded = [];
|
|
284
|
+
const scanAndLoadAPI = (dir) => {
|
|
333
285
|
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
286
|
+
// Primeiro carrega middleware deste diretório para cachear
|
|
287
|
+
getMiddlewaresForDir(dir);
|
|
334
288
|
for (const entry of entries) {
|
|
335
|
-
const
|
|
289
|
+
const name = entry.name;
|
|
290
|
+
if (name.startsWith('.') || name.startsWith('_'))
|
|
291
|
+
continue;
|
|
292
|
+
const fullPath = path_1.default.join(dir, name);
|
|
336
293
|
if (entry.isDirectory()) {
|
|
337
|
-
|
|
294
|
+
scanAndLoadAPI(fullPath);
|
|
338
295
|
}
|
|
339
|
-
else if (entry.isFile()) {
|
|
340
|
-
|
|
341
|
-
if (!isSupported)
|
|
296
|
+
else if (entry.isFile() && (name.endsWith('.ts') || name.endsWith('.tsx'))) {
|
|
297
|
+
if (name.startsWith('middleware'))
|
|
342
298
|
continue;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
routeFiles.push(relativePath);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
scanDirectory(backendRoutesDir);
|
|
355
|
-
// Otimização: pré-carrega todos os middlewares em um único passe
|
|
356
|
-
for (const [dirPath, middlewarePath] of middlewareFiles) {
|
|
357
|
-
try {
|
|
358
|
-
const resolvedPath = require.resolve(middlewarePath);
|
|
359
|
-
if (require.cache[resolvedPath]) {
|
|
360
|
-
delete require.cache[resolvedPath];
|
|
361
|
-
}
|
|
362
|
-
const middlewareModule = require(middlewarePath);
|
|
363
|
-
const middlewares = [];
|
|
364
|
-
if (typeof middlewareModule.default === 'function') {
|
|
365
|
-
middlewares.push(middlewareModule.default);
|
|
366
|
-
}
|
|
367
|
-
else if (Array.isArray(middlewareModule.default)) {
|
|
368
|
-
middlewares.push(...middlewareModule.default);
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
// Exports nomeados
|
|
372
|
-
for (const key in middlewareModule) {
|
|
373
|
-
if (key !== 'default' && typeof middlewareModule[key] === 'function') {
|
|
374
|
-
middlewares.push(middlewareModule[key]);
|
|
299
|
+
try {
|
|
300
|
+
const absolutePath = path_1.default.resolve(fullPath);
|
|
301
|
+
if (loadedFiles.has(absolutePath)) {
|
|
302
|
+
safeClearCache(absolutePath);
|
|
375
303
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (require.cache[resolvedPath]) {
|
|
394
|
-
delete require.cache[resolvedPath];
|
|
395
|
-
}
|
|
396
|
-
const routeModule = require(filePath);
|
|
397
|
-
if (routeModule.default?.pattern) {
|
|
398
|
-
const routeConfig = { ...routeModule.default };
|
|
399
|
-
// Se a rota NÃO tem middleware definido, usa os da pasta
|
|
400
|
-
if (!routeConfig.hasOwnProperty('middleware')) {
|
|
401
|
-
const routeDir = path_1.default.dirname(path_1.default.resolve(filePath));
|
|
402
|
-
const folderMiddlewares = loadedMiddlewares.get(routeDir);
|
|
403
|
-
if (folderMiddlewares && folderMiddlewares.length > 0) {
|
|
404
|
-
routeConfig.middleware = folderMiddlewares;
|
|
304
|
+
const mod = require(fullPath);
|
|
305
|
+
loadedFiles.add(absolutePath);
|
|
306
|
+
const config = mod.default;
|
|
307
|
+
if (config?.pattern) {
|
|
308
|
+
// Aplica middleware automaticamente se não definido
|
|
309
|
+
if (!config.middleware) {
|
|
310
|
+
const dirMiddlewares = getMiddlewaresForDir(dir);
|
|
311
|
+
if (dirMiddlewares.length > 0) {
|
|
312
|
+
config.middleware = dirMiddlewares;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// OTIMIZAÇÃO: Pré-compila regex
|
|
316
|
+
loaded.push({
|
|
317
|
+
config,
|
|
318
|
+
regex: compileRoutePatternWithGroups(config.pattern),
|
|
319
|
+
paramNames: []
|
|
320
|
+
});
|
|
405
321
|
}
|
|
406
322
|
}
|
|
407
|
-
|
|
323
|
+
catch (e) {
|
|
324
|
+
console_1.default.error(`Error loading API route ${fullPath}`, e);
|
|
325
|
+
}
|
|
408
326
|
}
|
|
409
327
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
}
|
|
328
|
+
};
|
|
329
|
+
scanAndLoadAPI(backendRoutesDir);
|
|
414
330
|
allBackendRoutes = loaded;
|
|
331
|
+
// Processa WebSockets após carregar rotas HTTP
|
|
332
|
+
processWebSocketRoutes();
|
|
415
333
|
}
|
|
416
|
-
/**
|
|
417
|
-
* Encontra a rota de API correspondente para uma URL e método HTTP.
|
|
418
|
-
* @param pathname O caminho da URL (ex: "/api/users/123").
|
|
419
|
-
* @param method O método HTTP da requisição (GET, POST, etc.).
|
|
420
|
-
* @returns Um objeto com a rota e os parâmetros, ou null se não encontrar.
|
|
421
|
-
*/
|
|
422
334
|
function findMatchingBackendRoute(pathname, method) {
|
|
335
|
+
const methodUpper = method.toUpperCase();
|
|
423
336
|
for (const route of allBackendRoutes) {
|
|
424
|
-
// Verifica
|
|
425
|
-
|
|
337
|
+
// Verifica método antes de rodar regex (otimização barata)
|
|
338
|
+
// @ts-ignore
|
|
339
|
+
if (!route.config[methodUpper])
|
|
426
340
|
continue;
|
|
427
|
-
const
|
|
428
|
-
// [[...param]] → opcional catch-all
|
|
429
|
-
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
|
|
430
|
-
// [...param] → obrigatório catch-all
|
|
431
|
-
.replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
|
|
432
|
-
// [[param]] → segmento opcional
|
|
433
|
-
.replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
|
|
434
|
-
// [param] → segmento obrigatório
|
|
435
|
-
.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
|
|
436
|
-
const regex = new RegExp(`^${regexPattern}/?$`);
|
|
437
|
-
const match = pathname.match(regex);
|
|
341
|
+
const match = pathname.match(route.regex);
|
|
438
342
|
if (match) {
|
|
439
343
|
return {
|
|
440
|
-
route,
|
|
344
|
+
route: route.config,
|
|
441
345
|
params: match.groups || {}
|
|
442
346
|
};
|
|
443
347
|
}
|
|
444
348
|
}
|
|
445
349
|
return null;
|
|
446
350
|
}
|
|
447
|
-
|
|
448
|
-
* Carrega o notFound.tsx se existir no diretório web
|
|
449
|
-
* @param webDir O diretório web onde procurar o notFound
|
|
450
|
-
* @returns O notFound carregado ou null se não existir
|
|
451
|
-
*/
|
|
351
|
+
// --- 404 Not Found ---
|
|
452
352
|
function loadNotFound(webDir) {
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
fs_1.default.existsSync(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// Otimização: limpa cache apenas se existir
|
|
462
|
-
try {
|
|
463
|
-
const resolvedPath = require.resolve(notFoundFile);
|
|
464
|
-
if (require.cache[resolvedPath]) {
|
|
465
|
-
delete require.cache[resolvedPath];
|
|
466
|
-
}
|
|
353
|
+
const files = ['notFound.tsx', 'notFound.ts'];
|
|
354
|
+
for (const file of files) {
|
|
355
|
+
const fullPath = path_1.default.join(webDir, file);
|
|
356
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
357
|
+
const absolutePath = path_1.default.resolve(fullPath);
|
|
358
|
+
const componentPath = path_1.default.relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
359
|
+
if (loadedFiles.has(absolutePath)) {
|
|
360
|
+
safeClearCache(absolutePath);
|
|
467
361
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
notFoundComponent = { componentPath };
|
|
472
|
-
return notFoundComponent;
|
|
473
|
-
}
|
|
474
|
-
catch (error) {
|
|
475
|
-
console_1.default.error(`Error loading notFound ${notFoundFile}:`, error);
|
|
362
|
+
// Também usa requireWithoutStyles para o 404
|
|
363
|
+
requireWithoutStyles(absolutePath);
|
|
364
|
+
loadedFiles.add(absolutePath);
|
|
476
365
|
notFoundComponent = { componentPath };
|
|
477
366
|
return notFoundComponent;
|
|
478
367
|
}
|
|
@@ -480,180 +369,105 @@ function loadNotFound(webDir) {
|
|
|
480
369
|
notFoundComponent = null;
|
|
481
370
|
return null;
|
|
482
371
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
*/
|
|
486
|
-
function getNotFound() {
|
|
487
|
-
return notFoundComponent;
|
|
488
|
-
}
|
|
489
|
-
// --- WebSocket Functions ---
|
|
490
|
-
// Guarda todas as rotas WebSocket encontradas
|
|
491
|
-
let allWebSocketRoutes = [];
|
|
492
|
-
// Conexões WebSocket ativas
|
|
493
|
-
let wsConnections = new Set();
|
|
494
|
-
/**
|
|
495
|
-
* Processa e registra rotas WebSocket encontradas nas rotas backend
|
|
496
|
-
*/
|
|
372
|
+
function getNotFound() { return notFoundComponent; }
|
|
373
|
+
// --- WebSocket (Mantendo lógica, otimizando lookup) ---
|
|
497
374
|
function processWebSocketRoutes() {
|
|
498
|
-
allWebSocketRoutes =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
allWebSocketRoutes.push(wsRoute);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
375
|
+
allWebSocketRoutes = allBackendRoutes
|
|
376
|
+
.filter(r => r.config.WS)
|
|
377
|
+
.map(r => ({
|
|
378
|
+
config: r.config,
|
|
379
|
+
regex: r.regex, // Reusa a regex já compilada da rota HTTP!
|
|
380
|
+
handler: r.config.WS,
|
|
381
|
+
middleware: r.config.middleware
|
|
382
|
+
}));
|
|
509
383
|
}
|
|
510
|
-
/**
|
|
511
|
-
* Encontra a rota WebSocket correspondente para uma URL
|
|
512
|
-
*/
|
|
513
384
|
function findMatchingWebSocketRoute(pathname) {
|
|
514
|
-
for (const
|
|
515
|
-
|
|
516
|
-
continue;
|
|
517
|
-
const regexPattern = route.pattern
|
|
518
|
-
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
|
|
519
|
-
.replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
|
|
520
|
-
.replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
|
|
521
|
-
.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
|
|
522
|
-
const regex = new RegExp(`^${regexPattern}/?$`);
|
|
523
|
-
const match = pathname.match(regex);
|
|
385
|
+
for (const wsRoute of allWebSocketRoutes) {
|
|
386
|
+
const match = pathname.match(wsRoute.regex);
|
|
524
387
|
if (match) {
|
|
525
388
|
return {
|
|
526
|
-
route
|
|
389
|
+
route: {
|
|
390
|
+
pattern: wsRoute.config.pattern,
|
|
391
|
+
handler: wsRoute.handler,
|
|
392
|
+
middleware: wsRoute.middleware
|
|
393
|
+
},
|
|
527
394
|
params: match.groups || {}
|
|
528
395
|
};
|
|
529
396
|
}
|
|
530
397
|
}
|
|
531
398
|
return null;
|
|
532
399
|
}
|
|
533
|
-
/**
|
|
534
|
-
* Trata uma nova conexão WebSocket
|
|
535
|
-
*/
|
|
536
400
|
function handleWebSocketConnection(ws, req, hwebReq) {
|
|
537
401
|
if (!req.url)
|
|
538
402
|
return;
|
|
539
403
|
const url = new url_1.URL(req.url, `http://${req.headers.host}`);
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
if (!matchedRoute) {
|
|
404
|
+
const match = findMatchingWebSocketRoute(url.pathname);
|
|
405
|
+
if (!match) {
|
|
543
406
|
ws.close(1000, 'Route not found');
|
|
544
407
|
return;
|
|
545
408
|
}
|
|
546
|
-
const params = extractWebSocketParams(pathname, matchedRoute.route.pattern);
|
|
547
|
-
const query = Object.fromEntries(url.searchParams.entries());
|
|
548
409
|
const context = {
|
|
549
410
|
vattsReq: hwebReq,
|
|
550
411
|
ws,
|
|
551
412
|
req,
|
|
552
413
|
url,
|
|
553
|
-
params,
|
|
554
|
-
query,
|
|
414
|
+
params: match.params,
|
|
415
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
555
416
|
send: (data) => {
|
|
556
417
|
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
557
|
-
|
|
558
|
-
ws.send(message);
|
|
418
|
+
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
|
559
419
|
}
|
|
560
420
|
},
|
|
561
|
-
close: (code, reason) =>
|
|
562
|
-
ws.close(code || 1000, reason);
|
|
563
|
-
},
|
|
421
|
+
close: (code, reason) => ws.close(code || 1000, reason),
|
|
564
422
|
broadcast: (data, exclude) => {
|
|
565
|
-
const
|
|
423
|
+
const msg = typeof data === 'string' ? data : JSON.stringify(data);
|
|
566
424
|
const excludeSet = new Set(exclude || []);
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
|
|
425
|
+
for (const conn of wsConnections) {
|
|
426
|
+
if (conn.readyState === ws_1.WebSocket.OPEN && !excludeSet.has(conn)) {
|
|
427
|
+
conn.send(msg);
|
|
570
428
|
}
|
|
571
|
-
}
|
|
429
|
+
}
|
|
572
430
|
}
|
|
573
431
|
};
|
|
574
432
|
try {
|
|
575
|
-
|
|
433
|
+
match.route.handler(context);
|
|
576
434
|
}
|
|
577
435
|
catch (error) {
|
|
578
436
|
console.error('Error in WebSocket handler:', error);
|
|
579
437
|
ws.close(1011, 'Internal server error');
|
|
580
438
|
}
|
|
581
439
|
}
|
|
582
|
-
/**
|
|
583
|
-
* Extrai parâmetros da URL para WebSocket
|
|
584
|
-
*/
|
|
585
|
-
function extractWebSocketParams(pathname, pattern) {
|
|
586
|
-
const params = {};
|
|
587
|
-
const regexPattern = pattern
|
|
588
|
-
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
|
|
589
|
-
.replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
|
|
590
|
-
.replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
|
|
591
|
-
.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
|
|
592
|
-
const regex = new RegExp(`^${regexPattern}/?$`);
|
|
593
|
-
const match = pathname.match(regex);
|
|
594
|
-
if (match && match.groups) {
|
|
595
|
-
Object.assign(params, match.groups);
|
|
596
|
-
}
|
|
597
|
-
return params;
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Configura WebSocket upgrade no servidor HTTP existente
|
|
601
|
-
* @param server Servidor HTTP (Express, Fastify ou Native)
|
|
602
|
-
* @param hotReloadManager Instância do gerenciador de hot-reload para coordenação
|
|
603
|
-
*/
|
|
604
440
|
function setupWebSocketUpgrade(server, hotReloadManager) {
|
|
605
|
-
|
|
606
|
-
// Em vez disso, coordena com o sistema existente
|
|
607
|
-
// Verifica se já existe um listener de upgrade
|
|
608
|
-
const existingListeners = server.listeners('upgrade');
|
|
609
|
-
// Se não há listeners, ou se o hot-reload ainda não foi configurado, adiciona o nosso
|
|
610
|
-
if (existingListeners.length === 0) {
|
|
611
|
-
server.on('upgrade', (request, socket, head) => {
|
|
612
|
-
handleWebSocketUpgrade(request, socket, head, hotReloadManager);
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
function handleWebSocketUpgrade(request, socket, head, hotReloadManager) {
|
|
617
|
-
const adapter = factory_1.FrameworkAdapterFactory.getCurrentAdapter();
|
|
618
|
-
if (!adapter) {
|
|
619
|
-
console.error('❌ Framework adapter not detected. Unable to process WebSocket upgrade.');
|
|
620
|
-
socket.destroy();
|
|
441
|
+
if (server.listeners('upgrade').length > 0)
|
|
621
442
|
return;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const { pathname } = new url_1.URL(request.url, `http://${request.headers.host}`);
|
|
626
|
-
// Prioridade 1: Hot reload (sistema interno)
|
|
627
|
-
if (pathname === '/hweb-hotreload/') {
|
|
628
|
-
if (hotReloadManager) {
|
|
629
|
-
hotReloadManager.handleUpgrade(request, socket, head);
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
443
|
+
server.on('upgrade', (request, socket, head) => {
|
|
444
|
+
const adapter = factory_1.FrameworkAdapterFactory.getCurrentAdapter();
|
|
445
|
+
if (!adapter) {
|
|
632
446
|
socket.destroy();
|
|
447
|
+
return;
|
|
633
448
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
wsConnections.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
449
|
+
const { pathname } = new url_1.URL(request.url, `http://${request.headers.host}`);
|
|
450
|
+
// Prioridade 1: Hot Reload
|
|
451
|
+
if (pathname === '/hweb-hotreload/') {
|
|
452
|
+
if (hotReloadManager)
|
|
453
|
+
hotReloadManager.handleUpgrade(request, socket, head);
|
|
454
|
+
else
|
|
455
|
+
socket.destroy();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Prioridade 2: Rotas App
|
|
459
|
+
const match = findMatchingWebSocketRoute(pathname);
|
|
460
|
+
if (match) {
|
|
461
|
+
const wss = new ws_1.WebSocketServer({ noServer: true, perMessageDeflate: false, maxPayload: 1024 * 1024 });
|
|
462
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
463
|
+
wsConnections.add(ws);
|
|
464
|
+
ws.on('close', () => wsConnections.delete(ws));
|
|
465
|
+
ws.on('error', () => wsConnections.delete(ws));
|
|
466
|
+
const hwebReq = new http_1.VattsRequest(adapter.parseRequest(request));
|
|
467
|
+
handleWebSocketConnection(ws, request, hwebReq);
|
|
652
468
|
});
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
socket.destroy();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
socket.destroy();
|
|
472
|
+
});
|
|
659
473
|
}
|