@thomas.f/prnext 0.1.12-alpha → 0.1.13-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 (50) hide show
  1. package/README.md +262 -33
  2. package/build/actions.mjs +4 -0
  3. package/build/app-config.mjs +2 -0
  4. package/build/app.mjs +20 -5
  5. package/build/bundle-runtime.mjs +82 -0
  6. package/build/compiler.mjs +3 -1
  7. package/build/config.mjs +9 -2
  8. package/build/font-cache.mjs +50 -0
  9. package/build/font-files.mjs +5 -3
  10. package/build/fonts.mjs +1 -1
  11. package/build/index.mjs +5 -0
  12. package/build/metadata-conflicts.mjs +13 -0
  13. package/build/metadata-images.mjs +3 -3
  14. package/build/metadata-routes.mjs +3 -3
  15. package/build/native-packages.mjs +5 -1
  16. package/build/npm-packages.mjs +2 -1
  17. package/build/optimize-imports.mjs +112 -0
  18. package/build/proxy-body-config.mjs +10 -0
  19. package/build/scan.mjs +9 -7
  20. package/cli.mjs +16 -0
  21. package/compat/after.cjs +51 -0
  22. package/compat/index.d.ts +3 -1
  23. package/compat/server.cjs +2 -1
  24. package/compat/server.d.cts +2 -0
  25. package/native/launch.mjs +19 -0
  26. package/package.json +7 -7
  27. package/runtime/api.mjs +15 -5
  28. package/runtime/app-render.mjs +78 -33
  29. package/runtime/app-route-plan.mjs +45 -0
  30. package/runtime/app-routing-state.mjs +101 -21
  31. package/runtime/app-routing.mjs +37 -18
  32. package/runtime/app-segments.mjs +25 -11
  33. package/runtime/app-static.mjs +1 -1
  34. package/runtime/app-stream-html.mjs +2 -1
  35. package/runtime/cache-request.mjs +10 -2
  36. package/runtime/cloud/archive.mjs +316 -0
  37. package/runtime/cloud/client.mjs +658 -0
  38. package/runtime/cloud/environment.mjs +60 -0
  39. package/runtime/cloud/state.mjs +141 -0
  40. package/runtime/cloud/upload.mjs +48 -0
  41. package/runtime/html-envelope.mjs +70 -7
  42. package/runtime/http.mjs +7 -4
  43. package/runtime/middleware.mjs +3 -3
  44. package/runtime/request-transport.mjs +1 -1
  45. package/runtime/route-static.mjs +1 -1
  46. package/runtime/rsc-worker.mjs +27 -4
  47. package/runtime/stream-utils.mjs +22 -3
  48. package/runtime/transport.mjs +16 -0
  49. package/runtime/worker-metadata.mjs +23 -0
  50. package/runtime/worker.mjs +36 -9
package/README.md CHANGED
@@ -10,7 +10,15 @@ A Rust-powered runtime for Next.js projects, with React and npm modules running
10
10
 
11
11
  ## Latest update
12
12
 
