@vistagenic/vista 0.2.4 → 0.2.6

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.
@@ -276,10 +276,15 @@ function startServer(port = 3003, compiler) {
276
276
  }, 100);
277
277
  };
278
278
  // Push compile errors to browser via SSE
279
- const pushCompileError = (errorMessage) => {
279
+ const pushCompileError = (errorMessages) => {
280
+ const normalized = (Array.isArray(errorMessages) ? errorMessages : [])
281
+ .map((entry) => String(entry || '').trim())
282
+ .filter(Boolean);
283
+ const fallback = normalized.length > 0 ? normalized.join('\n\n') : 'Unknown build error.';
280
284
  const errorData = JSON.stringify({
281
285
  type: 'error',
282
- message: errorMessage,
286
+ message: fallback,
287
+ errors: normalized.length > 0 ? normalized : [fallback],
283
288
  });
284
289
  sseClients.forEach((client) => {
285
290
  client.write(`data: ${errorData}\n\n`);
@@ -296,9 +301,17 @@ function startServer(port = 3003, compiler) {
296
301
  compiler.hooks.done.tap('VistaSSE', (stats) => {
297
302
  if (stats.hasErrors()) {
298
303
  const errors = stats.toJson().errors || [];
299
- const errorMessage = errors.map((e) => (typeof e === 'string' ? e : e.message)).join('\n');
304
+ const errorMessages = errors
305
+ .map((e) => {
306
+ if (typeof e === 'string')
307
+ return e;
308
+ if (e && typeof e.message === 'string')
309
+ return e.message;
310
+ return String(e || '');
311
+ })
312
+ .filter((entry) => entry.trim().length > 0);
300
313
  (0, logger_1.logEvent)('Build error detected, pushing to browser...');
301
- pushCompileError(errorMessage);
314
+ pushCompileError(errorMessages);
302
315
  }
303
316
  else {
304
317
  pushBuildSuccess();
@@ -296,6 +296,41 @@ function normalizeSSRManifest(manifest) {
296
296
  }
297
297
  const moduleMap = manifest.moduleMap;
298
298
  const aliasEntries = [];
299
+ const pushAlias = (key, exportsMap) => {
300
+ if (!key)
301
+ return;
302
+ aliasEntries.push([key, exportsMap]);
303
+ // React Flight may request module IDs with/without a trailing '#'.
304
+ if (key.endsWith('#')) {
305
+ aliasEntries.push([key.slice(0, -1), exportsMap]);
306
+ }
307
+ else {
308
+ aliasEntries.push([`${key}#`, exportsMap]);
309
+ }
310
+ // Normalize URI encoding variants for Windows paths with spaces, etc.
311
+ if (key.startsWith('file://')) {
312
+ try {
313
+ const decoded = decodeURI(key);
314
+ if (decoded !== key) {
315
+ aliasEntries.push([decoded, exportsMap]);
316
+ aliasEntries.push([decoded.endsWith('#') ? decoded.slice(0, -1) : `${decoded}#`, exportsMap]);
317
+ }
318
+ }
319
+ catch {
320
+ // ignore decode failures
321
+ }
322
+ try {
323
+ const encoded = encodeURI(key);
324
+ if (encoded !== key) {
325
+ aliasEntries.push([encoded, exportsMap]);
326
+ aliasEntries.push([encoded.endsWith('#') ? encoded.slice(0, -1) : `${encoded}#`, exportsMap]);
327
+ }
328
+ }
329
+ catch {
330
+ // ignore encode failures
331
+ }
332
+ }
333
+ };
299
334
  for (const [moduleKey, exportsMap] of Object.entries(moduleMap)) {
300
335
  const normalizedExports = {};
301
336
  for (const [exportName, rawEntry] of Object.entries(exportsMap || {})) {
@@ -308,10 +343,10 @@ function normalizeSSRManifest(manifest) {
308
343
  };
309
344
  }
310
345
  moduleMap[moduleKey] = normalizedExports;
311
- aliasEntries.push([moduleKey, normalizedExports]);
346
+ pushAlias(moduleKey, normalizedExports);
312
347
  for (const normalizedEntry of Object.values(normalizedExports)) {
313
348
  const aliasKey = String(normalizedEntry.id);
314
- aliasEntries.push([aliasKey, normalizedExports]);
349
+ pushAlias(aliasKey, normalizedExports);
315
350
  }
316
351
  }
317
352
  for (const [aliasKey, exportsMap] of aliasEntries) {
@@ -688,6 +723,55 @@ function wrapInDocumentShell(bodyContent, metadataHtml, chunkFiles, rootMode) {
688
723
  </body>
689
724
  </html>`;
690
725
  }
726
+ function normalizeWebpackErrors(stats) {
727
+ const errors = stats.toJson().errors || [];
728
+ const normalizedErrors = errors
729
+ .map((entry) => {
730
+ if (typeof entry === 'string')
731
+ return entry;
732
+ if (entry && typeof entry.message === 'string')
733
+ return entry.message;
734
+ return String(entry || '');
735
+ })
736
+ .filter((entry) => entry.trim().length > 0);
737
+ if (normalizedErrors.length > 0) {
738
+ return normalizedErrors;
739
+ }
740
+ return ['Unknown build error.'];
741
+ }
742
+ function renderCompilePendingHTML() {
743
+ return `<!DOCTYPE html>
744
+ <html lang="en">
745
+ <head>
746
+ <meta charset="utf-8" />
747
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
748
+ <title>Compiling...</title>
749
+ <style>
750
+ html,body{height:100%;margin:0}
751
+ body{
752
+ display:grid;
753
+ place-items:center;
754
+ font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
755
+ background:#09090b;
756
+ color:#f4f4f5;
757
+ }
758
+ .vista-dev-pending{
759
+ padding:20px 24px;
760
+ border-radius:14px;
761
+ border:1px solid rgba(255,255,255,0.15);
762
+ background:rgba(17,17,20,0.92);
763
+ box-shadow:0 18px 42px rgba(0,0,0,0.4);
764
+ font-size:14px;
765
+ letter-spacing:0.01em;
766
+ }
767
+ </style>
768
+ </head>
769
+ <body>
770
+ <div class="vista-dev-pending">Vista is compiling the client bundle. Retrying...</div>
771
+ <script>setTimeout(function(){window.location.reload();},700);</script>
772
+ </body>
773
+ </html>`;
774
+ }
691
775
  function startRSCServer(options = {}) {
692
776
  const app = (0, express_1.default)();
693
777
  const cwd = process.cwd();
@@ -847,6 +931,8 @@ function startRSCServer(options = {}) {
847
931
  // - Pushes compile errors/success from webpack client build
848
932
  // ========================================================================
849
933
  const sseReloadClients = new Set();
934
+ let clientCompileState = isDev && options.compiler ? 'compiling' : 'ready';
935
+ let clientCompileErrors = [];
850
936
  if (isDev) {
851
937
  app.get(constants_1.SSE_ENDPOINT, (req, res) => {
852
938
  res.setHeader('Content-Type', 'text/event-stream');
@@ -935,15 +1021,27 @@ function startRSCServer(options = {}) {
935
1021
  writeToDisk: true,
936
1022
  }));
937
1023
  // No webpack-hot-middleware — Vista uses SSE live-reload for RSC
1024
+ options.compiler.hooks.invalid.tap('VistaRSCCompileStateInvalid', () => {
1025
+ clientCompileState = 'compiling';
1026
+ clientCompileErrors = [];
1027
+ });
938
1028
  // Push compile errors/success to SSE clients
939
1029
  options.compiler.hooks.done.tap('VistaRSCLiveReload', (stats) => {
940
1030
  if (stats.hasErrors()) {
941
- const errors = stats.toJson().errors || [];
942
- const msg = errors.map((e) => (typeof e === 'string' ? e : e.message)).join('\n');
943
- const payload = JSON.stringify({ type: 'error', message: msg });
1031
+ const normalizedErrors = normalizeWebpackErrors(stats);
1032
+ const fallback = normalizedErrors.join('\n\n');
1033
+ clientCompileState = 'error';
1034
+ clientCompileErrors = normalizedErrors;
1035
+ const payload = JSON.stringify({
1036
+ type: 'error',
1037
+ message: fallback,
1038
+ errors: normalizedErrors,
1039
+ });
944
1040
  sseReloadClients.forEach((c) => c.write(`data: ${payload}\n\n`));
945
1041
  }
946
1042
  else {
1043
+ clientCompileState = 'ready';
1044
+ clientCompileErrors = [];
947
1045
  const payload = JSON.stringify({ type: 'ok' });
948
1046
  sseReloadClients.forEach((c) => c.write(`data: ${payload}\n\n`));
949
1047
  }
@@ -1031,6 +1129,16 @@ function startRSCServer(options = {}) {
1031
1129
  app.use(constants_1.URL_PREFIX, express_1.default.static(path_1.default.join(cwd, constants_1.BUILD_DIR)));
1032
1130
  app.use(express_1.default.static(path_1.default.join(cwd, constants_1.BUILD_DIR)));
1033
1131
  const proxyRSCRequest = async (req, res) => {
1132
+ if (isDev && options.compiler) {
1133
+ if (clientCompileState === 'compiling') {
1134
+ res.status(503).type('text/plain').send('[vista] Client bundle is compiling. Retry shortly.');
1135
+ return;
1136
+ }
1137
+ if (clientCompileState === 'error') {
1138
+ res.status(500).type('text/plain').send(clientCompileErrors.join('\n\n'));
1139
+ return;
1140
+ }
1141
+ }
1034
1142
  try {
1035
1143
  const fetchOptions = {
1036
1144
  method: req.method,
@@ -1122,6 +1230,19 @@ function startRSCServer(options = {}) {
1122
1230
  .send((0, dev_error_1.renderErrorHTML)([errorInfo]));
1123
1231
  return;
1124
1232
  }
1233
+ if (isDev && options.compiler) {
1234
+ if (clientCompileState === 'compiling') {
1235
+ res.status(503).type('text/html').send(renderCompilePendingHTML());
1236
+ return;
1237
+ }
1238
+ if (clientCompileState === 'error') {
1239
+ const errorInfos = (clientCompileErrors.length > 0
1240
+ ? clientCompileErrors
1241
+ : ['Unknown client build error.']).map((message) => ({ type: 'build', message }));
1242
+ res.status(500).type('text/html').send((0, dev_error_1.renderErrorHTML)(errorInfos));
1243
+ return;
1244
+ }
1245
+ }
1125
1246
  if (req.path.startsWith('/api/')) {
1126
1247
  await handleApiRoute(req, res, cwd, isDev, typedApiConfig);
1127
1248
  return;
@@ -16,6 +16,7 @@ exports.generateStaticPages = generateStaticPages;
16
16
  exports.revalidatePath = revalidatePath;
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const fs_1 = __importDefault(require("fs"));
19
+ const constants_1 = require("../constants");
19
20
  const static_cache_1 = require("./static-cache");
20
21
  const CjsModule = require('module');
21
22
  let staticRuntimeReady = false;
@@ -181,6 +182,21 @@ async function prerenderPage(urlPath, route, params, cwd) {
181
182
  try {
182
183
  const React = require('react');
183
184
  const { renderToString } = require('react-dom/server');
185
+ const isAsyncComponent = (component) => {
186
+ return (typeof component === 'function' &&
187
+ component.constructor &&
188
+ component.constructor.name === 'AsyncFunction');
189
+ };
190
+ const renderComponent = async (component, props, child) => {
191
+ if (isAsyncComponent(component)) {
192
+ const asyncProps = child === undefined ? props : { ...props, children: child };
193
+ return component(asyncProps);
194
+ }
195
+ if (child === undefined) {
196
+ return React.createElement(component, props);
197
+ }
198
+ return React.createElement(component, props, child);
199
+ };
184
200
  // Load page component from webpack-built server bundle
185
201
  const pageModule = require(route.pagePath);
186
202
  const PageComponent = pageModule.default;
@@ -188,15 +204,50 @@ async function prerenderPage(urlPath, route, params, cwd) {
188
204
  console.warn(`[vista:ssg] No default export in ${route.pagePath}`);
189
205
  return null;
190
206
  }
207
+ let metadata = {};
208
+ const searchParams = {};
209
+ for (const layoutPath of route.layoutPaths) {
210
+ try {
211
+ const layoutModule = require(layoutPath);
212
+ if (layoutModule?.metadata && typeof layoutModule.metadata === 'object') {
213
+ metadata = { ...metadata, ...layoutModule.metadata };
214
+ }
215
+ }
216
+ catch {
217
+ // Ignore layout metadata failures for static generation.
218
+ }
219
+ }
220
+ if (pageModule.metadata && typeof pageModule.metadata === 'object') {
221
+ metadata = { ...metadata, ...pageModule.metadata };
222
+ }
223
+ if (typeof pageModule.generateMetadata === 'function') {
224
+ try {
225
+ const dynamicMeta = await pageModule.generateMetadata({ params: params || {}, searchParams }, metadata);
226
+ if (dynamicMeta && typeof dynamicMeta === 'object') {
227
+ metadata = { ...metadata, ...dynamicMeta };
228
+ }
229
+ }
230
+ catch (metadataError) {
231
+ console.warn(`[vista:ssg] generateMetadata failed for ${urlPath}:`, metadataError?.message || String(metadataError));
232
+ }
233
+ }
234
+ let metadataHtml = '';
235
+ try {
236
+ const { generateMetadataHtml } = require('../metadata/generate');
237
+ metadataHtml = generateMetadataHtml(metadata);
238
+ }
239
+ catch {
240
+ metadataHtml = '';
241
+ }
191
242
  // Build the element, passing params as props
192
- let element = React.createElement(PageComponent, { params: params || {} });
243
+ let element = await renderComponent(PageComponent, { params: params || {} });
193
244
  // Wrap in layouts (outside-in)
194
245
  for (let i = route.layoutPaths.length - 1; i >= 0; i--) {
195
246
  try {
196
247
  const layoutModule = require(route.layoutPaths[i]);
197
248
  const LayoutComponent = layoutModule.default;
198
249
  if (LayoutComponent) {
199
- element = React.createElement(LayoutComponent, { params: params || {}, searchParams: {} }, element);
250
+ element = await renderComponent(LayoutComponent, { params: params || {}, searchParams: {} }, element);
200
251
  }
201
252
  }
202
253
  catch {
@@ -206,7 +257,7 @@ async function prerenderPage(urlPath, route, params, cwd) {
206
257
  // Render to HTML string
207
258
  const html = renderToString(element);
208
259
  return {
209
- html: wrapInDocument(html, urlPath),
260
+ html: wrapInDocument(html, urlPath, metadataHtml, cwd),
210
261
  generatedAt: Date.now(),
211
262
  revalidate: route.revalidate || 0,
212
263
  routePattern: route.pattern,
@@ -221,17 +272,49 @@ async function prerenderPage(urlPath, route, params, cwd) {
221
272
  /**
222
273
  * Wrap rendered HTML in a basic document shell.
223
274
  */
224
- function wrapInDocument(bodyHtml, _urlPath) {
225
- return `<!DOCTYPE html>
226
- <html lang="en">
227
- <head>
228
- <meta charset="utf-8" />
229
- <meta name="viewport" content="width=device-width, initial-scale=1" />
230
- <link rel="stylesheet" href="/styles.css" />
231
- </head>
232
- <body>
233
- <div id="root">${bodyHtml}</div>
234
- </body>
275
+ function injectBeforeClosingTag(html, tagName, injection) {
276
+ const closeTag = `</${tagName}>`;
277
+ if (html.includes(closeTag)) {
278
+ return html.replace(closeTag, `${injection}\n${closeTag}`);
279
+ }
280
+ return html;
281
+ }
282
+ function getCSSLinks(cwd) {
283
+ const links = ['<link rel="stylesheet" href="/styles.css" />'];
284
+ const chunksDir = path_1.default.join(cwd, constants_1.BUILD_DIR, 'static', 'chunks');
285
+ try {
286
+ if (fs_1.default.existsSync(chunksDir)) {
287
+ const files = fs_1.default.readdirSync(chunksDir).filter((entry) => entry.endsWith('.css'));
288
+ for (const file of files) {
289
+ links.push(`<link rel="stylesheet" href="${constants_1.STATIC_CHUNKS_PATH}${file}" />`);
290
+ }
291
+ }
292
+ }
293
+ catch {
294
+ // Ignore CSS discovery failures during static generation.
295
+ }
296
+ return links.join('\n ');
297
+ }
298
+ function wrapInDocument(bodyHtml, _urlPath, metadataHtml, cwd) {
299
+ const headInjection = `\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n ${metadataHtml}\n ${getCSSLinks(cwd)}`;
300
+ const hasDocumentMarkup = /<html(?:\s|>)/i.test(bodyHtml) && /<\/html>/i.test(bodyHtml);
301
+ if (hasDocumentMarkup) {
302
+ const htmlStart = bodyHtml.search(/<html(?:\s|>)/i);
303
+ let html = htmlStart > 0 ? bodyHtml.slice(htmlStart) : bodyHtml;
304
+ if (!/^\s*<!doctype html>/i.test(html)) {
305
+ html = `<!DOCTYPE html>\n${html}`;
306
+ }
307
+ html = injectBeforeClosingTag(html, 'head', headInjection);
308
+ return html;
309
+ }
310
+ return `<!DOCTYPE html>
311
+ <html lang="en">
312
+ <head>
313
+ ${headInjection}
314
+ </head>
315
+ <body>
316
+ <div id="root">${bodyHtml}</div>
317
+ </body>
235
318
  </html>`;
236
319
  }
237
320
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vistagenic/vista",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "The React Framework for Visionaries - Rust-powered SSR with Server Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -132,6 +132,5 @@
132
132
  "@types/webpack": "^5.28.5",
133
133
  "@types/webpack-hot-middleware": "^2.25.9",
134
134
  "typescript": "^5.7.2"
135
- },
136
- "gitHead": "9fac2fe1ed23e51248ba8bcecbd623b9e3a3b520"
135
+ }
137
136
  }