@thomas.f/prnext 0.1.15-alpha → 0.1.17-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 (126) hide show
  1. package/AI.md +63 -0
  2. package/README.md +387 -12
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +5 -0
  5. package/build/analyze.mjs +184 -0
  6. package/build/app-config.mjs +7 -8
  7. package/build/app-static.mjs +12 -7
  8. package/build/app.mjs +23 -10
  9. package/build/build-lock.mjs +6 -6
  10. package/build/bundle-runtime.mjs +20 -5
  11. package/build/check.mjs +56 -11
  12. package/build/compiler.mjs +25 -1
  13. package/build/concurrent.mjs +12 -0
  14. package/build/config.mjs +21 -13
  15. package/build/css-modules.mjs +15 -4
  16. package/build/dev-compiler-health.mjs +11 -0
  17. package/build/dev-css-update.mjs +83 -0
  18. package/build/dev-dependencies.mjs +44 -0
  19. package/build/dev-discovery.mjs +28 -0
  20. package/build/dev-worker.mjs +14 -5
  21. package/build/dev.mjs +55 -4
  22. package/build/edge.mjs +3 -2
  23. package/build/framework-imports.mjs +40 -0
  24. package/build/incremental.mjs +8 -4
  25. package/build/index.mjs +96 -101
  26. package/build/inspect.mjs +3 -0
  27. package/build/inspection-summary.mjs +37 -4
  28. package/build/loader-cache.mjs +53 -26
  29. package/build/metadata-routes.mjs +2 -3
  30. package/build/migrate.mjs +60 -57
  31. package/build/npm-packages.mjs +3 -18
  32. package/build/package-manager.mjs +308 -0
  33. package/build/persistent-cache.mjs +97 -0
  34. package/build/precompress.mjs +62 -30
  35. package/build/prerender-pages.mjs +90 -0
  36. package/build/prerender-worker.mjs +10 -0
  37. package/build/production-builder.mjs +80 -0
  38. package/build/scan.mjs +4 -4
  39. package/build/source-analysis.mjs +42 -0
  40. package/build/static-adapter.mjs +13 -5
  41. package/build/static-input.mjs +48 -0
  42. package/build/styles.mjs +15 -3
  43. package/build/transform-cache.mjs +37 -4
  44. package/build/type-command.mjs +41 -0
  45. package/build/typed-routes.mjs +91 -0
  46. package/build/typescript-dev-worker.mjs +18 -0
  47. package/build/typescript-dev.mjs +22 -0
  48. package/build/typescript-watch.mjs +22 -0
  49. package/build/typescript.mjs +5 -4
  50. package/build/webpack-graph.mjs +164 -27
  51. package/build/worker-preload.mjs +9 -0
  52. package/cli.mjs +105 -121
  53. package/compat/index.d.ts +21 -7
  54. package/compat/link.d.cts +4 -3
  55. package/compat/navigation.d.cts +4 -3
  56. package/compat/router.d.cts +4 -3
  57. package/compat/typed-route.d.cts +17 -0
  58. package/docs/ai/compatibility.md +106 -0
  59. package/docs/ai/data.md +128 -0
  60. package/docs/ai/deployment.md +111 -0
  61. package/docs/ai/manifest.json +11 -0
  62. package/docs/ai/quickstart.md +146 -0
  63. package/docs/ai/routing.md +132 -0
  64. package/docs/ai/troubleshooting.md +120 -0
  65. package/index.mjs +2 -0
  66. package/native/diagnostics.mjs +34 -0
  67. package/native/resolve.mjs +7 -2
  68. package/package.json +28 -10
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +6 -0
  71. package/runtime/ai-docs.mjs +202 -0
  72. package/runtime/app-client.mjs +17 -6
  73. package/runtime/app-render.mjs +12 -0
  74. package/runtime/cloud/archive.mjs +5 -6
  75. package/runtime/compile-cache.mjs +8 -0
  76. package/runtime/dev-ai-brands.mjs +26 -0
  77. package/runtime/dev-ai-style.mjs +99 -0
  78. package/runtime/dev-build-reasons.mjs +25 -0
  79. package/runtime/dev-cache-control.mjs +53 -0
  80. package/runtime/dev-client.mjs +239 -45
  81. package/runtime/dev-console-copy.mjs +19 -0
  82. package/runtime/dev-console.mjs +106 -0
  83. package/runtime/dev-dependency-graph.mjs +40 -0
  84. package/runtime/dev-element-picker-style.mjs +27 -0
  85. package/runtime/dev-element-picker.mjs +213 -0
  86. package/runtime/dev-error-overlay-style.mjs +41 -0
  87. package/runtime/dev-error-overlay.mjs +167 -0
  88. package/runtime/dev-error-report.mjs +121 -0
  89. package/runtime/dev-file-changes.mjs +1 -1
  90. package/runtime/dev-gateway.mjs +44 -0
  91. package/runtime/dev-host-relay.mjs +66 -0
  92. package/runtime/dev-indicator.mjs +186 -0
  93. package/runtime/dev-issue-motion.mjs +214 -0
  94. package/runtime/dev-lazy-tool.mjs +18 -0
  95. package/runtime/dev-logo.mjs +2 -0
  96. package/runtime/dev-notification.mjs +81 -0
  97. package/runtime/dev-popout.mjs +34 -0
  98. package/runtime/dev-position.mjs +107 -0
  99. package/runtime/dev-preview-touch.mjs +122 -0
  100. package/runtime/dev-runtime-pool.mjs +45 -0
  101. package/runtime/dev-runtime-spare.mjs +57 -0
  102. package/runtime/dev-tools-client.mjs +155 -0
  103. package/runtime/dev-tools-mcp.mjs +126 -0
  104. package/runtime/dev-tools-metrics.mjs +31 -0
  105. package/runtime/dev-tools-model.mjs +42 -0
  106. package/runtime/dev-tools-panel.mjs +341 -0
  107. package/runtime/dev-tools-result.mjs +37 -0
  108. package/runtime/dev-tools-server.mjs +256 -0
  109. package/runtime/dev-tools-source.mjs +104 -0
  110. package/runtime/dev-tooltip.mjs +94 -0
  111. package/runtime/dev-ui-icons.mjs +8 -0
  112. package/runtime/dev-workbench-style.mjs +106 -0
  113. package/runtime/dev-workbench.mjs +428 -0
  114. package/runtime/dev-worker-logs.mjs +31 -0
  115. package/runtime/development.mjs +307 -0
  116. package/runtime/env.mjs +2 -1
  117. package/runtime/pages-client.mjs +10 -1
  118. package/runtime/pages-navigation.mjs +6 -1
  119. package/runtime/persistent/client.mjs +6 -5
  120. package/runtime/persistent/daemon.mjs +66 -56
  121. package/runtime/persistent/native-proxy.mjs +63 -0
  122. package/runtime/preload.mjs +12 -0
  123. package/runtime/rsc-worker.mjs +7 -0
  124. package/runtime/typed-route.mjs +19 -0
  125. package/runtime/watch-project.mjs +12 -5
  126. package/runtime/worker.mjs +36 -2
