vatts 1.0.2-alpha.2 → 1.0.2-alpha.4

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.
@@ -17,7 +17,7 @@ export type RpcClient<TApi extends Record<string, any> = Record<string, any>> =
17
17
  * Typing:
18
18
  * - Without a generic: `const api = importServer('...')` -> `api.anyFn<MyReturn>()`
19
19
  * - With a generic: `importServer<typeof import('../../backend/helper')>('...')`
20
- * gives argument + default return types, while still allowing overrides.
20
+ * gives argument + default return types, while still allowing overrides.
21
21
  *
22
22
  * Note: server functions can be defined as `(req, ...args)`; the first arg is injected
23
23
  * by the server, so the client signature automatically drops that first parameter.
@@ -28,6 +28,21 @@ function asErrorMessage(err) {
28
28
  return 'Unknown error';
29
29
  }
30
30
  }
31
+ // Detecta se estamos rodando no Node.js
32
+ const isServer = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
33
+ function getRpcEndpoint() {
34
+ if (isServer) {
35
+ // Tenta pegar a porta das variáveis de ambiente ou usa 3000 como fallback.
36
+ // Nota: Se você usar uma porta customizada no vatts.config.ts sem setar ENV,
37
+ // precisará garantir que process.env.PORT esteja alinhado.
38
+ const port = process.env.PORT || 3000;
39
+ // Em SSR, sempre usamos HTTP e Loopback IP (127.0.0.1) para garantir
40
+ // que a requisição chegue no próprio servidor localmente sem sair pra rede externa.
41
+ return `http://127.0.0.1:${port}${types_1.RPC_ENDPOINT}`;
42
+ }
43
+ // No cliente (browser), URL relativa funciona perfeitamente
44
+ return types_1.RPC_ENDPOINT;
45
+ }
31
46
  /**
32
47
  * `importServer("src/backend/index.ts")` returns a Proxy where every property is
33
48
  * a function that performs a POST to `/api/rpc`.
@@ -35,7 +50,7 @@ function asErrorMessage(err) {
35
50
  * Typing:
36
51
  * - Without a generic: `const api = importServer('...')` -> `api.anyFn<MyReturn>()`
37
52
  * - With a generic: `importServer<typeof import('../../backend/helper')>('...')`
38
- * gives argument + default return types, while still allowing overrides.
53
+ * gives argument + default return types, while still allowing overrides.
39
54
  *
40
55
  * Note: server functions can be defined as `(req, ...args)`; the first arg is injected
41
56
  * by the server, so the client signature automatically drops that first parameter.
@@ -71,9 +86,11 @@ function importServer(file) {
71
86
  }
72
87
  }
73
88
  };
89
+ // Resolve a URL correta (Absoluta no server, Relativa no client)
90
+ const endpoint = getRpcEndpoint();
74
91
  let res;
75
92
  try {
76
- res = await fetch(types_1.RPC_ENDPOINT, {
93
+ res = await fetch(endpoint, {
77
94
  method: 'POST',
78
95
  headers: {
79
96
  'Content-Type': 'application/json'
@@ -0,0 +1,5 @@
1
+ export declare const loadEnv: (options: {
2
+ dir: string;
3
+ dev: boolean;
4
+ envFiles?: string[];
5
+ }) => void;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadEnv = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const console_1 = __importDefault(require("../api/console"));
10
+ function parse(src) {
11
+ const obj = {};
12
+ const lines = src.toString().split('\n');
13
+ for (const line of lines) {
14
+ const trimmedLine = line.trim();
15
+ if (trimmedLine.startsWith('#') || trimmedLine === '') {
16
+ continue;
17
+ }
18
+ const match = trimmedLine.match(/^([^=]+)=(.*)$/);
19
+ if (match) {
20
+ const key = match[1].trim();
21
+ let value = match[2].trim();
22
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
23
+ value = value.substring(1, value.length - 1);
24
+ }
25
+ obj[key] = value;
26
+ }
27
+ }
28
+ return obj;
29
+ }
30
+ function applyEnv(filePath) {
31
+ if (!fs_1.default.existsSync(filePath))
32
+ return;
33
+ try {
34
+ const fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
35
+ const parsed = parse(fileContent);
36
+ for (const key in parsed) {
37
+ if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
38
+ process.env[key] = parsed[key];
39
+ }
40
+ }
41
+ }
42
+ catch (e) {
43
+ console_1.default.error(`Error loading env file ${filePath}:`, e);
44
+ }
45
+ }
46
+ const loadEnv = (options) => {
47
+ const { dir, dev, envFiles = [] } = options;
48
+ const filesToLoad = [".env", ...envFiles].map((file) => path_1.default.join(dir, file));
49
+ filesToLoad.forEach(applyEnv);
50
+ if (dev) {
51
+ for (const file of filesToLoad) {
52
+ if (fs_1.default.existsSync(file)) {
53
+ let watchTimeout;
54
+ fs_1.default.watch(file, (eventType) => {
55
+ if (eventType === 'change') {
56
+ // Limpa o timeout anterior para evitar execuções múltiplas
57
+ clearTimeout(watchTimeout);
58
+ // Define um novo timeout de 100ms
59
+ watchTimeout = setTimeout(() => {
60
+ console_1.default.info(`Reloading environment variables from ${path_1.default.basename(file)}.`);
61
+ applyEnv(file);
62
+ }, 100);
63
+ }
64
+ });
65
+ }
66
+ }
67
+ }
68
+ };
69
+ exports.loadEnv = loadEnv;
package/dist/helpers.js CHANGED
@@ -132,6 +132,7 @@ async function loadVattsConfig(projectDir, phase) {
132
132
  individualRequestTimeout: 30000,
133
133
  maxUrlLength: 2048,
134
134
  accessLogging: true,
135
+ envFiles: [],
135
136
  };
136
137
  try {
137
138
  // Tenta primeiro .ts, depois .js
@@ -317,10 +318,12 @@ const parseBody = (req) => {
317
318
  */
