@thomas.f/prnext 0.1.19-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 (85) 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/bundle-runtime.mjs +9 -8
  5. package/build/cache-command.mjs +31 -0
  6. package/build/check.mjs +41 -3
  7. package/build/compilation-cache.mjs +32 -12
  8. package/build/compilation-storage.mjs +190 -0
  9. package/build/compiler.mjs +3 -3
  10. package/build/config.mjs +23 -2
  11. package/build/create.mjs +1 -1
  12. package/build/css-modules.mjs +4 -0
  13. package/build/dev-action-graph.mjs +21 -0
  14. package/build/font-cache.mjs +9 -6
  15. package/build/font-files.mjs +1 -1
  16. package/build/index.mjs +30 -13
  17. package/build/native-ppr.mjs +32 -0
  18. package/build/og.mjs +121 -1
  19. package/build/parallel-plan.mjs +55 -0
  20. package/build/persistent-cache.mjs +29 -24
  21. package/build/production-check.mjs +34 -0
  22. package/build/react-compiler-report.mjs +31 -8
  23. package/build/react-compiler.mjs +101 -10
  24. package/build/server-config.mjs +35 -0
  25. package/build/source-analysis.mjs +54 -4
  26. package/build/static-adapter.mjs +7 -3
  27. package/build/transform.mjs +38 -6
  28. package/build/typescript-dev.mjs +3 -1
  29. package/cli.mjs +14 -5
  30. package/compat/app-layout-context.cjs +7 -3
  31. package/compat/flight-delta.cjs +47 -12
  32. package/compat/index.d.ts +24 -1
  33. package/compat/navigation-authorization.cjs +8 -1
  34. package/docs/ai/compatibility.md +110 -7
  35. package/docs/ai/deployment.md +141 -0
  36. package/docs/ai/quickstart.md +5 -0
  37. package/package.json +12 -9
  38. package/release/fontkit.mjs +31 -0
  39. package/release/verify.mjs +3 -0
  40. package/runtime/api.mjs +5 -2
  41. package/runtime/app-client.mjs +150 -26
  42. package/runtime/app-partial.mjs +2 -10
  43. package/runtime/app-prefetch.mjs +2 -2
  44. package/runtime/app-render.mjs +11 -7
  45. package/runtime/app-stream-channel.mjs +3 -2
  46. package/runtime/app-styles.mjs +22 -4
  47. package/runtime/compile-cache-retention.mjs +53 -0
  48. package/runtime/compile-cache.mjs +28 -1
  49. package/runtime/dev-client.mjs +6 -3
  50. package/runtime/dev-gateway.mjs +3 -2
  51. package/runtime/dev-route-selection.mjs +64 -0
  52. package/runtime/dev-runtime-pool.mjs +3 -2
  53. package/runtime/dev-runtime-spare.mjs +1 -1
  54. package/runtime/dev-session-retention.mjs +154 -0
  55. package/runtime/development.mjs +27 -8
  56. package/runtime/env.mjs +9 -0
  57. package/runtime/flight-delta.mjs +31 -5
  58. package/runtime/native-ppr.mjs +35 -0
  59. package/runtime/og/NOTICE.txt +1 -1
  60. package/runtime/og/index.node.mjs +76 -31216
  61. package/runtime/og/upstream.json +115 -0
  62. package/runtime/persistent/client.mjs +4 -2
  63. package/runtime/persistent/daemon.mjs +3 -11
  64. package/runtime/persistent/health.mjs +58 -0
  65. package/runtime/project-lock.mjs +71 -0
  66. package/runtime/request-transport.mjs +112 -32
  67. package/runtime/storage-path.mjs +36 -0
  68. package/runtime/trace.mjs +44 -4
  69. package/runtime/worker.mjs +9 -4
  70. package/vendor/fontkit/NOTICE.txt +5 -0
  71. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  72. package/vendor/fontkit/index.mjs +3 -0
  73. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  74. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  75. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  76. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  77. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  78. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  79. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  80. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  81. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  82. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  83. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  84. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  85. package/vendor/fontkit/upstream.json +215 -0
@@ -1,19 +1,21 @@
1
1
  import { realpath } from 'node:fs/promises';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { cachedAsyncTransform } from './transform-cache.mjs';
