@thomas.f/prnext 0.1.16-alpha → 0.1.18-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 (215) hide show
  1. package/AI.md +63 -0
  2. package/README.md +35 -503
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +6 -1
  5. package/build/analyze.mjs +340 -0
  6. package/build/app-config.mjs +20 -8
  7. package/build/app-static.mjs +41 -14
  8. package/build/app-styles.mjs +79 -0
  9. package/build/app.mjs +99 -31
  10. package/build/build-lock.mjs +6 -6
  11. package/build/bundle-runtime.mjs +39 -11
  12. package/build/cache-components.mjs +1 -1
  13. package/build/cache-handlers.mjs +1 -1
  14. package/build/check.mjs +56 -11
  15. package/build/compact-css.mjs +53 -0
  16. package/build/compilation-cache.mjs +269 -0
  17. package/build/compilation-store.mjs +142 -0
  18. package/build/compiler.mjs +51 -4
  19. package/build/concurrent.mjs +12 -0
  20. package/build/config.mjs +50 -13
  21. package/build/create.mjs +239 -0
  22. package/build/css-modules.mjs +65 -10
  23. package/build/deployment-assets.mjs +89 -0
  24. package/build/deployment-config.mjs +18 -0
  25. package/build/dev-compiler-health.mjs +11 -0
  26. package/build/dev-css-update.mjs +85 -0
  27. package/build/dev-dependencies.mjs +44 -0
  28. package/build/dev-discovery.mjs +28 -0
  29. package/build/dev-worker.mjs +14 -5
  30. package/build/dev.mjs +64 -6
  31. package/build/dynamic.mjs +1 -1
  32. package/build/edge.mjs +5 -4
  33. package/build/fonts.mjs +1 -1
  34. package/build/framework-imports.mjs +50 -0
  35. package/build/html-only-config.mjs +53 -0
  36. package/build/html-only-graph.mjs +67 -0
  37. package/build/incremental.mjs +8 -4
  38. package/build/index.mjs +140 -112
  39. package/build/inspect.mjs +3 -0
  40. package/build/inspection-summary.mjs +37 -4
  41. package/build/instrumentation.mjs +30 -0
  42. package/build/loader-cache.mjs +53 -26
  43. package/build/metadata-routes.mjs +3 -4
  44. package/build/migrate.mjs +60 -57
  45. package/build/migration-audit.mjs +1 -1
  46. package/build/native-type-config.mjs +37 -0
  47. package/build/npm-packages.mjs +3 -18
  48. package/build/package-manager.mjs +308 -0
  49. package/build/persistent-cache.mjs +112 -0
  50. package/build/precompress.mjs +62 -30
  51. package/build/prerender-cache.mjs +70 -0
  52. package/build/prerender-config.mjs +29 -0
  53. package/build/prerender-pages.mjs +100 -0
  54. package/build/prerender-pool.mjs +165 -0
  55. package/build/prerender-process-worker.mjs +58 -0
  56. package/build/prerender-worker.mjs +10 -0
  57. package/build/production-builder.mjs +129 -0
  58. package/build/react-compiler-report.mjs +225 -0
  59. package/build/react-compiler.mjs +139 -0
  60. package/build/runtime-contract.mjs +2 -2
  61. package/build/scan.mjs +4 -4
  62. package/build/source-analysis.mjs +42 -0
  63. package/build/static-adapter.mjs +13 -5
  64. package/build/static-export.mjs +4 -4
  65. package/build/static-input.mjs +48 -0
  66. package/build/styles.mjs +17 -3
  67. package/build/transform-cache.mjs +37 -4
  68. package/build/transform.mjs +1 -1
  69. package/build/type-command.mjs +41 -0
  70. package/build/typed-routes.mjs +95 -0
  71. package/build/typescript-dev-worker.mjs +18 -0
  72. package/build/typescript-dev.mjs +49 -0
  73. package/build/typescript-native-worker.mjs +55 -0
  74. package/build/typescript-native.mjs +18 -0
  75. package/build/typescript-toolchain.mjs +19 -0
  76. package/build/typescript-watch.mjs +22 -0
  77. package/build/typescript.mjs +11 -8
  78. package/build/webpack-graph.mjs +170 -30
  79. package/build/worker-memory.mjs +9 -0
  80. package/build/worker-preload.mjs +9 -0
  81. package/cli.mjs +162 -122
  82. package/compat/activity-cache.cjs +26 -0
  83. package/compat/after.cjs +6 -4
  84. package/compat/app-layout-context.cjs +52 -2
  85. package/compat/base64.cjs +10 -0
  86. package/compat/cache-handlers.cjs +12 -2
  87. package/compat/cache-wire.cjs +28 -0
  88. package/compat/cache.cjs +2 -1
  89. package/compat/cache.d.cts +4 -0
  90. package/compat/cookies.cjs +21 -5
  91. package/compat/data-cache.cjs +12 -8
  92. package/compat/flight-delta.cjs +150 -0
  93. package/compat/headers.cjs +4 -2
  94. package/compat/incremental-cache.cjs +20 -7
  95. package/compat/index.d.ts +89 -10
  96. package/compat/link.cjs +12 -9
  97. package/compat/link.d.cts +4 -3
  98. package/compat/navigation-authorization.cjs +9 -0
  99. package/compat/navigation-stages.cjs +19 -0
  100. package/compat/navigation.d.cts +4 -3
  101. package/compat/offline-store.cjs +11 -0
  102. package/compat/offline.cjs +5 -0
  103. package/compat/offline.d.cts +2 -0
  104. package/compat/redis-cache.cjs +201 -0
  105. package/compat/redis-cache.d.cts +37 -0
  106. package/compat/router.d.cts +4 -3
  107. package/compat/server-primitives.cjs +31 -11
  108. package/compat/server.d.cts +15 -2
  109. package/compat/static-generation.cjs +7 -0
  110. package/compat/typed-route.d.cts +17 -0
  111. package/compat/use-cache.cjs +3 -0
  112. package/docs/ai/compatibility.md +275 -0
  113. package/docs/ai/data.md +288 -0
  114. package/docs/ai/deployment.md +269 -0
  115. package/docs/ai/manifest.json +11 -0
  116. package/docs/ai/quickstart.md +171 -0
  117. package/docs/ai/routing.md +214 -0
  118. package/docs/ai/troubleshooting.md +155 -0
  119. package/index.mjs +2 -0
  120. package/native/diagnostics.mjs +34 -0
  121. package/native/resolve.mjs +7 -2
  122. package/package.json +44 -7
  123. package/release/ai-docs.mjs +48 -0
  124. package/release/verify.mjs +2 -0
  125. package/runtime/action-client.mjs +10 -4
  126. package/runtime/action-server.mjs +2 -0
  127. package/runtime/ai-docs.mjs +202 -0
  128. package/runtime/app-client.mjs +226 -68
  129. package/runtime/app-content.mjs +4 -3
  130. package/runtime/app-partial-client.mjs +3 -0
  131. package/runtime/app-partial.mjs +1 -1
  132. package/runtime/app-prefetch-constants.mjs +4 -0
  133. package/runtime/app-prefetch.mjs +2 -3
  134. package/runtime/app-render-key.mjs +5 -0
  135. package/runtime/app-render.mjs +100 -28
  136. package/runtime/app-routing.mjs +22 -9
  137. package/runtime/app-static.mjs +18 -8
  138. package/runtime/app-stream-html.mjs +10 -53
  139. package/runtime/app-style-selection.mjs +34 -0
  140. package/runtime/app-styles.mjs +154 -0
  141. package/runtime/cache-components-rsc.mjs +4 -1
  142. package/runtime/cache-request.mjs +2 -1
  143. package/runtime/cloud/archive.mjs +5 -6
  144. package/runtime/compile-cache.mjs +8 -0
  145. package/runtime/deployment-client.mjs +19 -0
  146. package/runtime/dev-ai-brands.mjs +26 -0
  147. package/runtime/dev-ai-style.mjs +99 -0
  148. package/runtime/dev-build-reasons.mjs +25 -0
  149. package/runtime/dev-cache-control.mjs +53 -0
  150. package/runtime/dev-client.mjs +245 -46
  151. package/runtime/dev-compiler-broker.mjs +42 -0
  152. package/runtime/dev-console-copy.mjs +19 -0
  153. package/runtime/dev-console.mjs +106 -0
  154. package/runtime/dev-dependency-graph.mjs +40 -0
  155. package/runtime/dev-element-picker-style.mjs +27 -0
  156. package/runtime/dev-element-picker.mjs +213 -0
  157. package/runtime/dev-error-overlay-style.mjs +41 -0
  158. package/runtime/dev-error-overlay.mjs +167 -0
  159. package/runtime/dev-error-report.mjs +121 -0
  160. package/runtime/dev-file-changes.mjs +1 -1
  161. package/runtime/dev-gateway.mjs +44 -0
  162. package/runtime/dev-host-relay.mjs +66 -0
  163. package/runtime/dev-indicator.mjs +191 -0
  164. package/runtime/dev-issue-motion.mjs +214 -0
  165. package/runtime/dev-lazy-tool.mjs +34 -0
  166. package/runtime/dev-logo.mjs +2 -0
  167. package/runtime/dev-notification.mjs +81 -0
  168. package/runtime/dev-popout.mjs +34 -0
  169. package/runtime/dev-position.mjs +107 -0
  170. package/runtime/dev-preview-touch.mjs +122 -0
  171. package/runtime/dev-runtime-pool.mjs +45 -0
  172. package/runtime/dev-runtime-spare.mjs +57 -0
  173. package/runtime/dev-tools-client.mjs +155 -0
  174. package/runtime/dev-tools-mcp.mjs +126 -0
  175. package/runtime/dev-tools-metrics.mjs +31 -0
  176. package/runtime/dev-tools-model.mjs +42 -0
  177. package/runtime/dev-tools-panel.mjs +341 -0
  178. package/runtime/dev-tools-result.mjs +37 -0
  179. package/runtime/dev-tools-server.mjs +256 -0
  180. package/runtime/dev-tools-source.mjs +104 -0
  181. package/runtime/dev-tooltip.mjs +94 -0
  182. package/runtime/dev-ui-icons.mjs +8 -0
  183. package/runtime/dev-workbench-style.mjs +106 -0
  184. package/runtime/dev-workbench.mjs +428 -0
  185. package/runtime/dev-worker-logs.mjs +31 -0
  186. package/runtime/development.mjs +324 -0
  187. package/runtime/document.mjs +1 -1
  188. package/runtime/edge.mjs +6 -5
  189. package/runtime/env.mjs +2 -1
  190. package/runtime/fetch-cache.mjs +10 -8
  191. package/runtime/flight-delta.mjs +50 -0
  192. package/runtime/flight-tap.mjs +28 -0
  193. package/runtime/html-only.mjs +27 -0
  194. package/runtime/http.mjs +2 -0
  195. package/runtime/incremental-cache.mjs +25 -4
  196. package/runtime/instrumentation.mjs +49 -0
  197. package/runtime/navigation-recovery.mjs +36 -0
  198. package/runtime/navigation.mjs +7 -0
  199. package/runtime/node-html-stream.mjs +47 -0
  200. package/runtime/pages-client.mjs +24 -8
  201. package/runtime/pages-navigation.mjs +54 -64
  202. package/runtime/persistent/client.mjs +6 -5
  203. package/runtime/persistent/daemon.mjs +66 -56
  204. package/runtime/persistent/native-proxy.mjs +63 -0
  205. package/runtime/prefetch-scheduler.mjs +110 -0
  206. package/runtime/preload.mjs +12 -0
  207. package/runtime/profile.mjs +14 -0
  208. package/runtime/profiles.json +2 -2
  209. package/runtime/render.mjs +3 -0
  210. package/runtime/route-pattern.mjs +44 -0
  211. package/runtime/rsc-worker.mjs +41 -12
  212. package/runtime/trace.mjs +78 -0
  213. package/runtime/typed-route.mjs +19 -0
  214. package/runtime/watch-project.mjs +29 -18
  215. package/runtime/worker.mjs +102 -24
