@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
@@ -0,0 +1,35 @@
1
+ import { isIP } from 'node:net';
2
+
3
+ export function validateServerConfig(value) {
4
+ if (value === undefined) return { trustedProxies: [] };
5
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['publicOrigin', 'trustedProxies'].includes(key))) {
6
+ throw new TypeError('server must be an object with publicOrigin and trustedProxies.');
7
+ }
8
+ let publicOrigin;
9
+ if (value.publicOrigin !== undefined) {
10
+ const source = value.publicOrigin;
11
+ let url;
12
+ try { url = new URL(source); } catch { /* validated below */ }
13
+ if (typeof source !== 'string' || /[\s\\\u0000-\u001f\u007f]/.test(source) || !url || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash || source.includes('?') || source.includes('#')) {
14
+ throw new TypeError('server.publicOrigin must be an HTTP(S) origin without credentials, path, query or fragment, for example https://example.com.');
15
+ }
16
+ publicOrigin = url.origin;
17
+ }
18
+ const ranges = value.trustedProxies ?? [];
19
+ if (!Array.isArray(ranges) || ranges.length > 32 || ranges.some(range => {
20
+ if (typeof range !== 'string' || range.length > 64) return true;
21
+ const [address, bits, ...extra] = range.split('/');
22
+ const family = isIP(address);
23
+ return !family || extra.length || bits !== undefined && (!/^(0|[1-9]\d*)$/.test(bits) || Number(bits) > (family === 4 ? 32 : 128));
24
+ })) throw new TypeError('server.trustedProxies must contain at most 32 explicit IP addresses or CIDR ranges.');
25
+ const normalized = ranges.map(range => {
26
+ const [address, bits] = range.split('/');
27
+ if (isIP(address) !== 6) return range;
28
+ const mapped = /^\[::ffff:([\da-f]+):([\da-f]+)\]$/i.exec(new URL(`http://[${address}]`).hostname);
29
+ if (!mapped) return range;
30
+ if (bits !== undefined && Number(bits) < 96) throw new TypeError('Mapped IPv4 proxy CIDR ranges need a prefix of at least 96 bits.');
31
+ const high = parseInt(mapped[1], 16), low = parseInt(mapped[2], 16);
32
+ return `${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}${bits === undefined ? '' : '/' + (Number(bits) - 96)}`;
33
+ });
34
+ return { ...(publicOrigin ? { publicOrigin } : {}), trustedProxies: [...new Set(normalized)] };
35
+ }
@@ -6,31 +6,81 @@ import {createHash} from 'node:crypto';
6
6
  const cache=new Map();
7
7
  let bytes=0,hits=0,misses=0;
8
8
  export const sourceAnalysisStats=()=>({entries:cache.size,bytes,hits,misses});
