@thomas.f/prnext 0.1.16-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 (215) hide show
  1. package/AI.md +63 -0
  2. package/README.md +35 -503
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +6 -1
  5. package/build/analyze.mjs +340 -0
  6. package/build/app-config.mjs +20 -8
  7. package/build/app-static.mjs +41 -14
  8. package/build/app-styles.mjs +79 -0
  9. package/build/app.mjs +99 -31
  10. package/build/build-lock.mjs +6 -6
  11. package/build/bundle-runtime.mjs +39 -11
  12. package/build/cache-components.mjs +1 -1
  13. package/build/cache-handlers.mjs +1 -1
  14. package/build/check.mjs +56 -11
  15. package/build/compact-css.mjs +53 -0
  16. package/build/compilation-cache.mjs +269 -0
  17. package/build/compilation-store.mjs +142 -0
  18. package/build/compiler.mjs +51 -4
  19. package/build/concurrent.mjs +12 -0
  20. package/build/config.mjs +50 -13
  21. package/build/create.mjs +239 -0
  22. package/build/css-modules.mjs +65 -10
  23. package/build/deployment-assets.mjs +89 -0
  24. package/build/deployment-config.mjs +18 -0
  25. package/build/dev-compiler-health.mjs +11 -0
  26. package/build/dev-css-update.mjs +85 -0
  27. package/build/dev-dependencies.mjs +44 -0
  28. package/build/dev-discovery.mjs +28 -0
  29. package/build/dev-worker.mjs +14 -5
  30. package/build/dev.mjs +64 -6
  31. package/build/dynamic.mjs +1 -1
  32. package/build/edge.mjs +5 -4
  33. package/build/fonts.mjs +1 -1
  34. package/build/framework-imports.mjs +50 -0
  35. package/build/html-only-config.mjs +53 -0
  36. package/build/html-only-graph.mjs +67 -0
  37. package/build/incremental.mjs +8 -4
  38. package/build/index.mjs +140 -112
  39. package/build/inspect.mjs +3 -0
  40. package/build/inspection-summary.mjs +37 -4
  41. package/build/instrumentation.mjs +30 -0
  42. package/build/loader-cache.mjs +53 -26
  43. package/build/metadata-routes.mjs +3 -4
  44. package/build/migrate.mjs +60 -57
  45. package/build/migration-audit.mjs +1 -1
  46. package/build/native-type-config.mjs +37 -0
  47. package/build/npm-packages.mjs +3 -18
  48. package/build/package-manager.mjs +308 -0
  49. package/build/persistent-cache.mjs +112 -0
  50. package/build/precompress.mjs +62 -30
  51. package/build/prerender-cache.mjs +70 -0
  52. package/build/prerender-config.mjs +29 -0
  53. package/build/prerender-pages.mjs +100 -0
  54. package/build/prerender-pool.mjs +165 -0
  55. package/build/prerender-process-worker.mjs +58 -0
  56. package/build/prerender-worker.mjs +10 -0
  57. package/build/production-builder.mjs +129 -0
  58. package/build/react-compiler-report.mjs +225 -0
  59. package/build/react-compiler.mjs +139 -0
  60. package/build/runtime-contract.mjs +2 -2
  61. package/build/scan.mjs +4 -4
  62. package/build/source-analysis.mjs +42 -0
  63. package/build/static-adapter.mjs +13 -5
  64. package/build/static-export.mjs +4 -4
  65. package/build/static-input.mjs +48 -0
  66. package/build/styles.mjs +17 -3
  67. package/build/transform-cache.mjs +37 -4
  68. package/build/transform.mjs +1 -1
  69. package/build/type-command.mjs +41 -0
  70. package/build/typed-routes.mjs +95 -0
  71. package/build/typescript-dev-worker.mjs +18 -0
  72. package/build/typescript-dev.mjs +49 -0
  73. package/build/typescript-native-worker.mjs +55 -0
  74. package/build/typescript-native.mjs +18 -0
  75. package/build/typescript-toolchain.mjs +19 -0
  76. package/build/typescript-watch.mjs +22 -0
  77. package/build/typescript.mjs +11 -8
  78. package/build/webpack-graph.mjs +170 -30
  79. package/build/worker-memory.mjs +9 -0
  80. package/build/worker-preload.mjs +9 -0
  81. package/cli.mjs +162 -122
  82. package/compat/activity-cache.cjs +26 -0
  83. package/compat/after.cjs +6 -4
  84. package/compat/app-layout-context.cjs +52 -2
  85. package/compat/base64.cjs +10 -0
  86. package/compat/cache-handlers.cjs +12 -2
  87. package/compat/cache-wire.cjs +28 -0
  88. package/compat/cache.cjs +2 -1
  89. package/compat/cache.d.cts +4 -0
  90. package/compat/cookies.cjs +21 -5
  91. package/compat/data-cache.cjs +12 -8
  92. package/compat/flight-delta.cjs +150 -0
  93. package/compat/headers.cjs +4 -2
  94. package/compat/incremental-cache.cjs +20 -7
  95. package/compat/index.d.ts +89 -10
  96. package/compat/link.cjs +12 -9
  97. package/compat/link.d.cts +4 -3
  98. package/compat/navigation-authorization.cjs +9 -0
  99. package/compat/navigation-stages.cjs +19 -0
  100. package/compat/navigation.d.cts +4 -3
  101. package/compat/offline-store.cjs +11 -0
  102. package/compat/offline.cjs +5 -0
  103. package/compat/offline.d.cts +2 -0
  104. package/compat/redis-cache.cjs +201 -0
  105. package/compat/redis-cache.d.cts +37 -0
  106. package/compat/router.d.cts +4 -3
  107. package/compat/server-primitives.cjs +31 -11
  108. package/compat/server.d.cts +15 -2
  109. package/compat/static-generation.cjs +7 -0
  110. package/compat/typed-route.d.cts +17 -0
  111. package/compat/use-cache.cjs +3 -0
  112. package/docs/ai/compatibility.md +275 -0
  113. package/docs/ai/data.md +288 -0
  114. package/docs/ai/deployment.md +269 -0
  115. package/docs/ai/manifest.json +11 -0
  116. package/docs/ai/quickstart.md +171 -0
  117. package/docs/ai/routing.md +214 -0
  118. package/docs/ai/troubleshooting.md +155 -0
  119. package/index.mjs +2 -0
  120. package/native/diagnostics.mjs +34 -0
  121. package/native/resolve.mjs +7 -2
  122. package/package.json +44 -7
  123. package/release/ai-docs.mjs +48 -0
  124. package/release/verify.mjs +2 -0
  125. package/runtime/action-client.mjs +10 -4
  126. package/runtime/action-server.mjs +2 -0
  127. package/runtime/ai-docs.mjs +202 -0
  128. package/runtime/app-client.mjs +226 -68
  129. package/runtime/app-content.mjs +4 -3
  130. package/runtime/app-partial-client.mjs +3 -0
  131. package/runtime/app-partial.mjs +1 -1
  132. package/runtime/app-prefetch-constants.mjs +4 -0
  133. package/runtime/app-prefetch.mjs +2 -3
  134. package/runtime/app-render-key.mjs +5 -0
  135. package/runtime/app-render.mjs +100 -28
  136. package/runtime/app-routing.mjs +22 -9
  137. package/runtime/app-static.mjs +18 -8
  138. package/runtime/app-stream-html.mjs +10 -53
  139. package/runtime/app-style-selection.mjs +34 -0
  140. package/runtime/app-styles.mjs +154 -0
  141. package/runtime/cache-components-rsc.mjs +4 -1
  142. package/runtime/cache-request.mjs +2 -1
  143. package/runtime/cloud/archive.mjs +5 -6
  144. package/runtime/compile-cache.mjs +8 -0
  145. package/runtime/deployment-client.mjs +19 -0
  146. package/runtime/dev-ai-brands.mjs +26 -0
  147. package/runtime/dev-ai-style.mjs +99 -0
  148. package/runtime/dev-build-reasons.mjs +25 -0
  149. package/runtime/dev-cache-control.mjs +53 -0
  150. package/runtime/dev-client.mjs +245 -46
  151. package/runtime/dev-compiler-broker.mjs +42 -0
  152. package/runtime/dev-console-copy.mjs +19 -0
  153. package/runtime/dev-console.mjs +106 -0
  154. package/runtime/dev-dependency-graph.mjs +40 -0
  155. package/runtime/dev-element-picker-style.mjs +27 -0
  156. package/runtime/dev-element-picker.mjs +213 -0
  157. package/runtime/dev-error-overlay-style.mjs +41 -0
  158. package/runtime/dev-error-overlay.mjs +167 -0
  159. package/runtime/dev-error-report.mjs +121 -0
  160. package/runtime/dev-file-changes.mjs +1 -1
  161. package/runtime/dev-gateway.mjs +44 -0
  162. package/runtime/dev-host-relay.mjs +66 -0
  163. package/runtime/dev-indicator.mjs +191 -0
  164. package/runtime/dev-issue-motion.mjs +214 -0
  165. package/runtime/dev-lazy-tool.mjs +34 -0
  166. package/runtime/dev-logo.mjs +2 -0
  167. package/runtime/dev-notification.mjs +81 -0
  168. package/runtime/dev-popout.mjs +34 -0
  169. package/runtime/dev-position.mjs +107 -0
  170. package/runtime/dev-preview-touch.mjs +122 -0
  171. package/runtime/dev-runtime-pool.mjs +45 -0
  172. package/runtime/dev-runtime-spare.mjs +57 -0
  173. package/runtime/dev-tools-client.mjs +155 -0
  174. package/runtime/dev-tools-mcp.mjs +126 -0
  175. package/runtime/dev-tools-metrics.mjs +31 -0
  176. package/runtime/dev-tools-model.mjs +42 -0
  177. package/runtime/dev-tools-panel.mjs +341 -0
  178. package/runtime/dev-tools-result.mjs +37 -0
  179. package/runtime/dev-tools-server.mjs +256 -0
  180. package/runtime/dev-tools-source.mjs +104 -0
  181. package/runtime/dev-tooltip.mjs +94 -0
  182. package/runtime/dev-ui-icons.mjs +8 -0
  183. package/runtime/dev-workbench-style.mjs +106 -0
  184. package/runtime/dev-workbench.mjs +428 -0
  185. package/runtime/dev-worker-logs.mjs +31 -0
  186. package/runtime/development.mjs +324 -0
  187. package/runtime/document.mjs +1 -1
  188. package/runtime/edge.mjs +6 -5
  189. package/runtime/env.mjs +2 -1
  190. package/runtime/fetch-cache.mjs +10 -8
  191. package/runtime/flight-delta.mjs +50 -0
  192. package/runtime/flight-tap.mjs +28 -0
  193. package/runtime/html-only.mjs +27 -0
  194. package/runtime/http.mjs +2 -0
  195. package/runtime/incremental-cache.mjs +25 -4
  196. package/runtime/instrumentation.mjs +49 -0
  197. package/runtime/navigation-recovery.mjs +36 -0
  198. package/runtime/navigation.mjs +7 -0
  199. package/runtime/node-html-stream.mjs +47 -0
  200. package/runtime/pages-client.mjs +24 -8
  201. package/runtime/pages-navigation.mjs +54 -64
  202. package/runtime/persistent/client.mjs +6 -5
  203. package/runtime/persistent/daemon.mjs +66 -56
  204. package/runtime/persistent/native-proxy.mjs +63 -0
  205. package/runtime/prefetch-scheduler.mjs +110 -0
  206. package/runtime/preload.mjs +12 -0
  207. package/runtime/profile.mjs +14 -0
  208. package/runtime/profiles.json +2 -2
  209. package/runtime/render.mjs +3 -0
  210. package/runtime/route-pattern.mjs +44 -0
  211. package/runtime/rsc-worker.mjs +41 -12
  212. package/runtime/trace.mjs +78 -0
  213. package/runtime/typed-route.mjs +19 -0
  214. package/runtime/watch-project.mjs +29 -18
  215. package/runtime/worker.mjs +102 -24
