@thomas.f/prnext 0.1.18-alpha → 0.1.20-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 (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
@@ -1,7 +1,7 @@
1
1
  import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
2
2
  import {spawn} from 'node:child_process';
3
3
  import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
4
- import {randomUUID} from 'node:crypto';
4
+ import {randomUUID,randomBytes} from 'node:crypto';
5
5
  import path from 'node:path';
6
6
  import {fileURLToPath} from 'node:url';
7
7
  import {rename} from './fs.mjs';
@@ -20,7 +20,13 @@ import {createDevRuntimePool} from './dev-runtime-pool.mjs';
20
20
  import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
21
21
  import {devBuildReason} from './dev-build-reasons.mjs';
22
22
  import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
23
+ import {selectiveDevelopmentRoutes} from './dev-route-selection.mjs';
23
24
  import {prepareDevNotification} from './dev-notification.mjs';
25
+ import {writeAtomicFile} from './atomic-file.mjs';
26
+ import {lazyImportIdentifier,materializeDevImport} from './dev-lazy-imports.mjs';
27
+ import {discardLazyBuild} from './dev-lazy-staging.mjs';
28
+ import {pruneDevelopmentStyles} from './dev-style-retention.mjs';
29
+ import {createDevelopmentSessionOwner,pruneDevelopmentSessions,createRetiredGenerationCleanup} from './dev-session-retention.mjs';
24
30
 
25
31
  const packageRoot=fileURLToPath(new URL('../',import.meta.url));
26
32
  export function developmentRoute(routes,raw,config={}) {
@@ -79,23 +85,33 @@ function serverOptions(args) {
79
85
  return {hostname,port,backend,hostAi,profile,safe};
80
86
  }
81
87
  export async function develop(projectRoot,args,binary) {
82
- return withBuildLock(projectRoot,root=>runDevelopment(root,args,binary),{development:true});
88
+ let entered=false;
89
+ try{return await withBuildLock(projectRoot,root=>{entered=true;return runDevelopment(root,args,binary);},{development:true});}
90
+ finally{if(entered)try{const {pruneCompilationStorage}=await import('../build/compilation-storage.mjs');await pruneCompilationStorage(projectRoot);}catch{/* Optional compilation-cache maintenance cannot change shutdown correctness. */}}
83
91
  }
84
92
  async function runDevelopment(projectRoot,args,binary) {
85
93
  const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
94
+ // Private IPC state survives compiler recycling without exposing the key in
95
+ // the browser's session metadata or any development manifest/notification.
96
+ const devActionSecret=randomBytes(32).toString('base64');
86
97
  const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
87
98
  const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
88
99
  // The session is ours; never clean a path obtained from application config.
89
100
  const container=path.join(root,'.prnext-dev'),info=await lstat(container).catch(error=>{if(error.code!=='ENOENT')throw error;});
90
101
  if(info&&(!info.isDirectory()||info.isSymbolicLink()))throw Error('Development directory must be a real directory');
102
+ await pruneDevelopmentSessions(root);
91
103
  await mkdir(home,{recursive:true});
104
+ const sessionOwner=createDevelopmentSessionOwner(home,{session});
105
+ if(options.safe)sessionOwner.disable();
92
106
  await mkdir(diagnosticsDirectory,{recursive:true,mode:0o700});
93
107
  let compiler,gateway,watcher,timer,tools,hostRelay,hostRelayTask,stopping=false,current,discovery,typeChecker,readyState={state:'building'},typeErrors=[],sequence=0;
94
108
  let dependencyIndexValid=false;
95
109
  const lifecycle=new AbortController();
96
110
  let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
97
111
  const failedPaths=new Set();
98
- const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root);
112
+ const requestedImports=new Set();
113
+ 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+'/')});
114
+ const retiredCleanup=createRetiredGenerationCleanup({root,home,gateway:{request:(...args)=>gateway.request(...args)},stop});
99
115
  const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
100
116
  if(stopping)throw Error('Development is stopping');
101
117
  if(!discovery)await discover();
@@ -106,10 +122,10 @@ async function runDevelopment(projectRoot,args,binary) {
106
122
  if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
107
123
  return current.directory;
108
124
  })});
109
- const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
125
+ const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onBeforeChild:()=>sessionOwner.beforeSpawn(),onReady:(child,info,ticket)=>sessionOwner.spawned(ticket,child,[info.pid]),onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
110
126
  const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
