@thomas.f/prnext 0.1.17-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 (137) hide show
  1. package/README.md +26 -869
  2. package/build/actions.mjs +1 -1
  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 +81 -26
  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 +31 -8
  15. package/build/config.mjs +30 -1
  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-update.mjs +4 -2
  21. package/build/dev-dependencies.mjs +1 -1
  22. package/build/dev-discovery.mjs +3 -3
  23. package/build/dev.mjs +15 -8
  24. package/build/dynamic.mjs +1 -1
  25. package/build/edge.mjs +3 -3
  26. package/build/fonts.mjs +1 -1
  27. package/build/framework-imports.mjs +15 -5
  28. package/build/html-only-config.mjs +53 -0
  29. package/build/html-only-graph.mjs +67 -0
  30. package/build/index.mjs +59 -26
  31. package/build/instrumentation.mjs +30 -0
  32. package/build/metadata-routes.mjs +1 -1
  33. package/build/migration-audit.mjs +1 -1
  34. package/build/native-type-config.mjs +37 -0
  35. package/build/persistent-cache.mjs +18 -3
  36. package/build/precompress.mjs +1 -1
  37. package/build/prerender-cache.mjs +70 -0
  38. package/build/prerender-config.mjs +29 -0
  39. package/build/prerender-pages.mjs +19 -9
  40. package/build/prerender-pool.mjs +165 -0
  41. package/build/prerender-process-worker.mjs +58 -0
  42. package/build/prerender-worker.mjs +5 -5
  43. package/build/production-builder.mjs +58 -9
  44. package/build/react-compiler-report.mjs +225 -0
  45. package/build/react-compiler.mjs +139 -0
  46. package/build/runtime-contract.mjs +2 -2
  47. package/build/static-export.mjs +4 -4
  48. package/build/styles.mjs +6 -4
  49. package/build/transform.mjs +1 -1
  50. package/build/typed-routes.mjs +4 -0
  51. package/build/typescript-dev.mjs +35 -8
  52. package/build/typescript-native-worker.mjs +55 -0
  53. package/build/typescript-native.mjs +18 -0
  54. package/build/typescript-toolchain.mjs +19 -0
  55. package/build/typescript.mjs +6 -4
  56. package/build/webpack-graph.mjs +6 -3
  57. package/build/worker-memory.mjs +9 -0
  58. package/cli.mjs +60 -4
  59. package/compat/activity-cache.cjs +26 -0
  60. package/compat/after.cjs +6 -4
  61. package/compat/app-layout-context.cjs +52 -2
  62. package/compat/base64.cjs +10 -0
  63. package/compat/cache-handlers.cjs +12 -2
  64. package/compat/cache-wire.cjs +28 -0
  65. package/compat/cache.cjs +2 -1
  66. package/compat/cache.d.cts +4 -0
  67. package/compat/cookies.cjs +21 -5
  68. package/compat/data-cache.cjs +12 -8
  69. package/compat/flight-delta.cjs +150 -0
  70. package/compat/headers.cjs +4 -2
  71. package/compat/incremental-cache.cjs +20 -7
  72. package/compat/index.d.ts +68 -3
  73. package/compat/link.cjs +12 -9
  74. package/compat/navigation-authorization.cjs +9 -0
  75. package/compat/navigation-stages.cjs +19 -0
  76. package/compat/offline-store.cjs +11 -0
  77. package/compat/offline.cjs +5 -0
  78. package/compat/offline.d.cts +2 -0
  79. package/compat/redis-cache.cjs +201 -0
  80. package/compat/redis-cache.d.cts +37 -0
  81. package/compat/server-primitives.cjs +31 -11
  82. package/compat/server.d.cts +15 -2
  83. package/compat/static-generation.cjs +7 -0
  84. package/compat/use-cache.cjs +3 -0
  85. package/docs/ai/compatibility.md +171 -2
  86. package/docs/ai/data.md +165 -5
  87. package/docs/ai/deployment.md +158 -0
  88. package/docs/ai/quickstart.md +29 -4
  89. package/docs/ai/routing.md +82 -0
  90. package/docs/ai/troubleshooting.md +37 -2
  91. package/package.json +26 -7
  92. package/runtime/action-client.mjs +10 -4
  93. package/runtime/action-server.mjs +2 -0
  94. package/runtime/app-client.mjs +212 -65
  95. package/runtime/app-content.mjs +4 -3
  96. package/runtime/app-partial-client.mjs +3 -0
  97. package/runtime/app-partial.mjs +1 -1
  98. package/runtime/app-prefetch-constants.mjs +4 -0
  99. package/runtime/app-prefetch.mjs +2 -3
  100. package/runtime/app-render-key.mjs +5 -0
  101. package/runtime/app-render.mjs +88 -28
  102. package/runtime/app-routing.mjs +22 -9
  103. package/runtime/app-static.mjs +18 -8
  104. package/runtime/app-stream-html.mjs +10 -53
  105. package/runtime/app-style-selection.mjs +34 -0
  106. package/runtime/app-styles.mjs +154 -0
  107. package/runtime/cache-components-rsc.mjs +4 -1
  108. package/runtime/cache-request.mjs +2 -1
  109. package/runtime/deployment-client.mjs +19 -0
  110. package/runtime/dev-client.mjs +7 -2
  111. package/runtime/dev-compiler-broker.mjs +42 -0
  112. package/runtime/dev-indicator.mjs +8 -3
  113. package/runtime/dev-lazy-tool.mjs +16 -0
  114. package/runtime/development.mjs +29 -12
  115. package/runtime/document.mjs +1 -1
  116. package/runtime/edge.mjs +6 -5
  117. package/runtime/fetch-cache.mjs +10 -8
  118. package/runtime/flight-delta.mjs +50 -0
  119. package/runtime/flight-tap.mjs +28 -0
  120. package/runtime/html-only.mjs +27 -0
  121. package/runtime/http.mjs +2 -0
  122. package/runtime/incremental-cache.mjs +25 -4
  123. package/runtime/instrumentation.mjs +49 -0
  124. package/runtime/navigation-recovery.mjs +36 -0
  125. package/runtime/navigation.mjs +7 -0
  126. package/runtime/node-html-stream.mjs +47 -0
  127. package/runtime/pages-client.mjs +14 -7
  128. package/runtime/pages-navigation.mjs +48 -63
  129. package/runtime/prefetch-scheduler.mjs +110 -0
  130. package/runtime/profile.mjs +14 -0
  131. package/runtime/profiles.json +2 -2
  132. package/runtime/render.mjs +3 -0
  133. package/runtime/route-pattern.mjs +44 -0
  134. package/runtime/rsc-worker.mjs +34 -12
  135. package/runtime/trace.mjs +78 -0
  136. package/runtime/watch-project.mjs +19 -15
  137. package/runtime/worker.mjs +70 -26
