@thomas.f/prnext 0.1.18-alpha → 0.1.19-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +46 -25
  4. package/build/compiler.mjs +5 -4
  5. package/build/config.mjs +4 -2
  6. package/build/dev-css-assets.mjs +173 -0
  7. package/build/dev-css-update.mjs +14 -12
  8. package/build/dev-lazy-build.mjs +77 -0
  9. package/build/dynamic.mjs +13 -6
  10. package/build/incremental.mjs +112 -18
  11. package/build/index.mjs +32 -3
  12. package/build/lazy-dynamic-imports.mjs +137 -0
  13. package/build/migrate.mjs +4 -4
  14. package/build/migration-audit.mjs +19 -31
  15. package/build/migration-imports.mjs +245 -0
  16. package/build/migration-scenarios-runner.mjs +73 -0
  17. package/build/migration-scenarios.mjs +72 -0
  18. package/build/native-discovery.mjs +65 -5
  19. package/build/native-packages.mjs +2 -0
  20. package/build/npm-packages.mjs +1 -0
  21. package/build/production-builder.mjs +1 -1
  22. package/build/standalone.mjs +21 -3
  23. package/build/webpack-graph.mjs +13 -3
  24. package/cli.mjs +10 -1
  25. package/compat/app-dynamic.cjs +12 -1
  26. package/compat/cache-handlers.cjs +3 -1
  27. package/compat/flight-delta.cjs +3 -1
  28. package/compat/index.d.ts +1 -1
  29. package/compat/link.cjs +2 -2
  30. package/docs/ai/compatibility.md +77 -0
  31. package/docs/ai/routing.md +11 -0
  32. package/docs/ai/troubleshooting.md +79 -0
  33. package/package.json +21 -9
  34. package/runtime/app-client.mjs +119 -42
  35. package/runtime/atomic-file.mjs +27 -0
  36. package/runtime/cache-components-rsc.mjs +3 -1
  37. package/runtime/dev-client.mjs +7 -2
  38. package/runtime/dev-file-changes.mjs +123 -8
  39. package/runtime/dev-lazy-imports.mjs +121 -0
  40. package/runtime/dev-lazy-staging.mjs +14 -0
  41. package/runtime/dev-style-retention.mjs +15 -0
  42. package/runtime/development.mjs +60 -19
  43. package/runtime/flight-delta.mjs +28 -4
  44. package/runtime/fs.mjs +15 -8
  45. package/runtime/lazy-import-client.mjs +38 -0
  46. package/vendor/nft/LICENSE +7 -0
  47. package/vendor/nft/README.md +16 -0
  48. package/vendor/nft/out/analyze.d.ts +8 -0
  49. package/vendor/nft/out/analyze.js +1216 -0
  50. package/vendor/nft/out/analyze.js.map +1 -0
  51. package/vendor/nft/out/cli.d.ts +2 -0
  52. package/vendor/nft/out/cli.js +94 -0
  53. package/vendor/nft/out/cli.js.map +1 -0
  54. package/vendor/nft/out/fs.d.ts +22 -0
  55. package/vendor/nft/out/fs.js +106 -0
  56. package/vendor/nft/out/fs.js.map +1 -0
  57. package/vendor/nft/out/index.d.ts +4 -0
  58. package/vendor/nft/out/index.js +25 -0
  59. package/vendor/nft/out/index.js.map +1 -0
  60. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  61. package/vendor/nft/out/node-file-trace.js +380 -0
  62. package/vendor/nft/out/node-file-trace.js.map +1 -0
  63. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  64. package/vendor/nft/out/resolve-dependency.js +427 -0
  65. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  66. package/vendor/nft/out/types.d.ts +66 -0
  67. package/vendor/nft/out/types.js +2 -0
  68. package/vendor/nft/out/types.js.map +1 -0
  69. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  70. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  71. package/vendor/nft/out/utils/binary-locators.js +88 -0
  72. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  73. package/vendor/nft/out/utils/get-package-base.js +30 -0
  74. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  75. package/vendor/nft/out/utils/interop-require.js +24 -0
  76. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  77. package/vendor/nft/out/utils/node-version.js +10 -0
  78. package/vendor/nft/out/utils/node-version.js.map +1 -0
  79. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  80. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  81. package/vendor/nft/out/utils/special-cases.js +347 -0
  82. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  83. package/vendor/nft/out/utils/static-eval.js +579 -0
  84. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  85. package/vendor/nft/out/utils/types.js +2 -0
  86. package/vendor/nft/out/utils/types.js.map +1 -0
  87. package/vendor/nft/out/utils/wrappers.js +659 -0
  88. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  89. package/vendor/nft/package.json +4 -0
  90. package/vendor/nft/upstream.json +51 -0