111
127
  routes:(discovery?.project.routes||[]).filter(route=>!route.internal).map(route=>({path:(discovery.config.basePath||'')+(route.pattern==='/'?'/':route.pattern),kind:route.kind,source:route.file?path.relative(root,route.file).replaceAll(path.sep,'/'):undefined,compiled:current?.patterns.has(route.pattern)||false,pinned:[...pins.values()].some(pin=>pin.pattern===route.pattern)})),
112
- cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
128
+ cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,retiredCleanupPending:retiredCleanup.pending,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
113
129
  function captureOutput(child,source,{forwardStdout=true}={}){
114
130
  for(const [stream,level] of [[child.stdout,'info'],[child.stderr,'error']]){
115
131
  stream?.setEncoding('utf8');
@@ -124,10 +140,23 @@ async function runDevelopment(projectRoot,args,binary) {
124
140
  const route=developmentRoute(discovery.project.routes,raw,discovery.config);
125
141
  if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
126
142
  }
143
+ let publicationFailures=0,lastPublicationFailure;
127
144
  const writeState=value=>{
128
- const task=stateQueue.then(async()=>{if(value)readyState=value;const temporary=stateFile+'.'+randomUUID()+'.tmp';
129
- const {notification,text}=prepareDevNotification({...readyState,safeMode:options.safe,typeErrors});
130
- try{await writeFile(temporary,text);await rename(temporary,stateFile);if(gateway&&!gateway.stopping)await gateway.request('notify',{value:notification});}finally{await rm(temporary,{force:true});}});
145
+ const task=stateQueue.then(async()=>{
146
+ const next=value||readyState;
147
+ const {notification,text}=prepareDevNotification({...next,safeMode:options.safe,typeErrors,...(publicationFailures?{publication:{failures:publicationFailures,lastFailure:lastPublicationFailure}}:{})});
148
+ try{await writeAtomicFile(stateFile,text);}
149
+ catch(error){
150
+ publicationFailures++;lastPublicationFailure=error.atomicFile||{operation:'publish-development-state',code:error.code||'UNKNOWN'};
151
+ // A failed publication must leave the last complete state readable.
152
+ // This private bounded receipt contains no source path or payload.
153
+ await writeFile(path.join(diagnosticsDirectory,'state-publication.json'),JSON.stringify({failures:publicationFailures,lastFailure:lastPublicationFailure,lastSuccessfulSequence:readyState.sequence??null})+'\n',{mode:0o600}).catch(()=>{});
154
+ tools?.addServerLog('error','Development state publication failed.',{process:'compiler',...lastPublicationFailure});
155
+ throw error;
156
+ }
157
+ readyState=next;
158
+ if(gateway&&!gateway.stopping)await gateway.request('notify',{value:notification});
159
+ });
131
160
  stateQueue=task.catch(()=>{});return task;
132
161
  };
133
162
  const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
@@ -135,7 +164,9 @@ async function runDevelopment(projectRoot,args,binary) {
135
164
  if(!compiler||compiler.exitCode!==null||compiler.signalCode){
136
165
  const cache=await privateCacheDirectory(root,'dev-compiler-v8');
137
166
  await pruneCompilerCache(cache);
167
+ const ticket=sessionOwner.beforeSpawn();
138
168
  compiler=spawn(process.execPath,[path.join(packageRoot,'build/dev-worker.mjs')],{stdio:['pipe','pipe','pipe','ipc'],windowsHide:true,env:{...process.env,NODE_ENV:process.env.NODE_ENV||'development',...(options.safe?{PRNEXT_BUILD_CACHE:'0'}:{}),...(cache?{NODE_COMPILE_CACHE:cache}:{})}});
169
+ sessionOwner.spawned(ticket,compiler);
139
170
  captureOutput(compiler,'compiler');
140
171
  }
141
172
  const child=compiler;
@@ -153,14 +184,17 @@ async function runDevelopment(projectRoot,args,binary) {
153
184
  if(options.safe)discovery={...discovery,eager:true};
154
185
  const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
155
186
  await writeTypedRoutes(root,discovery.project,discovery.config);
156
- if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
187
+ 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);},{onBeforeChild:()=>sessionOwner.beforeSpawn(),onChild:(child,ticket)=>sessionOwner.spawned(ticket,child)});}
157
188
  }
