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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +122 -46
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +34 -10
  15. package/build/config.mjs +34 -3
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
package/build/app.mjs CHANGED
@@ -17,13 +17,18 @@ import { createNativePackages } from './native-packages.mjs';
17
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';
24
+ import {createLazyDynamicImports} from './lazy-dynamic-imports.mjs';
20
25
 
21
26
  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' };
27
+ const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
23
28
  const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
24
29
  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']);
30
+ 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']);
31
+ 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
32
 
28
33
  export async function validateReactVersions(resolveFromProject) {
29
34
  const names = ['react', 'react-dom', 'react-server-dom-webpack'];
@@ -39,7 +44,7 @@ export async function validateReactVersions(resolveFromProject) {
39
44
  }
40
45
 
41
46
  function sourceAst(source, filename) {
42
- return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
47
+ return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
43
48
  }
44
49
  function clientDirective(source, filename) {
45
50
  if (!source.includes('use client')) return false;
@@ -140,7 +145,7 @@ function routeEntry(route, globalError, dev) {
140
145
  if (globalError) imports.push(`export {default as GlobalError} from ${JSON.stringify(globalError)};`);
141
146
  imports.push(`export {default as NavigationBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-navigation-boundary.cjs'))};`);
142
147
  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'))};`);
148
+ imports.push(`export {LayoutProvider,LayoutSlot,ActivitySlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
144
149
  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
150
  if (route.routing) {
146
151
  let index = 0;
@@ -173,13 +178,18 @@ function routeEntry(route, globalError, dev) {
173
178
  }
174
179
 
175
180
  /** 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 = {} }) {
181
+ export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, devActionSecret, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {},lazyDynamicImports=false,devLazyRequest,devLazyOnly=false,devLazyActivated=[] }) {
182
+ if(devLazyOnly&&(!dev||!lazyDynamicImports||!devLazyRequest))throw Error('Deferred-only compilation requires a registered development import');
177
183
  const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
178
184
  const locations = dev ? createSourceLocationTransform(project.root) : undefined;
185
+ const compileReact = reactCompiler ? createReactCompilerTransform(project.root, reactCompiler, {dev}) : undefined;
179
186
  const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
180
187
  const publicPath = assetBase + '/';
181
- const routes = project.routes.filter(route => route.router === 'app');
182
- if (project.appNotFound) {
188
+ const lazy=dev&&lazyDynamicImports?createLazyDynamicImports({root:project.root,stage,manifest,request:devLazyRequest,activated:devLazyActivated}):undefined;
189
+ const routes = devLazyOnly?[]:project.routes.filter(route => route.router === 'app');
190
+ const htmlOnly = dev ? new Set() : selectHtmlOnlyRoutes(project.routes, manifest.config.htmlOnly, manifest.config);
191
+ const htmlGraph = !dev && htmlOnly.size ? createHtmlOnlyGraph(project.root) : undefined;
192
+ if (project.appNotFound&&!devLazyOnly) {
183
193
  const file = path.join(stage, '.entries', 'app-not-found.jsx');
184
194
  await writeFile(file, `import {notFound} from 'next/navigation';export default function MissingRoute(){notFound()}`);
185
195
  const segments = project.appNotFound.segments.map(segment => ({ ...segment }));
@@ -192,11 +202,12 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
192
202
  routes.push({ ...project.appNotFound, segments, id, file, pattern: '/_not-found', kind: 'page', router: 'app', internal: true });
193
203
  manifest.appNotFound = id;
194
204
  }
195
- if (!routes.length) return;
205
+ if (!routes.length&&!devLazyOnly) return;
196
206
  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.');
197
- const actions = createServerActions({ projectRoot: project.root });
198
- function actionTransform(source, file, mode, runtime = 'nodejs') {
199
- const result = actions.transform(source, file, mode);
207
+ 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.');
208
+ const actions = createServerActions({ projectRoot: project.root,developmentSession:dev?manifest.devSession:undefined,developmentSecret:dev?devActionSecret:undefined });
209
+ function actionTransform(source, file, mode, runtime = 'nodejs', revisionSource = source) {
210
+ const result = actions.transform(source, file, mode, {revisionSource});
200
211
  if (mode !== 'browser') for (const action of actions.actions.values()) if (action.file === file) {
201
212
  (action.runtimes ||= new Set()).add(runtime);
202
213
  }
@@ -204,6 +215,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
204
215
  }
205
216
  const boundaries = new Map();
206
217
  const cssFiles = new Set();
218
+ const styleGraph = createAppStyleGraph(project.root);
207
219
  const globalErrorCssFiles = new Set();
208
220
  const globalErrorOnlyCss = new Set();
209
221
  const canonicalPaths = new Map();
@@ -229,30 +241,33 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
229
241
  throw new Error('app/global-error must be a Client Component. Add the "use client" directive.');
230
242
  }
231
243
 
232
- function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true } = {}) {
244
+ function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true, omitAllStyles = false } = {}) {
233
245
  const browser = mode === 'browser';
234
246
  const rsc = mode === 'rsc';
235
247
  return {
236
248
  name: `prnext-app-${mode}`,
237
249
  setup(esbuild) {
238
- applicationEntry(esbuild,readCompilerFile);
250
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && (browser || mode==='ssr')});
239
251
  esbuild.onResolve({ filter: /^prnext-internal:use-cache$/ }, () => browser
240
252
  ? { errors: [{ text: "'use cache' cannot enter a Client Component graph" }] }
241
253
  : { path: '../compat/use-cache.cjs', external: true });
242
254
  if (browser && omitGlobalStyles) esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
243
- 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;
255
+ 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;
244
256
  const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextAppCss: true } });
245
- if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && globalErrorOnlyCss.has(await canonicalFile(resolved.path))) return { path: resolved.path, namespace: 'prnext-app-empty' };
257
+ if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && (omitAllStyles || globalErrorOnlyCss.has(await canonicalFile(resolved.path)))) return { path: resolved.path, namespace: 'prnext-app-empty' };
246
258
  });
247
259
  esbuild.onResolve({ filter: /^prnext-internal:action-(?:crypto|client|ssr)$/ }, args => {
248
260
  const helper = args.path.slice('prnext-internal:'.length);
249
261
  if (browser && helper !== 'action-client') return { errors: [{ text: 'Server Action implementation cannot enter the browser graph.' }] };
250
262
  return browser ? { path: path.join(packageRoot, 'runtime', helper + '.mjs') } : { path: '../runtime/' + helper + '.mjs', external: true };
251
263
  });
264
+ esbuild.onResolve({ filter: /^prnext-internal:metadata-route$/ }, () => browser
265
+ ? { errors: [{ text: 'Metadata route handlers are server-only.' }] }
266
+ : { path: '../runtime/metadata-route.mjs', external: true });
252
267
  esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
253
268
  const name = frameworkImportName(args.path);
254
269
  if (!compatibility.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}).` }] };
255
- if (browser && ['headers', 'server', 'cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
270
+ 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.` }] };
256
271
  const target = path.join(packageRoot, 'compat', (name === 'compat/router' ? 'compat-router' : name === 'dynamic' ? 'app-dynamic' : rsc && name === 'navigation' ? 'navigation-server' : name) + '.cjs');
257
272
  if (!browser && (!rsc || !forcedClient.has(path.basename(target)))) return { path: '../compat/' + path.basename(target), external: true };
258
273
  return { path: target };
@@ -296,18 +311,23 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
296
311
  if (!/\.module\.(?:css|scss|sass)$/i.test(args.path)) return { path: file, namespace: 'prnext-app-empty' };
297
312
  });
298
313
  }
299
- esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
314
+ esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
300
315
  if (browser && args.path === project.document) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a Client Component.' }] };
301
316
  const original=await readCompilerFile(args.path,args.prnextSource);
302
- const source = await fonts.transform(await expandActionExports(locations?await locations(original,args.path):original, args.path, async (specifier, importer) => {
317
+ htmlGraph?.source(args.path, original);
318
+ // Only Client Component graphs use React's memo-cache runtime. Server
319
+ // Components, route handlers and Server Action graphs remain untouched.
320
+ const compiled=compileReact && (browser || mode==='ssr') ? await compileReact(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
321
+ const source = await fonts.transform(await expandActionExports(locations?await locations(compiled,args.path):compiled, args.path, async (specifier, importer) => {
303
322
  const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
304
323
  return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
305
324
  }), args.path);
306
325
  const cacheSegment = cacheSegments.get(await canonicalFile(args.path)) || '';
307
326
  const cachedSource = transformCacheComponents(source, args.path, { enabled: manifest.config.cacheComponents, handlers: manifest.config.cacheHandlers, projectRoot: project.root, mode, buildId: manifest.cacheId, segment: cacheSegment });
308
- const actionSource = actionTransform(cachedSource, args.path, mode);
327
+ const actionSource = actionTransform(cachedSource, args.path, mode, 'nodejs', original);
309
328
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
310
329
  if (rsc && (builtin || clientDirective(source, args.path))) {
330
+ htmlGraph?.issue(args.path, 'a Client Component or interactive framework component');
311
331
  let boundary = boundaries.get(args.path);
312
332
  if (!boundary) {
313
333
  const exports = await exportedNames(args.path, esbuild);
@@ -317,8 +337,9 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
317
337
  const references = boundary.exports.map((name, index) => builtin && path.basename(args.path) === 'image.cjs' && name === 'getImageProps' ? `export {getImageProps} from ${JSON.stringify(path.join(packageRoot, 'compat/image-shared.cjs'))};` : `const ref${index}=registerClientReference(function(){throw new Error(${JSON.stringify(`Cannot call client export ${name} from a Server Component (${path.relative(project.root, args.path)}).`)});},${JSON.stringify(boundary.id)},${JSON.stringify(name)});export {ref${index} as ${JSON.stringify(name)}};`);
318
338
  return { contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n${references.join('\n')}`, loader: 'js', resolveDir: path.dirname(args.path) };
319
339
  }
320
- let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server' });
321
- if (browser && refresh && !actions.isServerModule(cachedSource,args.path)) contents = await refresh(contents, args.path);
340
+ let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server',dev });
341
+ if(browser&&lazy)contents=lazy.transform(contents,args.path);
342
+ if (browser && refresh && !actions.isServerModule(cachedSource,args.path,{revisionSource:original})) contents = await refresh(contents, args.path);
322
343
  return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
323
344
  });
