@thomas.f/prnext 0.1.19-alpha → 0.1.21-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 (113) hide show
  1. package/build/app.mjs +46 -78
  2. package/build/build-lock.mjs +2 -71
  3. package/build/build-retention.mjs +119 -0
  4. package/build/build-timings.mjs +38 -0
  5. package/build/bundle-runtime.mjs +17 -16
  6. package/build/cache-command.mjs +31 -0
  7. package/build/check.mjs +41 -3
  8. package/build/compilation-cache.mjs +32 -12
  9. package/build/compilation-storage.mjs +190 -0
  10. package/build/compiler.mjs +5 -4
  11. package/build/config.mjs +23 -2
  12. package/build/create.mjs +1 -1
  13. package/build/css-modules.mjs +4 -0
  14. package/build/dev-action-graph.mjs +21 -0
  15. package/build/flight-summary.mjs +64 -0
  16. package/build/font-cache.mjs +9 -6
  17. package/build/font-files.mjs +1 -1
  18. package/build/index.mjs +57 -36
  19. package/build/inspect.mjs +3 -0
  20. package/build/inspection-summary.mjs +7 -2
  21. package/build/native-discovery.mjs +18 -6
  22. package/build/native-ppr.mjs +32 -0
  23. package/build/npm-packages.mjs +2 -2
  24. package/build/og.mjs +121 -1
  25. package/build/parallel-plan.mjs +55 -0
  26. package/build/persistent-cache.mjs +29 -24
  27. package/build/production-check.mjs +34 -0
  28. package/build/react-compiler-report.mjs +31 -8
  29. package/build/react-compiler.mjs +101 -10
  30. package/build/server-config.mjs +35 -0
  31. package/build/source-analysis.mjs +108 -5
  32. package/build/standalone-copy.mjs +48 -0
  33. package/build/standalone.mjs +2 -22
  34. package/build/static-adapter.mjs +7 -3
  35. package/build/transform-cache.mjs +17 -3
  36. package/build/transform.mjs +38 -6
  37. package/build/typescript-dev.mjs +3 -1
  38. package/cli.mjs +27 -5
  39. package/compat/app-layout-context.cjs +7 -3
  40. package/compat/diagnostics.cjs +5 -4
  41. package/compat/flight-delta.cjs +47 -12
  42. package/compat/index.d.ts +24 -1
  43. package/compat/navigation-authorization.cjs +8 -1
  44. package/compat/runtime-sample.cjs +56 -0
  45. package/compat/use-cache.cjs +5 -2
  46. package/docs/ai/compatibility.md +113 -10
  47. package/docs/ai/deployment.md +144 -0
  48. package/docs/ai/quickstart.md +5 -0
  49. package/docs/ai/troubleshooting.md +22 -0
  50. package/package.json +25 -22
  51. package/release/fontkit.mjs +31 -0
  52. package/release/verify.mjs +3 -0
  53. package/runtime/api.mjs +5 -2
  54. package/runtime/app-client.mjs +246 -54
  55. package/runtime/app-partial.mjs +2 -10
  56. package/runtime/app-prefetch.mjs +2 -2
  57. package/runtime/app-render.mjs +42 -13
  58. package/runtime/app-stream-channel.mjs +20 -6
  59. package/runtime/app-stream-html.mjs +30 -11
  60. package/runtime/app-styles.mjs +22 -4
  61. package/runtime/cache-components-rsc.mjs +26 -6
  62. package/runtime/cache-result.mjs +58 -0
  63. package/runtime/compile-cache-retention.mjs +53 -0
  64. package/runtime/compile-cache.mjs +28 -1
  65. package/runtime/dev-client.mjs +32 -8
  66. package/runtime/dev-compiler-broker.mjs +20 -23
  67. package/runtime/dev-gateway.mjs +23 -6
  68. package/runtime/dev-route-selection.mjs +64 -0
  69. package/runtime/dev-runtime-pool.mjs +3 -2
  70. package/runtime/dev-runtime-spare.mjs +1 -1
  71. package/runtime/dev-session-retention.mjs +154 -0
  72. package/runtime/development-flight.mjs +59 -0
  73. package/runtime/development-queue.mjs +33 -0
  74. package/runtime/development.mjs +37 -16
  75. package/runtime/env.mjs +9 -0
  76. package/runtime/flight-delta.mjs +31 -5
  77. package/runtime/flight-diagnostics.mjs +48 -0
  78. package/runtime/flight-tap.mjs +10 -5
  79. package/runtime/html-envelope.mjs +34 -0
  80. package/runtime/loopback-rpc.mjs +92 -0
  81. package/runtime/native-ppr.mjs +88 -0
  82. package/runtime/og/NOTICE.txt +1 -1
  83. package/runtime/og/index.node.mjs +76 -31216
  84. package/runtime/og/upstream.json +115 -0
  85. package/runtime/persistent/client.mjs +5 -3
  86. package/runtime/persistent/common.mjs +7 -18
  87. package/runtime/persistent/daemon.mjs +4 -12
  88. package/runtime/persistent/health.mjs +58 -0
  89. package/runtime/persistent/native-proxy.mjs +1 -1
  90. package/runtime/persistent/watchdog.mjs +12 -27
  91. package/runtime/prewarm.mjs +35 -0
  92. package/runtime/project-lock.mjs +71 -0
  93. package/runtime/request-transport.mjs +112 -32
  94. package/runtime/rsc-worker.mjs +58 -8
  95. package/runtime/storage-path.mjs +36 -0
  96. package/runtime/trace.mjs +44 -4
  97. package/runtime/worker.mjs +37 -17
  98. package/vendor/fontkit/NOTICE.txt +5 -0
  99. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  100. package/vendor/fontkit/index.mjs +3 -0
  101. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  102. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  103. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  104. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  105. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  106. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  108. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  109. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  110. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  111. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  112. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  113. package/vendor/fontkit/upstream.json +215 -0
