arcway 0.4.13 → 0.4.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -118,6 +118,29 @@ function createPagesHandler(options) {
118
118
  if (!reloadEmitter) return;
119
119
  reloadEmitter.emit('update', event);
120
120
  }
121
+ async function renderRuntimeError(err, res) {
122
+ if (manifest.errorBundle && !res.headersSent) {
123
+ const errorMessage = devMode
124
+ ? err instanceof Error
125
+ ? err.stack || err.message
126
+ : String(err)
127
+ : 'An unexpected error occurred';
128
+ await renderErrorPage(
129
+ manifest.errorBundle,
130
+ 500,
131
+ { error: errorMessage },
132
+ outDir,
133
+ res,
134
+ componentCache,
135
+ manifest,
136
+ cacheVersion,
137
+ projectReact,
138
+ );
139
+ } else if (!res.headersSent) {
140
+ res.writeHead(500, getHtmlHeaders());
141
+ res.end('<h1>500 - Internal Server Error</h1>');
142
+ }
143
+ }
121
144
  async function handle(req, res) {
122
145
  const method = req.method ?? 'GET';
123
146
  const url = req.url ?? '/';
@@ -237,29 +260,10 @@ function createPagesHandler(options) {
237
260
  projectReact,
238
261
  devMode,
239
262
  viteDev,
263
+ (err) => renderRuntimeError(err, res),
240
264
  );
241
265
  } catch (err) {
242
- if (manifest.errorBundle && !res.headersSent) {
243
- const errorMessage = devMode
244
- ? err instanceof Error
245
- ? err.stack || err.message
246
- : String(err)
247
- : 'An unexpected error occurred';
248
- await renderErrorPage(
249
- manifest.errorBundle,
250
- 500,
251
- { error: errorMessage },
252
- outDir,
253
- res,
254
- componentCache,
255
- manifest,
256
- cacheVersion,
257
- projectReact,
258
- );
259
- } else if (!res.headersSent) {
260
- res.writeHead(500, getHtmlHeaders());
261
- res.end('<h1>500 - Internal Server Error</h1>');
262
- }
266
+ await renderRuntimeError(err, res);
263
267
  }
264
268
  return true;
265
269
  }
@@ -381,6 +381,26 @@ async function createLazyPagesContext(options) {
381
381
  return /^(components|hooks|client|packages)\//.test(rel);
382
382
  }
383
383
 
