@thomas.f/prnext 0.1.16-alpha → 0.1.18-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/AI.md +63 -0
  2. package/README.md +35 -503
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +6 -1
  5. package/build/analyze.mjs +340 -0
  6. package/build/app-config.mjs +20 -8
  7. package/build/app-static.mjs +41 -14
  8. package/build/app-styles.mjs +79 -0
  9. package/build/app.mjs +99 -31
  10. package/build/build-lock.mjs +6 -6
  11. package/build/bundle-runtime.mjs +39 -11
  12. package/build/cache-components.mjs +1 -1
  13. package/build/cache-handlers.mjs +1 -1
  14. package/build/check.mjs +56 -11
  15. package/build/compact-css.mjs +53 -0
  16. package/build/compilation-cache.mjs +269 -0
  17. package/build/compilation-store.mjs +142 -0
  18. package/build/compiler.mjs +51 -4
  19. package/build/concurrent.mjs +12 -0
  20. package/build/config.mjs +50 -13
  21. package/build/create.mjs +239 -0
  22. package/build/css-modules.mjs +65 -10
  23. package/build/deployment-assets.mjs +89 -0
  24. package/build/deployment-config.mjs +18 -0
  25. package/build/dev-compiler-health.mjs +11 -0
  26. package/build/dev-css-update.mjs +85 -0
  27. package/build/dev-dependencies.mjs +44 -0
  28. package/build/dev-discovery.mjs +28 -0
  29. package/build/dev-worker.mjs +14 -5
  30. package/build/dev.mjs +64 -6
  31. package/build/dynamic.mjs +1 -1
  32. package/build/edge.mjs +5 -4
  33. package/build/fonts.mjs +1 -1
  34. package/build/framework-imports.mjs +50 -0
  35. package/build/html-only-config.mjs +53 -0
  36. package/build/html-only-graph.mjs +67 -0
  37. package/build/incremental.mjs +8 -4
  38. package/build/index.mjs +140 -112
  39. package/build/inspect.mjs +3 -0
  40. package/build/inspection-summary.mjs +37 -4
  41. package/build/instrumentation.mjs +30 -0
  42. package/build/loader-cache.mjs +53 -26
  43. package/build/metadata-routes.mjs +3 -4
  44. package/build/migrate.mjs +60 -57
  45. package/build/migration-audit.mjs +1 -1
  46. package/build/native-type-config.mjs +37 -0
  47. package/build/npm-packages.mjs +3 -18
  48. package/build/package-manager.mjs +308 -0
  49. package/build/persistent-cache.mjs +112 -0
  50. package/build/precompress.mjs +62 -30
  51. package/build/prerender-cache.mjs +70 -0
  52. package/build/prerender-config.mjs +29 -0
  53. package/build/prerender-pages.mjs +100 -0
  54. package/build/prerender-pool.mjs +165 -0
  55. package/build/prerender-process-worker.mjs +58 -0
  56. package/build/prerender-worker.mjs +10 -0
  57. package/build/production-builder.mjs +129 -0
  58. package/build/react-compiler-report.mjs +225 -0
  59. package/build/react-compiler.mjs +139 -0
  60. package/build/runtime-contract.mjs +2 -2
  61. package/build/scan.mjs +4 -4
  62. package/build/source-analysis.mjs +42 -0
  63. package/build/static-adapter.mjs +13 -5
  64. package/build/static-export.mjs +4 -4
  65. package/build/static-input.mjs +48 -0
  66. package/build/styles.mjs +17 -3
  67. package/build/transform-cache.mjs +37 -4
  68. package/build/transform.mjs +1 -1
  69. package/build/type-command.mjs +41 -0
  70. package/build/typed-routes.mjs +95 -0
  71. package/build/typescript-dev-worker.mjs +18 -0
  72. package/build/typescript-dev.mjs +49 -0
  73. package/build/typescript-native-worker.mjs +55 -0
  74. package/build/typescript-native.mjs +18 -0
  75. package/build/typescript-toolchain.mjs +19 -0
  76. package/build/typescript-watch.mjs +22 -0
  77. package/build/typescript.mjs +11 -8
  78. package/build/webpack-graph.mjs +170 -30
  79. package/build/worker-memory.mjs +9 -0
  80. package/build/worker-preload.mjs +9 -0
  81. package/cli.mjs +162 -122
  82. package/compat/activity-cache.cjs +26 -0
  83. package/compat/after.cjs +6 -4
  84. package/compat/app-layout-context.cjs +52 -2
  85. package/compat/base64.cjs +10 -0
  86. package/compat/cache-handlers.cjs +12 -2
  87. package/compat/cache-wire.cjs +28 -0
  88. package/compat/cache.cjs +2 -1
  89. package/compat/cache.d.cts +4 -0
  90. package/compat/cookies.cjs +21 -5
  91. package/compat/data-cache.cjs +12 -8
  92. package/compat/flight-delta.cjs +150 -0
  93. package/compat/headers.cjs +4 -2
  94. package/compat/incremental-cache.cjs +20 -7
  95. package/compat/index.d.ts +89 -10
  96. package/compat/link.cjs +12 -9
  97. package/compat/link.d.cts +4 -3
  98. package/compat/navigation-authorization.cjs +9 -0
  99. package/compat/navigation-stages.cjs +19 -0
  100. package/compat/navigation.d.cts +4 -3
  101. package/compat/offline-store.cjs +11 -0
  102. package/compat/offline.cjs +5 -0
  103. package/compat/offline.d.cts +2 -0
  104. package/compat/redis-cache.cjs +201 -0
  105. package/compat/redis-cache.d.cts +37 -0
  106. package/compat/router.d.cts +4 -3
  107. package/compat/server-primitives.cjs +31 -11
  108. package/compat/server.d.cts +15 -2
  109. package/compat/static-generation.cjs +7 -0
  110. package/compat/typed-route.d.cts +17 -0
  111. package/compat/use-cache.cjs +3 -0
  112. package/docs/ai/compatibility.md +275 -0
  113. package/docs/ai/data.md +288 -0
  114. package/docs/ai/deployment.md +269 -0
  115. package/docs/ai/manifest.json +11 -0
  116. package/docs/ai/quickstart.md +171 -0
  117. package/docs/ai/routing.md +214 -0
  118. package/docs/ai/troubleshooting.md +155 -0
  119. package/index.mjs +2 -0
  120. package/native/diagnostics.mjs +34 -0
  121. package/native/resolve.mjs +7 -2
  122. package/package.json +44 -7
  123. package/release/ai-docs.mjs +48 -0
  124. package/release/verify.mjs +2 -0
  125. package/runtime/action-client.mjs +10 -4
  126. package/runtime/action-server.mjs +2 -0
  127. package/runtime/ai-docs.mjs +202 -0
  128. package/runtime/app-client.mjs +226 -68
  129. package/runtime/app-content.mjs +4 -3
  130. package/runtime/app-partial-client.mjs +3 -0
  131. package/runtime/app-partial.mjs +1 -1
  132. package/runtime/app-prefetch-constants.mjs +4 -0
  133. package/runtime/app-prefetch.mjs +2 -3
  134. package/runtime/app-render-key.mjs +5 -0
  135. package/runtime/app-render.mjs +100 -28
  136. package/runtime/app-routing.mjs +22 -9
  137. package/runtime/app-static.mjs +18 -8
  138. package/runtime/app-stream-html.mjs +10 -53
  139. package/runtime/app-style-selection.mjs +34 -0
  140. package/runtime/app-styles.mjs +154 -0
  141. package/runtime/cache-components-rsc.mjs +4 -1
  142. package/runtime/cache-request.mjs +2 -1
  143. package/runtime/cloud/archive.mjs +5 -6
  144. package/runtime/compile-cache.mjs +8 -0
  145. package/runtime/deployment-client.mjs +19 -0
  146. package/runtime/dev-ai-brands.mjs +26 -0
  147. package/runtime/dev-ai-style.mjs +99 -0
  148. package/runtime/dev-build-reasons.mjs +25 -0
  149. package/runtime/dev-cache-control.mjs +53 -0
  150. package/runtime/dev-client.mjs +245 -46
  151. package/runtime/dev-compiler-broker.mjs +42 -0
  152. package/runtime/dev-console-copy.mjs +19 -0
  153. package/runtime/dev-console.mjs +106 -0
  154. package/runtime/dev-dependency-graph.mjs +40 -0
  155. package/runtime/dev-element-picker-style.mjs +27 -0
  156. package/runtime/dev-element-picker.mjs +213 -0
  157. package/runtime/dev-error-overlay-style.mjs +41 -0
  158. package/runtime/dev-error-overlay.mjs +167 -0
  159. package/runtime/dev-error-report.mjs +121 -0
  160. package/runtime/dev-file-changes.mjs +1 -1
  161. package/runtime/dev-gateway.mjs +44 -0
  162. package/runtime/dev-host-relay.mjs +66 -0
  163. package/runtime/dev-indicator.mjs +191 -0
  164. package/runtime/dev-issue-motion.mjs +214 -0
  165. package/runtime/dev-lazy-tool.mjs +34 -0
  166. package/runtime/dev-logo.mjs +2 -0
  167. package/runtime/dev-notification.mjs +81 -0
  168. package/runtime/dev-popout.mjs +34 -0
  169. package/runtime/dev-position.mjs +107 -0
  170. package/runtime/dev-preview-touch.mjs +122 -0
  171. package/runtime/dev-runtime-pool.mjs +45 -0
  172. package/runtime/dev-runtime-spare.mjs +57 -0
  173. package/runtime/dev-tools-client.mjs +155 -0
  174. package/runtime/dev-tools-mcp.mjs +126 -0
  175. package/runtime/dev-tools-metrics.mjs +31 -0
  176. package/runtime/dev-tools-model.mjs +42 -0
  177. package/runtime/dev-tools-panel.mjs +341 -0
  178. package/runtime/dev-tools-result.mjs +37 -0
  179. package/runtime/dev-tools-server.mjs +256 -0
  180. package/runtime/dev-tools-source.mjs +104 -0
  181. package/runtime/dev-tooltip.mjs +94 -0
  182. package/runtime/dev-ui-icons.mjs +8 -0
  183. package/runtime/dev-workbench-style.mjs +106 -0
  184. package/runtime/dev-workbench.mjs +428 -0
  185. package/runtime/dev-worker-logs.mjs +31 -0
  186. package/runtime/development.mjs +324 -0
  187. package/runtime/document.mjs +1 -1
  188. package/runtime/edge.mjs +6 -5
  189. package/runtime/env.mjs +2 -1
  190. package/runtime/fetch-cache.mjs +10 -8
  191. package/runtime/flight-delta.mjs +50 -0
  192. package/runtime/flight-tap.mjs +28 -0
  193. package/runtime/html-only.mjs +27 -0
  194. package/runtime/http.mjs +2 -0
  195. package/runtime/incremental-cache.mjs +25 -4
  196. package/runtime/instrumentation.mjs +49 -0
  197. package/runtime/navigation-recovery.mjs +36 -0
  198. package/runtime/navigation.mjs +7 -0
  199. package/runtime/node-html-stream.mjs +47 -0
  200. package/runtime/pages-client.mjs +24 -8
  201. package/runtime/pages-navigation.mjs +54 -64
  202. package/runtime/persistent/client.mjs +6 -5
  203. package/runtime/persistent/daemon.mjs +66 -56
  204. package/runtime/persistent/native-proxy.mjs +63 -0
  205. package/runtime/prefetch-scheduler.mjs +110 -0
  206. package/runtime/preload.mjs +12 -0
  207. package/runtime/profile.mjs +14 -0
  208. package/runtime/profiles.json +2 -2
  209. package/runtime/render.mjs +3 -0
  210. package/runtime/route-pattern.mjs +44 -0
  211. package/runtime/rsc-worker.mjs +41 -12
  212. package/runtime/trace.mjs +78 -0
  213. package/runtime/typed-route.mjs +19 -0
  214. package/runtime/watch-project.mjs +29 -18
  215. package/runtime/worker.mjs +102 -24