@@ -0,0 +1,121 @@
1
+ import path from 'node:path';
2
+ import {constants} from 'node:fs';
3
+ import {readdir,readFile,writeFile,stat,lstat,open,mkdtemp,link,rm} from 'node:fs/promises';
4
+
5
+ export const MAX_LAZY_ASSET_BYTES=128*1024*1024;
6
+ export const MAX_LAZY_ASSET_FILES=16384;
7
+ const token=/^\/_prnext\/lazy\/([a-f0-9]{24})$/;
8
+ export function lazyImportIdentifier(raw){return token.exec(new URL(raw,'http://local').pathname)?.[1];}
9
+
10
+ function receiptFiles(receipt,cacheId){
11
+ if(!receipt||receipt.version!==1||typeof cacheId!=='string'||!cacheId||cacheId.length>256||receipt.baseCacheId!==cacheId||!Array.isArray(receipt.files))throw Error('Invalid or obsolete deferred asset receipt');
12
+ if(receipt.files.length>MAX_LAZY_ASSET_FILES)throw Error('Deferred import exceeded the asset count budget');
13
+ const names=new Set(),files=[];let total=0;
14
+ for(const file of receipt.files){
15
+ const name=file?.name;
16
+ if(typeof name!=='string'||!name||name.length>255||!/^[A-Za-z0-9_.-]+$/.test(name)||name==='.'||name==='..'||name.endsWith('.')||/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)||names.has(name.toLowerCase()))throw Error('Invalid or duplicate deferred asset receipt name');
17
+ if(!Number.isSafeInteger(file.bytes)||file.bytes<0||file.bytes>MAX_LAZY_ASSET_BYTES||(total+=file.bytes)>MAX_LAZY_ASSET_BYTES)throw Error('Deferred asset receipt exceeded the 128 MiB generation budget');
18
+ names.add(name.toLowerCase());files.push({name,bytes:file.bytes});
19
+ }
20
+ return files;
21
+ }
22
+
23
+ function regularAsset(info,bytes){
24
+ if(!info.isFile()||info.isSymbolicLink())throw Error('Invalid deferred asset: expected a regular file without symbolic links');
25
+ if(info.size!==bytes)throw Error('Deferred asset does not match the receipt byte size');
26
+ return info;
27
+ }
28
+
29
+ async function readReceiptAsset(filename,bytes,checked){
30
+ const expected=checked||regularAsset(await lstat(filename),bytes);
31
+ const file=await open(filename,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
32
+ try{
33
+ const info=regularAsset(await file.stat(),bytes);
34
+ if(info.dev!==expected.dev||info.ino!==expected.ino)throw Error('Deferred asset changed after receipt validation');
35
+ // Metadata can race a writer. Read only the declared size plus one byte,
36
+ // rather than letting readFile allocate for a later, unbounded file size.
37
+ const data=Buffer.allocUnsafe(bytes);let offset=0;
38
+ while(offset<bytes){
39
+ const {bytesRead}=await file.read(data,offset,bytes-offset,offset);
40
+ if(!bytesRead)throw Error('Deferred asset ended before the receipt byte size');
41
+ offset+=bytesRead;
42
+ }
43
+ if((await file.read(Buffer.allocUnsafe(1),0,1,bytes)).bytesRead)throw Error('Deferred asset exceeded the receipt byte size');
44
+ return data;
45
+ }finally{await file.close();}
46
+ }
47
+
48
+ async function readExistingReceiptAsset(filename,bytes){
49
+ const info=await lstat(filename);
50
+ if(!info.isFile()||info.isSymbolicLink())throw Error('Invalid deferred asset: expected a regular file without symbolic links');
51
+ if(info.size!==bytes)throw Error('Deferred compilation produced a conflicting immutable asset');
52
+ return readReceiptAsset(filename,bytes,info);
53
+ }
54
+
55
+ /** Only registered tokens can reach the compiler. The caller serializes builds. */
56
+ export async function materializeDevImport({root,current,id,compile}){
57
+ const registry=current.manifest.devLazy;
58
+ if(!registry||!registry.modules.some(item=>item.id===id))throw Error('Unknown deferred import identifier');
59
+ if(registry.outputs[id])return registry.outputs[id];
60
+ const built=await compile({id,modules:registry.modules,bridges:registry.bridges,shared:registry.shared});
61
+ const next=built.devLazy,target=next?.outputs?.[id];
62
+ if(!target)throw Error('The compiler did not produce the requested deferred import');
63
+ // New server implementations cannot be added to a running immutable runtime.
64
+ // Existing Server Action proxies are reused through the static ESM bridges.
65
+ if(Object.keys(built.app?.actions||{}).some(key=>!Object.hasOwn(current.manifest.app?.actions||{},key)))throw Error('A deferred module introduced a new Server Action. Import that action statically or disable experimental.lazyDynamicImports.');
66
+ const source=path.join(built.outputDirectory,'assets'),destination=path.join(root,current.directory,'assets');
67
+ const received=Object.hasOwn(built,'devLazyAssets');
68
+ const files=received?receiptFiles(built.devLazyAssets,current.manifest.cacheId):await readdir(source,{withFileTypes:true}),pending=[];let bytes=0;
69
+ if(files.length>MAX_LAZY_ASSET_FILES)throw Error('Deferred import exceeded the asset count budget');
70
+ if(received){
71
+ for(const directory of [source,destination]){const info=await lstat(directory);if(!info.isDirectory()||info.isSymbolicLink())throw Error('Invalid deferred asset directory');}
72
+ // Validate every declared file before reading any payload or publishing.
73
+ for(const file of files)file.info=regularAsset(await lstat(path.join(source,file.name)),file.bytes);
74
+ }
75
+ for(const file of files){
76
+ if(!received&&(!file.isFile()||file.name!==path.basename(file.name)))throw Error('Invalid deferred asset');
77
+ if(!received&&(await stat(path.join(source,file.name))).size>MAX_LAZY_ASSET_BYTES)throw Error('Deferred import asset exceeded the 128 MiB generation budget');
78
+ const data=received?await readReceiptAsset(path.join(source,file.name),file.bytes,file.info):await readFile(path.join(source,file.name));
79
+ const previous=await(received?readExistingReceiptAsset(path.join(destination,file.name),file.bytes):readFile(path.join(destination,file.name))).catch(error=>{if(error.code!=='ENOENT')throw error;});
80
+ if(previous){if(!previous.equals(data))throw Error('Deferred compilation produced a conflicting immutable asset');continue;}
81
+ bytes+=data.length;
82
+ if(bytes+(current.lazyAssetBytes||0)>MAX_LAZY_ASSET_BYTES)throw Error('Deferred import assets exceeded the 128 MiB generation budget');
83
+ pending.push([file.name,data]);
84
+ }
85
+ if(pending.length){
86
+ // Partial writes stay outside the served assets directory. A hard link
87
+ // publishes a complete file atomically and exclusively without replacing an
88
+ // immutable asset, even if another writer wins between inspection and I/O.
89
+ const temporary=await mkdtemp(path.join(root,current.directory,'.lazy-publish-'));
90
+ current.lazyAssetBytes=(current.lazyAssetBytes||0)+bytes;
91
+ let retained=0,failure;
92
+ try{
93
+ for(const [name,data]of pending)await writeFile(path.join(temporary,name),data,{flag:'wx'});
94
+ for(const [name,data]of pending){
95
+ // A failed publication can have an uncertain outcome. Keep its charge
96
+ // conservatively; a retry can reuse a complete asset without recharging.
97
+ retained+=data.length;
98
+ try{await link(path.join(temporary,name),path.join(destination,name));}
99
+ catch(error){
100
+ if(error.code!=='EEXIST')throw error;
101
+ const existing=received?await readExistingReceiptAsset(path.join(destination,name),data.length):await readFile(path.join(destination,name));
102
+ if(!existing.equals(data))throw Error('Deferred compilation produced a conflicting immutable asset');
103
+ retained-=data.length;
104
+ }
105
+ }
106
+ }catch(error){failure=error;}
107
+ try{
108
+ await rm(temporary,{recursive:true,force:true,maxRetries:10,retryDelay:100});
109
+ // Reclaim only unpublished bytes after deletion of our private staging
110
+ // files is confirmed. Complete public files are never removed on failure.
111
+ current.lazyAssetBytes-=bytes-retained;
112
+ }catch(error){
113
+ if(failure){failure.cleanupError=error;failure.message+='; deferred asset staging cleanup also failed';}
114
+ else failure=error;
115
+ }
116
+ if(failure)throw failure;
117
+ }
118
+ registry.modules=next.modules;registry.bridges=next.bridges;registry.shared=next.shared;
119
+ Object.assign(registry.outputs,next.outputs);
120
+ return target;
121
+ }
@@ -0,0 +1,14 @@
1
+ import { lstat, realpath, rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ /** Delete only an unpublished direct-child stage created by our lazy compiler. */
5
+ export async function discardLazyBuild(root, built) {
6
+ if (!built?.devLazyTemporary) return;
7
+ const canonicalRoot = await realpath(root), directory = path.resolve(built.outputDirectory);
8
+ if (path.dirname(directory) !== canonicalRoot ||
9
+ !/^\.prnext-build-[a-f0-9-]{36}$/.test(path.basename(directory))) throw Error('Invalid deferred staging directory');
10
+ const info = await lstat(directory).catch(error => { if (error.code !== 'ENOENT') throw error; });
11
+ if (!info) return;
12
+ if (!info.isDirectory() || info.isSymbolicLink() || await realpath(directory) !== directory) throw Error('Invalid deferred staging directory');
13
+ await rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
14
+ }
@@ -0,0 +1,15 @@
1
+ import {rm} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ // Asset-only generations do not own backend/compiler requests. Drain their
5
+ // asset leases and finish deleting the excess snapshot before announcing ready.
6
+ export async function pruneDevelopmentStyles(generations,gateway,root){
7
+ while(generations.length>3){
8
+ const old=generations[0];
9
+ await gateway.request('retire',{generation:old.id});
10
+ await gateway.request('forget',{generation:old.id});
11
+ await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});
12
+ // Keep failed deletions tracked so a later update can retry their cleanup.
13
+ generations.shift();
14
+ }
15
+ }
@@ -1,7 +1,7 @@
1
1
  import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
