@thomas.f/prnext 0.1.12-alpha → 0.1.13-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 +262 -33
- package/build/actions.mjs +4 -0
- package/build/app-config.mjs +2 -0
- package/build/app.mjs +20 -5
- package/build/bundle-runtime.mjs +82 -0
- package/build/compiler.mjs +3 -1
- package/build/config.mjs +9 -2
- package/build/font-cache.mjs +50 -0
- package/build/font-files.mjs +5 -3
- package/build/fonts.mjs +1 -1
- package/build/index.mjs +5 -0
- package/build/metadata-conflicts.mjs +13 -0
- package/build/metadata-images.mjs +3 -3
- package/build/metadata-routes.mjs +3 -3
- package/build/native-packages.mjs +5 -1
- package/build/npm-packages.mjs +2 -1
- package/build/optimize-imports.mjs +112 -0
- package/build/proxy-body-config.mjs +10 -0
- package/build/scan.mjs +9 -7
- package/cli.mjs +16 -0
- package/compat/after.cjs +51 -0
- package/compat/index.d.ts +3 -1
- package/compat/server.cjs +2 -1
- package/compat/server.d.cts +2 -0
- package/native/launch.mjs +19 -0
- package/package.json +7 -7
- package/runtime/api.mjs +15 -5
- package/runtime/app-render.mjs +78 -33
- package/runtime/app-route-plan.mjs +45 -0
- package/runtime/app-routing-state.mjs +101 -21
- package/runtime/app-routing.mjs +37 -18
- package/runtime/app-segments.mjs +25 -11
- package/runtime/app-static.mjs +1 -1
- package/runtime/app-stream-html.mjs +2 -1
- package/runtime/cache-request.mjs +10 -2
- package/runtime/cloud/archive.mjs +316 -0
- package/runtime/cloud/client.mjs +658 -0
- package/runtime/cloud/environment.mjs +60 -0
- package/runtime/cloud/state.mjs +141 -0
- package/runtime/cloud/upload.mjs +48 -0
- package/runtime/html-envelope.mjs +70 -7
- package/runtime/http.mjs +7 -4
- package/runtime/middleware.mjs +3 -3
- package/runtime/request-transport.mjs +1 -1
- package/runtime/route-static.mjs +1 -1
- package/runtime/rsc-worker.mjs +27 -4
- package/runtime/stream-utils.mjs +22 -3
- package/runtime/transport.mjs +16 -0
- package/runtime/worker-metadata.mjs +23 -0
- package/runtime/worker.mjs +36 -9
package/runtime/api.mjs
CHANGED
|
@@ -178,6 +178,7 @@ function parseBody(bytes, contentType = '') {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
export function runApi(options) {
|
|
181
|
+
if (options.timeoutMs === undefined && options.route?.cacheConfig?.maxDuration) options = { ...options, timeoutMs: options.route.cacheConfig.maxDuration * 1000 };
|
|
181
182
|
return withCacheRequest(options, options.route?.router === 'app' ? 'route' : 'pages', context => runApiInner(options, context));
|
|
182
183
|
}
|
|
183
184
|
|
|
@@ -189,11 +190,19 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
189
190
|
const mod = (pages && loadedPagesModule(modulePath)) || await withSignal(loadModule(modulePath, pages), stream ? signal : undefined);
|
|
190
191
|
const requestOriginal = originalUrl ? new URL(originalUrl, url) : undefined;
|
|
191
192
|
if (requestOriginal) requestOriginal.pathname = removeBasePath(requestOriginal.pathname, manifest.config?.basePath || '');
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
const appHandler = typeof mod.default !== 'function';
|
|
194
|
+
// App Route Handlers receive a NextRequest below. Constructing the Pages
|
|
195
|
+
// IncomingMessage here duplicates URL, header and cookie work and allocates
|
|
196
|
+
// an unused Duplex. Keep the existing Pages request path unchanged.
|
|
197
|
+
const { request, bytes } = appHandler
|
|
198
|
+
? { bytes: Buffer.isBuffer(body) ? body : Buffer.from(body || '', 'base64') }
|
|
199
|
+
: createRequest({ url, originalUrl: requestOriginal?.href, params, method, headers, body });
|
|
200
|
+
if (!appHandler) {
|
|
201
|
+
request.draftMode = cacheContext.draftMode;
|
|
202
|
+
request.previewData = cacheContext.previewData;
|
|
203
|
+
request.preview = cacheContext.previewData !== false;
|
|
204
|
+
}
|
|
205
|
+
if (appHandler) {
|
|
197
206
|
const {NextRequest, proxyRouteRequest} = await appRequestPrimitives();
|
|
198
207
|
if (route.router === 'app' && manifest.dev) {
|
|
199
208
|
const listing = await collectStaticParams({ page: mod, pageConfig: route.handlerConfig, segments: [] }, route.pattern);
|
|
@@ -221,6 +230,7 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
221
230
|
const timeout = lifecycle && setTimeout(() => lifecycle.controller.abort(apiTimeoutError('response headers', timeoutMs)), timeoutMs);
|
|
222
231
|
timeout?.unref();
|
|
223
232
|
const pendingResponse = runRequestContext({ url, method, headers, params, production, cacheComponents: manifest.config?.cacheComponents, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, distDir: path.dirname(path.dirname(modulePath)), signal: lifecycle?.controller.signal || signal, previewModeId: manifest.previewModeId, basePath: manifest.config?.basePath || '', routePattern: route.pattern, cacheConfig: route.cacheConfig, phase: 'route', cacheState: cacheContext.cacheState, staticState: cacheContext.staticState, mutableCookies: true }, async () => {
|
|
233
|
+
currentRequest().afterScope = cacheContext.afterScope;
|
|
224
234
|
let result;
|
|
225
235
|
try { result = await handler(proxyRouteRequest(new NextRequest(requestOriginal || url, init), currentRequest()), { params: Promise.resolve(params) }); }
|
|
226
236
|
catch (error) {
|
package/runtime/app-render.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { addBasePath, removeBasePath } from '../compat/paths.cjs';
|
|
|
7
7
|
import { isDynamicBailout } from '../compat/dynamic-bailout.cjs';
|
|
8
8
|
import { Readable } from 'node:stream';
|
|
9
9
|
import { Worker } from 'node:worker_threads';
|
|
10
|
-
import { workerMetadata } from './worker-metadata.mjs';
|
|
10
|
+
import { workerMetadata, freezeClientModules, isImmutableClientModules } from './worker-metadata.mjs';
|
|
11
11
|
import { workerArtifacts } from './worker-artifacts.mjs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_BYTES } from './app-prefetch.mjs';
|
|
@@ -40,6 +40,7 @@ const edgeManifests = new WeakMap();
|
|
|
40
40
|
const emptyManifest = Object.freeze({});
|
|
41
41
|
let nextId = 0;
|
|
42
42
|
const pending = new Map();
|
|
43
|
+
const noop = () => {};
|
|
43
44
|
// Avoid temporary arrays on every completion/cancellation at high concurrency.
|
|
44
45
|
function hasPending(worker, minimum = 1, cancelledOnly = false) {
|
|
45
46
|
let count = 0;
|
|
@@ -72,14 +73,14 @@ function flightDecoder(production) {
|
|
|
72
73
|
// React's development decoder guards module initialization with NODE_ENV.
|
|
73
74
|
// This require is synchronous; restore the host before any stream work.
|
|
74
75
|
process.env.NODE_ENV = production ? 'production' : 'development';
|
|
75
|
-
let decoder = require(filename)
|
|
76
|
-
if (typeof decoder !== 'function') {
|
|
76
|
+
let decoder = require(filename);
|
|
77
|
+
if (typeof decoder.createFromNodeStream !== 'function') {
|
|
77
78
|
// A consumer may have previously required the guarded development file
|
|
78
79
|
// in production, leaving an empty module in Node's cache.
|
|
79
80
|
delete require.cache[filename];
|
|
80
|
-
decoder = require(filename)
|
|
81
|
+
decoder = require(filename);
|
|
81
82
|
}
|
|
82
|
-
if (typeof decoder !== 'function') throw new Error('React Flight decoder is unavailable for the requested render mode');
|
|
83
|
+
if (typeof decoder.createFromNodeStream !== 'function') throw new Error('React Flight decoder is unavailable for the requested render mode');
|
|
83
84
|
flightDecoders.set(production, decoder);
|
|
84
85
|
return decoder;
|
|
85
86
|
} finally {
|
|
@@ -182,7 +183,7 @@ function getWorker(production) {
|
|
|
182
183
|
}
|
|
183
184
|
if (type === 'chunk') {
|
|
184
185
|
try { request.channel.chunk(chunk, byteOffset, byteLength); }
|
|
185
|
-
catch (error) { request.channel.error(error); current.postMessage({ id, type: 'cancel' }); request.cancel(error); }
|
|
186
|
+
catch (error) { request.channel.error(error); current.postMessage({ id, type: 'cancel' }); request.cancel(error); return; }
|
|
186
187
|
if (!done) return;
|
|
187
188
|
}
|
|
188
189
|
if (request.completed) {
|
|
@@ -194,6 +195,8 @@ function getWorker(production) {
|
|
|
194
195
|
// work can legitimately continue afterwards and retains its own bounds;
|
|
195
196
|
// supervise isolate responsiveness, not total background-task duration.
|
|
196
197
|
clearTimeout(request.timeout);
|
|
198
|
+
// A destroyed Timeout can still own its AsyncLocalStorage frame.
|
|
199
|
+
request.timeout = undefined;
|
|
197
200
|
request.releaseWorkWatch = workWatchdog.add();
|
|
198
201
|
request.detach?.();
|
|
199
202
|
if (type === 'end' || done) request.channel.end();
|
|
@@ -219,7 +222,11 @@ function getWorker(production) {
|
|
|
219
222
|
// Keep only settlement bookkeeping after sending the body. The
|
|
220
223
|
// isolate can still be flushing cache writes after its terminal frame.
|
|
221
224
|
request.channel = undefined;
|
|
222
|
-
|
|
225
|
+
// Both closures capture the original response promise / AbortSignal.
|
|
226
|
+
// Detaching the listener alone does not release those references while
|
|
227
|
+
// legitimate background work keeps this entry in the pending map.
|
|
228
|
+
request.cancel = request.detach = undefined;
|
|
229
|
+
request.resolve = request.reject = noop;
|
|
223
230
|
});
|
|
224
231
|
current.on('error', fail);
|
|
225
232
|
current.on('exit', code => fail(new Error(`App Router worker exited (${code})`), true));
|
|
@@ -227,12 +234,41 @@ function getWorker(production) {
|
|
|
227
234
|
return current;
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
// Stdio workers can retain referenced MessagePorts after rendering (including
|
|
238
|
+
// diagnostics piped from the RSC isolate). EOF must drain real cache/after()
|
|
239
|
+
// settlement before explicitly closing those isolates, not wait for Node's
|
|
240
|
+
// incidental event-loop liveness or terminate unfinished application work.
|
|
241
|
+
export async function drainAppRuntime() {
|
|
242
|
+
while (pending.size) await Promise.all([...pending.values()].map(request => request.settled));
|
|
243
|
+
await closeAppRuntime();
|
|
244
|
+
}
|
|
245
|
+
|
|
230
246
|
export function closeAppRuntime() {
|
|
231
247
|
const current = [...workers.values()];
|
|
232
248
|
return Promise.all(current.map(worker => lifecycles.get(worker).terminate()));
|
|
233
249
|
}
|
|
234
250
|
|
|
235
|
-
|
|
251
|
+
function requestDeadline(entry, lifecycle, id, hardTimeoutMs, delayMs) {
|
|
252
|
+
// Keep this callback outside renderFlight's request closure: cancelled work
|
|
253
|
+
// retains its hard deadline, but must not thereby retain the HTTP signal or
|
|
254
|
+
// response promise until an unrelated cache write settles.
|
|
255
|
+
return setTimeout(() => {
|
|
256
|
+
// A blocking npm dependency cannot honor an AbortSignal. Retire the
|
|
257
|
+
// isolate at the hard deadline so subsequent requests can recover.
|
|
258
|
+
void lifecycle.terminate(appTimeoutError('RSC worker', hardTimeoutMs)).finally(entry.settle).catch(error => console.error('[prnext] RSC worker termination failed:', error));
|
|
259
|
+
pending.delete(id);
|
|
260
|
+
entry.lease.release();
|
|
261
|
+
const error = appTimeoutError('RSC worker', hardTimeoutMs);
|
|
262
|
+
if (entry.completed) console.error('[prnext] RSC background work failed:', error);
|
|
263
|
+
entry.channel?.error(error);
|
|
264
|
+
entry.detach?.();
|
|
265
|
+
entry.reject(error);
|
|
266
|
+
}, delayMs);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, signal, deferFinalizer } = {}) {
|
|
270
|
+
softTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 25_000;
|
|
271
|
+
hardTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 28_000;
|
|
236
272
|
const deadline = performance.now() + hardTimeoutMs;
|
|
237
273
|
let current, lifecycle, lease;
|
|
238
274
|
for (;;) {
|
|
@@ -254,20 +290,10 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
254
290
|
current.ref();
|
|
255
291
|
const id = ++nextId;
|
|
256
292
|
let settle;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
void lifecycle.terminate(appTimeoutError('RSC worker', hardTimeoutMs)).finally(settle).catch(error => console.error('[prnext] RSC worker termination failed:', error));
|
|
262
|
-
pending.delete(id);
|
|
263
|
-
lease.release();
|
|
264
|
-
const error = appTimeoutError('RSC worker', hardTimeoutMs);
|
|
265
|
-
if (entry.completed) console.error('[prnext] RSC background work failed:', error);
|
|
266
|
-
entry.channel?.error(error);
|
|
267
|
-
entry.detach?.();
|
|
268
|
-
reject(error);
|
|
269
|
-
}, Math.max(0, deadline - performance.now()));
|
|
270
|
-
const entry = { worker: current, resolve, reject, timeout, operation: request.operation, lease, settle };
|
|
293
|
+
const settled = new Promise(resolve => { settle = resolve; });
|
|
294
|
+
trackRequestWork(settled, signal, { deadline, supervised: true });
|
|
295
|
+
const entry = { worker: current, resolve, reject, operation: request.operation, lease, settle, settled };
|
|
296
|
+
const timeout = entry.timeout = requestDeadline(entry, lifecycle, id, hardTimeoutMs, Math.max(0, deadline - performance.now()));
|
|
271
297
|
function cancel(reason) {
|
|
272
298
|
if (entry.cancelled) return;
|
|
273
299
|
// Keep the independent deadline until the isolate confirms settlement.
|
|
@@ -278,7 +304,11 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
278
304
|
// Pause new work at 16 abandoned requests while their acknowledgements
|
|
279
305
|
// settle. Existing mutation/cache work keeps its original hard deadline.
|
|
280
306
|
if (!hasPending(current) && !lifecycle.retiring) current.unref();
|
|
281
|
-
reject(reason instanceof Error ? reason : new Error('App Router response was cancelled'));
|
|
307
|
+
entry.reject(reason instanceof Error ? reason : new Error('App Router response was cancelled'));
|
|
308
|
+
// Settlement/deadline tracking remains live, but a disconnected caller
|
|
309
|
+
// no longer needs its stream, promise or signal retained by this entry.
|
|
310
|
+
entry.channel = entry.cancel = entry.detach = undefined;
|
|
311
|
+
entry.resolve = entry.reject = noop;
|
|
282
312
|
}
|
|
283
313
|
entry.cancel = cancel;
|
|
284
314
|
if (request.stream) entry.channel = parentStreamChannel(current, id, cancel);
|
|
@@ -294,6 +324,10 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
294
324
|
pending.set(id, entry);
|
|
295
325
|
try {
|
|
296
326
|
const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
|
|
327
|
+
if (request.action && deferFinalizer) {
|
|
328
|
+
wire.waitForResponse = true;
|
|
329
|
+
deferFinalizer(() => { if (pending.has(id)) current.postMessage({ id, type: 'after-response' }); });
|
|
330
|
+
}
|
|
297
331
|
if (request.production) {
|
|
298
332
|
const artifactId = artifacts.get(current)(wire.partialFlight);
|
|
299
333
|
if (artifactId !== undefined) { wire.partialFlightId = artifactId; delete wire.partialFlight; }
|
|
@@ -317,11 +351,12 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
317
351
|
}
|
|
318
352
|
|
|
319
353
|
export async function decodeFlight(body, clientModules = {}, distDir, { production = process.env.NODE_ENV === 'production' } = {}) {
|
|
320
|
-
const ids = Object.keys(clientModules);
|
|
321
354
|
let cached = ssrManifests.get(clientModules);
|
|
322
355
|
// Keep only the manifest's module mapping, never a decoded React model or
|
|
323
356
|
// request context. Weak ownership releases it with the build manifest.
|
|
324
|
-
|
|
357
|
+
const immutable = isImmutableClientModules(clientModules);
|
|
358
|
+
const ids = cached && cached.distDir === distDir && cached.immutable && immutable ? null : Object.keys(clientModules);
|
|
359
|
+
if (!cached || cached.distDir !== distDir || (ids && (ids.length !== cached.paths.size || ids.some(id => cached.paths.get(id) !== clientModules[id].ssrModule)))) {
|
|
325
360
|
const moduleMap = Object.create(null), paths = new Map();
|
|
326
361
|
for (const id of ids) {
|
|
327
362
|
const item = clientModules[id];
|
|
@@ -332,13 +367,20 @@ export async function decodeFlight(body, clientModules = {}, distDir, { producti
|
|
|
332
367
|
moduleMap[id] = { '*': { id: moduleUrl, chunks: [moduleUrl, moduleUrl], name: '*' } };
|
|
333
368
|
paths.set(id, item.ssrModule);
|
|
334
369
|
}
|
|
335
|
-
cached = { distDir, paths, moduleMap };
|
|
370
|
+
cached = { distDir, paths, moduleMap, immutable };
|
|
336
371
|
ssrManifests.set(clientModules, cached);
|
|
337
|
-
}
|
|
372
|
+
} else if (immutable) cached.immutable = true;
|
|
338
373
|
// Hosts can build/render both modes in one process. The encoded protocol,
|
|
339
374
|
// rather than the host's current environment or import cache, selects React.
|
|
340
375
|
const decoder = flightDecoder(Boolean(production));
|
|
341
|
-
|
|
376
|
+
const serverConsumerManifest = { moduleMap: cached.moduleMap, serverModuleMap: null, moduleLoading: null };
|
|
377
|
+
// React's Node decoder can consume Web streams directly. Keep the Flight
|
|
378
|
+
// bytes in their original stream instead of allocating a Node Readable,
|
|
379
|
+
// forwarding its events and buffering a second queue for each response.
|
|
380
|
+
if (body instanceof ReadableStream && typeof decoder.createFromReadableStream === 'function') {
|
|
381
|
+
return decoder.createFromReadableStream(body, { serverConsumerManifest });
|
|
382
|
+
}
|
|
383
|
+
return decoder.createFromNodeStream(body instanceof ReadableStream ? Readable.fromWeb(body) : Readable.from([body]), serverConsumerManifest);
|
|
342
384
|
}
|
|
343
385
|
|
|
344
386
|
export async function renderHtml(tree, router, { timeoutMs = 25_000, formState = null, staticGeneration = false, nonce, strictMode = true } = {}) {
|
|
@@ -414,13 +456,16 @@ export async function renderAppPage(options) {
|
|
|
414
456
|
return response;
|
|
415
457
|
}
|
|
416
458
|
|
|
417
|
-
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 }) {
|
|
459
|
+
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 }) {
|
|
418
460
|
let clientModules = manifest.app?.clientModules || emptyManifest;
|
|
419
461
|
if (route.cacheConfig?.runtime === 'edge') {
|
|
420
462
|
let edge = !manifest.dev && edgeManifests.get(clientModules);
|
|
421
463
|
if (!edge) {
|
|
422
464
|
edge = Object.fromEntries(Object.entries(clientModules).map(([id, item]) => [id, item.edgeSsrModule ? { ...item, ssrModule: item.edgeSsrModule } : item]));
|
|
423
|
-
if (!manifest.dev)
|
|
465
|
+
if (!manifest.dev) {
|
|
466
|
+
if (isImmutableClientModules(clientModules)) freezeClientModules(edge);
|
|
467
|
+
edgeManifests.set(clientModules, edge);
|
|
468
|
+
}
|
|
424
469
|
}
|
|
425
470
|
clientModules = edge;
|
|
426
471
|
}
|
|
@@ -490,7 +535,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
490
535
|
if (stream && (!request.action || request.action.id)) {
|
|
491
536
|
let progressive;
|
|
492
537
|
try {
|
|
493
|
-
progressive = await renderFlight({ ...request, stream: true }, { signal });
|
|
538
|
+
progressive = await renderFlight({ ...request, stream: true }, { signal, deferFinalizer });
|
|
494
539
|
const rsc = Object.entries(headers).some(([name, value]) => name.toLowerCase() === 'rsc' && value === '1');
|
|
495
540
|
if (rsc || request.action?.id) {
|
|
496
541
|
if (method === 'HEAD') await progressive.body.cancel();
|
|
@@ -514,7 +559,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
514
559
|
}
|
|
515
560
|
}
|
|
516
561
|
let result;
|
|
517
|
-
try { result = await renderFlight(request, { signal }); }
|
|
562
|
+
try { result = await renderFlight(request, { signal, deferFinalizer }); }
|
|
518
563
|
catch (error) {
|
|
519
564
|
const result = navigationResponse(error);
|
|
520
565
|
if (result) return { ...result, headers: { ...responseHeaders, ...result.headers, ...error.responseHeaders } };
|
|
@@ -531,7 +576,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
531
576
|
let html, model, recovery = false;
|
|
532
577
|
try {
|
|
533
578
|
model = await decodeFlight(result.body, clientModules, distDir, { production });
|
|
534
|
-
html = await renderHtml(model.tree, model.router, { formState: model.formState, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
|
|
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 });
|
|
535
580
|
} catch (error) {
|
|
536
581
|
if (error?.statusCode === 504) throw error;
|
|
537
582
|
const navigation = navigationResponse(error);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Cache only the compiler-owned, immutable routing scaffold. Component modules,
|
|
2
|
+
// configuration, request parameters and rendered trees are never memoized here.
|
|
3
|
+
const plans = new WeakMap();
|
|
4
|
+
const liveTrees = new WeakSet();
|
|
5
|
+
const ownValue = (object, name) => Object.getOwnPropertyDescriptor(object, name)?.value;
|
|
6
|
+
function frozenRecord(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || !Object.isFrozen(value)) return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) return false;
|
|
10
|
+
if (Array.isArray(value) && Object.hasOwn(value, Symbol.iterator)) return false;
|
|
11
|
+
return Reflect.ownKeys(value).every(key => 'value' in Object.getOwnPropertyDescriptor(value, key));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Mutable development/custom trees continue through the live traversal. */
|
|
15
|
+
export function appRoutePlan(root) {
|
|
16
|
+
if (!root || !Object.isFrozen(root)) return null;
|
|
17
|
+
const existing = plans.get(root);
|
|
18
|
+
if (existing) return existing;
|
|
19
|
+
if (liveTrees.has(root)) return null;
|
|
20
|
+
const pages = [], nodes = new Map(), rootLayouts = new Set();
|
|
21
|
+
let sequence = 0;
|
|
22
|
+
function visit(node, chain, branch, hasLayout) {
|
|
23
|
+
if (!frozenRecord(node)) return false;
|
|
24
|
+
const children = ownValue(node, 'children'), slots = ownValue(node, 'slots'), files = ownValue(node, 'files');
|
|
25
|
+
if (!Array.isArray(children) || !frozenRecord(children) || !frozenRecord(slots) || !frozenRecord(files)
|
|
26
|
+
|| typeof ownValue(node, 'path') !== 'string' || typeof ownValue(node, 'pattern') !== 'string'
|
|
27
|
+
|| typeof ownValue(node, 'segment') !== 'string' || nodes.has(node)) return false;
|
|
28
|
+
// Bound retained chains even for hand-written frozen manifests.
|
|
29
|
+
if (sequence >= 4096 || chain.length >= 64) return false;
|
|
30
|
+
const next = [...chain, node], start = sequence++;
|
|
31
|
+
const info = { start, end: start, depth: chain.length, branch, slots: Object.entries(slots), metadataSlots: Object.entries(slots).sort(([a], [b]) => a.localeCompare(b)) };
|
|
32
|
+
nodes.set(node, info);
|
|
33
|
+
if (files.layout && !hasLayout) rootLayouts.add(node.path);
|
|
34
|
+
if (files.page) pages.push({ node, chain: next, start, branch,
|
|
35
|
+
score: node.pattern.split('/').filter(Boolean).reduce((sum, value) => sum + (value.startsWith('[[...') ? 0 : value.startsWith('[...') ? 1 : value.startsWith('[') ? 3 : 6), 0) });
|
|
36
|
+
for (const child of children) if (!visit(child, next, branch, hasLayout || !!files.layout)) return false;
|
|
37
|
+
for (const child of Object.values(slots)) if (!visit(child, next, child, hasLayout || !!files.layout)) return false;
|
|
38
|
+
info.end = sequence;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (!visit(root, [], root, false)) { liveTrees.add(root); return null; }
|
|
42
|
+
const plan = { pages, nodes, rootLayouts };
|
|
43
|
+
plans.set(root, plan);
|
|
44
|
+
return plan;
|
|
45
|
+
}
|
|
@@ -1,26 +1,98 @@
|
|
|
1
1
|
// Route matching and navigation state do not need React or metadata rendering.
|
|
2
2
|
import { removeBasePath } from '../compat/paths.cjs';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// Only trusted route syntax is cached, never URLs, decoded parameters or request
|
|
5
|
+
// state. Bound the shared lookup's entry count and retained text/segments.
|
|
6
|
+
const patterns = new Map();
|
|
7
|
+
// Live manifest entries own their parsed syntax weakly. Large route tables must
|
|
8
|
+
// not churn the bounded cross-call cache once for every saved parallel slot.
|
|
9
|
+
// Their lifetime follows the existing manifest; no old manifest is retained.
|
|
10
|
+
// Re-read the pattern on each use so mutable development manifests stay live.
|
|
11
|
+
const routePatterns = new WeakMap();
|
|
12
|
+
let patternCharacters = 0, patternSegments = 0;
|
|
13
|
+
function compilePattern(pattern) {
|
|
14
|
+
let compiled = patterns.get(pattern);
|
|
15
|
+
if (compiled) return compiled;
|
|
16
|
+
const segments = pattern.split('/').filter(Boolean);
|
|
17
|
+
const kinds = new Uint8Array(segments.length);
|
|
18
|
+
let score = 0;
|
|
19
|
+
for (let index = 0; index < segments.length; index++) {
|
|
20
|
+
const segment = segments[index];
|
|
21
|
+
if (segment[0] !== '[') { score += 6; continue; }
|
|
22
|
+
score += segment.startsWith('[[...') ? 0 : segment.startsWith('[...') ? 1 : 3;
|
|
11
23
|
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(segment);
|
|
12
24
|
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(segment);
|
|
13
25
|
if (optional || catchAll) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
kinds[index] = optional ? 3 : 2;
|
|
27
|
+
segments[index] = (optional || catchAll)[1];
|
|
28
|
+
} else {
|
|
29
|
+
const dynamic = /^\[([^\]]+)\]$/.exec(segment);
|
|
30
|
+
if (dynamic) { kinds[index] = 1; segments[index] = dynamic[1]; }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
compiled = { segments, kinds, score, characters: pattern.length };
|
|
34
|
+
if (pattern.length <= 2048 && segments.length <= 64) {
|
|
35
|
+
while (patterns.size >= 1024 || patternCharacters + pattern.length > 128 * 1024 || patternSegments + segments.length > 8192) {
|
|
36
|
+
const oldest = patterns.keys().next().value;
|
|
37
|
+
const removed = patterns.get(oldest);
|
|
38
|
+
patterns.delete(oldest);
|
|
39
|
+
patternCharacters -= removed.characters;
|
|
40
|
+
patternSegments -= removed.segments.length;
|
|
17
41
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else if (segment !== parts[index]) return null;
|
|
42
|
+
patterns.set(pattern, compiled);
|
|
43
|
+
patternCharacters += pattern.length;
|
|
44
|
+
patternSegments += segments.length;
|
|
22
45
|
}
|
|
23
|
-
return
|
|
46
|
+
return compiled;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pathSegments(pathname) {
|
|
50
|
+
const raw = pathname.split('/').filter(Boolean);
|
|
51
|
+
try { return raw.map(decodeURIComponent); }
|
|
52
|
+
catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compiledRoute(route) {
|
|
56
|
+
const pattern = route.pattern;
|
|
57
|
+
const cached = routePatterns.get(route);
|
|
58
|
+
if (cached && cached.pattern === pattern) return cached.compiled;
|
|
59
|
+
const compiled = compilePattern(pattern);
|
|
60
|
+
if (pattern.length <= 2048 && compiled.segments.length <= 64) routePatterns.set(route, { pattern, compiled });
|
|
61
|
+
else routePatterns.delete(route);
|
|
62
|
+
return compiled;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function matchesParts({ segments, kinds }, parts, prefix = false, params) {
|
|
66
|
+
for (let index = 0; index < segments.length; index++) {
|
|
67
|
+
const kind = kinds[index];
|
|
68
|
+
if (kind >= 2) {
|
|
69
|
+
if (kind === 2 && index >= parts.length) return false;
|
|
70
|
+
if (params && index < parts.length) params[segments[index]] = parts.slice(index);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (index >= parts.length) return false;
|
|
74
|
+
if (kind === 1) { if (params) params[segments[index]] = parts[index]; }
|
|
75
|
+
else if (segments[index] !== parts[index]) return false;
|
|
76
|
+
}
|
|
77
|
+
return prefix || segments.length === parts.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function matchAppPattern(pattern, pathname, prefix = false) {
|
|
81
|
+
const compiled = compilePattern(pattern), parts = pathSegments(pathname);
|
|
82
|
+
if (!parts) return null;
|
|
83
|
+
const params = {};
|
|
84
|
+
return matchesParts(compiled, parts, prefix, params) ? params : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Decode a request pathname once; each match still owns its parameter values. */
|
|
88
|
+
export function createAppMatcher(pathname) {
|
|
89
|
+
const parts = pathSegments(pathname);
|
|
90
|
+
return (pattern, prefix = false) => {
|
|
91
|
+
const compiled = compilePattern(pattern);
|
|
92
|
+
if (!parts) return null;
|
|
93
|
+
const params = {};
|
|
94
|
+
return matchesParts(compiled, parts, prefix, params) ? params : null;
|
|
95
|
+
};
|
|
24
96
|
}
|
|
25
97
|
|
|
26
98
|
export function readRouterState(request) {
|
|
@@ -49,13 +121,21 @@ export function advancedRoutingSources(request, routes) {
|
|
|
49
121
|
const modules = new Set();
|
|
50
122
|
for (const saved of Object.values(state.slots)) {
|
|
51
123
|
const pathname = new URL(saved.url, 'http://prnext.local').pathname;
|
|
52
|
-
const
|
|
53
|
-
|
|
124
|
+
const parts = pathSegments(pathname);
|
|
125
|
+
if (!parts) continue;
|
|
126
|
+
let route, bestScore = -1;
|
|
127
|
+
for (let index = 0, length = routes.length; index < length; index++) {
|
|
128
|
+
if (!(index in routes)) continue;
|
|
129
|
+
const item = routes[index];
|
|
130
|
+
if (item.router !== 'app' || item.kind !== 'page' || item.internal) continue;
|
|
131
|
+
const compiled = compiledRoute(item);
|
|
132
|
+
// The original stable sort kept the first route when scores were equal.
|
|
133
|
+
// Lower-scoring routes cannot replace a match already found.
|
|
134
|
+
if (compiled.score <= bestScore || !matchesParts(compiled, parts)) continue;
|
|
135
|
+
route = item;
|
|
136
|
+
bestScore = compiled.score;
|
|
137
|
+
}
|
|
54
138
|
if (route) modules.add(route.module);
|
|
55
139
|
}
|
|
56
140
|
return [...modules];
|
|
57
141
|
}
|
|
58
|
-
|
|
59
|
-
function score(pattern) {
|
|
60
|
-
return pattern.split('/').filter(Boolean).reduce((sum, value) => sum + (value.startsWith('[[...') ? 0 : value.startsWith('[...') ? 1 : value.startsWith('[') ? 3 : 6), 0);
|
|
61
|
-
}
|
package/runtime/app-routing.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import path from 'node:path';
|
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { currentRequest, runRequestContext } from '../compat/headers.cjs';
|
|
8
8
|
import { staticParams } from '../compat/static-generation.cjs';
|
|
9
|
+
import { appRoutePlan } from './app-route-plan.mjs';
|
|
9
10
|
|
|
10
|
-
import { matchAppPattern, readRouterState } from './app-routing-state.mjs';
|
|
11
|
+
import { matchAppPattern, createAppMatcher, readRouterState } from './app-routing-state.mjs';
|
|
11
12
|
export { matchAppPattern, readRouterState, advancedRoutingSources } from './app-routing-state.mjs';
|
|
12
13
|
|
|
13
14
|
export async function loadAdvancedRouting(entry, request) {
|
|
@@ -51,10 +52,12 @@ function selectedSegments(chain, params) {
|
|
|
51
52
|
/** Parallel slots retain their original Flight children on soft navigation. */
|
|
52
53
|
export function createAdvancedTree(entry, request, searchParams) {
|
|
53
54
|
const pathname = removeBasePath(new URL(request.url).pathname, request.basePath || '');
|
|
55
|
+
const matchCurrent = createAppMatcher(pathname);
|
|
54
56
|
const old = readRouterState(request), slots = {}, activeParams = { ...request.params };
|
|
55
57
|
const rebuild = !!old && (old.refresh || old.restore || request.method === 'POST');
|
|
56
58
|
let invalid = false;
|
|
57
59
|
const root = entry.routing;
|
|
60
|
+
const plan = appRoutePlan(root);
|
|
58
61
|
let metadataHead;
|
|
59
62
|
const keyScopes = new Map();
|
|
60
63
|
const instantTrees = [];
|
|
@@ -62,18 +65,20 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
62
65
|
if (request.partialParams?.length) keyScopes.set(key, { key, prefix, suffix, names: Object.keys(values), pattern: node.pattern, ...(liveProps ? { liveProps } : {}), ...(segments ? { segments } : {}) });
|
|
63
66
|
return key;
|
|
64
67
|
}
|
|
65
|
-
const rootLayouts = new Set();
|
|
68
|
+
const rootLayouts = plan?.rootLayouts || new Set();
|
|
66
69
|
const metadataSelections = new Map();
|
|
67
70
|
let missing = entry.interceptionOnly && !old, rootPage;
|
|
68
71
|
const all = [];
|
|
69
72
|
function walk(node, chain = []) {
|
|
70
73
|
const next = [...chain, node];
|
|
71
74
|
if (node.files.layout && !chain.some(parent => parent.files.layout)) rootLayouts.add(node.path);
|
|
72
|
-
if (node.files.page) all.push({ node, chain: next, params:
|
|
75
|
+
if (node.files.page) all.push({ node, chain: next, params: matchCurrent(node.pattern) });
|
|
73
76
|
for (const child of node.children) walk(child, next);
|
|
74
77
|
for (const child of Object.values(node.slots)) walk(child, next);
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
if (plan) {
|
|
80
|
+
for (const page of plan.pages) all.push({ ...page, params: matchCurrent(page.node.pattern) });
|
|
81
|
+
} else walk(root);
|
|
77
82
|
let activeInterceptions;
|
|
78
83
|
function canIntercept(candidate) {
|
|
79
84
|
if (!candidate.node.interception || !old) return false;
|
|
@@ -97,14 +102,29 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
97
102
|
function candidates(node, chain = []) {
|
|
98
103
|
const result = [], next = [...chain, node];
|
|
99
104
|
if (node.files.page) {
|
|
100
|
-
const params =
|
|
105
|
+
const params = matchCurrent(node.pattern);
|
|
101
106
|
if (params && (!node.interception || canIntercept({ node }))) result.push({ node, chain: next, params });
|
|
102
107
|
}
|
|
103
108
|
for (const child of node.children) result.push(...candidates(child, next));
|
|
104
109
|
return result.sort((left, right) => Number(!!right.node.interception) - Number(!!left.node.interception) || score(right.node.pattern) - score(left.node.pattern));
|
|
105
110
|
}
|
|
111
|
+
function firstCandidate(node) {
|
|
112
|
+
if (!plan) return candidates(node)[0];
|
|
113
|
+
const scope = plan.nodes.get(node);
|
|
114
|
+
let best;
|
|
115
|
+
for (const candidate of all) {
|
|
116
|
+
if (!candidate.params || candidate.branch !== scope.branch || candidate.start < scope.start || candidate.start >= scope.end
|
|
117
|
+
|| (candidate.node.interception && !canIntercept(candidate))) continue;
|
|
118
|
+
if (!best || Number(!!candidate.node.interception) > Number(!!best.node.interception)
|
|
119
|
+
|| (!!candidate.node.interception === !!best.node.interception && candidate.score > best.score)) best = candidate;
|
|
120
|
+
}
|
|
121
|
+
// Separate request-owned params from the interception/scaffolding candidates,
|
|
122
|
+
// as in the live traversal. No parameter objects survive this request.
|
|
123
|
+
return best && { node: best.node, chain: best.chain.slice(scope.depth), params: matchCurrent(best.node.pattern) };
|
|
124
|
+
}
|
|
125
|
+
const slotEntries = node => plan?.nodes.get(node).slots || Object.entries(node.slots);
|
|
106
126
|
function scopedParams(node) {
|
|
107
|
-
const current =
|
|
127
|
+
const current = matchCurrent(node.pattern, true) || (old && matchAppPattern(node.pattern, old.source, true));
|
|
108
128
|
if (current) return current;
|
|
109
129
|
for (const [key, value] of Object.entries(old?.slots || {})) {
|
|
110
130
|
if (!key.startsWith(`${node.path}::`)) continue;
|
|
@@ -117,7 +137,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
117
137
|
return registerKey(`${node.path}::${JSON.stringify(scopedParams(node))}`, node, `${node.path}::`);
|
|
118
138
|
}
|
|
119
139
|
function sourceURL(node, previous) {
|
|
120
|
-
return
|
|
140
|
+
return matchCurrent(node.pattern, true) ? pathname + new URL(request.url).search : previous?.source || previous?.url || old?.source || pathname;
|
|
121
141
|
}
|
|
122
142
|
function accessURL(url, previous) {
|
|
123
143
|
if (previous?.url === url && previous.accessUrl) return previous.accessUrl;
|
|
@@ -148,8 +168,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
148
168
|
function slot(node, owner, name) {
|
|
149
169
|
const key = registerKey(`${scope(owner)}::${name}`, owner, `${owner.path}::`, `::${name}`, ['id']);
|
|
150
170
|
const previous = old?.slots[key];
|
|
151
|
-
let
|
|
152
|
-
let selected = rebuild && previous ? savedBranch(node, previous) : choices[0];
|
|
171
|
+
let selected = rebuild && previous ? savedBranch(node, previous) : firstCandidate(node);
|
|
153
172
|
if (rebuild && previous && !selected) { invalid = true; return { tree: null, segments: [] }; }
|
|
154
173
|
if (!rebuild) {
|
|
155
174
|
for (const candidate of interceptions) {
|
|
@@ -189,9 +208,9 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
189
208
|
// Find the nearest default whose route prefix contains the destination.
|
|
190
209
|
const defaults = [];
|
|
191
210
|
function findDefault(value, chain = []) {
|
|
192
|
-
if (!
|
|
211
|
+
if (!matchCurrent(value.pattern, true) || (value.interception && !old)) return;
|
|
193
212
|
const next = [...chain, value];
|
|
194
|
-
if (value.files.default) defaults.push({ node: value, chain: next, params:
|
|
213
|
+
if (value.files.default) defaults.push({ node: value, chain: next, params: matchCurrent(value.pattern, true), default: true });
|
|
195
214
|
for (const child of value.children) findDefault(child, next);
|
|
196
215
|
}
|
|
197
216
|
findDefault(node);
|
|
@@ -207,9 +226,9 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
207
226
|
let selectedTree = renderPath(selected.chain, selected, 0, name === 'children' && owner === node);
|
|
208
227
|
const context = request.routingContexts?.get(selectedURL);
|
|
209
228
|
if (context && request.renderRoutingBranch) selectedTree = request.renderRoutingBranch(selectedTree, context);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
instantTrees.push({path:(owner.path || '/')+' @'+name,tree:selectedTree});
|
|
229
|
+
if(request.instantValidation && name!=='children'){
|
|
230
|
+
const instant=selected.node.config?.page?.instant ?? selected.chain.findLast(item=>item.config?.layout?.instant!==undefined)?.config.layout.instant;
|
|
231
|
+
if(instantEnabled(instant,request.production)) instantTrees.push({path:(owner.path || '/')+' @'+name,tree:selectedTree});
|
|
213
232
|
}
|
|
214
233
|
return { tree: React.createElement(entry.LayoutSlot, { key, id: key, preserve: false }, selectedTree), segments: selected.default ? [] : selectedSegments(selected.chain.slice(node === owner ? 1 : 0), selected.params) };
|
|
215
234
|
}
|
|
@@ -248,7 +267,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
248
267
|
const selected = { children: selection.default ? [] : selectedSegments(chain.slice(index + 1), selection.params) };
|
|
249
268
|
if (preserveChild) selected.children = null;
|
|
250
269
|
const parallel = {};
|
|
251
|
-
for (const [name, child] of
|
|
270
|
+
for (const [name, child] of slotEntries(node)) {
|
|
252
271
|
const rendered = slot(child, node, name);
|
|
253
272
|
parallel[name] = rendered.tree; selected[name] = rendered.segments;
|
|
254
273
|
}
|
|
@@ -286,7 +305,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
286
305
|
if (layout) metadataItems.push({ module: node.files.layout, params: staticParams(layout.params, currentRequest()), filePath: page ? undefined : node.path, context: layout.context });
|
|
287
306
|
// Next orders paths containing named slots before ordinary children,
|
|
288
307
|
// with slot names in locale order (compareAppPaths in its app loader).
|
|
289
|
-
for (const [, child] of Object.entries(node.slots).sort(([a], [b]) => a.localeCompare(b))) visit(child);
|
|
308
|
+
for (const [, child] of plan?.nodes.get(node).metadataSlots || Object.entries(node.slots).sort(([a], [b]) => a.localeCompare(b))) visit(child);
|
|
290
309
|
if (page) metadataItems.push({ module: node.files[page.default ? 'default' : 'page'], params: staticParams(page.params, currentRequest()), searchParams: page.searchParams || searchParams, filePath: node.path, context: request.routingContexts?.get(page.url) });
|
|
291
310
|
for (const child of node.children) visit(child);
|
|
292
311
|
}
|
|
@@ -300,7 +319,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
300
319
|
metadataHead = head;
|
|
301
320
|
const main = slot(root, root, 'children');
|
|
302
321
|
const parallel = {}, selected = { children: main.segments };
|
|
303
|
-
for (const [name, child] of
|
|
322
|
+
for (const [name, child] of slotEntries(root)) {
|
|
304
323
|
const result = slot(child, root, name);
|
|
305
324
|
parallel[name] = result.tree; selected[name] = result.segments;
|
|
306
325
|
}
|
|
@@ -310,7 +329,7 @@ export function createAdvancedTree(entry, request, searchParams) {
|
|
|
310
329
|
const content = React.createElement(React.Fragment, null, main.tree, ...Object.values(parallel));
|
|
311
330
|
instantTrees.push({ path: root.path || '/', tree: root.files.loading ? React.createElement(React.Suspense, { fallback: React.createElement(component(root.files.loading, 'loading')) }, content) : content });
|
|
312
331
|
}
|
|
313
|
-
const instantDisabled = all.
|
|
332
|
+
const instantDisabled = request.instantValidation && all.some(item => item.chain.some(node => Object.values(node.config || {}).some(config => config.instant && typeof config.instant === 'object' && (config.instant.unstable_disableValidation || (request.production ? config.instant.unstable_disableBuildValidation : config.instant.unstable_disableDevValidation)))));
|
|
314
333
|
let tree = wrap(root, main.tree, parallel, selected, scope(root), {}, rootPage?.chain.slice(1).map(item => item.segment), rootPage?.node);
|
|
315
334
|
if (missing) {
|
|
316
335
|
request.responseStatus = 404;
|