@@ -35,6 +35,20 @@ export async function runIncrementalCache(options) {
35
35
  const input = JSON.parse(Buffer.from(options.body, 'base64').toString());
36
36
  if (!input || typeof input.key !== 'string' || input.key.length > 8192 || !kinds.has(input.kind)) throw new Error('Invalid incremental cache key or kind');
37
37
  const context = { ...options, cacheHandler: options.manifest.config.cacheHandler, cacheMaxMemorySize: options.manifest.config.cacheMaxMemorySize };
38
+ if (input.op === 'acquire' || input.op === 'release') {
39
+ const { handler } = await legacy.load(context);
40
+ if (input.op === 'release') {
41
+ if (handler.prnextLeaseVersion === 1 && typeof handler.release === 'function' && input.lease) await legacy.invoke(handler, 'release', [input.key, input.lease], context);
42
+ return empty(204);
43
+ }
44
+ if (handler.prnextLeaseVersion !== 1 || typeof handler.acquire !== 'function') return empty(204);
45
+ let lease;
46
+ try { lease = await legacy.invoke(handler, 'acquire', [input.key, { signal: options.signal }], context); }
47
+ catch (error) { if (error.code === 'PRNEXT_REDIS_BUSY') return empty(425); throw error; }
48
+ const encoded = JSON.stringify(lease);
49
+ if (!lease || encoded.length > 4096 || /[\r\n]/.test(encoded)) throw new Error('Invalid incremental cache generation lease');
50
+ return { ...empty(204), headers: { 'x-prnext-cache-lease': encoded } };
51
+ }
38
52
  if (input.op === 'invalidate') {
39
53
  await legacy.invalidate(context, { tags: input.tags, mode: 'expire' });
40
54
  return empty(204);
@@ -54,7 +68,8 @@ export async function runIncrementalCache(options) {
54
68
  }
55
69
  const tags = [...new Set([...info.tags, ...implicit(info.paths)])];
56
70
  if (tags.length) value.headers = { ...value.headers, 'x-next-cache-tags': tags.join(',') };
57
- await legacy.setEntry(input.key, value, context, { tags, cacheControl: { revalidate: info.revalidate }, isRoutePPREnabled: false });
71
+ const stored = await legacy.setEntry(input.key, value, context, { tags, cacheControl: { revalidate: info.revalidate }, isRoutePPREnabled: false, prnextLease: input.lease });
72
+ if (stored === false) return empty(409);
58
73
  return empty(204);
59
74
  }
60
75
  if (input.op !== 'get') throw new Error('Unknown incremental cache operation');
@@ -73,11 +88,17 @@ export async function runIncrementalCache(options) {
73
88
  data = input.kind === 'APP_PAGE' ? bytes(value.rscData) : Buffer.from(JSON.stringify(value.pageData));
74
89
  }
75
90
  if (html.length + data.length > MAX_BYTES) throw new Error('Incremental cache entry exceeds 16 MiB');
76
- const cacheVersion = createHash('sha256').update(JSON.stringify([entry.lastModified, value.status, value.headers, info])).update(html).update(data).digest('hex');
91
+ const softStale = entry.isStale === true;
92
+ const cacheVersion = createHash('sha256').update(JSON.stringify([entry.lastModified, softStale, value.status, value.headers, info])).update(html).update(data).digest('hex');
77
93
  if (input.knownVersion === cacheVersion) return { ...empty(304), headers: { 'x-prnext-cache-version': cacheVersion } };
78
94
  const headers = { ...value.headers }; delete headers['x-next-cache-tags'];
95
+ // A positive expired generation uses native stale-while-revalidate. -1/zero
96
+ // remains the existing hard-expiration path. The cacheVersion above stays
97
+ // stable even when replicas have slightly different wall clocks.
98
+ const revalidate = softStale && info.revalidate === false ? 1 : info.revalidate;
99
+ const lastModified = softStale ? Math.max(0, Math.min(Date.now(), entry.lastModified) - Math.max(1, revalidate || 1) * 1000 - 1) : entry.lastModified;
79
100
  return { status: value.status, headers, body: [html, data],
80
- isr: { ...(input.kind === 'APP_ROUTE' ? { kind: 'route' } : {}), revalidate: entry.lastModified < 0 ? 0 : info.revalidate,
101
+ isr: { ...(input.kind === 'APP_ROUTE' ? { kind: 'route' } : {}), revalidate: entry.lastModified < 0 ? 0 : revalidate,
81
102
  htmlLength: html.length, dataLength: data.length, tags: info.tags, paths: info.paths,
82
- lastModified: entry.lastModified, cacheVersion } };
103
+ lastModified, cacheVersion } };
83
104
  }