2
2
  import {spawn} from 'node:child_process';
3
3
  import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
4
- import {randomUUID} from 'node:crypto';
4
+ import {randomUUID,randomBytes} from 'node:crypto';
5
5
  import path from 'node:path';
6
6
  import {fileURLToPath} from 'node:url';
7
7
  import {rename} from './fs.mjs';
@@ -21,6 +21,10 @@ import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
21
21
  import {devBuildReason} from './dev-build-reasons.mjs';
22
22
  import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
23
23
  import {prepareDevNotification} from './dev-notification.mjs';
24
+ import {writeAtomicFile} from './atomic-file.mjs';
25
+ import {lazyImportIdentifier,materializeDevImport} from './dev-lazy-imports.mjs';
26
+ import {discardLazyBuild} from './dev-lazy-staging.mjs';
27
+ import {pruneDevelopmentStyles} from './dev-style-retention.mjs';
24
28
 
25
29
  const packageRoot=fileURLToPath(new URL('../',import.meta.url));
26
30
  export function developmentRoute(routes,raw,config={}) {
@@ -83,6 +87,9 @@ export async function develop(projectRoot,args,binary) {
83
87
  }
84
88
  async function runDevelopment(projectRoot,args,binary) {
85
89
  const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
90
+ // Private IPC state survives compiler recycling without exposing the key in
91
+ // the browser's session metadata or any development manifest/notification.
92
+ const devActionSecret=randomBytes(32).toString('base64');
86
93
  const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
87
94
  const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
88
95
  // The session is ours; never clean a path obtained from application config.
@@ -95,7 +102,8 @@ async function runDevelopment(projectRoot,args,binary) {
95
102
  const lifecycle=new AbortController();
96
103
  let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
97
104
  const failedPaths=new Set();
98
- const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root);
105
+ const requestedImports=new Set();
106
+ const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root,{directoryEventsAreRecursive:process.platform==='darwin',ignore:relative=>relative===output||relative.startsWith(output+'/')});
99
107
  const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
100
108
  if(stopping)throw Error('Development is stopping');
101
109
  if(!discovery)await discover();
@@ -124,10 +132,23 @@ async function runDevelopment(projectRoot,args,binary) {
124
132
  const route=developmentRoute(discovery.project.routes,raw,discovery.config);
125
133
  if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
126
134
  }
135
+ let publicationFailures=0,lastPublicationFailure;
127
136
  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});}});