@@ -20,11 +20,14 @@ 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';
24
25
  import {writeAtomicFile} from './atomic-file.mjs';
25
26
  import {lazyImportIdentifier,materializeDevImport} from './dev-lazy-imports.mjs';
26
27
  import {discardLazyBuild} from './dev-lazy-staging.mjs';
28
+ import {createDevelopmentQueue} from './development-queue.mjs';
27
29
  import {pruneDevelopmentStyles} from './dev-style-retention.mjs';
30
+ import {createDevelopmentSessionOwner,pruneDevelopmentSessions,createRetiredGenerationCleanup} from './dev-session-retention.mjs';
28
31
 
29
32
  const packageRoot=fileURLToPath(new URL('../',import.meta.url));
30
33
  export function developmentRoute(routes,raw,config={}) {
@@ -83,7 +86,9 @@ function serverOptions(args) {
83
86
  return {hostname,port,backend,hostAi,profile,safe};
84
87
  }
85
88
  export async function develop(projectRoot,args,binary) {
86
- return withBuildLock(projectRoot,root=>runDevelopment(root,args,binary),{development:true});
89
+ let entered=false;
90
+ try{return await withBuildLock(projectRoot,root=>{entered=true;return runDevelopment(root,args,binary);},{development:true});}
91
+ finally{if(entered)try{const {pruneCompilationStorage}=await import('../build/compilation-storage.mjs');await pruneCompilationStorage(projectRoot);}catch{/* Optional compilation-cache maintenance cannot change shutdown correctness. */}}
87
92
  }
88
93
  async function runDevelopment(projectRoot,args,binary) {
89
94
  const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
@@ -95,16 +100,21 @@ async function runDevelopment(projectRoot,args,binary) {
95
100
  // The session is ours; never clean a path obtained from application config.
96
101
  const container=path.join(root,'.prnext-dev'),info=await lstat(container).catch(error=>{if(error.code!=='ENOENT')throw error;});
97
102
  if(info&&(!info.isDirectory()||info.isSymbolicLink()))throw Error('Development directory must be a real directory');
103
+ await pruneDevelopmentSessions(root);
98
104
  await mkdir(home,{recursive:true});
105
+ const sessionOwner=createDevelopmentSessionOwner(home,{session});
106
+ if(options.safe)sessionOwner.disable();
99
107
  await mkdir(diagnosticsDirectory,{recursive:true,mode:0o700});
100
108
  let compiler,gateway,watcher,timer,tools,hostRelay,hostRelayTask,stopping=false,current,discovery,typeChecker,readyState={state:'building'},typeErrors=[],sequence=0;
101
109
  let dependencyIndexValid=false;
102
110
  const lifecycle=new AbortController();
103
- let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
111
+ const compilationQueue=createDevelopmentQueue({isStopping:()=>stopping});
112
+ let stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
104
113
  const failedPaths=new Set();
105
114
  const requestedImports=new Set();
106
115
  const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root,{directoryEventsAreRecursive:process.platform==='darwin',ignore:relative=>relative===output||relative.startsWith(output+'/')});
107
- const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
116
+ const retiredCleanup=createRetiredGenerationCleanup({root,home,gateway:{request:(...args)=>gateway.request(...args)},stop});
117
+ const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources,admission)=>serialized(async()=>{
108
118
  if(stopping)throw Error('Development is stopping');
109
119
  if(!discovery)await discover();
110
120
  const patterns=[...new Set([pattern,...sources])];
@@ -113,11 +123,11 @@ async function runDevelopment(projectRoot,args,binary) {
113
123
  for(const value of patterns)active.set(value,Date.now());
114
124
  if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
115
125
  return current.directory;
116
- })});
117
- const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
126
+ },admission)});
127
+ 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})});
118
128
  const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
119
129
  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)})),
120
- cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
130
+ 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}}});
121
131
  function captureOutput(child,source,{forwardStdout=true}={}){
122
132
  for(const [stream,level] of [[child.stdout,'info'],[child.stderr,'error']]){
123
133
  stream?.setEncoding('utf8');
@@ -151,12 +161,14 @@ async function runDevelopment(projectRoot,args,binary) {
151
161
  });
152
162
  stateQueue=task.catch(()=>{});return task;
153
163
  };
154
- const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
164
+ const serialized=(run,admission)=>compilationQueue.enqueue(run,admission);
155
165
  async function compilerRequest(type,values={}){
156
166
  if(!compiler||compiler.exitCode!==null||compiler.signalCode){
157
167
  const cache=await privateCacheDirectory(root,'dev-compiler-v8');
158
168
  await pruneCompilerCache(cache);
169
+ const ticket=sessionOwner.beforeSpawn();
159
170
  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}:{})}});
