@thomas.f/prnext 0.1.19-alpha → 0.1.20-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 (85) hide show
  1. package/build/app.mjs +46 -78
  2. package/build/build-lock.mjs +2 -71
  3. package/build/build-retention.mjs +119 -0
  4. package/build/bundle-runtime.mjs +9 -8
  5. package/build/cache-command.mjs +31 -0
  6. package/build/check.mjs +41 -3
  7. package/build/compilation-cache.mjs +32 -12
  8. package/build/compilation-storage.mjs +190 -0
  9. package/build/compiler.mjs +3 -3
  10. package/build/config.mjs +23 -2
  11. package/build/create.mjs +1 -1
  12. package/build/css-modules.mjs +4 -0
  13. package/build/dev-action-graph.mjs +21 -0
  14. package/build/font-cache.mjs +9 -6
  15. package/build/font-files.mjs +1 -1
  16. package/build/index.mjs +30 -13
  17. package/build/native-ppr.mjs +32 -0
  18. package/build/og.mjs +121 -1
  19. package/build/parallel-plan.mjs +55 -0
  20. package/build/persistent-cache.mjs +29 -24
  21. package/build/production-check.mjs +34 -0
  22. package/build/react-compiler-report.mjs +31 -8
  23. package/build/react-compiler.mjs +101 -10
  24. package/build/server-config.mjs +35 -0
  25. package/build/source-analysis.mjs +54 -4
  26. package/build/static-adapter.mjs +7 -3
  27. package/build/transform.mjs +38 -6
  28. package/build/typescript-dev.mjs +3 -1
  29. package/cli.mjs +14 -5
  30. package/compat/app-layout-context.cjs +7 -3
  31. package/compat/flight-delta.cjs +47 -12
  32. package/compat/index.d.ts +24 -1
  33. package/compat/navigation-authorization.cjs +8 -1
  34. package/docs/ai/compatibility.md +110 -7
  35. package/docs/ai/deployment.md +141 -0
  36. package/docs/ai/quickstart.md +5 -0
  37. package/package.json +12 -9
  38. package/release/fontkit.mjs +31 -0
  39. package/release/verify.mjs +3 -0
  40. package/runtime/api.mjs +5 -2
  41. package/runtime/app-client.mjs +150 -26
  42. package/runtime/app-partial.mjs +2 -10
  43. package/runtime/app-prefetch.mjs +2 -2
  44. package/runtime/app-render.mjs +11 -7
  45. package/runtime/app-stream-channel.mjs +3 -2
  46. package/runtime/app-styles.mjs +22 -4
  47. package/runtime/compile-cache-retention.mjs +53 -0
  48. package/runtime/compile-cache.mjs +28 -1
  49. package/runtime/dev-client.mjs +6 -3
  50. package/runtime/dev-gateway.mjs +3 -2
  51. package/runtime/dev-route-selection.mjs +64 -0
  52. package/runtime/dev-runtime-pool.mjs +3 -2
  53. package/runtime/dev-runtime-spare.mjs +1 -1
  54. package/runtime/dev-session-retention.mjs +154 -0
  55. package/runtime/development.mjs +27 -8
  56. package/runtime/env.mjs +9 -0
  57. package/runtime/flight-delta.mjs +31 -5
  58. package/runtime/native-ppr.mjs +35 -0
  59. package/runtime/og/NOTICE.txt +1 -1
  60. package/runtime/og/index.node.mjs +76 -31216
  61. package/runtime/og/upstream.json +115 -0
  62. package/runtime/persistent/client.mjs +4 -2
  63. package/runtime/persistent/daemon.mjs +3 -11
  64. package/runtime/persistent/health.mjs +58 -0
  65. package/runtime/project-lock.mjs +71 -0
  66. package/runtime/request-transport.mjs +112 -32
  67. package/runtime/storage-path.mjs +36 -0
  68. package/runtime/trace.mjs +44 -4
  69. package/runtime/worker.mjs +9 -4
  70. package/vendor/fontkit/NOTICE.txt +5 -0
  71. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  72. package/vendor/fontkit/index.mjs +3 -0
  73. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  74. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  75. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  76. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  77. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  78. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  79. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  80. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  81. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  82. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  83. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  84. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  85. package/vendor/fontkit/upstream.json +215 -0