package/build/index.mjs CHANGED
@@ -1,11 +1,20 @@
1
+ import {compileInstrumentation} from './instrumentation.mjs';
2
+ import {prerenderPages as prerender} from './prerender-pages.mjs';
3
+ import {isolatedPrerender,prerenderProduction} from './production-builder.mjs';
1
4
  import { rename } from '../runtime/fs.mjs';
2
5
  import { writeRuntimeContract } from './runtime-contract.mjs';
3
6
  import { writeBuildOutput } from './output-contract.mjs';
4
7
  import { bundleRuntime } from './bundle-runtime.mjs';
5
- import {frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
8
+ import {withPersistentTransforms} from './transform-cache.mjs';
9
+ import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
6
10
  import { expandLocales, localizedStaticPaths } from './i18n.mjs';
7
- import { build as bundle, withCompilation, configureCompiler, compilerSource } from './compiler.mjs';
8
- import { mkdir, readFile, writeFile, rm, cp, stat, readdir } from 'node:fs/promises';
11
+ import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile,compilationGraphCacheReport } from './compiler.mjs';
12
+ import {retainDeploymentAssets} from './deployment-assets.mjs';
13
+ import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
14
+ import {writeBuildAnalysis} from './analyze.mjs';
15
+ import {routeFiles} from './dev-discovery.mjs';
16
+ import {writeTypedRoutes} from './typed-routes.mjs';
17
+ import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir, realpath } from 'node:fs/promises';
9
18
  import path from 'node:path';
10
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
20
  import { createRequire } from 'node:module';