171
+ sessionOwner.spawned(ticket,compiler);
160
172
  captureOutput(compiler,'compiler');
161
173
  }
162
174
  const child=compiler;
@@ -174,14 +186,17 @@ async function runDevelopment(projectRoot,args,binary) {
174
186
  if(options.safe)discovery={...discovery,eager:true};
175
187
  const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
176
188
  await writeTypedRoutes(root,discovery.project,discovery.config);
177
- if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
189
+ 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)});}
178
190
  }
179
191
  async function launch(manifest,directory){
180
192
  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;});
193
+ if(!runtime)sessionOwner.disable();
181
194
  const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
182
195
  delete env.PRNEXT_DEV_BROKER;
183
196
  if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
197
+ const ticket=sessionOwner.beforeSpawn();
184
198
  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});
199
+ sessionOwner.spawned(ticket,child);
185
200
  captureOutput(child,'runtime',{forwardStdout:false});
186
201
  let output='';
187
202
  const port=await new Promise((resolve,reject)=>{
@@ -193,6 +208,8 @@ async function runDevelopment(projectRoot,args,binary) {
193
208
  return {id:manifest.buildId,child,runtime,port,directory};
194
209
  }
195
210
  async function rebuild(patterns,changedFiles=[],affectedRoutes,affectedModules,cause){
211
+ void retiredCleanup.retry();
212
+ if(retiredCleanup.pending>=64)throw Error('Development snapshots could not be removed. Close applications locking these files, then retry.');
196
213
  const startedAt=Date.now(),started=performance.now(),phases={};let mark=started;
197
214
  let reason=devBuildReason({initial:!current,safe:options.safe,changed:changedFiles,cause});
198
215
  await writeState({state:'building',reason});
@@ -233,7 +250,11 @@ async function runDevelopment(projectRoot,args,binary) {
233
250
  const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
234
251
  current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
235
252
  for(const id of requestedImports)if(!manifest.devLazy?.modules.some(item=>item.id===id))requestedImports.delete(id);
236
- const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
253
+ let affected;
254
+ if(dependencyIndexValid&&affectedRoutes) {
255
+ const nextAffected=await affectedDependencies(gateway,changedFiles).catch(()=>undefined);
256
+ affected=selectiveDevelopmentRoutes(previous?.manifest,manifest,{changed:changedFiles,affectedRoutes,affectedModules,nextAffected});
257
+ }
237
258
  current.affectedRoutes=affected;
238
259
  try{
239
260
  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)});
@@ -250,7 +271,7 @@ async function runDevelopment(projectRoot,args,binary) {
250
271
  while(generations.length>3){const old=generations.shift();
251
272
  // A deferred request may itself be awaiting this build. Never wait for
252
273
  // its generation lease while holding the serialized compiler queue.
253
- const cleanup=(async()=>{await gateway.request('retire',{generation:old.id});await old.runtime?.close();await stop(old.child);await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});})().catch(error=>{if(!stopping)console.warn('Development generation cleanup: '+error.message);}).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
274
+ const cleanup=retiredCleanup.enqueue(old).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
254
275
  }
255
276
  }
256
277
  await watcher?.refresh();
@@ -258,7 +279,7 @@ async function runDevelopment(projectRoot,args,binary) {
258
279
  }catch(error){if(!stopping){tools?.recordBuild({sequence,reason:reason.summary,reasonCode:reason.code,time:Date.now(),startedAt,duration:performance.now()-started,phases,state:'error'});tools?.addServerLog('error',error.message,{process:'compiler'});console.error('Build failed: '+error.message);await writeState({state:'error',reason,error:error.message});}throw error;}
259
280
  }
260
281
  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});
261
- async function ensure(raw,client,exact=false){
282
+ async function ensure(raw,client,exact=false,admission={}){
262
283
  if(new URL(raw,'http://local').pathname.startsWith('/_prnext/lazy/'))return serialized(async()=>{
263
284
  const id=lazyImportIdentifier(raw);
264
285
  if(!id||!current)throw Error('Unknown deferred import identifier');
@@ -270,7 +291,7 @@ async function runDevelopment(projectRoot,args,binary) {
270
291
  finally{try{await discardLazyBuild(root,built);}catch(error){if(failure){failure.cleanupError=error;failure.message+='; deferred compiler staging cleanup also failed';}else throw error;}}
271
292
  requestedImports.add(id);
272
293
  return {lazy};
273
- });
294
+ },admission);
274
295
  const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
275
296
  const warm=discovery&&resolve();
276
297
  if(!stopping&&current&&warm&&active.has(warm.pattern)&&current.patterns.has(warm.pattern)){
@@ -296,7 +317,7 @@ async function runDevelopment(projectRoot,args,binary) {
296
317
  try{await rebuild([...active.keys()]);}catch(error){if(route){failed.add(route.pattern);active.delete(route.pattern);}throw error;}
297
318
  }
298
319
  return updateInfo();
299
- });}
320
+ },admission);}
300
321
  async function changed(){
301
322
  try{
302
323
  const pending=[...changes];changes.clear();
@@ -329,8 +350,8 @@ async function runDevelopment(projectRoot,args,binary) {
329
350
  // Keep each native data-cache bridge alive until its externally owned
330
351
  // Node process has drained after() work and cache fills on stdin EOF.
331
352
  await runtimePool.close();
332
- await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
333
- await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
353
+ await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring,retiredCleanup.close()]);
354
+ await compilationQueue.idle();await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
334
355
  })();return closing;
