@thomas.f/prnext 0.1.18-alpha → 0.1.19-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 +46 -25
- package/build/compiler.mjs +5 -4
- package/build/config.mjs +4 -2
- 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/incremental.mjs +112 -18
- package/build/index.mjs +32 -3
- 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/npm-packages.mjs +1 -0
- package/build/production-builder.mjs +1 -1
- package/build/standalone.mjs +21 -3
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +10 -1
- package/compat/app-dynamic.cjs +12 -1
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +3 -1
- package/compat/index.d.ts +1 -1
- package/compat/link.cjs +2 -2
- package/docs/ai/compatibility.md +77 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +21 -9
- package/runtime/app-client.mjs +119 -42
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +60 -19
- package/runtime/flight-delta.mjs +28 -4
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -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,121 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {constants} from 'node:fs';
|
|
3
|
+
import {readdir,readFile,writeFile,stat,lstat,open,mkdtemp,link,rm} from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
export const MAX_LAZY_ASSET_BYTES=128*1024*1024;
|
|
6
|
+
export const MAX_LAZY_ASSET_FILES=16384;
|
|
7
|
+
const token=/^\/_prnext\/lazy\/([a-f0-9]{24})$/;
|
|
8
|
+
export function lazyImportIdentifier(raw){return token.exec(new URL(raw,'http://local').pathname)?.[1];}
|
|
9
|
+
|
|
10
|
+
function receiptFiles(receipt,cacheId){
|
|
11
|
+
if(!receipt||receipt.version!==1||typeof cacheId!=='string'||!cacheId||cacheId.length>256||receipt.baseCacheId!==cacheId||!Array.isArray(receipt.files))throw Error('Invalid or obsolete deferred asset receipt');
|
|
12
|
+
if(receipt.files.length>MAX_LAZY_ASSET_FILES)throw Error('Deferred import exceeded the asset count budget');
|
|
13
|
+
const names=new Set(),files=[];let total=0;
|
|
14
|
+
for(const file of receipt.files){
|
|
15
|
+
const name=file?.name;
|
|
16
|
+
if(typeof name!=='string'||!name||name.length>255||!/^[A-Za-z0-9_.-]+$/.test(name)||name==='.'||name==='..'||name.endsWith('.')||/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)||names.has(name.toLowerCase()))throw Error('Invalid or duplicate deferred asset receipt name');
|
|
17
|
+
if(!Number.isSafeInteger(file.bytes)||file.bytes<0||file.bytes>MAX_LAZY_ASSET_BYTES||(total+=file.bytes)>MAX_LAZY_ASSET_BYTES)throw Error('Deferred asset receipt exceeded the 128 MiB generation budget');
|
|
18
|
+
names.add(name.toLowerCase());files.push({name,bytes:file.bytes});
|
|
19
|
+
}
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function regularAsset(info,bytes){
|
|
24
|
+
if(!info.isFile()||info.isSymbolicLink())throw Error('Invalid deferred asset: expected a regular file without symbolic links');
|
|
25
|
+
if(info.size!==bytes)throw Error('Deferred asset does not match the receipt byte size');
|
|
26
|
+
return info;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readReceiptAsset(filename,bytes,checked){
|
|
30
|
+
const expected=checked||regularAsset(await lstat(filename),bytes);
|
|
31
|
+
const file=await open(filename,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
|
|
32
|
+
try{
|
|
33
|
+
const info=regularAsset(await file.stat(),bytes);
|
|
34
|
+
if(info.dev!==expected.dev||info.ino!==expected.ino)throw Error('Deferred asset changed after receipt validation');
|
|
35
|
+
// Metadata can race a writer. Read only the declared size plus one byte,
|
|
36
|
+
// rather than letting readFile allocate for a later, unbounded file size.
|
|
37
|
+
const data=Buffer.allocUnsafe(bytes);let offset=0;
|
|
38
|
+
while(offset<bytes){
|
|
39
|
+
const {bytesRead}=await file.read(data,offset,bytes-offset,offset);
|
|
40
|
+
if(!bytesRead)throw Error('Deferred asset ended before the receipt byte size');
|
|
41
|
+
offset+=bytesRead;
|
|
42
|
+
}
|
|
43
|
+
if((await file.read(Buffer.allocUnsafe(1),0,1,bytes)).bytesRead)throw Error('Deferred asset exceeded the receipt byte size');
|
|
44
|
+
return data;
|
|
45
|
+
}finally{await file.close();}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readExistingReceiptAsset(filename,bytes){
|
|
49
|
+
const info=await lstat(filename);
|
|
50
|
+
if(!info.isFile()||info.isSymbolicLink())throw Error('Invalid deferred asset: expected a regular file without symbolic links');
|
|
51
|
+
if(info.size!==bytes)throw Error('Deferred compilation produced a conflicting immutable asset');
|
|
52
|
+
return readReceiptAsset(filename,bytes,info);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Only registered tokens can reach the compiler. The caller serializes builds. */
|
|
56
|
+
export async function materializeDevImport({root,current,id,compile}){
|
|
57
|
+
const registry=current.manifest.devLazy;
|
|
58
|
+
if(!registry||!registry.modules.some(item=>item.id===id))throw Error('Unknown deferred import identifier');
|
|
59
|
+
if(registry.outputs[id])return registry.outputs[id];
|
|
60
|
+
const built=await compile({id,modules:registry.modules,bridges:registry.bridges,shared:registry.shared});
|
|
61
|
+
const next=built.devLazy,target=next?.outputs?.[id];
|
|
62
|
+
if(!target)throw Error('The compiler did not produce the requested deferred import');
|
|
63
|
+
// New server implementations cannot be added to a running immutable runtime.
|
|
64
|
+
// Existing Server Action proxies are reused through the static ESM bridges.
|
|
65
|
+
if(Object.keys(built.app?.actions||{}).some(key=>!Object.hasOwn(current.manifest.app?.actions||{},key)))throw Error('A deferred module introduced a new Server Action. Import that action statically or disable experimental.lazyDynamicImports.');
|
|
66
|
+
const source=path.join(built.outputDirectory,'assets'),destination=path.join(root,current.directory,'assets');
|
|
67
|
+
const received=Object.hasOwn(built,'devLazyAssets');
|
|
68
|
+
const files=received?receiptFiles(built.devLazyAssets,current.manifest.cacheId):await readdir(source,{withFileTypes:true}),pending=[];let bytes=0;
|
|
69
|
+
if(files.length>MAX_LAZY_ASSET_FILES)throw Error('Deferred import exceeded the asset count budget');
|
|
70
|
+
if(received){
|
|
71
|
+
for(const directory of [source,destination]){const info=await lstat(directory);if(!info.isDirectory()||info.isSymbolicLink())throw Error('Invalid deferred asset directory');}
|
|
72
|
+
// Validate every declared file before reading any payload or publishing.
|
|
73
|
+
for(const file of files)file.info=regularAsset(await lstat(path.join(source,file.name)),file.bytes);
|
|
74
|
+
}
|
|
75
|
+
for(const file of files){
|
|
76
|
+
if(!received&&(!file.isFile()||file.name!==path.basename(file.name)))throw Error('Invalid deferred asset');
|
|
77
|
+
if(!received&&(await stat(path.join(source,file.name))).size>MAX_LAZY_ASSET_BYTES)throw Error('Deferred import asset exceeded the 128 MiB generation budget');
|
|
78
|
+
const data=received?await readReceiptAsset(path.join(source,file.name),file.bytes,file.info):await readFile(path.join(source,file.name));
|
|
79
|
+
const previous=await(received?readExistingReceiptAsset(path.join(destination,file.name),file.bytes):readFile(path.join(destination,file.name))).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
80
|
+
if(previous){if(!previous.equals(data))throw Error('Deferred compilation produced a conflicting immutable asset');continue;}
|
|
81
|
+
bytes+=data.length;
|
|
82
|
+
if(bytes+(current.lazyAssetBytes||0)>MAX_LAZY_ASSET_BYTES)throw Error('Deferred import assets exceeded the 128 MiB generation budget');
|
|
83
|
+
pending.push([file.name,data]);
|
|
84
|
+
}
|
|
85
|
+
if(pending.length){
|
|
86
|
+
// Partial writes stay outside the served assets directory. A hard link
|
|
87
|
+
// publishes a complete file atomically and exclusively without replacing an
|
|
88
|
+
// immutable asset, even if another writer wins between inspection and I/O.
|
|
89
|
+
const temporary=await mkdtemp(path.join(root,current.directory,'.lazy-publish-'));
|
|
90
|
+
current.lazyAssetBytes=(current.lazyAssetBytes||0)+bytes;
|
|
91
|
+
let retained=0,failure;
|
|
92
|
+
try{
|
|
93
|
+
for(const [name,data]of pending)await writeFile(path.join(temporary,name),data,{flag:'wx'});
|
|
94
|
+
for(const [name,data]of pending){
|
|
95
|
+
// A failed publication can have an uncertain outcome. Keep its charge
|
|
96
|
+
// conservatively; a retry can reuse a complete asset without recharging.
|
|
97
|
+
retained+=data.length;
|
|
98
|
+
try{await link(path.join(temporary,name),path.join(destination,name));}
|
|
99
|
+
catch(error){
|
|
100
|
+
if(error.code!=='EEXIST')throw error;
|
|
101
|
+
const existing=received?await readExistingReceiptAsset(path.join(destination,name),data.length):await readFile(path.join(destination,name));
|
|
102
|
+
if(!existing.equals(data))throw Error('Deferred compilation produced a conflicting immutable asset');
|
|
103
|
+
retained-=data.length;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}catch(error){failure=error;}
|
|
107
|
+
try{
|
|
108
|
+
await rm(temporary,{recursive:true,force:true,maxRetries:10,retryDelay:100});
|
|
109
|
+
// Reclaim only unpublished bytes after deletion of our private staging
|
|
110
|
+
// files is confirmed. Complete public files are never removed on failure.
|
|
111
|
+
current.lazyAssetBytes-=bytes-retained;
|
|
112
|
+
}catch(error){
|
|
113
|
+
if(failure){failure.cleanupError=error;failure.message+='; deferred asset staging cleanup also failed';}
|
|
114
|
+
else failure=error;
|
|
115
|
+
}
|
|
116
|
+
if(failure)throw failure;
|
|
117
|
+
}
|
|
118
|
+
registry.modules=next.modules;registry.bridges=next.bridges;registry.shared=next.shared;
|
|
119
|
+
Object.assign(registry.outputs,next.outputs);
|
|
120
|
+
return target;
|
|
121
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { lstat, realpath, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Delete only an unpublished direct-child stage created by our lazy compiler. */
|
|
5
|
+
export async function discardLazyBuild(root, built) {
|
|
6
|
+
if (!built?.devLazyTemporary) return;
|
|
7
|
+
const canonicalRoot = await realpath(root), directory = path.resolve(built.outputDirectory);
|
|
8
|
+
if (path.dirname(directory) !== canonicalRoot ||
|
|
9
|
+
!/^\.prnext-build-[a-f0-9-]{36}$/.test(path.basename(directory))) throw Error('Invalid deferred staging directory');
|
|
10
|
+
const info = await lstat(directory).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
11
|
+
if (!info) return;
|
|
12
|
+
if (!info.isDirectory() || info.isSymbolicLink() || await realpath(directory) !== directory) throw Error('Invalid deferred staging directory');
|
|
13
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {rm} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Asset-only generations do not own backend/compiler requests. Drain their
|
|
5
|
+
// asset leases and finish deleting the excess snapshot before announcing ready.
|
|
6
|
+
export async function pruneDevelopmentStyles(generations,gateway,root){
|
|
7
|
+
while(generations.length>3){
|
|
8
|
+
const old=generations[0];
|
|
9
|
+
await gateway.request('retire',{generation:old.id});
|
|
10
|
+
await gateway.request('forget',{generation:old.id});
|
|
11
|
+
await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});
|
|
12
|
+
// Keep failed deletions tracked so a later update can retry their cleanup.
|
|
13
|
+
generations.shift();
|
|
14
|
+
}
|
|
15
|
+
}
|
package/runtime/development.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
|
|
2
2
|
import {spawn} from 'node:child_process';
|
|
3
3
|
import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
|
|
4
|
-
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {randomUUID,randomBytes} from 'node:crypto';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {fileURLToPath} from 'node:url';
|
|
7
7
|
import {rename} from './fs.mjs';
|
|
@@ -21,6 +21,10 @@ import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
|
|
|
21
21
|
import {devBuildReason} from './dev-build-reasons.mjs';
|
|
22
22
|
import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
|
|
23
23
|
import {prepareDevNotification} from './dev-notification.mjs';
|
|
24
|
+
import {writeAtomicFile} from './atomic-file.mjs';
|
|
25
|
+
import {lazyImportIdentifier,materializeDevImport} from './dev-lazy-imports.mjs';
|
|
26
|
+
import {discardLazyBuild} from './dev-lazy-staging.mjs';
|
|
27
|
+
import {pruneDevelopmentStyles} from './dev-style-retention.mjs';
|
|
24
28
|
|
|
25
29
|
const packageRoot=fileURLToPath(new URL('../',import.meta.url));
|
|
26
30
|
export function developmentRoute(routes,raw,config={}) {
|
|
@@ -83,6 +87,9 @@ export async function develop(projectRoot,args,binary) {
|
|
|
83
87
|
}
|
|
84
88
|
async function runDevelopment(projectRoot,args,binary) {
|
|
85
89
|
const root=await realpath(projectRoot),options=serverOptions(args),session=randomUUID();
|
|
90
|
+
// Private IPC state survives compiler recycling without exposing the key in
|
|
91
|
+
// the browser's session metadata or any development manifest/notification.
|
|
92
|
+
const devActionSecret=randomBytes(32).toString('base64');
|
|
86
93
|
const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
|
|
87
94
|
const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
|
|
88
95
|
// The session is ours; never clean a path obtained from application config.
|
|
@@ -95,7 +102,8 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
95
102
|
const lifecycle=new AbortController();
|
|
96
103
|
let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
|
|
97
104
|
const failedPaths=new Set();
|
|
98
|
-
const
|
|
105
|
+
const requestedImports=new Set();
|
|
106
|
+
const generations=[],styleGenerations=[],retiring=new Set(),pins=new Map(),routeCache=new Map(),filter=createDevChangeFilter(root,{directoryEventsAreRecursive:process.platform==='darwin',ignore:relative=>relative===output||relative.startsWith(output+'/')});
|
|
99
107
|
const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
|
|
100
108
|
if(stopping)throw Error('Development is stopping');
|
|
101
109
|
if(!discovery)await discover();
|
|
@@ -124,10 +132,23 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
124
132
|
const route=developmentRoute(discovery.project.routes,raw,discovery.config);
|
|
125
133
|
if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
|
|
126
134
|
}
|
|
135
|
+
let publicationFailures=0,lastPublicationFailure;
|
|
127
136
|
const writeState=value=>{
|
|
128
|
-
const task=stateQueue.then(async()=>{
|
|
129
|
-
const
|
|
130
|
-
|
|
137
|
+
const task=stateQueue.then(async()=>{
|
|
138
|
+
const next=value||readyState;
|
|
139
|
+
const {notification,text}=prepareDevNotification({...next,safeMode:options.safe,typeErrors,...(publicationFailures?{publication:{failures:publicationFailures,lastFailure:lastPublicationFailure}}:{})});
|
|
140
|
+
try{await writeAtomicFile(stateFile,text);}
|
|
141
|
+
catch(error){
|
|
142
|
+
publicationFailures++;lastPublicationFailure=error.atomicFile||{operation:'publish-development-state',code:error.code||'UNKNOWN'};
|
|
143
|
+
// A failed publication must leave the last complete state readable.
|
|
144
|
+
// This private bounded receipt contains no source path or payload.
|
|
145
|
+
await writeFile(path.join(diagnosticsDirectory,'state-publication.json'),JSON.stringify({failures:publicationFailures,lastFailure:lastPublicationFailure,lastSuccessfulSequence:readyState.sequence??null})+'\n',{mode:0o600}).catch(()=>{});
|
|
146
|
+
tools?.addServerLog('error','Development state publication failed.',{process:'compiler',...lastPublicationFailure});
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
readyState=next;
|
|
150
|
+
if(gateway&&!gateway.stopping)await gateway.request('notify',{value:notification});
|
|
151
|
+
});
|
|
131
152
|
stateQueue=task.catch(()=>{});return task;
|
|
132
153
|
};
|
|
133
154
|
const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
|
|
@@ -180,21 +201,21 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
180
201
|
if(!options.safe)void runtimePool.warm().catch(()=>{});
|
|
181
202
|
if(current&&!discovery.eager&&discovery.cssFast){
|
|
182
203
|
const directory=path.relative(root,path.join(home,'styles-'+randomUUID())).replaceAll(path.sep,'/');
|
|
183
|
-
const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,
|
|
204
|
+
const styles=await tryDevCssUpdate({root,source:path.join(root,current.cssDirectory||current.directory),baseSource:path.join(root,current.directory),destination:path.join(root,directory),manifest:current.manifest,changed:changedFiles,sequence:sequence+1,cssEngine:discovery.config.cssEngine,onFallback:code=>{reason=devBuildReason({changed:changedFiles,cause:code});}});
|
|
184
205
|
if(styles){
|
|
185
206
|
await gateway.request('assets',{generation:styles.buildId,directory});
|
|
186
207
|
try{styles.devDependencyIndex=await publishDependencyGraph(gateway,styles);dependencyIndexValid=true;}catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
|
|
187
|
-
sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
|
|
188
208
|
styleGenerations.push({id:styles.buildId,directory});
|
|
209
|
+
await pruneDevelopmentStyles(styleGenerations,gateway,root);
|
|
210
|
+
sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
|
|
189
211
|
reason=devBuildReason({cause:'css-only'});
|
|
190
212
|
await writeState({state:'ready',reason,buildId:styles.buildId,clientManifest:styles.devClient,sequence,session,changed,cssOnly:true,listenerPid:gateway.child.pid,compiler:current.compiler,transforms:current.transforms});
|
|
191
213
|
tools?.recordBuild({buildId:styles.buildId,reason:reason.summary,reasonCode:reason.code,sequence,time:Date.now(),startedAt,duration:performance.now()-started,phases:{styles:performance.now()-started},mode:'css',state:'ready'});
|
|
192
214
|
console.log('Styles updated. '+reason.summary);
|
|
193
|
-
while(styleGenerations.length>3){const old=styleGenerations.shift();await gateway.request('retire',{generation:old.id});await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});}
|
|
194
215
|
await watcher?.refresh();return styles;
|
|
195
216
|
}
|
|
196
217
|
}
|
|
197
|
-
const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
|
|
218
|
+
const result=await compilerRequest('build',{options:{...(discovery.eager?{}:{devRoutes:patterns}),devSafe:options.safe,devSequence:++sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devAffectedModules:affectedModules,devLazyActivated:[...requestedImports],devForceRefresh:options.safe||changedFiles.some(file=>/\.(?:css|scss|sass)$/.test(file)||/(?:postcss|browserslist|\.env|config)/.test(file))}});
|
|
198
219
|
phases.compile=performance.now()-mark;mark=performance.now();
|
|
199
220
|
if(stopping)throw Error('Development is stopping');
|
|
200
221
|
const consumed=filter.acceptInputs(result.inputs||[]);
|
|
@@ -211,18 +232,26 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
211
232
|
catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
|
|
212
233
|
const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
|
|
213
234
|
current.manifest=manifest;current.changed=changed;current.patterns=new Set(discovery.eager?discovery.project.routes.map(route=>route.pattern):patterns);
|
|
235
|
+
for(const id of requestedImports)if(!manifest.devLazy?.modules.some(item=>item.id===id))requestedImports.delete(id);
|
|
214
236
|
const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
|
|
215
237
|
current.affectedRoutes=affected;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
238
|
+
try{
|
|
239
|
+
await writeState({state:'ready',reason,buildId:manifest.buildId,clientManifest:manifest.devClient,sequence:manifest.devSequence,session,changed,affectedRoutes:affected,listenerPid:gateway.child.pid,compiler:result.compiler,transforms:result.transforms,routes:manifest.routes.filter(route=>!route.internal&&!route.devDeferred).map(route=>route.pattern)});
|
|
240
|
+
tools?.recordBuild({buildId:manifest.buildId,reason:reason.summary,reasonCode:reason.code,sequence:manifest.devSequence,time:Date.now(),startedAt,duration:performance.now()-started,phases,routes:patterns.length,state:'ready'});
|
|
241
|
+
console.log('Source compiled. '+reason.summary);
|
|
242
|
+
}finally{
|
|
243
|
+
// Activation already succeeded. A locked readiness receipt must not
|
|
244
|
+
// keep obsolete runtimes alive or bypass the generation bound. Retire
|
|
245
|
+
// through the gateway so existing requests drain before their owner
|
|
246
|
+
// closes; the newly activated current runtime remains available.
|
|
247
|
+
if(previous){
|
|
248
|
+
const retiringTask=(async()=>{await gateway.request('retire',{generation:previous.id}).catch(()=>{});await previous.runtime?.close();await stop(previous.child);})().finally(()=>retiring.delete(retiringTask));retiring.add(retiringTask);
|
|
249
|
+
}
|
|
250
|
+
while(generations.length>3){const old=generations.shift();
|
|
251
|
+
// A deferred request may itself be awaiting this build. Never wait for
|
|
252
|
+
// its generation lease while holding the serialized compiler queue.
|
|
253
|
+
const cleanup=(async()=>{await gateway.request('retire',{generation:old.id});await old.runtime?.close();await stop(old.child);await gateway.request('forget',{generation:old.id});await rm(path.join(root,old.directory),{recursive:true,force:true,maxRetries:10,retryDelay:100});})().catch(error=>{if(!stopping)console.warn('Development generation cleanup: '+error.message);}).finally(()=>retiring.delete(cleanup));retiring.add(cleanup);
|
|
254
|
+
}
|
|
226
255
|
}
|
|
227
256
|
await watcher?.refresh();
|
|
228
257
|
return manifest;
|
|
@@ -230,6 +259,18 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
230
259
|
}
|
|
231
260
|
const updateInfo=()=>({buildId:current.manifest.buildId,clientManifest:current.manifest.devClient,pagesManifest:current.manifest.pagesManifest,sequence:current.manifest.devSequence,session,changed:current.changed,cssOnly:current.cssOnly,affectedRoutes:current.affectedRoutes});
|
|
232
261
|
async function ensure(raw,client,exact=false){
|
|
262
|
+
if(new URL(raw,'http://local').pathname.startsWith('/_prnext/lazy/'))return serialized(async()=>{
|
|
263
|
+
const id=lazyImportIdentifier(raw);
|
|
264
|
+
if(!id||!current)throw Error('Unknown deferred import identifier');
|
|
265
|
+
let built,failure,lazy;
|
|
266
|
+
try{lazy=await materializeDevImport({root,current,id,compile:async request=>{
|
|
267
|
+
const result=await compilerRequest('build',{options:{devRoutes:[...current.patterns],devSafe:options.safe,devSequence:sequence,devSession:session,devActionSecret,devTools:tools?.bootstrap,devLazyRequest:request,devLazyBase:{cacheId:current.manifest.cacheId}}});
|
|
268
|
+
built=result.manifest;return built;
|
|
269
|
+
}});}catch(error){failure=error;throw error;}
|
|
270
|
+
finally{try{await discardLazyBuild(root,built);}catch(error){if(failure){failure.cleanupError=error;failure.message+='; deferred compiler staging cleanup also failed';}else throw error;}}
|
|
271
|
+
requestedImports.add(id);
|
|
272
|
+
return {lazy};
|
|
273
|
+
});
|
|
233
274
|
const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
|
|
234
275
|
const warm=discovery&&resolve();
|
|
235
276
|
if(!stopping&¤t&&warm&&active.has(warm.pattern)&¤t.patterns.has(warm.pattern)){
|
package/runtime/flight-delta.mjs
CHANGED
|
@@ -2,14 +2,38 @@ import { randomBytes, createHmac } from 'node:crypto';
|
|
|
2
2
|
import { deflateRawSync } from 'node:zlib';
|
|
3
3
|
import { TYPE, MAX_VALUE, collectStream, deltaStream } from '../compat/flight-delta.cjs';
|
|
4
4
|
|
|
5
|
-
export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now}={}) {
|
|
6
|
-
|
|
5
|
+
export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now,setTimer=setTimeout,clearTimer=clearTimeout}={}) {
|
|
6
|
+
if(!Number.isFinite(ttl)||ttl<0||typeof now!=='function'||typeof setTimer!=='function'||typeof clearTimer!=='function')throw new TypeError('Invalid Flight store expiration policy');
|
|
7
|
+
const entries=new Map();let size=0,captures=0,encoders=0,timer,timerExpires;
|
|
7
8
|
function reserve(kind){if(kind==='capture'?captures>=maxCaptures:encoders>=maxEncoders)return;kind==='capture'?captures++:encoders++;let released=false;return()=>{if(released)return;released=true;kind==='capture'?captures--:encoders--;};}
|
|
8
9
|
function remove(token){const entry=entries.get(token);if(entry){size-=entry.bytes.length;entries.delete(token);}}
|
|
9
|
-
function
|
|
10
|
+
function schedule(at){
|
|
11
|
+
let expires=Infinity;
|
|
12
|
+
for(const entry of entries.values())expires=Math.min(expires,entry.expires);
|
|
13
|
+
if(timer!==undefined&&timerExpires===expires)return;
|
|
14
|
+
if(timer!==undefined)clearTimer(timer);
|
|
15
|
+
timer=timerExpires=undefined;
|
|
16
|
+
if(!entries.size)return;
|
|
17
|
+
timerExpires=expires;
|
|
18
|
+
// One timer for the bounded store, not one per response. A cancelled cache
|
|
19
|
+
// entry must not leave its former deadline armed. Clamp long TTLs to Node's
|
|
20
|
+
// timer range; an early wake simply checks the authoritative clock again.
|
|
21
|
+
const handle=setTimer(()=>{
|
|
22
|
+
if(timer!==handle)return;
|
|
23
|
+
timer=timerExpires=undefined;prune(true);
|
|
24
|
+
},Math.min(2147483647,Math.max(1,expires-at)));
|
|
25
|
+
timer=handle;timer?.unref?.();
|
|
26
|
+
}
|
|
27
|
+
function prune(changed=false){
|
|
28
|
+
const at=now();
|
|
29
|
+
for(const[token,entry]of entries)if(entry.expires<=at){remove(token);changed=true;}
|
|
30
|
+
while(size>maxBytes||entries.size>maxEntries){remove(entries.keys().next().value);changed=true;}
|
|
31
|
+
// Ordinary hits keep the same timer without another full entry scan.
|
|
32
|
+
if(changed)schedule(at);
|
|
33
|
+
}
|
|
10
34
|
return {
|
|
11
35
|
get(token,identity){prune();const entry=entries.get(token);if(entry?.identity!==identity)return;return entry?.bytes;},
|
|
12
|
-
set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune();},
|
|
36
|
+
set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune(true);},
|
|
13
37
|
capture:()=>reserve('capture'),encoder:()=>reserve('encoder'),get inFlight(){return {captures,encoders};},
|
|
14
38
|
get bytes(){return size;},get size(){return entries.size;},
|
|
15
39
|
};
|
package/runtime/fs.mjs
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { rename as move } from 'node:fs/promises';
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
// Windows scanners and recently closed file handles can briefly deny rename.
|
|
5
6
|
// Keep the atomic swap: never delete the destination to work around a lock.
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
export function createAtomicRenamer({moveFile=move,wait=delay,platform=process.platform,now=()=>performance.now(),retryMs=2000}={}) {
|
|
8
|
+
return async function rename(source,destination) {
|
|
9
|
+
const started=now(),deadline=started+retryMs;
|
|
10
|
+
for(let attempt=1;;attempt++){
|
|
11
|
+
try{return await moveFile(source,destination);}
|
|
12
|
+
catch(error){
|
|
13
|
+
if(platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(error.code)||now()>=deadline){
|
|
14
|
+
error.atomicRename={operation:'rename',code:error.code||'UNKNOWN',attempts:attempt,elapsedMs:Math.max(0,now()-started),destination:path.basename(destination)};
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
await wait(Math.min(10*attempt,100,Math.max(0,deadline-now())));
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
}
|
|
20
|
+
};
|
|
15
21
|
}
|
|
22
|
+
export const rename=createAtomicRenamer();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Only emitted by the opt-in development compiler. Successful imports retain
|
|
2
|
+
// native ESM caching; failed compilation is retryable after a source correction.
|
|
3
|
+
const pending=new Map(),styles=new Map();
|
|
4
|
+
function stylesheet(url){
|
|
5
|
+
const existing=[...document.querySelectorAll('link[rel="stylesheet"]')].find(link=>link.href===new URL(url,location.href).href);
|
|
6
|
+
if(existing?.sheet)return Promise.resolve();
|
|
7
|
+
if(existing&&styles.has(url))return styles.get(url);
|
|
8
|
+
const promise=new Promise((resolve,reject)=>{
|
|
9
|
+
const link=existing||document.createElement('link');
|
|
10
|
+
const timer=setTimeout(()=>finish(Error('Deferred stylesheet timed out')),30000);
|
|
11
|
+
const finish=error=>{clearTimeout(timer);link.removeEventListener('load',loaded);link.removeEventListener('error',failed);if(error){styles.delete(url);if(!existing)link.remove();reject(error);}else resolve();};
|
|
12
|
+
const loaded=()=>finish(),failed=()=>finish(Error('Deferred stylesheet failed to load'));
|
|
13
|
+
link.addEventListener('load',loaded,{once:true});link.addEventListener('error',failed,{once:true});
|
|
14
|
+
if(!existing){link.rel='stylesheet';link.href=url;document.head.appendChild(link);}
|
|
15
|
+
});
|
|
16
|
+
styles.set(url,promise);
|
|
17
|
+
void promise.then(()=>styles.delete(url),()=>styles.delete(url));
|
|
18
|
+
return promise;
|
|
19
|
+
}
|
|
20
|
+
export function lazyImport(id,basePath=''){
|
|
21
|
+
const key=basePath+':'+id;
|
|
22
|
+
if(pending.has(key))return pending.get(key);
|
|
23
|
+
const task=(async()=>{
|
|
24
|
+
const path='/_prnext/lazy/'+id;
|
|
25
|
+
const response=await fetch(`${basePath}/_prnext/ensure?path=${encodeURIComponent(path)}`,{cache:'no-store',credentials:'same-origin',signal:AbortSignal.timeout(120000)});
|
|
26
|
+
if(!response.ok)throw Error(await response.text());
|
|
27
|
+
const value=await response.json(),target=value.lazy;
|
|
28
|
+
if(!target||typeof target.url!=='string'||!Array.isArray(target.css))throw Error('Invalid deferred import response');
|
|
29
|
+
for(const url of [target.url,...target.css]){
|
|
30
|
+
const parsed=new URL(url,location.href);
|
|
31
|
+
if(parsed.origin!==location.origin||!parsed.pathname.includes('/_prnext/assets/'))throw Error('Invalid deferred import asset URL');
|
|
32
|
+
}
|
|
33
|
+
await Promise.all(target.css.map(stylesheet));
|
|
34
|
+
const module=await import(target.url);
|
|
35
|
+
return target.native?module:module.__prnextLazyModule;
|
|
36
|
+
})().finally(()=>pending.delete(key));
|
|
37
|
+
pending.set(key,task);return task;
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2019 Vercel, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Node File Trace
|
|
2
|
+
|
|
3
|
+
The `out/` files and MIT license are unchanged files from `@vercel/nft` 1.11.0.
|
|
4
|
+
`upstream.json` records the exact npm archive integrity and every file's SHA-256.
|
|
5
|
+
`node scripts/vendor-nft.mjs verify` checks those bytes and the complete inventory.
|
|
6
|
+
|
|
7
|
+
This copy uses PRNext's explicit dependencies, including glob 14.0.2 and
|
|
8
|
+
minimatch 11.0.1. Upstream 1.11.0 still declares glob 13; overriding a workspace
|
|
9
|
+
lockfile would not repair that dependency for independently installed consumers.
|
|
10
|
+
The tracing implementation itself has no local patches. Its runtime, resolver,
|
|
11
|
+
filesystem, native tracing and relocated standalone contracts are tested with
|
|
12
|
+
the patched dependency versions.
|
|
13
|
+
|
|
14
|
+
When upstream supports the patched dependencies, prefer returning to its normal
|
|
15
|
+
package after validating the same contracts. Updates to this copy must retain
|
|
16
|
+
the upstream license, reviewed archive integrity and complete byte inventory.
|