@thomas.f/prnext 0.1.18-alpha → 0.1.19-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/build/actions.mjs +26 -14
- package/build/app.mjs +46 -25
- package/build/compiler.mjs +5 -4
- package/build/config.mjs +4 -2
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +14 -12
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dynamic.mjs +13 -6
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +32 -3
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/npm-packages.mjs +1 -0
- package/build/production-builder.mjs +1 -1
- package/build/standalone.mjs +21 -3
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +10 -1
- package/compat/app-dynamic.cjs +12 -1
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +3 -1
- package/compat/index.d.ts +1 -1
- package/compat/link.cjs +2 -2
- package/docs/ai/compatibility.md +77 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +21 -9
- package/runtime/app-client.mjs +119 -42
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +60 -19
- package/runtime/flight-delta.mjs +28 -4
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, readdir, lstat } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { compileApp } from './app.mjs';
|
|
5
|
+
import { configureCompiler } from './compiler.mjs';
|
|
6
|
+
import { createCssModules } from './css-modules.mjs';
|
|
7
|
+
import { createFonts } from './fonts.mjs';
|
|
8
|
+
import { createImages } from './images.mjs';
|
|
9
|
+
import { discardLazyBuild } from '../runtime/dev-lazy-staging.mjs';
|
|
10
|
+
import { loadEnvConfig } from '../runtime/env.mjs';
|
|
11
|
+
|
|
12
|
+
// Only the latest successful ordinary generation per project is reusable.
|
|
13
|
+
// This private compiler-process registry is not part of browser metadata or IPC.
|
|
14
|
+
const contexts = new Map();
|
|
15
|
+
const CONTEXT_LIMIT = 8, FILE_LIMIT = 16384, BYTE_LIMIT = 128 * 1024 * 1024;
|
|
16
|
+
export function forgetLazyBuildContext(root) {
|
|
17
|
+
if (root === undefined) contexts.clear(); else contexts.delete(root);
|
|
18
|
+
}
|
|
19
|
+
export function rememberLazyBuildContext(root, { manifest, config, environment, tsconfig, envMode }) {
|
|
20
|
+
contexts.delete(root);
|
|
21
|
+
if (!manifest.devLazy || !manifest.dev || !config.experimental.lazyDynamicImports) return;
|
|
22
|
+
while (contexts.size >= CONTEXT_LIMIT) contexts.delete(contexts.keys().next().value);
|
|
23
|
+
contexts.set(root, { config, environment, tsconfig, envMode, cacheId: manifest.cacheId,
|
|
24
|
+
buildId: manifest.buildId, session: manifest.devSession,
|
|
25
|
+
manifestConfig: structuredClone(manifest.config) });
|
|
26
|
+
}
|
|
27
|
+
/** Reuse the validated configuration of the running generation, not its routes. */
|
|
28
|
+
export async function tryBuildLazyGraph(root, { base, request, devInputs, devSession,
|
|
29
|
+
devActionSecret, incremental = true } = {}) {
|
|
30
|
+
if (!base || typeof base.cacheId !== 'string' || !request) throw Error('Invalid deferred build generation');
|
|
31
|
+
const context = contexts.get(root);
|
|
32
|
+
// Compiler recycling loses its private context. The caller retains the ordinary
|
|
33
|
+
// build path for this recovery case; it must never guess another generation.
|
|
34
|
+
if (!context || context.cacheId !== base.cacheId || context.session !== devSession) return;
|
|
35
|
+
// Preserve the running generation's env-file mode for supported build-time
|
|
36
|
+
// hooks. The enclosing build() scope restores the caller's env on every exit.
|
|
37
|
+
loadEnvConfig(root,{dev:true,mode:context.envMode,inputs:devInputs});
|
|
38
|
+
const { config, environment, tsconfig } = context;
|
|
39
|
+
const stage = path.join(root, '.prnext-build-' + randomUUID());
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(path.join(stage, '.entries'), { recursive: true });
|
|
42
|
+
const manifest = { version: 1, dev: true, buildId: context.buildId,
|
|
43
|
+
cacheId: context.cacheId, devSession, config: structuredClone(context.manifestConfig),
|
|
44
|
+
routes: [], prerendered: [] };
|
|
45
|
+
const project = { root, routes: [] };
|
|
46
|
+
configureCompiler({ config, root, stage, buildId: context.buildId, dev: true,
|
|
47
|
+
incremental, devInputs, tsconfig, assetBase: manifest.config.assetBase });
|
|
48
|
+
const fonts = createFonts({ projectRoot: root, stage, assetBase: manifest.config.assetBase });
|
|
49
|
+
const cssModules = createCssModules({ projectRoot: root, stage,
|
|
50
|
+
assetBase: manifest.config.assetBase, sassOptions: config.sassOptions,
|
|
51
|
+
cssEngine: config.cssEngine, dev: true, config });
|
|
52
|
+
const images = await createImages({ projectRoot: root, stage,
|
|
53
|
+
assetBase: manifest.config.assetBase, config: config.images });
|
|
54
|
+
await compileApp({ project, stage, manifest, cssModules, fonts, images, dev: true,
|
|
55
|
+
devActionSecret, reactCompiler: config.reactCompiler, defineEnvironment: environment,
|
|
56
|
+
moduleResolution: config.turbopack, serverExternalPackages: config.serverExternalPackages,
|
|
57
|
+
lazyDynamicImports: true, devLazyRequest: request, devLazyOnly: true });
|
|
58
|
+
const files = [];
|
|
59
|
+
let bytes = 0;
|
|
60
|
+
// This unpublished stage contains only this deferred graph, never other
|
|
61
|
+
// routes or a copied framework runtime. Publication uses this exact receipt.
|
|
62
|
+
for (const entry of await readdir(path.join(stage, 'assets'), { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isFile() || files.length >= FILE_LIMIT) throw Error('Invalid or excessive deferred output assets');
|
|
64
|
+
const info = await lstat(path.join(stage, 'assets', entry.name));
|
|
65
|
+
bytes += info.size;
|
|
66
|
+
if (!info.isFile() || bytes > BYTE_LIMIT) throw Error('Deferred output exceeds the 128 MiB generation budget');
|
|
67
|
+
files.push({ name: entry.name, bytes: info.size });
|
|
68
|
+
}
|
|
69
|
+
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
70
|
+
return { ...manifest, outputDirectory: stage, devLazyTemporary: true,
|
|
71
|
+
devLazyAssets: { version: 1, baseCacheId: context.cacheId, files } };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
try { await discardLazyBuild(root, { outputDirectory: stage, devLazyTemporary: true }); }
|
|
74
|
+
catch (cleanupError) { error.cleanupError = cleanupError; error.message += '; deferred compiler staging cleanup also failed'; }
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/build/dynamic.mjs
CHANGED
|
@@ -59,17 +59,19 @@ function loaderBody(loader) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Add shared hydration IDs without resolving, evaluating or eagerly importing loaders. */
|
|
62
|
-
export function transformDynamicImports(source, filename, { projectRoot, mode = 'browser' } = {}) {
|
|
62
|
+
export function transformDynamicImports(source, filename, { projectRoot, mode = 'browser',dev=false } = {}) {
|
|
63
63
|
if (![...sources].some(name => source.includes(name))) return source;
|
|
64
64
|
projectRoot ||= process.cwd();
|
|
65
|
-
return cachedTransform('dynamic',source,[filename,projectRoot,mode],()=>transformDynamicImportsUncached(source,filename,{projectRoot,mode}));
|
|
65
|
+
return cachedTransform('dynamic',source,[filename,projectRoot,mode,dev],()=>transformDynamicImportsUncached(source,filename,{projectRoot,mode,dev}));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
68
|
+
function transformDynamicImportsUncached(source, filename, {projectRoot,mode,dev}) {
|
|
69
69
|
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: filename,
|
|
70
70
|
plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
71
71
|
const relative = path.relative(projectRoot || process.cwd(), filename).replaceAll(path.sep, '/');
|
|
72
|
+
const developmentRevision=dev&&mode==='browser'?createHash('sha256').update(generate(ast,{compact:true,comments:false}).code).digest('hex'):undefined;
|
|
72
73
|
let changed = false;
|
|
74
|
+
let ordinal=0;
|
|
73
75
|
traverse(ast, {
|
|
74
76
|
CallExpression(call) {
|
|
75
77
|
let callee = call.get('callee');
|
|
@@ -105,6 +107,10 @@ function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
|
105
107
|
const generated = t.objectProperty(t.identifier('loadableGenerated'), t.objectExpression([
|
|
106
108
|
t.objectProperty(t.identifier('modules'), t.arrayExpression(ids.map(id => t.stringLiteral(id)))),
|
|
107
109
|
]));
|
|
110
|
+
const statement=call.findParent(value=>value.isStatement());
|
|
111
|
+
const declaration=statement?.parentPath?.isExportNamedDeclaration()?statement.parentPath:statement;
|
|
112
|
+
const stableModuleCall=call.scope===call.scope.getProgramParent()&&declaration?.parentPath?.isProgram()&&!t.isLoop(declaration.node);
|
|
113
|
+
const identity=developmentRevision&&stableModuleCall?t.objectProperty(t.identifier('__prnextDevIdentity'),t.stringLiteral(createHash('sha256').update(relative+'\0'+ordinal+++'\0'+developmentRevision).digest('hex').slice(0,24))):undefined;
|
|
108
114
|
|
|
109
115
|
if (noSSR && mode !== 'browser') {
|
|
110
116
|
const empty = t.arrowFunctionExpression([], t.nullLiteral(), true);
|
|
@@ -115,9 +121,10 @@ function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
|
115
121
|
loader.replaceWith(t.arrowFunctionExpression([], loader.node));
|
|
116
122
|
}
|
|
117
123
|
const target = secondOptions || firstOptions;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
else
|
|
124
|
+
const fields=[generated,...(identity?[identity]:[])];
|
|
125
|
+
if (target?.isObjectExpression()) target.node.properties.push(...fields);
|
|
126
|
+
else if (target?.node) target.replaceWith(t.objectExpression([t.spreadElement(target.node), ...fields]));
|
|
127
|
+
else call.node.arguments.push(t.objectExpression(fields));
|
|
121
128
|
changed = true;
|
|
122
129
|
},
|
|
123
130
|
});
|
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
|
@@ -13,6 +13,7 @@ import {retainDeploymentAssets} from './deployment-assets.mjs';
|
|
|
13
13
|
import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
|
|
14
14
|
import {writeBuildAnalysis} from './analyze.mjs';
|
|
15
15
|
import {routeFiles} from './dev-discovery.mjs';
|
|
16
|
+
import {tryBuildLazyGraph,rememberLazyBuildContext,forgetLazyBuildContext} from './dev-lazy-build.mjs';
|
|
16
17
|
import {writeTypedRoutes} from './typed-routes.mjs';
|
|
17
18
|
import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir, realpath } from 'node:fs/promises';
|
|
18
19
|
import path from 'node:path';
|
|
@@ -154,7 +155,12 @@ let buildQueue = Promise.resolve();
|
|
|
154
155
|
let developmentModulesLoaded = false;
|
|
155
156
|
/** Disposing shared compiler contexts must not interrupt another queued build. */
|
|
156
157
|
export function disposeBuildSession(root){
|
|
157
|
-
const task=buildQueue.then(async()=>{
|
|
158
|
+
const task=buildQueue.then(async()=>{
|
|
159
|
+
const canonicalRoot=root===undefined?undefined:await realpath(root).catch(error=>{if(error.code==='ENOENT'||error.code==='ENOTDIR')return path.resolve(root);throw error;});
|
|
160
|
+
forgetLazyBuildContext(canonicalRoot);
|
|
161
|
+
const {closeIncrementalCompiler}=await import('./incremental.mjs');
|
|
162
|
+
await closeIncrementalCompiler(canonicalRoot);
|
|
163
|
+
});
|
|
158
164
|
buildQueue=task.catch(()=>{});return task;
|
|
159
165
|
}
|
|
160
166
|
/** Isolate process.env between programmatic builds, including simultaneous calls. */
|
|
@@ -184,8 +190,13 @@ export function build(projectRoot, options = {}) {
|
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
/** 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 } = {}) {
|
|
193
|
+
async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs, devRoutes, devSequence, devSession, devActionSecret, devTools,devForceRefresh=false,devAffectedModules,devLazyRequest,devLazyBase,devLazyActivated=[] } = {}) {
|
|
188
194
|
const started = performance.now();
|
|
195
|
+
if (dev && devLazyRequest && devLazyBase) {
|
|
196
|
+
const deferred = await tryBuildLazyGraph(projectRoot,{base:devLazyBase,request:devLazyRequest,
|
|
197
|
+
devInputs,devSession,devActionSecret,incremental});
|
|
198
|
+
if (deferred) return deferred;
|
|
199
|
+
}
|
|
189
200
|
const config = await loadProjectConfig(projectRoot, { dev, envMode, devInputs });
|
|
190
201
|
const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
|
|
191
202
|
const environment = defineEnvironment(config);
|
|
@@ -201,6 +212,11 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
201
212
|
deferredRoutes=discovery.routes.filter(route=>!devRoutes.includes(route.pattern)&&!route.internal&&!route.errorStatus);
|
|
202
213
|
project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
|
|
203
214
|
} else project=await scanProject(projectRoot,scanOptions);
|
|
215
|
+
if(dev&&config.experimental.lazyDynamicImports){
|
|
216
|
+
if(project.routes.some(route=>route.router!=='app'&&!route.internal))throw Error('experimental.lazyDynamicImports currently requires an App Router-only application.');
|
|
217
|
+
if(config.webpack||Object.keys(config.turbopack?.rules||{}).length)throw Error('experimental.lazyDynamicImports currently requires the native compiler without webpack hooks or custom loaders.');
|
|
218
|
+
if(/^https?:\/\//.test(config.assetPrefix))throw Error('experimental.lazyDynamicImports requires same-origin development assets.');
|
|
219
|
+
}
|
|
204
220
|
if(!dev&&config.htmlOnly.length)selectHtmlOnlyRoutes(project.routes,config.htmlOnly,config);
|
|
205
221
|
if(!dev || !Array.isArray(devRoutes))await writeTypedRoutes(projectRoot,project,config);
|
|
206
222
|
const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
|
|
@@ -296,7 +312,18 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
296
312
|
await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
|
|
297
313
|
if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
|
|
298
314
|
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 });
|
|
315
|
+
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 });
|
|
316
|
+
if(manifest.devLazy){
|
|
317
|
+
const css=Object.values(manifest.devLazy.outputs).flatMap(module=>module.css);
|
|
318
|
+
for(const route of manifest.routes)if(route.router==='app'&&route.kind==='page'){
|
|
319
|
+
route.css=[...new Set([...route.css,...css])];
|
|
320
|
+
if(css.length&&Array.isArray(route.cssScopes)){
|
|
321
|
+
const root=route.cssScopes.find(scope=>scope.kind==='layout'&&!scope.path);
|
|
322
|
+
if(root)root.css=[...new Set([...root.css,...css])];
|
|
323
|
+
else route.cssScopes.push({kind:'layout',path:'',css:[...new Set(css)]});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
300
327
|
if(manifest.config.workerPreload)for(const pattern of config.workerPreload.routes){
|
|
301
328
|
const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
|
|
302
329
|
if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
|
|
@@ -353,6 +380,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
353
380
|
}
|
|
354
381
|
const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
|
|
355
382
|
await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
|
|
383
|
+
...(manifest.devLazy?{lazy:Object.values(manifest.devLazy.outputs)}:{}),
|
|
356
384
|
pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
|
|
357
385
|
...(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
386
|
}));
|
|
@@ -393,6 +421,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
393
421
|
} finally { await rm(pointerTemporary, {force:true}); }
|
|
394
422
|
await exported?.commit();
|
|
395
423
|
if (hadDestination) await rm(backup, { recursive: true, force: true });
|
|
424
|
+
if(dev&&!devLazyRequest)rememberLazyBuildContext(project.root,{manifest,config,environment,tsconfig,envMode});
|
|
396
425
|
return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
|
|
397
426
|
} catch (error) {
|
|
398
427
|
await rm(stage, { recursive: true, force: true });
|
|
@@ -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
|
|