@thomas.f/prnext 0.1.17-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 +27 -868
- package/build/actions.mjs +27 -15
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +122 -46
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +34 -10
- package/build/config.mjs +34 -3
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +18 -14
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +14 -7
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +90 -28
- package/build/instrumentation.mjs +30 -0
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/metadata-routes.mjs +1 -1
- 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-type-config.mjs +37 -0
- package/build/npm-packages.mjs +1 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +59 -10
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/standalone.mjs +21 -3
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +19 -6
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +70 -5
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +15 -3
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +152 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +69 -4
- package/compat/link.cjs +13 -10
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +248 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +93 -0
- package/docs/ai/troubleshooting.md +116 -2
- package/package.json +40 -9
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +316 -92
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +7 -2
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +13 -3
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +83 -25
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +74 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/fs.mjs +15 -8
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import {createHash} from 'node:crypto';
|
|
3
3
|
import {createReadStream} from 'node:fs';
|
|
4
|
-
import {stat, readdir} from 'node:fs/promises';
|
|
4
|
+
import {stat, lstat, readdir, opendir} from 'node:fs/promises';
|
|
5
5
|
import {shouldWatchProjectFile} from './env.mjs';
|
|
6
6
|
|
|
7
7
|
function directoryFingerprint(entries) {
|
|
@@ -12,16 +12,27 @@ function directoryFingerprint(entries) {
|
|
|
12
12
|
|
|
13
13
|
// Remember the bytes actually consumed by webpack, not the file's contents at
|
|
14
14
|
// the end of a build: an edit made after a read must still trigger a rebuild.
|
|
15
|
-
export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024}={}) {
|
|
15
|
+
export function createDevInputSnapshot(roots, {maxEntries=process.platform==='darwin'?8192:512,maxBytes=1024*1024}={}) {
|
|
16
16
|
const inputs=new Map();
|
|
17
|
+
const limitedPrefixes=new Set();let globalLimited=false;
|
|
17
18
|
let bytes=0;
|
|
19
|
+
function limited(relative) {
|
|
20
|
+
// Source files may overflow in many sibling folders. One source-root
|
|
21
|
+
// prefix remains bounded without invalidating a captured disjoint generator.
|
|
22
|
+
relative=relative==='.contentlayer/generated'||relative.startsWith('.contentlayer/generated/')?'.contentlayer/generated':relative.split('/')[0];
|
|
23
|
+
if(globalLimited)return;
|
|
24
|
+
if([...limitedPrefixes].some(prefix=>relative===prefix||relative.startsWith(prefix+'/')))return;
|
|
25
|
+
for(const prefix of limitedPrefixes)if(prefix.startsWith(relative+'/'))limitedPrefixes.delete(prefix);
|
|
26
|
+
limitedPrefixes.add(relative);
|
|
27
|
+
if(limitedPrefixes.size>32||[...limitedPrefixes].reduce((sum,value)=>sum+value.length*2,0)>4096){globalLimited=true;limitedPrefixes.clear();}
|
|
28
|
+
}
|
|
18
29
|
function remember(relative,value) {
|
|
19
30
|
if (inputs.has(relative)) {
|
|
20
31
|
if (inputs.get(relative)!==value) inputs.set(relative,null);
|
|
21
32
|
return;
|
|
22
33
|
}
|
|
23
34
|
const size=relative.length*2+value.length*2+64;
|
|
24
|
-
if (inputs.size>=maxEntries || bytes+size>maxBytes) return;
|
|
35
|
+
if (inputs.size>=maxEntries || bytes+size>maxBytes) {limited(value.startsWith('directory:')?relative:path.posix.dirname(relative)==='.'?relative:path.posix.dirname(relative));return;}
|
|
25
36
|
inputs.set(relative,value);bytes+=size;
|
|
26
37
|
}
|
|
27
38
|
return {
|
|
@@ -38,14 +49,15 @@ export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024
|
|
|
38
49
|
async captureDirectory(relative) {
|
|
39
50
|
// Called after an awaited content generator and before graph resolution.
|
|
40
51
|
// Include its types/aggregate indexes, which may not be runtime imports.
|
|
41
|
-
if (!shouldWatchProjectFile(relative)
|
|
52
|
+
if (!shouldWatchProjectFile(relative))return;
|
|
53
|
+
if(inputs.size>=maxEntries || bytes>=maxBytes){limited(relative);return;}
|
|
42
54
|
const file=path.join(roots[0],relative);
|
|
43
55
|
let entries;
|
|
44
56
|
try {entries=await readdir(file,{withFileTypes:true});}
|
|
45
57
|
catch(error){if(error.code==='ENOENT')return;throw error;}
|
|
46
58
|
remember(relative,directoryFingerprint(entries));
|
|
47
59
|
for(const entry of entries) {
|
|
48
|
-
if(inputs.size>=maxEntries || bytes>=maxBytes)break;
|
|
60
|
+
if(inputs.size>=maxEntries || bytes>=maxBytes){limited(relative);break;}
|
|
49
61
|
const child=relative+'/'+entry.name;
|
|
50
62
|
if(!shouldWatchProjectFile(child))continue;
|
|
51
63
|
if(entry.isDirectory())await this.captureDirectory(child);
|
|
@@ -58,14 +70,90 @@ export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024
|
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
72
|
},
|
|
61
|
-
entries:()=>
|
|
73
|
+
entries:()=>{
|
|
74
|
+
const values=[...inputs],coverage={version:1,limitedPrefixes:[...limitedPrefixes],globalLimited};
|
|
75
|
+
if(values.length)values[0]=[...values[0],coverage];
|
|
76
|
+
else values.push([null,null,coverage]);
|
|
77
|
+
return values;
|
|
78
|
+
},
|
|
62
79
|
};
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
// Generators may rewrite identical JSON/modules on every compilation. Retain
|
|
66
83
|
// only bounded path/digest pairs, never file contents or full directory lists.
|
|
67
|
-
export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}={}) {
|
|
84
|
+
export function createDevChangeFilter(root, {directoryEventsAreRecursive=false,maxEntries=directoryEventsAreRecursive?8192:512,maxBytes=1024*1024,ignore=()=>false,maxTreeEntries=8192,maxTreeBytes=1024*1024,onUncertainDirectory=()=>{}}={}) {
|
|
68
85
|
const fingerprints=new Map();
|
|
86
|
+
// Before the first build there are no consumed inputs to lose. A later
|
|
87
|
+
// acceptInputs without coverage metadata deliberately replaces this proof.
|
|
88
|
+
const trees=new Map(),diagnostics=new Map();let treeBytes=0,coverage={version:1,limitedPrefixes:[],globalLimited:false},consumedInputs=new Map();
|
|
89
|
+
const contains=(parent,file)=>file===parent||file.startsWith(parent+'/');
|
|
90
|
+
const allowed=file=>shouldWatchProjectFile(file)&&!ignore(file);
|
|
91
|
+
const limitedFor=file=>!coverage||coverage.globalLimited||coverage.limitedPrefixes.some(prefix=>contains(file,prefix)||contains(prefix,file));
|
|
92
|
+
function discardTree(file){const previous=trees.get(file);if(previous){trees.delete(file);treeBytes-=previous.bytes;}}
|
|
93
|
+
function uncertain(file,reason){
|
|
94
|
+
discardTree(file);
|
|
95
|
+
if(diagnostics.get(file)!==reason){
|
|
96
|
+
diagnostics.delete(file);diagnostics.set(file,reason);while(diagnostics.size>32)diagnostics.delete(diagnostics.keys().next().value);
|
|
97
|
+
onUncertainDirectory({directory:file.slice(0,256),reason});
|
|
98
|
+
}
|
|
99
|
+
return Object.assign(Error(`Cannot verify a coalesced directory change in ${file.slice(0,256)} (${reason}). The previous build is kept. Run a full build or narrow generated inputs before retrying.`),{code:'PRNEXT_RECURSIVE_CHANGE_UNVERIFIED'});
|
|
100
|
+
}
|
|
101
|
+
async function treeSnapshot(relative){
|
|
102
|
+
const observed=new Map();let entriesRead=0,retainedBytes=relative.length*2+128;
|
|
103
|
+
const incomplete=reason=>{throw Object.assign(Error('Incomplete recursive source snapshot'),{snapshotReason:reason});};
|
|
104
|
+
async function visit(name){
|
|
105
|
+
const file=path.join(root,name),before=await lstat(file);let value,semantic;
|
|
106
|
+
if(before.isSymbolicLink())incomplete('symbolic-link');
|
|
107
|
+
if(before.isDirectory()){
|
|
108
|
+
const entries=[],directory=await opendir(file);
|
|
109
|
+
for await(const entry of directory){if(++entriesRead>maxTreeEntries)incomplete('entry-capacity');entries.push(entry);}
|
|
110
|
+
value=directoryFingerprint(entries);
|
|
111
|
+
const watched=entries.filter(entry=>allowed(name+'/'+entry.name));semantic=directoryFingerprint(watched);
|
|
112
|
+
rememberObserved(name,value,semantic);
|
|
113
|
+
for(const entry of watched)await visit(name+'/'+entry.name);
|
|
114
|
+
}else if(before.isFile()){
|
|
115
|
+
const hash=createHash('sha256');for await(const chunk of createReadStream(file,{highWaterMark:64*1024}))hash.update(chunk);
|
|
116
|
+
value='file:'+hash.digest('hex');rememberObserved(name,value,value);
|
|
117
|
+
}else incomplete('special-file');
|
|
118
|
+
const after=await lstat(file);
|
|
119
|
+
if(before.dev!==after.dev||before.ino!==after.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)incomplete('changed-during-read');
|
|
120
|
+
}
|
|
121
|
+
function rememberObserved(file,value,semantic){
|
|
122
|
+
retainedBytes+=2*(file.length+value.length+semantic.length)+96;
|
|
123
|
+
if(observed.size>=maxTreeEntries||retainedBytes>maxTreeBytes)incomplete('memory-capacity');
|
|
124
|
+
observed.set(file,{value,semantic});
|
|
125
|
+
}
|
|
126
|
+
try{
|
|
127
|
+
await visit(relative);const hash=createHash('sha256');
|
|
128
|
+
for(const [file,value]of [...observed].sort(([a],[b])=>a.localeCompare(b)))hash.update(JSON.stringify([file,value.semantic]));
|
|
129
|
+
return {complete:true,observed,digest:hash.digest('hex'),bytes:retainedBytes};
|
|
130
|
+
}catch(error){return {complete:false,reason:error.snapshotReason||'read-failure'};}
|
|
131
|
+
}
|
|
132
|
+
function retainTree(file,snapshot){
|
|
133
|
+
discardTree(file);
|
|
134
|
+
while(trees.size&&(trees.size>=32||treeBytes+snapshot.bytes>maxTreeBytes))discardTree(trees.keys().next().value);
|
|
135
|
+
trees.set(file,snapshot);treeBytes+=snapshot.bytes;
|
|
136
|
+
}
|
|
137
|
+
function observeTree(file,snapshot){
|
|
138
|
+
for(const [name,previous]of [...fingerprints])if(contains(file,name)&&allowed(name)&&!snapshot.observed.has(name)&&previous.value!=='missing')remember(name,'missing');
|
|
139
|
+
for(const [name,value]of consumedInputs)if(contains(file,name)&&allowed(name)&&snapshot.observed.get(name)?.value!==value)consumedInputs.delete(name);
|
|
140
|
+
for(const [directory,previous]of trees){
|
|
141
|
+
if(!contains(file,directory)&&!contains(directory,file))continue;
|
|
142
|
+
const same=[...previous.observed].every(([name,value])=>!contains(file,name)||snapshot.observed.get(name)?.semantic===value.semantic)
|
|
143
|
+
&&[...snapshot.observed].every(([name,value])=>!contains(directory,name)||previous.observed.get(name)?.semantic===value.semantic);
|
|
144
|
+
if(!same)discardTree(directory);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function observePath(file,value){
|
|
148
|
+
for(const [directory,snapshot]of trees){
|
|
149
|
+
if(contains(directory,file)){
|
|
150
|
+
const observed=snapshot.observed.get(file);
|
|
151
|
+
if(value===undefined||(value==='missing'?observed!==undefined:observed?.value!==value))discardTree(directory);
|
|
152
|
+
}else if(contains(file,directory))discardTree(directory);
|
|
153
|
+
}
|
|
154
|
+
for(const [name,previous]of consumedInputs)if(contains(file,name)&&(name!==file||previous!==value))consumedInputs.delete(name);
|
|
155
|
+
if(!value?.startsWith('directory:'))for(const [name]of [...fingerprints])if(name!==file&&contains(file,name))remember(name,value===undefined?undefined:'missing');
|
|
156
|
+
}
|
|
69
157
|
let bytes=0;
|
|
70
158
|
function remember(file,value) {
|
|
71
159
|
const previous=fingerprints.get(file);
|
|
@@ -99,12 +187,39 @@ export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}=
|
|
|
99
187
|
const filter=async files => {
|
|
100
188
|
const changed=[];
|
|
101
189
|
for (const file of files) {
|
|
190
|
+
if(directoryEventsAreRecursive&&allowed(file)){
|
|
191
|
+
let directory=false;try{directory=(await stat(path.join(root,file))).isDirectory();}catch{}
|
|
192
|
+
if(directory){
|
|
193
|
+
const snapshot=await treeSnapshot(file);
|
|
194
|
+
if(!snapshot.complete)throw uncertain(file,snapshot.reason);
|
|
195
|
+
if(limitedFor(file))throw uncertain(file,'input-coverage');
|
|
196
|
+
observeTree(file,snapshot);
|
|
197
|
+
const previous=trees.get(file);
|
|
198
|
+
const consumed=[...snapshot.observed].every(([name,value])=>consumedInputs.get(name)===value.value)
|
|
199
|
+
&&[...consumedInputs].every(([name,value])=>!contains(file,name)||!allowed(name)||!value.startsWith('file:')||snapshot.observed.has(name));
|
|
200
|
+
if((!previous||previous.digest!==snapshot.digest)&&!consumed)changed.push(file);
|
|
201
|
+
retainTree(file,snapshot);
|
|
202
|
+
diagnostics.delete(file);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
102
206
|
const value=await fingerprint(file), previous=fingerprints.get(file);
|
|
207
|
+
if(directoryEventsAreRecursive)observePath(file,value);
|
|
103
208
|
if (value===undefined || !previous || previous.value!==value) changed.push(file);
|
|
104
209
|
remember(file,value);
|
|
105
210
|
}
|
|
106
211
|
return changed;
|
|
107
212
|
};
|
|
108
|
-
filter.acceptInputs=inputs=>{
|
|
213
|
+
filter.acceptInputs=inputs=>{
|
|
214
|
+
coverage=inputs.find(entry=>entry[2]?.version===1)?.[2];
|
|
215
|
+
// Only the latest bounded compiler receipt can acknowledge an uncached
|
|
216
|
+
// subtree. Event deduplication may keep older values in its independent LRU.
|
|
217
|
+
consumedInputs=new Map();for(const [file,value]of inputs)if(typeof file==='string'&&typeof value==='string')consumedInputs.set(file,value);
|
|
218
|
+
for(const [directory,snapshot]of trees){
|
|
219
|
+
if(limitedFor(directory)||inputs.some(([file,value])=>typeof file==='string'&&contains(directory,file)&&allowed(file)&&(value===null||snapshot.observed.get(file)?.value!==value)))discardTree(directory);
|
|
220
|
+
}
|
|
221
|
+
const changed=[];for(const [file,value] of inputs){if(typeof file!=='string')continue;if(fingerprints.get(file)?.value!==value)changed.push(file);remember(file,value);}return changed;
|
|
222
|
+
};
|
|
223
|
+
filter.recursiveDiagnostics=()=>[...diagnostics].map(([directory,reason])=>({directory,reason}));
|
|
109
224
|
return filter;
|
|
110
225
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import logo from './dev-logo.mjs';
|
|
2
|
-
import { createLazyDevTool } from './dev-lazy-tool.mjs';
|
|
2
|
+
import { createLazyDevTool, registerDevToolLoaders } from './dev-lazy-tool.mjs';
|
|
3
3
|
import { popoutIcon } from './dev-popout.mjs';
|
|
4
4
|
import { deviceIcons } from './dev-ui-icons.mjs';
|
|
5
5
|
import { createDevPosition } from './dev-position.mjs';
|
|
6
6
|
import { createDevIssueMotion } from './dev-issue-motion.mjs';
|
|
7
7
|
|
|
8
|
+
const loadDevTool = registerDevToolLoaders({
|
|
9
|
+
workbench: () => import('./dev-workbench.mjs'),
|
|
10
|
+
inspector: () => import('./dev-tools-panel.mjs'),
|
|
11
|
+
});
|
|
12
|
+
|
|
8
13
|
// This module is imported only by the development browser entry. No framework
|
|
9
14
|
// contexts, application CSS, network requests or additional SSE streams.
|
|
10
15
|
export function createDevIndicator({ position = 'bottom-left', snapshot, showIssues, capture, tools }) {
|
|
@@ -79,7 +84,7 @@ export function createDevIndicator({ position = 'bottom-left', snapshot, showIss
|
|
|
79
84
|
let workbench;
|
|
80
85
|
const toolError=error=>{feedback.textContent='Could not open development tool. Try again.';console.error(error);};
|
|
81
86
|
const getWorkbench = () => workbench ||= createLazyDevTool(async()=>{
|
|
82
|
-
const {createDevWorkbench}=await
|
|
87
|
+
const {createDevWorkbench}=await loadDevTool('workbench');
|
|
83
88
|
return createDevWorkbench({shadow,capture,tools,restoreFocus:()=>trigger.focus()});
|
|
84
89
|
},{onError:toolError,reserve(method,args){
|
|
85
90
|
if(!['popConsole','popPreview'].includes(method))return;
|
|
@@ -94,7 +99,7 @@ export function createDevIndicator({ position = 'bottom-left', snapshot, showIss
|
|
|
94
99
|
consoleRow.append(consoleAction,detachButton('Open console in new window',()=>{close(false);getWorkbench().popConsole();}));
|
|
95
100
|
let inspector;
|
|
96
101
|
const getInspector = () => inspector ||= createLazyDevTool(async()=>{
|
|
97
|
-
const {createDevToolsPanel}=await
|
|
102
|
+
const {createDevToolsPanel}=await loadDevTool('inspector');
|
|
98
103
|
return createDevToolsPanel({shadow,tools,restoreFocus:()=>trigger.focus()});
|
|
99
104
|
},{onError:toolError});
|
|
100
105
|
if (tools) {
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// Capture user activation synchronously for detached windows, even on a slow
|
|
2
2
|
// connection. Import failures/disposal close reservations instead of leaking tabs.
|
|
3
|
+
const loaderRegistry = Symbol.for('prnext.dev.tool.loaders');
|
|
4
|
+
|
|
5
|
+
// The indicator survives Fast Refresh, while hashed import URLs belong to a
|
|
6
|
+
// build generation. Publish the current factories on module evaluation so an
|
|
7
|
+
// older, unopened indicator never requests a retired generation's lazy chunk.
|
|
8
|
+
// Existing tool instances remain owned by createLazyDevTool and keep their DOM.
|
|
9
|
+
export function registerDevToolLoaders(loaders, target = globalThis) {
|
|
10
|
+
const registry = target[loaderRegistry] ||= Object.create(null);
|
|
11
|
+
Object.assign(registry, loaders);
|
|
12
|
+
return name => {
|
|
13
|
+
const load = registry[name];
|
|
14
|
+
if (typeof load !== 'function') throw Error('Development tool loader is unavailable.');
|
|
15
|
+
return load();
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export function createLazyDevTool(load,{onError=console.error,reserve}={}) {
|
|
4
20
|
let instance,pending,disposed=false,epoch=0;
|
|
5
21
|
const get=()=>pending||=(async()=>{const value=await load();if(disposed){value.dispose();throw Error('Development tool was closed');}return instance=value;})().catch(error=>{pending=undefined;throw error;});
|
|
@@ -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,6 +1,7 @@
|
|
|
1
|
+
import {createDevCompilerBroker} from './dev-compiler-broker.mjs';
|
|
1
2
|
import {spawn} from 'node:child_process';
|
|
2
3
|
import {mkdir,readdir,readFile,writeFile,rm,link,copyFile,realpath,lstat} from 'node:fs/promises';
|
|
3
|
-
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {randomUUID,randomBytes} from 'node:crypto';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import {fileURLToPath} from 'node:url';
|
|
6
7
|
import {rename} from './fs.mjs';
|
|
@@ -20,6 +21,10 @@ import {tryDevCssUpdate} from '../build/dev-css-update.mjs';
|
|
|
20
21
|
import {devBuildReason} from './dev-build-reasons.mjs';
|
|
21
22
|
import {publishDependencyGraph,affectedDependencies,canSkipUnopenedRoutes} from './dev-dependency-graph.mjs';
|
|
22
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';
|
|
23
28
|
|
|
24
29
|
const packageRoot=fileURLToPath(new URL('../',import.meta.url));
|
|
25
30
|
export function developmentRoute(routes,raw,config={}) {
|
|
@@ -82,6 +87,9 @@ export async function develop(projectRoot,args,binary) {
|
|
|
82
87
|
}
|
|
83
88
|
async function runDevelopment(projectRoot,args,binary) {
|
|
84
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');
|
|
85
93
|
const stateFile=path.join(root,'.prnext-dev.json'),home=path.join(root,'.prnext-dev',session);
|
|
86
94
|
const diagnosticsDirectory=process.env.PRNEXT_INSPECT_DIR||path.join(home,'diagnostics');
|
|
87
95
|
// The session is ours; never clean a path obtained from application config.
|
|
@@ -94,8 +102,19 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
94
102
|
const lifecycle=new AbortController();
|
|
95
103
|
let queue=Promise.resolve(),stateQueue=Promise.resolve(),output='.prnext',changes=new Set(),active=new Map(),failed=new Set();
|
|
96
104
|
const failedPaths=new Set();
|
|
97
|
-
const
|
|
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+'/')});
|
|
107
|
+
const compilerBroker=await createDevCompilerBroker({compile:(pattern,sources)=>serialized(async()=>{
|
|
108
|
+
if(stopping)throw Error('Development is stopping');
|
|
109
|
+
if(!discovery)await discover();
|
|
110
|
+
const patterns=[...new Set([pattern,...sources])];
|
|
111
|
+
if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))await discover();
|
|
112
|
+
if(patterns.some(p=>!discovery.project.routes.some(route=>route.pattern===p)))throw Error('Unknown deferred route pattern');
|
|
113
|
+
for(const value of patterns)active.set(value,Date.now());
|
|
114
|
+
if(!current||patterns.some(p=>!current.patterns.has(p)))await rebuild([...active.keys()]);
|
|
115
|
+
return current.directory;
|
|
116
|
+
})});
|
|
117
|
+
const runtimePool=createDevRuntimePool({binary,root,home,environment:{PRNEXT_DEV_COMPILER:compilerBroker.descriptor,PRNEXT_INSPECT_DIR:diagnosticsDirectory,...(options.profile?{PRNEXT_PROFILE:options.profile}:{})},onChild:child=>captureOutput({stderr:child.stderr,once:child.once.bind(child)},'runtime',{forwardStdout:false})});
|
|
99
118
|
const toolSnapshot=()=>({...readyState,safeMode:options.safe,sequence:readyState.sequence??sequence,session,typeErrors,
|
|
100
119
|
routes:(discovery?.project.routes||[]).filter(route=>!route.internal).map(route=>({path:(discovery.config.basePath||'')+(route.pattern==='/'?'/':route.pattern),kind:route.kind,source:route.file?path.relative(root,route.file).replaceAll(path.sep,'/'):undefined,compiled:current?.patterns.has(route.pattern)||false,pinned:[...pins.values()].some(pin=>pin.pattern===route.pattern)})),
|
|
101
120
|
cache:{activeRoutes:active.size,pinnedRoutes:new Set([...pins.values()].map(pin=>pin.pattern)).size,retainedGenerations:generations.length,lookupEntries:routeCache.size,compiler:readyState.compiler,transforms:readyState.transforms,persistentTransforms:true,dependencyIndex:dependencyIndexValid?{valid:true,...current?.manifest.devDependencyIndex}:{valid:false}}});
|
|
@@ -113,10 +132,23 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
113
132
|
const route=developmentRoute(discovery.project.routes,raw,discovery.config);
|
|
114
133
|
if(routeCache.size>=256)routeCache.delete(routeCache.keys().next().value);routeCache.set(key,route);return route;
|
|
115
134
|
}
|
|
135
|
+
let publicationFailures=0,lastPublicationFailure;
|
|
116
136
|
const writeState=value=>{
|
|
117
|
-
const task=stateQueue.then(async()=>{
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
+
});
|
|
120
152
|
stateQueue=task.catch(()=>{});return task;
|
|
121
153
|
};
|
|
122
154
|
const serialized=run=>{const task=queue.then(()=>{if(stopping)throw Error('Development is stopping');return run();});queue=task.catch(()=>{});return task;};
|
|
@@ -140,14 +172,13 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
140
172
|
async function discover(){
|
|
141
173
|
discovery=(await compilerRequest('discover')).discovery;routeCache.clear();output=discovery.config.distDir;
|
|
142
174
|
if(options.safe)discovery={...discovery,eager:true};
|
|
143
|
-
if(discovery.eager&&!options.safe)console.log('Development uses the complete route graph for middleware, rewrites or parallel routing.');
|
|
144
175
|
const {writeTypedRoutes}=await import('../build/typed-routes.mjs');
|
|
145
176
|
await writeTypedRoutes(root,discovery.project,discovery.config);
|
|
146
177
|
if(!typeChecker||typeChecker.inactive){const {startTypeChecker}=await import('../build/typescript-dev.mjs');typeChecker=await startTypeChecker(root,discovery.config,errors=>{typeErrors=errors;if(!stopping)void writeState().catch(console.error);});}
|
|
147
178
|
}
|
|
148
179
|
async function launch(manifest,directory){
|
|
149
180
|
const runtime=options.safe?undefined:await runtimePool.take(directory).catch(error=>{tools?.addServerLog('warn','Worker warmup was unavailable; starting a fresh worker.',{process:'runtime'});return undefined;});
|
|
150
|
-
const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory};
|
|
181
|
+
const env={...process.env,NODE_ENV:process.env.NODE_ENV||'development',PRNEXT_INSPECT_DIR:diagnosticsDirectory,PRNEXT_DEV_COMPILER:compilerBroker.descriptor};
|
|
151
182
|
delete env.PRNEXT_DEV_BROKER;
|
|
152
183
|
if(runtime)env.PRNEXT_DEV_BROKER=runtime.descriptor;
|
|
153
184
|
const child=spawn(binary,['start',root,'--build-dir',directory,'--worker',path.join(root,directory,'runtime/worker.mjs'),'--node',process.execPath,'--hostname','127.0.0.1','--port','0','--shutdown-on-stdin-eof',...options.backend],{stdio:['pipe','pipe','pipe'],windowsHide:true,env});
|
|
@@ -170,21 +201,21 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
170
201
|
if(!options.safe)void runtimePool.warm().catch(()=>{});
|
|
171
202
|
if(current&&!discovery.eager&&discovery.cssFast){
|
|
172
203
|
const directory=path.relative(root,path.join(home,'styles-'+randomUUID())).replaceAll(path.sep,'/');
|
|
173
|
-
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});}});
|
|
174
205
|
if(styles){
|
|
175
206
|
await gateway.request('assets',{generation:styles.buildId,directory});
|
|
176
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'});}
|
|
177
|
-
sequence++;current.manifest=styles;current.cssDirectory=directory;current.cssOnly=true;current.affectedRoutes=undefined;current.changed=changed;
|
|
178
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;
|
|
179
211
|
reason=devBuildReason({cause:'css-only'});
|
|
180
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});
|
|
181
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'});
|
|
182
214
|
console.log('Styles updated. '+reason.summary);
|
|
183
|
-
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});}
|
|
184
215
|
await watcher?.refresh();return styles;
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
|
-
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))}});
|
|
188
219
|
phases.compile=performance.now()-mark;mark=performance.now();
|
|
189
220
|
if(stopping)throw Error('Development is stopping');
|
|
190
221
|
const consumed=filter.acceptInputs(result.inputs||[]);
|
|
@@ -201,32 +232,57 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
201
232
|
catch(error){dependencyIndexValid=false;await gateway.request('graph',{routes:[]});affectedRoutes=undefined;affectedModules=undefined;tools?.addServerLog('warn','Selective HMR unavailable: '+error.message,{process:'compiler'});}
|
|
202
233
|
const previous=current;current=next;current.compiler=result.compiler;current.transforms=result.transforms;generations.push(next);failed.clear();failedPaths.clear();
|
|
203
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);
|
|
204
236
|
const affected=Object.keys(manifest.app?.actions||{}).length?undefined:affectedRoutes;
|
|
205
237
|
current.affectedRoutes=affected;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|
|
211
255
|
}
|
|
212
|
-
while(generations.length>3){const old=generations.shift();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});}
|
|
213
256
|
await watcher?.refresh();
|
|
214
257
|
return manifest;
|
|
215
258
|
}catch(error){if(!stopping){tools?.recordBuild({sequence,reason:reason.summary,reasonCode:reason.code,time:Date.now(),startedAt,duration:performance.now()-started,phases,state:'error'});tools?.addServerLog('error',error.message,{process:'compiler'});console.error('Build failed: '+error.message);await writeState({state:'error',reason,error:error.message});}throw error;}
|
|
216
259
|
}
|
|
217
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});
|
|
218
|
-
async function ensure(raw,client){
|
|
219
|
-
|
|
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
|
+
});
|
|
274
|
+
const resolve=()=>exact?discovery.project.routes.find(route=>route.pattern===raw):findRoute(raw);
|
|
275
|
+
const warm=discovery&&resolve();
|
|
220
276
|
if(!stopping&¤t&&warm&&active.has(warm.pattern)&¤t.patterns.has(warm.pattern)){
|
|
221
277
|
active.set(warm.pattern,Date.now());if(pins.has(client))pins.get(client).pattern=warm.pattern;
|
|
222
278
|
return updateInfo();
|
|
223
279
|
}
|
|
224
280
|
return serialized(async()=>{
|
|
225
281
|
if(!discovery)try{await discover();}catch(error){failedPaths.add(raw);await writeState({state:'error',error:error.message});throw error;}
|
|
226
|
-
let route=
|
|
282
|
+
let route=resolve();
|
|
227
283
|
// A newly written route can be requested before the watcher's debounce.
|
|
228
284
|
// Resolve that miss against the filesystem instead of returning a stale 404.
|
|
229
|
-
if(!route){await discover();route=
|
|
285
|
+
if(!route){await discover();route=resolve();}
|
|
230
286
|
if(route)active.set(route.pattern,Date.now());
|
|
231
287
|
if(route&&pins.has(client))pins.get(client).pattern=route.pattern;
|
|
232
288
|
const max=Math.max(0,Math.min(64,discovery.config.onDemandEntries?.pagesBufferLength??5));
|
|
@@ -235,7 +291,8 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
235
291
|
const inactive=[...active].filter(([pattern])=>!pinned.has(pattern)).sort((a,b)=>a[1]-b[1]);
|
|
236
292
|
const age=Math.min(86400000,discovery.config.onDemandEntries?.maxInactiveAge??60000);
|
|
237
293
|
while(inactive.length>max&&(Date.now()-inactive[0][1]>=age||active.size>64+pinned.size))active.delete(inactive.shift()[0]);
|
|
238
|
-
|
|
294
|
+
const graphChanged=current&&discovery.project.routes.some(candidate=>!current.manifest.routes.some(built=>built.id===candidate.id));
|
|
295
|
+
if(!current || graphChanged || route&&!current.patterns.has(route.pattern)){
|
|
239
296
|
try{await rebuild([...active.keys()]);}catch(error){if(route){failed.add(route.pattern);active.delete(route.pattern);}throw error;}
|
|
240
297
|
}
|
|
241
298
|
return updateInfo();
|
|
@@ -273,7 +330,7 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
273
330
|
// Node process has drained after() work and cache fills on stdin EOF.
|
|
274
331
|
await runtimePool.close();
|
|
275
332
|
await Promise.all([tools?.close(),...generations.map(g=>stop(g.child)),...retiring]);
|
|
276
|
-
await queue.catch(()=>{});await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
|
|
333
|
+
await queue.catch(()=>{});await compilerBroker.close();await stateQueue;await rm(home,{recursive:true,force:true,maxRetries:10,retryDelay:100});await rm(stateFile,{force:true});
|
|
277
334
|
})();return closing;
|
|
278
335
|
};
|
|
279
336
|
const fatal=error=>{if(!stopping){console.error(error.message);process.exitCode=1;void shutdown();}};
|
|
@@ -298,7 +355,8 @@ async function runDevelopment(projectRoot,args,binary) {
|
|
|
298
355
|
}
|
|
299
356
|
console.log(`PRNext development · http://${options.hostname.includes(':')?'['+options.hostname+']':options.hostname}:${listener.port} · on-demand routes`);
|
|
300
357
|
if(options.safe)console.log('Safe mode: full route builds, fresh compilers and no worker warmup.');
|
|
301
|
-
watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:file=>{
|
|
358
|
+
watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:fatal,onChange:(file,event)=>{
|
|
359
|
+
typeChecker?.invalidate?.(file,event);
|
|
302
360
|
if(stopping||!shouldWatchProjectFile(file))return;changes.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void serialized(changed).catch(()=>{}),75);
|
|
303
361
|
}});
|
|
304
362
|
await serialized(discover).catch(async error=>{console.error('Build failed: '+error.message);await writeState({state:'error',error:error.message});});
|