package/compat/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface InstantSample {
14
14
  export type InstantConfig = boolean | { level?: 'warning' | 'experimental-error'; unstable_samples?: InstantSample[]; unstable_disableValidation?: true; unstable_disableDevValidation?: true; unstable_disableBuildValidation?: true };
15
15
 
16
16
  export interface ReactCompilerOptions {
17
+ /** Babel by default. The native Oxc backend is experimental and opt-in. */
18
+ backend?: 'babel' | 'oxc';
17
19
  compilationMode?: 'infer' | 'annotation';
18
20
  panicThreshold?: 'none' | 'critical_errors' | 'all_errors';
19
21
  /** PRNext requires matching React 19 peers. */
@@ -121,6 +123,8 @@ export interface Rewrite extends CustomRoute { destination: string }
121
123
  export type ConfigRedirect = CustomRoute & Redirect;
122
124
  /** Implemented project options. Unsupported behavior-changing options fail the build. */
123
125
  export interface NextConfig {
126
+ /** Public origin and transport peers permitted to set X-Forwarded-Host/Proto. */
127
+ server?: {publicOrigin?: string; trustedProxies?: readonly string[]};
124
128
  /** PRNext only. CLI > environment > build configuration > profile.
125
129
  * 0 disables the corresponding retirement; keepWarm retains one lane per used host.
126
130
  */
@@ -131,6 +135,10 @@ export interface NextConfig {
131
135
  workerPreload?: false | { routes: string[]; maxRssMb?: number; timeoutMs?: number };
132
136
  /** Keep bounded previous public hashed assets. false disables carry-over. Defaults: one hour, two generations, 64 MiB. */
133
137
  deploymentRetention?: false | {maxAge?: number; maxGenerations?: number; maxBytes?: number};
138
+ /** Common disk budget for regenerable compiler caches, in bytes (default 256 MiB).
139
+ * Applied after builds; active dev sessions/workers can defer reclamation.
140
+ * Runtime data, invalidations, page and image caches are excluded. */
141
+ compilationCache?: {maxDiskSize?: number};
134
142
  /** PRNext production prerender processes; maxMemoryMb bounds their combined observed memory. */
135
143
  prerender?: {workers?: number; maxMemoryMb?: number};
136
144
  /** Explicit App route patterns that must produce complete static HTML without browser JavaScript. */
@@ -167,7 +175,20 @@ export interface NextConfig {
167
175
  implementation?: 'sass' | 'sass-embedded';
168
176
  [option: string]: unknown;
169
177
  };
170
- experimental?: { nextScriptWorkers?: boolean; /** App Router development only; compile literal client imports on first use. */ lazyDynamicImports?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
178
+ experimental?: {
179
+ nextScriptWorkers?: boolean;
180
+ /** App Router development only; compile literal client imports on first use. */
181
+ lazyDynamicImports?: boolean;
182
+ /** Start App Route Handlers before upload EOF; mutations must handle aborted uploads. */
183
+ streamingRequestBodies?: boolean;
184
+ /** Serve certified public PPR document shells before starting Node. Requires cacheComponents. */
185
+ nativePprShell?: boolean;
186
+ /** Bound independent production CSS builds after graph discovery. Disabled by default. */
187
+ parallelBuilds?: boolean | {maxWorkers?: number; maxMemoryMb?: number};
188
+ optimizePackageImports?: string[];
189
+ proxyClientMaxBodySize?: number | string;
190
+ serverActions?: true | {allowedOrigins?: string[]; bodySizeLimit?: number | string};
191
+ };
171
192
  trailingSlash?: boolean;
172
193
  skipTrailingSlashRedirect?: boolean;
173
194
  skipMiddlewareUrlNormalize?: boolean;
@@ -216,6 +237,7 @@ export interface StaticBuildOptions {
216
237
  }
217
238
  export interface ReactCompilerReport {
218
239
  version: 1;
240
+ backend?: 'babel' | 'oxc';
219
241
  compilerVersion: string | null;
220
242
  enabled: boolean;
221
243
  buildId: string | null;
@@ -227,6 +249,7 @@ export interface ReactCompilerReport {
227
249
  summary: {files: number; functions: number; optimized: number; skipped: number; errors: number; diagnostics: number; mixed: number; transforms: number; cacheReuses: number};
228
250
  files: Array<{
229
251
  file: string; transforms: number; cacheReuses: number; targets: string[]; notes: string[];
252
+ diagnostics?: Array<{kind: 'CompileError' | 'CompileDiagnostic'; status: 'error' | 'diagnostic'; reason: string | null; category: string | null; targets: Array<'browser' | 'node' | 'edge' | 'unspecified'>}>;
230
253
  functions: Array<{
231
254
  name: string | null;
232
255
  start: {line: number; column: number} | null;
@@ -1,9 +1,16 @@
1
1
  'use strict';
2
2
  const { createHmac } = require('node:crypto');
3
+ function developmentNavigationSecret(actionKey) {
4
+ if (typeof actionKey !== 'string' || !actionKey) return undefined;
5
+ // The compiler retains this private key across generations of one dev
6
+ // session. Derive a separate purpose key so worker replacement cannot look
7
+ // like an account change, without publishing or reusing the encryption key.
8
+ return createHmac('sha256', actionKey).update('prnext:development:navigation-authorization:v1').digest('hex');
9
+ }
3
10
  function navigationAuthorization(context, secret, cookieNames) {
4
11
  if (!secret || context?.staticState) return undefined;
5
12
  const names = cookieNames === undefined ? null : new Set(cookieNames);
6
13
  const cookies = context.cookies.getAll().filter(cookie => !names || names.has(cookie.name)).map(({ name, value }) => [name, value]).sort((a, b) => a[0].localeCompare(b[0]));
7
14
  return createHmac('sha256', secret).update(JSON.stringify([cookies, context.headers.get('authorization') || ''])).digest('hex');
8
15
  }
9
- module.exports = { navigationAuthorization };
16
+ module.exports = { navigationAuthorization, developmentNavigationSecret };
@@ -18,6 +18,13 @@ transpilation, image/font tooling and caching deserve targeted build and browser
18
18
  tests. Support for one configuration shape does not imply support for all
19
19
  upstream plugin behavior.
20
20
 
21
+ When moving from 0.1.19-alpha, use the installed
22
+ [upgrade checklist](deployment.md#upgrading-from-0119-alpha): rebuild matching
23
+ artifacts, configure the public HTTPS origin and proxy trust, verify strict
24
+ account transitions and review saved persistent health routes. Storage cleanup
25
+ must preserve runtime data and invalidation history. Experimental performance
26
+ options remain independent and opt-in.
27
+
21
28
  ## Instrumentation and tracing
22
29
 
23
30
  Use a single `instrumentation.ts`, `.js`, `.mjs` or `.cjs` at the project root
@@ -132,6 +139,66 @@ new source files and configuration changes restart the native watch generation.
132
139
  adding its generated declaration to the config. Compiler support does not imply
133
140
  that every application is ready for TypeScript 7's own language/config changes.
134
141
 
142
+ ## Experimental performance paths
143
+
144
+ These options are independent and disabled by default. Enable them in the
145
+ application's existing configuration, then qualify its actual workflows:
146
+
147
+ ```js
148
+ export default {
149
+ cacheComponents: true,
150
+ experimental: {
151
+ nativePprShell: true,
152
+ streamingRequestBodies: true,
153
+ parallelBuilds: {maxWorkers: 4, maxMemoryMb: 1024},
154
+ },
155
+ };
156
+ ```
157
+
158
+ `nativePprShell` lets Rust send the exact certified build shell before starting
159
+ Node, then resume its React continuation. The initial implementation covers
160
+ concrete App PPR document GETs without query strings, cookies, authorization,
161
+ middleware, rewrites, base paths, router-state/Flight/prefetch headers or CSP
162
+ nonces. Generic parameter shells, Edge, external caches and unsupported requests
163
+ keep the ordinary Node path. Prefixes and artifacts have checked hashes and
164
+ bounded sizes; continuations use a private transport field, never an HTTP flag.
165
+ Responses remain private and no-store. The cache generation must still be zero
166
+ and the build artifact unexpired: any persisted invalidation disables this build
167
+ snapshot optimization, including across server restarts. It does not disable
168
+ PPR itself or invalidate an already committed response. Requests already using
169
+ a snapshot finish with that same artifact when an invalidation races them.
170
+
171
+ `streamingRequestBodies` makes `Request.body` available to Node App Route
172
+ Handlers before upload EOF. It negotiates its own protocol capability and sends
173
+ at most one 64 KiB frame per reader credit. Unsupported workers use the existing
174
+ buffered path. Server Actions, Pages APIs and middleware-buffered requests retain
175
+ their previous buffering behavior. Streaming handlers can begin application
176
+ effects before a truncated or excessive upload is rejected: validate the input
177
+ and complete authorization before committing a mutation. The total request limit
178
+ remains 32 MiB. Existing ingress byte reservations are preserved; this feature
179
+ does not promise a smaller V8 heap or higher memory admission concurrency. A
180
+ cancelled or early-returned upload closes its single-use lane without replaying
181
+ the mutation, while retaining the healthy Node host.
182
+
183
+ `parallelBuilds` freezes App stylesheet plans after dependency discovery,
184
+ deduplicates outputs and emits independent jobs with deterministic linking.
185
+ Graph discovery itself remains ordered. Concurrency is bounded by CPU count,
186
+ available memory and the configured worker/budget limits. `maxWorkers` accepts
187
+ 1–8; `maxMemoryMb` accepts 256–8192 and is an admission estimate, not a hard RSS
188
+ limit for third-party code. Custom webpack/loaders, custom PostCSS pipelines and
189
+ Sass callbacks retain serial execution. A failed job stops new admission and
190
+ settles started jobs before the staging build is discarded; the last published
191
+ build is preserved. An enabled build records its actual stylesheet job count
192
+ and selected concurrency in `manifest.buildParallelism`.
193
+
194
+ Development refreshes can also target the routes that import a changed Server
195
+ Action or its transitive dependencies. Certification uses both dependency
196
+ graphs before and after a change. An unrelated tab still applies a deferred
197
+ generation before navigating to a changed route. Unknown or incomplete graphs,
198
+ binding/runtime/route changes, global configuration, rewrites/middleware,
199
+ internationalized or parallel/intercepting routing retain a global refresh.
200
+ Server generations still publish as a whole; pending mutations are never replayed.
201
+
135
202
  ## React Compiler
136
203
 
137
204
  Enable the bundled official React Compiler in the project's existing config:
@@ -142,7 +209,7 @@ export default {
142
209
  };
143
210
  ```
144
211
 
145
- No separate compiler installation is required. The default is disabled;
212
+ The default Babel backend needs no separate compiler installation. The default is disabled;
146
213
  `reactCompiler: false` preserves the uncompiled component path. Enabled builds
147
214
  optimize project-owned browser components in both Pages and App applications,
148
215
  in development and production. App Client Components also use the transform in
@@ -153,17 +220,43 @@ Components, Server Action modules, PRNext internals, generated entrypoints and
153
220
  npm packages are not compiled. Keep the matching React 19 peer versions required
154
221
  by the installed framework.
155
222
 
156
- Ordinary `.mts`/`.cts` hook helpers can be optimized; server/client directives
157
- in those extensions remain unsupported. Decorated `.mts`/`.cts` utility modules
223
+ With the default Babel backend, ordinary `.mts`/`.cts` hook helpers can be
224
+ optimized; server/client directives in those extensions remain unsupported.
225
+ Decorated `.mts`/`.cts` utility modules
158
226
  keep their native compilation path while importing React components can still
159
227
  be optimized.
160
228
 
161
229
  For gradual adoption, use `reactCompiler: { compilationMode: 'annotation' }`, then
162
230
  put `"use memo";` at the beginning of each component or hook to optimize. In
163
231
  either mode, `"use no memo";` excludes an individual function. Supported object
164
- options are `compilationMode: 'infer' | 'annotation'`,
232
+ options are `backend: 'babel' | 'oxc'`, `compilationMode: 'infer' | 'annotation'`,
165
233
  `panicThreshold: 'none' | 'critical_errors' | 'all_errors'` and `target: '19'`.
166
- Defaults are `'infer'`, `'none'` and `'19'`; other fields are rejected.
234
+ Defaults are Babel, `'infer'`, `'none'` and `'19'`; other fields are rejected.
235
+
236
+ The experimental native Rust backend is an explicit alternative:
237
+
238
+ ```sh
239
+ npm install --save-dev oxc-transform-react@0.157.0
240
+ ```
241
+
242
+ ```js
243
+ export default {
244
+ reactCompiler: { backend: 'oxc' },
245
+ };
246
+ ```
247
+
248
+ Oxc is an optional peer, so ordinary Babel builds do not install its compiler,
249
+ WASM distribution or native binding. Run the installation above in the application
250
+ project on the build machine, with optional platform dependencies enabled. A
251
+ global CLI also resolves this project-local peer. This option
252
+ requires the native binding; an absent or incompatible compiler, missing binding
253
+ or compilation failure fails explicitly. PRNext never silently changes the backend. JSX remains available
254
+ to the existing graph compiler, while this backend removes TypeScript syntax.
255
+ App Client Component server rendering uses Oxc's SSR output; browser graphs use
256
+ its client output. The same project boundaries and Server Action exclusions
257
+ apply. Source maps, loader maps and the existing Fast Refresh pipeline remain
258
+ part of the compilation path. Unsupported options are rejected rather than
259
+ passed through to the native compiler.
167
260
 
168
261
  After a successful build, inspect the compiler's actual results:
169
262
 
@@ -172,8 +265,8 @@ prnext compiler
172
265
  prnext compiler --json
173
266
  ```
174
267
 
175
- The report names observed functions, their project-relative files, optimization
176
- or skip status, and the compiler's reasons. These are transform decisions;
268
+ For Babel, the report names observed functions, their project-relative files,
269
+ optimization or skip status, and the compiler's reasons. These are transform decisions;
177
270
  server-code removal and tree shaking can later omit a function from the browser
178
271
  bundle. Cached transforms retain their
179
272
  diagnostics and are identified as reused. Browser and server graphs do not
@@ -183,6 +276,12 @@ complete application coverage. Disabled compilation is reported explicitly.
183
276
  The report describes the last successful build, stays outside public assets and
184
277
  does not include source code. A failed build preserves the previous report.
185
278
 
279
+ The Oxc report identifies the backend and package version, records native
280
+ diagnostics at file scope, and states that function optimization totals are
281
+ unavailable: its API does not expose Babel's function-success events. This is
282
+ not a count of zero optimized functions. Cached native diagnostics are replayed;
283
+ cache identities include the backend, compiler version and client/SSR mode.
284
+
186
285
  The compiler can skip code that cannot be safely optimized. Follow the Rules of
187
286
  React and check actual behavior rather than assuming every component was
188
287
  compiled. It reduces repeated component work; it does not guarantee faster
@@ -192,6 +291,10 @@ production build with the option enabled and disabled. Compilation adds build
192
291
  work; PRNext caches pure transforms with their compiler version and settings.
193
292
 
194
293
  See the [official React Compiler documentation](https://react.dev/learn/react-compiler).
294
+ The [Oxc React Compiler guide](https://oxc.rs/docs/guide/usage/transformer/react-compiler.html)
295
+ describes the experimental backend. Measure compilation and application behavior
296
+ with the selected backend before adopting it; selecting Rust alone is not a
297
+ measured speedup for a project.
195
298
 
196
299
  Only explicit private Next.js import adapters are supported. Do not add arbitrary
197
300
  `next/dist/...` imports or install another Next runtime to satisfy one: isolate
@@ -8,12 +8,90 @@ Build with the installed PRNext and run the resulting trusted output with its
8
8
  matching runtime:
9
9
 
10
10
  ```sh
11
+ prnext check --production
11
12
  prnext typecheck
12
13
  prnext build
13
14
  prnext output --json
14
15
  prnext start --hostname 127.0.0.1 --port 3000
15
16
  ```
16
17
 
18
+ ### Upgrading from 0.1.19-alpha
19
+
20
+ Install matching framework/native versions, then rebuild and restart the complete
21
+ application or standalone output. Keep Node and the React protocol peers aligned
22
+ with this package's metadata. Existing compiler entries are reused only when
23
+ their identities match; never delete `data.sqlite3` or invalidation history to
24
+ perform an upgrade. Read the storage section below before pruning compiler caches.
25
+
26
+ Persistent apps need a supervisor restart too. In a maintenance window for the
27
+ whole `PRNEXT_PM_HOME`, run `prnext pdown` with that same home before replacing
28
+ packages. It stops all managed apps and preserves saved settings and enabled
29
+ entries; `prestart` alone keeps the already loaded supervisor code. Update and
30
+ rebuild the installations that must return with 0.1.20, then launch `pstart` or
31
+ `prestart` through the new CLI. It also restores all enabled entries, with their
32
+ saved binary paths and health routes. Check `pstatus` and real health responses;
33
+ a global CLI update does not replace each app's package or binary. Keep independent
34
+ lifecycles in separate homes.
35
+
36
+ Set `server.publicOrigin` to the external HTTPS origin and `server.trustedProxies`
37
+ to the immediate proxy's IP/CIDR addresses. The configured origin wins; forwarded
38
+ host/protocol headers are trusted only from those peers. Rebuild after config
39
+ changes and check redirects, secure cookies and authentication callbacks over
40
+ the actual public URL. This does not establish a forwarded client-IP chain.
41
+
42
+ Run production preflight, then test account A → sign out → account B with the
43
+ actual `navigationCache.authCookies`, including shared layouts and browser
44
+ history. Changed or unknown strict identities require a fresh document; cached
45
+ PPR previews wait for fresh identity. Authorize protected reads and writes in
46
+ the application as before.
47
+
48
+ Use a dedicated complete HTTP 200 application health response for database/service
49
+ readiness. `/_prnext/health` checks native readiness only; redirects no longer
50
+ pass persistent health checks. Previously saved applications keep their health
51
+ path, so a redirecting `/` must be replaced in the saved persistent settings.
52
+ Observe the post-switch rollback window and exercise an explicit idempotent
53
+ retry; a lost response can follow a successful write, and mutations are never
54
+ automatically replayed. Keep schema changes compatible with the previous server.
55
+
56
+ ### Production preflight
57
+
58
+ Run `prnext check --production --json` to collect deployment diagnostics without
59
+ building or rewriting the application. It evaluates the production config and
60
+ environment files, restores the calling process environment afterward, and checks
61
+ installed dependencies, route conventions, the public origin, proxy trust,
62
+ authentication retention settings and active experimental options. JSON diagnostics
63
+ include `code`, `path`, `severity` and an action where one is needed. Errors fail
64
+ the command; warnings require review and do not change its exit status.
65
+
66
+ If a build already exists, preflight also compares its public origin, proxy and
67
+ authentication retention settings with the current config and rejects a development artifact. Rebuild after
68
+ changing deployment settings: `prnext start` reads the selected artifact and does
69
+ not reevaluate the source configuration. Missing output is informational before
70
+ the first build; unreadable existing metadata requires review.
71
+
72
+ For authenticated App Router applications, explicitly list every session cookie
73
+ name, including HTTPS variants used by the authentication library:
74
+
75
+ ```js
76
+ export default {
77
+ navigationCache: {
78
+ authCookies: ['session-cookie-name', '__Secure-session-cookie-name'],
79
+ },
80
+ };
81
+ ```
82
+
83
+ Replace these example names with the application's actual cookies; PRNext does
84
+ not infer authentication from cookie names. This enables strict retained-page
85
+ state resets when the server's authorization identity changes. Test account A →
86
+ sign out → account B with both active and retained pages, and confirm that private
87
+ drafts disappear. Public applications can review the guidance or disable page
88
+ retention with `navigationCache: false`.
89
+
90
+ A successful preflight checks configuration, not external services or application
91
+ authorization. Qualify the final build over public HTTPS, including authentication,
92
+ health checks, failure recovery and rollback. Experimental warnings identify the
93
+ options that need workload-specific validation; they are not performance claims.
94
+
17
95
  The default build directory is `.prnext`; `distDir` can change it. The private
18
96
  project output selector is `.prnext-output.json`. Do not assume `.next` layout,
19
97
  run `next start` on a PRNext build or hand-edit its manifests to bypass runtime
@@ -254,6 +332,69 @@ an explicit choice, not a reason to print credentials or copy `.env` into assets
254
332
 
255
333
  ## Observe before tuning
256
334
 
335
+ ### Installation and disk storage
336
+
337
+ The default installation includes Babel React Compiler support. The Oxc React
338
+ Compiler backend is an optional peer installed explicitly; read the
339
+ [compatibility guide](compatibility.md) before selecting it. Keep optional native
340
+ platform dependencies enabled. The framework's internal Fontkit bundle includes
341
+ its provenance and dependency licenses under `vendor/fontkit`; applications do
342
+ not need a separate Fontkit dependency for `next/font`.
343
+
344
+ Production builds retain one OpenGraph renderer when needed. A bounded analysis
345
+ of the compiled server graph may omit it only when the application can be
346
+ certified not to use `ImageResponse`. Development, Edge, unknown external modules
347
+ and computed loading keep it. No configuration change is needed. Deploy the
348
+ standalone output alone when using `output: 'standalone'`; copying the complete
349
+ ordinary build beside it duplicates those artifacts.
350
+
351
+ Compiler caches share a **256 MiB** retention budget per project:
352
+
353
+ ```js
354
+ export default {
355
+ compilationCache: {maxDiskSize: 128 * 1024 * 1024}, // bytes, 0–4 GiB
356
+ };
357
+ ```
358
+
359
+ PRNext applies the policy after a build, including failures, and after a clean
360
+ development shutdown. It is a retention target, not a hard filesystem quota:
361
+ active development, pinned V8 workers, busy files or an incomplete bounded scan
362
+ can defer reclamation. Existing per-cache limits still apply. Eviction can make
363
+ the next build slower; evicted downloaded fonts need network access again.
364
+ Large JSON entries use bounded, checksummed compression; old raw records remain
365
+ readable, and corrupt/incompatible records are cache misses.
366
+
367
+ Inspect and preview changes without executing application configuration:
368
+
369
+ ```sh
370
+ prnext cache --json
371
+ prnext cache --prune --dry-run --max-bytes 67108864 --json
372
+ prnext cache --prune --max-bytes 67108864
373
+ ```
374
+
375
+ The CLI reads the policy saved by the last build. `--max-bytes` overrides only
376
+ that purge; `0` removes eligible compiler records. Statistics cover recognized
377
+ framework cache files, excluding unknown paths; `scanComplete: false` means a
378
+ lower bound and prevents purging from an incomplete snapshot. Stop development
379
+ before purging. Concurrent builds are protected by the publisher lock, active
380
+ managed workers pin their namespace, and failed deletions can be retried.
381
+
382
+ These commands never remove runtime data, `data.sqlite3`, page/image indexes,
383
+ invalidation generations, tags or fill leases. Never delete/reset the data
384
+ database to reclaim compiler storage: that can lose invalidation history. The
385
+ runtime instead performs small incremental SQLite reclamation steps and bounded
386
+ orphan-file maintenance on its own timer. Existing SQLite databases without
387
+ incremental auto-vacuum retain their existing format; no full VACUUM runs on a
388
+ request. Older page files without a verifiable reader lease are retained.
389
+
390
+ Builds and dev sessions also journal their owned temporary directories. After a
391
+ crash, later starts reclaim eligible entries only after a grace period and proof
392
+ that their local owner/workers have exited. Unknown directories, foreign-host or
393
+ unverifiable owners and uncommitted build backups are kept for recovery. Do not
394
+ remove a recovery backup until the selected build has been checked.
395
+
396
+ ### Runtime observations
397
+
257
398
  Production rendering, API and regeneration admission use bounded queues grouped
258
399
  by resolved route pattern. Different parameter values share one pattern's
259
400
  waiting budget. Waiting patterns take turns as capacity becomes available;
@@ -13,6 +13,11 @@ react-server-dom-webpack 19.3.1**. Its Node engine range is
13
13
  `^22.23.4 || ^24.22.1 || >=26.12.1`. Check the installed package manifest when
14
14
  upgrading; these requirements may change between releases.
15
15
 
16
+ Optional peers are installed only for the features that use them:
17
+ `@redis/client` for [shared Redis caching](data.md), and `oxc-transform-react`
18
+ for the [experimental Oxc React Compiler](compatibility.md#react-compiler).
19
+ The default Babel compiler needs no additional compiler installation.
20
+
16
21
  Published releases include a matching native component through optional
17
22
  dependencies for Windows, macOS and glibc Linux, on x64 and ARM64. Keep optional
18
23
  dependencies enabled. Consumers do not need Rust; building PRNext from its source
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thomas.f/prnext",
3
- "version": "0.1.19-alpha",
3
+ "version": "0.1.20-alpha",
4
4
  "license": "MIT",
5
5
  "description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
6
6
  "homepage": "https://prnext.dev",
@@ -146,7 +146,6 @@
146
146
  "dotenv-expand": "11.0.7",
147
147
  "esbuild": "^0.28.2",
148
148
  "estree-walker": "2.0.2",
149
- "fontkit": "2.0.4",
150
149
  "get-tsconfig": "4.14.3",
151
150
  "glob": "14.0.2",
152
151
  "graceful-fs": "4.2.11",
@@ -173,20 +172,24 @@
173
172
  "@redis/client": "^6.3.0",
174
173
  "react": "19.3.1",
175
174
  "react-dom": "19.3.1",
176
- "react-server-dom-webpack": "19.3.1"
175
+ "react-server-dom-webpack": "19.3.1",
176
+ "oxc-transform-react": "0.157.0"
177
177
  },
178
178
  "peerDependenciesMeta": {
179
179
  "@redis/client": {
180
180
  "optional": true
181
+ },
182
+ "oxc-transform-react": {
183
+ "optional": true
181
184
  }
182
185
  },
183
186
  "optionalDependencies": {
184
- "prnext-darwin-arm64": "0.1.19-alpha",
185
- "prnext-darwin-x64": "0.1.19-alpha",
186
- "prnext-linux-arm64-gnu": "0.1.19-alpha",
187
- "prnext-linux-x64-gnu": "0.1.19-alpha",
188
- "@thomas.f/prnext-win32-x64-msvc": "0.1.19-alpha",
189
- "@thomas.f/prnext-win32-arm64-msvc": "0.1.19-alpha"
187
+ "prnext-darwin-arm64": "0.1.20-alpha",
188
+ "prnext-darwin-x64": "0.1.20-alpha",
189
+ "prnext-linux-arm64-gnu": "0.1.20-alpha",
190
+ "prnext-linux-x64-gnu": "0.1.20-alpha",
191
+ "@thomas.f/prnext-win32-x64-msvc": "0.1.20-alpha",
192
+ "@thomas.f/prnext-win32-arm64-msvc": "0.1.20-alpha"
190
193
  },
191
194
  "engines": {
192
195
  "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
@@ -0,0 +1,31 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {readFile, readdir} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+
6
+ const destination = fileURLToPath(new URL('../vendor/fontkit/', import.meta.url));
7
+ const digest = bytes => createHash('sha256').update(bytes).digest('hex');
8
+
9
+ /** Validate the shipped inventory without loading fontkit or its build dependencies. */
10
+ export async function verifyVendoredFontkit(directory = destination) {
11
+ const provenance = JSON.parse(await readFile(path.join(directory, 'upstream.json'), 'utf8'));
12
+ if (provenance.schemaVersion !== 1 || provenance.name !== 'fontkit' || provenance.version !== '2.0.4'
13
+ || provenance.tool?.name !== 'esbuild' || !provenance.files?.['index.mjs']
14
+ || !provenance.files?.['NOTICE.txt'] || !provenance.files?.['SOURCE-NOTICES.txt']
15
+ || !provenance.packages?.fontkit || !provenance.packages?.brotli) throw new Error('Incomplete fontkit provenance.');
16
+ const actual = [];
17
+ async function visit(directory, prefix = '') {
18
+ for (const entry of await readdir(directory, {withFileTypes: true})) {
19
+ const name = prefix + entry.name;
20
+ if (entry.isDirectory()) await visit(path.join(directory, entry.name), name + '/');
21
+ else if (!entry.isFile()) throw new Error('Vendored fontkit files must be regular files.');
22
+ else if (name !== 'upstream.json') actual.push(name);
23
+ }
24
+ }
25
+ await visit(directory);
26
+ if (JSON.stringify(actual.sort()) !== JSON.stringify(Object.keys(provenance.files).sort())) throw new Error('Vendored fontkit inventory changed.');
27
+ for (const name of actual) {
28
+ if (digest(await readFile(path.join(directory, name))) !== provenance.files[name]) throw new Error(`Vendored fontkit differs from its reviewed build: ${name}`);
29
+ }
30
+ return {version: provenance.version, files: actual.length, bytes: (await readFile(path.join(directory, 'index.mjs'))).length};
31
+ }
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
3
3
  import path from 'node:path';
4
4
  import { packageRoot, packageManifest, platforms, sourceCheckout } from '../native/resolve.mjs';
5
5
  import { verifyAiDocumentation } from './ai-docs.mjs';
6
+ import { verifyVendoredFontkit } from './fontkit.mjs';
6
7
 
7
8
  export async function verifyPackage() {
8
9
  const pkg = packageManifest;
@@ -27,6 +28,8 @@ export async function verifyPackage() {
27
28
  }
28
29
  await exported(pkg.exports);
29
30
  await verifyAiDocumentation(packageRoot, pkg);
31
+ await verifyVendoredFontkit();
32
+ if (pkg.dependencies.fontkit || pkg.dependencies['oxc-transform-react'] || pkg.peerDependenciesMeta?.['oxc-transform-react']?.optional !== true) throw new Error('Fontkit must be bundled and the Oxc React compiler must remain an optional peer.');
30
33
  await access(path.join(packageRoot, 'runtime/profiles.json'));
31
34
  for (const name of ['index.node.mjs', 'resvg.wasm', 'hb.wasm', 'Geist-Regular.ttf', 'LICENSE', 'LICENSE.fflate', 'LICENSE.Geist', 'LICENSE.harfbuzzjs', 'LICENSE.satori', 'NOTICE.txt']) await access(path.join(packageRoot, 'runtime/og', name));
32
35
  if (pkg.dependencies['@vercel/og'] || (await readFile(path.join(packageRoot, 'runtime/og/index.node.mjs'), 'utf8')).includes('fflate@0.7.3')) throw new Error('OG renderer still depends on vulnerable upstream fflate. Run scripts/prepare-og.mjs.');
package/runtime/api.mjs CHANGED
@@ -195,7 +195,7 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
195
195
  // IncomingMessage here duplicates URL, header and cookie work and allocates
196
196
  // an unused Duplex. Keep the existing Pages request path unchanged.
197
197
  const { request, bytes } = appHandler
198
- ? { bytes: Buffer.isBuffer(body) ? body : Buffer.from(body || '', 'base64') }
198
+ ? { bytes: body instanceof ReadableStream ? body : Buffer.isBuffer(body) ? body : Buffer.from(body || '', 'base64') }
199
199
  : createRequest({ url, originalUrl: requestOriginal?.href, params, method, headers, body });
200
200
  if (!appHandler) {
201
201
  request.draftMode = cacheContext.draftMode;
@@ -226,7 +226,10 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
226
226
  throw abortError(lifecycle.controller.signal.reason);
227
227
  }
228
228
  const init = { method, headers, ...(lifecycle ? { signal: lifecycle.controller.signal } : {}) };
229
- if (!['GET', 'HEAD'].includes(method) && bytes.length) init.body = bytes;
229
+ if (!['GET', 'HEAD'].includes(method) && (bytes instanceof ReadableStream || bytes.length)) {
230
+ init.body = bytes;
231
+ if (bytes instanceof ReadableStream) init.duplex = 'half';
232
+ }
230
233
  const timeout = lifecycle && setTimeout(() => lifecycle.controller.abort(apiTimeoutError('response headers', timeoutMs)), timeoutMs);
231
234
  timeout?.unref();
232
235
  const pendingResponse = runRequestContext({ url, method, headers, params, production, cacheComponents: manifest.config?.cacheComponents, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, distDir: path.dirname(path.dirname(modulePath)), signal: lifecycle?.controller.signal || signal, previewModeId: manifest.previewModeId, basePath: manifest.config?.basePath || '', routePattern: route.pattern, cacheConfig: route.cacheConfig, phase: 'route', cacheState: cacheContext.cacheState, staticState: cacheContext.staticState, mutableCookies: true }, async () => {