package/build/dev.mjs CHANGED
@@ -9,6 +9,44 @@ import {inlineSourceMap} from './source-maps.mjs';
9
9
 
10
10
  const packageRoot = fileURLToPath(new URL('../', import.meta.url));
11
11
  export const devClientFile = path.join(packageRoot, 'runtime/dev-client.mjs');
12
+ const refreshIdentities = new Map();
13
+ function namedDefaultComponents({types:t}){
14
+ return {visitor:{Program:{enter(program){
15
+ if(program.node.directives.some(item=>item.value.value==='use server'))return;
16
+ for(const statement of program.get('body')){
17
+ if(!statement.isExportDefaultDeclaration())continue;
18
+ const value=statement.get('declaration');if(!value.isArrowFunctionExpression())continue;
19
+ let jsx=false;value.traverse({JSXElement(p){jsx=true;p.stop();},JSXFragment(p){jsx=true;p.stop();}});if(!jsx)continue;
20
+ let name='PRNextDefaultComponent',suffix=0;
21
+ while(program.scope.hasBinding(name)||program.scope.hasGlobal(name))name='PRNextDefaultComponent'+(++suffix);
22
+ const id=t.identifier(name);
23
+ statement.replaceWithMultiple([t.variableDeclaration('const',[t.variableDeclarator(id,value.node)]),t.exportDefaultDeclaration(t.cloneNode(id))]);
24
+ program.scope.crawl();
25
+ }
26
+ }}}};
27
+ }
28
+ function sourceElements(id){return ({types:t})=>({visitor:{JSXOpeningElement(node){
29
+ const name=node.node.name;
30
+ if(name.type!=='JSXIdentifier'||!/^[a-z]/.test(name.name)||node.node.attributes.some(attribute=>attribute.type==='JSXAttribute'&&attribute.name.name==='data-prnext-source'))return;
31
+ const start=node.node.loc?.start;if(!start)return;
32
+ node.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-prnext-source'),t.stringLiteral(`${id}:${start.line}:${start.column+1}`)));
33
+ }}});}
34
+
35
+ /** The same original JSX locations must be present in SSR and browser output. */
36
+ export function createSourceLocationTransform(projectRoot){
37
+ projectRoot=path.resolve(projectRoot);const canonicalRoot=realpath(projectRoot).catch(()=>projectRoot);
38
+ return async(source,filename)=>{
39
+ filename=path.resolve(filename);
40
+ if(!source.includes('<')||filename.includes(`${path.sep}node_modules${path.sep}`)||filename.includes(`${path.sep}.prnext-build-`))return source;
41
+ const root=filename.startsWith(projectRoot+path.sep)?projectRoot:await canonicalRoot;
42
+ if(!filename.startsWith(root+path.sep))return source;
43
+ const id=path.relative(root,filename).split(path.sep).join('/');
44
+ return cachedAsyncTransform('dev-source-elements',source,[root,filename,'v1'],async()=>{
45
+ const result=await transformAsync(source,{filename,babelrc:false,configFile:false,sourceMaps:true,sourceFileName:filename,sourceType:'unambiguous',parserOpts:{plugins:['jsx',...(/\.tsx?$/.test(filename)?['typescript']:[])]},plugins:[sourceElements(id)]});
46
+ return inlineSourceMap(result.code,result.map);
47
+ });
48
+ };
49
+ }
12
50
 
13
51
  /** Keep React renderers and framework contexts identical across rebuilt ESM chunks. */
14
52
  export function devSingletonsPlugin() {
@@ -46,8 +84,9 @@ function componentExports(program) {
46
84
  }
47
85
 
48
86
  /** The official React transform records hook signatures and refresh families. */
49
- export function createRefreshTransform(projectRoot) {
87
+ export function createRefreshTransform(projectRoot, { generation,forceGeneration=false,forceFiles } = {}) {
50
88
  projectRoot = path.resolve(projectRoot);
89
+ const affected=forceFiles&&new Set(forceFiles.map(file=>path.resolve(projectRoot,file)));
51
90
  const canonicalRoot = realpath(projectRoot).catch(() => projectRoot);
52
91
  return async (source, filename) => {
53
92
  filename = path.resolve(filename);
@@ -57,13 +96,13 @@ export function createRefreshTransform(projectRoot) {
57
96
  const root = filename.startsWith(projectRoot + path.sep) ? projectRoot : await canonicalRoot;
58
97
  if (!filename.startsWith(root + path.sep)) return source;
59
98
  const hash = createHash('sha256').update(source).digest('hex');
60
- return cachedAsyncTransform('fast-refresh',source,[projectRoot,filename],async () => {
99
+ const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-v2'],async () => {
100
+ const id = path.relative(root, filename).replaceAll(path.sep, '/');
61
101
  const result = await transformAsync(source, { filename, babelrc: false, configFile: false,
62
102
  ast: true, sourceMaps: true, sourceFileName:filename, sourceType: 'unambiguous',
63
103
  parserOpts: { plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] },
64
- plugins: [[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }]],
104
+ plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id)],
65
105
  });
66
- const id = path.relative(root, filename).replaceAll(path.sep, '/');
67
106
  // An ESM import changes webpack's interpretation of module.exports.
68
107
  // Keep script/CommonJS inputs in their original module format.
69
108
  const runtime = result.ast.program.sourceType === 'script'
@@ -73,5 +112,17 @@ export function createRefreshTransform(projectRoot) {
73
112
  const prefix = `${strict}${runtime}function __prnextRefreshReg(type,id){__prnextRegister(type,${JSON.stringify(id + ':')}+id)};\n`;
74
113
  return inlineSourceMap(prefix + result.code + `\n__prnextRegisterModule(${JSON.stringify(id)},${JSON.stringify(hash)},${componentExports(result.ast.program)});\n`,{...result.map,mappings:';'+result.map.mappings});
75
114
  });
115
+ // ESM URLs are cached forever by the browser. A -> B -> exact A otherwise
116
+ // imports an already evaluated content-hashed chunk and never registers A
117
+ // with React Refresh again. Mark project modules per generation, after the
118
+ // persistent transform cache. Keep the marker stable for unchanged modules:
119
+ // esbuild can retain their parsed graph rather than reparsing every route.
120
+ if(!generation)return transformed;
121
+ const identity=refreshIdentities.get(filename);
122
+ if((affected?affected.has(filename):forceGeneration)||!identity||identity.hash!==hash){
123
+ if(refreshIdentities.size>=50_000&&!identity)refreshIdentities.delete(refreshIdentities.keys().next().value);
124
+ refreshIdentities.set(filename,{hash,generation});
125
+ }
126
+ return transformed + `\nglobalThis.__PRNEXT_DEV__.evaluatedGeneration=${JSON.stringify(refreshIdentities.get(filename).generation)};\n`;
76
127
  };
77
128
  }