158
189
  async function launch(manifest,directory){
159
190
  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;});
191
+ if(!runtime)sessionOwner.disable();
160
192
  const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
161
193
  delete env.PRNEXT_DEV_BROKER;
162
194
  if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
195
+ const ticket=sessionOwner.beforeSpawn();
163
196
  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});
197
+ sessionOwner.spawned(ticket,child);
164
198
  captureOutput(child,'runtime',{forwardStdout:false});
165
199
  let output='';
166
200
  const port=await new Promise((resolve,reject)=>{
@@ -172,6 +206,8 @@ async function runDevelopment(projectRoot,args,binary) {
172
206
  return {id:manifest.buildId,child,runtime,port,directory};
173
207
  }
174
208
  async function rebuild(patterns,changedFiles=[],affectedRoutes,affectedModules,cause){
209
+ void retiredCleanup.retry();
210
+ if(retiredCleanup.pending>=64)throw Error('Development snapshots could not be removed. Close applications locking these files, then retry.');
175
211
  const startedAt=Date.now(),started=performance.now(),phases={};let mark=started;
176
212
  let reason=devBuildReason({initial:!current,safe:options.safe,changed:changedFiles,cause});
177
213
  await writeState({state:'building',reason});
@@ -180,21 +216,21 @@ async function runDevelopment(projectRoot,args,binary) {
180
216
  if(!options.safe)void runtimePool.warm().catch(()=>{});
181
217
  if(current&&!discovery.eager&&discovery.cssFast){
182
218
  const directory=path.relative(root,path.join(home,'styles-'+randomUUID())).replaceAll(path.sep,'/');
183
- const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,snapshot,onFallback:code=>{reason=devBuildReason({changed:changedFiles,cause:code});}});
219
+ const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),baseSource:path.join(root,current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,onFallback:code=>{reason=devBuildReason({changed:changedFiles,cause:code});}});
184
220
  if(styles){
185
221
  await gateway.request('assets',{generation:styles.buildId,directory});
186
222
  try{styles.devDependencyIndex=await publishDependencyGraph(gateway,styles);dependencyIndexValid=true;}catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
187
- sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
188
223
  styleGenerations.push({id:styles.buildId,directory});
224
+ await pruneDevelopmentStyles(styleGenerations,gateway,root);
225
+ sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
189
226
  reason=devBuildReason({cause:'css-only'});
190
227
  await writeState({state:'ready',reason,buildId:styles.buildId,clientManifest:styles.devClient,sequence,session,changed,cssOnly:true,listenerPid:gateway.child.pid,compiler:current.compiler,transforms:current.transforms});
191
228
  tools?.recordBuild({buildId:styles.buildId,reason:reason.summary,reasonCode:reason.code,sequence,time:Date.now(),startedAt,duration:performance.now()-started,phases:{styles:performance.now()-started},mode:'css',state:'ready'});
192
229
  console.log('Styles updated. '+reason.summary);
193
- while(styleGenerations.length>3){const old=styleGenerations.shift();await gateway.request('retire',{generation:old.id});await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});}
194
230
  await watcher?.refresh();return styles;
195
231
  }
196
232
  }
197
- const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
233
+ const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devLazyActivated:[...requestedImports],devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
198
234
  phases.compile=performance.now()-mark;mark=performance.now();
199
235
  if(stopping)throw Error('Development is stopping');
200
236
  const consumed=filter.acceptInputs(result.inputs||[]);