13
- **0.1.12-alpha — stability and bounded resource usage.** Client cancellations retain healthy Node hosts; worker retirement protects tracked finalization; filesystem and proxy overload return explicit, noncacheable 503 responses. Clean installs and runtime/native/build consistency checks are part of the release path. Rebuild dynamic applications with `prnext build` after upgrading, then restart them.
13
+ **0.1.13-alpha — worker lifecycle, streaming and routing.** Lower Linux production-launcher overhead, parent-death containment for Linux workers, and earlier release of completed RSC response resources while background cache work remains supervised. Streaming API and middleware responses reuse their inactivity timer, preserving timeout and cancellation behavior. Ready response producers yield after bounded batches, including empty chunks, so network events and cancellation timers can run. Profile admission, V8 and idle policies remain distinct; see the runtime profile notes below.
14
+
15
+ The source candidate reuses immutable production App Router routing plans, decodes the destination pathname once per request and reduces temporary segment arrays. Component exports and configuration remain live; dynamic output and request parameters are not cached. Rebuild to enable these plans; older/mutable routing trees and development retain the compatible live traversal. These changes do not establish a general HTTP performance advantage over Next.js.
16
+
17
+ Client cancellations retain healthy Node hosts; worker retirement protects tracked finalization; filesystem and proxy overload return explicit, noncacheable 503 responses. Clean installs and runtime/native/build consistency checks remain part of the release path. Rebuild dynamic applications with `prnext build` after upgrading, then restart them.
18
+
19
+ Shutdown now drains tracked worker and ISR work before closing private cache services. Standalone service shutdown shares a 30-second deadline between HTTP responses and background work. Ordinary Host sleep and capacity eviction allow up to 300 seconds of tracked work, covering the supported `maxDuration` range, followed by process cleanup. Global Host shutdown remains bounded to 35 seconds; memory-budget violations retain their shorter enforced stop. Monitoring continues while applications drain. This protects supported `after()` callbacks, not arbitrary timers or durable jobs.
20
+
21
+ The October 15 comparison of a frozen local candidate with Next.js 16.4.1 measured `balanced` SSR at 365 vs 281 req/s on Windows and 218 vs 158 on Linux, with cumulative server RSS of 182 vs 362 MiB and 149 vs 247 MiB respectively. Production, one server CPU, 64 connections, five 30-second runs after 10-second warmups, the same 100-row application, no dynamic page cache. These measurements precede the final shutdown fix and are not final-package performance certification. TanStack Start was faster on the App Router-equivalent SSR, streaming and JSON workloads; PRNext does not claim a universal performance lead. See the [release methodology and limits](https://github.com/Lorkydey/PRNext/blob/main/RELEASE.md#performance-evidence).
14
22
 
15
23
  The experimental eight-app `balanced`/1 GiB Windows commit configuration with `NODE_OPTIONS=--max-semi-space-size=1` still has a reproduced allocation-failure regression relative to V5. It is not a supported density target and is not fixed. Defaults remain `start=balanced` and `host=memory`. Compatibility remains partial, including the compiler and TypeScript limits below.
16
24
 
@@ -61,6 +69,44 @@ Install only `@thomas.f/prnext`. Keep optional dependencies enabled: npm install
61
69
 
62
70
  `reactCompiler: false` and `typedRoutes: false` are supported. Enabling either option fails with an explicit diagnostic: PRNext does not yet run React Compiler or generate and validate typed route declarations. Keep builds that require these features on Next.js.
63
71
 
72
+ The current source candidate also supports `devIndicators: false`, configured
73
+ `experimental.optimizePackageImports` for pure ESM barrels with `sideEffects: false`,
74
+ and App Router `maxDuration` values from 1 to 300 seconds. Import optimization
75
+ preserves client/server boundaries and falls back to normal bundling when a barrel
76
+ cannot be safely expanded. Duplicate App convention files follow `pageExtensions`
77
+ priority and emit a warning. Identical public/App metadata files are accepted;
78
+ conflicting content still fails explicitly. Route Handlers can call shared
79
+ `use server` functions directly; pages retain their Flight/Server Action
80
+ registrations. Mixed npm component exports preserve the real React server
81
+ instance without requiring unused client hooks to exist in that environment.
82
+
83
+ Node Route Handlers and Server Actions can use `after()` from `next/server`. Callbacks run after the
84
+ response finishes or disconnects, including error responses, and retain their
85
+ request context. Tracked work prevents idle worker retirement without blocking the
86
+ next HTTP request. There are at most 128 tasks per request, within `maxDuration`
87
+ (25 seconds by default, including the response's running time). Cookie reads are supported;
88
+ post-response cookie writes are rejected. Server Actions wait for the HTTP response
89
+ to finish, not just their intermediate Flight render. Server Component and Edge
90
+ `after()` lifecycles are not implemented and fail explicitly. `revalidateTag`,
91
+ `revalidatePath` and `updateTag` are also rejected inside an `after()` callback;
92
+ perform supported invalidation in the Route Handler or Server Action before
93
+ returning. This differs from the full Next.js lifecycle. During service shutdown,
94
+ the 30-second standalone drain (35 seconds for global Host shutdown) can shorten a route's larger `maxDuration`. Ordinary sleep/eviction instead allows a 300-second drain. Forced
95
+ termination, a memory-budget violation or a machine failure can still interrupt
96
+ work; use durable external job storage for tasks that must survive these events.
97
+
98
+ `experimental.proxyClientMaxBodySize` accepts a byte count or a size such as
99
+ `'25mb'`, up to 32 MiB (default 10 MiB). Like Next's body clone, overflow retains
100
+ only complete chunks before the limit and warns; a chunk crossing the limit is
101
+ discarded. General request ingress remains capped at 32 MiB and Server Actions at
102
+ 8 MiB. PostgreSQL's `pg` stays external by default so its optional native driver
103
+ remains lazy; explicitly putting `pg` in `transpilePackages` opts out.
104
+
105
+ Google font build inputs are cached under `.prnext-cache/fonts-v1` (64 MiB,
106
+ 256 entries, verified content hashes). A warm cache permits offline rebuilding;
107
+ a cold build still needs access to Google's font endpoints. Remove this cache to
108
+ refresh downloaded font inputs. This does not cache dynamic page responses.
109
+
64
110
  Set `typescript: {}` to check an existing `tsconfig.json` during production builds using the application's installed `typescript` package. `typescript.tsconfigPath` selects a custom configuration for type checking and compilation, including its inherited path aliases. `typescript.ignoreBuildErrors: true` explicitly skips the production check; development builds also skip it. Checks run in a temporary child process without emitting files or editing the configuration. Unlike Next.js, omitting `typescript` preserves PRNext's existing transpile-only behavior. PRNext does not install TypeScript, create `next-env.d.ts`, generate Next.js route types, or implement the Next.js TypeScript language-service plugin; retain the project's type declarations and run separate editor checks as needed. See the [Next.js option definitions](https://nextjs.org/docs/app/api-reference/config/next-config-js/typescript).
65
111
 
66
112
  </details>
@@ -68,7 +114,7 @@ Set `typescript: {}` to check an existing `tsconfig.json` during production buil
68
114
  <details>
69
115
  <summary><strong>Upgrade · update an existing PRNext project</strong></summary>
70
116
 
71
- Stop your development or production process before changing dependencies. If you use `pstart`, follow the [persistent app upgrade guide](https://github.com/Lorkydey/PRNext#upgrade-persistent-apps) first.
117
+ Stop your development or production process before changing dependencies. If you use `pstart`, follow the [persistent app upgrade guide](#upgrade-persistent-apps) first.
72
118
 
73
119
  ```sh
74
120
  npm install @thomas.f/prnext@alpha react@19.3.0 react-dom@19.3.0 react-server-dom-webpack@19.3.0
@@ -223,9 +269,18 @@ Client disconnections release response permits and close incomplete transport la
223
269
 
224
270
  Explicit disposal of a worker pool starts a separate graceful shutdown: tracked work has up to 30 seconds to drain before Rust terminates and reaps the process, including a blocked JavaScript event loop. This shutdown grace does not limit normal idle cache finalization. Arbitrary detached tasks are outside the tracked lifecycle; applications that rely on them or long-lived in-process state should use `speed` or `classic` to keep their primary host warm. Such tasks are still not preserved across shutdown or worker failure.
225
271
 
226
- The `memory` profile allows 28 live responses with 8 render starts, a 1 MiB response buffer budget and its existing small V8 nursery. `balanced` retains 32 live responses and `speed` retains 128. These limits bound concurrency independently of idle retirement.
272
+ Each profile keeps its own admission, buffering and idle policy. Limits below are per worker host; render starts and live responses are separate budgets.
227
273
 
228
- `balanced` uses a 4 MiB V8 semi-space without `--optimize-for-size`, keeping its RSC young-generation setting at 16 MiB. This selection reduces measured CPU per response at the cost of higher RSS than the previous size policy. It keeps 16 render starts, a 4 MiB response buffer and 300-second host retirement; `speed` uses 32 starts, a 16 MiB buffer and keeps hosts warm. In the measured SSR/streaming fixtures, cumulative RSS remained below Next.js, but that is not a universal memory guarantee. The final Windows validation includes three 30-second measurements per scenario after warm-up and a one-hour mixed endurance run. The CPU/RSS trade-off remains workload-dependent; comparable Linux performance and Nginx benchmarks have not been run.
274
+ | Profile | Render starts | Live responses | API concurrency | Response buffer | Primary idle deadline |
275
+ |---|---:|---:|---:|---:|---|
276
+ | `balanced` | 16 | 32 | 256 | 4 MiB | 300 seconds |
277
+ | `speed` | 32 | 128 | 512 | 16 MiB | Keep warm |
278
+ | `memory` | 8 | 28 | 64 | 1 MiB | 30 seconds |
279
+ | `classic` | 16 | 32 | 512 | 8 MiB | Keep warm |
280
+
281
+ On Linux with Node's `process.execve` available (Node 22.15+/23.11+), production `prnext start` replaces its CLI process with the native server. This removes a resident Node launcher from every profile without shrinking render or response budgets. Rust handles SIGINT, SIGTERM and SIGHUP gracefully; Linux also kills Node workers if their native parent dies, including when JavaScript is blocked. Explicit `--shutdown-on-stdin-eof` remains supported. Windows, macOS, older Node versions, IPC callers and Node instrumentation keep the supervised launch path. Only heap-sizing Node flags are eligible for replacement; other flags and `NODE_V8_COVERAGE` conservatively retain supervision. Node still labels `process.execve` experimental. This change does not remove the supervisors used by `pstart`, or change `host` and development mode.
282
+
283
+ `balanced` keeps its 4 MiB V8 semi-space without `--optimize-for-size`. `speed` keeps its 16 MiB semi-space, larger concurrency and buffers, and warm hosts. `memory` keeps V8's size policy and early retirement; `classic` preserves its original V8 settings. Node heap flags can take precedence over the RSC isolate's young-generation resource limit: these settings are not total RSS caps. The earlier Windows V6 validation includes three 30-second measurements per scenario and a one-hour mixed endurance run; it predates this launcher change and is not an endurance result for this candidate. Workload and platform affect the CPU/RSS trade-off; a comparable Linux Nginx benchmark remains pending.
229
284
 
230
285
  An explicit `NODE_OPTIONS="--max-semi-space-size=4"` (including Node's underscore spelling) overrides the profile's nursery policy. PRNext also omits `--optimize-for-size` in that case: on Node 24.19.0, V8 otherwise replaces the explicit value with 1 MiB. This opts out of V8's broader size policy as well, so CPU and memory consumption can change; measure the chosen value for your workload. It does not add an old-generation heap limit or change the profile's admission, response buffers or idle retirement. Without an explicit override, PRNext uses the defaults of the selected profile.
231
286
 
@@ -247,74 +302,248 @@ improved small-file throughput by about 5% in two short configuration trials,
247
302
  but allows four times as many submitted helper tasks. It has not received the
248
303
  same endurance validation and is not a universal profile improvement.
249
304
 
250
- Results depend on your workload; see the [benchmark and its methodology](https://github.com/Lorkydey/PRNext#early-benchmarks-against-nextjs).
305
+ Results depend on your workload; see the [benchmark and its methodology](#early-benchmarks-against-nextjs).
306
+
307
+ </details>
308
+
309
+ ## Early benchmarks against Next.js
310
+
311
+ Dynamic streaming with 128 concurrent clients and a deterministic local backend delayed by 40 ms:
312
+
313
+ | Configuration | RAM | CPU load¹ | CPU/response | Throughput |
314
+ |---|---:|---:|---:|---:|
315
+ | Next.js | 605 MiB | 164% | 2.442 ms | 673 req/s |
316
+ | **Balanced** | **207 MiB** | 124% | 1.704 ms | 726 req/s |
317
+ | **Speed** | 496 MiB | 250% | **0.926 ms** | **2,698 req/s** |
318
+ | **Memory** | **179 MiB** | **75%** | 2.067 ms | 362 req/s |
319
+ | **Classic** | 303 MiB | 90% | 1.241 ms | 723 req/s |
320
+
321
+ <details>
322
+ <summary><strong>Benchmark methodology & trade-offs</strong></summary>
323
+
324
+ ¹ 100% CPU = one fully utilized CPU core. RAM is measured under load.
325
+
326
+ Measured September 27, 2026, on Apple M4 against Next.js 15.5.12. Values are medians of three six-second runs using identical application sources. This table covers one streaming scenario, not an overall average or guaranteed gains. Functional parity was checked before benchmarking.
327
+
328
+ Under concurrent streaming, `memory` used 179 MiB versus 207 MiB for `balanced`, but served 362 versus 726 responses/s. On concurrent SSR, it used slightly more memory than `balanced`. The lowest-memory profile is workload-dependent.
251
329
 
252
330
  </details>
253
331
 
254
332
  ## Guides
255
333
 
256
334
  <details>
257
- <summary><strong>Migration audit, inspector and multi-app hosting</strong></summary>
335
+ <summary><strong>Deploy to PRNext Cloud (local development)</strong></summary>
258
336
 
259
- ### Migration audit, inspector and multi-app hosting
337
+ ### Deploy to PRNext Cloud
338
+
339
+ The local CLI now supports browser login and source deployments. This feature is
340
+ not yet included in a published npm release or deployed to the public Cloud app.
260
341
 
261
342
  ```sh
343
+ prn login
344
+ prn deploy ./my-app
345
+ prn update ./my-app
346
+ prn whoami
347
+ prn logout
348
+ ```
349
+
350
+ `prnext` accepts the same commands. Login opens the browser and reuses its session,
351
+ or returns to terminal approval after web sign-in. Confirm the code shown in your
352
+ terminal. `--no-browser` prints the URL for manual authorization.
353
+
354
+ `deploy` creates an upload project and saves its link in the ignored
355
+ `.prnext-cloud/project.json`; `update` sends changes to that same project.
356
+ `deploy --project ID` links an existing upload project. `--dry-run` lists source
357
+ files without connecting or uploading; `--no-wait` returns after queueing.
358
+ `--timeout SECONDS` controls waiting (default 600). After interruption or
359
+ `--no-wait`, `update` resumes the recorded request first; run it again to deploy
360
+ further changes. Server-side request IDs prevent duplicate queued builds.
361
+
362
+ Archives exclude `.env*`, credentials, `node_modules`, `.git`, generated
363
+ `.next`/`.prnext*` output and `.local`. Root `.gitignore` and `.prnextignore`
364
+ support additional glob exclusions and `!` rules; nested/global Git ignore files
365
+ are not read. Mandatory exclusions cannot be re-included. Nonexcluded symbolic
366
+ links are refused. On a compatible Host server: 100 MiB ZIP, 500 MiB expanded,
367
+ 15,000 files and 64 MiB per file. Transfers use resumable 4 MiB chunks on the VPS;
368
+ legacy servers retain their 7 MiB upload limit. No object storage is required.
369
+
370
+ When production `.env` files are detected, `deploy` and `update` ask before
371
+ importing variables (default: no). `--with-env` explicitly opts in;
372
+ `--without-env` skips the prompt. Noninteractive commands skip import unless
373
+ `--with-env` is set. Variables travel through the encrypted environment API,
374
+ never inside the source ZIP or project link. Matching remote keys are replaced;
375
+ other keys remain. `NEXT_PUBLIC_` values may be exposed in the browser bundle.
376
+ This feature requires the updated Host API and remains unpublished.
377
+ Use project environment settings for secrets; inspect `--dry-run` for other
378
+ sensitive files in your own source tree.
379
+
380
+ Credentials stay under `~/.prnext/cloud/` (or private `PRNEXT_CLOUD_HOME`, outside
381
+ the source directory). Tokens are scoped to project reads/uploads/deployments,
382
+ expire after 30 days, and can be revoked with `logout` or in workspace settings.
383
+ Browser sign-out does not revoke authorized terminals.
384
+
385
+ For a local Cloud server, use `--server http://127.0.0.1:3003` or
386
+ `PRNEXT_CLOUD_URL`. From this checkout, run
387
+ `node packages/prnext/cli.mjs login --server http://127.0.0.1:3003`.
388
+ Production defaults to `https://app.prnext.dev`; credentials are isolated by origin.
389
+
390
+ A configured isolated Linux worker is required to deploy. A local Windows panel
391
+ supports login and dry runs, but reports unavailable hosting explicitly.
392
+ Approved offline dependencies, runtime network isolation and current Cloud quotas
393
+ still apply; these commands do not bypass the hosting worker or configure DNS/TLS.
394
+
395
+ </details>
396
+
397
+
398
+ <details>
399
+ <summary><strong>Check a migration before switching</strong></summary>
400
+
401
+ ### Check a migration before switching
402
+
403
+ ```sh
404
+ prnext migrate ./my-app --check
262
405
  prnext migrate ./my-app --check --json
263
406
  prnext migrate ./my-app --check --against http://localhost:3000 --candidate http://localhost:3001 --routes paths.json
407
+ ```
408
+
409
+ The source audit reports unsupported Next imports with file and line numbers, client/server import conflicts, dependency versions and scripts requiring manual changes. It leaves sources, package files and builds untouched. Configuration files are evaluated by the existing preflight; installed dependencies and custom loaders still require a build. Warnings are distinct from blocking errors. A failed audit or response comparison exits with code 1.
410
+
411
+ For comparison, start the existing Next application and the PRNext candidate separately. `paths.json` is a JSON array such as `["/", "/products/123", "/api/catalog"]`; without it, the command selects non-parameterized page routes from the audit. It sends GET requests without authentication, follows no redirects, and compares status, content type, redirect destinations and bodies. JSON object key order is ignored; HTML compares text with scripts/styles removed. This verifies response samples, not hydration, styling, interactive behavior or every dynamic path. Timing observations are not controlled performance benchmarks. `--timeout 10000` sets each response's deadline in milliseconds.
412
+
413
+ </details>
414
+
415
+ <details>
416
+ <summary><strong>Inspect routes, cache and timings</strong></summary>
417
+
418
+ ### Inspect routes, cache and timings
419
+
420
+ ```sh
421
+ prnext build ./my-app
264
422
  prnext start ./my-app --inspect
423
+ # In another terminal, after visiting the application:
424
+ prnext inspect ./my-app
265
425
  prnext inspect ./my-app --json
266
- prnext host prnext.host.json --check
267
- prnext host prnext.host.json
268
- prnext host prnext.host.json --status --json
269
426
  ```
270
427
 
271
- The migration audit reports file/line diagnostics without rewriting the application. Response comparison checks GET status, redirects and normalized bodies between servers you start separately. HTML comparison excludes scripts/styles and does not replace browser tests.
428
+ The inspector explains static, dynamic and partially prerendered routes using build metadata, including observed App prerendering bailouts and Pages data hooks. Opt-in measurements show native HTTP timings, fetch destinations and timings, native data cache hits/misses/stale reads, bypass reasons and successful tag/path invalidations. A custom cache handler is identified as a delegation; its internal hit rate is not inferred. Native page-file cache decisions and client router caching are not counted as data-cache hits.
429
+
430
+ Timings end when headers are ready, so streaming body time is excluded. Samples are bounded, can span process sessions, and are written under the app's `.prnext-cache/inspect` directory. Request bodies, headers, cookies, URL queries and credentials are omitted; runtime tag/path identifiers are hashed consistently. Logs retain at most 1 MiB per segment plus a previous segment, with old process sessions pruned. Under load, diagnostics may drop samples. `--inspect` also works with `prnext dev`; standalone/native launches can set `PRNEXT_INSPECT_DIR` explicitly. No public diagnostic endpoint is added.
431
+
432
+ </details>
433
+
434
+ <details>
435
+ <summary><strong>Host several applications</strong></summary>
436
+
437
+ ### Host several applications
438
+
439
+ ```sh
440
+ npm run build -- examples/basic
441
+ npm run build -- examples/app
442
+ node packages/prnext/cli.mjs host examples/hosting/prnext.host.json --check
443
+ node packages/prnext/cli.mjs host examples/hosting/prnext.host.json
444
+ # In another terminal:
445
+ node packages/prnext/cli.mjs host examples/hosting/prnext.host.json --status
446
+ ```
272
447
 
273
- The inspector explains build decisions and summarizes optional local HTTP, fetch, cache and invalidation samples. Durations end at response headers. Runtime logs omit request bodies, credentials and query strings, and hash tag/path identifiers. Measurements are bounded and may drop samples under load.
448
+ The example routes `basic.localhost:8080` and `app.localhost:8080` to separate applications. Resolve those hostnames to the listener address, or supply their exact `Host` header when testing. Installed packages use `prnext host config.json`. Configuration roots are relative to the JSON file. Each app has a unique name, root and exact hostname list, with optional `memoryMb`, `idleSeconds`, `workers`, `profile`, `env` and `inspect` settings. `--status --json` reads the local status snapshot beside the configuration, including its age; it does not connect to a live admin endpoint. Restart the host to apply configuration or build changes.
274
449
 
275
- The host uses separate native server/Node process trees, starts apps on demand and stops idle apps. A minimal configuration is `{"memoryMb":512,"apps":[{"name":"site","root":"./site","hosts":["site.localhost"],"memoryMb":256,"idleSeconds":300}]}`. Roots are relative to this file; apps must already have production builds. Additional app options are `workers`, `profile`, `env` and `inspect`. The listener defaults to `127.0.0.1:8080`; set `hostname` and `port` explicitly when needed.
450
+ The native gateway keeps each application asleep until requested. A waking app reserves its configured memory budget; if capacity is full, inactive apps can release theirs, while busy requests receive 503 rather than starting an unbudgeted app. Requests and streaming responses hold their reservation until completion or cancellation. Idle apps begin stopping after `idleSeconds` and wake on the next request. Sleep and capacity eviction retain the memory reservation while the child drains supported `after()` and ISR work, allowing up to 300 seconds plus five seconds for process cleanup. A wake waiting for that capacity can take longer. Status shows `draining` with the process still present; monitoring and memory-budget enforcement continue. Global Host shutdown shortens outstanding retirement to its own 35-second deadline. Persistent caches remain on disk; in-process state and untracked background jobs do not survive sleep. Apps receive their configured environment and a small platform environment allowlist, then load their own `.env` files.
276
451
 
277
- Memory budgets reserve capacity and monitor process-tree RSS about once per second. An excess stops the app and imposes a 30-second cooldown; this is not a kernel hard limit. Leave memory for the gateway, OS and overshoot. Sleeping loses in-process state and background work. Apps run as the same OS user, so this mode is for trusted applications. HTTP streaming is supported; WebSocket upgrades are not. Configure public DNS/TLS and OS services separately.
452
+ The watchdog samples the RSS of each app's native server and descendants about once per second. Exceeding `memoryMb` stops that process tree, can interrupt its in-flight requests, and imposes a 30-second cooldown. V8 also receives an initial heap guard unless the app config supplies `NODE_OPTIONS`. These are sampled operating budgets, not kernel-enforced hard limits; allow extra RAM for the gateway, the OS and sampling overshoot. Process separation isolates caches, environment and ordinary failures, but apps run as the same OS user: host only trusted applications. Use separate users or containers for untrusted tenants.
278
453
 
279
- The Windows release check used four apps on one shared CPU and a 1 GiB Job commit budget. Two 30-second mixed SSR/stream/API trials measured about 333 requests/s through `host` versus 361 directly with the same V8 heap guard. Matching direct controls followed the host trials; this roughly 8% cost is specific to the fixture. Three sleep/resume cycles per trial used `idleSeconds: 3`: 32 simultaneous arrivals completed in 1.92–2.35 seconds, with one startup per app and no failures. The default whole-app deadline remains 300 seconds. The gateway's status/RSS monitor continues while apps sleep, so idle CPU is not zero. No host endurance or Linux performance result is claimed.
454
+ The 0.1.12-alpha Windows host check used four applications sharing one CPU and a 1 GiB Job commit budget. Two 30-second mixed SSR/stream/API trials measured about 333 requests/s through `host` versus 361 directly with the same V8 heap guard (about 8% gateway cost in this fixture). The matching direct controls followed the host trials; these short runs are not a general capacity guarantee. Three sleep/resume cycles per trial used `idleSeconds: 3` to exercise retirement: 32 simultaneous arrivals completed in 1.92–2.35 seconds, with one startup per application and no failures. The default whole-app deadline remains 300 seconds. The gateway keeps its one-second status/RSS monitor running while apps sleep; idle CPU is not zero. No host endurance or Linux performance result is claimed.
280
455
 
281
- See the [repository guide and example configuration](https://github.com/Lorkydey/PRNext#host-several-applications) for the full workflow.
456
+ Hosting supports HTTP streaming and preserves the original application Host. Protocol upgrades such as WebSockets return 501. Put a TLS reverse proxy in front for public hosting; this command does not configure DNS, TLS, OS services or deploy to a VPS. Windows, macOS and Linux use the same JSON format and CLI.
282
457
 
283
458
  </details>
284
459
 
285
460
  <details>
286
- <summary><strong>Persistent production commands</strong></summary>
461
+ <summary><strong>Keep production apps running</strong></summary>
462
+
463
+ ### Keep production apps running
464
+
465
+ `prnext start` keeps its normal foreground behavior. Use the separate persistent commands when an app should survive closing the terminal:
466
+
467
+ ```sh
468
+ prnext build ./my-app
469
+ prnext pstart ./my-app --name web --port 3000
470
+ prnext pstatus
471
+ prnext plogs web --follow
472
+ # After rebuilding the app:
473
+ prnext prestart web
474
+ prnext pstop web
475
+ ```
476
+
477
+ `pstart` starts a detached supervisor and returns once the app passes its health check. Applications restart after a crash with increasing delays; ten failures within five minutes stop retries until `prestart`. A separate watchdog restarts the supervisor after a crash or an unresponsive event loop. Each app has a stable HTTP listener and its own native server and workers. This mode adds a Node HTTP proxy and two supervisor processes; it does not enable the idle sleep or memory reservations of `prnext host`.
478
+
479
+ `prestart` copies the current production build, starts a replacement on a private port, and sends a GET to `--health-path` (default `/`). A 2xx or 3xx response must arrive within `--health-timeout 15000` milliseconds. Only then does new traffic switch to the replacement. If startup or the health check fails, the existing healthy instance keeps serving. Existing HTTP streams can finish for up to `--drain-timeout 30000` milliseconds, after which remaining requests are closed. Choose a health route that checks the dependencies your application needs. Two generations temporarily use memory and disk space during replacement.
480
+
481
+ Build copies live under the project's ignored `.prnext-persistent` directory and are removed after their processes stop. Rebuilding does not replace the active generation's compiled files. Project dependencies, `public` files and persistent caches remain shared; avoid changing dependencies or deleting public assets while old requests still need them. HTTP streaming, original Host headers and multiple cookies are preserved. WebSocket/protocol upgrades return 501. A supervisor crash can interrupt requests; health-checked replacement is not a guarantee against machine failures or incompatible application changes. A TLS reverse proxy can sit in front of the persistent listener.
482
+
483
+ Available commands:
287
484
 
288
- ### Persistent production commands
485
+ | Command | Behavior |
486
+ | --- | --- |
487
+ | `prnext pstart [directory] --name NAME` | Start or restore an app; identical repeated starts are idempotent |
488
+ | `prnext prestart [name] [--all]` | Load the current build through a health-checked replacement |
489
+ | `prnext pstop [name] [--all]` | Stop apps and keep them stopped across supervisor restarts |
490
+ | `prnext pdelete [name] [--all]` | Stop apps and remove their saved configuration; retain logs |
491
+ | `prnext pstatus [name] [--json]` / `prnext plist` | Show status, PIDs, build IDs, starts and active requests |
492
+ | `prnext plogs [name] --lines 100 --follow` | Read/tail app output; `--supervisor` selects supervisor logs |
493
+ | `prnext pdown` | Stop the supervisor and apps, retaining enabled flags for their next launch |
494
+ | `prnext pstartup [--remove]` | Enable/remove automatic restoration at user login |
289
495
 
290
- `prnext start` remains a foreground command. Use `prnext pstart --name web --port 3000` to keep a built app running after the terminal closes. `prnext pstatus` (or `plist`) lists apps, `prnext plogs web --follow` tails logs, `prnext prestart web` loads the current build through a health check, and `prnext pstop web` stops it. `pdelete` removes saved settings; `pdown` stops the supervisor while preserving enabled apps for restoration. Lifecycle commands accept `--all` where shown by `prnext pstart --help`.
496
+ Without a name, `prestart`, `pstop`, `pdelete` and `plogs` select the app registered for the current directory. `pstart` accepts `--hostname`, `--port`, `--workers`, `--profile`, `--inspect`, `--health-path`, `--health-timeout` and `--drain-timeout`; see `prnext pstart --help`. Change saved listener/settings with `pdelete`, then `pstart`. Applications load their project `.env` files; arbitrary caller-shell variables and secrets are not copied into the saved registry. The state directory defaults to `~/.prnext/processes`; set `PRNEXT_PM_HOME` consistently for a separate supervisor. Keep that directory private: it contains local control credentials and application logs. Logs rotate at 10 MiB with three retained segments per app and for the supervisor. App output can contain whatever the application logs.
291
497
 
292
- Apps restart after crashes with increasing delays and a retry limit. A separate watchdog supervises the manager. Replacements get a private build copy and must return HTTP 2xx/3xx from `--health-path /` within `--health-timeout 15000` ms before traffic switches; a failed candidate leaves the old instance serving. Existing requests can finish for `--drain-timeout 30000` ms. Dependencies, public files and disk caches remain shared. This adds a Node HTTP proxy; WebSocket upgrades are not supported and crashes can interrupt requests.
498
+ `pstartup` is opt-in and enables restoration on the **next user login**. On Windows it registers a hidden scheduled task for the current account; on macOS it writes a user LaunchAgent; on Linux it enables a systemd user service. These definitions restart a failed watchdog once managed by the OS. Windows and macOS user startup do not run before login. A Linux administrator can enable user lingering separately for boot-time operation without an interactive login. No OS startup settings change just by running `pstart`, and the command does not install a Windows system service or alter other accounts. Run `pstartup` again after moving or upgrading Node or the PRNext package. Platform references: [Windows scheduled tasks](https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/new-scheduledtasksettingsset), [Apple launch agents](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html), [systemd user lingering](https://www.freedesktop.org/software/systemd/man/252/loginctl.html).
293
499
 
294
- Settings, control credentials and logs live in `~/.prnext/processes` (`PRNEXT_PM_HOME` overrides it). Logs rotate at 10 MiB plus three older segments. Apps load project `.env` files; caller-shell secrets are not saved. `prnext pstartup` opts into restoration on the next user login through Windows Task Scheduler, a macOS LaunchAgent or a Linux systemd user service; `--remove` removes that startup configuration. Windows/macOS startup is per-user, not a pre-login system service. Linux boot-time operation requires administrator-configured user lingering. Re-run `pstartup` after moving or upgrading Node or PRNext. See the [persistent command guide](https://github.com/Lorkydey/PRNext#keep-production-apps-running).
500
+ ### Upgrade persistent apps
295
501
 
296
- Before upgrading a supervised application, follow the [persistent app upgrade guide](https://github.com/Lorkydey/PRNext#upgrade-persistent-apps).
502
+ The supervisor may be shared by several projects. Use the same user and `PRNEXT_PM_HOME` throughout, and record the applications and their settings first:
503
+
504
+ ```sh
505
+ npx --no-install prnext pstatus --json
506
+ ```
507
+
508
+ Before changing package files, stop the apps and supervisor using the **old installation**. These commands affect **all apps managed by that supervisor**, so schedule the interruption accordingly:
509
+
510
+ ```sh
511
+ npx --no-install prnext pstop --all
512
+ npx --no-install prnext pdown
513
+ ```
514
+
515
+ Install the update and rebuild each affected project as described under [Install & upgrade](#install--upgrade). Using the new CLI, restart each app with its original name, port and other options. For example:
516
+
517
+ ```sh
518
+ npx --no-install prnext pstart --name web --port 3000
519
+ ```
520
+
521
+ If the saved Node or native executable path changed, remove that app's saved entry with `prnext pdelete NAME`, then register it again with `prnext pstart` and its recorded options. This removes saved settings but retains logs. If login startup was enabled, re-run `prnext pstartup` to refresh its paths.
522
+
523
+ `pdown` alone keeps apps enabled for restoration; stop them first to prevent an old configuration from restarting during the upgrade.
297
524
 
298
525
  </details>
299
526
 
300
527
  <details>
301
- <summary><strong>Platforms</strong></summary>
528
+ <summary><strong>Develop from this repository</strong></summary>
302
529
 
303
- ### Platforms
530
+ ### Develop from this repository
304
531
 
305
- Native package targets cover Windows MSVC arm64/x64, macOS arm64/x64 and Linux glibc arm64/x64. Windows runs directly from PowerShell or Command Prompt, without WSL. Alpine/musl is not a prebuilt target. The native Linux CI builds use Ubuntu 22.04 with glibc 2.35. Framework and native components use matching release versions.
532
+ Install Node.js 22+ and Rust with rustup, then run:
306
533
 
307
- Keep npm optional dependencies enabled: PRNext automatically installs the binary for your platform. Rust is not required to use the published package.
534
+ ```sh
535
+ npm ci
536
+ npm run build:native
537
+ npm run dev -- examples/app
538
+ ```
308
539
 
309
- </details>
540
+ These commands also work in PowerShell and Command Prompt. On Windows, source builds require Visual Studio Build Tools with the Desktop development with C++ workload and a Windows SDK, plus NASM on x64. Rustup selects the repository's Rust 1.98 toolchain. Linux source builds need a C/C++ toolchain, NASM and pkg-config; macOS needs Xcode command line tools and NASM.
310
541
 
311
- ## Links and feedback
542
+ For production locally, run `npm run build -- examples/app`, then `npm run start -- examples/app`. Standalone output runs with `node server.js` on its build OS and architecture, including Windows. Published native packages remove the Rust/C++ build requirement for consumers. Keep the framework and platform components on matching release versions.
312
543
 
313
- - Website and benchmarks: [prnext.dev](https://prnext.dev)
314
- - Source and contributions: [Lorkydey/PRNext](https://github.com/Lorkydey/PRNext)
315
- - Project updates on X: [@PRNextdev](https://x.com/PRNextdev)
316
- - Bug reports: [GitHub Issues](https://github.com/Lorkydey/PRNext/issues)
317
- - Questions and suggestions: [contact@heythomas.dev](mailto:contact@heythomas.dev)
544
+ For source builds, `cargo build --release --locked --features gzip-zlib-rs` enables an experimental alternative gzip/deflate backend. Ubuntu x64 trials reduced JSON API CPU cost but increased cumulative streaming RSS, so this option is disabled by default. It also changes the Rust PNG/TIFF codecs; runtime profiles and Node heap settings remain unchanged. Compare CPU, response size and memory on your workload before choosing it.
545
+
546
+ </details>
318
547
 
319
548
  <details>
320
549
  <summary><strong>License & credits</strong></summary>
package/build/actions.mjs CHANGED
@@ -205,6 +205,10 @@ export function createServerActions({ projectRoot }) {
205
205
  transform(source,file,mode) {
206
206
  if(!source.includes('use server')) return source;
207
207
  const result=analyze(source,file);
208
+ // A Route Handler invokes the implementation as ordinary server code.
209
+ // It does not serialize functions through Flight or manufacture proxies.
210
+ // Analyze first to preserve validation and the shared action registry.
211
+ if(mode==='route') return source;
208
212
  if(mode==='rsc') return result.server;
209
213
  if(result.moduleServer) return `import {createServerReference} from 'prnext-internal:action-${mode==='browser'?'client':'ssr'}';\n`+result.declared.map((item,index)=>`const action${index}=createServerReference(${JSON.stringify(item.id)});export {action${index} as ${JSON.stringify(item.name)}};`).join('\n');
210
214
  if(result.inline) throw new Error(`Inline Server Actions cannot be imported into the client graph (${file}); move the action to a module marked 'use server'.`);
@@ -79,6 +79,7 @@ export async function validateAppConfig(file) {
79
79
  config.generateStaticParams = true;
80
80
  continue;
81
81
  }
82
+ if (item.name === 'maxDuration' && Number.isSafeInteger(value) && value >= 1 && value <= 300) { config.maxDuration = value; continue; }
82
83
  if (allowed.includes(value) || (item.name === 'revalidate' && Number.isSafeInteger(value) && value >= 0)) { config[item.name] = value; continue; }
83
84
  throw new Error(`App Router export ${item.name} in ${file} is not supported with this value.${allowed.length ? ` Supported literal values: ${allowed.map(value => JSON.stringify(value)).join(', ')}${item.name === 'revalidate' ? ', or a nonnegative integer' : ''}.` : ''}`);
84
85
  }
@@ -105,6 +106,7 @@ export function mergeAppConfig(configs) {
105
106
  }
106
107
  let forceDynamic = false;
107
108
  for (const config of configs) {
109
+ if (config.maxDuration !== undefined) merged.maxDuration = config.maxDuration;
108
110
  if (config.runtime === 'edge') merged.runtime = 'edge';
109
111
  else if (config.runtime === 'nodejs') delete merged.runtime;
110
112
  // The first explicit ancestor decides whether a blocking route is allowed.
package/build/app.mjs CHANGED
@@ -118,7 +118,7 @@ function boundaryEntry(boundary) {
118
118
  return `import * as client from ${JSON.stringify(boundary.file)};\n` + boundary.exports.map((name, index) => `const ref${index}=client[${JSON.stringify(name)}];export {ref${index} as ${JSON.stringify(name)}};`).join('\n');
119
119
  }
120
120
 
121
- function routeEntry(route, globalError) {
121
+ function routeEntry(route, globalError, dev) {
122
122
  if (route.kind === 'api') return `export * from ${JSON.stringify(route.file)};`;
123
123
  const imports = [`import * as page from ${JSON.stringify(route.file)};`, 'export {page};', `export const pageConfig=${JSON.stringify(route.pageConfig || {})};`];
124
124
  const rootLayout = route.segments.find(segment => segment.layout)?.layout;
@@ -144,6 +144,9 @@ function routeEntry(route, globalError) {
144
144
  if (route.routing || route.segments.some(segment => segment.error)) imports.push(`export {default as ErrorBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-error-boundary.cjs'))};`);
145
145
  if (route.routing) {
146
146
  let index = 0;
147
+ // Only scaffold objects owned by this compiler are immutable. Imported
148
+ // modules and config values retain their original live bindings/behavior.
149
+ const structural = source => dev ? source : `Object.freeze(${source})`;
147
150
  function serialize(value) {
148
151
  const files = [];
149
152
  for (const [name, file] of Object.entries(value.files)) {
@@ -151,7 +154,7 @@ function routeEntry(route, globalError) {
151
154
  imports.push(`import * as ${binding} from ${JSON.stringify(file)};`);
152
155
  files.push(`${JSON.stringify(name)}:${binding}`);
153
156
  }
154
- return `{path:${JSON.stringify(value.path)},segment:${JSON.stringify(value.segment)},pattern:${JSON.stringify(value.pattern)},layoutId:${JSON.stringify(value.layoutId)},config:${JSON.stringify(value.config || {})},interception:${JSON.stringify(value.interception || null)},files:{${files.join(',')}},children:[${value.children.map(serialize).join(',')}],slots:{${Object.entries(value.slots).map(([name, child]) => `${JSON.stringify(name)}:${serialize(child)}`).join(',')}}}`;
157
+ return structural(`{path:${JSON.stringify(value.path)},segment:${JSON.stringify(value.segment)},pattern:${JSON.stringify(value.pattern)},layoutId:${JSON.stringify(value.layoutId)},config:${JSON.stringify(value.config || {})},interception:${JSON.stringify(value.interception || null)},files:${structural(`{${files.join(',')}}`)},children:${structural(`[${value.children.map(serialize).join(',')}]`)},slots:${structural(`{${Object.entries(value.slots).map(([name, child]) => `${JSON.stringify(name)}:${serialize(child)}`).join(',')}}`)}}`);
155
158
  }
156
159
  const routing = serialize(route.routing);
157
160
  imports.push(`export const routing=${routing};export const interceptionOnly=${!!route.interceptionOnly};`);
@@ -263,8 +266,17 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
263
266
  return { path: args.path, namespace: 'prnext-app-empty' };
264
267
  });
265
268
  esbuild.onLoad({ filter: /.*/, namespace: 'prnext-app-empty' }, () => ({ contents: '', loader: 'js' }));
269
+ if (rsc) esbuild.onLoad({ filter: /^react$/, namespace: 'prnext-rsc-react' }, () => ({
270
+ // Keep the real singleton while preserving bundler-style CJS access.
271
+ // Mixed npm modules may export both server-safe components and unused
272
+ // hook-based components. Native ESM named imports would reject the
273
+ // entire module because React's server build has no client hooks.
274
+ contents: `import React from 'react';export default React;export const {${Object.keys(resolveFromProject('react')).filter(name => t.isValidIdentifier(name) && name !== 'default').join(',')}}=React;`,
275
+ loader: 'js',
276
+ }));
266
277
  esbuild.onResolve({ filter: /^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/ }, args => {
267
278
  if (args.pluginData?.prnextExportScan) return;
279
+ if (rsc && args.path === 'react' && args.namespace !== 'prnext-rsc-react') return { path: 'react', namespace: 'prnext-rsc-react' };
268
280
  return browser ? { path: resolveFromProject.resolve(args.path) } : { path: args.path, external: true };
269
281
  });
270
282
  if (!browser) {
@@ -446,14 +458,14 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
446
458
  }
447
459
 
448
460
  await mkdir(path.join(stage, 'assets'), { recursive: true });
449
- const entries = {};
461
+ const entries = {}, handlerEntries = {};
450
462
  for (const route of routes) {
451
463
  const entry = path.join(stage, '.entries', route.id + '.server.mjs');
452
464
  if ((route.handlerConfig?.runtime === 'edge' || route.cacheConfig?.runtime === 'edge') && manifest.config?.cacheComponents) throw new Error(`Cache Components cannot run in the Edge Runtime (${route.file}).`);
453
- await writeFile(entry, routeEntry(route, globalError));
465
+ await writeFile(entry, routeEntry(route, globalError, dev));
454
466
  if (route.cacheConfig?.runtime === 'edge' && route.kind === 'page') await edgeAppBuild(entry, path.join(stage, 'server', route.id + '.mjs'), 'rsc');
455
467
  else if (route.handlerConfig?.runtime === 'edge') await compileEdge({ file: route.file, outfile: path.join(stage, 'server', route.id + '.mjs'), projectRoot: project.root, dev, defineEnvironment });
456
- else entries[route.id] = entry;
468
+ else (route.kind === 'api' ? handlerEntries : entries)[route.id] = entry;
457
469
  const instantConfigs = [route.pageConfig, ...(route.segments || []).map(segment=>segment.staticConfig)];
458
470
  const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
459
471
  collectInstant(route.routing);
@@ -466,6 +478,9 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
466
478
  }
467
479
  manifest.app = { clientModules: {}, actions: {}, actionKey: actions.actionKey };
468
480
  const actionRevision = () => [...actions.actions.values()].map(action=>action.id+':'+[...(action.runtimes || [])].sort().join(',')).join(';');
481
+ // Route Handlers have a Node React environment and must not share RSC chunks
482
+ // that import registerServerReference or client-reference registration.
483
+ await serverBuild(handlerEntries, 'server', 'route');
469
484
  for (;;) {
470
485
  const revision = `${actionRevision()}:${boundaries.size}:${cssFiles.size}:${globalErrorCssFiles.size}`;
471
486
  for (const action of actions.actions.values()) {
@@ -0,0 +1,82 @@
1
+ import { build } from 'esbuild';
2
+ import { lstat, realpath, readdir, writeFile, unlink } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ // Paths used by the native worker, generated application modules, prerender,
6
+ // standalone tracing, compatibility modules and the runtime lifecycle contract.
7
+ // Other relative imports are resolved into shared chunks by this single build.
8
+ export const runtimeEntryNames = Object.freeze([
9
+ 'worker', 'rsc-worker', 'app-render', 'app-stream-html', 'transport', 'request-work',
10
+ 'http', 'app-static', 'route-static', 'render', 'api', 'middleware', 'incremental-cache', 'app-instant',
11
+ 'action-crypto', 'action-client', 'action-ssr', 'edge', 'metadata-route', 'og-loader',
12
+ ].sort());
13
+
14
+ /** Run once, after compiling application/Edge graphs and before any prerender. */
15
+ export async function bundleRuntime(stage) {
16
+ const resolvedStage = path.resolve(stage);
17
+ if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
18
+ throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
19
+ }
20
+ const runtime = path.join(resolvedStage, 'runtime');
21
+ const [stageInfo, runtimeInfo] = await Promise.all([lstat(resolvedStage), lstat(runtime)]);
22
+ if (!stageInfo.isDirectory() || stageInfo.isSymbolicLink() || !runtimeInfo.isDirectory() || runtimeInfo.isSymbolicLink()) {
23
+ throw new Error('Runtime bundling requires ordinary stage and runtime directories.');
24
+ }
25
+ const [canonicalStage, canonicalRuntime] = await Promise.all([realpath(resolvedStage), realpath(runtime)]);
26
+ if (path.relative(canonicalStage, canonicalRuntime) !== 'runtime') {
27
+ throw new Error('Runtime bundling escaped the build stage.');
28
+ }
29
+ const entries = await readdir(runtime, { withFileTypes: true });
30
+ const originals = entries.filter(entry => entry.name.endsWith('.mjs'));
31
+ if (originals.some(entry => !entry.isFile() || entry.isSymbolicLink())) {
32
+ throw new Error('Runtime source modules must be ordinary files.');
33
+ }
34
+ for (const name of runtimeEntryNames) {
35
+ if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
36
+ }
37
+ // write:false makes compilation atomic with respect to the source runtime:
38
+ // no files change if parsing or resolution fails. The virtual output is a
39
+ // sibling so all relative paths have the same depth as the final runtime.
40
+ const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
41
+ const result = await build({
42
+ absWorkingDir: runtime,
43
+ entryPoints: runtimeEntryNames.map(name => path.join(runtime, `${name}.mjs`)),
44
+ outdir: virtualOutput,
45
+ outbase: runtime,
46
+ entryNames: '[name]',
47
+ chunkNames: '_shared-[hash]',
48
+ outExtension: { '.js': '.mjs' },
49
+ bundle: true,
50
+ splitting: true,
51
+ format: 'esm',
52
+ platform: 'node',
53
+ target: 'node22',
54
+ packages: 'external',
55
+ // Edge's prepared VM is already a self-contained CJS artifact. Keeping it
56
+ // external preserves its delayed import and original execution boundary.
57
+ external: ['../compat/*', './profiles.json', './*.cjs'],
58
+ minify: false,
59
+ sourcemap: false,
60
+ metafile: true,
61
+ write: false,
62
+ logLevel: 'silent',
63
+ });
64
+ const emitted = new Map();
65
+ for (const file of result.outputFiles) {
66
+ if (path.dirname(file.path) !== virtualOutput || !file.path.endsWith('.mjs')) {
67
+ throw new Error('Runtime bundler emitted a file outside its flat output directory.');
68
+ }
69
+ emitted.set(path.basename(file.path), file.contents);
70
+ }
71
+ if (runtimeEntryNames.some(name => !emitted.has(`${name}.mjs`))) throw new Error('Runtime bundle omitted a required entry.');
72
+ // This is an unpublished, disposable stage. On any write failure buildProject
73
+ // discards the stage and preserves the previous published build unchanged.
74
+ for (const [name, contents] of emitted) await writeFile(path.join(runtime, name), contents);
75
+ for (const entry of originals) {
76
+ if (!emitted.has(entry.name)) await unlink(path.join(runtime, entry.name));
77
+ }
78
+ // JSON, CJS, license files and the OG assets directory are never traversed or
79
+ // removed. The caller writes the normal runtime contract after prerender.
80
+ return { entryCount: runtimeEntryNames.length, moduleCount: emitted.size,
81
+ bytes: [...emitted.values()].reduce((sum, contents) => sum + contents.length, 0) };
82
+ }
@@ -12,6 +12,7 @@ import { incrementalBuild } from './incremental.mjs';
12
12
  import { webpackGraph } from './webpack-graph.mjs';
13
13
  import { turbopackRules, sourceReader } from './turbopack-conditions.mjs';
14
14
  import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.mjs';
15
+ import { optimizeImportsPlugins } from './optimize-imports.mjs';
15
16
 
16
17
  let validateOptions;
17
18
  const compilation = new AsyncLocalStorage();
@@ -20,7 +21,7 @@ export function configureCompiler(options) { Object.assign(compilation.getStore(
20
21
  export async function compilerSource(file, fallback) {
21
22
  return compilation.getStore()?.loaded.get(file)?.source ?? fallback ?? await readFile(file, 'utf8');
22
23
  }
23
- const framework = /^(?:prnext-pages-compatibility|prnext-app-(?:rsc|ssr|browser)|prnext-edge-app|prnext-edge|prnext-middleware-compatibility)$/;
24
+ const framework = /^(?:prnext-pages-compatibility|prnext-app-(?:rsc|ssr|browser|route)|prnext-edge-app|prnext-edge|prnext-middleware-compatibility)$/;
24
25
  function condition(value, input) {
25
26
  if (value === undefined) return true;
26
27
  if (value instanceof RegExp) { value.lastIndex = 0; return value.test(input); }
@@ -120,6 +121,7 @@ export async function validateCompilerConfig(config, root, {dev=false,edge=false
120
121
  }
121
122
  export async function build(options) {
122
123
  const state = compilation.getStore();
124
+ if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
123
125
  // esbuild's shared service dispatches callbacks from its own async context.
124
126
  // Re-enter this build so transforms never inherit another project's map mode,
125
127
  // action registry or configuration from the first service invocation.