@thomas.f/prnext 0.1.19-alpha → 0.1.21-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 (113) hide show
  1. package/build/app.mjs +46 -78
  2. package/build/build-lock.mjs +2 -71
  3. package/build/build-retention.mjs +119 -0
  4. package/build/build-timings.mjs +38 -0
  5. package/build/bundle-runtime.mjs +17 -16
  6. package/build/cache-command.mjs +31 -0
  7. package/build/check.mjs +41 -3
  8. package/build/compilation-cache.mjs +32 -12
  9. package/build/compilation-storage.mjs +190 -0
  10. package/build/compiler.mjs +5 -4
  11. package/build/config.mjs +23 -2
  12. package/build/create.mjs +1 -1
  13. package/build/css-modules.mjs +4 -0
  14. package/build/dev-action-graph.mjs +21 -0
  15. package/build/flight-summary.mjs +64 -0
  16. package/build/font-cache.mjs +9 -6
  17. package/build/font-files.mjs +1 -1
  18. package/build/index.mjs +57 -36
  19. package/build/inspect.mjs +3 -0
  20. package/build/inspection-summary.mjs +7 -2
  21. package/build/native-discovery.mjs +18 -6
  22. package/build/native-ppr.mjs +32 -0
  23. package/build/npm-packages.mjs +2 -2
  24. package/build/og.mjs +121 -1
  25. package/build/parallel-plan.mjs +55 -0
  26. package/build/persistent-cache.mjs +29 -24
  27. package/build/production-check.mjs +34 -0
  28. package/build/react-compiler-report.mjs +31 -8
  29. package/build/react-compiler.mjs +101 -10
  30. package/build/server-config.mjs +35 -0
  31. package/build/source-analysis.mjs +108 -5
  32. package/build/standalone-copy.mjs +48 -0
  33. package/build/standalone.mjs +2 -22
  34. package/build/static-adapter.mjs +7 -3
  35. package/build/transform-cache.mjs +17 -3
  36. package/build/transform.mjs +38 -6
  37. package/build/typescript-dev.mjs +3 -1
  38. package/cli.mjs +27 -5
  39. package/compat/app-layout-context.cjs +7 -3
  40. package/compat/diagnostics.cjs +5 -4
  41. package/compat/flight-delta.cjs +47 -12
  42. package/compat/index.d.ts +24 -1
  43. package/compat/navigation-authorization.cjs +8 -1
  44. package/compat/runtime-sample.cjs +56 -0
  45. package/compat/use-cache.cjs +5 -2
  46. package/docs/ai/compatibility.md +113 -10
  47. package/docs/ai/deployment.md +144 -0
  48. package/docs/ai/quickstart.md +5 -0
  49. package/docs/ai/troubleshooting.md +22 -0
  50. package/package.json +25 -22
  51. package/release/fontkit.mjs +31 -0
  52. package/release/verify.mjs +3 -0
  53. package/runtime/api.mjs +5 -2
  54. package/runtime/app-client.mjs +246 -54
  55. package/runtime/app-partial.mjs +2 -10
  56. package/runtime/app-prefetch.mjs +2 -2
  57. package/runtime/app-render.mjs +42 -13
  58. package/runtime/app-stream-channel.mjs +20 -6
  59. package/runtime/app-stream-html.mjs +30 -11
  60. package/runtime/app-styles.mjs +22 -4
  61. package/runtime/cache-components-rsc.mjs +26 -6
  62. package/runtime/cache-result.mjs +58 -0
  63. package/runtime/compile-cache-retention.mjs +53 -0
  64. package/runtime/compile-cache.mjs +28 -1
  65. package/runtime/dev-client.mjs +32 -8
  66. package/runtime/dev-compiler-broker.mjs +20 -23
  67. package/runtime/dev-gateway.mjs +23 -6
  68. package/runtime/dev-route-selection.mjs +64 -0
  69. package/runtime/dev-runtime-pool.mjs +3 -2
  70. package/runtime/dev-runtime-spare.mjs +1 -1
  71. package/runtime/dev-session-retention.mjs +154 -0
  72. package/runtime/development-flight.mjs +59 -0
  73. package/runtime/development-queue.mjs +33 -0
  74. package/runtime/development.mjs +37 -16
  75. package/runtime/env.mjs +9 -0
  76. package/runtime/flight-delta.mjs +31 -5
  77. package/runtime/flight-diagnostics.mjs +48 -0
  78. package/runtime/flight-tap.mjs +10 -5
  79. package/runtime/html-envelope.mjs +34 -0
  80. package/runtime/loopback-rpc.mjs +92 -0
  81. package/runtime/native-ppr.mjs +88 -0
  82. package/runtime/og/NOTICE.txt +1 -1
  83. package/runtime/og/index.node.mjs +76 -31216
  84. package/runtime/og/upstream.json +115 -0
  85. package/runtime/persistent/client.mjs +5 -3
  86. package/runtime/persistent/common.mjs +7 -18
  87. package/runtime/persistent/daemon.mjs +4 -12
  88. package/runtime/persistent/health.mjs +58 -0
  89. package/runtime/persistent/native-proxy.mjs +1 -1
  90. package/runtime/persistent/watchdog.mjs +12 -27
  91. package/runtime/prewarm.mjs +35 -0
  92. package/runtime/project-lock.mjs +71 -0
  93. package/runtime/request-transport.mjs +112 -32
  94. package/runtime/rsc-worker.mjs +58 -8
  95. package/runtime/storage-path.mjs +36 -0
  96. package/runtime/trace.mjs +44 -4
  97. package/runtime/worker.mjs +37 -17
  98. package/vendor/fontkit/NOTICE.txt +5 -0
  99. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  100. package/vendor/fontkit/index.mjs +3 -0
  101. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  102. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  103. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  104. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  105. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  106. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  108. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  109. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  110. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  111. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  112. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  113. package/vendor/fontkit/upstream.json +215 -0
