@thomas.f/prnext 0.1.17-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 (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +122 -46
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +34 -10
  15. package/build/config.mjs +34 -3
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import {createHash} from 'node:crypto';
3
3
  import {createReadStream} from 'node:fs';
4
- import {stat, readdir} from 'node:fs/promises';
4
+ import {stat, lstat, readdir, opendir} from 'node:fs/promises';
5
5
  import {shouldWatchProjectFile} from './env.mjs';
6
6
 
7
7
  function directoryFingerprint(entries) {
@@ -12,16 +12,27 @@ function directoryFingerprint(entries) {
12
12
 
13
13
  // Remember the bytes actually consumed by webpack, not the file's contents at
14
14
  // the end of a build: an edit made after a read must still trigger a rebuild.
15
- export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024}={}) {
15
+ export function createDevInputSnapshot(roots, {maxEntries=process.platform==='darwin'?8192:512,maxBytes=1024*1024}={}) {
16
16
  const inputs=new Map();
17
+ const limitedPrefixes=new Set();let globalLimited=false;
17
18
  let bytes=0;
19
+ function limited(relative) {
20
+ // Source files may overflow in many sibling folders. One source-root
21
+ // prefix remains bounded without invalidating a captured disjoint generator.
22
+ relative=relative==='.contentlayer/generated'||relative.startsWith('.contentlayer/generated/')?'.contentlayer/generated':relative.split('/')[0];
23
+ if(globalLimited)return;
24
+ if([...limitedPrefixes].some(prefix=>relative===prefix||relative.startsWith(prefix+'/')))return;
25
+ for(const prefix of limitedPrefixes)if(prefix.startsWith(relative+'/'))limitedPrefixes.delete(prefix);
26
+ limitedPrefixes.add(relative);
27
+ if(limitedPrefixes.size>32||[...limitedPrefixes].reduce((sum,value)=>sum+value.length*2,0)>4096){globalLimited=true;limitedPrefixes.clear();}
28
+ }
18
29
  function remember(relative,value) {
19
30
  if (inputs.has(relative)) {
20
31
  if (inputs.get(relative)!==value) inputs.set(relative,null);
21
32
  return;
22
33
  }
23
34
  const size=relative.length*2+value.length*2+64;
24
- if (inputs.size>=maxEntries || bytes+size>maxBytes) return;
35
+ if (inputs.size>=maxEntries || bytes+size>maxBytes) {limited(value.startsWith('directory:')?relative:path.posix.dirname(relative)==='.'?relative:path.posix.dirname(relative));return;}
25
36
  inputs.set(relative,value);bytes+=size;
26
37
  }
27
38
  return {
@@ -38,14 +49,15 @@ export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024
38
49
  async captureDirectory(relative) {
39
50
  // Called after an awaited content generator and before graph resolution.
40
51
  // Include its types/aggregate indexes, which may not be runtime imports.
41
- if (!shouldWatchProjectFile(relative) || inputs.size>=maxEntries || bytes>=maxBytes) return;
52
+ if (!shouldWatchProjectFile(relative))return;
53
+ if(inputs.size>=maxEntries || bytes>=maxBytes){limited(relative);return;}
42
54
  const file=path.join(roots[0],relative);
43
55
  let entries;
44
56
  try {entries=await readdir(file,{withFileTypes:true});}
45
57
  catch(error){if(error.code==='ENOENT')return;throw error;}
46
58
  remember(relative,directoryFingerprint(entries));
47
59
  for(const entry of entries) {
48
- if(inputs.size>=maxEntries || bytes>=maxBytes)break;
60
+ if(inputs.size>=maxEntries || bytes>=maxBytes){limited(relative);break;}
49
61
  const child=relative+'/'+entry.name;
50
62
  if(!shouldWatchProjectFile(child))continue;
51
63
  if(entry.isDirectory())await this.captureDirectory(child);
@@ -58,14 +70,90 @@ export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024
58
70
  }
59
71
  }
60
72
  },
61
- entries:()=>[...inputs],
73
+ entries:()=>{
74
+ const values=[...inputs],coverage={version:1,limitedPrefixes:[...limitedPrefixes],globalLimited};
75
+ if(values.length)values[0]=[...values[0],coverage];
76
+ else values.push([null,null,coverage]);
77
+ return values;
78
+ },
62
79
  };
63
80
  }
64
81
 
65
82
  // Generators may rewrite identical JSON/modules on every compilation. Retain
66
83
  // only bounded path/digest pairs, never file contents or full directory lists.
67
- export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}={}) {
84
+ export function createDevChangeFilter(root, {directoryEventsAreRecursive=false,maxEntries=directoryEventsAreRecursive?8192:512,maxBytes=1024*1024,ignore=()=>false,maxTreeEntries=8192,maxTreeBytes=1024*1024,onUncertainDirectory=()=>{}}={}) {
68
85
  const fingerprints=new Map();
86
+ // Before the first build there are no consumed inputs to lose. A later
87
+ // acceptInputs without coverage metadata deliberately replaces this proof.
88
+ const trees=new Map(),diagnostics=new Map();let treeBytes=0,coverage={version:1,limitedPrefixes:[],globalLimited:false},consumedInputs=new Map();
89
+ const contains=(parent,file)=>file===parent||file.startsWith(parent+'/');
90
+ const allowed=file=>shouldWatchProjectFile(file)&&!ignore(file);
91
+ const limitedFor=file=>!coverage||coverage.globalLimited||coverage.limitedPrefixes.some(prefix=>contains(file,prefix)||contains(prefix,file));
92
+ function discardTree(file){const previous=trees.get(file);if(previous){trees.delete(file);treeBytes-=previous.bytes;}}
93
+ function uncertain(file,reason){
94
+ discardTree(file);
95
+ if(diagnostics.get(file)!==reason){
96
+ diagnostics.delete(file);diagnostics.set(file,reason);while(diagnostics.size>32)diagnostics.delete(diagnostics.keys().next().value);
97
+ onUncertainDirectory({directory:file.slice(0,256),reason});
98
+ }
99
+ return Object.assign(Error(`Cannot verify a coalesced directory change in ${file.slice(0,256)} (${reason}). The previous build is kept. Run a full build or narrow generated inputs before retrying.`),{code:'PRNEXT_RECURSIVE_CHANGE_UNVERIFIED'});
100
+ }
101
+ async function treeSnapshot(relative){
102
+ const observed=new Map();let entriesRead=0,retainedBytes=relative.length*2+128;
103
+ const incomplete=reason=>{throw Object.assign(Error('Incomplete recursive source snapshot'),{snapshotReason:reason});};
104
+ async function visit(name){
105
+ const file=path.join(root,name),before=await lstat(file);let value,semantic;
106
+ if(before.isSymbolicLink())incomplete('symbolic-link');
107
+ if(before.isDirectory()){
108
+ const entries=[],directory=await opendir(file);
109
+ for await(const entry of directory){if(++entriesRead>maxTreeEntries)incomplete('entry-capacity');entries.push(entry);}
110
+ value=directoryFingerprint(entries);
111
+ const watched=entries.filter(entry=>allowed(name+'/'+entry.name));semantic=directoryFingerprint(watched);
112
+ rememberObserved(name,value,semantic);
113
+ for(const entry of watched)await visit(name+'/'+entry.name);
114
+ }else if(before.isFile()){
115
+ const hash=createHash('sha256');for await(const chunk of createReadStream(file,{highWaterMark:64*1024}))hash.update(chunk);
116
+ value='file:'+hash.digest('hex');rememberObserved(name,value,value);
117
+ }else incomplete('special-file');
118
+ const after=await lstat(file);
119
+ if(before.dev!==after.dev||before.ino!==after.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)incomplete('changed-during-read');
120
+ }
121
+ function rememberObserved(file,value,semantic){
122
+ retainedBytes+=2*(file.length+value.length+semantic.length)+96;
123
+ if(observed.size>=maxTreeEntries||retainedBytes>maxTreeBytes)incomplete('memory-capacity');
124
+ observed.set(file,{value,semantic});
125
+ }
126
+ try{
127
+ await visit(relative);const hash=createHash('sha256');
128
+ for(const [file,value]of [...observed].sort(([a],[b])=>a.localeCompare(b)))hash.update(JSON.stringify([file,value.semantic]));
129
+ return {complete:true,observed,digest:hash.digest('hex'),bytes:retainedBytes};
130
+ }catch(error){return {complete:false,reason:error.snapshotReason||'read-failure'};}
131
+ }
132
+ function retainTree(file,snapshot){
133
+ discardTree(file);
134
+ while(trees.size&&(trees.size>=32||treeBytes+snapshot.bytes>maxTreeBytes))discardTree(trees.keys().next().value);
135
+ trees.set(file,snapshot);treeBytes+=snapshot.bytes;
136
+ }
137
+ function observeTree(file,snapshot){
138
+ for(const [name,previous]of [...fingerprints])if(contains(file,name)&&allowed(name)&&!snapshot.observed.has(name)&&previous.value!=='missing')remember(name,'missing');
139
+ for(const [name,value]of consumedInputs)if(contains(file,name)&&allowed(name)&&snapshot.observed.get(name)?.value!==value)consumedInputs.delete(name);
140
+ for(const [directory,previous]of trees){
141
+ if(!contains(file,directory)&&!contains(directory,file))continue;
142
+ const same=[...previous.observed].every(([name,value])=>!contains(file,name)||snapshot.observed.get(name)?.semantic===value.semantic)
143
+ &&[...snapshot.observed].every(([name,value])=>!contains(directory,name)||previous.observed.get(name)?.semantic===value.semantic);
144
+ if(!same)discardTree(directory);
145
+ }
146
+ }
147
+ function observePath(file,value){
148
+ for(const [directory,snapshot]of trees){
149
+ if(contains(directory,file)){
150
+ const observed=snapshot.observed.get(file);
151
+ if(value===undefined||(value==='missing'?observed!==undefined:observed?.value!==value))discardTree(directory);
152
+ }else if(contains(file,directory))discardTree(directory);
153
+ }
154
+ for(const [name,previous]of consumedInputs)if(contains(file,name)&&(name!==file||previous!==value))consumedInputs.delete(name);
155
+ if(!value?.startsWith('directory:'))for(const [name]of [...fingerprints])if(name!==file&&contains(file,name))remember(name,value===undefined?undefined:'missing');
156
+ }
69
157
  let bytes=0;
70
158
  function remember(file,value) {
71
159
  const previous=fingerprints.get(file);
@@ -99,12 +187,39 @@ export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}=
99
187
  const filter=async files => {
100
188
  const changed=[];
101
189
  for (const file of files) {
190
+ if(directoryEventsAreRecursive&&allowed(file)){
191
+ let directory=false;try{directory=(await stat(path.join(root,file))).isDirectory();}catch{}
192
+ if(directory){
193
+ const snapshot=await treeSnapshot(file);
194
+ if(!snapshot.complete)throw uncertain(file,snapshot.reason);
195
+ if(limitedFor(file))throw uncertain(file,'input-coverage');
196
+ observeTree(file,snapshot);
197
+ const previous=trees.get(file);
198
+ const consumed=[...snapshot.observed].every(([name,value])=>consumedInputs.get(name)===value.value)
199
+ &&[...consumedInputs].every(([name,value])=>!contains(file,name)||!allowed(name)||!value.startsWith('file:')||snapshot.observed.has(name));
200
+ if((!previous||previous.digest!==snapshot.digest)&&!consumed)changed.push(file);
201
+ retainTree(file,snapshot);
202
+ diagnostics.delete(file);
203
+ continue;
204
+ }
205
+ }
102
206
  const value=await fingerprint(file), previous=fingerprints.get(file);
207
+ if(directoryEventsAreRecursive)observePath(file,value);
103
208
  if (value===undefined || !previous || previous.value!==value) changed.push(file);
104
209
  remember(file,value);
105
210
  }
106
211
  return changed;
107
212
  };
108
- filter.acceptInputs=inputs=>{const changed=[];for(const [file,value] of inputs){if(fingerprints.get(file)?.value!==value)changed.push(file);remember(file,value);}return changed;};
213
+ filter.acceptInputs=inputs=>{
214
+ coverage=inputs.find(entry=>entry[2]?.version===1)?.[2];
215
+ // Only the latest bounded compiler receipt can acknowledge an uncached
216
+ // subtree. Event deduplication may keep older values in its independent LRU.
217
+ consumedInputs=new Map();for(const [file,value]of inputs)if(typeof file==='string'&&typeof value==='string')consumedInputs.set(file,value);
218
+ for(const [directory,snapshot]of trees){
219
+ if(limitedFor(directory)||inputs.some(([file,value])=>typeof file==='string'&&contains(directory,file)&&allowed(file)&&(value===null||snapshot.observed.get(file)?.value!==value)))discardTree(directory);
220
+ }
221
+ const changed=[];for(const [file,value] of inputs){if(typeof file!=='string')continue;if(fingerprints.get(file)?.value!==value)changed.push(file);remember(file,value);}return changed;
222
+ };
223
+ filter.recursiveDiagnostics=()=>[...diagnostics].map(([directory,reason])=>({directory,reason}));
109
224
  return filter;
110
225
  }
@@ -1,10 +1,15 @@
1
1
  import logo from './dev-logo.mjs';
2
- import { createLazyDevTool } from './dev-lazy-tool.mjs';
2
+ import { createLazyDevTool, registerDevToolLoaders } from './dev-lazy-tool.mjs';
3
3
  import { popoutIcon } from './dev-popout.mjs';
4
4
  import { deviceIcons } from './dev-ui-icons.mjs';
5
5
  import { createDevPosition } from './dev-position.mjs';
6
6
  import { createDevIssueMotion } from './dev-issue-motion.mjs';
7
7
 
8
+ const loadDevTool = registerDevToolLoaders({
9
+ workbench: () => import('./dev-workbench.mjs'),
10
+ inspector: () => import('./dev-tools-panel.mjs'),
11
+ });
12
+
8
13
  // This module is imported only by the development browser entry. No framework
9
14
  // contexts, application CSS, network requests or additional SSE streams.
10
15
  export function createDevIndicator({ position = 'bottom-left', snapshot, showIssues, capture, tools }) {
@@ -79,7 +84,7 @@ export function createDevIndicator({ position = 'bottom-left', snapshot, showIss
79
84
  let workbench;
80
85
  const toolError=error=>{feedback.textContent='Could not open development tool. Try again.';console.error(error);};
81
86
  const getWorkbench = () => workbench ||= createLazyDevTool(async()=>{
82
- const {createDevWorkbench}=await import('./dev-workbench.mjs');
87
+ const {createDevWorkbench}=await loadDevTool('workbench');
83
88
  return createDevWorkbench({shadow,capture,tools,restoreFocus:()=>trigger.focus()});
84
89
  },{onError:toolError,reserve(method,args){
85
90
  if(!['popConsole','popPreview'].includes(method))return;
@@ -94,7 +99,7 @@ export function createDevIndicator({ position = 'bottom-left', snapshot, showIss
94
99
  consoleRow.append(consoleAction,detachButton('Open console in new window',()=>{close(false);getWorkbench().popConsole();}));
95
100
  let inspector;
96
101
  const getInspector = () => inspector ||= createLazyDevTool(async()=>{
97
- const {createDevToolsPanel}=await import('./dev-tools-panel.mjs');
102
+ const {createDevToolsPanel}=await loadDevTool('inspector');
98
103
  return createDevToolsPanel({shadow,tools,restoreFocus:()=>trigger.focus()});
99
104
  },{onError:toolError});
100
105
  if (tools) {
@@ -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
+ }
@@ -1,5 +1,21 @@
1
1
  // Capture user activation synchronously for detached windows, even on a slow
2
2
  // connection. Import failures/disposal close reservations instead of leaking tabs.
3
+ const loaderRegistry = Symbol.for('prnext.dev.tool.loaders');
4
+
5
+ // The indicator survives Fast Refresh, while hashed import URLs belong to a
6
+ // build generation. Publish the current factories on module evaluation so an
7
+ // older, unopened indicator never requests a retired generation's lazy chunk.
8
+ // Existing tool instances remain owned by createLazyDevTool and keep their DOM.
9
+ export function registerDevToolLoaders(loaders, target = globalThis) {
10
+ const registry = target[loaderRegistry] ||= Object.create(null);
11
+ Object.assign(registry, loaders);
12
+ return name => {
13
+ const load = registry[name];
14
+ if (typeof load !== 'function') throw Error('Development tool loader is unavailable.');
15
+ return load();
16
+ };
17
+ }
18
+
3
19
  export function createLazyDevTool(load,{onError=console.error,reserve}={}) {
4
20
  let instance,pending,disposed=false,epoch=0;
5
21
  const get=()=>pending||=(async()=>{const value=await load();if(disposed){value.dispose();throw Error('Development tool was closed');}return instance=value;})().catch(error=>{pending=undefined;throw error;});
@@ -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,6 +1,7 @@
1
+ import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
1
2
  import {spawn} from 'node:child_process';
2
3
  import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
3
- import {randomUUID} from 'node:crypto';
4
+ import {randomUUID,randomBytes} from 'node:crypto';
4
5
  import path from 'node:path';
5
6
  import {fileURLToPath} from 'node:url';
6
7
  import {rename} from './fs.mjs';
@@ -20,6 +21,10 @@ import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
20
21
  import {devBuildReason} from './dev-build-reasons.mjs';
21
22
  import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
22
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';
23
28
 
24
29
  const packageRoot=fileURLToPath(new URL('../',import.meta.url));
25
30
  export function developmentRoute(routes,raw,config={}) {
@@ -82,6 +87,9 @@ export async function develop(projectRoot,args,binary) {
82
87
  }
83
88
  async function runDevelopment(projectRoot,args,binary) {
84
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');
85
93
  const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
86
94
  const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
87
95
  // The session is ours; never clean a path obtained from application config.
@@ -94,8 +102,19 @@ async function runDevelopment(projectRoot,args,binary) {
94
102
  const lifecycle=new AbortController();
95
103
  let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
96
104
  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})});
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+'/')});
107
+ const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
108
+ if(stopping)throw Error('Development is stopping');
109
+ if(!discovery)await discover();
110
+ const patterns=[...new Set([pattern,...sources])];
111
+ if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))await discover();
112
+ if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))throw Error('Unknown deferred route pattern');
113
+ for(const value of patterns)active.set(value,Date.now());
114
+ if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
115
+ return current.directory;
116
+ })});
117
+ 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})});
99
118
  const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
