@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
@@ -3,6 +3,9 @@ import { writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { staticPath } from '../runtime/pages-paths.mjs';
6
+ import {mapConcurrent} from './concurrent.mjs';
7
+ import {mapPrerender,prerenderPath} from './prerender-pool.mjs';
8
+ import {validateHtmlOnlyPrerender} from './html-only-config.mjs';
6
9
 
7
10
  const MAX_STATIC_PATHS = 10_000;
8
11
  const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
@@ -48,7 +51,9 @@ export async function prerenderApp(stage, manifest, dev) {
48
51
  ? await import(pathToFileURL(path.join(stage, 'runtime/app-static.mjs')).href) : null;
49
52
  const handlers = routes.some(route => route.kind === 'api')
50
53
  ? await import(pathToFileURL(path.join(stage, 'runtime/route-static.mjs')).href) : null;
51
- for (const route of routes) {
54
+ const initialSeeds=[...manifest.prerendered];
55
+ const jobs=[],seeds=new Map(),partialRoutes=new Set();
56
+ await mapConcurrent(routes,async(route,routeIndex)=>{
52
57
  const handler = route.kind === 'api';
53
58
  const options = { modulePath: path.join(stage, route.module), distDir: stage, manifest, route, production: true };
54
59
  // This fixed internal entry shares the root layout, but its parameter
@@ -59,14 +64,22 @@ export async function prerenderApp(stage, manifest, dev) {
59
64
  if (handler) route.handlerMethods = inspected.methods;
60
65
  const dynamic = route.pattern.includes('[');
61
66
  const entries = appStaticEntries(route.pattern, dynamic ? inspected.params : [{}]);
67
+ if(route.htmlOnly && dynamic && (!inspected.generated || !entries.length || inspected.params.some(params=>appStaticEntries(route.pattern,[params]).length!==1))) {
68
+ throw new Error(`htmlOnly route ${route.pattern} requires nonempty complete generateStaticParams results.`);
69
+ }
62
70
  const config = route.cacheConfig;
71
+ if (config.ensureStatic === 'navigation') {
72
+ if (config.runtime === 'edge' || config.dynamic === 'force-dynamic' || config.revalidate === 0 || config.forceNoStore) throw new Error(`Route ${route.pattern}: ensureStatic='navigation' conflicts with request-time rendering configuration.`);
73
+ if (dynamic && (!inspected.generated || !entries.length || inspected.params.some(params => appStaticEntries(route.pattern, [params]).length !== 1))) throw new Error(`Route ${route.pattern}: ensureStatic='navigation' requires generateStaticParams() with complete, nonempty parameter sets.`);
74
+ }
63
75
  // Keep generated-path restrictions and method metadata for Edge routes,
64
76
  // while separating dynamic rendering from the persistent Data Cache.
65
77
  if (config.runtime === 'edge') {
66
78
  if (dynamic && config.dynamicParams === false) route.allowedPaths = entries.map(entry => entry.path);
67
- continue;
79
+ return;
68
80
  }
69
81
  const partial = !handler && manifest.config?.cacheComponents === true && (!config.dynamic || config.dynamic === 'auto');
82
+ if(partial)partialRoutes.add(route);
70
83
  if (partial && dynamic && !inspected.generated) entries.length = 0;
71
84
  if (partial && dynamic && inspected.generated && inspected.params.length === 0) {
72
85
  throw new Error(`generateStaticParams for ${route.pattern} must return at least one parameter object when cacheComponents is enabled.`);
@@ -99,17 +112,20 @@ export async function prerenderApp(stage, manifest, dev) {
99
112
  if (partial) route.pprFallback = config.dynamicParams !== false;
100
113
  if (dynamic && config.dynamicParams === false) route.allowedPaths = entries.map(entry => entry.path);
101
114
  if (handler) {
102
- if (!inspected.staticEligible) continue;
103
- if (dynamic && !inspected.generated && !['force-static', 'error'].includes(config.dynamic)) continue;
115
+ if (!inspected.staticEligible) return;
116
+ if (dynamic && !inspected.generated && !['force-static', 'error'].includes(config.dynamic)) return;
104
117
  } else {
105
- if (config.dynamic === 'force-dynamic' || (config.dynamic !== 'force-static' && (config.revalidate === 0 || config.forceNoStore))) continue;
106
- if (dynamic && !inspected.generated && !partial && !['force-static', 'error'].includes(config.dynamic)) continue;
118
+ if (config.dynamic === 'force-dynamic' || (config.dynamic !== 'force-static' && (config.revalidate === 0 || config.forceNoStore))) return;
119
+ if (dynamic && !inspected.generated && !partial && !['force-static', 'error'].includes(config.dynamic)) return;
107
120
  }
108
121
  route.ssg = true;
109
122
  route.fallback = config.dynamicParams === false ? false : 'blocking';
110
- for (const entry of entries) {
123
+ for (const [entryIndex,entry] of entries.entries()) {
111
124
  if (occupied.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
112
- const rendered = await (handler ? handlers.prerenderAppRouteHandler : pages.prerenderAppRoute)({ ...options, path: entry.path, params: entry.params });
125
+ occupied.add(entry.path);
126
+ jobs.push({route,routeIndex,entryIndex,path:entry.path,run:async()=>{
127
+ const rendered = await prerenderPath(handler?'handler':'app',{ ...options, path: entry.path, params: entry.params },handler?handlers.prerenderAppRouteHandler:pages.prerenderAppRoute);
128
+ if(route.htmlOnly)validateHtmlOnlyPrerender(route,rendered);
113
129
  if (rendered.partial) {
114
130
  const file = `static/partial-${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.json`;
115
131
  await writeFile(path.join(stage, file), JSON.stringify({ ...rendered.partial, ...rendered.metadata }));
@@ -117,7 +133,7 @@ export async function prerenderApp(stage, manifest, dev) {
117
133
  if (!dynamic) { delete route.ssg; delete route.fallback; }
118
134
  else (route.dynamicPaths ||= []).push(entry.path);
119
135
  occupied.add(entry.path);
120
- continue;
136
+ return;
121
137
  }
122
138
  if (rendered.dynamic) {
123
139
  route.dynamicReasons = [...new Set([...(route.dynamicReasons || []), rendered.reason || 'request-time data'])];
@@ -129,32 +145,43 @@ export async function prerenderApp(stage, manifest, dev) {
129
145
  // successful seeds survive, while ungenerated paths stay dynamic.
130
146
  if (handler && config.dynamicParams !== false) route.fallback = 'dynamic';
131
147
  }
132
- continue;
148
+ return;
133
149
  }
134
150
  const body = Buffer.isBuffer(rendered.body) ? rendered.body : Buffer.from(rendered.body);
135
- const flight = handler ? null : Buffer.isBuffer(rendered.flight) ? rendered.flight : Buffer.from(rendered.flight);
151
+ const flight = handler || route.htmlOnly ? null : Buffer.isBuffer(rendered.flight) ? rendered.flight : Buffer.from(rendered.flight);
136
152
  if (body.byteLength > MAX_RESPONSE_BYTES || (flight && flight.byteLength > MAX_RESPONSE_BYTES)) throw new Error(`Static App route ${entry.path} exceeds the 16 MiB response limit.`);
137
153
  const stem = `static/${handler ? 'route' : 'app'}-${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}`;
138
154
  const file = `${stem}.${handler ? 'body' : 'html'}`;
139
155
  // Flight is opaque text, not JSON; the native cache supplies text/x-component.
140
- const dataFile = handler ? undefined : `${stem}.txt`;
156
+ const dataFile = handler || route.htmlOnly ? undefined : `${stem}.txt`;
141
157
  await writeFile(path.join(stage, file), body);
142
158
  if (dataFile) await writeFile(path.join(stage, dataFile), flight);
143
159
  occupied.add(entry.path);
144
- manifest.prerendered.push({ routeId: route.id, path: entry.path, file, ...(dataFile ? { dataFile } : {}), status: rendered.status ?? 200,
160
+ seeds.set(entry.path,{ routeId: route.id, path: entry.path, file, ...(dataFile ? { dataFile } : {}), status: rendered.status ?? 200,
145
161
  headers: rendered.headers || {}, revalidate: rendered.revalidate ?? config.revalidate,
146
162
  generatedAt: rendered.generatedAt, tags: rendered.tags || [], paths: rendered.paths || [],
147
163
  });
164
+ }});
148
165
  }
166
+ });
167
+ jobs.sort((a,b)=>a.routeIndex-b.routeIndex||a.entryIndex-b.entryIndex);
168
+ await mapPrerender(jobs,job=>job.run());
169
+ manifest.prerendered=[...initialSeeds,...jobs.flatMap(job=>seeds.has(job.path)?[seeds.get(job.path)]:[])];
170
+ for(const route of routes){
171
+ const routeJobs=jobs.filter(job=>job.route===route);
172
+ if(route.dynamicPaths)route.dynamicPaths=routeJobs.filter(job=>route.dynamicPaths.includes(job.path)).map(job=>job.path);
173
+ if(route.ppr)route.ppr=Object.fromEntries(routeJobs.filter(job=>Object.hasOwn(route.ppr,job.path)).map(job=>[job.path,route.ppr[job.path]]));
174
+ if(route.dynamicReasons)route.dynamicReasons=[...new Set(route.dynamicReasons)].sort();
149
175
  // Unseen Cache Components paths are generated by the PPR runtime, which
150
176
  // handles either complete HTML/Flight or a resumable shell atomically.
151
- if (partial && dynamic) {
177
+ if (partialRoutes.has(route) && route.pattern.includes('[')) {
152
178
  if (manifest.prerendered.some(page => page.routeId === route.id)) {
153
179
  // Concrete complete seeds use the native invalidation-aware cache.
154
180
  // Other paths still reach the PPR runtime, including dynamic holes.
155
181
  route.ssg = true; route.fallback = 'ppr';
156
182
  } else { delete route.ssg; delete route.fallback; }
157
183
  }
184
+ if(route.htmlOnly)route.fallback=false;
158
185
  }
159
186
  } finally { await closeAppRuntime(); }
160
187
  }
@@ -0,0 +1,79 @@
1
+ import path from 'node:path';
2
+
3
+ const stylesheet = /\.(?:css|scss|sass)$/i;
4
+ const conventions = ['layout', 'template', 'loading', 'error', 'notFound', 'not-found', 'default', 'page'];
5
+ // Native esbuild identifies virtual inputs with absolute paths. The webpack
6
+ // bridge uses the same namespaces with project-relative paths instead. Only
7
+ // strip framework namespaces here; a Windows drive letter is part of the path.
8
+ const styleNamespace = /^prnext-(?:css-mapping|compiled-css|app-empty|edge-empty):/;
9
+
10
+ /** Join RSC references with their real client dependency graphs, without
11
+ * flattening CSS @import/composes dependencies into extra stylesheets. */
12
+ export function createAppStyleGraph(root) {
13
+ const graph = new Map();
14
+ const file = name => path.resolve(root, name.replace(styleNamespace, ''));
15
+ return {
16
+ alias(source, target) {
17
+ if (file(source) !== file(target)) graph.set(file(source), new Set([file(target)]));
18
+ },
19
+ record(metafile) {
20
+ for (const [name, input] of Object.entries(metafile?.inputs || {})) {
21
+ const key = file(name), dependencies = graph.get(key) || new Set();
22
+ for (const dependency of input.imports || []) if (!dependency.external) dependencies.add(file(dependency.path));
23
+ graph.set(key, dependencies);
24
+ }
25
+ },
26
+ collect(roots) {
27
+ const seen = new Set(), styles = new Set();
28
+ const pending = [...roots].reverse();
29
+ while (pending.length) {
30
+ const key = file(pending.pop());
31
+ if (seen.has(key)) continue;
32
+ seen.add(key);
33
+ if (stylesheet.test(key)) { styles.add(key); continue; }
34
+ const dependencies = [...graph.get(key) || []];
35
+ for (let index = dependencies.length - 1; index >= 0; index--) pending.push(dependencies[index]);
36
+ }
37
+ return [...styles];
38
+ },
39
+ };
40
+ }
41
+
42
+ /** Layout scopes precede their descendants. Shared scopes produce the same
43
+ * content-addressed sheet on every route, while a page never owns a sibling's
44
+ * styles. A pruned parallel tree also includes its loading/error/default and
45
+ * intercepted branches so a retained slot cannot lose its stylesheet. */
46
+ export function appStyleEntries(route) {
47
+ const scopes = [];
48
+ function add(files, path) {
49
+ const roots = conventions.filter(name => name !== 'page' && name !== 'default').map(name => files?.[name]).filter(file => typeof file === 'string');
50
+ if (roots.length) scopes.push({ path, kind:'layout', roots });
51
+ for (const kind of ['default', 'page']) if (typeof files?.[kind] === 'string') scopes.push({ path, kind, roots:[files[kind]] });
52
+ }
53
+ if (route.routing) {
54
+ function visit(node) {
55
+ add(node.files, node.path || '');
56
+ for (const child of node.children || []) visit(child);
57
+ for (const name of Object.keys(node.slots || {}).sort()) visit(node.slots[name]);
58
+ }
59
+ visit(route.routing);
60
+ } else {
61
+ for (const segment of route.segments || []) add(segment, segment.path || '');
62
+ if (route.file) add({ page: route.file }, '');
63
+ }
64
+ return scopes;
65
+ }
66
+
67
+ export const appStyleScopes = route => appStyleEntries(route).map(scope => scope.roots);
68
+
69
+ export function planAppStyleEntries(route, graph) {
70
+ const included = new Set(), groups = [];
71
+ for (const scope of appStyleEntries(route)) {
72
+ const styles = graph.collect(scope.roots).filter(file => !included.has(file));
73
+ for (const file of styles) included.add(file);
74
+ if (styles.length) groups.push({ ...scope, styles });
75
+ }
76
+ return groups;
77
+ }
78
+
79
+ export const planAppStyles = (route, graph) => planAppStyleEntries(route, graph).map(scope => scope.styles);
package/build/app.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import {frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
1
+ import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
2
2
  import { moduleResolutionPlugin } from './module-resolution.mjs';
3
- import { build as bundle, compilerSource } from './compiler.mjs';
3
+ import { build as bundle, compilerSource,readCompilerFile } from './compiler.mjs';
4
4
  import { readFile, writeFile, mkdir, cp, realpath } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { createHash } from 'node:crypto';
@@ -14,16 +14,20 @@ import { createServerActions } from './actions.mjs';
14
14
  import { expandActionExports } from './action-exports.mjs';
15
15
  import { compileEdge } from './edge.mjs';
16
16
  import { createNativePackages } from './native-packages.mjs';
17
- import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
17
+ import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
18
18
  import { npmPackagesPlugin } from './npm-packages.mjs';
19
19
  import { transformCacheComponents } from './cache-components.mjs';
20
+ import {createReactCompilerTransform} from './react-compiler.mjs';
21
+ import {createAppStyleGraph, appStyleScopes, planAppStyleEntries} from './app-styles.mjs';
22
+ import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
23
+ import {createHtmlOnlyGraph} from './html-only-graph.mjs';
20
24
 
21
25
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
- const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
26
+ const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
23
27
  const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
24
28
  const builtins = new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)]);
25
- const compatibility = new Set(['link', 'image', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'navigation', 'headers', 'server', 'cache', 'dynamic', 'script', 'og']);
26
- const forcedClient = new Set(['link.cjs', 'image.cjs', 'head.cjs', 'router.cjs', 'compat-router.cjs', 'next-router-context.cjs', 'next-app-router-context.cjs', 'app-context.cjs', 'script.cjs']);
29
+ const compatibility = new Set(['link', 'image', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'navigation', 'headers', 'server', 'cache', 'dynamic', 'script', 'og', 'offline', 'redis-cache']);
30
+ const forcedClient = new Set(['link.cjs', 'image.cjs', 'head.cjs', 'router.cjs', 'compat-router.cjs', 'next-router-context.cjs', 'next-app-router-context.cjs', 'app-context.cjs', 'script.cjs', 'offline.cjs']);
27
31
 
28
32
  export async function validateReactVersions(resolveFromProject) {
29
33
  const names = ['react', 'react-dom', 'react-server-dom-webpack'];
@@ -39,7 +43,7 @@ export async function validateReactVersions(resolveFromProject) {
39
43
  }
40
44
 
41
45
  function sourceAst(source, filename) {
42
- return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
46
+ return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
43
47
  }
44
48
  function clientDirective(source, filename) {
45
49
  if (!source.includes('use client')) return false;
@@ -140,7 +144,7 @@ function routeEntry(route, globalError, dev) {
140
144
  if (globalError) imports.push(`export {default as GlobalError} from ${JSON.stringify(globalError)};`);
141
145
  imports.push(`export {default as NavigationBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-navigation-boundary.cjs'))};`);
142
146
  imports.push(`export {ClientPageRoot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-client-page.cjs'))};`);
143
- imports.push(`export {LayoutProvider,LayoutSlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
147
+ imports.push(`export {LayoutProvider,LayoutSlot,ActivitySlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
144
148
  if (route.routing || route.segments.some(segment => segment.error)) imports.push(`export {default as ErrorBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-error-boundary.cjs'))};`);
145
149
  if (route.routing) {
146
150
  let index = 0;
@@ -173,11 +177,15 @@ function routeEntry(route, globalError, dev) {
173
177
  }
174
178
 
175
179
  /** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
176
- export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
177
- const refresh = dev ? createRefreshTransform(project.root) : undefined;
180
+ export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
181
+ const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
182
+ const locations = dev ? createSourceLocationTransform(project.root) : undefined;
183
+ const compileReact = reactCompiler ? createReactCompilerTransform(project.root, reactCompiler, {dev}) : undefined;
178
184
  const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
179
185
  const publicPath = assetBase + '/';
180
186
  const routes = project.routes.filter(route => route.router === 'app');
187
+ const htmlOnly = dev ? new Set() : selectHtmlOnlyRoutes(project.routes, manifest.config.htmlOnly, manifest.config);
188
+ const htmlGraph = !dev && htmlOnly.size ? createHtmlOnlyGraph(project.root) : undefined;
181
189
  if (project.appNotFound) {
182
190
  const file = path.join(stage, '.entries', 'app-not-found.jsx');
183
191
  await writeFile(file, `import {notFound} from 'next/navigation';export default function MissingRoute(){notFound()}`);
@@ -193,6 +201,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
193
201
  }
194
202
  if (!routes.length) return;
195
203
  if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.instant !== undefined || route.pageConfig?.instant !== undefined || route.segments?.some(segment => segment.staticConfig?.instant !== undefined))) throw new Error('instant requires cacheComponents: true.');
204
+ if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.ensureStatic !== undefined || route.pageConfig?.ensureStatic !== undefined || route.segments?.some(segment => segment.staticConfig?.ensureStatic !== undefined))) throw new Error('ensureStatic requires cacheComponents: true.');
196
205
  const actions = createServerActions({ projectRoot: project.root });
197
206
  function actionTransform(source, file, mode, runtime = 'nodejs') {
198
207
  const result = actions.transform(source, file, mode);
@@ -203,6 +212,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
203
212
  }
204
213
  const boundaries = new Map();
205
214
  const cssFiles = new Set();
215
+ const styleGraph = createAppStyleGraph(project.root);
206
216
  const globalErrorCssFiles = new Set();
207
217
  const globalErrorOnlyCss = new Set();
208
218
  const canonicalPaths = new Map();
@@ -228,29 +238,33 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
228
238
  throw new Error('app/global-error must be a Client Component. Add the "use client" directive.');
229
239
  }
230
240
 
231
- function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true } = {}) {
241
+ function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true, omitAllStyles = false } = {}) {
232
242
  const browser = mode === 'browser';
233
243
  const rsc = mode === 'rsc';
234
244
  return {
235
245
  name: `prnext-app-${mode}`,
236
246
  setup(esbuild) {
247
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && (browser || mode==='ssr')});
237
248
  esbuild.onResolve({ filter: /^prnext-internal:use-cache$/ }, () => browser
238
249
  ? { errors: [{ text: "'use cache' cannot enter a Client Component graph" }] }
239
250
  : { path: '../compat/use-cache.cjs', external: true });
240
251
  if (browser && omitGlobalStyles) esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
241
- if (!globalErrorOnlyCss.size || /\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss || args.kind === 'import-rule' || args.path.startsWith('prnext-css:')) return;
252
+ if ((!omitAllStyles && !globalErrorOnlyCss.size) || /\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss || args.kind === 'import-rule' || args.path.startsWith('prnext-css:')) return;
242
253
  const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextAppCss: true } });
243
- if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && globalErrorOnlyCss.has(await canonicalFile(resolved.path))) return { path: resolved.path, namespace: 'prnext-app-empty' };
254
+ if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && (omitAllStyles || globalErrorOnlyCss.has(await canonicalFile(resolved.path)))) return { path: resolved.path, namespace: 'prnext-app-empty' };
244
255
  });
245
256
  esbuild.onResolve({ filter: /^prnext-internal:action-(?:crypto|client|ssr)$/ }, args => {
246
257
  const helper = args.path.slice('prnext-internal:'.length);
247
258
  if (browser && helper !== 'action-client') return { errors: [{ text: 'Server Action implementation cannot enter the browser graph.' }] };
248
259
  return browser ? { path: path.join(packageRoot, 'runtime', helper + '.mjs') } : { path: '../runtime/' + helper + '.mjs', external: true };
249
260
  });
261
+ esbuild.onResolve({ filter: /^prnext-internal:metadata-route$/ }, () => browser
262
+ ? { errors: [{ text: 'Metadata route handlers are server-only.' }] }
263
+ : { path: '../runtime/metadata-route.mjs', external: true });
250
264
  esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
251
265
  const name = frameworkImportName(args.path);
252
266
  if (!compatibility.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}).` }] };
253
- if (browser && ['headers', 'server', 'cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
267
+ if (browser && ['headers', 'server', 'cache', 'og', 'redis-cache'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
254
268
  const target = path.join(packageRoot, 'compat', (name === 'compat/router' ? 'compat-router' : name === 'dynamic' ? 'app-dynamic' : rsc && name === 'navigation' ? 'navigation-server' : name) + '.cjs');
255
269
  if (!browser && (!rsc || !forcedClient.has(path.basename(target)))) return { path: '../compat/' + path.basename(target), external: true };
256
270
  return { path: target };
@@ -294,9 +308,14 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
294
308
  if (!/\.module\.(?:css|scss|sass)$/i.test(args.path)) return { path: file, namespace: 'prnext-app-empty' };
295
309
  });
296
310
  }
297
- esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
311
+ esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
298
312
  if (browser && args.path === project.document) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a Client Component.' }] };
299
- const source = await fonts.transform(await expandActionExports(args.prnextSource ?? await readFile(args.path, 'utf8'), args.path, async (specifier, importer) => {
313
+ const original=await readCompilerFile(args.path,args.prnextSource);
314
+ htmlGraph?.source(args.path, original);
315
+ // Only Client Component graphs use React's memo-cache runtime. Server
316
+ // Components, route handlers and Server Action graphs remain untouched.
317
+ const compiled=compileReact && (browser || mode==='ssr') ? await compileReact(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
318
+ const source = await fonts.transform(await expandActionExports(locations?await locations(compiled,args.path):compiled, args.path, async (specifier, importer) => {
300
319
  const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
301
320
  return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
302
321
  }), args.path);
@@ -305,6 +324,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
305
324
  const actionSource = actionTransform(cachedSource, args.path, mode);
306
325
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
307
326
  if (rsc && (builtin || clientDirective(source, args.path))) {
327
+ htmlGraph?.issue(args.path, 'a Client Component or interactive framework component');
308
328
  let boundary = boundaries.get(args.path);
309
329
  if (!boundary) {
310
330
  const exports = await exportedNames(args.path, esbuild);
@@ -315,7 +335,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
315
335
  return { contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n${references.join('\n')}`, loader: 'js', resolveDir: path.dirname(args.path) };
316
336
  }
317
337
  let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server' });
318
- if (browser && refresh) contents = await refresh(contents, args.path);
338
+ if (browser && refresh && !actions.isServerModule(cachedSource,args.path)) contents = await refresh(contents, args.path);
319
339
  return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
320
340
  });
321
341
  },
@@ -331,6 +351,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
331
351
  const rsc = mode === 'rsc';
332
352
  const framework = new Set(['link', 'image', 'head', 'router', 'compat-router', 'next-router-context', 'next-app-router-context', 'navigation', 'script', 'app-context', 'app-layout-context', 'app-navigation-boundary', 'app-client-page', 'app-error-boundary', 'app-dynamic']);
333
353
  return { name: 'prnext-edge-app', setup(esbuild) {
354
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && mode==='ssr'});
334
355
  esbuild.onResolve({filter:/^prnext-internal:action-(?:crypto|ssr)$/}, args => ({path:args.path.slice('prnext-internal:'.length),namespace:'prnext-edge-action'}));
335
356
  esbuild.onLoad({filter:/.*/,namespace:'prnext-edge-action'}, args => ({loader:'js',contents:(args.path==='action-crypto'?['encryptBoundArgs','decryptBoundArgs','bindEncryptedReference']:['createServerReference']).map(name=>`export const ${name}=globalThis.__PRNEXT_EDGE_ACTIONS[${JSON.stringify(args.path)}][${JSON.stringify(name)}];`).join('\n')}));
336
357
  esbuild.onResolve({ filter: frameworkSubpathPattern }, args => {
@@ -358,7 +379,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
358
379
  const names = await exportedNames(args.path, esbuild), name = path.basename(args.path, '.cjs');
359
380
  return { loader: 'js', contents: `const module=globalThis.__PRNEXT_EDGE_FRAMEWORK[${JSON.stringify(name)}];\n` + [...names].map((name, index) => `const value${index}=${name === 'default' ? '(module.default||module)' : `module[${JSON.stringify(name)}]`};export {value${index} as ${JSON.stringify(name)}};`).join('\n') };
360
381
  });
361
- if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
382
+ if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
362
383
  const source = args.prnextSource ?? await readFile(args.path, 'utf8');
363
384
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
364
385
  if (!builtin && !clientDirective(source, args.path)) return;
@@ -373,13 +394,18 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
373
394
  } };
374
395
  }
375
396
  async function edgeAppBuild(file, outfile, mode, defaultExport = false) {
376
- const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport,
397
+ const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport, transformTypedHelpers:!!compileReact && mode==='ssr',
377
398
  plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
378
- transformSource: async (source, file, builder) => actionTransform(await fonts.transform(await expandActionExports(source, file, async (specifier, importer) => {
379
- const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
380
- return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
381
- }), file), file, mode, 'edge') });
399
+ transformSource: async (source, file, builder, {loaderOutput=false}={}) => {
400
+ const compiled=compileReact && mode==='ssr' ? await compileReact(source,file,{loaderOutput}) : source;
401
+ return actionTransform(await fonts.transform(await expandActionExports(locations?await locations(compiled,file):compiled, file, async (specifier, importer) => {
402
+ const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
403
+ return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
404
+ }), file), file, mode, 'edge');
405
+ } });
382
406
  fonts.record(result.metafile);
407
+ styleGraph.record(result.metafile);
408
+ htmlGraph?.record(result.metafile);
383
409
  }
384
410
  async function serverBuild(entryPoints, outdir, mode, styleTarget = cssFiles) {
385
411
  if (!Object.keys(entryPoints).length) return;
@@ -407,6 +433,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
407
433
  }
408
434
  }
409
435
  fonts.record(output.metafile);
436
+ styleGraph.record(output.metafile);
437
+ htmlGraph?.record(output.metafile);
410
438
  for (const file of output.outputFiles) {
411
439
  await mkdir(path.dirname(file.path), { recursive: true });
412
440
  await writeFile(file.path, file.contents);
@@ -421,7 +449,6 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
421
449
  }
422
450
 
423
451
  async function collectBrowserStyles(browserEntries) {
424
- if (!globalErrorId) return;
425
452
  const revision = `${boundaries.size}:${actions.actions.size}:${nativePackages.version}`;
426
453
  if (browserStyleRevision === revision) return;
427
454
  // SSR intentionally erases ssr:false loaders. Discover their dependencies
@@ -431,6 +458,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
431
458
  define: { 'process.env': '{}', ...common.define },
432
459
  plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
433
460
  fonts.record(discovery.metafile);
461
+ styleGraph.record(discovery.metafile);
434
462
  const entryIds = new Map(await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [await canonicalFile(file), id])));
435
463
  const globalRoots = [], normalRoots = [];
436
464
  for (const output of Object.values(discovery.metafile.outputs)) {
@@ -473,6 +501,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
473
501
  const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
474
502
  collectInstant(route.routing);
475
503
  manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, router: 'app', module: `server/${route.id}.mjs`, css: [], ...(route.internal ? { internal: true } : {}),
504
+ ...(!dev && htmlOnly.has(route.id) ? { htmlOnly:true } : {}),
476
505
  ...(route.parallel ? { parallel: true, interception: !!route.interception } : {}),
477
506
  ...(route.cacheConfig ? { cacheConfig: route.cacheConfig } : {}),
478
507
  ...(route.kind === 'page' ? { instantSamples: [route.pageConfig, ...[...route.segments].reverse().map(segment => segment.staticConfig)].find(config => config?.instant?.unstable_samples)?.instant.unstable_samples, instantBuild: instantConfigs.some(config => config?.instant?.level === 'experimental-error'), hasStaticParams: route.id !== manifest.appNotFound && !!(route.pageConfig?.generateStaticParams || route.segments.some(segment => segment.staticConfig?.generateStaticParams)) }
@@ -481,11 +510,16 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
481
510
  }
482
511
  manifest.app = { clientModules: {}, actions: {}, actionKey: actions.actionKey };
483
512
  const actionRevision = () => [...actions.actions.values()].map(action=>action.id+':'+[...(action.runtimes || [])].sort().join(',')).join(';');
513
+ const boundaryRevision = () => [...boundaries.values()].map(boundary=>boundary.id+':'+!!boundary.edge).join(';');
514
+ const browserRevision = () => `${boundaryRevision()}:${cssFiles.size}:${globalErrorCssFiles.size}:${nativePackages.version}`;
484
515
  // Route Handlers have a Node React environment and must not share RSC chunks
485
516
  // that import registerServerReference or client-reference registration.
486
517
  await serverBuild(handlerEntries, 'server', 'route');
487
518
  for (;;) {
488
- const revision = `${actionRevision()}:${boundaries.size}:${cssFiles.size}:${globalErrorCssFiles.size}`;
519
+ // Each graph consumes discoveries from the preceding one. Discovering a
520
+ // client boundary in RSC is normal: SSR and browser below already include
521
+ // it. Only discoveries made too late for a graph require another pass.
522
+ const emittedActions = actionRevision();
489
523
  for (const action of actions.actions.values()) {
490
524
  const entry = path.join(stage, '.entries', 'action-' + action.id + '.mjs');
491
525
  await writeFile(entry, `export {${JSON.stringify(action.binding)} as invoke} from ${JSON.stringify(action.file)};`);
@@ -493,6 +527,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
493
527
  if (!action.runtimes?.size || action.runtimes.has('nodejs')) entries['action-' + action.id] = entry;
494
528
  }
495
529
  await serverBuild(entries, 'server', 'rsc');
530
+ const serverNativeVersion = nativePackages.version;
531
+ const emittedBoundaries = boundaryRevision();
496
532
  const ssrEntries = {};
497
533
  const globalSsrEntries = {};
498
534
  for (const boundary of boundaries.values()) {
@@ -512,6 +548,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
512
548
  await collectBrowserStyles(browserEntries);
513
549
  globalErrorOnlyCss.clear();
514
550
  for (const file of globalErrorCssFiles) if (!cssFiles.has(file)) globalErrorOnlyCss.add(file);
551
+ const emittedBrowserInputs = browserRevision();
515
552
  if (routes.some(route => route.kind === 'page')) {
516
553
  let globalErrorCss = [];
517
554
  if (globalErrorCssFiles.size) {
@@ -524,31 +561,62 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
524
561
  }
525
562
  if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
526
563
  const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
564
+ const htmlOnlyRoutes = !dev && htmlOnly.size ? routes.filter(route => route.kind === 'page' && !route.internal).map(route => ({pattern:route.pattern,...(htmlOnly.has(route.id)?{htmlOnly:true}:{})})) : undefined;
527
565
  const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
528
- await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}${[...cssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n')}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, trailingSlash, skipTrailingSlashRedirect, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient } } : {}) })}});\n`);
566
+ await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, htmlOnlyRoutes, trailingSlash, skipTrailingSlashRedirect, deploymentId: dev ? undefined : manifest.cacheId, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, partialPrefetching: manifest.config.partialPrefetching === true, navigationCache: manifest.config.navigationCache, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
529
567
  const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
530
568
  const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'app-chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
531
- define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser'), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: file => !globalErrorOnlyCss.has(file) })] });
569
+ define: { 'process.env': '{}', ...common.define, __PRNEXT_CACHE_COMPONENTS__: JSON.stringify(manifest.config.cacheComponents === true) }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitAllStyles: true }), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: () => false })] });
532
570
  fonts.record(browser.metafile);
533
571
  const entryIds = new Map(Object.entries(clientEntries).map(([id, file]) => [file, id]));
534
572
  const edgeBoundaries = new Set([...boundaries.values()].filter(boundary => boundary.edge).map(boundary => boundary.id));
535
- const styles = new Set();
536
573
  let client;
537
574
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
538
- if (metadata.cssBundle) styles.add(publicPath + path.basename(metadata.cssBundle));
539
575
  if (!metadata.entryPoint || !output.endsWith('.js')) continue;
540
576
  const id = entryIds.get(path.resolve(project.root, metadata.entryPoint));
541
577
  if (id === 'app-runtime') { client = publicPath + path.basename(output); continue; }
542
578
  if (!id) continue;
543
579
  manifest.app.clientModules[id] = { id, chunks: [id, publicPath + path.basename(output)], name: '*', ssrModule: `app-ssr/${id}.mjs`, ...(edgeBoundaries.has(id) ? { edgeSsrModule: `app-ssr-edge/${id}.mjs` } : {}), browserModule: publicPath + path.basename(output) };
544
580
  }