package/cli.mjs CHANGED
@@ -1,23 +1,27 @@
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
 
14
+ prnext create <directory> Create a project and install dependencies
15
+ prnext create <directory> --no-install Prepare project files without installing
16
+ prnext create --help TypeScript, JavaScript and creation options
18
17
  prnext build [directory] Build React/TypeScript and static HTML
18
+ prnext build [directory] --watch Incremental production rebuilds on changes
19
19
  prnext build [directory] --adapter static [--from dist] [--spa]
20
20
  Import an existing public build directory
21
+ prnext build [directory] --adapter auto
22
+ Detect a completed browser build
23
+ prnext build [directory] --adapter static --from .
24
+ Build a plain HTML/CSS/JS site
21
25
  prnext output [directory] [--json] Read the versioned build output contract
22
26
  prnext start [directory] [options] Start native Rust production server
23
27
  prnext pstart [directory] [options] Start production persistently in the background
@@ -28,9 +32,18 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
28
32
  prnext pstartup [--remove] Configure restoration at user login
29
33
  prnext pstart --help All persistent commands and options
30
34
  prnext dev [directory] [options] Rebuild and restart on source changes
35
+ prnext dev [directory] --host-ai Share local diagnostics with your connected Host AI
36
+ prnext dev [directory] --safe Use full builds and fresh workers for troubleshooting
37
+ prnext mcp [directory] Connect an AI to the running local dev server
38
+ prnext docs --ai [topic] Read the AI guide bundled with this version
39
+ prnext docs --ai --init [directory] Add a guide pointer to the project's AGENTS.md
31
40
  prnext routes [directory] Display compiled routes