@@ -211,18 +247,30 @@ async function runDevelopment(projectRoot,args,binary) {
211
247
  catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
212
248
  const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
213
249
  current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
214
- const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
215
- current.affectedRoutes=affected;
216
- await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal&&!route.devDeferred).map(route=>route.pattern)});
217
- tools?.recordBuild({buildId:manifest.buildId,reason:reason.summary,reasonCode:reason.code,sequence:manifest.devSequence,time:Date.now(),startedAt,duration:performance.now()-started,phases,routes:patterns.length,state:'ready'});
218
- console.log('Source compiled. '+reason.summary);
219
- if(previous){
220
- const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
250
+ for(const id of requestedImports)if(!manifest.devLazy?.modules.some(item=>item.id===id))requestedImports.delete(id);
251
+ let affected;
252
+ if(dependencyIndexValid&&affectedRoutes) {
253
+ const nextAffected=await affectedDependencies(gateway,changedFiles).catch(()=>undefined);
254
+ affected=selectiveDevelopmentRoutes(previous?.manifest,manifest,{changed:changedFiles,affectedRoutes,affectedModules,nextAffected});
221
255
  }
222
- while(generations.length>3){const old=generations.shift();
223
- // A deferred request may itself be awaiting this build. Never wait for
224
- // its generation lease while holding the serialized compiler queue.
225
- const cleanup=(async()=>{await gateway.request('retire',{generation:old.id});await old.runtime?.close();await stop(old.child);await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});})().catch(error=>{if(!stopping)console.warn('Development generation cleanup: '+error.message);}).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
256
+ current.affectedRoutes=affected;
257
+ try{
258
+ 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)});
259
+ 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'});
260
+ console.log('Source compiled. '+reason.summary);
261
+ }finally{
262
+ // Activation already succeeded. A locked readiness receipt must not
263
+ // keep obsolete runtimes alive or bypass the generation bound. Retire
264
+ // through the gateway so existing requests drain before their owner
265
+ // closes; the newly activated current runtime remains available.
266
+ if(previous){
267
+ 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);
268
+ }
269
+ while(generations.length>3){const old=generations.shift();
270
+ // A deferred request may itself be awaiting this build. Never wait for
271
+ // its generation lease while holding the serialized compiler queue.
272
+ const cleanup=retiredCleanup.enqueue(old).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
273
+ }
226
274
  }
227
275
  await watcher?.refresh();
228
276
  return manifest;
@@ -230,6 +278,18 @@ async function runDevelopment(projectRoot,args,binary) {
230
278
  }
231
279
  const updateInfo=()=>({buildId:current.manifest.buildId,clientManifest:current.manifest.devClient,pagesManifest:current.manifest.pagesManifest,sequence:current.manifest.devSequence,session,changed:current.changed,cssOnly:current.cssOnly,affectedRoutes:current.affectedRoutes});
232
280
  async function ensure(raw,client,exact=false){
281
+ if(new URL(raw,'http://local').pathname.startsWith('/_prnext/lazy/'))return serialized(async()=>{
282
+ const id=lazyImportIdentifier(raw);
283
+ if(!id||!current)throw Error('Unknown deferred import identifier');
284
+ let built,failure,lazy;
285
+ try{lazy=await materializeDevImport({root,current,id,compile:async request=>{
286
+ 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}}});
287
+ built=result.manifest;return built;
288
+ }});}catch(error){failure=error;throw error;}
289
+ finally{try{await discardLazyBuild(root,built);}catch(error){if(failure){failure.cleanupError=error;failure.message+='; deferred compiler staging cleanup also failed';}else throw error;}}
290
+ requestedImports.add(id);
291
+ return {lazy};
292
+ });
233
293
  const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
234
294
  const warm=discovery&&resolve();
235
295
  if(!stopping&&current&&warm&&active.has(warm.pattern)&&current.patterns.has(warm.pattern)){
@@ -288,7 +348,7 @@ async function runDevelopment(projectRoot,args,binary) {
288
348
  // Keep each native data-cache bridge alive until its externally owned
289
349
  // Node process has drained after() work and cache fills on stdin EOF.
290
350
  await runtimePool.close();
291
- await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
351
+ await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring,retiredCleanup.close()]);
292
352
  await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
293
353
  })();return closing;
294
354
  };
@@ -303,7 +363,7 @@ async function runDevelopment(projectRoot,args,binary) {
303
363
  return {...updateInfo(),...result};
304
364
  })});
305
365
  await writeState({state:'building',session});
306
- gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,
366
+ gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,onBeforeChild:()=>sessionOwner.beforeSpawn(),onChild:(child,ticket)=>sessionOwner.spawned(ticket,child),
307
367
  onOpen:message=>{const route=discovery&&findRoute(message.path);if(route)pins.set(message.client,{pattern:route.pattern,connection:message.connection});},
308
368
  onClose:message=>{if(pins.get(message.client)?.connection===message.connection)pins.delete(message.client);}});
309
369
  const listener=await gateway.ready;