6
6
  import { extractSourceMap, inlineSourceMap, sourceMapsEnabled } from './source-maps.mjs';
7
- import { compilerEvent, recordReactCompilerTransform } from './react-compiler-report.mjs';
7
+ import { compilerEvent, recordReactCompilerTransform, reactCompilerGraphTarget } from './react-compiler-report.mjs';
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
- // Only metadata is loaded here. Disabled builds do not load Babel or the compiler.
10
+ // Oxc is an optional peer. Disabled/Babel builds must not resolve it at all.
11
11
  export const REACT_COMPILER_VERSION = require('babel-plugin-react-compiler/package.json').version;
12
+ export const OXC_REACT_COMPILER_VERSION = require('../package.json').peerDependencies['oxc-transform-react'];
12
13
  const BABEL_VERSION = require('@babel/core/package.json').version;
13
14
  const frameworkRoot = fileURLToPath(new URL('../', import.meta.url));
14
- const options = new Set(['compilationMode', 'panicThreshold', 'target']);
15
+ const options = new Set(['backend', 'compilationMode', 'panicThreshold', 'target']);
15
16
  let implementation;
16
- const REPORT_CACHE_VERSION = 1;
17
+ const oxcImplementations = new Map();
18
+ const REPORT_CACHE_VERSION = 2;
17
19
  const optOutDirective = directives => directives?.map(item => item.value.value).find(value => value === 'use no memo' || value === 'use no forget') || false;
18
20
 
19
21
  function functionLocations(ast) {
@@ -48,14 +50,37 @@ export function normalizeReactCompilerConfig(value) {
48
50
  if (!options.has(key)) throw new TypeError(`reactCompiler.${String(key)} is not supported by PRNext.`);
49
51
  }
50
52
  const compilationMode = input.compilationMode === undefined ? 'infer' : input.compilationMode;
53
+ const backend = input.backend === undefined ? 'babel' : input.backend;
51
54
  const panicThreshold = input.panicThreshold === undefined ? 'none' : input.panicThreshold;
52
55
  const target = input.target === undefined ? '19' : input.target;
53
56
  if (!['infer', 'annotation'].includes(compilationMode)) throw new TypeError('reactCompiler.compilationMode must be infer or annotation.');
54
57
  if (!['none', 'critical_errors', 'all_errors'].includes(panicThreshold)) throw new TypeError('reactCompiler.panicThreshold must be none, critical_errors or all_errors.');
55
58
  if (target !== '19') throw new TypeError("reactCompiler.target must be '19'; PRNext requires React 19.");
56
- return Object.freeze({ compilationMode, panicThreshold, target });
59
+ if (!['babel', 'oxc'].includes(backend)) throw new TypeError('reactCompiler.backend must be babel or oxc.');
60
+ return Object.freeze({ ...(backend === 'oxc' ? { backend } : {}), compilationMode, panicThreshold, target });
57
61
  }
58
62
 