137
+ const task=stateQueue.then(async()=>{
138
+ const next=value||readyState;
139
+ const {notification,text}=prepareDevNotification({...next,safeMode:options.safe,typeErrors,...(publicationFailures?{publication:{failures:publicationFailures,lastFailure:lastPublicationFailure}}:{})});
140
+ try{await writeAtomicFile(stateFile,text);}
141
+ catch(error){
142
+ publicationFailures++;lastPublicationFailure=error.atomicFile||{operation:'publish-development-state',code:error.code||'UNKNOWN'};
143
+ // A failed publication must leave the last complete state readable.
144
+ // This private bounded receipt contains no source path or payload.
145
+ await writeFile(path.join(diagnosticsDirectory,'state-publication.json'),JSON.stringify({failures:publicationFailures,lastFailure:lastPublicationFailure,lastSuccessfulSequence:readyState.sequence??null})+'\n',{mode:0o600}).catch(()=>{});
146
+ tools?.addServerLog('error','Development state publication failed.',{process:'compiler',...lastPublicationFailure});
147
+ throw error;
148
+ }
149
+ readyState=next;
150
+ if(gateway&&!gateway.stopping)await gateway.request('notify',{value:notification});
151
+ });
131
152
  stateQueue=task.catch(()=>{});return task;