318
319
  async function initNativeServer(vattsApp, options, port, hostname) {
319
320
  const time = Date.now();
320
- await vattsApp.prepare();
321
321
  const projectDir = options.dir || process.cwd();
322
322
  const phase = options.dev ? 'development' : 'production';
323
323
  const vattsConfig = await loadVattsConfig(projectDir, phase);
324
+ // Passa envFiles da config para as opções do vatts
325
+ options.envFiles = vattsConfig.envFiles;
326
+ await vattsApp.prepare();
324
327
  const handler = vattsApp.getRequestHandler();
325
328
  const msg = console_1.default.dynamicLine(`${console_1.Colors.Bright}Starting Vatts.js on port ${options.port}${console_1.Colors.Reset}`);
326
329
  // --- LÓGICA DO LISTENER (REUTILIZÁVEL) ---
package/dist/index.js CHANGED
@@ -59,13 +59,14 @@ const fs_1 = __importDefault(require("fs"));
59
59
  const express_1 = require("./adapters/express");
60
60
  const builder_1 = require("./builder");
61
61
  const router_1 = require("./router");
62
- const renderer_1 = require("./renderer");
62
+ const renderer_1 = require("./renderer"); // Usando a nova função de streaming
63
63
  const http_1 = require("./api/http");
64
64
  Object.defineProperty(exports, "VattsRequest", { enumerable: true, get: function () { return http_1.VattsRequest; } });
65
65
  Object.defineProperty(exports, "VattsResponse", { enumerable: true, get: function () { return http_1.VattsResponse; } });
66
66
  const hotReload_1 = require("./hotReload");
67
67
  const factory_1 = require("./adapters/factory");
68
68
  const console_1 = __importStar(require("./api/console"));
69
+ const env_1 = require("./env/env");
69
70
  // RPC
70
71
  const server_1 = require("./rpc/server");
71
72
  const types_1 = require("./rpc/types");
@@ -120,13 +121,9 @@ function isLargeProject(projectDir) {
120
121
  }
121
122
  }
122
123
  scanDirectory(srcDir);
123
- // Considera projeto grande se:
124
- // - Mais de 20 arquivos de frontend/style
125
- // - Ou tamanho total > 500KB
126
124
  return totalFiles > 20 || totalSize > 500 * 1024;
127
125
  }