384
+ function isSpecialPageSource(filePath) {
385
+ const absolutePath = path.resolve(filePath);
386
+ return [errorPages.errorPage, errorPages.notFoundPage]
387
+ .filter(Boolean)
388
+ .some((pagePath) => path.resolve(pagePath) === absolutePath);
389
+ }
390
+
391
+ async function rebuildErrorPages() {
392
+ const errorBundles = await buildErrorPageBundles(errorPages, outDir, serverTarget, {
393
+ rootDir,
394
+ devMode,
395
+ minify,
396
+ limit,
397
+ });
398
+ if (errorBundles.error) manifest.errorBundle = errorBundles.error;
399
+ else delete manifest.errorBundle;
400
+ if (errorBundles.notFound) manifest.notFoundBundle = errorBundles.notFound;
401
+ else delete manifest.notFoundBundle;
402
+ }
403
+
384
404
  function markAffectedStale(filePath) {
385
405
  const affected = mapFileToAffected(filePath, manifest);
386
406
  if (shouldInvalidateWholeTree(filePath, affected)) {
@@ -433,12 +453,21 @@ async function createLazyPagesContext(options) {
433
453
 
434
454
  async function handleSourceChange(filePath) {
435
455
  if (disposed) throw new Error('Lazy pages context is disposed');
456
+ const specialPageSource = isSpecialPageSource(filePath);
457
+ const rebuildSpecialPages =
458
+ specialPageSource ||
459
+ shouldInvalidateWholeTree(filePath, { pages: [], layouts: [], middlewares: [] });
436
460
  const { touched, affected } = markAffectedStale(filePath);
437
- if (!touched) return { touched: false, affected, hmr: null };
461
+ if (!touched && !rebuildSpecialPages) return { touched: false, affected, hmr: null };
438
462
  if (viteDev) {
463
+ if (rebuildSpecialPages) await rebuildErrorPages();
439
464
  bumpVersion();
440
465
  emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
441
- return { touched: true, affected, hmr: null };
466
+ const hmr = specialPageSource ? { type: 'reload', timestamp: Date.now() } : null;
467
+ if (hmr) {
468
+ emit('update', { ...hmr, filePath, reason: 'special-page', version: manifest.version });
469
+ }
470
+ return { touched: true, affected, hmr };
442
471
  }
443
472
  if (inFlightClientRefresh) return inFlightClientRefresh;
444
473
 
@@ -449,6 +478,7 @@ async function createLazyPagesContext(options) {
449
478
  const [clientResult, cssBundle] = await Promise.all([
450
479
  clientCtx?.rebuild() ?? Promise.resolve(null),
451
480
  buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss, rootDir),
481
+ rebuildSpecialPages ? rebuildErrorPages() : Promise.resolve(),
452
482
  ]);
453
483
 
454
484
  if (clientResult) {
@@ -459,8 +489,15 @@ async function createLazyPagesContext(options) {
459
489
  bumpVersion();
460
490
  emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
461
491
 
462
- let hmr = null;
463
- if (clientResult && previousMetafile) {
492
+ let hmr = specialPageSource ? { type: 'reload', timestamp } : null;
493
+ if (hmr) {
494
+ emit('update', {
495
+ ...hmr,
496
+ filePath,
497
+ reason: 'special-page',
498
+ version: manifest.version,
499
+ });
500
+ } else if (clientResult && previousMetafile) {
464
501
  const diff = diffClientMetafiles(previousMetafile, clientResult.metafile, outDir);
465
502
  const modules = [...diff.changedHydrationBundles, ...diff.changedNavBundles];
466
503
  const routeBundles = {};
@@ -19,10 +19,7 @@ function syncReactInternals(projectReactModule) {
19
19
  const projectInternals = projectReactModule[REACT_INTERNALS_KEY];
20
20
  const fwInternals = fwReact[REACT_INTERNALS_KEY];
21
21
  if (projectInternals && fwInternals && projectInternals !== fwInternals) {
22
- const keys = new Set([
23
- ...Object.keys(projectInternals),
24
- ...Object.keys(fwInternals),
25
- ]);
22
+ const keys = new Set([...Object.keys(projectInternals), ...Object.keys(fwInternals)]);
26
23
  for (const key of keys) {
27
24
  Object.defineProperty(fwInternals, key, {
28
25
  get() {
@@ -51,7 +48,10 @@ async function loadComponent(bundlePath, cacheKey, componentCache, cacheVersion)
51
48
  componentCache.set(cacheKey, Component);
52
49
  return Component;
53
50
  } catch (error) {
54
- console.error(`[arcway] failed to load component ${cacheKey} from ${bundlePath}:`, error && error.stack ? error.stack : error);
51
+ console.error(
52
+ `[arcway] failed to load component ${cacheKey} from ${bundlePath}:`,
53
+ error && error.stack ? error.stack : error,
54
+ );
55
55
  // Component failed to load (missing file, syntax error) — caller handles null
56
56
  return null;
57
57
  }
@@ -152,6 +152,7 @@ async function renderPage(
152
152
  react,
153
153
  devMode,
154
154
  viteDev = false,
155
+ onRenderFailure = null,
155
156
  ) {
156
157
  const { createElement, renderToPipeableStream } = react;
157
158
  const bundlePath = path.join(outDir, route.serverBundle);
@@ -198,9 +199,16 @@ async function renderPage(
198
199
  const { PassThrough } = await import('node:stream');
199
200
  let pass = null;
200
201
  let closed = false;
202
+ let renderFailureHandled = false;
203
+ let resolveRender;
204
+ const renderComplete = new Promise((resolve) => {
205
+ resolveRender = resolve;
206
+ });
201
207
  const { pipe, abort } = renderToPipeableStream(element, {
202
208
  onAllReady() {
203
- if (res.headersSent || closed) return;
209
+ // React may call onAllReady after onShellError but before the recovery
210
+ // promise starts. Never let the failed stream win that race and commit 200.
211
+ if (res.headersSent || closed || renderFailureHandled) return;
204
212
  res.writeHead(200, getHtmlHeaders());
205
213
  const headHtml = renderHeadToString(headData);
206
214
  const shell = buildHtmlShell({
@@ -225,19 +233,38 @@ ${liveReloadTag}`,
225
233
  res.end();
226
234
  }
227
235
  clearSSRHeadData();
236
+ resolveRender();
228
237
  });
229
238
  pipe(pass);
230
239
  },
231
240
  onError(err) {
232
- clearSSRHeadData();
233
241
  // A client disconnect aborts the render and surfaces here; that's a normal
234
242
  // peer disconnect, not a server fault — don't log it or write to a dead res.
235
243
  if (closed) return;
236
244
  console.error('SSR render error:', err);
237
- if (!res.headersSent) {
245
+ },
246
+ onShellError(err) {
247
+ clearSSRHeadData();
248
+ if (closed || res.headersSent || renderFailureHandled) return;
249
+ renderFailureHandled = true;
250
+
251
+ if (!onRenderFailure) {
238
252
  res.writeHead(500, getHtmlHeaders());
239
253
  res.end('<h1>500 - Render Error</h1>');
254
+ resolveRender();
255
+ return;
240
256
  }
257
+
258
+ Promise.resolve()
259
+ .then(() => onRenderFailure(err))
260
+ .catch((renderError) => {
261
+ console.error('SSR error-page render error:', renderError);
262
+ if (!closed && !res.headersSent) {
263
+ res.writeHead(500, getHtmlHeaders());
264
+ res.end('<h1>500 - Internal Server Error</h1>');
265
+ }
266
+ })
267
+ .finally(resolveRender);
241
268
  },
242
269
  });
243
270
  // Client gone (mid-stream, or before the shell was ready): stop React from
@@ -247,7 +274,9 @@ ${liveReloadTag}`,
247
274
  closed = true;
248
275
  abort();
249
276
  if (pass) pass.destroy();
277
+ resolveRender();
250
278
  });
279
+ await renderComplete;
251
280
  }
252
281
  async function renderErrorPage(
253
282
  bundlePath,
@@ -289,6 +318,10 @@ async function renderErrorPage(
289
318
  const { PassThrough } = await import('node:stream');
290
319
  let pass = null;
291
320
  let closed = false;
321
+ let resolveRender;
322
+ const renderComplete = new Promise((resolve) => {
323
+ resolveRender = resolve;
324
+ });
292
325
  const { pipe, abort } = renderToPipeableStream(element, {
293
326
  onAllReady() {
294
327
  if (res.headersSent || closed) return;
@@ -306,6 +339,7 @@ async function renderErrorPage(
306
339
  res.end();
307
340
  }
308
341
  clearSSRHeadData();
342
+ resolveRender();
309
343
  });
310
344
  pipe(pass);
311
345
  },
@@ -319,6 +353,7 @@ async function renderErrorPage(
319
353
  `<h1>${statusCode} - ${statusCode === 404 ? 'Not Found' : 'Internal Server Error'}</h1>`,
320
354
  );
321
355
  }
356
+ resolveRender();
322
357
  },
323
358
  });
324
359
  res.on('close', () => {
@@ -326,7 +361,9 @@ async function renderErrorPage(
326
361
  closed = true;
327
362
  abort();
328
363
  if (pass) pass.destroy();
364
+ resolveRender();
329
365
  });
366
+ await renderComplete;
330
367
  }
331
368
  export {
332
369
  buildCssLinkTag,