@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
package/build/styles.mjs CHANGED
@@ -39,10 +39,12 @@ async function loadConfiguration(root) {
39
39
  return /\.(?:js|mjs|cjs)$/.test(filename) ? importConfiguration(filename) : JSON.parse(await readFile(filename, 'utf8'));
40
40
  }
41
41
 
42
- async function createProcessor(root, engine) {
42
+ export const resolveStandardBrowserTargets = root => browserslist(undefined,{path:path.resolve(root)});
43
+
44
+ async function createProcessor(root, engine, browserTargets) {
43
45
  const config = await loadConfiguration(root);
44
46
  if (config === undefined && engine !== 'postcss') {
45
- const targets=browserslistToTargets(browserslist(undefined,{path:root}));
47
+ const targets=browserslistToTargets(browserTargets ? await browserTargets() : resolveStandardBrowserTargets(root));
46
48
  return {async process(css,options){
47
49
  const result=transformCss({filename:options.from,code:Buffer.from(css),targets,minify:false,sourceMap:!!options.map,
48
50
  ...(options.map?.prev ? {inputSourceMap:JSON.stringify(options.map.prev)} : {}),drafts:{customMedia:true}});
@@ -134,7 +136,7 @@ function packageImporter(require) {
134
136
  }
135
137
 
136
138
  /** Build-only transformations, shared and deduplicated across all three React graphs. */
137
- export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine = 'auto' }) {
139
+ export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine = 'auto', browserTargets }) {
138
140
  const root = path.resolve(projectRoot);
139
141
  const require = createRequire(path.join(root, 'package.json'));
140
142
  const cache = new Map();
@@ -168,7 +170,7 @@ export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine
168
170
  sourceMap = compiled.sourceMap;
169
171
  for (const url of compiled.loadedUrls) if (url.protocol === 'file:') dependencies.add(fileURLToPath(url));
170
172
  }
171
- processor ??= createProcessor(root,cssEngine);
173
+ processor ??= createProcessor(root,cssEngine,browserTargets);
172
174
  const processed = await (await processor).process(css, { from: file, to: file, map: sourceMap ? { prev: sourceMap, inline: false, annotation: false } : false });
173
175
  for (const message of processed.messages) if (message.type === 'dependency' && message.file) dependencies.add(path.resolve(root, message.file));
174
176
  rebaseReferences(processed.root, file);
@@ -51,7 +51,7 @@ export async function rewriteServerChunks(file,chunks,publicPath){
51
51
  }
52
52
 
53
53
  function parserOptions(filename) {
54
- return { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...( /\.tsx?$/.test(filename) ? ['typescript'] : [])] };
54
+ return { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...( /\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] };
55
55
  }
56
56
 
57
57
  export function assertSupportedSource(source, filename) {
@@ -3,6 +3,8 @@ import {createRequire} from 'node:module';
3
3
  import {randomUUID} from 'node:crypto';
4
4
  import path from 'node:path';
5
5
  import {fileURLToPath} from 'node:url';
6
+ import {resolveTypeScript} from './typescript-toolchain.mjs';
7
+ import {nativeTypeConfig} from './native-type-config.mjs';
6
8
 
7
9
  const marker='// Generated by PRNext: typed route registry. Do not edit.';
8
10
  async function changed(file,contents,updates){
@@ -52,6 +54,8 @@ async function bindTypes(filename,ts,updates){
52
54
  async function includeTypes(root,config,helper,updates){
53
55
  const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');
54
56
  const source=await readFile(filename,'utf8').catch(error=>{if(error.code==='ENOENT')return;throw error;});if(source===undefined)return;
57
+ const toolchain=await resolveTypeScript(root);
58
+ if(toolchain.native){await changed(filename,await nativeTypeConfig(filename,helper),updates);return;}
55
59
  let ts;try{ts=createRequire(path.join(root,'package.json'))('typescript');}catch{throw Error('typedRoutes requires TypeScript installed in the application');}
56
60
  const parsed=ts.getParsedCommandLineOfConfigFile(filename,{noEmit:true},{...ts.sys,onUnRecoverableConfigFileDiagnostic:diagnostic=>{throw Error(ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n'));}});
57
61
  if(!parsed||parsed.errors.some(error=>error.code!==18003))throw Error('Cannot configure typed routes: invalid tsconfig');
@@ -1,22 +1,49 @@
1
1
  import {fork} from 'node:child_process';
2
2
  import {access} from 'node:fs/promises';
3
- import {createRequire} from 'node:module';
3
+ import {resolveTypeScript} from './typescript-toolchain.mjs';
4
4
  import path from 'node:path';
5
5
  import {privateCacheDirectory} from './persistent-cache.mjs';
6
- export async function startTypeChecker(root,config,notify){
6
+ export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000}={}){
7
7
  const noop={inactive:true,async close(){}};
8
8
  if(config.typescript?.devChecks===false)return noop;
9
9
  const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');try{await access(filename);}catch{return noop;}
10
- let compiler;try{compiler=createRequire(path.join(root,'package.json')).resolve('typescript');}catch{console.warn('TypeScript diagnostics require typescript installed in the application.');return noop;}
10
+ let toolchain;try{toolchain=await resolveTypeScript(root);}catch(error){console.warn('TypeScript diagnostics could not start: '+error.message);return noop;}
11
11
  const directory=await privateCacheDirectory(root,'typescript-dev');
12
- let child,closed=false,failures=0,timer;
12
+ let child,closed=false,failures=0,timer,restartTimer,planned,pendingInvalidation;
13
13
  function launch(){
14
14
  if(closed)return;
15
- child=fork(new URL('./typescript-dev-worker.mjs',import.meta.url),[compiler,filename,directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
15
+ const worker=toolchain.native?'./typescript-native-worker.mjs':'./typescript-dev-worker.mjs';
16
+ child=spawnChecker(new URL(worker,import.meta.url),[toolchain.compiler,filename,toolchain.native?'watch':directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
17
+ const current=child;
16
18
  let details='',healthyAt=0;child.stderr.on('data',chunk=>{details=(details+chunk).slice(-2048);});
17
- child.on('message',message=>{if(message?.type==='diagnostics'&&Array.isArray(message.errors)){healthyAt ||= Date.now();notify(message.errors.filter(value=>typeof value==='string').slice(0,40));}});
19
+ child.on('message',message=>{
20
+ if(closed||child!==current||planned===current)return;
21
+ if(message?.type==='diagnostics'&&Array.isArray(message.errors)){
22
+ // The native watcher normally handles edits incrementally. Its startup
23
+ // can miss an edit; retain one bounded fallback when no report arrives.
24
+ if(pendingInvalidation?.worker===current&&!pendingInvalidation.forced){clearTimeout(restartTimer);pendingInvalidation=undefined;}
25
+ healthyAt ||= Date.now();notify(message.errors.filter(value=>typeof value==='string').slice(0,40));
26
+ }
27
+ });
18
28
  child.on('error',error=>{notify(['TypeScript checker: '+error.message]);});
19
- child.once('exit',()=>{if(closed)return;if(healthyAt&&Date.now()-healthyAt>=60000)failures=0;if(++failures>=3){notify(['TypeScript checker stopped after repeated failures. '+details]);return;}timer=setTimeout(launch,1000*failures);});
29
+ child.once('exit',()=>{
30
+ if(pendingInvalidation?.worker===current){clearTimeout(restartTimer);pendingInvalidation=undefined;}
31
+ if(closed)return;if(planned===current){planned=undefined;launch();return;}if(healthyAt&&Date.now()-healthyAt>=60000)failures=0;if(++failures>=3){notify(['TypeScript checker stopped after repeated failures. '+details]);return;}timer=setTimeout(launch,1000*failures);
32
+ });
20
33
  }
21
- launch();return {async close(){closed=true;clearTimeout(timer);const old=child;if(!old||old.exitCode!==null||old.signalCode)return;await new Promise(resolve=>{const timer=setTimeout(()=>old.kill('SIGKILL'),3000);old.once('exit',()=>{clearTimeout(timer);resolve();});if(old.connected)old.disconnect();else old.kill();});}};
34
+ launch();return {
35
+ invalidate(file,event){
36
+ if(!toolchain.native||closed||!child?.connected)return;
37
+ const extension=path.extname(file);if(extension&&!/^\.(?:[cm]?[jt]sx?|json)$/i.test(extension))return;
38
+ const forced=event==='rename'||/\.json$/i.test(file);
39
+ if(!forced&&event!=='change')return;
40
+ clearTimeout(restartTimer);
41
+ const pending={worker:child,forced:forced||pendingInvalidation?.forced===true};pendingInvalidation=pending;
42
+ restartTimer=setTimeout(()=>{
43
+ if(closed||pendingInvalidation!==pending||child!==pending.worker||!child?.connected)return;
44
+ pendingInvalidation=undefined;planned=child;child.disconnect();
45
+ },pending.forced?150:recheckDelayMs);
46
+ },
47
+ async close(){closed=true;clearTimeout(timer);clearTimeout(restartTimer);const old=child;if(!old||old.exitCode!==null||old.signalCode)return;await new Promise(resolve=>{const timer=setTimeout(()=>old.kill('SIGKILL'),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});if(old.connected)old.disconnect();else old.kill();});},
48
+ };
22
49
  }
@@ -0,0 +1,55 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {StringDecoder} from 'node:string_decoder';
3
+ import {mkdtemp,rm} from 'node:fs/promises';
4
+ import {tmpdir} from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const [compiler,filename,mode]=process.argv.slice(2);
8
+ const watching=mode==='watch';
9
+ let closing=false,buffer='',output='',overflow=false;
10
+ const cache=await mkdtemp(path.join(tmpdir(),'prnext-typescript-native-'));
11
+ const child=spawn(process.execPath,[compiler,'--project',filename,'--noEmit','--incremental','true','--tsBuildInfoFile',path.join(cache,'project.tsbuildinfo'),'--pretty','false','--locale','en',...(watching?['--watch','--preserveWatchOutput']:[])],{
12
+ cwd:process.cwd(),windowsHide:true,detached:process.platform!=='win32',stdio:['ignore','pipe','pipe'],
13
+ // Native TS respects explicit project watch options. The polling defaults
14
+ // also cover editors/filesystems that silently lose native watch events.
15
+ env:{...process.env,TSC_WATCHFILE:process.env.TSC_WATCHFILE||'DynamicPriorityPolling',TSC_WATCHDIRECTORY:process.env.TSC_WATCHDIRECTORY||'DynamicPriorityPolling'},
16
+ });
17
+ function send(value){if(process.connected)process.send(value);}
18
+ function consume(chunk){
19
+ const text=chunk.toString('utf8').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'');
20
+ if(!watching){output+=text;if(output.length>4*1024*1024){overflow=true;void close();}return;}
21
+ buffer+=text;
22
+ // Consume complete lines, not arbitrary OS chunks. Start/completion markers
23
+ // delimit each diagnostic generation; a successful edit clears old errors.
24
+ let end;
25
+ while((end=buffer.indexOf('\n'))!==-1){
26
+ const line=buffer.slice(0,end).replace(/\r$/,'');buffer=buffer.slice(end+1);
27
+ if(/Starting (?:incremental )?compilation in watch mode|File change detected/.test(line))output='';
28
+ else if(/Found \d+ errors?\. Watching for file changes\./.test(line)){
29
+ send({type:'diagnostics',errors:/Found 0 errors/.test(line)?[]:output.trim()?[output.trim()]:['TypeScript reported errors. Run prnext typecheck for details.']});output='';
30
+ }else if(line.trim()&&output.length<20000)output+=(line+'\n').slice(0,20000-output.length);
31
+ }
32
+ if(buffer.length>20000)buffer=buffer.slice(-20000);
33
+ }
34
+ for(const stream of [child.stdout,child.stderr]){
35
+ const decoder=new StringDecoder('utf8');stream.on('data',chunk=>consume(decoder.write(chunk)));stream.on('end',()=>consume(decoder.end()));
36
+ }
37
+ child.once('error',error=>{send({type:'failure',message:error.message});process.exitCode=1;});
38
+ child.once('close',async(code,signal)=>{
39
+ // Only the directory returned by mkdtemp above is removed.
40
+ if(path.dirname(cache)===path.resolve(tmpdir())&&path.basename(cache).startsWith('prnext-typescript-native-'))await rm(cache,{recursive:true,force:true,maxRetries:5,retryDelay:100});
41
+ send({type:'complete',code:overflow?1:code??1,output:overflow?'TypeScript output exceeded 4 MiB. Run tsc directly for the complete output.':output,signal});
42
+ if(process.connected)process.disconnect();
43
+ });
44
+ async function close(){
45
+ if(closing)return;closing=true;
46
+ if(child.exitCode!==null||child.signalCode)return;
47
+ if(process.platform==='win32'){
48
+ // Own exactly this compiler subtree; TypeScript's official Node launcher
49
+ // starts its native executable as a child on Windows.
50
+ const killer=spawn('taskkill',['/PID',String(child.pid),'/T','/F'],{windowsHide:true,stdio:'ignore'});
51
+ killer.on('error',()=>child.kill('SIGKILL'));
52
+ }else{try{process.kill(-child.pid,'SIGTERM');}catch{child.kill('SIGTERM');}}
53
+ }
54
+ process.once('disconnect',()=>{void close();});
55
+ process.once('SIGTERM',()=>{void close();});process.once('SIGINT',()=>{void close();});
@@ -0,0 +1,18 @@
1
+ import {fork} from 'node:child_process';
2
+
3
+ export function checkNativeTypeScript(compiler,filename,root){
4
+ return new Promise((resolve,reject)=>{
5
+ const child=fork(new URL('./typescript-native-worker.mjs',import.meta.url),[compiler,filename,'check'],{cwd:root,windowsHide:true,execArgv:[],stdio:['ignore','ignore','pipe','ipc']});
6
+ let result,details='',timedOut=false,force;
7
+ const deadline=setTimeout(()=>{timedOut=true;if(child.connected)child.disconnect();force=setTimeout(()=>child.kill('SIGKILL'),5000);},120000);
8
+ child.stderr.on('data',chunk=>{details=(details+chunk).slice(-8192);});
9
+ child.on('message',message=>{if(message?.type==='complete')result=message;else if(message?.type==='failure')details=String(message.message).slice(0,8192);});
10
+ child.once('error',error=>{clearTimeout(deadline);clearTimeout(force);reject(error);});
11
+ child.once('exit',()=>{
12
+ clearTimeout(deadline);clearTimeout(force);
13
+ if(timedOut)reject(new Error('TypeScript check exceeded 120 seconds. Run tsc directly to inspect the project.'));
14
+ else if(result?.code===0)resolve();
15
+ else reject(new Error(result?.output||details||'TypeScript native checker exited before returning diagnostics.'));
16
+ });
17
+ });
18
+ }
@@ -0,0 +1,19 @@
1
+ import {readFile,stat} from 'node:fs/promises';
2
+ import {createRequire} from 'node:module';
3
+ import path from 'node:path';
4
+
5
+ // Select from the application's installation, never from the framework's own
6
+ // development dependencies. Native TypeScript deliberately lacks the old API.
7
+ export async function resolveTypeScript(root) {
8
+ const require=createRequire(path.join(root,'package.json'));
9
+ const manifest=require.resolve('typescript/package.json');
10
+ const metadata=JSON.parse(await readFile(manifest,'utf8'));
11
+ const major=Number(/^([0-9]+)\./.exec(metadata.version)?.[1]);
12
+ if(!Number.isInteger(major))throw new Error('The installed TypeScript package has no valid version.');
13
+ if(major<7)return {native:false,compiler:require.resolve('typescript'),version:metadata.version};
14
+ const entry=typeof metadata.bin==='string'?metadata.bin:metadata.bin?.tsc;
15
+ if(typeof entry!=='string'||!entry)throw new Error('The installed TypeScript package has no tsc command. Reinstall TypeScript with its optional native dependencies.');
16
+ const directory=path.dirname(manifest),bin=path.resolve(directory,entry);
17
+ if(!bin.startsWith(directory+path.sep)||!(await stat(bin)).isFile())throw new Error('The installed TypeScript tsc command is invalid.');
18
+ return {native:true,compiler:bin,version:metadata.version};
19
+ }
@@ -1,9 +1,10 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { createRequire } from 'node:module';
3
2
  import { execFile } from 'node:child_process';
4
3
  import { promisify } from 'node:util';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import path from 'node:path';
6
+ import {resolveTypeScript} from './typescript-toolchain.mjs';
7
+ import {checkNativeTypeScript} from './typescript-native.mjs';
7
8
 
8
9
  const execute = promisify(execFile);
9
10
 
@@ -37,11 +38,12 @@ export async function checkTypeScript(root, config, { dev = false, tsconfig, for
37
38
  if (!force && (dev || !config.typescript || config.typescript.ignoreBuildErrors)) return;
38
39
  tsconfig ??= await projectTsconfig(root, config);
39
40
  if (!tsconfig) throw new Error('TypeScript checking was explicitly enabled, but tsconfig.json does not exist. Create a TypeScript configuration, set typescript.tsconfigPath to an existing configuration, or explicitly set typescript.ignoreBuildErrors: true if types are checked separately.');
40
- let compiler;
41
- try { compiler = createRequire(path.join(root, 'package.json')).resolve('typescript'); }
41
+ let toolchain;
42
+ try { toolchain = await resolveTypeScript(root); }
42
43
  catch (error) { throw new Error('TypeScript checking requires typescript in the project. Install it as a dev dependency, or explicitly set typescript.ignoreBuildErrors: true if a separate process checks types.', { cause: error }); }
43
44
  try {
44
- await execute(process.execPath, [fileURLToPath(new URL('./typescript-check.mjs', import.meta.url)), compiler, tsconfig], {
45
+ if(toolchain.native){await checkNativeTypeScript(toolchain.compiler,tsconfig,root);return;}
46
+ await execute(process.execPath, [fileURLToPath(new URL('./typescript-check.mjs', import.meta.url)), toolchain.compiler, tsconfig], {
45
47
  cwd: root, windowsHide: true, maxBuffer: 4 * 1024 * 1024,
46
48
  });
47
49
  } catch (error) {
@@ -77,6 +77,13 @@ function pluginIdentity(plugin,webpack){
77
77
  export const webpackCompilerStats=()=>({contexts:sessions.size,created,reused,limit:sessionLimit,buildLimit:sessionBuildLimit});
78
78
  function queued(callback){const task=graphQueue.then(callback);graphQueue=task.catch(()=>{});return task;}
79
79
  export function closeWebpackCompiler(root){return queued(async()=>{
80
+ if(root!==undefined){
81
+ try{root=await realpath(root);}
82
+ catch(error){
83
+ if(error.code!=='ENOENT'&&error.code!=='ENOTDIR')throw error;
84
+ root=path.resolve(root);
85
+ }
86
+ }
80
87
  const closing=[];for(const[key,session]of sessions)if(root===undefined||session.root===root){sessions.delete(key);closing.push(session.dispose());}
81
88
  const results=await Promise.allSettled(closing),errors=results.filter(result=>result.status==='rejected').map(result=>result.reason);
82
89
  if(errors.length)throw new AggregateError(errors,'Unable to dispose webpack compiler sessions');
@@ -84,6 +91,9 @@ export function closeWebpackCompiler(root){return queued(async()=>{
84
91
 
85
92
  async function compileGraph(resolver,loads,options,{state,config,target}){
86
93
  return queued(async()=>{
94
+ // Ownership uses one canonical identity for invalidation and disposal.
95
+ // Compiler paths and graph fingerprints retain the caller's lexical root.
96
+ const ownerRoot=await realpath(state.root);
87
97
  const webpack=(await import('webpack')).default;
88
98
  let reusable=state.dev&&state.incremental,identity;
89
99
  if(reusable){try{identity=await buildCacheIdentity(state.root);}catch{}if(!identity)reusable=false;}
@@ -97,17 +107,17 @@ async function compileGraph(resolver,loads,options,{state,config,target}){
97
107
  // Unknown data structures, safe mode and unavailable identities all keep
98
108
  // compilation correct through a one-shot compiler. Hidden mutable closure
99
109
  // state cannot be fingerprinted: replace the plugin instance when it changes.
100
- if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===state.root){sessions.delete(key);await session.dispose();}
110
+ if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===ownerRoot){sessions.delete(key);await session.dispose();}
101
111
  let session=reusable&&sessions.get(key);
102
112
  if(session&&session.builds>=sessionBuildLimit){sessions.delete(key);await session.dispose();session=undefined;}
103
113
  if(reusable){
104
- for(const[old,previous]of sessions)if(previous.root===state.root&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
114
+ for(const[old,previous]of sessions)if(previous.root===ownerRoot&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
105
115
  }
106
116
  if(session){sessions.delete(key);reused++;}
107
117
  else{
108
118
  if(reusable)while(sessions.size>=sessionLimit){const[old,previous]=sessions.entries().next().value;sessions.delete(old);await previous.dispose();}
109
119
  session=await createGraphSession(state.root,{webpack,reusable,stage:state.stage});created++;
110
- Object.assign(session,{root:state.root,base,identity,builds:0});
120
+ Object.assign(session,{root:ownerRoot,base,identity,builds:0});
111
121
  }
112
122
  if(reusable)sessions.set(key,session);
113
123
  try{session.builds++;return await session.run(resolver,loads,options,{state,config,target});}
@@ -233,7 +243,7 @@ async function createGraphSession(root,{webpack,reusable,stage}) {
233
243
  cssChunkFilename:'style-[contenthash:16].css',module:true,library:{type:'module'},chunkFormat:'module',chunkLoading:'import',
234
244
  publicPath:target==='node'?'./':state.assetBase+'/',environment:{module:true,dynamicImport:true},compareBeforeEmit:false,clean:false},
235
245
  performance:false,
236
- optimization:{minimize:!!options.minify,minimizer:[{apply(compiler){
246
+ optimization:{minimize:!!options.minify,mangleExports:options.minify?'size':false,minimizer:[{apply(compiler){
237
247
  compiler.hooks.compilation.tap('PRNextMinify',compilation=>{
238
248
  compilation.hooks.processAssets.tapPromise({name:'PRNextMinify',stage:webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE},async()=>{
239
249
  // Sequential native esbuild transforms avoid a Terser worker pool.
@@ -247,7 +257,7 @@ async function createGraphSession(root,{webpack,reusable,stage}) {
247
257
  });
248
258
  });
249
259
  }}],runtimeChunk:options.splitting?{name:()=>runtime}:false,
250
- splitChunks:options.splitting?{chunks:'all',minSize:0,cacheGroups:{default:false,defaultVendors:false,shared:{minChunks:2,test:module=>!module.type.startsWith('css'),name:false}}}:false},
260
+ splitChunks:options.splitting?{chunks:'all',minSize:0,cacheGroups:{default:false,defaultVendors:false,shared:{minChunks:2,reuseExistingChunk:true,test:module=>!module.type.startsWith('css'),name:false}}}:false},
251
261
  resolve:{alias:config.resolution.resolveAlias || {},conditionNames:[...(options.conditions || []),'...'],extensions:config.resolution.resolveExtensions || ['.tsx','.ts','.jsx','.js','.mjs','.json'],fullySpecified:false},
252
262
  module:{unsafeCache:false,rules:[{test:/./,type:'javascript/auto',enforce:'post',use:[{loader:loaderPath,options:{bridge:bridgeId}}]},
253
263
  {test:/\.json$/,type:'json'},{test:/\.(?:css|scss|sass)$/,type:'css'},
@@ -379,7 +389,10 @@ async function createGraphSession(root,{webpack,reusable,stage}) {
379
389
  for(const name of previousOutputs)if(!currentOutputs.has(name))await discardPrivateFile(path.join(actualOut,name),actualOut);
380
390
  previousOutputs.clear();for(const name of currentOutputs)previousOutputs.add(name);
381
391
  }
382
- return {errors:[],warnings:[],metafile:{inputs,outputs},...(options.write===false?{outputFiles}:{})};
392
+ // This compatibility metafile does not yet encode webpack's runtime and
393
+ // async chunk-group edges. Consumers must not mistake empty import lists
394
+ // for a complete initial browser graph.
395
+ return {errors:[],warnings:[],metafile:{inputs,outputs,initialGraphComplete:false},...(options.write===false?{outputFiles}:{})};
383
396
  } finally {
384
397
  active.delete(bridgeId);
385
398
  for(const file of virtualFiles)if(!usedVirtualFiles.has(file)){virtualFiles.delete(file);await discardPrivateFile(file,home);}
@@ -0,0 +1,9 @@
1
+ export function validateWorkerMemory(value) {
2
+ if (value === undefined || value === false) return undefined;
3
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
4
+ Object.keys(value).some(key => key !== 'maxRssMb') ||
5
+ !Number.isSafeInteger(value.maxRssMb) || value.maxRssMb < 64 || value.maxRssMb > 65536) {
6
+ throw new TypeError('workerMemory must be false or { maxRssMb: 64..65536 }');
7
+ }
8
+ return { maxRssMb: value.maxRssMb };
9
+ }
package/cli.mjs CHANGED
@@ -11,6 +11,9 @@ const help = `PRNext ${packageManifest.version}
11
11
 
12
12
  Use prnext for portable scripts; prn is a shortcut on compatible shells.
13
13
 
14
+ prnext create <directory> Create a project and install dependencies
15
+ prnext create <directory> --no-install Prepare project files without installing
16
+ prnext create --help TypeScript, JavaScript and creation options
14
17
  prnext build [directory] Build React/TypeScript and static HTML
15
18
  prnext build [directory] --watch Incremental production rebuilds on changes
16
19
  prnext build [directory] --adapter static [--from dist] [--spa]
@@ -40,6 +43,7 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
40
43
  prnext typecheck [directory] [--json] Generate types and check TypeScript without building
41
44
  prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
42
45
  prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
46
+ prnext compiler [directory] [--json] React Compiler results from the last successful build
43
47
  prnext host [prnext.host.json] Host built apps by hostname; wake on demand
44
48
  prnext host [config] --check Validate hosting configuration and builds
45
49
  prnext host [config] --status [--json] Show memory, process and idle status
@@ -54,6 +58,8 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
54
58
  prnext migrate --check Audit source compatibility without migrating
55
59
  prnext migrate --check --against URL --candidate URL [--routes paths.json]
56
60
  Compare GET responses before switching
61
+ prnext migrate --check --against URL --candidate URL --browser-scenarios ./scenarios.mjs
62
+ Run trusted application browser assertions
57
63
  Migration keeps Next.js and backs up package.json and existing lockfiles.
58
64
  Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
59
65
  Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
@@ -69,10 +75,12 @@ Analysis options: --metric raw|gzip|brotli (default raw), --baseline analysis.js
69
75
  --max-increase-bytes 1024 --max-increase-percent 5
70
76
  --budgets budgets.json Exact route keys with maxJs (kB),
71
77
  maxIncreaseBytes and/or maxIncreasePercent
72
- Analysis exit codes: 0 passed, 1 budget exceeded, 2 invalid input or missing build.
78
+ Analysis exit codes: 0 passed, 1 budget exceeded or initial total unverifiable,
79
+ 2 invalid input or missing build. Request-dependent totals are lower bounds.
73
80
 
74
81
  Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
75
82
  Production profiles: --profile balanced|speed|memory|classic
83
+ Render readiness: --prewarm --readiness-file /private/path/ready.json
76
84
  Worker retirement: --worker-idle-seconds 300 --lane-idle-seconds 30 --keep-warm [true|false]
77
85
  Memory favors lower RAM under concurrency, accepting more waiting and CPU per response.
78
86
  Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
@@ -101,6 +109,30 @@ function stop(child) {
101
109
  async function main() {
102
110
  if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
103
111
  if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
112
+ if (command === 'create') {
113
+ const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
114
+ if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
115
+ const { directory, options } = parseCreateArgs(argv);
116
+ const controller = new AbortController();
117
+ let interrupted;
118
+ const interrupt = signal => { interrupted = signal; controller.abort(); };
119
+ const onInt = () => interrupt('SIGINT'), onTerm = () => interrupt('SIGTERM');
120
+ process.on('SIGINT', onInt); process.on('SIGTERM', onTerm);
121
+ try {
122
+ const report = await createProject(directory, { ...options, signal: controller.signal,
123
+ onOutput: options.json ? () => {} : (chunk, stream) => process[stream].write(chunk) });
124
+ if (options.json) console.log(JSON.stringify(report, null, 2));
125
+ else {
126
+ console.log(`PRNext project ${report.status}: ${report.root}`);
127
+ if (report.error) console.error(report.error);
128
+ if (!report.ok) console.log('Project files were preserved. Retry installation from this directory.');
129
+ if (path.resolve(process.cwd()) !== directory) console.log(`\nOpen ${report.root} in your terminal, then run:`);
130
+ for (const next of report.next) console.log(` ${next}`);
131
+ }
132
+ if (!report.ok) process.exitCode = interrupted === 'SIGINT' ? 130 : interrupted === 'SIGTERM' ? 143 : 1;
133
+ } finally { process.removeListener('SIGINT', onInt); process.removeListener('SIGTERM', onTerm); }
134
+ return;
135
+ }
104
136
  if (command === 'docs') {
105
137
  const { executeAiDocs } = await import('./runtime/ai-docs.mjs');
106
138
  const result = await executeAiDocs(argv);
@@ -161,13 +193,20 @@ async function main() {
161
193
  report.comparison = await compareServers({ ...options, routes });
162
194
  report.ok &&= report.comparison.ok;
163
195
  }
196
+ if (options.browserScenarios) {
197
+ const { runMigrationScenarios } = await import('./build/migration-scenarios.mjs');
198
+ report.browser = await runMigrationScenarios({ directory, ...options, scenarios: options.browserScenarios });
199
+ report.ok &&= report.browser.ok;
200
+ }
164
201
  if (options.json) console.log(JSON.stringify(report, null, 2));
165
202
  else {
166
203
  console.log(`PRNext migration check ${report.ok ? 'passed' : 'failed'}: ${directory}`);
167
204
  console.log(`${report.files} source files; ${report.routes.length} routes.`);
168
205
  for (const item of report.findings) console.log(`${item.severity.toUpperCase()} ${item.file || 'project'}${item.line ? ':' + item.line : ''}: ${item.message}\n ${item.action}`);
169
206
  for (const result of report.comparison?.results || []) console.log(`${result.equal ? 'MATCH' : 'DIFFERENT'} ${result.route}: ${result.error || result.differences.join(', ') || 'responses match'}`);
170
- for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : [])]) console.log(note);
207
+ for (const result of report.browser?.results || []) console.log(`BROWSER ${result.scenario} (${result.target}): ${result.status}${result.reason ? ' — ' + result.reason : ''}`);
208
+ if (report.browser?.reason) console.log(`BROWSER unknown: ${report.browser.reason}`);
209
+ for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : []), ...(report.browser ? [report.browser.scope] : [])]) console.log(note);
171
210
  }
172
211
  if (!report.ok) process.exitCode = 1;
173
212
  return;
@@ -185,10 +224,28 @@ async function main() {
185
224
  if (!report.ok) process.exitCode = 1;
186
225
  return;
187
226
  }
188
- if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
227
+ if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'compiler', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
189
228
  const args = [...argv];
190
229
  const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
191
- if (['check','typegen','typecheck','analyze'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
230
+ if (['check','typegen','typecheck','analyze','compiler'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
231
+ if(command==='compiler'){
232
+ try {
233
+ if(args.some(arg=>arg!=='--json') || args.filter(arg=>arg==='--json').length>1)throw new Error('compiler accepts only [directory] and --json');
234
+ const {readReactCompilerReport,printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
235
+ const {readBuildOutput}=await import('./build/output-contract.mjs');
236
+ const directory=path.join(root,await readBuildDirectory(root));
237
+ const output=await readBuildOutput(directory);
238
+ if(output.adapter!=='react')throw new Error('React Compiler reports apply to React builds. This application uses the static adapter.');
239
+ const report=await readReactCompilerReport(directory,{buildId:output.buildId});
240
+ if(!report)throw new Error('No React Compiler report is available. Rebuild this application with the installed PRNext version.');
241
+ if(args.includes('--json'))console.log(JSON.stringify(report,null,2));else printReactCompilerReport(report,{details:true});
242
+ } catch(error) {
243
+ if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
244
+ else console.error(`PRNext compiler: ${error.message}`);
245
+ process.exitCode=2;
246
+ }
247
+ return;
248
+ }
192
249
  if(command==='analyze'){
193
250
  try {
194
251
  const {analyzeBuild,printAnalysis,parseAnalysisArgs}=await import('./build/analyze.mjs');
@@ -307,7 +364,15 @@ async function main() {
307
364
  await buildStatic(root, options);
308
365
  } else {
309
366
  const { build } = await import('./build/index.mjs');
310
- await build(root, { ...options, dev, envMode });
367
+ const result=await build(root, { ...options, dev, envMode });
368
+ if(!dev&&result.compilationCache?.hits){
369
+ const {hits,reusedBytes}=result.compilationCache;
370
+ console.log(`Compilation cache: ${hits} ${hits===1?'graph':'graphs'} reused (${Math.ceil(reusedBytes/1024)} KiB).`);
371
+ }
372
+ if(result.reactCompilerReport?.enabled){
373
+ const {printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
374
+ printReactCompilerReport(result.reactCompilerReport,{details:false});
375
+ }
311
376
  }
312
377
  console.log(`PRNext built ${root} in ${((performance.now() - started) / 1000).toFixed(2)}s`);
313
378
  return;
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ // A shared budget across layout slots, rather than a separate page budget at
4
+ // every nesting level. Only inactive branches enter this registry.
5
+ function createActivityCache({ limit = 4, ttl = 120000, now = Date.now, schedule = setTimeout, cancel = clearTimeout } = {}) {
6
+ const entries = new Map(); let timer;
7
+ function arm() {
8
+ cancel(timer); timer = undefined;
9
+ if (entries.size) {
10
+ timer = schedule(prune, Math.max(1, Math.min(...[...entries.values()].map(entry => entry.expires)) - now()));
11
+ timer?.unref?.();
12
+ }
13
+ }
14
+ function prune() {
15
+ for (const [key, entry] of entries) if (entry.expires <= now()) { entries.delete(key); entry.evict(); }
16
+ while (entries.size > limit) { const [key, entry] = entries.entries().next().value; entries.delete(key); entry.evict(); }
17
+ arm();
18
+ }
19
+ return {
20
+ retain(key, evict) { if (!entries.has(key)) entries.set(key, { evict, expires: now() + ttl }); prune(); },
21
+ release(key) { entries.delete(key); arm(); },
22
+ clear() { const previous = [...entries.values()]; entries.clear(); cancel(timer); timer = undefined; for (const entry of previous) entry.evict(); },
23
+ get size() { return entries.size; },
24
+ };
25
+ }
26
+ module.exports = { createActivityCache };
package/compat/after.cjs CHANGED
@@ -4,11 +4,11 @@ const { AsyncLocalStorage } = require('node:async_hooks');
4
4
  function after(task) {
5
5
  const context = require('./headers.cjs').currentRequest();
6
6
  if (require('./data-cache.cjs').inCacheScope()) throw new Error('after() cannot be registered inside a cache scope');
7
- if (!context.afterScope || !['route', 'action', 'after'].includes(context.phase)) throw new Error('after() currently requires a Node Route Handler or Server Action in PRNext. Server Component and Edge after() lifecycles are not implemented.');
7
+ if (!context.afterScope || !['route', 'action', 'render', 'after'].includes(context.phase)) throw new Error('after() requires a Node Server Component, Route Handler or Server Action. Edge after() is not implemented.');
8
8
  context.afterScope.add(task, context, AsyncLocalStorage.snapshot());
9
9
  }
10
10
 
11
- function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
11
+ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, onRegistered, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
12
12
  const queue = []; let count = 0, closed = false, finishing;
13
13
  const deadline = Date.now() + timeoutMs;
14
14
  const track = promise => signal?.[Symbol.for('prnext.request-work')]?.(promise, { timeoutMs: Math.max(1, deadline - Date.now()) });
@@ -18,6 +18,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
18
18
  if (closed) throw new Error('after() request has already completed');
19
19
  if (typeof task !== 'function' && (!task || typeof task.then !== 'function')) throw new TypeError('after() expects a callback or a Promise');
20
20
  if (++count > 128) throw new Error('after() exceeds 128 tasks per request');
21
+ if (count === 1) onRegistered?.();
21
22
  const pending = typeof task === 'function' ? null : observe(task);
22
23
  if (pending) track(pending);
23
24
  queue.push({ task: pending ? null : task, pending, context, run });
@@ -27,8 +28,9 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
27
28
  try {
28
29
  while (queue.length) {
29
30
  const item = queue.shift(), remaining = deadline - Date.now();
30
- if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); report(error); onTimeout(error); break; }
31
+ if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); await item.run(()=>report(error)); onTimeout(error); break; }
31
32
  const abort = new AbortController();
33
+ item.context.afterRestricted ||= item.context.phase === 'render';
32
34
  item.context.cookies._mutable = false;
33
35
  item.context.phase = 'after';
34
36
  item.context.signal = abort.signal;
@@ -38,7 +40,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
38
40
  let timer;
39
41
  try {
40
42
  const outcome = await Promise.race([work, new Promise(resolve => { timer = setTimeout(() => { const error = new Error('after() exceeded the request maxDuration'); abort.abort(error); resolve({ error }); }, remaining); })]);
41
- if ('error' in outcome) report(outcome.error);
43
+ if ('error' in outcome) await item.run(()=>report(outcome.error));
42
44
  if (abort.signal.aborted) { onTimeout(outcome.error); break; }
43
45
  } finally { clearTimeout(timer); item.context.cacheState.closed = true; await onComplete?.(item.context); }
44
46
  }
@@ -19,7 +19,18 @@ function dynamic(input, options) {
19
19
  if (!opts.ssr && typeof React.useEffect !== 'function') {
20
20
  throw new Error('ssr: false is not supported with next/dynamic in Server Components. Move it into a Client Component.');
21
21
  }
22
- const Lazy = React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
22
+ let Lazy;
23
+ if(process.env.NODE_ENV==='development'&&typeof window!=='undefined'&&typeof opts.__prnextDevIdentity==='string'){
24
+ const registry=globalThis.__PRNEXT_DEV_DYNAMIC__||=new Map();
25
+ let entry=registry.get(opts.__prnextDevIdentity);
26
+ if(!entry||entry.failed){
27
+ entry={loader:opts.loader};
28
+ entry.lazy=React.lazy(()=>Promise.resolve().then(()=>entry.loader()).then(moduleOf,error=>{entry.failed=true;throw error;}));
29
+ if(registry.size>=2048)registry.delete(registry.keys().next().value);
30
+ registry.set(opts.__prnextDevIdentity,entry);
31
+ }else entry.loader=opts.loader;
32
+ Lazy=entry.lazy;
33
+ }else Lazy=React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
23
34
  function DynamicComponent(props) {
24
35
  const fallback = opts.loading ? React.createElement(opts.loading, { isLoading: true, pastDelay: true, error: null }) : null;
25
36
  const child = React.createElement(Lazy, props);