@thomas.f/prnext 0.1.18-alpha → 0.1.20-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/build/actions.mjs +26 -14
- package/build/app.mjs +90 -101
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +8 -7
- package/build/config.mjs +25 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +14 -12
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dynamic.mjs +13 -6
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +61 -15
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-ppr.mjs +32 -0
- package/build/npm-packages.mjs +1 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-builder.mjs +1 -1
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/standalone.mjs +21 -3
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +24 -6
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +7 -3
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +50 -13
- package/compat/index.d.ts +24 -1
- package/compat/link.cjs +2 -2
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +187 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +26 -11
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +254 -53
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +13 -5
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +86 -26
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +59 -9
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
package/build/incremental.mjs
CHANGED
|
@@ -1,14 +1,76 @@
|
|
|
1
1
|
import { context } from 'esbuild';
|
|
2
2
|
import { readFile, writeFile, mkdir, realpath } from 'node:fs/promises';
|
|
3
|
+
import { realpath as legacyRealpath } from 'node:fs';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import {clearDirectoryCache} from './directory-cache.mjs';
|
|
6
7
|
import {closeWebpackCompiler,webpackCompilerStats} from './webpack-graph.mjs';
|
|
7
8
|
const contexts = new Map();
|
|
8
9
|
const LIMIT = 8;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const AUXILIARY_LIMIT = 2;
|
|
11
|
+
const RESERVES = Object.freeze({ main: 6, styles: 2 });
|
|
12
|
+
const counters = () => ({ created: 0, reused: 0, evicted: 0 });
|
|
13
|
+
const roles = () => ({ main: counters(), styles: counters(), auxiliary: counters() });
|
|
14
|
+
const totals = counters(), roleTotals = roles();
|
|
15
|
+
|
|
16
|
+
function record(metrics, role, event) {
|
|
17
|
+
totals[event]++;
|
|
18
|
+
roleTotals[role][event]++;
|
|
19
|
+
metrics[event]++;
|
|
20
|
+
metrics.roles[role][event]++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function roleStats(metrics, active) {
|
|
24
|
+
return Object.fromEntries(Object.entries(metrics).map(([role, values]) => [role, {
|
|
25
|
+
...values,
|
|
26
|
+
contexts: active.filter(slot => slot.role === role).length,
|
|
27
|
+
}]));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function incrementalCompilerStats() {
|
|
31
|
+
const active = [...contexts.values()];
|
|
32
|
+
// Counters belong to retained projects, rather than an unbounded registry of
|
|
33
|
+
// every project ever compiled by this process. Global role totals persist.
|
|
34
|
+
const projects = [...new Set(active.map(slot => slot.metrics))].map(metrics => {
|
|
35
|
+
const retained = active.filter(slot => slot.metrics === metrics);
|
|
36
|
+
return {
|
|
37
|
+
root: metrics.root,
|
|
38
|
+
created: metrics.created,
|
|
39
|
+
reused: metrics.reused,
|
|
40
|
+
evicted: metrics.evicted,
|
|
41
|
+
contexts: retained.length,
|
|
42
|
+
roles: roleStats(metrics.roles, retained),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
contexts: contexts.size,
|
|
47
|
+
...totals,
|
|
48
|
+
roles: roleStats(roleTotals, active),
|
|
49
|
+
projects,
|
|
50
|
+
limits: { project: LIMIT, auxiliary: AUXILIARY_LIMIT, reserves: { ...RESERVES } },
|
|
51
|
+
webpack: webpackCompilerStats(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function evictionCandidate(group, role) {
|
|
56
|
+
const candidates = [...contexts].filter(([, slot]) => slot.group === group);
|
|
57
|
+
if (group === 'auxiliary') return candidates[0];
|
|
58
|
+
const count = candidateRole => candidates.filter(([, slot]) => slot.role === candidateRole).length;
|
|
59
|
+
// Both roles can borrow unused capacity. Once full, a role at its reserve
|
|
60
|
+
// replaces its own oldest graph; a role below its reserve reclaims a loan.
|
|
61
|
+
if (count(role) >= RESERVES[role]) return candidates.find(([, slot]) => slot.role === role);
|
|
62
|
+
return candidates.find(([, slot]) => count(slot.role) > RESERVES[slot.role]);
|
|
63
|
+
}
|
|
11
64
|
export async function closeIncrementalCompiler(root) {
|
|
65
|
+
if (root !== undefined) {
|
|
66
|
+
try { root = await realpath(root); }
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Disposal also runs after users remove a project directory. A supplied
|
|
69
|
+
// canonical path must still match its retained contexts in that case.
|
|
70
|
+
if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
|
|
71
|
+
root = path.resolve(root);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
12
74
|
clearDirectoryCache();
|
|
13
75
|
const active=[];for(const [key,slot] of contexts)if(root===undefined||slot.root===root){contexts.delete(key);active.push(slot);}
|
|
14
76
|
const results=await Promise.allSettled([...active.map(slot=>slot.context.dispose()),closeWebpackCompiler(root)]);
|
|
@@ -16,18 +78,42 @@ export async function closeIncrementalCompiler(root) {
|
|
|
16
78
|
if(errors.length)throw new AggregateError(errors,'Unable to dispose incremental compiler sessions');
|
|
17
79
|
}
|
|
18
80
|
/** Retain esbuild's parsed module graphs while rebinding per-build framework plugins. */
|
|
19
|
-
export async function incrementalBuild(options, {root,stage,group='project',run=callback=>callback()}) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const canonical = value => typeof value === 'string' ? value.replaceAll(root, canonicalRoot) : Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' && !(value instanceof RegExp) && !ArrayBuffer.isView(value) ? Object.fromEntries(Object.entries(value).map(([key,item]) => [key,canonical(item)])) : value;
|
|
23
|
-
return incrementalBuild(canonical(options),{root:canonicalRoot,stage:stage.replace(root,canonicalRoot),group,run});
|
|
81
|
+
export async function incrementalBuild(options, {root,stage,group='project',role=group==='auxiliary'?'auxiliary':'main',run=callback=>callback()}) {
|
|
82
|
+
if (!['project', 'auxiliary'].includes(group) || (group === 'project' ? !Object.hasOwn(RESERVES, role) : role !== 'auxiliary')) {
|
|
83
|
+
throw new TypeError('Invalid incremental compiler group or role');
|
|
24
84
|
}
|
|
85
|
+
// Canonical identity is for ownership and disposal only. Compilation paths
|
|
86
|
+
// and application strings retain the caller's lexical spelling.
|
|
87
|
+
const ownerRoot = await realpath(root);
|
|
25
88
|
const virtual=path.join(root,'.prnext-incremental-stage');
|
|
89
|
+
const rootPaths=[root,ownerRoot];
|
|
90
|
+
if(process.platform==='win32'&&root!==ownerRoot){
|
|
91
|
+
// Windows resolvers can preserve a short ancestor while resolving the
|
|
92
|
+
// junction target. The JS realpath walk retains that third spelling.
|
|
93
|
+
rootPaths.push(await new Promise((resolve,reject)=>legacyRealpath(root,(error,value)=>error?reject(error):resolve(value))));
|
|
94
|
+
}
|
|
95
|
+
const virtualPaths=[...new Set(rootPaths.map(directory=>path.join(directory,'.prnext-incremental-stage')))];
|
|
96
|
+
const stageRelative=path.relative(root,stage);
|
|
97
|
+
const stagePaths=[...new Set([stage,...rootPaths.map(directory=>path.resolve(directory,stageRelative))])];
|
|
98
|
+
const relativePath=(directory,value)=>{
|
|
99
|
+
if(typeof value!=='string'||!path.isAbsolute(value))return;
|
|
100
|
+
const relative=path.relative(directory,value);
|
|
101
|
+
if(relative===''||!path.isAbsolute(relative)&&relative!=='..'&&!relative.startsWith('..'+path.sep))return relative;
|
|
102
|
+
};
|
|
103
|
+
const withinPaths=(directories,value)=>directories.some(directory=>relativePath(directory,value)!==undefined);
|
|
104
|
+
const remapPath=(value,directories,target)=>{
|
|
105
|
+
for(const directory of directories){const relative=relativePath(directory,value);if(relative!==undefined)return path.join(target,relative);}
|
|
106
|
+
return value;
|
|
107
|
+
};
|
|
26
108
|
const toVirtual=value=>typeof value==='string'?value.replaceAll(stage,virtual):value;
|
|
27
|
-
|
|
109
|
+
// esbuild can canonicalize only the parent of a nonexistent virtual file.
|
|
110
|
+
// Recognize both staging identities on path fields, never application text.
|
|
111
|
+
const fromVirtual=value=>remapPath(value,[...virtualPaths,...stagePaths],stage);
|
|
112
|
+
const toVirtualPath=value=>remapPath(value,stagePaths,virtual);
|
|
113
|
+
const isVirtual=value=>withinPaths(virtualPaths,value),isStaged=value=>withinPaths(stagePaths,value);
|
|
28
114
|
const deep=(value,map)=>typeof value==='string'?map(value):Array.isArray(value)?value.map(item=>deep(item,map)):value&&typeof value==='object'&&!(value instanceof RegExp)&&!ArrayBuffer.isView(value)?Object.fromEntries(Object.entries(value).map(([key,item])=>[map(key),deep(item,map)])):value;
|
|
29
115
|
const realResult=result=>result && {...result,...(result.path?{path:fromVirtual(result.path)}:{}),...(result.resolveDir?{resolveDir:fromVirtual(result.resolveDir)}:{})};
|
|
30
|
-
const virtualResult=result=>result && {...result,...(result.path?{path:
|
|
116
|
+
const virtualResult=result=>result && {...result,...(result.path?{path:toVirtualPath(result.path)}:{}),...(result.resolveDir?{resolveDir:toVirtualPath(result.resolveDir)}:{}),...(result.watchFiles?{watchFiles:result.watchFiles.map(toVirtualPath)}:{}),...(result.watchDirs?{watchDirs:result.watchDirs.map(toVirtualPath)}:{})};
|
|
31
117
|
const hooks=[];
|
|
32
118
|
const initialOptions={...options,plugins:undefined,write:false};
|
|
33
119
|
let slot;
|
|
@@ -39,18 +125,24 @@ export async function incrementalBuild(options, {root,stage,group='project',run=
|
|
|
39
125
|
});
|
|
40
126
|
const normalized=deep(initialOptions,toVirtual);
|
|
41
127
|
const signature=JSON.stringify([normalized,hooks.map(({kind,settings})=>[kind,settings?.namespace,settings?.filter?.source,settings?.filter?.flags]),(options.plugins||[]).map(plugin=>plugin.name)]);
|
|
42
|
-
const key=group
|
|
128
|
+
const key=JSON.stringify([ownerRoot,root,group,role,createHash('sha256').update(signature).digest('hex')]);
|
|
43
129
|
slot=contexts.get(key);
|
|
44
|
-
if(slot){contexts.delete(key);reused
|
|
130
|
+
if(slot){contexts.delete(key);record(slot.metrics,role,'reused');}
|
|
45
131
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
132
|
+
const metrics = [...contexts.values()].find(slot => slot.root === ownerRoot)?.metrics || { root: ownerRoot, ...counters(), roles: roles() };
|
|
133
|
+
while ([...contexts.values()].filter(slot => slot.group === group).length >= (group === 'auxiliary' ? AUXILIARY_LIMIT : LIMIT)) {
|
|
134
|
+
const [oldest, previous] = evictionCandidate(group, role);
|
|
135
|
+
contexts.delete(oldest);
|
|
136
|
+
record(previous.metrics, previous.role, 'evicted');
|
|
137
|
+
await previous.context.dispose();
|
|
138
|
+
}
|
|
139
|
+
slot={group,role,root:ownerRoot,metrics};
|
|
140
|
+
slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage,isVirtual,isStaged};
|
|
49
141
|
slot.context=await context({...normalized,plugins:[{name:'prnext-incremental-delegates',setup(builder){
|
|
50
142
|
slot.builder=builder;
|
|
51
143
|
builder.onResolve({filter:/.*/},async args=>{
|
|
52
144
|
const active=slot.current;
|
|
53
|
-
if(args.pluginData?.prnextVirtualResolution || !(args.path
|
|
145
|
+
if(args.pluginData?.prnextVirtualResolution || !(active.isVirtual(args.path)||active.isStaged(args.path)||active.isVirtual(args.resolveDir)||active.isStaged(args.resolveDir)))return;
|
|
54
146
|
const result=await builder.resolve(active.fromVirtual(args.path),{resolveDir:active.fromVirtual(args.resolveDir),importer:active.fromVirtual(args.importer),kind:args.kind,with:args.with,pluginData:{...args.pluginData,prnextVirtualResolution:true}});
|
|
55
147
|
const pluginData={...(result.pluginData || args.pluginData)}; delete pluginData.prnextVirtualResolution;
|
|
56
148
|
return {...active.virtualResult(result),pluginData};
|
|
@@ -63,13 +155,15 @@ export async function incrementalBuild(options, {root,stage,group='project',run=
|
|
|
63
155
|
if(settings)builder[kind](settings,callback);else builder[kind](callback);
|
|
64
156
|
});
|
|
65
157
|
builder.onLoad({filter:/.*/,namespace:'file'},async args=>{
|
|
66
|
-
|
|
67
|
-
|
|
158
|
+
const active=slot.current;
|
|
159
|
+
if(!active.isVirtual(args.path))return;
|
|
160
|
+
const actual=active.fromVirtual(args.path),extension=path.extname(actual);
|
|
68
161
|
return {contents:await readFile(actual),loader:options.loader?.[extension] || ({'.json':'json','.ts':'ts','.tsx':'tsx','.jsx':'jsx','.css':'css'}[extension] || 'js'),resolveDir:path.dirname(args.path)};
|
|
69
162
|
});
|
|
70
163
|
}}]});
|
|
164
|
+
record(metrics,role,'created');
|
|
71
165
|
}
|
|
72
|
-
slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage};
|
|
166
|
+
slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage,isVirtual,isStaged};
|
|
73
167
|
contexts.set(key,slot);
|
|
74
168
|
const result=await slot.context.rebuild();
|
|
75
169
|
const relativeVirtual=path.basename(virtual),relativeStage=path.basename(stage);
|
package/build/index.mjs
CHANGED
|
@@ -6,13 +6,17 @@ import { writeRuntimeContract } from './runtime-contract.mjs';
|
|
|
6
6
|
import { writeBuildOutput } from './output-contract.mjs';
|
|
7
7
|
import { bundleRuntime } from './bundle-runtime.mjs';
|
|
8
8
|
import {withPersistentTransforms} from './transform-cache.mjs';
|
|
9
|
+
import {withCompilationStorage,configureCompilationStorage} from './compilation-storage.mjs';
|
|
10
|
+
import {reclaimBuildStages,trackBuildStage} from './build-retention.mjs';
|
|
9
11
|
import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
|
|
10
12
|
import { expandLocales, localizedStaticPaths } from './i18n.mjs';
|
|
11
13
|
import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile,compilationGraphCacheReport } from './compiler.mjs';
|
|
12
14
|
import {retainDeploymentAssets} from './deployment-assets.mjs';
|
|
13
15
|
import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
|
|
14
16
|
import {writeBuildAnalysis} from './analyze.mjs';
|
|
17
|
+
import {prepareNativePprShells} from './native-ppr.mjs';
|
|
15
18
|
import {routeFiles} from './dev-discovery.mjs';
|
|
19
|
+
import {tryBuildLazyGraph,rememberLazyBuildContext,forgetLazyBuildContext} from './dev-lazy-build.mjs';
|
|
16
20
|
import {writeTypedRoutes} from './typed-routes.mjs';
|
|
17
21
|
import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir, realpath } from 'node:fs/promises';
|
|
18
22
|
import path from 'node:path';
|
|
@@ -36,7 +40,7 @@ import { compileCustomRoutes } from './custom-routes.mjs';
|
|
|
36
40
|
import { compileMiddleware } from './middleware.mjs';
|
|
37
41
|
import { prepareScriptWorkers } from './script-workers.mjs';
|
|
38
42
|
import { prepareMetadataRoutes } from './metadata-routes.mjs';
|
|
39
|
-
import { prepareImageResponse } from './og.mjs';
|
|
43
|
+
import { prepareImageResponse, imageResponseRequirement } from './og.mjs';
|
|
40
44
|
import { prepareCacheHandler, prepareCacheHandlers } from './cache-handlers.mjs';
|
|
41
45
|
import { npmPackagesPlugin } from './npm-packages.mjs';
|
|
42
46
|
import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
@@ -73,7 +77,7 @@ async function copyRuntimeFiles(stage,{development=true}={}) {
|
|
|
73
77
|
if (failed) throw failed.reason;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler }) {
|
|
80
|
+
function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler, nativeCompiler = false }) {
|
|
77
81
|
return {
|
|
78
82
|
name: 'prnext-pages-compatibility',
|
|
79
83
|
setup(esbuild) {
|
|
@@ -107,8 +111,13 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, se
|
|
|
107
111
|
// loaders. Keep that entire graph original: an uppercase JSX utility
|
|
108
112
|
// may be called normally there, outside a React render. Browser render
|
|
109
113
|
// graphs receive memoization before instrumentation and JSX lowering.
|
|
110
|
-
const
|
|
111
|
-
|
|
114
|
+
const compile = browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path);
|
|
115
|
+
// Oxc can hoist static JSX without a source-map position. Attach the
|
|
116
|
+
// same original inspector attributes as SSR before that transformation.
|
|
117
|
+
const locateFirst = !!(compile && nativeCompiler && locations);
|
|
118
|
+
const input = locateFirst ? await locations(original,args.path) : original;
|
|
119
|
+
const compiled = compile ? await reactCompiler(input,args.path,{loaderOutput:args.prnextSource!==undefined}) : input;
|
|
120
|
+
const source = await fonts.transform(locations && !locateFirst ? await locations(compiled,args.path) : compiled, args.path);
|
|
112
121
|
assertSupportedSource(source, args.path);
|
|
113
122
|
const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
|
|
114
123
|
let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
|
|
@@ -154,7 +163,12 @@ let buildQueue = Promise.resolve();
|
|
|
154
163
|
let developmentModulesLoaded = false;
|
|
155
164
|
/** Disposing shared compiler contexts must not interrupt another queued build. */
|
|
156
165
|
export function disposeBuildSession(root){
|
|
157
|
-
const task=buildQueue.then(async()=>{
|
|
166
|
+
const task=buildQueue.then(async()=>{
|
|
167
|
+
const canonicalRoot=root===undefined?undefined:await realpath(root).catch(error=>{if(error.code==='ENOENT'||error.code==='ENOTDIR')return path.resolve(root);throw error;});
|
|
168
|
+
forgetLazyBuildContext(canonicalRoot);
|
|
169
|
+
const {closeIncrementalCompiler}=await import('./incremental.mjs');
|
|
170
|
+
await closeIncrementalCompiler(canonicalRoot);
|
|
171
|
+
});
|
|
158
172
|
buildQueue=task.catch(()=>{});return task;
|
|
159
173
|
}
|
|
160
174
|
/** Isolate process.env between programmatic builds, including simultaneous calls. */
|
|
@@ -173,8 +187,9 @@ export function build(projectRoot, options = {}) {
|
|
|
173
187
|
const envMode = options.envMode || (process.env.NODE_ENV === 'test' ? 'test' : options.dev ? 'development' : 'production');
|
|
174
188
|
process.env.NODE_ENV = options.dev ? 'development' : 'production';
|
|
175
189
|
try { return await withBuildLock(projectRoot, async root => {
|
|
190
|
+
await reclaimBuildStages(root);
|
|
176
191
|
const started=performance.now();
|
|
177
|
-
const result=await withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode })));
|
|
192
|
+
const result=await withCompilationStorage(root,()=>withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode }))));
|
|
178
193
|
return {...result,durationMs:Math.round(performance.now()-started)};
|
|
179
194
|
}); }
|
|
180
195
|
finally { restore(); }
|
|
@@ -184,12 +199,18 @@ export function build(projectRoot, options = {}) {
|
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
/** Compile Pages Router and App Router projects. A failed build preserves .prnext. */
|
|
187
|
-
async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs, devRoutes, devSequence, devSession, devTools,devForceRefresh=false,devAffectedModules } = {}) {
|
|
202
|
+
async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs, devRoutes, devSequence, devSession, devActionSecret, devTools,devForceRefresh=false,devAffectedModules,devLazyRequest,devLazyBase,devLazyActivated=[] } = {}) {
|
|
188
203
|
const started = performance.now();
|
|
204
|
+
if (dev && devLazyRequest && devLazyBase) {
|
|
205
|
+
const deferred = await tryBuildLazyGraph(projectRoot,{base:devLazyBase,request:devLazyRequest,
|
|
206
|
+
devInputs,devSession,devActionSecret,incremental});
|
|
207
|
+
if (deferred) return deferred;
|
|
208
|
+
}
|
|
189
209
|
const config = await loadProjectConfig(projectRoot, { dev, envMode, devInputs });
|
|
210
|
+
configureCompilationStorage(config.compilationCache);
|
|
190
211
|
const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
|
|
191
212
|
const environment = defineEnvironment(config);
|
|
192
|
-
const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
|
|
213
|
+
const publicPaths = { server: config.server, trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
|
|
193
214
|
const customRoutes = await compileCustomRoutes(config);
|
|
194
215
|
const buildId = await generateBuildId(config, { dev });
|
|
195
216
|
const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
|
|
@@ -201,17 +222,24 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
201
222
|
deferredRoutes=discovery.routes.filter(route=>!devRoutes.includes(route.pattern)&&!route.internal&&!route.errorStatus);
|
|
202
223
|
project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
|
|
203
224
|
} else project=await scanProject(projectRoot,scanOptions);
|
|
225
|
+
if(dev&&config.experimental.lazyDynamicImports){
|
|
226
|
+
if(project.routes.some(route=>route.router!=='app'&&!route.internal))throw Error('experimental.lazyDynamicImports currently requires an App Router-only application.');
|
|
227
|
+
if(config.webpack||Object.keys(config.turbopack?.rules||{}).length)throw Error('experimental.lazyDynamicImports currently requires the native compiler without webpack hooks or custom loaders.');
|
|
228
|
+
if(/^https?:\/\//.test(config.assetPrefix))throw Error('experimental.lazyDynamicImports requires same-origin development assets.');
|
|
229
|
+
}
|
|
204
230
|
if(!dev&&config.htmlOnly.length)selectHtmlOnlyRoutes(project.routes,config.htmlOnly,config);
|
|
205
231
|
if(!dev || !Array.isArray(devRoutes))await writeTypedRoutes(projectRoot,project,config);
|
|
206
232
|
const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
|
|
207
233
|
const destination = await prepareBuildDirectory(project.root, config.distDir);
|
|
208
234
|
const backup = path.join(project.root, `.prnext-backup-${randomUUID()}`);
|
|
235
|
+
const recovery=await trackBuildStage(project.root,{stage,backup}).catch(()=>null);
|
|
236
|
+
try {
|
|
209
237
|
await mkdir(path.join(stage, '.entries'), { recursive: true });
|
|
210
238
|
await mkdir(path.join(stage, 'static'), { recursive: true });
|
|
211
239
|
await addBuiltinPagesErrors(project, stage);
|
|
212
240
|
const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
|
|
213
241
|
...(dev && Array.isArray(devRoutes) ? {devOnDemand:true} : {}), ...(dev && devSession ? {devSequence,devSession} : {}), ...(dev ? {devForceRefresh,devAffectedModules,devIndicators:config.devIndicators,...(devTools?{devTools}:{})} : {}),
|
|
214
|
-
config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
|
|
242
|
+
config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, streamingRequestBodies: config.experimental.streamingRequestBodies, nativePprShell: config.experimental.nativePprShell, ...publicPaths }, customRoutes, routes: [], prerendered: [],
|
|
215
243
|
...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
|
|
216
244
|
// Its build-unique name is known before bundling and avoids a hash cycle
|
|
217
245
|
// between page entry URLs and the manifest that lists those entries.
|
|
@@ -234,7 +262,6 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
234
262
|
// Runtime copies must work even when dotenv is nested beneath the PRNext
|
|
235
263
|
// package instead of hoisted beside the application's dependencies.
|
|
236
264
|
await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
|
|
237
|
-
await prepareImageResponse(stage);
|
|
238
265
|
manifest.config.serverActions = config.experimental.serverActions;
|
|
239
266
|
if(config.workerMemory && !dev)manifest.config.workerMemory=config.workerMemory;
|
|
240
267
|
if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
|
|
@@ -279,7 +306,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
279
306
|
}
|
|
280
307
|
}
|
|
281
308
|
if (Object.keys(clientEntries).length) {
|
|
282
|
-
const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler }), cssModules.plugin(true)] });
|
|
309
|
+
const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler, nativeCompiler: config.reactCompiler?.backend === 'oxc' }), cssModules.plugin(true)] });
|
|
283
310
|
fonts.record(browser.metafile);
|
|
284
311
|
const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
|
|
285
312
|
for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
|
|
@@ -296,7 +323,18 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
296
323
|
await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
|
|
297
324
|
if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
|
|
298
325
|
await prepareMetadataRoutes(project, stage);
|
|
299
|
-
await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
|
|
326
|
+
await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, devActionSecret:dev?devActionSecret:undefined, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages, lazyDynamicImports:dev&&config.experimental.lazyDynamicImports,devLazyRequest,devLazyActivated, parallelBuilds: !dev && config.experimental.parallelBuilds && await cssModules.parallelSafe() ? config.experimental.parallelBuilds : false });
|
|
327
|
+
if(manifest.devLazy){
|
|
328
|
+
const css=Object.values(manifest.devLazy.outputs).flatMap(module=>module.css);
|
|
329
|
+
for(const route of manifest.routes)if(route.router==='app'&&route.kind==='page'){
|
|
330
|
+
route.css=[...new Set([...route.css,...css])];
|
|
331
|
+
if(css.length&&Array.isArray(route.cssScopes)){
|
|
332
|
+
const root=route.cssScopes.find(scope=>scope.kind==='layout'&&!scope.path);
|
|
333
|
+
if(root)root.css=[...new Set([...root.css,...css])];
|
|
334
|
+
else route.cssScopes.push({kind:'layout',path:'',css:[...new Set(css)]});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
300
338
|
if(manifest.config.workerPreload)for(const pattern of config.workerPreload.routes){
|
|
301
339
|
const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
|
|
302
340
|
if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
|
|
@@ -311,14 +349,18 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
311
349
|
await checkTypeScript(project.root, config, { dev, tsconfig });
|
|
312
350
|
fonts.attach(manifest);
|
|
313
351
|
expandLocales(manifest);
|
|
352
|
+
const imageResponse = await imageResponseRequirement(stage, {dev, manifest});
|
|
353
|
+
if (imageResponse.included) await prepareImageResponse(stage);
|
|
354
|
+
else await rm(path.join(stage, 'compat/og.cjs'), {force: true});
|
|
314
355
|
// All runtime preparation must finish before its shared modules are built;
|
|
315
356
|
// no staged renderer has been imported before this production-only point.
|
|
316
|
-
if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime')});
|
|
357
|
+
if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime'),imageResponse:imageResponse.included});
|
|
317
358
|
// Node caches CJS React by filename, not NODE_ENV. Once development metadata
|
|
318
359
|
// imports load it, subsequent production renders need a clean process too.
|
|
319
360
|
if(dev) developmentModulesLoaded = true;
|
|
320
361
|
if(!dev) Object.assign(manifest,await (incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender}));
|
|
321
362
|
else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
|
|
363
|
+
if (!dev) await prepareNativePprShells(stage, manifest);
|
|
322
364
|
const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal && !route.devDeferred);
|
|
323
365
|
const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
|
|
324
366
|
if (pages.length || manifest.pagesErrors) {
|
|
@@ -353,6 +395,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
353
395
|
}
|
|
354
396
|
const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
|
|
355
397
|
await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
|
|
398
|
+
...(manifest.devLazy?{lazy:Object.values(manifest.devLazy.outputs)}:{}),
|
|
356
399
|
pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
|
|
357
400
|
...(appRoute ? { app: { client: appRoute.client, css: appRoute.css, routes: manifest.routes.filter(route=>route.router==='app'&&route.kind==='page'&&route.client).map(({id,pattern,css,cssScopes})=>({id,pattern,css,cssScopes:cssScopes||[]})) } } : {}),
|
|
358
401
|
}));
|
|
@@ -392,11 +435,14 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
392
435
|
throw error;
|
|
393
436
|
} finally { await rm(pointerTemporary, {force:true}); }
|
|
394
437
|
await exported?.commit();
|
|
395
|
-
|
|
438
|
+
await recovery?.committed().catch(()=>{});
|
|
439
|
+
if(!recovery&&hadDestination)await rm(backup,{recursive:true,force:true});
|
|
440
|
+
if(dev&&!devLazyRequest)rememberLazyBuildContext(project.root,{manifest,config,environment,tsconfig,envMode});
|
|
396
441
|
return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
|
|
397
442
|
} catch (error) {
|
|
398
|
-
await rm(stage, { recursive: true, force: true });
|
|
443
|
+
if(!recovery)await rm(stage, { recursive: true, force: true });
|
|
399
444
|
if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
|
|
400
445
|
throw error;
|
|
401
446
|
}
|
|
447
|
+
}finally{await recovery?.cleanup();}
|
|
402
448
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {readFile,writeFile} from 'node:fs/promises';
|
|
5
|
+
import {parse} from '@babel/parser';
|
|
6
|
+
import traverseModule from '@babel/traverse';
|
|
7
|
+
import generateModule from '@babel/generator';
|
|
8
|
+
import * as t from '@babel/types';
|
|
9
|
+
import {generateMapped} from './source-maps.mjs';
|
|
10
|
+
|
|
11
|
+
const traverse=traverseModule.default||traverseModule;
|
|
12
|
+
const generate=generateModule.default||generateModule;
|
|
13
|
+
const packageRoot=fileURLToPath(new URL('../',import.meta.url));
|
|
14
|
+
const client=fileURLToPath(new URL('../runtime/lazy-import-client.mjs',import.meta.url));
|
|
15
|
+
export const LAZY_IMPORT_LIMIT=128;
|
|
16
|
+
export const LAZY_BRIDGE_LIMIT=4096;
|
|
17
|
+
const javascript=/\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
18
|
+
// esbuild exposes JSON as an ESM entry with a shared default object and named
|
|
19
|
+
// exports. Give it the same native URL identity as JavaScript export bridges.
|
|
20
|
+
const bridgeable=file=>javascript.test(file)||file.endsWith('.json');
|
|
21
|
+
const digest=value=>createHash('sha256').update(value).digest('hex').slice(0,24);
|
|
22
|
+
const suffixError=()=>({errors:[{text:'lazyDynamicImports cannot safely defer a module with a query or fragment suffix. Disable experimental.lazyDynamicImports to compile this graph normally.'}]});
|
|
23
|
+
|
|
24
|
+
/** Development-only graph boundaries. No source path is sent to the browser. */
|
|
25
|
+
export function createLazyDynamicImports({root,stage,manifest,request,activated=[]}){
|
|
26
|
+
const modules=new Map(),bridges=new Map(),shared=new Set(),existingShared=new Set(),entries=new Map(),targetFiles=new Map(),nativeOutputs=new Map(),outputs={};
|
|
27
|
+
const active=new Set(activated);
|
|
28
|
+
if(active.size>LAZY_IMPORT_LIMIT)throw Error(`lazyDynamicImports supports at most ${LAZY_IMPORT_LIMIT} active imports`);
|
|
29
|
+
if(request){
|
|
30
|
+
if(!/^[a-f0-9]{24}$/.test(request.id)||!Array.isArray(request.modules)||request.modules.length>LAZY_IMPORT_LIMIT||!Array.isArray(request.bridges)||request.bridges.length>LAZY_BRIDGE_LIMIT||!Array.isArray(request.shared)||request.shared.length>LAZY_BRIDGE_LIMIT)throw Error('Invalid deferred import compiler descriptor');
|
|
31
|
+
for(const item of request.modules){
|
|
32
|
+
const file=path.resolve(root,item.importer);
|
|
33
|
+
if(!file.startsWith(root+path.sep)||file.includes(`${path.sep}node_modules${path.sep}`)||!javascript.test(file)||typeof item.request!=='string'||item.request.length>4096||item.id!==digest(item.importer+'\0'+item.request))throw Error('Invalid deferred import registration');
|
|
34
|
+
modules.set(item.id,item);
|
|
35
|
+
}
|
|
36
|
+
if(!modules.has(request.id))throw Error('Unknown deferred import identifier');
|
|
37
|
+
for(const item of request.bridges){
|
|
38
|
+
if(typeof item.file!=='string'||typeof item.url!=='string'||!item.url.startsWith(manifest.config.assetBase+'/')||/[\r\n\0]/.test(item.url))throw Error('Invalid deferred module bridge');
|
|
39
|
+
bridges.set(path.resolve(root,item.file),item.url);
|
|
40
|
+
}
|
|
41
|
+
for(const file of request.shared){const absolute=path.resolve(root,file);shared.add(absolute);existingShared.add(absolute);}
|
|
42
|
+
active.clear();active.add(request.id);
|
|
43
|
+
}
|
|
44
|
+
const sourceAst=(source,file)=>parse(source,{sourceType:'unambiguous',sourceFilename:file,plugins:['jsx',...(/\.[cm]?tsx?$/.test(file)?['typescript']:[])]});
|
|
45
|
+
const eligible=file=>file.startsWith(root+path.sep)&&!file.includes(`${path.sep}node_modules${path.sep}`)&&!file.includes(`${path.sep}.prnext-build-`);
|
|
46
|
+
function transform(source,file){
|
|
47
|
+
if(!eligible(file)||!source.includes('import'))return source;
|
|
48
|
+
const ast=sourceAst(source,file);let binding,changed=false;
|
|
49
|
+
const replace=node=>{
|
|
50
|
+
const value=t.isStringLiteral(node)?node.value:t.isTemplateLiteral(node)&&!node.expressions.length?node.quasis[0].value.cooked:undefined;
|
|
51
|
+
// Suffixes are part of ESM identity; path-only export bridges cannot
|
|
52
|
+
// represent them. Keep explicit suffixes in the ordinary compiler graph.
|
|
53
|
+
if(!value||value.length>4096||/[?#]/.test(value)||/^(?:[a-z]+:|\/\/)/i.test(value))return;
|
|
54
|
+
const importer=path.relative(root,file).split(path.sep).join('/'),id=digest(importer+'\0'+value);
|
|
55
|
+
if(!modules.has(id)&&modules.size>=LAZY_IMPORT_LIMIT)throw Error(`lazyDynamicImports supports at most ${LAZY_IMPORT_LIMIT} registered imports per generation`);
|
|
56
|
+
modules.set(id,{id,importer,request:value});
|
|
57
|
+
changed=true;return id;
|
|
58
|
+
};
|
|
59
|
+
traverse(ast,{Program(p){binding=p.scope.generateUidIdentifier('prnextLazyImport');},CallExpression(p){
|
|
60
|
+
if(!t.isImport(p.node.callee)||p.node.arguments.length!==1)return;
|
|
61
|
+
const id=replace(p.node.arguments[0]);if(id)p.replaceWith(t.callExpression(t.cloneNode(binding),[t.stringLiteral(id),t.stringLiteral(manifest.config.basePath||'')]));
|
|
62
|
+
},ImportExpression(p){if(p.node.options)return;const id=replace(p.node.source);if(id)p.replaceWith(t.callExpression(t.cloneNode(binding),[t.stringLiteral(id),t.stringLiteral(manifest.config.basePath||'')]));}});
|
|
63
|
+
if(!changed)return source;
|
|
64
|
+
ast.program.body.unshift(t.importDeclaration([t.importSpecifier(t.cloneNode(binding),t.identifier('lazyImport'))],t.stringLiteral(client)));
|
|
65
|
+
return generateMapped(generate,ast,{comments:true},source,file);
|
|
66
|
+
}
|
|
67
|
+
async function discover(metafile){
|
|
68
|
+
for(const input of Object.keys(metafile.inputs)){
|
|
69
|
+
const file=path.resolve(root,input);
|
|
70
|
+
if(!bridgeable(file)||input.startsWith('prnext-')||file.startsWith(packageRoot)||file.startsWith(stage+path.sep)||/\/node_modules\/(?:react|react-dom|react-refresh|react-server-dom-webpack)\//.test(file.split(path.sep).join('/'))||shared.has(file))continue;
|
|
71
|
+
if(shared.size>=LAZY_BRIDGE_LIMIT)throw Error(`lazyDynamicImports supports at most ${LAZY_BRIDGE_LIMIT} static JavaScript modules`);
|
|
72
|
+
shared.add(file);
|
|
73
|
+
// Real ESM entries expose live bindings. CommonJS named exports cannot be
|
|
74
|
+
// approximated with snapshots: a deferred graph sharing one is rejected.
|
|
75
|
+
if(file.endsWith('.json')||sourceAst(await readFile(file,'utf8'),file).program.sourceType==='module')bridges.set(file,undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function targetEntries(){
|
|
79
|
+
const result={};
|
|
80
|
+
for(const id of active){
|
|
81
|
+
if(!modules.has(id))continue;
|
|
82
|
+
const name='lazy-'+id,file=path.join(stage,'.entries',name+'.mjs');
|
|
83
|
+
await writeFile(file,`import * as namespace from ${JSON.stringify('prnext-lazy-entry:'+id)};export {namespace as __prnextLazyModule};`);
|
|
84
|
+
entries.set(file,{kind:'target',id});result[name]=file;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function bridgeEntries(){
|
|
89
|
+
const result=Object.fromEntries([...bridges].filter(([,url])=>!url).map(([file])=>{entries.set(file,{kind:'bridge',file});return ['lazy-bridge-'+digest(file),file];}));
|
|
90
|
+
for(const file of targetFiles.values())if(!bridges.has(file)&&bridgeable(file)){
|
|
91
|
+
entries.set(file,{kind:'native',file});result['lazy-native-'+digest(file)]=file;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function plugin(){return {name:'prnext-lazy-dynamic-boundaries',setup(builder){
|
|
96
|
+
builder.onResolve({filter:/^prnext-lazy-entry:/},async args=>{
|
|
97
|
+
const item=modules.get(args.path.slice('prnext-lazy-entry:'.length));
|
|
98
|
+
if(!item)return {errors:[{text:'Unknown deferred module entry'}]};
|
|
99
|
+
const result=await builder.resolve(item.request,{resolveDir:path.dirname(path.resolve(root,item.importer)),kind:'dynamic-import',pluginData:{prnextLazyEntry:true}});
|
|
100
|
+
if(!result.errors.length&&result.suffix)return suffixError();
|
|
101
|
+
const file=path.resolve(result.path||root),url=bridges.get(file);
|
|
102
|
+
if(!result.errors.length&&result.namespace==='file')targetFiles.set(item.id,file);
|
|
103
|
+
if(existingShared.has(file)&&!url)return {errors:[{text:`lazyDynamicImports cannot safely reuse the shared CommonJS module ${path.relative(root,file)}. Convert it to ESM or disable experimental.lazyDynamicImports.`}]};
|
|
104
|
+
return url?{path:url,external:true}:result;
|
|
105
|
+
});
|
|
106
|
+
if(request)builder.onResolve({filter:/.*/},async args=>{
|
|
107
|
+
if(args.pluginData?.prnextLazyResolving||args.pluginData?.prnextLazyEntry||args.kind==='entry-point'||args.namespace&&args.namespace!=='file')return;
|
|
108
|
+
const result=await builder.resolve(args.path,{resolveDir:args.resolveDir,importer:args.importer,kind:args.kind,with:args.with,pluginData:{...args.pluginData,prnextLazyResolving:true}});
|
|
109
|
+
// Aliases and transitive imports may introduce a suffix even when the
|
|
110
|
+
// registered literal had none. Reject it before looking up a bare bridge.
|
|
111
|
+
if(!result.errors.length&&result.suffix)return suffixError();
|
|
112
|
+
if(result.errors.length||result.external||result.namespace!=='file')return;
|
|
113
|
+
const file=path.resolve(result.path);
|
|
114
|
+
if(bridges.get(file))return {path:bridges.get(file),external:true};
|
|
115
|
+
if(existingShared.has(file))return {errors:[{text:`lazyDynamicImports cannot safely reuse the shared CommonJS module ${path.relative(root,file)}. Convert it to ESM or disable experimental.lazyDynamicImports.`}]};
|
|
116
|
+
});
|
|
117
|
+
}};}
|
|
118
|
+
function record(metadata,output){
|
|
119
|
+
const entry=entries.get(path.resolve(root,metadata.entryPoint||''));
|
|
120
|
+
if(!entry||!output.endsWith('.js'))return false;
|
|
121
|
+
const url=manifest.config.assetBase+'/'+path.basename(output);
|
|
122
|
+
if(entry.kind==='bridge'||entry.kind==='native'){
|
|
123
|
+
if(entry.kind==='bridge')bridges.set(entry.file,url);
|
|
124
|
+
nativeOutputs.set(entry.file,{url,css:metadata.cssBundle?[manifest.config.assetBase+'/'+path.basename(metadata.cssBundle)]:[]});
|
|
125
|
+
}
|
|
126
|
+
else outputs[entry.id]={url,css:metadata.cssBundle?[manifest.config.assetBase+'/'+path.basename(metadata.cssBundle)]:[]};
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
function finish(){
|
|
130
|
+
for(const [id,file]of targetFiles){
|
|
131
|
+
const native=nativeOutputs.get(file),url=native?.url||bridges.get(file);
|
|
132
|
+
if(url&&outputs[id])outputs[id]={url,native:true,css:[...new Set([...outputs[id].css,...(native?.css||[])])]};
|
|
133
|
+
}
|
|
134
|
+
manifest.devLazy={modules:[...modules.values()],bridges:[...bridges].filter(([,url])=>url).map(([file,url])=>({file:path.relative(root,file).split(path.sep).join('/'),url})),shared:[...shared].map(file=>path.relative(root,file).split(path.sep).join('/')),outputs};
|
|
135
|
+
}
|
|
136
|
+
return {transform,discover,targetEntries,bridgeEntries,plugin,record,finish};
|
|
137
|
+
}
|
package/build/migrate.mjs
CHANGED
|
@@ -19,18 +19,18 @@ export function parseMigrationArgs(args) {
|
|
|
19
19
|
const arg = args[index];
|
|
20
20
|
if (arg === '--dry-run') options.dryRun = true;
|
|
21
21
|
else if (arg === '--check') options.check = true;
|
|
22
|
-
else if (['--against', '--candidate', '--routes', '--timeout'].includes(arg)) {
|
|
22
|
+
else if (['--against', '--candidate', '--routes', '--timeout', '--browser-scenarios'].includes(arg)) {
|
|
23
23
|
const value = args[++index];
|
|
24
24
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
25
|
-
options[arg.slice(2)] = arg === '--timeout' ? Number(value) : value;
|
|
25
|
+
options[arg === '--browser-scenarios' ? 'browserScenarios' : arg.slice(2)] = arg === '--timeout' ? Number(value) : value;
|
|
26
26
|
}
|
|
27
27
|
else if (arg === '--no-install') options.install = false;
|
|
28
28
|
else if (arg === '--json') options.json = true;
|
|
29
|
-
else if (arg.startsWith('-')) throw new Error(`Unknown migrate option: ${arg}. Use --check, --dry-run, --no-install or --json.`);
|
|
29
|
+
else if (arg.startsWith('-')) throw new Error(`Unknown migrate option: ${arg}. Use --check, --dry-run, --no-install, --browser-scenarios or --json.`);
|
|
30
30
|
else if (directory === undefined) directory = arg;
|
|
31
31
|
else throw new Error('migrate accepts a single project directory.');
|
|
32
32
|
}
|
|
33
|
-
if ((options.against || options.candidate || options.routes || options.timeout !== undefined) && (!options.check || !options.against || !options.candidate)) throw new Error('Server comparison requires --check, --against and --candidate.');
|
|
33
|
+
if ((options.against || options.candidate || options.routes || options.timeout !== undefined || options.browserScenarios) && (!options.check || !options.against || !options.candidate)) throw new Error('Server comparison or browser execution requires --check, --against and --candidate.');
|
|
34
34
|
return { directory: path.resolve(directory || '.'), options };
|
|
35
35
|
}
|
|
36
36
|
|