@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,34 @@
|
|
|
1
|
+
function ownsBranch(scope, branch) {
|
|
2
|
+
const path = branch.replace(/#default$/, '');
|
|
3
|
+
if (scope.kind === 'layout') return !scope.path || path === scope.path || path.startsWith(scope.path + '/');
|
|
4
|
+
return path === scope.path && (scope.kind === 'default') === branch.endsWith('#default');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Parallel route entries include both canonical and intercepted alternatives.
|
|
8
|
+
* Their actual router selection determines which sheets may become active. */
|
|
9
|
+
export function selectAppStyles(model) {
|
|
10
|
+
if (!model.routing || !Array.isArray(model.cssScopes)) return;
|
|
11
|
+
const branches = Object.values(model.routing.slots || {}).map(slot => slot.branch);
|
|
12
|
+
model.cssScopes = model.cssScopes.filter(scope => (scope.kind === 'layout' && !scope.path) || branches.some(branch => ownsBranch(scope, branch)));
|
|
13
|
+
model.css = [...new Set(model.cssScopes.flatMap(scope => scope.css || []))];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A retained parallel/intercepted slot keeps only the styles belonging to
|
|
17
|
+
* that branch. Carrying the preceding route's complete list would leak its
|
|
18
|
+
* inactive page globals into the new main page. */
|
|
19
|
+
export function retainAppStyles(model, previous) {
|
|
20
|
+
selectAppStyles(model);
|
|
21
|
+
if (!model.routing || !previous?.routing || model.globalError) return;
|
|
22
|
+
const retained = Object.entries(model.routing.slots || {}).flatMap(([key, slot]) => {
|
|
23
|
+
const old = previous.routing.slots?.[key];
|
|
24
|
+
return old && old.branch === slot.branch && old.url === slot.url ? [slot.branch] : [];
|
|
25
|
+
});
|
|
26
|
+
if (!retained.length) return;
|
|
27
|
+
const scopes = [...model.cssScopes || []];
|
|
28
|
+
for (const scope of previous.cssScopes || []) {
|
|
29
|
+
if (scopes.some(current => current.path === scope.path && current.kind === scope.kind)) continue;
|
|
30
|
+
if (retained.some(branch => ownsBranch(scope, branch))) scopes.push(scope);
|
|
31
|
+
}
|
|
32
|
+
model.cssScopes = scopes;
|
|
33
|
+
model.css = [...new Set([...(model.css || []), ...scopes.flatMap(scope => scope.css || [])])];
|
|
34
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const managers = new WeakMap();
|
|
2
|
+
|
|
3
|
+
function aborted() { return new DOMException('Navigation cancelled', 'AbortError'); }
|
|
4
|
+
|
|
5
|
+
/** Styles are downloaded while the old page remains visible, but become
|
|
6
|
+
* active together in the new tree's layout effect, before the next paint.
|
|
7
|
+
* Keeping one manager per document also covers development runtime updates. */
|
|
8
|
+
export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
9
|
+
if (managers.has(document)) return managers.get(document);
|
|
10
|
+
const sheets = new Map();
|
|
11
|
+
let handed = new Map();
|
|
12
|
+
let cleanup = 0;
|
|
13
|
+
const reserved = sheet => handed.get(sheet.link.href) === sheet;
|
|
14
|
+
function urls(values) {
|
|
15
|
+
if (!Array.isArray(values) || values.length > 1024) throw new Error('Invalid App Router stylesheet list');
|
|
16
|
+
return [...new Set(values.map(value => {
|
|
17
|
+
if (typeof value !== 'string' || value.length > 4096) throw new Error('Invalid App Router stylesheet URL');
|
|
18
|
+
const url = new URL(value, document.baseURI);
|
|
19
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Invalid App Router stylesheet protocol');
|
|
20
|
+
return url.href;
|
|
21
|
+
}))];
|
|
22
|
+
}
|
|
23
|
+
function remove(href, sheet) {
|
|
24
|
+
if (sheet.removed) return;
|
|
25
|
+
sheet.removed = true;
|
|
26
|
+
sheet.detach?.();
|
|
27
|
+
sheet.link.remove();
|
|
28
|
+
const records = sheets.get(href);
|
|
29
|
+
if (records) {
|
|
30
|
+
const index = records.indexOf(sheet);
|
|
31
|
+
if (index >= 0) records.splice(index, 1);
|
|
32
|
+
if (!records.length) sheets.delete(href);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function acquire(href, before, replace = false) {
|
|
36
|
+
const records = sheets.get(href) || [];
|
|
37
|
+
const current = records.at(-1);
|
|
38
|
+
if (current && !replace) return current;
|
|
39
|
+
// A replacement keeps the old active (or local-reset recovery) occurrence
|
|
40
|
+
// in place until commit. Discard older abandoned candidates immediately.
|
|
41
|
+
if (replace) for (const record of [...records]) if (!record.active && !record.recovery && !reserved(record)) remove(href, record);
|
|
42
|
+
const tracked = new Set(records.map(record => record.link));
|
|
43
|
+
const existing = replace ? undefined : [...document.querySelectorAll('link[rel="stylesheet"]')].find(link => link.href === href && !tracked.has(link));
|
|
44
|
+
const link = existing || document.createElement('link');
|
|
45
|
+
const sheet = { link, active: !!existing, ready: !!existing?.sheet, recovery: false };
|
|
46
|
+
const retained = sheets.get(href) || [];
|
|
47
|
+
retained.push(sheet); sheets.set(href, retained);
|
|
48
|
+
if (sheet.ready) sheet.promise = Promise.resolve();
|
|
49
|
+
else sheet.promise = new Promise((resolve, reject) => {
|
|
50
|
+
let finished = false;
|
|
51
|
+
const detach = () => { clearTimeout(timer); link.removeEventListener('load', loaded); link.removeEventListener('error', failed); };
|
|
52
|
+
const finish = error => {
|
|
53
|
+
if (finished) return;
|
|
54
|
+
finished = true; detach(); sheet.detach = undefined;
|
|
55
|
+
if (error) { remove(href, sheet); reject(error); }
|
|
56
|
+
else { sheet.ready = true; resolve(); }
|
|
57
|
+
};
|
|
58
|
+
const loaded = () => finish();
|
|
59
|
+
const failed = () => finish(new Error(`Could not load stylesheet ${href}`));
|
|
60
|
+
const timer = setTimeout(() => finish(new Error('App Router stylesheet loading timed out')), timeout);
|
|
61
|
+
sheet.detach = () => finish(aborted());
|
|
62
|
+
link.addEventListener('load', loaded);
|
|
63
|
+
link.addEventListener('error', failed);
|
|
64
|
+
});
|
|
65
|
+
// Cancellation and a competing navigation can remove a pending sheet.
|
|
66
|
+
// Each caller still observes the original rejection through prepare().
|
|
67
|
+
void sheet.promise.catch(() => {});
|
|
68
|
+
if (!existing) {
|
|
69
|
+
link.rel = 'stylesheet'; link.href = href; link.media = 'not all';
|
|
70
|
+
link.dataset.prnextAppStyle = '';
|
|
71
|
+
if (before) document.head.insertBefore(link, before);
|
|
72
|
+
else document.head.append(link);
|
|
73
|
+
}
|
|
74
|
+
return sheet;
|
|
75
|
+
}
|
|
76
|
+
const manager = {
|
|
77
|
+
async prepare(values = [], signal) {
|
|
78
|
+
if (signal?.aborted) throw aborted();
|
|
79
|
+
const wanted = urls(values);
|
|
80
|
+
const prepared = [];
|
|
81
|
+
let before;
|
|
82
|
+
// Establish cascade order before downloading. Re-inserting an already
|
|
83
|
+
// loaded link can unload its CSSOM and refetch, even within the same head.
|
|
84
|
+
// Keep a correctly ordered suffix; prepare a separate inactive occurrence
|
|
85
|
+
// when an existing sheet would otherwise need to move across that suffix.
|
|
86
|
+
for (let index = wanted.length - 1; index >= 0; index--) {
|
|
87
|
+
const href = wanted[index];
|
|
88
|
+
let sheet = acquire(href, before);
|
|
89
|
+
if (before && !(sheet.link.compareDocumentPosition(before) & 4)) sheet = acquire(href, before, true);
|
|
90
|
+
prepared.push(sheet.promise);
|
|
91
|
+
before = sheet.link;
|
|
92
|
+
}
|
|
93
|
+
const pending = Promise.all(prepared);
|
|
94
|
+
if (!signal) return pending;
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
const cancel = () => reject(aborted());
|
|
97
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
98
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener('abort', cancel));
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
handoff(values = []) {
|
|
102
|
+
// A concurrent React render can suspend after its CSS has loaded. Pin
|
|
103
|
+
// these exact occurrences until that render commits or is superseded;
|
|
104
|
+
// pagehide/actions must cancel network work, not its already-ready CSS.
|
|
105
|
+
const next = new Map(urls(values).map(href => {
|
|
106
|
+
const sheet = sheets.get(href)?.at(-1);
|
|
107
|
+
if (!sheet?.ready) throw new Error('App Router styles were not prepared before rendering');
|
|
108
|
+
return [href, sheet];
|
|
109
|
+
}));
|
|
110
|
+
const previous = handed; handed = next;
|
|
111
|
+
for (const [href, sheet] of previous) if (!reserved(sheet) && !sheet.active && !sheet.recovery) remove(href, sheet);
|
|
112
|
+
},
|
|
113
|
+
commit(values = []) {
|
|
114
|
+
cleanup++;
|
|
115
|
+
const wanted = new Set(urls(values));
|
|
116
|
+
const handedUrls = [...handed.keys()];
|
|
117
|
+
const pinned = wanted.size === handed.size && [...wanted].every((href, index) => handedUrls[index] === href) ? handed : null;
|
|
118
|
+
for (const href of wanted) {
|
|
119
|
+
const records = sheets.get(href) || [];
|
|
120
|
+
const prior = !pinned && handed.size ? records.find(record => record.active) || records.find(record => record.recovery) : undefined;
|
|
121
|
+
const sheet = pinned?.get(href) || prior || records.findLast(record => record.ready) || acquire(href);
|
|
122
|
+
// Initial HTML links are render-blocking. A missing sheet is restored
|
|
123
|
+
// on explicit recovery; normal navigation always prepares first.
|
|
124
|
+
sheet.active = true; sheet.recovery = false; sheet.link.media = '';
|
|
125
|
+
for (const previous of [...sheets.get(href)]) if (previous !== sheet && !reserved(previous)) remove(href, previous);
|
|
126
|
+
}
|
|
127
|
+
if (pinned) handed = new Map();
|
|
128
|
+
for (const [href, records] of sheets) {
|
|
129
|
+
for (const sheet of [...records]) if ((!wanted.has(href) || !sheet.active) && !reserved(sheet)) remove(href, sheet);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
release() {
|
|
133
|
+
const revision = ++cleanup;
|
|
134
|
+
// A normal tree update or StrictMode's effect replay commits in the same
|
|
135
|
+
// turn. Only a genuine fatal/global unmount retires the normal styles.
|
|
136
|
+
queueMicrotask(() => {
|
|
137
|
+
if (cleanup !== revision) return;
|
|
138
|
+
const recovery = new Set([...sheets.values()].flat().filter(sheet => sheet.ready && (sheet.active || reserved(sheet))));
|
|
139
|
+
for (const [href, records] of sheets) for (const sheet of [...records]) {
|
|
140
|
+
// Local reset is allowed to remount the same server tree without a
|
|
141
|
+
// request. Keep already loaded sheets inert so that reset can restore
|
|
142
|
+
// them synchronously, rather than painting before a second load event.
|
|
143
|
+
if (recovery.has(sheet)) { sheet.active = false; sheet.recovery = true; sheet.link.media = 'not all'; }
|
|
144
|
+
else if (recovery.size || !sheet.recovery) remove(href, sheet);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
cancel() {
|
|
149
|
+
for (const [href, records] of sheets) for (const sheet of [...records]) if (!sheet.active && !sheet.recovery && !reserved(sheet)) remove(href, sheet);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
managers.set(document, manager);
|
|
153
|
+
return manager;
|
|
154
|
+
}
|
|
@@ -44,7 +44,10 @@ globalThis[codecSymbol] = {
|
|
|
44
44
|
const bundler = flightBundler(context.clientModules, false);
|
|
45
45
|
let failure;
|
|
46
46
|
// React calls then inside its rendering context, preserving React.cache isolation.
|
|
47
|
-
|
|
47
|
+
// React can subscribe more than once while unwrapping a thenable. Start
|
|
48
|
+
// lazily in its rendering context, but never execute the producer twice.
|
|
49
|
+
let execution;
|
|
50
|
+
const result = { then(resolve, reject) { (execution ||= Promise.resolve().then(callback)).then(resolve, reject); } };
|
|
48
51
|
const stream = renderToReadableStream(result, bundler, { signal: context.signal, temporaryReferences: encoded.serverTemporaryReferences, onError(error) { failure ||= error; return 'PRNextCacheRenderError'; } });
|
|
49
52
|
const chunks = [];
|
|
50
53
|
let size = 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
2
3
|
import { flushCacheInvalidations, flushCacheWork } from '../compat/data-cache.cjs';
|
|
3
4
|
import { createAfterScope } from '../compat/after.cjs';
|
|
@@ -9,7 +10,7 @@ export function withCacheRequest(options, phase, callback) {
|
|
|
9
10
|
// only needs metadata, and may survive the response for stale revalidation.
|
|
10
11
|
return runRequestContext({ ...options, body: undefined, routePattern: options.route?.pattern, cacheConfig: options.route?.cacheConfig, phase }, async () => {
|
|
11
12
|
const context = currentRequest();
|
|
12
|
-
if (phase === 'route' && options.route?.cacheConfig?.runtime !== 'edge') context.afterScope = createAfterScope({ signal: options.signal, timeoutMs: options.timeoutMs ?? (options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : 25000) });
|
|
13
|
+
if (phase === 'route' && options.route?.cacheConfig?.runtime !== 'edge') context.afterScope = createAfterScope({report:error=>{console.error('[prnext] after() failed:',error?.stack||error);return reportRequestError(error);}, signal: options.signal, timeoutMs: options.timeoutMs ?? (options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : 25000) });
|
|
13
14
|
let finalized;
|
|
14
15
|
const finalizeCache = () => finalized ||= (async () => { await context.afterScope?.finish(); await flushCacheWork(context); })();
|
|
15
16
|
// The worker owns response completion even when application code throws.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const HEADER = 'x-prnext-deployment-id';
|
|
2
|
+
|
|
3
|
+
/** A generation is an artifact identity, independent from a user-supplied buildId. */
|
|
4
|
+
export function deploymentHeaders(id) { return id ? { [HEADER]: id } : {}; }
|
|
5
|
+
|
|
6
|
+
export async function assertDeploymentResponse(response, expected) {
|
|
7
|
+
if (response.headers.get('x-prnext-document-navigation') === '1') {
|
|
8
|
+
await response.body?.cancel().catch(() => {});
|
|
9
|
+
const error = new Error('This route requires a full document navigation.');
|
|
10
|
+
error.code = 'PRNEXT_DOCUMENT_NAVIGATION';
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
if (!expected) return;
|
|
14
|
+
if (response.headers.get(HEADER) === expected && response.status !== 409) return;
|
|
15
|
+
await response.body?.cancel().catch(() => {});
|
|
16
|
+
const error = new Error('This page belongs to a previous deployment. Reload the page before continuing.');
|
|
17
|
+
error.code = 'PRNEXT_DEPLOYMENT_CHANGED';
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
package/runtime/dev-client.mjs
CHANGED
|
@@ -199,7 +199,9 @@ async function applyUpdate(message, {navigation=false}={}) {
|
|
|
199
199
|
const pending = state.pending;
|
|
200
200
|
if (pending) await application.devPrepare?.(pending.options);
|
|
201
201
|
if (state.unsafe) { reload('An edited module exports values outside React components. Page reloaded.'); return; }
|
|
202
|
-
|
|
202
|
+
if (kind === 'app' && application.devPrepareStyles) await application.devPrepareStyles(manifest.app.routes);
|
|
203
|
+
else await updateStyles(kind === 'pages' ? route.css : application.devStyles?.(manifest.app.routes) || manifest.app.css);
|
|
204
|
+
if (kind === 'app') application.devStylesApplied?.(manifest.app.routes, { commit: !!message.cssOnly || !pending });
|
|
203
205
|
// An unrelated type-only edit may publish a new generation without changing
|
|
204
206
|
// any browser module. Keep its existing runtime/hydration issues and current
|
|
205
207
|
// selection until application code changes, rather than racing diagnostics.
|
|
@@ -287,7 +289,10 @@ function connect(options) {
|
|
|
287
289
|
if (message.buildId === state.buildId) return;
|
|
288
290
|
// A cold navigation may receive SSE readiness before its ensure response.
|
|
289
291
|
// Updating modules is safe; refreshing the old route would cancel the click.
|
|
290
|
-
|
|
292
|
+
// A retained branch can become cold after ensure() has returned. Its
|
|
293
|
+
// worker-triggered compilation is still part of the pending navigation;
|
|
294
|
+
// committing a refresh here would abort it and restore the previous URL.
|
|
295
|
+
void applyUpdate(message,{navigation:state.ensuring>0||message.reason?.code==='route-opened'});
|
|
291
296
|
};
|
|
292
297
|
globalThis.addEventListener('error', event => { if (event.error) showError(event.error); });
|
|
293
298
|
globalThis.addEventListener('unhandledrejection', event => showError(event.reason));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import {randomBytes,timingSafeEqual} from 'node:crypto';
|
|
3
|
+
import {readFile,realpath} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Private control plane. Request headers and bodies never cross this endpoint.
|
|
7
|
+
export async function createDevCompilerBroker({compile,maxPending=64}) {
|
|
8
|
+
const token=randomBytes(32).toString('base64url');let pending=0;
|
|
9
|
+
const server=http.createServer(async(req,res)=>{
|
|
10
|
+
const supplied=Buffer.from(req.headers.authorization||''),expected=Buffer.from('Bearer '+token);
|
|
11
|
+
if(req.method!=='POST'||req.url!=='/compile'||supplied.length!==expected.length||!timingSafeEqual(supplied,expected)){res.writeHead(403).end();return;}
|
|
12
|
+
if(pending>=maxPending){res.writeHead(503).end();return;}
|
|
13
|
+
pending++;
|
|
14
|
+
try{
|
|
15
|
+
const chunks=[];let bytes=0;
|
|
16
|
+
for await(const chunk of req){bytes+=chunk.length;if(bytes>65536)throw Error('Compiler request too large');chunks.push(chunk);}
|
|
17
|
+
// Decode once: an HTTP chunk boundary may split a UTF-8 route name.
|
|
18
|
+
const input=JSON.parse(Buffer.concat(chunks,bytes).toString('utf8'));if(typeof input.pattern!=='string'||!input.pattern.startsWith('/')||input.pattern.length>4096)throw Error('Invalid route pattern');
|
|
19
|
+
if(input.sources!==undefined&&(!Array.isArray(input.sources)||input.sources.length>32||!input.sources.every(p=>typeof p==='string'&&p.startsWith('/')&&p.length<=4096)))throw Error('Invalid retained route patterns');
|
|
20
|
+
const directory=await compile(input.pattern,input.sources||[]);
|
|
21
|
+
res.writeHead(200,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({directory}));
|
|
22
|
+
}catch(error){res.writeHead(500,{'content-type':'application/json'}).end(JSON.stringify({error:String(error.message).slice(0,2048)}));}
|
|
23
|
+
finally{pending--;}
|
|
24
|
+
});
|
|
25
|
+
server.requestTimeout=125000;server.headersTimeout=5000;
|
|
26
|
+
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
|
|
27
|
+
return {descriptor:JSON.stringify({port:server.address().port,token}),close:()=>new Promise(resolve=>{server.close(resolve);server.closeIdleConnections();})};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deferredCompiler(descriptor,{root,home}) {
|
|
31
|
+
const value=JSON.parse(descriptor);
|
|
32
|
+
if(!Number.isInteger(value.port)||value.port<1||value.port>65535||!/^[\w-]{43}$/.test(value.token))throw Error('Invalid private compiler endpoint');
|
|
33
|
+
return async(pattern,signal,sources=[])=>{
|
|
34
|
+
const response=await fetch(`http://127.0.0.1:${value.port}/compile`,{method:'POST',headers:{authorization:'Bearer '+value.token,'content-type':'application/json'},body:JSON.stringify({pattern,sources}),signal:AbortSignal.any([...(signal?[signal]:[]),AbortSignal.timeout(120000)])});
|
|
35
|
+
const result=await response.json();if(!response.ok)throw Error(result.error||'Deferred route compilation failed');
|
|
36
|
+
const directory=await realpath(path.resolve(root,result.directory)),parent=await realpath(home),relative=path.relative(parent,directory);
|
|
37
|
+
if(!relative||relative.startsWith('..')||path.isAbsolute(relative))throw Error('Compiler generation escaped its development session');
|
|
38
|
+
const manifest=JSON.parse(await readFile(path.join(directory,'manifest.json'),'utf8'));
|
|
39
|
+
if(!manifest.dev||manifest.devSession!==path.basename(parent))throw Error('Invalid development generation');
|
|
40
|
+
return {distDir:directory,manifest};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import logo from './dev-logo.mjs';
|
|
2
|
-
import { createLazyDevTool } from './dev-lazy-tool.mjs';
|
|
2
|
+
import { createLazyDevTool, registerDevToolLoaders } from './dev-lazy-tool.mjs';
|
|
3
3
|
import { popoutIcon } from './dev-popout.mjs';
|
|
4
4
|
import { deviceIcons } from './dev-ui-icons.mjs';
|
|
5
5
|
import { createDevPosition } from './dev-position.mjs';
|
|
6
6
|
import { createDevIssueMotion } from './dev-issue-motion.mjs';
|
|
7
7
|
|
|
8
|
+
const loadDevTool = registerDevToolLoaders({
|
|
9
|
+
workbench: () => import('./dev-workbench.mjs'),
|
|
10
|
+
inspector: () => import('./dev-tools-panel.mjs'),
|
|
11
|
+
});
|
|
12
|
+
|
|
8
13
|
// This module is imported only by the development browser entry. No framework
|
|
9
14
|
// contexts, application CSS, network requests or additional SSE streams.
|
|
10
15
|
export function createDevIndicator({ position = 'bottom-left', snapshot, showIssues, capture, tools }) {
|
|
@@ -79,7 +84,7 @@ export function createDevIndicator({ position = 'bottom-left', snapshot, showIss
|
|
|
79
84
|
let workbench;
|
|
80
85
|
const toolError=error=>{feedback.textContent='Could not open development tool. Try again.';console.error(error);};
|
|
81
86
|
const getWorkbench = () => workbench ||= createLazyDevTool(async()=>{
|
|
82
|
-
const {createDevWorkbench}=await
|
|
87
|
+
const {createDevWorkbench}=await loadDevTool('workbench');
|
|
83
88
|
return createDevWorkbench({shadow,capture,tools,restoreFocus:()=>trigger.focus()});
|
|
84
89
|
},{onError:toolError,reserve(method,args){
|
|
85
90
|
if(!['popConsole','popPreview'].includes(method))return;
|
|
@@ -94,7 +99,7 @@ export function createDevIndicator({ position = 'bottom-left', snapshot, showIss
|
|
|
94
99
|
consoleRow.append(consoleAction,detachButton('Open console in new window',()=>{close(false);getWorkbench().popConsole();}));
|
|
95
100
|
let inspector;
|
|
96
101
|
const getInspector = () => inspector ||= createLazyDevTool(async()=>{
|
|
97
|
-
const {createDevToolsPanel}=await
|
|
102
|
+
const {createDevToolsPanel}=await loadDevTool('inspector');
|
|
98
103
|
return createDevToolsPanel({shadow,tools,restoreFocus:()=>trigger.focus()});
|
|
99
104
|
},{onError:toolError});
|
|
100
105
|
if (tools) {
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// Capture user activation synchronously for detached windows, even on a slow
|
|
2
2
|
// connection. Import failures/disposal close reservations instead of leaking tabs.
|
|
3
|
+
const loaderRegistry = Symbol.for('prnext.dev.tool.loaders');
|
|
4
|
+
|
|
5
|
+
// The indicator survives Fast Refresh, while hashed import URLs belong to a
|
|
6
|
+
// build generation. Publish the current factories on module evaluation so an
|
|
7
|
+
// older, unopened indicator never requests a retired generation's lazy chunk.
|
|
8
|
+
// Existing tool instances remain owned by createLazyDevTool and keep their DOM.
|
|
9
|
+
export function registerDevToolLoaders(loaders, target = globalThis) {
|
|
10
|
+
const registry = target[loaderRegistry] ||= Object.create(null);
|
|
11
|
+
Object.assign(registry, loaders);
|
|
12
|
+
return name => {
|
|
13
|
+
const load = registry[name];
|
|
14
|
+
if (typeof load !== 'function') throw Error('Development tool loader is unavailable.');
|
|
15
|
+
return load();
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export function createLazyDevTool(load,{onError=console.error,reserve}={}) {
|
|
4
20
|
let instance,pending,disposed=false,epoch=0;
|
|
5
21
|
const get=()=>pending||=(async()=>{const value=await load();if(disposed){value.dispose();throw Error('Development tool was closed');}return instance=value;})().catch(error=>{pending=undefined;throw error;});
|
package/runtime/development.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
|
|
1
2
|
import {spawn} from 'node:child_process';
|
|
2
3
|
import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
|
|
3
4
|
import {randomUUID} from 'node:crypto';
|
|
@@ -95,7 +96,17 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
95
96
|
let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
|
|
96
97
|
const failedPaths=new Set();
|
|
97
98
|
const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root);
|
|
98
|
-
const
|
|
99
|
+
const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
|
|
100
|
+
if(stopping)throw Error('Development is stopping');
|
|
101
|
+
if(!discovery)await discover();
|
|
102
|
+
const patterns=[...new Set([pattern,...sources])];
|
|
103
|
+
if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))await discover();
|
|
104
|
+
if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))throw Error('Unknown deferred route pattern');
|
|
105
|
+
for(const value of patterns)active.set(value,Date.now());
|
|
106
|
+
if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
|
|
107
|
+
return current.directory;
|
|
108
|
+
})});
|
|
109
|
+
const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
|
|
99
110
|
const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
|
|
100
111
|
routes:(discovery?.project.routes||[]).filter(route=>!route.internal).map(route=>({path:(discovery.config.basePath||'')+(route.pattern==='/'?'/':route.pattern),kind:route.kind,source:route.file?path.relative(root,route.file).replaceAll(path.sep,'/'):undefined,compiled:current?.patterns.has(route.pattern)||false,pinned:[...pins.values()].some(pin=>pin.pattern===route.pattern)})),
|
|
101
112
|
cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
|
|
@@ -140,14 +151,13 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
140
151
|
async function discover(){
|
|
141
152
|
discovery=(await compilerRequest('discover')).discovery;routeCache.clear();output=discovery.config.distDir;
|
|
142
153
|
if(options.safe)discovery={...discovery,eager:true};
|
|
143
|
-
if(discovery.eager&&!options.safe)console.log('Development uses the complete route graph for middleware, rewrites or parallel routing.');
|
|
144
154
|
const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
|
|
145
155
|
await writeTypedRoutes(root,discovery.project,discovery.config);
|
|
146
156
|
if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
|
|
147
157
|
}
|
|
148
158
|
async function launch(manifest,directory){
|
|
149
159
|
const runtime=options.safe?undefined:await runtimePool.take(directory).catch(error=>{tools?.addServerLog('warn','Worker warmup was unavailable; starting a fresh worker.',{process:'runtime'});return undefined;});
|
|
150
|
-
const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory};
|
|
160
|
+
const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
|
|
151
161
|
delete env.PRNEXT_DEV_BROKER;
|
|
152
162
|
if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
|
|
153
163
|
const child=spawn(binary,['start',root,'--build-dir',directory,'--worker',path.join(root,directory,'runtime/worker.mjs'),'--node',process.execPath,'--hostname','127.0.0.1','--port','0','--shutdown-on-stdin-eof',...options.backend],{stdio:['pipe','pipe','pipe'],windowsHide:true,env});
|
|
@@ -203,30 +213,35 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
203
213
|
current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
|
|
204
214
|
const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
|
|
205
215
|
current.affectedRoutes=affected;
|
|
206
|
-
await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal).map(route=>route.pattern)});
|
|
216
|
+
await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal&&!route.devDeferred).map(route=>route.pattern)});
|
|
207
217
|
tools?.recordBuild({buildId:manifest.buildId,reason:reason.summary,reasonCode:reason.code,sequence:manifest.devSequence,time:Date.now(),startedAt,duration:performance.now()-started,phases,routes:patterns.length,state:'ready'});
|
|
208
218
|
console.log('Source compiled. '+reason.summary);
|
|
209
219
|
if(previous){
|
|
210
220
|
const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
|
|
211
221
|
}
|
|
212
|
-
while(generations.length>3){const old=generations.shift();
|
|
222
|
+
while(generations.length>3){const old=generations.shift();
|
|
223
|
+
// A deferred request may itself be awaiting this build. Never wait for
|
|
224
|
+
// its generation lease while holding the serialized compiler queue.
|
|
225
|
+
const cleanup=(async()=>{await gateway.request('retire',{generation:old.id});await old.runtime?.close();await stop(old.child);await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});})().catch(error=>{if(!stopping)console.warn('Development generation cleanup: '+error.message);}).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
|
|
226
|
+
}
|
|
213
227
|
await watcher?.refresh();
|
|
214
228
|
return manifest;
|
|
215
229
|
}catch(error){if(!stopping){tools?.recordBuild({sequence,reason:reason.summary,reasonCode:reason.code,time:Date.now(),startedAt,duration:performance.now()-started,phases,state:'error'});tools?.addServerLog('error',error.message,{process:'compiler'});console.error('Build failed: '+error.message);await writeState({state:'error',reason,error:error.message});}throw error;}
|
|
216
230
|
}
|
|
217
231
|
const updateInfo=()=>({buildId:current.manifest.buildId,clientManifest:current.manifest.devClient,pagesManifest:current.manifest.pagesManifest,sequence:current.manifest.devSequence,session,changed:current.changed,cssOnly:current.cssOnly,affectedRoutes:current.affectedRoutes});
|
|
218
|
-
async function ensure(raw,client){
|
|
219
|
-
const
|
|
232
|
+
async function ensure(raw,client,exact=false){
|
|
233
|
+
const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
|
|
234
|
+
const warm=discovery&&resolve();
|
|
220
235
|
if(!stopping&¤t&&warm&&active.has(warm.pattern)&¤t.patterns.has(warm.pattern)){
|
|
221
236
|
active.set(warm.pattern,Date.now());if(pins.has(client))pins.get(client).pattern=warm.pattern;
|
|
222
237
|
return updateInfo();
|
|
223
238
|
}
|
|
224
239
|
return serialized(async()=>{
|
|
225
240
|
if(!discovery)try{await discover();}catch(error){failedPaths.add(raw);await writeState({state:'error',error:error.message});throw error;}
|
|
226
|
-
let route=
|
|
241
|
+
let route=resolve();
|
|
227
242
|
// A newly written route can be requested before the watcher's debounce.
|
|
228
243
|
// Resolve that miss against the filesystem instead of returning a stale 404.
|
|
229
|
-
if(!route){await discover();route=
|
|
244
|
+
if(!route){await discover();route=resolve();}
|
|
230
245
|
if(route)active.set(route.pattern,Date.now());
|
|
231
246
|
if(route&&pins.has(client))pins.get(client).pattern=route.pattern;
|
|
232
247
|
const max=Math.max(0,Math.min(64,discovery.config.onDemandEntries?.pagesBufferLength??5));
|
|
@@ -235,7 +250,8 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
235
250
|
const inactive=[...active].filter(([pattern])=>!pinned.has(pattern)).sort((a,b)=>a[1]-b[1]);
|
|
236
251
|
const age=Math.min(86400000,discovery.config.onDemandEntries?.maxInactiveAge??60000);
|
|
237
252
|
while(inactive.length>max&&(Date.now()-inactive[0][1]>=age||active.size>64+pinned.size))active.delete(inactive.shift()[0]);
|
|
238
|
-
|
|
253
|
+
const graphChanged=current&&discovery.project.routes.some(candidate=>!current.manifest.routes.some(built=>built.id===candidate.id));
|
|
254
|
+
if(!current || graphChanged || route&&!current.patterns.has(route.pattern)){
|
|
239
255
|
try{await rebuild([...active.keys()]);}catch(error){if(route){failed.add(route.pattern);active.delete(route.pattern);}throw error;}
|
|
240
256
|
}
|
|
241
257
|
return updateInfo();
|
|
@@ -273,7 +289,7 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
273
289
|
// Node process has drained after() work and cache fills on stdin EOF.
|
|
274
290
|
await runtimePool.close();
|
|
275
291
|
await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
|
|
276
|
-
await queue.catch(()=>{});await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
|
|
292
|
+
await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
|
|
277
293
|
})();return closing;
|
|
278
294
|
};
|
|
279
295
|
const fatal=error=>{if(!stopping){console.error(error.message);process.exitCode=1;void shutdown();}};
|
|
@@ -298,7 +314,8 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
298
314
|
}
|
|
299
315
|
console.log(`PRNext development · http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port} · on-demand routes`);
|
|
300
316
|
if(options.safe)console.log('Safe mode: full route builds, fresh compilers and no worker warmup.');
|
|
301
|
-
watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:file=>{
|
|
317
|
+
watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:(file,event)=>{
|
|
318
|
+
typeChecker?.invalidate?.(file,event);
|
|
302
319
|
if(stopping||!shouldWatchProjectFile(file))return;changes.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void serialized(changed).catch(()=>{}),75);
|
|
303
320
|
}});
|
|
304
321
|
await serialized(discover).catch(async error=>{console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});});
|
package/runtime/document.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export async function renderDocument({ Document = DefaultDocument, App = Default
|
|
|
64
64
|
throw new TypeError('Document.getInitialProps must return an object with an html string');
|
|
65
65
|
}
|
|
66
66
|
if (Buffer.byteLength(initial.html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
|
|
67
|
-
const payload = { props, appProps, router: snapshot, buildId, route: { pattern: snapshot.pathname }, dynamicIds: [...dynamicModules] };
|
|
67
|
+
const payload = { props, appProps, router: snapshot, buildId, ...(manifest?.config?.deploymentId ? {deploymentId:manifest.config.deploymentId} : {}), route: { pattern: snapshot.pathname }, dynamicIds: [...dynamicModules] };
|
|
68
68
|
const assetPrefix = manifest?.config?.assetPrefix || manifest?.config?.basePath || '';
|
|
69
69
|
const nextData = { props: { ...appProps, pageProps: props }, page: snapshot.pathname, query: snapshot.query, buildId,
|
|
70
70
|
...(snapshot.locale ? {locale:snapshot.locale,locales:snapshot.locales,defaultLocale:snapshot.defaultLocale} : {}),
|
package/runtime/edge.mjs
CHANGED
|
@@ -75,14 +75,15 @@ async function createEdgeVM(reactMode) {
|
|
|
75
75
|
export async function loadEdgeModule(code, name, reactMode) {
|
|
76
76
|
if (reactMode !== undefined && reactMode !== 'rsc' && reactMode !== 'ssr') throw new Error('Unknown Edge React mode');
|
|
77
77
|
let pending;
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
{
|
|
79
|
+
const key=reactMode||'handler';
|
|
80
|
+
pending = reactVMs.get(key);
|
|
80
81
|
if (!pending) {
|
|
81
82
|
pending = createEdgeVM(reactMode);
|
|
82
|
-
reactVMs.set(
|
|
83
|
-
void pending.catch(() => { if (reactVMs.get(
|
|
83
|
+
reactVMs.set(key, pending);
|
|
84
|
+
void pending.catch(() => { if (reactVMs.get(key) === pending) reactVMs.delete(key); });
|
|
84
85
|
}
|
|
85
|
-
}
|
|
86
|
+
}
|
|
86
87
|
// This runtime module belongs to one compiled build. Its two React realms
|
|
87
88
|
// are shared lazily by all pages/boundaries; each bundle has its own factory.
|
|
88
89
|
const vm = await pending;
|
package/runtime/fetch-cache.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {withTrace,propagatedFetch} from './trace.mjs';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import cache from '../compat/data-cache.cjs';
|
|
3
4
|
import { dynamicUsage, trackStaticDependency } from '../compat/static-generation.cjs';
|
|
@@ -12,6 +13,7 @@ const MAX_MEMO_BYTES = 8 * 1024 * 1024;
|
|
|
12
13
|
const CAPTURE_IDLE_MS = 1000;
|
|
13
14
|
const CAPTURE_LIFETIME_MS = 20_000;
|
|
14
15
|
let registration;
|
|
16
|
+
const telemetryFetch=(input,init)=>nativeFetch(...propagatedFetch(input,init));
|
|
15
17
|
|
|
16
18
|
class UncacheableResponse extends Error {
|
|
17
19
|
constructor(response) { super('The fetch response is not cacheable'); this.response = response; }
|
|
@@ -305,24 +307,24 @@ export function installFetchCache() {
|
|
|
305
307
|
}, error => { diagnostics.record('fetch', { ...fields, error: true, durationMs: performance.now() - started }); throw error; });
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
|
-
return
|
|
310
|
+
return telemetryFetch(input, init);
|
|
309
311
|
};
|
|
310
312
|
Object.defineProperty(fetch, PATCHED, { value: dispatcher });
|
|
311
313
|
globalThis.fetch = fetch;
|
|
312
314
|
}
|
|
313
315
|
if (registration && dispatcher.handlers.includes(registration)) return globalThis.fetch;
|
|
314
316
|
async function fetch(input, init, context) {
|
|
315
|
-
if (context.phase === 'pages' || init?.next?.internal) return
|
|
317
|
+
if (context.phase === 'pages' || init?.next?.internal) return telemetryFetch(input, init);
|
|
316
318
|
if (inCacheScope()) {
|
|
317
319
|
const next = init?.next || input?.next || {};
|
|
318
320
|
if (next.revalidate !== undefined && next.revalidate !== false && (typeof next.revalidate !== 'number' || Number.isNaN(next.revalidate) || next.revalidate < 0)) throw new TypeError('fetch next.revalidate must be a nonnegative number or false');
|
|
319
321
|
recordCacheDependency(next);
|
|
320
|
-
return
|
|
322
|
+
return telemetryFetch(input, init);
|
|
321
323
|
}
|
|
322
324
|
const policy = fetchPolicy(input, init, context);
|
|
323
325
|
if (context.staticState) trackStaticDependency({ tags: policy.tags, paths: getCachePaths(context),
|
|
324
326
|
revalidate: context.cacheConfig?.dynamic === 'force-static' && policy.revalidate === 0 ? null : policy.revalidate }, context);
|
|
325
|
-
if (init?.dispatcher || init?.agent) return
|
|
327
|
+
if (init?.dispatcher || init?.agent) return telemetryFetch(input, init);
|
|
326
328
|
const state = cacheState(context);
|
|
327
329
|
const explicitSignal = init?.signal !== undefined || input instanceof Request;
|
|
328
330
|
const method = String(init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
@@ -331,9 +333,9 @@ export function installFetchCache() {
|
|
|
331
333
|
const persist = policy.persist && !hardRefresh && !context.draftMode && !context.cacheConfig?.forceNoStore;
|
|
332
334
|
if (diagnostics.enabled && !persist) diagnostics.record('cache', { route: diagnostics.route(context), state: 'bypass',
|
|
333
335
|
reason: context.draftMode ? 'draft mode' : hardRefresh ? 'development hard refresh' : context.cacheConfig?.forceNoStore ? 'route disables caching' : 'fetch policy does not opt into persistent caching' });
|
|
334
|
-
if (!persist && !memoize) return
|
|
336
|
+
if (!persist && !memoize) return telemetryFetch(input, init);
|
|
335
337
|
const prepared = await prepareRequest(input, init);
|
|
336
|
-
if (!prepared) return
|
|
338
|
+
if (!prepared) return telemetryFetch(input, init);
|
|
337
339
|
const request = prepared.request;
|
|
338
340
|
request.signal.throwIfAborted();
|
|
339
341
|
const memo = memoize ? memoState(state) : null;
|
|
@@ -361,7 +363,7 @@ export function installFetchCache() {
|
|
|
361
363
|
entry = undefined;
|
|
362
364
|
}
|
|
363
365
|
const producer = async ({ background = false, signal, cache: canPersist = true } = {}) => {
|
|
364
|
-
const response = await
|
|
366
|
+
const response = await telemetryFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
|
|
365
367
|
// Set-Cookie responses must not replay a session mutation, even when the
|
|
366
368
|
// request itself explicitly opted into shared caching.
|
|
367
369
|
if (response.headers.has('set-cookie') || (persist && response.status !== 200) || (!canPersist && !entry)) throw new UncacheableResponse(response);
|
|
@@ -384,7 +386,7 @@ export function installFetchCache() {
|
|
|
384
386
|
throw error;
|
|
385
387
|
}
|
|
386
388
|
}
|
|
387
|
-
registration = { context: optionalRequestContext, fetch };
|
|
389
|
+
registration = { context: optionalRequestContext, fetch:(input,init,context)=>withTrace({kind:3},'prnext.fetch',()=>fetch(input,init,context)) };
|
|
388
390
|
dispatcher.handlers.push(registration);
|
|
389
391
|
return globalThis.fetch;
|
|
390
392
|
}
|