@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.
- package/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +100 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- 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
|
|
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 :
|
|
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
|
|
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
|
+
}
|
package/runtime/navigation.mjs
CHANGED
|
@@ -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';
|
package/runtime/pages-client.mjs
CHANGED
|
@@ -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
|
-
|
|
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(),
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
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);
|
|
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 = () => {
|
|
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);
|