@thomas.f/prnext 0.1.17-alpha → 0.1.19-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +122 -46
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +34 -10
  15. package/build/config.mjs +34 -3
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
@@ -0,0 +1,269 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { lstat, readFile, opendir, realpath, readlink, mkdir, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { version as esbuildVersion } from 'esbuild';
5
+ import { buildCacheIdentity, cacheHash, privateCacheDirectory, readCacheRecord, writeCacheRecord } from './persistent-cache.mjs';
6
+ import {mapConcurrent} from './concurrent.mjs';
7
+ import { readCompilationRecord, writeCompilationRecord } from './compilation-store.mjs';
8
+
9
+ // Graph outputs have a narrower contract than source transforms. In particular,
10
+ // application graph callbacks discover actions, boundaries and font assets. They
11
+ // must run on every generation and are never skipped by this cache.
12
+ const reports = new AsyncLocalStorage();
13
+ const MAX_FILES = 4096, MAX_DIRECTORIES = 8192, MAX_NAMES = 32768;
14
+ const MAX_FILE_BYTES = 16 * 1024 * 1024, MAX_BYTES = 48 * 1024 * 1024;
15
+ const RECORD_LIMIT = 64 * 1024 * 1024;
16
+ const SHA = /^[a-f0-9]{64}$/;
17
+ const frameworkDirectory=name=>['.git','.prnext-cache','.prnext','.prnext-incremental-stage'].includes(name)||/^\.prnext-(?:build|backup|config|postcss)-/.test(name);
18
+ const frameworkPointer=name=>/^\.prnext-output(?:\.json|-.+\.tmp)$/.test(name);
19
+ const ignored = entry => entry.isDirectory() && (entry.name==='node_modules'||frameworkDirectory(entry.name)) || frameworkPointer(entry.name);
20
+ const safePath = file => typeof file === 'string' && file.length < 32768 && path.isAbsolute(file) && !file.includes('\0');
21
+ const inside = (root, file) => { const relative = path.relative(root, file); return relative === '' || !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep); };
22
+ export const withCompilationGraphCacheReporting = run => reports.run({ kinds: new Map(), identities:new Map(), listings:new Map(),listingNames:0 }, run);
23
+ export function compilationGraphCacheReport() {
24
+ const kinds = [...(reports.getStore()?.kinds || [])].map(([kind, value]) => ({ kind, ...value, reasons: { ...value.reasons } }));
25
+ return { version: 1, scope: 'eligible-production-compilation-graphs', kinds,
26
+ hits: kinds.reduce((sum, item) => sum + item.hits, 0), misses: kinds.reduce((sum, item) => sum + item.misses, 0),
27
+ bypasses: kinds.reduce((sum, item) => sum + item.bypasses, 0), reusedBytes: kinds.reduce((sum, item) => sum + item.reusedBytes, 0) };
28
+ }
29
+ function record(kind, outcome, reason, bytes = 0) {
30
+ const state = reports.getStore(); if (!state) return;
31
+ if (!state.kinds.has(kind)) state.kinds.set(kind, { hits: 0, misses: 0, bypasses: 0, writes: 0, reusedBytes: 0, reasons: {} });
32
+ const item = state.kinds.get(kind); item[outcome]++; item.reusedBytes += bytes;
33
+ if (reason) item.reasons[reason] = (item.reasons[reason] || 0) + 1;
34
+ }
35
+ function serializable(value) {
36
+ if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return true;
37
+ if (Array.isArray(value)) return value.every(serializable);
38
+ return value && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(item => item === undefined || serializable(item));
39
+ }
40
+ const directorySignature=info=>[info.dev,info.ino,info.mode,info.mtimeNs,info.ctimeNs].join(':');
41
+ async function directoryEntries(directory,budget){
42
+ const state=reports.getStore(),before=await lstat(directory,{bigint:true});
43
+ if(!before.isDirectory()||before.isSymbolicLink())throw Error('unstable-resolution');
44
+ const signature=directorySignature(before),saved=state?.listings.get(directory);
45
+ if(saved?.signature===signature){
46
+ const after=await lstat(directory,{bigint:true});
47
+ if(directorySignature(after)===signature){budget.names+=saved.entries.length;if(budget.names>MAX_NAMES)throw Error('resolution-limit');return {entries:saved.entries,info:after,stable:true};}
48
+ }
49
+ const entries=[];
50
+ for await(const entry of await opendir(directory)){if(++budget.names>MAX_NAMES)throw Error('resolution-limit');entries.push(entry);}
51
+ const after=await lstat(directory,{bigint:true}),stable=signature===directorySignature(after);
52
+ if(state&&stable){
53
+ if(state.listings.has(directory)){state.listingNames-=state.listings.get(directory).entries.length;state.listings.delete(directory);}
54
+ while(state.listings.size&&(state.listings.size>=MAX_DIRECTORIES||state.listingNames+entries.length>MAX_NAMES)){
55
+ const [oldest,value]=state.listings.entries().next().value;state.listings.delete(oldest);state.listingNames-=value.entries.length;
56
+ }
57
+ if(entries.length<=MAX_NAMES){state.listings.set(directory,{signature,entries});state.listingNames+=entries.length;}
58
+ }
59
+ return {entries,info:after,stable};
60
+ }
61
+ async function fileDigest(file, started = Infinity, budget = {bytes:0}) {
62
+ const before = await lstat(file);
63
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_FILE_BYTES || before.mtimeMs > started || before.ctimeMs > started) throw Error('unstable-input');
64
+ budget.bytes+=before.size;if(budget.bytes>MAX_BYTES)throw Error('dependency-limit');
65
+ const bytes = await readFile(file), after = await lstat(file);
66
+ if (bytes.length !== before.size || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw Error('unstable-input');
67
+ return cacheHash(bytes);
68
+ }
69
+ async function directoryDigest(directory, budget, started = Infinity, lookup = false, excluded = []) {
70
+ let listing;
71
+ try{listing=await directoryEntries(directory,budget);}catch(error){if(error.code==='ENOENT')return 'missing';throw error;}
72
+ const {info}=listing;
73
+ if(started!==Infinity&&(Number(info.mtimeNs)/1e6>started||Number(info.ctimeNs)/1e6>started))throw Error('unstable-resolution');
74
+ const namesAt=async listing=>{
75
+ const entries=listing.entries.filter(entry=>lookup?['package.json','tsconfig.json','jsconfig.json','.browserslistrc','browserslist','node_modules'].includes(entry.name):!ignored(entry)&&!excluded.includes(path.join(directory,entry.name)));
76
+ entries.sort((a,b)=>a.name.localeCompare(b.name));
77
+ return mapConcurrent(entries,async entry=>[entry.name,entry.isDirectory()?'directory':entry.isSymbolicLink()?['link',await readlink(path.join(directory,entry.name))]:entry.isFile()?'file':'other']);
78
+ };
79
+ const names=await namesAt(listing),digest=cacheHash(JSON.stringify(names));
80
+ if(!listing.stable&&cacheHash(JSON.stringify(await namesAt(await directoryEntries(directory,budget))))!==digest)throw Error('unstable-resolution');
81
+ return digest;
82
+ }
83
+ async function snapshot(root, dependencies, { started = Infinity, imports = [], rejectTsconfig = false, styleTargets=false, baseline, excluded = [] } = {}) {
84
+ if (!Array.isArray(dependencies) || dependencies.length > MAX_FILES || dependencies.some(file => !safePath(file))) throw Error('dependency-limit');
85
+ const files = new Set(dependencies), directories = new Set(), trees = new Set([root]), fullDirectories = new Set();
86
+ if(dependencies.some(file=>path.dirname(file).split(path.sep).some(frameworkDirectory)||frameworkPointer(path.basename(file))||excluded.some(directory=>inside(directory,file))))throw Error('generated-input');
87
+ for(const name of ['package.json','package-lock.json','npm-shrinkwrap.json','pnpm-lock.yaml','yarn.lock','bun.lock','bun.lockb','.env','.env.local','.env.production','.env.production.local','.env.development','.env.development.local','.env.test','.env.test.local']){
88
+ const file=path.join(root,name);try{const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink())throw Error('unsafe-config');files.add(file);}catch(error){if(error.code!=='ENOENT')throw error;}
89
+ }
90
+ const addParents = async file => {
91
+ let directory = path.dirname(file);
92
+ while (true) {
93
+ directories.add(directory);
94
+ // Node's package search includes missing nearer node_modules directories.
95
+ directories.add(path.join(directory, 'node_modules'));
96
+ for (const name of ['package.json', 'tsconfig.json', 'jsconfig.json', ...(styleTargets?['.browserslistrc','browserslist']:[])]) {
97
+ const candidate = path.join(directory, name);
98
+ try {
99
+ const info = await lstat(candidate);
100
+ if(name==='browserslist'&&info.isDirectory())continue;
101
+ if (rejectTsconfig && /^(?:ts|js)config\.json$/.test(name)) throw Error('untracked-tsconfig');
102
+ if (!info.isFile() || info.isSymbolicLink() || info.size>MAX_FILE_BYTES) throw Error('unsafe-config');
103
+ if(styleTargets&&(name==='.browserslistrc'||name==='browserslist'||name==='package.json')){
104
+ const source=await readFile(candidate,'utf8');
105
+ const targets=name==='package.json'?JSON.stringify(JSON.parse(source).browserslist):source;
106
+ if(typeof targets==='string'&&/\b(?:extends|stats)\b/i.test(targets))throw Error('custom-browser-targets');
107
+ }
108
+ files.add(candidate);
109
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
110
+ }
111
+ const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
112
+ }
113
+ };
114
+ for (const file of dependencies) {
115
+ await addParents(file);
116
+ // Package export/browser maps may choose an absent file anywhere inside the
117
+ // package. Directory names, including absence, participate in invalidation.
118
+ let directory = path.dirname(file);
119
+ while (true) {
120
+ try { if ((await lstat(path.join(directory, 'package.json'))).isFile()) { trees.add(directory); break; } }
121
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
122
+ const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
123
+ }
124
+ }
125
+ for (const [importer, specifier] of imports) {
126
+ if (typeof specifier !== 'string' || /^(?:node:|[./\\]|[A-Za-z]:)/.test(specifier)) continue;
127
+ const parts = specifier.split('/'), name = parts.slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
128
+ if (!name || name.includes('\0') || name.split('/').some(part => part === '..')) throw Error('unsafe-resolution');
129
+ let directory = path.dirname(importer);
130
+ while (true) {
131
+ const candidate = path.join(directory, 'node_modules', name);
132
+ try { const info = await lstat(candidate); if (info.isSymbolicLink()) throw Error('linked-package'); if (info.isDirectory()) trees.add(candidate); }
133
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
134
+ const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
135
+ }
136
+ }
137
+ const walked={names:0};
138
+ const walk = async (directory, depth = 0) => {
139
+ if (depth > 32 || directories.size > MAX_DIRECTORIES) throw Error('resolution-limit');
140
+ const listing=await directoryEntries(directory,walked);
141
+ directories.add(directory); fullDirectories.add(directory);
142
+ for (const entry of listing.entries) {
143
+ if (ignored(entry) || excluded.includes(path.join(directory,entry.name))) continue;
144
+ if (entry.isSymbolicLink()) throw Error('linked-input');
145
+ if (entry.isDirectory()) await walk(path.join(directory, entry.name), depth + 1);
146
+ }
147
+ };
148
+ for (const tree of trees) await walk(tree);
149
+ if (files.size > MAX_FILES || directories.size > MAX_DIRECTORIES) throw Error('dependency-limit');
150
+ const budget = { names: 0,bytes:0 };
151
+ const knownFiles = new Map(baseline?.files || []), knownDirectories = new Map((baseline?.directories || []).map(item => [item[0], item]));
152
+ const result = { excluded, files: await mapConcurrent([...files].sort(),async file => [file, await fileDigest(file, knownFiles.has(file) ? Infinity : started,budget)]),
153
+ directories: await mapConcurrent([...directories].sort(),async directory => {
154
+ const lookup = !fullDirectories.has(directory) && path.basename(directory) !== 'node_modules';
155
+ const known = knownDirectories.get(directory), comparable = known && known[2] === lookup;
156
+ return [directory, await directoryDigest(directory, budget, comparable ? Infinity : started, lookup, excluded), lookup];
157
+ }) };
158
+ for (const [file,digest] of result.files) if (knownFiles.has(file) && knownFiles.get(file) !== digest) throw Error('unstable-input');
159
+ for (const [directory,digest,lookup] of result.directories) {
160
+ const known = knownDirectories.get(directory);
161
+ if (known && known[2] === lookup && known[1] !== digest) throw Error('unstable-resolution');
162
+ }
163
+ return result;
164
+ }
165
+ async function validSnapshot(value) {
166
+ const pairs = (list, missing) => Array.isArray(list) && list.length <= (missing ? MAX_DIRECTORIES : MAX_FILES)
167
+ && new Set(list.map(item => item?.[0])).size === list.length
168
+ && list.every(item => Array.isArray(item) && item.length === (missing ? 3 : 2) && safePath(item[0]) && (SHA.test(item[1]) || missing && item[1] === 'missing') && (!missing || typeof item[2] === 'boolean'));
169
+ if (!value || !Array.isArray(value.excluded) || value.excluded.length>32 || !value.excluded.every(safePath) || !pairs(value.files, false) || !pairs(value.directories, true)) return false;
170
+ const budget = { names: 0,bytes:0 };
171
+ for (const [file, digest] of value.files) if (await fileDigest(file,Infinity,budget) !== digest) return false;
172
+ for (const [directory, digest, lookup] of value.directories) if (await directoryDigest(directory, budget, Infinity, lookup, value.excluded) !== digest) return false;
173
+ return true;
174
+ }
175
+ /** Framework-owned graph producers explicitly return dependencies and payloads. */
176
+ export async function cachedCompilation({ root, kind, settings, inputs = [], excluded = [], enabled = true, reason, validate, produce, bytes = () => 0, rejectTsconfig = false,styleTargets=false, binary = false }) {
177
+ if (!enabled || process.env.PRNEXT_BUILD_CACHE === '0' || !serializable(settings)) {
178
+ record(kind, 'bypasses', process.env.PRNEXT_BUILD_CACHE === '0' ? 'disabled' : reason || 'unsupported-configuration'); return (await produce()).value;
179
+ }
180
+ let directory, key;
181
+ try {
182
+ root = await realpath(root); directory = await privateCacheDirectory(root, binary ? 'compilation-graphs-v2' : 'compilation-graphs-v1');
183
+ if(!directory)throw Error('unavailable');
184
+ // Only metadata for the same current environment is shared. Individual
185
+ // graph snapshots still re-hash source/config bytes and validate listings.
186
+ const state=reports.getStore(),environment=cacheHash(JSON.stringify(Object.entries(process.env).sort())),identityKey=root+'\0'+environment;
187
+ if(state&&!state.identities.has(identityKey)){
188
+ if(state.identities.size>=8)state.identities.delete(state.identities.keys().next().value);
189
+ state.identities.set(identityKey,buildCacheIdentity(root));
190
+ }
191
+ const identity = directory && await (state?.identities.get(identityKey)||buildCacheIdentity(root));
192
+ if (!identity) throw Error('unavailable');
193
+ if(excluded.length>32||!excluded.every(file=>safePath(file)&&inside(root,file)&&file!==root))throw Error('unsafe-output-directory');
194
+ key = cacheHash(JSON.stringify([identity, esbuildVersion, kind, settings, excluded]));
195
+ const cached = binary ? await readCompilationRecord(directory, key) : await readCacheRecord(directory, key, RECORD_LIMIT);
196
+ if (cached?.format === 1 && validate(cached.value) && await validSnapshot(cached.snapshot)) {
197
+ record(kind, 'hits', undefined, bytes(cached.value)); return cached.value;
198
+ }
199
+ } catch { /* Corrupt, excessive or unavailable caches are optional. */ }
200
+ record(kind, 'misses');
201
+ let baseline, baselineError;
202
+ if(directory && key)try{baseline=await snapshot(root,inputs,{rejectTsconfig,excluded,styleTargets});}catch(error){baselineError=error;}
203
+ const started = Date.now(), produced = await produce();
204
+ if (directory && key && validate(produced.value)) try {
205
+ if(!baseline)throw baselineError || Error('snapshot-unavailable');
206
+ const current = await snapshot(root, produced.dependencies, { started, imports: produced.imports, rejectTsconfig,styleTargets, baseline, excluded });
207
+ const saved = { format: 1, snapshot: current, value: produced.value };
208
+ if (await (binary ? writeCompilationRecord(directory, key, saved) : writeCacheRecord(directory, key, saved, RECORD_LIMIT,{maxFiles:1024}))) record(kind, 'writes');
209
+ } catch (error) { record(kind, 'bypasses', ['unstable-input', 'unstable-resolution', 'untracked-tsconfig', 'linked-package', 'linked-input', 'resolution-limit', 'dependency-limit','generated-input','custom-browser-targets'].includes(error.message) ? error.message : 'snapshot-unavailable'); }
210
+ else if(!validate(produced.value))record(kind,'bypasses','generation-specific-output');
211
+ return produced.value;
212
+ }
213
+
214
+ function validMetafile(value){
215
+ const object=value=>value&&typeof value==='object'&&!Array.isArray(value),text=value=>typeof value==='string'&&value.length<32768&&!value.includes('\0');
216
+ const count=value=>Number.isSafeInteger(value)&&value>=0;
217
+ const imports=value=>Array.isArray(value)&&value.length<=MAX_FILES&&value.every(item=>object(item)&&text(item.path)&&text(item.kind)&&(item.external===undefined||typeof item.external==='boolean'));
218
+ return object(value)&&object(value.inputs)&&object(value.outputs)&&Object.keys(value.inputs).length<=MAX_FILES&&Object.keys(value.outputs).length<=MAX_FILES
219
+ &&Object.entries(value.inputs).every(([file,input])=>text(file)&&object(input)&&count(input.bytes)&&imports(input.imports))
220
+ &&Object.entries(value.outputs).every(([file,output])=>text(file)&&object(output)&&count(output.bytes)&&imports(output.imports)
221
+ &&(output.entryPoint===undefined||text(output.entryPoint))&&(output.cssBundle===undefined||text(output.cssBundle))
222
+ &&Array.isArray(output.exports)&&output.exports.length<=MAX_FILES&&output.exports.every(text)&&object(output.inputs)
223
+ &&Object.keys(output.inputs).length<=MAX_FILES&&Object.entries(output.inputs).every(([file,input])=>text(file)&&object(input)&&count(input.bytesInOutput)));
224
+ }
225
+ function validGraph(value) {
226
+ return value && Array.isArray(value.files) && value.files.length > 0 && value.files.length <= MAX_FILES
227
+ && value.files.every(file => Array.isArray(file) && file.length === 3 && typeof file[0] === 'string' && !path.isAbsolute(file[0])
228
+ && !/[\0<>:"|?*]/.test(file[0]) && file[0].split(/[\\/]/).every(part => part && part !== '.' && part !== '..') && file[1] instanceof Uint8Array && file[1].byteLength <= MAX_FILE_BYTES && typeof file[2] === 'string')
229
+ && new Set(value.files.map(file => file[0])).size === value.files.length
230
+ && value.files.reduce((sum, file) => sum + file[1].byteLength, 0) <= MAX_BYTES
231
+ && validMetafile(value.metafile);
232
+ }
233
+ const deepMap = (value, convert) => typeof value === 'string' ? convert(value) : Array.isArray(value) ? value.map(item => deepMap(item, convert))
234
+ : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([key, item]) => [convert(key), deepMap(item, convert)])) : value;
235
+ /** Cache native esbuild graphs only; plugins and generated entries need replay. */
236
+ export async function cachedGraphBuild(options, { root, stage, dev = false, config = {} }, compile) {
237
+ const output = options.outdir || options.outfile && path.dirname(options.outfile);
238
+ const permitted = !dev && !(options.plugins?.length) && options.bundle === true && output && stage && inside(stage, path.resolve(output))
239
+ && !options.stdin && !options.tsconfig && !options.tsconfigRaw && options.mangleCache===undefined && !config.webpack && !Object.keys(config.turbopack?.rules || {}).length;
240
+ if (!permitted) { record('javascript', 'bypasses', dev ? 'development' : options.plugins?.length ? 'graph-side-effects' : 'unsupported-configuration'); return compile(options); }
241
+ const token = '__PRNEXT_COMPILATION_STAGE__', toSaved = value => value.replaceAll(stage,()=>token).replaceAll(path.basename(stage),()=>token), fromSaved = value => value.replaceAll(token,()=>stage);
242
+ const rawSignature = { ...options, plugins: undefined, write: false, metafile: true };
243
+ if(!serializable(rawSignature)){record('javascript','bypasses','unsupported-configuration');return compile(options);}
244
+ const signature = deepMap(rawSignature, toSaved);
245
+ if (!serializable(signature)) { record('javascript', 'bypasses', 'unsupported-configuration'); return compile(options); }
246
+ let fresh;
247
+ const entries = Array.isArray(options.entryPoints) ? options.entryPoints.map(entry=>typeof entry==='string'?entry:entry.in) : Object.values(options.entryPoints || {});
248
+ const excluded = [path.resolve(root,config.distDir || '.prnext'),...(config.output==='export'?[path.join(root,'out')]:[])];
249
+ const saved = await cachedCompilation({ root, kind: 'javascript', settings: signature, inputs:entries.map(file=>path.resolve(options.absWorkingDir || process.cwd(),file)), excluded, validate: validGraph, rejectTsconfig: true, binary: true,
250
+ bytes: value => value.files.reduce((sum, file) => sum + file[1].byteLength, 0),
251
+ produce: async () => {
252
+ fresh = await compile({ ...options, write: false, metafile: true });
253
+ // Warnings may carry plugin details/locations; do not replay approximate diagnostics.
254
+ if (fresh.warnings?.length) return { value: null, dependencies: [] };
255
+ if(JSON.stringify(fresh.metafile).includes(token))return {value:null,dependencies:[]};
256
+ const dependencies = Object.keys(fresh.metafile.inputs).map(file => path.resolve(options.absWorkingDir || process.cwd(), file));
257
+ if (dependencies.some(file => inside(stage, file))) return { value: null, dependencies: [] };
258
+ const files = fresh.outputFiles.map(file => [path.relative(output, file.path), file.contents, file.hash]);
259
+ if (fresh.outputFiles.some(file => Buffer.from(file.contents).includes(Buffer.from(stage)) || Buffer.from(file.contents).includes(Buffer.from(path.basename(stage))))) return { value: null, dependencies: [] };
260
+ const imports = Object.entries(fresh.metafile.inputs).flatMap(([file, input]) => input.imports.filter(item => item.external).map(item => [path.resolve(options.absWorkingDir || process.cwd(), file), item.path]));
261
+ return { value: { files, metafile: deepMap(fresh.metafile, toSaved) }, dependencies, imports };
262
+ } });
263
+ const outputFiles = saved ? saved.files.map(([name, data, hash]) => ({ path: path.join(output, name), contents: Buffer.from(data.buffer, data.byteOffset, data.byteLength), hash, get text() { return this.contents.toString(); } })) : fresh.outputFiles;
264
+ if (options.write !== false) for (const file of outputFiles) { await mkdir(path.dirname(file.path), { recursive: true }); await writeFile(file.path, file.contents); }
265
+ return { ...(fresh || { errors: [], warnings: [] }), ...(options.metafile ? { metafile: saved ? deepMap(saved.metafile, fromSaved) : fresh.metafile } : { metafile: undefined }),
266
+ outputFiles: options.write === false ? outputFiles : undefined };
267
+ }
268
+
269
+ export const compilationCacheLimits = Object.freeze({ files: MAX_FILES, directories: MAX_DIRECTORIES, names: MAX_NAMES, fileBytes: MAX_FILE_BYTES, bytes: MAX_BYTES });
@@ -0,0 +1,142 @@
1
+ // Optional content-addressed compilation outputs. Records contain metadata and
2
+ // references only; binary output bytes are shared across compilation graphs.
3
+ import { constants } from 'node:fs';
4
+ import { open, lstat, rename, unlink, readdir } from 'node:fs/promises';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { serialize, deserialize } from 'node:v8';
7
+ import { deflateRaw, inflateRaw } from 'node:zlib';
8
+ import { promisify } from 'node:util';
9
+ import path from 'node:path';
10
+ import { cacheHash } from './persistent-cache.mjs';
11
+
12
+ const compress = promisify(deflateRaw), decompress = promisify(inflateRaw);
13
+ const MAGIC = Buffer.from('PRNG2\0');
14
+ const HEADER = MAGIC.length + 1 + 4 + 32;
15
+ const MAX_BLOB = 16 * 1024 * 1024, MAX_RECORD = 16 * 1024 * 1024;
16
+ const MAX_DISK = 64 * 1024 * 1024, MAX_SCAN = 8192, MAX_RECORDS = 1024;
17
+ const digest = /^[a-f0-9]{64}$/;
18
+ const namePattern = /^([a-f0-9]{64})\.(graph|blob)$/;
19
+
20
+ async function encode(bytes) {
21
+ let payload = bytes, codec = 0;
22
+ if (bytes.length >= 16384) {
23
+ const packed = await compress(bytes, { level: 1 });
24
+ if (packed.length < bytes.length * 0.9) { payload = packed; codec = 1; }
25
+ }
26
+ const header = Buffer.alloc(HEADER);
27
+ MAGIC.copy(header); header[MAGIC.length] = codec;
28
+ header.writeUInt32BE(bytes.length, MAGIC.length + 1);
29
+ Buffer.from(cacheHash(bytes), 'hex').copy(header, MAGIC.length + 5);
30
+ return [header, payload];
31
+ }
32
+
33
+ async function read(directory, name, limit) {
34
+ let file;
35
+ try {
36
+ const filename = path.join(directory, name), info = await lstat(filename);
37
+ if (!info.isFile() || info.isSymbolicLink() || info.size > limit + HEADER || info.size < HEADER) return null;
38
+ file = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
39
+ const stat = await file.stat();
40
+ if (!stat.isFile() || stat.size > limit + HEADER) return null;
41
+ const packed = await file.readFile();
42
+ if (!packed.subarray(0, MAGIC.length).equals(MAGIC)) return null;
43
+ const size = packed.readUInt32BE(MAGIC.length + 1), codec = packed[MAGIC.length];
44
+ if (size > limit || codec > 1) return null;
45
+ const payload = packed.subarray(HEADER);
46
+ const bytes = codec ? await decompress(payload, { maxOutputLength: Math.max(1, size) }) : payload;
47
+ if (bytes.length !== size || cacheHash(bytes) !== packed.subarray(MAGIC.length + 5, HEADER).toString('hex')) return null;
48
+ return bytes;
49
+ } catch { return null; } finally { await file?.close().catch(() => {}); }
50
+ }
51
+
52
+ async function write(directory, name, bytes) {
53
+ const filename = path.join(directory, name), temporary = filename + '.' + randomUUID() + '.tmp';
54
+ try {
55
+ const parts = await encode(bytes), file = await open(temporary, 'wx', 0o600);
56
+ try { for (const part of parts) await file.writeFile(part); } finally { await file.close(); }
57
+ await rename(temporary, filename);
58
+ } finally { await unlink(temporary).catch(() => {}); }
59
+ }
60
+
61
+ function references(record) {
62
+ const files = record?.value?.files;
63
+ if (record?.format !== 2 || !Array.isArray(files) || files.length > 4096) return null;
64
+ if (!files.every(file => Array.isArray(file) && file.length === 4 && typeof file[0] === 'string' && digest.test(file[1]) && typeof file[2] === 'string' && Number.isInteger(file[3]) && file[3] >= 0 && file[3] <= MAX_BLOB)) return null;
65
+ if (files.reduce((sum, file) => sum + file[3], 0) > 48 * 1024 * 1024) return null;
66
+ return files;
67
+ }
68
+
69
+ export async function readCompilationRecord(directory, key) {
70
+ if (!directory || !digest.test(key)) return null;
71
+ try {
72
+ const bytes = await read(directory, key + '.graph', MAX_RECORD);
73
+ if (!bytes) return null;
74
+ const record = deserialize(bytes), files = references(record);
75
+ if (!files || record.cacheKey !== key) return null;
76
+ const blobs = new Map(), restored = [];
77
+ for (const [name, id, hash, size] of files) {
78
+ let value = blobs.get(id);
79
+ if (!value) {
80
+ value = await read(directory, id + '.blob', MAX_BLOB);
81
+ if (!value || value.length !== size || cacheHash(value) !== id) return null;
82
+ blobs.set(id, value);
83
+ }
84
+ if (value.length !== size) return null;
85
+ restored.push([name, value, hash]);
86
+ }
87
+ return { ...record, format: 1, value: { ...record.value, files: restored } };
88
+ } catch { return null; }
89
+ }
90
+
91
+ // Concurrent eviction may turn a read into a miss, never into a partial hit.
92
+ // Retain the newest complete records and their shared blobs within one budget.
93
+ export async function pruneCompilationRecords(directory) {
94
+ try {
95
+ const entries = await readdir(directory, { withFileTypes: true });
96
+ const files = [];
97
+ for (const entry of entries.slice(0, MAX_SCAN)) {
98
+ if (!entry.isFile() || !namePattern.test(entry.name)) continue;
99
+ const info = await lstat(path.join(directory, entry.name));
100
+ if (info.isFile() && !info.isSymbolicLink()) files.push({ name: entry.name, size: info.size, time: info.mtimeMs });
101
+ }
102
+ const blobs = new Map(files.filter(file => file.name.endsWith('.blob')).map(file => [file.name.slice(0, -5), file]));
103
+ const retained = new Set(); let total = 0, count = 0;
104
+ for (const file of files.filter(file => file.name.endsWith('.graph')).sort((a, b) => b.time - a.time)) {
105
+ let refs;
106
+ try { const bytes = await read(directory, file.name, MAX_RECORD); refs = bytes && references(deserialize(bytes)); } catch {}
107
+ const ids = refs && [...new Set(refs.map(item => item[1]))];
108
+ const added = file.size + (ids || []).reduce((sum, id) => sum + (retained.has(id + '.blob') ? 0 : blobs.get(id)?.size ?? MAX_DISK), 0);
109
+ if (ids && ids.every(id => blobs.has(id)) && count < MAX_RECORDS && total + added <= MAX_DISK) {
110
+ retained.add(file.name); for (const id of ids) retained.add(id + '.blob');
111
+ total += added; count++;
112
+ }
113
+ }
114
+ for (const file of files) if (!retained.has(file.name)) await unlink(path.join(directory, file.name)).catch(() => {});
115
+ } catch { /* Disk cache maintenance cannot fail a build. */ }
116
+ }
117
+
118
+ export async function writeCompilationRecord(directory, key, record) {
119
+ if (!directory || !digest.test(key)) return false;
120
+ try {
121
+ const files = [], written = new Set();
122
+ for (const [name, data, hash] of record.value.files) {
123
+ if (!(data instanceof Uint8Array) || data.byteLength > MAX_BLOB) return false;
124
+ const bytes = Buffer.from(data.buffer, data.byteOffset, data.byteLength), id = cacheHash(bytes);
125
+ files.push([name, id, hash, bytes.length]);
126
+ if (!written.has(id)) {
127
+ // Verify existing bytes before reuse, including a corrupted blob from an
128
+ // interrupted previous build. Atomic replacement repairs that entry.
129
+ const existing = await read(directory, id + '.blob', MAX_BLOB);
130
+ if (!existing || cacheHash(existing) !== id) await write(directory, id + '.blob', bytes);
131
+ written.add(id);
132
+ }
133
+ }
134
+ const saved = { ...record, format: 2, cacheKey: key, value: { ...record.value, files } };
135
+ if (!references(saved)) return false;
136
+ const bytes = serialize(saved);
137
+ if (bytes.length > MAX_RECORD) return false;
138
+ await write(directory, key + '.graph', bytes);
139
+ await pruneCompilationRecords(directory);
140
+ return true;
141
+ } catch { return false; }
142
+ }
@@ -6,6 +6,7 @@ import { mkdir, writeFile, readFile } from 'node:fs/promises';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createRequire } from 'node:module';
9
+ import {createHash} from 'node:crypto';
9
10
  import { moduleResolutionPlugin, validateWebpackResolution } from './module-resolution.mjs';