@@ -1,36 +1,139 @@
1
1
  import {parseSync} from 'oxc-parser';
2
2
  import {createHash} from 'node:crypto';
3
+ import parserPackage from 'oxc-parser/package' with {type:'json'};
4
+ import {cachedValidatedTransform} from './transform-cache.mjs';
3
5
 
4
6
  // Read-only analyses share one Rust parse. Transformations still use Babel's
5
7
  // binding-aware AST: ESTree nodes must never be passed to Babel transforms.
6
8
  const cache=new Map();
7
9
  let bytes=0,hits=0,misses=0;
8
- export const sourceAnalysisStats=()=>({entries:cache.size,bytes,hits,misses});
10
+ const summaries=new Map();
11
+ let summaryBytes=0,summaryHits=0,summaryMisses=0,summaryReplays=0;
12
+ const SUMMARY_BYTES=8*1024*1024,SUMMARY_ENTRIES=4096,SUMMARY_ITEM=128*1024;
13
+ export const sourceAnalysisStats=()=>({entries:cache.size,bytes,hits,misses,
14
+ summaries:{entries:summaries.size,bytes:summaryBytes,hits:summaryHits,misses:summaryMisses,replays:summaryReplays}});
15
+ const summaryFields=['directives','imports','computedRequire','hasServerDirective','runtimeExports','exportStars','frameworkBoundary'];
16
+ const text=value=>typeof value==='string'&&value.length<=32768;
17
+ const texts=value=>Array.isArray(value)&&value.length<=4096&&value.every(text);
18
+ function validSummary(value){
19
+ return value&&Object.getPrototypeOf(value)===Object.prototype&&Object.keys(value).length===summaryFields.length
20
+ &&summaryFields.every(name=>Object.hasOwn(value,name))&&texts(value.directives)&&texts(value.runtimeExports)&&texts(value.exportStars)
21
+ &&['computedRequire','hasServerDirective','frameworkBoundary'].every(name=>typeof value[name]==='boolean')
22
+ &&Array.isArray(value.imports)&&value.imports.length<=4096&&value.imports.every(item=>item&&Object.getPrototypeOf(item)===Object.prototype
23
+ &&Object.keys(item).length===2&&text(item.path)&&['import-statement','dynamic-import','require-call'].includes(item.kind));
24
+ }
25
+ function immutableSummary(value){
26
+ return Object.freeze({directives:Object.freeze([...value.directives]),imports:Object.freeze(value.imports.map(item=>Object.freeze({path:item.path,kind:item.kind}))),
27
+ computedRequire:value.computedRequire,hasServerDirective:value.hasServerDirective,runtimeExports:Object.freeze([...value.runtimeExports]),
28
+ exportStars:Object.freeze([...value.exportStars]),frameworkBoundary:value.frameworkBoundary});
29
+ }
30
+
31
+ /** Pure, versioned facts only: no AST, source, resolver, registry or generation. */
32
+ export function analyzeSourceSummary(source,file='source.js'){
33
+ const digest=createHash('sha256').update(source).digest('hex'),key=file+'\0'+digest;
34
+ const found=summaries.get(key);
35
+ if(found){summaries.delete(key);summaries.set(key,found);summaryHits++;return found.value;}
36
+ summaryMisses++;
37
+ let produced=false,producedAnalysis;
38
+ const validate=raw=>{
39
+ if(typeof raw!=='string'||Buffer.byteLength(raw)>SUMMARY_ITEM)return false;
40
+ const record=JSON.parse(raw);
41
+ return record&&Object.keys(record).length===5&&record.version===1&&record.parser===parserPackage.version
42
+ &&record.file===file&&record.digest===digest&&validSummary(record.analysis);
43
+ };
44
+ const produce=()=>{
45
+ produced=true;
46
+ const analysis=producedAnalysis=analyzeSource(source,file);
47
+ return JSON.stringify({version:1,parser:parserPackage.version,file,digest,analysis:Object.fromEntries(summaryFields.map(name=>[name,analysis[name]]))});
48
+ };
49
+ // Oversized summaries remain ordinary uncached parses, preserving the old
50
+ // accepted input surface without weakening bounded disk/memory retention.
51
+ let raw,value;
52
+ try{raw=cachedValidatedTransform('source-summary',source,[file,parserPackage.version,'v1'],validate,produce);value=JSON.parse(raw).analysis;}
53
+ catch(error){if(error?.message!=='Invalid typed compiler cache result')throw error;value=Object.fromEntries(summaryFields.map(name=>[name,producedAnalysis[name]]));}
54
+ if(!produced)summaryReplays++;
55
+ const immutable=immutableSummary(value),size=Buffer.byteLength(JSON.stringify(value))*2+Buffer.byteLength(key)*2;
56
+ if(size<=SUMMARY_ITEM){
57
+ while(summaries.size&&(summaries.size>=SUMMARY_ENTRIES||summaryBytes+size>SUMMARY_BYTES)){const[old,entry]=summaries.entries().next().value;summaries.delete(old);summaryBytes-=entry.size;}
58
+ summaries.set(key,{value:immutable,size});summaryBytes+=size;
59
+ }
60
+ return immutable;
61
+ }
62
+ const identifier=(node,name)=>node?.type==='Identifier'&&(name===undefined||node.name===name);
63
+ const property=node=>node?.computed?node.property?.value:node?.property?.name;
64
+ const moduleExports=node=>node?.type==='MemberExpression'&&identifier(node.object,'module')&&property(node)==='exports';
65
+ function bindingNames(node,names){
66
+ if(!node)return;
67
+ if(identifier(node))names.add(node.name);
68
+ else if(node.type==='ObjectPattern')for(const item of node.properties)bindingNames(item.type==='RestElement'?item.argument:item.value,names);
69
+ else if(node.type==='ArrayPattern')for(const item of node.elements)bindingNames(item,names);
70
+ else if(node.type==='RestElement')bindingNames(node.argument,names);
71
+ else if(node.type==='AssignmentPattern')bindingNames(node.left,names);
72
+ }
73
+ function runtimeDeclaration(node,names){
74
+ if(!node||node.declare)return;
75
+ if(node.type==='VariableDeclaration')for(const item of node.declarations)bindingNames(item.id,names);
76
+ else if(['FunctionDeclaration','ClassDeclaration','TSEnumDeclaration','TSModuleDeclaration'].includes(node.type))bindingNames(node.id,names);
77
+ }
9
78
  export function analyzeSource(source,file='source.js') {
10
79
  const key=file+'\0'+createHash('sha256').update(source).digest('hex');
11
80
  if(cache.has(key)){const hit=cache.get(key);cache.delete(key);cache.set(key,hit);hits++;return hit.value;}
12
81
  misses++;
13
- const parsed=parseSync(file,source,{sourceType:'unambiguous',allowReturnOutsideFunction:true});
82
+ // Custom loaders also return JSX under their original asset filenames (for
83
+ // example .svg). Match the compiler's JSX fallback for non-TypeScript inputs,
84
+ // preserving TypeScript's extension rules, including angle assertions in .ts.
85
+ const parsed=parseSync(file,source,{sourceType:'unambiguous',allowReturnOutsideFunction:true,...(/\.(?:[cm]?ts|tsx)$/.test(file)?{}:{lang:'jsx'})});
14
86
  if(parsed.errors.length)throw new SyntaxError(`${file}: ${parsed.errors.map(error=>error.message).join('; ')}`);
15
87
  const program=parsed.program,directives=[];
16
88
  for(const node of program.body){if(!node.directive)break;directives.push(node.directive);}
17
- const imports=[];
18
- let computedRequire=false;
89
+ const imports=[],runtimeExports=new Set(),exportStars=[];
90
+ for(const node of program.body){
91
+ if(node.exportKind==='type')continue;
92
+ if(node.type==='ExportDefaultDeclaration'&&!node.declaration?.declare&&!['TSInterfaceDeclaration','TSTypeAliasDeclaration'].includes(node.declaration?.type))runtimeExports.add('default');
93
+ if(node.type==='ExportNamedDeclaration'){
94
+ runtimeDeclaration(node.declaration,runtimeExports);
95
+ for(const item of node.specifiers)if(item.exportKind!=='type')runtimeExports.add(item.exported.name??item.exported.value);
96
+ }
97
+ if(node.type==='ExportAllDeclaration'){
98
+ if(node.exported)runtimeExports.add(node.exported.name??node.exported.value);
99
+ else exportStars.push(node.source.value);
100
+ }
101
+ }
102
+ let computedRequire=false,hasServerDirective=false;
19
103
  const pending=[program];
20
104
  while(pending.length){
21
105
  const node=pending.pop();
106
+ if(node.directive==='use server')hasServerDirective=true;
22
107
  if(['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration'].includes(node.type)&&node.source&&node.importKind!=='type'&&node.exportKind!=='type')imports.push({path:node.source.value,kind:'import-statement'});
23
108
  if(node.type==='ImportExpression'&&typeof node.source?.value==='string')imports.push({path:node.source.value,kind:'dynamic-import'});
24
109
  if(node.type==='CallExpression'&&node.callee?.type==='Identifier'&&node.callee.name==='require'){
25
110
  const argument=node.arguments[0];
26
111
  if(argument?.type==='Literal'&&typeof argument.value==='string')imports.push({path:argument.value,kind:'require-call'});else computedRequire=true;
27
112
  }
113
+ if(node.type==='CallExpression'&&node.callee?.type==='MemberExpression'&&identifier(node.callee.object,'Object')&&property(node.callee)==='defineProperty'&&identifier(node.arguments[0],'exports')&&typeof node.arguments[1]?.value==='string'&&node.arguments[1].value!=='__esModule'){
114
+ runtimeExports.add('default');runtimeExports.add(node.arguments[1].value);
115
+ }
116
+ if(node.type==='AssignmentExpression'&&node.left.type==='MemberExpression'){
117
+ const left=node.left;
118
+ if(moduleExports(left)){
119
+ runtimeExports.add('default');
120
+ if(node.right.type==='ObjectExpression')for(const item of node.right.properties){
121
+ const key=item.computed?item.key?.value:item.key?.name??item.key?.value;
122
+ if(typeof key==='string')runtimeExports.add(key);
123
+ }
124
+ }
125
+ if(identifier(left.object,'exports')||moduleExports(left.object)){
126
+ runtimeExports.add('default');if(typeof property(left)==='string')runtimeExports.add(property(left));
127
+ }
128
+ }
28
129
  for(const value of Object.values(node)){
29
130
  if(Array.isArray(value)){for(let i=value.length-1;i>=0;i--)if(value[i]?.type)pending.push(value[i]);}
30
131
  else if(value&&typeof value==='object'&&value.type)pending.push(value);
31
132
  }
32
133
  }
33
- const value={program,directives,imports,computedRequire,frameworkBoundary:directives.some(item=>item==='use client'||item==='use server')};
134
+ runtimeExports.delete('__esModule');
135
+ const value=Object.freeze({program,directives:Object.freeze(directives),imports:Object.freeze(imports.map(Object.freeze)),computedRequire,hasServerDirective,
136
+ runtimeExports:Object.freeze([...runtimeExports]),exportStars:Object.freeze(exportStars),frameworkBoundary:directives.some(item=>item==='use client'||item==='use server')});
34
137
  // ASTs are substantially larger than source. Bound both count and estimated
35
138
  // retained size; large individual npm modules are analyzed without retention.
36
139
  const size=Buffer.byteLength(source)*16+Buffer.byteLength(file);
@@ -0,0 +1,48 @@
1
+ import {lstat, realpath, mkdir, copyFile, stat} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {mapConcurrent} from './concurrent.mjs';
4
+
5
+ const within = (root, file) => file === root || file.startsWith(root + path.sep);
6
+ const version = info => [info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs];
7
+ const unchanged = (left, right) => version(left).every((value, index) => value === version(right)[index]);
8
+
9
+ /** Materialize a finite trace, keeping links deferred until all file writes settle. */
10
+ export async function copyStandaloneTrace(files, {tracingRoot, relocate, maxFiles, maxBytes, concurrency = 8}, io = {lstat, realpath, mkdir, copyFile, stat}) {
11
+ const sources = Array.from(files);
12
+ if (sources.length > maxFiles) throw new Error('Standalone output exceeds 100000 files / 2 GiB. Narrow tracing includes.');
13
+ // Metadata can overlap, but admission and destination ordering remain exactly
14
+ // the trace's order. No bytes are copied before the whole budget is admitted.
15
+ const metadata = await mapConcurrent(sources, async source => {
16
+ const info = await io.lstat(source);
17
+ if (info.isSymbolicLink()) return {source, info, destination: await io.realpath(source)};
18
+ return {source, info};
19
+ }, concurrency);
20
+ let copiedFiles = 0, copiedBytes = 0;
21
+ const pendingLinks = [], groups = new Map();
22
+ for (const {source, info, destination} of metadata) {
23
+ if (info.isSymbolicLink() && !within(tracingRoot, source) && within(destination, tracingRoot)) continue;
24
+ const target = relocate(source);
25
+ if (++copiedFiles > maxFiles || (copiedBytes += info.size) > maxBytes) throw new Error('Standalone output exceeds 100000 files / 2 GiB. Narrow tracing includes.');
26
+ if (info.isSymbolicLink()) { pendingLinks.push([target, relocate(destination)]); continue; }
27
+ if (!info.isFile()) continue;
28
+ const key = process.platform === 'win32' ? target.toLowerCase() : target;
29
+ if (!groups.has(key)) groups.set(key, []);
30
+ groups.get(key).push({source, target, info});
31
+ }
32
+ const directories = new Map();
33
+ await mapConcurrent(groups.values(), async entries => {
34
+ // Alias collisions retain the old serial last-writer order. Unrelated files
35
+ // share a small I/O worker set and one mkdir promise per destination parent.
36
+ for (const {source, target, info} of entries) {
37
+ const before = await io.lstat(source);
38
+ if (!before.isFile() || !unchanged(info, before)) throw new Error('Standalone input changed before copying.');
39
+ const directory = path.dirname(target);
40
+ if (!directories.has(directory)) directories.set(directory, io.mkdir(directory, {recursive: true}));
41
+ await directories.get(directory);
42
+ await io.copyFile(source, target);
43
+ const [after, copied] = await Promise.all([io.lstat(source), io.stat(target)]);
44
+ if (!after.isFile() || !unchanged(before, after) || !copied.isFile() || copied.size !== before.size) throw new Error('Standalone input changed while copying.');
45
+ }
46
+ }, concurrency);
47
+ return {copiedFiles, copiedBytes, pendingLinks};
48
+ }
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import { resolveNativeBinary } from '../native/resolve.mjs';
8
8
  import { createHash } from 'node:crypto';
9
9
  import { createRequire } from 'node:module';
10
+ import {copyStandaloneTrace} from './standalone-copy.mjs';
10
11
 
11
12
  const slash = value => value.split(path.sep).join('/');
12
13
  const within = (root, file) => file === root || file.startsWith(root + path.sep);
@@ -219,28 +220,7 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
219
220
  } else traceFiles.add(target);