package/runtime/env.mjs CHANGED
@@ -57,6 +57,15 @@ export function loadEnvConfig(projectRoot, { dev = false, env = process.env, mod
57
57
  return { combinedEnv: env, parsedEnv, loadedEnvFiles };
58
58
  }
59
59
 
60
+ /** Native runtime settings need the same project env as its Node workers.
61
+ * Return a fresh object: persistent apps must never share loaded credentials.
62
+ */
63
+ export function productionEnvironment(projectRoot, {env=process.env}={}) {
64
+ const combined={...env,NODE_ENV:env.NODE_ENV||'production'};
65
+ loadEnvConfig(projectRoot,{dev:false,env:combined});
66
+ return combined;
67
+ }
68
+
60
69
  export function shouldWatchProjectFile(filename) {
61
70
  if (!filename) return false;
62
71
  const parts = String(filename).split(/[\\/]/);
@@ -1,21 +1,72 @@
1
1
  import { randomBytes, createHmac } from 'node:crypto';
2
- import { deflateRawSync } from 'node:zlib';
2
+ import { deflateRaw } from 'node:zlib';
3
3
  import { TYPE, MAX_VALUE, collectStream, deltaStream } from '../compat/flight-delta.cjs';
4
4
 
5
- export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now}={}) {
6
- const entries=new Map();let size=0,captures=0,encoders=0;
5
+ export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now,setTimer=setTimeout,clearTimer=clearTimeout}={}) {
6
+ if(!Number.isFinite(ttl)||ttl<0||typeof now!=='function'||typeof setTimer!=='function'||typeof clearTimer!=='function')throw new TypeError('Invalid Flight store expiration policy');
7
+ const entries=new Map();let size=0,captures=0,encoders=0,timer,timerExpires;
7
8
  function reserve(kind){if(kind==='capture'?captures>=maxCaptures:encoders>=maxEncoders)return;kind==='capture'?captures++:encoders++;let released=false;return()=>{if(released)return;released=true;kind==='capture'?captures--:encoders--;};}
8
9
  function remove(token){const entry=entries.get(token);if(entry){size-=entry.bytes.length;entries.delete(token);}}
9
- function prune(){for(const[token,entry]of entries)if(entry.expires<=now())remove(token);while(size>maxBytes||entries.size>maxEntries)remove(entries.keys().next().value);}
10
+ function schedule(at){
11
+ let expires=Infinity;
12
+ for(const entry of entries.values())expires=Math.min(expires,entry.expires);
13
+ if(timer!==undefined&&timerExpires===expires)return;
14
+ if(timer!==undefined)clearTimer(timer);
15
+ timer=timerExpires=undefined;
16
+ if(!entries.size)return;
17
+ timerExpires=expires;
18
+ // One timer for the bounded store, not one per response. A cancelled cache
19
+ // entry must not leave its former deadline armed. Clamp long TTLs to Node's
20
+ // timer range; an early wake simply checks the authoritative clock again.
21
+ const handle=setTimer(()=>{
22
+ if(timer!==handle)return;
23
+ timer=timerExpires=undefined;prune(true);
24
+ },Math.min(2147483647,Math.max(1,expires-at)));
25
+ timer=handle;timer?.unref?.();
26
+ }
27
+ function prune(changed=false){
28
+ const at=now();
29
+ for(const[token,entry]of entries)if(entry.expires<=at){remove(token);changed=true;}
30
+ while(size>maxBytes||entries.size>maxEntries){remove(entries.keys().next().value);changed=true;}
31
+ // Ordinary hits keep the same timer without another full entry scan.
32
+ if(changed)schedule(at);
33
+ }
10
34
  return {
11
35
  get(token,identity){prune();const entry=entries.get(token);if(entry?.identity!==identity)return;return entry?.bytes;},
12
- set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune();},
36
+ set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune(true);},
13
37
  capture:()=>reserve('capture'),encoder:()=>reserve('encoder'),get inFlight(){return {captures,encoders};},
14
38
  get bytes(){return size;},get size(){return entries.size;},
15
39
  };
16
40
  }
17
41
  const store=createFlightStore();