324
345
  },
@@ -334,7 +355,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
334
355
  const rsc = mode === 'rsc';
335
356
  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']);
336
357
  return { name: 'prnext-edge-app', setup(esbuild) {
337
- applicationEntry(esbuild,readCompilerFile);
358
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && mode==='ssr'});
338
359
  esbuild.onResolve({filter:/^prnext-internal:action-(?:crypto|ssr)$/}, args => ({path:args.path.slice('prnext-internal:'.length),namespace:'prnext-edge-action'}));
339
360
  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')}));
340
361
  esbuild.onResolve({ filter: frameworkSubpathPattern }, args => {
@@ -362,7 +383,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
362
383
  const names = await exportedNames(args.path, esbuild), name = path.basename(args.path, '.cjs');
363
384
  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') };
364
385
  });
365
- if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
386
+ if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
366
387
  const source = args.prnextSource ?? await readFile(args.path, 'utf8');
367
388
  const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
368
389
  if (!builtin && !clientDirective(source, args.path)) return;
@@ -377,13 +398,18 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
377
398
  } };
378
399
  }
379
400
  async function edgeAppBuild(file, outfile, mode, defaultExport = false) {
380
- const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport,
401
+ const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport, transformTypedHelpers:!!compileReact && mode==='ssr',
381
402
  plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
382
- transformSource: async (source, file, builder) => actionTransform(await fonts.transform(await expandActionExports(locations?await locations(source,file):source, file, async (specifier, importer) => {
383
- const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
384
- return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
385
- }), file), file, mode, 'edge') });
403
+ transformSource: async (source, file, builder, {loaderOutput=false}={}) => {
404
+ const compiled=compileReact && mode==='ssr' ? await compileReact(source,file,{loaderOutput}) : source;
405
+ return actionTransform(await fonts.transform(await expandActionExports(locations?await locations(compiled,file):compiled, file, async (specifier, importer) => {
406
+ const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
407
+ return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
408
+ }), file), file, mode, 'edge', source);
409
+ } });
386
410
  fonts.record(result.metafile);