100
119
  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
120
  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,10 +132,23 @@ async function runDevelopment(projectRoot,args,binary) {
113
132
  const route=developmentRoute(discovery.project.routes,raw,discovery.config);
114
133
  if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
115
134
  }
135
+ let publicationFailures=0,lastPublicationFailure;
116
136
  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});}});
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
+ });
120
152
  stateQueue=task.catch(()=>{});return task;
121
153
  };
122
154
  const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
@@ -140,14 +172,13 @@ async function runDevelopment(projectRoot,args,binary) {
140
172
  async function discover(){
141
173
  discovery=(await compilerRequest('discover')).discovery;routeCache.clear();output=discovery.config.distDir;
142
174
  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
175
  const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
145
176
  await writeTypedRoutes(root,discovery.project,discovery.config);
146
177
  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
178
  }
148
179
  async function launch(manifest,directory){
149
180
  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};
181
+ const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
151
182
  delete env.PRNEXT_DEV_BROKER;
152
183
  if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
153
184
  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});
@@ -170,21 +201,21 @@ async function runDevelopment(projectRoot,args,binary) {
170
201
  if(!options.safe)void runtimePool.warm().catch(()=>{});
171
202
  if(current&&!discovery.eager&&discovery.cssFast){
172
203
  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});}});
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});}});
174
205
  if(styles){
175
206
  await gateway.request('assets',{generation:styles.buildId,directory});
176
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'});}
177
- sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
178
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;
179
211
  reason=devBuildReason({cause:'css-only'});
180
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});
181
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'});
182
214
  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