@@ -30,28 +39,52 @@ import { prepareMetadataRoutes } from './metadata-routes.mjs';
30
39
  import { prepareImageResponse } from './og.mjs';
31
40
  import { prepareCacheHandler, prepareCacheHandlers } from './cache-handlers.mjs';
32
41
  import { npmPackagesPlugin } from './npm-packages.mjs';
33
- import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
42
+ import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
34
43
  import { snapshotEnvConfig } from '../runtime/env.mjs';
35
44
  import { validateStaticPaths } from '../runtime/pages-paths.mjs';
36
45
  import { projectTsconfig, checkTypeScript } from './typescript.mjs';
46
+ import {createReactCompilerTransform} from './react-compiler.mjs';
47
+ import {writeReactCompilerReport,reactCompilerReport} from './react-compiler-report.mjs';
37
48
  export { staticPath } from '../runtime/pages-paths.mjs';
38
49
 
39
50
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
40
- const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og']);
41
- const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
51
+ const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og', 'offline', 'redis-cache']);
52
+ const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
42
53
  const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
43
54
 
44
- function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fonts, refresh }) {
55
+ // These framework files have already been enumerated: copyFile avoids cp's
56
+ // recursive traversal/stat work for every file. Bound I/O on large installations.
57
+ async function copyRuntimeFiles(stage,{development=true}={}) {
58
+ const files = (await Promise.all(['runtime', 'compat'].map(async directory => {
59
+ await mkdir(path.join(stage, directory), { recursive: true });
60
+ return (await readdir(path.join(packageRoot, directory)))
61
+ .filter(name => directory === 'runtime' ? name === 'profiles.json' || development&&name.endsWith('.mjs') && !name.endsWith('.test.mjs') : name.endsWith('.cjs'))
62
+ .map(name => path.join(directory, name));
63
+ }))).flat();
64
+ let next = 0;
65
+ const copying = await Promise.allSettled(Array.from({ length: Math.min(8, files.length) }, async () => {
66
+ while (next < files.length) {
67
+ const file = files[next++];
68
+ await copyFile(path.join(packageRoot, file), path.join(stage, file));
69
+ }
70
+ }));
71
+ // Let outstanding writes finish before the caller removes a failed stage.
72
+ const failed = copying.find(result => result.status === 'rejected');
73
+ if (failed) throw failed.reason;
74
+ }
75
+
76
+ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler }) {
45
77
  return {
46
78
  name: 'prnext-pages-compatibility',
47
79
  setup(esbuild) {
80
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:browser && !!reactCompiler});
48
81
  esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, args => args.importer === documentFile
49
82
  ? { errors: [{ text: 'CSS cannot be imported within pages/_document. Move global styles to pages/_app, or return collected styles from Document.getInitialProps.' }] } : undefined);
50
83
  esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
51
84
  const name = frameworkImportName(args.path);