411
+ styleGraph.record(result.metafile);
412
+ htmlGraph?.record(result.metafile);
387
413
  }
388
414
  async function serverBuild(entryPoints, outdir, mode, styleTarget = cssFiles) {
389
415
  if (!Object.keys(entryPoints).length) return;
@@ -411,6 +437,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
411
437
  }
412
438
  }
413
439
  fonts.record(output.metafile);
440
+ styleGraph.record(output.metafile);
441
+ htmlGraph?.record(output.metafile);
414
442
  for (const file of output.outputFiles) {
415
443
  await mkdir(path.dirname(file.path), { recursive: true });
416
444
  await writeFile(file.path, file.contents);
@@ -424,22 +452,25 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
424
452
  return output;
425
453
  }
426
454
 
427
- async function collectBrowserStyles(browserEntries) {
428
- if (!globalErrorId) return;
455
+ async function collectBrowserStyles(browserEntries,force=false) {
429
456
  const revision = `${boundaries.size}:${actions.actions.size}:${nativePackages.version}`;
430
- if (browserStyleRevision === revision) return;
457
+ if (!force&&browserStyleRevision === revision) return;
431
458
  // SSR intentionally erases ssr:false loaders. Discover their dependencies
432
459
  // in one browser graph, then classify styles by reachability from each root.
433
460
  const discovery = await bundle({ ...common, write: false, entryPoints: browserEntries, outdir: path.join(stage, '.browser-styles'),
434
461
  platform: 'browser', format: 'esm', target: ['es2022'], splitting: true,
435
462
  define: { 'process.env': '{}', ...common.define },
436
- plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
463
+ plugins: [...(lazy?[lazy.plugin()]:[]),moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
464
+ await lazy?.discover(discovery.metafile);
437
465
  fonts.record(discovery.metafile);
438
- const entryIds = new Map(await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [await canonicalFile(file), id])));
466
+ styleGraph.record(discovery.metafile);
467
+ const entryIds = new Map((await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [[path.resolve(file), id], [await canonicalFile(file), id]]))).flat());
439
468
  const globalRoots = [], normalRoots = [];
440
469
  for (const output of Object.values(discovery.metafile.outputs)) {
441
470
  if (!output.entryPoint) continue;
442
- const id = entryIds.get(await canonicalFile(path.resolve(project.root, output.entryPoint)));
471
+ // Dynamic entry points can contain a query/fragment identity rather
472
+ // than a physical filename. Only configured roots need classification.
473
+ const id = entryIds.get(path.resolve(project.root, output.entryPoint));
443
474
  if (id) (id === globalErrorId ? globalRoots : normalRoots).push(output.entryPoint);
444
475
  }
445
476
  async function collect(roots, target) {
@@ -477,6 +508,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
477
508
  const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
478
509
  collectInstant(route.routing);
479
510
  manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, router: 'app', module: `server/${route.id}.mjs`, css: [], ...(route.internal ? { internal: true } : {}),
511
+ ...(!dev && htmlOnly.has(route.id) ? { htmlOnly:true } : {}),
480
512
  ...(route.parallel ? { parallel: true, interception: !!route.interception } : {}),
481
513
  ...(route.cacheConfig ? { cacheConfig: route.cacheConfig } : {}),
482
514
  ...(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)) }
