@thomas.f/prnext 0.1.17-alpha → 0.1.19-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 (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  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 +122 -46
  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 +34 -10
  15. package/build/config.mjs +34 -3
  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-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
@@ -1,3 +1,4 @@
1
+ import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
1
2
  import { htmlEnvelope } from './html-envelope.mjs';
2
3
  import React from 'react';
3
4
  import { fontPreloads } from './font-preload.mjs';
@@ -12,55 +13,9 @@ import { addBasePath } from '../compat/paths.cjs';
12
13
  import { renderAppRecoveryShell } from './app-recovery.mjs';
13
14
  import { partialModel } from './app-partial-model.mjs';
14
15
  import { streamSelector } from './stream-select.mjs';
16
+ import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
15
17
 
16
- // The SSR decoder and HTML bootstrap share each Flight allocation. Unlike
17
- // ReadableStream.tee(), this tap stops reading when the HTML consumer stalls.
18
- // Before a document shell exists, at most the overall 16 MiB response limit can
19
- // accumulate: an async root layout may need the complete model to produce it.
20
- function flightTap(source) {
21
- const reader = source.getReader();
22
- const queue = [];
23
- let bytes = 0;
24
- let done = false;
25
- let failure;
26
- let outputStarted = false;
27
- let wakeData;
28
- let wakeSpace;
29
- const finish = error => {
30
- done = true; failure = error;
31
- wakeData?.(); wakeData = undefined;
32
- wakeSpace?.(); wakeSpace = undefined;
33
- };
34
- const cancel = async reason => { finish(reason); await reader.cancel(reason).catch(() => {}); };
35
- const body = new ReadableStream({
36
- async pull(controller) {
37
- try {
38
- while (!done && outputStarted && bytes >= 128 * 1024) await new Promise(resolve => { wakeSpace = resolve; });
39
- if (done) { if (failure) controller.error(failure); else controller.close(); return; }
40
- const next = await reader.read();
41
- if (next.done) { finish(); controller.close(); return; }
42
- queue.push(next.value); bytes += next.value.byteLength;
43
- wakeData?.(); wakeData = undefined;
44
- controller.enqueue(next.value);
45
- } catch (error) { finish(error); controller.error(error); }
46
- },
47
- cancel,
48
- }, { highWaterMark: 0 });
49
- return {
50
- body, cancel,
51
- startOutput() { outputStarted = true; },
52
- async next() {
53
- while (!queue.length && !done) await new Promise(resolve => { wakeData = resolve; });
54
- if (queue.length) {
55
- const value = queue.shift(); bytes -= value.byteLength;
56
- wakeSpace?.(); wakeSpace = undefined;
57
- return { value, done: false };
58
- }
59
- if (failure) throw failure;
60
- return { done: true };
61
- },
62
- };
63
- }
18
+ import {flightTap} from './flight-tap.mjs';
64
19
 
65
20
  export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs, partial }) {
66
21
  timeoutMs ??= route?.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : 25_000;
@@ -92,6 +47,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
92
47
  onError(error) {
93
48
  if (isDynamicBailout(error)) return error.digest;
94
49
  if (combinedSignal.aborted) return error?.digest;
50
+ if(result.reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);
95
51
  const navigation = navigationResponse(error);
96
52
  if (navigation) {
97
53
  if (committed) control = navigation;
@@ -104,8 +60,9 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
104
60
  const element = React.createElement(request.strictMode !== false ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router: resumedModel.router, nonce: request.nonce }, appContent(resumedModel.tree)));
105
61
  // React owns and mutates its opaque continuation during resume. Parsed
106
62
  // artifacts can be shared, but each HTML request needs its own continuation.
107
- const html = partial ? await resume(element, structuredClone(partial.postponed), renderOptions) : await renderToReadableStream(element, renderOptions);
108
- reader = html.getReader();
63
+ reader = !partial&&request.cacheConfig?.runtime!=='edge'&&useNodeHtml()
64
+ ? await nodeHtmlReader(element,renderOptions)
65
+ : (partial ? await resume(element, structuredClone(partial.postponed), renderOptions) : await renderToReadableStream(element, renderOptions)).getReader();
109
66
  const decoder = new TextDecoder();
110
67
  let prefix = partial?.shell || '';
111
68
  let htmlDone = false;
@@ -116,13 +73,13 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
116
73
  const next = await reader.read();
117
74
  if (next.done) { htmlDone = true; prefix += decoder.decode(); break; }
118
75
  prefix += decoder.decode(next.value, { stream: true });
119
- if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw new Error('HTML shell exceeds the 16 MiB PRNext limit');
76
+ if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw Object.assign(new Error('HTML shell exceeds the 16 MiB PRNext limit'),{code:'PRNEXT_RESPONSE_TOO_LARGE'});
120
77
  }
121
78
  if (renderError) throw renderError;
122
79
  if (!/<html(?:\s|>)/i.test(prefix) || !/<body(?:\s|>)/i.test(prefix) || !/<\/head>/i.test(prefix)) {
123
80
  throw Object.assign(new Error('App Router root layout must render <html>, <head> and <body> elements'), { code: 'PRNEXT_INVALID_ROOT_LAYOUT' });
124
81
  }