18
- export function compressedDeltaPolicy(acceptEncoding='') {
42
+ const compress=value=>new Promise((resolve,reject)=>deflateRaw(value,{level:1},(error,result)=>error?reject(error):resolve(result)));
43
+
44
+ // At most two probes (four small zlib jobs) across all stores and requests.
45
+ // Admission never queues: a saturated probe keeps ordinary Flight. An aborted
46
+ // caller stops waiting, but its slot remains occupied until native work ends.
47
+ export function createCompressionProbe({maxConcurrent=2,compress:run=compress}={}) {
48
+ if(!Number.isInteger(maxConcurrent)||maxConcurrent<1||maxConcurrent>8||typeof run!=='function')throw new TypeError('Invalid Flight compression probe policy');
49
+ let active=0;
50
+ const probe=(raw,encoded,signal)=>{
51
+ signal?.throwIfAborted();
52
+ if(raw.length>64*1024||encoded.length+32>=raw.length||active>=maxConcurrent)return false;
53
+ active++;
54
+ const work=Promise.allSettled([Promise.resolve().then(()=>run(encoded)),Promise.resolve().then(()=>run(raw))])
55
+ .then(results=>results.every(result=>result.status==='fulfilled')&&results[0].value.length+32<results[1].value.length*0.9)
56
+ .finally(()=>{active--;});
57
+ if(!signal)return work;
58
+ return new Promise((resolve,reject)=>{
59
+ const abort=()=>{signal.removeEventListener('abort',abort);reject(signal.reason);};
60
+ signal.addEventListener('abort',abort,{once:true});
61
+ if(signal.aborted)abort();
62
+ work.then(resolve,reject).finally(()=>signal.removeEventListener('abort',abort));
63
+ });
64
+ };
65
+ Object.defineProperty(probe,'inFlight',{get:()=>active});
66
+ return probe;
67
+ }
68
+ const compressionProbe=createCompressionProbe();
69
+ export function compressedDeltaPolicy(acceptEncoding='',{signal}={}) {
19
70
  const compressed=acceptEncoding.split(',').some(value=>{
20
71
  const [name,...parameters]=value.trim().toLowerCase().split(';');
21
72
  return /^(gzip|deflate|br|zstd|\*)$/.test(name)&&!parameters.some(p=>/^\s*q\s*=\s*0(?:\.0*)?\s*$/.test(p));
@@ -24,8 +75,7 @@ export function compressedDeltaPolicy(acceptEncoding='') {
24
75
  // This conservative, bounded probe is not the response compressor. It avoids
25
76
  // choosing thousands of references to JSON that gzip already compresses well.
26
77
  // A large/expensive chunk keeps ordinary Flight instead of delaying a flush.
27
- return (raw,encoded)=>raw.length<=64*1024&&encoded.length+32<raw.length&&
28
- deflateRawSync(encoded,{level:1}).length+32<deflateRawSync(raw,{level:1}).length*0.9;
78
+ return (raw,encoded)=>compressionProbe(raw,encoded,signal);
29
79
  }
30
80
  export async function differentialFlight(response,options,cache=store) {
31
81
  const headers=new Headers(options.headers), outgoing=new Headers(response.headers);
@@ -40,7 +90,7 @@ export async function differentialFlight(response,options,cache=store) {
40
90
  controller.enqueue(raw instanceof Uint8Array?raw:raw instanceof ArrayBuffer?new Uint8Array(raw):new TextEncoder().encode(String(raw??'')));controller.close();
41
91
  }});
42
92
  const collected=collectStream(body,{capture:!!releaseCapture,onComplete:bytes=>cache.set(token,identity,bytes),onFinally:releaseCapture});
43
- encoded=base?await deltaStream(collected,base,{assess:compressedDeltaPolicy(headers.get('accept-encoding')||'')}):{delta:false,body:collected};
93
+ encoded=base?await deltaStream(collected,base,{assess:compressedDeltaPolicy(headers.get('accept-encoding')||'',{signal:options.signal})}):{delta:false,body:collected};
44
94
  if(!encoded.delta)releaseEncoder?.();
45
95
  else encoded.body=collectStream(encoded.body,{capture:false,onFinally:releaseEncoder});
46
96
  }catch(error){releaseEncoder?.();releaseCapture?.();throw error;}
package/runtime/fs.mjs CHANGED
@@ -1,15 +1,22 @@
1
1
  import { rename as move } from 'node:fs/promises';
2
2
  import { setTimeout as delay } from 'node:timers/promises';
3
+ import path from 'node:path';
3
4
 
4
5
  // Windows scanners and recently closed file handles can briefly deny rename.
5
6
  // Keep the atomic swap: never delete the destination to work around a lock.
6
- export async function rename(source, destination) {
7
- const deadline = Date.now() + 2000;
8
- for (let attempt = 0; ; attempt++) {
9
- try { return await move(source, destination); }
10
- catch (error) {
11
- if (process.platform !== 'win32' || !['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || Date.now() >= deadline) throw error;
12
- await delay(Math.min(10 * (attempt + 1), 100));
7
+ export function createAtomicRenamer({moveFile=move,wait=delay,platform=process.platform,now=()=>performance.now(),retryMs=2000}={}) {
8
+ return async function rename(source,destination) {
9
+ const started=now(),deadline=started+retryMs;
10
+ for(let attempt=1;;attempt++){
11
+ try{return await moveFile(source,destination);}
12
+ catch(error){
13
+ if(platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(error.code)||now()>=deadline){
14
+ error.atomicRename={operation:'rename',code:error.code||'UNKNOWN',attempts:attempt,elapsedMs:Math.max(0,now()-started),destination:path.basename(destination)};
15
+ throw error;
16
+ }
17
+ await wait(Math.min(10*attempt,100,Math.max(0,deadline-now())));
18
+ }
13
19
  }
14
- }
20
+ };
15
21
  }
22
+ export const rename=createAtomicRenamer();
@@ -0,0 +1,38 @@
1
+ // Only emitted by the opt-in development compiler. Successful imports retain
2
+ // native ESM caching; failed compilation is retryable after a source correction.
3
+ const pending=new Map(),styles=new Map();
4
+ function stylesheet(url){
5
+ const existing=[...document.querySelectorAll('link[rel="stylesheet"]')].find(link=>link.href===new URL(url,location.href).href);
6
+ if(existing?.sheet)return Promise.resolve();
7
+ if(existing&&styles.has(url))return styles.get(url);
8
+ const promise=new Promise((resolve,reject)=>{
9
+ const link=existing||document.createElement('link');
10
+ const timer=setTimeout(()=>finish(Error('Deferred stylesheet timed out')),30000);
11
+ const finish=error=>{clearTimeout(timer);link.removeEventListener('load',loaded);link.removeEventListener('error',failed);if(error){styles.delete(url);if(!existing)link.remove();reject(error);}else resolve();};
12
+ const loaded=()=>finish(),failed=()=>finish(Error('Deferred stylesheet failed to load'));
13
+ link.addEventListener('load',loaded,{once:true});link.addEventListener('error',failed,{once:true});
14
+ if(!existing){link.rel='stylesheet';link.href=url;document.head.appendChild(link);}
15
+ });
16
+ styles.set(url,promise);
17
+ void promise.then(()=>styles.delete(url),()=>styles.delete(url));
18
+ return promise;
19
+ }
20
+ export function lazyImport(id,basePath=''){
21
+ const key=basePath+':'+id;
22
+ if(pending.has(key))return pending.get(key);
23
+ const task=(async()=>{
24
+ const path='/_prnext/lazy/'+id;
25
+ const response=await fetch(`${basePath}/_prnext/ensure?path=${encodeURIComponent(path)}`,{cache:'no-store',credentials:'same-origin',signal:AbortSignal.timeout(120000)});
26
+ if(!response.ok)throw Error(await response.text());
27
+ const value=await response.json(),target=value.lazy;
28
+ if(!target||typeof target.url!=='string'||!Array.isArray(target.css))throw Error('Invalid deferred import response');
29
+ for(const url of [target.url,...target.css]){
30
+ const parsed=new URL(url,location.href);
31
+ if(parsed.origin!==location.origin||!parsed.pathname.includes('/_prnext/assets/'))throw Error('Invalid deferred import asset URL');
32
+ }
33
+ await Promise.all(target.css.map(stylesheet));
34
+ const module=await import(target.url);
35
+ return target.native?module:module.__prnextLazyModule;
36
+ })().finally(()=>pending.delete(key));
37
+ pending.set(key,task);return task;
38
+ }
@@ -0,0 +1,35 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {readFile, stat} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {escapeHtml} from './http.mjs';
5
+
6
+ export const NATIVE_PPR_ARTIFACT_BYTES = 2 * 1024 * 1024;
7
+ export const NATIVE_PPR_PREFIX_BYTES = 128 * 1024;
8
+ export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
9
+
10
+ export function partialShell(artifact, route) {
11
+ const bootstrap = '<script>self.__PRNEXT_FLIGHT_STREAM__=self.__PRNEXT_FLIGHT_STREAM__||[];</script>' +
12
+ (route.client ? `<script type="module" async src="${escapeHtml(route.client)}"></script>` : '');
13
+ const end = artifact.shell.lastIndexOf('</body>');
14
+ if (end < 0 || !/<\/body><\/html>$/i.test(artifact.shell)) throw new Error('Invalid partial prerender document');
15
+ return artifact.shell.slice(0, end) + bootstrap + artifact.shell.slice(end);
16
+ }
17
+
18
+ // This is a private transport field, never an HTTP header. Rust has already
19
+ // checked cache generation and committed these exact build bytes. Pinning the
20
+ // same artifact gives the request one snapshot even if invalidation races Node.
21
+ export async function nativePprArtifact({renderMode, manifest, route, url, distDir}) {
22
+ if (!renderMode?.startsWith('native-ppr:')) return null;
23
+ if (!manifest.config?.nativePprShell || manifest.dev || !/^[a-f\d]{64}$/.test(renderMode.slice(11))) throw new Error('Invalid native PPR continuation');
24
+ const pathname = new URL(url).pathname;
25
+ const descriptor = manifest.config.nativePprArtifacts?.find(item => item.routeId === route.id && item.path === pathname && item.artifactHash === renderMode.slice(11));
26
+ if (!descriptor || new URL(url).search || descriptor.artifact !== route.ppr?.[pathname]) throw new Error('Native PPR artifact does not match the route');
27
+ const filename = path.resolve(distDir, descriptor.artifact);
28
+ if (!descriptor.artifact.startsWith('static/') || !filename.startsWith(path.resolve(distDir) + path.sep) || (await stat(filename)).size > NATIVE_PPR_ARTIFACT_BYTES) throw new Error('Invalid native PPR artifact path or size');
29
+ const bytes = await readFile(filename);
30
+ if (bytes.length > NATIVE_PPR_ARTIFACT_BYTES || sha256(bytes) !== descriptor.artifactHash) throw new Error('Native PPR artifact changed after publication');
31
+ const artifact = JSON.parse(bytes);
32
+ const prefix = Buffer.from(partialShell(artifact, route).replace(/<\/body><\/html>$/i, ''));
33
+ if (artifact.version !== 1 || !artifact.postponed || artifact.complete || artifact.generic || sha256(prefix) !== descriptor.prefixHash || sha256(artifact.flight) !== descriptor.identity) throw new Error('Invalid native PPR snapshot');
34
+ return {artifact, descriptor};
35
+ }
@@ -1,4 +1,4 @@
1
- Image renderer: @vercel/og 1.0.3 (https://github.com/vercel/og), with bundled Satori 0.33.5 (https://github.com/vercel/satori). PRNext replaces embedded fflate 0.7.3 with fflate 0.7.5, bundles it, adds Node CommonJS globals and includes adjacent WASM/font assets. Optional sharp backends require librsvg 2.63.2 or newer; otherwise rendering uses resvg. Upstream bundled licenses are retained. Rebuild: node scripts/prepare-og.mjs.
1
+ Image renderer: @vercel/og 1.0.3 (https://github.com/vercel/og), with bundled Satori 0.33.5 (https://github.com/vercel/satori). PRNext replaces embedded fflate 0.7.3 with fflate 0.7.5, bundles and minifies it with function/class names preserved, adds Node CommonJS globals and includes adjacent WASM/font assets. Optional sharp backends require librsvg 2.63.2 or newer; otherwise rendering uses resvg. Upstream bundled licenses are retained. Rebuild: node scripts/prepare-og.mjs.
2
2
  Geist Regular 1.800: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git), SIL OFL 1.1; LICENSE.Geist is the upstream OFL.txt.
3
3
  HarfBuzzJS 0.10.0: https://github.com/harfbuzz/harfbuzzjs; see LICENSE.harfbuzzjs.
4
4
  resvg-js 2.4.1: https://github.com/yisibl/resvg-js/tree/v2.4.1, MPL-2.0 (LICENSE).