545
- for (const route of manifest.routes) if (route.router === 'app' && route.kind === 'page') { route.client = client; route.css = [...styles]; }
581
+ const styleBundles = new Map();
582
+ for (const route of routes.filter(route => route.kind === 'page')) {
583
+ // All esbuild graphs canonicalize their inputs, including symlinked
584
+ // convention files. Match those identities before querying reachability.
585
+ for (const files of appStyleScopes(route)) for (const file of files) styleGraph.alias(file, await canonicalFile(file));
586
+ const plan = planAppStyleEntries(route, styleGraph);
587
+ const styles = [];
588
+ const cssScopes = [];
589
+ for (const { styles:files, path:scopePath, kind } of plan) {
590
+ const signature = JSON.stringify(files);
591
+ if (!styleBundles.has(signature)) {
592
+ const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
593
+ const styleEntry = path.join(stage, '.entries', id + '.mjs');
594
+ await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
595
+ const output = await bundle({ ...common, entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
596
+ plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
597
+ styleBundles.set(signature, Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle)));
598
+ }
599
+ const css = styleBundles.get(signature);
600
+ styles.push(...css);
601
+ if (route.parallel) cssScopes.push({ path:scopePath, kind, css });
602
+ }
603
+ const emitted = manifest.routes.find(item => item.id === route.id);
604
+ if (!emitted.htmlOnly) emitted.client = client;
605
+ emitted.css = [...new Set(styles)];
606
+ if (route.parallel) emitted.cssScopes = cssScopes;
607
+ }
546
608
  }