125
- const styles = fontPreloads(route.fonts, request.nonce) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
82
+ const styles = fontPreloads(route.fonts, request.nonce) + (model.css ?? route.css ?? []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
126
83
  if (styles && !partial) prefix = prefix.replace(/<\/head>/i, `${styles}</head>`);
127
84
  tap.startOutput();
128
85
  committed = true;
@@ -183,7 +140,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
183
140
  return { status, headers: { ...responseHeaders, ...result.headers, 'content-type': 'text/html; charset=utf-8' }, body, cancel };
184
141
  } catch (error) {
185
142
  if (!committed && !combinedSignal.aborted && error?.statusCode !== 504 &&
186
- error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
143
+ error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && error?.code !== 'PRNEXT_RESPONSE_TOO_LARGE' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
187
144
  try {
188
145
  // Next emits an empty document after a shell failure. The original
189
146
  // Flight stream still carries the failing subtree to client boundaries;
@@ -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
+ }
@@ -0,0 +1,27 @@
1
+ import {open,rm} from 'node:fs/promises';
2
+ import {randomUUID} from 'node:crypto';
3
+ import path from 'node:path';
4
+ import {rename} from './fs.mjs';
5
+
6
+ // The old destination stays readable until the complete replacement exists.
7
+ // Keeping an injectable I/O boundary lets tests exercise real partial-write and
8
+ // rename failures without depending on an antivirus or a physical disk fault.
9
+ export function createAtomicFileWriter({openFile=open,moveFile=rename,removeFile=rm}={}){
10
+ return async function writeAtomicFile(filename,data){
11
+ const temporary=filename+'.'+randomUUID()+'.tmp';let file,created=false,failure,phase='create';
12
+ try{
13
+ file=await openFile(temporary,'wx',0o600);created=true;phase='write';await file.writeFile(data);phase='close';await file.close();file=undefined;
14
+ phase='publish';await moveFile(temporary,filename);
15
+ }catch(error){
16
+ error.atomicFile={operation:'publish-development-state',phase,code:error.code||'UNKNOWN',destination:path.basename(filename),...(error.atomicRename||{})};
17
+ failure=error;
18
+ throw error;
19
+ }finally{
20
+ await file?.close().catch(()=>{});
21
+ // This unpredictable temporary is owned by this invocation. Never remove
22
+ // the destination to work around a Windows lock.
23
+ if(created)try{await removeFile(temporary,{force:true});}catch(error){if(failure)failure.atomicFile.cleanupCode=error.code||'UNKNOWN';else throw error;}
24
+ }
25
+ };
26
+ }
27
+ export const writeAtomicFile=createAtomicFileWriter();
@@ -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;
@@ -54,7 +57,9 @@ globalThis[codecSymbol] = {
54
57
  chunks.push(Buffer.from(value));
55
58
  }
56
59
  if (failure) throw failure;
57
- return Buffer.concat(chunks, size);
60
+ // Each chunk above is already an owned copy. A single chunk needs no
61
+ // second allocation; never expose the producer's mutable stream storage.
62
+ return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
58
63
  },
59
64
  async decode(bytes, encoded, context) {
60
65
  return createFromReadableStream(new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), {
@@ -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
+ }
@@ -195,11 +195,18 @@ async function applyUpdate(message, {navigation=false}={}) {
195
195
  const route = kind === 'pages' ? manifest.pages.find(route => route.pattern === application.router.snapshot().pathname) : null;
196
196
  const entry = kind === 'pages' ? route?.client : manifest.app?.client;
197
197
  if (!entry) { reload('The current route changed. Page reloaded.'); return; }
198
- if(!message.cssOnly)await import(/* webpackIgnore: true */ entry);
198
+ if(!message.cssOnly){
199
+ await import(/* webpackIgnore: true */ entry);
200
+ // Activated deferred modules must register their updated component
201
+ // families before Refresh runs, even when their loader is already cached.
202
+ await Promise.all((manifest.lazy||[]).map(module=>import(/* webpackIgnore: true */ module.url)));
203
+ }
199
204
  const pending = state.pending;
200
205
  if (pending) await application.devPrepare?.(pending.options);
201
206
  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);
207
+ if (kind === 'app' && application.devPrepareStyles) await application.devPrepareStyles(manifest.app.routes);
208
+ else await updateStyles([...(kind === 'pages' ? route.css : application.devStyles?.(manifest.app.routes) || manifest.app.css),...(manifest.lazy||[]).flatMap(module=>module.css)]);
209
+ if (kind === 'app') application.devStylesApplied?.(manifest.app.routes, { commit: !!message.cssOnly || !pending });
203
210
  // An unrelated type-only edit may publish a new generation without changing
204
211
  // any browser module. Keep its existing runtime/hydration issues and current
205
212
  // selection until application code changes, rather than racing diagnostics.
@@ -287,7 +294,10 @@ function connect(options) {
287
294
  if (message.buildId === state.buildId) return;
288
295
  // A cold navigation may receive SSE readiness before its ensure response.
289
296
  // Updating modules is safe; refreshing the old route would cancel the click.
290
- void applyUpdate(message,{navigation:state.ensuring>0});
297
+ // A retained branch can become cold after ensure() has returned. Its
298
+ // worker-triggered compilation is still part of the pending navigation;
299
+ // committing a refresh here would abort it and restore the previous URL.
300
+ void applyUpdate(message,{navigation:state.ensuring>0||message.reason?.code==='route-opened'});
291
301
  };
292
302
  globalThis.addEventListener('error', event => { if (event.error) showError(event.error); });
293
303
  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
+ }