335
356
  };
336
357
  const fatal=error=>{if(!stopping){console.error(error.message);process.exitCode=1;void shutdown();}};
@@ -344,7 +365,7 @@ async function runDevelopment(projectRoot,args,binary) {
344
365
  return {...updateInfo(),...result};
345
366
  })});
346
367
  await writeState({state:'building',session});
347
- gateway=new DevGateway(binary,root,{...options,onEnsure:ensure,onFatal:fatal,
368
+ gateway=new DevGateway(binary,root,{...options,onEnsure:(raw,client,admission)=>ensure(raw,client,false,admission),onFatal:fatal,onBeforeChild:()=>sessionOwner.beforeSpawn(),onChild:(child,ticket)=>sessionOwner.spawned(ticket,child),
348
369
  onOpen:message=>{const route=discovery&&findRoute(message.path);if(route)pins.set(message.client,{pattern:route.pattern,connection:message.connection});},
349
370
  onClose:message=>{if(pins.get(message.client)?.connection===message.connection)pins.delete(message.client);}});
350
371
  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,5 +1,5 @@
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
5
  export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now,setTimer=setTimeout,clearTimer=clearTimeout}={}) {
@@ -39,7 +39,34 @@ export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000
39
39
  };
40
40
  }
41
41
  const store=createFlightStore();
42
- 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}={}) {
43
70
  const compressed=acceptEncoding.split(',').some(value=>{
44
71
  const [name,...parameters]=value.trim().toLowerCase().split(';');
45
72
  return /^(gzip|deflate|br|zstd|\*)$/.test(name)&&!parameters.some(p=>/^\s*q\s*=\s*0(?:\.0*)?\s*$/.test(p));
@@ -48,8 +75,7 @@ export function compressedDeltaPolicy(acceptEncoding='') {
48
75
  // This conservative, bounded probe is not the response compressor. It avoids
49
76
  // choosing thousands of references to JSON that gzip already compresses well.
50
77
  // A large/expensive chunk keeps ordinary Flight instead of delaying a flush.
51
- return (raw,encoded)=>raw.length<=64*1024&&encoded.length+32<raw.length&&
52
- deflateRawSync(encoded,{level:1}).length+32<deflateRawSync(raw,{level:1}).length*0.9;
78
+ return (raw,encoded)=>compressionProbe(raw,encoded,signal);
53
79
  }
54
80
  export async function differentialFlight(response,options,cache=store) {
55
81
  const headers=new Headers(options.headers), outgoing=new Headers(response.headers);
@@ -64,7 +90,7 @@ export async function differentialFlight(response,options,cache=store) {
64
90
  controller.enqueue(raw instanceof Uint8Array?raw:raw instanceof ArrayBuffer?new Uint8Array(raw):new TextEncoder().encode(String(raw??'')));controller.close();
65
91
  }});
66
92
  const collected=collectStream(body,{capture:!!releaseCapture,onComplete:bytes=>cache.set(token,identity,bytes),onFinally:releaseCapture});
67
- 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};
68
94
  if(!encoded.delta)releaseEncoder?.();
69
95
  else encoded.body=collectStream(encoded.body,{capture:false,onFinally:releaseEncoder});
70
96
  }catch(error){releaseEncoder?.();releaseCapture?.();throw error;}
@@ -0,0 +1,48 @@
1
+ import diagnostics from '../compat/diagnostics.cjs';
2
+
3
+ const phases = new Set(['ssr', 'rsc']);
4
+ const outcomes = new Set(['completed', 'failed', 'cancelled', 'timedOut']);
5
+ const count = value => Number.isSafeInteger(value) && value >= 0 ? Math.min(value, 32 * 1024 * 1024) : 0;
6
+
7
+ // Disabled in the ordinary request path. No body, URL, parameters, nonce, model,
8
+ // client references or request context is retained, even when enabled. Timings
9
+ // include asynchronous rendering/waiting; they are NOT isolated encoder CPU.
10
+ export function createFlightMeasurement(request, phase, options) {
11
+ if (!(options?.enabled ?? diagnostics.enabled) || !phases.has(phase)) return;
12
+ const now = options?.now ?? (() => performance.now());
13
+ const record = options?.record ?? diagnostics.record;
14
+ const started = now();
15
+ const route = typeof request?.routePattern === 'string' ? request.routePattern.slice(0, 256) : '<unattributed>';
16
+ const method = /^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/.test(request?.method) ? request.method : '_OTHER';
17
+ let ended = false, firstChunkMs, modelReadyMs, shellReadyMs, ppr;
18
+ const elapsed = () => Math.max(0, Math.min(86400000, now() - started));
19
+ const metrics = {
20
+ flightBytes: 0, flightChunks: 0, responseBytes: 0, peakQueuedBytes: 0,
21
+ chunk(bytes) {
22
+ if (ended) return;
23
+ firstChunkMs ??= elapsed();
24
+ metrics.flightBytes += count(bytes);
25
+ metrics.flightChunks++;
26
+ },
27
+ mark(name) {
28
+ if (ended) return;
29
+ if (name === 'modelReady') modelReadyMs ??= elapsed();
30
+ else if (name === 'shellReady') shellReadyMs ??= elapsed();
31
+ },
32
+ partial(mode) { if (!ended && ['direct', 'normalized'].includes(mode)) ppr = mode; },
33
+ finish(outcome, status) {
34
+ if (ended || !outcomes.has(outcome)) return;
35
+ ended = true;
36
+ const event = { route, method, phase, outcome, durationMs: elapsed(),
37
+ flightBytes: count(metrics.flightBytes), flightChunks: count(metrics.flightChunks),
38
+ responseBytes: count(metrics.responseBytes), peakQueuedBytes: count(metrics.peakQueuedBytes),
39
+ ...(Number.isInteger(status) && status >= 100 && status <= 599 ? {status} : {}),
40
+ ...(firstChunkMs !== undefined ? {firstChunkMs} : {}),
41
+ ...(modelReadyMs !== undefined ? {modelReadyMs} : {}),
42
+ ...(shellReadyMs !== undefined ? {shellReadyMs} : {}), ...(ppr ? {ppr} : {}) };
43
+ // Diagnostics must never replace a response/error with an export failure.
44
+ try { record('flight', event); } catch {}
45
+ },
46
+ };
47
+ return metrics;
48
+ }
@@ -1,7 +1,7 @@
1
1
  // Share Flight allocations with the SSR decoder, then group only already