32
- prnext check [directory] [--json] Check an existing Next project before building
41
+ prnext check [directory] [--json] Diagnose config, routes, Node, native and dependencies
42
+ prnext typegen [directory] [--json] Generate route types without building the app
43
+ prnext typecheck [directory] [--json] Generate types and check TypeScript without building
33
44
  prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
45
+ prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
46
+ prnext compiler [directory] [--json] React Compiler results from the last successful build
34
47
  prnext host [prnext.host.json] Host built apps by hostname; wake on demand
35
48
  prnext host [config] --check Validate hosting configuration and builds
36
49
  prnext host [config] --status [--json] Show memory, process and idle status
@@ -47,9 +60,25 @@ Migration options: --dry-run (preview), --no-install (prepare only), --json
47
60
  Compare GET responses before switching
48
61
  Migration keeps Next.js and backs up package.json and existing lockfiles.
49
62
  Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
63
+ Installation honors npm, pnpm, Yarn Classic/Berry and the workspace root.
64
+ The preview reports the manager, root and selected workspace. Failed installs
65
+ restore the selected manifest/lockfiles; dependency caches may need a retry.
66
+
67
+ Type commands require an application tsconfig and TypeScript installation.
68
+ They generate route types unless typedRoutes: false, preserve JSONC/extends,
69
+ and report updates to tsconfig/prnext-env.d.ts. typecheck always checks errors.
70
+
71
+ Analysis options: --metric raw|gzip|brotli (default raw), --baseline analysis.json
72
+ --max-js 150 Maximum initial JS per route, in kB
73
+ --max-increase-bytes 1024 --max-increase-percent 5
74
+ --budgets budgets.json Exact route keys with maxJs (kB),
75
+ maxIncreaseBytes and/or maxIncreasePercent
76
+ Analysis exit codes: 0 passed, 1 budget exceeded or initial total unverifiable,
77
+ 2 invalid input or missing build. Request-dependent totals are lower bounds.
50
78
 
