The vinext:image-imports transform rewrote `import X from './a.png'` into
two imports plus `const X = {...}`. The original import is a module-scoped
binding initialized before module-body execution, but the `const` is
block-scoped and TDZ-bound until its textual line, so any reference to `X`
that runs earlier — a hoisted function called above the import, or a
circular-import re-entry — flipped from working to a runtime
`Cannot access 'X' before initialization`.
Emit `var X = {...}` instead: it hoists like the import it replaces, so the
forward reference no longer throws (it reads `undefined` before the line,
strictly better than crashing). Matches Next.js, which leaves the consumer's
`import X` untouched and rewrites the image module itself.
Closes#1975
next/dynamic's ssr:false server branch rendered the loading component with
pastDelay:false, while the client's first (pre-mount) render and Suspense
fallback default pastDelay to true. For a loading component that branches on
pastDelay (the documented `if (!pastDelay) return null` pattern), the server
emitted nothing but the client rendered the spinner, causing a React hydration
mismatch and a visible content jump.
Drop the hardcoded pastDelay:false override on the server so it defaults to
true, matching both the client render and Next.js App Router, which always
renders the loading fallback with pastDelay=true on server and client. Loading
components that ignore pastDelay are unaffected.
Update the three tests that asserted the buggy server-side pastDelay:false, and
add a server-equals-client-first-render parity assertion.
Fixes#1967
The progressive (form-state) non-redirect server-action path returned the
raw `getAndClearPendingCookies()` array, so calling `cookies().set(name, ...)`
twice for the same name emitted two `Set-Cookie` headers for that name on
no-JS form submissions. Next.js stores mutable cookies in a name-keyed
`ResponseCookies` map (last value wins) and collapses to one header, as do the
redirect and RSC action paths in vinext via `dedupePendingCookies()`.
Wrap the non-redirect branch's cookie collection in `dedupePendingCookies()`
to match the sibling paths, so same-name sets collapse to a single Set-Cookie
(last value wins). The single binding feeds both the action-failed and success
returns, covering both non-redirect sub-cases.
Follow-up to the #1481 dedup fix, which landed on the redirect and RSC paths
but missed the progressive non-redirect branch.
Adds a regression test mirroring the existing redirect-dedup test for the
non-redirect form-state path.
createRscOnErrorHandler treated ANY error carrying a digest field as a
control-flow signal: it returned String(error.digest) and skipped
reportRequestError, so a genuine server-render error that merely carried a
digest (e.g. a hashed digest from sanitizeErrorForClient, or one transported
from a nested boundary) was silently swallowed with no instrumentation and
its digest shipped to the client as if it were a signal.
Mirror Next.js getDigestForWellKnownError: add a helper that recognizes only
genuine signals (redirect / notFound+http-fallback / bailout-to-CSR /
dynamic-server) and gate the short-circuit on it. Non-signal digest errors now
fall through to reportRequestError and keep their existing digest as-is.
Fix the same conflation in createAppPageRscErrorTracker.onRenderError, which
routed any digest-bearing error to the special 307/404-swap slot; gate it on
isNavigationSignalError so real errors reach the error boundary and a later
real signal can still win the slot.
Closes#1978
App Router page responses commonly already carry the internal RSC Vary header before response finalization runs. Re-running the generic Vary merge in that case splits and deduplicates the known header value on every SSR response without changing the result.
Read the current Vary value first, set the internal header directly when missing, and only fall back to the generic merge path for custom Vary values that still need preservation and deduplication.
* perf(app-router): defer layout, template and boundary modules to first request
The generated RSC entry statically imported every route's layout, template,
loading/error/not-found/forbidden/unauthorized boundary and parallel-slot
module at the top level. On a cold isolate — and on the dev server's first
request — that eagerly evaluated the whole app's segment tree, including
routes never visited in that lifetime. Page and route-handler modules were
already lazy; the surrounding segment modules were not.
Emit those modules as lazy `() => import()` loaders paired with `null`
placeholders in the manifest, and hydrate them onto the route's synchronous
fields in `ensureAppRouteModulesLoaded` before any read (extended from
page/handler to layouts, templates, boundaries and parallel-slot modules).
Intercept layouts hydrate via the dedicated `loadAppInterceptLayouts` because
they live on the match object, not the route. Every reader already runs behind
`ensureAppRouteModulesLoaded` (handler dispatch and `buildPageElements`) or the
intercept resolvers, so no synchronous module field is read before hydration.
The shared intercept `interceptLayouts` arrays keep their `readonly` public
types; the loader is their sole writer and fills slots through one
boundary-local cast, so route metadata stays immutable to callers.
Dev cold start is unchanged on page-heavy apps — pages were already lazy — but
on a boundary-heavy app (80 independent sections, each with
layout+template+error+loading+not-found) first-request module compile drops
704ms -> 534ms (~24%) and time-to-first-200 1917ms -> 1665ms. The saving scales
with the number of unrelated segments deferred.
* test(app-router): localize react max header font fixture
* fix(app-router): dedupe lazy intercept module loads
* fix(app-router): share intercept load state across matches
* docs(app-router): explain root layout loader reuse
---------
Co-authored-by: James <james@eli.cx>
* fix(proxy): match Next.js file conventions
* test(proxy): keep convention fixture out of workspace
* test(proxy): provide middleware path in handler fixture
sendCompressed wrote the full body for every request. HEAD requests routed
through it (bot/crawler-buffered Pages HTML and Pages API routes) still
compressed and emitted a payload that Node then discards at the socket level.
Short-circuit with res.end() after writeHead when req.method is HEAD,
mirroring sendWebResponse. This skips spinning up a compressor for a body
that would be thrown away and keeps the buffered sender consistent with the
streamed and static-file senders, which already handle HEAD.
Co-authored-by: James <james@eli.cx>
* perf(ssr): isolate open redirect detection
* perf(ssr): isolate stream cleanup runtime
* Apply suggestions from code review
Co-authored-by: James Anderson <james@eli.cx>
* remove comments
---------
Co-authored-by: James Anderson <james@eli.cx>