63
+ function oxcDependency(projectRoot) {
64
+ const candidates = projectRoot ? [createRequire(path.join(path.resolve(projectRoot), 'package.json')), require] : [require];
65
+ let metadata, owner, missing;
66
+ for (const candidate of candidates) {
67
+ try { metadata = candidate('oxc-transform-react/package.json'); owner = candidate; break; }
68
+ catch (cause) {
69
+ if (cause.code !== 'MODULE_NOT_FOUND' && cause.code !== 'ERR_MODULE_NOT_FOUND') throw cause;
70
+ missing = cause;
71
+ }
72
+ }
73
+ if (!owner) throw Object.assign(new Error(`reactCompiler.backend="oxc" requires oxc-transform-react@${OXC_REACT_COMPILER_VERSION}. Install it with npm install --save-dev oxc-transform-react@${OXC_REACT_COMPILER_VERSION}, or select backend:"babel".`, {cause: missing}), {code: 'PRNEXT_OXC_MISSING'});
74
+ if (metadata.name !== 'oxc-transform-react' || metadata.version !== OXC_REACT_COMPILER_VERSION) {
75
+ throw Object.assign(new Error(`reactCompiler.backend="oxc" requires oxc-transform-react@${OXC_REACT_COMPILER_VERSION}; found ${metadata.version || 'an unknown version'}. Install the supported version with npm install --save-dev oxc-transform-react@${OXC_REACT_COMPILER_VERSION}.`), {code: 'PRNEXT_OXC_VERSION'});
76
+ }
77
+ // Resolve and import from the same dependency owner. A global CLI must use
78
+ // its application's peer, while nested framework installations still work.
79
+ return {version: metadata.version, entry: owner.resolve('oxc-transform-react')};
80
+ }
81
+
82
+ export const reactCompilerVersion = (config, projectRoot) => config?.backend === 'oxc' ? oxcDependency(projectRoot).version : REACT_COMPILER_VERSION;
83
+
59
84
  function inside(root, filename) {
60
85
  const relative = path.relative(root, filename);
61
86
  return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
@@ -75,14 +100,74 @@ function loadImplementation() {
75
100
  return implementation;
76
101
  }
77
102
 
103
+ function loadOxcImplementation(dependency) {
104
+ if (!oxcImplementations.has(dependency.entry)) oxcImplementations.set(dependency.entry, Promise.all([import(pathToFileURL(dependency.entry).href), import('oxc-parser')]).then(([compiler, parser]) => {
105
+ if (compiler.__napiBindingTarget !== 'native') throw new Error('reactCompiler.backend="oxc" requires the native platform binding. Install optional dependencies or select backend:"babel".');
106
+ return { transform: compiler.transform, parse: parser.parseSync };
107
+ }).catch(cause => {
108
+ throw new Error(`Cannot load the native oxc-transform-react@${OXC_REACT_COMPILER_VERSION} compiler. Reinstall it with optional dependencies enabled on this platform, or select backend:"babel". ${cause.message}`, {cause});
109
+ }));
110
+ return oxcImplementations.get(dependency.entry);
111
+ }
112
+
113
+ /** Compose only source-map coordinates; application strings never enter this path. */
114
+ async function composeOxcMap(output, input) {
115
+ if (!input || !output) return output;
116
+ const { TraceMap, decodedMappings, originalPositionFor, sourceContentFor, encodedMap } = await import('@jridgewell/trace-mapping');
117
+ const previous = new TraceMap(input), current = new TraceMap(output);
118
+ const sources = [], contents = [], names = [];
119
+ const mappings = decodedMappings(current).map(line => line.map(segment => {
120
+ if (segment.length < 4) return [segment[0]];
121
+ const position = originalPositionFor(previous, { line: segment[2] + 1, column: segment[3] });
122
+ if (position.source === null || position.line === null || position.column === null) return [segment[0]];
123
+ let source = sources.indexOf(position.source);
124
+ if (source < 0) { source = sources.length; sources.push(position.source); contents.push(sourceContentFor(previous, position.source)); }
125
+ const mapped = [segment[0], source, position.line - 1, position.column];
126
+ const name = position.name || (segment.length > 4 ? current.names[segment[4]] : null);
127
+ if (name) { let index = names.indexOf(name); if (index < 0) { index = names.length; names.push(name); } mapped.push(index); }
128
+ return mapped;
129
+ }));
130
+ return encodedMap(new TraceMap({ version: 3, sources, sourcesContent: contents, names, mappings }));
131
+ }
132
+
133
+ async function transformOxc(source, filename, normalized, { mapped, outputMode, loaderOutput, root, dependency }) {
134
+ const { transform, parse } = await loadOxcImplementation(dependency);
135
+ const { code, map } = extractSourceMap(source);
136
+ const lang = /\.tsx$/i.test(filename) ? 'tsx' : /\.[cm]?ts$/i.test(filename) ? 'ts' : 'jsx';
137
+ // The native transform does not expose directives. Inspect its original input
138
+ // with the existing Rust parser, never Babel, before entering executable modules.
139
+ const parsed = parse(filename, code, { lang, sourceType: 'unambiguous' });
140
+ const directives = [];
141
+ if (!parsed.errors.length) for (const statement of parsed.program.body) {
142
+ if (!statement.directive) break;
143
+ directives.push(statement.directive);
144
+ }
145
+ if (directives.includes('use server')) return { code: source, events: [], notes: ['Server Action module excluded.'] };
146
+ const result = await transform(filename, code, { lang, sourceType: 'unambiguous', jsx: 'preserve', sourcemap: mapped,
147
+ reactCompiler: { compilationMode: normalized.compilationMode, panicThreshold: normalized.panicThreshold,
148
+ target: normalized.target, outputMode, reportDiagnostics: true, sources: [''] } });
149
+ const diagnostics = result.errors.slice(0, 16).map(error => compilerEvent({
150
+ kind: result.fatal ? 'CompileError' : 'CompileDiagnostic', reason: error.message, detail: { category: 'Oxc' },
151
+ }, [], { root })).filter(Boolean);
152
+ const notes = ['Experimental Oxc backend; upstream does not expose per-function optimization events. Function optimization totals are unavailable.',
153
+ `Oxc output mode: ${outputMode}. TypeScript syntax is removed; JSX is preserved for the graph compiler.`];
154
+ if (normalized.compilationMode === 'annotation') notes.push('Annotation mode only selects functions with "use memo".');
155
+ if (loaderOutput) notes.push('Locations refer to JavaScript produced by the configured loader.');
156
+ return { code: result.fatal ? source : mapped ? inlineSourceMap(result.code, await composeOxcMap(result.map, map)) : result.code,
157
+ events: [], diagnostics, notes, fatal: result.fatal, truncated: result.errors.length > 16 };
158
+ }
159
+
78
160
  /**
79
- * Run the official compiler on application source before JSX lowering, React
80
- * Refresh and framework instrumentation. JSX/TypeScript/module format remain
81
- * intact for the existing graph compiler. No app Babel configuration is loaded.
161
+ * Compile application source before JSX lowering and React Refresh. The Oxc
162
+ * development path instruments original JSX first, before native hoisting.
163
+ * JSX remains available to the graph compiler; Oxc also removes TypeScript.
164
+ * No application Babel configuration is loaded.
82
165
  */
83
166
  export function createReactCompilerTransform(projectRoot, config, { dev = false } = {}) {
84
167
  const normalized = normalizeReactCompilerConfig(config);
85
168
  if (!normalized) return async source => source;
169
+ const dependency = normalized.backend === 'oxc' ? oxcDependency(projectRoot) : null;
170
+ const compilerVersion = dependency?.version || REACT_COMPILER_VERSION;
86
171
  const root = path.resolve(projectRoot);
87
172
  const canonicalRoot = realpath(root).catch(error => {
88
173
  if (error.code === 'ENOENT') return root;
@@ -102,10 +187,15 @@ export function createReactCompilerTransform(projectRoot, config, { dev = false
102
187
  throw error;
103
188
  });
104
189
  if (excluded(actualRoot, actualFile) || inside(await canonicalFramework, actualFile)) return source;
190
+ const backend = normalized.backend || 'babel';
191
+ const graph = reactCompilerGraphTarget();
192
+ const outputMode = graph === 'node' || graph === 'edge' ? 'ssr' : 'client';
105
193
  let cacheHit = true;
106
194
  const serialized = await cachedAsyncTransform('react-compiler-report', source,
107
- [REPORT_CACHE_VERSION, REACT_COMPILER_VERSION, BABEL_VERSION, normalized, root, actualRoot, filename, actualFile, !!dev, !!loaderOutput], async () => {
195
+ [REPORT_CACHE_VERSION, backend, compilerVersion, dependency?.entry, backend === 'babel' ? BABEL_VERSION : null,
196
+ normalized, root, actualRoot, filename, actualFile, !!dev, !!loaderOutput, backend === 'oxc' ? outputMode : null], async () => {
108
197
  cacheHit = false;
198
+ if (backend === 'oxc') return JSON.stringify(await transformOxc(source, filename, normalized, { mapped: sourceMapsEnabled(), outputMode, loaderOutput, root, dependency }));
109
199
  const { babel, compiler } = await loadImplementation();
110
200
  const mapped = sourceMapsEnabled();
111
201
  const { code, map } = extractSourceMap(source);
@@ -134,6 +224,7 @@ export function createReactCompilerTransform(projectRoot, config, { dev = false
134
224
  });
135
225
  const payload = JSON.parse(serialized);
136
226
  recordReactCompilerTransform(inside(root, filename) ? filename : path.resolve(root, path.relative(actualRoot, actualFile)), payload, { cacheHit });
227
+ if (payload.fatal) throw new Error(`React Compiler (oxc ${OXC_REACT_COMPILER_VERSION}) failed for ${filename}: ${payload.diagnostics.map(value => value.reason).join('; ') || 'Native compilation failed.'}`);
137
228
  return payload.code;
138
229
  };
139
230
  }
@@ -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);
@@ -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=>{
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
@@ -109,6 +112,10 @@ function stop(child) {
109
112
  async function main() {
110
113
  if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
111
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
+ }
112
119
  if (command === 'create') {
113
120
  const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
114
121
  if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
@@ -313,12 +320,12 @@ async function main() {
313
320
  process.env.PWD = root;
314
321
  }
315
322
  if (command === 'check') {
316
- 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');
317
324
  const { checkProject } = await import('./build/check.mjs');
318
- const report = await checkProject(root);
325
+ const report = await checkProject(root,{production:args.includes('--production')});
319
326
  if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
320
327
  else {
321
- console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
328
+ console.log(`PRNext ${report.mode==='production'?'production ':''}preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
322
329
  console.log(`${report.routes.length} routes. Installed versions: ${Object.entries(report.versions).map(([name,version])=>`${name}@${version||'missing'}`).join(', ')}`);
323
330
  for (const item of report.diagnostics) {
324
331
  const output = item.severity === 'error' ? console.error : console.log;
@@ -380,7 +387,9 @@ async function main() {
380
387
  const binary = await resolveNativeBinary();
381
388
  if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
382
389
  const outputDirectory = await readBuildDirectory(root);
383
- 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'};
384
393
  if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
385
394
  if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
386
395
  '--node',process.execPath,...args],serverEnvironment))return;
@@ -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(() => {
@@ -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,12 +44,47 @@ 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}={}) {
@@ -58,10 +93,10 @@ async function deltaStream(source,base,{assess}={}) {
58
93
  // A short first chunk always selects ordinary Flight. Do not build a base
59
94
  // index that this response cannot use, or wait for a later streaming chunk.
60
95
  let index=first.value.length>=WINDOW&&base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
61
- 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);
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);
62
97
  const firstFrames=index&&first.value.length>=WINDOW?encodeChunk(first.value,base,index):null;
63
98
  let useDelta;
64
- 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;}
65
100
  let pending=useDelta?blocks([MAGIC,...firstFrames]):[first.value], pendingAt=0,ended=false,tail=false,commands=firstFrames?.commands||0;
66
101
  if(!useDelta){base=undefined;index=undefined;}
67
102
  return {delta:!!useDelta,body:new ReadableStream({
@@ -69,7 +104,7 @@ async function deltaStream(source,base,{assess}={}) {
69
104
  if(pendingAt===pending.length&&!ended){pending=[];pendingAt=0;const next=await reader.read();
70
105
  if(next.done){ended=true;if(useDelta&&!tail)pending=[new Uint8Array([2])];}
71
106
  else if(useDelta&&!tail){const encoded=encodeChunk(next.value,base,index);commands+=encoded?.commands||0;
72
- 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]);}
73
108
  else pending=blocks(encoded);
74
109
  }else pending=[next.value];
75
110
  }
@@ -134,19 +169,19 @@ function createFlightTransport() {
134
169
  let current,serial=0;
135
170
  return {
136
171
  snapshot(){return current;},
137
- async response(response,base) {
172
+ async response(response,base,{onEnd}={}) {
138
173
  const version=++serial;
139
174
  const token=response.headers.get('x-prnext-flight-token'), headers=new Headers(response.headers);
140
175
  let source=response.body;
141
- if(!source)return response;
176
+ if(!source){onEnd?.();return response;}
142
177
  if(headers.get('content-type')?.startsWith(TYPE)) {
143
178
  if(!base||headers.get('x-prnext-flight-base')!==base.token){await source.cancel();throw new Error('Missing Flight delta base');}
144
179
  source=decodeStream(source,base.bytes);
145
180
  }
146
181
  headers.set('content-type','text/x-component; charset=utf-8');headers.delete('content-length');headers.delete('content-encoding');
147
- 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});
148
183
  },
149
184
  clear(){serial++;current=undefined;},
150
185
  };
151
186
  }
152
- module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport};
187
+ module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport,ownFlightStream};