@@ -4,6 +4,7 @@ import {readFile} from 'node:fs/promises';
4
4
  import {analyzeSource} from './source-analysis.mjs';
5
5
  import {cachedTransform} from './transform-cache.mjs';
6
6
  const adapters = new Map([
7
+ ['cache/redis','redis-cache'],
7
8
  ['dist/shared/lib/router-context.shared-runtime','next-router-context'],
8
9
  ['dist/shared/lib/router-context','next-router-context'],
9
10
  ['dist/shared/lib/app-router-context.shared-runtime','next-app-router-context'],
@@ -23,33 +24,42 @@ export function frameworkImportName(specifier) {
23
24
 
24
25
  // Application graphs must not traverse the programmatic build API's native tools.
25
26
  // Outside a compiled application, the package root retains its full Node API.
26
- export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8')){
27
- // esbuild understands .mts/.cts, but our framework transforms do not. Never
27
+ export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8'),{transformTypedHelpers=false}={}){
28
+ // Server/client module boundaries in .mts/.cts are not supported. Never
28
29
  // silently bundle a server implementation or misclassify a client boundary.
29
30
  builder.onLoad({filter:/\.[cm]ts$/i,namespace:'file'},async args=>{
30
31
  // Loader bridges dispatch framework callbacks independently of their filters.
31
32
  if(!/\.[cm]ts$/i.test(args.path))return;
32
33
  const source=await readSource(args.path,args.prnextSource);
33
34
  const original=args.prnextSource===undefined?source:await readSource(args.path);
35
+ let nativeDecorators=false;
34
36
  // A loader may remove a directive as well as introduce one. Both versions
35
37
  // must be safe before its output can become an ordinary browser module.
36
38
  for(const candidate of new Set([original,source])){
37
- const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v3'],()=>{
39
+ const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v4-native-decorators'],()=>{
38
40
  const analysis=analyzeSource(candidate,args.path);
39
41
  if(analysis.frameworkBoundary)return 'yes';
40
- const pending=[analysis.program];
42
+ const pending=[analysis.program];let decorators=false;
41
43
  while(pending.length){
42
44
  const node=pending.pop();
43
45
  if(node.type==='ExpressionStatement'&&node.directive==='use server')return 'yes';
46
+ if(node.type==='Decorator'||node.decorators?.length)decorators=true;
44
47
  for(const value of Object.values(node)){
45
48
  if(Array.isArray(value)){for(const item of value)if(item?.type)pending.push(item);}
46
49
  else if(value&&typeof value==='object'&&value.type)pending.push(value);
47
50
  }
48
51
  }
49
- return 'no';
52
+ return decorators?'decorated':'no';
50
53
  });
51
54
  if(unsupported==='yes')throw new Error(`React server/client directives are not supported in ${args.path}. Rename this file to .ts or .tsx.`);
55
+ nativeDecorators ||= unsupported==='decorated';
52
56
  }
57
+ // Compiler-enabled render graphs can transform ordinary typed helpers after
58
+ // this boundary check. Other consumers keep the historical esbuild path.
59
+ // Decorated typed helpers already have a native esbuild implementation.
60
+ // Keep that path instead of widening the syntax accepted by every Babel
61
+ // transform, while their importing React components can still be compiled.
62
+ if(transformTypedHelpers && !nativeDecorators)return;
53
63
  return {contents:source,loader:args.prnextSource===undefined?'ts':'tsx',resolveDir:dirname(args.path)};
54
64
  });
55
65
  builder.onResolve({filter:/^(?:prnext|@thomas\.f\/prnext)(?:\/route(?:\.js)?)?$/},()=>
@@ -0,0 +1,53 @@
1
+ import {assertHtmlOnlyDocument} from '../runtime/html-only.mjs';
2
+ const MAX_PATTERNS = 128;
3
+
4
+ export function validateHtmlOnly(value) {
5
+ if (value === undefined) return [];
6
+ if (!Array.isArray(value) || value.length > MAX_PATTERNS) throw new TypeError('htmlOnly must be an array of at most 128 App route patterns.');
7
+ for (const pattern of value) {
8
+ if (typeof pattern !== 'string' || pattern.length > 512 || !pattern.startsWith('/') || pattern.startsWith('//') ||
9
+ /[\s\\?#\u0000-\u001f\u007f]/.test(pattern) || /%(?![\da-f]{2})/i.test(pattern)) throw new TypeError('htmlOnly entries must be clean absolute route patterns without queries, fragments, whitespace or backslashes.');
10
+ const pathname = pattern.endsWith('/**') ? pattern.slice(0, -3) || '/' : pattern;
11
+ if (pathname.includes('*') || (pathname !== '/' && pathname.endsWith('/')) || pathname.slice(1).includes('//')) throw new TypeError('htmlOnly supports exact registered route patterns and a final /** subtree suffix only.');
12
+ for (const segment of pathname.split('/').slice(1)) {
13
+ let decoded;
14
+ try { decoded = decodeURIComponent(segment); } catch { throw new TypeError('htmlOnly patterns must use valid URL encoding.'); }
15
+ if (decoded === '.' || decoded === '..' || /[\\/\u0000-\u001f\u007f]/.test(decoded)) throw new TypeError('htmlOnly patterns cannot contain dot segments or encoded path separators.');
16
+ }
17
+ }
18
+ return [...new Set(value)];
19
+ }
20
+
21
+ export function matchesHtmlOnly(pattern, routePattern) {
22
+ if (!pattern.endsWith('/**')) return pattern === routePattern;
23
+ const prefix = pattern.slice(0, -3);
24
+ return !prefix || routePattern === prefix || routePattern.startsWith(prefix + '/');
25
+ }
26
+
27
+ export function selectHtmlOnlyRoutes(routes, patterns, { cacheComponents = false } = {}) {
28
+ const selected = new Set();
29
+ for (const pattern of validateHtmlOnly(patterns)) {
30
+ const matches = routes.filter(route => !route.internal && matchesHtmlOnly(pattern, route.pattern));
31
+ if (!matches.length) throw new Error(`htmlOnly pattern ${JSON.stringify(pattern)} does not match a registered route.`);
32
+ for (const route of matches) {
33
+ if (route.router !== 'app' || route.kind !== 'page') throw new Error(`htmlOnly route ${route.pattern} must be an App Router page; Pages Router and Route Handlers are not supported.`);
34
+ const config = route.cacheConfig || {};
35
+ if (cacheComponents || config.runtime === 'edge' || config.dynamic === 'force-dynamic' || config.forceNoStore || (config.revalidate !== undefined && config.revalidate !== false)) {
36
+ throw new Error(`htmlOnly route ${route.pattern} must be fully prerendered without Edge, Cache Components/PPR, dynamic rendering or ISR.`);
37
+ }
38
+ if (route.parallel || route.interception) throw new Error(`htmlOnly route ${route.pattern} does not support parallel or intercepted routing.`);
39
+ if (route.pattern.includes('[') && config.dynamicParams !== false) throw new Error(`htmlOnly route ${route.pattern} requires dynamicParams: false and complete generateStaticParams results.`);
40
+ selected.add(route.id);
41
+ }
42
+ }
43
+ return selected;
44
+ }
45
+
46
+ export function validateHtmlOnlyPrerender(route, rendered) {
47
+ if (!route.htmlOnly) return;
48
+ if (rendered.dynamic || rendered.partial || rendered.revalidate !== false || (rendered.status ?? 200) !== 200) {
49
+ throw new Error(`htmlOnly route ${route.pattern} must produce a complete successful static HTML document without dynamic data, partial rendering, redirects or ISR.`);
50
+ }
51
+ const html = Buffer.isBuffer(rendered.body) ? rendered.body.toString('utf8') : rendered.body;
52
+ assertHtmlOnlyDocument(html, route.pattern);
53
+ }
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import { builtinModules } from 'node:module';
3
+ import { analyzeSource } from './source-analysis.mjs';
4
+ import { frameworkImportName, frameworkImportPattern } from './framework-imports.mjs';
5
+
6
+ const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
7
+ const interactive = new Set(['link','image','head','script','dynamic','router','compat/router','next-router-context','next-app-router-context']);
8
+ const namespace = /^prnext-(?:css-mapping|compiled-css|app-empty|edge-empty):/;
9
+
10
+ export function createHtmlOnlyGraph(root) {
11
+ const graph = new Map(), issues = new Map();
12
+ const file = value => path.resolve(root, value.replace(namespace, ''));
13
+ function issue(filename, reason) {
14
+ const key = file(filename);
15
+ if (!issues.has(key)) issues.set(key, reason);
16
+ }
17
+ return {
18
+ issue,
19
+ alias(source, target) {
20
+ if (file(source) !== file(target)) graph.set(file(source), [{ path:file(target) }]);
21
+ },
22
+ source(filename, source) {
23
+ const { program, directives, computedRequire, imports } = analyzeSource(source, filename);
24
+ if (directives.includes('use client')) issue(filename, 'a Client Component (use client)');
25
+ if (computedRequire) issue(filename, 'a computed require() whose dependencies cannot be validated');
26
+ for (const dependency of imports) if (frameworkImportPattern.test(dependency.path) && interactive.has(frameworkImportName(dependency.path))) issue(filename, `interactive framework import ${dependency.path}; use ordinary HTML elements instead`);
27
+ const pending = [program];
28
+ while (pending.length) {
29
+ const node = pending.pop();
30
+ if (node.directive === 'use server') issue(filename, 'a Server Action (use server)');
31
+ if (node.type === 'ImportExpression' && typeof node.source?.value !== 'string') issue(filename, 'a computed import() whose dependencies cannot be validated');
32
+ if (node.type === 'JSXOpeningElement' && node.name?.type === 'JSXIdentifier' && ['script','iframe','object','embed'].includes(node.name.name.toLowerCase())) issue(filename, `a <${node.name.name}> element`);
33
+ if (node.type === 'JSXAttribute' && node.name?.type === 'JSXIdentifier' && /^on[A-Z]/.test(node.name.name)) issue(filename, `interactive event property ${node.name.name}`);
34
+ for (const value of Object.values(node)) {
35
+ if (Array.isArray(value)) { for (const child of value) if (child?.type) pending.push(child); }
36
+ else if (value && typeof value === 'object' && value.type) pending.push(value);
37
+ }
38
+ }
39
+ },
40
+ record(metafile) {
41
+ for (const [name, input] of Object.entries(metafile?.inputs || {})) {
42
+ const key=file(name), dependencies=graph.get(key) || [];
43
+ for (const dependency of input.imports || []) {
44
+ if (!dependency.external) dependencies.push({path:file(dependency.path)});
45
+ else if (!builtins.has(dependency.path) && !/^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/.test(dependency.path) && !/^\.\.\/compat\/(?:navigation|headers|cache|server|font-local|font-google|og)\.cjs$/.test(dependency.path)) {
46
+ dependencies.push({external:dependency.path});
47
+ }
48
+ }
49
+ graph.set(key,dependencies);
50
+ }
51
+ },
52
+ validate(route) {
53
+ const roots = new Set([route.file, ...Object.values(route.segments || {}).flatMap(segment => Object.values(segment).filter(value => typeof value === 'string' && /\.[cm]?[jt]sx?$/.test(value))), ...(route.metadataFiles || []).map(item => item.file)].filter(Boolean));
54
+ const pending=[...roots].map(file), seen=new Set();
55
+ while (pending.length) {
56
+ const current=pending.pop();
57
+ if (seen.has(current)) continue;
58
+ seen.add(current);
59
+ if (issues.has(current)) throw new Error(`htmlOnly route ${route.pattern} reaches ${issues.get(current)} in ${path.relative(root,current)}.`);
60
+ for (const dependency of graph.get(current) || []) {
61
+ if (dependency.external) throw new Error(`htmlOnly route ${route.pattern} reaches unverified external dependency ${dependency.external} from ${path.relative(root,current)}. Bundle a static server dependency or remove it from this HTML-only route.`);
62
+ pending.push(dependency.path);
63
+ }
64
+ }
65
+ },
66
+ };
67
+ }
package/build/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
+ import {compileInstrumentation} from './instrumentation.mjs';
1
2
  import {prerenderPages as prerender} from './prerender-pages.mjs';
2
- import {isolatedPrerender} from './production-builder.mjs';
3
+ import {isolatedPrerender,prerenderProduction} from './production-builder.mjs';
3
4
  import { rename } from '../runtime/fs.mjs';
4
5
  import { writeRuntimeContract } from './runtime-contract.mjs';
5
6
  import { writeBuildOutput } from './output-contract.mjs';
@@ -7,11 +8,13 @@ import { bundleRuntime } from './bundle-runtime.mjs';
7
8
  import {withPersistentTransforms} from './transform-cache.mjs';
8
9
  import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
9
10
  import { expandLocales, localizedStaticPaths } from './i18n.mjs';
10
- import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile } from './compiler.mjs';
11
+ import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile,compilationGraphCacheReport } from './compiler.mjs';
12
+ import {retainDeploymentAssets} from './deployment-assets.mjs';
13
+ import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
11
14
  import {writeBuildAnalysis} from './analyze.mjs';
12
15
  import {routeFiles} from './dev-discovery.mjs';
13
16
  import {writeTypedRoutes} from './typed-routes.mjs';
14
- import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir } from 'node:fs/promises';
17
+ import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir, realpath } from 'node:fs/promises';
15
18
  import path from 'node:path';
