@thomas.f/prnext 0.1.16-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 +384 -9
  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 +25 -7
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +2 -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/cli.mjs CHANGED
@@ -1,23 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { rename } from './runtime/fs.mjs';
3
2
  import { spawn } from 'node:child_process';
4
- import { readFile, writeFile, rm, mkdir, readdir } from 'node:fs/promises';
5
- import { fileURLToPath } from 'node:url';
3
+ import { readFile, rm, mkdir, readdir } from 'node:fs/promises';
6
4
  import path from 'node:path';
7
5
  import { resolveNativeBinary, packageManifest } from './native/resolve.mjs';
8
6
  import { replaceWithNative } from './native/launch.mjs';
9
7
  import { readBuildDirectory } from './runtime/build-directory.mjs';
10
- import { shouldWatchProjectFile } from './runtime/env.mjs';
11
8
 
12
- const self = fileURLToPath(import.meta.url);
13
9
  const [command = 'help', ...argv] = process.argv.slice(2);
14
10
  const help = `PRNext ${packageManifest.version}
15
11
 
16
12
  Use prnext for portable scripts; prn is a shortcut on compatible shells.
17
13
 
18
14
  prnext build [directory] Build React/TypeScript and static HTML
15
+ prnext build [directory] --watch Incremental production rebuilds on changes
19
16
  prnext build [directory] --adapter static [--from dist] [--spa]
20
17
  Import an existing public build directory
18
+ prnext build [directory] --adapter auto
19
+ Detect a completed browser build
20
+ prnext build [directory] --adapter static --from .
21
+ Build a plain HTML/CSS/JS site
21
22
  prnext output [directory] [--json] Read the versioned build output contract
22
23
  prnext start [directory] [options] Start native Rust production server
23
24
  prnext pstart [directory] [options] Start production persistently in the background
@@ -28,9 +29,17 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
28
29
  prnext pstartup [--remove] Configure restoration at user login
29
30
  prnext pstart --help All persistent commands and options
30
31
  prnext dev [directory] [options] Rebuild and restart on source changes
32
+ prnext dev [directory] --host-ai Share local diagnostics with your connected Host AI
33
+ prnext dev [directory] --safe Use full builds and fresh workers for troubleshooting
34
+ prnext mcp [directory] Connect an AI to the running local dev server
35
+ prnext docs --ai [topic] Read the AI guide bundled with this version
36
+ prnext docs --ai --init [directory] Add a guide pointer to the project's AGENTS.md
31
37
  prnext routes [directory] Display compiled routes
32
- prnext check [directory] [--json] Check an existing Next project before building
38
+ prnext check [directory] [--json] Diagnose config, routes, Node, native and dependencies
39
+ prnext typegen [directory] [--json] Generate route types without building the app
40
+ prnext typecheck [directory] [--json] Generate types and check TypeScript without building
33
41
  prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
42
+ prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
34
43
  prnext host [prnext.host.json] Host built apps by hostname; wake on demand
35
44
  prnext host [config] --check Validate hosting configuration and builds
36
45
  prnext host [config] --status [--json] Show memory, process and idle status
@@ -47,6 +56,20 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
47
56
  Compare GET responses before switching
48
57
  Migration keeps Next.js and backs up package.json and existing lockfiles.
49
58
  Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
59
+ Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
60
+ The preview reports the manager, root and selected workspace. Failed installs
61
+ restore the selected manifest/lockfiles; dependency caches may need a retry.
62
+
63
+ Type commands require an application tsconfig and TypeScript installation.
64
+ They generate route types unless typedRoutes: false, preserve JSONC/extends,
65
+ and report updates to tsconfig/prnext-env.d.ts. typecheck always checks errors.
66
+
67
+ Analysis options: --metric raw|gzip|brotli (default raw), --baseline analysis.json
68
+ --max-js 150 Maximum initial JS per route, in kB
69
+ --max-increase-bytes 1024 --max-increase-percent 5
70
+ --budgets budgets.json Exact route keys with maxJs (kB),
71
+ maxIncreaseBytes and/or maxIncreasePercent
72
+ Analysis exit codes: 0 passed, 1 budget exceeded, 2 invalid input or missing build.
50
73
 
51
74
  Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
52
75
  Production profiles: --profile balanced|speed|memory|classic
@@ -78,6 +101,18 @@ function stop(child) {
78
101
  async function main() {
79
102
  if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
80
103
  if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
104
+ if (command === 'docs') {
105
+ const { executeAiDocs } = await import('./runtime/ai-docs.mjs');
106
+ const result = await executeAiDocs(argv);
107
+ process.stdout.write(result.output);
108
+ return;
109
+ }
110
+ if (command === 'mcp') {
111
+ if (argv.includes('--help') || argv.includes('-h')) { console.log('prnext mcp [directory]\nLocal development MCP over stdio. Start prnext dev first; no shell or production access is exposed.'); return; }
112
+ if (argv.length > 1 || argv.some(arg => arg.startsWith('-'))) throw Error('mcp accepts only [directory]');
113
+ const {runDevMcp}=await import('./runtime/dev-tools-mcp.mjs');
114
+ await runDevMcp(path.resolve(argv[0] || '.'));return;
115
+ }
81
116
  if (['login', 'logout', 'whoami', 'deploy', 'update'].includes(command)) {
82
117
  const { cloudCommand } = await import('./runtime/cloud/client.mjs');
83
118
  await cloudCommand(command, argv);
@@ -150,9 +185,41 @@ async function main() {
150
185
  if (!report.ok) process.exitCode = 1;
151
186
  return;
152
187
  }
153
- if (!['build', 'start', 'dev', 'routes', 'check', 'inspect', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
188
+ if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
154
189
  const args = [...argv];
155
190
  const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
191
+ if (['check','typegen','typecheck','analyze'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
192
+ if(command==='analyze'){
193
+ try {
194
+ const {analyzeBuild,printAnalysis,parseAnalysisArgs}=await import('./build/analyze.mjs');
195
+ const options=parseAnalysisArgs(args),report=await analyzeBuild(root,options);
196
+ if(options.json)console.log(JSON.stringify(report,null,2));else printAnalysis(report);
197
+ if(!report.ok)process.exitCode=1;
198
+ } catch(error) {
199
+ if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
200
+ else console.error(`PRNext analysis: ${error.message}`);
201
+ process.exitCode=2;
202
+ }
203
+ return;
204
+ }
205
+ if (command === 'typegen' || command === 'typecheck') {
206
+ if (args.some(arg => arg !== '--json') || args.filter(arg => arg === '--json').length > 1) throw new Error(`${command} accepts only [directory] and --json`);
207
+ const { runTypeCommand } = await import('./build/type-command.mjs');
208
+ process.chdir(root);
209
+ process.env.INIT_CWD = root;
210
+ process.env.PWD = root;
211
+ const report = await runTypeCommand(root, { check: command === 'typecheck' });
212
+ if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
213
+ else {
214
+ console.log(`PRNext ${command} ${report.ok ? 'passed' : 'failed'}: ${root}`);
215
+ console.log(`${report.routes} page routes; tsconfig: ${report.tsconfig || 'unavailable'}; route types: ${report.typedRoutes ? 'enabled' : 'disabled'}.`);
216
+ for (const file of report.updated) console.log(`Updated ${file}`);
217
+ for (const note of report.notes) console.log(note);
218
+ for (const error of report.errors) console.error(error);
219
+ }
220
+ if (!report.ok) process.exitCode = 1;
221
+ return;
222
+ }
156
223
  if (command === 'output') {
157
224
  if (args.some(arg => arg !== '--json')) throw new Error('output accepts only [directory] and --json');
158
225
  const { readBuildOutput } = await import('./build/output-contract.mjs');
@@ -196,7 +263,10 @@ async function main() {
196
263
  else {
197
264
  console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
198
265
  console.log(`${report.routes.length} routes. Installed versions: ${Object.entries(report.versions).map(([name,version])=>`${name}@${version||'missing'}`).join(', ')}`);
199
- for (const error of report.errors) console.error(error);
266
+ for (const item of report.diagnostics) {
267
+ const output = item.severity === 'error' ? console.error : console.log;
268
+ output(`${item.severity}: ${item.message}${item.action ? ` ${item.action}` : ''}`);
269
+ }
200
270
  for (const note of report.notes) console.log(note);
201
271
  }
202
272
  if (!report.ok) process.exitCode = 1;
@@ -207,6 +277,7 @@ async function main() {
207
277
  for (let i = 0; i < args.length; i++) {
208
278
  const flag = args[i];
209
279
  if (flag === '--dev') options.dev = true;
280
+ else if (flag === '--watch') options.watch = true;
210
281
  else if (flag === '--spa') options.spa = true;
211
282
  else if (flag === '--adapter' || flag === '--from') {
212
283
  const value = args[++i];
@@ -220,6 +291,17 @@ async function main() {
220
291
  const envMode = process.env.NODE_ENV === 'test' ? 'test' : undefined;
221
292
  process.env.NODE_ENV = dev ? 'development' : 'production';
222
293
  const started = performance.now();
294
+ if(options.watch){
295
+ if(dev)throw new Error('--watch builds production; use prnext dev for development.');
296
+ const {watchProduction}=await import('./build/production-builder.mjs');
297
+ delete options.watch;await watchProduction(root,options);return;
298
+ }
299
+ if (options.adapter === undefined || options.adapter === 'auto') {
300
+ const { detectBuildAdapter } = await import('./build/static-input.mjs');
301
+ const selected = await detectBuildAdapter(root, { prepared: options.adapter === 'auto' });
302
+ if (selected.adapter === 'static') Object.assign(options, selected);
303
+ else if (options.adapter === 'auto') options.adapter = 'react';
304
+ }
223
305
  if (options.adapter === 'static') {
224
306
  const { buildStatic } = await import('./build/static-adapter.mjs');
225
307
  await buildStatic(root, options);
@@ -231,120 +313,22 @@ async function main() {
231
313
  return;
232
314
  }
233
315
  const binary = await resolveNativeBinary();
234
- let outputDirectory = await readBuildDirectory(root);
235
- const serverEnvironment = { ...process.env, NODE_ENV: process.env.NODE_ENV || (command === 'dev' ? 'development' : 'production') };
236
- if (command === 'start' && serverEnvironment.NODE_ENV !== 'development') {
237
- if (replaceWithNative(binary, ['start', root, '--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'),
238
- '--node', process.execPath, ...args], serverEnvironment)) return;
239
- }
240
- const launch = () => spawn(binary, [command === 'routes' ? 'routes' : 'start', root,
241
- ...(command === 'routes' ? [] : ['--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'), '--node', process.execPath,
242
- '--shutdown-on-stdin-eof']), ...args], { stdio: ['pipe', 'inherit', 'inherit'], windowsHide: true, env: { ...process.env, NODE_ENV: process.env.NODE_ENV || (command === 'dev' ? 'development' : 'production') } });
243
- let server;
244
- let stopping = false;
245
- let watcher;
246
- let rebuildChild;
247
- let timer;
248
- const devStateFile = path.join(root, '.prnext-dev.json');
249
- const changedFiles = new Set();
250
- async function notifyDev(value) {
251
- const temporary = `${devStateFile}.${process.pid}.tmp`;
252
- await writeFile(temporary, JSON.stringify(value));
253
- await rename(temporary, devStateFile);
316
+ if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
317
+ const outputDirectory = await readBuildDirectory(root);
318
+ const serverEnvironment = {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
319
+ if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
320
+ if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
321
+ '--node',process.execPath,...args],serverEnvironment))return;
254
322
  }
255
- const shutdown = async () => {
256
- if (stopping) return;
257
- stopping = true;
258
- clearTimeout(timer);
259
- watcher?.close();
260
- await Promise.all([stop(server), stop(rebuildChild)]);
261
- if (command === 'dev') await rm(devStateFile, { force: true });
262
- };
263
- process.once('SIGINT', shutdown);
264
- process.once('SIGTERM', shutdown);
265
- if (command !== 'dev') {
266
- server = launch();
267
- await done(server).catch(error => { if (!stopping) throw error; });
268
- return;
269
- }
270
- let building = false;
271
- let queued = false;
272
- let attempted = false;
273
- const {createDevChangeFilter} = await import('./runtime/dev-file-changes.mjs');
274
- const filterChanges = createDevChangeFilter(root);
275
- async function rebuild() {
276
- if (stopping) return;
277
- if (building) { queued = true; return; }
278
- building = true;
279
- const pending = [...changedFiles];
280
- changedFiles.clear();
281
- let buildOutput = '';
282
- try {
283
- const actualChanges = await filterChanges(pending);
284
- if (stopping) return;
285
- if (attempted && !actualChanges.length) return;
286
- attempted = true;
287
- const changed = actualChanges.slice(0, 32).map(file => file.slice(0, 256));
288
- await notifyDev({ state: 'building' });
289
- if (actualChanges.some(file => /(?:^|\/)(?:(?:next|prnext|contentlayer)\.config\.|package(?:-lock)?\.json$|(?:pnpm-lock\.yaml|yarn\.lock)$)/.test(file))) { await stop(rebuildChild); rebuildChild = undefined; }
290
- if (stopping) return;
291
- if (!rebuildChild || rebuildChild.exitCode !== null || rebuildChild.signalCode) {
292
- rebuildChild = spawn(process.execPath, [path.join(path.dirname(self), 'build/dev-worker.mjs')], { stdio: ['inherit', 'pipe', 'pipe', 'ipc'], env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' } });
293
- }
294
- const stdout = chunk => { process.stdout.write(chunk); buildOutput = (buildOutput + chunk).slice(-12_000); };
295
- const stderr = chunk => { process.stderr.write(chunk); buildOutput = (buildOutput + chunk).slice(-12_000); };
296
- rebuildChild.stdout.on('data', stdout); rebuildChild.stderr.on('data', stderr);
297
- let result;
298
- try {
299
- result = await new Promise((resolve, reject) => {
300
- const worker = rebuildChild;
301
- const cleanup = () => { worker.off('message', message); worker.off('exit', exited); worker.off('error', failed); };
302
- const message = value => { if (value?.type === 'result') { cleanup(); resolve(value); } };
303
- const exited = (code, signal) => { cleanup(); reject(new Error(`Compiler exited ${signal || code}`)); };
304
- const failed = error => { cleanup(); reject(error); };
305
- worker.on('message', message); worker.once('exit', exited); worker.once('error', failed);
306
- worker.send({type:'build',root});
307
- });
308
- } finally { rebuildChild.stdout.off('data', stdout); rebuildChild.stderr.off('data', stderr); }
309
- if (result.recycle) { await stop(rebuildChild); rebuildChild = undefined; }
310
- if (!result.ok) throw new Error(result.error);
311
- filterChanges.acceptInputs(result.inputs || []);
312
- if (!stopping) {
313
- await stop(server);
314
- outputDirectory = await readBuildDirectory(root);
315
- await watcher.refresh();
316
- // Shutdown can arrive while the previous server is draining. Never
317
- // leave a replacement running after shutdown already stopped its peers.
318
- if (stopping) return;
319
- for (const file of changedFiles) {
320
- if (file === outputDirectory || file.startsWith(outputDirectory + '/') || outputDirectory.startsWith(file + '/')) changedFiles.delete(file);
321
- }
322
- if (!changedFiles.size) { clearTimeout(timer); queued = false; }
323
- server = launch();
324
- server.on('error', error => console.error(`PRNext server: ${error.message}`));
325
- const manifest = JSON.parse(await readFile(path.join(root, outputDirectory, 'manifest.json'), 'utf8'));
326
- if (stopping) return;
327
- await notifyDev({ state: 'ready', buildId: manifest.buildId, clientManifest: manifest.devClient, changed });
328
- console.log('Source compiled. React Fast Refresh applied in connected browsers.');
329
- }
330
- } catch (error) { if (!stopping) { console.error(`Build failed: ${error.message}`); await notifyDev({ state: 'error', error: buildOutput || error.message }); } }
331
- finally { building = false; if (queued && !stopping) { queued = false; void rebuild(); } }
332
- }
333
- const { watchProject } = await import('./runtime/watch-project.mjs');
334
- watcher = await watchProject(root, {
335
- ignore: relative => relative === outputDirectory || relative.startsWith(outputDirectory + '/'),
336
- onError: error => { console.error(`PRNext development watcher: ${error.message}`); process.exitCode = 1; void shutdown(); },
337
- onChange: filename => {
338
- if (stopping) return;
339
- const relative = String(filename || '').replaceAll(path.sep, '/');
340
- if (relative === outputDirectory || relative.startsWith(outputDirectory + '/') || !shouldWatchProjectFile(filename)) return;
341
- changedFiles.add(String(filename).replaceAll(path.sep, '/'));
342
- clearTimeout(timer);
343
- timer = setTimeout(() => { if (changedFiles.size) void rebuild(); }, 150);
344
- },
345
- });
346
- if (stopping) { watcher.close(); return; }
347
- await rebuild();
323
+ const server=spawn(binary,[command==='routes'?'routes':'start',root,
324
+ ...(command==='routes'?[]:['--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),'--node',process.execPath,'--shutdown-on-stdin-eof']),...args],
325
+ {stdio:['pipe','inherit','inherit'],windowsHide:true,env:serverEnvironment});
326
+ let stopping=false;
327
+ const shutdown=async()=>{if(stopping)return;stopping=true;await stop(server);};
328
+ process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
329
+ try{await done(server).catch(error=>{if(!stopping)throw error;});}
330
+ finally{process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);await stop(server);}
331
+
348
332
  }
349
333
 
350
334
  main().catch(error => { console.error(`PRNext: ${error.message}`); process.exitCode = 1; });
package/compat/index.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  import type { ComponentType } from 'react';
3
3
  import type { IncomingMessage, ServerResponse } from 'node:http';
4
4
  export type { Metadata, Viewport, ResolvingMetadata, ResolvingViewport, MetadataRoute } from './metadata.cjs';
5
+ export {route} from './typed-route.cjs';
6
+ export type {Route, RoutePattern, RouteParameters} from './typed-route.cjs';
5
7
 
6
8
  export interface InstantSample {
7
9
  cookies?: Array<{name: string; value: string | null}>;
@@ -116,10 +118,12 @@ export interface NextConfig {
116
118
  * 0 disables the corresponding retirement; keepWarm retains one lane per used host.
117
119
  */
118
120
  workerRetirement?: { idleSeconds?: number; laneIdleSeconds?: number; keepWarm?: boolean };
119
- /** Next dev page-retention hints; PRNext builds all routes without that eviction queue. */
121
+ /** Import selected Node routes on startup; top-level module code executes. RSS is a soft import budget. */
122
+ workerPreload?: false | { routes: string[]; maxRssMb?: number; timeoutMs?: number };
123
+ /** Bounded active development routes; default retains five requested routes. */
120
124
  onDemandEntries?: { maxInactiveAge?: number; pagesBufferLength?: number };
121
- /** Disable the Next development indicator. Error overlays and Fast Refresh remain enabled. */
122
- devIndicators?: false;
125
+ /** PRNext development menu; bottom-left by default. false hides it without disabling errors or Fast Refresh. */
126
+ devIndicators?: false | { position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' };
123
127
  i18n?: { locales: string[]; defaultLocale: string; localeDetection?: boolean; domains?: Array<{domain: string; defaultLocale: string; locales?: string[]; http?: boolean}> };
124
128
  webpack?: (config: import('webpack').Configuration, context: { buildId: string; dev: boolean; isServer: boolean; nextRuntime?: 'nodejs' | 'edge'; webpack: typeof import('webpack'); defaultLoaders: {babel: {loader: string}} }) => import('webpack').Configuration | Promise<import('webpack').Configuration>;
125
129
  turbopack?: { root?: string; rules?: Record<string, TurbopackRule | TurbopackRule[]>; resolveAlias?: Record<string, string | { browser: string }>; resolveExtensions?: string[] };
@@ -128,10 +132,12 @@ export interface NextConfig {
128
132
  reactStrictMode?: boolean;
129
133
  /** Enabled React Compiler transforms are not implemented. */
130
134
  reactCompiler?: false;
131
- /** Typed route generation and validation are not implemented. */
132
- typedRoutes?: false;
135
+ /** Generate route completions and validate Link/router destinations. */
136
+ typedRoutes?: boolean;
137
+ /** Native CSS for default styles; custom PostCSS pipelines remain authoritative. */
138
+ cssEngine?: 'auto'|'postcss'|'lightningcss';
133
139
  /** Production checks use the project's installed TypeScript without emitting files. */
134
- typescript?: { ignoreBuildErrors?: boolean; tsconfigPath?: string };
140
+ typescript?: { ignoreBuildErrors?: boolean; tsconfigPath?: string; devChecks?: boolean };
135
141
  cacheComponents?: boolean;
136
142
  cacheLife?: Record<string, import('./cache.cjs').CacheLife>;
137
143
  transpilePackages?: string[];
@@ -191,9 +197,17 @@ export interface StaticBuildOptions {
191
197
  dev?: false;
192
198
  }
193
199
  export interface BuildResult { buildId: string; outputDirectory: string; }
200
+ export interface ProductionBuilder {
201
+ build(options?: {envMode?: 'production' | 'test'}): Promise<BuildResult>;
202
+ stats(): {contexts: number; created: number; reused: number};
203
+ dispose(): Promise<void>;
204
+ }
205
+ export function createBuilder(projectRoot: string, options?: {envMode?: 'production' | 'test'}): Promise<ProductionBuilder>;
194
206
  export interface StaticBuildResult extends BuildResult { files: number; bytes: number; }
195
207
  export function build(projectRoot: string, options: StaticBuildOptions & { adapter: 'static' }): Promise<StaticBuildResult>;
196
- export function build(projectRoot: string, options?: { adapter?: 'react'; dev?: boolean; envMode?: 'production' | 'development' | 'test' }): Promise<BuildResult>;
208
+ /** Detect a completed browser output or a PRNext React project; does not run another builder. */
209
+ export function build(projectRoot: string, options: { adapter: 'auto'; dev?: false; spa?: boolean }): Promise<BuildResult | StaticBuildResult>;
210
+ export function build(projectRoot: string, options?: { adapter?: 'react'; dev?: boolean; incremental?: boolean; envMode?: 'production' | 'development' | 'test' }): Promise<BuildResult>;
197
211
  export function buildStatic(projectRoot: string, options?: StaticBuildOptions): Promise<StaticBuildResult>;
198
212
  export function readBuildOutput(buildDirectory: string): Promise<BuildOutput>;
199
213
  export function validateBuildOutput(value: unknown): BuildOutput;
package/compat/link.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { AnchorHTMLAttributes, ForwardRefExoticComponent, ReactNode, RefAttributes } from 'react';
2
2
  import type { UrlObject } from './router.cjs';
3
- export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
4
- href: string | UrlObject;
3
+ import type {Route,TypedUrlObject} from './typed-route.cjs';
4
+ export interface LinkProps<T extends string=string> extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
5
+ href: Route<T> | TypedUrlObject;
5
6
  as?: string | UrlObject;
6
7
  replace?: boolean;
7
8
  scroll?: boolean;
@@ -13,5 +14,5 @@ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
13
14
  children?: ReactNode;
14
15
  onNavigate?: (event: { preventDefault(): void }) => void;
15
16
  }
16
- declare const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>;
17
+ declare function Link<T extends string>(props:LinkProps<T> & RefAttributes<HTMLAnchorElement>): import('react').ReactElement|null;
17
18
  export default Link;
@@ -1,12 +1,13 @@
1
+ import type {Route} from './typed-route.cjs';
1
2
  export interface NavigateOptions { scroll?: boolean }
2
3
  export interface PrefetchOptions { onInvalidate?: () => void }
3
4
  export interface AppRouterInstance {
4
- push(href: string, options?: NavigateOptions): void;
5
- replace(href: string, options?: NavigateOptions): void;
5
+ push<T extends string>(href: Route<T>, options?: NavigateOptions): void;
6
+ replace<T extends string>(href: Route<T>, options?: NavigateOptions): void;
6
7
  refresh(): void;
7
8
  back(): void;
8
9
  forward(): void;
9
- prefetch(href: string, options?: PrefetchOptions): void;
10
+ prefetch<T extends string>(href: Route<T>, options?: PrefetchOptions): void;
10
11
  }
11
12
  export class ReadonlyURLSearchParams extends URLSearchParams {
12
13
  append(): never;
@@ -1,4 +1,5 @@
1
1
  import type { ComponentType, ReactNode, ReactElement } from 'react';
2
+ import type {Route,TypedUrlObject} from './typed-route.cjs';
2
3
  export interface UrlObject {
3
4
  auth?: string | null;
4
5
  host?: string | null;
@@ -33,9 +34,9 @@ export interface PRNextRouter extends RouterSnapshot {
33
34
  isFallback: boolean;
34
35
  isPreview: boolean;
35
36
  basePath: string;
36
- push(url: string | UrlObject, as?: string | UrlObject, options?: { scroll?: boolean; shallow?: boolean; locale?: string | false }): Promise<boolean>;
37
- replace(url: string | UrlObject, as?: string | UrlObject, options?: { scroll?: boolean; shallow?: boolean; locale?: string | false }): Promise<boolean>;
38
- prefetch(url: string | UrlObject, as?: string | UrlObject, options?: { priority?: boolean; locale?: string | false }): Promise<void>;
37
+ push<T extends string>(url: Route<T> | TypedUrlObject, as?: string | UrlObject, options?: { scroll?: boolean; shallow?: boolean; locale?: string | false }): Promise<boolean>;
38
+ replace<T extends string>(url: Route<T> | TypedUrlObject, as?: string | UrlObject, options?: { scroll?: boolean; shallow?: boolean; locale?: string | false }): Promise<boolean>;
39
+ prefetch<T extends string>(url: Route<T> | TypedUrlObject, as?: string | UrlObject, options?: { priority?: boolean; locale?: string | false }): Promise<void>;
39
40
  beforePopState(callback: (state: { url: string; as: string; options: { shallow?: boolean; scroll?: boolean } }) => boolean): void;
40
41
  events: RouterEvents;
41
42
  reload(): void;
@@ -0,0 +1,17 @@
1
+ import type {UrlObject} from './router.cjs';
2
+ declare global { namespace PRNext { interface RouteMap {} interface RouteConfig {} } }
3
+ type GeneratedRoutes = keyof PRNext.RouteMap extends never ? PRNext.RouteConfig extends {enabled:true} ? true : false : true;
4
+ export type RoutePattern = GeneratedRoutes extends true ? Extract<keyof PRNext.RouteMap,string> : string;
5
+ export type RouteParameters<P extends string> = P extends keyof PRNext.RouteMap ? PRNext.RouteMap[P] : Record<string,string|string[]|undefined>;
6
+ type Suffix = '' | `?${string}` | `#${string}`;
7
+ type External = `https://${string}` | `http://${string}` | `mailto:${string}` | `tel:${string}` | `#${string}` | `?${string}`;
8
+ type Expand<P extends string> = P extends `${infer A}/[[...${string}]]` ? A | `${A}/${string}` : P extends `${infer A}[${string}]${infer B}` ? `${A}${string}${Expand<B>}` : P;
9
+ type Strip<P extends string> = P extends `${infer A}#${string}` ? Strip<A> : P extends `${infer A}?${string}` ? A : P;
10
+ type Segment<S extends string> = S extends ''|'.'|'..' ? false : S extends `${string}/${string}`|`${string}\\${string}` ? false : true;
11
+ type Segments<S extends string> = S extends `${infer H}/${infer R}` ? Segment<H> extends true ? Segments<R> : false : Segment<S>;
12
+ type Match<P extends string,S extends string> = P extends `${infer A}/[[...${string}]]` ? S extends A ? true : Match<`${A}/[...rest]`,S> : P extends `[...${string}]` ? Segments<S> : P extends `${infer H}/${infer R}` ? S extends `${infer V}/${infer T}` ? H extends `[${string}]` ? Segment<V> extends true ? Match<R,T> : false : H extends V ? Match<R,T> : false : false : P extends `[${string}]` ? Segment<S> : P extends S ? true : false;
13
+ type Matching<S extends string,P extends string=RoutePattern> = P extends unknown ? Match<P,S> extends true ? true : never : never;
14
+ type Suggestions = Expand<RoutePattern> extends infer P extends string ? `${P}${Suffix}` : never;
15
+ export type Route<T extends string=string> = GeneratedRoutes extends false ? T : string extends T ? Suggestions|External : T extends External ? T : true extends Matching<Strip<T>> ? T : never;
16
+ export type TypedUrlObject = GeneratedRoutes extends false ? UrlObject : {[P in RoutePattern]: Omit<UrlObject,'pathname'|'query'> & {pathname:P} & ({} extends RouteParameters<P> ? {query?:RouteParameters<P>&Record<string,unknown>} : {query:RouteParameters<P>&Record<string,unknown>})}[RoutePattern];
17
+ export function route<P extends RoutePattern>(pattern:P,...args:{} extends RouteParameters<P> ? [params?:RouteParameters<P>] : [params:RouteParameters<P>]): Route;
@@ -0,0 +1,106 @@
1
+ # Compatibility and migration
2
+
3
+ [AI documentation index](../../AI.md)
4
+
5
+ ## Treat compatibility as a checked contract
6
+
7
+ PRNext adapts many public Next.js conventions; it is not a drop-in implementation
8
+ of every Next.js version. Package installation, a recognized import or a green
9
+ source audit does not prove an application works. The installed package's
10
+ metadata, declarations, diagnostics and executable behavior are the authority
11
+ for this release. Do not use an old repository checklist to override them.
12
+
13
+ Common supported areas include App/Pages routing, React Server Components,
14
+ Server Actions, request APIs, navigation, route metadata, supported cache APIs,
15
+ CSS Modules and the project's PostCSS pipeline. Scope varies by runtime and
16
+ option. Advanced parallel/intercepting routing, custom webpack/loaders, package
17
+ transpilation, image/font tooling and caching deserve targeted build and browser
18
+ tests. Support for one configuration shape does not imply support for all
19
+ upstream plugin behavior.
20
+
21
+ Configuration uses one `next.config.*` or `prnext.config.*` file and rejects
22
+ unknown keys rather than ignoring them. `reactCompiler: false` is accepted;
23
+ enabling React Compiler is explicitly unsupported. `typedRoutes: true` is
24
+ implemented through PRNext's generated route registry. Do not copy a full
25
+ Next.js configuration template into a new application.
26
+
27
+ Only explicit private Next.js import adapters are supported. Do not add arbitrary
28
+ `next/dist/...` imports or install another Next runtime to satisfy one: isolate
29
+ or replace the incompatible dependency, retain the original framework where
30
+ needed, and describe the limitation honestly.
31
+
32
+ ## Migrate conservatively
33
+
34
+ Use the project's existing package manager. For npm, install the intended
35
+ published PRNext release and its matching React peers, then inspect the dry run:
36
+
37
+ ```sh
38
+ npm i --save-exact @thomas.f/prnext react@19.3.1 react-dom@19.3.1 react-server-dom-webpack@19.3.1
39
+ npx --no-install prnext migrate --dry-run
40
+ npx --no-install prnext migrate --check --json
41
+ ```
42
+
43
+ Review scripts, manager/workspace detection, configuration and reported source
44
+ conflicts. When the plan is correct:
45
+
46
+ ```sh
47
+ npx --no-install prnext migrate
48
+ npx --no-install prnext check --json
49
+ npx --no-install prnext typecheck
50
+ npx --no-install prnext build
51
+ ```
52
+
53
+ Migration changes selected scripts/dependencies, backs up package files and
54
+ existing lockfiles, and keeps Next.js and application source. Public `next/*`
55
+ imports can remain. Do not remove Next, replace all imports or recreate the
56
+ application without a separate need.
57
+
58
+ Installation honors npm, pnpm and Yarn Classic/Berry and the owning workspace
59
+ root. Conflicting managers, unsafe filesystem entries and unlisted workspaces
60
+ can block automatic install. `migrate --no-install` prepares the manifest for
61
+ manual manager execution. Custom shell scripts may require a manual edit.
62
+ An installation failure attempts safe manifest/lockfile restoration; it cannot
63
+ roll back every dependency directory or manager cache. Never solve peer conflicts
64
+ by silently adding `--force` or `--legacy-peer-deps`.
65
+
66
+ `migrate --check` audits without applying a migration. Configuration evaluation
67
+ can run project code, and installed custom loaders still require a real build.
68
+ The audit is not a sandbox for untrusted projects.
69
+
70
+ ## Compare running applications
71
+
72
+ Start the original application and the candidate on separate available ports.
73
+ Create `paths.json` with representative concrete URLs:
74
+
75
+ ```json
76
+ ["/", "/products/demo", "/api/health"]
77
+ ```
78
+
79
+ Then compare public GET responses:
80
+
81
+ ```sh
82
+ prnext migrate --check --against http://localhost:3000 --candidate http://localhost:3001 --routes paths.json
83
+ ```
84
+
85
+ The comparison does not authenticate or follow redirects. It compares response
86
+ samples, including status, content type and selected body semantics; it does not
87
+ prove hydration, styling, actions or all dynamic routes. Visit important routes
88
+ in a real browser, test authorized and unauthorized states, and run the existing
89
+ application tests. Keep the original setup available until the candidate is
90
+ actually verified. Timings from this comparison are not controlled benchmarks.
91
+
92
+ ## Boundaries that often matter
93
+
94
+ React server/client directives in `.mts`/`.cts` are rejected; use `.ts`/`.tsx`.
95
+ App Route Handlers use Web Request/Response, while Pages APIs retain their own
96
+ request/response contract. App and Pages router hooks are not interchangeable.
97
+ Edge code cannot assume Node APIs or Node-only package behavior.
98
+
99
+ HTTP/1 WebSockets can tunnel through supported external HTTP(S) rewrites and
100
+ hosting gateways; App/Pages handlers do not gain a WebSocket server API. Static
101
+ imports accept completed public browser output, not another framework's SSR
102
+ server. See [Deployment](deployment.md) before choosing an adapter.
103
+
104
+ When a feature is missing, give its exact failing API/option and a supported
105
+ alternative. Do not claim full compatibility, invent a replacement method or
106
+ mask the error with an empty shim.