@thomas.f/prnext 0.1.18-alpha → 0.1.20-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/build/actions.mjs +26 -14
- package/build/app.mjs +90 -101
- 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 +8 -7
- package/build/config.mjs +25 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +14 -12
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dynamic.mjs +13 -6
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +61 -15
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-ppr.mjs +32 -0
- package/build/npm-packages.mjs +1 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-builder.mjs +1 -1
- 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/standalone.mjs +21 -3
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +24 -6
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +7 -3
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +50 -13
- package/compat/index.d.ts +24 -1
- package/compat/link.cjs +2 -2
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +187 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +26 -11
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +254 -53
- 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/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +13 -5
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- 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/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +86 -26
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +59 -9
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- 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/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
|
|
3
|
+
export function validateServerConfig(value) {
|
|
4
|
+
if (value === undefined) return { trustedProxies: [] };
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['publicOrigin', 'trustedProxies'].includes(key))) {
|
|
6
|
+
throw new TypeError('server must be an object with publicOrigin and trustedProxies.');
|
|
7
|
+
}
|
|
8
|
+
let publicOrigin;
|
|
9
|
+
if (value.publicOrigin !== undefined) {
|
|
10
|
+
const source = value.publicOrigin;
|
|
11
|
+
let url;
|
|
12
|
+
try { url = new URL(source); } catch { /* validated below */ }
|
|
13
|
+
if (typeof source !== 'string' || /[\s\\\u0000-\u001f\u007f]/.test(source) || !url || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash || source.includes('?') || source.includes('#')) {
|
|
14
|
+
throw new TypeError('server.publicOrigin must be an HTTP(S) origin without credentials, path, query or fragment, for example https://example.com.');
|
|
15
|
+
}
|
|
16
|
+
publicOrigin = url.origin;
|
|
17
|
+
}
|
|
18
|
+
const ranges = value.trustedProxies ?? [];
|
|
19
|
+
if (!Array.isArray(ranges) || ranges.length > 32 || ranges.some(range => {
|
|
20
|
+
if (typeof range !== 'string' || range.length > 64) return true;
|
|
21
|
+
const [address, bits, ...extra] = range.split('/');
|
|
22
|
+
const family = isIP(address);
|
|
23
|
+
return !family || extra.length || bits !== undefined && (!/^(0|[1-9]\d*)$/.test(bits) || Number(bits) > (family === 4 ? 32 : 128));
|
|
24
|
+
})) throw new TypeError('server.trustedProxies must contain at most 32 explicit IP addresses or CIDR ranges.');
|
|
25
|
+
const normalized = ranges.map(range => {
|
|
26
|
+
const [address, bits] = range.split('/');
|
|
27
|
+
if (isIP(address) !== 6) return range;
|
|
28
|
+
const mapped = /^\[::ffff:([\da-f]+):([\da-f]+)\]$/i.exec(new URL(`http://[${address}]`).hostname);
|
|
29
|
+
if (!mapped) return range;
|
|
30
|
+
if (bits !== undefined && Number(bits) < 96) throw new TypeError('Mapped IPv4 proxy CIDR ranges need a prefix of at least 96 bits.');
|
|
31
|
+
const high = parseInt(mapped[1], 16), low = parseInt(mapped[2], 16);
|
|
32
|
+
return `${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}${bits === undefined ? '' : '/' + (Number(bits) - 96)}`;
|
|
33
|
+
});
|
|
34
|
+
return { ...(publicOrigin ? { publicOrigin } : {}), trustedProxies: [...new Set(normalized)] };
|
|
35
|
+
}
|
|
@@ -6,31 +6,81 @@ import {createHash} from 'node:crypto';
|
|
|
6
6
|
const cache=new Map();
|
|
7
7
|
let bytes=0,hits=0,misses=0;
|
|
8
8
|
export const sourceAnalysisStats=()=>({entries:cache.size,bytes,hits,misses});
|
|
9
|
+
const identifier=(node,name)=>node?.type==='Identifier'&&(name===undefined||node.name===name);
|
|
10
|
+
const property=node=>node?.computed?node.property?.value:node?.property?.name;
|
|
11
|
+
const moduleExports=node=>node?.type==='MemberExpression'&&identifier(node.object,'module')&&property(node)==='exports';
|
|
12
|
+
function bindingNames(node,names){
|
|
13
|
+
if(!node)return;
|
|
14
|
+
if(identifier(node))names.add(node.name);
|
|
15
|
+
else if(node.type==='ObjectPattern')for(const item of node.properties)bindingNames(item.type==='RestElement'?item.argument:item.value,names);
|
|
16
|
+
else if(node.type==='ArrayPattern')for(const item of node.elements)bindingNames(item,names);
|
|
17
|
+
else if(node.type==='RestElement')bindingNames(node.argument,names);
|
|
18
|
+
else if(node.type==='AssignmentPattern')bindingNames(node.left,names);
|
|
19
|
+
}
|
|
20
|
+
function runtimeDeclaration(node,names){
|
|
21
|
+
if(!node||node.declare)return;
|
|
22
|
+
if(node.type==='VariableDeclaration')for(const item of node.declarations)bindingNames(item.id,names);
|
|
23
|
+
else if(['FunctionDeclaration','ClassDeclaration','TSEnumDeclaration','TSModuleDeclaration'].includes(node.type))bindingNames(node.id,names);
|
|
24
|
+
}
|
|
9
25
|
export function analyzeSource(source,file='source.js') {
|
|
10
26
|
const key=file+'\0'+createHash('sha256').update(source).digest('hex');
|
|
11
27
|
if(cache.has(key)){const hit=cache.get(key);cache.delete(key);cache.set(key,hit);hits++;return hit.value;}
|
|
12
28
|
misses++;
|
|
13
|
-
|
|
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/standalone.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { nodeFileTrace } from '
|
|
2
|
-
import resolver from '
|
|
1
|
+
import { nodeFileTrace } from '../vendor/nft/out/index.js';
|
|
2
|
+
import resolver from '../vendor/nft/out/resolve-dependency.js';
|
|
3
|
+
import { CachedFileSystem } from '../vendor/nft/out/fs.js';
|
|
3
4
|
import picomatch from 'picomatch';
|
|
4
5
|
import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink, cp } from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
@@ -12,6 +13,10 @@ const within = (root, file) => file === root || file.startsWith(root + path.sep)
|
|
|
12
13
|
const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
|
|
13
14
|
const maxFiles = 100_000;
|
|
14
15
|
const maxBytes = 2 * 1024 ** 3;
|
|
16
|
+
const linuxRuntimeProbes = new Set([
|
|
17
|
+
'/proc/self', '/proc/self/exe', '/usr/bin/ldd',
|
|
18
|
+
'/proc/version', '/proc/self/cgroup', '/.dockerenv', '/run/.containerenv',
|
|
19
|
+
]);
|
|
15
20
|
|
|
16
21
|
export async function resolveTraceDependency(...args) {
|
|
17
22
|
const result = await resolver.default(...args);
|
|
@@ -75,6 +80,10 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
75
80
|
const warnings = new Set();
|
|
76
81
|
let omittedWarnings = 0;
|
|
77
82
|
const cache = {};
|
|
83
|
+
const traceFileSystem = new CachedFileSystem({ cache, fileIOConcurrency: 16 });
|
|
84
|
+
// libc and environment detectors read the running host, not application assets.
|
|
85
|
+
// Keep this exact and outside the tracing root; other external files still fail.
|
|
86
|
+
const systemProbe = file => process.platform === 'linux' && !within(tracingRoot, file) && linuxRuntimeProbes.has(file);
|
|
78
87
|
const traceLocations = new Map();
|
|
79
88
|
let traceReads = 0;
|
|
80
89
|
let traceBytes = 0;
|
|
@@ -120,7 +129,16 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
120
129
|
for (const conditions of [['node'], ['node', 'react-server']]) {
|
|
121
130
|
const result = await nodeFileTrace(entries, {
|
|
122
131
|
base: path.parse(projectRoot).root, processCwd: projectRoot, conditions, cache, fileIOConcurrency: 16,
|
|
123
|
-
ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
|
|
132
|
+
ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return systemProbe(absolute) || !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
|
|
133
|
+
// Share the metadata concurrency limit between stat and readlink.
|
|
134
|
+
stat: file => traceFileSystem.stat(file),
|
|
135
|
+
async readlink(file) {
|
|
136
|
+
// NFT follows symlinks before ignore(), so never follow these probes
|
|
137
|
+
// into /proc/<pid> or the build machine's Node executable.
|
|
138
|
+
if (systemProbe(file)) return null;
|
|
139
|
+
try { return await traceFileSystem.readlink(file); }
|
|
140
|
+
catch (error) { if (error.code === 'ENOTDIR') return null; throw error; }
|
|
141
|
+
},
|
|
124
142
|
async readFile(file) {
|
|
125
143
|
let info;
|
|
126
144
|
try { info = await stat(file); } catch (error) { if (missing(error)) return null; throw error; }
|
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/build/webpack-graph.mjs
CHANGED
|
@@ -77,6 +77,13 @@ function pluginIdentity(plugin,webpack){
|
|
|
77
77
|
export const webpackCompilerStats=()=>({contexts:sessions.size,created,reused,limit:sessionLimit,buildLimit:sessionBuildLimit});
|
|
78
78
|
function queued(callback){const task=graphQueue.then(callback);graphQueue=task.catch(()=>{});return task;}
|
|
79
79
|
export function closeWebpackCompiler(root){return queued(async()=>{
|
|
80
|
+
if(root!==undefined){
|
|
81
|
+
try{root=await realpath(root);}
|
|
82
|
+
catch(error){
|
|
83
|
+
if(error.code!=='ENOENT'&&error.code!=='ENOTDIR')throw error;
|
|
84
|
+
root=path.resolve(root);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
80
87
|
const closing=[];for(const[key,session]of sessions)if(root===undefined||session.root===root){sessions.delete(key);closing.push(session.dispose());}
|
|
81
88
|
const results=await Promise.allSettled(closing),errors=results.filter(result=>result.status==='rejected').map(result=>result.reason);
|
|
82
89
|
if(errors.length)throw new AggregateError(errors,'Unable to dispose webpack compiler sessions');
|
|
@@ -84,6 +91,9 @@ export function closeWebpackCompiler(root){return queued(async()=>{
|
|
|
84
91
|
|
|
85
92
|
async function compileGraph(resolver,loads,options,{state,config,target}){
|
|
86
93
|
return queued(async()=>{
|
|
94
|
+
// Ownership uses one canonical identity for invalidation and disposal.
|
|
95
|
+
// Compiler paths and graph fingerprints retain the caller's lexical root.
|
|
96
|
+
const ownerRoot=await realpath(state.root);
|
|
87
97
|
const webpack=(await import('webpack')).default;
|
|
88
98
|
let reusable=state.dev&&state.incremental,identity;
|
|
89
99
|
if(reusable){try{identity=await buildCacheIdentity(state.root);}catch{}if(!identity)reusable=false;}
|
|
@@ -97,17 +107,17 @@ async function compileGraph(resolver,loads,options,{state,config,target}){
|
|
|
97
107
|
// Unknown data structures, safe mode and unavailable identities all keep
|
|
98
108
|
// compilation correct through a one-shot compiler. Hidden mutable closure
|
|
99
109
|
// state cannot be fingerprinted: replace the plugin instance when it changes.
|
|
100
|
-
if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===
|
|
110
|
+
if(state.dev&&!reusable)for(const[key,session]of sessions)if(session.root===ownerRoot){sessions.delete(key);await session.dispose();}
|
|
101
111
|
let session=reusable&&sessions.get(key);
|
|
102
112
|
if(session&&session.builds>=sessionBuildLimit){sessions.delete(key);await session.dispose();session=undefined;}
|
|
103
113
|
if(reusable){
|
|
104
|
-
for(const[old,previous]of sessions)if(previous.root===
|
|
114
|
+
for(const[old,previous]of sessions)if(previous.root===ownerRoot&&(previous.identity!==identity||previous.base===base&&old!==key)){sessions.delete(old);await previous.dispose();}
|
|
105
115
|
}
|
|
106
116
|
if(session){sessions.delete(key);reused++;}
|
|
107
117
|
else{
|
|
108
118
|
if(reusable)while(sessions.size>=sessionLimit){const[old,previous]=sessions.entries().next().value;sessions.delete(old);await previous.dispose();}
|
|
109
119
|
session=await createGraphSession(state.root,{webpack,reusable,stage:state.stage});created++;
|
|
110
|
-
Object.assign(session,{root:
|
|
120
|
+
Object.assign(session,{root:ownerRoot,base,identity,builds:0});
|
|
111
121
|
}
|
|
112
122
|
if(reusable)sessions.set(key,session);
|
|
113
123
|
try{session.builds++;return await session.run(resolver,loads,options,{state,config,target});}
|
package/cli.mjs
CHANGED
|
@@ -38,12 +38,15 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
|
38
38
|
prnext docs --ai [topic] Read the AI guide bundled with this version
|
|
39
39
|
prnext docs --ai --init [directory] Add a guide pointer to the project's AGENTS.md
|
|
40
40
|
prnext routes [directory] Display compiled routes
|
|
41
|
-
prnext check [directory] [--json]
|
|
41
|
+
prnext check [directory] [--production] [--json] Check config, deployment and dependencies
|
|
42
42
|
prnext typegen [directory] [--json] Generate route types without building the app
|
|
43
43
|
prnext typecheck [directory] [--json] Generate types and check TypeScript without building
|
|
44
44
|
prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
|
|
45
45
|
prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
|
|
46
46
|
prnext compiler [directory] [--json] React Compiler results from the last successful build
|
|
47
|
+
prnext cache [directory] [--json] Show compilation cache sizes and disk budget
|
|
48
|
+
prnext cache [directory] --prune [--dry-run] [--max-bytes N] [--json]
|
|
49
|
+
Reclaim regenerable compilation caches
|
|
47
50
|
prnext host [prnext.host.json] Host built apps by hostname; wake on demand
|
|
48
51
|
prnext host [config] --check Validate hosting configuration and builds
|
|
49
52
|
prnext host [config] --status [--json] Show memory, process and idle status
|
|
@@ -58,6 +61,8 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
|
|
|
58
61
|
prnext migrate --check Audit source compatibility without migrating
|
|
59
62
|
prnext migrate --check --against URL --candidate URL [--routes paths.json]
|
|
60
63
|
Compare GET responses before switching
|
|
64
|
+
prnext migrate --check --against URL --candidate URL --browser-scenarios ./scenarios.mjs
|
|
65
|
+
Run trusted application browser assertions
|
|
61
66
|
Migration keeps Next.js and backs up package.json and existing lockfiles.
|
|
62
67
|
Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
|
|
63
68
|
Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
|
|
@@ -107,6 +112,10 @@ function stop(child) {
|
|
|
107
112
|
async function main() {
|
|
108
113
|
if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
|
|
109
114
|
if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
|
|
115
|
+
if(command==='cache'){
|
|
116
|
+
const {cacheCommand}=await import('./build/cache-command.mjs');
|
|
117
|
+
await cacheCommand(argv);return;
|
|
118
|
+
}
|
|
110
119
|
if (command === 'create') {
|
|
111
120
|
const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
|
|
112
121
|
if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
|
|
@@ -191,13 +200,20 @@ async function main() {
|
|
|
191
200
|
report.comparison = await compareServers({ ...options, routes });
|
|
192
201
|
report.ok &&= report.comparison.ok;
|
|
193
202
|
}
|
|
203
|
+
if (options.browserScenarios) {
|
|
204
|
+
const { runMigrationScenarios } = await import('./build/migration-scenarios.mjs');
|
|
205
|
+
report.browser = await runMigrationScenarios({ directory, ...options, scenarios: options.browserScenarios });
|
|
206
|
+
report.ok &&= report.browser.ok;
|
|
207
|
+
}
|
|
194
208
|
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
195
209
|
else {
|
|
196
210
|
console.log(`PRNext migration check ${report.ok ? 'passed' : 'failed'}: ${directory}`);
|
|
197
211
|
console.log(`${report.files} source files; ${report.routes.length} routes.`);
|
|
198
212
|
for (const item of report.findings) console.log(`${item.severity.toUpperCase()} ${item.file || 'project'}${item.line ? ':' + item.line : ''}: ${item.message}\n ${item.action}`);
|
|
199
213
|
for (const result of report.comparison?.results || []) console.log(`${result.equal ? 'MATCH' : 'DIFFERENT'} ${result.route}: ${result.error || result.differences.join(', ') || 'responses match'}`);
|
|
200
|
-
for (const
|
|
214
|
+
for (const result of report.browser?.results || []) console.log(`BROWSER ${result.scenario} (${result.target}): ${result.status}${result.reason ? ' — ' + result.reason : ''}`);
|
|
215
|
+
if (report.browser?.reason) console.log(`BROWSER unknown: ${report.browser.reason}`);
|
|
216
|
+
for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : []), ...(report.browser ? [report.browser.scope] : [])]) console.log(note);
|
|
201
217
|
}
|
|
202
218
|
if (!report.ok) process.exitCode = 1;
|
|
203
219
|
return;
|
|
@@ -304,12 +320,12 @@ async function main() {
|
|
|
304
320
|
process.env.PWD = root;
|
|
305
321
|
}
|
|
306
322
|
if (command === 'check') {
|
|
307
|
-
if (args.some(arg =>
|
|
323
|
+
if (args.some(arg => !['--json','--production'].includes(arg))||new Set(args).size!==args.length) throw new Error('check accepts [directory], --production and --json once each');
|
|
308
324
|
const { checkProject } = await import('./build/check.mjs');
|
|
309
|
-
const report = await checkProject(root);
|
|
325
|
+
const report = await checkProject(root,{production:args.includes('--production')});
|
|
310
326
|
if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
|
311
327
|
else {
|
|
312
|
-
console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
|
|
328
|
+
console.log(`PRNext ${report.mode==='production'?'production ':''}preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
|
|
313
329
|
console.log(`${report.routes.length} routes. Installed versions: ${Object.entries(report.versions).map(([name,version])=>`${name}@${version||'missing'}`).join(', ')}`);
|
|
314
330
|
for (const item of report.diagnostics) {
|
|
315
331
|
const output = item.severity === 'error' ? console.error : console.log;
|
|
@@ -371,7 +387,9 @@ async function main() {
|
|
|
371
387
|
const binary = await resolveNativeBinary();
|
|
372
388
|
if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
|
|
373
389
|
const outputDirectory = await readBuildDirectory(root);
|
|
374
|
-
const serverEnvironment =
|
|
390
|
+
const serverEnvironment = command==='start'
|
|
391
|
+
? (await import('./runtime/env.mjs')).productionEnvironment(root)
|
|
392
|
+
: {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
|
|
375
393
|
if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
|
|
376
394
|
if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
|
|
377
395
|
'--node',process.execPath,...args],serverEnvironment))return;
|
package/compat/app-dynamic.cjs
CHANGED
|
@@ -19,7 +19,18 @@ function dynamic(input, options) {
|
|
|
19
19
|
if (!opts.ssr && typeof React.useEffect !== 'function') {
|
|
20
20
|
throw new Error('ssr: false is not supported with next/dynamic in Server Components. Move it into a Client Component.');
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
let Lazy;
|
|
23
|
+
if(process.env.NODE_ENV==='development'&&typeof window!=='undefined'&&typeof opts.__prnextDevIdentity==='string'){
|
|
24
|
+
const registry=globalThis.__PRNEXT_DEV_DYNAMIC__||=new Map();
|
|
25
|
+
let entry=registry.get(opts.__prnextDevIdentity);
|
|
26
|
+
if(!entry||entry.failed){
|
|
27
|
+
entry={loader:opts.loader};
|
|
28
|
+
entry.lazy=React.lazy(()=>Promise.resolve().then(()=>entry.loader()).then(moduleOf,error=>{entry.failed=true;throw error;}));
|
|
29
|
+
if(registry.size>=2048)registry.delete(registry.keys().next().value);
|
|
30
|
+
registry.set(opts.__prnextDevIdentity,entry);
|
|
31
|
+
}else entry.loader=opts.loader;
|
|
32
|
+
Lazy=entry.lazy;
|
|
33
|
+
}else Lazy=React.lazy(() => Promise.resolve().then(opts.loader).then(moduleOf));
|
|
23
34
|
function DynamicComponent(props) {
|
|
24
35
|
const fallback = opts.loading ? React.createElement(opts.loading, { isLoading: true, pastDelay: true, error: null }) : null;
|
|
25
36
|
const child = React.createElement(Lazy, props);
|
|
@@ -12,17 +12,21 @@ function ActivitySlot({ identity, children, authorizationKey }) {
|
|
|
12
12
|
const router = React.useContext(AppRouterContext);
|
|
13
13
|
const epoch = retention?.epoch ?? 0;
|
|
14
14
|
const authorizationEpoch = retention?.authorizationEpoch ?? 0;
|
|
15
|
+
const flight = retention?.flight;
|
|
15
16
|
const registrations = React.useRef(new Set());
|
|
16
17
|
const serial = React.useRef(0);
|
|
17
18
|
const [saved, setSaved] = React.useState(() => ({ epoch, authorizationEpoch, active: identity, entries: new Map() }));
|
|
18
19
|
let entries = saved.entries;
|
|
19
20
|
const current = entries.get(identity);
|
|
20
|
-
|
|
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(() => {
|
|
@@ -80,7 +80,9 @@ async function read(entry, context) {
|
|
|
80
80
|
if (size > MAX_BYTES) throw new Error('CacheEntry exceeds 2 MiB');
|
|
81
81
|
chunks.push(Buffer.from(value));
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// Buffer.from above owns each chunk even when a custom producer reuses its
|
|
84
|
+
// Uint8Array on the next pull. Keep that copy, but do not copy it twice.
|
|
85
|
+
return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
|
|
84
86
|
} finally {
|
|
85
87
|
if (!complete) void reader.cancel().catch(() => {});
|
|
86
88
|
reader.releaseLock();
|
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,22 +44,59 @@ function blocks(parts) {
|
|
|
44
44
|
for(const part of parts)for(let offset=0;offset<part.length;){const n=Math.min(BLOCK_BYTES-size,part.length-offset);pending.push(part.subarray(offset,offset+n));size+=n;offset+=n;if(size===BLOCK_BYTES)flush();}
|
|
45
45
|
flush();return output;
|
|
46
46
|
}
|
|
47
|
-
|
|
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}={}) {
|
|
56
91
|
const reader=source.getReader(), first=await reader.read();
|
|
57
92
|
if(first.done)return {delta:false,body:new ReadableStream({start(c){c.close();}})};
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
// A short first chunk always selects ordinary Flight. Do not build a base
|
|
94
|
+
// index that this response cannot use, or wait for a later streaming chunk.
|
|
95
|
+
let index=first.value.length>=WINDOW&&base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
|
|
96
|
+
const profitable=async(raw,frames,initial)=>frames&&frames.reduce((n,b)=>n+b.length,0)+(initial?5:1)<raw.length*0.9&&(!assess||await assess(raw,join(initial?[MAGIC,...frames,new Uint8Array([2])]:frames,frames.reduce((n,b)=>n+b.length,0)+(initial?5:0)))===true);
|
|
60
97
|
const firstFrames=index&&first.value.length>=WINDOW?encodeChunk(first.value,base,index):null;
|
|
61
98
|
let useDelta;
|
|
62
|
-
try{useDelta=!!profitable(first.value,firstFrames,true);}catch(error){await reader.cancel(error).catch(()=>{});throw error;}
|
|
99
|
+
try{useDelta=!!(await profitable(first.value,firstFrames,true));}catch(error){await reader.cancel(error).catch(()=>{});throw error;}
|
|
63
100
|
let pending=useDelta?blocks([MAGIC,...firstFrames]):[first.value], pendingAt=0,ended=false,tail=false,commands=firstFrames?.commands||0;
|
|
64
101
|
if(!useDelta){base=undefined;index=undefined;}
|
|
65
102
|
return {delta:!!useDelta,body:new ReadableStream({
|
|
@@ -67,7 +104,7 @@ async function deltaStream(source,base,{assess}={}) {
|
|
|
67
104
|
if(pendingAt===pending.length&&!ended){pending=[];pendingAt=0;const next=await reader.read();
|
|
68
105
|
if(next.done){ended=true;if(useDelta&&!tail)pending=[new Uint8Array([2])];}
|
|
69
106
|
else if(useDelta&&!tail){const encoded=encodeChunk(next.value,base,index);commands+=encoded?.commands||0;
|
|
70
|
-
if(commands>MAX_COMMANDS||!profitable(next.value,encoded,false)){tail=true;index=undefined;base=undefined;pending=blocks([new Uint8Array([3]),next.value]);}
|
|
107
|
+
if(commands>MAX_COMMANDS||!await profitable(next.value,encoded,false)){tail=true;index=undefined;base=undefined;pending=blocks([new Uint8Array([3]),next.value]);}
|
|
71
108
|
else pending=blocks(encoded);
|
|
72
109
|
}else pending=[next.value];
|
|
73
110
|
}
|
|
@@ -132,19 +169,19 @@ function createFlightTransport() {
|
|
|
132
169
|
let current,serial=0;
|
|
133
170
|
return {
|
|
134
171
|
snapshot(){return current;},
|
|
135
|
-
async response(response,base) {
|
|
172
|
+
async response(response,base,{onEnd}={}) {
|
|
136
173
|
const version=++serial;
|
|
137
174
|
const token=response.headers.get('x-prnext-flight-token'), headers=new Headers(response.headers);
|
|
138
175
|
let source=response.body;
|
|
139
|
-
if(!source)return response;
|
|
176
|
+
if(!source){onEnd?.();return response;}
|
|
140
177
|
if(headers.get('content-type')?.startsWith(TYPE)) {
|
|
141
178
|
if(!base||headers.get('x-prnext-flight-base')!==base.token){await source.cancel();throw new Error('Missing Flight delta base');}
|
|
142
179
|
source=decodeStream(source,base.bytes);
|
|
143
180
|
}
|
|
144
181
|
headers.set('content-type','text/x-component; charset=utf-8');headers.delete('content-length');headers.delete('content-encoding');
|
|
145
|
-
return new Response(collectStream(source,{capture:!!token&&/^[a-f0-9]{48}$/.test(token),onComplete:bytes=>{if(version===serial&&token&&/^[a-f0-9]{48}$/.test(token))current={token,bytes};}}),{status:response.status,statusText:response.statusText,headers});
|
|
182
|
+
return new Response(collectStream(source,{capture:!!token&&/^[a-f0-9]{48}$/.test(token),onEnd,onComplete:bytes=>{if(version===serial&&token&&/^[a-f0-9]{48}$/.test(token))current={token,bytes};}}),{status:response.status,statusText:response.statusText,headers});
|
|
146
183
|
},
|
|
147
184
|
clear(){serial++;current=undefined;},
|
|
148
185
|
};
|
|
149
186
|
}
|
|
150
|
-
module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport};
|
|
187
|
+
module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport,ownFlightStream};
|