16
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
20
  import { createRequire } from 'node:module';
@@ -40,20 +43,22 @@ import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlu
40
43
  import { snapshotEnvConfig } from '../runtime/env.mjs';
41
44
  import { validateStaticPaths } from '../runtime/pages-paths.mjs';
42
45
  import { projectTsconfig, checkTypeScript } from './typescript.mjs';
46
+ import {createReactCompilerTransform} from './react-compiler.mjs';
47
+ import {writeReactCompilerReport,reactCompilerReport} from './react-compiler-report.mjs';
43
48
  export { staticPath } from '../runtime/pages-paths.mjs';
44
49
 
45
50
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
46
- const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og']);
47
- const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
51
+ const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og', 'offline', 'redis-cache']);
52
+ const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
48
53
  const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
49
54
 
50
55
  // These framework files have already been enumerated: copyFile avoids cp's
51
56
  // recursive traversal/stat work for every file. Bound I/O on large installations.
52
- async function copyRuntimeFiles(stage) {
57
+ async function copyRuntimeFiles(stage,{development=true}={}) {
53
58
  const files = (await Promise.all(['runtime', 'compat'].map(async directory => {
54
59
  await mkdir(path.join(stage, directory), { recursive: true });
55
60
  return (await readdir(path.join(packageRoot, directory)))
56
- .filter(name => directory === 'runtime' ? name === 'profiles.json' || name.endsWith('.mjs') && !name.endsWith('.test.mjs') : name.endsWith('.cjs'))
61
+ .filter(name => directory === 'runtime' ? name === 'profiles.json' || development&&name.endsWith('.mjs') && !name.endsWith('.test.mjs') : name.endsWith('.cjs'))
57
62
  .map(name => path.join(directory, name));
58
63
  }))).flat();
59
64
  let next = 0;
@@ -68,18 +73,18 @@ async function copyRuntimeFiles(stage) {
68
73
  if (failed) throw failed.reason;
69
74
  }
70
75
 
71
- function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fonts, refresh, locations }) {
76
+ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler }) {
72
77
  return {
73
78
  name: 'prnext-pages-compatibility',
74
79
  setup(esbuild) {
75
- applicationEntry(esbuild,readCompilerFile);
80
+ applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:browser && !!reactCompiler});
76
81
  esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, args => args.importer === documentFile
77
82
  ? { errors: [{ text: 'CSS cannot be imported within pages/_document. Move global styles to pages/_app, or return collected styles from Document.getInitialProps.' }] } : undefined);
