@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.
- package/build/app.mjs +46 -78
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +3 -3
- package/build/config.mjs +23 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/index.mjs +30 -13
- package/build/native-ppr.mjs +32 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/cli.mjs +14 -5
- package/compat/app-layout-context.cjs +7 -3
- package/compat/flight-delta.cjs +47 -12
- package/compat/index.d.ts +24 -1
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +110 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/package.json +12 -9
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +150 -26
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +6 -3
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/development.mjs +27 -8
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +31 -5
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
package/build/react-compiler.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/build/static-adapter.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/build/transform.mjs
CHANGED
|
@@ -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=
|
|
46
|
-
if(map)
|
|
47
|
-
|
|
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) {
|
package/build/typescript-dev.mjs
CHANGED
|
@@ -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]
|
|
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 =>
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(() => {
|
package/compat/flight-delta.cjs
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
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) {
|
|
52
|
-
|
|
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};
|