547
609
  manifest.app.actions = Object.fromEntries([...actions.actions.values()].map(action => [action.id, {
548
610
  module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
549
611
  ...(action.runtimes?.has('edge') ? {edgeModule:`server/action-edge-${action.id}.mjs`} : {}),
550
612
  }]));
551
- if (revision === `${actionRevision()}:${boundaries.size}:${cssFiles.size}:${globalErrorCssFiles.size}`) break;
613
+ if (emittedActions === actionRevision() && emittedBoundaries === boundaryRevision() &&
614
+ serverNativeVersion === nativePackages.version && emittedBrowserInputs === browserRevision()) break;
615
+ }
616
+
617
+ if (htmlGraph) for (const route of routes) if (htmlOnly.has(route.id)) {
618
+ for (const file of [...appStyleScopes(route).flat(), ...(route.metadataFiles || []).map(item=>item.file)].filter(Boolean)) htmlGraph.alias(file, await canonicalFile(file));
619
+ htmlGraph.validate(route);
552
620
  }
553
621
 
554
622
  }
@@ -11,7 +11,7 @@ function alive(pid) {
11
11
  }
12
12
  async function owner(directory) {
13
13
  const info = await lstat(directory);
14
- if (!info.isDirectory() || info.isSymbolicLink()) throw busy('The build lock is not a real directory. Check .prnext-build.lock before retrying.');
14
+ if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
15
15
  const file = await open(path.join(directory, 'owner.json'), 'r');
16
16
  try {
17
17
  if ((await file.stat()).size > 4096) throw new Error('oversized owner');
@@ -22,8 +22,8 @@ async function owner(directory) {
22
22
  }
23
23
 
24
24
  /** One publisher per canonical project, covering React and direct static imports. */
25
- export async function withBuildLock(directory, action) {
26
- const root = await realpath(directory), lock = path.join(root, LOCK);
25
+ export async function withBuildLock(directory, action, {development = false} = {}) {
26
+ const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
27
27
  const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
28
28
  const prepared = path.join(root, `.prnext-lock-${identity.token}`);
29
29
  await mkdir(prepared, { mode: 0o700 });
@@ -37,16 +37,16 @@ export async function withBuildLock(directory, action) {
37
37
  try { previous = await owner(lock); }
38
38
  catch (cause) {
39
39
  if (cause.code === 'ENOENT' && attempt < 8) continue;
40
- throw busy('Cannot verify the build lock. Check .prnext-build.lock before retrying.');
40
+ throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
41
41
  }
42
- if (previous.host !== identity.host || alive(previous.pid)) throw busy(`A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
42
+ if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
43
43
  // Exactly one contender can recover this generation. Never delete a lock
44
44
  // merely because it is old: long builds and suspended processes are valid.
45
45
  let claim;
46
46
  try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
47
47
  catch (cause) {
48
48
  if (cause.code === 'ENOENT' && attempt < 8) continue;
49
- throw busy('Build lock recovery is in progress. Retry shortly; if recovery was interrupted, check .prnext-build.lock manually.');
49
+ throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
50
50
  }
51
51
  const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
52
52
  try {