132
153
  };
133
154
  const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
@@ -180,21 +201,21 @@ async function runDevelopment(projectRoot,args,binary) {
180
201
  if(!options.safe)void runtimePool.warm().catch(()=>{});
181
202
  if(current&&!discovery.eager&&discovery.cssFast){
182
203
  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});}});
204
+ const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),baseSource:path.join(root,current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,onFallback:code=>{reason=devBuildReason({changed:changedFiles,cause:code});}});
184
205
  if(styles){
185
206
  await gateway.request('assets',{generation:styles.buildId,directory});
186
207
  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
208
  styleGenerations.push({id:styles.buildId,directory});
209
+ await pruneDevelopmentStyles(styleGenerations,gateway,root);
210
+ sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
189
211
  reason=devBuildReason({cause:'css-only'});
190
212
  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
213
  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
214
  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
215
  await watcher?.refresh();return styles;
195
216
  }
196
217
  }
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))}});
218
+ const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devLazyActivated:[...requestedImports],devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
198
219
  phases.compile=performance.now()-mark;mark=performance.now();
199
220
  if(stopping)throw Error('Development is stopping');
200
221
  const consumed=filter.acceptInputs(result.inputs||[]);
@@ -211,18 +232,26 @@ async function runDevelopment(projectRoot,args,binary) {
211
232
  catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
212
233
  const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
213
234
  current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
235
+ for(const id of requestedImports)if(!manifest.devLazy?.modules.some(item=>item.id===id))requestedImports.delete(id);
214
236
  const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
215
237
  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);
238
+ try{
239
+ 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)});
240
+ 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'});
241
+ console.log('Source compiled. '+reason.summary);
242
+ }finally{
243
+ // Activation already succeeded. A locked readiness receipt must not
244
+ // keep obsolete runtimes alive or bypass the generation bound. Retire
245
+ // through the gateway so existing requests drain before their owner
246
+ // closes; the newly activated current runtime remains available.
247
+ if(previous){
248
+ 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);
249
+ }
250
+ while(generations.length>3){const old=generations.shift();
251
+ // A deferred request may itself be awaiting this build. Never wait for
252
+ // its generation lease while holding the serialized compiler queue.
253
+ 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);
254
+ }
226
255
  }
227
256
  await watcher?.refresh();
228
257
  return manifest;
@@ -230,6 +259,18 @@ async function runDevelopment(projectRoot,args,binary) {
230
259
  }
231
260
  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
