Commit Graph

4 Commits

Author SHA1 Message Date
Josh Story e0966df114 test: migrate caching deploy exclusions to force gates (#98154)
## Summary

- migrate Cache Components, PPR, prefetching, prerendering,
revalidation, and SSG deployment exclusions to `@force-gate`
- remove the corresponding `skipDeployment` options and `skipped`
control flow while preserving each suite's rationale

This keeps caching-related exclusions together so their deployed
semantics can be reviewed by the same owners.

## Verification

- verified on the combined top-of-stack tree with `pnpm typescript`
- compared ordinary-mode collection before and after: all 4,670 existing
test names matched
- collected all 510 affected files in deploy mode: 4,578 skipped tests,
zero failures

<!-- NEXT_JS_LLM -->
2026-09-09 08:25:36 -07:00
Niklas Mischkulnig e3790aa911 More granular cache keys for use-cache entries (#95233)
Caveats:
- I had to skip `react[-dom][/*]` and
`private-next-rsc-server-reference` and `private-next-rsc-cache-wrapper`
imports in the code hash and env-var tracking. Because those all end up
pulling in app-page-turbo.runtime.prod.js which reads many env vars and
would cause constant deopting. But this should still be correct. The
code of these imports is included via the Next.js version, and no env
vars should change the semantics of any of those imports.
- Static env var reads are collected, but too dynamic accesses are
silently ignored and don't lead to deopts (same goes for env var reads
in native NAPI addons). This means that this cache reuse is not
guaranteed to be 100% guaranteed to never lead to stale caches.
- Code hashing and env var collection happens on a per-module basis. So
if you put all use-cache functions into a single file and/or together
with react components, then you will see extraneous invalidations. This
will be fixed by either generally enabling module splitting for all of
Turbopack, or by adding a special transform that does it for use-cache
functions.

Followups:
- There are some env var static analysis gaps that will be immediate
followups before broader testing. These are the various TODOs added in
https://github.com/vercel/next.js/pull/95310
- Client components invalidation is very coarse grained right now
(statically imports any client component anywhere -> deopt completely).
Followup for the future. But the current setup is correct. This would
just improve effectiveness further
- Do this in dev as well. Currently there is no NFT at all in dev (for
performance reasons)
- Module splitting for more granular tracking
- Include entropy when serializing server reference arguments for cache
key


Todo:

- [x] Use implementation code hash (includes inlined env vars): from
#94234
- [x] Include non-inlined runtime env vars: from #95310
- [x] Include client reference manifest (very coarse for now)
- [x] Include Next.js version (for wire format, etc)
- [x] This is now done for all use-cache entries now. Not just for `use
cache: remote`. Is that the intended behavior? Yes
- [ ] ~~if `NEXT_DEPLOYMENT_ID` is in the env vars. just deopt and don't
care about stringifing and hashing the env vars~~
- [x] Is cache key size a problem? Currently you can get this: (values
are always hashed)
`CustomCacheHandler::get
["80e6f6560092f0078775e7e787c1c10ecf6dea0bc4",[],["d984fbaa996274737f3b59345a300a20","16.4.0-canary.5","__NEXT_NO_MIDDLEWARE_URL_NORMALIZE=undefined","NEXT_OTEL_PERFORMANCE_PREFIX=undefined","__NEXT_PRIVATE_ORIGIN=a04f4b9d6a8f42724740a480e9a2bc67c053dc04377be831c0e2c407a1422004","NEXT_PRIVATE_RESPONSE_CACHE_TTL=undefined","NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE=undefined","__NEXT_CACHE_COMPONENTS=b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b","__NEXT_ROUTER_BASEPATH=undefined","__NEXT_MANUAL_CLIENT_BASE_PATH=undefined","__NEXT_INSTRUMENTATION_CLIENT_ROUTER_TRANSITION_EVENTS=undefined","__NEXT_APP_NAV_FAIL_HANDLING=undefined","__NEXT_GESTURE_TRANSITION=undefined","__NEXT_USE_OFFLINE=undefined","NEXT_DEBUG_BUILD=undefined","__NEXT_VERBOSE_LOGGING=undefined...]]
[["_N_T_/layout","_N_T_/page","_N_T_/","_N_T_/index"]]`
2026-08-28 10:17:15 -07:00
Hendrik Liebau 286862e35b Fix stale dev 'use cache' for cookieless requests and route handlers (#96022)
In development, editing a file does not evict existing `'use cache'`
entries; an entry is invalidated only by re-keying, and the key includes
an HMR refresh hash. That hash was delivered through the
`__next_hmr_refresh_hash__` cookie, which the browser HMR client set
from each server-components change and sent back on later requests. Two
cases were therefore broken. First, any request that does not carry the
cookie (a `curl`, a plain `fetch`, a fresh browser profile, a second
device) served stale cached content after an edit, for both pages and
route handlers. Second, on Turbopack a route handler served stale
content even when fetched from a page whose HMR client had the cookie,
because a route-handler edit never advanced the hash there and so never
updated the cookie; on webpack that case happens to work, leaving only
cookieless route handlers broken.

The hash is server-authored to begin with (webpack's `stats.hash`,
Turbopack's `hmrHash` counter), so this change sources it directly on
the server and removes the cookie entirely. The hot-reloader exposes it,
and `base-server` attaches it to each request with `addRequestMeta`,
next to `serverComponentsHmrCache`, so it reaches every render,
including the internal Cache Components validation and warmup renders.
It is threaded onto the request store the same way
`serverComponentsHmrCache` is, so `'use cache'` is invalidated for every
client regardless of whether it ran the HMR client. Route handlers,
whose `NextRequest` does not carry request meta, receive it through the
app-route template's `renderOpts`.

Editing a route handler also has to advance the hash in the first place.
On webpack it already did, but on Turbopack the `app-route` entry wired
no change subscription, so a route edit recompiled the module without
advancing `hmrHash`; it now subscribes to the route endpoint so an edit
advances the hash without broadcasting a page refresh. That subscription
is also made the only thing that advances `hmrHash`: the
config-invalidation and server Fast Refresh reset paths previously
bumped it even without a content change, for example on a route's first
load, and with the hash now read synchronously instead of bounced back
through a cookie, such a bump would land between a route's cold write
and warm read and evict a still-valid entry. The getter returns the
counter unconditionally so the key is present and stable for every
request, matching webpack's always-present `stats.hash`.

With the cookie gone, the `hash` field on the `SERVER_COMPONENT_CHANGES`
HMR message has no remaining consumer, so it is dropped from the message
type and both bundlers.

This is a short-term stopgap until the granular implementation hash
(`codeHash` plus `runtimeEnvVars` plus the Next.js version) is used in
`next dev`, which invalidates server-authoritatively by construction and
will let this mechanism be removed.

> [!TIP]
> Best reviewed with hidden whitespace changes. 

closes NAR-894

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-07-22 15:50:21 +02:00
Hendrik Liebau 34dbafb46d Skip saving expire: 0 values in the default cache handler in prod (#95363)
A `'use cache'` value with an explicit `cacheLife({ expire: 0 })` is
expired the moment it is produced, so in production the `'use cache'`
wrapper regenerates it on every read rather than serving the stored
copy. The entry written to the cache handler is never served back, so
persisting it is wasted work, and for a remote handler a needless
round-trip and stored payload.

Whether to skip that write is left to each cache handler. This change
implements it in the built-in default handler; a custom handler can opt
into the same behavior. Beyond the saved write, this makes `cacheLife({
expire: 0 })` a reliable way to keep a value out of the server-side
cache handler, which covers two cases. In a conditional opt-out, a
function with an otherwise normal cache life sets `expire: 0` for a
result that is not worth persisting, for example when an upstream fetch
errored. In client-only caching, a value is `expire: 0` by design so
that it lives only in the client cache, via Cached Navigations or
Runtime Prefetches when the route opts in, and never on the server.

The skip is gated on `!process.env.__NEXT_DEV_SERVER`. In production the
value is regenerated on every read regardless, so not storing it changes
nothing that is served. Development keeps storing it, because the
default handler's minimum retention serves the previously cached value
across reloads while the entry re-warms in the background.

A new `use-cache-default-handler-expire-zero` end-to-end test enables
the default handler's debug logging (`NEXT_PRIVATE_DEBUG_CACHE`) and
asserts on its `set()` decisions. Because the debug output is only
available from the CLI, the test skips deployment.
2026-07-02 23:53:16 +02:00