package/build/edge.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
2
- import {frameworkImportName, frameworkSubpathPattern} from './framework-imports.mjs';
2
+ import {applicationEntry, frameworkImportName, frameworkSubpathPattern} from './framework-imports.mjs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { builtinModules, createRequire } from 'node:module';
6
- import { build } from './compiler.mjs';
6
+ import { build, readCompilerFile } from './compiler.mjs';
7
7
  import { parse } from '@babel/parser';
8
8
  import traversePackage from '@babel/traverse';
9
9
  import { cachedTransform } from './transform-cache.mjs';
@@ -85,6 +85,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
85
85
  mainFields: ['browser', 'module', 'main'], logLevel: 'silent', metafile: true,
86
86
  define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...defineEnvironment, 'process.env.NEXT_RUNTIME': '"edge"' },
87
87
  plugins: [...plugins, { name: 'prnext-edge', setup(builder) {
88
+ applicationEntry(builder,readCompilerFile);
88
89
  builder.onResolve({ filter: /.*/ }, args => {
89
90
  if (builtin.has(args.path) || args.path.startsWith('node:')) return { errors: [{ text: `Node.js module ${args.path} cannot run in the Edge Runtime (${args.importer}).` }] };
90
91
  if (reactMode && ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node'].includes(args.path)) return { path: args.path, namespace: 'prnext-edge-react' };
@@ -1,3 +1,8 @@
1
+ import {fileURLToPath} from 'node:url';
2
+ import {dirname} from 'node:path';
3
+ import {readFile} from 'node:fs/promises';
4
+ import {analyzeSource} from './source-analysis.mjs';
5
+ import {cachedTransform} from './transform-cache.mjs';
1
6
  const adapters = new Map([
2
7
  ['dist/shared/lib/router-context.shared-runtime','next-router-context'],
3
8
  ['dist/shared/lib/router-context','next-router-context'],
@@ -15,3 +20,38 @@ export function frameworkImportName(specifier) {
15
20
  const name=specifier.replace(frameworkSubpathPattern, '').replace(/\.js$/,'');
16
21
  return adapters.get(name) || name;
17
22
  }
23
+
24
+ // Application graphs must not traverse the programmatic build API's native tools.
25
+ // 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
28
+ // silently bundle a server implementation or misclassify a client boundary.
29
+ builder.onLoad({filter:/\.[cm]ts$/i,namespace:'file'},async args=>{
30
+ // Loader bridges dispatch framework callbacks independently of their filters.
31
+ if(!/\.[cm]ts$/i.test(args.path))return;
32
+ const source=await readSource(args.path,args.prnextSource);
33
+ const original=args.prnextSource===undefined?source:await readSource(args.path);
34
+ // A loader may remove a directive as well as introduce one. Both versions
35
+ // must be safe before its output can become an ordinary browser module.
36
+ for(const candidate of new Set([original,source])){
37
+ const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v3'],()=>{
38
+ const analysis=analyzeSource(candidate,args.path);
39
+ if(analysis.frameworkBoundary)return 'yes';
40
+ const pending=[analysis.program];
41
+ while(pending.length){
42
+ const node=pending.pop();
43
+ if(node.type==='ExpressionStatement'&&node.directive==='use server')return 'yes';
44
+ for(const value of Object.values(node)){
45
+ if(Array.isArray(value)){for(const item of value)if(item?.type)pending.push(item);}
46
+ else if(value&&typeof value==='object'&&value.type)pending.push(value);
47
+ }
48
+ }
49
+ return 'no';
50
+ });
51
+ if(unsupported==='yes')throw new Error(`React server/client directives are not supported in ${args.path}. Rename this file to .ts or .tsx.`);
52
+ }
53
+ return {contents:source,loader:args.prnextSource===undefined?'ts':'tsx',resolveDir:dirname(args.path)};
54
+ });
55
+ builder.onResolve({filter:/^(?:prnext|@thomas\.f\/prnext)(?:\/route(?:\.js)?)?$/},()=>
56
+ ({path:fileURLToPath(new URL('../runtime/typed-route.mjs',import.meta.url))}));
57
+ }
@@ -3,13 +3,17 @@ import { readFile, writeFile, mkdir, realpath } from 'node:fs/promises';
3
3
  import { createHash } from 'node:crypto';
4
4
  import path from 'node:path';
5
5
  import {clearDirectoryCache} from './directory-cache.mjs';
6
+ import {closeWebpackCompiler,webpackCompilerStats} from './webpack-graph.mjs';
6
7
  const contexts = new Map();
7
8
  const LIMIT = 8;
8
9
  let created = 0, reused = 0;
9
- export const incrementalCompilerStats = () => ({contexts:contexts.size, created, reused});
10
- export async function closeIncrementalCompiler() {
10
+ export const incrementalCompilerStats = () => ({contexts:contexts.size, created, reused,webpack:webpackCompilerStats()});
11
+ export async function closeIncrementalCompiler(root) {
11
12
  clearDirectoryCache();
12
- const active=[...contexts.values()];contexts.clear();await Promise.all(active.map(slot=>slot.context.dispose()));
13
+ const active=[];for(const [key,slot] of contexts)if(root===undefined||slot.root===root){contexts.delete(key);active.push(slot);}
14
+ const results=await Promise.allSettled([...active.map(slot=>slot.context.dispose()),closeWebpackCompiler(root)]);
15
+ const errors=results.filter(result=>result.status==='rejected').map(result=>result.reason);
16
+ if(errors.length)throw new AggregateError(errors,'Unable to dispose incremental compiler sessions');
13
17
  }
14
18
  /** Retain esbuild's parsed module graphs while rebinding per-build framework plugins. */
15
19
  export async function incrementalBuild(options, {root,stage,group='project',run=callback=>callback()}) {
@@ -40,7 +44,7 @@ export async function incrementalBuild(options, {root,stage,group='project',run=
40
44
  if(slot){contexts.delete(key);reused++;}
41
45
  else {
42
46
  while([...contexts.values()].filter(slot=>slot.group===group).length>=(group==='auxiliary'?2:LIMIT)){const [oldest,previous]=[...contexts].find(([,slot])=>slot.group===group);contexts.delete(oldest);await previous.context.dispose();}
43
- created++;slot={group};
47
+ created++;slot={group,root};
44
48
  slot.current={hooks,run,toVirtual,fromVirtual,virtualResult,stage};
45
49
  slot.context=await context({...normalized,plugins:[{name:'prnext-incremental-delegates',setup(builder){
46
50
  slot.builder=builder;
package/build/index.mjs CHANGED
@@ -1,11 +1,17 @@
1
+ import {prerenderPages as prerender} from './prerender-pages.mjs';
2
+ import {isolatedPrerender} from './production-builder.mjs';
1
3
  import { rename } from '../runtime/fs.mjs';
2
4
  import { writeRuntimeContract } from './runtime-contract.mjs';
3
5
  import { writeBuildOutput } from './output-contract.mjs';
4
6
  import { bundleRuntime } from './bundle-runtime.mjs';
5
- import {frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
7
+ import {withPersistentTransforms} from './transform-cache.mjs';
8
+ import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
6
9
  import { expandLocales, localizedStaticPaths } from './i18n.mjs';
7
- import { build as bundle, withCompilation, configureCompiler, compilerSource } from './compiler.mjs';
8
- import { mkdir, readFile, writeFile, rm, cp, stat, readdir } from 'node:fs/promises';
10
+ import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile } from './compiler.mjs';
11
+ import {writeBuildAnalysis} from './analyze.mjs';
12
+ import {routeFiles} from './dev-discovery.mjs';
13
+ import {writeTypedRoutes} from './typed-routes.mjs';
14
+ import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir } from 'node:fs/promises';
9
15
  import path from 'node:path';
10
16
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
17
  import { createRequire } from 'node:module';
@@ -30,7 +36,7 @@ import { prepareMetadataRoutes } from './metadata-routes.mjs';
30
36
  import { prepareImageResponse } from './og.mjs';
31
37
  import { prepareCacheHandler, prepareCacheHandlers } from './cache-handlers.mjs';
32
38
  import { npmPackagesPlugin } from './npm-packages.mjs';
33
- import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
39
+ import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
34
40
  import { snapshotEnvConfig } from '../runtime/env.mjs';
35
41
  import { validateStaticPaths } from '../runtime/pages-paths.mjs';
36
42
  import { projectTsconfig, checkTypeScript } from './typescript.mjs';
@@ -41,10 +47,32 @@ const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'n
41
47
  const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
42
48
  const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
43
49
 
44
- function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fonts, refresh }) {
50
+ // These framework files have already been enumerated: copyFile avoids cp's
51
+ // recursive traversal/stat work for every file. Bound I/O on large installations.
52
+ async function copyRuntimeFiles(stage) {
53
+ const files = (await Promise.all(['runtime', 'compat'].map(async directory => {
54
+ await mkdir(path.join(stage, directory), { recursive: true });
55
+ 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'))
57
+ .map(name => path.join(directory, name));
58
+ }))).flat();
59
+ let next = 0;
60
+ const copying = await Promise.allSettled(Array.from({ length: Math.min(8, files.length) }, async () => {
61
+ while (next < files.length) {
62
+ const file = files[next++];
63
+ await copyFile(path.join(packageRoot, file), path.join(stage, file));
64
+ }
65
+ }));
66
+ // Let outstanding writes finish before the caller removes a failed stage.
67
+ const failed = copying.find(result => result.status === 'rejected');
68
+ if (failed) throw failed.reason;
69
+ }
70
+
71
+ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fonts, refresh, locations }) {
45
72
  return {
46
73
  name: 'prnext-pages-compatibility',
47
74
  setup(esbuild) {
75
+ applicationEntry(esbuild,readCompilerFile);
48
76
  esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, args => args.importer === documentFile
49
77
  ? { errors: [{ text: 'CSS cannot be imported within pages/_document. Move global styles to pages/_app, or return collected styles from Document.getInitialProps.' }] } : undefined);
50
78
  esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
@@ -69,7 +97,8 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fo
69
97
  }
70
98
  esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
71
99
  if (browser && args.path === documentFile) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a browser page or component.' }] };
72
- const source = await fonts.transform(args.prnextSource ?? await readFile(args.path, 'utf8'), args.path);
100
+ const original=await readCompilerFile(args.path,args.prnextSource);
101
+ const source = await fonts.transform(locations?await locations(original,args.path):original, args.path);
73
102
  assertSupportedSource(source, args.path);
74
103
  const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
75
104
  let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
@@ -110,91 +139,34 @@ async function addBuiltinPagesErrors(project, stage) {
110
139
  }
111
140
  }
112
141
 
113
- async function prerender(stage, manifest, dev) {
114
- const { prerenderRoute, renderFallback } = await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
115
- const paths = new Set();
116
- const seedsByRoute = new Map();
117
- const validatedDocuments = new Set();
118
- const staticListings = new Map();
119
- // Error page data is available when another static page returns notFound.
120
- const ordered = [...manifest.routes].sort((left, right) => Number(!!right.errorStatus) - Number(!!left.errorStatus));
121
- for (const route of ordered) {
122
- if (route.kind !== 'page' || route.router === 'app') continue;
123
- const modulePath = path.join(stage, route.module);
124
- const page = await import(pathToFileURL(modulePath).href);
125
- if (page.__prnextDocument && !validatedDocuments.has(page.__prnextDocument)) {
126
- for (const name of ['getStaticProps', 'getStaticPaths', 'getServerSideProps']) {
127
- if (Object.hasOwn(page.__prnextDocument, name) || typeof page.Document?.[name] === 'function') throw new Error(`pages/_document does not support ${name}. Use Document.getInitialProps for server document rendering.`);
128
- }
129
- validatedDocuments.add(page.__prnextDocument);
130
- }
131
- if (route.errorStatus && (page.getServerSideProps || page.default?.getInitialProps)) throw new Error(`pages${route.pattern} must be static and cannot use getServerSideProps or getInitialProps. Use getStaticProps instead.`);
132
- route.gip = typeof page.default?.getInitialProps === 'function';
133
- route.appGip = !!page.App && page.App.getInitialProps !== page.App.origGetInitialProps;
134
- if (route.internal && route.errorStatus && route.appGip) {
135
- // A custom App hook makes the built-in Error request-dependent. Select
136
- // its actual _error module so the default status props and visible URL
137
- // reach App.getInitialProps instead of a synthetic static wrapper.
138
- const kind = route.errorStatus === 404 ? 'notFound' : 'serverError';
139
- if (manifest.pagesErrors?.[kind] === route.id) delete manifest.pagesErrors[kind];
140
- }
141
- if (route.gip && (page.getStaticProps || page.getServerSideProps)) throw new Error(`Page ${route.pattern} cannot combine getInitialProps with getStaticProps or getServerSideProps.`);
142
- if (route.internal && !route.errorStatus) {
143
- if (page.getStaticProps || page.getServerSideProps || page.getStaticPaths) throw new Error('pages/_error does not support getStaticProps, getServerSideProps or getStaticPaths. Use ErrorComponent.getInitialProps instead.');
144
- continue;
145
- }
146
- if (page.getServerSideProps && page.getStaticProps) throw new Error(`Page ${route.pattern} exports both getServerSideProps and getStaticProps. Choose one.`);
147
- if (page.getStaticPaths && !page.getStaticProps) throw new Error(`Page ${route.pattern} exports getStaticPaths without getStaticProps.`);
148
- if (page.getServerSideProps) { route.ssp = true; continue; }
149
- if (page.getStaticProps) route.ssg = true;
150
- else if (route.gip || route.appGip) continue;
151
- let entries = [{ path: route.pattern, params: {} }];
152
- if (isDynamic(route.pattern)) {
153
- if (!page.getStaticProps) continue;
154
- if (!page.getStaticPaths) throw new Error(`Dynamic page ${route.pattern} with getStaticProps must export getStaticPaths.`);
155
- if (!staticListings.has(modulePath)) staticListings.set(modulePath, Promise.resolve().then(() => page.getStaticPaths(manifest.config.i18n ? {locales: manifest.config.i18n.locales, defaultLocale: manifest.config.i18n.defaultLocale} : {})));
156
- const result = localizedStaticPaths(route, await staticListings.get(modulePath), manifest.config.i18n);
157
- route.fallback = result.fallback;
158
- entries = result.paths;
159
- } else if (page.getStaticPaths) throw new Error(`getStaticPaths is only valid on dynamic pages (${route.pattern}).`);
160
- for (const entry of entries) {
161
- if (paths.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
162
- paths.add(entry.path);
163
- const rendered = await prerenderRoute({ ...manifest.config, manifest, distDir: stage, route, modulePath, path: entry.path, params: entry.params, client: route.client, css: route.css, pattern: route.pattern, errorStatus: route.errorStatus, buildId: manifest.buildId, production: !dev });
164
- const file = `static/${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.html`;
165
- await writeFile(path.join(stage, file), rendered.body);
166
- const seed = { routeId: route.id, path: entry.path, file, status: route.errorStatus || rendered.status || 200, headers: rendered.headers || {} };
167
- if (route.ssg) {
168
- seed.dataFile = file.replace(/\.html$/, '.json');
169
- seed.revalidate = rendered.revalidate ?? false;
170
- seed.generatedAt = rendered.generatedAt;
171
- await writeFile(path.join(stage, seed.dataFile), rendered.dataJSON);
172
- }
173
- manifest.prerendered.push(seed);
174
- if (!seedsByRoute.has(route.id)) seedsByRoute.set(route.id, []);
175
- seedsByRoute.get(route.id).push(seed);
176
- }
177
- if (route.fallback === true) {
178
- const rendered = await renderFallback({ ...manifest.config, modulePath, path: route.pattern, params: {}, client: route.client, css: route.css, pattern: route.pattern, buildId: manifest.buildId, production: !dev });
179
- route.fallbackFile = `static/fallback-${route.id}.html`;
180
- await writeFile(path.join(stage, route.fallbackFile), rendered.body);
181
- }
182
- }
183
- // Rendering errors first must not change the published route/seed ordering.
184
- manifest.prerendered = manifest.routes.flatMap(route => seedsByRoute.get(route.id) || []);
185
- }
186
142
 
187
143
  let buildQueue = Promise.resolve();
144
+ let developmentModulesLoaded = false;
145
+ /** Disposing shared compiler contexts must not interrupt another queued build. */
146
+ export function disposeBuildSession(root){
147
+ const task=buildQueue.then(async()=>{const {closeIncrementalCompiler}=await import('./incremental.mjs');await closeIncrementalCompiler(root);});
148
+ buildQueue=task.catch(()=>{});return task;
149
+ }
188
150
  /** Isolate process.env between programmatic builds, including simultaneous calls. */
189
151
  export function build(projectRoot, options = {}) {
190
152
  const task = buildQueue.then(async () => {
153
+ if (options.adapter === undefined || options.adapter === 'auto') {
154
+ const { detectBuildAdapter } = await import('./static-input.mjs');
155
+ const selected = await detectBuildAdapter(projectRoot, { prepared: options.adapter === 'auto' });
156
+ if (selected.adapter === 'static') return (await import('./static-adapter.mjs')).buildStatic(projectRoot, { ...selected, ...options });
157
+ if (options.adapter === 'auto') options = { ...options, adapter: 'react' };
158
+ }
191
159
  if (options.adapter === 'static') return (await import('./static-adapter.mjs')).buildStatic(projectRoot, options);
192
160
  if (options.adapter !== undefined && options.adapter !== 'react') throw new Error(`Unknown PRNext adapter: ${options.adapter}`);
193
161
  if (options.from !== undefined || options.spa !== undefined) throw new Error('from and spa are only supported by the static adapter.');
194
162
  const restore = snapshotEnvConfig();
195
163
  const envMode = options.envMode || (process.env.NODE_ENV === 'test' ? 'test' : options.dev ? 'development' : 'production');
196
164
  process.env.NODE_ENV = options.dev ? 'development' : 'production';
197
- try { return await withBuildLock(projectRoot, root => withCompilation(() => buildProject(root, { ...options, envMode }))); }
165
+ try { return await withBuildLock(projectRoot, async root => {
166
+ const started=performance.now();
167
+ const result=await withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode })));
168
+ return {...result,durationMs:Math.round(performance.now()-started)};
169
+ }); }
198
170
  finally { restore(); }
199
171
  });
200
172
  buildQueue = task.catch(() => {});
@@ -202,15 +174,23 @@ export function build(projectRoot, options = {}) {
202
174
  }
203
175
 
204
176
  /** Compile Pages Router and App Router projects. A failed build preserves .prnext. */
205
- async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs } = {}) {
177
+ async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs, devRoutes, devSequence, devSession, devTools,devForceRefresh=false,devAffectedModules } = {}) {
206
178
  const started = performance.now();
207
- const config = await loadProjectConfig(projectRoot, { dev, envMode });
179
+ const config = await loadProjectConfig(projectRoot, { dev, envMode, devInputs });
208
180
  const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
209
181
  const environment = defineEnvironment(config);
210
182
  const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
211
183
  const customRoutes = await compileCustomRoutes(config);
212
184
  const buildId = await generateBuildId(config, { dev });
213
- const project = await scanProject(projectRoot, { basePath: config.basePath, pageExtensions: config.pageExtensions, i18n: config.i18n });
185
+ const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
186
+ let project;
187
+ if(dev && Array.isArray(devRoutes)) {
188
+ const discovery=await scanProject(projectRoot,{...scanOptions,devAppFiles:new Set()});
189
+ project=await scanProject(projectRoot,{...scanOptions,devAppFiles:routeFiles(discovery,devRoutes)});
190
+ await writeTypedRoutes(projectRoot,discovery,config);
191
+ project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
192
+ } else project=await scanProject(projectRoot,scanOptions);
193
+ if(!dev || !Array.isArray(devRoutes))await writeTypedRoutes(projectRoot,project,config);
214
194
  const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
215
195
  const destination = await prepareBuildDirectory(project.root, config.distDir);
216
196
  const backup = path.join(project.root, `.prnext-backup-${randomUUID()}`);
@@ -218,33 +198,28 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
218
198
  await mkdir(path.join(stage, 'static'), { recursive: true });
219
199
  await addBuiltinPagesErrors(project, stage);
220
200
  const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
201
+ ...(dev && Array.isArray(devRoutes) ? {devOnDemand:true} : {}), ...(dev && devSession ? {devSequence,devSession} : {}), ...(dev ? {devForceRefresh,devAffectedModules,devIndicators:config.devIndicators,...(devTools?{devTools}:{})} : {}),
221
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: [],
222
203
  ...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
223
204
  // Its build-unique name is known before bundling and avoids a hash cycle
224
205
  // between page entry URLs and the manifest that lists those entries.
225
206
  const pagesManifestUrl = `${publicPaths.assetBase}/pages-manifest-${manifest.cacheId}.json`;
226
207
  if (dev) manifest.devClient = `${publicPaths.assetBase}/dev-manifest-${manifest.cacheId}.json`;
227
- const development = dev ? { buildId, clientManifest: manifest.devClient } : undefined;
228
- const refresh = dev ? createRefreshTransform(project.root) : undefined;
208
+ const development = dev ? { buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:devSequence, session:devSession, indicator:config.devIndicators,...(devTools?{tools:devTools}:{}) } : undefined;
209
+ const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:devForceRefresh,forceFiles:devAffectedModules}) : undefined;
210
+ const locations = dev ? createSourceLocationTransform(project.root) : undefined;
229
211
  const fonts = createFonts({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
230
- const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions });
212
+ const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine });
231
213
  configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,tsconfig,assetBase:publicPaths.assetBase});
232
214
  try {
233
- await mkdir(path.join(stage, 'runtime'), { recursive: true });
234
- await mkdir(path.join(stage, 'compat'), { recursive: true });
235
- for (const name of await readdir(path.join(packageRoot, 'runtime'))) {
236
- if (name.endsWith('.mjs') && !name.endsWith('.test.mjs')) await cp(path.join(packageRoot, 'runtime', name), path.join(stage, 'runtime', name));
237
- }
238
- await cp(path.join(packageRoot, 'runtime/profiles.json'), path.join(stage, 'runtime/profiles.json'));
215
+ await copyRuntimeFiles(stage);
239
216
  // Runtime copies must work even when dotenv is nested beneath the PRNext
240
217
  // package instead of hoisted beside the application's dependencies.
241
218
  await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
242
- for (const name of await readdir(path.join(packageRoot, 'compat'))) {
243
- if (name.endsWith('.cjs')) await cp(path.join(packageRoot, 'compat', name), path.join(stage, 'compat', name));
244
- }
245
219
  await prepareImageResponse(stage);
246
220
  manifest.config.serverActions = config.experimental.serverActions;
247
221
  if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
222
+ if(config.workerPreload&&!dev)manifest.config.workerPreload=config.workerPreload;
248
223
  const cacheHandlers = await prepareCacheHandlers({ config, projectRoot: project.root, stage, environment });
249
224
  if (Object.keys(cacheHandlers).length) manifest.config.cacheHandlers = cacheHandlers;
250
225
  const cacheHandler = await prepareCacheHandler({ config, projectRoot: project.root, stage, environment });
@@ -270,7 +245,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
270
245
  }
271
246
  const common = { absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders, assetNames: '[name]-[hash]', publicPath: publicPaths.assetBase, metafile: true, define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...environment } };