@@ -0,0 +1,49 @@
1
+ import {withTrace,markTraceError} from './trace.mjs';
2
+ import {readFile} from 'node:fs/promises';
3
+ import {AsyncLocalStorage} from 'node:async_hooks';
4
+ import {pathToFileURL} from 'node:url';
5
+ import path from 'node:path';
6
+ import {isControlError} from './navigation.mjs';
7
+
8
+ const modules=new Map(),scope=new AsyncLocalStorage(),reported=new WeakSet();
9
+ function bounded(promise,milliseconds,label){
10
+ let timer;
11
+ return Promise.race([Promise.resolve(promise),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error(label+' timed out')),milliseconds);})]).finally(()=>clearTimeout(timer));
12
+ }
13
+ export function initializeInstrumentation(distDir,manifest,{realm='node',reactMode,loader}={}) {
14
+ const file=manifest.instrumentation?.[realm];if(!file)return Promise.resolve({});
15
+ const filename=path.resolve(distDir,file),key=filename+':'+(reactMode||realm);let pending=modules.get(key);
16
+ loader??=realm==='edge'?async file=>{const {loadEdgeModule}=await import('./edge.mjs');const asset=file.replace(/\.mjs$/,'.edge.js');return loadEdgeModule(await readFile(asset,'utf8'),asset,reactMode);}:file=>import(pathToFileURL(file).href);
17
+ if(!pending){
18
+ pending=(async()=>{const module=await loader(filename);if(module.register!==undefined&&typeof module.register!=='function')throw Error('instrumentation.register must be a function');if(module.onRequestError!==undefined&&typeof module.onRequestError!=='function')throw Error('instrumentation.onRequestError must be a function');await bounded(Promise.resolve().then(()=>module.register?.()),30000,'instrumentation.register');return module;})();
19
+ // Retain failures too: a failed initialization must never run application code.
20
+ modules.set(key,pending);
21
+ }
22
+ return pending;
23
+ }
24
+ export function wasRequestErrorReported(error){return !!error&&typeof error==='object'&&(error.instrumentationReported===true||reported.has(error));}
25
+ export function acknowledgeRequestError(error){if(error&&typeof error==='object'){scope.getStore()?.seen.add(error);reported.add(error);}markTraceError();}
26
+ export function reportRequestError(error,extra={}) {
27
+ if(isControlError(error))return;markTraceError();
28
+ const state=scope.getStore();if(error?.instrumentationReported===true||!state?.module.onRequestError||state.reporting)return;
29
+ const value=error&&typeof error==='object'?error:new Error(String(error));
30
+ if(state.seen.has(value)||state.seen.size>=32)return;state.seen.add(value);reported.add(value);
31
+ const pending=scope.run({...state,reporting:true},()=>bounded(Promise.resolve().then(()=>state.module.onRequestError(value,state.request,{...state.context,...extra})),5000,'instrumentation.onRequestError')).catch(()=>{console.error('[prnext] instrumentation.onRequestError failed');});
32
+ state.pending.add(pending);void pending.finally(()=>state.pending.delete(pending));return pending;
33
+ }
34
+ export async function withInstrumentation(options,callback,{realm='node',module}={}) {
35
+ const edge=options.route?.cacheConfig?.runtime==='edge'||options.route?.handlerConfig?.runtime==='edge'||options.route?.runtime==='edge';
36
+ const renderRealm=realm;
37
+ if(edge)realm='edge';
38
+ module??=await initializeInstrumentation(options.distDir,options.manifest||{},{realm,reactMode:edge&&options.route?.router==='app'&&options.route?.kind==='page'?(renderRealm==='rsc'?'rsc':'ssr'):undefined});
39
+ const traced=()=>withTrace(options,renderRealm==='rsc'?'prnext.rsc':'prnext.node',callback);
40
+ if(!module.onRequestError)return traced();
41
+ const url=new URL(options.originalUrl||options.url||'http://localhost/'),route=options.route||{};
42
+ const request=Object.freeze({path:url.pathname+url.search,method:options.method||'GET',headers:Object.freeze(Object.fromEntries(new Headers(options.headers)))});
43
+ const context={routerKind:route.router==='app'?'App Router':'Pages Router',routePath:route.pattern||options.routePattern||url.pathname,
44
+ routeType:options.action||new Headers(options.headers).has('next-action')?'action':route.kind==='middleware'?'proxy':route.kind==='api'?'route':'render',
45
+ renderSource:renderRealm==='rsc'?'react-server-components':new Headers(options.headers).get('rsc')==='1'?'react-server-components-payload':'server-rendering',
46
+ revalidateReason:['stale','on-demand'].includes(options.revalidateReason)?options.revalidateReason:undefined,renderType:options.partialFlight?'dynamic-resume':'dynamic'};
47
+ const state={module,request,context,seen:new Set(),pending:new Set()};
48
+ return scope.run(state,async()=>{try{return await traced();}catch(error){if(!options.signal?.aborted||error?.statusCode===504)reportRequestError(error);throw error;}finally{await Promise.all([...state.pending]);}});
49
+ }
@@ -0,0 +1,36 @@
1
+ import offline from '../compat/offline-store.cjs';
2
+
3
+ export function networkFailure(error){
4
+ return error?.name==='NetworkError'||((error instanceof TypeError||error?.name==='TypeError')&&/fetch|network|load failed|terminated/i.test(error.message||''))||/^Connection closed\.?$/.test(error?.message||'');
5
+ }
6
+ function waitForConnection({signal,events,online,milliseconds,waitingOffline}){
7
+ return new Promise((resolve,reject)=>{
8
+ const finish=callback=>value=>{clearTimeout(timer);signal?.removeEventListener('abort',aborted);events?.removeEventListener('online',connected);callback(value);};
9
+ const done=finish(resolve),fail=finish(reject),aborted=()=>fail(signal.reason||new DOMException('Navigation cancelled','AbortError'));
10
+ const connected=()=>{if(online())done();};
11
+ const timer=setTimeout(()=>done(),milliseconds);
12
+ signal?.addEventListener('abort',aborted,{once:true});events?.addEventListener('online',connected);
13
+ if(signal?.aborted)aborted();
14
+ else if(waitingOffline&&online())done();
15
+ });
16
+ }
17
+ // Called exclusively around navigation GETs. Server Actions never enter this
18
+ // path: a lost response does not prove their mutation failed to execute.
19
+ export async function recoverNavigation(read,{signal,events=globalThis.window,online=()=>globalThis.navigator?.onLine!==false,budgetMs=30000,maxAttempts=3}={}){
20
+ const until=performance.now()+budgetMs;let waiting,lastError,attempts=0;
21
+ const failure=()=>Object.assign(new Error('Unable to load the page. Check your connection and try again.',{cause:lastError}),{code:'PRNEXT_NAVIGATION_NETWORK'});
22
+ const deadline=new AbortController(),combined=signal?AbortSignal.any([signal,deadline.signal]):deadline.signal;
23
+ const timer=setTimeout(()=>deadline.abort(failure()),budgetMs);
24
+ try{
25
+ for(;;){
26
+ combined.throwIfAborted();
27
+ if(online()||attempts===0){
28
+ try{return await read(combined);}catch(error){if(combined.aborted)throw combined.reason;if(!networkFailure(error))throw error;lastError=error;attempts++;waiting||=offline.beginWait();}
29
+ }
30
+ const remaining=until-performance.now();
31
+ if(remaining<=0||attempts>=maxAttempts)throw failure();
32
+ const waitingOffline=!online();
33
+ await waitForConnection({signal:combined,events,online,waitingOffline,milliseconds:Math.min(remaining,waitingOffline?remaining:250*2**Math.max(0,attempts-1))});
34
+ }
35
+ }finally{clearTimeout(timer);waiting?.();}
36
+ }
@@ -14,3 +14,10 @@ export function navigationResponse(error) {
14
14
  if (!['http:', 'https:'].includes(parsed.protocol)) return null;
15
15
  return { status, headers: { location: destination, 'cache-control': 'no-store' }, body: Buffer.alloc(0) };
16
16
  }