215
  await watcher?.refresh();return styles;
185
216
  }
186
217
  }
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))}});
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))}});
188
219
  phases.compile=performance.now()-mark;mark=performance.now();
189
220
  if(stopping)throw Error('Development is stopping');
190
221
  const consumed=filter.acceptInputs(result.inputs||[]);
@@ -201,32 +232,57 @@ async function runDevelopment(projectRoot,args,binary) {
201
232
  catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
202
233
  const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
203
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);
204
236
  const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
205
237
  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);
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
+ }
211
255
  }
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
256
  await watcher?.refresh();
214
257
  return manifest;
215
258
  }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
259
  }
217
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});
218
- async function ensure(raw,client){
219
- const warm=discovery&&findRoute(raw);
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
+ });
274
+ const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
275
+ const warm=discovery&&resolve();
220
276
  if(!stopping&&current&&warm&&active.has(warm.pattern)&&current.patterns.has(warm.pattern)){
221
277
  active.set(warm.pattern,Date.now());if(pins.has(client))pins.get(client).pattern=warm.pattern;
222
278
  return updateInfo();
223
279
  }
224
280
  return serialized(async()=>{
225
281
  if(!discovery)try{await discover();}catch(error){failedPaths.add(raw);await writeState({state:'error',error:error.message});throw error;}
226
- let route=findRoute(raw);
282
+ let route=resolve();
227
283
  // A newly written route can be requested before the watcher's debounce.
228
284
  // Resolve that miss against the filesystem instead of returning a stale 404.
229
- if(!route){await discover();route=findRoute(raw);}
285
+ if(!route){await discover();route=resolve();}
230
286
  if(route)active.set(route.pattern,Date.now());
231
287
  if(route&&pins.has(client))pins.get(client).pattern=route.pattern;
232
288
  const max=Math.max(0,Math.min(64,discovery.config.onDemandEntries?.pagesBufferLength??5));
@@ -235,7 +291,8 @@ async function runDevelopment(projectRoot,args,binary) {
235
291
  const inactive=[...active].filter(([pattern])=>!pinned.has(pattern)).sort((a,b)=>a[1]-b[1]);
236
292
  const age=Math.min(86400000,discovery.config.onDemandEntries?.maxInactiveAge??60000);
237
293
  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)){
294
+ const graphChanged=current&&discovery.project.routes.some(candidate=>!current.manifest.routes.some(built=>built.id===candidate.id));
295
+ if(!current || graphChanged || route&&!current.patterns.has(route.pattern)){
239
296
  try{await rebuild([...active.keys()]);}catch(error){if(route){failed.add(route.pattern);active.delete(route.pattern);}throw error;}
240
297
  }
241
298
  return updateInfo();
@@ -273,7 +330,7 @@ async function runDevelopment(projectRoot,args,binary) {
273
330
  // Node process has drained after() work and cache fills on stdin EOF.
274
331
  await runtimePool.close();
275
332
  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});
333
+ await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
277
334
  })();return closing;
278
335
  };
279
336
  const fatal=error=>{if(!stopping){console.error(error.message);process.exitCode=1;void shutdown();}};
@@ -298,7 +355,8 @@ async function runDevelopment(projectRoot,args,binary) {
298
355
  }
299
356
  console.log(`PRNext development · http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port} · on-demand routes`);
300
357
  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=>{
358
+ watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:(file,event)=>{
359
+ typeChecker?.invalidate?.(file,event);
302
360
  if(stopping||!shouldWatchProjectFile(file))return;changes.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void serialized(changed).catch(()=>{}),75);
303
361
  }});
304
362
  await serialized(discover).catch(async error=>{console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});});