52
85
  if (!compatibleModules.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}). Supported runtime imports: next/link, next/head, next/router, next/image, next/cache, next/dynamic, next/error, next/document, next/app, next/script.` }] };
53
86
  if (browser && name === 'document') return { errors: [{ text: `${args.path} is server-only. Import Document, Html, Head, Main and NextScript from pages/_document, not from a browser page or component.` }] };
54
- if (browser && ['cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
87
+ if (browser && ['cache', 'og', 'redis-cache'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
55
88
  const file = name === 'compat/router' ? 'compat-router' : name;
56
89
  return browser ? { path: path.join(packageRoot, 'compat', file + '.cjs') } : { path: `../compat/${file}.cjs`, external: true };
57
90
  });
@@ -67,9 +100,15 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fo
67
100
  });
68
101
  esbuild.onLoad({ filter: /.*/, namespace: 'prnext-empty' }, () => ({ contents: '', loader: 'js' }));
69
102
  }
70
- esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
103
+ esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
71
104
  if (browser && args.path === documentFile) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a browser page or component.' }] };
72
- const source = await fonts.transform(args.prnextSource ?? await readFile(args.path, 'utf8'), args.path);
105
+ const original=await readCompilerFile(args.path,args.prnextSource);
106
+ // Pages shares a server graph with API handlers, Document and data
107
+ // loaders. Keep that entire graph original: an uppercase JSX utility
108
+ // may be called normally there, outside a React render. Browser render
109
+ // graphs receive memoization before instrumentation and JSX lowering.
110
+ const compiled=browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path) ? await reactCompiler(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
111
+ const source = await fonts.transform(locations?await locations(compiled,args.path):compiled, args.path);
73
112
  assertSupportedSource(source, args.path);
74
113
  const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
75
114
  let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
@@ -110,91 +149,34 @@ async function addBuiltinPagesErrors(project, stage) {
110
149
  }
111
150
  }
112
151
 
113
- async function prerender(stage, manifest, dev) {
114
- const { prerenderRoute, renderFallback } = await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
115
- const paths = new Set();
116
- const seedsByRoute = new Map();
117
- const validatedDocuments = new Set();
118
- const staticListings = new Map();
119
- // Error page data is available when another static page returns notFound.
120
- const ordered = [...manifest.routes].sort((left, right) => Number(!!right.errorStatus) - Number(!!left.errorStatus));
121
- for (const route of ordered) {
122
- if (route.kind !== 'page' || route.router === 'app') continue;
123
- const modulePath = path.join(stage, route.module);
124
- const page = await import(pathToFileURL(modulePath).href);
125
- if (page.__prnextDocument && !validatedDocuments.has(page.__prnextDocument)) {
126
- for (const name of ['getStaticProps', 'getStaticPaths', 'getServerSideProps']) {
127
- if (Object.hasOwn(page.__prnextDocument, name) || typeof page.Document?.[name] === 'function') throw new Error(`pages/_document does not support ${name}. Use Document.getInitialProps for server document rendering.`);
128
- }
129
- validatedDocuments.add(page.__prnextDocument);
130
- }
131
- if (route.errorStatus && (page.getServerSideProps || page.default?.getInitialProps)) throw new Error(`pages${route.pattern} must be static and cannot use getServerSideProps or getInitialProps. Use getStaticProps instead.`);
132
- route.gip = typeof page.default?.getInitialProps === 'function';
133
- route.appGip = !!page.App && page.App.getInitialProps !== page.App.origGetInitialProps;
134
- if (route.internal && route.errorStatus && route.appGip) {
135
- // A custom App hook makes the built-in Error request-dependent. Select
136
- // its actual _error module so the default status props and visible URL
137
- // reach App.getInitialProps instead of a synthetic static wrapper.
138
- const kind = route.errorStatus === 404 ? 'notFound' : 'serverError';
139
- if (manifest.pagesErrors?.[kind] === route.id) delete manifest.pagesErrors[kind];
140
- }
141
- if (route.gip && (page.getStaticProps || page.getServerSideProps)) throw new Error(`Page ${route.pattern} cannot combine getInitialProps with getStaticProps or getServerSideProps.`);
142
- if (route.internal && !route.errorStatus) {
143
- if (page.getStaticProps || page.getServerSideProps || page.getStaticPaths) throw new Error('pages/_error does not support getStaticProps, getServerSideProps or getStaticPaths. Use ErrorComponent.getInitialProps instead.');
144
- continue;
145
- }
146
- if (page.getServerSideProps && page.getStaticProps) throw new Error(`Page ${route.pattern} exports both getServerSideProps and getStaticProps. Choose one.`);
147
- if (page.getStaticPaths && !page.getStaticProps) throw new Error(`Page ${route.pattern} exports getStaticPaths without getStaticProps.`);
148
- if (page.getServerSideProps) { route.ssp = true; continue; }
149
- if (page.getStaticProps) route.ssg = true;
150
- else if (route.gip || route.appGip) continue;
151
- let entries = [{ path: route.pattern, params: {} }];
152
- if (isDynamic(route.pattern)) {
153
- if (!page.getStaticProps) continue;
154
- if (!page.getStaticPaths) throw new Error(`Dynamic page ${route.pattern} with getStaticProps must export getStaticPaths.`);
155
- if (!staticListings.has(modulePath)) staticListings.set(modulePath, Promise.resolve().then(() => page.getStaticPaths(manifest.config.i18n ? {locales: manifest.config.i18n.locales, defaultLocale: manifest.config.i18n.defaultLocale} : {})));
156
- const result = localizedStaticPaths(route, await staticListings.get(modulePath), manifest.config.i18n);
157
- route.fallback = result.fallback;
158
- entries = result.paths;
159
- } else if (page.getStaticPaths) throw new Error(`getStaticPaths is only valid on dynamic pages (${route.pattern}).`);
160
- for (const entry of entries) {
161
- if (paths.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
162
- paths.add(entry.path);
163
- const rendered = await prerenderRoute({ ...manifest.config, manifest, distDir: stage, route, modulePath, path: entry.path, params: entry.params, client: route.client, css: route.css, pattern: route.pattern, errorStatus: route.errorStatus, buildId: manifest.buildId, production: !dev });
164
- const file = `static/${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.html`;
165
- await writeFile(path.join(stage, file), rendered.body);
166
- const seed = { routeId: route.id, path: entry.path, file, status: route.errorStatus || rendered.status || 200, headers: rendered.headers || {} };
167
- if (route.ssg) {
168
- seed.dataFile = file.replace(/\.html$/, '.json');
169
- seed.revalidate = rendered.revalidate ?? false;
170
- seed.generatedAt = rendered.generatedAt;
171
- await writeFile(path.join(stage, seed.dataFile), rendered.dataJSON);
172
- }
173
- manifest.prerendered.push(seed);
174
- if (!seedsByRoute.has(route.id)) seedsByRoute.set(route.id, []);
175
- seedsByRoute.get(route.id).push(seed);
176
- }
177
- if (route.fallback === true) {
178
- const rendered = await renderFallback({ ...manifest.config, modulePath, path: route.pattern, params: {}, client: route.client, css: route.css, pattern: route.pattern, buildId: manifest.buildId, production: !dev });
179
- route.fallbackFile = `static/fallback-${route.id}.html`;
180
- await writeFile(path.join(stage, route.fallbackFile), rendered.body);
181
- }
182
- }
183
- // Rendering errors first must not change the published route/seed ordering.
184
- manifest.prerendered = manifest.routes.flatMap(route => seedsByRoute.get(route.id) || []);
185
- }
186
152
 
187
153
  let buildQueue = Promise.resolve();
154
+ let developmentModulesLoaded = false;
155
+ /** Disposing shared compiler contexts must not interrupt another queued build. */
156
+ export function disposeBuildSession(root){
157
+ const task=buildQueue.then(async()=>{const {closeIncrementalCompiler}=await import('./incremental.mjs');await closeIncrementalCompiler(root);});
158
+ buildQueue=task.catch(()=>{});return task;
159
+ }
188
160
  /** Isolate process.env between programmatic builds, including simultaneous calls. */
189
161
  export function build(projectRoot, options = {}) {
190
162
  const task = buildQueue.then(async () => {
163
+ if (options.adapter === undefined || options.adapter === 'auto') {
164
+ const { detectBuildAdapter } = await import('./static-input.mjs');
165
+ const selected = await detectBuildAdapter(projectRoot, { prepared: options.adapter === 'auto' });
166
+ if (selected.adapter === 'static') return (await import('./static-adapter.mjs')).buildStatic(projectRoot, { ...selected, ...options });
167
+ if (options.adapter === 'auto') options = { ...options, adapter: 'react' };
168
+ }
191
169
  if (options.adapter === 'static') return (await import('./static-adapter.mjs')).buildStatic(projectRoot, options);
192
170
  if (options.adapter !== undefined && options.adapter !== 'react') throw new Error(`Unknown PRNext adapter: ${options.adapter}`);
193
171
  if (options.from !== undefined || options.spa !== undefined) throw new Error('from and spa are only supported by the static adapter.');
194
172
  const restore = snapshotEnvConfig();
195
173
  const envMode = options.envMode || (process.env.NODE_ENV === 'test' ? 'test' : options.dev ? 'development' : 'production');
196
174
  process.env.NODE_ENV = options.dev ? 'development' : 'production';
197
- try { return await withBuildLock(projectRoot, root => withCompilation(() => buildProject(root, { ...options, envMode }))); }
175
+ try { return await withBuildLock(projectRoot, async root => {
176
+ const started=performance.now();
177
+ const result=await withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode })));
178
+ return {...result,durationMs:Math.round(performance.now()-started)};
179
+ }); }
198
180
  finally { restore(); }
199
181
  });
200
182
  buildQueue = task.catch(() => {});
@@ -202,15 +184,25 @@ export function build(projectRoot, options = {}) {
202
184
  }
203
185
 
204
186
  /** Compile Pages Router and App Router projects. A failed build preserves .prnext. */
205
- async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs } = {}) {
187
+ async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs, devRoutes, devSequence, devSession, devTools,devForceRefresh=false,devAffectedModules } = {}) {
206
188
  const started = performance.now();
207
- const config = await loadProjectConfig(projectRoot, { dev, envMode });
189
+ const config = await loadProjectConfig(projectRoot, { dev, envMode, devInputs });
208
190
  const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
209
191
  const environment = defineEnvironment(config);
210
192
  const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
211
193
  const customRoutes = await compileCustomRoutes(config);
212
194
  const buildId = await generateBuildId(config, { dev });
213
- const project = await scanProject(projectRoot, { basePath: config.basePath, pageExtensions: config.pageExtensions, i18n: config.i18n });
195
+ const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
196
+ let project,deferredRoutes=[];
197
+ if(dev && Array.isArray(devRoutes)) {
198
+ const discovery=await scanProject(projectRoot,{...scanOptions,devAppFiles:new Set()});
199
+ project=await scanProject(projectRoot,{...scanOptions,devAppFiles:routeFiles(discovery,devRoutes)});
200
+ await writeTypedRoutes(projectRoot,discovery,config);
201
+ deferredRoutes=discovery.routes.filter(route=>!devRoutes.includes(route.pattern)&&!route.internal&&!route.errorStatus);
202
+ project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
203
+ } else project=await scanProject(projectRoot,scanOptions);
204
+ if(!dev&&config.htmlOnly.length)selectHtmlOnlyRoutes(project.routes,config.htmlOnly,config);
205
+ if(!dev || !Array.isArray(devRoutes))await writeTypedRoutes(projectRoot,project,config);
214
206
  const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
215
207
  const destination = await prepareBuildDirectory(project.root, config.distDir);
216
208
  const backup = path.join(project.root, `.prnext-backup-${randomUUID()}`);
@@ -218,33 +210,35 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
218
210
  await mkdir(path.join(stage, 'static'), { recursive: true });
219
211
  await addBuiltinPagesErrors(project, stage);
220
212
  const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
221
- config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, cacheLife: config.cacheLife, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
213
+ ...(dev && Array.isArray(devRoutes) ? {devOnDemand:true} : {}), ...(dev && devSession ? {devSequence,devSession} : {}), ...(dev ? {devForceRefresh,devAffectedModules,devIndicators:config.devIndicators,...(devTools?{devTools}:{})} : {}),
214
+ config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
222
215
  ...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
223
216
  // Its build-unique name is known before bundling and avoids a hash cycle
224
217
  // between page entry URLs and the manifest that lists those entries.
218
+ if(!dev)manifest.config.deploymentId=manifest.cacheId;
225
219
  const pagesManifestUrl = `${publicPaths.assetBase}/pages-manifest-${manifest.cacheId}.json`;
226
220
  if (dev) manifest.devClient = `${publicPaths.assetBase}/dev-manifest-${manifest.cacheId}.json`;
227
- const development = dev ? { buildId, clientManifest: manifest.devClient } : undefined;
228
- const refresh = dev ? createRefreshTransform(project.root) : undefined;
221
+ const development = dev ? { buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:devSequence, session:devSession, indicator:config.devIndicators,...(devTools?{tools:devTools}:{}) } : undefined;
222
+ const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:devForceRefresh,forceFiles:devAffectedModules}) : undefined;
223
+ const locations = dev ? createSourceLocationTransform(project.root) : undefined;
224
+ const reactCompiler = config.reactCompiler ? createReactCompilerTransform(project.root, config.reactCompiler, {dev}) : undefined;
225
+ const serverOnlyFiles = new Set();
226
+ if (reactCompiler) for (const file of [...project.routes.filter(route=>route.kind==='api').map(route=>route.file),...(project.document?[project.document]:[])]) {
227
+ serverOnlyFiles.add(file);serverOnlyFiles.add(await realpath(file));
228
+ }
229
229
  const fonts = createFonts({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
230
- const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions });
230
+ const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine,dev,config });
231
231
  configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,tsconfig,assetBase:publicPaths.assetBase});
232
232
  try {
233
- await mkdir(path.join(stage, 'runtime'), { recursive: true });
234
- await mkdir(path.join(stage, 'compat'), { recursive: true });
235
- for (const name of await readdir(path.join(packageRoot, 'runtime'))) {
236
- if (name.endsWith('.mjs') && !name.endsWith('.test.mjs')) await cp(path.join(packageRoot, 'runtime', name), path.join(stage, 'runtime', name));
237
- }
238
- await cp(path.join(packageRoot, 'runtime/profiles.json'), path.join(stage, 'runtime/profiles.json'));
233
+ await copyRuntimeFiles(stage,{development:dev});
239
234
  // Runtime copies must work even when dotenv is nested beneath the PRNext
240
235
  // package instead of hoisted beside the application's dependencies.
241
236
  await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
242
- for (const name of await readdir(path.join(packageRoot, 'compat'))) {
243
- if (name.endsWith('.cjs')) await cp(path.join(packageRoot, 'compat', name), path.join(stage, 'compat', name));
244
- }
245
237
  await prepareImageResponse(stage);
246
238
  manifest.config.serverActions = config.experimental.serverActions;
239
+ if(config.workerMemory && !dev)manifest.config.workerMemory=config.workerMemory;
247
240
  if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
241
+ if(config.workerPreload&&!dev)manifest.config.workerPreload=config.workerPreload;
248
242
  const cacheHandlers = await prepareCacheHandlers({ config, projectRoot: project.root, stage, environment });
249
243
  if (Object.keys(cacheHandlers).length) manifest.config.cacheHandlers = cacheHandlers;
250
244
  const cacheHandler = await prepareCacheHandler({ config, projectRoot: project.root, stage, environment });
@@ -261,7 +255,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
261
255
  serverEntries[route.id] = serverEntry;
262
256
  if (route.kind === 'page') {
263
257
  const clientEntry = path.join(stage, '.entries', route.id + '.client.jsx');
264
- await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, strictMode: config.reactStrictMode === true }, development));
258
+ await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, deploymentId:dev?undefined:manifest.cacheId, strictMode: config.reactStrictMode === true }, development));
265
259
  clientEntries[route.id] = clientEntry;
266
260
  }
267
261
  manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, module: `server/${route.id}.mjs`, css: [],
@@ -270,7 +264,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
270
264
  }
271
265
  const common = { absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders, assetNames: '[name]-[hash]', publicPath: publicPaths.assetBase, metafile: true, define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...environment } };
272
266
  if (Object.keys(serverEntries).length) {
273
- const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, fonts }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
267
+ const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, locations, reactCompiler }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
274
268
  fonts.record(server.metafile);
275
269
  const serverChunks = new Set(Object.keys(server.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
276
270
  for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
@@ -285,7 +279,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
285
279
  }
286
280
  }
287
281
  if (Object.keys(clientEntries).length) {
288
- const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, refresh }), cssModules.plugin(true)] });
282
+ const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler }), cssModules.plugin(true)] });
289
283
  fonts.record(browser.metafile);
290
284
  const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
291
285
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
@@ -302,20 +296,35 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
302
296
  await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
303
297
  if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
304
298
  await prepareMetadataRoutes(project, stage);
305
- await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
299
+ await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
300
+ if(manifest.config.workerPreload)for(const pattern of config.workerPreload.routes){
301
+ const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
302
+ if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
303
+ }
304
+ await compileInstrumentation({root:project.root,stage,manifest,config,dev,environment});
305
+ if(deferredRoutes.length){
306
+ for(const route of deferredRoutes){
307
+ const module=`server/dev-deferred-${route.id}.mjs`;await writeFile(path.join(stage,module),'export {};\n');
308
+ manifest.routes.push({id:route.id,pattern:route.pattern,kind:route.kind,router:route.router,module,devDeferred:true,css:[],cacheConfig:{maxDuration:120}});
309
+ }
310
+ }
306
311
  await checkTypeScript(project.root, config, { dev, tsconfig });
307
312
  fonts.attach(manifest);
308
313
  expandLocales(manifest);
309
314
  // All runtime preparation must finish before its shared modules are built;
310
315
  // no staged renderer has been imported before this production-only point.
311
- if (!dev) await bundleRuntime(stage);
312
- await prerender(stage, manifest, dev);
313
- await prerenderApp(stage, manifest, dev);
314
- const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
316
+ if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime')});
317
+ // Node caches CJS React by filename, not NODE_ENV. Once development metadata
318
+ // imports load it, subsequent production renders need a clean process too.
319
+ if(dev) developmentModulesLoaded = true;
320
+ if(!dev) Object.assign(manifest,await (incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender}));
321
+ else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
322
+ const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal && !route.devDeferred);
315
323
  const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
316
324
  if (pages.length || manifest.pagesErrors) {
317
325
  const publicPages = {
318
326
  buildId: manifest.buildId,
327
+ ...(manifest.config.deploymentId?{deploymentId:manifest.config.deploymentId}:{}),
319
328
  ...publicPaths,
320
329
  // Header rules cannot select a different page. Other server routing
321
330
  // stays authoritative, including middleware with conditional behavior.
@@ -332,16 +341,35 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
332
341
  }
333
342
  await rm(path.join(stage, '.entries'), { recursive: true });
334
343
  if (dev) {
344
+ manifest.devStyles={bundles:devStylePlans(),dependencies:devStyleDependencies(),modules:await cssModules.mappings()};
345
+ for(const [,files]of manifest.devStyles.dependencies)for(const module of manifest.devStyles.modules)if(files.includes(module.file))for(const file of module.watchFiles)if(!files.includes(file))files.push(file);
346
+ manifest.devDependencies=devDependencies(project,manifest);
347
+ manifest.devModules=devModules(project.root);
348
+ manifest.devDependencyStatus={complete:!manifest.devDependencies.incomplete&&!manifest.devModules.incomplete,reason:manifest.devDependencies.incomplete||manifest.devModules.incomplete};
349
+ for(const route of manifest.devDependencies)for(const module of manifest.devStyles.modules){
350
+ if(route.files.includes(path.relative(project.root,module.file).replaceAll(path.sep,'/')))for(const file of module.watchFiles){
351
+ const relative=path.relative(project.root,file).replaceAll(path.sep,'/');if(!relative.startsWith('../')&&!route.files.includes(relative))route.files.push(relative);
352
+ }
353
+ }
335
354
  const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
336
355
  await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
337
356
  pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
338
- ...(appRoute ? { app: { client: appRoute.client, css: appRoute.css } } : {}),
357
+ ...(appRoute ? { app: { client: appRoute.client, css: appRoute.css, routes: manifest.routes.filter(route=>route.router==='app'&&route.kind==='page'&&route.client).map(({id,pattern,css,cssScopes})=>({id,pattern,css,cssScopes:cssScopes||[]})) } } : {}),
339
358
  }));
340
359
  }
341
- await writeRuntimeContract(stage);
360
+ // A report belongs to this successful staging generation, never to the
361
+ // runtime manifest or public assets. Publishing the stage is atomic below.
362
+ if(!dev)await retainDeploymentAssets({stage,destination,manifest,policy:config.deploymentRetention});
363
+ const compilationCache=compilationGraphCacheReport();
364
+ if(!dev)await writeFile(path.join(stage,'compilation-cache-report.json'),JSON.stringify(compilationCache,null,2)+'\n');
365
+ const compilerReport=await writeReactCompilerReport(stage,reactCompilerReport());
366
+ await writeRuntimeContract(stage,{deployment:!dev,htmlOnly:!dev&&manifest.routes.some(route=>route.htmlOnly),workerMemory:Boolean(manifest.config.workerMemory),incrementalCache:Boolean(manifest.config.cacheHandler)});
342
367
  await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
343
368
  await writeBuildOutput(stage, manifest);
344
- if (config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
369
+ // Offline compression is a deployment optimization. Recompressing source
370
+ // maps and every entry after a save stalls Fast Refresh for no local benefit.
371
+ if (!dev && config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
372
+ if(!dev)await writeBuildAnalysis({root:project.root,stage,destination,manifest,records:buildAnalysisRecords(),routeInputs:buildAnalysisRoutes(),precompressed:config.compress});
345
373
  if (config.output === 'standalone' && !dev) await (await import('./standalone-runner.mjs')).prepareStandalone({ projectRoot: project.root, stage, manifest, config });
346
374
  const exporting = config.output === 'export' && !dev ? await import('./static-export.mjs') : null;
347
375
  const exportSource = exporting && await exporting.prepareStaticExport({ project, stage, manifest, config });
@@ -365,7 +393,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
365
393
  } finally { await rm(pointerTemporary, {force:true}); }
366
394
  await exported?.commit();
367
395
  if (hadDestination) await rm(backup, { recursive: true, force: true });
368
- return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started) };
396
+ return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
369
397
  } catch (error) {
370
398
  await rm(stage, { recursive: true, force: true });
371
399
  if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
package/build/inspect.mjs CHANGED
@@ -69,6 +69,7 @@ export async function inspectProject(directory) {
69
69
  sample: { events: recent.length, from: recent[0]?.time, to: recent.at(-1)?.time },
70
70
  notes: ['Enable measurements with prn start --inspect (or prn dev --inspect), visit routes, then run prn inspect again.',
71
71
  'HTTP and fetch durations end when response headers are ready. Samples are bounded and can span server sessions; they are not full-response benchmarks.',
72
+ 'Request-stage samples additionally separate admission, worker first byte, response headers and complete body consumption, with completion/cancellation/deadline outcomes. Stages overlap; do not add their durations. Body completion means the HTTP layer consumed the application body, not remote delivery.',
72
73
  'Fetch destinations omit queries and credentials. Runtime cache tags/paths are hashed; matching identifiers link reads to invalidations. No diagnostic HTTP endpoint is exposed.'] };
73
74
  }
74
75
  export function printInspection(report) {
@@ -76,6 +77,8 @@ export function printInspection(report) {
76
77
  console.log(`Recorded sample: ${report.summary.requests} HTTP requests; p50 ${report.summary.p50Ms ?? 'n/a'} ms; p95 ${report.summary.p95Ms ?? 'n/a'} ms (response headers); ${report.summary.errors} server errors.`);
77
78
  if(report.summary.wakeP95Ms !== null) console.log(`Worker startup p95: ${report.summary.wakeP95Ms} ms.`);
78
79
  if(report.summary.workerRssBytes !== null) console.log(`Latest recorded worker RSS: ${(report.summary.workerRssBytes/1048576).toFixed(1)} MiB. Native server memory is not included.`);
80
+ for(const stage of report.requestStages||[]) console.log(`STAGE ${stage.stage}: ${stage.samples} sample(s), p95 ${stage.p95Ms ?? 'n/a'} ms; ${stage.completed} completed, ${stage.failed} failed, ${stage.cancelled} cancelled, ${stage.timedOut} timed out.`);
81
+ for(const budget of report.budgets||[])console.log(`BUDGET ${budget.budget}: ${budget.used ?? 'n/a'}/${budget.limit ?? 'n/a'} ${budget.unit}, ${budget.waiting ?? 'n/a'} waiting; ${budget.rejected ?? 'n/a'} rejected, ${budget.timedOut ?? 'n/a'} timed out, ${budget.cancelledWaits ?? 'n/a'} cancelled waits.`);
79
82
  for(const finding of report.findings)console.log(`${finding.level.toUpperCase()}: ${finding.message} ${finding.action}`);
80
83
  for (const route of report.routes) console.log(`${route.mode.padEnd(13)} ${route.pattern}\n ${route.reasons.join('; ') || `${route.prerenderedPaths} prerendered path(s); revalidate: ${route.revalidate.join(', ') || 'on demand'}`}`);
81
84
  for (const item of report.measurements) console.log(`${item.type.toUpperCase()} ${item.route}${item.destination ? ' -> ' + item.destination : ''}: ${item.count} sample(s)${item.state ? ', ' + item.state : ''}${item.reason ? ' (' + item.reason + ')' : ''}${item.p95Ms !== undefined ? ', p95 ' + item.p95Ms.toFixed(2) + ' ms' : ''}, ${item.errors} error(s)`);
@@ -1,22 +1,46 @@
1
1
  const quantile = (values, q) => values.length ? Math.round([...values].sort((a,b)=>a-b)[Math.ceil(values.length*q)-1]*100)/100 : null;
2
2
  const durations = events => events.map(e=>e.durationMs).filter(v=>Number.isFinite(v)&&v>=0);
3
+ const stages=['admission','renderStart','bodyAdmission','bodyRead','workerQueue','workerFirstByte','workerHeaders','httpHeaders','responseBody','requestTotal'];
4
+ const outcomes=['completed','failed','cancelled','timedOut'];
5
+ const budgetNames=['renderLive','apiLive','renderStart','inputBytes','responseBytes'];
6
+ const validTime=value=>Number.isSafeInteger(value)&&value>=0&&value<=18446744073709;
7
+ function summarizeStages(events) {
8
+ return stages.flatMap(stage=>{
9
+ const sample=events.filter(event=>event.stage===stage), times=durations(sample);
10
+ return sample.length?[{stage,samples:sample.length,p50Ms:quantile(times,.5),p95Ms:quantile(times,.95),...Object.fromEntries(outcomes.map(outcome=>[outcome,sample.filter(event=>event.outcome===outcome).length]))}]:[];
11
+ });
12
+ }
13
+ function currentBudgets(snapshot) {
14
+ if(!Array.isArray(snapshot?.workers?.budgets))return null;
15
+ return budgetNames.flatMap(budget=>{
16
+ const value=snapshot.workers.budgets.find(value=>value?.budget===budget);
17
+ if(!value)return [];
18
+ const row={budget,unit:budget.endsWith('Bytes')?'bytes':'requests'};
19
+ for(const key of ['limit','used','peakUsed','waiting','peakWaiting','acquired','rejected','timedOut','cancelledWaits'])row[key]=Number.isSafeInteger(value[key])&&value[key]>=0?value[key]:null;
20
+ return [row];
21
+ });
22
+ }
3
23
  export function summarizeDiagnostics(events, snapshots = []) {
4
24
  const http=events.filter(e=>e.type==='http'), times=durations(http);
5
25
  const wake=durations(events.filter(e=>e.type==='wake'&&e.phase==='node-ready'));
6
26
  const processes=new Map();
7
27
  for(const event of events)if(event.type==='process'&&Number.isSafeInteger(event.pid)&&Number.isFinite(event.rssBytes)&&event.rssBytes>=0)processes.set(event.pid,event);
8
- const last=snapshots.sort((a,b)=>b.unixMs-a.unixMs)[0];
28
+ const last=[...snapshots].sort((a,b)=>b.unixMs-a.unixMs)[0];
9
29
  const current=last&&Date.now()-last.unixMs<30000&&Date.now()>=last.unixMs;
10
30
  const rss=[...processes.values()].filter(e=>Date.now()-e.time<30000&&Date.now()>=e.time);
31
+ const stageSample=events.filter(e=>e.type==='requestStage'&&stages.includes(e.stage)&&outcomes.includes(e.outcome)&&Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&validTime(e.time)).map(({stage,outcome,durationMs,time})=>({stage,outcome,durationMs,time}));
32
+ const requestStages=summarizeStages(stageSample), totals=requestStages.find(s=>s.stage==='requestTotal');
11
33
  const summary={requests:http.length,errors:http.filter(e=>e.status>=500).length,p50Ms:quantile(times,.5),p95Ms:quantile(times,.95),wakeP95Ms:quantile(wake,.95),workerRssBytes:rss.length?rss.reduce((n,e)=>n+e.rssBytes,0):null,
12
- queuedJobs:current?last.workers.queuedJobs??null:null,activeJobs:current?last.workers.activeJobs??null:null, runtimeSampleAt:last?.unixMs??null};
34
+ queuedJobs:current?last.workers?.queuedJobs??null:null,activeJobs:current?last.workers?.activeJobs??null:null, runtimeSampleAt:last?.unixMs??null,
35
+ completedResponseP95Ms:quantile(durations(stageSample.filter(e=>e.stage==='requestTotal'&&e.outcome==='completed')),.95),cancelledResponses:totals?.cancelled??null,timedOutResponses:totals?.timedOut??null};
13
36
  const findings=[];
14
37
  if(!http.length)findings.push({level:'info',code:'NO_SAMPLE',message:'No HTTP sample recorded.',action:'Start with --inspect, visit the site and run inspect again.'});
15
38
  if(summary.queuedJobs>0)findings.push({level:'warning',code:'WORKER_QUEUE',message:`${summary.queuedJobs} jobs are waiting for a worker.`,action:'Check slow routes and available CPU/RAM before increasing concurrency.'});
16
39
  if(summary.p95Ms>=1000)findings.push({level:'warning',code:'SLOW_HEADERS',message:'The slowest 5% of recorded responses take at least one second to prepare headers.',action:'Inspect route and fetch timings below; this includes queueing and startup.'});
17
40
  if(summary.errors)findings.push({level:'warning',code:'SERVER_ERRORS',message:`${summary.errors} recorded requests returned a server error.`,action:'Check application logs for the same sample period.'});
18
41
  if(summary.wakeP95Ms>=1000)findings.push({level:'info',code:'SLOW_WAKE',message:'Recorded worker startup p95 exceeds one second.',action:'Review startup imports or use keep-warm if the extra memory is acceptable.'});
19
- return {summary,findings,metricSample:http.filter(e=>Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&Number.isSafeInteger(e.time)&&e.time>=0&&e.time<=18446744073709).map(e=>({route:typeof e.route==='string'?e.route.slice(0,256):'<unmatched>',method:/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)$/.test(e.method)?e.method:'_OTHER',durationMs:e.durationMs,time:e.time}))};
42
+ if(summary.timedOutResponses>0)findings.push({level:'warning',code:'RESPONSE_DEADLINES',message:`${summary.timedOutResponses} recorded HTTP responses reached their request deadline.`,action:'Compare admission, worker and body timings before increasing the configured timeout.'});
43
+ return {summary,findings,requestStages,budgets:current?currentBudgets(last):null,stageSample,metricSample:http.filter(e=>Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&validTime(e.time)).map(e=>({route:typeof e.route==='string'?e.route.slice(0,256):'<unmatched>',method:/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)$/.test(e.method)?e.method:'_OTHER',durationMs:e.durationMs,time:e.time}))};
20
44
  }
21
45
 
22
46
  /** OTLP/JSON Metrics export of a bounded local sample, never a complete traffic counter. */
@@ -33,5 +57,14 @@ export function inspectionMetrics(report) {
33
57
  return {attributes:[{key:'http.route',value:{stringValue:group.route}},{key:'http.request.method',value:{stringValue:group.method}}],
34
58
  startTimeUnixNano:String(BigInt(Math.trunc(group.start))*1000000n),timeUnixNano:String(BigInt(Math.trunc(group.end))*1000000n),count:String(group.values.length),sum:group.values.reduce((a,b)=>a+b,0),min:Math.min(...group.values),max:Math.max(...group.values),explicitBounds:bounds,bucketCounts:buckets.map(String)};
35
59
  });
36
- return {resourceMetrics:[{resource:{attributes:[{key:'service.name',value:{stringValue:'prnext'}},{key:'prnext.inspect.sampled',value:{boolValue:true}}]},scopeMetrics:[{scope:{name:'prnext.inspect',version:'1'},metrics:[{name:'prnext.http.response.headers.duration',description:'Bounded diagnostic sample of time until response headers; excludes response body streaming.',unit:'s',histogram:{aggregationTemporality:1,dataPoints}}]}]}]};
60
+ const stageGroups=new Map();
61
+ for(const event of report.stageSample||[]) {
62
+ if(!stages.includes(event.stage)||!outcomes.includes(event.outcome)||!Number.isFinite(event.durationMs)||event.durationMs<0||event.durationMs>86400000||!validTime(event.time))continue;
63
+ const key=JSON.stringify([event.stage,event.outcome]);
64
+ if(!stageGroups.has(key))stageGroups.set(key,{stage:event.stage,outcome:event.outcome,count:0,sum:0,min:Infinity,max:0,start:event.time,end:event.time,buckets:Array(bounds.length+1).fill(0)});
65
+ const group=stageGroups.get(key),value=event.durationMs/1000,index=bounds.findIndex(bound=>value<=bound);
66
+ group.count++;group.sum+=value;group.min=Math.min(group.min,value);group.max=Math.max(group.max,value);group.start=Math.min(group.start,event.time);group.end=Math.max(group.end,event.time);group.buckets[index<0?bounds.length:index]++;
67
+ }
68
+ const stagePoints=[...stageGroups.values()].map(g=>({attributes:[{key:'prnext.request.stage',value:{stringValue:g.stage}},{key:'prnext.request.outcome',value:{stringValue:g.outcome}}],startTimeUnixNano:String(BigInt(g.start)*1000000n),timeUnixNano:String(BigInt(g.end)*1000000n),count:String(g.count),sum:g.sum,min:g.min,max:g.max,explicitBounds:bounds,bucketCounts:g.buckets.map(String)}));
69
+ return {resourceMetrics:[{resource:{attributes:[{key:'service.name',value:{stringValue:'prnext'}},{key:'prnext.inspect.sampled',value:{boolValue:true}}]},scopeMetrics:[{scope:{name:'prnext.inspect',version:'1'},metrics:[{name:'prnext.http.response.headers.duration',description:'Bounded diagnostic sample of time until response headers; excludes response body streaming.',unit:'s',histogram:{aggregationTemporality:1,dataPoints}},{name:'prnext.request.stage.duration',description:'Bounded diagnostic sample of request stages and terminal outcomes; stages overlap and must not be added together.',unit:'s',histogram:{aggregationTemporality:1,dataPoints:stagePoints}}]}]}]};
37
70
  }
@@ -0,0 +1,30 @@
1
+ import {access,writeFile,mkdir} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {build} from './compiler.mjs';
4
+ import {compileEdge} from './edge.mjs';
5
+ import {moduleResolutionPlugin} from './module-resolution.mjs';
6
+
7
+ export async function compileInstrumentation({root,stage,manifest,config,dev,environment}) {
8
+ const found=[];
9
+ for(const directory of ['', 'src'])for(const extension of ['ts','js','mjs','cjs']){
10
+ const file=path.join(root,directory,'instrumentation.'+extension);
11
+ try{await access(file);found.push(file);}catch(error){if(error.code!=='ENOENT')throw error;}
12
+ }
13
+ if(!found.length)return;
14
+ if(found.length>1)throw Error('Only one instrumentation file is allowed, in the project root or src directory.');
15
+ const file=found[0],entry=path.join(stage,'.entries/instrumentation.mjs');manifest.instrumentation={};
16
+ await mkdir(path.dirname(entry),{recursive:true});
17
+ await writeFile(entry,`import * as hooks from ${JSON.stringify(file)};export const register=hooks.register??hooks.default?.register;export const onRequestError=hooks.onRequestError??hooks.default?.onRequestError;`);
18
+ for(const runtime of ['node','rsc']){
19
+ const module=`server/instrumentation-${runtime}.mjs`;
20
+ await build({absWorkingDir:root,entryPoints:[entry],outfile:path.join(stage,module),bundle:true,packages:'external',platform:'node',format:'esm',target:'node22',banner:{js:"import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);"},conditions:runtime==='rsc'?['react-server']:[],sourcemap:dev,logLevel:'silent',
21
+ define:{...environment,'process.env.NODE_ENV':JSON.stringify(dev?'development':'production'),'process.env.NEXT_RUNTIME':'"nodejs"'},
22
+ plugins:[moduleResolutionPlugin(config.turbopack,root)]});
23
+ manifest.instrumentation[runtime]=module;
24
+ }
25
+ if(manifest.middleware?.runtime==='edge'||manifest.routes.some(route=>route.cacheConfig?.runtime==='edge'||route.handlerConfig?.runtime==='edge')){
26
+ const module='server/instrumentation-edge.mjs';
27
+ await compileEdge({file:entry,outfile:path.join(stage,module),projectRoot:root,dev,defineEnvironment:environment});
28
+ manifest.instrumentation.edge=module;
29
+ }
30
+ }