17
+
18
+ /** Expected routing/render controls must not become application error reports. */
19
+ export function isControlError(error) {
20
+ return !!navigationResponse(error) || error?.digest === 'BAILOUT_TO_CLIENT_SIDE_RENDERING'
21
+ || error?.code === 'PRNEXT_DYNAMIC_SERVER_USAGE' || error?.code === 'PRNEXT_PPR_DYNAMIC'
22
+ || error?.digest === 'PRNEXT_PPR_DYNAMIC';
23
+ }
@@ -0,0 +1,47 @@
1
+ import {Writable} from 'node:stream';
2
+ import {renderToPipeableStream} from 'react-dom/server';
3
+
4
+ // One bounded Node stream, without a second Web reader queue. Partial React
5
+ // continuations keep using their original Web API and opaque resume state.
6
+ export function nodeHtmlReader(element,options={}){
7
+ return new Promise((resolve,reject)=>{
8
+ const {signal,...renderOptions}=options;
9
+ let renderer,settled=false,finished=false,slot,pending,acknowledge,failure;
10
+ const cleanup=()=>signal?.removeEventListener('abort',aborted);
11
+ const deliver=()=>{
12
+ if(!pending)return;
13
+ const {resolve,reject}=pending;
14
+ if(failure){pending=undefined;reject(failure);}
15
+ else if(slot){pending=undefined;const current=slot;slot=undefined;acknowledge=current.done;resolve({value:current.chunk,done:false});}
16
+ else if(finished){pending=undefined;resolve({done:true});}
17
+ };
18
+ // A single writable replaces PassThrough's writable+readable machinery.
19
+ // Keep its write callback until the next read: React may reuse a chunk only
20
+ // after its consumer has finished, and native backpressure stays bounded.
21
+ const stream=new Writable({highWaterMark:64*1024,
22
+ write(chunk,_encoding,done){slot={chunk,done};deliver();},
23
+ final(done){finished=true;cleanup();deliver();done();},
24
+ });
25
+ const fail=error=>{failure=error;if(!settled){settled=true;cleanup();reject(error);}deliver();stream.destroy(error);};
26
+ stream.on('error',error=>{failure=error;cleanup();deliver();});
27
+ const aborted=()=>{const error=signal.reason||new Error('HTML render was cancelled');renderer?.abort(error);fail(error);};
28
+ if(signal?.aborted){aborted();return;}
29
+ signal?.addEventListener('abort',aborted,{once:true});
30
+ try{
31
+ renderer=renderToPipeableStream(element,{...renderOptions,onShellError:fail,onShellReady(){
32
+ if(settled)return;settled=true;
33
+ resolve({
34
+ read(){
35
+ if(pending)return Promise.reject(new Error('HTML reads must be sequential.'));
36
+ const done=acknowledge;acknowledge=undefined;done?.();
37
+ return new Promise((resolve,reject)=>{pending={resolve,reject};deliver();});
38
+ },
39
+ async cancel(reason){if(finished)return;finished=true;cleanup();renderer.abort(reason);const done=acknowledge||slot?.done;acknowledge=undefined;slot=undefined;stream.destroy();done?.();deliver();},
40
+ releaseLock(){if(finished)cleanup();},
41
+ });
42
+ renderer.pipe(stream);
43
+ }});
44
+ }catch(error){fail(error);}
45
+ });
46
+ }
47
+ export const useNodeHtml=()=>process.env.PRNEXT_SSR_STREAM==='node';
@@ -1,3 +1,4 @@
1
+ import { deploymentHeaders, assertDeploymentResponse } from './deployment-client.mjs';
1
2
  import React from 'react';
