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 +1 -1
- package/server/pages/handler.js +25 -21
- package/server/pages/lazy-context.js +41 -4
- package/server/pages/ssr.js +45 -8
package/package.json
CHANGED
package/server/pages/handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 = {};
|
package/server/pages/ssr.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|