272
247
  if (Object.keys(serverEntries).length) {
273
- const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, fonts }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
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)] });
274
249
  fonts.record(server.metafile);
275
250
  const serverChunks = new Set(Object.keys(server.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
276
251
  for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
@@ -285,7 +260,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
285
260
  }
286
261
  }
287
262
  if (Object.keys(clientEntries).length) {
288
- const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, refresh }), cssModules.plugin(true)] });
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)] });
289
264
  fonts.record(browser.metafile);
290
265
  const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
291
266
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
@@ -303,14 +278,21 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
303
278
  if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
304
279
  await prepareMetadataRoutes(project, stage);
305
280
  await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
281
+ if(manifest.config.workerPreload)for(const pattern of config.workerPreload.routes){
282
+ const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
283
+ if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
284
+ }
306
285
  await checkTypeScript(project.root, config, { dev, tsconfig });
307
286
  fonts.attach(manifest);
308
287
  expandLocales(manifest);
309
288
  // All runtime preparation must finish before its shared modules are built;
310
289
  // no staged renderer has been imported before this production-only point.
311
- if (!dev) await bundleRuntime(stage);
312
- await prerender(stage, manifest, dev);
313
- await prerenderApp(stage, manifest, dev);
290
+ if (!dev) await bundleRuntime(stage,{projectRoot:project.root});
291
+ // Node caches CJS React by filename, not NODE_ENV. Once development metadata
292
+ // imports load it, subsequent production renders need a clean process too.
293
+ if(dev) developmentModulesLoaded = true;
294
+ if(!dev && (incremental || developmentModulesLoaded)) Object.assign(manifest,await isolatedPrerender(project.root,stage,manifest));
295
+ else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
314
296
  const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
