@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
@@ -0,0 +1,324 @@
1
+ import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
2
+ import {spawn} from 'node:child_process';
3
+ import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
4
+ import {randomUUID} from 'node:crypto';
5
+ import path from 'node:path';
6
+ import {fileURLToPath} from 'node:url';
7
+ import {rename} from './fs.mjs';
8
+ import {watchProject} from './watch-project.mjs';
9
+ import {createDevChangeFilter} from './dev-file-changes.mjs';
10
+ import {shouldWatchProjectFile} from './env.mjs';
11
+ import {privateCacheDirectory,pruneCompilerCache} from '../build/persistent-cache.mjs';
12
+ import {DevGateway} from './dev-gateway.mjs';
13
+ import {withBuildLock} from '../build/build-lock.mjs';
14
+ import {createDevToolsServer} from './dev-tools-server.mjs';
15
+ import {createDevSourceTools} from './dev-tools-source.mjs';
16
+ import {createDevMetricsCollector} from './dev-tools-metrics.mjs';
17
+ import {invalidateDevRouteCache} from './dev-cache-control.mjs';
18
+ import {parseDevLogLine} from './dev-worker-logs.mjs';
19
+ import {createDevRuntimePool} from './dev-runtime-pool.mjs';
20
+ import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
21
+ import {devBuildReason} from './dev-build-reasons.mjs';
22
+ import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
23
+ import {prepareDevNotification} from './dev-notification.mjs';
24
+
25
+ const packageRoot=fileURLToPath(new URL('../',import.meta.url));
26
+ export function developmentRoute(routes,raw,config={}) {
27
+ let pathname=new URL(raw,'http://local').pathname;
28
+ const base=config.basePath||'';
29
+ if(base){if(pathname!==base&&!pathname.startsWith(base+'/'))return;pathname=pathname.slice(base.length)||'/';}
30
+ const data=/^\/(?:_prnext|_next)\/data\/[^/]+\/(.*)\.json$/.exec(pathname);
31
+ if(data){pathname='/'+data[1];if(pathname==='/index')pathname='/';else if(pathname.startsWith('/index/index'))pathname=pathname.slice(6);}
32
+ let parts;try{parts=pathname.replace(/\/$/,'').split('/').slice(1).map(decodeURIComponent);if(parts.length===1&&!parts[0])parts=[];}catch{return;}
33
+ if(config.i18n?.locales.includes(parts[0]))parts.shift();
34
+ const matches=[];
35
+ for(const route of routes){
36
+ if(route.internal)continue;
37
+ const pattern=route.pattern==='/'?[]:route.pattern.split('/').slice(1);let index=0,valid=true;const rank=[];
38
+ for(const part of pattern){
39
+ if(/^\[\[\.\.\./.test(part)){index=parts.length;rank.push(0);break;}
40
+ if(/^\[\.\.\./.test(part)){if(index===parts.length)valid=false;index=parts.length;rank.push(1);break;}
41
+ if(/^\[/.test(part)){if(index>=parts.length)valid=false;rank.push(2);}
42
+ else {if(parts[index]!==part)valid=false;rank.push(3);}
43
+ index++;
44
+ }
45
+ if(valid&&index===parts.length)matches.push({route,rank});
46
+ }
47
+ return matches.sort((a,b)=>{for(let i=0;i<Math.max(a.rank.length,b.rank.length);i++){const difference=(b.rank[i]??4)-(a.rank[i]??4);if(difference)return difference;}return 0;})[0]?.route;
48
+ }
49
+ async function stop(child) {
50
+ if(!child||child.exitCode!==null||child.signalCode)return;
51
+ const compiler=child.connected;
52
+ await new Promise(resolve=>{const timer=setTimeout(()=>child.kill('SIGKILL'),compiler?1000:5000);child.once('exit',()=>{clearTimeout(timer);resolve();});if(child.connected)child.disconnect();if(child.stdin)child.stdin.end();else child.kill('SIGTERM');});
53
+ }
54
+ async function snapshot(source,destination) {
55
+ const files=[];
56
+ async function walk(directory,relative=''){
57
+ await mkdir(path.join(destination,relative),{recursive:true});
58
+ for(const entry of await readdir(directory,{withFileTypes:true})){
59
+ const name=path.join(relative,entry.name),file=path.join(directory,entry.name);
60
+ if(entry.isDirectory())await walk(file,name);
61
+ else if(entry.isFile())files.push(name);
62
+ else throw Error('Development build snapshots cannot contain symbolic links');
63
+ }
64
+ }
65
+ await walk(source);let next=0;
66
+ const results=await Promise.allSettled(Array.from({length:8},async()=>{while(next<files.length){const file=files[next++];try{await link(path.join(source,file),path.join(destination,file));}catch(error){if(!['EXDEV','EPERM','ENOTSUP','EACCES'].includes(error.code))throw error;await copyFile(path.join(source,file),path.join(destination,file));}}}));
67
+ const failure=results.find(result=>result.status==='rejected');if(failure)throw failure.reason;
68
+ }
69
+ export {snapshot as snapshotDevelopmentBuild};
70
+ function serverOptions(args) {
71
+ let hostname='127.0.0.1',port=3000,hostAi=false,profile,safe=false;const backend=[];
72
+ for(let i=0;i<args.length;i++){
73
+ const [flag,inline]=args[i].split('=',2);
74
+ if(['--port','-p','--hostname'].includes(flag)){
75
+ const value=inline??args[++i];if(flag==='--hostname')hostname=value;else if(/^\d+$/.test(value||'')&&Number(value)<=65535)port=Number(value);else throw Error('Invalid development port');
76
+ }else if(flag==='--safe'){if(inline!==undefined)throw Error('Use --safe without a value');safe=true;}
77
+ else if(flag==='--host-ai'&&inline===undefined)hostAi=true;else {if(flag==='--profile')profile=inline??args[i+1];backend.push(args[i]);}
78
+ }
79
+ return {hostname,port,backend,hostAi,profile,safe};
80
+ }
81
+ export async function develop(projectRoot,args,binary) {
82
+ return withBuildLock(projectRoot,root=>runDevelopment(root,args,binary),{development:true});
83
+ }
84
+ async function runDevelopment(projectRoot,args,binary) {
85
+ const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
86
+ const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
87
+ const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
88
+ // The session is ours; never clean a path obtained from application config.
89
+ const container=path.join(root,'.prnext-dev'),info=await lstat(container).catch(error=>{if(error.code!=='ENOENT')throw error;});
90
+ if(info&&(!info.isDirectory()||info.isSymbolicLink()))throw Error('Development directory must be a real directory');
91
+ await mkdir(home,{recursive:true});
92
+ await mkdir(diagnosticsDirectory,{recursive:true,mode:0o700});
93
+ let compiler,gateway,watcher,timer,tools,hostRelay,hostRelayTask,stopping=false,current,discovery,typeChecker,readyState={state:'building'},typeErrors=[],sequence=0;
94
+ let dependencyIndexValid=false;
95
+ const lifecycle=new AbortController();
96
+ let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
97
+ const failedPaths=new Set();
98
+ const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root);
99
+ const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
100
+ if(stopping)throw Error('Development is stopping');
101
+ if(!discovery)await discover();
102
+ const patterns=[...new Set([pattern,...sources])];
103
+ if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))await discover();
104
+ if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))throw Error('Unknown deferred route pattern');
105
+ for(const value of patterns)active.set(value,Date.now());
106
+ if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
107
+ return current.directory;
108
+ })});
109
+ const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
110
+ const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
111
+ routes:(discovery?.project.routes||[]).filter(route=>!route.internal).map(route=>({path:(discovery.config.basePath||'')+(route.pattern==='/'?'/':route.pattern),kind:route.kind,source:route.file?path.relative(root,route.file).replaceAll(path.sep,'/'):undefined,compiled:current?.patterns.has(route.pattern)||false,pinned:[...pins.values()].some(pin=>pin.pattern===route.pattern)})),
112
+ cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
113
+ function captureOutput(child,source,{forwardStdout=true}={}){
114
+ for(const [stream,level] of [[child.stdout,'info'],[child.stderr,'error']]){
115
+ stream?.setEncoding('utf8');
116
+ const emit=(line,newline)=>{const {text,level:reported,...metadata}=parseDevLogLine(line);tools?.addServerLog(reported||level,text,{process:source,buildId:current?.id,...metadata});if(level==='error'||forwardStdout)(level==='error'?process.stderr:process.stdout).write(text+(newline?'\n':''));};
117
+ let pending='';stream?.on('data',chunk=>{pending+=String(chunk);let end;while((end=pending.indexOf('\n'))>=0){emit(pending.slice(0,end),true);pending=pending.slice(end+1);}if(pending.length>32768){emit(pending.slice(0,16000)+' [line truncated]',true);pending='';}});
118
+ child.once('exit',()=>{if(pending)emit(pending,false);});
119
+ }
120
+ }
121
+ function findRoute(raw){
122
+ const key=new URL(raw,'http://local').pathname;
123
+ if(routeCache.has(key))return routeCache.get(key);
124
+ const route=developmentRoute(discovery.project.routes,raw,discovery.config);
125
+ if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
126
+ }
127
+ const writeState=value=>{
128
+ const task=stateQueue.then(async()=>{if(value)readyState=value;const temporary=stateFile+'.'+randomUUID()+'.tmp';
129
+ const {notification,text}=prepareDevNotification({...readyState,safeMode:options.safe,typeErrors});
130
+ try{await writeFile(temporary,text);await rename(temporary,stateFile);if(gateway&&!gateway.stopping)await gateway.request('notify',{value:notification});}finally{await rm(temporary,{force:true});}});
131
+ stateQueue=task.catch(()=>{});return task;
132
+ };
133
+ const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
134
+ async function compilerRequest(type,values={}){
135
+ if(!compiler||compiler.exitCode!==null||compiler.signalCode){
136
+ const cache=await privateCacheDirectory(root,'dev-compiler-v8');
137
+ await pruneCompilerCache(cache);
138
+ compiler=spawn(process.execPath,[path.join(packageRoot,'build/dev-worker.mjs')],{stdio:['pipe','pipe','pipe','ipc'],windowsHide:true,env:{...process.env,NODE_ENV:process.env.NODE_ENV||'development',...(options.safe?{PRNEXT_BUILD_CACHE:'0'}:{}),...(cache?{NODE_COMPILE_CACHE:cache}:{})}});
139
+ captureOutput(compiler,'compiler');
140
+ }
141
+ const child=compiler;
142
+ const result=await new Promise((resolve,reject)=>{
143
+ const clean=()=>{child.off('message',message);child.off('error',error);child.off('exit',exit);};
144
+ const message=value=>{if(value?.type==='result'){clean();resolve(value);}};
145
+ const error=value=>{clean();reject(value);};const exit=()=>error(Error('Development compiler exited'));
146
+ child.on('message',message);child.once('error',error);child.once('exit',exit);child.send({type,root,...values});
147
+ });
148
+ if(result.recycle){compiler=undefined;await stop(child);}
149
+ if(!result.ok)throw Error(result.error);return result;
150
+ }
151
+ async function discover(){
152
+ discovery=(await compilerRequest('discover')).discovery;routeCache.clear();output=discovery.config.distDir;
153
+ if(options.safe)discovery={...discovery,eager:true};
154
+ const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
155
+ await writeTypedRoutes(root,discovery.project,discovery.config);
156
+ if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
157
+ }
158
+ async function launch(manifest,directory){
159
+ const runtime=options.safe?undefined:await runtimePool.take(directory).catch(error=>{tools?.addServerLog('warn','Worker warmup was unavailable; starting a fresh worker.',{process:'runtime'});return undefined;});
160
+ const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
161
+ delete env.PRNEXT_DEV_BROKER;
162
+ if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
163
+ const child=spawn(binary,['start',root,'--build-dir',directory,'--worker',path.join(root,directory,'runtime/worker.mjs'),'--node',process.execPath,'--hostname','127.0.0.1','--port','0','--shutdown-on-stdin-eof',...options.backend],{stdio:['pipe','pipe','pipe'],windowsHide:true,env});
164
+ captureOutput(child,'runtime',{forwardStdout:false});
165
+ let output='';
166
+ const port=await new Promise((resolve,reject)=>{
167
+ const timer=setTimeout(()=>fail(Error('Development backend did not start')),15000);
168
+ const clean=()=>{clearTimeout(timer);child.off('error',fail);child.off('exit',exit);};
169
+ const fail=error=>{clean();reject(error);};const exit=()=>fail(Error('Development backend exited before listening'));
170
+ child.once('error',fail);child.once('exit',exit);child.stdout.on('data',chunk=>{output=(output+chunk).slice(-8192);process.stdout.write(chunk);const match=/http:\/\/127\.0\.0\.1:(\d+)/.exec(output);if(match){clean();resolve(Number(match[1]));}});
171
+ }).catch(async error=>{await stop(child);await runtime?.close();throw error;});
172
+ return {id:manifest.buildId,child,runtime,port,directory};
173
+ }
174
+ async function rebuild(patterns,changedFiles=[],affectedRoutes,affectedModules,cause){
175
+ const startedAt=Date.now(),started=performance.now(),phases={};let mark=started;
176
+ let reason=devBuildReason({initial:!current,safe:options.safe,changed:changedFiles,cause});
177
+ await writeState({state:'building',reason});
178
+ const changed=changedFiles.slice(0,32).map(file=>file.slice(0,256));
179
+ try{
180
+ if(!options.safe)void runtimePool.warm().catch(()=>{});
181
+ if(current&&!discovery.eager&&discovery.cssFast){
182
+ const directory=path.relative(root,path.join(home,'styles-'+randomUUID())).replaceAll(path.sep,'/');
183
+ const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,snapshot,onFallback:code=>{reason=devBuildReason({changed:changedFiles,cause:code});}});
184
+ if(styles){
185
+ await gateway.request('assets',{generation:styles.buildId,directory});
186
+ try{styles.devDependencyIndex=await publishDependencyGraph(gateway,styles);dependencyIndexValid=true;}catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
187
+ sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
188
+ styleGenerations.push({id:styles.buildId,directory});
189
+ reason=devBuildReason({cause:'css-only'});
190
+ await writeState({state:'ready',reason,buildId:styles.buildId,clientManifest:styles.devClient,sequence,session,changed,cssOnly:true,listenerPid:gateway.child.pid,compiler:current.compiler,transforms:current.transforms});
191
+ tools?.recordBuild({buildId:styles.buildId,reason:reason.summary,reasonCode:reason.code,sequence,time:Date.now(),startedAt,duration:performance.now()-started,phases:{styles:performance.now()-started},mode:'css',state:'ready'});
192
+ console.log('Styles updated. '+reason.summary);
193
+ while(styleGenerations.length>3){const old=styleGenerations.shift();await gateway.request('retire',{generation:old.id});await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});}
194
+ await watcher?.refresh();return styles;
195
+ }
196
+ }
197
+ const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
198
+ phases.compile=performance.now()-mark;mark=performance.now();
199
+ if(stopping)throw Error('Development is stopping');
200
+ const consumed=filter.acceptInputs(result.inputs||[]);
201
+ // Config can change after the watch batch was captured but before the
202
+ // compiler reads it. Publish that change once and acknowledge its bytes.
203
+ for(const file of consumed)if(/(?:^|\/)(?:(?:prnext|next)\.config\.|\.env)/.test(file)&&!changed.includes(file))changed.push(file.slice(0,256));
204
+ const manifest=result.manifest;
205
+ const directory=path.relative(root,path.join(home,manifest.buildId)).replaceAll(path.sep,'/');
206
+ await snapshot(manifest.outputDirectory,path.join(root,directory));
207
+ phases.snapshot=performance.now()-mark;mark=performance.now();
208
+ if(stopping)throw Error('Development is stopping');
209
+ let next;try{next=await launch(manifest,directory);phases.launch=performance.now()-mark;mark=performance.now();if(stopping)throw Error('Development is stopping');await gateway.request('activate',{generation:next.id,port:next.port,directory});phases.activate=performance.now()-mark;if(stopping)throw Error('Development is stopping');}catch(error){await stop(next?.child);await rm(path.join(root,directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});throw error;}
210
+ try{manifest.devDependencyIndex=await publishDependencyGraph(gateway,manifest);dependencyIndexValid=true;}
211
+ catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
212
+ const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
213
+ current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
214
+ const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
215
+ current.affectedRoutes=affected;
216
+ await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal&&!route.devDeferred).map(route=>route.pattern)});
217
+ tools?.recordBuild({buildId:manifest.buildId,reason:reason.summary,reasonCode:reason.code,sequence:manifest.devSequence,time:Date.now(),startedAt,duration:performance.now()-started,phases,routes:patterns.length,state:'ready'});
218
+ console.log('Source compiled. '+reason.summary);
219
+ if(previous){
220
+ const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
221
+ }
222
+ while(generations.length>3){const old=generations.shift();
223
+ // A deferred request may itself be awaiting this build. Never wait for
224
+ // its generation lease while holding the serialized compiler queue.
225
+ const cleanup=(async()=>{await gateway.request('retire',{generation:old.id});await old.runtime?.close();await stop(old.child);await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});})().catch(error=>{if(!stopping)console.warn('Development generation cleanup: '+error.message);}).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
226
+ }
227
+ await watcher?.refresh();
228
+ return manifest;
229
+ }catch(error){if(!stopping){tools?.recordBuild({sequence,reason:reason.summary,reasonCode:reason.code,time:Date.now(),startedAt,duration:performance.now()-started,phases,state:'error'});tools?.addServerLog('error',error.message,{process:'compiler'});console.error('Build failed: '+error.message);await writeState({state:'error',reason,error:error.message});}throw error;}
230
+ }
231
+ const updateInfo=()=>({buildId:current.manifest.buildId,clientManifest:current.manifest.devClient,pagesManifest:current.manifest.pagesManifest,sequence:current.manifest.devSequence,session,changed:current.changed,cssOnly:current.cssOnly,affectedRoutes:current.affectedRoutes});
232
+ async function ensure(raw,client,exact=false){
233
+ const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
234
+ const warm=discovery&&resolve();
235
+ if(!stopping&&current&&warm&&active.has(warm.pattern)&&current.patterns.has(warm.pattern)){
236
+ active.set(warm.pattern,Date.now());if(pins.has(client))pins.get(client).pattern=warm.pattern;
237
+ return updateInfo();
238
+ }
239
+ return serialized(async()=>{
240
+ if(!discovery)try{await discover();}catch(error){failedPaths.add(raw);await writeState({state:'error',error:error.message});throw error;}
241
+ let route=resolve();
242
+ // A newly written route can be requested before the watcher's debounce.
243
+ // Resolve that miss against the filesystem instead of returning a stale 404.
244
+ if(!route){await discover();route=resolve();}
245
+ if(route)active.set(route.pattern,Date.now());
246
+ if(route&&pins.has(client))pins.get(client).pattern=route.pattern;
247
+ const max=Math.max(0,Math.min(64,discovery.config.onDemandEntries?.pagesBufferLength??5));
248
+ const pinned=new Set([...pins.values()].map(value=>value.pattern));
249
+ if(route)pinned.add(route.pattern);
250
+ const inactive=[...active].filter(([pattern])=>!pinned.has(pattern)).sort((a,b)=>a[1]-b[1]);
251
+ const age=Math.min(86400000,discovery.config.onDemandEntries?.maxInactiveAge??60000);
252
+ while(inactive.length>max&&(Date.now()-inactive[0][1]>=age||active.size>64+pinned.size))active.delete(inactive.shift()[0]);
253
+ const graphChanged=current&&discovery.project.routes.some(candidate=>!current.manifest.routes.some(built=>built.id===candidate.id));
254
+ if(!current || graphChanged || route&&!current.patterns.has(route.pattern)){
255
+ try{await rebuild([...active.keys()]);}catch(error){if(route){failed.add(route.pattern);active.delete(route.pattern);}throw error;}
256
+ }
257
+ return updateInfo();
258
+ });}
259
+ async function changed(){
260
+ try{
261
+ const pending=[...changes];changes.clear();
262
+ const actual=await filter(pending.filter(file=>file!==output&&!file.startsWith(output+'/')&&!output.startsWith(file+'/')));if(!actual.length)return;
263
+ if(actual.some(file=>/(?:^|\/)(?:package(?:-lock)?\.json|(?:pnpm-lock\.yaml|yarn\.lock)|(?:prnext|next|contentlayer)\.config\.|\.env)/.test(file))){const old=compiler;compiler=undefined;if(old){old.disconnect();await stop(old);}await typeChecker?.close();typeChecker=undefined;typeErrors=[];}
264
+ await discover();
265
+ for(const raw of failedPaths){const route=findRoute(raw);if(route)active.set(route.pattern,Date.now());}
266
+ const valid=new Set(discovery.project.routes.map(route=>route.pattern));for(const pattern of active.keys())if(!valid.has(pattern))active.delete(pattern);
267
+ let affected,modules,cause;
268
+ if(current&&!dependencyIndexValid)cause='dependency-capacity';
269
+ if(current&&dependencyIndexValid&&!discovery.eager&&!failed.size&&!failedPaths.size){
270
+ const result=await affectedDependencies(gateway,actual);
271
+ if(result.unknown.length)cause='unknown-dependency';
272
+ if(!result.complete)cause='dependency-capacity';
273
+ const dormant=new Set(discovery.project.routes.filter(route=>!current.patterns.has(route.pattern)).map(route=>path.relative(root,route.file).replaceAll(path.sep,'/')));
274
+ if(canSkipUnopenedRoutes(result,actual,dormant,dependencyIndexValid)){const skipped=devBuildReason({cause:'outside-active-graph'});tools?.recordBuild({time:Date.now(),duration:0,state:'skipped',reason:skipped.summary,reasonCode:skipped.code,changed:actual});return;}
275
+ if(!result.unknown.length&&result.complete){
276
+ if(result.modules.length)modules=result.modules;
277
+ if(result.routes.length)affected=result.routes;
278
+ }
279
+ }
280
+ if(current||failed.size||failedPaths.size)await rebuild([...new Set([...active.keys(),...failed])],actual,affected,modules,cause);
281
+ }catch(error){if(!stopping&&readyState.state!=='error'){console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});}}
282
+ }
283
+ let closing;
284
+ const shutdown=()=>{
285
+ if(closing)return closing;stopping=true;lifecycle.abort();clearTimeout(timer);watcher?.close();
286
+ closing=(async()=>{
287
+ await Promise.all([gateway?.close(),hostRelay?.close(),hostRelayTask,stop(compiler),typeChecker?.close()]);
288
+ // Keep each native data-cache bridge alive until its externally owned
289
+ // Node process has drained after() work and cache fills on stdin EOF.
290
+ await runtimePool.close();
291
+ await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
292
+ await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
293
+ })();return closing;
294
+ };
295
+ const fatal=error=>{if(!stopping){console.error(error.message);process.exitCode=1;void shutdown();}};
296
+ process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
297
+ try{
298
+ tools=await createDevToolsServer({root,session,snapshot:toolSnapshot,sourceTools:await createDevSourceTools(root),collectMetrics:createDevMetricsCollector(diagnosticsDirectory),ensureRoute:raw=>ensure(raw,'mcp'),invalidateRoute:raw=>serialized(async()=>{
299
+ if(!discovery)await discover();const route=findRoute(raw);if(!route)throw Error('No project route matches this pathname');
300
+ if(!current||current.child.exitCode!==null)return {invalidated:false,scope:'native-route-cache',reason:'No active runtime cache yet. Visit the route first.'};
301
+ const base=discovery.config.basePath||'',logical=base&&(raw===base||raw.startsWith(base+'/'))?raw.slice(base.length)||'/':raw;
302
+ const result=await invalidateDevRouteCache({root,directory:current.directory,session,buildId:current.id,paths:[raw,logical]});
303
+ return {...updateInfo(),...result};
304
+ })});
305
+ await writeState({state:'building',session});
306
+ gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,
307
+ onOpen:message=>{const route=discovery&&findRoute(message.path);if(route)pins.set(message.client,{pattern:route.pattern,connection:message.connection});},
308
+ onClose:message=>{if(pins.get(message.client)?.connection===message.connection)pins.delete(message.client);}});
309
+ const listener=await gateway.ready;
310
+ tools.setAppUrl(`http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port}`);
311
+ if(options.hostAi){
312
+ tools.setHostRelayState({enabled:true,linked:false});
313
+ hostRelayTask=import('./dev-host-relay.mjs').then(({startDevHostRelay})=>startDevHostRelay({root,tools,signal:lifecycle.signal})).then(async relay=>{hostRelay=relay;if(stopping)await relay.close();}).catch(error=>{if(!stopping){tools.setHostRelayState({enabled:true,linked:false,error:error.message});tools.addServerLog('warn','Host AI relay is unavailable. Local development remains running.',{process:'host-relay'});}});
314
+ }
315
+ console.log(`PRNext development · http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port} · on-demand routes`);
316
+ if(options.safe)console.log('Safe mode: full route builds, fresh compilers and no worker warmup.');
317
+ watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:(file,event)=>{
318
+ typeChecker?.invalidate?.(file,event);
319
+ if(stopping||!shouldWatchProjectFile(file))return;changes.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void serialized(changed).catch(()=>{}),75);
320
+ }});
321
+ await serialized(discover).catch(async error=>{console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});});
322
+ await gateway.closed;
323
+ }finally{await shutdown();process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);}
324
+ }
@@ -64,7 +64,7 @@ export async function renderDocument({ Document = DefaultDocument, App = Default
64
64
  throw new TypeError('Document.getInitialProps must return an object with an html string');
65
65
  }
66
66
  if (Buffer.byteLength(initial.html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
67
- const payload = { props, appProps, router: snapshot, buildId, route: { pattern: snapshot.pathname }, dynamicIds: [...dynamicModules] };
67
+ const payload = { props, appProps, router: snapshot, buildId, ...(manifest?.config?.deploymentId ? {deploymentId:manifest.config.deploymentId} : {}), route: { pattern: snapshot.pathname }, dynamicIds: [...dynamicModules] };
68
68
  const assetPrefix = manifest?.config?.assetPrefix || manifest?.config?.basePath || '';
69
69
  const nextData = { props: { ...appProps, pageProps: props }, page: snapshot.pathname, query: snapshot.query, buildId,
70
70
  ...(snapshot.locale ? {locale:snapshot.locale,locales:snapshot.locales,defaultLocale:snapshot.defaultLocale} : {}),
package/runtime/edge.mjs CHANGED
@@ -75,14 +75,15 @@ async function createEdgeVM(reactMode) {
75
75
  export async function loadEdgeModule(code, name, reactMode) {
76
76
  if (reactMode !== undefined && reactMode !== 'rsc' && reactMode !== 'ssr') throw new Error('Unknown Edge React mode');
77
77
  let pending;
78
- if (reactMode) {
79
- pending = reactVMs.get(reactMode);
78
+ {
79
+ const key=reactMode||'handler';
80
+ pending = reactVMs.get(key);
80
81
  if (!pending) {
81
82
  pending = createEdgeVM(reactMode);
82
- reactVMs.set(reactMode, pending);
83
- void pending.catch(() => { if (reactVMs.get(reactMode) === pending) reactVMs.delete(reactMode); });
83
+ reactVMs.set(key, pending);
84
+ void pending.catch(() => { if (reactVMs.get(key) === pending) reactVMs.delete(key); });
84
85
  }
85
- } else pending = createEdgeVM();
86
+ }
86
87
  // This runtime module belongs to one compiled build. Its two React realms
87
88
  // are shared lazily by all pages/boundaries; each bundle has its own factory.
88
89
  const vm = await pending;
package/runtime/env.mjs CHANGED
@@ -24,7 +24,7 @@ export function envFileNames({ dev = false, env = process.env, mode = env.NODE_E
24
24
  }
25
25
 
26
26
  /** Load root env files in Next's precedence order, without logging values. */
27
- export function loadEnvConfig(projectRoot, { dev = false, env = process.env, mode } = {}) {
27
+ export function loadEnvConfig(projectRoot, { dev = false, env = process.env, mode, inputs } = {}) {
28
28
  for (const [key, value] of loadedValues.get(env) || []) {
29
29
  if (env[key] === value) delete env[key];
30
30
  }
@@ -38,6 +38,7 @@ export function loadEnvConfig(projectRoot, { dev = false, env = process.env, mod
38
38
  try {
39
39
  if (!statSync(filename).isFile()) continue;
40
40
  contents = readFileSync(filename, 'utf8');
41
+ inputs?.record(filename,contents);
41
42
  } catch (error) {
42
43
  if (error.code === 'ENOENT') continue;
43
44
  throw new Error(`Could not load ${filename}: ${error.code || error.message}`, { cause: error });
@@ -1,3 +1,4 @@
1
+ import {withTrace,propagatedFetch} from './trace.mjs';
1
2
  import { createHash } from 'node:crypto';
2
3
  import cache from '../compat/data-cache.cjs';
3
4
  import { dynamicUsage, trackStaticDependency } from '../compat/static-generation.cjs';
@@ -12,6 +13,7 @@ const MAX_MEMO_BYTES = 8 * 1024 * 1024;
12
13
  const CAPTURE_IDLE_MS = 1000;
13
14
  const CAPTURE_LIFETIME_MS = 20_000;
14
15
  let registration;
16
+ const telemetryFetch=(input,init)=>nativeFetch(...propagatedFetch(input,init));
15
17
 
16
18
  class UncacheableResponse extends Error {
17
19
  constructor(response) { super('The fetch response is not cacheable'); this.response = response; }
@@ -305,24 +307,24 @@ export function installFetchCache() {
305
307
  }, error => { diagnostics.record('fetch', { ...fields, error: true, durationMs: performance.now() - started }); throw error; });
306
308
  }
307
309
  }
308
- return nativeFetch(input, init);
310
+ return telemetryFetch(input, init);
309
311
  };
310
312
  Object.defineProperty(fetch, PATCHED, { value: dispatcher });
311
313
  globalThis.fetch = fetch;
312
314
  }
313
315
  if (registration && dispatcher.handlers.includes(registration)) return globalThis.fetch;
314
316
  async function fetch(input, init, context) {
315
- if (context.phase === 'pages' || init?.next?.internal) return nativeFetch(input, init);
317
+ if (context.phase === 'pages' || init?.next?.internal) return telemetryFetch(input, init);
316
318
  if (inCacheScope()) {
317
319
  const next = init?.next || input?.next || {};
318
320
  if (next.revalidate !== undefined && next.revalidate !== false && (typeof next.revalidate !== 'number' || Number.isNaN(next.revalidate) || next.revalidate < 0)) throw new TypeError('fetch next.revalidate must be a nonnegative number or false');
319
321
  recordCacheDependency(next);
320
- return nativeFetch(input, init);
322
+ return telemetryFetch(input, init);
321
323
  }
322
324
  const policy = fetchPolicy(input, init, context);
323
325
  if (context.staticState) trackStaticDependency({ tags: policy.tags, paths: getCachePaths(context),
324
326
  revalidate: context.cacheConfig?.dynamic === 'force-static' && policy.revalidate === 0 ? null : policy.revalidate }, context);
325
- if (init?.dispatcher || init?.agent) return nativeFetch(input, init);
327
+ if (init?.dispatcher || init?.agent) return telemetryFetch(input, init);
326
328
  const state = cacheState(context);
327
329
  const explicitSignal = init?.signal !== undefined || input instanceof Request;
328
330
  const method = String(init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
@@ -331,9 +333,9 @@ export function installFetchCache() {
331
333
  const persist = policy.persist && !hardRefresh && !context.draftMode && !context.cacheConfig?.forceNoStore;
332
334
  if (diagnostics.enabled && !persist) diagnostics.record('cache', { route: diagnostics.route(context), state: 'bypass',
333
335
  reason: context.draftMode ? 'draft mode' : hardRefresh ? 'development hard refresh' : context.cacheConfig?.forceNoStore ? 'route disables caching' : 'fetch policy does not opt into persistent caching' });
334
- if (!persist && !memoize) return nativeFetch(input, init);
336
+ if (!persist && !memoize) return telemetryFetch(input, init);
335
337
  const prepared = await prepareRequest(input, init);
336
- if (!prepared) return nativeFetch(input, init);
338
+ if (!prepared) return telemetryFetch(input, init);
337
339
  const request = prepared.request;
338
340
  request.signal.throwIfAborted();
339
341
  const memo = memoize ? memoState(state) : null;
@@ -361,7 +363,7 @@ export function installFetchCache() {
361
363
  entry = undefined;
362
364
  }
363
365
  const producer = async ({ background = false, signal, cache: canPersist = true } = {}) => {
364
- const response = await nativeFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
366
+ const response = await telemetryFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
365
367
  // Set-Cookie responses must not replay a session mutation, even when the
366
368
  // request itself explicitly opted into shared caching.
367
369
  if (response.headers.has('set-cookie') || (persist && response.status !== 200) || (!canPersist && !entry)) throw new UncacheableResponse(response);
@@ -384,7 +386,7 @@ export function installFetchCache() {
384
386
  throw error;
385
387
  }
386
388
  }
387
- registration = { context: optionalRequestContext, fetch };
389
+ registration = { context: optionalRequestContext, fetch:(input,init,context)=>withTrace({kind:3},'prnext.fetch',()=>fetch(input,init,context)) };
388
390
  dispatcher.handlers.push(registration);
389
391
  return globalThis.fetch;
390
392
  }
@@ -0,0 +1,50 @@
1
+ import { randomBytes, createHmac } from 'node:crypto';
2
+ import { deflateRawSync } from 'node:zlib';
3
+ import { TYPE, MAX_VALUE, collectStream, deltaStream } from '../compat/flight-delta.cjs';
4
+
5
+ export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now}={}) {
6
+ const entries=new Map();let size=0,captures=0,encoders=0;
7
+ function reserve(kind){if(kind==='capture'?captures>=maxCaptures:encoders>=maxEncoders)return;kind==='capture'?captures++:encoders++;let released=false;return()=>{if(released)return;released=true;kind==='capture'?captures--:encoders--;};}
8
+ function remove(token){const entry=entries.get(token);if(entry){size-=entry.bytes.length;entries.delete(token);}}
9
+ function prune(){for(const[token,entry]of entries)if(entry.expires<=now())remove(token);while(size>maxBytes||entries.size>maxEntries)remove(entries.keys().next().value);}
10
+ return {
11
+ get(token,identity){prune();const entry=entries.get(token);if(entry?.identity!==identity)return;return entry?.bytes;},
12
+ set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune();},
13
+ capture:()=>reserve('capture'),encoder:()=>reserve('encoder'),get inFlight(){return {captures,encoders};},
14
+ get bytes(){return size;},get size(){return entries.size;},
15
+ };
16
+ }
17
+ const store=createFlightStore();
18
+ export function compressedDeltaPolicy(acceptEncoding='') {
19
+ const compressed=acceptEncoding.split(',').some(value=>{
20
+ const [name,...parameters]=value.trim().toLowerCase().split(';');
21
+ return /^(gzip|deflate|br|zstd|\*)$/.test(name)&&!parameters.some(p=>/^\s*q\s*=\s*0(?:\.0*)?\s*$/.test(p));
22
+ });
23
+ if(!compressed)return;
24
+ // This conservative, bounded probe is not the response compressor. It avoids
25
+ // choosing thousands of references to JSON that gzip already compresses well.
26
+ // A large/expensive chunk keeps ordinary Flight instead of delaying a flush.
27
+ return (raw,encoded)=>raw.length<=64*1024&&encoded.length+32<raw.length&&
28
+ deflateRawSync(encoded,{level:1}).length+32<deflateRawSync(raw,{level:1}).length*0.9;
29
+ }
30
+ export async function differentialFlight(response,options,cache=store) {
31
+ const headers=new Headers(options.headers), outgoing=new Headers(response.headers);
32
+ if((options.method||'GET')!=='GET'||headers.get('rsc')!=='1'||headers.get('x-prnext-flight-accept')!=='v1'||headers.get('x-prnext-prefetch')==='1'||response.status!==200||!outgoing.get('content-type')?.startsWith('text/x-component'))return response;
33
+ const secret=options.manifest?.app?.actionKey;if(!secret)return response;
34
+ const identity=createHmac('sha256',secret).update(JSON.stringify([options.manifest.cacheId,headers.get('cookie')||'',headers.get('authorization')||''])).digest('hex');
35
+ const releaseCapture=cache.capture(),token=releaseCapture?randomBytes(24).toString('hex'):undefined,previous=headers.get('x-prnext-flight-base'),candidate=cache.get(previous,identity),releaseEncoder=candidate&&cache.encoder(),base=releaseEncoder?candidate:undefined;
36
+ let encoded;
37
+ try{
38
+ const raw=response.body;
39
+ const body=raw instanceof ReadableStream?raw:raw?.[Symbol.asyncIterator]?ReadableStream.from(raw):new ReadableStream({start(controller){
40
+ controller.enqueue(raw instanceof Uint8Array?raw:raw instanceof ArrayBuffer?new Uint8Array(raw):new TextEncoder().encode(String(raw??'')));controller.close();
41
+ }});
42
+ const collected=collectStream(body,{capture:!!releaseCapture,onComplete:bytes=>cache.set(token,identity,bytes),onFinally:releaseCapture});
43
+ encoded=base?await deltaStream(collected,base,{assess:compressedDeltaPolicy(headers.get('accept-encoding')||'')}):{delta:false,body:collected};
44
+ if(!encoded.delta)releaseEncoder?.();
45
+ else encoded.body=collectStream(encoded.body,{capture:false,onFinally:releaseEncoder});
46
+ }catch(error){releaseEncoder?.();releaseCapture?.();throw error;}
47
+ if(token)outgoing.set('x-prnext-flight-token',token);else outgoing.delete('x-prnext-flight-token');outgoing.set('cache-control','private, no-store');outgoing.delete('content-length');
48
+ if(encoded.delta){outgoing.set('content-type',TYPE);outgoing.set('x-prnext-flight-base',previous);}
49
+ return {...response,headers:Object.fromEntries(outgoing),body:encoded.body,cancel:reason=>{releaseEncoder?.();releaseCapture?.();void encoded.body.cancel(reason).catch(()=>{});return response.cancel?.(reason);}};
50
+ }
@@ -0,0 +1,28 @@
1
+ // Share Flight allocations with the SSR decoder, then group only already
2
+ // available bootstrap bytes. Never wait for a future Suspense chunk to fill a
3
+ // block. The queue's pre-shell ceiling remains the overall response budget.
4
+ export function flightTap(source) {
5
+ const reader=source.getReader(),queue=[];
6
+ let head=0,bytes=0,done=false,failure,outputStarted=false,wakeData,wakeSpace,cancelled;
7
+ const finish=error=>{done=true;failure=error;wakeData?.();wakeData=undefined;wakeSpace?.();wakeSpace=undefined;};
8
+ const cancel=reason=>cancelled??=(async()=>{finish(reason);queue.length=0;head=0;bytes=0;await reader.cancel(reason).catch(()=>{});})();
9
+ const body=new ReadableStream({
10
+ async pull(controller){try{
11
+ while(!done&&outputStarted&&bytes>=128*1024)await new Promise(resolve=>{wakeSpace=resolve;});
12
+ if(done){if(failure)controller.error(failure);else controller.close();return;}
13
+ const next=await reader.read();if(next.done){finish();controller.close();return;}
14
+ queue.push(next.value);bytes+=next.value.byteLength;wakeData?.();wakeData=undefined;controller.enqueue(next.value);
15
+ }catch(error){finish(error);controller.error(error);}},cancel,
16
+ },{highWaterMark:0});
17
+ return {body,cancel,startOutput(){outputStarted=true;},async next(){
18
+ while(head===queue.length&&!done)await new Promise(resolve=>{wakeData=resolve;});
19
+ if(head<queue.length){
20
+ const parts=[];let size=0;
21
+ do{const value=queue[head];queue[head++]=undefined;parts.push(value);size+=value.byteLength;}while(head<queue.length&&size+queue[head].byteLength<=32*1024);
22
+ bytes-=size;if(head===queue.length){queue.length=0;head=0;}else if(head>=64){queue.splice(0,head);head=0;}
23
+ wakeSpace?.();wakeSpace=undefined;
24
+ return {value:parts.length===1?parts[0]:Buffer.concat(parts,size),done:false};
25
+ }
26
+ if(failure)throw failure;return {done:true};
27
+ }};
28
+ }
@@ -0,0 +1,27 @@
1
+ /** Reject executable markup instead of silently stripping author content or
2
+ * React reveal instructions. HTML-only documents must already be complete. */
3
+ export function assertHtmlOnlyDocument(html, pattern = '') {
4
+ const fail = reason => { throw new Error(`htmlOnly route ${pattern} emitted ${reason}. Use static HTML elements without JavaScript.`); };
5
+ if (typeof html !== 'string') fail('an invalid HTML document');
6
+ if (/<\s*(?:script|iframe|object|embed)(?=[\s/>])/i.test(html)) fail('a script or embedded browsing context');
7
+ const tags = /<[a-z][^<>"']*(?:(?:"[^"]*"|'[^']*')[^<>"']*)*>/gi;
8
+ const attributes = /([^\s/>=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
9
+ const decode = value => value.replace(/&#(?:x([\da-f]+)|(\d+));?/gi, (_, hex, decimal) => String.fromCodePoint(Math.min(0x10ffff, parseInt(hex || decimal, hex ? 16 : 10)))).replace(/&(?:colon|tab|newline);/gi, entity => ({'&colon;':':','&tab;':'\t','&newline;':'\n'})[entity.toLowerCase()]);
10
+ for (const match of html.matchAll(tags)) {
11
+ const tag = match[0], start = /^<[\w:-]+/.exec(tag)[0].length;
12
+ const values = new Map();
13
+ for (const attribute of tag.slice(start).matchAll(attributes)) {
14
+ const name = attribute[1].toLowerCase(), value = decode(attribute[2] ?? attribute[3] ?? attribute[4] ?? '');
15
+ // HTML ignores later duplicate attributes; preserve the same first value
16
+ // when interpreting link token lists and resource destinations.
17
+ if (!values.has(name)) values.set(name,value);
18
+ if (/^on[a-z]/.test(name) || name === 'srcdoc') fail(`interactive attribute ${name}`);
19
+ if (name === 'name' && value.startsWith('$ACTION_')) fail('a Server Action form');
20
+ if (['href','src','action','formaction','xlink:href'].includes(name)) {
21
+ if (/^javascript:/i.test(value.replace(/[\u0000-\u0020]/g, ''))) fail('a JavaScript URL');
22
+ }
23
+ }
24
+ if (/^<link\b/i.test(tag) && ((values.get('rel') || '').toLowerCase().split(/[\t\n\f\r ]+/).includes('modulepreload') || (values.get('as') || '').trim().toLowerCase() === 'script')) fail('a JavaScript preload');
25
+ }
26
+ return html;
27
+ }
package/runtime/http.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import {reportRequestError} from './instrumentation.mjs';
1
2
  import { createRequire } from 'node:module';
2
3
  // ESM node:http evaluates lazy WebSocket exports and initializes Undici.
3
4
  // Use the module object to avoid that startup cost while preserving HTTP patches.
@@ -186,6 +187,7 @@ export function plainResponse(status, message) {
186
187
  }
187
188
 
188
189
  export function errorResponse(error, production = process.env.NODE_ENV === 'production') {
190
+ reportRequestError(error);
189
191
  console.error('[prnext]', error?.stack || error);
190
192
  const status = [400, 503, 504].includes(error?.statusCode) ? error.statusCode : 500;
191
193
  const message = status === 400 ? 'Bad Request' : status === 503 ? 'Service Unavailable' : status === 504 ? 'Gateway Timeout' : 'Internal Server Error';