261
  async function ensure(raw,client,exact=false){
262
+ if(new URL(raw,'http://local').pathname.startsWith('/_prnext/lazy/'))return serialized(async()=>{
263
+ const id=lazyImportIdentifier(raw);
264
+ if(!id||!current)throw Error('Unknown deferred import identifier');
265
+ let built,failure,lazy;
266
+ try{lazy=await materializeDevImport({root,current,id,compile:async request=>{
267
+ const result=await compilerRequest('build',{options:{devRoutes:[...current.patterns],devSafe:options.safe,devSequence:sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devLazyRequest:request,devLazyBase:{cacheId:current.manifest.cacheId}}});
268
+ built=result.manifest;return built;
269
+ }});}catch(error){failure=error;throw error;}
270
+ finally{try{await discardLazyBuild(root,built);}catch(error){if(failure){failure.cleanupError=error;failure.message+='; deferred compiler staging cleanup also failed';}else throw error;}}
271
+ requestedImports.add(id);
272
+ return {lazy};
273
+ });
233
274
  const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
234
275
  const warm=discovery&&resolve();
235
276
  if(!stopping&&current&&warm&&active.has(warm.pattern)&&current.patterns.has(warm.pattern)){
@@ -2,14 +2,38 @@ import { randomBytes, createHmac } from 'node:crypto';
2
2
  import { deflateRawSync } from 'node:zlib';
3
3
  import { TYPE, MAX_VALUE, collectStream, deltaStream } from '../compat/flight-delta.cjs';
4
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;
5
+ export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now,setTimer=setTimeout,clearTimer=clearTimeout}={}) {
6
+ if(!Number.isFinite(ttl)||ttl<0||typeof now!=='function'||typeof setTimer!=='function'||typeof clearTimer!=='function')throw new TypeError('Invalid Flight store expiration policy');
7
+ const entries=new Map();let size=0,captures=0,encoders=0,timer,timerExpires;
7
8
  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
9
  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
+ function schedule(at){
11
+ let expires=Infinity;
12
+ for(const entry of entries.values())expires=Math.min(expires,entry.expires);
13
+ if(timer!==undefined&&timerExpires===expires)return;
14
+ if(timer!==undefined)clearTimer(timer);
15
+ timer=timerExpires=undefined;
16
+ if(!entries.size)return;
17
+ timerExpires=expires;
18
+ // One timer for the bounded store, not one per response. A cancelled cache
19
+ // entry must not leave its former deadline armed. Clamp long TTLs to Node's
20
+ // timer range; an early wake simply checks the authoritative clock again.
21
+ const handle=setTimer(()=>{
22
+ if(timer!==handle)return;
23
+ timer=timerExpires=undefined;prune(true);
24
+ },Math.min(2147483647,Math.max(1,expires-at)));
25
+ timer=handle;timer?.unref?.();
26
+ }
27
+ function prune(changed=false){
28
+ const at=now();
29
+ for(const[token,entry]of entries)if(entry.expires<=at){remove(token);changed=true;}
30
+ while(size>maxBytes||entries.size>maxEntries){remove(entries.keys().next().value);changed=true;}
31
+ // Ordinary hits keep the same timer without another full entry scan.
32
+ if(changed)schedule(at);
33
+ }
10
34
  return {
11
35
  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();},
36
+ set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune(true);},
13
37
  capture:()=>reserve('capture'),encoder:()=>reserve('encoder'),get inFlight(){return {captures,encoders};},
14
38
  get bytes(){return size;},get size(){return entries.size;},
15
39
  };
package/runtime/fs.mjs CHANGED
@@ -1,15 +1,22 @@
1
1
  import { rename as move } from 'node:fs/promises';
2
2
  import { setTimeout as delay } from 'node:timers/promises';
3
+ import path from 'node:path';
3
4
 
4
5
  // Windows scanners and recently closed file handles can briefly deny rename.
5
6
  // Keep the atomic swap: never delete the destination to work around a lock.