315
297
  const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
316
298
  if (pages.length || manifest.pagesErrors) {
@@ -332,6 +314,16 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
332
314
  }
333
315
  await rm(path.join(stage, '.entries'), { recursive: true });
334
316
  if (dev) {
317
+ manifest.devStyles={bundles:devStylePlans(),dependencies:devStyleDependencies(),modules:await cssModules.mappings()};
318
+ for(const [,files]of manifest.devStyles.dependencies)for(const module of manifest.devStyles.modules)if(files.includes(module.file))for(const file of module.watchFiles)if(!files.includes(file))files.push(file);
319
+ manifest.devDependencies=devDependencies(project,manifest);
320
+ manifest.devModules=devModules(project.root);
321
+ manifest.devDependencyStatus={complete:!manifest.devDependencies.incomplete&&!manifest.devModules.incomplete,reason:manifest.devDependencies.incomplete||manifest.devModules.incomplete};
322
+ for(const route of manifest.devDependencies)for(const module of manifest.devStyles.modules){
323
+ if(route.files.includes(path.relative(project.root,module.file).replaceAll(path.sep,'/')))for(const file of module.watchFiles){
324
+ const relative=path.relative(project.root,file).replaceAll(path.sep,'/');if(!relative.startsWith('../')&&!route.files.includes(relative))route.files.push(relative);
325
+ }
326
+ }
335
327
  const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
336
328
  await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
337
329
  pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
@@ -341,7 +333,10 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
341
333
  await writeRuntimeContract(stage);
342
334
  await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
343
335
  await writeBuildOutput(stage, manifest);
344
- if (config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
336
+ // Offline compression is a deployment optimization. Recompressing source
337
+ // maps and every entry after a save stalls Fast Refresh for no local benefit.
338
+ if (!dev && config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
339
+ if(!dev)await writeBuildAnalysis({root:project.root,stage,destination,manifest,records:buildAnalysisRecords(),routeInputs:buildAnalysisRoutes(),precompressed:config.compress});
345
340
  if (config.output === 'standalone' && !dev) await (await import('./standalone-runner.mjs')).prepareStandalone({ projectRoot: project.root, stage, manifest, config });
346
341
  const exporting = config.output === 'export' && !dev ? await import('./static-export.mjs') : null;
347
342
  const exportSource = exporting && await exporting.prepareStaticExport({ project, stage, manifest, config });
package/build/inspect.mjs CHANGED
@@ -69,6 +69,7 @@ export async function inspectProject(directory) {
69
69
  sample: { events: recent.length, from: recent[0]?.time, to: recent.at(-1)?.time },
70
70
  notes: ['Enable measurements with prn start --inspect (or prn dev --inspect), visit routes, then run prn inspect again.',
71
71
  'HTTP and fetch durations end when response headers are ready. Samples are bounded and can span server sessions; they are not full-response benchmarks.',
72
+ 'Request-stage samples additionally separate admission, worker first byte, response headers and complete body consumption, with completion/cancellation/deadline outcomes. Stages overlap; do not add their durations. Body completion means the HTTP layer consumed the application body, not remote delivery.',
72
73
  'Fetch destinations omit queries and credentials. Runtime cache tags/paths are hashed; matching identifiers link reads to invalidations. No diagnostic HTTP endpoint is exposed.'] };
73
74
  }
74
75
  export function printInspection(report) {
@@ -76,6 +77,8 @@ export function printInspection(report) {
76
77
  console.log(`Recorded sample: ${report.summary.requests} HTTP requests; p50 ${report.summary.p50Ms ?? 'n/a'} ms; p95 ${report.summary.p95Ms ?? 'n/a'} ms (response headers); ${report.summary.errors} server errors.`);
77
78
  if(report.summary.wakeP95Ms !== null) console.log(`Worker startup p95: ${report.summary.wakeP95Ms} ms.`);
78
79
  if(report.summary.workerRssBytes !== null) console.log(`Latest recorded worker RSS: ${(report.summary.workerRssBytes/1048576).toFixed(1)} MiB. Native server memory is not included.`);
80
+ for(const stage of report.requestStages||[]) console.log(`STAGE ${stage.stage}: ${stage.samples} sample(s), p95 ${stage.p95Ms ?? 'n/a'} ms; ${stage.completed} completed, ${stage.failed} failed, ${stage.cancelled} cancelled, ${stage.timedOut} timed out.`);
81
+ for(const budget of report.budgets||[])console.log(`BUDGET ${budget.budget}: ${budget.used ?? 'n/a'}/${budget.limit ?? 'n/a'} ${budget.unit}, ${budget.waiting ?? 'n/a'} waiting; ${budget.rejected ?? 'n/a'} rejected, ${budget.timedOut ?? 'n/a'} timed out, ${budget.cancelledWaits ?? 'n/a'} cancelled waits.`);
79
82
  for(const finding of report.findings)console.log(`${finding.level.toUpperCase()}: ${finding.message} ${finding.action}`);
80
83
  for (const route of report.routes) console.log(`${route.mode.padEnd(13)} ${route.pattern}\n ${route.reasons.join('; ') || `${route.prerenderedPaths} prerendered path(s); revalidate: ${route.revalidate.join(', ') || 'on demand'}`}`);
81
84
  for (const item of report.measurements) console.log(`${item.type.toUpperCase()} ${item.route}${item.destination ? ' -> ' + item.destination : ''}: ${item.count} sample(s)${item.state ? ', ' + item.state : ''}${item.reason ? ' (' + item.reason + ')' : ''}${item.p95Ms !== undefined ? ', p95 ' + item.p95Ms.toFixed(2) + ' ms' : ''}, ${item.errors} error(s)`);
@@ -1,22 +1,46 @@
1
1
  const quantile = (values, q) => values.length ? Math.round([...values].sort((a,b)=>a-b)[Math.ceil(values.length*q)-1]*100)/100 : null;
2
2
  const durations = events => events.map(e=>e.durationMs).filter(v=>Number.isFinite(v)&&v>=0);
3
+ const stages=['admission','renderStart','bodyAdmission','bodyRead','workerQueue','workerFirstByte','workerHeaders','httpHeaders','responseBody','requestTotal'];
4
+ const outcomes=['completed','failed','cancelled','timedOut'];
5
+ const budgetNames=['renderLive','apiLive','renderStart','inputBytes','responseBytes'];
6
+ const validTime=value=>Number.isSafeInteger(value)&&value>=0&&value<=18446744073709;
7
+ function summarizeStages(events) {
8
+ return stages.flatMap(stage=>{
9
+ const sample=events.filter(event=>event.stage===stage), times=durations(sample);
10
+ return sample.length?[{stage,samples:sample.length,p50Ms:quantile(times,.5),p95Ms:quantile(times,.95),...Object.fromEntries(outcomes.map(outcome=>[outcome,sample.filter(event=>event.outcome===outcome).length]))}]:[];
11
+ });
12
+ }
13
+ function currentBudgets(snapshot) {
14
+ if(!Array.isArray(snapshot?.workers?.budgets))return null;
15
+ return budgetNames.flatMap(budget=>{
16
+ const value=snapshot.workers.budgets.find(value=>value?.budget===budget);
17
+ if(!value)return [];
18
+ const row={budget,unit:budget.endsWith('Bytes')?'bytes':'requests'};
19
+ for(const key of ['limit','used','peakUsed','waiting','peakWaiting','acquired','rejected','timedOut','cancelledWaits'])row[key]=Number.isSafeInteger(value[key])&&value[key]>=0?value[key]:null;
20
+ return [row];
21
+ });
22
+ }
3
23
  export function summarizeDiagnostics(events, snapshots = []) {
4
24
  const http=events.filter(e=>e.type==='http'), times=durations(http);
5
25
  const wake=durations(events.filter(e=>e.type==='wake'&&e.phase==='node-ready'));
6
26
  const processes=new Map();
7
27
  for(const event of events)if(event.type==='process'&&Number.isSafeInteger(event.pid)&&Number.isFinite(event.rssBytes)&&event.rssBytes>=0)processes.set(event.pid,event);
8
- const last=snapshots.sort((a,b)=>b.unixMs-a.unixMs)[0];
28
+ const last=[...snapshots].sort((a,b)=>b.unixMs-a.unixMs)[0];
9
29
  const current=last&&Date.now()-last.unixMs<30000&&Date.now()>=last.unixMs;
10
30
  const rss=[...processes.values()].filter(e=>Date.now()-e.time<30000&&Date.now()>=e.time);
31
+ const stageSample=events.filter(e=>e.type==='requestStage'&&stages.includes(e.stage)&&outcomes.includes(e.outcome)&&Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&validTime(e.time)).map(({stage,outcome,durationMs,time})=>({stage,outcome,durationMs,time}));
32
+ const requestStages=summarizeStages(stageSample), totals=requestStages.find(s=>s.stage==='requestTotal');
11
33
  const summary={requests:http.length,errors:http.filter(e=>e.status>=500).length,p50Ms:quantile(times,.5),p95Ms:quantile(times,.95),wakeP95Ms:quantile(wake,.95),workerRssBytes:rss.length?rss.reduce((n,e)=>n+e.rssBytes,0):null,
12
- queuedJobs:current?last.workers.queuedJobs??null:null,activeJobs:current?last.workers.activeJobs??null:null, runtimeSampleAt:last?.unixMs??null};
34
+ queuedJobs:current?last.workers?.queuedJobs??null:null,activeJobs:current?last.workers?.activeJobs??null:null, runtimeSampleAt:last?.unixMs??null,
35
+ completedResponseP95Ms:quantile(durations(stageSample.filter(e=>e.stage==='requestTotal'&&e.outcome==='completed')),.95),cancelledResponses:totals?.cancelled??null,timedOutResponses:totals?.timedOut??null};
13
36
  const findings=[];
14
37
  if(!http.length)findings.push({level:'info',code:'NO_SAMPLE',message:'No HTTP sample recorded.',action:'Start with --inspect, visit the site and run inspect again.'});
15
38
  if(summary.queuedJobs>0)findings.push({level:'warning',code:'WORKER_QUEUE',message:`${summary.queuedJobs} jobs are waiting for a worker.`,action:'Check slow routes and available CPU/RAM before increasing concurrency.'});
16
39
  if(summary.p95Ms>=1000)findings.push({level:'warning',code:'SLOW_HEADERS',message:'The slowest 5% of recorded responses take at least one second to prepare headers.',action:'Inspect route and fetch timings below; this includes queueing and startup.'});
17
40
  if(summary.errors)findings.push({level:'warning',code:'SERVER_ERRORS',message:`${summary.errors} recorded requests returned a server error.`,action:'Check application logs for the same sample period.'});
18
41
  if(summary.wakeP95Ms>=1000)findings.push({level:'info',code:'SLOW_WAKE',message:'Recorded worker startup p95 exceeds one second.',action:'Review startup imports or use keep-warm if the extra memory is acceptable.'});
19
- return {summary,findings,metricSample:http.filter(e=>Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&Number.isSafeInteger(e.time)&&e.time>=0&&e.time<=18446744073709).map(e=>({route:typeof e.route==='string'?e.route.slice(0,256):'<unmatched>',method:/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)$/.test(e.method)?e.method:'_OTHER',durationMs:e.durationMs,time:e.time}))};
42
+ if(summary.timedOutResponses>0)findings.push({level:'warning',code:'RESPONSE_DEADLINES',message:`${summary.timedOutResponses} recorded HTTP responses reached their request deadline.`,action:'Compare admission, worker and body timings before increasing the configured timeout.'});
43
+ return {summary,findings,requestStages,budgets:current?currentBudgets(last):null,stageSample,metricSample:http.filter(e=>Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&validTime(e.time)).map(e=>({route:typeof e.route==='string'?e.route.slice(0,256):'<unmatched>',method:/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)$/.test(e.method)?e.method:'_OTHER',durationMs:e.durationMs,time:e.time}))};
20
44
  }
21
45
 
22
46
  /** OTLP/JSON Metrics export of a bounded local sample, never a complete traffic counter. */
@@ -33,5 +57,14 @@ export function inspectionMetrics(report) {
33
57
  return {attributes:[{key:'http.route',value:{stringValue:group.route}},{key:'http.request.method',value:{stringValue:group.method}}],
34
58
  startTimeUnixNano:String(BigInt(Math.trunc(group.start))*1000000n),timeUnixNano:String(BigInt(Math.trunc(group.end))*1000000n),count:String(group.values.length),sum:group.values.reduce((a,b)=>a+b,0),min:Math.min(...group.values),max:Math.max(...group.values),explicitBounds:bounds,bucketCounts:buckets.map(String)};
35
59
  });