2
2
  // available bootstrap bytes. Never wait for a future Suspense chunk to fill a
3
3
  // block. The queue's pre-shell ceiling remains the overall response budget.
4
- export function flightTap(source) {
4
+ export function flightTap(source, metrics) {
5
5
  const reader=source.getReader(),queue=[];
6
6
  let head=0,bytes=0,done=false,failure,outputStarted=false,wakeData,wakeSpace,cancelled;
7
7
  const finish=error=>{done=true;failure=error;wakeData?.();wakeData=undefined;wakeSpace?.();wakeSpace=undefined;};
@@ -11,17 +11,22 @@ export function flightTap(source) {
11
11
  while(!done&&outputStarted&&bytes>=128*1024)await new Promise(resolve=>{wakeSpace=resolve;});
12
12
  if(done){if(failure)controller.error(failure);else controller.close();return;}
13
13
  const next=await reader.read();if(next.done){finish();controller.close();return;}
14
- queue.push(next.value);bytes+=next.value.byteLength;wakeData?.();wakeData=undefined;controller.enqueue(next.value);
14
+ queue.push(next.value);bytes+=next.value.byteLength;
15
+ if(metrics){metrics.chunk(next.value.byteLength);metrics.peakQueuedBytes=Math.max(metrics.peakQueuedBytes,bytes);}
16
+ wakeData?.();wakeData=undefined;controller.enqueue(next.value);
15
17
  }catch(error){finish(error);controller.error(error);}},cancel,
16
18
  },{highWaterMark:0});
17
19
  return {body,cancel,startOutput(){outputStarted=true;},async next(){
18
20
  while(head===queue.length&&!done)await new Promise(resolve=>{wakeData=resolve;});
19
21
  if(head<queue.length){
20
- const parts=[];let size=0;
21
- do{const value=queue[head];queue[head++]=undefined;parts.push(value);size+=value.byteLength;}while(head<queue.length&&size+queue[head].byteLength<=32*1024);
22
+ const first=queue[head];queue[head++]=undefined;
23
+ let size=first.byteLength,parts;
24
+ while(head<queue.length&&size+queue[head].byteLength<=32*1024){
25
+ parts??=[first];const value=queue[head];queue[head++]=undefined;parts.push(value);size+=value.byteLength;
26
+ }
22
27
  bytes-=size;if(head===queue.length){queue.length=0;head=0;}else if(head>=64){queue.splice(0,head);head=0;}
23
28
  wakeSpace?.();wakeSpace=undefined;
24
- return {value:parts.length===1?parts[0]:Buffer.concat(parts,size),done:false};
29
+ return {value:parts?Buffer.concat(parts,size):first,done:false};
25
30
  }
26
31
  if(failure)throw failure;return {done:true};
27
32
  }};
@@ -1,9 +1,39 @@
1
1
  import { MAX_RESPONSE_BYTES } from './http.mjs';
2
2
 
