@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.
Files changed (137) hide show
  1. package/README.md +26 -869
  2. package/build/actions.mjs +1 -1
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +81 -26
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +31 -8
  15. package/build/config.mjs +30 -1
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-update.mjs +4 -2
  21. package/build/dev-dependencies.mjs +1 -1
  22. package/build/dev-discovery.mjs +3 -3
  23. package/build/dev.mjs +15 -8
  24. package/build/dynamic.mjs +1 -1
  25. package/build/edge.mjs +3 -3
  26. package/build/fonts.mjs +1 -1
  27. package/build/framework-imports.mjs +15 -5
  28. package/build/html-only-config.mjs +53 -0
  29. package/build/html-only-graph.mjs +67 -0
  30. package/build/index.mjs +59 -26
  31. package/build/instrumentation.mjs +30 -0
  32. package/build/metadata-routes.mjs +1 -1
  33. package/build/migration-audit.mjs +1 -1
  34. package/build/native-type-config.mjs +37 -0
  35. package/build/persistent-cache.mjs +18 -3
  36. package/build/precompress.mjs +1 -1
  37. package/build/prerender-cache.mjs +70 -0
  38. package/build/prerender-config.mjs +29 -0
  39. package/build/prerender-pages.mjs +19 -9
  40. package/build/prerender-pool.mjs +165 -0
  41. package/build/prerender-process-worker.mjs +58 -0
  42. package/build/prerender-worker.mjs +5 -5
  43. package/build/production-builder.mjs +58 -9
  44. package/build/react-compiler-report.mjs +225 -0
  45. package/build/react-compiler.mjs +139 -0
  46. package/build/runtime-contract.mjs +2 -2
  47. package/build/static-export.mjs +4 -4
  48. package/build/styles.mjs +6 -4
  49. package/build/transform.mjs +1 -1
  50. package/build/typed-routes.mjs +4 -0
  51. package/build/typescript-dev.mjs +35 -8
  52. package/build/typescript-native-worker.mjs +55 -0
  53. package/build/typescript-native.mjs +18 -0
  54. package/build/typescript-toolchain.mjs +19 -0
  55. package/build/typescript.mjs +6 -4
  56. package/build/webpack-graph.mjs +6 -3
  57. package/build/worker-memory.mjs +9 -0
  58. package/cli.mjs +60 -4
  59. package/compat/activity-cache.cjs +26 -0
  60. package/compat/after.cjs +6 -4
  61. package/compat/app-layout-context.cjs +52 -2
  62. package/compat/base64.cjs +10 -0
  63. package/compat/cache-handlers.cjs +12 -2
  64. package/compat/cache-wire.cjs +28 -0
  65. package/compat/cache.cjs +2 -1
  66. package/compat/cache.d.cts +4 -0
  67. package/compat/cookies.cjs +21 -5
  68. package/compat/data-cache.cjs +12 -8
  69. package/compat/flight-delta.cjs +150 -0
  70. package/compat/headers.cjs +4 -2
  71. package/compat/incremental-cache.cjs +20 -7
  72. package/compat/index.d.ts +68 -3
  73. package/compat/link.cjs +12 -9
  74. package/compat/navigation-authorization.cjs +9 -0
  75. package/compat/navigation-stages.cjs +19 -0
  76. package/compat/offline-store.cjs +11 -0
  77. package/compat/offline.cjs +5 -0
  78. package/compat/offline.d.cts +2 -0
  79. package/compat/redis-cache.cjs +201 -0
  80. package/compat/redis-cache.d.cts +37 -0
  81. package/compat/server-primitives.cjs +31 -11
  82. package/compat/server.d.cts +15 -2
  83. package/compat/static-generation.cjs +7 -0
  84. package/compat/use-cache.cjs +3 -0
  85. package/docs/ai/compatibility.md +171 -2
  86. package/docs/ai/data.md +165 -5
  87. package/docs/ai/deployment.md +158 -0
  88. package/docs/ai/quickstart.md +29 -4
  89. package/docs/ai/routing.md +82 -0
  90. package/docs/ai/troubleshooting.md +37 -2
  91. package/package.json +26 -7
  92. package/runtime/action-client.mjs +10 -4
  93. package/runtime/action-server.mjs +2 -0
  94. package/runtime/app-client.mjs +212 -65
  95. package/runtime/app-content.mjs +4 -3
  96. package/runtime/app-partial-client.mjs +3 -0
  97. package/runtime/app-partial.mjs +1 -1
  98. package/runtime/app-prefetch-constants.mjs +4 -0
  99. package/runtime/app-prefetch.mjs +2 -3
  100. package/runtime/app-render-key.mjs +5 -0
  101. package/runtime/app-render.mjs +88 -28
  102. package/runtime/app-routing.mjs +22 -9
  103. package/runtime/app-static.mjs +18 -8
  104. package/runtime/app-stream-html.mjs +10 -53
  105. package/runtime/app-style-selection.mjs +34 -0
  106. package/runtime/app-styles.mjs +154 -0
  107. package/runtime/cache-components-rsc.mjs +4 -1
  108. package/runtime/cache-request.mjs +2 -1
  109. package/runtime/deployment-client.mjs +19 -0
  110. package/runtime/dev-client.mjs +7 -2
  111. package/runtime/dev-compiler-broker.mjs +42 -0
  112. package/runtime/dev-indicator.mjs +8 -3
  113. package/runtime/dev-lazy-tool.mjs +16 -0
  114. package/runtime/development.mjs +29 -12
  115. package/runtime/document.mjs +1 -1
  116. package/runtime/edge.mjs +6 -5
  117. package/runtime/fetch-cache.mjs +10 -8
  118. package/runtime/flight-delta.mjs +50 -0
  119. package/runtime/flight-tap.mjs +28 -0
  120. package/runtime/html-only.mjs +27 -0
  121. package/runtime/http.mjs +2 -0
  122. package/runtime/incremental-cache.mjs +25 -4
  123. package/runtime/instrumentation.mjs +49 -0
  124. package/runtime/navigation-recovery.mjs +36 -0
  125. package/runtime/navigation.mjs +7 -0
  126. package/runtime/node-html-stream.mjs +47 -0
  127. package/runtime/pages-client.mjs +14 -7
  128. package/runtime/pages-navigation.mjs +48 -63
  129. package/runtime/prefetch-scheduler.mjs +110 -0
  130. package/runtime/profile.mjs +14 -0
  131. package/runtime/profiles.json +2 -2
  132. package/runtime/render.mjs +3 -0
  133. package/runtime/route-pattern.mjs +44 -0
  134. package/runtime/rsc-worker.mjs +34 -12
  135. package/runtime/trace.mjs +78 -0
  136. package/runtime/watch-project.mjs +19 -15
  137. 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
