@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
@@ -32,6 +32,7 @@ import { PartialTemplates, canResumePartialDirectly, partialTemplateReuse } from
32
32
  import { createAfterScope } from '../compat/after.cjs';
33
33
  import {renderKeyFor} from './app-render-key.mjs';
34
34
  import { selectAppStyles } from './app-style-selection.mjs';
35
+ import {createFlightMeasurement} from './flight-diagnostics.mjs';
35
36
 
36
37
  installFetchCache();
37
38
  if (process.env.PRNEXT_INSPECT_DIR) parentPort.postMessage({type:'rsc-ready'});
@@ -173,7 +174,24 @@ function errorBoundary(entry, error, production, before = Infinity) {
173
174
  return index < 0 ? null : { kind: 'error', index, error: publicError(error, production) };
174
175
  }
175
176
 
176
- async function renderFlight(request, fallback = null, channel = null) {
177
+ function renderFlight(request, fallback = null, channel = null) {
178
+ const measurement=createFlightMeasurement(request,'rsc');
179
+ if(!measurement)return renderFlightModel(request,fallback,channel);
180
+ return observeFlightModel(request,fallback,channel,measurement);
181
+ }
182
+ async function observeFlightModel(request,fallback,channel,measurement){
183
+ let status;
184
+ try{
185
+ const result=await renderFlightModel(request,fallback,channel,measurement,measurement?value=>{status=value;}:undefined);
186
+ measurement?.finish('completed',result?.status??status);
187
+ return result;
188
+ }catch(error){
189
+ const reason=channel?.signal?.reason;
190
+ measurement?.finish(error?.statusCode===504||reason?.statusCode===504?'timedOut':channel?.signal?.aborted?'cancelled':'failed',status??error?.statusCode);
191
+ throw error;
192
+ }
193
+ }
194
+ async function renderFlightModel(request, fallback, channel,measurement,onStatus) {
177
195
  return runRequestContext({ ...request, phase: 'render' }, async () => {
178
196
  const cacheContext = currentRequest();
179
197
  try {
@@ -220,7 +238,9 @@ async function renderFlight(request, fallback = null, channel = null) {
220
238
  const stored = request.production
221
239
  ? await partialTemplates.get(request.partialFlight, consumer.serverConsumerManifest, decode, renderSignal)
222
240
  : await decode();
223
- const live = !model.routing && canResumePartialDirectly(stored) ? model : await createFromReadableStream(boundedPartialFlight(renderToReadableStream(model, bundlerConfig, {
241
+ const direct=!model.routing&&canResumePartialDirectly(stored);
242
+ measurement?.partial(direct?'direct':'normalized');
243
+ const live = direct ? model : await createFromReadableStream(boundedPartialFlight(renderToReadableStream(model, bundlerConfig, {
224
244
  signal: renderSignal,
225
245
  onError(error) { return publicError(error, request.production).digest; },
226
246
  })), consumer);
@@ -295,9 +315,14 @@ async function renderFlight(request, fallback = null, channel = null) {
295
315
  let offset = 0;
296
316
  for (const item of batch) { value.set(item, offset); offset += item.byteLength; }
297
317
  }
298
- if (!channel.started) channel.start({ status: request.partialFlight ? 200 : fallback?.kind === 'not-found' || (entry.routing && renderError?.digest === 'NEXT_HTTP_ERROR_FALLBACK;404') ? 404 : renderError || fallback?.kind === 'error' ? 500 : request.responseStatus || 200,
299
- rscError: Boolean(ordinaryRenderError(renderError)), boundaryIndex: fallback?.index, headers: request.responseHeaders });
318
+ if (!channel.started) {
319
+ const status=request.partialFlight?200:fallback?.kind==='not-found'||(entry.routing&&renderError?.digest==='NEXT_HTTP_ERROR_FALLBACK;404')?404:renderError||fallback?.kind==='error'?500:request.responseStatus||200;
320
+ onStatus?.(status);
321
+ channel.start({status,rscError:Boolean(ordinaryRenderError(renderError)),boundaryIndex:fallback?.index,headers:request.responseHeaders});
322
+ }
323
+ const emittedBytes=measurement?value.byteLength:0;
300
324
  await channel.write(value, abort.signal, Boolean(next.result?.done));
325
+ measurement?.chunk(emittedBytes);
301
326
  continue;
302
327
  }
303
328
  // The byte stream hands ownership of each Uint8Array to its reader.
@@ -315,7 +340,7 @@ async function renderFlight(request, fallback = null, channel = null) {
315
340
  if (channel?.started && renderError.statusCode !== 504) { channel.end(); return; }
316
341
  if (renderError.digest === 'NEXT_HTTP_ERROR_FALLBACK;404' && !entry.routing) {
317
342
  const boundary = (entry.segments || []).findLastIndex((segment, index) => index < (fallback?.index ?? Infinity) && segment.notFound);
318
- if (boundary >= 0) return renderFlight({ ...request, cacheState: cacheContext.cacheState, staticState: cacheContext.staticState }, { kind: 'not-found', index: boundary }, channel);
343
+ if (boundary >= 0) return renderFlightModel({ ...request, cacheState: cacheContext.cacheState, staticState: cacheContext.staticState }, { kind: 'not-found', index: boundary }, channel,measurement,onStatus);
319
344
  } else if (ordinaryRenderError(renderError)) {
320
345
  if (cacheContext.staticState) throw renderError;
321
346
  // Preserve the original errored Flight tree. React's browser
@@ -329,7 +354,7 @@ async function renderFlight(request, fallback = null, channel = null) {
329
354
  }
330
355
  }
331
356
  if (channel) {
332
- if (!channel.started) channel.start({ status: request.responseStatus || 200, headers: request.responseHeaders });
357
+ if (!channel.started) {onStatus?.(request.responseStatus||200);channel.start({ status: request.responseStatus || 200, headers: request.responseHeaders });}
333
358
  channel.end();
334
359
  return;
335
360
  }
@@ -340,6 +365,7 @@ async function renderFlight(request, fallback = null, channel = null) {
340
365
  for (const chunk of chunks) {
341
366
  body.set(chunk, offset);
342
367
  offset += chunk.byteLength;
368
+ measurement?.chunk(chunk.byteLength);
343
369
  }
344
370
  return { body: body.buffer, status: staticNavigation?.status || (fallback?.kind === 'not-found' || (entry.routing && renderError?.digest === 'NEXT_HTTP_ERROR_FALLBACK;404') ? 404 : renderError || fallback?.kind === 'error' ? 500 : request.responseStatus || 200),
345
371
  rscError: Boolean(ordinaryRenderError(renderError)), boundaryIndex: fallback?.index, headers: { ...request.responseHeaders, ...staticNavigation?.headers },
@@ -409,11 +435,35 @@ const activeRequests = new Set();
409
435
  const responseCompletions = new Map();
410
436
  const manifests = new Map();
411
437
  const artifacts = new Map();
438
+ const preloading = new Map();
412
439
  parentPort.on('message', async ({ id, request, type, value, token }) => {
413
440
  if(type==='telemetry-flush'){await drainTraces();parentPort.postMessage({type:'telemetry-flushed',token});return;}
441
+ if(type==='preload-cancel'){preloading.get(id)?.abort(new Error('Framework prewarm was cancelled'));return;}
414
442
  if(type==='preload'){
415
- try{await preloadModules(value.files,value.options,file=>routeEntry({modulePath:file,production:true}));parentPort.postMessage({type:'preloaded',id});}
416
- catch(error){parentPort.postMessage({type:'preloaded',id,error:error.message});}return;
443
+ const abort=new AbortController();preloading.set(id,abort);let timer,reader,reply,transfer;
444
+ try{
445
+ if(value.options.frameworkWarmup){
446
+ if(value.files.length||!Number.isFinite(value.options.timeoutMs)||value.options.timeoutMs<=0||value.options.timeoutMs>30000||!Number.isFinite(value.options.maxRssMb)||value.options.maxRssMb<=0||value.options.maxRssMb>2048)throw new TypeError('Invalid synthetic renderer prewarm');
447
+ const deadline=performance.now()+value.options.timeoutMs;
448
+ const available=()=>{abort.signal.throwIfAborted();if(performance.now()>=deadline||process.memoryUsage().rss>=value.options.maxRssMb*1024*1024)throw new Error('Synthetic renderer prewarm budget exhausted');};
449
+ timer=setTimeout(()=>abort.abort(new Error('Synthetic renderer prewarm timed out')),value.options.timeoutMs);
450
+ available();
451
+ // Host elements and literals only: no routeEntry, application imports,
452
+ // fetches, caches, contexts or handlers participate in this warm-up.
453
+ const model={tree:React.createElement('html',null,React.createElement('head'),React.createElement('body',null,React.createElement('main',null,React.createElement('ul',null,Array.from({length:8},(_,index)=>React.createElement('li',{key:index},'PRNext framework '+index)))))),router:{pathname:'/_prnext-prewarm',query:{}}};
454
+ reader=renderToReadableStream(model,{}, {signal:abort.signal,onError:()=> 'PRNEXT_FRAMEWORK_PREWARM'}).getReader();
455
+ const chunks=[];let length=0;
456
+ for(;;){available();const next=await reader.read();if(next.done)break;length+=next.value.byteLength;if(length>65536)throw new Error('Synthetic renderer prewarm Flight exceeds its byte budget');chunks.push(new Uint8Array(next.value));}
457
+ available();const bytes=new Uint8Array(length);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}
458
+ await reader.cancel();reader=undefined;
459
+ reply={type:'preloaded',id,frameworkFlight:bytes.buffer};transfer=[bytes.buffer];
460
+ }else{
461
+ abort.signal.throwIfAborted();await preloadModules(value.files,value.options,file=>routeEntry({modulePath:file,production:true}));abort.signal.throwIfAborted();reply={type:'preloaded',id};
462
+ }
463
+ }catch(error){reply={type:'preloaded',id,error:error.message};}
464
+ finally{clearTimeout(timer);await reader?.cancel().catch(()=>{});preloading.delete(id);}
465
+ parentPort.postMessage(reply,transfer);
466
+ return;
417
467
  }
418
468
  if (type === 'after-response') { responseCompletions.get(id)?.(); return; }
419
469
  // Only the host arms these probes, while completed responses still have
@@ -0,0 +1,36 @@
1
+ import {lstat, realpath} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const normalized = filename => process.platform === 'win32' ? path.resolve(filename).toLowerCase() : path.resolve(filename);
5
+ export const sameStoragePath = (left, right) => normalized(left) === normalized(right);
6
+ const ordinary = info => info?.isDirectory() && !info.isSymbolicLink();
7
+ const identity = (left, right) => left.dev === right.dev && left.ino === right.ino && left.birthtimeMs === right.birthtimeMs;
8
+
9
+ /** Callers supply canonical roots and explicit descendants, never glob results. */
10
+ export async function captureStorageDirectory(directory) {
11
+ try {
12
+ directory = path.resolve(directory);
13
+ const info = await lstat(directory);
14
+ if (!ordinary(info) || !sameStoragePath(await realpath(directory), directory)) return null;
15
+ const current = await lstat(directory);
16
+ if (!ordinary(current) || !identity(info, current)) return null;
17
+ return {directory, dev: info.dev, ino: info.ino, birthtimeMs: info.birthtimeMs};
18
+ } catch { return null; }
19
+ }
20
+
21
+ /** Recheck ancestors around each filesystem operation. Node does not expose an
22
+ * atomic openat/unlinkat boundary: these guards reject observed replacements,
23
+ * but are not isolation from a malicious process with the same filesystem rights. */
24
+ export async function verifyStorageDirectories(snapshots) {
25
+ if (!Array.isArray(snapshots) || !snapshots.length) return false;
26
+ for (const snapshot of snapshots) {
27
+ if (!snapshot) return false;
28
+ try {
29
+ const info = await lstat(snapshot.directory);
30
+ if (!ordinary(info) || !identity(info, snapshot) || !sameStoragePath(await realpath(snapshot.directory), snapshot.directory)) return false;
31
+ const current = await lstat(snapshot.directory);
32
+ if (!ordinary(current) || !identity(current, snapshot)) return false;
33
+ } catch { return false; }
34
+ }
35
+ return true;
36
+ }
package/runtime/trace.mjs CHANGED
@@ -2,6 +2,7 @@ import {isControlError} from './navigation.mjs';
2
2
  import {AsyncLocalStorage} from 'node:async_hooks';
3
3
  import {randomBytes} from 'node:crypto';
4
4
  import {createRequire} from 'node:module';
5
+ import {channel} from 'node:diagnostics_channel';
5
6
  const scope=new AsyncLocalStorage(),rawFetch=globalThis.fetch;
6
7
  let api;
7
8
  try{api=createRequire(import.meta.url)('@opentelemetry/api');}catch{}
@@ -9,6 +10,17 @@ const endpoint=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||(process.env.OTEL
9
10
  const protocol=process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL||process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
10
11
  const enabled=process.env.OTEL_SDK_DISABLED!=='true'&&endpoint&&(!protocol||protocol==='http/json');
11
12
  const queue=[];let timer,flushing;
13
+ const diagnostics=channel('prnext.telemetry');
14
+ const counters={droppedSpans:0,rejectedSpans:0,exportAttempts:0,exportFailures:0,failedBatches:0,exportedSpans:0};
15
+ let lastWarning=-Infinity;
16
+ export function telemetryDiagnostics(){return {enabled:!!enabled,queuedSpans:queue.length,...counters};}
17
+ function notify(reason){
18
+ const snapshot=telemetryDiagnostics();
19
+ // This local channel and fixed-field warning never pass through the exporter.
20
+ // No collector URL, headers, exception or request attributes are included.
21
+ if(diagnostics.hasSubscribers)diagnostics.publish({reason,...snapshot});
22
+ if(reason!=='exported'&&Date.now()-lastWarning>=10000){lastWarning=Date.now();try{console.error('[prnext] telemetry '+JSON.stringify({reason,...snapshot}));}catch{}}
23
+ }
12
24
  const epoch=BigInt(Date.now())*1000000n-process.hrtime.bigint();
13
25
  const nanos=()=>String(epoch+process.hrtime.bigint());
14
26
  export function parseTraceparent(value){
@@ -23,9 +35,25 @@ export function propagatedFetch(input,init){
23
35
  return [input,{...init,headers}];
24
36
  }
25
37
  function enqueue(span){
26
- if(!enabled||!span.flags||queue.length>=1024)return;
38
+ if(!enabled||!span.flags)return;
39
+ if(queue.length>=1024){counters.droppedSpans++;notify('queue_full');return;}
27
40
  queue.push(span);if(!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();
28
41
  }
42
+ async function collectorResult(response,count){
43
+ const reader=response.body?.getReader();if(!reader)return {rejected:0,warning:false};
44
+ const chunks=[];let size=0;
45
+ try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>16384)throw Error('collector response too large');chunks.push(value);}}
46
+ catch(error){await reader.cancel().catch(()=>{});throw error;}finally{reader.releaseLock();}
47
+ const body=size?JSON.parse(Buffer.concat(chunks,size).toString('utf8')):{};
48
+ if(!body||typeof body!=='object'||Array.isArray(body))throw Error('invalid collector response');
49
+ const partial=body.partialSuccess??body.partial_success;if(partial==null)return {rejected:0,warning:false};
50
+ if(typeof partial!=='object'||Array.isArray(partial))throw Error('invalid collector partial success');
51
+ const raw=partial.rejectedSpans??partial.rejected_spans??0;
52
+ if(typeof raw!=='number'&&!(typeof raw==='string'&&/^\d{1,20}$/.test(raw)))throw Error('invalid collector rejection count');
53
+ const rejected=Number(raw);
54
+ if(!Number.isSafeInteger(rejected)||rejected<0||rejected>count)throw Error('invalid collector rejection count');
55
+ return {rejected,warning:typeof(partial.errorMessage??partial.error_message)==='string'&&!!(partial.errorMessage??partial.error_message)};
56
+ }
29
57
  export async function flushTraces(){
30
58
  if(flushing)return flushing;
31
59
  if(!queue.length)return;
@@ -36,15 +64,27 @@ export async function flushTraces(){
36
64
  const spans=queue.splice(0,128);
37
65
  flushing=(async()=>{
38
66
  const body=JSON.stringify({resourceSpans:[{resource:{attributes:[{key:'service.name',value:{stringValue:process.env.OTEL_SERVICE_NAME||'prnext'}}]},scopeSpans:[{scope:{name:'prnext.runtime'},spans}]}]});
67
+ let exported=false;
39
68
  for(let attempt=0;attempt<3;attempt++){
40
- let retry=false;
69
+ let retry=false,receivedSuccess=false,accepted;
70
+ counters.exportAttempts++;
41
71
  try{
42
72
  const response=await rawFetch(endpoint,{method:'POST',redirect:'error',headers,body,signal:AbortSignal.timeout(1000)});
43
- retry=[429,502,503,504].includes(response.status);await response.body?.cancel();
44
- }catch{retry=true;} // Telemetry failure never changes an application response.
73
+ receivedSuccess=response.ok;
74
+ if(receivedSuccess)accepted=await collectorResult(response,spans.length);
75
+ else{retry=[429,502,503,504].includes(response.status);await response.body?.cancel();}
76
+ }catch{retry=!receivedSuccess;} // Never replay a successful/partial acknowledgement.
77
+ if(accepted){
78
+ exported=true;counters.exportedSpans+=spans.length-accepted.rejected;
79
+ if(accepted.rejected){counters.rejectedSpans+=accepted.rejected;counters.droppedSpans+=accepted.rejected;counters.exportFailures++;counters.failedBatches++;notify('collector_rejected');}
80
+ else notify(accepted.warning?'collector_warning':'exported');
81
+ break;
82
+ }
83
+ counters.exportFailures++;
45
84
  if(!retry)break;
46
85
  if(attempt<2)await new Promise(resolve=>setTimeout(resolve,100*2**attempt).unref());
47
86
  }
87
+ if(!exported){counters.failedBatches++;counters.droppedSpans+=spans.length;notify('export_failed');}
48
88
  })().finally(()=>{flushing=undefined;if(queue.length&&!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();});
49
89
  return flushing;
50
90
  }
@@ -7,6 +7,7 @@ import {deferredCompiler} from './dev-compiler-broker.mjs';
7
7
  import { requestFrames } from './request-transport.mjs';
8
8
  import {scheduleCompileCacheFlush} from './compile-cache.mjs';
9
9
  import {preloadModules} from './preload.mjs';
10
+ import {prewarmFramework} from './prewarm.mjs';
10
11
  import { requestWork } from './request-work.mjs';
11
12
  import { freezeClientModules } from './worker-metadata.mjs';
12
13
  import { AsyncResource } from 'node:async_hooks';
@@ -70,6 +71,29 @@ const routes = new Map(manifest.routes.map(route => [route.id, route]));
70
71
  const prerendered = new Set((manifest.prerendered || []).map(item => item.path));
71
72
  const deployment={distDir,manifest,routes,prerendered,modules:new Map()};
72
73
  const deployments=new Map([[distDir,deployment]]);
74
+ const prewarmRequested = process.env.PRNEXT_WORKER_PREWARM === '1';
75
+ delete process.env.PRNEXT_WORKER_PREWARM;
76
+ async function rendererFor(selected,name){
77
+ if(selected.modules.has(name))return selected.modules.get(name);
78
+ const pending=workScope.runInAsyncScope(()=>runtimeModule(name,selected.distDir));
79
+ selected.modules.set(name,pending);
80
+ try{const module=await pending;selected.modules.set(name,module);return module;}
81
+ catch(error){if(selected.modules.get(name)===pending)selected.modules.delete(name);throw error;}
82
+ }
83
+ async function prepareFramework(signal) {
84
+ const configured = manifest.config?.workerPreload;
85
+ const options = { maxRssMb: configured?.maxRssMb ?? 256, timeoutMs: Math.min(15000, configured?.timeoutMs ?? 15000) };
86
+ const result = await prewarmFramework({ routes: manifest.routes, options, signal,
87
+ load: name => rendererFor(deployment, name) });
88
+ if (diagnostics.enabled) diagnostics.record('wake', { phase: 'framework-prewarm', ...result });
89
+ if (!result.ready) console.warn(`[prnext] Framework prewarm incomplete (${result.reason}); normal requests remain available.`);
90
+ return result.ready;
91
+ }
92
+ let frameworkPrewarmed = false;
93
+ if (production && prewarmRequested) {
94
+ try { frameworkPrewarmed = await prepareFramework(); }
95
+ catch { console.warn('[prnext] Framework prewarm failed; normal requests remain available.'); }
96
+ }
73
97
  const compileDeferred=!production&&compilerDescriptor?deferredCompiler(compilerDescriptor,{root:projectRoot,home:path.dirname(distDir)}):undefined;
74
98
  async function deferredDeployment(route,signal,sources=[]){
75
99
  if(!compileDeferred)throw Error('Deferred development compiler is unavailable');
@@ -91,15 +115,8 @@ async function drainRuntime(){
91
115
 
92
116
  async function handleRequest(input, output, signalController, responseComplete, selected=deployment) {
93
117
  const {manifest,distDir,routes,prerendered}=selected;
94
- const renderer=async name=>{
95
- if(selected.modules.has(name))return selected.modules.get(name);
96
- // Imports share one pending job, then retain only the namespace. Starting
97
- // outside the visitor's ALS scope prevents the module cache retaining it.
98
- const pending=workScope.runInAsyncScope(()=>runtimeModule(name,distDir));
99
- selected.modules.set(name,pending);
100
- try{const module=await pending;selected.modules.set(name,module);return module;}
101
- catch(error){if(selected.modules.get(name)===pending)selected.modules.delete(name);throw error;}
102
- };
118
+ // Bootstrap and requests share pending imports outside the visitor's ALS.
119
+ const renderer=name=>rendererFor(selected,name);
103
120
  let responseStarted = false;
104
121
  const finalizers = new Set();
105
122
  try {
@@ -108,10 +125,8 @@ async function handleRequest(input, output, signalController, responseComplete,
108
125
  const options=manifest.config?.workerPreload||(frameworkOnly?{routes:[],maxRssMb:256,timeoutMs:15000}:undefined);
109
126
  if(!production||!options){await output(input.id,{status:204,body:''},input);return;}
110
127
  if(frameworkOnly){
111
- if(manifest.routes.some(route=>route.router==='app'&&route.kind==='page'))await(await renderer('app-render')).prewarmAppRenderer(options);
112
- if(manifest.routes.some(route=>route.kind==='page'&&route.router!=='app'))await renderer('render');
113
- if(manifest.routes.some(route=>route.kind==='api'))await renderer('api');
114
- scheduleCompileCacheFlush();await output(input.id,{status:204,body:''},input);return;
128
+ frameworkPrewarmed = await prepareFramework(signalController.signal);
129
+ scheduleCompileCacheFlush();await output(input.id,{status:frameworkPrewarmed?204:503,body:''},input);return;
115
130
  }
116
131
  const selected=options.routes.map(pattern=>manifest.routes.find(route=>route.pattern===pattern&&!route.internal)).filter(Boolean);
117
132
  const deadline=performance.now()+options.timeoutMs;
@@ -142,6 +157,9 @@ async function handleRequest(input, output, signalController, responseComplete,
142
157
  ? { ...manifest.middleware, id: '__prnext_middleware', kind: 'middleware' }
143
158
  : routes.get(input.routeId);
144
159
  if (!route) { await output(input.id, { status: 404, body: 'Not Found' }, input); return; }
160
+ if (input.bodyStream && (route.router !== 'app' || route.kind !== 'api' || input.renderMode || !manifest.config?.streamingRequestBodies)) {
161
+ throw new Error('Streaming request bodies require an enabled App Route Handler');
162
+ }
145
163
  const sourceModules=!production&&route.router==='app'&&route.kind==='page'?advancedRoutingSources({...input,basePath:manifest.config?.basePath||''},manifest.routes):[];
146
164
  const deferredSources=sourceModules.length?manifest.routes.filter(candidate=>candidate.devDeferred&&sourceModules.includes(candidate.module)).map(candidate=>candidate.pattern):[];
147
165
  if(route.devDeferred||deferredSources.length){
@@ -197,7 +215,9 @@ async function handleRequest(input, output, signalController, responseComplete,
197
215
  // The application's scope was aborted above, but the lane is still valid
198
216
  // for its one error/redirect response. Do not mistake that cleanup signal
199
217
  // for a client disconnect while writing the replacement headers.
200
- await output(input?.id ?? null, navigationResponse(error) || errorResponse(error, production), { ...input, production });
218
+ const ingressFailure = /^PRNEXT_BODY_(LIMIT|TIMEOUT|READ|LENGTH)$/.test(error?.code || '')
219
+ ? { status: error.statusCode, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 'Request body transport failed' } : undefined;
220
+ await output(input?.id ?? null, ingressFailure || navigationResponse(error) || errorResponse(error, production), { ...input, production });
201
221
  responseComplete?.();
202
222
  } finally {
203
223
  if (finalizers.size) {
@@ -261,7 +281,7 @@ if (token && laneLimit > 1) {
261
281
  const candidate = Buffer.from(line);
262
282
  if (candidate.length !== expected.length || !timingSafeEqual(candidate, expected)) throw new Error('Worker authentication failed');
263
283
  clearTimeout(authTimer); socket.write('ready\n');
264
- } })) {
284
+ }, streamBodies: true, credit: frame => new Promise((resolve, reject) => socket.write(JSON.stringify(frame) + '\n', error => error ? reject(error) : resolve())) })) {
265
285
  if (invocations.size >= laneLimit) throw new Error('Worker invocation capacity exceeded');
266
286
  controller = new AbortController();
267
287
  const active = controller;
@@ -339,10 +359,10 @@ if (token && laneLimit > 1) {
339
359
  });
340
360
  server.listen(0, '127.0.0.1', () => {
341
361
  if(!production&&typeof globalThis.__PRNEXT_DEV_WORKER_READY__==='function'){
342
- globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true});
362
+ globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true,requestBodyStream:1});
343
363
  delete globalThis.__PRNEXT_DEV_WORKER_READY__;
344
364
  }
345
- writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true }) + '\n');
365
+ writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true, requestBodyStream: 1, frameworkPrewarmed }) + '\n');
346
366
  heartbeat = setInterval(() => {
347
367
  // Coalesce the idle transition with the existing heartbeat. Normal
348
368
  // request completion adds no stdout messages, and short cache bursts
@@ -0,0 +1,5 @@
1
+ Fontkit 2.0.4 Node create() bundle, generated by scripts/prepare-fontkit.mjs.
2
+ Upstream: https://github.com/foliojs/fontkit
3
+ The original code is bundled/minified without patches. Source copyright notices, dependency licenses, README license declarations and exact versions/integrities are retained alongside it.
4
+ fontkit, dfa and brotli declare MIT in their package metadata/README and publish no separate license file. brotli also contains Google Apache-2.0 source notices, retained in SOURCE-NOTICES.txt; the Apache-2.0 text is included with @swc/helpers.
5
+ Use upstream.json to audit/update this bundled dependency when security fixes are released.
@@ -0,0 +1,209 @@
1
+ /* Copyright 2013 Google Inc. All Rights Reserved.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
15
+ Bit reading helpers
16
+ */
17
+
18
+ /* Copyright 2013 Google Inc. All Rights Reserved.
19
+
20
+ Licensed under the Apache License, Version 2.0 (the "License");
21
+ you may not use this file except in compliance with the License.
22
+ You may obtain a copy of the License at
23
+
24
+ http://www.apache.org/licenses/LICENSE-2.0
25
+
26
+ Unless required by applicable law or agreed to in writing, software
27
+ distributed under the License is distributed on an "AS IS" BASIS,
28
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29
+ See the License for the specific language governing permissions and
30
+ limitations under the License.
31
+
32
+ Collection of static dictionary words.
33
+ */
34
+
35
+ /* Copyright 2013 Google Inc. All Rights Reserved.
36
+
37
+ Licensed under the Apache License, Version 2.0 (the "License");
38
+ you may not use this file except in compliance with the License.
39
+ You may obtain a copy of the License at
40
+
41
+ http://www.apache.org/licenses/LICENSE-2.0
42
+
43
+ Unless required by applicable law or agreed to in writing, software
44
+ distributed under the License is distributed on an "AS IS" BASIS,
45
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
46
+ See the License for the specific language governing permissions and
47
+ limitations under the License.
48
+
49
+ Lookup table to map the previous two bytes to a context id.
50
+
51
+ There are four different context modeling modes defined here:
52
+ CONTEXT_LSB6: context id is the least significant 6 bits of the last byte,
53
+ CONTEXT_MSB6: context id is the most significant 6 bits of the last byte,
54
+ CONTEXT_UTF8: second-order context model tuned for UTF8-encoded text,
55
+ CONTEXT_SIGNED: second-order context model tuned for signed integers.
56
+
57
+ The context id for the UTF8 context model is calculated as follows. If p1
58
+ and p2 are the previous two bytes, we calcualte the context as
59
+
60
+ context = kContextLookup[p1] | kContextLookup[p2 + 256].
61
+
62
+ If the previous two bytes are ASCII characters (i.e. < 128), this will be
63
+ equivalent to
64
+
65
+ context = 4 * context1(p1) + context2(p2),
66
+
67
+ where context1 is based on the previous byte in the following way:
68
+
69
+ 0 : non-ASCII control
70
+ 1 : \t, \n, \r
71
+ 2 : space
72
+ 3 : other punctuation
73
+ 4 : " '
74
+ 5 : %
75
+ 6 : ( < [ {
76
+ 7 : ) > ] }
77
+ 8 : , ; :
78
+ 9 : .
79
+ 10 : =
80
+ 11 : number
81
+ 12 : upper-case vowel
82
+ 13 : upper-case consonant
83
+ 14 : lower-case vowel
84
+ 15 : lower-case consonant
85
+
86
+ and context2 is based on the second last byte:
87
+
88
+ 0 : control, space
89
+ 1 : punctuation
90
+ 2 : upper-case letter, number
91
+ 3 : lower-case letter
92
+
93
+ If the last byte is ASCII, and the second last byte is not (in a valid UTF8
94
+ stream it will be a continuation byte, value between 128 and 191), the
95
+ context is the same as if the second last byte was an ASCII control or space.
96
+
97
+ If the last byte is a UTF8 lead byte (value >= 192), then the next byte will
98
+ be a continuation byte and the context id is 2 or 3 depending on the LSB of
99
+ the last byte and to a lesser extent on the second last byte if it is ASCII.
100
+
101
+ If the last byte is a UTF8 continuation byte, the second last byte can be:
102
+ - continuation byte: the next byte is probably ASCII or lead byte (assuming
103
+ 4-byte UTF8 characters are rare) and the context id is 0 or 1.
104
+ - lead byte (192 - 207): next byte is ASCII or lead byte, context is 0 or 1
105
+ - lead byte (208 - 255): next byte is continuation byte, context is 2 or 3
106
+
107
+ The possible value combinations of the previous two bytes, the range of
108
+ context ids and the type of the next byte is summarized in the table below:
109
+
110
+ |--------\-----------------------------------------------------------------|
111
+ | \ Last byte |
112
+ | Second \---------------------------------------------------------------|
113
+ | last byte \ ASCII | cont. byte | lead byte |
114
+ | \ (0-127) | (128-191) | (192-) |
115
+ |=============|===================|=====================|==================|
116
+ | ASCII | next: ASCII/lead | not valid | next: cont. |
117
+ | (0-127) | context: 4 - 63 | | context: 2 - 3 |
118
+ |-------------|-------------------|---------------------|------------------|
119
+ | cont. byte | next: ASCII/lead | next: ASCII/lead | next: cont. |
120
+ | (128-191) | context: 4 - 63 | context: 0 - 1 | context: 2 - 3 |
121
+ |-------------|-------------------|---------------------|------------------|
122
+ | lead byte | not valid | next: ASCII/lead | not valid |
123
+ | (192-207) | | context: 0 - 1 | |
124
+ |-------------|-------------------|---------------------|------------------|
125
+ | lead byte | not valid | next: cont. | not valid |
126
+ | (208-) | | context: 2 - 3 | |
127
+ |-------------|-------------------|---------------------|------------------|
128
+
129
+ The context id for the signed context mode is calculated as:
130
+
131
+ context = (kContextLookup[512 + p1] << 3) | kContextLookup[512 + p2].
132
+
133
+ For any context modeling modes, the context ids can be calculated by |-ing
134
+ together two lookups from one table using context model dependent offsets:
135
+
136
+ context = kContextLookup[offset1 + p1] | kContextLookup[offset2 + p2].
137
+
138
+ where offset1 and offset2 are dependent on the context mode.
139
+ */
140
+
141
+ /* Copyright 2013 Google Inc. All Rights Reserved.
142
+
143
+ Licensed under the Apache License, Version 2.0 (the "License");
144
+ you may not use this file except in compliance with the License.
145
+ You may obtain a copy of the License at
146
+
147
+ http://www.apache.org/licenses/LICENSE-2.0
148
+
149
+ Unless required by applicable law or agreed to in writing, software
150
+ distributed under the License is distributed on an "AS IS" BASIS,
151
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
152
+ See the License for the specific language governing permissions and
153
+ limitations under the License.
154
+
155
+ Lookup tables to map prefix codes to value ranges. This is used during
156
+ decoding of the block lengths, literal insertion lengths and copy lengths.
157
+ */
158
+
159
+ /* Copyright 2013 Google Inc. All Rights Reserved.
160
+
161
+ Licensed under the Apache License, Version 2.0 (the "License");
162
+ you may not use this file except in compliance with the License.
163
+ You may obtain a copy of the License at
164
+
165
+ http://www.apache.org/licenses/LICENSE-2.0
166
+
167
+ Unless required by applicable law or agreed to in writing, software
168
+ distributed under the License is distributed on an "AS IS" BASIS,
169
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
170
+ See the License for the specific language governing permissions and
171
+ limitations under the License.
172
+
173
+ Transformations on dictionary words.
174
+ */
175
+
176
+ /* Copyright 2013 Google Inc. All Rights Reserved.
177
+
178
+ Licensed under the Apache License, Version 2.0 (the "License");
179
+ you may not use this file except in compliance with the License.
180
+ You may obtain a copy of the License at
181
+
182
+ http://www.apache.org/licenses/LICENSE-2.0
183
+
184
+ Unless required by applicable law or agreed to in writing, software
185
+ distributed under the License is distributed on an "AS IS" BASIS,
186
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
187
+ See the License for the specific language governing permissions and
188
+ limitations under the License.
189
+ */
190
+
191
+ /**
192
+ * The font's copyright information
193
+ * @type {string}
194
+ */
195
+
196
+ /******************************************************************************
197
+ Copyright (c) Microsoft Corporation.
198
+
199
+ Permission to use, copy, modify, and/or distribute this software for any
200
+ purpose with or without fee is hereby granted.
201
+
202
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
203
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
204
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
205
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
206
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
207
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
208
+ PERFORMANCE OF THIS SOFTWARE.
209
+ ***************************************************************************** */