128
126
  catch (error) {
129
- // Em caso de erro, assume que não é um projeto grande
130
127
  return false;
131
128
  }
132
129
  }
@@ -136,44 +133,32 @@ function createEntryFile(projectDir, routes) {
136
133
  const tempDir = path_1.default.join(projectDir, '.vatts', 'temp');
137
134
  fs_1.default.mkdirSync(tempDir, { recursive: true });
138
135
  const entryFilePath = path_1.default.join(tempDir, 'entry.client.js');
139
- // Verifica se há layout
140
136
  const layout = (0, router_1.getLayout)();
141
- // Verifica se há notFound personalizado
142
137
  const notFound = (0, router_1.getNotFound)();
143
- // Gera imports dinâmicos para cada componente
144
138
  const imports = routes
145
139
  .map((route, index) => {
146
140
  const relativePath = path_1.default.relative(tempDir, route.componentPath).replace(/\\/g, '/');
147
141
  return `import route${index} from '${relativePath}';`;
148
142
  })
149
143
  .join('\n');
150
- // Import do layout se existir
151
144
  const layoutImport = layout
152
145
  ? `import LayoutComponent from '${path_1.default.relative(tempDir, layout.componentPath).replace(/\\/g, '/')}';`
153
146
  : '';
154
- // Import do notFound se existir
155
147
  const notFoundImport = notFound
156
148
  ? `import NotFoundComponent from '${path_1.default.relative(tempDir, notFound.componentPath).replace(/\\/g, '/')}';`
157
149
  : '';
158
- // Registra os componentes no window para o cliente acessar
159
150
  const componentRegistration = routes
160
151
  .map((route, index) => ` '${route.componentPath}': route${index}.component || route${index}.default?.component,`)
161
152
  .join('\n');
162
- // Registra o layout se existir
163
153
  const layoutRegistration = layout
164
154
  ? `window.__VATTS_LAYOUT__ = LayoutComponent.default || LayoutComponent;`
165
155
  : `window.__VATTS_LAYOUT__ = null;`;
166
- // Registra o notFound se existir
167
156
  const notFoundRegistration = notFound
168
157
  ? `window.__VATTS_NOT_FOUND__ = NotFoundComponent.default || NotFoundComponent;`
169
158
  : `window.__VATTS_NOT_FOUND__ = null;`;
170
- // Caminho correto para o entry.client
171
- // IMPORTANT: quando o pacote é instalado via npm, bundlers (Rollup/Vite/etc.) não transpilam TSX em node_modules.
172
- // Por isso, aqui a gente sempre aponta para os artefatos compilados em dist/.
173
- const sdkDir = path_1.default.dirname(__dirname); // pasta do pacote (pai de dist/ e src/)
159
+ const sdkDir = path_1.default.dirname(__dirname);
174
160
  const entryClientPath = path_1.default.join(sdkDir, 'dist', 'client', 'entry.client.js');
175
161
  const relativeEntryPath = path_1.default.relative(tempDir, entryClientPath).replace(/\\/g, '/');
176
- // Import do DefaultNotFound do SDK (compilado)
177
162
  const defaultNotFoundPath = path_1.default.join(sdkDir, 'dist', 'client', 'DefaultNotFound.js');
178
163
  const relativeDefaultNotFoundPath = path_1.default.relative(tempDir, defaultNotFoundPath).replace(/\\/g, '/');
179
164
  const entryContent = `// Arquivo gerado automaticamente pelo vatts
@@ -182,28 +167,22 @@ ${layoutImport}
182
167
  ${notFoundImport}
183
168
  import DefaultNotFound from '${relativeDefaultNotFoundPath}';
184
169
 
185
- // Registra os componentes para o cliente
186
170
  window.__VATTS_COMPONENTS__ = {
187
171
  ${componentRegistration}
188
172
  };
189
173
 
190
- // Registra o layout se existir
191
174
  ${layoutRegistration}
192
-
193
- // Registra o notFound se existir
194
175
  ${notFoundRegistration}
195
176
 
196
-
197
177
  window.__VATTS_DEFAULT_NOT_FOUND__ = DefaultNotFound;
198
178
 
199
- // Importa e executa o entry.client.tsx
200
179
  import '${relativeEntryPath}';
201
180
  `;
202
181
  try {
203
182
  fs_1.default.writeFileSync(entryFilePath, entryContent);
204
183
  }
205
184
  catch (e) {
206
- console.error("sdfijnsdfnijfsdijnfsdnijsdfnijfsdnijfsdnijfsdn", e);
185
+ console.error("Error writing entry file", e);
207
186
  }
208
187
  return entryFilePath;
209
188
  }
