@thomas.f/prnext 0.1.17-alpha → 0.1.18-alpha
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/README.md +26 -869
- package/build/actions.mjs +1 -1
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +81 -26
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +31 -8
- package/build/config.mjs +30 -1
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-update.mjs +4 -2
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/index.mjs +59 -26
- package/build/instrumentation.mjs +30 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +58 -9
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +6 -3
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +60 -4
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +68 -3
- package/compat/link.cjs +12 -9
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +171 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +82 -0
- package/docs/ai/troubleshooting.md +37 -2
- package/package.json +26 -7
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +212 -65
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/development.mjs +29 -12
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
package/runtime/app-prefetch.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Prefetch contains only prerendered segments. Request-dependent holes are
|
|
2
2
|
// resolved by a fresh, authenticated navigation; they never enter this cache.
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
export const PARTIAL_PREFETCH_CACHE_BYTES = 2 * 1024 * 1024;
|
|
3
|
+
import { PARTIAL_PREFETCH_BYTES } from './app-prefetch-constants.mjs';
|
|
4
|
+
export { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_BYTES, PARTIAL_PREFETCH_CACHE_BYTES } from './app-prefetch-constants.mjs';
|
|
6
5
|
|
|
7
6
|
export async function readPartialPrefetch(response) {
|
|
8
7
|
const reader = response.body.getReader();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Local counters can repeat in independent prerender processes. Cached Flight
|
|
2
|
+
// must not give different documents the same error/navigation reset identity.
|
|
3
|
+
export function renderKeyFor(request, id) {
|
|
4
|
+
return JSON.stringify([new URL(request.originalUrl || request.url || 'http://localhost/').pathname, id]);
|
|
5
|
+
}
|
package/runtime/app-render.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
|
|
2
|
+
import {currentTraceparent} from './trace.mjs';
|
|
1
3
|
import React from 'react';
|
|
4
|
+
import { randomUUID, createHmac, randomBytes } from 'node:crypto';
|
|
2
5
|
import diagnostics from '../compat/diagnostics.cjs';
|
|
3
|
-
import {
|
|
6
|
+
import { rscResourceLimits } from './profile.mjs';
|
|
4
7
|
import { fontPreloads } from './font-preload.mjs';
|
|
5
8
|
import { advancedRoutingSources } from './app-routing-state.mjs';
|
|
6
9
|
import { addBasePath, removeBasePath } from '../compat/paths.cjs';
|
|
@@ -29,6 +32,8 @@ import { isDraftRequest } from '../compat/draft.cjs';
|
|
|
29
32
|
import { createRscLifecycle } from './rsc-lifecycle.mjs';
|
|
30
33
|
import { trackRequestWork } from './request-work.mjs';
|
|
31
34
|
import { createRscWorkWatchdog } from './rsc-work-watchdog.mjs';
|
|
35
|
+
import {assertHtmlOnlyDocument} from './html-only.mjs';
|
|
36
|
+
import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
|
|
32
37
|
|
|
33
38
|
installFetchCache();
|
|
34
39
|
|
|
@@ -38,6 +43,7 @@ const metadata = new WeakMap();
|
|
|
38
43
|
const artifacts = new WeakMap();
|
|
39
44
|
const edgeManifests = new WeakMap();
|
|
40
45
|
const emptyManifest = Object.freeze({});
|
|
46
|
+
const developmentAuthorizationSecret = randomBytes(32).toString('hex');
|
|
41
47
|
let nextId = 0;
|
|
42
48
|
const pending = new Map();
|
|
43
49
|
const noop = () => {};
|
|
@@ -54,7 +60,7 @@ const ssrModuleLoaders = new Map();
|
|
|
54
60
|
const ssrManifests = new WeakMap();
|
|
55
61
|
const require = createRequire(import.meta.url);
|
|
56
62
|
const flightDecoders = new Map();
|
|
57
|
-
let htmlRuntime, recoveryRuntime, domRuntime;
|
|
63
|
+
let htmlRuntime, recoveryRuntime, domRuntime, staticRuntime;
|
|
58
64
|
async function renderProgressiveAppHtml(options) {
|
|
59
65
|
htmlRuntime ??= await import('./app-stream-html.mjs');
|
|
60
66
|
return htmlRuntime.renderProgressiveAppHtml(options);
|
|
@@ -108,7 +114,7 @@ function getWorker(production) {
|
|
|
108
114
|
execArgv: ['--conditions=react-server'], stdout: true, stderr: true,
|
|
109
115
|
// Production nursery policy; an explicit Node semi-space flag takes
|
|
110
116
|
// precedence. This does not cap the old generation or process RSS.
|
|
111
|
-
|
|
117
|
+
resourceLimits:rscResourceLimits(process.env,production),
|
|
112
118
|
env: { ...process.env, NODE_ENV: production ? 'production' : 'development' },
|
|
113
119
|
});
|
|
114
120
|
workers.set(production, current);
|
|
@@ -155,7 +161,7 @@ function getWorker(production) {
|
|
|
155
161
|
request.reject(error);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
|
-
current.on('message', ({ id, type, token, chunk, byteOffset, byteLength, done, body, status, rscError, boundaryIndex, headers, error, staticParams, staticMetadata, navigation }) => {
|
|
164
|
+
current.on('message', ({ id, type, token, chunk, byteOffset, byteLength, done, body, status, rscError, boundaryIndex, headers, error, staticParams, staticMetadata, navigation, value }) => {
|
|
159
165
|
if (type === 'work-responsive') { workWatchdog.ack(token); return; }
|
|
160
166
|
const request = pending.get(id);
|
|
161
167
|
if (!request || request.worker !== current) return;
|
|
@@ -164,6 +170,7 @@ function getWorker(production) {
|
|
|
164
170
|
if (error?.retireWorker) {
|
|
165
171
|
void lifecycle.terminate(new Error(error.message)).catch(error => console.error('[prnext] RSC worker termination failed:', error));
|
|
166
172
|
}
|
|
173
|
+
if(type==='instrumentation-error'){if(typeof value==='string'&&value.length<=1024){request.reportedDigests??=new Set();if(request.reportedDigests.size<32)request.reportedDigests.add(value);}return;}
|
|
167
174
|
if (request.cancelled && type !== 'settled') return;
|
|
168
175
|
if (type === 'settled') {
|
|
169
176
|
pending.delete(id); clearTimeout(request.timeout);
|
|
@@ -178,7 +185,7 @@ function getWorker(production) {
|
|
|
178
185
|
}
|
|
179
186
|
if (type === 'start') {
|
|
180
187
|
if (chunk) request.channel.chunk(chunk, byteOffset, byteLength);
|
|
181
|
-
request.resolve({ body: request.channel.body, status: status || 200, rscError, boundaryIndex, headers });
|
|
188
|
+
request.resolve({ body: request.channel.body, status: status || 200, rscError, reportedDigests:request.reportedDigests??=new Set(), boundaryIndex, headers });
|
|
182
189
|
if (!done) return;
|
|
183
190
|
}
|
|
184
191
|
if (type === 'chunk') {
|
|
@@ -202,6 +209,7 @@ function getWorker(production) {
|
|
|
202
209
|
if (type === 'end' || done) request.channel.end();
|
|
203
210
|
else if (error) {
|
|
204
211
|
const failure = new Error(error.message);
|
|
212
|
+
if(error.instrumentationReported===true)failure.instrumentationReported=true;
|
|
205
213
|
if (error.stack) failure.stack = error.stack;
|
|
206
214
|
if (error.digest) failure.digest = error.digest;
|
|
207
215
|
if ([400, 504].includes(error.statusCode)) failure.statusCode = error.statusCode;
|
|
@@ -217,7 +225,7 @@ function getWorker(production) {
|
|
|
217
225
|
request.reject(new Error('Invalid App Router Flight response buffer'));
|
|
218
226
|
} else {
|
|
219
227
|
// ArrayBuffer overload: a view of the transferred allocation, no copy.
|
|
220
|
-
request.resolve({ body: Buffer.from(body), status: status || 200, rscError, boundaryIndex, headers, staticMetadata, navigation });
|
|
228
|
+
request.resolve({ body: Buffer.from(body), status: status || 200, rscError, reportedDigests:request.reportedDigests??=new Set(), boundaryIndex, headers, staticMetadata, navigation });
|
|
221
229
|
}
|
|
222
230
|
// Keep only settlement bookkeeping after sending the body. The
|
|
223
231
|
// isolate can still be flushing cache writes after its terminal frame.
|
|
@@ -233,6 +241,10 @@ function getWorker(production) {
|
|
|
233
241
|
current.unref();
|
|
234
242
|
return current;
|
|
235
243
|
}
|
|
244
|
+
export async function prewarmAppRenderer(options){
|
|
245
|
+
flightDecoder(true);
|
|
246
|
+
await preloadAppRoutes([],options);
|
|
247
|
+
}
|
|
236
248
|
export async function preloadAppRoutes(files,options){
|
|
237
249
|
const worker=getWorker(true),id=`preload-${++nextId}`;
|
|
238
250
|
worker.ref();
|
|
@@ -252,6 +264,12 @@ export async function preloadAppRoutes(files,options){
|
|
|
252
264
|
// incidental event-loop liveness or terminate unfinished application work.
|
|
253
265
|
export async function drainAppRuntime() {
|
|
254
266
|
while (pending.size) await Promise.all([...pending.values()].map(request => request.settled));
|
|
267
|
+
if(process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||process.env.OTEL_EXPORTER_OTLP_ENDPOINT)await Promise.all([...workers.values()].map(worker=>new Promise(resolve=>{
|
|
268
|
+
const token=randomUUID();let timer;
|
|
269
|
+
const clean=()=>{clearTimeout(timer);worker.off('message',message);worker.off('exit',clean);resolve();};
|
|
270
|
+
const message=value=>{if(value.type==='telemetry-flushed'&&value.token===token)clean();};
|
|
271
|
+
worker.on('message',message);worker.once('exit',clean);timer=setTimeout(clean,3000);worker.postMessage({type:'telemetry-flush',token});
|
|
272
|
+
})));
|
|
255
273
|
await closeAppRuntime();
|
|
256
274
|
}
|
|
257
275
|
|
|
@@ -278,7 +296,8 @@ function requestDeadline(entry, lifecycle, id, hardTimeoutMs, delayMs) {
|
|
|
278
296
|
}, delayMs);
|
|
279
297
|
}
|
|
280
298
|
|
|
281
|
-
export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, signal, deferFinalizer } = {}) {
|
|
299
|
+
export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, signal, deferFinalizer, settleBeforeReturn = false } = {}) {
|
|
300
|
+
if(settleBeforeReturn&&request.stream)throw new Error('Streaming Flight must be consumed before waiting for settlement.');
|
|
282
301
|
softTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 25_000;
|
|
283
302
|
hardTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 28_000;
|
|
284
303
|
const deadline = performance.now() + hardTimeoutMs;
|
|
@@ -304,7 +323,11 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
|
|
|
304
323
|
let settle;
|
|
305
324
|
const settled = new Promise(resolve => { settle = resolve; });
|
|
306
325
|
trackRequestWork(settled, signal, { deadline, supervised: true });
|
|
307
|
-
|
|
326
|
+
// Build callers may close this isolate as soon as their last path returns.
|
|
327
|
+
// Its terminal Flight frame precedes after()/cache completion; keep that
|
|
328
|
+
// path outstanding until the worker acknowledges bounded settlement.
|
|
329
|
+
const resolveResult=settleBeforeReturn?value=>{void settled.then(()=>resolve(value));}:resolve;
|
|
330
|
+
const entry = { worker: current, resolve:resolveResult, reject, operation: request.operation, lease, settle, settled };
|
|
308
331
|
const timeout = entry.timeout = requestDeadline(entry, lifecycle, id, hardTimeoutMs, Math.max(0, deadline - performance.now()));
|
|
309
332
|
function cancel(reason) {
|
|
310
333
|
if (entry.cancelled) return;
|
|
@@ -336,7 +359,7 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
|
|
|
336
359
|
pending.set(id, entry);
|
|
337
360
|
try {
|
|
338
361
|
const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
|
|
339
|
-
if (request.
|
|
362
|
+
if (request.cacheConfig?.runtime !== 'edge' && deferFinalizer) {
|
|
340
363
|
wire.waitForResponse = true;
|
|
341
364
|
deferFinalizer(() => { if (pending.has(id)) current.postMessage({ id, type: 'after-response' }); });
|
|
342
365
|
}
|
|
@@ -395,15 +418,17 @@ export async function decodeFlight(body, clientModules = {}, distDir, { producti
|
|
|
395
418
|
return decoder.createFromNodeStream(body instanceof ReadableStream ? Readable.fromWeb(body) : Readable.from([body]), serverConsumerManifest);
|
|
396
419
|
}
|
|
397
420
|
|
|
398
|
-
export async function renderHtml(tree, router, { timeoutMs = 25_000, formState = null, staticGeneration = false, nonce, strictMode = true } = {}) {
|
|
421
|
+
export async function renderHtml(tree, router, { timeoutMs = 25_000, formState = null, staticGeneration = false, nonce, strictMode = true, htmlOnly = false, reportedDigests } = {}) {
|
|
399
422
|
const abort = new AbortController();
|
|
400
423
|
const timeout = setTimeout(() => abort.abort(appTimeoutError('HTML render', timeoutMs)), timeoutMs);
|
|
401
424
|
let renderError;
|
|
402
425
|
try {
|
|
403
426
|
domRuntime ??= await import('react-dom/server');
|
|
404
|
-
const
|
|
427
|
+
const element = React.createElement(strictMode ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router, nonce }, appContent(tree)));
|
|
428
|
+
const renderOptions = {
|
|
405
429
|
nonce,
|
|
406
430
|
signal: abort.signal, onError(error) {
|
|
431
|
+
if(!abort.signal.aborted){if(reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);}
|
|
407
432
|
if (isDynamicBailout(error)) return error.digest;
|
|
408
433
|
// Suspense can recover client SSR failures by emitting its fallback and
|
|
409
434
|
// retrying in the browser. Static generation must still reject them:
|
|
@@ -413,9 +438,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
|
|
|
413
438
|
return appErrorDigest(error);
|
|
414
439
|
},
|
|
415
440
|
formState,
|
|
416
|
-
}
|
|
417
|
-
|
|
441
|
+
};
|
|
442
|
+
let stream;
|
|
443
|
+
if (htmlOnly) {
|
|
444
|
+
staticRuntime ??= await import('react-dom/static');
|
|
445
|
+
stream = (await staticRuntime.prerender(element, renderOptions)).prelude;
|
|
446
|
+
} else if(!useNodeHtml())stream = await domRuntime.renderToReadableStream(element, renderOptions);
|
|
447
|
+
const reader = stream ? stream.getReader() : await nodeHtmlReader(element,renderOptions);
|
|
418
448
|
const chunks = [];
|
|
449
|
+
const decoder = new TextDecoder();
|
|
419
450
|
let length = 0;
|
|
420
451
|
try {
|
|
421
452
|
for (;;) {
|
|
@@ -428,12 +459,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
|
|
|
428
459
|
await reader.cancel(error);
|
|
429
460
|
throw error;
|
|
430
461
|
}
|
|
431
|
-
|
|
462
|
+
// Decode each owned chunk once instead of copying every byte into
|
|
463
|
+
// per-chunk Buffers and a second aggregate Buffer before stringifying.
|
|
464
|
+
chunks.push(decoder.decode(value,{stream:true}));
|
|
432
465
|
}
|
|
433
466
|
} finally { reader.releaseLock(); }
|
|
434
467
|
if (abort.signal.aborted) throw abort.signal.reason;
|
|
435
468
|
if (renderError) throw renderError;
|
|
436
|
-
|
|
469
|
+
chunks.push(decoder.decode());
|
|
470
|
+
return chunks.join('');
|
|
437
471
|
} catch (error) {
|
|
438
472
|
if (abort.signal.aborted && abort.signal.reason?.statusCode === 504) throw abort.signal.reason;
|
|
439
473
|
throw error;
|
|
@@ -452,9 +486,12 @@ export function completeAppHtml(html, flight, route = {}) {
|
|
|
452
486
|
}
|
|
453
487
|
const styles = fontPreloads(route.fonts, route.nonce) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
|
|
454
488
|
if (styles) html = beforeClosingTag(html, 'head', styles);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
489
|
+
if (route.htmlOnly) assertHtmlOnlyDocument(html, route.pattern);
|
|
490
|
+
else {
|
|
491
|
+
const payload = `<script id="__PRNEXT_FLIGHT__" type="application/octet-stream">${flight.toString('base64')}</script>`;
|
|
492
|
+
const bootstrap = route.client ? `<script type="module"${route.nonce ? ` nonce="${escapeHtml(route.nonce)}"` : ''} src="${escapeHtml(route.client)}"></script>` : '';
|
|
493
|
+
html = beforeClosingTag(html, 'body', `${payload}${bootstrap}`);
|
|
494
|
+
}
|
|
458
495
|
if (!html.startsWith('<!DOCTYPE html>')) html = `<!DOCTYPE html>${html}`;
|
|
459
496
|
if (Buffer.byteLength(html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
|
|
460
497
|
return html;
|
|
@@ -462,13 +499,15 @@ export function completeAppHtml(html, flight, route = {}) {
|
|
|
462
499
|
|
|
463
500
|
const devStaticChecks = new Map();
|
|
464
501
|
|
|
502
|
+
import { differentialFlight } from './flight-delta.mjs';
|
|
465
503
|
export async function renderAppPage(options) {
|
|
466
504
|
const response = await renderAppPageInner(options);
|
|
467
505
|
if (options.method !== 'POST' && response.headers?.location) response.headers.location = addBasePath(response.headers.location, options.manifest?.config?.basePath || '');
|
|
468
|
-
return response;
|
|
506
|
+
return differentialFlight(response, options);
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distDir = path.dirname(path.dirname(modulePath)), url = 'http://localhost/', originalUrl, params = {}, method = 'GET', headers = {}, routingRequestHeaders, routingResolver, body = '', production = process.env.NODE_ENV === 'production', stream = false, signal, deferFinalizer }) {
|
|
510
|
+
if (route.htmlOnly) return { status:409, headers:{'content-type':'text/plain; charset=utf-8','cache-control':'private, no-store','x-prnext-document-navigation':'1'}, body:Buffer.from('This HTML-only page requires its prerendered document.') };
|
|
472
511
|
let clientModules = manifest.app?.clientModules || emptyManifest;
|
|
473
512
|
if (route.cacheConfig?.runtime === 'edge') {
|
|
474
513
|
let edge = !manifest.dev && edgeManifests.get(clientModules);
|
|
@@ -482,8 +521,8 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
482
521
|
clientModules = edge;
|
|
483
522
|
}
|
|
484
523
|
const responseHeaders = { 'cache-control': 'private, no-cache, no-store, max-age=0', vary: 'RSC' };
|
|
485
|
-
const request = { modulePath, routePattern: route.pattern, cacheConfig: route.cacheConfig, css: route.css || [], fonts: route.fonts || [], nonce: scriptNonce(headers), basePath: manifest.config?.basePath || '', trailingSlash: manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: manifest?.config?.skipTrailingSlashRedirect || false, url, originalUrl, params, method, headers, clientModules, production,
|
|
486
|
-
distDir, strictMode: manifest.config?.reactStrictMode !== false, cacheComponents: manifest.config?.cacheComponents, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, serverActions: manifest.config?.serverActions, previewModeId: manifest.previewModeId, actions: manifest.app?.actions || emptyManifest, actionKey: manifest.app?.actionKey };
|
|
524
|
+
const request = { modulePath, routePattern: route.pattern, cacheConfig: route.cacheConfig, css: route.css || [], cssScopes: route.cssScopes || [], fonts: route.fonts || [], nonce: scriptNonce(headers), basePath: manifest.config?.basePath || '', trailingSlash: manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: manifest?.config?.skipTrailingSlashRedirect || false, url, originalUrl, params, method, headers, clientModules, production,
|
|
525
|
+
distDir, traceparent:currentTraceparent(), instrumentation:manifest.instrumentation, strictMode: manifest.config?.reactStrictMode !== false, cacheComponents: manifest.config?.cacheComponents, navigationCache: manifest.config?.navigationCache, authorizationSecret: production ? manifest.app?.actionKey : developmentAuthorizationSecret, partialPrefetching: manifest.config?.partialPrefetching, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, serverActions: manifest.config?.serverActions, previewModeId: manifest.previewModeId, actions: manifest.app?.actions || emptyManifest, actionKey: manifest.app?.actionKey };
|
|
487
526
|
if (route.parallel) {
|
|
488
527
|
request.routingResolver = routingResolver;
|
|
489
528
|
request.skipProxyUrlNormalize = manifest.config?.skipProxyUrlNormalize;
|
|
@@ -515,6 +554,26 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
515
554
|
}
|
|
516
555
|
}
|
|
517
556
|
const header = name => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
557
|
+
if (request.partialPrefetching && manifest.app?.actionKey) {
|
|
558
|
+
// Preview identity includes authorization, but exposes no cookie material or
|
|
559
|
+
// guessable credential hash. A changed login cannot reveal an old preview.
|
|
560
|
+
responseHeaders['x-prnext-ppr-id'] = 'stage:' + createHmac('sha256', manifest.app.actionKey)
|
|
561
|
+
.update(JSON.stringify([manifest.cacheId, route.id, originalUrl || url, header('cookie') || '', header('authorization') || ''])).digest('hex');
|
|
562
|
+
}
|
|
563
|
+
const prefetchStage = header('x-prnext-prefetch-stage');
|
|
564
|
+
if (request.partialPrefetching && method === 'GET' && header('rsc') === '1' && header('x-prnext-prefetch') === '1' && ['shell', 'prefetch'].includes(prefetchStage)) {
|
|
565
|
+
const requirement = route.cacheConfig?.ensureStatic;
|
|
566
|
+
const staticOnly = ['prefetch', 'navigation'].includes(requirement) || prefetchStage === 'shell' && requirement === 'shell';
|
|
567
|
+
if (!staticOnly) {
|
|
568
|
+
const rendered = await renderFlight({ ...request, navigationStage: prefetchStage }, { signal, deferFinalizer });
|
|
569
|
+
if (rendered.status !== 200 || rendered.navigation || !responseHeaders['x-prnext-ppr-id']) return { status: 204, headers: responseHeaders, body: Buffer.alloc(0) };
|
|
570
|
+
const value = { version: 1, id: responseHeaders['x-prnext-ppr-id'], flight: rendered.body.toString('base64'), keys: [], unknownParams: false,
|
|
571
|
+
router: { pathname: new URL(originalUrl || url).pathname, search: new URL(originalUrl || url).search, params, basePath: request.basePath } };
|
|
572
|
+
const bytes = Buffer.from(JSON.stringify(value));
|
|
573
|
+
if (bytes.length > PARTIAL_PREFETCH_BYTES) return { status: 204, headers: responseHeaders, body: Buffer.alloc(0) };
|
|
574
|
+
return { status: 200, headers: { ...responseHeaders, 'content-type': PARTIAL_PREFETCH_TYPE }, body: bytes };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
518
577
|
if (method === 'POST') {
|
|
519
578
|
const id = header('next-action');
|
|
520
579
|
if (id && !Object.hasOwn(request.actions, id)) return { status: 404, headers: responseHeaders, body: Buffer.from('Unknown Server Action') };
|
|
@@ -526,19 +585,20 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
526
585
|
request.action = { id, contentType: header('content-type'), body: bytes };
|
|
527
586
|
}
|
|
528
587
|
if (!manifest.dev && request.cacheComponents && (route.ppr || route.pprFallback) && ['GET', 'HEAD'].includes(method) &&
|
|
588
|
+
!(request.partialPrefetching && header('rsc') === '1' && header('x-prnext-prefetch') !== '1' && !['shell', 'prefetch', 'navigation'].includes(route.cacheConfig?.ensureStatic)) &&
|
|
529
589
|
!request.nonce && !(route.parallel && header('x-prnext-router-state')) && !isDraftRequest(request)) {
|
|
530
590
|
const pathname = staticPath(route.pattern, new URL(url).pathname).path;
|
|
531
591
|
const generic = route.pprGeneric?.find(item => Object.entries(item.params).every(([name, value]) => JSON.stringify(params[name]) === JSON.stringify(value)));
|
|
532
592
|
const filename = (!originalUrl ? route.ppr?.[pathname] : undefined) || generic?.file;
|
|
533
593
|
if (filename || route.pprFallback) {
|
|
534
594
|
const artifact = await partialArtifact({ request, route, manifest, filename, identity: filename === generic?.file ? filename : `${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal,
|
|
535
|
-
regenerate: async build => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production,
|
|
595
|
+
regenerate: async build => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, renderMode:'isr',
|
|
536
596
|
path: build?.generic?.path || pathname, originalUrl: build?.generic ? undefined : originalUrl,
|
|
537
597
|
params: build?.generic?.params || params, partialParams: build?.generic?.unknown }),
|
|
538
598
|
});
|
|
539
|
-
if (!artifact.dynamic) return renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, rsc: header('rsc') === '1', prefetch: header('x-prnext-prefetch') === '1', knownPrefetch: header('x-prnext-prefetch-known'),
|
|
599
|
+
if (!artifact.dynamic) return renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, deferFinalizer, rsc: header('rsc') === '1', prefetch: header('x-prnext-prefetch') === '1', knownPrefetch: header('x-prnext-prefetch-known'),
|
|
540
600
|
concrete: artifact.generic ? (continuationSignal => partialArtifact({ request, route, manifest, identity: `concrete:${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal: continuationSignal,
|
|
541
|
-
regenerate: async () => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, path: pathname, originalUrl, params, seed: artifact }),
|
|
601
|
+
regenerate: async () => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, renderMode:'isr', path: pathname, originalUrl, params, seed: artifact }),
|
|
542
602
|
})) : undefined });
|
|
543
603
|
}
|
|
544
604
|
}
|
|
@@ -588,7 +648,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
588
648
|
let html, model, recovery = false;
|
|
589
649
|
try {
|
|
590
650
|
model = await decodeFlight(result.body, clientModules, distDir, { production });
|
|
591
|
-
html = await renderHtml(model.tree, model.router, { timeoutMs: route.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : undefined, formState: model.formState, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
|
|
651
|
+
html = await renderHtml(model.tree, model.router, { timeoutMs: route.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : undefined, formState: model.formState, reportedDigests:result.reportedDigests, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
|
|
592
652
|
} catch (error) {
|
|
593
653
|
if (error?.statusCode === 504) throw error;
|
|
594
654
|
const navigation = navigationResponse(error);
|
|
@@ -601,14 +661,14 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
601
661
|
recovery = true;
|
|
602
662
|
}
|
|
603
663
|
const { body: flight, status } = result;
|
|
604
|
-
html = completeAppHtml(html, flight, { ...route, nonce: request.nonce, ...(recovery ? { css: [], fonts: [] } : {}) });
|
|
664
|
+
html = completeAppHtml(html, flight, { ...route, css: model?.css ?? route.css, nonce: request.nonce, ...(recovery ? { css: [], fonts: [] } : {}) });
|
|
605
665
|
// A Flight error inside Suspense need not fail the HTML shell. Only shell
|
|
606
666
|
// failure selects 500 for an ordinary GET; action/navigation statuses remain.
|
|
607
667
|
const htmlStatus = result.rscError && status === 500 && ['GET', 'HEAD'].includes(method) ? 200 : status;
|
|
608
668
|
return { status: recovery ? route.parallel && status === 404 ? 404 : 500 : htmlStatus, headers: { ...responseHeaders, 'content-type': 'text/html; charset=utf-8' }, body: Buffer.from(html) };
|
|
609
669
|
}
|
|
610
670
|
|
|
611
|
-
async function renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, rsc, concrete, prefetch, knownPrefetch }) {
|
|
671
|
+
async function renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, deferFinalizer, rsc, concrete, prefetch, knownPrefetch }) {
|
|
612
672
|
const keys = [...partialKeyMap(artifact.keyScopes, request.params, removeBasePath(new URL(request.url).pathname, request.basePath))];
|
|
613
673
|
const partialRequest = { ...request, partialFlight: artifact.flight, ...(keys.length ? { partialKeys: keys } : {}) };
|
|
614
674
|
const headers = { ...artifact.headers, ...responseHeaders, 'x-prnext-prerender': artifact.complete ? 'complete' : 'partial', 'content-type': rsc ? 'text/x-component; charset=utf-8' : 'text/html; charset=utf-8' };
|
|
@@ -631,7 +691,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
|
|
|
631
691
|
return { status: resolved.status || 200, headers: resolved.headers || {}, body: streaming
|
|
632
692
|
? new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }) : bytes };
|
|
633
693
|
}
|
|
634
|
-
return renderFlight({ ...partialRequest, partialFlight: resolved.flight || artifact.flight, stream: streaming }, { signal: continuationSignal });
|
|
694
|
+
return renderFlight({ ...partialRequest, partialFlight: resolved.flight || artifact.flight, stream: streaming }, { signal: continuationSignal, deferFinalizer });
|
|
635
695
|
}
|
|
636
696
|
if (rsc) {
|
|
637
697
|
const result = await continueFlight(signal, stream);
|
package/runtime/app-routing.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
7
7
|
import { currentRequest, runRequestContext } from '../compat/headers.cjs';
|
|
8
8
|
import { staticParams } from '../compat/static-generation.cjs';
|
|
9
9
|
import { appRoutePlan } from './app-route-plan.mjs';
|
|
10
|
+
import { navigationAuthorization } from '../compat/navigation-authorization.cjs';
|
|
10
11
|
|
|
11
12
|
import { matchAppPattern, createAppMatcher, readRouterState } from './app-routing-state.mjs';
|
|
12
13
|
export { matchAppPattern, readRouterState, advancedRoutingSources } from './app-routing-state.mjs';
|
|
@@ -51,6 +52,9 @@ function selectedSegments(chain, params) {
|
|
|
51
52
|
|
|
52
53
|
/** Parallel slots retain their original Flight children on soft navigation. */
|
|
53
54
|
export function createAdvancedTree(entry, request, searchParams) {
|
|
55
|
+
function ScopedLayoutSlot(props) {
|
|
56
|
+
return React.createElement(entry.LayoutSlot, { ...props, authorizationKey: navigationAuthorization(currentRequest(), request.authorizationSecret || request.actionKey, request.navigationCache?.authCookies) });
|
|
57
|
+
}
|
|
54
58
|
const pathname = removeBasePath(new URL(request.url).pathname, request.basePath || '');
|
|
55
59
|
const matchCurrent = createAppMatcher(pathname);
|
|
56
60
|
const old = readRouterState(request), slots = {}, activeParams = { ...request.params };
|
|
@@ -166,7 +170,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
function slot(node, owner, name) {
|
|
169
|
-
const key = registerKey(`${scope(owner)}::${name}`, owner, `${owner.path}::`, `::${name}`, ['id']);
|
|
173
|
+
const key = registerKey(`${scope(owner)}::${name}`, owner, `${owner.path}::`, `::${name}`, ['id', 'activityKey', 'authorizationKey']);
|
|
170
174
|
const previous = old?.slots[key];
|
|
171
175
|
let selected = rebuild && previous ? savedBranch(node, previous) : firstCandidate(node);
|
|
172
176
|
if (rebuild && previous && !selected) { invalid = true; return { tree: null, segments: [] }; }
|
|
@@ -202,7 +206,10 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
202
206
|
metadataSelections.set(saved.node.path, saved);
|
|
203
207
|
if (name === 'children' && owner === root) rootPage = saved;
|
|
204
208
|
}
|
|
205
|
-
|
|
209
|
+
let tree = React.createElement(ScopedLayoutSlot, { key, id: key, preserve: true, activityKey: previous.branch });
|
|
210
|
+
const context = request.routingContexts?.get(previous.url);
|
|
211
|
+
if (context && request.renderRoutingBranch) tree = request.renderRoutingBranch(tree, context);
|
|
212
|
+
return { tree, segments: null };
|
|
206
213
|
}
|
|
207
214
|
if (!selected) {
|
|
208
215
|
// Find the nearest default whose route prefix contains the destination.
|
|
@@ -224,13 +231,16 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
224
231
|
slots[key] = { branch: selected.node.path + (selected.default ? '#default' : ''), url: selectedURL, source: sourceURL(owner, previous), accessUrl: selected.accessUrl || accessURL(selectedURL, previous) };
|
|
225
232
|
if (name === 'children' && owner === root) rootPage = selected;
|
|
226
233
|
let selectedTree = renderPath(selected.chain, selected, 0, name === 'children' && owner === node);
|
|
227
|
-
const context = request.routingContexts?.get(selectedURL);
|
|
228
|
-
if (context && request.renderRoutingBranch) selectedTree = request.renderRoutingBranch(selectedTree, context);
|
|
229
234
|
if(request.instantValidation && name!=='children'){
|
|
230
235
|
const instant=selected.node.config?.page?.instant ?? selected.chain.findLast(item=>item.config?.layout?.instant!==undefined)?.config.layout.instant;
|
|
231
236
|
if(instantEnabled(instant,request.production)) instantTrees.push({path:(owner.path || '/')+' @'+name,tree:selectedTree});
|
|
232
237
|
}
|
|
233
|
-
|
|
238
|
+
const retainedLayout = selected.chain.find(item => item !== owner && item.files.layout);
|
|
239
|
+
const activityKey = retainedLayout ? scope(retainedLayout) : JSON.stringify([slots[key].branch, slots[key].url]);
|
|
240
|
+
let tree = React.createElement(ScopedLayoutSlot, { key, id: key, preserve: false, activityKey }, selectedTree);
|
|
241
|
+
const context = request.routingContexts?.get(selectedURL);
|
|
242
|
+
if (context && request.renderRoutingBranch) tree = request.renderRoutingBranch(tree, context);
|
|
243
|
+
return { tree, segments: selected.default ? [] : selectedSegments(selected.chain.slice(node === owner ? 1 : 0), selected.params) };
|
|
234
244
|
}
|
|
235
245
|
function renderPath(chain, selection, index, skipOwner) {
|
|
236
246
|
const node = chain[index];
|
|
@@ -245,7 +255,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
245
255
|
: React.createElement(Page, { ...props, key: registerKey(`${node.path}:${JSON.stringify(selection.params)}`, node, `${node.path}:`, '', undefined, selection.params) });
|
|
246
256
|
} else children = null;
|
|
247
257
|
if (skipOwner) return children;
|
|
248
|
-
const childKey = registerKey(`${key}::children`, node, `${node.path}::`, '::children', ['id']);
|
|
258
|
+
const childKey = registerKey(`${key}::children`, node, `${node.path}::`, '::children', ['id', 'activityKey', 'activityPreserve', 'authorizationKey']);
|
|
249
259
|
const previousChild = old?.slots[childKey];
|
|
250
260
|
const preserveChild = !!previousChild && !rebuild && (interceptOwners.has(node.path) || (selection.scaffold && index === chain.length - 1));
|
|
251
261
|
if (rebuild && previousChild && previousChild.branch !== selection.node.path + (selection.default ? '#default' : '')) {
|
|
@@ -263,7 +273,10 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
263
273
|
const saved = savedBranch(node, previousChild);
|
|
264
274
|
if (saved) metadataSelections.set(saved.node.path, saved);
|
|
265
275
|
}
|
|
266
|
-
|
|
276
|
+
const nextLayout = chain.slice(index + 1).some(child => child.files.layout);
|
|
277
|
+
const activityKey = nextLayout ? `${chain[index + 1].path}:${JSON.stringify(scopedParams(chain[index + 1]))}` : JSON.stringify([slots[childKey].branch, slots[childKey].url]);
|
|
278
|
+
const activityPreserve = !!(rebuild && previousChild && previousChild.branch !== selection.node.path + (selection.default ? '#default' : ''));
|
|
279
|
+
children = React.createElement(ScopedLayoutSlot, { key: childKey, id: childKey, preserve: preserveChild, activityKey, activityPreserve }, preserveChild ? null : children);
|
|
267
280
|
const selected = { children: selection.default ? [] : selectedSegments(chain.slice(index + 1), selection.params) };
|
|
268
281
|
if (preserveChild) selected.children = null;
|
|
269
282
|
const parallel = {};
|
|
@@ -279,8 +292,8 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
279
292
|
}
|
|
280
293
|
function wrap(node, children, parallel, selected, key, params, templateSegments, selectionNode) {
|
|
281
294
|
if (node.files.loading) children = React.createElement(React.Suspense, { fallback: React.createElement(component(node.files.loading, 'loading')) }, children);
|
|
282
|
-
if (node.files.error) children = React.createElement(entry.ErrorBoundary, { key: registerKey(`error:${key}`, node, `error:${node.path}
|
|
283
|
-
if (node.files.notFound || node === root) children = React.createElement(entry.NavigationBoundary, { key: registerKey(`navigation:${key}`, node, `navigation:${node.path}
|
|
295
|
+
if (node.files.error) children = React.createElement(entry.ErrorBoundary, { key: registerKey(`error:${key}`, node, `error:${node.path}::`, '', ['resetKey']), errorComponent: component(node.files.error, 'error'), resetKey: request.renderKey }, children);
|
|
296
|
+
if (node.files.notFound || node === root) children = React.createElement(entry.NavigationBoundary, { key: registerKey(`navigation:${key}`, node, `navigation:${node.path}::`, '', ['resetKey']), resetKey: request.renderKey,
|
|
284
297
|
notFound: node.files.notFound ? React.createElement(component(node.files.notFound, 'not-found')) : React.createElement('h1', null, '404: This page could not be found.') }, children);
|
|
285
298
|
if (node.files.template) children = React.createElement(component(node.files.template, 'template'), { key: registerKey(selected.children?.join('/') || key, selectionNode || node, `${node.path}::`, '', undefined, scopedParams(node), templateSegments) }, children);
|
|
286
299
|
if (node.files.layout) {
|
package/runtime/app-static.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {currentTraceparent} from './trace.mjs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { addBasePath, removeBasePath } from '../compat/paths.cjs';
|
|
3
4
|
import { renderFlight, decodeFlight, renderHtml, completeAppHtml } from './app-render.mjs';
|
|
@@ -12,12 +13,13 @@ export function requestOptions(options) {
|
|
|
12
13
|
const canonical = new URL('http://prnext.local');
|
|
13
14
|
canonical.pathname = new URL(options.path || options.url || '/', canonical).pathname;
|
|
14
15
|
return { modulePath: options.modulePath, distDir: options.distDir || path.dirname(path.dirname(options.modulePath)),
|
|
15
|
-
|
|
16
|
+
...(options.renderMode==='isr'?{runtimeInstrumentation:true,instrumentation:options.manifest?.instrumentation,traceparent:currentTraceparent(),revalidateReason:options.revalidateReason}:{}),
|
|
17
|
+
routePattern: route.pattern, cacheConfig: route.cacheConfig || {}, css: route.css || [], cssScopes: route.cssScopes || [], fonts: route.fonts || [], production: options.production ?? true,
|
|
16
18
|
url: canonical.href, originalUrl: options.originalUrl ? new URL(new URL(options.originalUrl).pathname, canonical).href : undefined,
|
|
17
19
|
partialParams: options.partialParams, basePath: options.manifest?.config?.basePath || '', trailingSlash: options.manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: options.manifest?.config?.skipTrailingSlashRedirect || false, params: options.params || {}, method: 'GET', headers: {},
|
|
18
20
|
...(options.seed ? { partialFlight: options.seed.flight, partialKeys: [...partialKeyMap(options.seed.keyScopes, options.params, removeBasePath(canonical.pathname, options.manifest?.config?.basePath || ''))] } : {}),
|
|
19
21
|
clientModules: options.manifest?.app?.clientModules || {},
|
|
20
|
-
cacheComponents: options.manifest?.config?.cacheComponents, cacheLife: options.manifest?.config?.cacheLife, cacheHandlers: options.manifest?.config?.cacheHandlers, cacheHandler: options.manifest?.config?.cacheHandler, cacheMaxMemorySize: options.manifest?.config?.cacheMaxMemorySize,
|
|
22
|
+
cacheComponents: options.manifest?.config?.cacheComponents, navigationCache: options.manifest?.config?.navigationCache, partialPrefetching: options.manifest?.config?.partialPrefetching, cacheLife: options.manifest?.config?.cacheLife, cacheHandlers: options.manifest?.config?.cacheHandlers, cacheHandler: options.manifest?.config?.cacheHandler, cacheMaxMemorySize: options.manifest?.config?.cacheMaxMemorySize,
|
|
21
23
|
actions: options.manifest?.app?.actions || {}, actionKey: options.manifest?.app?.actionKey };
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -29,11 +31,12 @@ export async function prerenderAppRoute(options) {
|
|
|
29
31
|
const request = requestOptions(options);
|
|
30
32
|
const mode = request.cacheConfig.dynamic || 'auto';
|
|
31
33
|
if (request.cacheConfig.runtime === 'edge' || mode === 'force-dynamic' || (request.cacheConfig.revalidate === 0 && mode !== 'force-static')) {
|
|
34
|
+
if (request.cacheConfig.ensureStatic === 'navigation') throw new Error(`Route ${request.routePattern}: ensureStatic='navigation' conflicts with request-time rendering configuration.`);
|
|
32
35
|
return { dynamic: true, reason: request.cacheConfig.runtime === 'edge' ? 'runtime: edge' : mode === 'force-dynamic' ? 'dynamic: force-dynamic' : 'revalidate: 0' };
|
|
33
36
|
}
|
|
34
37
|
try {
|
|
35
|
-
const partial = request.cacheComponents === true && mode === 'auto';
|
|
36
|
-
const result = await renderFlight({ ...request, staticGeneration: { mode, partial } });
|
|
38
|
+
const partial = request.cacheComponents === true && mode === 'auto' && request.cacheConfig.ensureStatic !== 'navigation';
|
|
39
|
+
const result = await renderFlight({ ...request, staticGeneration: { mode, partial } },{settleBeforeReturn:true});
|
|
37
40
|
return await runRequestContext({ ...request, phase: 'render', staticGeneration: { mode } }, async () => {
|
|
38
41
|
const context = currentRequest();
|
|
39
42
|
try {
|
|
@@ -61,21 +64,28 @@ export async function prerenderAppRoute(options) {
|
|
|
61
64
|
}
|
|
62
65
|
html = completeAppHtml(shell.shell, result.body, { ...options.route, css: [], fonts: [] });
|
|
63
66
|
} else {
|
|
64
|
-
html = completeAppHtml(await renderHtml(model.tree, model.router, { timeoutMs: options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : undefined, staticGeneration: true, strictMode: options.manifest?.config?.reactStrictMode !== false }), result.body, options.route);
|
|
67
|
+
html = completeAppHtml(await renderHtml(model.tree, model.router, { timeoutMs: options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : undefined, staticGeneration: true, strictMode: options.manifest?.config?.reactStrictMode !== false, htmlOnly:options.route?.htmlOnly === true }), result.body, { ...options.route, css:model.css ?? options.route?.css });
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
if (staticMetadata(context)?.externalCache)
|
|
68
|
-
|
|
70
|
+
if (staticMetadata(context)?.externalCache) {
|
|
71
|
+
if (request.cacheConfig.ensureStatic === 'navigation') throw new Error(`Route ${request.routePattern}: external cache validation prevents guaranteed static navigation output.`);
|
|
72
|
+
return { dynamic: true, reason: 'External cache handlers require validation on every request' };
|
|
73
|
+
}
|
|
74
|
+
return { body: html, ...(options.route?.htmlOnly ? {} : { flight:result.body }), ...staticMetadata(context), generatedAt: Date.now(),
|
|
69
75
|
status: result.status, headers: { ...result.headers, ...(result.headers?.location ? { location: addBasePath(result.headers.location, request.basePath) } : {}), 'content-type': 'text/html; charset=utf-8' } };
|
|
70
76
|
} finally { await flushCacheWork(context); }
|
|
71
77
|
});
|
|
72
78
|
} catch (error) {
|
|
73
|
-
if (error.code === 'PRNEXT_DYNAMIC_SERVER_USAGE' && mode === 'auto')
|
|
79
|
+
if (error.code === 'PRNEXT_DYNAMIC_SERVER_USAGE' && mode === 'auto') {
|
|
80
|
+
if (request.cacheConfig.ensureStatic === 'navigation') throw new Error(`Route ${request.routePattern}: ensureStatic='navigation' requires static server output; ${error.dynamicReason || error.message}.`, { cause: error });
|
|
81
|
+
return { dynamic: true, reason: error.dynamicReason || error.message };
|
|
82
|
+
}
|
|
74
83
|
throw error;
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
export async function renderAppIsrPage(options) {
|
|
88
|
+
if (options.route?.htmlOnly) throw new Error(`htmlOnly route ${options.route.pattern} cannot use ISR or request-time regeneration.`);
|
|
79
89
|
const rendered = await prerenderAppRoute(options);
|
|
80
90
|
if (rendered.dynamic) return { status: 200, headers: {}, body: [],
|
|
81
91
|
isr: { dynamic: true, revalidate: 0, htmlLength: 0, dataLength: 0, tags: [], paths: [] } };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { htmlEnvelope } from './html-envelope.mjs';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { fontPreloads } from './font-preload.mjs';
|
|
@@ -12,55 +13,9 @@ import { addBasePath } from '../compat/paths.cjs';
|
|
|
12
13
|
import { renderAppRecoveryShell } from './app-recovery.mjs';
|
|
13
14
|
import { partialModel } from './app-partial-model.mjs';
|
|
14
15
|
import { streamSelector } from './stream-select.mjs';
|
|
16
|
+
import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
// ReadableStream.tee(), this tap stops reading when the HTML consumer stalls.
|
|
18
|
-
// Before a document shell exists, at most the overall 16 MiB response limit can
|
|
19
|
-
// accumulate: an async root layout may need the complete model to produce it.
|
|
20
|
-
function flightTap(source) {
|
|
21
|
-
const reader = source.getReader();
|
|
22
|
-
const queue = [];
|
|
23
|
-
let bytes = 0;
|
|
24
|
-
let done = false;
|
|
25
|
-
let failure;
|
|
26
|
-
let outputStarted = false;
|
|
27
|
-
let wakeData;
|
|
28
|
-
let wakeSpace;
|
|
29
|
-
const finish = error => {
|
|
30
|
-
done = true; failure = error;
|
|
31
|
-
wakeData?.(); wakeData = undefined;
|
|
32
|
-
wakeSpace?.(); wakeSpace = undefined;
|
|
33
|
-
};
|
|
34
|
-
const cancel = async reason => { finish(reason); await reader.cancel(reason).catch(() => {}); };
|
|
35
|
-
const body = new ReadableStream({
|
|
36
|
-
async pull(controller) {
|
|
37
|
-
try {
|
|
38
|
-
while (!done && outputStarted && bytes >= 128 * 1024) await new Promise(resolve => { wakeSpace = resolve; });
|
|
39
|
-
if (done) { if (failure) controller.error(failure); else controller.close(); return; }
|
|
40
|
-
const next = await reader.read();
|
|
41
|
-
if (next.done) { finish(); controller.close(); return; }
|
|
42
|
-
queue.push(next.value); bytes += next.value.byteLength;
|
|
43
|
-
wakeData?.(); wakeData = undefined;
|
|
44
|
-
controller.enqueue(next.value);
|
|
45
|
-
} catch (error) { finish(error); controller.error(error); }
|
|
46
|
-
},
|
|
47
|
-
cancel,
|
|
48
|
-
}, { highWaterMark: 0 });
|
|
49
|
-
return {
|
|
50
|
-
body, cancel,
|
|
51
|
-
startOutput() { outputStarted = true; },
|
|
52
|
-
async next() {
|
|
53
|
-
while (!queue.length && !done) await new Promise(resolve => { wakeData = resolve; });
|
|
54
|
-
if (queue.length) {
|
|
55
|
-
const value = queue.shift(); bytes -= value.byteLength;
|
|
56
|
-
wakeSpace?.(); wakeSpace = undefined;
|
|
57
|
-
return { value, done: false };
|
|
58
|
-
}
|
|
59
|
-
if (failure) throw failure;
|
|
60
|
-
return { done: true };
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
18
|
+
import {flightTap} from './flight-tap.mjs';
|
|
64
19
|
|
|
65
20
|
export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs, partial }) {
|
|
66
21
|
timeoutMs ??= route?.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : 25_000;
|
|
@@ -92,6 +47,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
92
47
|
onError(error) {
|
|
93
48
|
if (isDynamicBailout(error)) return error.digest;
|
|
94
49
|
if (combinedSignal.aborted) return error?.digest;
|
|
50
|
+
if(result.reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);
|
|
95
51
|
const navigation = navigationResponse(error);
|
|
96
52
|
if (navigation) {
|
|
97
53
|
if (committed) control = navigation;
|
|
@@ -104,8 +60,9 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
104
60
|
const element = React.createElement(request.strictMode !== false ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router: resumedModel.router, nonce: request.nonce }, appContent(resumedModel.tree)));
|
|
105
61
|
// React owns and mutates its opaque continuation during resume. Parsed
|
|
106
62
|
// artifacts can be shared, but each HTML request needs its own continuation.
|
|
107
|
-
|
|
108
|
-
|
|
63
|
+
reader = !partial&&request.cacheConfig?.runtime!=='edge'&&useNodeHtml()
|
|
64
|
+
? await nodeHtmlReader(element,renderOptions)
|
|
65
|
+
: (partial ? await resume(element, structuredClone(partial.postponed), renderOptions) : await renderToReadableStream(element, renderOptions)).getReader();
|
|
109
66
|
const decoder = new TextDecoder();
|
|
110
67
|
let prefix = partial?.shell || '';
|
|
111
68
|
let htmlDone = false;
|
|
@@ -116,13 +73,13 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
116
73
|
const next = await reader.read();
|
|
117
74
|
if (next.done) { htmlDone = true; prefix += decoder.decode(); break; }
|
|
118
75
|
prefix += decoder.decode(next.value, { stream: true });
|
|
119
|
-
if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw new Error('HTML shell exceeds the 16 MiB PRNext limit');
|
|
76
|
+
if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw Object.assign(new Error('HTML shell exceeds the 16 MiB PRNext limit'),{code:'PRNEXT_RESPONSE_TOO_LARGE'});
|
|
120
77
|
}
|
|
121
78
|
if (renderError) throw renderError;
|
|
122
79
|
if (!/<html(?:\s|>)/i.test(prefix) || !/<body(?:\s|>)/i.test(prefix) || !/<\/head>/i.test(prefix)) {
|
|
123
80
|
throw Object.assign(new Error('App Router root layout must render <html>, <head> and <body> elements'), { code: 'PRNEXT_INVALID_ROOT_LAYOUT' });
|
|
124
81
|
}
|
|
125
|
-
const styles = fontPreloads(route.fonts, request.nonce) + (route.css
|
|
82
|
+
const styles = fontPreloads(route.fonts, request.nonce) + (model.css ?? route.css ?? []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
|
|
126
83
|
if (styles && !partial) prefix = prefix.replace(/<\/head>/i, `${styles}</head>`);
|
|
127
84
|
tap.startOutput();
|
|
128
85
|
committed = true;
|
|
@@ -183,7 +140,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
183
140
|
return { status, headers: { ...responseHeaders, ...result.headers, 'content-type': 'text/html; charset=utf-8' }, body, cancel };
|
|
184
141
|
} catch (error) {
|
|
185
142
|
if (!committed && !combinedSignal.aborted && error?.statusCode !== 504 &&
|
|
186
|
-
error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
|
|
143
|
+
error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && error?.code !== 'PRNEXT_RESPONSE_TOO_LARGE' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
|
|
187
144
|
try {
|
|
188
145
|
// Next emits an empty document after a shell failure. The original
|
|
189
146
|
// Flight stream still carries the failing subtree to client boundaries;
|