* perf(build): parallelize prerender across a pool of render processes
Build-time prerender rendered every static route by fetching it from a
single in-process production server driven by a promise pool. React
SSR/RSC rendering is CPU-bound JS, so the pool only overlaps I/O — every
render serialized on one core, and raising --prerender-concurrency did
nothing. Next.js forks a worker pool and saturates every core.
Fork a pool of production-server child processes (one per core, capped)
and round-robin the per-route render fetch across them, keeping route
scanning, getStaticPaths/static-params resolution, file writing and the
manifest on the main process. Pool size scales by cores AND routes, so
small apps, low-memory machines, and --prerender-concurrency 1 keep the
single in-process server (no fork, no regression); running from source
(no built .js worker entry) also falls back to single-process.
child_process, not worker_threads: worker threads contend for CPU on this
workload (measured ~2x slower per route and non-scaling), which is also
why Next.js uses processes.
react.dev (809 routes, cold cache, same machine): 29.9s -> 15.1s, now
faster than its own Next.js build (~19.3s). An 801-route static fixture:
~22s -> ~2-6s. Prerender output is byte-identical to the single-process
path on deterministic renders; workers install the same NoOp cache
handler the in-process path uses. A worker that exits unexpectedly fails
the build loudly instead of shipping partial output.
* fix(build): harden prerender worker pool
---------
Co-authored-by: James <james@eli.cx>
* perf(client): omit unused server action client
* fix(client): capture server action state before lazy load
---------
Co-authored-by: James <james@eli.cx>
* fix(scss): preprocess SCSS CSS-module composes deps via SassAwareFileSystemLoader
postcss-modules' built-in FileSystemLoader reads files referenced by
`composes: className from './other.module.scss'` as raw text — without
Sass preprocessing. For .scss files this leaves SCSS variables and bare
@import directives in the CSS output, causing LightningCSS to crash with
"Invalid empty selector" during production builds.
Fix: introduce SassAwareFileSystemLoader that delegates to Vite's
`preprocessCSS` API for every `composes:` dependency, so Sass compilation
runs before CSS-module scoping. Register it via `css.modules.Loader` in
vinext's Vite config hook and thread the resolved config through via a new
`setSassLoaderResolvedConfig` call in the `configResolved` hook.
Fixes the following deploy-suite tests (both sass and sass-embedded variants):
- test/e2e/app-dir/scss/nm-module/nm-module.test.ts
- test/e2e/app-dir/scss/nm-module-nested/nm-module-nested.test.ts
- test/e2e/app-dir/scss/composes-external/composes-external.test.ts
Part of #1825 (non-tilde CSS-module/composes cases; complements #1881)
* fix(scss): address review feedback on SassAwareFileSystemLoader
- Guard the css.modules.Loader injection: respect a user's explicit
css.modules: false (mergeConfigRecursively would otherwise re-enable
CSS Modules with an object override) and never clobber a
user-provided custom Loader. The override is precomputed as a typed
const so the conditional spread doesn't disturb the config literal's
contextual typing.
- Surface composes preprocessing failures via config.logger.warn
instead of a bare catch {}, so missing composed classes are
observable; a missing/unreadable dependency file now rejects like
postcss-modules' FileSystemLoader did.
- Use Vite's exported preprocessCSS + PreprocessCSSResult type via a
top-level import instead of the lazy dynamic import and stringly
typed casts.
- Preserve parity for composes from non-*.module.* files: hand
preprocessCSS a virtual *.module.* filename so plain '.css' deps are
still scoped and export tokens extracted, matching the built-in
FileSystemLoader.
- Port the three failing Next.js fixtures (composes-external,
nm-module, nm-module-nested) as production-build regression tests in
tests/scss-composes.test.ts, plus a plain .module.css / non-module
.css parity test. All three SCSS tests reproduce the original
"[lightningcss minify] Invalid empty selector" crash when the
Loader injection is disabled.
- Add sass as a root devDependency so the SCSS tests (including the
previously always-skipped tests/scss.test.ts) actually run in CI;
drop the now-redundant knip ignore entry. Installing sass makes
Vite's SassPreprocessorOptions resolve to the concrete sass types
(previously any), so the tilde-importer's importers array now
carries an explicit SassPreprocessorOptions["importers"] cast.
* fix(scss): bind composes-loader config per plugin instance; scope extensionless deps
- Replace the module-level resolved-config singleton with a per-vinext-
instance binding (createSassAwareFileSystemLoader), so multiple builds in
one process never preprocess composes deps with another build's config.
- Cover extensionless composes deps in the virtual *.module.* rename
(append .module.css) so their tokens are scoped instead of silently
dropped; add a parity test case.
- Pin url() pass-through behaviour for composed deps in the parity test
(verbatim, never mangled into placeholders — matches the built-in
FileSystemLoader, verified against a vanilla Vite build).
* docs(scss): document the per-subtree recursion boundary of the composes loader
Notes the two structural consequences of delegating nested composes
recursion to preprocessCSS (per-subtree duplicate inlining collapsed by
LightningCSS, and no cross-boundary cycle short-circuit for invalid
circular composes), per review.
* fix(scss): fail the build on composed-dep preprocessing errors
Only a missing Sass implementation is downgraded to a logged warning;
any other preprocessCSS error (e.g. a Sass syntax error in the composed
dependency) now propagates and fails the build, matching postcss-modules'
built-in FileSystemLoader instead of shipping a green build with
silently-missing classes. Docstrings updated to accurately describe the
silent empty-token path when no resolved config is bound. Adds a
regression test for the fail-loudly path.
* chore(deps): move sass to the workspace catalog; drop the now-dead dynamic skip in scss.test.ts
Review feedback on #1882: sass belongs in the pnpm workspace catalog
rather than pinned inline in the root package.json. With sass now a
guaranteed root devDependency, the runtime describeIfSass skip in
tests/scss.test.ts is dead code — the suite always runs — so make the
unskip explicit by removing the conditional machinery.
* rebuild lockfile
* fix(app-router): isolate production page CSS chunks
Production App Router builds statically imported every page module into the generated RSC manifest. That let Vite/Rolldown concatenate page-level global CSS from sibling routes into shared CSS assets, so a sibling hash page with scroll-padding-top changed native hash scroll offsets on unrelated pages.
The manifest now emits cached dynamic loaders for page modules and loads the matched page module at the route dispatch boundary. Page generateStaticParams sources use the same loader with an explicit missing-export sentinel so prerender still distinguishes absent exports from malformed results.
* test(app-router): add focused unit test for route module loader
Address the non-blocking review note from PR #1738: per AGENTS.md, behavior
moved out of a template string should get a focused helper test. Cover
createRouteModuleLoader (promise dedup, rejection propagation),
loadRouteModules (page assignment, concurrent dedup, no-op cases, missing
`page` key) and loadRouteMatch (null short-circuit, match load).
Also cover createLazyGenerateStaticParamsSource (delegation, missing
sentinel for null/empty/non-function exports).
Document the rejected-load cache intent and the per-request intercept copy
in the WeakMap so future readers don't try to "fix" them as bugs.
* test: stabilize dev overlay HMR recovery
* chore: retrigger ci
* chore: fix knip check config
* test(app-router): guard intercepted modal CSS + load lazy page before probe
Adds the positive-direction guard the bonk review asked for: navigating
/feed -> intercepted /photos/[id] must load the lazy modal page and emit
its scroll-padding-top CSS chunk. The existing test only proved the
negative (CSS absent on a direct /feed visit), so a regression where the
lazy modal page failed to load its CSS would have passed silently.
Also fixes the probePage() intercept branch, which read intercept.page
without resolving the now-lazy __pageLoader. The render path awaits the
loader (resolveAppPageInterceptState), but the probe path did not, so the
dynamic-bailout probe silently inspected an undefined component and never
observed the intercept page's searchParams/headers access. Resolve the
lazy page before probing to match the render path.
---------
Co-authored-by: James <james@eli.cx>
* chore(deps): bump knip and node types
Bump Knip to 6.16.0 and @types/node to 25.9.2.
Knip 6.16 reports previously unreported unused exported types, stale ignore entries, and external CLI binaries. Remove unnecessary type exports, refine the script entry coverage, and keep only narrow binary ignores for system or user-project commands.
Validation: vp run knip; vp run check.
* chore: retrigger ci
* refactor(cache): extract Cloudflare cache adapters into @vinext/cloudflare
Move the Cloudflare KV data cache and edge CDN cache adapters out of
vinext into a new publishable @vinext/cloudflare package:
- cache/kv-data-adapter(.runtime).ts (KVCacheHandler, kvDataAdapter)
- cache/cdn-adapter(.runtime).ts (CloudflareCdnCacheAdapter, cdnAdapter)
tpr.ts stays in vinext. vinext now depends on @vinext/cloudflare
(workspace:*) and the package declares vinext as a peer dep; both build
from source via tsconfig paths so there is no build-order cycle. The
vinext/cloudflare barrel still re-exports KVCacheHandler for back-compat.
Wires up tsconfig paths, a vitest source alias, root build/postinstall,
and the preview/publish workflows for the new package. Updates internal
consumers (apps/web, examples/workers-cache), docs, and tests.
* ci(create-next-app): install @vinext/cloudflare from local tarball
vinext now depends on @vinext/cloudflare, which isn't published to npm
yet. The create-next-app smoke test packs vinext locally and resolves
its deps from the registry, so the install (and dev server) failed with
ERR_PNPM_FETCH_404 for @vinext/cloudflare.
Pack @vinext/cloudflare alongside vinext and add a pnpm override in the
scaffolded project pointing at the local tarball so the dependency
resolves offline.
* refactor(cloudflare): address review feedback
- Remove the root barrel export from @vinext/cloudflare; expose only the
./cache/* subpaths via a wildcard export (no root main/types).
- vinext/cloudflare re-exports KVCacheHandler from the full subpath.
- Drop the redundant .npmignore (the package.json "files" allowlist
already restricts the publish to dist).
- Remove the unsupported imperative setCacheHandler/KVCacheHandler usage
from both READMEs; the cache plugin config is the supported approach.
- Simplify test wiring: drop the now-unused @vinext/cloudflare tsconfig
path and dedupe the vitest source alias into a shared constant.
* chore(cloudflare): drop unused vite devDependency
The @vinext/cloudflare config uses vite-plus and nothing imports vite, so
the vite devDependency was unused. build/check/knip stay green without it.
* Apply suggestion from @james-elicx
* feat(skip): add layout safety observation foundations
Introduce the per-layout observation primitives that later slices need
to decide whether retained static layouts can be reused. This slice
adds the machinery only; nothing reads the observations yet, so
behavior is unchanged.
- app-layout-param-observation: per-layout completeness, param keys,
structural scope, finite revalidate, request API, cacheLife, cache
tag, cacheable fetch, dynamic fetch, and unstable_cache observation
tracker.
- app-page-probe: bounded React server subtree probe that walks
layout-returned children including memo, forwardRef, and React.lazy
wrappers, enforces depth and node limits, and refuses single-use
iterable children.
- shims: thenable params observer for sync, await, destructure, and
enumeration. Cache, fetch, and unified request context record
unstable_cache, cacheable fetch, and request-scoped observation
state.
- app-page-params: structural segment-param scope so empty optional
catch-all targets are not treated as param-free downstream.
No skip transport behavior. Later slices wire the planner, manifest,
and encoder onto these observations.
* fix(skip): address review feedback on observation foundations
- Add currentFetchSoftTags reset in runWithIsolatedProbeDependencies to prevent soft tag leak from parent scope (matches runWithFetchCache isolation)
- Replace unnecessary JSON.stringify in cacheLife check with direct null comparison
- Add comment documenting intentional omission of markProbeComplete on probe errors (keeps completeness as unknown, safe fallback to render-and-send)
* fix(skip): record cacheable-fetch observation synchronously before first await
The patched fetch deferred observation and tag collection past
await buildFetchCacheKey(). If a layout probe started a cacheable
fetch and returned synchronously without awaiting it, the probe
snapshot would see cacheableFetchCount: 0 -- a false proof that
could let the skip planner reuse a stale retained layout.
Move observation and tag recording before the first await in the
cacheable branch. If key generation later fails and the fetch falls
back to dynamic, recording both is conservative (false unsafe costs
performance, not correctness).
Add an integration test that exercises the real patched fetch shim
without mocks to verify synchronous observation capture.
* fix(test): remove polluting fetch restore and catch background promise
Do not restore globalThis.fetch in test cleanup. ensureFetchPatch
sets a global _PATCH_KEY guard; restoring fetch without clearing
that marker makes later calls skip reinstallation silently.
Catch the background fetch promise inside the probe so the patched
fetch continuation (cache lookup, network fetch) cannot produce an
unhandled rejection after the synchronous assertions complete.
* chore: empty commit to trigger CI
* chore: empty commit to trigger CI
* feat(skip): add static layout reuse proof model
Add the proof/planner vocabulary that decides whether a verified
static layout entry from a client reuse manifest is reusable.
Nothing renders this decision yet, so no payload is omitted.
- skip-cache-proof: static layout reuse planner with per-entry budget,
exact artifact-compatibility check, payload hash, route id, and
invalidation proof. Preserves rejection traces. Returns the existing
disabled disposition when not authorized.
- static-layout-client-reuse-proof: static layout payload hash, route
id, and artifact compatibility helpers used by the planner.
- client-reuse-manifest: extends the rejection code union with the
layout-safety rejection codes the planner emits, and adds the
enabled skip disposition variant so future slices can carry an
authorized result without retyping the union.
This slice is the proof vocabulary only. The planner is callable but
not invoked from the render path; encoder behavior is unchanged.
* fix(skip): address review — budget counts all wire entries, default enforcement, triple-walk justification, hash stability comment, forward declarations comment, absent-manifest test
* fix(skip): replace as const with explicit return type annotation
* fix(skip): replace as const with ProofFieldPair type alias
* fix(skip): address copilot review — pairs serialization, budget disambiguation, skip-ineligible tracking, compatibility-bridged test
* fix(skip): remove routeId from renderEpoch, bind verifier entryId, add sibling-route test
* fix(skip): exclude routeId from payload hash, add end-to-end sibling-route proof test
* chore(skip): move proof fields to artifact-compatibility, rename to maxWireEntriesToVerify, short-circuit exact check
* chore(skip): address review comments — fix hash comment, remove redundant guard, drop budget alias, dedupe exact-compatibility check
* feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest
Adds a /compatibility page to apps/web that visualises Next.js
compatibility over time, populated by the nightly deploy-suite workflow.
- D1 binding (`vinext-metrics`) with Drizzle schema and migrations.
Tables are namespaced (`compat_runs`, `compat_file_results`) so
future metric kinds can live alongside.
- POST /api/compatibility ingest endpoint. Authenticates with a
`COMPAT_INGEST_SECRET` worker secret and upserts by
(kind, runKey).
- GitHub-style contribution grid (one dot per test file) and a
pass-rate-over-time line chart. Both are SVG, no chart libs.
Pass rate excludes Next.js-skipped tests from the denominator.
- Shared header/footer extracted into app/_components and rendered
by the root layout. Nav uses next/link for soft client nav.
- Workflow change: on top of the existing per-test pass/fail/skip
aggregation, the report job now POSTs results to the ingest
endpoint. Guarded to only run when (a) filter=all and
(b) targeting main, so partial runs and branch spot-checks don't
pollute the historical record.
The first nightly run after merge will seed the production D1.
* address review + add ISR with KV cache
Review fixes (apps/web/app/api/compatibility/route.ts):
- constant-time secret comparison via SHA-256 + bytewise XOR
- single-statement atomic upsert with onConflictDoUpdate (no
SELECT->INSERT race for concurrent retried runs)
- top-level try/catch with structured error body so workflow logs
show why ingest failed instead of a generic 500
- MAX_FILES=2000 cap in validation as defensive bound
Page perf (apps/web/app/compatibility/page.tsx):
- latest-run and trend D1 queries run in parallel via Promise.all
ISR + KV:
- new vinext-web-cache KV namespace bound as VINEXT_CACHE
- worker installs KVCacheHandler once per isolate
- both pages opt into ISR with revalidate = 300; compat page drops
force-dynamic
* address bonk re-review
- Drop the ?kind= query param. Only one kind exists today; gating it
behind an unused param invited ISR cache pollution. Hardcoded to
"deploy" with a comment pointing future kinds at a dedicated route.
- Wrap the DELETE + chunked INSERTs in a single `db.batch()` so a
crash mid-write can no longer leave a run with zero file results.
D1 executes batch statements inside one transaction.
- Resolve workflow expressions once via step-level `env:` and read
them as `process.env.*` from the github-script body. Removes the
fragile splicing of `${{ inputs.next-ref || 'v16.2.6' }}` into JS
string literals (the nested single quotes were the worst offender).
* fix(pages-router): include _app module assets in served page HTML
When a Pages Router page renders, vinext's prod server collects asset
tags only for the matched page module. _app.tsx — which wraps every
page — is omitted, so any CSS/JS it imports is built and emitted but
never <link>ed from the rendered HTML.
For SCSS users this is the LHF-5 cluster from the Next.js deploy suite:
when global.scss is imported via _app.tsx, Vite compiles it correctly
and emits assets/_app-*.css, but the browser never sees a
<link rel="stylesheet"> for it. The page renders with the user-agent
default colour (rgb(0, 0, 0)) instead of the SCSS-defined colour.
Fix: embed the resolved pages/_app file path at build time and include
it in the moduleIds passed to collectAssetTags, so the SSR manifest's
CSS/JS entries for _app are emitted alongside the page's own.
Includes a regression test (tests/scss.test.ts) plus a fixture page
that imports a .scss file with a variable. The test covers App Router
dev, App Router production build, Pages Router dev, and the Pages
Router production server end-to-end (build → start prod server →
fetch page → assert linked stylesheet has the resolved colour).
Skipped automatically when sass is not installed — vinext relies on
Vite's built-in SCSS preprocessor, which requires the user to install
sass (or sass-embedded) themselves. Matches Vite's model.
* fix: isolate SCSS fixture and silence TS check for optional sass import
The first revision of this PR added an SCSS fixture under
tests/fixtures/app-basic/app/nextjs-compat/scss-test/. That directory
is scanned by every test that boots the shared app-basic fixture, so
when sass is not installed (CI default) every page in app-basic fails
the route-graph scan and 500s. jsx-in-js, app-css, node-modules-css,
and ~dozens of integration tests went red as a result (CI run
https://github.com/cloudflare/vinext/actions/runs/25913568939).
The TypeScript check job also failed because `await import("sass")`
produces TS2307 when sass isn't declared as a workspace dep.
Fixes:
- Drop the shared fixture under app-basic. tests/scss.test.ts now
materialises its Pages Router SCSS fixture in a per-test tmpdir
(same pattern as the existing Pages Router scenario), so the shared
fixture's route-graph scan never sees a .scss import. No other test
is affected.
- The App Router test scenarios from the first revision are removed.
They required additional node_modules wiring for @vitejs/plugin-rsc
to resolve from a tmpdir and weren't needed to validate the fix —
the actual change is in the Pages Router asset-collection path, and
the Pages Router tests exercise it end-to-end (dev + production
build + linked stylesheet assertion).
- Replace @ts-expect-error with @ts-ignore on the dynamic sass import.
sass may or may not be installed depending on the environment; when
it is, there's no type error to expect and @ts-expect-error itself
errors as unused. @ts-ignore is the right tool for an optional peer.
The pages-server-entry.ts fix from the first revision is unchanged —
this commit only re-shapes the test surface so it doesn't poison the
shared fixture.
* feat(server): add disabled cache proof model
Route-scoped cache work currently lacks a typed proof boundary for representing render observations, variants, and breaker fallback decisions. That makes future cache reuse work easy to wire before proof completeness is explicit.
The missing invariant is that cache proof data must be canonical, redacted, bounded, and incapable of authorizing runtime reuse while the model is disabled.
Add a v0 cache proof model with RenderObservation, CacheVariant, BoundaryOutcomeCompatibility, variant budgets, and breaker fallbacks. The disabled decision always returns canReuse false, and tests cover canonical redacted dimensions, budget breaker paths, negative request API proof completeness, exact boundary outcome matching, and the disabled reuse gate.
Refs #726
* fix(server): use distinct invalid cache budget code
Invalid cache proof budget fields currently report the route variant ceiling reason code, even when the failing field is unrelated to route cardinality.
That conflates malformed configuration with an actual per-route variant ceiling breach.
Add CP_INVALID_VARIANT_BUDGET for invalid budget fields while leaving CP_ROUTE_VARIANT_CEILING_EXCEEDED for real route variant count failures. Cover maxEncodedLength as the regression case.
Refs #726
* fix(server): harden disabled cache proof invariants
Disabled cache proof variants currently collapse null and empty-string output scope fields into the same encoded value. That makes a future empty-string field addition able to collide silently with absent values.
The matcher also leaves two defensive contracts implicit: breaker fallback detection depends on the generic kind field, and the private boundary matcher would treat unknown as a match if called directly.
Preserve null through JSON output encoding, use the fallback-specific code field for the internal breaker guard, and make unknown outcomes non-matching at the matcher level. Add coverage for null versus empty-string scope keys and for missing request API observations not counting as negative proof.
Refs #726
* refactor(app-rsc-entry): delegate pure helpers to typed server modules
The generated RSC entry template contained five inline implementations of
pure runtime helpers: post-middleware request context construction,
RSC error handler creation, request context cleanup, RSC stream preload
hint normalization, and hook warning suppression. These inline blocks
repeated runtime behavior across multiple call sites and could not be
unit-tested independently.
Move each helper to a dedicated typed module under server/ so the
generated entry imports and wires them instead of owning the logic:
- buildPostMwRequestContext → server/app-post-middleware-context.ts
- createRscOnErrorHandler → server/app-rsc-error-handler.ts
- __clearRequestContext + setNavigationContext → server/app-request-context.ts
- renderToReadableStream preload hints → server/rsc-stream-hints.ts (createRscRenderer)
- hook warning console.error patch → server/app-hook-warning-suppression.ts
Each helper module has focused unit tests. The entry template is 55 lines
shorter and now describes the app shape; the typed modules own behaviour.
* fix: remove redundant as cast and fix test module caching
app-hook-warning-suppression.ts contained an `as string` cast that was
redundant because the typeof check on the prior line already narrows the
type. The hook warning suppression tests in tests 2-4 were passing
vacuously because dynamic import() returned the cached module whose
console.error patch was no longer active after test 1 restored the
original. Use vi.resetModules() before each test to force fresh module
evaluation.
clearAppRequestContext now delegates to setAppNavigationContext(null)
instead of calling the raw navigation and root-params setters directly,
matching the DRY pattern from the original inline code.
* fix: resolve entry helper rebase conflicts
* ci: rerun checks (font-google flake)
* Update packages/vinext/src/entries/app-rsc-entry.ts
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
---------
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* refactor(app-router): extract buildPageElements into typed helper module
The generated RSC entry template owned the central element-construction
path (buildPageElements) as an inline template function with ~110 lines
of behaviour. This made the code untestable directly and forced any change
to element-tree construction to be verified through snapshot diffs of the
generated output rather than through behavioural assertions.
The generated entry should describe the app shape (route manifests,
module references); runtime behaviour belongs in normal typed modules.
Extract buildPageElements into a new server/app-page-element-builder.ts
module with fully typed options (BuildPageElementsOptions), a route shape
(AppPageBuildRoute extending AppPageRouteWiringRoute), and intercept
types. The generated entry now delegates to this module with a 12-line
closure that passes the per-app module references (globalErrorModule,
rootNotFoundModule, etc.).
Next.js equivalents:
- create-component-tree.tsx -- builds the nested React element tree
- create-metadata.tsx -- resolves page head metadata including parallel
route interpolation
Behavioural changes: none. All existing app-router behaviour is preserved.
Tests added: 9 focused unit tests covering error payload construction,
interception context threading, search-params-triggered dynamic marking,
slot override wiring, and thenable-params proxy behaviour.
* chore: register app-page-element-builder in knip entry list
The module is imported by the generated RSC entry template as a string
literal, so knip cannot statically trace the dependency. Same pattern as
app-page-dispatch.ts, app-page-head.ts, and prerender-work-unit-setup.ts.
* refactor(app-router): extract app page dispatch
App Router page requests now enter a shared dispatch module after generated route matching. The generated RSC entry still describes route modules, app-shape callbacks, and request-local values, but cache reads, static generation context, dynamic param checks, intercept responses, and lifecycle rendering move into normal typed code.
This keeps page orchestration out of codegen without changing the observable App Router behavior covered by the integration suite.
* chore: rerun ci
* fix: unstable_io() returns hanging promise during prerendering (#972)
During prerendering (static export, --prerender-all, TPR), unstable_io()
must return a hanging promise to suspend React past the IO boundary.
Previously it always returned a resolved promise, matching only the
browser/client implementation.
Changes:
- Add makeHangingPromise utility (never-resolving promise, rejects on abort)
- Define typed WorkUnitStore discriminated union in workUnitAsyncStorage
- unstable_io() branches on work unit store type per Next.js's io.ts
- Set prerender work unit store in RSC entry when VINEXT_PRERENDER=1
- Add tests for prerender, request, cache, and abort scenarios
* fix: add knip entries for prerender-work-unit and work-unit types (#972)
* fix(prerender): address PR review feedback for unstable_io work unit store
- AbortController now properly aborted in .finally() to prevent listener leaks
- Add optional route field to PrerenderStore for better error messages
- Pass pathname as route in app-rsc-entry handler
- Use actual route instead of hardcoded 'unknown' in unstable_io()
- Add explicit return in default branch of cache.ts for clarity
- Remove premature knip entry for make-hanging-promise.ts
- Replace V8-internal .status check with Promise.race pattern in tests
* fix(app-rsc-entry): place route option in correct location
Move { route: __pathname } to __runWithPrerenderWorkUnit() call
instead of incorrectly appending to _handleRequest closing brace.
Fixes parse error introduced in 035a54f.
Refs #979
* fix: address prerender hanging promise review feedback
- Suppress unhandled rejection when signal already aborted in makeHangingPromise
- Add lazy pathname extraction via getter function in runWithPrerenderWorkUnit
- Add listener array cleanup after abort in makeHangingPromise
- Add test for unhandled rejection suppression on early-return path
- Update entry template snapshots for lazy route getter
* fix: resolve merge conflicts with main for prerender work unit
- Take main's refactored app-rsc-entry.ts as base (extracted runtime primitives)
- Apply prerender-work-unit wrapping around handler function
- Update tests to match refactored code structure (delegated helpers)
- Update entry-templates snapshots for new generated code structure
* fix: resolve merge conflicts with main for test files
- Keep main's tests/app-router.test.ts (already has correct refactored tests)
- Update entry-templates snapshots to include prerender-work-unit generated code
Middleware cookies set through NextResponse only updated Set-Cookie, so cookies() in the same request could not observe values written by middleware. External middleware rewrites were normalized to pathname and search, so cross-origin destinations were routed locally instead of being proxied.
Mirror middleware cookie mutations into x-middleware-set-cookie and merge that internal header back into the request cookie store. Preserve cross-origin rewrite URLs through middleware execution and proxy them across app, pages, prod, and deploy request paths, while stripping internal middleware headers from client and upstream responses.
Cached App Router pages only carried implicit revalidatePath tags derived from the resolved URL. That left dynamic routes tagged with concrete values, so pattern-scoped invalidation like revalidatePath("/blog/[slug]", "layout") could miss entries rendered at /blog/hello.
The generated RSC entry now delegates implicit tag construction to a typed helper that combines exact pathname tags with route-segment-derived layout, page, and route handler tags. Focused tests cover dynamic route patterns, route groups, root aliases, and route handler leaves.
Refs cloudflare/vinext#921