@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
@@ -1,11 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { Agent, createServer, request } from 'node:http';
2
+ import { request } from 'node:http';
3
3
  import { cp, mkdir, readFile, rm, realpath } from 'node:fs/promises';
4
4
  import { randomUUID, createHash } from 'node:crypto';
5
5
  import path from 'node:path';
6
6
  import { readBuildDirectory } from '../build-directory.mjs';
7
7
  import { atomicJSON, delay, environment, validName, validHealthPath } from './common.mjs';
8
8
  import { RotatingLog } from './logger.mjs';
9
+ import {nativeGateway,stopNativeProxy} from './native-proxy.mjs';
9
10
 
10
11
  const home = process.argv[2];
11
12
  const registry = path.join(home, 'apps.json');
@@ -16,6 +17,7 @@ const send = message => { if (process.connected) process.send(message); };
16
17
 
17
18
  function validate(config) {
18
19
  validName(config.name); validHealthPath(config.healthPath);
20
+ if(config.rollbackWindow!==undefined&&(!Number.isInteger(config.rollbackWindow)||config.rollbackWindow<0||config.rollbackWindow>120000))throw new Error('rollbackWindow must be between 0 and 120000 milliseconds');
19
21
  if (!path.isAbsolute(config.root) || !path.isAbsolute(config.binary) || !path.isAbsolute(config.node)) throw new Error('Application, Node and native executable paths must be absolute');
20
22
  if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) throw new Error('--port must be between 1 and 65535');
21
23
  if (typeof config.hostname !== 'string' || !config.hostname || /[\s\x00-\x1f]/.test(config.hostname)) throw new Error('Choose a valid --hostname');
@@ -27,7 +29,8 @@ async function persist() {
27
29
  await atomicJSON(registry, { version: 1, apps: [...apps.values()].map(app => ({ config: app.config, enabled: app.enabled, snapshots: [...app.snapshots] })) });
28
30
  }
29
31
  function newApp(config, enabled = false) {
30
- return { config, enabled, snapshots: new Set(), active: null, generations: new Set(), gateway: null, sockets: new Set(), state: 'stopped', error: null, starts: 0, failures: [], retry: null,
32
+ config={rollbackWindow:15000,...config};
33
+ return { config, enabled, snapshots: new Set(), active: null, generations: new Set(), gateway: null, state: 'stopped', error: null, starts: 0, failures: [], retry: null, probation:null,
31
34
  log: new RotatingLog(path.join(home, 'logs', `app-${config.name}.log`)) };
32
35
  }
33
36
  function summary(app) {
@@ -36,44 +39,13 @@ function summary(app) {
36
39
  activeRequests: [...app.generations].reduce((sum, generation) => sum + generation.active, 0),
37
40
  draining: [...app.generations].filter(generation => generation !== app.active).length,
38
41
  buildId: app.active?.buildId || null, startedAt: app.active?.startedAt || null, error: app.error,
42
+ proxyPid:app.gateway?.pid||null,
43
+ rollback:app.probation?{previousBuildId:app.probation.previous.buildId,until:app.probation.deadline}:null,
39
44
  log: path.join(home, 'logs', `app-${app.config.name}.log`) };
40
45
  }
41
- function hopHeaders(headers) {
42
- const result = { ...headers };
43
- const nominated = String(headers.connection || '').split(',').map(value => value.trim().toLowerCase());
44
- for (const key of ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ...nominated]) delete result[key];
45
- return result;
46
- }
47
46
  async function listen(app) {
48
47
  if (app.gateway) return;
49
- const server = createServer({ requestTimeout: 300000, headersTimeout: 30000 }, (incoming, outgoing) => {
50
- const generation = app.active;
51
- if (!incoming.url.startsWith('/')) { outgoing.writeHead(400).end('Use a local request path.'); return; }
52
- if (!generation || generation.dead) { outgoing.writeHead(503, { 'retry-after': '1' }).end('Application is restarting.'); incoming.resume(); return; }
53
- if (generation.active >= 512) { outgoing.writeHead(503, { 'retry-after': '1' }).end('Application is busy.'); incoming.resume(); return; }
54
- generation.active++;
55
- const headers = hopHeaders(incoming.headers);
56
- // Preserve original Host and trusted reverse-proxy headers. Never invent
57
- // retry/replay behavior for POSTs or requests that have already streamed.
58
- const upstream = request({ host: '127.0.0.1', port: generation.port, method: incoming.method, path: incoming.url, headers, agent: generation.agent }, response => {
59
- outgoing.writeHead(response.statusCode, hopHeaders(response.headers));
60
- response.on('error', () => outgoing.destroy()); response.pipe(outgoing);
61
- });
62
- generation.requests.add(upstream);
63
- let released = false;
64
- const release = () => { if (released) return; released = true; generation.active--; generation.requests.delete(upstream); upstream.destroy(); };
65
- outgoing.once('finish', release); outgoing.once('close', release);
66
- incoming.once('aborted', () => outgoing.destroy());
67
- incoming.once('error', () => outgoing.destroy());
68
- upstream.once('error', () => { if (!outgoing.headersSent) outgoing.writeHead(502).end('Application connection closed.'); else outgoing.destroy(); });
69
- incoming.pipe(upstream);
70
- });
71
- server.on('upgrade', (_request, socket) => socket.end('HTTP/1.1 501 Not Implemented\r\nConnection: close\r\nContent-Length: 0\r\n\r\n'));
72
- server.on('connection', socket => { app.sockets.add(socket); socket.once('close', () => app.sockets.delete(socket)); });
73
- server.maxConnections = 1024;
74
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(app.config.port, app.config.hostname, resolve); });
75
- server.on('error', error => { app.error = error.message; app.log.message(error.message); });
76
- app.gateway = server;
48
+ app.gateway=await nativeGateway(app.config,{home,notify:send,onFatal:error=>{console.error(error.message);if(!closing)process.exit(1);}});
77
49
  }
