@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,58 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ let stage, manifest, busy = false;
5
+ let closeRuntime;
6
+ const send = value => new Promise((resolve, reject) => process.send(value, error => error ? reject(error) : resolve()));
7
+ const memory = () => ({ rss: process.memoryUsage().rss, pid: process.pid });
8
+ const pulse = setInterval(() => { if (process.connected) process.send({ type: 'memory', ...memory() }); }, 100);
9
+ pulse.unref();
10
+
11
+ process.on('message', async message => {
12
+ try {
13
+ if (message.type === 'init') {
14
+ if (stage) throw new Error('Prerender worker was initialized twice');
15
+ ({ stage, manifest } = message);
16
+ await send({ type: 'ready', ...memory() });
17
+ return;
18
+ }
19
+ if (message.type === 'update') {
20
+ manifest = message.manifest;
21
+ await send({ type: 'updated', revision: message.revision });
22
+ return;
23
+ }
24
+ if (message.type === 'close') {
25
+ if (busy) throw new Error('Cannot close an active prerender worker');
26
+ if (closeRuntime) await closeRuntime();
27
+ clearInterval(pulse);
28
+ process.disconnect();
29
+ return;
30
+ }
31
+ if (message.type !== 'job' || !stage || busy) throw new Error('Invalid prerender worker operation');
32
+ busy = true;
33
+ const options = { ...message.options, manifest };
34
+ let rendered;
35
+ if (message.kind === 'pages' || message.kind === 'fallback') {
36
+ const runtime = await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
37
+ rendered = await runtime[message.kind === 'pages' ? 'prerenderRoute' : 'renderFallback'](options);
38
+ } else if (message.kind === 'app' || message.kind === 'handler') {
39
+ const runtime = await import(pathToFileURL(path.join(stage, `runtime/${message.kind === 'app' ? 'app-static' : 'route-static'}.mjs`)).href);
40
+ if (message.kind === 'app') closeRuntime ||= (await import(pathToFileURL(path.join(stage, 'runtime/app-render.mjs')).href)).closeAppRuntime;
41
+ rendered = await runtime[message.kind === 'app' ? 'prerenderAppRoute' : 'prerenderAppRouteHandler'](options);
42
+ } else throw new Error('Unknown prerender job kind');
43
+ busy = false;
44
+ // Runtime prerender entrypoints already flush their request/cache work.
45
+ // Do not transfer retained finalizers, contexts or incidental worker data.
46
+ const result = {};
47
+ for (const field of ['body','dataJSON','status','headers','revalidate','generatedAt','tags','paths','partial','metadata','dynamic','reason','flight']) {
48
+ if (rendered[field] !== undefined) result[field] = rendered[field];
49
+ }
50
+ await send({ type: 'result', id: message.id, rendered: result, ...memory() });
51
+ } catch (error) {
52
+ busy = false;
53
+ await send({ type: 'failure', id: message.id, error: error.stack || error.message, code: error.code, ...memory() }).catch(() => {});
54
+ }
55
+ });
56
+
57
+ // A dead coordinator must not leave application work or an RSC thread behind.
58
+ process.once('disconnect', () => process.exit(0));
@@ -1,9 +1,9 @@
1
- import {prerenderPages} from './prerender-pages.mjs';
2
- import {prerenderApp} from './app-static.mjs';
3
- process.once('message',async({stage,manifest})=>{
1
+ import {prerenderProduction} from './production-builder.mjs';
2
+ const controller=new AbortController();
3
+ process.on('message',message=>{if(message.type==='abort')controller.abort(new Error('Production prerender cancelled by supervisor'));});
4
+ process.once('message',async({root,stage,manifest,options})=>{
4
5
  try{
5
- await prerenderPages(stage,manifest,false);
6
- await prerenderApp(stage,manifest,false);
6
+ await prerenderProduction(root,stage,manifest,{...options,signal:controller.signal});
7
7
  process.send({ok:true,manifest},()=>process.exit(0));
8
8
  }catch(error){process.send({ok:false,error:error.stack||error.message},()=>process.exit(1));}
9
9
  });
@@ -4,6 +4,9 @@ import path from 'node:path';
4
4
  import {watchProject} from '../runtime/watch-project.mjs';
5
5
  import {createDevChangeFilter} from '../runtime/dev-file-changes.mjs';
6
6
  import {incrementalCompilerStats} from './incremental.mjs';
7
+ import {withPrerenderSession} from './prerender-pool.mjs';
8
+ import {prerenderPages} from './prerender-pages.mjs';
9
+ import {prerenderApp} from './app-static.mjs';
7
10
 
8
11
  /** Explicit reusable build session for integrations; always dispose it. */
9
12
  export async function createBuilder(projectRoot,options={}){
@@ -21,23 +24,69 @@ export async function createBuilder(projectRoot,options={}){
21
24
 
22
25
  // Application imports, SSG and RSC isolates belong to one build only. Keeping
23
26
  // esbuild's graph must not retain application globals or execute stale modules.
24
- export function isolatedPrerender(root,stage,manifest){
25
- return new Promise((resolve,reject)=>{
26
- const child=fork(new URL('./prerender-worker.mjs',import.meta.url),[],{cwd:root,execArgv:[],stdio:['ignore','inherit','inherit','ipc'],windowsHide:true});
27
- let result;
28
- const timeout=setTimeout(()=>{child.kill();reject(new Error('Production prerender exceeded ten minutes'));},600000);
29
- child.once('error',error=>{clearTimeout(timeout);reject(error);});
30
- child.on('message',value=>{result=value;});
31
- child.once('exit',(code,signal)=>{clearTimeout(timeout);result?.ok&&code===0?resolve(result.manifest):reject(new Error(result?.error||`Prerender exited ${signal||code}`));});
32
- child.send({stage,manifest});
27
+ export async function prerenderProduction(root,stage,manifest,{prerender,signal}={}){
28
+ await withPrerenderSession(root,stage,manifest,prerender,async()=>{
29
+ await prerenderPages(stage,manifest,false);
30
+ await prerenderApp(stage,manifest,false);
31
+ },{signal});
32
+ return manifest;
33
+ }
34
+
35
+ export function isolatedPrerender(root, stage, manifest, options = {}, dependencies = {}) {
36
+ const spawn = dependencies.fork || fork;
37
+ const { timeoutMs = 600000, abortGraceMs = 30000, killGraceMs = 5000 } = dependencies;
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawn(new URL('./prerender-worker.mjs', import.meta.url), [], {
40
+ cwd: root, execArgv: [], stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true,
41
+ });
42
+ let result, failure, terminateTimer, killTimer, closed = false, terminating = false;
43
+ const running = () => !closed && child.pid && child.exitCode === null && child.signalCode === null;
44
+ const terminate = () => {
45
+ if (!running() || terminating) return;
46
+ terminating = true;
47
+ child.kill('SIGTERM');
48
+ // SIGTERM is an application-observable signal on POSIX. Never wait on
49
+ // its handler before arming the unconditional final termination.
50
+ killTimer ||= setTimeout(() => { if (running()) child.kill('SIGKILL'); }, killGraceMs);
51
+ };
52
+ const fail = error => {
53
+ failure ||= error;
54
+ clearTimeout(timeout);
55
+ clearTimeout(terminateTimer);
56
+ terminate();
57
+ };
58
+ const send = message => {
59
+ if (!child.connected) { fail(new Error('Prerender coordinator IPC disconnected')); return; }
60
+ try { child.send(message, error => { if (error && !closed) fail(error); }); }
61
+ catch (error) { fail(error); }
62
+ };
63
+ const timeout = setTimeout(() => {
64
+ failure ||= new Error('Production prerender exceeded ten minutes');
65
+ // Cooperative cancellation drains the coordinator's owned path workers.
66
+ // A blocked application generator may never process this IPC message.
67
+ send({ type: 'abort' });
68
+ if (!closed) terminateTimer = setTimeout(terminate, abortGraceMs);
69
+ }, timeoutMs);
70
+ child.on('error', fail);
71
+ child.once('disconnect', () => { if (!closed && !result) fail(new Error('Prerender coordinator IPC disconnected before completion')); });
72
+ child.on('message', value => { if (typeof value?.ok === 'boolean') result = value; });
73
+ // Failed spawns emit error+close without exit. Publish/reject only after
74
+ // the final terminal event, so stage cleanup cannot race this process.
75
+ child.once('close', (code, signal) => {
76
+ closed = true;
77
+ clearTimeout(timeout); clearTimeout(terminateTimer); clearTimeout(killTimer);
78
+ if (!failure && result?.ok && code === 0) resolve(result.manifest);
79
+ else reject(failure || new Error(result?.error || `Prerender exited ${signal || code}`));
80
+ });
81
+ send({ root, stage, manifest, options });
33
82
  });
34
83
  }
35
84
 
36
85
  /** Retain production graphs during local rebuilds; no HTTP listener/daemon. */
37
86
  export async function watchProduction(root,options={}){
38
87
  let worker,watcher,stopping=false,timer,busy=false,queued=false;
39
- const changed=new Set(),filter=createDevChangeFilter(root);
40
88
  let output='.prnext';
89
+ const changed=new Set(),filter=createDevChangeFilter(root,{directoryEventsAreRecursive:process.platform==='darwin',ignore:relative=>relative===output||relative.startsWith(output+'/')});
41
90
  const stopWorker=async()=>{
42
91
  const old=worker;worker=undefined;if(!old||old.exitCode!==null||old.signalCode)return;
43
92
  await new Promise(resolve=>{const timer=setTimeout(()=>old.kill(),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});old.disconnect();});
@@ -0,0 +1,225 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { mkdir, open, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const reports = new AsyncLocalStorage(), graphs = new AsyncLocalStorage();
6
+ const FILE_LIMIT = 1024, FUNCTION_LIMIT = 4096, EVENT_LIMIT = 16, TOTAL_EVENT_LIMIT = 8192;
7
+ const GRAPH_INPUT_LIMIT = 4096;
8
+ const REPORT_LIMIT = 8 * 1024 * 1024;
9
+ export const REACT_COMPILER_REPORT_FILE = 'react-compiler-report.json';
10
+ const statuses = new Set(['optimized', 'skipped', 'error', 'diagnostic', 'mixed']);
11
+ const kinds = new Set(['CompileSuccess', 'CompileSkip', 'CompileError', 'CompileDiagnostic', 'PipelineError']);
12
+ const slash = value => value.split(path.sep).join('/');
13
+ const position = value => value && Number.isSafeInteger(value.line) && value.line > 0 && Number.isSafeInteger(value.column) && value.column >= 0
14
+ ? { line: value.line, column: value.column } : null;
15
+ function text(value, root = '') {
16
+ if (typeof value !== 'string') return null;
17
+ return value.replaceAll(root || '\0', '<project>').replace(/https?:\/\/\S+/g, '')
18
+ .replace(/(?:file:\/\/\/)?[A-Za-z]:[\\/][^\s'")]+/g, '<path>')
19
+ .replace(/(?:^|\s)\/(?:[^\s/'"]+\/)+[^\s'")]+/g, ' <path>')
20
+ .replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 384) || null;
21
+ }
22
+ function relativeFile(root, file) {
23
+ if (typeof root !== 'string' || typeof file !== 'string') return null;
24
+ const relative = path.relative(root, file);
25
+ return !relative || relative.length > 1024 || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative) ? null : slash(relative);
26
+ }
27
+
28
+ /** Each build owns its collector. Failed/overlapping rebuilds cannot replace it. */
29
+ export const withReactCompilerReporting = run => reports.run({ files: new Map(), graphs: new Map(), functionCount: 0, eventCount: 0, truncated: false }, run);
30
+ export function configureReactCompilerReporting({ root, config, buildId, dev, compilerVersion }) {
31
+ const state = reports.getStore();
32
+ if (state) Object.assign(state, { root: path.resolve(root), enabled: !!config?.reactCompiler,
33
+ options: config?.reactCompiler || null, buildId, dev: !!dev, compilerVersion });
34
+ }
35
+ export const withReactCompilerGraph = (target, run) => graphs.run(target, run);
36
+
37
+ /** Capture a narrow, source-free subset of the pinned compiler's public events. */
38
+ export function compilerEvent(event, functions, { moduleOptOut = false, root = '' } = {}) {
39
+ if (!event || !kinds.has(event.kind)) return null;
40
+ const start = position(event.fnLoc?.start), end = position(event.fnLoc?.end);
41
+ const contains = fn => start && fn.start && fn.end && (fn.start.line < start.line || fn.start.line === start.line && fn.start.column <= start.column)
42
+ && (fn.end.line > start.line || fn.end.line === start.line && fn.end.column >= start.column);
43
+ const fn = functions.filter(contains).sort((a, b) => b.start.line - a.start.line || b.start.column - a.start.column)[0];
44
+ const detail = event.detail?.options || event.detail;
45
+ let status = event.kind === 'CompileSuccess' ? 'optimized' : event.kind === 'CompileSkip' ? 'skipped'
46
+ : event.kind === 'CompileDiagnostic' ? 'diagnostic' : 'error';
47
+ let reason = event.kind === 'PipelineError' ? 'React Compiler could not complete this function.' : text(event.reason ?? detail?.reason, root);
48
+ let category = text(detail?.category, root), policy = false;
49
+ // Compiler 1.0.0 emits CompileSuccess even when its module opt-out prevents
50
+ // emitting the compiled function. Success is not proof of emitted memoization.
51
+ if (moduleOptOut && event.kind === 'CompileSuccess') {
52
+ status = 'skipped'; reason = `Module uses "${moduleOptOut === true ? 'use no memo' : moduleOptOut}".`; category = 'Directive'; policy = true;
53
+ } else if (event.kind === 'CompileSkip' && fn?.optOut) {
54
+ reason = `Function uses "${fn.optOut === true ? 'use no memo' : fn.optOut}".`; category = 'Directive'; policy = true;
55
+ }
56
+ const output = { kind: event.kind, status, name: text(fn?.name || event.fnName, root),
57
+ start: fn?.start || start, end: fn?.end || end, reason, category, policy };
58
+ if (status === 'optimized') for (const key of ['memoSlots', 'memoBlocks', 'memoValues']) {
59
+ if (Number.isSafeInteger(event[key]) && event[key] >= 0) output[key] = event[key];
60
+ }
61
+ return output;
62
+ }
63
+
64
+ /** Cache payloads contain code and these immutable event values, never ASTs. */
65
+ export function recordReactCompilerTransform(file, payload, { cacheHit = false } = {}) {
66
+ const state = reports.getStore();
67
+ if (!state?.enabled) return;
68
+ const relative = relativeFile(state.root, file);
69
+ if (!relative) return;
70
+ if (!state.files.has(relative) && state.files.size >= FILE_LIMIT) { state.truncated = true; return; }
71
+ let record = state.files.get(relative);
72
+ if (!record) state.files.set(relative, record = { file: relative, transforms: 0, cacheReuses: 0,
73
+ targets: new Set(), notes: new Set(), functions: new Map() });
74
+ record.transforms++; record.cacheReuses += Number(cacheHit);
75
+ const target = graphs.getStore() || 'unspecified';
76
+ record.targets.add(target);
77
+ for (const note of payload.notes || []) record.notes.add(note);
78
+ for (const event of payload.events || []) {
79
+ const key = JSON.stringify([event.start, event.name]);
80
+ let fn = record.functions.get(key);
81
+ if (!fn) {
82
+ if (state.functionCount >= FUNCTION_LIMIT) { state.truncated = true; continue; }
83
+ record.functions.set(key, fn = { name: event.name, start: event.start, end: event.end,
84
+ statuses: new Set(), targets: new Set(), events: new Map() });
85
+ state.functionCount++;
86
+ }
87
+ fn.statuses.add(event.status); fn.targets.add(target);
88
+ const detail = { kind: event.kind, status: event.status, reason: event.reason, category: event.category,
89
+ ...(event.policy ? { policy: true } : {}), ...Object.fromEntries(['memoSlots', 'memoBlocks', 'memoValues']
90
+ .filter(key => event[key] !== undefined).map(key => [key, event[key]])) };
91
+ const detailKey = JSON.stringify(detail);
92
+ if (fn.events.has(detailKey)) { fn.events.get(detailKey).targets.add(target); continue; }
93
+ if (fn.events.size < EVENT_LIMIT && state.eventCount < TOTAL_EVENT_LIMIT) { fn.events.set(detailKey, { ...detail, targets: new Set([target]) }); state.eventCount++; }
94
+ else state.truncated = true;
95
+ }
96
+ if (payload.truncated) state.truncated = true;
97
+ }
98
+ export function recordReactCompilerGraph(result) {
99
+ const state = reports.getStore(), target = graphs.getStore();
100
+ if (!state?.enabled || !target) return;
101
+ let record = state.graphs.get(target);
102
+ if (!record) state.graphs.set(target, record = { target, inputs: new Set(), metadataAvailable: true });
103
+ if (!result?.metafile?.inputs) { record.metadataAvailable = false; return; }
104
+ for (const file of Object.keys(result.metafile.inputs)) {
105
+ if (!/\.(?:[cm]?[jt]s|[jt]sx)$/i.test(file)) continue;
106
+ const relative = relativeFile(state.root, path.resolve(state.root, file));
107
+ if (relative && !relative.split('/').some(part => part === 'node_modules' || part.startsWith('.prnext'))) {
108
+ if (record.inputs.size < GRAPH_INPUT_LIMIT || record.inputs.has(relative)) record.inputs.add(relative);
109
+ else { state.truncated = true; record.metadataAvailable = false; }
110
+ }
111
+ }
112
+ }
113
+ export function reactCompilerReport() {
114
+ const state = reports.getStore();
115
+ if (!state) return null;
116
+ const files = [...state.files.values()].sort((a, b) => a.file.localeCompare(b.file)).map(file => ({
117
+ file: file.file, transforms: file.transforms, cacheReuses: file.cacheReuses, targets: [...file.targets].sort(), notes: [...file.notes].sort(),
118
+ functions: [...file.functions.values()].map(fn => ({ name: fn.name, start: fn.start, end: fn.end,
119
+ status: fn.statuses.size === 1 ? [...fn.statuses][0] : 'mixed', targets: [...fn.targets].sort(),
120
+ events: [...fn.events.values()].map(({ targets, ...event }) => ({ ...event, targets: [...targets].sort() })) }))
121
+ .sort((a, b) => (a.start?.line || 0) - (b.start?.line || 0) || (a.start?.column || 0) - (b.start?.column || 0)),
122
+ }));
123
+ const summary = summarize(files);
124
+ return { version: 1, compilerVersion: state.compilerVersion || null, enabled: !!state.enabled,
125
+ buildId: state.buildId || null, mode: state.dev ? 'development' : 'production', scope: 'observed-transforms',
126
+ options: state.enabled ? state.options : null, truncated: state.truncated, summary, files,
127
+ graphs: [...state.graphs.values()].map(graph => ({ target: graph.target, metadataAvailable: graph.metadataAvailable,
128
+ projectJavaScriptInputs: graph.inputs.size, observedFiles: files.filter(file => file.targets.includes(graph.target)).length,
129
+ unobservedInputs: [...graph.inputs].filter(file => !state.files.get(file)?.targets.has(graph.target)).length })),
130
+ };
131
+ }
132
+
133
+ export async function writeReactCompilerReport(directory, report = reactCompilerReport()) {
134
+ if (!report) return null;
135
+ let serialized = JSON.stringify(report) + '\n';
136
+ // Developer diagnostics must never make an otherwise valid large app fail.
137
+ while (Buffer.byteLength(serialized) > REPORT_LIMIT && report.files.length) {
138
+ const files = report.files.slice(0, Math.floor(report.files.length / 2));
139
+ report = { ...report, truncated: true, files, summary: summarize(files), graphs: report.graphs.map(graph => ({ ...graph,
140
+ metadataAvailable: false, observedFiles: files.filter(file => file.targets.includes(graph.target)).length,
141
+ unobservedInputs: graph.projectJavaScriptInputs })) };
142
+ serialized = JSON.stringify(report) + '\n';
143
+ }
144
+ if (Buffer.byteLength(serialized) > REPORT_LIMIT) throw new Error('Invalid React Compiler report metadata.');
145
+ await mkdir(directory, { recursive: true });
146
+ await writeFile(path.join(directory, REACT_COMPILER_REPORT_FILE), serialized);
147
+ return report;
148
+ }
149
+ export async function readReactCompilerReport(directory, { buildId } = {}) {
150
+ let serialized, handle;
151
+ try {
152
+ handle = await open(path.join(directory, REACT_COMPILER_REPORT_FILE), 'r');
153
+ const stat = await handle.stat();
154
+ if (!stat.isFile() || stat.size > REPORT_LIMIT) throw new Error('Invalid React Compiler report: size limit exceeded.');
155
+ const buffer = Buffer.alloc(Math.min(stat.size + 1, REPORT_LIMIT + 1));
156
+ let length = 0;
157
+ while (length < buffer.length) {
158
+ const result = await handle.read(buffer, length, buffer.length - length, length);
159
+ if (!result.bytesRead) break;
160
+ length += result.bytesRead;
161
+ }
162
+ // A concurrently growing/replaced report is not a successful build artifact.
163
+ if (length > stat.size || (await handle.stat()).size !== stat.size) throw new Error('Invalid or stale React Compiler report. Rebuild the application.');
164
+ serialized = buffer.subarray(0, length);
165
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
166
+ finally { await handle?.close(); }
167
+ let value;
168
+ try { value = JSON.parse(serialized); } catch { throw new Error('Invalid React Compiler report JSON.'); }
169
+ if (!validReport(value) || buildId !== undefined && value.buildId !== buildId) throw new Error('Invalid or stale React Compiler report. Rebuild the application.');
170
+ return value;
171
+ }
172
+ function validReport(report) {
173
+ const keys = (value, allowed) => value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => allowed.includes(key));
174
+ const safePath = file => typeof file === 'string' && file.length <= 1024 && !file.includes('\\') && !/[\u0000-\u001f\u007f]/.test(file)
175
+ && !path.posix.isAbsolute(file) && !/^[A-Za-z]:/.test(file) && file.split('/').every(part => part && part !== '.' && part !== '..');
176
+ const boundedText = value => value === null || typeof value === 'string' && value.length <= 384 && !/[\u0000-\u001f\u007f]/.test(value);
177
+ const count = value => Number.isSafeInteger(value) && value >= 0;
178
+ const targets = value => Array.isArray(value) && value.length <= 4 && value.every(target => ['browser', 'node', 'edge', 'unspecified'].includes(target));
179
+ const validPosition = value => value === null || keys(value, ['line', 'column']) && !!position(value);
180
+ const options = report?.options;
181
+ const validOptions = options === null || options === true || keys(options, ['compilationMode', 'panicThreshold', 'target'])
182
+ && ['infer', 'annotation'].includes(options.compilationMode) && ['none', 'critical_errors', 'all_errors'].includes(options.panicThreshold) && options.target === '19';
183
+ return keys(report, ['version', 'compilerVersion', 'enabled', 'buildId', 'mode', 'scope', 'options', 'truncated', 'summary', 'files', 'graphs'])
184
+ && report.version === 1 && report.scope === 'observed-transforms' && typeof report.enabled === 'boolean' && validOptions
185
+ && typeof report.truncated === 'boolean' && ['development', 'production'].includes(report.mode)
186
+ && boundedText(report.buildId) && boundedText(report.compilerVersion)
187
+ && keys(report.summary, ['files', 'functions', 'optimized', 'skipped', 'errors', 'diagnostics', 'mixed', 'transforms', 'cacheReuses'])
188
+ && ['files', 'functions', 'optimized', 'skipped', 'errors', 'diagnostics', 'mixed', 'transforms', 'cacheReuses'].every(key => count(report.summary[key]))
189
+ && Array.isArray(report.files) && report.files.length <= FILE_LIMIT && report.summary.files === report.files.length
190
+ && report.files.every(file => file && typeof file === 'object')
191
+ && new Set(report.files.map(file => file.file)).size === report.files.length
192
+ && report.files.every(file => keys(file, ['file', 'transforms', 'cacheReuses', 'targets', 'notes', 'functions'])
193
+ && safePath(file.file) && count(file.transforms) && count(file.cacheReuses) && file.cacheReuses <= file.transforms && targets(file.targets)
194
+ && Array.isArray(file.notes) && file.notes.length <= 16 && file.notes.every(boundedText)
195
+ && Array.isArray(file.functions) && file.functions.length <= FUNCTION_LIMIT && file.functions.every(fn => keys(fn, ['name', 'start', 'end', 'status', 'targets', 'events'])
196
+ && boundedText(fn.name) && statuses.has(fn.status) && validPosition(fn.start) && validPosition(fn.end) && targets(fn.targets)
197
+ && Array.isArray(fn.events) && fn.events.length <= EVENT_LIMIT && fn.events.every(event => keys(event, ['kind', 'status', 'reason', 'category', 'policy', 'memoSlots', 'memoBlocks', 'memoValues', 'targets'])
198
+ && kinds.has(event.kind) && statuses.has(event.status) && boundedText(event.reason) && boundedText(event.category) && targets(event.targets)
199
+ && (event.policy === undefined || typeof event.policy === 'boolean')
200
+ && ['memoSlots', 'memoBlocks', 'memoValues'].every(key => event[key] === undefined || count(event[key])))))
201
+ && report.files.reduce((sum, file) => sum + file.functions.length, 0) <= FUNCTION_LIMIT
202
+ && Object.entries(summarize(report.files)).every(([key, value]) => report.summary[key] === value)
203
+ && (!report.enabled ? report.files.length === 0 && options === null : options !== null)
204
+ && Array.isArray(report.graphs) && report.graphs.length <= 4 && report.graphs.every(graph => keys(graph, ['target', 'metadataAvailable', 'projectJavaScriptInputs', 'observedFiles', 'unobservedInputs'])
205
+ && targets([graph.target]) && typeof graph.metadataAvailable === 'boolean' && ['projectJavaScriptInputs', 'observedFiles', 'unobservedInputs'].every(key => count(graph[key]))
206
+ && graph.unobservedInputs <= graph.projectJavaScriptInputs);
207
+ }
208
+ function summarize(files) {
209
+ const summary = { files: files.length, functions: 0, optimized: 0, skipped: 0, errors: 0, diagnostics: 0, mixed: 0, transforms: 0, cacheReuses: 0 };
210
+ for (const file of files) { summary.transforms += file.transforms; summary.cacheReuses += file.cacheReuses;
211
+ for (const fn of file.functions) { summary.functions++; summary[({ error: 'errors', diagnostic: 'diagnostics' })[fn.status] || fn.status]++; } }
212
+ return summary;
213
+ }
214
+ export function printReactCompilerReport(report, { details = false, log = console.log } = {}) {
215
+ if (!report.enabled) { log('React Compiler: disabled. Enable reactCompiler in the application configuration.'); return; }
216
+ const summary = report.summary;
217
+ log(`React Compiler ${report.compilerVersion}: ${summary.optimized} optimized, ${summary.skipped} skipped, ${summary.errors} errors, ${summary.diagnostics} diagnostics, ${summary.mixed} mixed.`);
218
+ log(`Observed transforms only: ${summary.files} files; ${summary.cacheReuses}/${summary.transforms} transforms reused cached compiler results.${report.truncated ? ' Report truncated at its safety limit.' : ''}`);
219
+ if (report.graphs.some(graph => graph.unobservedInputs || !graph.metadataAvailable)) log('Some graph inputs were not observed by React Compiler; totals do not describe every application component.');
220
+ if (details) for (const file of report.files) {
221
+ log(file.file);
222
+ for (const fn of file.functions) log(` ${fn.name || '(anonymous)'}${fn.start ? ':' + fn.start.line : ''}: ${fn.status}${fn.events.some(event => event.reason) ? ' — ' + [...new Set(fn.events.map(event => event.reason).filter(Boolean))].join('; ') : ''}`);
223
+ for (const note of file.notes) log(' ' + note);
224
+ }
225
+ }
@@ -0,0 +1,139 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { cachedAsyncTransform } from './transform-cache.mjs';
6
+ import { extractSourceMap, inlineSourceMap, sourceMapsEnabled } from './source-maps.mjs';
7
+ import { compilerEvent, recordReactCompilerTransform } from './react-compiler-report.mjs';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ // Only metadata is loaded here. Disabled builds do not load Babel or the compiler.
11
+ export const REACT_COMPILER_VERSION = require('babel-plugin-react-compiler/package.json').version;
12
+ const BABEL_VERSION = require('@babel/core/package.json').version;
13
+ const frameworkRoot = fileURLToPath(new URL('../', import.meta.url));
14
+ const options = new Set(['compilationMode', 'panicThreshold', 'target']);
15
+ let implementation;
16
+ const REPORT_CACHE_VERSION = 1;
17
+ const optOutDirective = directives => directives?.map(item => item.value.value).find(value => value === 'use no memo' || value === 'use no forget') || false;
18
+
19
+ function functionLocations(ast) {
20
+ const functions = [], stack = [{ node: ast.program, parent: null }];
21
+ while (stack.length && functions.length < 4096) {
22
+ const { node, parent } = stack.pop();
23
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') continue;
24
+ if (['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) && node.loc) {
25
+ const name = node.id?.name || (parent?.type === 'VariableDeclarator' ? parent.id?.name : null)
26
+ || (parent?.type === 'ObjectProperty' ? parent.key?.name : null);
27
+ functions.push({ name: name || null, start: { line: node.loc.start.line, column: node.loc.start.column },
28
+ end: { line: node.loc.end.line, column: node.loc.end.column },
29
+ optOut: optOutDirective(node.body?.directives) });
30
+ }
31
+ for (const [key, value] of Object.entries(node)) {
32
+ if (['loc', 'extra', 'leadingComments', 'innerComments', 'trailingComments', 'comments', 'tokens'].includes(key)) continue;
33
+ if (Array.isArray(value)) for (const child of value) stack.push({ node: child, parent: node });
34
+ else if (value && typeof value === 'object') stack.push({ node: value, parent: node });
35
+ }
36
+ }
37
+ return functions;
38
+ }
39
+
40
+ /** Deliberately expose only supported, serializable compiler options. */
41
+ export function normalizeReactCompilerConfig(value) {
42
+ if (value === undefined || value === false) return false;
43
+ const input = value === true ? {} : value;
44
+ if (!input || typeof input !== 'object' || Array.isArray(input) || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) {
45
+ throw new TypeError('reactCompiler must be false, true or an options object.');
46
+ }
47
+ for (const key of Reflect.ownKeys(input)) {
48
+ if (!options.has(key)) throw new TypeError(`reactCompiler.${String(key)} is not supported by PRNext.`);
49
+ }
50
+ const compilationMode = input.compilationMode === undefined ? 'infer' : input.compilationMode;
51
+ const panicThreshold = input.panicThreshold === undefined ? 'none' : input.panicThreshold;
52
+ const target = input.target === undefined ? '19' : input.target;
53
+ if (!['infer', 'annotation'].includes(compilationMode)) throw new TypeError('reactCompiler.compilationMode must be infer or annotation.');
54
+ if (!['none', 'critical_errors', 'all_errors'].includes(panicThreshold)) throw new TypeError('reactCompiler.panicThreshold must be none, critical_errors or all_errors.');
55
+ if (target !== '19') throw new TypeError("reactCompiler.target must be '19'; PRNext requires React 19.");
56
+ return Object.freeze({ compilationMode, panicThreshold, target });
57
+ }
58
+
59
+ function inside(root, filename) {
60
+ const relative = path.relative(root, filename);
61
+ return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
62
+ }
63
+
64
+ function excluded(root, filename) {
65
+ if (!inside(root, filename)) return true;
66
+ const relative = path.relative(root, filename).split(path.sep);
67
+ return relative.some(part => /^(?:node_modules|\.git|\.next|\.contentlayer|\.prnext|\.prnext-cache|\.prnext-dev|\.prnext-types|out|dist)$/.test(part)
68
+ || part.startsWith('.prnext-build-') || part.startsWith('.prnext-incremental-'));
69
+ }
70
+
71
+ function loadImplementation() {
72
+ implementation ??= Promise.all([import('@babel/core'), import('babel-plugin-react-compiler')]).then(([babel, plugin]) => ({
73
+ babel, compiler: plugin.default?.default ?? plugin.default,
74
+ }));
75
+ return implementation;
76
+ }
77
+
78
+ /**
79
+ * Run the official compiler on application source before JSX lowering, React
80
+ * Refresh and framework instrumentation. JSX/TypeScript/module format remain
81
+ * intact for the existing graph compiler. No app Babel configuration is loaded.
82
+ */
83
+ export function createReactCompilerTransform(projectRoot, config, { dev = false } = {}) {
84
+ const normalized = normalizeReactCompilerConfig(config);
85
+ if (!normalized) return async source => source;
86
+ const root = path.resolve(projectRoot);
87
+ const canonicalRoot = realpath(root).catch(error => {
88
+ if (error.code === 'ENOENT') return root;
89
+ throw error;
90
+ });
91
+ const canonicalFramework = realpath(frameworkRoot).catch(() => frameworkRoot);
92
+ return async (source, filename, { loaderOutput = false } = {}) => {
93
+ filename = path.resolve(filename);
94
+ if ((!loaderOutput && !/\.(?:[cm]?[jt]s|[jt]sx)$/i.test(filename)) || /\.d\.[cm]?ts$/i.test(filename)) return source;
95
+ const actualRoot = await canonicalRoot;
96
+ if (excluded(root, filename) && excluded(actualRoot, filename)) return source;
97
+ // Bundlers canonicalize linked project roots. Also resolve linked source
98
+ // files so a project symlink cannot pull dependencies/framework/generated
99
+ // files into the application compiler accidentally.
100
+ const actualFile = await realpath(filename).catch(error => {
101
+ if (error.code === 'ENOENT') return inside(root, filename) ? path.resolve(actualRoot, path.relative(root, filename)) : filename;
102
+ throw error;
103
+ });
104
+ if (excluded(actualRoot, actualFile) || inside(await canonicalFramework, actualFile)) return source;
105
+ let cacheHit = true;
106
+ const serialized = await cachedAsyncTransform('react-compiler-report', source,
107
+ [REPORT_CACHE_VERSION, REACT_COMPILER_VERSION, BABEL_VERSION, normalized, root, actualRoot, filename, actualFile, !!dev, !!loaderOutput], async () => {
108
+ cacheHit = false;
109
+ const { babel, compiler } = await loadImplementation();
110
+ const mapped = sourceMapsEnabled();
111
+ const { code, map } = extractSourceMap(source);
112
+ const syntax = { filename, babelrc: false, configFile: false, sourceType: 'unambiguous',
113
+ parserOpts: { plugins: ['jsx', ...(/\.(?:[cm]?ts|tsx)$/i.test(filename) ? ['typescript'] : [])] } };
114
+ const ast = await babel.parseAsync(code, syntax);
115
+ // Server Action files are executable server modules, not render graphs.
116
+ if (ast.program.directives.some(item => item.value.value === 'use server')) return JSON.stringify({ code: source, events: [], notes: ['Server Action module excluded.'] });
117
+ const functions = functionLocations(ast), events = [], notes = [];
118
+ const moduleOptOut = optOutDirective(ast.program.directives);
119
+ if (moduleOptOut) notes.push(`Module uses "${moduleOptOut}"; compiled functions are not emitted.`);
120
+ if (normalized.compilationMode === 'annotation') notes.push('Annotation mode only selects functions with "use memo". Unselected functions have no compiler events.');
121
+ if (loaderOutput) notes.push('Locations refer to JavaScript produced by the configured loader.');
122
+ let truncated = false;
123
+ const logger = { logEvent(_filename, event) {
124
+ const value = compilerEvent(event, functions, { moduleOptOut, root });
125
+ if (value) { if (events.length < 256) events.push(value); else truncated = true; }
126
+ } };
127
+ const result = await babel.transformFromAstAsync(ast, code, { ...syntax, cloneInputAst: false,
128
+ sourceMaps: mapped, sourceFileName: filename, inputSourceMap: mapped ? map ?? false : false,
129
+ // Boundary checks above use real path segments. Avoid the plugin's
130
+ // default substring test rejecting e.g. /my-node_modules-example/.
131
+ plugins: [[compiler, { ...normalized, sources: null, logger }]],
132
+ });
133
+ return JSON.stringify({ code: mapped ? inlineSourceMap(result.code, result.map) : result.code, events, notes, truncated });
134
+ });
135
+ const payload = JSON.parse(serialized);
136
+ recordReactCompilerTransform(inside(root, filename) ? filename : path.resolve(root, path.relative(actualRoot, actualFile)), payload, { cacheHit });
137
+ return payload.code;
138
+ };
139
+ }
@@ -5,12 +5,12 @@ import path from 'node:path';
5
5
  // Protocol compatibility does not imply safe retirement. These modules jointly
6
6
  // implement activity tracking, cancellation and post-response draining.
7
7
  export const lifecycleModules = ['worker.mjs', 'transport.mjs', 'request-work.mjs', 'app-render.mjs', 'app-stream-html.mjs', 'rsc-worker.mjs'];
8
- export async function writeRuntimeContract(stage) {
8
+ export async function writeRuntimeContract(stage, {deployment=false,htmlOnly=false,workerMemory=false,incrementalCache=false} = {}) {
9
9
  const files = (await Promise.all(['runtime', 'compat'].map(async directory =>
10
10
  (await readdir(path.join(stage,directory))).filter(name => /\.(?:mjs|cjs|json)$/.test(name)).map(name => `${directory}/${name}`)))).flat().sort();
11
11
  const modules = Object.fromEntries(await Promise.all(files.map(async name =>
12
12
  [name, createHash('sha256').update(await readFile(path.join(stage, name))).digest('hex')])));
13
- const contract = { schema: 1, protocol: 2, lifecycle: 1,
13
+ const contract = { schema: 1, protocol: 2, lifecycle: 1, ...(deployment ? {deployment:1} : {}), ...(htmlOnly ? {htmlOnly:1} : {}), ...(workerMemory ? {workerMemory:1} : {}), ...(incrementalCache ? {incrementalCache:1} : {}),
14
14
  capabilities: ['activity', 'tracked-finalization', 'graceful-drain', 'lane-cancellation'], modules };
15
15
  await writeFile(path.join(stage, 'runtime-contract.json'), JSON.stringify(contract, null, 2) + '\n');
16
16
  }
@@ -1,5 +1,6 @@
1
- import { nodeFileTrace } from '@vercel/nft';
2
- import resolver from '@vercel/nft/out/resolve-dependency.js';
1
+ import { nodeFileTrace } from '../vendor/nft/out/index.js';
2
+ import resolver from '../vendor/nft/out/resolve-dependency.js';
3
+ import { CachedFileSystem } from '../vendor/nft/out/fs.js';
3
4
  import picomatch from 'picomatch';
4
5
  import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink, cp } from 'node:fs/promises';
5
6
  import path from 'node:path';
@@ -12,6 +13,10 @@ const within = (root, file) => file === root || file.startsWith(root + path.sep)
12
13
  const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
13
14
  const maxFiles = 100_000;
14
15
  const maxBytes = 2 * 1024 ** 3;
16
+ const linuxRuntimeProbes = new Set([
17
+ '/proc/self', '/proc/self/exe', '/usr/bin/ldd',
18
+ '/proc/version', '/proc/self/cgroup', '/.dockerenv', '/run/.containerenv',
19
+ ]);
15
20
 
16
21
  export async function resolveTraceDependency(...args) {
17
22
  const result = await resolver.default(...args);
@@ -75,6 +80,10 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
75
80
  const warnings = new Set();
76
81
  let omittedWarnings = 0;
77
82
  const cache = {};
83
+ const traceFileSystem = new CachedFileSystem({ cache, fileIOConcurrency: 16 });
84
+ // libc and environment detectors read the running host, not application assets.
85
+ // Keep this exact and outside the tracing root; other external files still fail.
86
+ const systemProbe = file => process.platform === 'linux' && !within(tracingRoot, file) && linuxRuntimeProbes.has(file);
78
87
  const traceLocations = new Map();
79
88
  let traceReads = 0;
80
89
  let traceBytes = 0;
@@ -120,7 +129,16 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
120
129
  for (const conditions of [['node'], ['node', 'react-server']]) {
121
130
  const result = await nodeFileTrace(entries, {
122
131
  base: path.parse(projectRoot).root, processCwd: projectRoot, conditions, cache, fileIOConcurrency: 16,
123
- ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
132
+ ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return systemProbe(absolute) || !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
133
+ // Share the metadata concurrency limit between stat and readlink.
134
+ stat: file => traceFileSystem.stat(file),
135
+ async readlink(file) {
136
+ // NFT follows symlinks before ignore(), so never follow these probes
137
+ // into /proc/<pid> or the build machine's Node executable.
138
+ if (systemProbe(file)) return null;
139
+ try { return await traceFileSystem.readlink(file); }
140
+ catch (error) { if (error.code === 'ENOTDIR') return null; throw error; }
141
+ },
124
142
  async readFile(file) {
125
143
  let info;
126
144
  try { info = await stat(file); } catch (error) { if (missing(error)) return null; throw error; }
@@ -65,16 +65,16 @@ export async function prepareStaticExport({ project, stage, manifest, config })
65
65
  if (route?.kind === 'api') { await put(mount + pathname, body); continue; }
66
66
  let html = body.toString();
67
67
  if (html.includes(config.images.path + '?')) reject(`optimized images in ${pathname} require images.unoptimized or a custom loader`);
68
- html = html.replace(/<head(?:\s[^>]*)?>/i, value => value + '<script>window.__PRNEXT_STATIC_EXPORT__=true;</script>');
68
+ if (!route.htmlOnly) html = html.replace(/<head(?:\s[^>]*)?>/i, value => value + '<script>window.__PRNEXT_STATIC_EXPORT__=true;</script>');
69
69
  const output = pathname === '/' ? '/index.html' : config.trailingSlash && !['/404', '/500'].includes(pathname) ? pathname + '/index.html' : pathname + '.html';
70
70
  await put(mount + output, html);
71
71
  if (entry.dataFile) {
72
72
  const data = await readFile(path.join(stage, entry.dataFile));
73
- if (route?.router === 'app') await put(mount + '/_prnext/flight' + (pathname === '/' ? '' : pathname) + '/index.txt', data);
74
- else for (const prefix of ['/_prnext/data/', '/_next/data/']) await put(mount + prefix + manifest.buildId + (pathname === '/' ? '/index' : pathname) + '.json', data);
73
+ if (route?.router === 'app') await put(mount + '/_prnext/flight' + (manifest.config.deploymentId ? '/' + encodeURIComponent(manifest.config.deploymentId) : '') + (pathname === '/' ? '' : pathname) + '/index.txt', data);
74
+ else for (const prefix of ['/_prnext/data/', '/_next/data/']) await put(mount + prefix + encodeURIComponent(manifest.config.deploymentId ? manifest.buildId + '~' + manifest.config.deploymentId : manifest.buildId) + (pathname === '/' ? '/index' : pathname) + '.json', data);
75
75
  }
76
76
  }
77
- await writeFile(path.join(folder, marker), JSON.stringify({ version: 1, buildId: manifest.buildId, basePath: mount, trailingSlash: config.trailingSlash }));
77
+ await writeFile(path.join(folder, marker), JSON.stringify({ version: 1, buildId: manifest.buildId, deploymentId: manifest.config.deploymentId, basePath: mount, trailingSlash: config.trailingSlash }));
78
78
  return folder;
79
79
  }
80
80