220
221
  }
221
222
  await mkdir(output, { recursive: true });
222
- let copiedBytes = 0;
223
- let copiedFiles = 0;
224
- const pendingLinks = [];
225
- for (const source of traceFiles) {
226
- const info = await lstat(source);
227
- // NFT also emits ancestor aliases used by absolute workspace links, such as
228
- // macOS /var -> /private/var. Their canonical children are already traced;
229
- // these host directory aliases are not dependencies of the artifact.
230
- // Real files and links to unrelated external workspaces still go through
231
- // relocated(), which enforces the configured tracing boundary.
232
- if (info.isSymbolicLink() && !within(tracingRoot, source) && within(await realpath(source), tracingRoot)) continue;
233
- const target = relocated(source);
234
- if (++copiedFiles > maxFiles || (copiedBytes += info.size) > maxBytes) throw new Error('Standalone output exceeds 100000 files / 2 GiB. Narrow tracing includes.');
235
- if (info.isSymbolicLink()) {
236
- const destination = await realpath(source);
237
- pendingLinks.push([target, relocated(destination)]);
238
- continue;
239
- }
240
- if (!info.isFile()) continue;
241
- await mkdir(path.dirname(target), { recursive: true });
242
- await copyFile(source, target);
243
- }
223
+ let {copiedBytes, copiedFiles, pendingLinks} = await copyStandaloneTrace(traceFiles, {tracingRoot, relocate: relocated, maxFiles, maxBytes});
244
224
  const owners = new Set(bridges.values());