@@ -489,12 +521,13 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
489
521
  const browserRevision = () => `${boundaryRevision()}:${cssFiles.size}:${globalErrorCssFiles.size}:${nativePackages.version}`;
490
522
  // Route Handlers have a Node React environment and must not share RSC chunks
491
523
  // that import registerServerReference or client-reference registration.
492
- await serverBuild(handlerEntries, 'server', 'route');
524
+ if (!devLazyOnly) await serverBuild(handlerEntries, 'server', 'route');
493
525
  for (;;) {
494
526
  // Each graph consumes discoveries from the preceding one. Discovering a
495
527
  // client boundary in RSC is normal: SSR and browser below already include
496
528
  // it. Only discoveries made too late for a graph require another pass.
497
529
  const emittedActions = actionRevision();
530
+ if (!devLazyOnly) {
498
531
  for (const action of actions.actions.values()) {
499
532
  const entry = path.join(stage, '.entries', 'action-' + action.id + '.mjs');
500
533
  await writeFile(entry, `export {${JSON.stringify(action.binding)} as invoke} from ${JSON.stringify(action.file)};`);
@@ -502,8 +535,10 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
502
535
  if (!action.runtimes?.size || action.runtimes.has('nodejs')) entries['action-' + action.id] = entry;
503
536
  }
504
537
  await serverBuild(entries, 'server', 'rsc');
538
+ }
505
539
  const serverNativeVersion = nativePackages.version;
506
540
  const emittedBoundaries = boundaryRevision();
541
+ if (!devLazyOnly) {
507
542
  const ssrEntries = {};
508
543
  const globalSsrEntries = {};
509
544
  for (const boundary of boundaries.values()) {
@@ -514,54 +549,95 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
514
549
  await serverBuild(ssrEntries, 'app-ssr', 'ssr');
515
550
  for (const boundary of boundaries.values()) if (boundary.edge) await edgeAppBuild(path.join(stage, '.entries', boundary.id + '.ssr.mjs'), path.join(stage, 'app-ssr-edge', boundary.id + '.mjs'), 'ssr', boundary.exports.includes('default'));
516
551
  await serverBuild(globalSsrEntries, 'app-ssr', 'ssr', globalErrorCssFiles);
552
+ }
517
553
  const browserEntries = {};
518
554
  for (const boundary of boundaries.values()) {
519
555
  const entry = path.join(stage, '.entries', boundary.id + '.browser.mjs');
520
556
  await writeFile(entry, boundaryEntry(boundary));
521
557
  browserEntries[boundary.id] = entry;
522
558
  }
523
- await collectBrowserStyles(browserEntries);
559
+ if(!devLazyRequest)await collectBrowserStyles(browserEntries);
560
+ let lazyEntries={};
561
+ if(lazy)for(;;){
562
+ const next=await lazy.targetEntries();
563
+ if(Object.keys(next).join(',')===Object.keys(lazyEntries).join(','))break;
564
+ lazyEntries=next;
565
+ await collectBrowserStyles(devLazyRequest?lazyEntries:{...browserEntries,...lazyEntries},true);
566
+ }
524
567
  globalErrorOnlyCss.clear();
525
568
  for (const file of globalErrorCssFiles) if (!cssFiles.has(file)) globalErrorOnlyCss.add(file);
526
569
  const emittedBrowserInputs = browserRevision();
527
- if (routes.some(route => route.kind === 'page')) {
570
+ if (devLazyOnly||routes.some(route => route.kind === 'page')) {
528
571
  let globalErrorCss = [];
529
572
  if (globalErrorCssFiles.size) {
530
573
  const styleEntry = path.join(stage, '.entries', 'app-global-error-style.mjs');
531
574
  await writeFile(styleEntry, [...globalErrorCssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n'));
532
575
  const styles = await bundle({ ...common, entryPoints: { 'app-global-error-style': styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]',
533
576
  platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
534
- plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
577
+ plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] }, { role: 'styles' });
535
578
  globalErrorCss = Object.values(styles.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle));
536
579
  }
537
580
  if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
538
581
  const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
582
+ 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;
539
583
  const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
540
- 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, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
541
- const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
584
+ if(!devLazyRequest)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`);
585
+ const clientEntries = devLazyRequest?{...lazyEntries,...lazy.bridgeEntries()}:{ 'app-runtime': runtimeEntry, ...browserEntries,...lazyEntries,...(lazy?lazy.bridgeEntries():{}) };
542
586
  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,
543
- 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) })] });
587
+ define: { 'process.env': '{}', ...common.define, __PRNEXT_CACHE_COMPONENTS__: JSON.stringify(manifest.config.cacheComponents === true) }, plugins: [...(lazy?[lazy.plugin()]:[]),moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitAllStyles: !lazy }), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: () => !!lazy })] });
544
588
  fonts.record(browser.metafile);
545
589
  const entryIds = new Map(Object.entries(clientEntries).map(([id, file]) => [file, id]));
546
590
  const edgeBoundaries = new Set([...boundaries.values()].filter(boundary => boundary.edge).map(boundary => boundary.id));
547
- const styles = new Set();
548
591
  let client;
549
592
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
550
- if (metadata.cssBundle) styles.add(publicPath + path.basename(metadata.cssBundle));
551
593
  if (!metadata.entryPoint || !output.endsWith('.js')) continue;
594
+ if(lazy?.record(metadata,output))continue;
552
595
  const id = entryIds.get(path.resolve(project.root, metadata.entryPoint));
553
596
  if (id === 'app-runtime') { client = publicPath + path.basename(output); continue; }
554
597
  if (!id) continue;
555
598
  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) };
556
599
  }
557
- for (const route of manifest.routes) if (route.router === 'app' && route.kind === 'page') { route.client = client; route.css = [...styles]; }
600
+ const styleBundles = new Map();
601
+ for (const route of routes.filter(route => route.kind === 'page')) {
602
+ // All esbuild graphs canonicalize their inputs, including symlinked
603
+ // convention files. Match those identities before querying reachability.
604
+ for (const files of appStyleScopes(route)) for (const file of files) styleGraph.alias(file, await canonicalFile(file));
605
+ const plan = planAppStyleEntries(route, styleGraph);
606
+ const styles = [];
607
+ const cssScopes = [];
608
+ for (const { styles:files, path:scopePath, kind } of plan) {
609
+ const signature = JSON.stringify(files);
610
+ if (!styleBundles.has(signature)) {
611
+ 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);
612
+ const styleEntry = path.join(stage, '.entries', id + '.mjs');
613
+ await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
614
+ 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,
615
+ plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] }, { role: 'styles' });
616
+ styleBundles.set(signature, Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle)));
617
+ }
618
+ const css = styleBundles.get(signature);
619
+ styles.push(...css);
620
+ if (route.parallel) cssScopes.push({ path:scopePath, kind, css });
621
+ }
622
+ const emitted = manifest.routes.find(item => item.id === route.id);
623
+ if (!emitted.htmlOnly) emitted.client = client;
624
+ emitted.css = [...new Set(styles)];
625
+ if (route.parallel) emitted.cssScopes = cssScopes;
626
+ }
558
627
  }
559
628
  manifest.app.actions = Object.fromEntries([...actions.actions.values()].map(action => [action.id, {
560
629
  module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
561
630
  ...(action.runtimes?.has('edge') ? {edgeModule:`server/action-edge-${action.id}.mjs`} : {}),
562
631
  }]));
563
- if (emittedActions === actionRevision() && emittedBoundaries === boundaryRevision() &&
632
+ if ((devLazyOnly || emittedActions === actionRevision() && emittedBoundaries === boundaryRevision()) &&
564
633
  serverNativeVersion === nativePackages.version && emittedBrowserInputs === browserRevision()) break;
565
634
  }
566
635
 
636
+ lazy?.finish();
637
+
638
+ if (htmlGraph) for (const route of routes) if (htmlOnly.has(route.id)) {
639
+ for (const file of [...appStyleScopes(route).flat(), ...(route.metadataFiles || []).map(item=>item.file)].filter(Boolean)) htmlGraph.alias(file, await canonicalFile(file));
640
+ htmlGraph.validate(route);
641
+ }
642
+
567
643
  }
@@ -15,7 +15,7 @@ export const runtimeEntryNames = Object.freeze([
15
15
  ].sort());
16
16
 
17
17
  /** Run once, after compiling application/Edge graphs and before any prerender. */
18
- export async function bundleRuntime(stage,{projectRoot}={}) {
18
+ export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
19
19
  const resolvedStage = path.resolve(stage);
20
20
  if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
21
21
  throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
@@ -29,20 +29,31 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
29
29
  if (path.relative(canonicalStage, canonicalRuntime) !== 'runtime') {
30
30
  throw new Error('Runtime bundling escaped the build stage.');
31
31
  }
32
- const entries = await readdir(runtime, { withFileTypes: true });
33
- const originals = entries.filter(entry => entry.name.endsWith('.mjs'));
32
+ const input=sourceDirectory?await realpath(sourceDirectory):runtime;
33
+ const entries = await readdir(input, { withFileTypes: true });
34
+ const originals = entries.filter(entry => entry.name.endsWith('.mjs')&&!entry.name.endsWith('.test.mjs'));
34
35
  if (originals.some(entry => !entry.isFile() || entry.isSymbolicLink())) {
35
36
  throw new Error('Runtime source modules must be ordinary files.');
36
37
  }
37
38
  for (const name of runtimeEntryNames) {
38
39
  if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
39
40
  }
41
+ const sources=new Map();
42
+ // Production compiles the installed sources directly. Only generated stage
43
+ // overrides (currently the bundled env loader) replace an installed module.
44
+ // Hash and compile the same captured bytes; do not trust mtimes or skip app
45
+ // graph callbacks. This avoids copying hundreds of modules into every stage.
46
+ for(const entry of originals){
47
+ let file=path.join(input,entry.name);
48
+ if(sourceDirectory){const override=path.join(runtime,entry.name);const info=await lstat(override).catch(e=>{if(e.code!=='ENOENT')throw e;});if(info){if(!info.isFile()||info.isSymbolicLink())throw Error('Runtime overrides must be ordinary files');file=override;}}
49
+ sources.set(path.join(input,entry.name),await readFile(file));
50
+ }
40
51
  let directory,key,cached;
41
52
  if(projectRoot){
42
- directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v1');
53
+ directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v2');
43
54
  if(directory){
44
55
  const hash=createHash('sha256').update(esbuildVersion).update(JSON.stringify(runtimeEntryNames)).update(await readFile(new URL('./bundle-runtime.mjs',import.meta.url)));
45
- for(const entry of [...originals].sort((a,b)=>a.name.localeCompare(b.name)))hash.update(JSON.stringify([entry.name,createHash('sha256').update(await readFile(path.join(runtime,entry.name))).digest('hex')]));
56
+ for(const entry of [...originals].sort((a,b)=>a.name.localeCompare(b.name)))hash.update(JSON.stringify([entry.name,createHash('sha256').update(sources.get(path.join(input,entry.name))).digest('hex')]));
46
57
  key=hash.digest('hex');cached=await readCacheRecord(directory,key);
47
58
  }
48
59
  }
@@ -52,10 +63,10 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
52
63
  const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
53
64
  const validCache=cached&&Array.isArray(cached.files)&&cached.files.length<=128&&cached.files.every(item=>Array.isArray(item)&&item.length===2&&/^[\w-]+\.mjs$/.test(item[0])&&typeof item[1]==='string')&&new Set(cached.files.map(([name])=>name)).size===cached.files.length&&runtimeEntryNames.every(name=>cached.files.some(([file])=>file===name+'.mjs'));
54
65
  const result = validCache?null:await build({
55
- absWorkingDir: runtime,
56
- entryPoints: runtimeEntryNames.map(name => path.join(runtime, `${name}.mjs`)),
66
+ absWorkingDir: input,
67
+ entryPoints: runtimeEntryNames.map(name => path.join(input, `${name}.mjs`)),
57
68
  outdir: virtualOutput,
58
- outbase: runtime,
69
+ outbase: input,
59
70
  entryNames: '[name]',
60
71
  chunkNames: '_shared-[hash]',
61
72
  outExtension: { '.js': '.mjs' },
@@ -73,6 +84,7 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
73
84
  metafile: true,
74
85
  write: false,
75
86
  logLevel: 'silent',
87
+ plugins:[{name:'prnext-runtime-snapshot',setup(build){build.onLoad({filter:/\.mjs$/},args=>sources.has(args.path)?{contents:sources.get(args.path),loader:'js',resolveDir:path.dirname(args.path)}:undefined);}}],
76
88
  });
77
89
  const emitted = new Map();
78
90
  if(validCache)for(const [name,contents]of cached.files)emitted.set(name,Buffer.from(contents));
@@ -87,7 +99,8 @@ export async function bundleRuntime(stage,{projectRoot}={}) {
87
99
  // This is an unpublished, disposable stage. On any write failure buildProject
88
100
  // discards the stage and preserves the previous published build unchanged.
89
101
  for (const [name, contents] of emitted) await writeFile(path.join(runtime, name), contents);
90
- for (const entry of originals) {
102
+ for (const entry of await readdir(runtime,{withFileTypes:true})) {
103
+ if(!entry.name.endsWith('.mjs')||!entry.isFile())continue;
91
104
  if (!emitted.has(entry.name)) await unlink(path.join(runtime, entry.name));
92
105
  }
93
106
  // JSON, CJS, license files and the OG assets directory are never traversed or
@@ -15,7 +15,7 @@ export function transformCacheComponents(source, file, { enabled, projectRoot, m
15
15
  return cachedTransform('cache-components',source,[file,enabled,projectRoot,mode,buildId,configuredSegment,handlers],()=>transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}));
16
16
  }
17
17
  function transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}) {
18
- const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
18
+ const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
19
19
  let program;
20
20
  traverse(ast, { Program(value) { program = value; value.stop(); } });
21
21
  const moduleDirective = ast.program.directives.find(cacheDirective);
@@ -47,7 +47,7 @@ export async function prepareCacheHandlers({ config, projectRoot, stage, environ
47
47
  platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent', define: environment,
48
48
  banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" },
49
49
  plugins: [{ name: 'cache-handler-compatibility', setup(builder) {
50
- builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache|headers|server)(?:\.js)?$/ }, ({ path: specifier }) => ({ path: `../compat/${frameworkImportName(specifier)}.cjs`, external: true }));
50
+ builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache(?:\/redis)?|headers|server)(?:\.js)?$/ }, ({ path: specifier }) => ({ path: `../compat/${frameworkImportName(specifier)}.cjs`, external: true }));
51
51
  } }],
52
52
  });
53
53
  result[name] = output;
@@ -0,0 +1,53 @@
1
+ import postcss from 'postcss';
2
+ import selectors from 'postcss-selector-parser';
3
+ import values from 'postcss-value-parser';
4
+ import { createHash } from 'node:crypto';
5
+
6
+ // Names are independent of compilation order and of which routes are built.
7
+ // A collision fails the build instead of changing one of two published maps.
8
+ export function compactCssModule(css, exports, {globals = new Set(), hash = name => 'p' + createHash('sha256').update(name).digest('base64url').slice(0, 10)} = {}) {
9
+ const names = new Map(), reverse = new Map();
10
+ for (const list of Object.values(exports)) for (const name of list.split(/\s+/).filter(Boolean)) {
11
+ if (names.has(name) || globals.has(name)) continue;
12
+ const short = hash(name);
13
+ if (!/^[a-zA-Z_][\w-]*$/.test(short) || globals.has(short) && short!==name || reverse.has(short) && reverse.get(short) !== name) throw new Error('CSS Module compact-name collision.');
14
+ names.set(name, short); reverse.set(short, name);
15
+ }
16
+ const root = postcss.parse(css);
17
+ root.walkRules(rule => {
18
+ rule.selector = selectors(ast => ast.walkClasses(node => {
19
+ if (names.has(node.value)) node.value = names.get(node.value);
20
+ })).processSync(rule.selector);
21
+ });
22
+ root.walkAtRules(/^(?:-webkit-)?keyframes$/i, rule => {
23
+ const parsed = values(rule.params);
24
+ parsed.walk(node => { if (node.type === 'word' && names.has(node.value)) node.value = names.get(node.value); });
25
+ rule.params = parsed.toString();
26
+ });
27
+ root.walkDecls(/^(?:-webkit-)?animation(?:-name)?$/i, declaration => {
28
+ const parsed = values(declaration.value);
29
+ parsed.walk(node => { if (node.type === 'word' && names.has(node.value)) node.value = names.get(node.value); });
30
+ declaration.value = parsed.toString();
31
+ });
32
+ return { css: root.toString(), names:[...names], mapping: Object.fromEntries(Object.entries(exports).map(([key, list]) => [key, list.split(/\s+/).map(name => names.get(name) || name).join(' ')])) };
33
+ }
34
+
35
+ export function globalCssNames(css, {local = true} = {}, result = new Set()) {
36
+ const root = postcss.parse(css);
37
+ root.walkRules(rule => {
38
+ selectors(ast => ast.each(selector => {
39
+ let global = !local;
40
+ selector.each(node => {
41
+ if (node.type === 'pseudo' && [':global',':local'].includes(node.value)) {
42
+ if (node.nodes?.length) { if (node.value === ':global') node.walkClasses(item=>result.add(item.value)); }
43
+ else global = node.value === ':global';
44
+ } else if (global) { if(node.type==='class') result.add(node.value); node.walkClasses?.(item=>result.add(item.value)); }
45
+ });
46
+ })).processSync(rule.selector);
47
+ });
48
+ root.walkDecls(/^composes$/i, declaration => {
49
+ const words = values(declaration.value).nodes.filter(node=>node.type==='word').map(node=>node.value);
50
+ if (words.at(-1)==='global' && words.at(-2)==='from') for(const name of words.slice(0,-2)) result.add(name);
51
+ });
52
+ return result;
53
+ }