@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.
Files changed (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
@@ -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
- let created = 0, reused = 0;
10
- export const incrementalCompilerStats = () => ({contexts:contexts.size, created, reused,webpack:webpackCompilerStats()});
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
- const canonicalRoot = await realpath(root);
21
- if (canonicalRoot !== root) {
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
- const fromVirtual=value=>typeof value==='string'?value.replaceAll(virtual,stage):value;
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:toVirtual(result.path)}:{}),...(result.resolveDir?{resolveDir:toVirtual(result.resolveDir)}:{}),...(result.watchFiles?{watchFiles:result.watchFiles.map(toVirtual)}:{}),...(result.watchDirs?{watchDirs:result.watchDirs.map(toVirtual)}:{})};
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+':'+createHash('sha256').update(signature).digest('hex');
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
- while([...contexts.values()].filter(slot=>slot.group===group).length>=(group==='auxiliary'?2:LIMIT)){const [oldest,previous]=[...contexts].find(([,slot])=>slot.group===group);contexts.delete(oldest);await previous.context.dispose();}
47
- created++;slot={group,root};
48
- slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage};
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.startsWith(virtual+path.sep)||args.path.startsWith(active.stage+path.sep)||(args.resolveDir===virtual || args.resolveDir.startsWith(virtual+path.sep))))return;
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
- if(!args.path.startsWith(virtual+path.sep))return;
67
- const actual=slot.current.fromVirtual(args.path),extension=path.extname(actual);
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 compiled=browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path) ? await reactCompiler(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
111
- const source = await fonts.transform(locations?await locations(compiled,args.path):compiled, args.path);
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()=>{const {closeIncrementalCompiler}=await import('./incremental.mjs');await closeIncrementalCompiler(root);});
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
- if (hadDestination) await rm(backup, { recursive: true, force: true });
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