51
79
  Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
52
80
  Production profiles: --profile balanced|speed|memory|classic
81
+ Render readiness: --prewarm --readiness-file /private/path/ready.json
53
82
  Worker retirement: --worker-idle-seconds 300 --lane-idle-seconds 30 --keep-warm [true|false]
54
83
  Memory favors lower RAM under concurrency, accepting more waiting and CPU per response.
55
84
  Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
@@ -78,6 +107,42 @@ function stop(child) {
78
107
  async function main() {
79
108
  if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
80
109
  if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
110
+ if (command === 'create') {
111
+ const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
112
+ if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
113
+ const { directory, options } = parseCreateArgs(argv);
114
+ const controller = new AbortController();
115
+ let interrupted;
116
+ const interrupt = signal => { interrupted = signal; controller.abort(); };
117
+ const onInt = () => interrupt('SIGINT'), onTerm = () => interrupt('SIGTERM');
118
+ process.on('SIGINT', onInt); process.on('SIGTERM', onTerm);
119
+ try {
120
+ const report = await createProject(directory, { ...options, signal: controller.signal,
121
+ onOutput: options.json ? () => {} : (chunk, stream) => process[stream].write(chunk) });
122
+ if (options.json) console.log(JSON.stringify(report, null, 2));
123
+ else {
124
+ console.log(`PRNext project ${report.status}: ${report.root}`);
125
+ if (report.error) console.error(report.error);
126
+ if (!report.ok) console.log('Project files were preserved. Retry installation from this directory.');
127
+ if (path.resolve(process.cwd()) !== directory) console.log(`\nOpen ${report.root} in your terminal, then run:`);
128
+ for (const next of report.next) console.log(` ${next}`);
129
+ }
130
+ if (!report.ok) process.exitCode = interrupted === 'SIGINT' ? 130 : interrupted === 'SIGTERM' ? 143 : 1;
131
+ } finally { process.removeListener('SIGINT', onInt); process.removeListener('SIGTERM', onTerm); }
132
+ return;
133
+ }
134
+ if (command === 'docs') {
135
+ const { executeAiDocs } = await import('./runtime/ai-docs.mjs');
136
+ const result = await executeAiDocs(argv);
137
+ process.stdout.write(result.output);
138
+ return;
139
+ }
140
+ if (command === 'mcp') {
141
+ 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; }
142
+ if (argv.length > 1 || argv.some(arg => arg.startsWith('-'))) throw Error('mcp accepts only [directory]');
143
+ const {runDevMcp}=await import('./runtime/dev-tools-mcp.mjs');
144
+ await runDevMcp(path.resolve(argv[0] || '.'));return;
145
+ }
81
146
  if (['login', 'logout', 'whoami', 'deploy', 'update'].includes(command)) {
82
147
  const { cloudCommand } = await import('./runtime/cloud/client.mjs');
83
148
  await cloudCommand(command, argv);
@@ -150,9 +215,59 @@ async function main() {
150
215
  if (!report.ok) process.exitCode = 1;
151
216
  return;
152
217
  }
153
- if (!['build', 'start', 'dev', 'routes', 'check', 'inspect', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
218
+ if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'compiler', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
154
219
  const args = [...argv];
155
220
  const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
221
+ if (['check','typegen','typecheck','analyze','compiler'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
222
+ if(command==='compiler'){
223
+ try {
224
+ if(args.some(arg=>arg!=='--json') || args.filter(arg=>arg==='--json').length>1)throw new Error('compiler accepts only [directory] and --json');
225
+ const {readReactCompilerReport,printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
226
+ const {readBuildOutput}=await import('./build/output-contract.mjs');
227
+ const directory=path.join(root,await readBuildDirectory(root));
228
+ const output=await readBuildOutput(directory);
229
+ if(output.adapter!=='react')throw new Error('React Compiler reports apply to React builds. This application uses the static adapter.');
230
+ const report=await readReactCompilerReport(directory,{buildId:output.buildId});
231
+ if(!report)throw new Error('No React Compiler report is available. Rebuild this application with the installed PRNext version.');
232
+ if(args.includes('--json'))console.log(JSON.stringify(report,null,2));else printReactCompilerReport(report,{details:true});
233
+ } catch(error) {
234
+ if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
235
+ else console.error(`PRNext compiler: ${error.message}`);
236
+ process.exitCode=2;
237
+ }
238
+ return;
239
+ }
240
+ if(command==='analyze'){
241
+ try {
242
+ const {analyzeBuild,printAnalysis,parseAnalysisArgs}=await import('./build/analyze.mjs');
243
+ const options=parseAnalysisArgs(args),report=await analyzeBuild(root,options);
244
+ if(options.json)console.log(JSON.stringify(report,null,2));else printAnalysis(report);
245
+ if(!report.ok)process.exitCode=1;
246
+ } catch(error) {
247
+ if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
248
+ else console.error(`PRNext analysis: ${error.message}`);
249
+ process.exitCode=2;
250
+ }
251
+ return;
252
+ }
253
+ if (command === 'typegen' || command === 'typecheck') {
254
+ if (args.some(arg => arg !== '--json') || args.filter(arg => arg === '--json').length > 1) throw new Error(`${command} accepts only [directory] and --json`);
255
+ const { runTypeCommand } = await import('./build/type-command.mjs');
256
+ process.chdir(root);
257
+ process.env.INIT_CWD = root;
258
+ process.env.PWD = root;
259
+ const report = await runTypeCommand(root, { check: command === 'typecheck' });
260
+ if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
261
+ else {
262
+ console.log(`PRNext ${command} ${report.ok ? 'passed' : 'failed'}: ${root}`);
263
+ console.log(`${report.routes} page routes; tsconfig: ${report.tsconfig || 'unavailable'}; route types: ${report.typedRoutes ? 'enabled' : 'disabled'}.`);
264
+ for (const file of report.updated) console.log(`Updated ${file}`);
265
+ for (const note of report.notes) console.log(note);
266
+ for (const error of report.errors) console.error(error);
267
+ }
268
+ if (!report.ok) process.exitCode = 1;
269
+ return;
270
+ }
156
271
  if (command === 'output') {
157
272
  if (args.some(arg => arg !== '--json')) throw new Error('output accepts only [directory] and --json');
158
273
  const { readBuildOutput } = await import('./build/output-contract.mjs');
@@ -196,7 +311,10 @@ async function main() {
196
311
  else {
197
312
  console.log(`PRNext preflight ${report.ok ? 'passed' : 'failed'}: ${root}`);
198
313
  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);
314
+ for (const item of report.diagnostics) {
315
+ const output = item.severity === 'error' ? console.error : console.log;
316
+ output(`${item.severity}: ${item.message}${item.action ? ` ${item.action}` : ''}`);
317
+ }
200
318
  for (const note of report.notes) console.log(note);
201
319
  }
202
320
  if (!report.ok) process.exitCode = 1;
@@ -207,6 +325,7 @@ async function main() {
207
325
  for (let i = 0; i < args.length; i++) {
208
326
  const flag = args[i];
209
327
  if (flag === '--dev') options.dev = true;
328
+ else if (flag === '--watch') options.watch = true;
210
329
  else if (flag === '--spa') options.spa = true;
211
330
  else if (flag === '--adapter' || flag === '--from') {
212
331
  const value = args[++i];
@@ -220,131 +339,52 @@ async function main() {
220
339
  const envMode = process.env.NODE_ENV === 'test' ? 'test' : undefined;
221
340
  process.env.NODE_ENV = dev ? 'development' : 'production';
222
341
  const started = performance.now();
342
+ if(options.watch){
343
+ if(dev)throw new Error('--watch builds production; use prnext dev for development.');
344
+ const {watchProduction}=await import('./build/production-builder.mjs');
345
+ delete options.watch;await watchProduction(root,options);return;
346
+ }
347
+ if (options.adapter === undefined || options.adapter === 'auto') {
348
+ const { detectBuildAdapter } = await import('./build/static-input.mjs');
349
+ const selected = await detectBuildAdapter(root, { prepared: options.adapter === 'auto' });
350
+ if (selected.adapter === 'static') Object.assign(options, selected);
351
+ else if (options.adapter === 'auto') options.adapter = 'react';
352
+ }
223
353
  if (options.adapter === 'static') {
224
354
  const { buildStatic } = await import('./build/static-adapter.mjs');
225
355
  await buildStatic(root, options);
226
356
  } else {
227
357
  const { build } = await import('./build/index.mjs');
228
- await build(root, { ...options, dev, envMode });
358
+ const result=await build(root, { ...options, dev, envMode });
359
+ if(!dev&&result.compilationCache?.hits){
360
+ const {hits,reusedBytes}=result.compilationCache;
361
+ console.log(`Compilation cache: ${hits} ${hits===1?'graph':'graphs'} reused (${Math.ceil(reusedBytes/1024)} KiB).`);
362
+ }
363
+ if(result.reactCompilerReport?.enabled){
364
+ const {printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
365
+ printReactCompilerReport(result.reactCompilerReport,{details:false});
366
+ }
229
367
  }
230
368
  console.log(`PRNext built ${root} in ${((performance.now() - started) / 1000).toFixed(2)}s`);
231
369
  return;
232
370
  }
233
371
  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);
372
+ if(command==='dev') {const {develop}=await import('./runtime/development.mjs');await develop(root,args,binary);return;}
373
+ const outputDirectory = await readBuildDirectory(root);
374
+ const serverEnvironment = {...process.env,NODE_ENV:process.env.NODE_ENV||'production'};
375
+ if(command==='start'&&serverEnvironment.NODE_ENV!=='development'){
376
+ if(replaceWithNative(binary,['start',root,'--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),
377
+ '--node',process.execPath,...args],serverEnvironment))return;
254
378
  }
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();
379
+ const server=spawn(binary,[command==='routes'?'routes':'start',root,
380
+ ...(command==='routes'?[]:['--worker',path.join(root,outputDirectory,'runtime/worker.mjs'),'--node',process.execPath,'--shutdown-on-stdin-eof']),...args],
381
+ {stdio:['pipe','inherit','inherit'],windowsHide:true,env:serverEnvironment});
382
+ let stopping=false;
383
+ const shutdown=async()=>{if(stopping)return;stopping=true;await stop(server);};
384
+ process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
385
+ try{await done(server).catch(error=>{if(!stopping)throw error;});}
386
+ finally{process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);await stop(server);}
387
+
348
388
  }
349
389
 
350
390
  main().catch(error => { console.error(`PRNext: ${error.message}`); process.exitCode = 1; });
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ // A shared budget across layout slots, rather than a separate page budget at
4
+ // every nesting level. Only inactive branches enter this registry.
5
+ function createActivityCache({ limit = 4, ttl = 120000, now = Date.now, schedule = setTimeout, cancel = clearTimeout } = {}) {
6
+ const entries = new Map(); let timer;
7
+ function arm() {
8
+ cancel(timer); timer = undefined;
9
+ if (entries.size) {
10
+ timer = schedule(prune, Math.max(1, Math.min(...[...entries.values()].map(entry => entry.expires)) - now()));
11
+ timer?.unref?.();
12
+ }
13
+ }
14
+ function prune() {
15
+ for (const [key, entry] of entries) if (entry.expires <= now()) { entries.delete(key); entry.evict(); }
16
+ while (entries.size > limit) { const [key, entry] = entries.entries().next().value; entries.delete(key); entry.evict(); }
17
+ arm();
18
+ }
19
+ return {
20
+ retain(key, evict) { if (!entries.has(key)) entries.set(key, { evict, expires: now() + ttl }); prune(); },
21
+ release(key) { entries.delete(key); arm(); },
22
+ clear() { const previous = [...entries.values()]; entries.clear(); cancel(timer); timer = undefined; for (const entry of previous) entry.evict(); },
23
+ get size() { return entries.size; },
24
+ };
25
+ }
26
+ module.exports = { createActivityCache };
package/compat/after.cjs CHANGED
@@ -4,11 +4,11 @@ const { AsyncLocalStorage } = require('node:async_hooks');
4
4
  function after(task) {
5
5
  const context = require('./headers.cjs').currentRequest();
6
6
  if (require('./data-cache.cjs').inCacheScope()) throw new Error('after() cannot be registered inside a cache scope');
7
- if (!context.afterScope || !['route', 'action', 'after'].includes(context.phase)) throw new Error('after() currently requires a Node Route Handler or Server Action in PRNext. Server Component and Edge after() lifecycles are not implemented.');
7
+ if (!context.afterScope || !['route', 'action', 'render', 'after'].includes(context.phase)) throw new Error('after() requires a Node Server Component, Route Handler or Server Action. Edge after() is not implemented.');
8
8
  context.afterScope.add(task, context, AsyncLocalStorage.snapshot());
9
9
  }
10
10
 
11
- function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
11
+ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, onRegistered, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
12
12
  const queue = []; let count = 0, closed = false, finishing;
13
13
  const deadline = Date.now() + timeoutMs;
14
14
  const track = promise => signal?.[Symbol.for('prnext.request-work')]?.(promise, { timeoutMs: Math.max(1, deadline - Date.now()) });
@@ -18,6 +18,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
18
18
  if (closed) throw new Error('after() request has already completed');
19
19
  if (typeof task !== 'function' && (!task || typeof task.then !== 'function')) throw new TypeError('after() expects a callback or a Promise');
20
20
  if (++count > 128) throw new Error('after() exceeds 128 tasks per request');
21
+ if (count === 1) onRegistered?.();
21
22
  const pending = typeof task === 'function' ? null : observe(task);
22
23
  if (pending) track(pending);
23
24
  queue.push({ task: pending ? null : task, pending, context, run });
@@ -27,8 +28,9 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
27
28
  try {
28
29
  while (queue.length) {
29
30
  const item = queue.shift(), remaining = deadline - Date.now();
30
- if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); report(error); onTimeout(error); break; }
31
+ if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); await item.run(()=>report(error)); onTimeout(error); break; }
31
32
  const abort = new AbortController();
33
+ item.context.afterRestricted ||= item.context.phase === 'render';
32
34
  item.context.cookies._mutable = false;
33
35
  item.context.phase = 'after';
34
36
  item.context.signal = abort.signal;
@@ -38,7 +40,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
38
40
  let timer;
39
41
  try {
40
42
  const outcome = await Promise.race([work, new Promise(resolve => { timer = setTimeout(() => { const error = new Error('after() exceeded the request maxDuration'); abort.abort(error); resolve({ error }); }, remaining); })]);
41
- if ('error' in outcome) report(outcome.error);
43
+ if ('error' in outcome) await item.run(()=>report(outcome.error));
42
44
  if (abort.signal.aborted) { onTimeout(outcome.error); break; }
43
45
  } finally { clearTimeout(timer); item.context.cacheState.closed = true; await onComplete?.(item.context); }
44
46
  }
@@ -1,8 +1,53 @@
1
1
  'use client';
2
2
  'use strict';
3
3
  const React = require('react');
4
+ const { AppRouterContext } = require('./app-context.cjs');
4
5
  const LayoutContext = React.createContext({ children: [] });
5
6
  const LayoutCacheContext = React.createContext(null);
7
+ const ActivityCacheContext = React.createContext(null);
8
+
9
+ function ActivitySlot({ identity, children, authorizationKey }) {
10
+ const retention = React.useContext(ActivityCacheContext);
11
+ authorizationKey ??= retention?.authorizationKey;
12
+ const router = React.useContext(AppRouterContext);
13
+ const epoch = retention?.epoch ?? 0;
14
+ const authorizationEpoch = retention?.authorizationEpoch ?? 0;
15
+ const registrations = React.useRef(new Set());
16
+ const serial = React.useRef(0);
17
+ const [saved, setSaved] = React.useState(() => ({ epoch, authorizationEpoch, active: identity, entries: new Map() }));
18
+ let entries = saved.entries;
19
+ const current = entries.get(identity);
20
+ if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey) {
21
+ const sameAuthorization = saved.authorizationEpoch === authorizationEpoch && !(current?.authorizationKey && authorizationKey && current.authorizationKey !== authorizationKey);
22
+ entries = saved.epoch === epoch && sameAuthorization ? new Map(entries) : new Map();
23
+ const preserve = current && saved.authorizationEpoch === authorizationEpoch && (sameAuthorization && (saved.epoch === epoch || saved.active === identity)
24
+ || !retention?.strictAuthorization && saved.active === identity);
25
+ entries.set(identity, { children, router, authorizationKey, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
26
+ setSaved({ epoch, authorizationEpoch, active: identity, entries });
27
+ }
28
+ React.useLayoutEffect(() => {
29
+ const cache = retention?.cache;
30
+ if (!cache) return;
31
+ const inactive = new Set([...entries].filter(([key]) => key !== identity).map(([, entry]) => entry.token));
32
+ for (const token of registrations.current) if (!inactive.has(token)) cache.release(token);
33
+ for (const [key, entry] of entries) {
34
+ if (key === identity) cache.release(entry.token);
35
+ else cache.retain(entry.token, () => setSaved(previous => {
36
+ if (!previous.entries.has(key)) return previous;
37
+ const next = new Map(previous.entries); next.delete(key); return { ...previous, entries: next };
38
+ }));
39
+ }
40
+ registrations.current = inactive;
41
+ }, [entries, identity, retention?.cache]);
42
+ React.useLayoutEffect(() => () => {
43
+ for (const token of registrations.current) retention?.cache.release(token);
44
+ registrations.current.clear();
45
+ }, [retention?.cache]);
46
+ // Identical boundary structure during SSR and hydration. Hidden branches get
47
+ // their original router context and their effects are disconnected by React.
48
+ return [...entries].map(([key, entry]) => React.createElement(React.Activity, { key: entry.reactKey, mode: key === identity ? 'visible' : 'hidden' },
49
+ React.createElement(AppRouterContext.Provider, { value: key === identity ? router : entry.router }, entry.children)));
50
+ }
6
51
 
7
52
  function LayoutProvider({ id, segments, children }) {
8
53
  const cache = React.useContext(LayoutCacheContext);
@@ -13,15 +58,20 @@ function LayoutProvider({ id, segments, children }) {
13
58
  return React.createElement(LayoutContext.Provider, { value: current }, children);
14
59
  }
15
60
 
16
- function LayoutSlot({ id, preserve, children }) {
61
+ function LayoutSlot({ id, preserve, children, activityKey, activityPreserve, authorizationKey }) {
17
62
  const cache = React.useContext(LayoutCacheContext);
18
63
  const previous = React.useRef(children);
19
64
  const current = preserve && cache && Object.hasOwn(cache.slots, id) ? cache.slots[id] : preserve ? previous.current : children;
20
65
  React.useLayoutEffect(() => { previous.current = current; if (cache && id) cache.slots[id] = current; }, [cache, id, current]);
21
- return current ?? null;
66
+ const previousKey = React.useRef(activityKey);
67
+ const identity = preserve || activityPreserve ? previousKey.current : activityKey;
68
+ React.useLayoutEffect(() => { previousKey.current = identity; }, [identity]);
69
+ return activityKey === undefined ? current ?? null : React.createElement(ActivitySlot, { identity, authorizationKey }, current);
22
70
  }
23
71
 
24
72
  module.exports.LayoutContext = LayoutContext;
25
73
  module.exports.LayoutCacheContext = LayoutCacheContext;
26
74
  module.exports.LayoutProvider = LayoutProvider;
27
75
  module.exports.LayoutSlot = LayoutSlot;
76
+ module.exports.ActivitySlot = ActivitySlot;
77
+ module.exports.ActivityCacheContext = ActivityCacheContext;
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+ // Native decoding avoids a JavaScript callback for each Flight byte. The loop
3
+ // remains available on browsers that do not implement Uint8Array.fromBase64.
4
+ function decodeBase64(value) {
5
+ if(typeof Uint8Array.fromBase64==='function')return Uint8Array.fromBase64(value);
6
+ const binary=atob(value),bytes=new Uint8Array(binary.length);
7
+ for(let n=0;n<binary.length;n++)bytes[n]=binary.charCodeAt(n);
8
+ return bytes;
9
+ }
10
+ module.exports={decodeBase64};
@@ -139,10 +139,19 @@ async function customCachedValue(key, kind, producer, options) {
139
139
  const timestamp = Date.now();
140
140
  const record = { revision, timestamp, work: undefined, metadata: undefined };
141
141
  work = (async () => {
142
+ let lease,canStore=true;
143
+ try {
144
+ if(typeof handler.acquire==='function'){
145
+ try{
146
+ lease=await call(handler,'acquire',[key,{signal:context.signal}],context);
147
+ const shared=await call(handler,'get',[key,tags],context);
148
+ if(shared){const sharedInfo=metadata(shared);if(Date.now()-sharedInfo.timestamp<Math.min(sharedInfo.expire,sharedInfo.revalidate)*1000){record.metadata=sharedInfo;options.onReadMetadata?.(sharedInfo);return await read(shared,context);}void shared.value.cancel().catch(()=>{});}
149
+ }catch{context.signal?.throwIfAborted();canStore=false;}
150
+ }
142
151
  const result = await producer();
143
152
  const info = resolveMetadata();
144
153
  record.metadata = info;
145
- if (result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
154
+ if (canStore && result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
146
155
  try {
147
156
  const changed = await expiration(handler, [...new Set([...tags, ...info.tags])], context);
148
157
  // Many backends record Date.now() millisecond timestamps. Treat the
@@ -150,12 +159,13 @@ async function customCachedValue(key, kind, producer, options) {
150
159
  // hide an invalidation which actually happened after the read started.
151
160
  if ((changed === Infinity || changed < Math.floor(timestamp)) && revision === (revisions.get(handler) || 0)) {
152
161
  const value = stream(result);
153
- try { await call(handler, 'set', [key, Promise.resolve({ value, tags: info.tags, stale: info.stale, revalidate: info.revalidate, expire: info.expire, timestamp })], context); }
162
+ try { await call(handler, 'set', [key, Promise.resolve({ value, tags: info.tags, stale: info.stale, revalidate: info.revalidate, expire: info.expire, timestamp }),{lease,softTags:tags}], context); }
154
163
  catch { if (!value.locked) void value.cancel().catch(() => {}); }
155
164
  }
156
165
  } catch { context.signal?.throwIfAborted(); }
157
166
  }
158
167
  return result;
168
+ }finally{try{await lease?.release();}catch{/* A failed cache cleanup does not replay application work. */}}
159
169
  })();
160
170
  record.work = work;
161
171
  if (pendingCount < 64) {
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+ // Versioned private framing: bounded JSON metadata followed by uninterpreted bytes.
3
+ const TYPE = 'application/vnd.prnext.cache-v2';
4
+ const MAX_VALUE = 2 * 1024 * 1024;
5
+ const MAX_META = 2 * 1024 * 1024;
6
+ function encode(operation) {
7
+ const { value, ...metadata } = operation;
8
+ if (value !== undefined && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) throw new TypeError('Cache wire values must be bytes');
9
+ const bytes = value === undefined ? undefined : Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
10
+ if (bytes && (metadata.op !== 'commit' || bytes.length > MAX_VALUE)) throw new Error('Invalid cache wire value');
11
+ if (metadata.op === 'commit' && !bytes) throw new Error('Cache commit requires bytes');
12
+ const json = Buffer.from(JSON.stringify(metadata));
13
+ if (json.length > MAX_META) throw new Error('Cache wire metadata exceeds limit');
14
+ const prefix = Buffer.allocUnsafe(4); prefix.writeUInt32BE(json.length);
15
+ return bytes ? [prefix, json, bytes] : [prefix, json];
16
+ }
17
+ function decode(bytes) {
18
+ if (bytes.length < 4) throw new Error('Truncated cache wire frame');
19
+ const length = bytes.readUInt32BE(0);
20
+ if (length > MAX_META || length > bytes.length - 4) throw new Error('Invalid cache wire metadata length');
21
+ const value = JSON.parse(bytes.subarray(4, 4 + length).toString('utf8'));
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid cache wire metadata');
23
+ const raw = bytes.subarray(4 + length);
24
+ if (raw.length > MAX_VALUE || (value.value !== true && raw.length) || (value.value !== undefined && value.value !== true)) throw new Error('Invalid cache wire payload');
25
+ if (value.value === true) value.value = raw;
26
+ return value;
27
+ }
28
+ module.exports = { TYPE, MAX_VALUE, MAX_META, encode, decode };
package/compat/cache.cjs CHANGED
@@ -96,4 +96,5 @@ function unstable_noStore() {
96
96
  }
97
97
  function refresh() { cacheState(mutationContext('refresh', true)).refresh = true; }
98
98
 
99
- module.exports = { unstable_cache, revalidateTag, updateTag, revalidatePath, unstable_noStore, refresh, cacheTag, cacheLife };
99
+ const { navigation, prefetch } = require('./navigation-stages.cjs');
100
+ module.exports = { unstable_cache, revalidateTag, updateTag, revalidatePath, unstable_noStore, refresh, cacheTag, cacheLife, navigation, prefetch };
@@ -34,3 +34,7 @@ export function updateTag(tag: string): void;
34
34
  export function revalidatePath(path: string, type?: 'page' | 'layout'): void;
35
35
  export function unstable_noStore(): void;
36
36
  export function refresh(): void;
37
+ /** Defer runtime work until navigation, while allowing static prerenders. */
38
+ export function navigation(): Promise<void>;
39
+ /** Defer runtime work until a per-link prefetch or navigation. */
40
+ export function prefetch(): Promise<void>;