3
+ const flightScriptEndText = '");document.currentScript.remove()</script>';
4
+ const flightScriptEnd = Buffer.from(flightScriptEndText);
5
+
6
+ // nonceAttribute is already escaped by the response's HTML renderer. Keep its
7
+ // tiny prefix per response, not a full base64 script string for each chunk.
8
+ // The returned Buffer is owned by the caller and can go straight to transport.
9
+ export function flightBootstrap(nonceAttribute = '') {
10
+ const start = `<script${nonceAttribute}>(self.__PRNEXT_FLIGHT_STREAM__||=[]).push("`;
11
+ let prefix, first = true;
12
+ return value => {
13
+ if (!(value instanceof Uint8Array) || value.byteLength > MAX_RESPONSE_BYTES) throw new TypeError('Invalid Flight bootstrap bytes');
14
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
15
+ const encoded = bytes.toString('base64');
16
+ // Most tiny responses finish with one frame. Keep their short original
17
+ // encoding path instead of paying for a reusable prefix plus three writes.
18
+ if (first && bytes.byteLength <= 2048) { first = false; return Buffer.from(start + encoded + flightScriptEndText); }
19
+ first = false;
20
+ prefix ??= Buffer.from(start);
21
+ const result = Buffer.allocUnsafe(prefix.length + encoded.length + flightScriptEnd.length);
22
+ prefix.copy(result);
23
+ result.write(encoded, prefix.length, encoded.length, 'ascii');
24
+ flightScriptEnd.copy(result, prefix.length + encoded.length);
25
+ return result;
26
+ };
27
+ }
28
+
3
29
  // A raw element cannot be interrupted with a Flight script. Keep its chunks
4
30
  // until its closing tag arrives, but only scan newly received characters.
5
31
  // Joining and rescanning a growing inline script on every chunk is quadratic.
6
32
  const whitespace = /\s/;