245
225
  const includedOwners = new Set();
246
226
  for (const file of traceFiles) for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) if (owners.has(dir)) includedOwners.add(dir);
@@ -7,6 +7,8 @@ import { OUTPUT_POINTER, prepareBuildDirectory, readBuildDirectory, validateDist
7
7
  import { writeBuildOutput } from './output-contract.mjs';
8
8
  import { precompressBuild } from './precompress.mjs';
9
9
  import { withBuildLock } from './build-lock.mjs';
10
+ import {withCompilationStorage} from './compilation-storage.mjs';
11
+ import {reclaimBuildStages,trackBuildStage} from './build-retention.mjs';
10
12
  import { plainStaticPath } from './static-input.mjs';
11
13
 
12
14
  const MAX_FILES = 20000, MAX_BYTES = 512 * 1024 * 1024;
@@ -15,7 +17,7 @@ const urlFor = file => '/' + file.split('/').map(encodeURIComponent).join('/');
15
17
 
16
18
  /** Import an already-built public directory. Never evaluate its config or code. */
17
19
  export async function buildStatic(directory, { from = 'dist', spa = false, distDir, dev = false } = {}) {
18
- return withBuildLock(directory, root => importStatic(root, { from, spa, distDir, dev }));
20
+ return withBuildLock(directory, async root => {await reclaimBuildStages(root);return withCompilationStorage(root,()=>importStatic(root, { from, spa, distDir, dev }));});
19
21
  }
20
22
 
21
23
  async function importStatic(directory, { from, spa, distDir, dev }) {
@@ -41,6 +43,7 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
41
43
 
42
44
  const id = randomUUID(), stage = path.join(root, `.prnext-build-${id}`);
43
45
  const backup = path.join(root, `.prnext-backup-${id}`), pointer = path.join(root, `.prnext-output-${id}.tmp`);
46
+ const recovery=await trackBuildStage(root,{stage,backup}).catch(()=>null);
44
47
  const manifest = { version: 1, buildId: id, config: { staticOnly: true, staticFallback: spa, compress: true, poweredByHeader: true }, routes: [], prerendered: [] };
45
48
  const paths = new Set();
46
49
  let count = 0, bytes = 0, previous = false, published = false;
@@ -99,10 +102,11 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
99
102
  if (previous) await rename(backup, destination);
100
103
  throw error;
101
104
  }
102
- if (previous) await rm(backup, { recursive: true, force: true });
105
+ await recovery?.committed().catch(()=>{});
106
+ if (!recovery&&previous) await rm(backup, { recursive: true, force: true });
103
107
  return { ...manifest, outputDirectory: destination, files: count, bytes };
104
108
  } finally {
105
109
  await rm(pointer, { force: true });
106
- await rm(stage, { recursive: true, force: true });
110
+ if(recovery)await recovery.cleanup();else await rm(stage, { recursive: true, force: true });
107
111
  }