6
- export async function rename(source, destination) {
7
- const deadline = Date.now() + 2000;
8
- for (let attempt = 0; ; attempt++) {
9
- try { return await move(source, destination); }
10
- catch (error) {
11
- if (process.platform !== 'win32' || !['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || Date.now() >= deadline) throw error;
12
- await delay(Math.min(10 * (attempt + 1), 100));
7
+ export function createAtomicRenamer({moveFile=move,wait=delay,platform=process.platform,now=()=>performance.now(),retryMs=2000}={}) {
8
+ return async function rename(source,destination) {
9
+ const started=now(),deadline=started+retryMs;
10
+ for(let attempt=1;;attempt++){
11
+ try{return await moveFile(source,destination);}
12
+ catch(error){
13
+ if(platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(error.code)||now()>=deadline){
14
+ error.atomicRename={operation:'rename',code:error.code||'UNKNOWN',attempts:attempt,elapsedMs:Math.max(0,now()-started),destination:path.basename(destination)};
15
+ throw error;
16
+ }
17
+ await wait(Math.min(10*attempt,100,Math.max(0,deadline-now())));
18
+ }
13
19
  }
14
- }
20
+ };
15
21
  }
22
+ export const rename=createAtomicRenamer();
@@ -0,0 +1,38 @@
1
+ // Only emitted by the opt-in development compiler. Successful imports retain
2
+ // native ESM caching; failed compilation is retryable after a source correction.
3
+ const pending=new Map(),styles=new Map();
4
+ function stylesheet(url){
5
+ const existing=[...document.querySelectorAll('link[rel="stylesheet"]')].find(link=>link.href===new URL(url,location.href).href);
6
+ if(existing?.sheet)return Promise.resolve();
7
+ if(existing&&styles.has(url))return styles.get(url);
8
+ const promise=new Promise((resolve,reject)=>{
9
+ const link=existing||document.createElement('link');
10
+ const timer=setTimeout(()=>finish(Error('Deferred stylesheet timed out')),30000);
11
+ const finish=error=>{clearTimeout(timer);link.removeEventListener('load',loaded);link.removeEventListener('error',failed);if(error){styles.delete(url);if(!existing)link.remove();reject(error);}else resolve();};
12
+ const loaded=()=>finish(),failed=()=>finish(Error('Deferred stylesheet failed to load'));
13
+ link.addEventListener('load',loaded,{once:true});link.addEventListener('error',failed,{once:true});
14
+ if(!existing){link.rel='stylesheet';link.href=url;document.head.appendChild(link);}
15
+ });
16
+ styles.set(url,promise);
17
+ void promise.then(()=>styles.delete(url),()=>styles.delete(url));
18
+ return promise;
19
+ }
20
+ export function lazyImport(id,basePath=''){
21
+ const key=basePath+':'+id;
22
+ if(pending.has(key))return pending.get(key);
23
+ const task=(async()=>{
24
+ const path='/_prnext/lazy/'+id;
25
+ const response=await fetch(`${basePath}/_prnext/ensure?path=${encodeURIComponent(path)}`,{cache:'no-store',credentials:'same-origin',signal:AbortSignal.timeout(120000)});
26
+ if(!response.ok)throw Error(await response.text());
27
+ const value=await response.json(),target=value.lazy;
28
+ if(!target||typeof target.url!=='string'||!Array.isArray(target.css))throw Error('Invalid deferred import response');
29
+ for(const url of [target.url,...target.css]){
30
+ const parsed=new URL(url,location.href);
31
+ if(parsed.origin!==location.origin||!parsed.pathname.includes('/_prnext/assets/'))throw Error('Invalid deferred import asset URL');
32
+ }
33
+ await Promise.all(target.css.map(stylesheet));
34
+ const module=await import(target.url);
35
+ return target.native?module:module.__prnextLazyModule;
36
+ })().finally(()=>pending.delete(key));
37
+ pending.set(key,task);return task;
38
+ }
@@ -0,0 +1,7 @@
1
+ Copyright 2019 Vercel, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ # Node File Trace
2
+
3
+ The `out/` files and MIT license are unchanged files from `@vercel/nft` 1.11.0.
4
+ `upstream.json` records the exact npm archive integrity and every file's SHA-256.
5
+ `node scripts/vendor-nft.mjs verify` checks those bytes and the complete inventory.
6
+
7
+ This copy uses PRNext's explicit dependencies, including glob 14.0.2 and
8
+ minimatch 11.0.1. Upstream 1.11.0 still declares glob 13; overriding a workspace
9
+ lockfile would not repair that dependency for independently installed consumers.
10
+ The tracing implementation itself has no local patches. Its runtime, resolver,
11
+ filesystem, native tracing and relocated standalone contracts are tested with
12
+ the patched dependency versions.
13
+
14
+ When upstream supports the patched dependencies, prefer returning to its normal
15
+ package after validating the same contracts. Updates to this copy must retain
16
+ the upstream license, reviewed archive integrity and complete byte inventory.
@@ -0,0 +1,8 @@
1
+ import { Job } from './node-file-trace';
2
+ export interface AnalyzeResult {
3
+ assets: Set<string>;
4
+ deps: Set<string>;
5
+ imports: Set<string>;
6
+ isESM: boolean;
7
+ }
8
+ export default function analyze(id: string, code: string, job: Job): Promise<AnalyzeResult>;