@thomas.f/prnext 0.1.15-alpha → 0.1.17-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/AI.md +63 -0
- package/README.md +387 -12
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +5 -0
- package/build/analyze.mjs +184 -0
- package/build/app-config.mjs +7 -8
- package/build/app-static.mjs +12 -7
- package/build/app.mjs +23 -10
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +20 -5
- package/build/check.mjs +56 -11
- package/build/compiler.mjs +25 -1
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +21 -13
- package/build/css-modules.mjs +15 -4
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +83 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +55 -4
- package/build/edge.mjs +3 -2
- package/build/framework-imports.mjs +40 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +96 -101
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +2 -3
- package/build/migrate.mjs +60 -57
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +97 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-pages.mjs +90 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +80 -0
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +15 -3
- package/build/transform-cache.mjs +37 -4
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +91 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +22 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +5 -4
- package/build/webpack-graph.mjs +164 -27
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +105 -121
- package/compat/index.d.ts +21 -7
- package/compat/link.d.cts +4 -3
- package/compat/navigation.d.cts +4 -3
- package/compat/router.d.cts +4 -3
- package/compat/typed-route.d.cts +17 -0
- package/docs/ai/compatibility.md +106 -0
- package/docs/ai/data.md +128 -0
- package/docs/ai/deployment.md +111 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +146 -0
- package/docs/ai/routing.md +132 -0
- package/docs/ai/troubleshooting.md +120 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +28 -10
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +6 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +17 -6
- package/runtime/app-render.mjs +12 -0
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +239 -45
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +186 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +18 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +307 -0
- package/runtime/env.mjs +2 -1
- package/runtime/pages-client.mjs +10 -1
- package/runtime/pages-navigation.mjs +6 -1
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/rsc-worker.mjs +7 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +12 -5
- package/runtime/worker.mjs +36 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {readFile,writeFile,stat,open} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {gzip,brotliCompress,constants} from 'node:zlib';
|
|
5
|
+
import {promisify} from 'node:util';
|
|
6
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
7
|
+
import {readBuildDirectory} from '../runtime/build-directory.mjs';
|
|
8
|
+
|
|
9
|
+
export function recordBuildAnalysis(state,options,result){
|
|
10
|
+
if(!state?.stage||!result.metafile)return;
|
|
11
|
+
state.analysis ||= new Map();
|
|
12
|
+
state.analysisRoutes ||= new Map();
|
|
13
|
+
const outputs=new Map(Object.entries(result.metafile.outputs).map(([file,info])=>[path.resolve(options.absWorkingDir||state.root,file),info]));
|
|
14
|
+
for(const [absolute,info] of outputs){
|
|
15
|
+
const relative=path.relative(state.stage,absolute).replaceAll(path.sep,'/');
|
|
16
|
+
if(!info.entryPoint||!relative.startsWith('server/'))continue;
|
|
17
|
+
const visited=new Set(),inputs=new Set(),pending=[absolute];
|
|
18
|
+
while(pending.length){
|
|
19
|
+
const next=pending.pop();if(visited.has(next))continue;visited.add(next);
|
|
20
|
+
const output=outputs.get(next);if(!output)continue;
|
|
21
|
+
for(const name of Object.keys(output.inputs||{}))inputs.add(path.relative(state.root,path.resolve(options.absWorkingDir||state.root,name)).replaceAll(path.sep,'/'));
|
|
22
|
+
for(const item of output.imports||[])if(!item.external)pending.push(path.resolve(options.absWorkingDir||state.root,item.path));
|
|
23
|
+
}
|
|
24
|
+
state.analysisRoutes.set(relative,inputs);
|
|
25
|
+
}
|
|
26
|
+
for(const [file,info] of Object.entries(result.metafile.outputs)){
|
|
27
|
+
const absolute=path.resolve(options.absWorkingDir||state.root,file);
|
|
28
|
+
const relative=path.relative(path.join(state.stage,'assets'),absolute).replaceAll(path.sep,'/');
|
|
29
|
+
if(relative.startsWith('../')||path.isAbsolute(relative)||!relative.endsWith('.js'))continue;
|
|
30
|
+
state.analysis.set(relative,{file:relative,bytes:info.bytes,imports:(info.imports||[]).filter(item=>!item.external).map(item=>({file:path.relative(path.join(state.stage,'assets'),path.resolve(options.absWorkingDir||state.root,item.path)).replaceAll(path.sep,'/'),kind:item.kind})),
|
|
31
|
+
modules:Object.entries(info.inputs||{}).map(([name,record])=>({file:path.relative(state.root,path.resolve(options.absWorkingDir||state.root,name)).replaceAll(path.sep,'/').replace(/\.prnext-build-[^/]+\//g,'<generated>/'),bytes:record.bytesInOutput})).sort((a,b)=>b.bytes-a.bytes).slice(0,100)});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function initialFiles(entry,files){
|
|
35
|
+
const seen=new Set(),pending=[entry];
|
|
36
|
+
while(pending.length){const name=pending.pop();if(seen.has(name)||!files.has(name))continue;seen.add(name);for(const item of files.get(name).imports)if(item.kind!=='dynamic-import')pending.push(item.file);}
|
|
37
|
+
return [...seen].sort();
|
|
38
|
+
}
|
|
39
|
+
const gzipAsync=promisify(gzip),brotliAsync=promisify(brotliCompress);
|
|
40
|
+
export async function writeBuildAnalysis({root,stage,destination,manifest,records,routeInputs,precompressed=false}){
|
|
41
|
+
const files=new Map(records||[]);
|
|
42
|
+
await mapConcurrent(files.values(),async info=>{
|
|
43
|
+
const file=path.join(stage,'assets',info.file),data=await readFile(file);
|
|
44
|
+
info.bytes=data.length;
|
|
45
|
+
// Sidecars in this unpublished stage have just passed decode/hash validation.
|
|
46
|
+
// Never trust arbitrary sidecars when analysis is called independently.
|
|
47
|
+
const size=async(extension,compress)=>{
|
|
48
|
+
if(precompressed)try{return(await stat(file+extension)).size;}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
49
|
+
return(await compress()).length;
|
|
50
|
+
};
|
|
51
|
+
info.gzipBytes=await size('.gz',()=>gzipAsync(data));
|
|
52
|
+
info.brotliBytes=await size('.br',()=>brotliAsync(data,{params:{[constants.BROTLI_PARAM_QUALITY]:5}}));
|
|
53
|
+
});
|
|
54
|
+
const routes=manifest.routes.filter(route=>!route.internal&&route.kind==='page').map(route=>{
|
|
55
|
+
const client=route.client?decodeURIComponent(new URL(route.client,'http://local').pathname).split('/').pop():null;
|
|
56
|
+
const entries=client?[client]:[];
|
|
57
|
+
if(route.router==='app')for(const file of routeInputs?.get(route.module)||[]){
|
|
58
|
+
const id='client-'+createHash('sha256').update(file).digest('hex').slice(0,16);
|
|
59
|
+
const entry=manifest.app?.clientModules?.[id]?.browserModule;
|
|
60
|
+
if(entry)entries.push(new URL(entry,'http://local').pathname.split('/').pop());
|
|
61
|
+
}
|
|
62
|
+
const assets=[...new Set(entries.flatMap(entry=>initialFiles(entry,files)))].sort();
|
|
63
|
+
return {route:route.pattern,files:assets,jsBytes:assets.reduce((total,file)=>total+files.get(file).bytes,0),gzipBytes:assets.reduce((total,file)=>total+files.get(file).gzipBytes,0),brotliBytes:assets.reduce((total,file)=>total+files.get(file).brotliBytes,0)};
|
|
64
|
+
});
|
|
65
|
+
const report={version:1,buildId:manifest.buildId,routes,files:[...files.values()],totalJsBytes:[...files.values()].reduce((n,item)=>n+item.bytes,0)};
|
|
66
|
+
try{const previous=await readReport(path.join(destination,'build-analysis.json'));report.previous={buildId:previous.buildId,totalJsBytes:previous.totalJsBytes,routes:previous.routes.map(({route,jsBytes,gzipBytes,brotliBytes})=>({route,jsBytes,gzipBytes,brotliBytes}))};}catch(error){if(error.code!=='ENOENT')console.warn('Previous bundle analysis could not be read; comparison unavailable.');}
|
|
67
|
+
await writeFile(path.join(stage,'build-analysis.json'),JSON.stringify(report));
|
|
68
|
+
return report;
|
|
69
|
+
}
|
|
70
|
+
const metrics={raw:'jsBytes',gzip:'gzipBytes',brotli:'brotliBytes'};
|
|
71
|
+
const size=value=>Number.isSafeInteger(value)&&value>=0;
|
|
72
|
+
const text=value=>typeof value==='string'&&value.length>0&&value.length<=4096&&!/[\0\r\n]/.test(value);
|
|
73
|
+
const object=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
74
|
+
async function boundedJson(file,label){
|
|
75
|
+
const handle=await open(file,'r'),limit=16*1024*1024;
|
|
76
|
+
try{
|
|
77
|
+
const info=await handle.stat();
|
|
78
|
+
if(!info.isFile()||info.size>limit)throw new Error(`${label} must be a regular JSON file of at most 16 MiB.`);
|
|
79
|
+
// Fixed allocation also bounds reads when a concurrent writer grows it.
|
|
80
|
+
const data=Buffer.alloc(limit+1);let length=0;
|
|
81
|
+
while(length<data.length){const {bytesRead}=await handle.read(data,length,data.length-length,null);if(!bytesRead)break;length+=bytesRead;}
|
|
82
|
+
if(length>limit)throw new Error(`${label} exceeds 16 MiB.`);
|
|
83
|
+
try{return JSON.parse(data.subarray(0,length).toString('utf8'));}catch{throw new Error(`${label} contains invalid JSON.`);}
|
|
84
|
+
}finally{await handle.close();}
|
|
85
|
+
}
|
|
86
|
+
function validateRoutes(routes,{complete=true}={}){
|
|
87
|
+
if(!Array.isArray(routes)||routes.length>10000)throw new Error('Invalid analysis routes (maximum 10,000).');
|
|
88
|
+
const seen=new Set();
|
|
89
|
+
for(const route of routes){
|
|
90
|
+
if(!object(route)||!text(route.route)||!route.route.startsWith('/')||seen.has(route.route)||!size(route.jsBytes))throw new Error('Invalid or duplicate route in bundle analysis.');
|
|
91
|
+
seen.add(route.route);
|
|
92
|
+
for(const key of ['gzipBytes','brotliBytes'])if((complete||route[key]!==undefined)&&!size(route[key]))throw new Error(`Invalid ${key} in bundle analysis.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function readReport(file){
|
|
96
|
+
const value=await boundedJson(file,'Bundle analysis');
|
|
97
|
+
if(!object(value)||value.version!==1||!text(value.buildId)||!size(value.totalJsBytes)||!Array.isArray(value.files)||value.files.length>100000)throw new Error('Invalid bundle analysis; rebuild with this PRNext version.');
|
|
98
|
+
validateRoutes(value.routes);
|
|
99
|
+
for(const route of value.routes)if(!Array.isArray(route.files)||route.files.length>100000||route.files.some(file=>!text(file)))throw new Error('Invalid route asset list in bundle analysis.');
|
|
100
|
+
for(const file of value.files){
|
|
101
|
+
if(!object(file)||!text(file.file)||!size(file.bytes)||!size(file.gzipBytes)||!size(file.brotliBytes)||!Array.isArray(file.modules)||file.modules.length>100||file.modules.some(module=>!object(module)||!text(module.file)||!size(module.bytes))||!Array.isArray(file.imports)||file.imports.length>100000||file.imports.some(item=>!object(item)||!text(item.file)||!text(item.kind)))throw new Error('Invalid file, module or import in bundle analysis.');
|
|
102
|
+
}
|
|
103
|
+
if(value.previous!==undefined){
|
|
104
|
+
if(!object(value.previous)||!text(value.previous.buildId)||value.previous.totalJsBytes!==undefined&&!size(value.previous.totalJsBytes))throw new Error('Invalid previous bundle analysis.');
|
|
105
|
+
validateRoutes(value.previous.routes,{complete:false});
|
|
106
|
+
}
|
|
107
|
+
// Retain only the versioned schema, rather than echoing arbitrary nested
|
|
108
|
+
// properties from a supplied baseline into a machine-readable report.
|
|
109
|
+
const cleanRoute=({route,files,jsBytes,gzipBytes,brotliBytes})=>({route,files,jsBytes,gzipBytes,brotliBytes});
|
|
110
|
+
return {version:1,buildId:value.buildId,totalJsBytes:value.totalJsBytes,routes:value.routes.map(cleanRoute),
|
|
111
|
+
files:value.files.map(({file,bytes,gzipBytes,brotliBytes,imports,modules})=>({file,bytes,gzipBytes,brotliBytes,imports:imports.map(({file,kind})=>({file,kind})),modules:modules.map(({file,bytes})=>({file,bytes}))})),
|
|
112
|
+
...(value.previous?{previous:{buildId:value.previous.buildId,totalJsBytes:value.previous.totalJsBytes,routes:value.previous.routes.map(({route,jsBytes,gzipBytes,brotliBytes})=>({route,jsBytes,gzipBytes,brotliBytes}))}}:{})};
|
|
113
|
+
}
|
|
114
|
+
function nonnegative(value,name,{integer=false}={}){
|
|
115
|
+
if(typeof value!=='number'||!Number.isFinite(value)||value<0||value>Number.MAX_SAFE_INTEGER||(integer&&!Number.isSafeInteger(value)))throw new Error(`${name} requires a finite nonnegative ${integer?'integer':'number'}.`);
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
export function parseAnalysisArgs(args){
|
|
119
|
+
const options={};
|
|
120
|
+
const numeric={'--max-js':'maxJs','--max-increase-bytes':'maxIncreaseBytes','--max-increase-percent':'maxIncreasePercent'};
|
|
121
|
+
const values={'--baseline':'baseline','--metric':'metric','--budgets':'budgets'};
|
|
122
|
+
for(let index=0;index<args.length;index++){
|
|
123
|
+
const flag=args[index],key=Object.hasOwn(numeric,flag)?numeric[flag]:Object.hasOwn(values,flag)?values[flag]:undefined;
|
|
124
|
+
if(flag==='--json'&&!options.json){options.json=true;continue;}
|
|
125
|
+
if(!key||options[key]!==undefined)throw new Error(`Unknown or duplicate analyze option: ${flag}`);
|
|
126
|
+
const value=args[++index];if(!value||value.startsWith('--'))throw new Error(`${flag} requires a value.`);
|
|
127
|
+
if(Object.hasOwn(numeric,flag)){
|
|
128
|
+
if(!/^\d+(?:\.\d+)?$/.test(value))throw new Error(`${flag} requires a nonnegative number.`);
|
|
129
|
+
options[key]=nonnegative(Number(value),flag,{integer:key==='maxIncreaseBytes'});
|
|
130
|
+
}else options[key]=value;
|
|
131
|
+
}
|
|
132
|
+
if(options.metric!==undefined&&!Object.hasOwn(metrics,options.metric))throw new Error('--metric must be raw, gzip or brotli.');
|
|
133
|
+
return options;
|
|
134
|
+
}
|
|
135
|
+
async function routeBudgets(filename){
|
|
136
|
+
if(!filename)return {};
|
|
137
|
+
const value=await boundedJson(filename,'Route budgets');
|
|
138
|
+
if(!object(value)||Object.keys(value).length>10000)throw new Error('Route budgets must be an object with at most 10,000 route keys.');
|
|
139
|
+
for(const [route,budget]of Object.entries(value)){
|
|
140
|
+
if(!text(route)||!route.startsWith('/')||!object(budget)||!Object.keys(budget).length)throw new Error('Each route budget must be a nonempty object keyed by an exact route pattern.');
|
|
141
|
+
for(const [key,limit]of Object.entries(budget)){
|
|
142
|
+
if(!['maxJs','maxIncreaseBytes','maxIncreasePercent'].includes(key))throw new Error(`Unknown route budget field: ${key}`);
|
|
143
|
+
nonnegative(limit,`Budget ${route}.${key}`,{integer:key==='maxIncreaseBytes'});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
export async function analyzeBuild(root,{maxJs,baseline,metric='raw',maxIncreaseBytes,maxIncreasePercent,budgets}={}){
|
|
149
|
+
if(!Object.hasOwn(metrics,metric))throw new Error('Analysis metric must be raw, gzip or brotli.');
|
|
150
|
+
for(const [key,value]of Object.entries({maxJs,maxIncreaseBytes,maxIncreasePercent}))if(value!==undefined)nonnegative(value,key,{integer:key==='maxIncreaseBytes'});
|
|
151
|
+
const report=await readReport(path.join(root,await readBuildDirectory(root),'build-analysis.json'));
|
|
152
|
+
const limits=await routeBudgets(budgets?path.resolve(root,budgets):null);
|
|
153
|
+
const routeNames=new Set(report.routes.map(route=>route.route));
|
|
154
|
+
for(const route of Object.keys(limits))if(!routeNames.has(route))throw new Error(`Route budget does not match a current route: ${route}`);
|
|
155
|
+
const reference=baseline?await readReport(path.resolve(root,baseline)):report.previous;
|
|
156
|
+
const needsBaseline=maxIncreaseBytes!==undefined||maxIncreasePercent!==undefined||Object.values(limits).some(value=>value.maxIncreaseBytes!==undefined||value.maxIncreasePercent!==undefined);
|
|
157
|
+
if(needsBaseline&&!reference)throw new Error('Regression budgets require --baseline or a previous build analysis.');
|
|
158
|
+
const field=metrics[metric],previous=new Map((reference?.routes||[]).map(route=>[route.route,route[field]]));
|
|
159
|
+
report.metric=metric;
|
|
160
|
+
report.baseline=reference?{buildId:reference.buildId,source:baseline?'file':'previous-build'}:null;
|
|
161
|
+
report.routes=report.routes.map(route=>{
|
|
162
|
+
const budget={maxJs,maxIncreaseBytes,maxIncreasePercent,...(limits[route.route]||{})};
|
|
163
|
+
const before=previous.get(route.route),current=route[field],hasBaseline=size(before),delta=hasBaseline?current-before:null;
|
|
164
|
+
const regressionBudget=budget.maxIncreaseBytes!==undefined||budget.maxIncreasePercent!==undefined;
|
|
165
|
+
const exceeded=[];
|
|
166
|
+
if(budget.maxJs!==undefined&¤t>budget.maxJs*1000)exceeded.push('maxJs');
|
|
167
|
+
if(regressionBudget&&!hasBaseline)exceeded.push('baseline-missing');
|
|
168
|
+
if(hasBaseline&&budget.maxIncreaseBytes!==undefined&&delta>budget.maxIncreaseBytes)exceeded.push('maxIncreaseBytes');
|
|
169
|
+
if(hasBaseline&&budget.maxIncreasePercent!==undefined&&(before===0?delta>0:delta/before*100>budget.maxIncreasePercent))exceeded.push('maxIncreasePercent');
|
|
170
|
+
return {...route,selectedBytes:current,baselineBytes:hasBaseline?before:null,deltaBytes:delta,deltaPercent:hasBaseline&&before>0?delta/before*100:hasBaseline&¤t===0?0:null,
|
|
171
|
+
comparison:hasBaseline?'compared':reference?'new-or-unavailable':'no-baseline',budget:{...budget,exceeded}};
|
|
172
|
+
}).sort((a,b)=>b.selectedBytes-a.selectedBytes||a.route.localeCompare(b.route));
|
|
173
|
+
report.budget={metric,maxJsBytes:maxJs===undefined?null:maxJs*1000,maxIncreaseBytes:maxIncreaseBytes??null,maxIncreasePercent:maxIncreasePercent??null,exceeded:report.routes.filter(route=>route.budget.exceeded.length).map(route=>route.route)};
|
|
174
|
+
report.ok=report.budget.exceeded.length===0;
|
|
175
|
+
return report;
|
|
176
|
+
}
|
|
177
|
+
export function printAnalysis(report){
|
|
178
|
+
const kb=n=>(n/1000).toFixed(1)+' kB';
|
|
179
|
+
console.log(`PRNext bundle analysis (${report.buildId})\nInitial browser JavaScript per route; shared files counted once per route. Dynamic imports are excluded. Budget metric: ${report.metric||'raw'}.`);
|
|
180
|
+
for(const route of report.routes)console.log(`${route.route}: ${kb(route.jsBytes)} JS / ${kb(route.gzipBytes)} gzip / ${kb(route.brotliBytes)} br${route.deltaBytes===null?'':` (${route.deltaBytes>=0?'+':''}${kb(route.deltaBytes)} ${report.metric||'raw'} vs baseline${route.deltaPercent===null?'':`, ${route.deltaPercent.toFixed(1)}%`})`}${route.budget?.exceeded.length?` [${route.budget.exceeded.join(', ')}]`:''}`);
|
|
181
|
+
console.log('\nLargest browser modules (per output chunk):');
|
|
182
|
+
for(const item of report.files.flatMap(chunk=>chunk.modules.map(module=>({...module,chunk:chunk.file}))).sort((a,b)=>b.bytes-a.bytes).slice(0,10))console.log(` ${kb(item.bytes)} ${item.file} [${item.chunk}]`);
|
|
183
|
+
if(report.budget.exceeded.length)console.error(`JavaScript budget exceeded: ${report.budget.exceeded.join(', ')}`);
|
|
184
|
+
}
|
package/build/app-config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import {analyzeSource} from './source-analysis.mjs';
|
|
3
3
|
import {validSamples} from '../compat/instant-samples.cjs';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
|
|
@@ -23,19 +23,19 @@ const supported = new Map([
|
|
|
23
23
|
|
|
24
24
|
function literal(node) {
|
|
25
25
|
while (['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'TypeCastExpression'].includes(node?.type)) node = node.expression;
|
|
26
|
-
if (node?.type === '
|
|
26
|
+
if (node?.type === 'Literal' && node.value === null) return null;
|
|
27
27
|
if (node?.type === 'ArrayExpression') { const values = node.elements.map(literal); return values.includes(undefined) ? undefined : values; }
|
|
28
28
|
if (node?.type === 'ObjectExpression') {
|
|
29
29
|
const output = {};
|
|
30
30
|
for (const item of node.properties) {
|
|
31
|
-
if (item.type !== '
|
|
31
|
+
if (item.type !== 'Property' || item.kind !== 'init' || item.method || item.computed || item.shorthand) return undefined;
|
|
32
32
|
const key = item.key.name ?? item.key.value, value = literal(item.value);
|
|
33
33
|
if (typeof key !== 'string' || value === undefined || key === '__proto__') return undefined;
|
|
34
34
|
Object.defineProperty(output, key, { value, enumerable: true });
|
|
35
35
|
}
|
|
36
36
|
return output;
|
|
37
37
|
}
|
|
38
|
-
return ['
|
|
38
|
+
return node?.type === 'Literal' && ['string','boolean','number'].includes(typeof node.value) ? node.value : undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export async function validateAppConfig(file) {
|
|
@@ -48,8 +48,7 @@ export async function validateAppConfig(file) {
|
|
|
48
48
|
}
|
|
49
49
|
parsedMisses++;
|
|
50
50
|
if (hit) { parsedConfigs.delete(file); parsedBytes -= hit.size; }
|
|
51
|
-
const
|
|
52
|
-
plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
51
|
+
const analysis=analyzeSource(source,file), ast={program:analysis.program};
|
|
53
52
|
const locals = new Map();
|
|
54
53
|
const exports = [];
|
|
55
54
|
for (const statement of ast.program.body) {
|
|
@@ -71,11 +70,11 @@ export async function validateAppConfig(file) {
|
|
|
71
70
|
const value = locals.get(item.local);
|
|
72
71
|
const allowed = supported.get(item.name);
|
|
73
72
|
if (item.name === 'instant') {
|
|
74
|
-
if (
|
|
73
|
+
if (analysis.directives.includes('use client')) throw new Error(`instant cannot be exported by a Client Component (${file}).`);
|
|
75
74
|
if (value && typeof value === 'object' && Object.entries(value).every(([name, value]) => name === 'unstable_samples' ? validSamples(value) : name === 'level' ? ['warning', 'experimental-error'].includes(value) : ['unstable_disableValidation', 'unstable_disableDevValidation', 'unstable_disableBuildValidation'].includes(name) && value === true)) { config.instant = value; continue; }
|
|
76
75
|
}
|
|
77
76
|
if (item.name === 'generateStaticParams') {
|
|
78
|
-
if (
|
|
77
|
+
if (analysis.directives.includes('use client')) throw new Error(`generateStaticParams cannot be exported by a Client Component (${file}).`);
|
|
79
78
|
config.generateStaticParams = true;
|
|
80
79
|
continue;
|
|
81
80
|
}
|
package/build/app-static.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { staticPath } from '../runtime/pages-paths.mjs';
|
|
6
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
6
7
|
|
|
7
8
|
const MAX_STATIC_PATHS = 10_000;
|
|
8
9
|
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
@@ -48,7 +49,8 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
48
49
|
? await import(pathToFileURL(path.join(stage, 'runtime/app-static.mjs')).href) : null;
|
|
49
50
|
const handlers = routes.some(route => route.kind === 'api')
|
|
50
51
|
? await import(pathToFileURL(path.join(stage, 'runtime/route-static.mjs')).href) : null;
|
|
51
|
-
|
|
52
|
+
const initialSeeds=[...manifest.prerendered];
|
|
53
|
+
await mapConcurrent(routes,async route=>{
|
|
52
54
|
const handler = route.kind === 'api';
|
|
53
55
|
const options = { modulePath: path.join(stage, route.module), distDir: stage, manifest, route, production: true };
|
|
54
56
|
// This fixed internal entry shares the root layout, but its parameter
|
|
@@ -64,7 +66,7 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
64
66
|
// while separating dynamic rendering from the persistent Data Cache.
|
|
65
67
|
if (config.runtime === 'edge') {
|
|
66
68
|
if (dynamic && config.dynamicParams === false) route.allowedPaths = entries.map(entry => entry.path);
|
|
67
|
-
|
|
69
|
+
return;
|
|
68
70
|
}
|
|
69
71
|
const partial = !handler && manifest.config?.cacheComponents === true && (!config.dynamic || config.dynamic === 'auto');
|
|
70
72
|
if (partial && dynamic && !inspected.generated) entries.length = 0;
|
|
@@ -99,16 +101,17 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
99
101
|
if (partial) route.pprFallback = config.dynamicParams !== false;
|
|
100
102
|
if (dynamic && config.dynamicParams === false) route.allowedPaths = entries.map(entry => entry.path);
|
|
101
103
|
if (handler) {
|
|
102
|
-
if (!inspected.staticEligible)
|
|
103
|
-
if (dynamic && !inspected.generated && !['force-static', 'error'].includes(config.dynamic))
|
|
104
|
+
if (!inspected.staticEligible) return;
|
|
105
|
+
if (dynamic && !inspected.generated && !['force-static', 'error'].includes(config.dynamic)) return;
|
|
104
106
|
} else {
|
|
105
|
-
if (config.dynamic === 'force-dynamic' || (config.dynamic !== 'force-static' && (config.revalidate === 0 || config.forceNoStore)))
|
|
106
|
-
if (dynamic && !inspected.generated && !partial && !['force-static', 'error'].includes(config.dynamic))
|
|
107
|
+
if (config.dynamic === 'force-dynamic' || (config.dynamic !== 'force-static' && (config.revalidate === 0 || config.forceNoStore))) return;
|
|
108
|
+
if (dynamic && !inspected.generated && !partial && !['force-static', 'error'].includes(config.dynamic)) return;
|
|
107
109
|
}
|
|
108
110
|
route.ssg = true;
|
|
109
111
|
route.fallback = config.dynamicParams === false ? false : 'blocking';
|
|
110
112
|
for (const entry of entries) {
|
|
111
113
|
if (occupied.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
|
|
114
|
+
occupied.add(entry.path);
|
|
112
115
|
const rendered = await (handler ? handlers.prerenderAppRouteHandler : pages.prerenderAppRoute)({ ...options, path: entry.path, params: entry.params });
|
|
113
116
|
if (rendered.partial) {
|
|
114
117
|
const file = `static/partial-${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.json`;
|
|
@@ -155,6 +158,8 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
155
158
|
route.ssg = true; route.fallback = 'ppr';
|
|
156
159
|
} else { delete route.ssg; delete route.fallback; }
|
|
157
160
|
}
|
|
158
|
-
}
|
|
161
|
+
});
|
|
162
|
+
const appSeeds=manifest.prerendered.slice(initialSeeds.length);
|
|
163
|
+
manifest.prerendered=[...initialSeeds,...routes.flatMap(route=>appSeeds.filter(seed=>seed.routeId===route.id))];
|
|
159
164
|
} finally { await closeAppRuntime(); }
|
|
160
165
|
}
|
package/build/app.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
1
|
+
import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
2
2
|
import { moduleResolutionPlugin } from './module-resolution.mjs';
|
|
3
|
-
import { build as bundle, compilerSource } from './compiler.mjs';
|
|
3
|
+
import { build as bundle, compilerSource,readCompilerFile } from './compiler.mjs';
|
|
4
4
|
import { readFile, writeFile, mkdir, cp, realpath } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
@@ -14,7 +14,7 @@ import { createServerActions } from './actions.mjs';
|
|
|
14
14
|
import { expandActionExports } from './action-exports.mjs';
|
|
15
15
|
import { compileEdge } from './edge.mjs';
|
|
16
16
|
import { createNativePackages } from './native-packages.mjs';
|
|
17
|
-
import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
17
|
+
import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
18
18
|
import { npmPackagesPlugin } from './npm-packages.mjs';
|
|
19
19
|
import { transformCacheComponents } from './cache-components.mjs';
|
|
20
20
|
|
|
@@ -174,7 +174,8 @@ function routeEntry(route, globalError, dev) {
|
|
|
174
174
|
|
|
175
175
|
/** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
|
|
176
176
|
export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
|
|
177
|
-
const refresh = dev ? createRefreshTransform(project.root) : undefined;
|
|
177
|
+
const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
|
|
178
|
+
const locations = dev ? createSourceLocationTransform(project.root) : undefined;
|
|
178
179
|
const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
|
|
179
180
|
const publicPath = assetBase + '/';
|
|
180
181
|
const routes = project.routes.filter(route => route.router === 'app');
|
|
@@ -234,6 +235,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
234
235
|
return {
|
|
235
236
|
name: `prnext-app-${mode}`,
|
|
236
237
|
setup(esbuild) {
|
|
238
|
+
applicationEntry(esbuild,readCompilerFile);
|
|
237
239
|
esbuild.onResolve({ filter: /^prnext-internal:use-cache$/ }, () => browser
|
|
238
240
|
? { errors: [{ text: "'use cache' cannot enter a Client Component graph" }] }
|
|
239
241
|
: { path: '../compat/use-cache.cjs', external: true });
|
|
@@ -296,7 +298,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
296
298
|
}
|
|
297
299
|
esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
|
|
298
300
|
if (browser && args.path === project.document) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a Client Component.' }] };
|
|
299
|
-
const
|
|
301
|
+
const original=await readCompilerFile(args.path,args.prnextSource);
|
|
302
|
+
const source = await fonts.transform(await expandActionExports(locations?await locations(original,args.path):original, args.path, async (specifier, importer) => {
|
|
300
303
|
const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
301
304
|
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
302
305
|
}), args.path);
|
|
@@ -315,7 +318,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
315
318
|
return { contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n${references.join('\n')}`, loader: 'js', resolveDir: path.dirname(args.path) };
|
|
316
319
|
}
|
|
317
320
|
let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server' });
|
|
318
|
-
if (browser && refresh) contents = await refresh(contents, args.path);
|
|
321
|
+
if (browser && refresh && !actions.isServerModule(cachedSource,args.path)) contents = await refresh(contents, args.path);
|
|
319
322
|
return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
|
|
320
323
|
});
|
|
321
324
|
},
|
|
@@ -331,6 +334,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
331
334
|
const rsc = mode === 'rsc';
|
|
332
335
|
const framework = new Set(['link', 'image', 'head', 'router', 'compat-router', 'next-router-context', 'next-app-router-context', 'navigation', 'script', 'app-context', 'app-layout-context', 'app-navigation-boundary', 'app-client-page', 'app-error-boundary', 'app-dynamic']);
|
|
333
336
|
return { name: 'prnext-edge-app', setup(esbuild) {
|
|
337
|
+
applicationEntry(esbuild,readCompilerFile);
|
|
334
338
|
esbuild.onResolve({filter:/^prnext-internal:action-(?:crypto|ssr)$/}, args => ({path:args.path.slice('prnext-internal:'.length),namespace:'prnext-edge-action'}));
|
|
335
339
|
esbuild.onLoad({filter:/.*/,namespace:'prnext-edge-action'}, args => ({loader:'js',contents:(args.path==='action-crypto'?['encryptBoundArgs','decryptBoundArgs','bindEncryptedReference']:['createServerReference']).map(name=>`export const ${name}=globalThis.__PRNEXT_EDGE_ACTIONS[${JSON.stringify(args.path)}][${JSON.stringify(name)}];`).join('\n')}));
|
|
336
340
|
esbuild.onResolve({ filter: frameworkSubpathPattern }, args => {
|
|
@@ -375,7 +379,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
375
379
|
async function edgeAppBuild(file, outfile, mode, defaultExport = false) {
|
|
376
380
|
const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport,
|
|
377
381
|
plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
|
|
378
|
-
transformSource: async (source, file, builder) => actionTransform(await fonts.transform(await expandActionExports(source, file, async (specifier, importer) => {
|
|
382
|
+
transformSource: async (source, file, builder) => actionTransform(await fonts.transform(await expandActionExports(locations?await locations(source,file):source, file, async (specifier, importer) => {
|
|
379
383
|
const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
|
|
380
384
|
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
381
385
|
}), file), file, mode, 'edge') });
|
|
@@ -481,11 +485,16 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
481
485
|
}
|
|
482
486
|
manifest.app = { clientModules: {}, actions: {}, actionKey: actions.actionKey };
|
|
483
487
|
const actionRevision = () => [...actions.actions.values()].map(action=>action.id+':'+[...(action.runtimes || [])].sort().join(',')).join(';');
|
|
488
|
+
const boundaryRevision = () => [...boundaries.values()].map(boundary=>boundary.id+':'+!!boundary.edge).join(';');
|
|
489
|
+
const browserRevision = () => `${boundaryRevision()}:${cssFiles.size}:${globalErrorCssFiles.size}:${nativePackages.version}`;
|
|
484
490
|
// Route Handlers have a Node React environment and must not share RSC chunks
|
|
485
491
|
// that import registerServerReference or client-reference registration.
|
|
486
492
|
await serverBuild(handlerEntries, 'server', 'route');
|
|
487
493
|
for (;;) {
|
|
488
|
-
|
|
494
|
+
// Each graph consumes discoveries from the preceding one. Discovering a
|
|
495
|
+
// client boundary in RSC is normal: SSR and browser below already include
|
|
496
|
+
// it. Only discoveries made too late for a graph require another pass.
|
|
497
|
+
const emittedActions = actionRevision();
|
|
489
498
|
for (const action of actions.actions.values()) {
|
|
490
499
|
const entry = path.join(stage, '.entries', 'action-' + action.id + '.mjs');
|
|
491
500
|
await writeFile(entry, `export {${JSON.stringify(action.binding)} as invoke} from ${JSON.stringify(action.file)};`);
|
|
@@ -493,6 +502,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
493
502
|
if (!action.runtimes?.size || action.runtimes.has('nodejs')) entries['action-' + action.id] = entry;
|
|
494
503
|
}
|
|
495
504
|
await serverBuild(entries, 'server', 'rsc');
|
|
505
|
+
const serverNativeVersion = nativePackages.version;
|
|
506
|
+
const emittedBoundaries = boundaryRevision();
|
|
496
507
|
const ssrEntries = {};
|
|
497
508
|
const globalSsrEntries = {};
|
|
498
509
|
for (const boundary of boundaries.values()) {
|
|
@@ -512,6 +523,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
512
523
|
await collectBrowserStyles(browserEntries);
|
|
513
524
|
globalErrorOnlyCss.clear();
|
|
514
525
|
for (const file of globalErrorCssFiles) if (!cssFiles.has(file)) globalErrorOnlyCss.add(file);
|
|
526
|
+
const emittedBrowserInputs = browserRevision();
|
|
515
527
|
if (routes.some(route => route.kind === 'page')) {
|
|
516
528
|
let globalErrorCss = [];
|
|
517
529
|
if (globalErrorCssFiles.size) {
|
|
@@ -525,7 +537,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
525
537
|
if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
|
|
526
538
|
const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
|
|
527
539
|
const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
|
|
528
|
-
await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}${[...cssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n')}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, trailingSlash, skipTrailingSlashRedirect, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient } } : {}) })}});\n`);
|
|
540
|
+
await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}${[...cssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n')}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, trailingSlash, skipTrailingSlashRedirect, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
|
|
529
541
|
const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
|
|
530
542
|
const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'app-chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
|
|
531
543
|
define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser'), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: file => !globalErrorOnlyCss.has(file) })] });
|
|
@@ -548,7 +560,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
548
560
|
module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
|
|
549
561
|
...(action.runtimes?.has('edge') ? {edgeModule:`server/action-edge-${action.id}.mjs`} : {}),
|
|
550
562
|
}]));
|
|
551
|
-
if (
|
|
563
|
+
if (emittedActions === actionRevision() && emittedBoundaries === boundaryRevision() &&
|
|
564
|
+
serverNativeVersion === nativePackages.version && emittedBrowserInputs === browserRevision()) break;
|
|
552
565
|
}
|
|
553
566
|
|
|
554
567
|
}
|
package/build/build-lock.mjs
CHANGED
|
@@ -11,7 +11,7 @@ function alive(pid) {
|
|
|
11
11
|
}
|
|
12
12
|
async function owner(directory) {
|
|
13
13
|
const info = await lstat(directory);
|
|
14
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(
|
|
14
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
|
|
15
15
|
const file = await open(path.join(directory, 'owner.json'), 'r');
|
|
16
16
|
try {
|
|
17
17
|
if ((await file.stat()).size > 4096) throw new Error('oversized owner');
|
|
@@ -22,8 +22,8 @@ async function owner(directory) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** One publisher per canonical project, covering React and direct static imports. */
|
|
25
|
-
export async function withBuildLock(directory, action) {
|
|
26
|
-
const root = await realpath(directory), lock = path.join(root, LOCK);
|
|
25
|
+
export async function withBuildLock(directory, action, {development = false} = {}) {
|
|
26
|
+
const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
|
|
27
27
|
const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
|
|
28
28
|
const prepared = path.join(root, `.prnext-lock-${identity.token}`);
|
|
29
29
|
await mkdir(prepared, { mode: 0o700 });
|
|
@@ -37,16 +37,16 @@ export async function withBuildLock(directory, action) {
|
|
|
37
37
|
try { previous = await owner(lock); }
|
|
38
38
|
catch (cause) {
|
|
39
39
|
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
40
|
-
throw busy(
|
|
40
|
+
throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
|
|
41
41
|
}
|
|
42
|
-
if (previous.host !== identity.host || alive(previous.pid)) throw busy(`A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
42
|
+
if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
43
43
|
// Exactly one contender can recover this generation. Never delete a lock
|
|
44
44
|
// merely because it is old: long builds and suspended processes are valid.
|
|
45
45
|
let claim;
|
|
46
46
|
try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
|
|
47
47
|
catch (cause) {
|
|
48
48
|
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
49
|
-
throw busy(
|
|
49
|
+
throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
|
|
50
50
|
}
|
|
51
51
|
const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
|
|
52
52
|
try {
|
package/build/bundle-runtime.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { build } from 'esbuild';
|
|
2
|
-
import { lstat, realpath, readdir, writeFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import { lstat, realpath, readdir, writeFile, unlink,readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {version as esbuildVersion} from 'esbuild';
|
|
6
|
+
import {privateCacheDirectory,readCacheRecord,writeCacheRecord} from './persistent-cache.mjs';
|
|
4
7
|
|
|
5
8
|
// Paths used by the native worker, generated application modules, prerender,
|
|
6
9
|
// standalone tracing, compatibility modules and the runtime lifecycle contract.
|
|
@@ -12,7 +15,7 @@ export const runtimeEntryNames = Object.freeze([
|
|
|
12
15
|
].sort());
|
|
13
16
|
|
|
14
17
|
/** Run once, after compiling application/Edge graphs and before any prerender. */
|
|
15
|
-
export async function bundleRuntime(stage) {
|
|
18
|
+
export async function bundleRuntime(stage,{projectRoot}={}) {
|
|
16
19
|
const resolvedStage = path.resolve(stage);
|
|
17
20
|
if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
|
|
18
21
|
throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
|
|
@@ -34,11 +37,21 @@ export async function bundleRuntime(stage) {
|
|
|
34
37
|
for (const name of runtimeEntryNames) {
|
|
35
38
|
if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
|
|
36
39
|
}
|
|
40
|
+
let directory,key,cached;
|
|
41
|
+
if(projectRoot){
|
|
42
|
+
directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v1');
|
|
43
|
+
if(directory){
|
|
44
|
+
const hash=createHash('sha256').update(esbuildVersion).update(JSON.stringify(runtimeEntryNames)).update(await readFile(new URL('./bundle-runtime.mjs',import.meta.url)));
|
|
45
|
+
for(const entry of [...originals].sort((a,b)=>a.name.localeCompare(b.name)))hash.update(JSON.stringify([entry.name,createHash('sha256').update(await readFile(path.join(runtime,entry.name))).digest('hex')]));
|
|
46
|
+
key=hash.digest('hex');cached=await readCacheRecord(directory,key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
// write:false makes compilation atomic with respect to the source runtime:
|
|
38
50
|
// no files change if parsing or resolution fails. The virtual output is a
|
|
39
51
|
// sibling so all relative paths have the same depth as the final runtime.
|
|
40
52
|
const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
|
|
41
|
-
const
|
|
53
|
+
const validCache=cached&&Array.isArray(cached.files)&&cached.files.length<=128&&cached.files.every(item=>Array.isArray(item)&&item.length===2&&/^[\w-]+\.mjs$/.test(item[0])&&typeof item[1]==='string')&&new Set(cached.files.map(([name])=>name)).size===cached.files.length&&runtimeEntryNames.every(name=>cached.files.some(([file])=>file===name+'.mjs'));
|
|
54
|
+
const result = validCache?null:await build({
|
|
42
55
|
absWorkingDir: runtime,
|
|
43
56
|
entryPoints: runtimeEntryNames.map(name => path.join(runtime, `${name}.mjs`)),
|
|
44
57
|
outdir: virtualOutput,
|
|
@@ -62,13 +75,15 @@ export async function bundleRuntime(stage) {
|
|
|
62
75
|
logLevel: 'silent',
|
|
63
76
|
});
|
|
64
77
|
const emitted = new Map();
|
|
65
|
-
for
|
|
78
|
+
if(validCache)for(const [name,contents]of cached.files)emitted.set(name,Buffer.from(contents));
|
|
79
|
+
for (const file of result?.outputFiles||[]) {
|
|
66
80
|
if (path.dirname(file.path) !== virtualOutput || !file.path.endsWith('.mjs')) {
|
|
67
81
|
throw new Error('Runtime bundler emitted a file outside its flat output directory.');
|
|
68
82
|
}
|
|
69
83
|
emitted.set(path.basename(file.path), file.contents);
|
|
70
84
|
}
|
|
71
85
|
if (runtimeEntryNames.some(name => !emitted.has(`${name}.mjs`))) throw new Error('Runtime bundle omitted a required entry.');
|
|
86
|
+
if(directory&&!validCache)await writeCacheRecord(directory,key,{files:[...emitted].map(([name,bytes])=>[name,Buffer.from(bytes).toString()])});
|
|
72
87
|
// This is an unpublished, disposable stage. On any write failure buildProject
|
|
73
88
|
// discards the stage and preserves the previous published build unchanged.
|
|
74
89
|
for (const [name, contents] of emitted) await writeFile(path.join(runtime, name), contents);
|
|
@@ -77,6 +92,6 @@ export async function bundleRuntime(stage) {
|
|
|
77
92
|
}
|
|
78
93
|
// JSON, CJS, license files and the OG assets directory are never traversed or
|
|
79
94
|
// removed. The caller writes the normal runtime contract after prerender.
|
|
80
|
-
return { entryCount: runtimeEntryNames.length, moduleCount: emitted.size,
|
|
95
|
+
return { entryCount: runtimeEntryNames.length, moduleCount: emitted.size,cached:!!validCache,
|
|
81
96
|
bytes: [...emitted.values()].reduce((sum, contents) => sum + contents.length, 0) };
|
|
82
97
|
}
|