10
11
  import { cachedLoader } from './loader-cache.mjs';
11
12
  import { incrementalBuild } from './incremental.mjs';
@@ -15,11 +16,19 @@ import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.
15
16
  import { optimizeImportsPlugins } from './optimize-imports.mjs';
16
17
  import {recordBuildAnalysis} from './analyze.mjs';
17
18
  import {recordDevDependencies,developmentDependencies,developmentStylePlans,developmentStyleDependencies,developmentModules} from './dev-dependencies.mjs';
19
+ import {normalizeReactCompilerConfig,REACT_COMPILER_VERSION} from './react-compiler.mjs';
20
+ import {withReactCompilerReporting,configureReactCompilerReporting,withReactCompilerGraph,recordReactCompilerGraph} from './react-compiler-report.mjs';
21
+ import {withCompilationGraphCacheReporting,cachedGraphBuild} from './compilation-cache.mjs';
22
+ export {reactCompilerReport} from './react-compiler-report.mjs';
23
+ export {compilationGraphCacheReport} from './compilation-cache.mjs';
18
24
 
19
25
  let validateOptions;
20
26
  const compilation = new AsyncLocalStorage();
21
- export const withCompilation = callback => withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback));
22
- export function configureCompiler(options) { Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps); }
27
+ export const withCompilation = callback => withCompilationGraphCacheReporting(()=>withReactCompilerReporting(()=>withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback))));
28
+ export function configureCompiler(options) {
29
+ Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps);
30
+ configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:REACT_COMPILER_VERSION});
31
+ }
23
32
  export const buildAnalysisRecords=()=>compilation.getStore()?.analysis;