2
3
  import { hydrateRoot } from 'react-dom/client';
3
4
  import Router, { RouterProvider, useRouter, installPagesRouter } from '../compat/router.cjs';
@@ -62,9 +63,10 @@ async function readPageData(response, limit = MAX_DATA_BYTES, { discard = false,
62
63
  }
63
64
 
64
65
  export async function readPageDataResponse(response, visibleURL, {
65
- maxBytes = MAX_DATA_BYTES, includeBytes = false, fetcher = globalThis.fetch, signal, requestURL = visibleURL,
66
+ maxBytes = MAX_DATA_BYTES, includeBytes = false, deploymentId, fetcher = globalThis.fetch, signal, requestURL = visibleURL,
66
67
  } = {}) {
67
68
  try {
69
+ await assertDeploymentResponse(response, deploymentId);
68
70
  let legacy = false;
69
71
  for (let redirects = 0; response.headers.get('x-prnext-legacy-navigation') === '1'; redirects++) {
70
72
  legacy = true;
@@ -79,7 +81,8 @@ export async function readPageDataResponse(response, visibleURL, {
79
81
  await response.body?.cancel();
80
82
  signal?.throwIfAborted();
81
83
  response = await fetcher(target.href, { credentials: 'same-origin', redirect: 'follow', signal,
82
- headers: { accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1' } });
84
+ headers: { ...deploymentHeaders(deploymentId), accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1' } });
85
+ await assertDeploymentResponse(response, deploymentId);
83
86
  requestURL = target.href;
84
87
  }
85
88
  if (legacy && !response.ok && (response.status < 300 || response.status >= 400)) throw new Error(`Page data request failed (${response.status})`);
@@ -140,9 +143,10 @@ function pagesView(props) {
140
143
  }
141
144
 
142
145
  let application;
143
- async function startPages({ Page, App, pattern, manifestUrl, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, assetBase, strictMode = false }) {
146
+ async function startPages({ deploymentId, Page, App, pattern, manifestUrl, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, assetBase, strictMode = false }) {
144
147
  const data = window.__PRNEXT_DATA__;
145
148
  if (!data?.router || !data.props) throw new Error('Missing PRNext page data');
149
+ if (deploymentId && deploymentId !== data.deploymentId) throw new Error('The document and its JavaScript belong to different deployments.');
146
150
  const rewrite = readRewriteMarker(document);
147
151
  const initial = { ...data, router: { ...data.router, basePath, trailingSlash, skipTrailingSlashRedirect }, Page, App, ...(rewrite ? { rewrite } : {}) };
148
152
  let buildId = data.buildId;
@@ -163,23 +167,32 @@ async function startPages({ Page, App, pattern, manifestUrl, basePath = '', trai
163
167
  if (signal.aborted) cancel();
164
168
  const timer = setTimeout(() => abort.abort(new Error('Page data request timed out')), 30_000);
165
169
  try {
166
- const requestURL = pageDataURL(buildId, url.href, basePath);
170
+ const requestURL = pageDataURL(window.__PRNEXT_STATIC_EXPORT__ && deploymentId ? buildId + '~' + deploymentId : buildId, url.href, basePath);
167
171
  const response = await fetcher(requestURL, {
168
172
  credentials: 'same-origin', redirect: 'manual', signal: abort.signal,
169
- headers: { accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1', ...(speculative ? { purpose: 'prefetch' } : {}) },
173
+ headers: { ...deploymentHeaders(deploymentId), accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1', ...(speculative ? { purpose: 'prefetch' } : {}) },
170
174
  });
171
175
  return await readPageDataResponse(response, url.href, { maxBytes: speculative ? 2 * 1024 * 1024 : MAX_DATA_BYTES,
172
- includeBytes: speculative, fetcher, signal: abort.signal, requestURL });
176
+ deploymentId: window.__PRNEXT_STATIC_EXPORT__ ? undefined : deploymentId, includeBytes: speculative, fetcher, signal: abort.signal, requestURL });
173
177
  } finally { clearTimeout(timer); signal.removeEventListener('abort', cancel); }
174
178
  },
175
179
  });
176
180
  installPagesRouter(controller);
177
181
  let resolveHydration;
178
182
  const hydrated = new Promise(resolve => { resolveHydration = resolve; });
179
- root = hydrateRoot(container, pagesView({ state: initial, strictMode, committed: resolveHydration, reportError: controller.reportRenderError }));
183
+ root = hydrateRoot(container, pagesView({ state: initial, strictMode, committed: resolveHydration, reportError: controller.reportRenderError }), process.env.NODE_ENV === 'development' ? {
184
+ onRecoverableError(error, info) {
185
+ console.error(error);
186
+ globalThis.__PRNEXT_DEV__?.reportRecoverableError(error, info);
187
+ },
188
+ } : undefined);
180
189
  await hydrated;
181
190
  if (data.router.isFallback) void controller.refreshFallback();
182
191
  return { root, router: controller, ...(process.env.NODE_ENV === 'development' ? {
192
+ devSync(options) {
193
+ buildId=options.dev.buildId;window.__PRNEXT_DATA__.buildId=buildId;
194
+ controller.devSetManifest({buildId,manifestUrl:options.manifestUrl});
195
+ },
183
196
  async devCommit(options) {
184
197
  buildId = options.dev.buildId;
185
198
  window.__PRNEXT_DATA__.buildId = buildId;
@@ -192,6 +205,9 @@ export function bootstrapPages(options) {
192
205
  if (process.env.NODE_ENV === 'development' && options.dev && globalThis.__PRNEXT_DEV__) {
193
206
  return globalThis.__PRNEXT_DEV__.bootstrap('pages', options, () => startPages(options));
194
207
  }
195
- application ||= startPages(options);
208
+ if (!application) {
209
+ application = startPages(options);
210
+ void application.catch(error => console.error('[PRNext] Pages startup failed:', error));
211
+ }
196
212
  return application;
197
213
  }
@@ -4,7 +4,10 @@ import { rewriteQuery } from '../compat/rewrite.cjs';
4
4
  import { normalizeBasePath, removeBasePath } from '../compat/paths.cjs';
5
5
  import { publicNavigationURL, isApplicationURL } from './client-navigation.mjs';
6
6
  import DefaultError from '../compat/error.cjs';
7
+ import { createPrefetchScheduler, prefetchCacheVictim } from './prefetch-scheduler.mjs';
7
8
  import { applicationProps, createInitialPropsRunner } from './pages-initial-props.mjs';
9
+ import { matchPagePattern } from './route-pattern.mjs';
10
+ export { matchPagePattern } from './route-pattern.mjs';
8
11
 
9
12
  const PREFETCH_LIMIT = 8;
10
13
  const PREFETCH_TTL = 30_000;
@@ -51,33 +54,6 @@ export function pageRouterSnapshot(router, requestURL, visibleURL, rewrite) {
51
54
  ...(rewrite ? { rewrite } : {}) };
52
55
  }
53
56
 
54
- export function matchPagePattern(pattern, pathname) {
55
- let segments;
56
- try { segments = pathname.replace(/\/+$/, '').split('/').slice(1).map(decodeURIComponent); }
57
- catch { return null; }
58
- if (segments.length === 1 && !segments[0]) segments = [];
59
- if (segments.some(segment => !segment || /[/\\\0]/.test(segment) || segment === '.' || segment === '..')) return null;
60
- const parts = pattern === '/' ? [] : pattern.split('/').slice(1), params = Object.create(null), rank = [];
61
- let index = 0;
62
- for (const part of parts) {
63
- const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(part);
64
- const catchall = /^\[\.\.\.([^\]]+)\]$/.exec(part);
65
- const parameter = /^\[([^\]]+)\]$/.exec(part);
66
- if (optional || catchall) {
67
- if (catchall && index === segments.length) return null;
68
- if (index !== segments.length) params[(optional || catchall)[1]] = segments.slice(index);
69
- index = segments.length; rank.push(optional ? 0 : 1);
70
- } else if (parameter) {
71
- if (index === segments.length) return null;
72
- params[parameter[1]] = segments[index++]; rank.push(2);
73
- } else {
74
- if (segments[index++] !== part) return null;
75
- rank.push(3);
76
- }
77
- }
78
- return index === segments.length ? { params, rank } : null;
79
- }
80
-
81
57
  export function resolvePageRoute(manifest, pathname) {
82
58
  let winner;
83
59
  for (const [routes, pages] of [[manifest.routes, true], [manifest.nonPagesRoutes || [], false]]) {
@@ -129,7 +105,8 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
129
105
  initial = undefined;
130
106
  let manifestPromise, active, renderingError, sequence = 0, beforePop = () => true;
131
107
  let currentKey = win.history.state?.__prnextPages?.key || 0, nextKey = currentKey + 1;
132
- const prefetches = new Map(), queue = []; let prefetchRunning = 0, prefetchBytes = 0;
108
+ const prefetches = new Map(), scheduler = createPrefetchScheduler({ window: win, document: doc });
109
+ let prefetchBytes = 0;
133
110
 
134
111
  function assetURL(value) {
135
112
  const url = new URL(value, win.location.href);
@@ -147,7 +124,7 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
147
124
  const abort = new AbortController(), timer = setTimeout(() => abort.abort(), 30_000);
148
125
  try {
149
126
  const value = JSON.parse(await consume(await fetchAsset(manifestUrl, abort.signal), ASSET_LIMIT));
150
- if (value.buildId !== buildId || !Array.isArray(value.routes) || value.routes.length > 10_000) throw new Error('Invalid Pages manifest');
127
+ if (value.buildId !== buildId || current.deploymentId && value.deploymentId !== current.deploymentId || !Array.isArray(value.routes) || value.routes.length > 10_000) throw new Error('Invalid Pages manifest');
151
128
  for (const route of [...value.routes, ...Object.values(value.errors || {}).filter(Boolean)]) {
152
129
  if (typeof route.pattern !== 'string' || !Array.isArray(route.css)) throw new Error('Invalid page route');
153
130
  assetURL(route.client); for (const css of route.css) assetURL(css);
@@ -278,44 +255,45 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
278
255
  prefetchBytes -= entry.bytes || 0; entry.bytes = 0;
279
256
  if (abort) entry.abort.abort(); else entry.consumed = true;
280
257
  }
281
- function pumpPrefetch() {
282
- while (prefetchRunning < 2 && queue.length) {
283
- const entry = queue.shift();
284
- if (entry.abort.signal.aborted) { entry.resolve(); continue; }
285
- prefetchRunning++;
286
- (async () => {
287
- const map = await abortable(manifest(), entry.abort.signal), matched = resolvePageRoute(map, internalPath(entry.request));
288
- if (!matched?.pages) return;
289
- // Fetch rather than evaluate speculative modules: fetch is cancellable,
290
- // whereas the browser's import() cannot cancel a blocked module request.
291
- await consume(await fetchAsset(matched.route.client, entry.abort.signal), ASSET_LIMIT);
292
- if (entry.abort.signal.aborted) return;
293
- // Warm browser HTTP caches without applying another page's CSS early.
294
- for (const css of matched.route.css) await consume(await fetchAsset(css, entry.abort.signal), ASSET_LIMIT);
295
- if (matched.route.ssg) {
296
- const decoded = await requestData(entry.request, entry.abort.signal, true);
297
- if (entry.abort.signal.aborted || (!prefetches.has(entry.key) && !entry.consumed) || decoded.cacheable === false) return;
298
- entry.data = decoded;
299
- if (entry.consumed) return;
300
- entry.bytes = decoded.bytes || 0; prefetchBytes += entry.bytes;
301
- while (prefetchBytes > PREFETCH_BYTES) dropPrefetch(prefetches.keys().next().value);
302
- }
303
- })().catch(() => { dropPrefetch(entry.key); }).finally(() => { prefetchRunning--; entry.resolve(); pumpPrefetch(); });
258
+ async function runPrefetch(entry) {
259
+ const map = await abortable(manifest(), entry.abort.signal), matched = resolvePageRoute(map, internalPath(entry.request));
260
+ if (!matched?.pages) return;
261
+ // Fetch rather than evaluate speculative modules: fetch can be cancelled,
262
+ // whereas import() can leave an uncancellable module request in flight.
263
+ await consume(await fetchAsset(matched.route.client, entry.abort.signal), ASSET_LIMIT);
264
+ if (entry.abort.signal.aborted) return;
265
+ for (const css of matched.route.css) await consume(await fetchAsset(css, entry.abort.signal), ASSET_LIMIT);
266
+ if (matched.route.ssg) {
267
+ const decoded = await requestData(entry.request, entry.abort.signal, true);
268
+ if (entry.abort.signal.aborted || (prefetches.get(entry.key) !== entry && !entry.consumed) || decoded.cacheable === false) return;
269
+ entry.data = decoded;
270
+ if (entry.consumed) return;
271
+ entry.bytes = decoded.bytes || 0; prefetchBytes += entry.bytes;
272
+ while (prefetchBytes > PREFETCH_BYTES) {
273
+ const victim = prefetchCacheVictim([...prefetches].filter(([, value]) => value.bytes > 0), 1);
274
+ if (victim === undefined) break;
275
+ dropPrefetch(victim);
276
+ }
304
277
  }
305
278
  }
306
- function prefetch(url, as, options) {
279
+ function prefetch(url, as, options = {}) {
307
280
  let request;
308
281
  try { ({ request } = targets(url, as, options)); } catch { return Promise.resolve(); }
309
282
  if (!isApplicationURL(request, win.location.origin, basePath) || process.env.NODE_ENV !== 'production') return Promise.resolve();
310
283
  request.hash = ''; const key = request.href;
311
- if (prefetches.has(key)) return prefetches.get(key).promise;
312
- while (prefetches.size >= PREFETCH_LIMIT) dropPrefetch(prefetches.keys().next().value);
313
- // Queued cancelled entries must not grow with an unbounded list of hovered links.
314
- for (let index = queue.length - 1; index >= 0; index--) if (queue[index].abort.signal.aborted) { queue[index].resolve(); queue.splice(index, 1); }
315
- const entry = { key, request, abort: new AbortController() };
316
- entry.promise = new Promise(resolve => { entry.resolve = resolve; });
284
+ const priority = options.priority === 'viewport' ? 0 : 1;
285
+ const previous = prefetches.get(key);
286
+ if (previous && !previous.abort.signal.aborted) { previous.priority = Math.max(previous.priority, priority); previous.task.promote(options.priority); return previous.promise; }
287
+ if (previous) dropPrefetch(key);
288
+ const victim = prefetches.size >= PREFETCH_LIMIT ? prefetchCacheVictim(prefetches, priority) : undefined;
289
+ if (prefetches.size >= PREFETCH_LIMIT && victim === undefined) return Promise.resolve();
290
+ const entry = { key, request, priority, abort: new AbortController() };
291
+ entry.task = scheduler.schedule(() => runPrefetch(entry), { abort: entry.abort, priority: options.priority });
292
+ if (!entry.task) return Promise.resolve();
293
+ if (victim !== undefined) dropPrefetch(victim);
294
+ entry.promise = entry.task.promise.catch(() => { if (prefetches.get(key) === entry) dropPrefetch(key); });
317
295
  entry.timer = setTimeout(() => dropPrefetch(key), PREFETCH_TTL);
318
- prefetches.set(key, entry); queue.push(entry); pumpPrefetch(); return entry.promise;
296
+ prefetches.set(key, entry); return entry.promise;
319
297
  }
320
298
  function cancelActive() {
321
299
  if (!active) return;
@@ -328,9 +306,13 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
328
306
  async function navigate(url, as, options = {}) {
329
307
  let { request, destination } = targets(url, as, options);
330
308
  if (!isApplicationURL(destination, win.location.origin, basePath) || !isApplicationURL(request, win.location.origin, basePath)) { hard(destination, options.replace); return true; }
309
+ if(process.env.NODE_ENV==='development'&&!options.devRefresh)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(request.href);
331
310
  cancelActive(); renderingError?.abort.abort(); renderingError = undefined; saveScroll();
332
311
  const job = { id: ++sequence, abort: new AbortController(), asPath: destination.pathname + destination.search + destination.hash, shallow: !!options.shallow };
333
312
  active = job;
313
+ const prefetchKey = new URL(request); prefetchKey.hash = '';
314
+ const releaseNavigation = scheduler.navigation(prefetches.get(prefetchKey.href)?.task);
315
+ job.abort.signal.addEventListener('abort', releaseNavigation, { once: true });
334
316
  const timer = setTimeout(() => job.abort.abort(new Error('Page navigation timed out')), 30_000);
335
317
  const hashOnly = destination.pathname === currentURL.pathname && destination.search === currentURL.search &&
336
318
  (destination.hash !== currentURL.hash || !!destination.hash) && !options.shallow;
@@ -458,7 +440,7 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
458
440
  job.finished = true; active = undefined;
459
441
  events.emit('routeChangeError', error, job.asPath, { shallow: job.shallow });
460
442
  hard(destination, options.replace || options.pop); return false;
461
- } finally { clearTimeout(timer); }
443
+ } finally { clearTimeout(timer); job.abort.signal.removeEventListener('abort', releaseNavigation); releaseNavigation(); }
462
444
  }
463
445
  async function reportRenderError(error, failedView) {
464
446
  if (failedView !== current && (failedView.clientSnapshot || failedView.Page !== current.Page || failedView.props !== current.props)) return false;
@@ -549,17 +531,25 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
549
531
  };
550
532
  let frame;
551
533
  const onScroll = () => { if (frame !== undefined) return; frame = win.requestAnimationFrame(() => { frame = undefined; saveScroll(); }); };
552
- const pagehide = () => { saveScroll(); active?.abort.abort(); renderingError?.abort.abort(); for (const key of prefetches.keys()) dropPrefetch(key); };
534
+ const pagehide = () => {
535
+ saveScroll(); cancelActive(); active = undefined;
536
+ renderingError?.abort.abort(); renderingError = undefined;
537
+ for (const key of prefetches.keys()) dropPrefetch(key);
538
+ };
553
539
  win.history.replaceState(historyState(currentURL, currentRequest, currentKey, {}, [win.scrollX, win.scrollY]), '', currentURL.href);
554
540
  win.history.scrollRestoration = 'manual';
555
541
  win.addEventListener('popstate', popstate); win.addEventListener('scroll', onScroll, { passive: true }); win.addEventListener('pagehide', pagehide);
556
542
  return { navigate, prefetch, saveScroll, reportRenderError, snapshot: () => current.router, beforePopState(callback) { beforePop = callback; },
543
+ ...(process.env.NODE_ENV === 'development' ? { devSetManifest(options) {
544
+ buildId=options.buildId;manifestUrl=options.manifestUrl;manifestPromise=undefined;
545
+ for(const key of prefetches.keys())dropPrefetch(key);
546
+ }} : {}),
557
547
  ...(process.env.NODE_ENV === 'development' ? { async devRefresh(options) {
558
548
  buildId = options.buildId; manifestUrl = options.manifestUrl; manifestPromise = undefined;
559
549
  for (const key of prefetches.keys()) dropPrefetch(key);
560
- await navigate(currentRequest, currentURL, { replace: true, scroll: false });
550
+ await navigate(currentRequest, currentURL, { replace: true, scroll: false, devRefresh:true });
561
551
  } } : {}),
562
552
  refreshFallback,
563
- dispose() { pagehide(); win.removeEventListener('popstate', popstate); win.removeEventListener('scroll', onScroll); win.removeEventListener('pagehide', pagehide); if (frame !== undefined) win.cancelAnimationFrame(frame); },
553
+ dispose() { pagehide(); scheduler.dispose(); win.removeEventListener('popstate', popstate); win.removeEventListener('scroll', onScroll); win.removeEventListener('pagehide', pagehide); if (frame !== undefined) win.cancelAnimationFrame(frame); },
564
554
  };
565
555
  }
@@ -19,13 +19,14 @@ export const persistentHelp = `Persistent apps (ordinary prn start stays in the
19
19
  pstart options: --name NAME --port 3000 --hostname 127.0.0.1 --workers 1
20
20
  --profile balanced|speed|memory|classic --inspect
21
21
  --health-path / --health-timeout 15000 --drain-timeout 30000 (milliseconds)
22
+ --rollback-window 15000 (post-switch monitoring; 0 disables, maximum 120000 ms)
22
23
  prestart/pstop/plogs default to the app in the current directory.
23
24
  plogs options: --lines 100 --follow. App environment comes from its .env files.
24
25
  State and logs: ~/.prnext/processes (override with PRNEXT_PM_HOME).
25
26
  `;
26
27
 
27
28
  export function parsePersistent(command, argv) {
28
- const allowed = command === 'pstart' ? ['name', 'port', 'hostname', 'workers', 'profile', 'inspect', 'health-path', 'health-timeout', 'drain-timeout']
29
+ const allowed = command === 'pstart' ? ['name', 'port', 'hostname', 'workers', 'profile', 'inspect', 'health-path', 'health-timeout', 'drain-timeout', 'rollback-window']
29
30
  : command === 'plogs' ? ['follow', 'lines', 'supervisor'] : command === 'pstartup' ? ['remove']
30
31
  : ['pstatus', 'plist'].includes(command) ? ['json'] : ['prestart', 'pstop', 'pdelete'].includes(command) ? ['all'] : [];
31
32
  const flags = new Set(['inspect', 'follow', 'supervisor', 'remove', 'json', 'all']);
@@ -41,9 +42,9 @@ export function parsePersistent(command, argv) {
41
42
  } else { if (target !== undefined) throw new Error(`${command} accepts one ${command === 'pstart' ? 'directory' : 'app name'}`); target = arg; }
42
43
  }
43
44
  if (target && (options.all || options.supervisor || ['pstartup', 'pdown'].includes(command))) throw new Error('Choose an app name or a global option, not both.');
44
- for (const key of ['port', 'workers', 'health-timeout', 'drain-timeout', 'lines']) if (key in options) {
45
- const max = key === 'port' ? 65535 : key === 'workers' ? 64 : key === 'lines' ? 10000 : 60000;
46
- const min = key.endsWith('timeout') ? 100 : 1;
45
+ for (const key of ['port', 'workers', 'health-timeout', 'drain-timeout', 'rollback-window', 'lines']) if (key in options) {
46
+ const max = key === 'port' ? 65535 : key === 'workers' ? 64 : key === 'lines' ? 10000 : key === 'rollback-window' ? 120000 : 60000;
47
+ const min = key === 'rollback-window' ? 0 : key.endsWith('timeout') ? 100 : 1;
47
48
  if (!/^\d+$/.test(options[key]) || Number(options[key]) < min || Number(options[key]) > max) throw new Error(`--${key} must be between ${min} and ${max}`);
48
49
  options[key] = Number(options[key]);
49
50
  }
@@ -156,7 +157,7 @@ export async function persistentCommand(command, argv) {
156
157
  if (command === 'pstart') {
157
158
  const root = await realpath(path.resolve(target || '.'));
158
159
  const name = validName(options.name || path.basename(root).replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^[^a-zA-Z0-9]+/, '').slice(0, 64));
159
- request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] || '/', healthTimeout: options['health-timeout'] || 15000, drainTimeout: options['drain-timeout'] || 30000 } };
160
+ request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] || '/', healthTimeout: options['health-timeout'] || 15000, drainTimeout: options['drain-timeout'] || 30000, rollbackWindow: options['rollback-window'] ?? 15000 } };
160
161
  } else request = { command: ({ prestart: 'restart', pstop: 'stop', pdelete: 'delete' })[command], name: target, all: !!options.all, root: target || options.all ? undefined : await realpath(process.cwd()) };
161
162
  await ensure(connection);
162
163
  const report = await rpc(connection, request);