@thomas.f/prnext 0.1.17-alpha → 0.1.18-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -869
- package/build/actions.mjs +1 -1
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +81 -26
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +31 -8
- package/build/config.mjs +30 -1
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-update.mjs +4 -2
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/index.mjs +59 -26
- package/build/instrumentation.mjs +30 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +58 -9
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +6 -3
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +60 -4
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +68 -3
- package/compat/link.cjs +12 -9
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +171 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +82 -0
- package/docs/ai/troubleshooting.md +37 -2
- package/package.json +26 -7
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +212 -65
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/development.mjs +29 -12
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const INTENT = 1;
|
|
2
|
+
const abortError = () => Object.assign(new Error('Speculative navigation cancelled'), { name: 'AbortError' });
|
|
3
|
+
|
|
4
|
+
// The completed-result cache obeys the same priority as the producer queue.
|
|
5
|
+
// Otherwise a viewport scan could evict the hovered route immediately after
|
|
6
|
+
// the scheduler had correctly protected its request.
|
|
7
|
+
export function prefetchCacheVictim(cache, priority) {
|
|
8
|
+
let candidate;
|
|
9
|
+
for (const [key, entry] of cache) {
|
|
10
|
+
if (entry.abort.signal.aborted) return key;
|
|
11
|
+
if (!candidate || entry.priority < candidate[1].priority) candidate = [key, entry];
|
|
12
|
+
}
|
|
13
|
+
return candidate && candidate[1].priority <= priority ? candidate[0] : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Speculative work never competes with a committed navigation. The same policy
|
|
17
|
+
// is used by both routers; data and authorization remain in their own caches.
|
|
18
|
+
export function createPrefetchScheduler({ window: win = globalThis.window, document: doc = globalThis.document,
|
|
19
|
+
concurrency = 2, limit = 8 } = {}) {
|
|
20
|
+
const tasks = new Set();
|
|
21
|
+
let serial = 0, active = 0, navigating = 0, disposed = false, scheduled = false;
|
|
22
|
+
const connection = win?.navigator?.connection;
|
|
23
|
+
const priorityOf = value => value === 'viewport' ? 0 : INTENT;
|
|
24
|
+
function policy(priority) {
|
|
25
|
+
const network = win?.navigator?.connection;
|
|
26
|
+
if (doc?.visibilityState === 'hidden' || win?.navigator?.onLine === false || network?.saveData || /^(slow-)?2g$/.test(network?.effectiveType || '')) return 0;
|
|
27
|
+
const slow = network?.effectiveType === '3g' || (network?.downlink > 0 && network.downlink < 1.5);
|
|
28
|
+
return slow ? (priority < INTENT ? 0 : 1) : concurrency;
|
|
29
|
+
}
|
|
30
|
+
function finish(task, error, value) {
|
|
31
|
+
if (!tasks.delete(task)) return;
|
|
32
|
+
if (task.running) active--;
|
|
33
|
+
task.abort.signal.removeEventListener('abort', task.cancelled);
|
|
34
|
+
const { resolve, reject } = task;
|
|
35
|
+
task.run = task.resolve = task.reject = task.cancelled = undefined;
|
|
36
|
+
error ? reject(error) : resolve(value);
|
|
37
|
+
wake();
|
|
38
|
+
}
|
|
39
|
+
function start(task) {
|
|
40
|
+
task.running = true; active++;
|
|
41
|
+
let result;
|
|
42
|
+
try { result = task.run(task.abort.signal); } catch (error) { finish(task, error); return; }
|
|
43
|
+
Promise.resolve(result).then(value => finish(task, null, value), error => finish(task, error));
|
|
44
|
+
}
|
|
45
|
+
function pump() {
|
|
46
|
+
scheduled = false;
|
|
47
|
+
if (disposed) return;
|
|
48
|
+
const queued = [...tasks].filter(task => !task.running).sort((a, b) => b.priority - a.priority || a.order - b.order);
|
|
49
|
+
for (const task of queued) {
|
|
50
|
+
if (!tasks.has(task)) continue;
|
|
51
|
+
if (task.priority === 2) { start(task); continue; }
|
|
52
|
+
if (navigating) break;
|
|
53
|
+
const slots = policy(task.priority);
|
|
54
|
+
if (!slots) { task.abort.abort(abortError()); continue; }
|
|
55
|
+
if (active >= slots) continue;
|
|
56
|
+
start(task);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function wake() {
|
|
60
|
+
if (scheduled || disposed) return;
|
|
61
|
+
scheduled = true; queueMicrotask(pump);
|
|
62
|
+
}
|
|
63
|
+
function changed() {
|
|
64
|
+
for (const task of tasks) if (task.priority !== 2 && !policy(task.priority)) task.abort.abort(abortError());
|
|
65
|
+
wake();
|
|
66
|
+
}
|
|
67
|
+
doc?.addEventListener?.('visibilitychange', changed);
|
|
68
|
+
win?.addEventListener?.('online', changed);
|
|
69
|
+
win?.addEventListener?.('offline', changed);
|
|
70
|
+
connection?.addEventListener?.('change', changed);
|
|
71
|
+
return {
|
|
72
|
+
schedule(run, { priority, abort = new AbortController() } = {}) {
|
|
73
|
+
const rank = priorityOf(priority);
|
|
74
|
+
if (disposed || abort.signal.aborted || !policy(rank)) return undefined;
|
|
75
|
+
if (tasks.size >= limit) {
|
|
76
|
+
// Keep intent ahead of broad viewport scans. Never evict demand work.
|
|
77
|
+
const oldest = [...tasks].filter(task => task.priority < 2).sort((a, b) => a.priority - b.priority || a.order - b.order)[0];
|
|
78
|
+
if (!oldest || oldest.priority > rank) return undefined;
|
|
79
|
+
oldest.abort.abort(abortError());
|
|
80
|
+
}
|
|
81
|
+
const task = { run, abort, priority: rank, order: serial++ };
|
|
82
|
+
const promise = new Promise((resolve, reject) => { task.resolve = resolve; task.reject = reject; });
|
|
83
|
+
// Cancellation also settles a task whose producer ignores AbortSignal.
|
|
84
|
+
// A late result cannot regain a queue slot or resurrect an invalid cache.
|
|
85
|
+
task.cancelled = () => finish(task, abort.signal.reason || abortError());
|
|
86
|
+
tasks.add(task); abort.signal.addEventListener('abort', task.cancelled, { once: true });
|
|
87
|
+
wake();
|
|
88
|
+
return { abort, promise,
|
|
89
|
+
promote(priority = 'intent') { if (tasks.has(task)) { task.priority = Math.max(task.priority, priorityOf(priority)); wake(); } },
|
|
90
|
+
demand() { if (tasks.has(task)) { task.priority = 2; wake(); } },
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
navigation(target) {
|
|
94
|
+
navigating++;
|
|
95
|
+
target?.demand();
|
|
96
|
+
for (const task of tasks) if (task.abort !== target?.abort && task.priority !== 2) task.abort.abort(abortError());
|
|
97
|
+
let released = false;
|
|
98
|
+
return () => { if (!released) { released = true; navigating--; wake(); } };
|
|
99
|
+
},
|
|
100
|
+
dispose() {
|
|
101
|
+
if (disposed) return;
|
|
102
|
+
disposed = true;
|
|
103
|
+
doc?.removeEventListener?.('visibilitychange', changed);
|
|
104
|
+
win?.removeEventListener?.('online', changed);
|
|
105
|
+
win?.removeEventListener?.('offline', changed);
|
|
106
|
+
connection?.removeEventListener?.('change', changed);
|
|
107
|
+
for (const task of tasks) task.abort.abort(abortError());
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/runtime/profile.mjs
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import presets from './profiles.json' with { type: 'json' };
|
|
2
2
|
|
|
3
|
+
export function rscResourceLimits(environment=process.env,production=environment.NODE_ENV!=='development'){
|
|
4
|
+
if(!production||/--max[-_]semi[-_]space[-_]size(?:[=\s]|$)/.test(environment.NODE_OPTIONS||''))return undefined;
|
|
5
|
+
return {maxYoungGenerationSizeMb:runtimeProfile(environment,production).youngGenerationMiB};
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export function runtimeProfile(environment = process.env, production = environment.NODE_ENV !== 'development') {
|
|
4
9
|
const requested = environment.PRNEXT_PROFILE || (environment.PRNEXT_MEMORY_PROFILE === 'compact' ? 'compact' : 'balanced');
|
|
5
10
|
const name = requested === 'standard' ? 'classic' : requested;
|
|
6
11
|
if (!Object.hasOwn(presets, name)) throw new Error(`Unknown runtime profile ${JSON.stringify(name)}; use ${Object.keys(presets).join(', ')}`);
|
|
7
12
|
const effective = production ? name : 'classic';
|
|
13
|
+
if(production&&environment.PRNEXT_EFFECTIVE_PROFILE){
|
|
14
|
+
let value;
|
|
15
|
+
try{value=JSON.parse(environment.PRNEXT_EFFECTIVE_PROFILE);}catch{throw new Error('Invalid effective runtime profile');}
|
|
16
|
+
const settings=value?.[1],original=presets[effective];
|
|
17
|
+
if(!Array.isArray(value)||value.length!==2||value[0]!==effective||!settings||
|
|
18
|
+
Object.keys(settings).length!==Object.keys(original).length||Object.keys(original).some(key=>!Object.hasOwn(settings,key)||
|
|
19
|
+
(original[key]===null?settings[key]!==null&&(!Number.isSafeInteger(settings[key])||settings[key]<1):typeof original[key]==='boolean'?typeof settings[key]!=='boolean':!Number.isSafeInteger(settings[key])||settings[key]<1||settings[key]>Math.max(512,original[key]))))throw new Error('Invalid effective runtime profile');
|
|
20
|
+
return {name:effective,...settings};
|
|
21
|
+
}
|
|
8
22
|
return { name: effective, ...presets[effective] };
|
|
9
23
|
}
|
package/runtime/profiles.json
CHANGED
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"renderIdleSeconds": null
|
|
41
41
|
},
|
|
42
42
|
"memory": {
|
|
43
|
-
"optimizeForSize":
|
|
43
|
+
"optimizeForSize": false,
|
|
44
44
|
"semiSpaceMiB": 2,
|
|
45
|
-
"youngGenerationMiB":
|
|
45
|
+
"youngGenerationMiB": 8,
|
|
46
46
|
"renderStarts": 8,
|
|
47
47
|
"liveResponses": 28,
|
|
48
48
|
"apiConcurrency": 64,
|
package/runtime/render.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { localePath } from '../compat/locale.cjs';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import DefaultApp, { loadGetInitialProps } from '../compat/app.cjs';
|
|
@@ -94,6 +95,7 @@ export async function renderPageRequest(options) {
|
|
|
94
95
|
try { result = await renderPage({ ...options, capturedResponse }); }
|
|
95
96
|
catch (error) {
|
|
96
97
|
if ([400, 504].includes(error?.statusCode) || options.signal?.aborted) throw error;
|
|
98
|
+
reportRequestError(error);
|
|
97
99
|
console.error('[prnext]', error?.stack || error);
|
|
98
100
|
return renderPageError(options, { statusCode: 500, error, responseHeaders: capturedResponse.getHeaders() });
|
|
99
101
|
}
|
|
@@ -266,6 +268,7 @@ export async function renderIsrPage(options) {
|
|
|
266
268
|
catch (error) {
|
|
267
269
|
if (!options.capturePageFailure) throw error;
|
|
268
270
|
const pageFailure = summarizePageFailure(error);
|
|
271
|
+
reportRequestError(error);
|
|
269
272
|
console.error('[prnext] Page generation failed:', pageFailure.stack || pageFailure.message);
|
|
270
273
|
return { status: 500, headers: {}, body: Buffer.alloc(0), pageFailure };
|
|
271
274
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Shared browser route matching; importing this never loads either router. */
|
|
2
|
+
export function matchPagePattern(pattern, pathname) {
|
|
3
|
+
let segments;
|
|
4
|
+
try { segments = pathname.replace(/\/+$/, '').split('/').slice(1).map(decodeURIComponent); }
|
|
5
|
+
catch { return null; }
|
|
6
|
+
if (segments.length === 1 && !segments[0]) segments = [];
|
|
7
|
+
if (segments.some(segment => !segment || /[/\\\0]/.test(segment) || segment === '.' || segment === '..')) return null;
|
|
8
|
+
const parts = pattern === '/' ? [] : pattern.split('/').slice(1), params = Object.create(null), rank = [];
|
|
9
|
+
let index = 0;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(part);
|
|
12
|
+
const catchall = /^\[\.\.\.([^\]]+)\]$/.exec(part);
|
|
13
|
+
const parameter = /^\[([^\]]+)\]$/.exec(part);
|
|
14
|
+
if (optional || catchall) {
|
|
15
|
+
if (catchall && index === segments.length) return null;
|
|
16
|
+
if (index !== segments.length) params[(optional || catchall)[1]] = segments.slice(index);
|
|
17
|
+
index = segments.length; rank.push(optional ? 0 : 1);
|
|
18
|
+
} else if (parameter) {
|
|
19
|
+
if (index === segments.length) return null;
|
|
20
|
+
params[parameter[1]] = segments[index++]; rank.push(2);
|
|
21
|
+
} else {
|
|
22
|
+
if (segments[index++] !== part) return null;
|
|
23
|
+
rank.push(3);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return index === segments.length ? { params, rank } : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isHtmlDocumentRoute(routes, pathname) {
|
|
30
|
+
let winner;
|
|
31
|
+
for (const route of routes || []) {
|
|
32
|
+
const match = matchPagePattern(route.pattern, pathname);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
const rank = winner && (() => {
|
|
35
|
+
for (let index = 0; index < Math.max(match.rank.length, winner.rank.length); index++) {
|
|
36
|
+
const difference = (match.rank[index] ?? 4) - (winner.rank[index] ?? 4);
|
|
37
|
+
if (difference) return difference;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
})();
|
|
41
|
+
if (!winner || rank > 0) winner = { ...match, route };
|
|
42
|
+
}
|
|
43
|
+
return winner?.route.htmlOnly === true;
|
|
44
|
+
}
|
package/runtime/rsc-worker.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {drainTraces} from './trace.mjs';
|
|
2
|
+
import {withInstrumentation,reportRequestError,wasRequestErrorReported} from './instrumentation.mjs';
|
|
1
3
|
import React from 'react';
|
|
2
4
|
import {scheduleCompileCacheFlush} from './compile-cache.mjs';
|
|
3
5
|
import {preloadModules} from './preload.mjs';
|
|
@@ -8,6 +10,7 @@ import { createFromReadableStream } from 'react-server-dom-webpack/client.edge';
|
|
|
8
10
|
import { parentPort } from 'node:worker_threads';
|
|
9
11
|
import { pathToFileURL } from 'node:url';
|
|
10
12
|
import { createHash } from 'node:crypto';
|
|
13
|
+
import { navigationAuthorization } from '../compat/navigation-authorization.cjs';
|
|
11
14
|
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
12
15
|
import { Metadata } from './app-metadata.mjs';
|
|
13
16
|
import { appTimeoutError } from './app-errors.mjs';
|
|
@@ -27,6 +30,8 @@ import { flightConsumer } from './cache-components-rsc.mjs';
|
|
|
27
30
|
import { partialModel, isPartialHole, boundedPartialFlight } from './app-partial-model.mjs';
|
|
28
31
|
import { PartialTemplates, canResumePartialDirectly, partialTemplateReuse } from './partial-template.mjs';
|
|
29
32
|
import { createAfterScope } from '../compat/after.cjs';
|
|
33
|
+
import {renderKeyFor} from './app-render-key.mjs';
|
|
34
|
+
import { selectAppStyles } from './app-style-selection.mjs';
|
|
30
35
|
|
|
31
36
|
installFetchCache();
|
|
32
37
|
if (process.env.PRNEXT_INSPECT_DIR) parentPort.postMessage({type:'rsc-ready'});
|
|
@@ -71,6 +76,9 @@ function queryObject(url) {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
function createTree(entry, request, fallback = null) {
|
|
79
|
+
function RetainedBranch(props) {
|
|
80
|
+
return React.createElement(entry.ActivitySlot, { ...props, authorizationKey: navigationAuthorization(currentRequest(), request.authorizationSecret || request.actionKey, request.navigationCache?.authCookies) });
|
|
81
|
+
}
|
|
74
82
|
const params = staticParams(request.params || {});
|
|
75
83
|
const searchParams = staticSearchParams(queryObject(new URL(request.url)), currentRequest(), entry.page?.default?.$$typeof === Symbol.for('react.client.reference'));
|
|
76
84
|
if (entry.routing && !fallback) return createAdvancedTree(entry, request, searchParams);
|
|
@@ -100,7 +108,7 @@ function createTree(entry, request, fallback = null) {
|
|
|
100
108
|
const scopedParams = paramsBySegment(segments, request.params || {});
|
|
101
109
|
for (const segment of segments) {
|
|
102
110
|
scopes.set(segment, `${segment.path || ''}:${JSON.stringify(scopedParams.get(segment))}`);
|
|
103
|
-
if (request.partialParams?.length) for (const prefix of ['', 'error:', 'navigation:', 'layout:']) keyScope(`${prefix}${segment.path || ''}:`, scopedParams.get(segment), prefix === 'layout:' ? ['segments'] : undefined);
|
|
111
|
+
if (request.partialParams?.length) for (const prefix of ['', 'error:', 'navigation:', 'layout:', 'activity:']) keyScope(`${prefix}${segment.path || ''}:`, scopedParams.get(segment), prefix === 'layout:' ? ['segments'] : prefix === 'activity:' ? ['identity', 'authorizationKey'] : ['error:', 'navigation:'].includes(prefix) ? ['resetKey'] : undefined);
|
|
104
112
|
}
|
|
105
113
|
let instantConfig = request.instantValidation ? entry.pageConfig?.instant : undefined;
|
|
106
114
|
for (let index = fallback ? Math.min(fallback.index, segments.length - 1) : segments.length - 1; index >= 0; index--) {
|
|
@@ -128,6 +136,10 @@ function createTree(entry, request, fallback = null) {
|
|
|
128
136
|
tree = React.createElement(component(segment.template, 'template'), { key: child ? scopes.get(child) : pageKey }, tree);
|
|
129
137
|
}
|
|
130
138
|
if (segment.layout) {
|
|
139
|
+
if (entry.ActivitySlot) tree = React.createElement(RetainedBranch, {
|
|
140
|
+
identity: segments.slice(index + 1).some(child => child.layout) ? scopes.get(segments[index + 1]) : pageKey + new URL(request.originalUrl || request.url).search,
|
|
141
|
+
key: `activity:${scopes.get(segment)}`,
|
|
142
|
+
}, tree);
|
|
131
143
|
if (request.cacheComponents && segment === rootLayout) tree = React.createElement(React.Fragment, null, head, tree);
|
|
132
144
|
tree = React.createElement(component(segment.layout, 'layout'), { params: staticParams(scopedParams.get(segment)), key: scopes.get(segment) }, tree);
|
|
133
145
|
if (entry.LayoutProvider) tree = React.createElement(entry.LayoutProvider, { key: `layout:${scopes.get(segment)}`,
|
|
@@ -196,8 +208,9 @@ async function renderFlight(request, fallback = null, channel = null) {
|
|
|
196
208
|
try {
|
|
197
209
|
if (request.routingContexts?.size) request.renderRoutingBranch = routingBranchRenderer(cacheContext, bundlerConfig,
|
|
198
210
|
renderSignal, error => publicError(error, request.production).digest);
|
|
199
|
-
let model = request.controlModel || { ...(allowedRouting ? createTree(entry, request, fallback) : { tree: null, routingInvalid: true }), router, css: request.css || [], fonts: request.fonts || [], ...(request.instantDiagnostics?.length ? { instantDiagnostics: request.instantDiagnostics } : {}), ...request.actionModel };
|
|
211
|
+
let model = request.controlModel || { ...(allowedRouting ? createTree(entry, request, fallback) : { tree: null, routingInvalid: true }), router, styleRoute: request.routePattern, css: request.css || [], ...(request.cssScopes?.length ? { cssScopes: request.cssScopes } : {}), fonts: request.fonts || [], ...(request.instantDiagnostics?.length ? { instantDiagnostics: request.instantDiagnostics } : {}), ...request.actionModel };
|
|
200
212
|
if (model.routing?.params) model.router.params = model.routing.params;
|
|
213
|
+
selectAppStyles(model);
|
|
201
214
|
if (request.partialFlight) {
|
|
202
215
|
const consumer = { serverConsumerManifest: flightConsumer(request), replayConsoleLogs: false };
|
|
203
216
|
const decode = () => {
|
|
@@ -214,14 +227,16 @@ async function renderFlight(request, fallback = null, channel = null) {
|
|
|
214
227
|
const keyMap = request.partialKeys ? new Map(request.partialKeys) : undefined;
|
|
215
228
|
model = partialModel(stored, { live, keyMap, reuse: partialTemplateReuse(stored, keyMap) });
|
|
216
229
|
}
|
|
230
|
+
model.authorizationKey = navigationAuthorization(cacheContext, request.authorizationSecret || request.actionKey, request.navigationCache?.authCookies);
|
|
217
231
|
const stream = renderToReadableStream(model, bundlerConfig, {
|
|
218
232
|
signal: renderSignal,
|
|
219
233
|
temporaryReferences: request.temporaryReferences,
|
|
220
234
|
onError(error) {
|
|
221
235
|
const failure = error && typeof error === 'object' ? error : new Error(String(error));
|
|
222
|
-
if (cacheContext.staticState?.partial && isPartialHole(failure)) return 'PRNEXT_PPR_DYNAMIC';
|
|
236
|
+
if ((cacheContext.staticState?.partial || request.navigationStage) && isPartialHole(failure)) return 'PRNEXT_PPR_DYNAMIC';
|
|
223
237
|
renderError ??= failure;
|
|
224
238
|
const digest = publicError(failure, request.production).digest;
|
|
239
|
+
if(ordinaryRenderError(failure)&&!abort.signal.aborted&&!channel?.signal.aborted){if(!failure.digest&&Object.isExtensible(failure))failure.digest=digest;const reporting=reportRequestError(failure);if(reporting&&digest.length<=1024&&request.instrumentationId!==undefined)parentPort.postMessage({id:request.instrumentationId,type:'instrumentation-error',value:digest});}
|
|
225
240
|
if (!abort.signal.aborted && !channel?.signal.aborted && ordinaryRenderError(failure)) console.error(`[prnext] ${digest}`, failure?.stack || failure);
|
|
226
241
|
return digest;
|
|
227
242
|
},
|
|
@@ -342,7 +357,12 @@ async function renderFlight(request, fallback = null, channel = null) {
|
|
|
342
357
|
});
|
|
343
358
|
}
|
|
344
359
|
|
|
345
|
-
async function handleRequest(request,
|
|
360
|
+
async function handleRequest(request,channel=null){
|
|
361
|
+
process.env.NEXT_RUNTIME='nodejs';
|
|
362
|
+
if(!request.runtimeInstrumentation&&(request.operation==='static-params'||request.staticGeneration||request.staticState))return handleRequestInner(request,channel);
|
|
363
|
+
return withInstrumentation({...request,signal:channel?.signal,manifest:{instrumentation:request.instrumentation},route:{router:'app',kind:'page',pattern:request.routePattern,cacheConfig:request.cacheConfig}},()=>handleRequestInner(request,channel),{realm:'rsc'});
|
|
364
|
+
}
|
|
365
|
+
async function handleRequestInner(request, channel = null) {
|
|
346
366
|
if (request.operation === 'static-params') {
|
|
347
367
|
return runRequestContext({ ...request, phase: 'render' }, async () => {
|
|
348
368
|
const context = currentRequest();
|
|
@@ -390,6 +410,7 @@ const responseCompletions = new Map();
|
|
|
390
410
|
const manifests = new Map();
|
|
391
411
|
const artifacts = new Map();
|
|
392
412
|
parentPort.on('message', async ({ id, request, type, value, token }) => {
|
|
413
|
+
if(type==='telemetry-flush'){await drainTraces();parentPort.postMessage({type:'telemetry-flushed',token});return;}
|
|
393
414
|
if(type==='preload'){
|
|
394
415
|
try{await preloadModules(value.files,value.options,file=>routeEntry({modulePath:file,production:true}));parentPort.postMessage({type:'preloaded',id});}
|
|
395
416
|
catch(error){parentPort.postMessage({type:'preloaded',id,error:error.message});}return;
|
|
@@ -410,17 +431,18 @@ parentPort.on('message', async ({ id, request, type, value, token }) => {
|
|
|
410
431
|
for (const field of ['clientModules', 'actions']) {
|
|
411
432
|
if (request[field + 'Id'] !== undefined) request[field] = manifests.get(request[field + 'Id']);
|
|
412
433
|
}
|
|
434
|
+
request.instrumentationId=id;
|
|
413
435
|
const channel = request.stream && (!request.action || request.action.id) ? workerStreamChannel(parentPort, id) : null;
|
|
414
436
|
let responseDone, responseTimer;
|
|
415
|
-
if (request.
|
|
437
|
+
if (request.cacheConfig?.runtime !== 'edge') {
|
|
416
438
|
const timeoutMs = request.renderTimeoutMs ?? 25000;
|
|
417
|
-
request.afterScope = createAfterScope({ timeoutMs, onComplete: flushCacheWork,
|
|
418
|
-
onTimeout: error => parentPort.postMessage({ id, error: { message: error.message, retireWorker: true } })
|
|
419
|
-
|
|
439
|
+
request.afterScope = createAfterScope({report:error=>{console.error('[prnext] after() failed:',error?.stack||error);return reportRequestError(error);}, timeoutMs, onComplete: flushCacheWork,
|
|
440
|
+
onTimeout: error => parentPort.postMessage({ id, error: { message: error.message, retireWorker: true } }),
|
|
441
|
+
onRegistered() { if (request.waitForResponse) responseDone = new Promise(resolve => {
|
|
420
442
|
const finish = () => { clearTimeout(responseTimer); resolve(); };
|
|
421
443
|
responseCompletions.set(id, finish);
|
|
422
444
|
responseTimer = setTimeout(finish, timeoutMs);
|
|
423
|
-
});
|
|
445
|
+
}); } });
|
|
424
446
|
}
|
|
425
447
|
activeRequests.add(id);
|
|
426
448
|
if (channel) streamChannels.set(id, channel);
|
|
@@ -432,14 +454,14 @@ parentPort.on('message', async ({ id, request, type, value, token }) => {
|
|
|
432
454
|
delete request.partialFlightId;
|
|
433
455
|
}
|
|
434
456
|
if (channel) {
|
|
435
|
-
await handleRequest({ ...request, renderKey: id }, channel);
|
|
457
|
+
await handleRequest({ ...request, renderKey: renderKeyFor(request, id) }, channel);
|
|
436
458
|
return;
|
|
437
459
|
}
|
|
438
|
-
const result = await handleRequest({ ...request, renderKey: id });
|
|
460
|
+
const result = await handleRequest({ ...request, renderKey: renderKeyFor(request, id) });
|
|
439
461
|
parentPort.postMessage({ id, ...result }, result.body instanceof ArrayBuffer ? [result.body] : []);
|
|
440
462
|
} catch (error) {
|
|
441
463
|
parentPort.postMessage({ id, error: {
|
|
442
|
-
message: error?.message || String(error), stack: error?.stack, digest: error?.digest, statusCode: error?.statusCode,
|
|
464
|
+
instrumentationReported:wasRequestErrorReported(error), message: error?.message || String(error), stack: error?.stack, digest: error?.digest, statusCode: error?.statusCode,
|
|
443
465
|
retireWorker: error?.retireWorker, responseHeaders: error?.responseHeaders,
|
|
444
466
|
code: error?.code, staticMode: error?.staticMode, dynamicReason: error?.dynamicReason,
|
|
445
467
|
} });
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {isControlError} from './navigation.mjs';
|
|
2
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import {createRequire} from 'node:module';
|
|
5
|
+
const scope=new AsyncLocalStorage(),rawFetch=globalThis.fetch;
|
|
6
|
+
let api;
|
|
7
|
+
try{api=createRequire(import.meta.url)('@opentelemetry/api');}catch{}
|
|
8
|
+
const endpoint=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/,'')+'/v1/traces':undefined);
|
|
9
|
+
const protocol=process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL||process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
|
|
10
|
+
const enabled=process.env.OTEL_SDK_DISABLED!=='true'&&endpoint&&(!protocol||protocol==='http/json');
|
|
11
|
+
const queue=[];let timer,flushing;
|
|
12
|
+
const epoch=BigInt(Date.now())*1000000n-process.hrtime.bigint();
|
|
13
|
+
const nanos=()=>String(epoch+process.hrtime.bigint());
|
|
14
|
+
export function parseTraceparent(value){
|
|
15
|
+
const match=/^00-([a-f0-9]{32})-([a-f0-9]{16})-([a-f0-9]{2})$/.exec(value||'');
|
|
16
|
+
return match&&!/^0+$/.test(match[1])&&!/^0+$/.test(match[2])?{traceId:match[1],spanId:match[2],traceFlags:parseInt(match[3],16)&1}:undefined;
|
|
17
|
+
}
|
|
18
|
+
export function currentTraceparent(){const value=scope.getStore();return value?`00-${value.traceId}-${value.spanId}-${value.traceFlags?'01':'00'}`:undefined;}
|
|
19
|
+
export function propagatedFetch(input,init){
|
|
20
|
+
const parent=currentTraceparent();if(!parent)return [input,init];
|
|
21
|
+
const headers=new Headers(init?.headers??(input instanceof Request?input.headers:undefined));
|
|
22
|
+
if(!headers.has('traceparent'))headers.set('traceparent',parent);
|
|
23
|
+
return [input,{...init,headers}];
|
|
24
|
+
}
|
|
25
|
+
function enqueue(span){
|
|
26
|
+
if(!enabled||!span.flags||queue.length>=1024)return;
|
|
27
|
+
queue.push(span);if(!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();
|
|
28
|
+
}
|
|
29
|
+
export async function flushTraces(){
|
|
30
|
+
if(flushing)return flushing;
|
|
31
|
+
if(!queue.length)return;
|
|
32
|
+
const headers={'content-type':'application/json'};
|
|
33
|
+
for(const part of (process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS||process.env.OTEL_EXPORTER_OTLP_HEADERS||'').split(',')){
|
|
34
|
+
const split=part.indexOf('=');if(split>0)try{headers[part.slice(0,split).trim()]=decodeURIComponent(part.slice(split+1).trim());}catch{}
|
|
35
|
+
}
|
|
36
|
+
const spans=queue.splice(0,128);
|
|
37
|
+
flushing=(async()=>{
|
|
38
|
+
const body=JSON.stringify({resourceSpans:[{resource:{attributes:[{key:'service.name',value:{stringValue:process.env.OTEL_SERVICE_NAME||'prnext'}}]},scopeSpans:[{scope:{name:'prnext.runtime'},spans}]}]});
|
|
39
|
+
for(let attempt=0;attempt<3;attempt++){
|
|
40
|
+
let retry=false;
|
|
41
|
+
try{
|
|
42
|
+
const response=await rawFetch(endpoint,{method:'POST',redirect:'error',headers,body,signal:AbortSignal.timeout(1000)});
|
|
43
|
+
retry=[429,502,503,504].includes(response.status);await response.body?.cancel();
|
|
44
|
+
}catch{retry=true;} // Telemetry failure never changes an application response.
|
|
45
|
+
if(!retry)break;
|
|
46
|
+
if(attempt<2)await new Promise(resolve=>setTimeout(resolve,100*2**attempt).unref());
|
|
47
|
+
}
|
|
48
|
+
})().finally(()=>{flushing=undefined;if(queue.length&&!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();});
|
|
49
|
+
return flushing;
|
|
50
|
+
}
|
|
51
|
+
export async function drainTraces(){
|
|
52
|
+
const deadline=Date.now()+2500;let timeout;
|
|
53
|
+
try{
|
|
54
|
+
await Promise.race([(async()=>{while((flushing||queue.length)&&Date.now()<deadline)await(flushing||flushTraces());})(),new Promise(resolve=>{timeout=setTimeout(resolve,2500);})]);
|
|
55
|
+
}finally{clearTimeout(timeout);clearTimeout(timer);timer=undefined;}
|
|
56
|
+
}
|
|
57
|
+
export function markTraceError(){const state=scope.getStore();if(state?.span)state.span.status={code:2};state?.sdkSpan?.setStatus({code:2});}
|
|
58
|
+
export async function withTrace(options,name,callback){
|
|
59
|
+
const parent=scope.getStore()||parseTraceparent(options.traceparent||new Headers(options.headers).get('traceparent'));
|
|
60
|
+
if(!enabled&&!api&&!parent)return callback();
|
|
61
|
+
let sdkSpan;
|
|
62
|
+
if(!enabled&&api){
|
|
63
|
+
const remote=parent?api.trace.setSpanContext(api.context.active(),{traceId:parent.traceId,spanId:parent.spanId,traceFlags:parent.traceFlags,isRemote:true}):api.context.active();
|
|
64
|
+
sdkSpan=api.trace.getTracer('prnext.runtime').startSpan(name,{kind:options.kind||1},remote);
|
|
65
|
+
const context=sdkSpan.spanContext();
|
|
66
|
+
if(!api.trace.isSpanContextValid(context)){sdkSpan.end();return callback();}
|
|
67
|
+
const run=()=>scope.run({...context,sdkSpan},callback);
|
|
68
|
+
try{return await api.context.with(api.trace.setSpan(remote,sdkSpan),run);}catch(error){if(!isControlError(error))sdkSpan.setStatus({code:2});throw error;}finally{sdkSpan.end();}
|
|
69
|
+
}
|
|
70
|
+
const span={traceId:parent?.traceId||randomBytes(16).toString('hex'),spanId:randomBytes(8).toString('hex'),parentSpanId:parent?.spanId,
|
|
71
|
+
flags:parent?.traceFlags??1,name,kind:options.kind||1,startTimeUnixNano:nanos(),attributes:[]};
|
|
72
|
+
if(options.route?.pattern)span.attributes.push({key:'http.route',value:{stringValue:options.route.pattern}});
|
|
73
|
+
const state={traceId:span.traceId,spanId:span.spanId,traceFlags:span.flags,span};
|
|
74
|
+
const run=()=>scope.run(state,async()=>{
|
|
75
|
+
try{const result=await callback();if(result?.status>=500)span.status={code:2};return result;}catch(error){if(!isControlError(error))span.status={code:2};throw error;}finally{span.endTimeUnixNano=nanos();enqueue(span);}
|
|
76
|
+
});
|
|
77
|
+
return api?api.context.with(api.trace.setSpanContext(api.context.active(),{traceId:state.traceId,spanId:state.spanId,traceFlags:state.traceFlags}),run):run();
|
|
78
|
+
}
|
|
@@ -23,14 +23,14 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
|
|
|
23
23
|
// Rebuilding on those reads restarts the server (and its SSE stream).
|
|
24
24
|
try { if (statSync(path.join(root, relative)).isDirectory()) return; } catch {}
|
|
25
25
|
}
|
|
26
|
-
onChange(relative);
|
|
26
|
+
onChange(relative,event);
|
|
27
27
|
});
|
|
28
28
|
watcher.on('error', error => { watcher.close(); onError(error); });
|
|
29
29
|
return { close: () => watcher.close(), async refresh() {} };
|
|
30
30
|
}
|
|
31
31
|
const directories = new Map();
|
|
32
|
-
const pending = new
|
|
33
|
-
let closed = false, ready = false, timer, running
|
|
32
|
+
const pending = new Map();
|
|
33
|
+
let closed = false, ready = false, timer, running;
|
|
34
34
|
const allowedDirectory = relative => !relative || (!ignore(relative) && (relative === '.contentlayer' || shouldWatchProjectFile(relative)));
|
|
35
35
|
const allowedFile = relative => relative && !ignore(relative) && shouldWatchProjectFile(relative);
|
|
36
36
|
function remove(relative) {
|
|
@@ -51,9 +51,9 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
|
|
|
51
51
|
close();
|
|
52
52
|
onError(error);
|
|
53
53
|
}
|
|
54
|
-
function queue(relative) {
|
|
54
|
+
function queue(relative, recursive = false) {
|
|
55
55
|
if (closed) return;
|
|
56
|
-
pending.
|
|
56
|
+
pending.set(relative, recursive || pending.get(relative) === true);
|
|
57
57
|
clearTimeout(timer);
|
|
58
58
|
timer = setTimeout(() => { void drain().catch(fail); }, 20);
|
|
59
59
|
}
|
|
@@ -84,18 +84,24 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
|
|
|
84
84
|
if (process.platform === 'win32' && event === 'change') {
|
|
85
85
|
try { metadataOnly = statSync(path.join(root, changed)).isDirectory(); } catch {}
|
|
86
86
|
}
|
|
87
|
-
if (allowedFile(changed) && !metadataOnly) onChange(changed);
|
|
87
|
+
if (allowedFile(changed) && !metadataOnly) onChange(changed,event);
|
|
88
88
|
// The .contentlayer container itself is ignored as a source, but
|
|
89
89
|
// its generated directory may be created after the watcher starts.
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
90
|
+
// Coalesced notifications can name an already subscribed ancestor,
|
|
91
|
+
// without a separate event from the changed descendant. Reconcile
|
|
92
|
+
// that subtree even when its existing directory watches are intact.
|
|
93
|
+
// Ordinary file edits still scan only their immediate parent.
|
|
94
|
+
if (!filename) queue(relative, true);
|
|
95
|
+
else if (allowedDirectory(changed) || allowedFile(changed)) {
|
|
96
|
+
queue(relative);
|
|
97
|
+
if (!metadataOnly && directories.has(changed)) queue(changed, true);
|
|
98
|
+
}
|
|
93
99
|
});
|
|
94
100
|
} catch (error) { if (relative && missing(error)) { disappeared(relative); return; } throw error; }
|
|
95
101
|
directories.set(relative, watcher);
|
|
96
102
|
// Files may already exist by the time a newly created directory is
|
|
97
103
|
// subscribed. Its discovery must also invalidate the source snapshot.
|
|
98
|
-
if (ready && allowedFile(relative)) onChange(relative);
|
|
104
|
+
if (ready && allowedFile(relative)) onChange(relative,'rename');
|
|
99
105
|
watcher.on('error', error => {
|
|
100
106
|
if (closed) return;
|
|
101
107
|
if (relative && missing(error)) disappeared(relative);
|
|
@@ -109,7 +115,7 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
|
|
|
109
115
|
const children = new Set();
|
|
110
116
|
for (const entry of entries) {
|
|
111
117
|
const child = path.posix.join(relative, entry.name);
|
|
112
|
-
if (fresh && ready && entry.isFile() && allowedFile(child)) onChange(child);
|
|
118
|
+
if (fresh && ready && entry.isFile() && allowedFile(child)) onChange(child,'rename');
|
|
113
119
|
if (!entry.isDirectory() || !allowedDirectory(child)) continue;
|
|
114
120
|
children.add(child);
|
|
115
121
|
if (recursive || !directories.has(child)) await scan(child, recursive);
|
|
@@ -123,8 +129,7 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
|
|
|
123
129
|
running = (async () => {
|
|
124
130
|
while (!closed && pending.size) {
|
|
125
131
|
const batch = [...pending]; pending.clear();
|
|
126
|
-
const recursive
|
|
127
|
-
for (const relative of batch) await scan(relative, recursive);
|
|
132
|
+
for (const [relative, recursive] of batch) await scan(relative, recursive);
|
|
128
133
|
}
|
|
129
134
|
})().finally(() => { running = undefined; });
|
|
130
135
|
return running;
|
|
@@ -136,8 +141,7 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
|
|
|
136
141
|
close,
|
|
137
142
|
async refresh() {
|
|
138
143
|
if (closed) return;
|
|
139
|
-
|
|
140
|
-
pending.add('');
|
|
144
|
+
pending.set('', true);
|
|
141
145
|
await drain();
|
|
142
146
|
},
|
|
143
147
|
};
|