9
+ const identifier=(node,name)=>node?.type==='Identifier'&&(name===undefined||node.name===name);
10
+ const property=node=>node?.computed?node.property?.value:node?.property?.name;
11
+ const moduleExports=node=>node?.type==='MemberExpression'&&identifier(node.object,'module')&&property(node)==='exports';
12
+ function bindingNames(node,names){
13
+ if(!node)return;
14
+ if(identifier(node))names.add(node.name);
15
+ else if(node.type==='ObjectPattern')for(const item of node.properties)bindingNames(item.type==='RestElement'?item.argument:item.value,names);
16
+ else if(node.type==='ArrayPattern')for(const item of node.elements)bindingNames(item,names);
17
+ else if(node.type==='RestElement')bindingNames(node.argument,names);
18
+ else if(node.type==='AssignmentPattern')bindingNames(node.left,names);
19
+ }
20
+ function runtimeDeclaration(node,names){
21
+ if(!node||node.declare)return;
22
+ if(node.type==='VariableDeclaration')for(const item of node.declarations)bindingNames(item.id,names);
23
+ else if(['FunctionDeclaration','ClassDeclaration','TSEnumDeclaration','TSModuleDeclaration'].includes(node.type))bindingNames(node.id,names);
24
+ }
9
25
  export function analyzeSource(source,file='source.js') {
10
26
  const key=file+'\0'+createHash('sha256').update(source).digest('hex');
11
27
  if(cache.has(key)){const hit=cache.get(key);cache.delete(key);cache.set(key,hit);hits++;return hit.value;}
12
28
  misses++;
13
- const parsed=parseSync(file,source,{sourceType:'unambiguous',allowReturnOutsideFunction:true});
29
+ // Custom loaders also return JSX under their original asset filenames (for
30
+ // example .svg). Match the compiler's JSX fallback for non-TypeScript inputs,
31
+ // preserving TypeScript's extension rules, including angle assertions in .ts.
32
+ const parsed=parseSync(file,source,{sourceType:'unambiguous',allowReturnOutsideFunction:true,...(/\.(?:[cm]?ts|tsx)$/.test(file)?{}:{lang:'jsx'})});
14
33
  if(parsed.errors.length)throw new SyntaxError(`${file}: ${parsed.errors.map(error=>error.message).join('; ')}`);
15
34
  const program=parsed.program,directives=[];
16
35
  for(const node of program.body){if(!node.directive)break;directives.push(node.directive);}
17
- const imports=[];
18
- let computedRequire=false;
36
+ const imports=[],runtimeExports=new Set(),exportStars=[];
37
+ for(const node of program.body){
38
+ if(node.exportKind==='type')continue;
39
+ if(node.type==='ExportDefaultDeclaration'&&!node.declaration?.declare&&!['TSInterfaceDeclaration','TSTypeAliasDeclaration'].includes(node.declaration?.type))runtimeExports.add('default');
40
+ if(node.type==='ExportNamedDeclaration'){
41
+ runtimeDeclaration(node.declaration,runtimeExports);
42
+ for(const item of node.specifiers)if(item.exportKind!=='type')runtimeExports.add(item.exported.name??item.exported.value);
43
+ }
44
+ if(node.type==='ExportAllDeclaration'){
45
+ if(node.exported)runtimeExports.add(node.exported.name??node.exported.value);
46
+ else exportStars.push(node.source.value);
47
+ }
48
+ }
49
+ let computedRequire=false,hasServerDirective=false;
19
50
  const pending=[program];
20
51
  while(pending.length){
21
52
  const node=pending.pop();
53
+ if(node.directive==='use server')hasServerDirective=true;
22
54
  if(['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration'].includes(node.type)&&node.source&&node.importKind!=='type'&&node.exportKind!=='type')imports.push({path:node.source.value,kind:'import-statement'});
23
55
  if(node.type==='ImportExpression'&&typeof node.source?.value==='string')imports.push({path:node.source.value,kind:'dynamic-import'});
24
56
  if(node.type==='CallExpression'&&node.callee?.type==='Identifier'&&node.callee.name==='require'){
25
57
  const argument=node.arguments[0];
26
58
  if(argument?.type==='Literal'&&typeof argument.value==='string')imports.push({path:argument.value,kind:'require-call'});else computedRequire=true;
27
59
  }
60
+ if(node.type==='CallExpression'&&node.callee?.type==='MemberExpression'&&identifier(node.callee.object,'Object')&&property(node.callee)==='defineProperty'&&identifier(node.arguments[0],'exports')&&typeof node.arguments[1]?.value==='string'&&node.arguments[1].value!=='__esModule'){
61
+ runtimeExports.add('default');runtimeExports.add(node.arguments[1].value);
62
+ }
63
+ if(node.type==='AssignmentExpression'&&node.left.type==='MemberExpression'){
64
+ const left=node.left;
65
+ if(moduleExports(left)){
66
+ runtimeExports.add('default');
67
+ if(node.right.type==='ObjectExpression')for(const item of node.right.properties){
68
+ const key=item.computed?item.key?.value:item.key?.name??item.key?.value;
69
+ if(typeof key==='string')runtimeExports.add(key);
70
+ }
71
+ }
72
+ if(identifier(left.object,'exports')||moduleExports(left.object)){
73
+ runtimeExports.add('default');if(typeof property(left)==='string')runtimeExports.add(property(left));
74
+ }
75
+ }
28
76
  for(const value of Object.values(node)){
29
77
  if(Array.isArray(value)){for(let i=value.length-1;i>=0;i--)if(value[i]?.type)pending.push(value[i]);}
30
78
  else if(value&&typeof value==='object'&&value.type)pending.push(value);
31
79
  }
32
80
  }
33
- const value={program,directives,imports,computedRequire,frameworkBoundary:directives.some(item=>item==='use client'||item==='use server')};
81
+ runtimeExports.delete('__esModule');
82
+ const value=Object.freeze({program,directives:Object.freeze(directives),imports:Object.freeze(imports.map(Object.freeze)),computedRequire,hasServerDirective,
83
+ runtimeExports:Object.freeze([...runtimeExports]),exportStars:Object.freeze(exportStars),frameworkBoundary:directives.some(item=>item==='use client'||item==='use server')});
34
84
  // ASTs are substantially larger than source. Bound both count and estimated
35
85
  // retained size; large individual npm modules are analyzed without retention.
36
86
  const size=Buffer.byteLength(source)*16+Buffer.byteLength(file);
@@ -1,5 +1,6 @@
1
- import { nodeFileTrace } from '@vercel/nft';
2
- import resolver from '@vercel/nft/out/resolve-dependency.js';
1
+ import { nodeFileTrace } from '../vendor/nft/out/index.js';
2
+ import resolver from '../vendor/nft/out/resolve-dependency.js';
3
+ import { CachedFileSystem } from '../vendor/nft/out/fs.js';
3
4
  import picomatch from 'picomatch';
4
5
  import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink, cp } from 'node:fs/promises';
5
6
  import path from 'node:path';
@@ -12,6 +13,10 @@ const within = (root, file) => file === root || file.startsWith(root + path.sep)
12
13
  const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
13
14
  const maxFiles = 100_000;
14
15
  const maxBytes = 2 * 1024 ** 3;
16
+ const linuxRuntimeProbes = new Set([
17
+ '/proc/self', '/proc/self/exe', '/usr/bin/ldd',
18
+ '/proc/version', '/proc/self/cgroup', '/.dockerenv', '/run/.containerenv',
19
+ ]);
15
20
 