108
112
  }
@@ -25,7 +25,14 @@ export async function withPersistentTransforms(root,{enabled=true,dev=false}={},
25
25
  return result;
26
26
  }
27
27
  function identity(kind,source,settings){return createHash('sha256').update(persistent.getStore()?.key||'memory').update(JSON.stringify([kind,settings,sourceMapsEnabled()])).update('\0').update(source).digest('hex');}
28
- function diskValue(key){const scope=persistent.getStore();if(!scope?.values.has(key))return;const value=scope.values.get(key);scope.values.delete(key);scope.values.set(key,value);diskHits++;hits++;return value;}
28
+ function discard(key){const value=entries.get(key);if(value){entries.delete(key);bytes-=value.size;}}
29
+ function validValue(value,validate){if(!validate)return true;try{return validate(value)===true;}catch{return false;}}
30
+ function diskValue(key,validate){
31
+ const scope=persistent.getStore();if(!scope?.values.has(key))return;
32
+ const value=scope.values.get(key);scope.values.delete(key);
33
+ if(!validValue(value,validate)){scope.bytes-=Buffer.byteLength(value);scope.dirty=true;return;}
34
+ scope.values.set(key,value);diskHits++;hits++;return value;
35
+ }
29
36
  function remember(key,output){
30
37
  const scope=persistent.getStore();if(!scope||typeof output!=='string')return;
31
38
  const size=Buffer.byteLength(output);if(size>128*1024)return;
@@ -38,16 +45,23 @@ function remember(key,output){
38
45
  // closure or mutable build metadata survives in the cache. Include all options
39
46
  // affecting output in settings, including build identity and target mode.
40
47
  export function cachedTransform(kind, source, settings, transform) {
48
+ return cachedValidatedTransform(kind,source,settings,undefined,transform);
49
+ }
50
+
51
+ /** Typed compiler facts may replay only structurally validated string records. */
52
+ export function cachedValidatedTransform(kind, source, settings, validate, transform) {
41
53
  const key = identity(kind,source,settings);
42
54
  const found = entries.get(key);
43
- if (found) {
55
+ if (found && validValue(found.output,validate)) {
44
56
  hits++; entries.delete(key); entries.set(key,found);
45
57
  if(persistent.getStore()&&!persistent.getStore().values.has(key))remember(key,found.output);
46
58
  return found.output;
47
59
  }
48
- const saved=diskValue(key);if(saved!==undefined)return saved;
60
+ if(found)discard(key);
61
+ const saved=diskValue(key,validate);if(saved!==undefined)return saved;
49
62
  misses++;
50
63
  const output = transform();
64
+ if(!validValue(output,validate))throw new TypeError('Invalid typed compiler cache result');
51
65
  remember(key,output);
52
66
  // UTF-16 is an upper bound for the retained string payload. Metadata adds
53
67
  // engine overhead; the entry count also bounds that overhead.
@@ -37,17 +37,49 @@ function relativeChunksUncached(source,chunks,prefix) {
37
37
  return source;
38
38
  }
39
39
 
40
+ function rewriteChunkSource(source,map,file,chunks,publicPath){
41
+ const result=relativeServerChunkImports(inlineSourceMap(source,map),chunks,publicPath);
42
+ if(map){
43
+ const mapped=extractSourceMap(result);
44
+ return {source:mapped.code.replace(/\/\/[#@]\s*sourceMappingURL=[^\s]+/g,'')+'\n//# sourceMappingURL='+path.basename(file)+'.map\n',map:JSON.stringify({...mapped.map,file:path.basename(file)})};
45
+ }
46
+ return {source:result};
47
+ }
48
+
49
+ /** Rewrite before the first disk write, without modifying compiler/cache buffers. */
50
+ export function rewriteServerChunkOutputFiles(files,chunks,publicPath,importers){
51
+ const outputs=new Map(files.map(file=>[path.resolve(file.path),file])),replacements=new Map();
52
+ const replace=(file,source)=>{
53
+ const contents=Buffer.from(source),metadata=Object.getOwnPropertyDescriptors(file);
54
+ // esbuild's hash describes the original buffer. Do not retain a stale hash.
55
+ // Reading/spreading esbuild's lazy text getter would decode the old buffer
56
+ // again and retain that string even though only the new bytes are needed.
57
+ delete metadata.hash;delete metadata.contents;delete metadata.text;
58
+ const output=Object.create(Object.getPrototypeOf(file),metadata);
59
+ Object.defineProperties(output,{contents:{value:contents,writable:true,enumerable:true,configurable:true},text:{get(){return this.contents.toString();},enumerable:true,configurable:true}});
60
+ replacements.set(file,output);
61
+ };
62
+ for(const file of files){
63
+ if(!file.path.endsWith('.mjs')||importers&&!importers.has(path.resolve(file.path)))continue;
64
+ const source=Buffer.from(file.contents.buffer,file.contents.byteOffset,file.contents.byteLength).toString();
65
+ if(!source.includes(publicPath.replace(/\/+$/,'')+'/'))continue;
66
+ const mapFile=sourceMapsEnabled()&&outputs.get(path.resolve(file.path+'.map'));
67
+ const map=mapFile?JSON.parse(Buffer.from(mapFile.contents).toString()):undefined;
68
+ const result=rewriteChunkSource(source,map,file.path,chunks,publicPath);
69
+ if(result.source!==source)replace(file,result.source);
70
+ if(mapFile)replace(mapFile,result.map);
71
+ }
72
+ return files.map(file=>replacements.get(file)||file);
73
+ }
74
+
40
75
  export async function rewriteServerChunks(file,chunks,publicPath){
41
76
  const source=await readFile(file,'utf8');
42
77
  if(!source.includes(publicPath.replace(/\/+$/,'')+'/'))return;
43
78
  let map;
44
79
  if(sourceMapsEnabled())try{map=JSON.parse(await readFile(file+'.map','utf8'));}catch(error){if(error.code!=='ENOENT')throw error;}
45
- const result=relativeServerChunkImports(inlineSourceMap(source,map),chunks,publicPath);
46
- if(map){
47
- const mapped=extractSourceMap(result);
48
- await writeFile(file+'.map',JSON.stringify({...mapped.map,file:path.basename(file)}));
49
- await writeFile(file,mapped.code.replace(/\/\/[#@]\s*sourceMappingURL=[^\s]+/g,'')+'\n//# sourceMappingURL='+path.basename(file)+'.map\n');
50
- }else await writeFile(file,result);
80
+ const result=rewriteChunkSource(source,map,file,chunks,publicPath);
81
+ if(map)await writeFile(file+'.map',result.map);
82
+ await writeFile(file,result.source);
51
83
  }
52
84
 
53
85
  function parserOptions(filename) {
@@ -3,7 +3,7 @@ import {access} from 'node:fs/promises';
3
3
  import {resolveTypeScript} from './typescript-toolchain.mjs';
4
4
  import path from 'node:path';
5
5
  import {privateCacheDirectory} from './persistent-cache.mjs';
6
- export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000}={}){
6
+ export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000,onBeforeChild=()=>{},onChild=()=>{}}={}){
7
7
  const noop={inactive:true,async close(){}};
8
8
  if(config.typescript?.devChecks===false)return noop;
9
9
  const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');try{await access(filename);}catch{return noop;}
@@ -13,7 +13,9 @@ export async function startTypeChecker(root,config,notify,{fork:spawnChecker=for
13
13
  function launch(){
14
14
  if(closed)return;
15
15
  const worker=toolchain.native?'./typescript-native-worker.mjs':'./typescript-dev-worker.mjs';
16
+ const ticket=onBeforeChild();
16
17
  child=spawnChecker(new URL(worker,import.meta.url),[toolchain.compiler,filename,toolchain.native?'watch':directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
18
+ onChild(child,ticket);
17
19
  const current=child;
18
20
  let details='',healthyAt=0;child.stderr.on('data',chunk=>{details=(details+chunk).slice(-2048);});
19
21
  child.on('message',message=>{
package/cli.mjs CHANGED
@@ -22,6 +22,7 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
22
22
  Detect a completed browser build
23
23
  prnext build [directory] --adapter static --from .
24
24
  Build a plain HTML/CSS/JS site
25
+ prnext build [directory] --timings Show time spent in React build phases
25
26
  prnext output [directory] [--json] Read the versioned build output contract
26
27
  prnext start [directory] [options] Start native Rust production server
27
28
  prnext pstart [directory] [options] Start production persistently in the background
@@ -38,12 +39,15 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
38
39
  prnext docs --ai [topic] Read the AI guide bundled with this version
39
40
  prnext docs --ai --init [directory] Add a guide pointer to the project's AGENTS.md
40
41
  prnext routes [directory] Display compiled routes
41
- prnext check [directory] [--json] Diagnose config, routes, Node, native and dependencies
42
+ prnext check [directory] [--production] [--json] Check config, deployment and dependencies
42
43
  prnext typegen [directory] [--json] Generate route types without building the app
43
44
  prnext typecheck [directory] [--json] Generate types and check TypeScript without building
44
45
  prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
45
46
  prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
46
47
  prnext compiler [directory] [--json] React Compiler results from the last successful build
48
+ prnext cache [directory] [--json] Show compilation cache sizes and disk budget
49
+ prnext cache [directory] --prune [--dry-run] [--max-bytes N] [--json]
50
+ Reclaim regenerable compilation caches
47
51
  prnext host [prnext.host.json] Host built apps by hostname; wake on demand
48
52
  prnext host [config] --check Validate hosting configuration and builds
49
53
  prnext host [config] --status [--json] Show memory, process and idle status
@@ -109,6 +113,10 @@ function stop(child) {
109
113
  async function main() {
110
114
  if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
111
115
  if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
116
+ if(command==='cache'){
117
+ const {cacheCommand}=await import('./build/cache-command.mjs');
118
+ await cacheCommand(argv);return;
119
+ }
112
120
  if (command === 'create') {
113
121
  const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
114
122
  if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
@@ -313,12 +321,12 @@ async function main() {
313
321
  process.env.PWD = root;
314
322
  }
315
323
  if (command === 'check') {
316
- if (args.some(arg => arg !== '--json')) throw new Error('check accepts only [directory] and --json');
324
+ if (args.some(arg => !['--json','--production'].includes(arg))||new Set(args).size!==args.length) throw new Error('check accepts [directory], --production and --json once each');
317
325
  const { checkProject } = await import('./build/check.mjs');
318
- const report = await checkProject(root);
326
+ const report = await checkProject(root,{production:args.includes('--production')});
319
327
  if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
320
328
  else {
321
- console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
329
+ console.log(`PRNext ${report.mode==='production'?'production ':''}preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
322
330
  console.log(`${report.routes.length} routes. Installed versions: ${Object.entries(report.versions).map(([name,version])=>`${name}@${version||'missing'}`).join(', ')}`);
323
331
  for (const item of report.diagnostics) {
324
332
  const output = item.severity === 'error' ? console.error : console.log;
@@ -331,11 +339,16 @@ async function main() {
331
339
  }
332
340
  if (command === 'build') {
333
341
  const options = {};
342
+ let showTimings = false;
334
343
  for (let i = 0; i < args.length; i++) {
335
344
  const flag = args[i];
336
345
  if (flag === '--dev') options.dev = true;
337
346
  else if (flag === '--watch') options.watch = true;
338
347
  else if (flag === '--spa') options.spa = true;
348
+ else if (flag === '--timings') {
349
+ if (showTimings) throw new Error('Duplicate build option: --timings');
350
+ showTimings = true;
351
+ }
339
352
  else if (flag === '--adapter' || flag === '--from') {
340
353
  const value = args[++i];
341
354
  if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`);
@@ -349,6 +362,7 @@ async function main() {
349
362
  process.env.NODE_ENV = dev ? 'development' : 'production';
350
363
  const started = performance.now();
351
364
  if(options.watch){
365
+ if(showTimings)throw new Error('--timings reports one build. Run prnext build --timings without --watch.');
352
366
  if(dev)throw new Error('--watch builds production; use prnext dev for development.');
353
367
  const {watchProduction}=await import('./build/production-builder.mjs');
354
368
  delete options.watch;await watchProduction(root,options);return;
@@ -360,11 +374,17 @@ async function main() {
360
374
  else if (options.adapter === 'auto') options.adapter = 'react';
361
375
  }
362
376
  if (options.adapter === 'static') {
377
+ if(showTimings)throw new Error('--timings requires the React build adapter.');
363
378
  const { buildStatic } = await import('./build/static-adapter.mjs');
364
379
  await buildStatic(root, options);
365
380
  } else {
366
381
  const { build } = await import('./build/index.mjs');
367
382
  const result=await build(root, { ...options, dev, envMode });
383
+ if(showTimings&&result.buildTimings){
384
+ console.log('Build timings (milliseconds; nested phases are not additive):');
385
+ for(const[name,phase]of Object.entries(result.buildTimings.phases))console.log(` ${name}: ${phase.milliseconds.toFixed(2)} (${phase.calls} ${phase.calls===1?'call':'calls'})`);
386
+ console.log(` total: ${result.buildTimings.totalMilliseconds.toFixed(2)} (excludes CLI module startup)`);
387
+ }
368
388
  if(!dev&&result.compilationCache?.hits){
369
389
  const {hits,reusedBytes}=result.compilationCache;
370
390
  console.log(`Compilation cache: ${hits} ${hits===1?'graph':'graphs'} reused (${Math.ceil(reusedBytes/1024)} KiB).`);
@@ -380,7 +400,9 @@ async function main() {
380
400
  const binary = await resolveNativeBinary();
381
401
  if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
382
402
  const outputDirectory = await readBuildDirectory(root);
383
- const serverEnvironment = {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
403
+ const serverEnvironment = command==='start'
404
+ ? (await import('./runtime/env.mjs')).productionEnvironment(root)
405
+ : {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
384
406
  if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
385
407
  if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
386
408
  '--node',process.execPath,...args],serverEnvironment))return;
@@ -12,17 +12,21 @@ function ActivitySlot({ identity, children, authorizationKey }) {
12
12
  const router = React.useContext(AppRouterContext);
13
13
  const epoch = retention?.epoch ?? 0;
14
14
  const authorizationEpoch = retention?.authorizationEpoch ?? 0;
15
+ const flight = retention?.flight;
15
16
  const registrations = React.useRef(new Set());
16
17
  const serial = React.useRef(0);
17
18
  const [saved, setSaved] = React.useState(() => ({ epoch, authorizationEpoch, active: identity, entries: new Map() }));
18
19
  let entries = saved.entries;
19
20
  const current = entries.get(identity);
20
- if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey) {
21
+ const incompleteInactive = [...entries].some(([key, entry]) => key !== identity && entry.flight?.complete === false);
22
+ if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey || current?.flight !== flight || incompleteInactive) {
21
23
  const sameAuthorization = saved.authorizationEpoch === authorizationEpoch && !(current?.authorizationKey && authorizationKey && current.authorizationKey !== authorizationKey);
22
- entries = saved.epoch === epoch && sameAuthorization ? new Map(entries) : new Map();
24
+ // Aborted lazy Flight rows cannot remain in an inactive Activity. Restore
25
+ // complete siblings while keeping the current shared layout's React key.
26
+ entries = saved.epoch === epoch && sameAuthorization ? new Map([...entries].filter(([key, entry]) => key === identity || entry.flight?.complete !== false)) : new Map();
23
27
  const preserve = current && saved.authorizationEpoch === authorizationEpoch && (sameAuthorization && (saved.epoch === epoch || saved.active === identity)
24
28
  || !retention?.strictAuthorization && saved.active === identity);
25
- entries.set(identity, { children, router, authorizationKey, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
29
+ entries.set(identity, { children, router, authorizationKey, flight, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
26
30
  setSaved({ epoch, authorizationEpoch, active: identity, entries });
27
31
  }
28
32
  React.useLayoutEffect(() => {
@@ -4,10 +4,11 @@ const directory = process.env.PRNEXT_INSPECT_DIR;
4
4
  const { threadId } = require('node:worker_threads');
5
5
  let queue = [], scheduled = false, writing = false, size;
6
6
  const limit = 1024 * 1024;
7
- if (directory) setInterval(() => {
8
- const {rss, heapUsed} = process.memoryUsage();
9
- record('process', {rssBytes:rss, heapUsedBytes:heapUsed});
10
- }, 5000).unref();
7
+ if (directory) {
8
+ // No GC observer or periodic sampler exists when diagnostics are disabled.
9
+ const sampler = require('./runtime-sample.cjs').createRuntimeSampler();
10
+ setInterval(() => record('process', sampler.sample()), 5000).unref();
11
+ }
11
12
  function record(type, fields = {}) {
12
13
  if (!directory || queue.length >= 256) return;
13
14
  const line = JSON.stringify({ time: Date.now(), pid: process.pid, type, ...fields }) + '\n';