78
83
  esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
79
84
  const name = frameworkImportName(args.path);
80
85
  if (!compatibleModules.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}). Supported runtime imports: next/link, next/head, next/router, next/image, next/cache, next/dynamic, next/error, next/document, next/app, next/script.` }] };
81
86
  if (browser && name === 'document') return { errors: [{ text: `${args.path} is server-only. Import Document, Html, Head, Main and NextScript from pages/_document, not from a browser page or component.` }] };
82
- if (browser && ['cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
87
+ if (browser && ['cache', 'og', 'redis-cache'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
83
88
  const file = name === 'compat/router' ? 'compat-router' : name;
84
89
  return browser ? { path: path.join(packageRoot, 'compat', file + '.cjs') } : { path: `../compat/${file}.cjs`, external: true };
85
90
  });
@@ -95,10 +100,15 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fo
95
100
  });
96
101
  esbuild.onLoad({ filter: /.*/, namespace: 'prnext-empty' }, () => ({ contents: '', loader: 'js' }));
97
102
  }
98
- esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
103
+ esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
99
104
  if (browser && args.path === documentFile) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a browser page or component.' }] };
100
105
  const original=await readCompilerFile(args.path,args.prnextSource);
101
- const source = await fonts.transform(locations?await locations(original,args.path):original, args.path);
106
+ // Pages shares a server graph with API handlers, Document and data
107
+ // loaders. Keep that entire graph original: an uppercase JSX utility
108
+ // may be called normally there, outside a React render. Browser render
109
+ // graphs receive memoization before instrumentation and JSX lowering.
110
+ const compiled=browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path) ? await reactCompiler(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
111
+ const source = await fonts.transform(locations?await locations(compiled,args.path):compiled, args.path);
102
112
  assertSupportedSource(source, args.path);
103
113
  const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
104
114
  let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
@@ -183,13 +193,15 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
183
193
  const customRoutes = await compileCustomRoutes(config);
184
194
  const buildId = await generateBuildId(config, { dev });
185
195
  const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
186
- let project;
196
+ let project,deferredRoutes=[];
187
197
  if(dev && Array.isArray(devRoutes)) {
188
198
  const discovery=await scanProject(projectRoot,{...scanOptions,devAppFiles:new Set()});
189
199
  project=await scanProject(projectRoot,{...scanOptions,devAppFiles:routeFiles(discovery,devRoutes)});
190
200
  await writeTypedRoutes(projectRoot,discovery,config);
201
+ deferredRoutes=discovery.routes.filter(route=>!devRoutes.includes(route.pattern)&&!route.internal&&!route.errorStatus);
191
202
  project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
192
203
  } else project=await scanProject(projectRoot,scanOptions);
204
+ if(!dev&&config.htmlOnly.length)selectHtmlOnlyRoutes(project.routes,config.htmlOnly,config);
193
205
  if(!dev || !Array.isArray(devRoutes))await writeTypedRoutes(projectRoot,project,config);
194
206
  const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
195
207
  const destination = await prepareBuildDirectory(project.root, config.distDir);
@@ -199,25 +211,32 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
199
211
  await addBuiltinPagesErrors(project, stage);
200
212
  const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
201
213
  ...(dev && Array.isArray(devRoutes) ? {devOnDemand:true} : {}), ...(dev && devSession ? {devSequence,devSession} : {}), ...(dev ? {devForceRefresh,devAffectedModules,devIndicators:config.devIndicators,...(devTools?{devTools}:{})} : {}),
202
- config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, cacheLife: config.cacheLife, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
214
+ config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
203
215
  ...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
204
216
  // Its build-unique name is known before bundling and avoids a hash cycle
205
217
  // between page entry URLs and the manifest that lists those entries.
218
+ if(!dev)manifest.config.deploymentId=manifest.cacheId;
206
219
  const pagesManifestUrl = `${publicPaths.assetBase}/pages-manifest-${manifest.cacheId}.json`;
207
220
  if (dev) manifest.devClient = `${publicPaths.assetBase}/dev-manifest-${manifest.cacheId}.json`;
208
221
  const development = dev ? { buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:devSequence, session:devSession, indicator:config.devIndicators,...(devTools?{tools:devTools}:{}) } : undefined;
209
222
  const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:devForceRefresh,forceFiles:devAffectedModules}) : undefined;
210
223
  const locations = dev ? createSourceLocationTransform(project.root) : undefined;
224
+ const reactCompiler = config.reactCompiler ? createReactCompilerTransform(project.root, config.reactCompiler, {dev}) : undefined;
225
+ const serverOnlyFiles = new Set();
226
+ if (reactCompiler) for (const file of [...project.routes.filter(route=>route.kind==='api').map(route=>route.file),...(project.document?[project.document]:[])]) {
227
+ serverOnlyFiles.add(file);serverOnlyFiles.add(await realpath(file));
228
+ }
211
229
  const fonts = createFonts({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
212
- const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine });
230
+ const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine,dev,config });
213
231
  configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,tsconfig,assetBase:publicPaths.assetBase});
214
232
  try {
215
- await copyRuntimeFiles(stage);
233
+ await copyRuntimeFiles(stage,{development:dev});
216
234
  // Runtime copies must work even when dotenv is nested beneath the PRNext
217
235
  // package instead of hoisted beside the application's dependencies.
218
236
  await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
219
237
  await prepareImageResponse(stage);
220
238
  manifest.config.serverActions = config.experimental.serverActions;
239
+ if(config.workerMemory && !dev)manifest.config.workerMemory=config.workerMemory;
221
240
  if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
222
241
  if(config.workerPreload&&!dev)manifest.config.workerPreload=config.workerPreload;
223
242
  const cacheHandlers = await prepareCacheHandlers({ config, projectRoot: project.root, stage, environment });
@@ -236,7 +255,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
236
255
  serverEntries[route.id] = serverEntry;
237
256
  if (route.kind === 'page') {
238
257
  const clientEntry = path.join(stage, '.entries', route.id + '.client.jsx');
239
- await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, strictMode: config.reactStrictMode === true }, development));
258
+ await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, deploymentId:dev?undefined:manifest.cacheId, strictMode: config.reactStrictMode === true }, development));
240
259
  clientEntries[route.id] = clientEntry;
241
260
  }
242
261
  manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, module: `server/${route.id}.mjs`, css: [],
@@ -245,7 +264,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
245
264
  }
246
265
  const common = { absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders, assetNames: '[name]-[hash]', publicPath: publicPaths.assetBase, metafile: true, define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...environment } };
247
266
  if (Object.keys(serverEntries).length) {
248
- const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, locations }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
267
+ const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, locations, reactCompiler }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
249
268
  fonts.record(server.metafile);
250
269
  const serverChunks = new Set(Object.keys(server.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
251
270
  for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
@@ -260,7 +279,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
260
279
  }
261
280
  }
262
281
  if (Object.keys(clientEntries).length) {
263
- const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, refresh, locations }), cssModules.plugin(true)] });
282
+ const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler }), cssModules.plugin(true)] });
264
283
  fonts.record(browser.metafile);
265
284
  const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
266
285
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
@@ -277,27 +296,35 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
277
296
  await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
278
297
  if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
279
298
  await prepareMetadataRoutes(project, stage);
280
- await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
299
+ await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
281
300
  if(manifest.config.workerPreload)for(const pattern of config.workerPreload.routes){
282
301
  const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
283
302
  if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
284
303
  }
304
+ await compileInstrumentation({root:project.root,stage,manifest,config,dev,environment});
305
+ if(deferredRoutes.length){
306
+ for(const route of deferredRoutes){
307
+ const module=`server/dev-deferred-${route.id}.mjs`;await writeFile(path.join(stage,module),'export {};\n');
308
+ manifest.routes.push({id:route.id,pattern:route.pattern,kind:route.kind,router:route.router,module,devDeferred:true,css:[],cacheConfig:{maxDuration:120}});
309
+ }
310
+ }
285
311
  await checkTypeScript(project.root, config, { dev, tsconfig });
286
312
  fonts.attach(manifest);
287
313
  expandLocales(manifest);
288
314
  // All runtime preparation must finish before its shared modules are built;
289
315
  // no staged renderer has been imported before this production-only point.
290
- if (!dev) await bundleRuntime(stage,{projectRoot:project.root});
316
+ if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime')});
291
317
  // Node caches CJS React by filename, not NODE_ENV. Once development metadata
292
318
  // imports load it, subsequent production renders need a clean process too.
293
319
  if(dev) developmentModulesLoaded = true;
294
- if(!dev && (incremental || developmentModulesLoaded)) Object.assign(manifest,await isolatedPrerender(project.root,stage,manifest));
320
+ if(!dev) Object.assign(manifest,await (incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender}));
295
321
  else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
296
- const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
322
+ const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal && !route.devDeferred);
297
323
  const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
298
324
  if (pages.length || manifest.pagesErrors) {
299
325
  const publicPages = {
300
326
  buildId: manifest.buildId,
327
+ ...(manifest.config.deploymentId?{deploymentId:manifest.config.deploymentId}:{}),
301
328
  ...publicPaths,
302
329
  // Header rules cannot select a different page. Other server routing
303
330
  // stays authoritative, including middleware with conditional behavior.
@@ -327,10 +354,16 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
327
354
  const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
328
355
  await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
329
356
  pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
330
- ...(appRoute ? { app: { client: appRoute.client, css: appRoute.css } } : {}),
357
+ ...(appRoute ? { app: { client: appRoute.client, css: appRoute.css, routes: manifest.routes.filter(route=>route.router==='app'&&route.kind==='page'&&route.client).map(({id,pattern,css,cssScopes})=>({id,pattern,css,cssScopes:cssScopes||[]})) } } : {}),
331
358
  }));
332
359
  }
333
- await writeRuntimeContract(stage);
360
+ // A report belongs to this successful staging generation, never to the
361
+ // runtime manifest or public assets. Publishing the stage is atomic below.
362
+ if(!dev)await retainDeploymentAssets({stage,destination,manifest,policy:config.deploymentRetention});
363
+ const compilationCache=compilationGraphCacheReport();
364
+ if(!dev)await writeFile(path.join(stage,'compilation-cache-report.json'),JSON.stringify(compilationCache,null,2)+'\n');
365
+ const compilerReport=await writeReactCompilerReport(stage,reactCompilerReport());
366
+ await writeRuntimeContract(stage,{deployment:!dev,htmlOnly:!dev&&manifest.routes.some(route=>route.htmlOnly),workerMemory:Boolean(manifest.config.workerMemory),incrementalCache:Boolean(manifest.config.cacheHandler)});
334
367
  await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
335
368
  await writeBuildOutput(stage, manifest);
336
369
  // Offline compression is a deployment optimization. Recompressing source
@@ -360,7 +393,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
360
393
  } finally { await rm(pointerTemporary, {force:true}); }
361
394
  await exported?.commit();
362
395
  if (hadDestination) await rm(backup, { recursive: true, force: true });
363
- return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started) };
396
+ return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
364
397
  } catch (error) {
365
398
  await rm(stage, { recursive: true, force: true });
366
399
  if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
@@ -0,0 +1,30 @@
1
+ import {access,writeFile,mkdir} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {build} from './compiler.mjs';
4
+ import {compileEdge} from './edge.mjs';
5
+ import {moduleResolutionPlugin} from './module-resolution.mjs';
6
+
7
+ export async function compileInstrumentation({root,stage,manifest,config,dev,environment}) {
8
+ const found=[];
9
+ for(const directory of ['', 'src'])for(const extension of ['ts','js','mjs','cjs']){
10
+ const file=path.join(root,directory,'instrumentation.'+extension);
11
+ try{await access(file);found.push(file);}catch(error){if(error.code!=='ENOENT')throw error;}
12
+ }
13
+ if(!found.length)return;
14
+ if(found.length>1)throw Error('Only one instrumentation file is allowed, in the project root or src directory.');
15
+ const file=found[0],entry=path.join(stage,'.entries/instrumentation.mjs');manifest.instrumentation={};
16
+ await mkdir(path.dirname(entry),{recursive:true});
17
+ await writeFile(entry,`import * as hooks from ${JSON.stringify(file)};export const register=hooks.register??hooks.default?.register;export const onRequestError=hooks.onRequestError??hooks.default?.onRequestError;`);
18
+ for(const runtime of ['node','rsc']){
19
+ const module=`server/instrumentation-${runtime}.mjs`;
20
+ await build({absWorkingDir:root,entryPoints:[entry],outfile:path.join(stage,module),bundle:true,packages:'external',platform:'node',format:'esm',target:'node22',banner:{js:"import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);"},conditions:runtime==='rsc'?['react-server']:[],sourcemap:dev,logLevel:'silent',
21
+ define:{...environment,'process.env.NODE_ENV':JSON.stringify(dev?'development':'production'),'process.env.NEXT_RUNTIME':'"nodejs"'},
22
+ plugins:[moduleResolutionPlugin(config.turbopack,root)]});
23
+ manifest.instrumentation[runtime]=module;
24
+ }
25
+ if(manifest.middleware?.runtime==='edge'||manifest.routes.some(route=>route.cacheConfig?.runtime==='edge'||route.handlerConfig?.runtime==='edge')){
26
+ const module='server/instrumentation-edge.mjs';
27
+ await compileEdge({file:entry,outfile:path.join(stage,module),projectRoot:root,dev,defineEnvironment:environment});
28
+ manifest.instrumentation.edge=module;
29
+ }
30
+ }
@@ -60,7 +60,7 @@ export async function prepareMetadataRoutes(project, stage) {
60
60
  let source;
61
61
  if (dynamic) {
62
62
  const config = await validateAppConfig(route.file);
63
- source = `import * as source from ${JSON.stringify(route.file)};import {metadataHandler,metadataStaticParams} from '../runtime/metadata-route.mjs';export * from ${JSON.stringify(route.file)};\n`;
63
+ source = `import * as source from ${JSON.stringify(route.file)};import {metadataHandler,metadataStaticParams} from 'prnext-internal:metadata-route';export * from ${JSON.stringify(route.file)};\n`;
64
64
  if (config.revalidate === undefined) source += 'export const revalidate=false;\n';
65
65
  const options = JSON.stringify({ kind, image, multiple, contentType });
66
66
  source += `export const GET=metadataHandler(source,${options});\n`;
@@ -6,7 +6,7 @@ import { checkProject } from './check.mjs';
6
6
  import { frameworkImportName } from './framework-imports.mjs';
7
7
  import { packageManifest } from '../native/resolve.mjs';
8
8
 
9
- const supported = new Set([...Object.keys(packageManifest.exports).map(name => name.slice(2)), 'font/google', 'font/local', 'next-router-context', 'next-app-router-context']);
9
+ const supported = new Set([...Object.keys(packageManifest.exports).map(name => frameworkImportName(name.slice(2))), 'font/google', 'font/local', 'next-router-context', 'next-app-router-context']);
10
10
  const ignored = new Set(['node_modules', '.git', '.next', '.prnext', '.prnext-cache', 'dist', 'build', 'out', 'coverage', 'test-results', '__tests__']);
11
11
  const code = /\.(?:[cm]?[jt]s|[jt]sx)$/;
12
12
  function walkAst(node, visit) {
@@ -0,0 +1,37 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+ import {parseTsconfig,createPathsMatcher} from 'get-tsconfig';
5
+ import {parse,modify,applyEdits} from 'jsonc-parser';
6
+
7
+ // TypeScript 7 has no stable legacy compiler API. Parse configuration only with
8
+ // the dedicated JSONC/config resolvers; semantic checking stays in native tsc.
9
+ export async function nativeTypeConfig(filename,helper){
10
+ let source=await readFile(filename,'utf8');const errors=[];
11
+ const own=parse(source,errors,{allowTrailingComma:true});
12
+ if(errors.length||!own||typeof own!=='object'||Array.isArray(own))throw new Error('Cannot configure typed routes: invalid tsconfig.');
13
+ const config=parseTsconfig(filename),directory=path.dirname(filename);
14
+ const format={insertSpaces:true,tabSize:2,eol:source.includes('\r\n')?'\r\n':'\n'};
15
+ const update=(location,value)=>{source=applyEdits(source,modify(source,location,value,{formattingOptions:format}));};
16
+ const files=own.files??config.files??[];
17
+ if(!Array.isArray(files)||files.some(file=>typeof file!=='string'))throw new Error('tsconfig.files must be an array.');
18
+ if(!files.some(file=>path.resolve(directory,file)===helper))update(['files'],[...files,path.relative(directory,helper).replaceAll(path.sep,'/')]);
19
+ const options=config.compilerOptions||{},paths=options.paths||{};
20
+ const missing=['next','next/*'].filter(name=>!Object.hasOwn(paths,name));
21
+ if(!missing.length)return source;
22
+ const base=options.baseUrl?path.resolve(directory,options.baseUrl):directory;
23
+ const relative=filename=>{const value=path.relative(base,filename).replaceAll(path.sep,'/');return value.startsWith('.')?value:'./'+value;};
24
+ const resolved=own.compilerOptions?.paths?{...paths}:{};
25
+ if(!own.compilerOptions?.paths){
26
+ const matcher=createPathsMatcher({path:filename,config});
27
+ for(const pattern of Object.keys(paths)){
28
+ const probe='__prnext_type_path__',values=matcher?.(pattern.replace('*',probe));
29
+ if(!values?.length)throw new Error('Cannot resolve inherited TypeScript path: '+pattern);
30
+ resolved[pattern]=values.map(value=>relative(value).replaceAll(probe,'*'));
31
+ }
32
+ }
33
+ const compat=fileURLToPath(new URL('../compat/',import.meta.url));
34
+ for(const name of missing)resolved[name]=[relative(path.join(compat,name==='next'?'index.d.ts':'*.d.cts'))];
35
+ update(['compilerOptions','paths'],resolved);
36
+ return source;
37
+ }
@@ -17,7 +17,21 @@ async function frameworkHash(){
17
17
  const hash=createHash('sha256');
18
18
  for(const f of files)hash.update(f).update(await readFile(path.join(packageRoot,'build',f)));
19
19
  hash.update(await readFile(path.join(packageRoot,'package.json')));
20
- for(const name of ['@babel/core','@babel/parser','@babel/traverse','@babel/generator','@babel/types','esbuild'])hash.update(name+':'+require(name+'/package.json').version);
20
+ for(const name of ['@babel/core','@babel/parser','@babel/traverse','@babel/generator','@babel/types','babel-plugin-react-compiler','esbuild'])hash.update(name+':'+require(name+'/package.json').version);
21
+ // CSS graph outputs also depend on the installed style engine and browser
22
+ // database, including nested/non-hoisted installations with different pins.
23
+ for(const name of ['lightningcss','browserslist','caniuse-lite','electron-to-chromium','postcss','postcss-value-parser','postcss-flexbugs-fixes','postcss-preset-env']){
24
+ let filename;
25
+ try{filename=require.resolve(name+'/package.json');}
26
+ catch{
27
+ let directory=path.dirname(require.resolve(name));
28
+ while(true){
29
+ try{const candidate=path.join(directory,'package.json'),value=JSON.parse(await readFile(candidate,'utf8'));if(value.name===name){filename=candidate;break;}}catch{}
30
+ const parent=path.dirname(directory);if(parent===directory)throw new Error(`Cannot fingerprint ${name}`);directory=parent;
31
+ }
32
+ }
33
+ hash.update(name).update(await readFile(filename));
34
+ }
21
35
  return hash.digest('hex');
22
36
  })();
23
37
  return frameworkIdentity;
@@ -78,8 +92,9 @@ export async function readCacheRecord(directory,key,limit=16*1024*1024){
78
92
  return JSON.parse(record.payload);
79
93
  }catch{return null;}finally{await file?.close().catch(()=>{});}
80
94
  }
81
- export async function writeCacheRecord(directory,key,value,limit=16*1024*1024){
95
+ export async function writeCacheRecord(directory,key,value,limit=16*1024*1024,{maxFiles=64}={}){
82
96
  if(!directory||!/^[a-f0-9]{64}$/.test(key))return false;
97
+ if(!Number.isInteger(maxFiles)||maxFiles<1||maxFiles>4096)return false;
83
98
  const filename=path.join(directory,key+'.json'),temporary=filename+'.'+randomUUID()+'.tmp';
84
99
  try{
85
100
  const payload=JSON.stringify(value),body=JSON.stringify({version:1,key,checksum:cacheHash(payload),payload});
@@ -91,7 +106,7 @@ export async function writeCacheRecord(directory,key,value,limit=16*1024*1024){
91
106
  const stat=await lstat(path.join(directory,name)).catch(()=>null);if(stat?.isFile()&&!stat.isSymbolicLink())records.push({name,size:stat.size,time:stat.mtimeMs});
92
107
  }
93
108
  records.sort((a,b)=>b.time-a.time);let total=0;
94
- for(let i=0;i<records.length;i++){total+=records[i].size;if(i>=64||total>MAX_DISK)await unlink(path.join(directory,records[i].name)).catch(()=>{});}
109
+ for(let i=0;i<records.length;i++){total+=records[i].size;if(i>=maxFiles||total>MAX_DISK)await unlink(path.join(directory,records[i].name)).catch(()=>{});}
95
110
  return true;
96
111
  }catch{return false;}finally{await unlink(temporary).catch(()=>{});}
97
112
  }
@@ -20,7 +20,7 @@ async function digest(stream, limit = Infinity) {
20
20
  // this bounded index and serialize publication/eviction, including overwrites.
21
21
  // Cached bytes are still decoded and hashed before reuse below.
22
22
  async function compressionCache(directory) {
23
- if (!directory) return;
23
+ if (!directory || process.env.PRNEXT_BUILD_CACHE === '0') return;
24
24
  try {
25
25
  await mkdir(directory,{recursive:true});
26
26
  const info=await lstat(directory);