@thomas.f/prnext 0.1.16-alpha → 0.1.17-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 (126) hide show
  1. package/AI.md +63 -0
  2. package/README.md +384 -9
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +5 -0
  5. package/build/analyze.mjs +184 -0
  6. package/build/app-config.mjs +7 -8
  7. package/build/app-static.mjs +12 -7
  8. package/build/app.mjs +23 -10
  9. package/build/build-lock.mjs +6 -6
  10. package/build/bundle-runtime.mjs +20 -5
  11. package/build/check.mjs +56 -11
  12. package/build/compiler.mjs +25 -1
  13. package/build/concurrent.mjs +12 -0
  14. package/build/config.mjs +21 -13
  15. package/build/css-modules.mjs +15 -4
  16. package/build/dev-compiler-health.mjs +11 -0
  17. package/build/dev-css-update.mjs +83 -0
  18. package/build/dev-dependencies.mjs +44 -0
  19. package/build/dev-discovery.mjs +28 -0
  20. package/build/dev-worker.mjs +14 -5
  21. package/build/dev.mjs +55 -4
  22. package/build/edge.mjs +3 -2
  23. package/build/framework-imports.mjs +40 -0
  24. package/build/incremental.mjs +8 -4
  25. package/build/index.mjs +96 -101
  26. package/build/inspect.mjs +3 -0
  27. package/build/inspection-summary.mjs +37 -4
  28. package/build/loader-cache.mjs +53 -26
  29. package/build/metadata-routes.mjs +2 -3
  30. package/build/migrate.mjs +60 -57
  31. package/build/npm-packages.mjs +3 -18
  32. package/build/package-manager.mjs +308 -0
  33. package/build/persistent-cache.mjs +97 -0
  34. package/build/precompress.mjs +62 -30
  35. package/build/prerender-pages.mjs +90 -0
  36. package/build/prerender-worker.mjs +10 -0
  37. package/build/production-builder.mjs +80 -0
  38. package/build/scan.mjs +4 -4
  39. package/build/source-analysis.mjs +42 -0
  40. package/build/static-adapter.mjs +13 -5
  41. package/build/static-input.mjs +48 -0
  42. package/build/styles.mjs +15 -3
  43. package/build/transform-cache.mjs +37 -4
  44. package/build/type-command.mjs +41 -0
  45. package/build/typed-routes.mjs +91 -0
  46. package/build/typescript-dev-worker.mjs +18 -0
  47. package/build/typescript-dev.mjs +22 -0
  48. package/build/typescript-watch.mjs +22 -0
  49. package/build/typescript.mjs +5 -4
  50. package/build/webpack-graph.mjs +164 -27
  51. package/build/worker-preload.mjs +9 -0
  52. package/cli.mjs +105 -121
  53. package/compat/index.d.ts +21 -7
  54. package/compat/link.d.cts +4 -3
  55. package/compat/navigation.d.cts +4 -3
  56. package/compat/router.d.cts +4 -3
  57. package/compat/typed-route.d.cts +17 -0
  58. package/docs/ai/compatibility.md +106 -0
  59. package/docs/ai/data.md +128 -0
  60. package/docs/ai/deployment.md +111 -0
  61. package/docs/ai/manifest.json +11 -0
  62. package/docs/ai/quickstart.md +146 -0
  63. package/docs/ai/routing.md +132 -0
  64. package/docs/ai/troubleshooting.md +120 -0
  65. package/index.mjs +2 -0
  66. package/native/diagnostics.mjs +34 -0
  67. package/native/resolve.mjs +7 -2
  68. package/package.json +25 -7
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +2 -0
  71. package/runtime/ai-docs.mjs +202 -0
  72. package/runtime/app-client.mjs +17 -6
  73. package/runtime/app-render.mjs +12 -0
  74. package/runtime/cloud/archive.mjs +5 -6
  75. package/runtime/compile-cache.mjs +8 -0
  76. package/runtime/dev-ai-brands.mjs +26 -0
  77. package/runtime/dev-ai-style.mjs +99 -0
  78. package/runtime/dev-build-reasons.mjs +25 -0
  79. package/runtime/dev-cache-control.mjs +53 -0
  80. package/runtime/dev-client.mjs +239 -45
  81. package/runtime/dev-console-copy.mjs +19 -0
  82. package/runtime/dev-console.mjs +106 -0
  83. package/runtime/dev-dependency-graph.mjs +40 -0
  84. package/runtime/dev-element-picker-style.mjs +27 -0
  85. package/runtime/dev-element-picker.mjs +213 -0
  86. package/runtime/dev-error-overlay-style.mjs +41 -0
  87. package/runtime/dev-error-overlay.mjs +167 -0
  88. package/runtime/dev-error-report.mjs +121 -0
  89. package/runtime/dev-file-changes.mjs +1 -1
  90. package/runtime/dev-gateway.mjs +44 -0
  91. package/runtime/dev-host-relay.mjs +66 -0
  92. package/runtime/dev-indicator.mjs +186 -0
  93. package/runtime/dev-issue-motion.mjs +214 -0
  94. package/runtime/dev-lazy-tool.mjs +18 -0
  95. package/runtime/dev-logo.mjs +2 -0
  96. package/runtime/dev-notification.mjs +81 -0
  97. package/runtime/dev-popout.mjs +34 -0
  98. package/runtime/dev-position.mjs +107 -0
  99. package/runtime/dev-preview-touch.mjs +122 -0
  100. package/runtime/dev-runtime-pool.mjs +45 -0
  101. package/runtime/dev-runtime-spare.mjs +57 -0
  102. package/runtime/dev-tools-client.mjs +155 -0
  103. package/runtime/dev-tools-mcp.mjs +126 -0
  104. package/runtime/dev-tools-metrics.mjs +31 -0
  105. package/runtime/dev-tools-model.mjs +42 -0
  106. package/runtime/dev-tools-panel.mjs +341 -0
  107. package/runtime/dev-tools-result.mjs +37 -0
  108. package/runtime/dev-tools-server.mjs +256 -0
  109. package/runtime/dev-tools-source.mjs +104 -0
  110. package/runtime/dev-tooltip.mjs +94 -0
  111. package/runtime/dev-ui-icons.mjs +8 -0
  112. package/runtime/dev-workbench-style.mjs +106 -0
  113. package/runtime/dev-workbench.mjs +428 -0
  114. package/runtime/dev-worker-logs.mjs +31 -0
  115. package/runtime/development.mjs +307 -0
  116. package/runtime/env.mjs +2 -1
  117. package/runtime/pages-client.mjs +10 -1
  118. package/runtime/pages-navigation.mjs +6 -1
  119. package/runtime/persistent/client.mjs +6 -5
  120. package/runtime/persistent/daemon.mjs +66 -56
  121. package/runtime/persistent/native-proxy.mjs +63 -0
  122. package/runtime/preload.mjs +12 -0
  123. package/runtime/rsc-worker.mjs +7 -0
  124. package/runtime/typed-route.mjs +19 -0
  125. package/runtime/watch-project.mjs +12 -5
  126. package/runtime/worker.mjs +36 -2
