vatts 2.0.2 → 2.0.3-canary.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.
@@ -301,13 +301,13 @@ function initializeClient() {
301
301
  console.error('[Watts] Critical Error rendering application:', error);
302
302
  // Exibe erro na tela caso algo crítico falhe
303
303
  if (typeof document !== 'undefined') {
304
- document.body.innerHTML = `
305
- <div style="font-family: monospace; padding: 20px; color: #ff4444; background: #1a1a1a; min-height: 100vh;">
306
- <h1>Vatts Client Error</h1>
307
- <p>A critical error occurred while initializing the application.</p>
308
- <pre style="background: #000; padding: 15px; border-radius: 5px; overflow: auto;">${error?.message || error}</pre>
309
- <pre style="color: #666; font-size: 12px; margin-top: 10px;">${error?.stack || ''}</pre>
310
- </div>
304
+ document.body.innerHTML = `
305
+ <div style="font-family: monospace; padding: 20px; color: #ff4444; background: #1a1a1a; min-height: 100vh;">
306
+ <h1>Vatts Client Error</h1>
307
+ <p>A critical error occurred while initializing the application.</p>
308
+ <pre style="background: #000; padding: 15px; border-radius: 5px; overflow: auto;">${error?.message || error}</pre>
309
+ <pre style="color: #666; font-size: 12px; margin-top: 10px;">${error?.stack || ''}</pre>
310
+ </div>
311
311
  `;
312
312
  }
313
313
  }
@@ -26,6 +26,94 @@ const router_1 = require("../router");
26
26
  const fs_1 = __importDefault(require("fs"));
27
27
  const path_1 = __importDefault(require("path"));
28
28
  const BuildingPage_1 = __importDefault(require("../react/BuildingPage"));
29
+ const server_error_1 = __importDefault(require("../react/server-error"));
30
+ function stripScriptTags(html) {
31
+ if (!html)
32
+ return '';
33
+ return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
34
+ }
35
+ function getRequestUrl(req) {
36
+ return req?.originalUrl || req?.url;
37
+ }
38
+ function toError(err) {
39
+ if (err instanceof Error)
40
+ return err;
41
+ if (typeof err === 'string')
42
+ return new Error(err);
43
+ try {
44
+ return new Error(JSON.stringify(err));
45
+ }
46
+ catch {
47
+ return new Error(String(err));
48
+ }
49
+ }
50
+ function buildShellHtml(options) {
51
+ const { lang, title, metaTagsHtml, stylesHtml, hotReloadScript, obfuscatedData, scriptsHtml } = options;
52
+ return `<!DOCTYPE html>
53
+ <html lang="${lang}">
54
+ <head>
55
+ <meta charset="utf-8" />
56
+ <title>${title}</title>
57
+ ${metaTagsHtml || ''}
58
+ ${stylesHtml || ''}
59
+ </head>
60
+ <body>
61
+ <script id="__vatts_data__" type="text/plain" data-h="${obfuscatedData}"></script>
62
+ <div id="root"></div>
63
+ ${scriptsHtml || ''}
64
+ ${hotReloadScript ? `<div style="display:none">${hotReloadScript}</div>` : ''}
65
+ </body>
66
+ </html>`;
67
+ }
68
+ async function sendReactSsrFallback(options) {
69
+ const { req, res, isProduction, error, assets, lang, title, metaTagsHtml, stylesHtml, hotReloadScript, obfuscatedData, } = options;
70
+ const scriptsHtml = assets.scripts
71
+ .map((src) => `<script type="module" src="${src}"></script>`)
72
+ .join('\n');
73
+ // No DEV a gente remove scripts do head pra garantir que só aparece a página de erro.
74
+ const safeMetaTagsHtmlForDev = stripScriptTags(metaTagsHtml);
75
+ if (res.headersSent) {
76
+ try {
77
+ res.end();
78
+ }
79
+ catch {
80
+ // ignore
81
+ }
82
+ return;
83
+ }
84
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
85
+ res.statusCode = isProduction ? 200 : 500;
86
+ if (isProduction) {
87
+ res.end(buildShellHtml({
88
+ lang,
89
+ title,
90
+ metaTagsHtml,
91
+ stylesHtml,
92
+ hotReloadScript: '',
93
+ obfuscatedData,
94
+ scriptsHtml,
95
+ }));
96
+ return;
97
+ }
98
+ const err = toError(error);
99
+ return new Promise((resolve) => {
100
+ const { pipe } = (0, server_1.renderToPipeableStream)(react_1.default.createElement(ServerRoot, { lang: lang, title: title, metaTagsHtml: safeMetaTagsHtmlForDev, stylesHtml: stylesHtml, initialDataScript: `/* Data Injection */`, hotReloadScript: '', dataScript: react_1.default.createElement("script", { id: "__vatts_data__", type: "text/plain", "data-h": obfuscatedData }) },
101
+ react_1.default.createElement(server_error_1.default, { error: err, requestUrl: getRequestUrl(req), hint: "O SSR falhou ao renderizar essa rota. Veja o erro abaixo." })), {
102
+ onAllReady() {
103
+ pipe(res);
104
+ resolve();
105
+ },
106
+ onError() {
107
+ // ignore (já estamos numa tela de erro)
108
+ },
109
+ onShellError() {
110
+ // fallback extremo
111
+ res.end('<h1>SSR Error</h1>');
112
+ resolve();
113
+ },
114
+ });
115
+ });
116
+ }
29
117
  // --- Helpers de Servidor ---
30
118
  // Função auxiliar para importar módulos ignorando CSS (mesma lógica do router.ts)
31
119
  function requireWithoutStyles(modulePath) {
@@ -222,12 +310,12 @@ function getBuildAssets(req) {
222
310
  }
223
311
  function ServerRoot({ lang, title, metaTagsHtml, stylesHtml, initialDataScript, hotReloadScript, dataScript, children }) {
224
312
  // Concatena tudo que vai no head em uma única string
225
- const headContent = `
226
- <meta charset="utf-8" />
227
- <title>${title}</title>
228
- ${initialDataScript ? `<script>${initialDataScript}</script>` : ''}
229
- ${metaTagsHtml || ''}
230
- ${stylesHtml || ''}
313
+ const headContent = `
314
+ <meta charset="utf-8" />
315
+ <title>${title}</title>
316
+ ${initialDataScript ? `<script>${initialDataScript}</script>` : ''}
317
+ ${metaTagsHtml || ''}
318
+ ${stylesHtml || ''}
231
319
  `;
232
320
  return (react_1.default.createElement("html", { lang: lang },
233
321
  react_1.default.createElement("head", { dangerouslySetInnerHTML: { __html: headContent } }),
@@ -241,22 +329,23 @@ async function render({ req, res, route, params, allRoutes }) {
241
329
  const isProduction = !req.hwebDev;
242
330
  const hotReloadManager = req.hotReloadManager;
243
331
  // SILENCIAR CONSOLE: Inicia o silêncio para evitar logs de renderização
332
+ let assets = null;
333
+ let metadata = { title: 'Vatts App' };
334
+ let layoutInfo = null;
244
335
  try {
245
336
  // 1. Verificar Build - Se não tiver scripts, retorna tela de Loading
246
- const assets = getBuildAssets(req);
337
+ assets = getBuildAssets(req);
247
338
  if (!assets || assets.scripts.length === 0) {
248
- // Se falhar o build, restauramos o console para o erro aparecer se necessário
249
- // Usando stream para a tela de loading também, agora via React Component
250
339
  const { pipe } = (0, server_1.renderToPipeableStream)(react_1.default.createElement(BuildingPage_1.default, null), {
251
340
  onShellReady() {
252
- res.setHeader('Content-Type', 'text/html');
341
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
253
342
  pipe(res);
254
- }
343
+ },
255
344
  });
256
345
  return;
257
346
  }
258
347
  // 2. Preparar Layout
259
- const layoutInfo = (0, router_1.getLayout)();
348
+ layoutInfo = (0, router_1.getLayout)();
260
349
  let LayoutComponent = null;
261
350
  if (layoutInfo) {
262
351
  try {
@@ -269,7 +358,6 @@ async function render({ req, res, route, params, allRoutes }) {
269
358
  }
270
359
  }
271
360
  // 3. Preparar Metadata
272
- let metadata = { title: 'Vatts App' };
273
361
  if (layoutInfo && layoutInfo.metadata) {
274
362
  metadata = { ...metadata, ...layoutInfo.metadata };
275
363
  }
@@ -311,37 +399,88 @@ async function render({ req, res, route, params, allRoutes }) {
311
399
  if (LayoutComponent) {
312
400
  AppTree = react_1.default.createElement(LayoutComponent, null, AppTree);
313
401
  }
314
- // 6. Streaming
315
- return new Promise((resolve, reject) => {
402
+ // 6. Streaming (segura até onAllReady para conseguir fallback sem HTML parcial)
403
+ return new Promise((resolve) => {
316
404
  let didError = false;
317
- const { pipe } = (0, server_1.renderToPipeableStream)(react_1.default.createElement(ServerRoot, { lang: htmlLang, title: metadata.title || 'Vatts.js', metaTagsHtml: metaTagsHtml, stylesHtml: stylesHtml,
318
- // Recriando o script de dados exatamente como o client espera
319
- initialDataScript: `/* Data Injection */`, hotReloadScript: hotReloadScript, dataScript: react_1.default.createElement("script", { id: "__vatts_data__", type: "text/plain", "data-h": obfuscatedData }) }, AppTree), {
320
- // Usar bootstrapModules para scripts tipo módulo (ESM)
405
+ let firstError = null;
406
+ const stream = (0, server_1.renderToPipeableStream)(react_1.default.createElement(ServerRoot, { lang: htmlLang, title: metadata.title || 'Vatts.js', metaTagsHtml: metaTagsHtml, stylesHtml: stylesHtml, initialDataScript: `/* Data Injection */`, hotReloadScript: hotReloadScript, dataScript: react_1.default.createElement("script", { id: "__vatts_data__", type: "text/plain", "data-h": obfuscatedData }) }, AppTree), {
321
407
  bootstrapModules: assets.scripts,
322
- onShellReady() {
323
- res.setHeader('Content-Type', 'text/html');
324
- pipe(res);
408
+ onAllReady() {
409
+ if (didError) {
410
+ stream.abort();
411
+ sendReactSsrFallback({
412
+ req,
413
+ res,
414
+ isProduction,
415
+ error: firstError || new Error('SSR error'),
416
+ assets: assets,
417
+ lang: htmlLang,
418
+ title: metadata.title || 'Vatts.js',
419
+ metaTagsHtml,
420
+ stylesHtml,
421
+ hotReloadScript,
422
+ obfuscatedData,
423
+ }).then(resolve);
424
+ return;
425
+ }
426
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
427
+ stream.pipe(res);
325
428
  resolve();
326
429
  },
327
430
  onShellError(error) {
328
- console.error('Streaming Shell Error:', error);
329
- res.statusCode = 500;
330
- res.setHeader('Content-Type', 'text/html');
331
- res.end('<h1>Internal Server Error</h1>');
332
- resolve();
431
+ firstError ||= error;
432
+ didError = true;
333
433
  },
334
434
  onError(error) {
435
+ firstError ||= error;
335
436
  didError = true;
336
- // Log de erro de stream ainda é importante, mas podemos filtrar se quiser
337
- // Aqui mantemos o log de erro original do React que vai para stderr
338
- console.error('Streaming Error:', error);
339
- }
437
+ if (!isProduction) {
438
+ console.error('Streaming Error:', error);
439
+ }
440
+ },
340
441
  });
341
442
  });
342
443
  }
343
444
  catch (err) {
344
- console.error("Critical Render Error:", err);
345
- throw err;
445
+ if (!assets) {
446
+ // sem assets não dá pra montar shell completo; evita crash
447
+ if (!res.headersSent) {
448
+ res.statusCode = 500;
449
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
450
+ res.end(isProduction ? '' : '<h1>SSR Error</h1>');
451
+ }
452
+ return;
453
+ }
454
+ await sendReactSsrFallback({
455
+ req,
456
+ res,
457
+ isProduction,
458
+ error: err,
459
+ assets,
460
+ lang: metadata?.language || 'pt-BR',
461
+ title: metadata.title || 'Vatts.js',
462
+ metaTagsHtml: (() => {
463
+ try {
464
+ return generateMetaTags(metadata);
465
+ }
466
+ catch {
467
+ return '';
468
+ }
469
+ })(),
470
+ stylesHtml: assets.styles.map((styleUrl) => `<link rel="stylesheet" href="${styleUrl}">`).join('\n'),
471
+ hotReloadScript: !isProduction && hotReloadManager ? hotReloadManager.getClientScript() : '',
472
+ obfuscatedData: (() => {
473
+ try {
474
+ return obfuscateData({
475
+ routes: [],
476
+ initialComponentPath: route.componentPath,
477
+ initialParams: params,
478
+ });
479
+ }
480
+ catch {
481
+ return '';
482
+ }
483
+ })(),
484
+ });
346
485
  }
347
486
  }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export interface ServerErrorProps {
3
+ title?: string;
4
+ error?: unknown;
5
+ hint?: string;
6
+ requestUrl?: string;
7
+ }
8
+ export default function ServerError({ title, error, hint, requestUrl, }: ServerErrorProps): React.JSX.Element;
@@ -0,0 +1,64 @@
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.default = ServerError;
7
+ const react_1 = __importDefault(require("react"));
8
+ function formatUnknownError(error) {
9
+ if (!error)
10
+ return { message: 'Erro desconhecido no SSR.' };
11
+ if (error instanceof Error) {
12
+ return { message: error.message || String(error), stack: error.stack };
13
+ }
14
+ if (typeof error === 'string') {
15
+ return { message: error };
16
+ }
17
+ try {
18
+ return { message: JSON.stringify(error, null, 2) };
19
+ }
20
+ catch {
21
+ return { message: String(error) };
22
+ }
23
+ }
24
+ function ServerError({ title = 'Erro no Server-Side Renderer', error, hint, requestUrl, }) {
25
+ const { message, stack } = formatUnknownError(error);
26
+ return (react_1.default.createElement("div", { style: {
27
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',
28
+ padding: 24,
29
+ color: '#0f172a',
30
+ background: '#ffffff',
31
+ } },
32
+ react_1.default.createElement("div", { style: { maxWidth: 980, margin: '0 auto' } },
33
+ react_1.default.createElement("h1", { style: { fontSize: 20, margin: 0 } }, title),
34
+ requestUrl ? (react_1.default.createElement("p", { style: { marginTop: 8, marginBottom: 0, color: '#334155' } },
35
+ "URL: ",
36
+ react_1.default.createElement("code", { style: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' } }, requestUrl))) : null,
37
+ hint ? react_1.default.createElement("p", { style: { marginTop: 8, color: '#334155' } }, hint) : null,
38
+ react_1.default.createElement("div", { style: {
39
+ marginTop: 16,
40
+ padding: 16,
41
+ border: '1px solid #e2e8f0',
42
+ borderRadius: 10,
43
+ background: '#f8fafc',
44
+ } },
45
+ react_1.default.createElement("div", { style: { fontSize: 12, color: '#64748b', marginBottom: 8 } }, "Mensagem"),
46
+ react_1.default.createElement("pre", { style: {
47
+ whiteSpace: 'pre-wrap',
48
+ overflowWrap: 'anywhere',
49
+ margin: 0,
50
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
51
+ fontSize: 13,
52
+ } }, message),
53
+ stack ? (react_1.default.createElement(react_1.default.Fragment, null,
54
+ react_1.default.createElement("div", { style: { fontSize: 12, color: '#64748b', marginTop: 16, marginBottom: 8 } }, "Stack"),
55
+ react_1.default.createElement("pre", { style: {
56
+ whiteSpace: 'pre-wrap',
57
+ overflowWrap: 'anywhere',
58
+ margin: 0,
59
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
60
+ fontSize: 12,
61
+ color: '#334155',
62
+ } }, stack))) : null),
63
+ react_1.default.createElement("p", { style: { marginTop: 16, color: '#475569', fontSize: 13 } }, "Dica: em desenvolvimento isso aparece para debugar. Em produ\u00E7\u00E3o o SSR falha de forma silenciosa e o cliente assume a renderiza\u00E7\u00E3o."))));
64
+ }