- const result = { then(resolve, reject) { Promise.resolve().then(callback).then(resolve, reject); } };
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
+ }
@@ -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
- await updateStyles(kind === 'pages' ? route.css : manifest.app.css);
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
- void applyUpdate(message,{navigation:state.ensuring>0});
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 import('./dev-workbench.mjs');
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 import('./dev-tools-panel.mjs');
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;});
@@ -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 runtimePool=createDevRuntimePool({binary,root,home,environment:{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
+ 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();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});}
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 warm=discovery&&findRoute(raw);
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&&current&&warm&&active.has(warm.pattern)&&current.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=findRoute(raw);
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=findRoute(raw);}
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
- if(!current || route&&!current.patterns.has(route.pattern)){
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});});
@@ -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
- if (reactMode) {
79
- pending = reactVMs.get(reactMode);
78
+ {
79
+ const key=reactMode||'handler';
80
+ pending = reactVMs.get(key);
80
81
  if (!pending) {
81
82
  pending = createEdgeVM(reactMode);
82
- reactVMs.set(reactMode, pending);
83
- void pending.catch(() => { if (reactVMs.get(reactMode) === pending) reactVMs.delete(reactMode); });
83
+ reactVMs.set(key, pending);
84
+ void pending.catch(() => { if (reactVMs.get(key) === pending) reactVMs.delete(key); });
84
85
  }
85
- } else pending = createEdgeVM();
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;
@@ -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 nativeFetch(input, init);
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 nativeFetch(input, init);
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 nativeFetch(input, init);
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 nativeFetch(input, init);
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 nativeFetch(input, init);
336
+ if (!persist && !memoize) return telemetryFetch(input, init);
335
337
  const prepared = await prepareRequest(input, init);
336
- if (!prepared) return nativeFetch(input, init);
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 nativeFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
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
  }