@thomas.f/prnext 0.1.16-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/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- 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 +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- 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-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- 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 +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- 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/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- 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 +100 -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/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- 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 +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -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 +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
|
@@ -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,22 @@ 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
|
+
}
|
|
248
|
+
export async function preloadAppRoutes(files,options){
|
|
249
|
+
const worker=getWorker(true),id=`preload-${++nextId}`;
|
|
250
|
+
worker.ref();
|
|
251
|
+
try{await new Promise((resolve,reject)=>{
|
|
252
|
+
const clean=()=>{clearTimeout(timer);worker.off('message',message);worker.off('error',error);worker.off('exit',exit);};
|
|
253
|
+
const message=value=>{if(value.type==='preloaded'&&value.id===id){clean();value.error?reject(new Error(value.error)):resolve();}};
|
|
254
|
+
const error=value=>{clean();reject(value);};const exit=()=>error(new Error('RSC worker exited while preloading'));
|
|
255
|
+
const timer=setTimeout(()=>{clean();void lifecycles.get(worker)?.terminate(new Error('RSC preload timed out'));reject(new Error('RSC preload timed out'));},options.timeoutMs);
|
|
256
|
+
worker.on('message',message);worker.once('error',error);worker.once('exit',exit);
|
|
257
|
+
worker.postMessage({type:'preload',id,value:{files,options}});
|
|
258
|
+
});}finally{if(!hasPending(worker)&&!lifecycles.get(worker)?.retiring)worker.unref();}
|
|
259
|
+
}
|
|
236
260
|
|
|
237
261
|
// Stdio workers can retain referenced MessagePorts after rendering (including
|
|
238
262
|
// diagnostics piped from the RSC isolate). EOF must drain real cache/after()
|
|
@@ -240,6 +264,12 @@ function getWorker(production) {
|
|
|
240
264
|
// incidental event-loop liveness or terminate unfinished application work.
|
|
241
265
|
export async function drainAppRuntime() {
|
|
242
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
|
+
})));
|
|
243
273
|
await closeAppRuntime();
|
|
244
274
|
}
|
|
245
275
|
|
|
@@ -266,7 +296,8 @@ function requestDeadline(entry, lifecycle, id, hardTimeoutMs, delayMs) {
|
|
|
266
296
|
}, delayMs);
|
|
267
297
|
}
|
|
268
298
|
|
|
269
|
-
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.');
|
|
270
301
|
softTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 25_000;
|
|
271
302
|
hardTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 28_000;
|
|
272
303
|
const deadline = performance.now() + hardTimeoutMs;
|
|
@@ -292,7 +323,11 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
|
|
|
292
323
|
let settle;
|
|
293
324
|
const settled = new Promise(resolve => { settle = resolve; });
|
|
294
325
|
trackRequestWork(settled, signal, { deadline, supervised: true });
|
|
295
|
-
|
|
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 };
|
|
296
331
|
const timeout = entry.timeout = requestDeadline(entry, lifecycle, id, hardTimeoutMs, Math.max(0, deadline - performance.now()));
|
|
297
332
|
function cancel(reason) {
|
|
298
333
|
if (entry.cancelled) return;
|
|
@@ -324,7 +359,7 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
|
|
|
324
359
|
pending.set(id, entry);
|
|
325
360
|
try {
|
|
326
361
|
const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
|
|
327
|
-
if (request.
|
|
362
|
+
if (request.cacheConfig?.runtime !== 'edge' && deferFinalizer) {
|
|
328
363
|
wire.waitForResponse = true;
|
|
329
364
|
deferFinalizer(() => { if (pending.has(id)) current.postMessage({ id, type: 'after-response' }); });
|
|
330
365
|
}
|
|
@@ -383,15 +418,17 @@ export async function decodeFlight(body, clientModules = {}, distDir, { producti
|
|
|
383
418
|
return decoder.createFromNodeStream(body instanceof ReadableStream ? Readable.fromWeb(body) : Readable.from([body]), serverConsumerManifest);
|
|
384
419
|
}
|
|
385
420
|
|
|
386
|
-
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 } = {}) {
|
|
387
422
|
const abort = new AbortController();
|
|
388
423
|
const timeout = setTimeout(() => abort.abort(appTimeoutError('HTML render', timeoutMs)), timeoutMs);
|
|
389
424
|
let renderError;
|
|
390
425
|
try {
|
|
391
426
|
domRuntime ??= await import('react-dom/server');
|
|
392
|
-
const
|
|
427
|
+
const element = React.createElement(strictMode ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router, nonce }, appContent(tree)));
|
|
428
|
+
const renderOptions = {
|
|
393
429
|
nonce,
|
|
394
430
|
signal: abort.signal, onError(error) {
|
|
431
|
+
if(!abort.signal.aborted){if(reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);}
|
|
395
432
|
if (isDynamicBailout(error)) return error.digest;
|
|
396
433
|
// Suspense can recover client SSR failures by emitting its fallback and
|
|
397
434
|
// retrying in the browser. Static generation must still reject them:
|
|
@@ -401,9 +438,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
|
|
|
401
438
|
return appErrorDigest(error);
|
|
402
439
|
},
|
|
403
440
|
formState,
|
|
404
|
-
}
|
|
405
|
-
|
|
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);
|
|
406
448
|
const chunks = [];
|
|
449
|
+
const decoder = new TextDecoder();
|
|
407
450
|
let length = 0;
|
|
408
451
|
try {
|
|
409
452
|
for (;;) {
|
|
@@ -416,12 +459,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
|
|
|
416
459
|
await reader.cancel(error);
|
|
417
460
|
throw error;
|
|
418
461
|
}
|
|
419
|
-
|
|
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}));
|
|
420
465
|
}
|
|
421
466
|
} finally { reader.releaseLock(); }
|
|
422
467
|
if (abort.signal.aborted) throw abort.signal.reason;
|
|
423
468
|
if (renderError) throw renderError;
|
|
424
|
-
|
|
469
|
+
chunks.push(decoder.decode());
|
|
470
|
+
return chunks.join('');
|
|
425
471
|
} catch (error) {
|
|
426
472
|
if (abort.signal.aborted && abort.signal.reason?.statusCode === 504) throw abort.signal.reason;
|
|
427
473
|
throw error;
|
|
@@ -440,9 +486,12 @@ export function completeAppHtml(html, flight, route = {}) {
|
|
|
440
486
|
}
|
|
441
487
|
const styles = fontPreloads(route.fonts, route.nonce) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
|
|
442
488
|
if (styles) html = beforeClosingTag(html, 'head', styles);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|
|
446
495
|
if (!html.startsWith('<!DOCTYPE html>')) html = `<!DOCTYPE html>${html}`;
|
|
447
496
|
if (Buffer.byteLength(html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
|
|
448
497
|
return html;
|
|
@@ -450,13 +499,15 @@ export function completeAppHtml(html, flight, route = {}) {
|
|
|
450
499
|
|
|
451
500
|
const devStaticChecks = new Map();
|
|
452
501
|
|
|
502
|
+
import { differentialFlight } from './flight-delta.mjs';
|
|
453
503
|
export async function renderAppPage(options) {
|
|
454
504
|
const response = await renderAppPageInner(options);
|
|
455
505
|
if (options.method !== 'POST' && response.headers?.location) response.headers.location = addBasePath(response.headers.location, options.manifest?.config?.basePath || '');
|
|
456
|
-
return response;
|
|
506
|
+
return differentialFlight(response, options);
|
|
457
507
|
}
|
|
458
508
|
|
|
459
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.') };
|
|
460
511
|
let clientModules = manifest.app?.clientModules || emptyManifest;
|
|
461
512
|
if (route.cacheConfig?.runtime === 'edge') {
|
|
462
513
|
let edge = !manifest.dev && edgeManifests.get(clientModules);
|
|
@@ -470,8 +521,8 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
470
521
|
clientModules = edge;
|
|
471
522
|
}
|
|
472
523
|
const responseHeaders = { 'cache-control': 'private, no-cache, no-store, max-age=0', vary: 'RSC' };
|
|
473
|
-
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,
|
|
474
|
-
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 };
|
|
475
526
|
if (route.parallel) {
|
|
476
527
|
request.routingResolver = routingResolver;
|
|
477
528
|
request.skipProxyUrlNormalize = manifest.config?.skipProxyUrlNormalize;
|
|
@@ -503,6 +554,26 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
503
554
|
}
|
|
504
555
|
}
|
|
505
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
|
+
}
|
|
506
577
|
if (method === 'POST') {
|
|
507
578
|
const id = header('next-action');
|
|
508
579
|
if (id && !Object.hasOwn(request.actions, id)) return { status: 404, headers: responseHeaders, body: Buffer.from('Unknown Server Action') };
|
|
@@ -514,19 +585,20 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
514
585
|
request.action = { id, contentType: header('content-type'), body: bytes };
|
|
515
586
|
}
|
|
516
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)) &&
|
|
517
589
|
!request.nonce && !(route.parallel && header('x-prnext-router-state')) && !isDraftRequest(request)) {
|
|
518
590
|
const pathname = staticPath(route.pattern, new URL(url).pathname).path;
|
|
519
591
|
const generic = route.pprGeneric?.find(item => Object.entries(item.params).every(([name, value]) => JSON.stringify(params[name]) === JSON.stringify(value)));
|
|
520
592
|
const filename = (!originalUrl ? route.ppr?.[pathname] : undefined) || generic?.file;
|
|
521
593
|
if (filename || route.pprFallback) {
|
|
522
594
|
const artifact = await partialArtifact({ request, route, manifest, filename, identity: filename === generic?.file ? filename : `${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal,
|
|
523
|
-
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',
|
|
524
596
|
path: build?.generic?.path || pathname, originalUrl: build?.generic ? undefined : originalUrl,
|
|
525
597
|
params: build?.generic?.params || params, partialParams: build?.generic?.unknown }),
|
|
526
598
|
});
|
|
527
|
-
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'),
|
|
528
600
|
concrete: artifact.generic ? (continuationSignal => partialArtifact({ request, route, manifest, identity: `concrete:${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal: continuationSignal,
|
|
529
|
-
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 }),
|
|
530
602
|
})) : undefined });
|
|
531
603
|
}
|
|
532
604
|
}
|
|
@@ -576,7 +648,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
576
648
|
let html, model, recovery = false;
|
|
577
649
|
try {
|
|
578
650
|
model = await decodeFlight(result.body, clientModules, distDir, { production });
|
|
579
|
-
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 });
|
|
580
652
|
} catch (error) {
|
|
581
653
|
if (error?.statusCode === 504) throw error;
|
|
582
654
|
const navigation = navigationResponse(error);
|
|
@@ -589,14 +661,14 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
589
661
|
recovery = true;
|
|
590
662
|
}
|
|
591
663
|
const { body: flight, status } = result;
|
|
592
|
-
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: [] } : {}) });
|
|
593
665
|
// A Flight error inside Suspense need not fail the HTML shell. Only shell
|
|
594
666
|
// failure selects 500 for an ordinary GET; action/navigation statuses remain.
|
|
595
667
|
const htmlStatus = result.rscError && status === 500 && ['GET', 'HEAD'].includes(method) ? 200 : status;
|
|
596
668
|
return { status: recovery ? route.parallel && status === 404 ? 404 : 500 : htmlStatus, headers: { ...responseHeaders, 'content-type': 'text/html; charset=utf-8' }, body: Buffer.from(html) };
|
|
597
669
|
}
|
|
598
670
|
|
|
599
|
-
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 }) {
|
|
600
672
|
const keys = [...partialKeyMap(artifact.keyScopes, request.params, removeBasePath(new URL(request.url).pathname, request.basePath))];
|
|
601
673
|
const partialRequest = { ...request, partialFlight: artifact.flight, ...(keys.length ? { partialKeys: keys } : {}) };
|
|
602
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' };
|
|
@@ -619,7 +691,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
|
|
|
619
691
|
return { status: resolved.status || 200, headers: resolved.headers || {}, body: streaming
|
|
620
692
|
? new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }) : bytes };
|
|
621
693
|
}
|
|
622
|
-
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 });
|
|
623
695
|
}
|
|
624
696
|
if (rsc) {
|
|
625
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;
|