16
21
  export async function resolveTraceDependency(...args) {
17
22
  const result = await resolver.default(...args);
@@ -75,6 +80,10 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
75
80
  const warnings = new Set();
76
81
  let omittedWarnings = 0;
77
82
  const cache = {};
83
+ const traceFileSystem = new CachedFileSystem({ cache, fileIOConcurrency: 16 });
84
+ // libc and environment detectors read the running host, not application assets.
85
+ // Keep this exact and outside the tracing root; other external files still fail.
86
+ const systemProbe = file => process.platform === 'linux' && !within(tracingRoot, file) && linuxRuntimeProbes.has(file);
78
87
  const traceLocations = new Map();
79
88
  let traceReads = 0;
80
89
  let traceBytes = 0;
@@ -120,7 +129,16 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
120
129
  for (const conditions of [['node'], ['node', 'react-server']]) {
121
130
  const result = await nodeFileTrace(entries, {
122
131
  base: path.parse(projectRoot).root, processCwd: projectRoot, conditions, cache, fileIOConcurrency: 16,
123
- ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
132
+ ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return systemProbe(absolute) || !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
133
+ // Share the metadata concurrency limit between stat and readlink.
134
+ stat: file => traceFileSystem.stat(file),
135
+ async readlink(file) {
136
+ // NFT follows symlinks before ignore(), so never follow these probes
137
+ // into /proc/<pid> or the build machine's Node executable.
138
+ if (systemProbe(file)) return null;
139
+ try { return await traceFileSystem.readlink(file); }
140
+ catch (error) { if (error.code === 'ENOTDIR') return null; throw error; }
141
+ },
124
142
  async readFile(file) {
125
143
  let info;
126
144
  try { info = await stat(file); } catch (error) { if (missing(error)) return null; throw error; }
@@ -7,6 +7,8 @@ import { OUTPUT_POINTER, prepareBuildDirectory, readBuildDirectory, validateDist
7
7
  import { writeBuildOutput } from './output-contract.mjs';
8
8
  import { precompressBuild } from './precompress.mjs';
9
9
  import { withBuildLock } from './build-lock.mjs';
10
+ import {withCompilationStorage} from './compilation-storage.mjs';
11
+ import {reclaimBuildStages,trackBuildStage} from './build-retention.mjs';
10
12
  import { plainStaticPath } from './static-input.mjs';
11
13
 
12
14
  const MAX_FILES = 20000, MAX_BYTES = 512 * 1024 * 1024;
@@ -15,7 +17,7 @@ const urlFor = file => '/' + file.split('/').map(encodeURIComponent).join('/');
15
17
 
16
18
  /** Import an already-built public directory. Never evaluate its config or code. */
17
19
  export async function buildStatic(directory, { from = 'dist', spa = false, distDir, dev = false } = {}) {
18
- return withBuildLock(directory, root => importStatic(root, { from, spa, distDir, dev }));
20
+ return withBuildLock(directory, async root => {await reclaimBuildStages(root);return withCompilationStorage(root,()=>importStatic(root, { from, spa, distDir, dev }));});
19
21
  }
20
22
 
21
23
  async function importStatic(directory, { from, spa, distDir, dev }) {
@@ -41,6 +43,7 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
41
43
 
42
44
  const id = randomUUID(), stage = path.join(root, `.prnext-build-${id}`);
43
45
  const backup = path.join(root, `.prnext-backup-${id}`), pointer = path.join(root, `.prnext-output-${id}.tmp`);
46
+ const recovery=await trackBuildStage(root,{stage,backup}).catch(()=>null);
44
47
  const manifest = { version: 1, buildId: id, config: { staticOnly: true, staticFallback: spa, compress: true, poweredByHeader: true }, routes: [], prerendered: [] };
45
48
  const paths = new Set();
46
49
  let count = 0, bytes = 0, previous = false, published = false;
@@ -99,10 +102,11 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
99
102
  if (previous) await rename(backup, destination);
100
103
  throw error;
101
104
  }
102
- if (previous) await rm(backup, { recursive: true, force: true });
105
+ await recovery?.committed().catch(()=>{});
106
+ if (!recovery&&previous) await rm(backup, { recursive: true, force: true });
103
107
  return { ...manifest, outputDirectory: destination, files: count, bytes };
104
108
  } finally {
105
109
  await rm(pointer, { force: true });
106
- await rm(stage, { recursive: true, force: true });
110
+ if(recovery)await recovery.cleanup();else await rm(stage, { recursive: true, force: true });
107
111
  }
108
112
  }
@@ -37,17 +37,49 @@ function relativeChunksUncached(source,chunks,prefix) {
37
37
  return source;
38
38
  }
39
39
 
40
+ function rewriteChunkSource(source,map,file,chunks,publicPath){
41
+ const result=relativeServerChunkImports(inlineSourceMap(source,map),chunks,publicPath);
42
+ if(map){
43
+ const mapped=extractSourceMap(result);
44
+ return {source:mapped.code.replace(/\/\/[#@]\s*sourceMappingURL=[^\s]+/g,'')+'\n//# sourceMappingURL='+path.basename(file)+'.map\n',map:JSON.stringify({...mapped.map,file:path.basename(file)})};
45
+ }
46
+ return {source:result};
47
+ }
48
+
49
+ /** Rewrite before the first disk write, without modifying compiler/cache buffers. */
50
+ export function rewriteServerChunkOutputFiles(files,chunks,publicPath,importers){
51
+ const outputs=new Map(files.map(file=>[path.resolve(file.path),file])),replacements=new Map();
52
+ const replace=(file,source)=>{
53
+ const contents=Buffer.from(source),metadata=Object.getOwnPropertyDescriptors(file);
54
+ // esbuild's hash describes the original buffer. Do not retain a stale hash.
55
+ // Reading/spreading esbuild's lazy text getter would decode the old buffer
56
+ // again and retain that string even though only the new bytes are needed.
57
+ delete metadata.hash;delete metadata.contents;delete metadata.text;
58
+ const output=Object.create(Object.getPrototypeOf(file),metadata);
59
+ Object.defineProperties(output,{contents:{value:contents,writable:true,enumerable:true,configurable:true},text:{get(){return this.contents.toString();},enumerable:true,configurable:true}});
60
+ replacements.set(file,output);
61
+ };
62
+ for(const file of files){
63
+ if(!file.path.endsWith('.mjs')||importers&&!importers.has(path.resolve(file.path)))continue;
64
+ const source=Buffer.from(file.contents.buffer,file.contents.byteOffset,file.contents.byteLength).toString();
65
+ if(!source.includes(publicPath.replace(/\/+$/,'')+'/'))continue;
66
+ const mapFile=sourceMapsEnabled()&&outputs.get(path.resolve(file.path+'.map'));
67
+ const map=mapFile?JSON.parse(Buffer.from(mapFile.contents).toString()):undefined;
68
+ const result=rewriteChunkSource(source,map,file.path,chunks,publicPath);
69
+ if(result.source!==source)replace(file,result.source);
70
+ if(mapFile)replace(mapFile,result.map);
71
+ }
72
+ return files.map(file=>replacements.get(file)||file);
73
+ }
74
+
40
75
  export async function rewriteServerChunks(file,chunks,publicPath){
41
76
  const source=await readFile(file,'utf8');
42
77
  if(!source.includes(publicPath.replace(/\/+$/,'')+'/'))return;
43
78
  let map;
44
79
  if(sourceMapsEnabled())try{map=JSON.parse(await readFile(file+'.map','utf8'));}catch(error){if(error.code!=='ENOENT')throw error;}
45
- const result=relativeServerChunkImports(inlineSourceMap(source,map),chunks,publicPath);
46
- if(map){
47
- const mapped=extractSourceMap(result);
48
- await writeFile(file+'.map',JSON.stringify({...mapped.map,file:path.basename(file)}));
49
- await writeFile(file,mapped.code.replace(/\/\/[#@]\s*sourceMappingURL=[^\s]+/g,'')+'\n//# sourceMappingURL='+path.basename(file)+'.map\n');
50
- }else await writeFile(file,result);
80
+ const result=rewriteChunkSource(source,map,file,chunks,publicPath);
81
+ if(map)await writeFile(file+'.map',result.map);
82
+ await writeFile(file,result.source);
51
83
  }
52
84
 
53
85
  function parserOptions(filename) {
@@ -3,7 +3,7 @@ import {access} from 'node:fs/promises';
3
3
  import {resolveTypeScript} from './typescript-toolchain.mjs';
4
4
  import path from 'node:path';
5
5
  import {privateCacheDirectory} from './persistent-cache.mjs';
6
- export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000}={}){
6
+ export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000,onBeforeChild=()=>{},onChild=()=>{}}={}){
7
7
  const noop={inactive:true,async close(){}};
8
8
  if(config.typescript?.devChecks===false)return noop;
9
9
  const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');try{await access(filename);}catch{return noop;}
@@ -13,7 +13,9 @@ export async function startTypeChecker(root,config,notify,{fork:spawnChecker=for
13
13
  function launch(){
14
14
  if(closed)return;
15
15
  const worker=toolchain.native?'./typescript-native-worker.mjs':'./typescript-dev-worker.mjs';
16
+ const ticket=onBeforeChild();
16
17
  child=spawnChecker(new URL(worker,import.meta.url),[toolchain.compiler,filename,toolchain.native?'watch':directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
18
+ onChild(child,ticket);
17
19
  const current=child;
18
20
  let details='',healthyAt=0;child.stderr.on('data',chunk=>{details=(details+chunk).slice(-2048);});
19
21
  child.on('message',message=>{
@@ -77,6 +77,13 @@ function pluginIdentity(plugin,webpack){
77
77
  export const webpackCompilerStats=()=>({contexts:sessions.size,created,reused,limit:sessionLimit,buildLimit:sessionBuildLimit});
78
78
  function queued(callback){const task=graphQueue.then(callback);graphQueue=task.catch(()=>{});return task;}
79
79
  export function closeWebpackCompiler(root){return queued(async()=>{
80
+ if(root!==undefined){
81
+ try{root=await realpath(root);}
82
+ catch(error){
83
+ if(error.code!=='ENOENT'&&error.code!=='ENOTDIR')throw error;
84
+ root=path.resolve(root);
85
+ }
86
+ }
80
87
  const closing=[];for(const[key,session]of sessions)if(root===undefined||session.root===root){sessions.delete(key);closing.push(session.dispose());}
81
88
  const results=await Promise.allSettled(closing),errors=results.filter(result=>result.status==='rejected').map(result=>result.reason);
82
89
  if(errors.length)throw new AggregateError(errors,'Unable to dispose webpack compiler sessions');
@@ -84,6 +91,9 @@ export function closeWebpackCompiler(root){return queued(async()=>{
84
91
 
85
92
  async function compileGraph(resolver,loads,options,{state,config,target}){
86
93
  return queued(async()=>{
94
+ // Ownership uses one canonical identity for invalidation and disposal.
95
+ // Compiler paths and graph fingerprints retain the caller's lexical root.
96
+ const ownerRoot=await realpath(state.root);
87
97
  const webpack=(await import('webpack')).default;
88
98
  let reusable=state.dev&&state.incremental,identity;
89
99
  if(reusable){try{identity=await buildCacheIdentity(state.root);}catch{}if(!identity)reusable=false;}
@@ -97,17 +107,17 @@ async function compileGraph(resolver,loads,options,{state,config,target}){
97
107
  // Unknown data structures, safe mode and unavailable identities all keep
98
108
  // compilation correct through a one-shot compiler. Hidden mutable closure
99
109
  // state cannot be fingerprinted: replace the plugin instance when it changes.
100
- if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===state.root){sessions.delete(key);await session.dispose();}
110
+ if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===ownerRoot){sessions.delete(key);await session.dispose();}
101
111
  let session=reusable&&sessions.get(key);
102
112
  if(session&&session.builds>=sessionBuildLimit){sessions.delete(key);await session.dispose();session=undefined;}
103
113
  if(reusable){
104
- for(const[old,previous]of sessions)if(previous.root===state.root&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
114
+ for(const[old,previous]of sessions)if(previous.root===ownerRoot&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
105
115
  }
106
116
  if(session){sessions.delete(key);reused++;}
107
117
  else{
108
118
  if(reusable)while(sessions.size>=sessionLimit){const[old,previous]=sessions.entries().next().value;sessions.delete(old);await previous.dispose();}
109
119
  session=await createGraphSession(state.root,{webpack,reusable,stage:state.stage});created++;
110
- Object.assign(session,{root:state.root,base,identity,builds:0});
120
+ Object.assign(session,{root:ownerRoot,base,identity,builds:0});
111
121
  }
112
122
  if(reusable)sessions.set(key,session);
113
123
  try{session.builds++;return await session.run(resolver,loads,options,{state,config,target});}
package/cli.mjs CHANGED
@@ -38,12 +38,15 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
38
38
  prnext docs --ai [topic] Read the AI guide bundled with this version
39
39
  prnext docs --ai --init [directory] Add a guide pointer to the project's AGENTS.md
40
40
  prnext routes [directory] Display compiled routes
41
- prnext check [directory] [--json] Diagnose config, routes, Node, native and dependencies
41
+ prnext check [directory] [--production] [--json] Check config, deployment and dependencies
42
42
  prnext typegen [directory] [--json] Generate route types without building the app
43
43
  prnext typecheck [directory] [--json] Generate types and check TypeScript without building
44
44
  prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
45
45
  prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
46
46
  prnext compiler [directory] [--json] React Compiler results from the last successful build
47
+ prnext cache [directory] [--json] Show compilation cache sizes and disk budget
48
+ prnext cache [directory] --prune [--dry-run] [--max-bytes N] [--json]
49
+ Reclaim regenerable compilation caches
47
50
  prnext host [prnext.host.json] Host built apps by hostname; wake on demand
48
51
  prnext host [config] --check Validate hosting configuration and builds
49
52
  prnext host [config] --status [--json] Show memory, process and idle status
@@ -58,6 +61,8 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
58
61
  prnext migrate --check Audit source compatibility without migrating
59
62
  prnext migrate --check --against URL --candidate URL [--routes paths.json]
60
63
  Compare GET responses before switching
64
+ prnext migrate --check --against URL --candidate URL --browser-scenarios ./scenarios.mjs
65
+ Run trusted application browser assertions
61
66
  Migration keeps Next.js and backs up package.json and existing lockfiles.
62
67
  Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
63
68
  Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
@@ -107,6 +112,10 @@ function stop(child) {
107
112
  async function main() {
108
113
  if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
109
114
  if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
115
+ if(command==='cache'){
116
+ const {cacheCommand}=await import('./build/cache-command.mjs');
117
+ await cacheCommand(argv);return;
118
+ }
110
119
  if (command === 'create') {
111
120
  const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
112
121
  if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
@@ -191,13 +200,20 @@ async function main() {
191
200
  report.comparison = await compareServers({ ...options, routes });
192
201
  report.ok &&= report.comparison.ok;
193
202
  }
203
+ if (options.browserScenarios) {
204
+ const { runMigrationScenarios } = await import('./build/migration-scenarios.mjs');
205
+ report.browser = await runMigrationScenarios({ directory, ...options, scenarios: options.browserScenarios });
206
+ report.ok &&= report.browser.ok;
207
+ }
194
208
  if (options.json) console.log(JSON.stringify(report, null, 2));
195
209
  else {
196
210
  console.log(`PRNext migration check ${report.ok ? 'passed' : 'failed'}: ${directory}`);
197
211
  console.log(`${report.files} source files; ${report.routes.length} routes.`);
198
212
  for (const item of report.findings) console.log(`${item.severity.toUpperCase()} ${item.file || 'project'}${item.line ? ':' + item.line : ''}: ${item.message}\n ${item.action}`);
199
213
  for (const result of report.comparison?.results || []) console.log(`${result.equal ? 'MATCH' : 'DIFFERENT'} ${result.route}: ${result.error || result.differences.join(', ') || 'responses match'}`);
200
- for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : [])]) console.log(note);
214
+ for (const result of report.browser?.results || []) console.log(`BROWSER ${result.scenario} (${result.target}): ${result.status}${result.reason ? ' — ' + result.reason : ''}`);
215
+ if (report.browser?.reason) console.log(`BROWSER unknown: ${report.browser.reason}`);
216
+ for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : []), ...(report.browser ? [report.browser.scope] : [])]) console.log(note);
201
217
  }
202
218
  if (!report.ok) process.exitCode = 1;
203
219
  return;
@@ -304,12 +320,12 @@ async function main() {
304
320
  process.env.PWD = root;
305
321
  }
306
322
  if (command === 'check') {
307
- if (args.some(arg => arg !== '--json')) throw new Error('check accepts only [directory] and --json');
323
+ if (args.some(arg => !['--json','--production'].includes(arg))||new Set(args).size!==args.length) throw new Error('check accepts [directory], --production and --json once each');
308
324
  const { checkProject } = await import('./build/check.mjs');
309
- const report = await checkProject(root);
325
+ const report = await checkProject(root,{production:args.includes('--production')});
310
326
  if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
311
327
  else {
312
- console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
328
+ console.log(`PRNext ${report.mode==='production'?'production ':''}preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
313
329
  console.log(`${report.routes.length} routes. Installed versions: ${Object.entries(report.versions).map(([name,version])=>`${name}@${version||'missing'}`).join(', ')}`);
314
330
  for (const item of report.diagnostics) {
315
331
  const output = item.severity === 'error' ? console.error : console.log;
@@ -371,7 +387,9 @@ async function main() {
371
387
  const binary = await resolveNativeBinary();
372
388
  if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
373
389
  const outputDirectory = await readBuildDirectory(root);
374
- const serverEnvironment = {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
390
+ const serverEnvironment = command==='start'
391
+ ? (await import('./runtime/env.mjs')).productionEnvironment(root)
392
+ : {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
375
393
  if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
376
394
  if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
377
395
  '--node',process.execPath,...args],serverEnvironment))return;
@@ -19,7 +19,18 @@ function dynamic(input, options) {
19
19
  if (!opts.ssr && typeof React.useEffect !== 'function') {
20
20
  throw new Error('ssr: false is not supported with next/dynamic in Server Components. Move it into a Client Component.');
21
21
  }
22
- const Lazy = React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
22
+ let Lazy;
23
+ if(process.env.NODE_ENV==='development'&&typeof window!=='undefined'&&typeof opts.__prnextDevIdentity==='string'){
24
+ const registry=globalThis.__PRNEXT_DEV_DYNAMIC__||=new Map();
25
+ let entry=registry.get(opts.__prnextDevIdentity);
26
+ if(!entry||entry.failed){
27
+ entry={loader:opts.loader};
28
+ entry.lazy=React.lazy(()=>Promise.resolve().then(()=>entry.loader()).then(moduleOf,error=>{entry.failed=true;throw error;}));
29
+ if(registry.size>=2048)registry.delete(registry.keys().next().value);
30
+ registry.set(opts.__prnextDevIdentity,entry);
31
+ }else entry.loader=opts.loader;
32
+ Lazy=entry.lazy;
33
+ }else Lazy=React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
23
34
  function DynamicComponent(props) {
24
35
  const fallback = opts.loading ? React.createElement(opts.loading, { isLoading: true, pastDelay: true, error: null }) : null;
25
36
  const child = React.createElement(Lazy, props);
@@ -12,17 +12,21 @@ function ActivitySlot({ identity, children, authorizationKey }) {
12
12
  const router = React.useContext(AppRouterContext);
13
13
  const epoch = retention?.epoch ?? 0;
14
14
  const authorizationEpoch = retention?.authorizationEpoch ?? 0;
15
+ const flight = retention?.flight;
15
16
  const registrations = React.useRef(new Set());
16
17
  const serial = React.useRef(0);
17
18
  const [saved, setSaved] = React.useState(() => ({ epoch, authorizationEpoch, active: identity, entries: new Map() }));
18
19
  let entries = saved.entries;
19
20
  const current = entries.get(identity);
20
- if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey) {
21
+ const incompleteInactive = [...entries].some(([key, entry]) => key !== identity && entry.flight?.complete === false);
22
+ if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey || current?.flight !== flight || incompleteInactive) {
21
23
  const sameAuthorization = saved.authorizationEpoch === authorizationEpoch && !(current?.authorizationKey && authorizationKey && current.authorizationKey !== authorizationKey);
22
- entries = saved.epoch === epoch && sameAuthorization ? new Map(entries) : new Map();
24
+ // Aborted lazy Flight rows cannot remain in an inactive Activity. Restore
25
+ // complete siblings while keeping the current shared layout's React key.
26
+ entries = saved.epoch === epoch && sameAuthorization ? new Map([...entries].filter(([key, entry]) => key === identity || entry.flight?.complete !== false)) : new Map();
23
27
  const preserve = current && saved.authorizationEpoch === authorizationEpoch && (sameAuthorization && (saved.epoch === epoch || saved.active === identity)
24
28
  || !retention?.strictAuthorization && saved.active === identity);
25
- entries.set(identity, { children, router, authorizationKey, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
29
+ entries.set(identity, { children, router, authorizationKey, flight, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
26
30
  setSaved({ epoch, authorizationEpoch, active: identity, entries });
27
31
  }
28
32
  React.useLayoutEffect(() => {
@@ -80,7 +80,9 @@ async function read(entry, context) {
80
80
  if (size > MAX_BYTES) throw new Error('CacheEntry exceeds 2 MiB');
81
81
  chunks.push(Buffer.from(value));
82
82
  }
83
- return Buffer.concat(chunks, size);
83
+ // Buffer.from above owns each chunk even when a custom producer reuses its
84
+ // Uint8Array on the next pull. Keep that copy, but do not copy it twice.
85
+ return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
84
86
  } finally {
85
87
  if (!complete) void reader.cancel().catch(() => {});
86
88
  reader.releaseLock();
@@ -3,7 +3,7 @@
3
3
  const WINDOW = 32, MAX_VALUE = 2 * 1024 * 1024, MAX_RESPONSE = 16 * 1024 * 1024;
4
4
  const TYPE = 'application/x-prnext-flight-delta';
5
5
  const MAGIC = new Uint8Array([80,82,68,49]);
6
- const BLOCK_BYTES = 32 * 1024, MAX_COMMANDS_PER_CHUNK = 256, MAX_COMMANDS = 2048;
6
+ const BLOCK_BYTES = 32 * 1024, MAX_COMMANDS_PER_CHUNK = 256, MAX_COMMANDS = 2048, MAX_CAPTURE_PARTS = 2048;
7
7
  let POWER = 1; for(let n=1;n<WINDOW;n++) POWER = Math.imul(POWER,31) >>> 0;
8
8
  function hash(bytes,offset) { let result=0; for(let n=0;n<WINDOW;n++) result=(Math.imul(result,31)+bytes[offset+n])>>>0; return result; }
9
9
  function createIndex(base) {
@@ -44,22 +44,59 @@ function blocks(parts) {
44
44
  for(const part of parts)for(let offset=0;offset<part.length;){const n=Math.min(BLOCK_BYTES-size,part.length-offset);pending.push(part.subarray(offset,offset+n));size+=n;offset+=n;if(size===BLOCK_BYTES)flush();}
45
45
  flush();return output;
46
46
  }
47
- function collectStream(source,{onComplete,onCancel,onFinally,capture=true}={}) {
48
- const reader=source.getReader(), parts=[];let size=0,complete=false,finished=false;
47
+ // Only the worker bridge registers streams here. Their backing allocations
48
+ // crossed a transfer boundary, have no producer aliases, and the framework's
49
+ // outgoing transport does not mutate or transfer them again. Arbitrary React,
50
+ // fetch and application streams retain the defensive per-chunk copy.
51
+ // Even an owned partial view is copied: retaining its much larger backing
52
+ // allocation would defeat the byte-based capture limit.
53
+ const ownedStreams=new WeakSet();
54
+ function ownFlightStream(source){ownedStreams.add(source);return source;}
55
+ function collectStream(source,{onComplete,onEnd,onCancel,onFinally,capture=true}={}) {
56
+ const owned=ownedStreams.has(source);
57
+ const reader=source.getReader(), parts=[];let size=0,complete=false,finished=false,cancelled=false;
49
58
  const finish=()=>{if(!finished){finished=true;onFinally?.();}};
50
59
  return new ReadableStream({
51
- async pull(controller) {try{const next=await reader.read();if(next.done){complete=true;if(capture&&size<=MAX_VALUE)onComplete?.(join(parts,size));parts.length=0;finish();controller.close();return;}if(!(next.value instanceof Uint8Array))throw new Error('Invalid Flight bytes');size+=next.value.length;if(size>MAX_RESPONSE)throw new Error('Flight exceeds transport budget');if(capture&&size<=MAX_VALUE)parts.push(next.value.slice());else parts.length=0;controller.enqueue(next.value);}catch(error){parts.length=0;finish();controller.error(error);await reader.cancel(error).catch(()=>{});}},
52
- async cancel(reason){parts.length=0;finish();if(!complete)onCancel?.();await reader.cancel(reason);},
60
+ async pull(controller) {
61
+ try {
62
+ const next=await reader.read();
63
+ if(cancelled)return;
64
+ if(next.done){
65
+ complete=true;
66
+ if(capture)onComplete?.(join(parts,size));
67
+ parts.length=0;onEnd?.();finish();controller.close();return;
68
+ }
69
+ const value=next.value;
70
+ if(!(value instanceof Uint8Array))throw new Error('Invalid Flight bytes');
71
+ size+=value.length;
72
+ if(size>MAX_RESPONSE)throw new Error('Flight exceeds transport budget');
73
+ if(capture){
74
+ // Limits include the number of retained allocations, not just their
75
+ // payload bytes. An abandoned capture never interrupts the response.
76
+ if(size>MAX_VALUE||value.length&&parts.length>=MAX_CAPTURE_PARTS){capture=false;parts.length=0;}
77
+ else if(value.length){
78
+ const wholeOwned=owned&&value.buffer instanceof ArrayBuffer&&value.byteOffset===0&&value.byteLength===value.buffer.byteLength;
79
+ // Buffer.slice() aliases its producer's backing allocation. The
80
+ // plain Uint8Array constructor always copies just the visible bytes.
81
+ parts.push(wholeOwned?value:new Uint8Array(value));
82
+ }
83
+ }
84
+ controller.enqueue(value);
85
+ }catch(error){parts.length=0;finish();if(cancelled)return;controller.error(error);await reader.cancel(error).catch(()=>{});}
86
+ },
87
+ async cancel(reason){cancelled=true;parts.length=0;finish();if(!complete)onCancel?.();await reader.cancel(reason);},
53
88
  },{highWaterMark:0});
54
89
  }
55
90
  async function deltaStream(source,base,{assess}={}) {
56
91
  const reader=source.getReader(), first=await reader.read();
57
92
  if(first.done)return {delta:false,body:new ReadableStream({start(c){c.close();}})};
58
- let index=base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
59
- const profitable=(raw,frames,initial)=>frames&&frames.reduce((n,b)=>n+b.length,0)+(initial?5:1)<raw.length*0.9&&(!assess||assess(raw,join(initial?[MAGIC,...frames,new Uint8Array([2])]:frames,frames.reduce((n,b)=>n+b.length,0)+(initial?5:0)))===true);
93
+ // A short first chunk always selects ordinary Flight. Do not build a base
94
+ // index that this response cannot use, or wait for a later streaming chunk.
95
+ let index=first.value.length>=WINDOW&&base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
96
+ const profitable=async(raw,frames,initial)=>frames&&frames.reduce((n,b)=>n+b.length,0)+(initial?5:1)<raw.length*0.9&&(!assess||await assess(raw,join(initial?[MAGIC,...frames,new Uint8Array([2])]:frames,frames.reduce((n,b)=>n+b.length,0)+(initial?5:0)))===true);
60
97
  const firstFrames=index&&first.value.length>=WINDOW?encodeChunk(first.value,base,index):null;
61
98
  let useDelta;
62
- try{useDelta=!!profitable(first.value,firstFrames,true);}catch(error){await reader.cancel(error).catch(()=>{});throw error;}
99
+ try{useDelta=!!(await profitable(first.value,firstFrames,true));}catch(error){await reader.cancel(error).catch(()=>{});throw error;}
63
100
  let pending=useDelta?blocks([MAGIC,...firstFrames]):[first.value], pendingAt=0,ended=false,tail=false,commands=firstFrames?.commands||0;
64
101
  if(!useDelta){base=undefined;index=undefined;}
65
102
  return {delta:!!useDelta,body:new ReadableStream({
@@ -67,7 +104,7 @@ async function deltaStream(source,base,{assess}={}) {
67
104
  if(pendingAt===pending.length&&!ended){pending=[];pendingAt=0;const next=await reader.read();
68
105
  if(next.done){ended=true;if(useDelta&&!tail)pending=[new Uint8Array([2])];}
69
106
  else if(useDelta&&!tail){const encoded=encodeChunk(next.value,base,index);commands+=encoded?.commands||0;
70
- if(commands>MAX_COMMANDS||!profitable(next.value,encoded,false)){tail=true;index=undefined;base=undefined;pending=blocks([new Uint8Array([3]),next.value]);}
107
+ if(commands>MAX_COMMANDS||!await profitable(next.value,encoded,false)){tail=true;index=undefined;base=undefined;pending=blocks([new Uint8Array([3]),next.value]);}
71
108
  else pending=blocks(encoded);
72
109
  }else pending=[next.value];
73
110
  }
@@ -132,19 +169,19 @@ function createFlightTransport() {
132
169
  let current,serial=0;
133
170
  return {
134
171
  snapshot(){return current;},
135
- async response(response,base) {
172
+ async response(response,base,{onEnd}={}) {
136
173
  const version=++serial;
137
174
  const token=response.headers.get('x-prnext-flight-token'), headers=new Headers(response.headers);
138
175
  let source=response.body;
139
- if(!source)return response;
176
+ if(!source){onEnd?.();return response;}
140
177
  if(headers.get('content-type')?.startsWith(TYPE)) {
141
178
  if(!base||headers.get('x-prnext-flight-base')!==base.token){await source.cancel();throw new Error('Missing Flight delta base');}
142
179
  source=decodeStream(source,base.bytes);
143
180
  }
144
181
  headers.set('content-type','text/x-component; charset=utf-8');headers.delete('content-length');headers.delete('content-encoding');
145
- return new Response(collectStream(source,{capture:!!token&&/^[a-f0-9]{48}$/.test(token),onComplete:bytes=>{if(version===serial&&token&&/^[a-f0-9]{48}$/.test(token))current={token,bytes};}}),{status:response.status,statusText:response.statusText,headers});
182
+ return new Response(collectStream(source,{capture:!!token&&/^[a-f0-9]{48}$/.test(token),onEnd,onComplete:bytes=>{if(version===serial&&token&&/^[a-f0-9]{48}$/.test(token))current={token,bytes};}}),{status:response.status,statusText:response.statusText,headers});
146
183
  },
147
184
  clear(){serial++;current=undefined;},
148
185
  };
149
186
  }
150
- module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport};
187
+ module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport,ownFlightStream};