33
+ // Complete, unquoted ordinary markup needs no per-tag JS scan. Reject every
34
+ // raw/document/comment candidate (including split prefixes) before this path.
35
+ // Quoted attributes and incomplete tags keep the general streaming parser.
36
+ const specialMarkup = /<(?:!|\?|\/(?:body|html)|script|style|textarea|title|svg|math)/i;
7
37
  function holdRaw(state, text) {
8
38
  if (!text) return;
9
39
  // Compact small chunks in tiers: even single-character input cannot retain
@@ -60,6 +90,10 @@ export function htmlEnvelope({ resumed = false } = {}) {
60
90
  append(text) {
61
91
  received += Buffer.byteLength(text);
62
92
  if (received > MAX_RESPONSE_BYTES) throw new Error('HTML response exceeds the 16 MiB PRNext limit');
93
+ if (!pending && !rawPending) {
94
+ if (!text.includes('<') || !text.includes('"') && !text.includes("'") &&
95
+ !specialMarkup.test(text) && text.lastIndexOf('>') > text.lastIndexOf('<')) return text;
96
+ }
63
97
  let ready = '';
64
98
  if (rawPending) {
65
99
  const end = rawEnd(text, rawPending);
@@ -0,0 +1,92 @@
1
+ import net from 'node:net';
2
+ import {createHmac,randomBytes,timingSafeEqual} from 'node:crypto';
3
+
4
+ const protocol='prnext-loopback-v2',noncePattern=/^[a-f0-9]{64}$/;
5
+ const nonce=()=>randomBytes(32).toString('hex');
6
+ const authenticationError=()=>Object.assign(Error('Loopback peer authentication failed'),{code:'ERR_PRNEXT_PEER_AUTH'});
7
+ const proof=(token,context,role,client,server,...body)=>createHmac('sha256',token).update(JSON.stringify([protocol,context,role,client,server,...body])).digest('hex');
8
+ function matches(supplied,expected){return typeof supplied==='string'&&noncePattern.test(supplied)&&timingSafeEqual(Buffer.from(supplied,'hex'),Buffer.from(expected,'hex'));}
9
+ function encode(value,limit){const body=JSON.stringify(value);if(typeof body!=='string'||Buffer.byteLength(body)>limit)throw Error('Loopback control body is too large');return body;}
10
+ function send(socket,value){socket.write(JSON.stringify({protocol,...value})+'\n');}
11
+
12
+ function frames(socket,limit,receive,fail){
13
+ let buffer=Buffer.alloc(0);
14
+ socket.on('data',chunk=>{
15
+ try{
16
+ buffer=Buffer.concat([buffer,chunk]);
17
+ for(;;){
18
+ const end=buffer.indexOf(10),maximum=limit();
19
+ if(end<0){if(buffer.length>maximum)throw Error('Loopback control frame is too large');return;}
20
+ if(end>maximum)throw Error('Loopback control frame is too large');
21
+ const message=JSON.parse(buffer.subarray(0,end).toString('utf8'));buffer=buffer.subarray(end+1);
22
+ if(!message||message.protocol!==protocol)throw authenticationError();
23
+ receive(message);if(socket.destroyed)return;
24
+ }
25
+ }catch(error){fail(error);}
26
+ });
27
+ }
28
+
29
+ // One fresh transcript authorizes exactly one payload. Role and context labels
30
+ // prevent reflection and cross-service reuse; both nonces prevent replay across
31
+ // connections and watchdog restarts. The first client frame contains only a
32
+ // nonce; the command follows a verified server proof. The key is never sent.
33
+ export function requestLoopback({port,token},request,{context,timeout=120000,signal,maxRequestBytes=65536,maxResponseBytes=1024*1024}={}) {
34
+ if(signal?.aborted)return Promise.reject(signal.reason||Error('Loopback request aborted'));
35
+ let body;try{body=encode(request,maxRequestBytes);}catch(error){return Promise.reject(error);}
36
+ return new Promise((resolve,reject)=>{
37
+ const socket=net.connect({host:'127.0.0.1',port});
38
+ const client=nonce();let server,state='challenge',finished=false;
39
+ const finish=(error,value)=>{if(finished)return;finished=true;clearTimeout(handshake);clearTimeout(total);signal?.removeEventListener('abort',abort);socket.destroy();error?reject(error):resolve(value);};
40
+ const abort=()=>finish(signal.reason||Error('Loopback request aborted'));
41
+ const total=setTimeout(()=>finish(Error('Loopback request timed out')),timeout);
42
+ const handshake=setTimeout(()=>finish(authenticationError()),Math.min(5000,timeout));
43
+ signal?.addEventListener('abort',abort,{once:true});
44
+ socket.once('connect',()=>send(socket,{type:'hello',client}));
45
+ socket.on('error',error=>finish(error));
46
+ socket.on('end',()=>finish(state==='challenge'?authenticationError():Error('Loopback peer disconnected')));
47
+ socket.on('close',()=>{if(!finished)finish(state==='challenge'?authenticationError():Error('Loopback peer disconnected'));});
48
+ frames(socket,()=>state==='challenge'?1024:2*maxResponseBytes+1024,message=>{
49
+ if(state==='challenge'){
50
+ if(message.type!=='challenge'||typeof message.server!=='string'||!noncePattern.test(message.server)||!matches(message.proof,proof(token,context,'server',client,message.server)))throw authenticationError();
51
+ server=message.server;state='response';clearTimeout(handshake);
52
+ send(socket,{type:'request',body,proof:proof(token,context,'request',client,server,body)});
53
+ return;
54
+ }
55
+ if(state!=='response'||message.type!=='response'||typeof message.body!=='string'||Buffer.byteLength(message.body)>maxResponseBytes||!matches(message.proof,proof(token,context,'response',client,server,body,message.body)))throw authenticationError();
56
+ const result=JSON.parse(message.body);state='done';
57
+ if(!result||typeof result.ok!=='boolean')throw Error('Invalid loopback response');
58
+ finish(result.ok?null:Error(typeof result.error==='string'?result.error:'Loopback request failed'),result.value);
59
+ },error=>finish(error));
60
+ });
61
+ }
62
+
63
+ export function serveLoopback(socket,{token,context,handle,timeout=125000,handshakeTimeout=5000,maxRequestBytes=65536,maxResponseBytes=1024*1024}) {
64
+ const controller=new AbortController();let client,server,requestBody,state='hello';
65
+ const deadlineMs=Date.now()+timeout;
66
+ const close=()=>{state='closed';controller.abort();socket.destroy();};
67
+ const total=setTimeout(close,timeout),handshake=setTimeout(close,Math.min(handshakeTimeout,timeout));
68
+ socket.on('error',()=>{});
69
+ socket.once('close',()=>{state='closed';clearTimeout(total);clearTimeout(handshake);controller.abort();});
70
+ socket.on('end',close);
71
+ frames(socket,()=>state==='hello'?1024:2*maxRequestBytes+1024,message=>{
72
+ if(state==='hello'){
73
+ if(message.type!=='hello'||typeof message.client!=='string'||!noncePattern.test(message.client))throw authenticationError();
74
+ client=message.client;server=nonce();state='request';
75
+ send(socket,{type:'challenge',server,proof:proof(token,context,'server',client,server)});return;
76
+ }
77
+ if(state!=='request'||message.type!=='request'||typeof message.body!=='string'||Buffer.byteLength(message.body)>maxRequestBytes||!matches(message.proof,proof(token,context,'request',client,server,message.body)))throw authenticationError();
78
+ requestBody=message.body;const request=JSON.parse(requestBody);
79
+ if(!request||typeof request!=='object'||Array.isArray(request))throw Error('Invalid loopback request');
80
+ clearTimeout(handshake);state='busy';
81
+ const reply=result=>{
82
+ if(state!=='busy')return;
83
+ let body;
84
+ try{body=encode(result,maxResponseBytes);}catch{body=encode({ok:false,error:'Loopback response is too large'},maxResponseBytes);}
85
+ state='done';socket.end(JSON.stringify({protocol,type:'response',body,proof:proof(token,context,'response',client,server,requestBody,body)})+'\n');
86
+ };
87
+ Promise.resolve().then(()=>{
88
+ if(controller.signal.aborted)throw Error('Loopback client disconnected');
89
+ return handle(request,{signal:controller.signal,deadlineMs});
90
+ }).then(value=>reply({ok:true,value}),error=>reply({ok:false,error:String(error.message).slice(0,2048)})).catch(close);
91
+ },close);
92
+ }
@@ -0,0 +1,88 @@
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
+ // Only immutable build JSON is retained, after the caller verified the current
11
+ // file bytes. No live model, decoder, request context or continuation is cached.
12
+ export class NativePprArtifactCache {
13
+ constructor({maxEntries=8,maxBytes=4*1024*1024,maxEntryBytes=1024*1024,maxNodes=8192}={}) {
14
+ for(const value of [maxEntries,maxBytes,maxEntryBytes,maxNodes])if(!Number.isSafeInteger(value)||value<0)throw new TypeError('Invalid native PPR parsing cache budget');
15
+ this.maxEntries=maxEntries;this.maxBytes=maxBytes;this.maxEntryBytes=maxEntryBytes;this.maxNodes=maxNodes;
16
+ this.entries=new Map();this.bytes=0;this.hits=0;this.parses=0;
17
+ }
18
+ parse(filename,bytes,route,descriptor) {
19
+ const key=JSON.stringify([filename,descriptor.artifactHash,route.client||null]);
20
+ let entry=this.entries.get(key);
21
+ if(entry){this.hits++;this.entries.delete(key);this.entries.set(key,entry);}
22
+ else{
23
+ this.parses++;
24
+ const artifact=JSON.parse(bytes);
25
+ const prefix=Buffer.from(partialShell(artifact,route).replace(/<\/body><\/html>$/i,''));
26
+ entry={artifact,prefixHash:sha256(prefix),identity:sha256(artifact.flight)};
27
+ if(artifact.version!==1||!artifact.postponed||artifact.complete||artifact.generic||entry.prefixHash!==descriptor.prefixHash||entry.identity!==descriptor.identity)throw new Error('Invalid native PPR snapshot');
28
+ // Reserve substantial expansion for JSON slots/strings before walking.
29
+ // This is a conservative admission estimate, not an exact V8 heap size.
30
+ let size=bytes.length*16+key.length*2+256;
31
+ if(this.maxEntries&&size<=Math.min(this.maxBytes,this.maxEntryBytes)){
32
+ const pending=[artifact],objects=[];let nodes=1,eligible=this.maxNodes>0;
33
+ while(eligible&&pending.length){
34
+ const value=pending.pop();objects.push(value);size+=128;
35
+ // JSON arrays are dense: reject wide arrays before enumerating keys
36
+ // or allocating any list of their primitive/object children.
37
+ if(Array.isArray(value)&&value.length>this.maxNodes-nodes){eligible=false;break;}
38
+ for(const name in value){
39
+ if(!Object.hasOwn(value,name))continue;
40
+ if(++nodes>this.maxNodes){eligible=false;break;}
41
+ const child=value[name];size+=16+name.length*2+(typeof child==='string'?child.length*2:0);
42
+ if(size>Math.min(this.maxBytes,this.maxEntryBytes)){eligible=false;break;}
43
+ if(child&&typeof child==='object'){
44
+ if(Array.isArray(child)&&child.length>this.maxNodes-nodes){eligible=false;break;}
45
+ pending.push(child);
46
+ }
47
+ }
48
+ }
49
+ if(eligible&&!pending.length&&size<=Math.min(this.maxBytes,this.maxEntryBytes)){
50
+ for(const value of objects)Object.freeze(value);
51
+ while(this.entries.size&&(this.entries.size>=this.maxEntries||this.bytes+size>this.maxBytes)){
52
+ const [oldKey,old]=this.entries.entries().next().value;this.entries.delete(oldKey);this.bytes-=old.size;
53
+ }
54
+ entry.size=size;this.entries.set(key,entry);this.bytes+=size;
55
+ }
56
+ }
57
+ }
58
+ // Descriptor bindings remain authoritative even when the public JSON hit.
59
+ if(entry.prefixHash!==descriptor.prefixHash||entry.identity!==descriptor.identity)throw new Error('Invalid native PPR snapshot');
60
+ return entry.artifact;
61
+ }
62
+ }
63
+ const parsedArtifacts=new NativePprArtifactCache();
64
+
65
+ export function partialShell(artifact, route) {
66
+ const bootstrap = '<script>self.__PRNEXT_FLIGHT_STREAM__=self.__PRNEXT_FLIGHT_STREAM__||[];</script>' +
67
+ (route.client ? `<script type="module" async src="${escapeHtml(route.client)}"></script>` : '');
68
+ const end = artifact.shell.lastIndexOf('</body>');
69
+ if (end < 0 || !/<\/body><\/html>$/i.test(artifact.shell)) throw new Error('Invalid partial prerender document');
70
+ return artifact.shell.slice(0, end) + bootstrap + artifact.shell.slice(end);
71
+ }
72
+
73
+ // This is a private transport field, never an HTTP header. Rust has already
74
+ // checked cache generation and committed these exact build bytes. Pinning the
75
+ // same artifact gives the request one snapshot even if invalidation races Node.
76
+ export async function nativePprArtifact({renderMode, manifest, route, url, distDir},cache=parsedArtifacts) {
77
+ if (!renderMode?.startsWith('native-ppr:')) return null;
78
+ if (!manifest.config?.nativePprShell || manifest.dev || !/^[a-f\d]{64}$/.test(renderMode.slice(11))) throw new Error('Invalid native PPR continuation');
79
+ const pathname = new URL(url).pathname;
80
+ const descriptor = manifest.config.nativePprArtifacts?.find(item => item.routeId === route.id && item.path === pathname && item.artifactHash === renderMode.slice(11));
81
+ if (!descriptor || new URL(url).search || descriptor.artifact !== route.ppr?.[pathname]) throw new Error('Native PPR artifact does not match the route');
82
+ const filename = path.resolve(distDir, descriptor.artifact);
83
+ 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');
84
+ const bytes = await readFile(filename);
85
+ if (bytes.length > NATIVE_PPR_ARTIFACT_BYTES || sha256(bytes) !== descriptor.artifactHash) throw new Error('Native PPR artifact changed after publication');
86
+ const artifact = cache.parse(filename,bytes,route,descriptor);
87
+ return {artifact, descriptor};
88
+ }
@@ -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).