78
50
  async function removeSnapshot(app, relative) {
79
51
  // Only delete a directory generated by this manager inside this exact root.
@@ -120,11 +92,9 @@ async function stopGeneration(app, generation, drain = true) {
120
92
  generation.expected = true;
121
93
  generation.stopping = (async () => {
122
94
  const deadline = Date.now() + (drain ? app.config.drainTimeout : 0);
123
- while (generation.active && Date.now() < deadline && !generation.dead) await delay(25);
124
- for (const req of generation.requests) req.destroy();
125
- generation.agent.destroy();
95
+ await app.gateway?.retire(generation,drain?Math.max(0,deadline-Date.now()):0).catch(error=>app.log.message(`Proxy retirement: ${error.message}`));
126
96
  generation.child.stdin?.end();
127
- await Promise.race([generation.closed, delay(3000)]);
97
+ await Promise.race([generation.closed, delay(drain?Math.max(1000,deadline-Date.now()):3000)]);
128
98
  if (!generation.dead) generation.child.kill('SIGKILL');
129
99
  if (process.platform !== 'win32' && generation.child.pid) { try { process.kill(-generation.child.pid, 'SIGKILL'); } catch {} }
130
100
  await generation.closed;
@@ -156,7 +126,7 @@ async function launch(app) {
156
126
  env: { ...environment(), NODE_ENV: 'production', ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
157
127
  });
158
128
  child.stdin.on('error', () => {});
159
- const generation = { child, ...build, active: 0, requests: new Set(), agent: new Agent({ keepAlive: true, maxSockets: 512, maxFreeSockets: 16, timeout: 30000 }), dead: false, expected: false, port: null, startedAt: Date.now() };
129
+ const generation = { child, ...build, active: 0, dead: false, expected: false, port: null, startedAt: Date.now() };
160
130
  app.generations.add(generation);
161
131
  if (child.pid) send({ type: 'child', pid: child.pid, running: true });
162
132
  let output = '', failure;
@@ -173,12 +143,12 @@ async function launch(app) {
173
143
  generation.closed = new Promise(resolve => child.once('close', (code, signal) => {
174
144
  generation.dead = true;
175
145
  if (child.pid) send({ type: 'child', pid: child.pid, running: false });
176
- if (app.active === generation) app.active = null;
146
+ if (app.active === generation) {app.active = null;void app.gateway?.pause(generation).catch(error=>app.log.message(error.message));}
177
147
  if (!generation.expected) {
178
148
  app.log.message(`Process ${child.pid || 'unknown'} exited (${signal || code}).`);
179
149
  if (generation.ready) {
180
150
  // Serialize recovery and cleanup with explicit lifecycle commands.
181
- sequence = sequence.then(async () => { await stopGeneration(app, generation, false); await persist(); retry(app, new Error(`Application exited (${signal || code})`)); }).catch(error => app.log.message(error.stack));
151
+ sequence = sequence.then(async () => { if(app.probation?.next===generation&&await rollback(app,new Error(`Replacement exited (${signal||code})`)))return;await stopGeneration(app, generation, false); await persist(); if(!app.active)retry(app, new Error(`Application exited (${signal || code})`)); }).catch(error => app.log.message(error.stack));
182
152
  }
183
153
  }
184
154
  resolve();
@@ -197,7 +167,42 @@ async function launch(app) {
197
167
  } catch (error) { await stopGeneration(app, generation, false); throw error; }
198
168
  finally { child.stdout.off('data', started); }
199
169
  }
170
+ async function finishProbation(app){
171
+ const probation=app.probation;if(!probation)return;
172
+ clearTimeout(probation.timer);app.probation=null;
173
+ await stopGeneration(app,probation.previous).catch(error=>app.log.message(`Previous build cleanup: ${error.message}`));
174
+ await persist();
175
+ }
176
+ async function rollback(app,error){
177
+ const probation=app.probation;if(!probation)return false;
178
+ clearTimeout(probation.timer);app.probation=null;
179
+ if(!app.enabled||closing||probation.previous.dead||probation.previous.expected){
180
+ await stopGeneration(app,probation.previous,false);return false;
181
+ }
182
+ // A live previous process still has to pass its health check. Never roll
183
+ // traffic back to an instance whose dependencies have also become unhealthy.
184
+ try{await health(probation.previous,app.config,Math.min(2000,app.config.healthTimeout));await app.gateway.activate(probation.previous);}
185
+ catch(restore){app.log.message(`Rollback unavailable: ${restore.message}`);await stopGeneration(app,probation.previous,false);return false;}
186
+ app.active=probation.previous;app.state='online';app.error=`Rolled back: ${error.message}`;
187
+ app.log.message(`Rolled back to build ${app.active.buildId}: ${error.message}`);
188
+ await stopGeneration(app,probation.next);await persist();return true;
189
+ }
190
+ function beginProbation(app,next,previous){
191
+ const probation=app.probation={next,previous,deadline:Date.now()+app.config.rollbackWindow,failures:0};
192
+ const schedule=()=>{probation.timer=setTimeout(()=>{
193
+ sequence=sequence.then(async()=>{
194
+ if(app.probation!==probation||!app.enabled||closing)return;
195
+ if(next.dead){await rollback(app,new Error('Replacement exited during monitoring'));return;}
196
+ if(Date.now()>=probation.deadline){await finishProbation(app);return;}
197
+ try{await health(next,app.config,Math.min(2000,app.config.healthTimeout));probation.failures=0;}
198
+ catch(error){if(++probation.failures>=3){if(!await rollback(app,error)){await stopGeneration(app,next,false);retry(app,error);}return;}}
199
+ if(app.probation===probation)schedule();
200
+ }).catch(error=>app.log.message(`Replacement monitoring: ${error.stack||error}`));
201
+ },Math.min(1000,Math.max(1,probation.deadline-Date.now())));};
202
+ schedule();
203
+ }
200
204
  async function activate(app) {
205
+ await finishProbation(app);
201
206
  await listen(app);
202
207
  const previous = app.active;
203
208
  const wasEnabled = app.enabled;
@@ -208,9 +213,13 @@ async function activate(app) {
208
213
  app.enabled = true;
209
214
  await persist();
210
215
  if (next.dead) throw new Error('Application exited before traffic could switch');
216
+ await app.gateway.activate(next);
217
+ if (next.dead) throw new Error('Application exited while traffic switched');
211
218
  app.active = next; app.state = 'online'; app.error = null;
212
219
  app.log.message(`Ready: pid ${next.child.pid}, build ${next.buildId || 'unknown'}, ${app.config.hostname}:${app.config.port}`);
213
220
  } catch (error) {
221
+ if (previous && !previous.dead) await app.gateway.activate(previous).catch(restore=>app.log.message(restore.message));
222
+ else await app.gateway?.pause().catch(restore=>app.log.message(restore.message));
214
223
  if (next) await stopGeneration(app, next, false).catch(cleanup => app.log.message(cleanup.message));
215
224
  app.enabled = wasEnabled;
216
225
  app.state = previous && !previous.dead ? 'online' : 'errored'; app.error = error.message;
@@ -218,21 +227,17 @@ async function activate(app) {
218
227
  app.log.message(`Start failed; ${previous && !previous.dead ? 'previous generation remains active' : 'no healthy generation'}: ${error.message}`);
219
228
  throw error;
220
229
  }
221
- if (previous) {
222
- await stopGeneration(app, previous).catch(error => app.log.message(`Previous build cleanup: ${error.message}`));
223
- await persist();
224
- }
230
+ if(previous && app.config.rollbackWindow>0)beginProbation(app,next,previous);
231
+ else if(previous){await stopGeneration(app,previous);await persist();}
225
232
  }
226
233
  async function stopApp(app) {
234
+ if(app.probation)clearTimeout(app.probation.timer);app.probation=null;
227
235
  app.enabled = false; clearTimeout(app.retry); app.retry = null;
228
236
  await persist();
229
237
  app.state = 'stopping'; app.active = null;
230
- const gateway = app.gateway; app.gateway = null;
231
- const closed = gateway ? new Promise(resolve => gateway.close(resolve)) : Promise.resolve();
232
- gateway?.closeIdleConnections();
238
+ await app.gateway?.pause();
233
239
  await Promise.all([...app.generations].map(generation => stopGeneration(app, generation)));
234
- for (const socket of app.sockets) socket.destroy();
235
- await closed; app.state = 'stopped'; app.error = null; await persist();
240
+ await app.gateway?.close();app.gateway=null;app.state = 'stopped'; app.error = null; await persist();
236
241
  app.log.message('Stopped.');
237
242
  }
238
243
  function select(message) {
@@ -242,8 +247,12 @@ function select(message) {
242
247
  return selected;
243
248
  }
244
249
  async function command(message) {
245
- if (message.command === 'status') return { daemonPid: process.pid, apps: [...apps.values()].map(summary) };
250
+ if (message.command === 'status') {
251
+ for(const app of apps.values())if(app.gateway){const counts=await app.gateway.counts();for(const g of app.generations)g.active=counts[g.relative]||0;}
252
+ return { daemonPid: process.pid, apps: [...apps.values()].map(summary) };
253
+ }
246
254
  if (message.command === 'start') {
255
+ message.config={rollbackWindow:15000,...message.config};
247
256
  validate(message.config);
248
257
  message.config.root = await realpath(message.config.root);
249
258
  let app = apps.get(message.config.name);
@@ -274,12 +283,13 @@ async function shutdown() {
274
283
  await sequence;
275
284
  // A supervisor shutdown preserves each enabled flag for the next launch.
276
285
  await Promise.all([...apps.values()].map(async app => {
277
- clearTimeout(app.retry); app.active = null; app.gateway?.close();
286
+ if(app.probation)clearTimeout(app.probation.timer);app.probation=null;
287
+ clearTimeout(app.retry); app.active = null;await app.gateway?.pause();
278
288
  await Promise.all([...app.generations].map(generation => stopGeneration(app, generation)));
279
- for (const socket of app.sockets) socket.destroy();
289
+ await app.gateway?.close();app.gateway=null;
280
290
  app.log.end();
281
291
  }));
282
- await persist(); process.exit(0);
292
+ await stopNativeProxy();await persist(); process.exit(0);
283
293
  }
284
294
  process.on('disconnect', () => void shutdown());
285
295
  process.on('SIGTERM', () => void shutdown());
@@ -0,0 +1,63 @@
1
+ // Lifecycle RPC only: no public HTTP bytes, headers or response bodies enter JS.
2
+ import {spawn} from 'node:child_process';
3
+ import {environment,delay} from './common.mjs';
4
+ let manager;
5
+ class NativeProxy {
6
+ constructor(binary,{home,notify,onFatal}){
7
+ this.pending=new Map();this.next=0;this.stopping=false;
8
+ this.child=spawn(binary,['persistent-proxy'],{cwd:home,windowsHide:true,detached:process.platform!=='win32',stdio:['pipe','pipe','inherit'],env:{...environment(),TOKIO_WORKER_THREADS:'1'}});
9
+ this.pid=this.child.pid;if(this.pid)notify({type:'child',pid:this.pid,running:true});
10
+ this.closed=new Promise(resolve=>this.child.once('close',()=>{if(this.pid)notify({type:'child',pid:this.pid,running:false});resolve();}));
11
+ this.ready=new Promise((resolve,reject)=>{
12
+ const timer=setTimeout(()=>{reject(new Error('Native persistent proxy did not initialize'));this.child.kill();},10000);timer.unref();
13
+ let buffer='';
14
+ this.child.stdout.setEncoding('utf8');
15
+ this.child.stdout.on('data',chunk=>{
16
+ buffer+=chunk;
17
+ if(buffer.length>1024*1024){this.child.kill();return;}
18
+ for(let end;(end=buffer.indexOf('\n'))>=0;){
19
+ const line=buffer.slice(0,end);buffer=buffer.slice(end+1);let message;
20
+ try{message=JSON.parse(line);}catch{this.child.kill();return;}
21
+ if(message.type==='ready'&&message.protocol===1){clearTimeout(timer);resolve();continue;}
22
+ const request=this.pending.get(message.id);if(!request)continue;
23
+ this.pending.delete(message.id);clearTimeout(request.timer);message.ok?request.resolve(message.value):request.reject(new Error(message.error||'Native proxy request failed'));
24
+ }
25
+ });
26
+ const failed=error=>{
27
+ clearTimeout(timer);reject(error);
28
+ for(const request of this.pending.values()){clearTimeout(request.timer);request.reject(error);}this.pending.clear();
29
+ if(!this.stopping)onFatal(error);
30
+ };
31
+ this.child.once('error',failed);this.child.once('exit',(code,signal)=>failed(new Error(`Native persistent proxy exited (${signal||code})`)));
32
+ });
33
+ this.child.stdin.on('error',()=>{});
34
+ }
35
+ async request(command,values={},timeout=10000){
36
+ await this.ready;
37
+ if(this.stopping||this.child.exitCode!==null||this.pending.size>=32)throw new Error('Native persistent proxy unavailable');
38
+ const id=++this.next;
39
+ return new Promise((resolve,reject)=>{
40
+ const timer=setTimeout(()=>{
41
+ this.pending.delete(id);reject(new Error('Native proxy control request timed out'));
42
+ // A timed-out switch/listen might still mutate native state later.
43
+ // End that uncertain control plane; the watchdog restores saved apps.
44
+ this.child.kill('SIGKILL');
45
+ },timeout);timer.unref();
46
+ this.pending.set(id,{resolve,reject,timer});
47
+ this.child.stdin.write(JSON.stringify({id,command,...values})+'\n',error=>{if(error){const request=this.pending.get(id);if(request){clearTimeout(request.timer);this.pending.delete(id);request.reject(error);}}});
48
+ });
49
+ }
50
+ async stop(){this.stopping=true;this.child.stdin.end();await Promise.race([this.closed,delay(3000)]);if(this.child.exitCode===null)this.child.kill('SIGKILL');await this.closed;}
51
+ }
52
+ export async function nativeGateway(config,options){
53
+ manager??=new NativeProxy(config.binary,options);await manager.ready;
54
+ const app=config.name;await manager.request('listen',{app,hostname:config.hostname,port:config.port});
55
+ return {pid:manager.pid,
56
+ activate:g=>manager.request('activate',{app,generation:g.relative,port:g.port}),
57
+ pause:g=>manager.request('pause',{app,...(g?{generation:g.relative}:{})}),
58
+ retire:(g,timeout)=>manager.request('retire',{app,generation:g.relative,timeout},timeout+5000),
59
+ counts:async()=> (await manager.request('status',{app})).generations,
60
+ close:()=>manager.request('close',{app}),
61
+ };
62
+ }
63
+ export async function stopNativeProxy(){if(manager){const active=manager;manager=undefined;await active.stop();}}
@@ -0,0 +1,12 @@
1
+ import path from 'node:path';
2
+ import {pathToFileURL} from 'node:url';
3
+ // This is an import budget, not a total RSS limit. An individual module and its
4
+ // transitive imports can exceed it; imported JavaScript cannot be unloaded.
5
+ export async function preloadModules(files,{maxRssMb,timeoutMs},load=file=>import(pathToFileURL(file).href)){
6
+ const deadline=performance.now()+timeoutMs,loaded=[];
7
+ for(const file of files){
8
+ if(performance.now()>=deadline||process.memoryUsage().rss>=maxRssMb*1024*1024)break;
9
+ await load(path.resolve(file));loaded.push(file);
10
+ }
11
+ return loaded.length;
12
+ }
@@ -1,4 +1,6 @@
1
1
  import React from 'react';
2
+ import {scheduleCompileCacheFlush} from './compile-cache.mjs';
3
+ import {preloadModules} from './preload.mjs';
2
4
  import { flightBundler } from './worker-metadata.mjs';
3
5
  import { removeBasePath, normalizeTrailingSlash } from '../compat/paths.cjs';
4
6
  import { renderToReadableStream } from 'react-server-dom-webpack/server.node';
@@ -40,6 +42,7 @@ async function routeEntry(request) {
40
42
  return entry;
41
43
  }
42
44
  const entry = await import(pathToFileURL(key).href);
45
+ scheduleCompileCacheFlush();
43
46
  if (request.production) {
44
47
  if (routeEntries.size >= 64) routeEntries.delete(routeEntries.keys().next().value);
45
48
  routeEntries.set(key, entry);
@@ -387,6 +390,10 @@ const responseCompletions = new Map();
387
390
  const manifests = new Map();
388
391
  const artifacts = new Map();
389
392
  parentPort.on('message', async ({ id, request, type, value, token }) => {
393
+ if(type==='preload'){
394
+ try{await preloadModules(value.files,value.options,file=>routeEntry({modulePath:file,production:true}));parentPort.postMessage({type:'preloaded',id});}
395
+ catch(error){parentPort.postMessage({type:'preloaded',id,error:error.message});}return;
396
+ }
390
397
  if (type === 'after-response') { responseCompletions.get(id)?.(); return; }
391
398
  // Only the host arms these probes, while completed responses still have
392
399
  // tracked cache work. There is no timer or heartbeat in an idle RSC isolate.
@@ -0,0 +1,19 @@
1
+ /** Materialize a discovered route pattern without interpolating path separators. */
2
+ export function route(pattern,params={}) {
3
+ if(typeof pattern!=='string'||!pattern.startsWith('/')||pattern.startsWith('//')||pattern.length>4096||!params||typeof params!=='object'||Array.isArray(params))throw TypeError('Invalid route pattern or parameters');
4
+ const used=new Set(),parts=[];
5
+ const segment=value=>{
6
+ if(typeof value!=='string'||!value||value==='.'||value==='..'||/[/\\\0]/.test(value))throw TypeError('Route parameters must be nonempty path segments');
7
+ return encodeURIComponent(value);
8
+ };
9
+ for(const part of pattern.split('/').slice(1)){
10
+ const match=/^(?:\[\[\.\.\.([A-Za-z_$][\w$]*)\]\]|\[\.\.\.([A-Za-z_$][\w$]*)\]|\[([A-Za-z_$][\w$]*)\])$/.exec(part);
11
+ if(!match){if(/[\[\]?#\\\0]/.test(part)||part==='.'||part==='..'||!part&&pattern!=='/')throw TypeError('Invalid route segment');parts.push(part);continue;}
12
+ const name=match[1]||match[2]||match[3];used.add(name);const value=params[name];
13
+ if(match[1]&&value===undefined)continue;
14
+ if(match[1]||match[2]){if(!Array.isArray(value)||match[2]&&!value.length)throw TypeError(`Route parameter ${name} must be an array of segments`);parts.push(...value.map(segment));}
15
+ else parts.push(segment(value));
16
+ }
17
+ for(const name of Object.keys(params))if(!used.has(name))throw TypeError(`Unknown route parameter ${name}`);
18
+ const result='/'+parts.join('/');if(result.length>8192)throw TypeError('Route exceeds 8 KiB');return result;
19
+ }
@@ -9,9 +9,10 @@ const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
9
9
  // including ignored build trees. Atomic build swaps can delete a descendant
10
10
  // during that scan. Watch only source directories, and handle removal races
11
11
  // in our own traversal. No file contents or node_modules watches are retained.
12
- export async function watchProject(root, { onChange, onError, ignore = () => false, watch = watchDirectory, maxDirectories = 8192, nativeRecursive = process.platform === 'darwin' || process.platform === 'win32' }) {
13
- // macOS and Windows have native recursive backends. Separate directory
14
- // subscriptions can lose coalesced events during rapid renames.
12
+ export async function watchProject(root, { onChange, onError, ignore = () => false, watch = watchDirectory, maxDirectories = 8192, nativeRecursive = process.platform === 'darwin' }) {
13
+ // Windows' recursive notification buffer can overflow while V8/webpack flush
14
+ // caches. Source-only subscriptions avoid losing edits in that disk traffic.
15
+ // macOS uses its native recursive backend for coalesced directory renames.
15
16
  if (nativeRecursive) {
16
17
  const watcher = watch(root, { recursive: true }, (event, filename) => {
17
18
  const relative = filename && String(filename).replaceAll(path.sep, '/');
@@ -65,7 +66,8 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
65
66
  async function scan(relative, recursive = false) {
66
67
  if (closed) return;
67
68
  if (!allowedDirectory(relative)) { remove(relative); return; }
68
- if (!directories.has(relative)) {
69
+ const fresh = !directories.has(relative);
70
+ if (fresh) {
69
71
  if (directories.size >= maxDirectories) throw new Error(`Development watcher exceeds ${maxDirectories} source directories. Narrow the application directory.`);
70
72
  let watcher;
71
73
  try {
@@ -78,7 +80,11 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
78
80
  return;
79
81
  }
80
82
  const changed = filename ? path.posix.join(relative, String(filename).replaceAll(path.sep, '/')) : relative;
81
- if (allowedFile(changed)) onChange(changed);
83
+ let metadataOnly = false;
84
+ if (process.platform === 'win32' && event === 'change') {
85
+ try { metadataOnly = statSync(path.join(root, changed)).isDirectory(); } catch {}
86
+ }
87
+ if (allowedFile(changed) && !metadataOnly) onChange(changed);
82
88
  // The .contentlayer container itself is ignored as a source, but
83
89
  // its generated directory may be created after the watcher starts.
84
90
  // Platforms can coalesce a directory rename into a change event.
@@ -103,6 +109,7 @@ export async function watchProject(root, { onChange, onError, ignore = () => fal
103
109
  const children = new Set();
104
110
  for (const entry of entries) {
105
111
  const child = path.posix.join(relative, entry.name);
112
+ if (fresh && ready && entry.isFile() && allowedFile(child)) onChange(child);
106
113
  if (!entry.isDirectory() || !allowedDirectory(child)) continue;
107
114
  children.add(child);
108
115
  if (recursive || !directories.has(child)) await scan(child, recursive);
@@ -1,6 +1,8 @@
1
1
  // prnext-transport:socket-v2
2
2
  // prnext-concurrency:512
3
3
  import { requestFrames } from './request-transport.mjs';
4
+ import {scheduleCompileCacheFlush} from './compile-cache.mjs';
5
+ import {preloadModules} from './preload.mjs';
4
6
  import { requestWork } from './request-work.mjs';
5
7
  import { freezeClientModules } from './worker-metadata.mjs';
6
8
  import { AsyncResource } from 'node:async_hooks';
@@ -36,12 +38,20 @@ loadEnvConfig(projectRoot, { dev: !!manifest.dev });
36
38
  // browser graph and the RSC worker's mode, including dev launched from CI.
37
39
  process.env.NODE_ENV = manifest.dev ? 'development' : 'production';
38
40
  const production = !manifest.dev;
41
+ let dispatchRequest=handleRequest;
42
+ if(!production){
43
+ const {createDevWorkerConsole}=await import('./dev-worker-logs.mjs'),logs=createDevWorkerConsole();globalThis.console=logs.console;
44
+ dispatchRequest=(input,...args)=>logs.run({requestId:input.id,route:routes.get(input.routeId)?.pattern},()=>handleRequest(input,...args));
45
+ const {publishDevCacheCapability}=await import('./dev-cache-control.mjs');
46
+ await publishDevCacheCapability({root:projectRoot,distDir,manifest}).catch(()=>{console.warn('[prnext] Local cache controls are unavailable for this development generation.');});
47
+ }
39
48
  if (production) freezeClientModules(manifest.app?.clientModules);
40
49
  // Load each renderer beside the application only when that request needs it.
41
50
  // Middleware and API workers do not pay for React or the Pages/App renderers.
42
51
  const runtimeModule = async name => {
43
52
  const started = diagnostics.enabled ? performance.now() : 0;
44
53
  const module = await import(pathToFileURL(path.join(distDir, `runtime/${name}.mjs`)).href);
54
+ scheduleCompileCacheFlush();
45
55
  if (diagnostics.enabled) diagnostics.record('wake', { phase: 'module', module: name, durationMs: performance.now() - started });
46
56
  return module;
47
57
  };
@@ -62,6 +72,25 @@ async function handleRequest(input, output, signalController, responseComplete)
62
72
  let responseStarted = false;
63
73
  const finalizers = new Set();
64
74
  try {
75
+ if(input.routeId==='__prnext_preload'&&input.renderMode==='preload'){
76
+ const options=manifest.config?.workerPreload;
77
+ if(!production||!options){await output(input.id,{status:204,body:''},input);return;}
78
+ const selected=options.routes.map(pattern=>manifest.routes.find(route=>route.pattern===pattern&&!route.internal)).filter(Boolean);
79
+ const deadline=performance.now()+options.timeoutMs;
80
+ for(const route of selected){
81
+ if(performance.now()>=deadline||process.memoryUsage().rss>=options.maxRssMb*1024*1024)break;
82
+ const remaining={...options,timeoutMs:Math.max(1,deadline-performance.now())};
83
+ if(route.router==='app'&&route.kind==='page'){
84
+ appRuntime??=await runtimeModule('app-render');
85
+ await appRuntime.preloadAppRoutes([path.resolve(distDir,route.module)],remaining);
86
+ }else{
87
+ if(route.kind==='page')pagesRuntime??=await runtimeModule('render');else apiRuntime??=await runtimeModule('api');
88
+ await preloadModules([path.resolve(distDir,route.module)],remaining);
89
+ }
90
+ if(process.memoryUsage().rss>=options.maxRssMb*1024*1024)break;
91
+ }
92
+ scheduleCompileCacheFlush();await output(input.id,{status:204,body:''},input);return;
93
+ }
65
94
  if (input.renderMode === 'incremental-cache' && input.routeId === '__prnext_incremental_cache') {
66
95
  incrementalRuntime ??= await runtimeModule('incremental-cache');
67
96
  const result = await incrementalRuntime.runIncrementalCache({ ...input, manifest, distDir, production, signal: signalController.signal });
@@ -223,7 +252,7 @@ if (token && laneLimit > 1) {
223
252
  workTimer.unref();
224
253
  };
225
254
  active.signal.addEventListener('abort', retireIfAbandoned, { once: true });
226
- try { await handleRequest(input, output, active, () => { controller = undefined; }); }
255
+ try { await dispatchRequest(input, output, active, () => { controller = undefined; }); }
227
256
  finally {
228
257
  finished = true;
229
258
  if (!pendingWork) { invocations.delete(active); clearTimeout(workTimer); }
@@ -262,6 +291,10 @@ if (token && laneLimit > 1) {
262
291
  else throw error;
263
292
  });
264
293
  server.listen(0, '127.0.0.1', () => {
294
+ if(!production&&typeof globalThis.__PRNEXT_DEV_WORKER_READY__==='function'){
295
+ globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true});
296
+ delete globalThis.__PRNEXT_DEV_WORKER_READY__;
297
+ }
265
298
  writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true }) + '\n');
266
299
  heartbeat = setInterval(() => {
267
300
  // Coalesce the idle transition with the existing heartbeat. Normal
@@ -269,6 +302,7 @@ if (token && laneLimit > 1) {
269
302
  // cannot add two messages for every response.
270
303
  reportedBusy = reportActivity && invocations.size > 0;
271
304
  writeProtocol(reportedBusy ? 'busy\n' : 'alive\n');
305
+ if(!production)globalThis.__PRNEXT_DEV_WORKER_PULSE__?.(reportedBusy?'busy\n':'alive\n');
272
306
  }, 1000).unref();
273
307
  });
274
308
  } else {
@@ -276,7 +310,7 @@ if (token && laneLimit > 1) {
276
310
  for await (let line of readline.createInterface({ input: process.stdin, crlfDelay: Infinity })) {
277
311
  if (!line.trim()) continue;
278
312
  const input = JSON.parse(line); line = '';
279
- try { await handleRequest(input, output, new AbortController()); }
313
+ try { await dispatchRequest(input, output, new AbortController()); }
280
314
  catch (error) { console.error('[prnext] Worker transport failed:', error?.stack || error); process.exit(1); }
281
315
  }
282
316
  // The maintenance/ISR transport is serial and awaits its own finalizers.