36
- return {resourceMetrics:[{resource:{attributes:[{key:'service.name',value:{stringValue:'prnext'}},{key:'prnext.inspect.sampled',value:{boolValue:true}}]},scopeMetrics:[{scope:{name:'prnext.inspect',version:'1'},metrics:[{name:'prnext.http.response.headers.duration',description:'Bounded diagnostic sample of time until response headers; excludes response body streaming.',unit:'s',histogram:{aggregationTemporality:1,dataPoints}}]}]}]};
60
+ const stageGroups=new Map();
61
+ for(const event of report.stageSample||[]) {
62
+ if(!stages.includes(event.stage)||!outcomes.includes(event.outcome)||!Number.isFinite(event.durationMs)||event.durationMs<0||event.durationMs>86400000||!validTime(event.time))continue;
63
+ const key=JSON.stringify([event.stage,event.outcome]);
64
+ if(!stageGroups.has(key))stageGroups.set(key,{stage:event.stage,outcome:event.outcome,count:0,sum:0,min:Infinity,max:0,start:event.time,end:event.time,buckets:Array(bounds.length+1).fill(0)});
65
+ const group=stageGroups.get(key),value=event.durationMs/1000,index=bounds.findIndex(bound=>value<=bound);
66
+ group.count++;group.sum+=value;group.min=Math.min(group.min,value);group.max=Math.max(group.max,value);group.start=Math.min(group.start,event.time);group.end=Math.max(group.end,event.time);group.buckets[index<0?bounds.length:index]++;
67
+ }
68
+ const stagePoints=[...stageGroups.values()].map(g=>({attributes:[{key:'prnext.request.stage',value:{stringValue:g.stage}},{key:'prnext.request.outcome',value:{stringValue:g.outcome}}],startTimeUnixNano:String(BigInt(g.start)*1000000n),timeUnixNano:String(BigInt(g.end)*1000000n),count:String(g.count),sum:g.sum,min:g.min,max:g.max,explicitBounds:bounds,bucketCounts:g.buckets.map(String)}));
69
+ return {resourceMetrics:[{resource:{attributes:[{key:'service.name',value:{stringValue:'prnext'}},{key:'prnext.inspect.sampled',value:{boolValue:true}}]},scopeMetrics:[{scope:{name:'prnext.inspect',version:'1'},metrics:[{name:'prnext.http.response.headers.duration',description:'Bounded diagnostic sample of time until response headers; excludes response body streaming.',unit:'s',histogram:{aggregationTemporality:1,dataPoints}},{name:'prnext.request.stage.duration',description:'Bounded diagnostic sample of request stages and terminal outcomes; stages overlap and must not be added together.',unit:'s',histogram:{aggregationTemporality:1,dataPoints:stagePoints}}]}]}]};
37
70
  }