24
33
  export const buildAnalysisRoutes=()=>compilation.getStore()?.analysisRoutes;
25
34
  export const devDependencies=(project,manifest)=>developmentDependencies(compilation.getStore(),project,manifest);
@@ -118,7 +127,7 @@ async function targetConfig(state, target) {
118
127
  }
119
128
  const validateDefinitions = (values,prefix='') => {for(const [name,value] of Object.entries(values)) {
120
129
  const key=prefix+name;
121
- if (/^(?:process\.env\.(?:NODE_ENV|NEXT_RUNTIME|PRNEXT_)|__prnext)/.test(key.replace(/^typeof /,''))) throw new Error(`DefinePlugin cannot override reserved ${key}`);
130
+ if (/^(?:process\.env\.(?:NODE_ENV|NEXT_RUNTIME|PRNEXT_)|__prnext|__PRNEXT_CACHE_COMPONENTS__(?:$|\.))/.test(key.replace(/^typeof /,''))) throw new Error(`DefinePlugin cannot override reserved ${key}`);
122
131
  if(value && Object.getPrototypeOf(value)===Object.prototype)validateDefinitions(value,key+'.');
123
132
  }};
124
133
  validateDefinitions(plugin.definitions);
@@ -136,15 +145,29 @@ export async function validateCompilerConfig(config, root, {dev=false,edge=false
136
145
  const state={config,root,dev,buildId:'prnext-check',targets:new Map()};
137
146
  for (const target of ['node','browser',...(edge?['edge']:[])]) await targetConfig(state,target);
138
147
  }
139
- export async function build(options) {
148
+ export async function build(options, { role = 'main' } = {}) {
149
+ if (role !== 'main' && role !== 'styles') throw new TypeError('Invalid compiler graph role');
140
150
  const state=compilation.getStore();
141
- const result=await compileGraph(options);
142
- recordBuildAnalysis(state,options,result);
143
- recordDevDependencies(state,options,result);
144
- return result;
151
+ const target=options.define?.['process.env.NEXT_RUNTIME']==='"edge"'?'edge':options.platform==='browser'?'browser':'node';
152
+ return withReactCompilerGraph(target,async()=>{
153
+ const result=await compileGraph(options,role);
154
+ recordReactCompilerGraph(result);
155
+ recordBuildAnalysis(state,options,result);
156
+ recordDevDependencies(state,options,result);
157
+ return result;
158
+ });
145
159
  }
146
- async function compileGraph(options) {
160
+ async function compileGraph(options,role) {
147
161
  const state = compilation.getStore();
162
+ // The transform is captured by the framework's per-build callbacks. Give both
163
+ // esbuild and the webpack bridge a new graph identity when its options change,
164
+ // even if none of the application's source files have changed. The marker has
165
+ // no hooks or runtime output and leaves the opt-out graph exactly as before.
166
+ if ((options.plugins || []).some(plugin => framework.test(plugin.name)) && state?.config?.reactCompiler) {
167
+ const settings=normalizeReactCompilerConfig(state.config.reactCompiler);
168
+ const fingerprint=createHash('sha256').update(JSON.stringify([REACT_COMPILER_VERSION,settings])).digest('hex').slice(0,24);
169
+ options={...options,plugins:[...options.plugins,{name:'prnext-react-compiler-'+fingerprint,setup(){}}]};
170
+ }
148
171
  if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
149
172
  // esbuild's shared service dispatches callbacks from its own async context.
150
173
  // Re-enter this build so transforms never inherit another project's map mode,
@@ -161,7 +184,8 @@ async function compileGraph(options) {
161
184
  // Runtime vendor bundles are framework-owned, not project webpack targets.
162
185
  const projectGraph = (options.plugins || []).some(plugin => framework.test(plugin.name));
163
186
  if (projectGraph && state?.tsconfig) options = { ...options, tsconfig: state.tsconfig };
164
- const compile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
187
+ const nativeCompile = input => state?.incremental && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',role:projectGraph?role:'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
188
+ const compile = input => state?.root && state?.stage ? cachedGraphBuild(input,state,nativeCompile) : nativeCompile(input);
165
189
  if (!(state?.config?.webpack || Object.keys(state?.config?.turbopack?.rules || {}).length) || !projectGraph) return compile(options);
166
190
  const target = options.define?.['process.env.NEXT_RUNTIME'] === '"edge"' ? 'edge' : options.platform === 'browser' ? 'browser' : 'node';
167
191
  const config = await targetConfig(state,target);
package/build/config.mjs CHANGED
@@ -16,18 +16,29 @@ import { validateTurbopack } from './module-resolution.mjs';
16
16
  import { validateI18n } from './i18n.mjs';
17
17
  import { frameworkPackages } from './framework-imports.mjs';
18
18
  import { validateTypeScript } from './typescript.mjs';
19
+ import {validateWorkerMemory} from './worker-memory.mjs';
19
20
  import { validateWorkerRetirement } from './worker-retirement.mjs';
20
21
  import {validateWorkerPreload} from './worker-preload.mjs';
21
22
  import { validateOptimizePackageImports } from './optimize-imports.mjs';
23
+ import {validateDeploymentRetention} from './deployment-config.mjs';
24
+ import {validatePrerenderConfig} from './prerender-config.mjs';
25
+ import {validateHtmlOnly} from './html-only-config.mjs';
22
26
  import { validateProxyBodySize } from './proxy-body-config.mjs';
27
+ import { normalizeReactCompilerConfig } from './react-compiler.mjs';
23
28
 
24
29
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
30
  const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
26
31
  const supported = new Set(['env', 'redirects', 'headers', 'rewrites', 'generateBuildId', 'compress', 'poweredByHeader', 'productionBrowserSourceMaps', 'basePath', 'assetPrefix', 'experimental', 'sassOptions', 'transpilePackages', 'serverExternalPackages', 'cacheComponents', 'cacheLife', 'images', 'reactStrictMode', 'pageExtensions', 'trailingSlash', 'skipTrailingSlashRedirect', 'skipMiddlewareUrlNormalize', 'skipProxyUrlNormalize', 'cacheHandlers', 'distDir']);
27
32
  const defaults = { distDir: '.prnext', env: {}, compress: true, poweredByHeader: true, productionBrowserSourceMaps: false, basePath: '', assetPrefix: '', trailingSlash: false, skipTrailingSlashRedirect: false, skipMiddlewareUrlNormalize: false, experimental: { nextScriptWorkers: false } };
28
33
  supported.add('cacheHandler');
34
+ supported.add('partialPrefetching');
35
+ supported.add('navigationCache');
36
+ supported.add('workerMemory');
29
37
  supported.add('workerRetirement');
30
38
  supported.add('workerPreload');
39
+ supported.add('deploymentRetention');
40
+ supported.add('prerender');
41
+ supported.add('htmlOnly');
31
42
  supported.add('cssEngine');
32
43
  supported.add('cacheMaxMemorySize');
33
44
  supported.add('turbopack');
@@ -90,10 +101,14 @@ export function validateProjectConfig(input, filename = 'project config') {
90
101
  // as in common process.env-based Next configuration wrappers.
91
102
  const config = { ...defaults, ...Object.fromEntries(Object.entries(input).filter(([,value]) => value !== undefined)) };
92
103
  if(config.typedRoutes!==undefined&&typeof config.typedRoutes!=='boolean')throw new TypeError('typedRoutes must be a boolean');
93
- if(config.reactCompiler!==undefined&&config.reactCompiler!==false)throw new Error('reactCompiler currently supports only false. PRNext does not run the React Compiler transform; keep Next.js for builds that require it.');
104
+ config.reactCompiler = normalizeReactCompilerConfig(config.reactCompiler);
94
105
  config.typescript = validateTypeScript(config.typescript);
106
+ config.workerMemory = validateWorkerMemory(config.workerMemory);
95
107
  config.workerRetirement = validateWorkerRetirement(config.workerRetirement);
96
108
  config.workerPreload=validateWorkerPreload(config.workerPreload);
109
+ config.deploymentRetention=validateDeploymentRetention(config.deploymentRetention);
110
+ config.prerender=validatePrerenderConfig(config.prerender);
111
+ config.htmlOnly=validateHtmlOnly(config.htmlOnly);
97
112
  config.cssEngine ??= 'auto';
98
113
  if (!['auto','postcss','lightningcss'].includes(config.cssEngine)) throw new TypeError('cssEngine must be auto, postcss or lightningcss.');
99
114
  if (config.devIndicators !== false) {
@@ -122,6 +137,20 @@ export function validateProjectConfig(input, filename = 'project config') {
122
137
  if (config.reactStrictMode !== undefined && typeof config.reactStrictMode !== 'boolean') throw new Error(`${filename}: reactStrictMode must be a boolean.`);
123
138
  config.cacheComponents ??= false;
124
139
  if (typeof config.cacheComponents !== 'boolean') throw new TypeError('cacheComponents must be a boolean');
140
+ config.partialPrefetching ??= false;
141
+ if (typeof config.partialPrefetching !== 'boolean') throw new TypeError('partialPrefetching must be a boolean');
142
+ if (config.partialPrefetching && !config.cacheComponents) throw new Error('partialPrefetching requires cacheComponents: true.');
143
+ if (config.navigationCache === false) config.navigationCache = { maxEntries: 0, ttlMs: 120000 };
144
+ else {
145
+ if (config.navigationCache === null) throw new TypeError('navigationCache must be false or an object with maxEntries and ttlMs.');
146
+ const value = config.navigationCache ?? {};
147
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['maxEntries', 'ttlMs', 'authCookies'].includes(key))) throw new TypeError('navigationCache must be false or an object with maxEntries, ttlMs and authCookies.');
148
+ const maxEntries = value.maxEntries ?? 4, ttlMs = value.ttlMs ?? 120000;
149
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 0 || maxEntries > 8 || !Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 600000) throw new TypeError('navigationCache requires maxEntries between 0 and 8, and ttlMs between 1000 and 600000.');
150
+ const authCookies = value.authCookies;
151
+ if (authCookies !== undefined && (!Array.isArray(authCookies) || authCookies.length > 32 || authCookies.some(name => typeof name !== 'string' || !name || name.length > 256 || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)))) throw new TypeError('navigationCache.authCookies must contain at most 32 valid cookie names.');
152
+ config.navigationCache = { maxEntries, ttlMs, ...(authCookies !== undefined ? {authCookies: [...new Set(authCookies)].sort()} : {}) };
153
+ }
125
154
  config.cacheLife = cacheProfiles.profiles(config.cacheLife);
126
155
  for (const key of ['transpilePackages', 'serverExternalPackages']) {
127
156
  const names = config[key] ?? [];
@@ -135,11 +164,13 @@ export function validateProjectConfig(input, filename = 'project config') {
135
164
  if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
136
165
  if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
137
166
  for (const key of Object.keys(config.experimental)) {
138
- if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
167
+ if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize', 'lazyDynamicImports'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
139
168
  }
140
169
  const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
141
170
  if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
142
- config.experimental = { nextScriptWorkers, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
171
+ const lazyDynamicImports=config.experimental.lazyDynamicImports===undefined?false:config.experimental.lazyDynamicImports;
172
+ if(typeof lazyDynamicImports!=='boolean')throw new Error(`${filename}: experimental.lazyDynamicImports must be a boolean.`);
173
+ config.experimental = { nextScriptWorkers, lazyDynamicImports, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
143
174
  for (const key of ['basePath', 'assetPrefix']) {
144
175
  if (typeof config[key] !== 'string' || Buffer.byteLength(config[key]) > 4096) throw new Error(`${filename}: ${key} must be a string of at most 4096 bytes.`);
145
176
  }