@@ -213,77 +192,58 @@ import '${relativeEntryPath}';
213
192
  }
214
193
  }
215
194
  function vatts(options) {
216
- const { dev = true, dir = process.cwd(), port = 3000 } = options;
195
+ const { dev = true, dir = process.cwd(), port = 3000, envFiles } = options;
196
+ (0, env_1.loadEnv)({ dir, dev, envFiles });
217
197
  // @ts-ignore
218
198
  process.vatts = options;
199
+ // @ts-ignore
200
+ process.env.PORT = options.port;
219
201
  const userWebDir = path_1.default.join(dir, 'src', 'web');
220
202
  const userWebRoutesDir = path_1.default.join(userWebDir, 'routes');
221
203
  const userBackendRoutesDir = path_1.default.join(dir, 'src', 'backend', 'routes');
222
- /**
223
- * Executa middlewares sequencialmente e depois o handler final
224
- * @param middlewares Array de middlewares para executar
225
- * @param finalHandler Handler final da rota
226
- * @param request Requisição do Vatts.js
227
- * @param params Parâmetros da rota
228
- * @returns Resposta do middleware ou handler final
229
- */
230
204
  async function executeMiddlewareChain(middlewares, finalHandler, request, params) {
231
205
  if (!middlewares || middlewares.length === 0) {
232
- // Não há middlewares, executa diretamente o handler final
233
206
  return await finalHandler(request, params);
234
207
  }
235
208
  let currentIndex = 0;
236
- // Função next que será chamada pelos middlewares
237
209
  const next = async () => {
238
210
  if (currentIndex < middlewares.length) {
239
- // Ainda há middlewares para executar
240
211
  const currentMiddleware = middlewares[currentIndex];
241
212
  currentIndex++;
242
213
  return await currentMiddleware(request, params, next);
243
214
  }
244
215
  else {
245
- // Todos os middlewares foram executados, chama o handler final
246
216
  return await finalHandler(request, params);
247
217
  }
248
218
  };
249
- // Inicia a cadeia de execução
250
219
  return await next();
251
220
  }
252
221
  let frontendRoutes = [];
253
222
  let hotReloadManager = null;
254
223
  let entryPoint;
255
- let outfile;
256
- // Função para regenerar o entry file
257
224
  const regenerateEntryFile = () => {
258
- // Recarrega todas as rotas e componentes
259
225
  const newFrontendRoutes = (0, router_1.loadRoutes)(userWebRoutesDir);
260
226
  const newLayout = (0, router_1.loadLayout)(userWebDir);
261
227
  const newNotFound = (0, router_1.loadNotFound)(userWebDir);
262
- // Se nada mudou, não reescreve o entry file (evita disparar rebuild extra)
263
228
  const oldKey = frontendRoutes.map(r => `${r.pattern ?? ''}:${r.componentPath}`).join('|');
264
229
  const newKey = newFrontendRoutes.map(r => `${r.pattern ?? ''}:${r.componentPath}`).join('|');
265
230
  if (oldKey === newKey) {
266
- // Ainda atualiza refs internas de layout/notFound, mas evita re-gerar o entry.
267
231
  frontendRoutes = newFrontendRoutes;
268
232
  return;
269
233
  }
270
234
  frontendRoutes = newFrontendRoutes;
271
- // Regenera o entry file
272
235
  entryPoint = createEntryFile(dir, frontendRoutes);
273
236
  };
274
237
  return {
275
238
  prepare: async () => {
276
239
  const isProduction = !dev;
277
240
  if (!isProduction) {
278
- // Inicia hot reload apenas em desenvolvimento (com suporte ao main)
279
241
  hotReloadManager = new hotReload_1.HotReloadManager(dir);
280
242
  await hotReloadManager.start();
281
- // Adiciona callback para recarregar TUDO quando qualquer arquivo mudar
282
243
  hotReloadManager.onBackendApiChange(() => {
283
244
  (0, router_1.loadBackendRoutes)(userBackendRoutesDir);
284
- (0, router_1.processWebSocketRoutes)(); // Processa rotas WS após recarregar backend
245
+ (0, router_1.processWebSocketRoutes)();
285
246
  });
286
- // Adiciona callback para regenerar entry file quando frontend mudar
287
247
  hotReloadManager.onFrontendChange(() => {
288
248
  regenerateEntryFile();
289
249
  });
@@ -295,13 +255,10 @@ function vatts(options) {
295
255
  const spinner1 = setInterval(() => {
296
256
  timee.update(` ${console_1.Colors.FgYellow}${spinnerFrames1[frameIndex1]}${console_1.Colors.Reset} Loading routes and components...`);
297
257
  frameIndex1 = (frameIndex1 + 1) % spinnerFrames1.length;
298
- }, 100); // muda a cada 100ms
299
- // ORDEM IMPORTANTE: Carrega TUDO antes de criar o arquivo de entrada
258
+ }, 100);
300
259
  frontendRoutes = (0, router_1.loadRoutes)(userWebRoutesDir);
301
260
  (0, router_1.loadBackendRoutes)(userBackendRoutesDir);
302
- // Processa rotas WebSocket após carregar backend
303
261
  (0, router_1.processWebSocketRoutes)();
304
- // Carrega layout.tsx ANTES de criar o entry file
305
262
  const layout = (0, router_1.loadLayout)(userWebDir);
306
263
  const notFound = (0, router_1.loadNotFound)(userWebDir);
307
264
  const outDir = path_1.default.join(dir, '.vatts');
@@ -311,20 +268,18 @@ function vatts(options) {
311
268
  timee.end(`Routes and components loaded in ${Date.now() - now}ms`);
312
269
  if (isProduction) {
313
270
  const time = console_1.default.dynamicLine(`Starting client build`);
314
- // Spinner
315
271
  const spinnerFrames = ['|', '/', '-', '\\'];
316
272
  let frameIndex = 0;
317
273
  const spinner = setInterval(() => {
318
274
  time.update(` ${console_1.Colors.FgYellow}${spinnerFrames[frameIndex]}${console_1.Colors.Reset} Building...`);
319
275
  frameIndex = (frameIndex + 1) % spinnerFrames.length;
320
- }, 100); // muda a cada 100ms
276
+ }, 100);
321
277
  const now = Date.now();
322
278
  await (0, builder_1.buildWithChunks)(entryPoint, outDir, isProduction);
323
279
  const elapsed = Date.now() - now;
324
- clearInterval(spinner); // para o spinner
325
- time.update(""); // limpa a linha
280
+ clearInterval(spinner);
281
+ time.update("");
326
282
  time.end(`Client build completed in ${elapsed}ms`);
327
- // Notifica o hot reload manager que o build foi concluído
328
283
  if (hotReloadManager) {
329
284
  hotReloadManager.onBuildComplete(true);
330
285
  }
@@ -339,13 +294,10 @@ function vatts(options) {
339
294
  }
340
295
  },
341
296
  executeInstrumentation: () => {
342
- // verificar se dir/src/instrumentation.(tsx/jsx/js/ts) existe com regex
343
297
  const instrumentationFile = fs_1.default.readdirSync(path_1.default.join(dir, 'src')).find(file => /^vattsweb\.(tsx|jsx|js|ts)$/.test(file));
344
298
  if (instrumentationFile) {
345
299
  const instrumentationPath = path_1.default.join(dir, 'src', instrumentationFile);
346
- // dar require, e executar a função principal do arquivo
347
300
  const instrumentation = require(instrumentationPath);
348
- // Registra o listener de hot reload se existir
349
301
  if (instrumentation.hotReloadListener && typeof instrumentation.hotReloadListener === 'function') {
350
302
  if (hotReloadManager) {
351
303
  hotReloadManager.setHotReloadListener(instrumentation.hotReloadListener);
@@ -364,17 +316,14 @@ function vatts(options) {
364
316
  },
365
317
  getRequestHandler: () => {
366
318
  return async (req, res) => {
367
- // Detecta o framework e cria request/response genéricos
368
319
  const adapter = factory_1.FrameworkAdapterFactory.detectFramework(req, res);
369
320
  const genericReq = adapter.parseRequest(req);
370
321
  const genericRes = adapter.createResponse(res);
371
- // Adiciona informações do hweb na requisição genérica
372
322
  genericReq.hwebDev = dev;
373
323
  genericReq.hotReloadManager = hotReloadManager;
374
324
  const { hostname } = req.headers;
375
325
  const method = (genericReq.method || 'GET').toUpperCase();
376
326
  const pathname = new URL(genericReq.url, `http://${hostname}:${port}`).pathname;
377
- // RPC endpoint (antes das rotas de backend)
378
327
  if (pathname === types_1.RPC_ENDPOINT && method === 'POST') {
379
328
  try {
380
329
  const result = await (0, server_1.executeRpc)({
@@ -391,12 +340,9 @@ function vatts(options) {
391
340
  return;
392
341
  }
393
342
  }
394
- // 1. Verifica se é WebSocket upgrade para hot reload
395
343
  if (pathname === '/hweb-hotreload/' && genericReq.headers.upgrade === 'websocket' && hotReloadManager) {
396
- // Framework vai chamar o evento 'upgrade' do servidor HTTP
397
344
  return;
398
345
  }
399
- // 2. Primeiro verifica se é um arquivo estático da pasta public
400
346
  if (pathname !== '/' && !pathname.startsWith('/api/') && !pathname.startsWith('/.vatts')) {
401
347
  const publicDir = path_1.default.join(dir, 'public');
402
348
  if (!isSuspiciousPathname(pathname)) {
@@ -425,7 +371,6 @@ function vatts(options) {
425
371
  '.zip': 'application/zip'
426
372
  };
427
373
  genericRes.header('Content-Type', contentTypes[ext] || 'application/octet-stream');
428
- // Para arquivos estáticos, usamos o método nativo do framework
429
374
  if (adapter.type === 'express') {
430
375
  res.sendFile(filePath);
431
376
  }
@@ -454,7 +399,6 @@ function vatts(options) {
454
399
  '.map': 'application/json'
455
400
  };
456
401
  genericRes.header('Content-Type', contentTypes[ext] || 'text/plain');
457
- // Para arquivos estáticos, usamos o método nativo do framework
458
402
  if (adapter.type === 'express') {
459
403
  res.sendFile(filePath);
460
404
  }
@@ -470,18 +414,13 @@ function vatts(options) {
470
414
  }
471
415
  }
472
416
  }
473
- // 4. REMOVIDO: Verificação de arquivos React UMD - não precisamos mais
474
- // O React agora será bundlado diretamente no main.js
475
- // 5. Verifica se é uma rota de API (backend)
476
417
  const backendMatch = (0, router_1.findMatchingBackendRoute)(pathname, method);
477
418
  if (backendMatch) {
478
419
  try {
479
420
  const handler = backendMatch.route[method];
480
421
  if (handler) {
481
422
  const hwebReq = new http_1.VattsRequest(genericReq);
482
- // Executa middlewares e depois o handler final
483
423
  const hwebRes = await executeMiddlewareChain(backendMatch.route.middleware, handler, hwebReq, backendMatch.params);
484
- // Aplica a resposta usando o adapter correto
485
424
  hwebRes._applyTo(genericRes);
486
425
  return;
487
426
  }
@@ -492,24 +431,41 @@ function vatts(options) {
492
431
  return;
493
432
  }
494
433
  }
495
- // 6. Por último, tenta renderizar uma página (frontend) ou 404
434
+ // Renderização de Página (Frontend)
496
435
  const pageMatch = (0, router_1.findMatchingRoute)(pathname);
436
+ // Determina o objeto de resposta "cru" para o stream do React
437
+ // Se for Fastify, o Writable stream está em res.raw. Se for Express/Native, é o próprio res.
438
+ const rawRes = (res.raw || res);
497
439
  if (!pageMatch) {
498
- // Em vez de enviar texto simples, renderiza a página 404 React
499
440
  try {
500
- // Cria uma "rota falsa" para a página 404
501
441
  const notFoundRoute = {
502
442
  pattern: '/__404__',
503
- component: () => null, // Componente vazio, será tratado no cliente
443
+ component: () => null, // Será renderizado via SSR se tiver componente de 404 definido no Client
504
444
  componentPath: '__404__'
505
445
  };
506
- const html = await (0, renderer_1.render)({
446
+ // Tenta usar componente 404 customizado se houver
447
+ const notFound = (0, router_1.getNotFound)();
448
+ if (notFound) {
449
+ // Carrega o componente 404 real para o SSR funcionar
450
+ try {
451
+ const nfModule = require(path_1.default.resolve(process.cwd(), notFound.componentPath));
452
+ // @ts-ignore
453
+ notFoundRoute.component = nfModule.default || nfModule;
454
+ }
455
+ catch (e) { }
456
+ }
457
+ // Define status 404 antes de iniciar o stream
458
+ if (rawRes.statusCode)
459
+ rawRes.statusCode = 404; // Native/Fastify
460
+ if (rawRes.status)
461
+ rawRes.status(404); // Express (mas pode quebrar se for raw, melhor usar statusCode)
462
+ await (0, renderer_1.renderAsStream)({
507
463
  req: genericReq,
464
+ res: rawRes,
508
465
  route: notFoundRoute,
509
466
  params: {},
510
467
  allRoutes: frontendRoutes
511
468
  });
512
- genericRes.status(404).header('Content-Type', 'text/html').send(html);
513
469
  return;
514
470
  }
515
471
  catch (error) {
@@ -519,26 +475,28 @@ function vatts(options) {
519
475
  }
520
476
  }
521
477
  try {
522
- const html = await (0, renderer_1.render)({
478
+ // Renderização via Stream (SSR + Streaming)
479
+ await (0, renderer_1.renderAsStream)({
523
480
  req: genericReq,
481
+ res: rawRes,
524
482
  route: pageMatch.route,
525
483
  params: pageMatch.params,
526
484
  allRoutes: frontendRoutes
527
485
  });
528
- genericRes.status(200).header('Content-Type', 'text/html').send(html);
529
486
  }
530
487
  catch (error) {
531
488
  console_1.default.error(`Error rendering page ${pathname}:`, error);
532
- genericRes.status(500).text('Internal server error');
489
+ // Se o stream já começou, não dá pra enviar erro 500 limpo.
490
+ // O renderAsStream tenta lidar com isso no onError.
491
+ if (!rawRes.headersSent) {
492
+ genericRes.status(500).text('Internal server error');
493
+ }
533
494
  }
534
495
  };
535
496
  },
536
- // Método para configurar WebSocket upgrade nos servidores Express e Fastify
537
497
  setupWebSocket: (server) => {
538
- // Detecta se é um servidor Express ou Fastify
539
498
  const isExpressServer = factory_1.FrameworkAdapterFactory.getCurrentAdapter() instanceof express_1.ExpressAdapter;
540
499
  const actualServer = isExpressServer ? server : (server.server || server);
541
- // Usa o sistema coordenado de WebSocket upgrade que integra hot-reload e rotas de usuário
542
500
  (0, router_1.setupWebSocketUpgrade)(actualServer, hotReloadManager);
543
501
  },
544
502
  stop: () => {
@@ -2,6 +2,7 @@ import { RouteConfig } from './types';
2
2
  import type { GenericRequest } from './types/framework';
3
3
  interface RenderOptions {
4
4
  req: GenericRequest;
5
+ res: any;
5
6
  route: RouteConfig & {
6
7
  componentPath: string;
7
8
  };
@@ -10,5 +11,6 @@ interface RenderOptions {
10
11
  componentPath: string;
11
12
  })[];
12
13
  }
13
- export declare function render({ req, route, params, allRoutes }: RenderOptions): Promise<string>;
14
+ export declare function renderAsStream({ req, res, route, params, allRoutes }: RenderOptions): Promise<void>;
15
+ export declare function render(options: any): Promise<string>;
14
16
  export {};