@@ -0,0 +1,307 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
3
+ import {randomUUID} from 'node:crypto';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {rename} from './fs.mjs';
7
+ import {watchProject} from './watch-project.mjs';
8
+ import {createDevChangeFilter} from './dev-file-changes.mjs';
9
+ import {shouldWatchProjectFile} from './env.mjs';
10
+ import {privateCacheDirectory,pruneCompilerCache} from '../build/persistent-cache.mjs';
11
+ import {DevGateway} from './dev-gateway.mjs';
12
+ import {withBuildLock} from '../build/build-lock.mjs';
13
+ import {createDevToolsServer} from './dev-tools-server.mjs';
14
+ import {createDevSourceTools} from './dev-tools-source.mjs';
15
+ import {createDevMetricsCollector} from './dev-tools-metrics.mjs';
16
+ import {invalidateDevRouteCache} from './dev-cache-control.mjs';
17
+ import {parseDevLogLine} from './dev-worker-logs.mjs';
18
+ import {createDevRuntimePool} from './dev-runtime-pool.mjs';
19
+ import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
20
+ import {devBuildReason} from './dev-build-reasons.mjs';
21
+ import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
22
+ import {prepareDevNotification} from './dev-notification.mjs';
23
+
24
+ const packageRoot=fileURLToPath(new URL('../',import.meta.url));
25
+ export function developmentRoute(routes,raw,config={}) {
26
+ let pathname=new URL(raw,'http://local').pathname;
27
+ const base=config.basePath||'';
28
+ if(base){if(pathname!==base&&!pathname.startsWith(base+'/'))return;pathname=pathname.slice(base.length)||'/';}
29
+ const data=/^\/(?:_prnext|_next)\/data\/[^/]+\/(.*)\.json$/.exec(pathname);
30
+ if(data){pathname='/'+data[1];if(pathname==='/index')pathname='/';else if(pathname.startsWith('/index/index'))pathname=pathname.slice(6);}
31
+ let parts;try{parts=pathname.replace(/\/$/,'').split('/').slice(1).map(decodeURIComponent);if(parts.length===1&&!parts[0])parts=[];}catch{return;}
32
+ if(config.i18n?.locales.includes(parts[0]))parts.shift();
33
+ const matches=[];
34
+ for(const route of routes){
35
+ if(route.internal)continue;
36
+ const pattern=route.pattern==='/'?[]:route.pattern.split('/').slice(1);let index=0,valid=true;const rank=[];
37
+ for(const part of pattern){
38
+ if(/^\[\[\.\.\./.test(part)){index=parts.length;rank.push(0);break;}
39
+ if(/^\[\.\.\./.test(part)){if(index===parts.length)valid=false;index=parts.length;rank.push(1);break;}
40
+ if(/^\[/.test(part)){if(index>=parts.length)valid=false;rank.push(2);}
41
+ else {if(parts[index]!==part)valid=false;rank.push(3);}
42
+ index++;
43
+ }
44
+ if(valid&&index===parts.length)matches.push({route,rank});
45
+ }
46
+ 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;
47
+ }
48
+ async function stop(child) {
49
+ if(!child||child.exitCode!==null||child.signalCode)return;
50
+ const compiler=child.connected;
51
+ 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');});
52
+ }
53
+ async function snapshot(source,destination) {
54
+ const files=[];
55
+ async function walk(directory,relative=''){
56
+ await mkdir(path.join(destination,relative),{recursive:true});
57
+ for(const entry of await readdir(directory,{withFileTypes:true})){
58
+ const name=path.join(relative,entry.name),file=path.join(directory,entry.name);
59
+ if(entry.isDirectory())await walk(file,name);
60
+ else if(entry.isFile())files.push(name);
61
+ else throw Error('Development build snapshots cannot contain symbolic links');
62
+ }
63
+ }
64
+ await walk(source);let next=0;
65
+ 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));}}}));
66
+ const failure=results.find(result=>result.status==='rejected');if(failure)throw failure.reason;
67
+ }
68
+ export {snapshot as snapshotDevelopmentBuild};
69
+ function serverOptions(args) {
70
+ let hostname='127.0.0.1',port=3000,hostAi=false,profile,safe=false;const backend=[];
71
+ for(let i=0;i<args.length;i++){
72
+ const [flag,inline]=args[i].split('=',2);
73
+ if(['--port','-p','--hostname'].includes(flag)){
74
+ 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');
75
+ }else if(flag==='--safe'){if(inline!==undefined)throw Error('Use --safe without a value');safe=true;}
76
+ else if(flag==='--host-ai'&&inline===undefined)hostAi=true;else {if(flag==='--profile')profile=inline??args[i+1];backend.push(args[i]);}
77
+ }
78
+ return {hostname,port,backend,hostAi,profile,safe};
79
+ }
80
+ export async function develop(projectRoot,args,binary) {
81
+ return withBuildLock(projectRoot,root=>runDevelopment(root,args,binary),{development:true});
82
+ }
83
+ async function runDevelopment(projectRoot,args,binary) {
84
+ const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
85
+ const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
86
+ const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
87
+ // The session is ours; never clean a path obtained from application config.
88
+ const container=path.join(root,'.prnext-dev'),info=await lstat(container).catch(error=>{if(error.code!=='ENOENT')throw error;});
89
+ if(info&&(!info.isDirectory()||info.isSymbolicLink()))throw Error('Development directory must be a real directory');
90
+ await mkdir(home,{recursive:true});
91
+ await mkdir(diagnosticsDirectory,{recursive:true,mode:0o700});
92
+ let compiler,gateway,watcher,timer,tools,hostRelay,hostRelayTask,stopping=false,current,discovery,typeChecker,readyState={state:'building'},typeErrors=[],sequence=0;
93
+ let dependencyIndexValid=false;
94
+ const lifecycle=new AbortController();
95
+ let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
96
+ const failedPaths=new Set();
97
+ const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root);
98
+ const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
99
+ const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
100
+ routes:(discovery?.project.routes||[]).filter(route=>!route.internal).map(route=>({path:(discovery.config.basePath||'')+(route.pattern==='/'?'/':route.pattern),kind:route.kind,source:route.file?path.relative(root,route.file).replaceAll(path.sep,'/'):undefined,compiled:current?.patterns.has(route.pattern)||false,pinned:[...pins.values()].some(pin=>pin.pattern===route.pattern)})),
101
+ 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}}});
102
+ function captureOutput(child,source,{forwardStdout=true}={}){
103
+ for(const [stream,level] of [[child.stdout,'info'],[child.stderr,'error']]){
104
+ stream?.setEncoding('utf8');
105
+ 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':''));};
106
+ 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='';}});
107
+ child.once('exit',()=>{if(pending)emit(pending,false);});
108
+ }
109
+ }
110
+ function findRoute(raw){
111
+ const key=new URL(raw,'http://local').pathname;
112
+ if(routeCache.has(key))return routeCache.get(key);
113
+ const route=developmentRoute(discovery.project.routes,raw,discovery.config);
114
+ if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
115
+ }
116
+ const writeState=value=>{
117
+ const task=stateQueue.then(async()=>{if(value)readyState=value;const temporary=stateFile+'.'+randomUUID()+'.tmp';
118
+ const {notification,text}=prepareDevNotification({...readyState,safeMode:options.safe,typeErrors});
119
+ 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});}});
120
+ stateQueue=task.catch(()=>{});return task;
121
+ };
122
+ const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
123
+ async function compilerRequest(type,values={}){
124
+ if(!compiler||compiler.exitCode!==null||compiler.signalCode){
125
+ const cache=await privateCacheDirectory(root,'dev-compiler-v8');
126
+ await pruneCompilerCache(cache);
127
+ 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}:{})}});
128
+ captureOutput(compiler,'compiler');
129
+ }
130
+ const child=compiler;
131
+ const result=await new Promise((resolve,reject)=>{
132
+ const clean=()=>{child.off('message',message);child.off('error',error);child.off('exit',exit);};
133
+ const message=value=>{if(value?.type==='result'){clean();resolve(value);}};
134
+ const error=value=>{clean();reject(value);};const exit=()=>error(Error('Development compiler exited'));
135
+ child.on('message',message);child.once('error',error);child.once('exit',exit);child.send({type,root,...values});
136
+ });
137
+ if(result.recycle){compiler=undefined;await stop(child);}
138
+ if(!result.ok)throw Error(result.error);return result;
139
+ }
140
+ async function discover(){
141
+ discovery=(await compilerRequest('discover')).discovery;routeCache.clear();output=discovery.config.distDir;
142
+ if(options.safe)discovery={...discovery,eager:true};
143
+ if(discovery.eager&&!options.safe)console.log('Development uses the complete route graph for middleware, rewrites or parallel routing.');
144
+ const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
145
+ await writeTypedRoutes(root,discovery.project,discovery.config);
146
+ if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
147
+ }
148
+ async function launch(manifest,directory){
149
+ const runtime=options.safe?undefined:await runtimePool.take(directory).catch(error=>{tools?.addServerLog('warn','Worker warmup was unavailable; starting a fresh worker.',{process:'runtime'});return undefined;});
150
+ const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory};
151
+ delete env.PRNEXT_DEV_BROKER;
152
+ if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
153
+ 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});
154
+ captureOutput(child,'runtime',{forwardStdout:false});
155
+ let output='';
156
+ const port=await new Promise((resolve,reject)=>{
157
+ const timer=setTimeout(()=>fail(Error('Development backend did not start')),15000);
158
+ const clean=()=>{clearTimeout(timer);child.off('error',fail);child.off('exit',exit);};
159
+ const fail=error=>{clean();reject(error);};const exit=()=>fail(Error('Development backend exited before listening'));
160
+ 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]));}});
161
+ }).catch(async error=>{await stop(child);await runtime?.close();throw error;});
162
+ return {id:manifest.buildId,child,runtime,port,directory};
163
+ }
164
+ async function rebuild(patterns,changedFiles=[],affectedRoutes,affectedModules,cause){
165
+ const startedAt=Date.now(),started=performance.now(),phases={};let mark=started;
166
+ let reason=devBuildReason({initial:!current,safe:options.safe,changed:changedFiles,cause});
167
+ await writeState({state:'building',reason});
168
+ const changed=changedFiles.slice(0,32).map(file=>file.slice(0,256));
169
+ try{
170
+ if(!options.safe)void runtimePool.warm().catch(()=>{});
171
+ if(current&&!discovery.eager&&discovery.cssFast){
172
+ const directory=path.relative(root,path.join(home,'styles-'+randomUUID())).replaceAll(path.sep,'/');
173
+ 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});}});
174
+ if(styles){
175
+ await gateway.request('assets',{generation:styles.buildId,directory});
176
+ 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'});}
177
+ sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
178
+ styleGenerations.push({id:styles.buildId,directory});
179
+ reason=devBuildReason({cause:'css-only'});
180
+ 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});
181
+ 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'});
182
+ console.log('Styles updated. '+reason.summary);
183
+ 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});}
184
+ await watcher?.refresh();return styles;
185
+ }
186
+ }
187
+ 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))}});
188
+ phases.compile=performance.now()-mark;mark=performance.now();
189
+ if(stopping)throw Error('Development is stopping');
190
+ const consumed=filter.acceptInputs(result.inputs||[]);
191
+ // Config can change after the watch batch was captured but before the
192
+ // compiler reads it. Publish that change once and acknowledge its bytes.
193
+ for(const file of consumed)if(/(?:^|\/)(?:(?:prnext|next)\.config\.|\.env)/.test(file)&&!changed.includes(file))changed.push(file.slice(0,256));
194
+ const manifest=result.manifest;
195
+ const directory=path.relative(root,path.join(home,manifest.buildId)).replaceAll(path.sep,'/');
196
+ await snapshot(manifest.outputDirectory,path.join(root,directory));
197
+ phases.snapshot=performance.now()-mark;mark=performance.now();
198
+ if(stopping)throw Error('Development is stopping');
199
+ 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;}
200
+ try{manifest.devDependencyIndex=await publishDependencyGraph(gateway,manifest);dependencyIndexValid=true;}
201
+ catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
202
+ const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
203
+ current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
204
+ const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
205
+ current.affectedRoutes=affected;
206
+ await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal).map(route=>route.pattern)});
207
+ tools?.recordBuild({buildId:manifest.buildId,reason:reason.summary,reasonCode:reason.code,sequence:manifest.devSequence,time:Date.now(),startedAt,duration:performance.now()-started,phases,routes:patterns.length,state:'ready'});
208
+ console.log('Source compiled. '+reason.summary);
209
+ if(previous){
210
+ const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
211
+ }
212
+ while(generations.length>3){const old=generations.shift();await gateway.request('retire',{generation:old.id});await old.runtime?.close();await stop(old.child);await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});}
213
+ await watcher?.refresh();
214
+ return manifest;
215
+ }catch(error){if(!stopping){tools?.recordBuild({sequence,reason:reason.summary,reasonCode:reason.code,time:Date.now(),startedAt,duration:performance.now()-started,phases,state:'error'});tools?.addServerLog('error',error.message,{process:'compiler'});console.error('Build failed: '+error.message);await writeState({state:'error',reason,error:error.message});}throw error;}
216
+ }
217
+ const updateInfo=()=>({buildId:current.manifest.buildId,clientManifest:current.manifest.devClient,pagesManifest:current.manifest.pagesManifest,sequence:current.manifest.devSequence,session,changed:current.changed,cssOnly:current.cssOnly,affectedRoutes:current.affectedRoutes});
218
+ async function ensure(raw,client){
219
+ const warm=discovery&&findRoute(raw);
220
+ if(!stopping&&current&&warm&&active.has(warm.pattern)&&current.patterns.has(warm.pattern)){
221
+ active.set(warm.pattern,Date.now());if(pins.has(client))pins.get(client).pattern=warm.pattern;
222
+ return updateInfo();
223
+ }
224
+ return serialized(async()=>{
225
+ if(!discovery)try{await discover();}catch(error){failedPaths.add(raw);await writeState({state:'error',error:error.message});throw error;}
226
+ let route=findRoute(raw);
227
+ // A newly written route can be requested before the watcher's debounce.
228
+ // Resolve that miss against the filesystem instead of returning a stale 404.
229
+ if(!route){await discover();route=findRoute(raw);}
230
+ if(route)active.set(route.pattern,Date.now());
231
+ if(route&&pins.has(client))pins.get(client).pattern=route.pattern;
232
+ const max=Math.max(0,Math.min(64,discovery.config.onDemandEntries?.pagesBufferLength??5));
233
+ const pinned=new Set([...pins.values()].map(value=>value.pattern));
234
+ if(route)pinned.add(route.pattern);
235
+ const inactive=[...active].filter(([pattern])=>!pinned.has(pattern)).sort((a,b)=>a[1]-b[1]);
236
+ const age=Math.min(86400000,discovery.config.onDemandEntries?.maxInactiveAge??60000);
237
+ while(inactive.length>max&&(Date.now()-inactive[0][1]>=age||active.size>64+pinned.size))active.delete(inactive.shift()[0]);
238
+ if(!current || route&&!current.patterns.has(route.pattern)){
239
+ try{await rebuild([...active.keys()]);}catch(error){if(route){failed.add(route.pattern);active.delete(route.pattern);}throw error;}
240
+ }
241
+ return updateInfo();
242
+ });}
243
+ async function changed(){
244
+ try{
245
+ const pending=[...changes];changes.clear();
246
+ const actual=await filter(pending.filter(file=>file!==output&&!file.startsWith(output+'/')&&!output.startsWith(file+'/')));if(!actual.length)return;
247
+ 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=[];}
248
+ await discover();
249
+ for(const raw of failedPaths){const route=findRoute(raw);if(route)active.set(route.pattern,Date.now());}
250
+ const valid=new Set(discovery.project.routes.map(route=>route.pattern));for(const pattern of active.keys())if(!valid.has(pattern))active.delete(pattern);
251
+ let affected,modules,cause;
252
+ if(current&&!dependencyIndexValid)cause='dependency-capacity';
253
+ if(current&&dependencyIndexValid&&!discovery.eager&&!failed.size&&!failedPaths.size){
254
+ const result=await affectedDependencies(gateway,actual);
255
+ if(result.unknown.length)cause='unknown-dependency';
256
+ if(!result.complete)cause='dependency-capacity';
257
+ const dormant=new Set(discovery.project.routes.filter(route=>!current.patterns.has(route.pattern)).map(route=>path.relative(root,route.file).replaceAll(path.sep,'/')));
258
+ 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;}
259
+ if(!result.unknown.length&&result.complete){
260
+ if(result.modules.length)modules=result.modules;
261
+ if(result.routes.length)affected=result.routes;
262
+ }
263
+ }
264
+ if(current||failed.size||failedPaths.size)await rebuild([...new Set([...active.keys(),...failed])],actual,affected,modules,cause);
265
+ }catch(error){if(!stopping&&readyState.state!=='error'){console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});}}
266
+ }
267
+ let closing;
268
+ const shutdown=()=>{
269
+ if(closing)return closing;stopping=true;lifecycle.abort();clearTimeout(timer);watcher?.close();
270
+ closing=(async()=>{
271
+ await Promise.all([gateway?.close(),hostRelay?.close(),hostRelayTask,stop(compiler),typeChecker?.close()]);
272
+ // Keep each native data-cache bridge alive until its externally owned
273
+ // Node process has drained after() work and cache fills on stdin EOF.
274
+ await runtimePool.close();
275
+ await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
276
+ await queue.catch(()=>{});await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
277
+ })();return closing;
278
+ };
279
+ const fatal=error=>{if(!stopping){console.error(error.message);process.exitCode=1;void shutdown();}};
280
+ process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
281
+ try{
282
+ tools=await createDevToolsServer({root,session,snapshot:toolSnapshot,sourceTools:await createDevSourceTools(root),collectMetrics:createDevMetricsCollector(diagnosticsDirectory),ensureRoute:raw=>ensure(raw,'mcp'),invalidateRoute:raw=>serialized(async()=>{
283
+ if(!discovery)await discover();const route=findRoute(raw);if(!route)throw Error('No project route matches this pathname');
284
+ if(!current||current.child.exitCode!==null)return {invalidated:false,scope:'native-route-cache',reason:'No active runtime cache yet. Visit the route first.'};
285
+ const base=discovery.config.basePath||'',logical=base&&(raw===base||raw.startsWith(base+'/'))?raw.slice(base.length)||'/':raw;
286
+ const result=await invalidateDevRouteCache({root,directory:current.directory,session,buildId:current.id,paths:[raw,logical]});
287
+ return {...updateInfo(),...result};
288
+ })});
289
+ await writeState({state:'building',session});
290
+ gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,
291
+ onOpen:message=>{const route=discovery&&findRoute(message.path);if(route)pins.set(message.client,{pattern:route.pattern,connection:message.connection});},
292
+ onClose:message=>{if(pins.get(message.client)?.connection===message.connection)pins.delete(message.client);}});
293
+ const listener=await gateway.ready;
294
+ tools.setAppUrl(`http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port}`);
295
+ if(options.hostAi){
296
+ tools.setHostRelayState({enabled:true,linked:false});
297
+ 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'});}});
298
+ }
299
+ console.log(`PRNext development · http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port} · on-demand routes`);
300
+ if(options.safe)console.log('Safe mode: full route builds, fresh compilers and no worker warmup.');
301
+ watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:file=>{
302
+ if(stopping||!shouldWatchProjectFile(file))return;changes.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void serialized(changed).catch(()=>{}),75);
303
+ }});
304
+ await serialized(discover).catch(async error=>{console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});});
305
+ await gateway.closed;
306
+ }finally{await shutdown();process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);}
307
+ }
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 });
@@ -176,10 +176,19 @@ async function startPages({ Page, App, pattern, manifestUrl, basePath = '', trai
176
176
  installPagesRouter(controller);
177
177
  let resolveHydration;
178
178
  const hydrated = new Promise(resolve => { resolveHydration = resolve; });
179
- root = hydrateRoot(container, pagesView({ state: initial, strictMode, committed: resolveHydration, reportError: controller.reportRenderError }));
179
+ root = hydrateRoot(container, pagesView({ state: initial, strictMode, committed: resolveHydration, reportError: controller.reportRenderError }), process.env.NODE_ENV === 'development' ? {
180
+ onRecoverableError(error, info) {
181
+ console.error(error);
182
+ globalThis.__PRNEXT_DEV__?.reportRecoverableError(error, info);
183
+ },
184
+ } : undefined);
180
185
  await hydrated;
181
186
  if (data.router.isFallback) void controller.refreshFallback();
182
187
  return { root, router: controller, ...(process.env.NODE_ENV === 'development' ? {
188
+ devSync(options) {
189
+ buildId=options.dev.buildId;window.__PRNEXT_DATA__.buildId=buildId;
190
+ controller.devSetManifest({buildId,manifestUrl:options.manifestUrl});
191
+ },
183
192
  async devCommit(options) {
184
193
  buildId = options.dev.buildId;
185
194
  window.__PRNEXT_DATA__.buildId = buildId;
@@ -328,6 +328,7 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
328
328
  async function navigate(url, as, options = {}) {
329
329
  let { request, destination } = targets(url, as, options);
330
330
  if (!isApplicationURL(destination, win.location.origin, basePath) || !isApplicationURL(request, win.location.origin, basePath)) { hard(destination, options.replace); return true; }
331
+ if(process.env.NODE_ENV==='development'&&!options.devRefresh)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(request.href);
331
332
  cancelActive(); renderingError?.abort.abort(); renderingError = undefined; saveScroll();
332
333
  const job = { id: ++sequence, abort: new AbortController(), asPath: destination.pathname + destination.search + destination.hash, shallow: !!options.shallow };
333
334
  active = job;
@@ -554,10 +555,14 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
554
555
  win.history.scrollRestoration = 'manual';
555
556
  win.addEventListener('popstate', popstate); win.addEventListener('scroll', onScroll, { passive: true }); win.addEventListener('pagehide', pagehide);
556
557
  return { navigate, prefetch, saveScroll, reportRenderError, snapshot: () => current.router, beforePopState(callback) { beforePop = callback; },
558
+ ...(process.env.NODE_ENV === 'development' ? { devSetManifest(options) {
559
+ buildId=options.buildId;manifestUrl=options.manifestUrl;manifestPromise=undefined;
560
+ for(const key of prefetches.keys())dropPrefetch(key);
561
+ }} : {}),
557
562
  ...(process.env.NODE_ENV === 'development' ? { async devRefresh(options) {
558
563
  buildId = options.buildId; manifestUrl = options.manifestUrl; manifestPromise = undefined;
559
564
  for (const key of prefetches.keys()) dropPrefetch(key);
560
- await navigate(currentRequest, currentURL, { replace: true, scroll: false });
565
+ await navigate(currentRequest, currentURL, { replace: true, scroll: false, devRefresh:true });
561
566
  } } : {}),
562
567
  refreshFallback,
563
568
  dispose() { pagehide(); win.removeEventListener('popstate', popstate); win.removeEventListener('scroll', onScroll); win.removeEventListener('pagehide', pagehide); if (frame !== undefined) win.cancelAnimationFrame(frame); },
@@ -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);