Commit Graph

31 Commits

Author SHA1 Message Date
James 92aa5cee9b fix(cache): complete cacheable route handler responses 2026-08-27 01:56:39 +01:00
James 9f7de189ea test(cache): allow existing Pages warm fills 2026-08-27 00:21:57 +01:00
James c739b6b31f fix(cache): avoid cookie response variation 2026-08-27 00:21:57 +01:00
James 9c000472ea fix(cache): isolate request-conditioned CDN responses 2026-08-27 00:21:57 +01:00
James a6ea94ec94 test(cache): cover RSC manifest admission 2026-08-27 00:21:57 +01:00
James 542bcff588 fix(cloudflare): use the full staged discovery deadline 2026-08-27 00:17:31 +01:00
James Anderson 4146d69021 fix(cloudflare): verify Worker version IDs during CDN warmup (#3072)
* fix(cloudflare): revalidate stale warmup retries

* fix(cloudflare): report CDN warmup retry progress

* test(cloudflare): retry prewarm verification on fresh Worker

* feat(cloudflare): configure CDN readiness probes

* fix(cloudflare): validate CDN warmup version overrides

* fix(cloudflare): verify CDN warmup version ids

* test(cloudflare): opt into dangerous warmup fallback

* fix(cloudflare): harden warmup version validation

* docs(cloudflare): clarify version binding environments
2026-08-24 23:03:15 +01:00
James Anderson e2e2b177eb fix(cloudflare): complete warmup response and promotion contracts (#3046)
* test(cloudflare): prove deployed Pages CDN warmup

* fix(cloudflare): reject incomplete no-promote warmup

* fix(cloudflare): keep warm discovery fail-closed

* test(cloudflare): prove prewarmed App HTML reuse

* fix(cloudflare): enforce no-promote warmup contract

* fix(cloudflare): warm terminal ISR responses
2026-08-24 23:03:14 +01:00
James Anderson f1efcc1e60 fix(cloudflare): harden canonical RSC warmup end to end (#3040)
* fix(cloudflare): validate reusable warm responses

* fix(cloudflare): retry staged warmup failures safely

* fix(cloudflare): extend staged propagation retries

* fix(build): filter Pages-owned RSC warm paths

* fix(cloudflare): keep CDN warmup correct with custom adapters and i18n

* fix(cloudflare): reject unusable CDN warm responses

* fix(cloudflare): keep CDN warmup bound to the deployed build

* fix(router): reuse warmed RSC on locale-prefixed hybrid routes

* fix(cloudflare): verify exact build and variant during CDN warmup

* fix(router): keep warmed RSC ownership rewrite-safe

* test(cloudflare): prove browser reuse of warmed RSC variants

* test(cloudflare): bind RSC warm proof to each deploy attempt

* fix(rsc): keep deferred loading shells contextual

* fix(build): keep CDN warm discovery route-correct

* fix(cache): preserve valid CDN policy during RSC warmup

* fix(cloudflare): warm the deployed Worker route host

* fix(cloudflare): preserve staged warmup guarantees

* fix(build): discover localized Pages warmup paths

* fix(cloudflare): wait for staged Worker before CDN warmup

* fix(cloudflare): preserve strict warmup deploy invariants

* fix(build): fail closed during CDN warm discovery
2026-08-24 23:03:14 +01:00
James Anderson 369e031883 test(cloudflare): preserve independent RSC warm proof (#3020)
* test(cloudflare): preserve independent RSC warm proof

* test(cloudflare): allow cache propagation after promotion
2026-08-24 23:03:13 +01:00
James Anderson 8bb09481d6 feat(cloudflare): prewarm canonical ISR RSC requests (#3002)
* feat(cloudflare): prewarm canonical ISR RSC requests

* fix(cloudflare): forward preview deploy options

* fix(cloudflare): harden canonical RSC prewarming

* fix(test): wait for preview alias propagation

* fix(cloudflare): guarantee exact RSC prewarm reuse

* fix(cloudflare): harden canonical RSC prewarming

* fix(cloudflare): preserve definitive RSC warm identity

* fix(cloudflare): warm immutable worker versions

* test(form): expect Next-compatible absolute navigation

* fix(cloudflare): harden RSC prewarm deployment

* test(cloudflare): stabilize deployed RSC proof

* test(cloudflare): await RSC prefetch completion

* test(cloudflare): isolate RSC readiness probes

* fix(cloudflare): warm RSC through version overrides

* fix(cloudflare): parse labeled Worker version IDs

* fix(cloudflare): warm document requests as HTML

* fix(cloudflare): retry staged version propagation

* test(cloudflare): seed prewarm proof with prior Worker

* fix(cloudflare): await version override propagation

* fix(cloudflare): serialize staged cache warming

* fix(cloudflare): verify warmed RSC build identity

* fix(cloudflare): harden staged RSC cache warming

* fix(cache): keep RSC prewarm identity build-scoped

* fix(cloudflare): gate warmup on uploaded build

* fix(cache): certify canonical RSC warmup responses

* fix(cache): harden canonical RSC warmup propagation

* fix(cache): close RSC warmup review gaps

* fix(cache): preserve explicit response cache policies

* feat(cloudflare): prewarm canonical RSC variants without prerendering

* fix(cloudflare): forward deploy config from CLI

* fix(cache): close RSC prewarm reuse gaps

* fix(cloudflare): parse labeled Worker version IDs

* fix(router): preserve canonical prefetch invalidation

* test(cloudflare): align RSC seed Worker bindings

* fix(cloudflare): scope warmup deadlines per request

* test(cloudflare): avoid caching stale warmup seed

* fix(cloudflare): fail permanent warmup errors promptly

* fix(router): keep contextual RSC requests hashed

* test(cloudflare): await promoted warmup Worker

* test(cloudflare): retry browser warmup source

* revert: restore focused warmup deadline fix

* fix(cloudflare): remove warmup propagation deadline

* fix(cloudflare): allow slower warmup responses

* fix(cloudflare): confirm warmed cache hits

* fix(cloudflare): confirm cache hits without backoff

* fix(cloudflare): avoid duplicate cache warm requests

* test(cloudflare): allow prewarm propagation

* test(cloudflare): isolate loading prewarm propagation

* test(cloudflare): await promoted prewarm worker

* fix(cloudflare): wait for warm cache propagation before promotion

* test(cloudflare): allow promoted warm cache to settle

* test(cloudflare): wait after promoted build is reachable

* test(cloudflare): poll deployed version before warm cache checks

* test(cloudflare): trace warm cache identity

* test(cloudflare): trace prewarm verification location

* test(cloudflare): compare warm cache clients

* test(cloudflare): trace bypassed response identity

* fix(cloudflare): serialize warm cache variants

* test(cloudflare): report prewarm cache miss context

* test(cloudflare): await target RSC deployment readiness

* test(cloudflare): await stable worker promotion

* test(cloudflare): isolate RSC prewarm target

* fix(cloudflare): defer CDN warmup retries

* fix(cloudflare): verify warmed RSC variants

* test(cloudflare): trace RSC warm cache paths

* fix(cloudflare): separate loading-shell warm cache key

* test(cloudflare): make RSC warm reuse proof deterministic

* test(cloudflare): tolerate stale promoted Worker routing

* fix(rsc): enforce definitive loading-shell cache key
2026-08-24 23:03:12 +01:00
James Anderson efa916579b fix(app-router): settle optimistic search navigations (#2952)
* test(app-router): reproduce deployed optimistic search navigation stall

Add a production-shaped provider filter that combines useOptimistic with same-path search navigation and streamed server results. Cover local controls and run a bounded provider cycle against the deployed Workers example, where the intermittent commit failure occurs.\n\nRefs #2865.

* test(app-router): reproduce local optimistic navigation stall

* fix(app-router): settle optimistic search navigations

* fix(app-router): preserve streaming during optimistic recovery

---------

Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
2026-08-17 14:23:57 +01:00
James Anderson 1ca8763fec fix(build): preserve require export conditions (#2881)
* fix(build): preserve require export conditions

* fix(build): resolve require conditions in dev

* test(build): assert visible conditional export output

* fix(build): harden require condition resolution

* perf(build): skip ineligible require condition scans

* test(build): cover require conditions on Workers
2026-08-17 10:59:34 +00:00
James Anderson c296ad34db fix(app-router): retain shared layouts across loading shells (#2940)
* test(app-router): reproduce shared layout teardown on soft navigation

Add a `/layout-identity` fixture to the app-router-cloudflare example and
e2e coverage showing that a shared `[slug]` layout is discarded during a
soft navigation that only changes segments below it.

The fixture pairs the shared layout with a `loading.tsx` boundary above
it, a parallel slot beside `children`, and a nested dynamic segment with
its own layout, matching the shape a real app has. Removing the
`loading.tsx` boundary makes every assertion pass, which isolates it as
the trigger.

The five layout-identity assertions fail on main; the soft-navigation
control passes, so the teardown is the router discarding a layout it
should have retained rather than a full document load.

* fix(app-router): retain shared layouts across loading shells

* fix(app-router): preserve omitted slot segment identity

* test(app-router): follow shared param canonicalizer
2026-08-17 11:28:26 +01:00
James Anderson c2362842c9 fix(build): support deployed web worker graphs (#2314)
* fix(build): support deployed web worker graphs

* fix(build): address worker parity review findings

* fix(build): preserve web worker URL origins

* test(e2e): cover deployed web workers

* test(e2e): port upstream Cloudflare worker suite

* fix(build): preserve worker asset semantics

* fix(build): tolerate minimal resolved config mocks

* test(worker): classify Vite-equivalent coverage
2026-08-16 01:06:42 +01:00
Nathan Nguyen fefc8e21d1 fix(app-router): preserve request.cf in route handlers (#2886)
* fix(app-router): preserve request.cf in route handlers

Cloudflare Workers attaches request.cf at the inbound request boundary, but App Router dispatch rebuilt Route Handler requests with the standard Request constructor and discarded that runtime metadata.\n\nUse the existing metadata-preserving URL clone boundary so both Node and Edge Route Handlers retain cf while keeping their current URL normalization semantics.

* fix(app-router): preserve request.cf through runtime wrapping

Route Handler request tracking can rebuild an already-normalized request when restoring basePath or applying middleware header overrides. Those reconstructions discarded Cloudflare metadata before the final NextRequest reached user code.\n\nPreserve cf at both reconstruction points while retaining the existing body-transfer and RequestInit behavior, with runtime-level coverage for each path.

* fix(app-router): treat request.cf as dynamic state

Preserving Cloudflare request metadata makes request.cf observable to Route Handlers, but the request proxy did not classify reads as dynamic. Geo-dependent responses could therefore enter the route-wide ISR cache, while static route modes exposed request-specific metadata.\n\nApply the existing ip and geo policy to cf: track reads in automatic mode, hide it under force-static, and reject it under dynamic error mode.

* fix(app-router): preserve request.cf across clones

Tracked Route Handler requests inherit the standard Request clone implementation, which omits Cloudflare request metadata. A handler that cloned before reading cf therefore lost the metadata even though the original tracked request retained it.\n\nCentralize tracked request cloning so cf is reattached before the clone is recursively wrapped with the same dynamic request policy.

* fix(app-router): track reflective request.cf access

Tracked Route Handler requests enforced dynamic policy only through the Proxy get trap. Descriptor, membership, and key-enumeration reads could expose Cloudflare metadata without marking the handler dynamic or respecting static modes.

Route explicit cf reflection through the same policy, filtering configurable cf keys under force-static and rejecting reflective access under dynamic error mode.

* fix(app-router): retain request proxy through valueOf

Binding every Request method to the underlying NextRequest let valueOf return the raw target. Subsequent request.cf reads could then bypass dynamic tracking and static-mode policy.

Bind valueOf to the proxy receiver while leaving branded Request methods on the underlying target, and cover the escape in all three request modes.

* fix(app-router): bind reflection to request proxy

Inherited Object reflection helpers were still bound to the raw NextRequest, allowing ownership checks to bypass request.cf dynamic policy.

Bind exact Object.prototype methods to the proxy receiver while preserving the branded target for Web Request methods, and cover ownership and enumerability checks in every request mode.

* fix(app-router): preserve proxy for request extensions

Binding unknown request properties to the raw NextRequest let user-defined methods and getters bypass request.cf policy.

Snapshot the runtime's built-in Request surface for branded target access while keeping own and unknown extensions on the proxy receiver. Cover both method and accessor escapes in every request mode.

* fix(app-router): hide cf before locking requests

Force-static proxies filtered request.cf from reflective operations, but a non-extensible target made that omission violate Proxy invariants.

Remove the configurable metadata before preventing extensions, and cover preventExtensions, seal, and freeze while preserving force-static policy.

* fix(app-router): bind Worker request members correctly

Cloudflare may expose Web IDL Request members as own properties, so classifying every own member as a user extension caused illegal invocation errors in Workers.

Use the captured built-in API surface regardless of property placement, while keeping unknown extension names on the proxy receiver. Add coverage for an own branded accessor.

* fix(app-router): distinguish request member shadows

Name-only branded member detection fixed Workers own-property layouts but treated post-wrap user shadows as runtime Web IDL members, allowing their this-based reads to escape the proxy.

Snapshot runtime-owned descriptors before route code receives the request and bind only unchanged implementations to the target. Later shadows retain the proxy receiver in every request mode.

* fix(app-router): detect request prototype shadows

Own-descriptor snapshots still treated replaced inherited Request members as branded, allowing prototype getters to bypass request.cf policy.

Snapshot each resolved built-in implementation and compare the currently resolved descriptor before selecting the raw target. Own and prototype shadows now retain the tracked receiver.

* fix(app-router): snapshot request built-ins before routes

* fix(app-router): preserve branded Worker request methods

* fix(app-router): preserve reflected cf after locking

* refactor(app-router): contain request.cf policy on its target

Route handlers need Workers metadata to follow static-generation policy without changing the semantics of every Request property. Use the configurable cf accessor copied onto NextRequest as the single policy boundary, and delegate request reconstruction to the canonical clone helpers.

* fix(app-router): avoid synthesizing absent request.cf

Ordinary requests should not gain an own cf property merely because route-handler dynamic tracking is active. Install the target accessor only when Workers metadata exists, while keeping direct absent reads subject to automatic, force-static, and error policy.

* fix(app-router): harden request.cf tracking

* chore(test): register worker route fixtures

* chore(app-router): deduplicate request.cf descriptor

---------

Co-authored-by: James <james@eli.cx>
2026-08-13 11:39:47 +01:00
James Anderson 71dbb45ca8 fix(pages): isolate on-demand revalidation requests (#2495)
* fix(pages): pin revalidate loopback origin

* test(pages): cover production revalidation origin

* fix(pages): preserve internal revalidation boundaries

* fix(pages): preserve on-demand revalidation boundaries

* fix(pages): dispatch Worker revalidation internally

* fix(pages): authenticate revalidation request context

* fix(pages): isolate revalidation transport headers

* test(pages): account for generated route table size

* fix(pages): align on-demand revalidation semantics

* fix(pages): align revalidation response cache parity

* fix(pages): preserve regenerated ISR representations

* fix(pages): align ISR cache representations

* fix(pages): preserve canonical ISR representations

* fix(pages): align cached response parity

* fix(pages): preserve custom App render props

* fix(pages): preserve optional App page props

* fix(pages): preserve optional App props on the client

* fix(pages): normalize client App page props

* fix(pages): preserve App data merge semantics

* fix(pages): keep redirect status helper internal

* fix(pages): match Next.js terminal ISR behavior

* fix(pages): preserve custom app error envelopes

* fix(pages): match Next.js dev revalidation semantics

* test(pages): align dev revalidation parity coverage

* chore(pages): remove stale cache helper exports

* fix(cache): preserve explicit no-store context
2026-07-20 18:45:06 +01:00
James Anderson 0b6faab89d fix(cache): isolate draft route responses (#2591)
* fix(cache): isolate draft route handler responses

* fix(cache): enforce draft policy on route errors

* fix(cache): privatize draft mode transitions

* fix(cache): respect live draft transitions

* fix(cache): discard route cookies on failed handlers

* Reviewed PR #2591: solid fix, no blocks

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

* test(cloudflare): use public CDN adapter export

* fix(ci): align Cloudflare fixture lock entry

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-07-15 14:06:00 +01:00
James Anderson 4f7eec967e fix(router): avoid repeated App path decoding (#2556)
* fix(router): avoid repeated App path decoding

* test(router): align encoded middleware pathname

* fix(router): match Next encoded path semantics

* fix(prerender): accept canonical encoded app params

* fix(router): preserve encoded interception params

* test(build): tolerate watch output replacement

* test(router): cover repeatedly encoded production paths

* fix(router): preserve action request route identity

* refactor(middleware): remove stale pathname argument

* refactor(router): align fallback interception paths

* refactor(router): derive request path identity from value

* fix(router): preserve encoded route parity

* fix(router): preserve raw config path matching

* fix(router): preserve raw encoded route identity

* fix(router): canonicalize encoded dot segments

* fix(router): restore encoded path parity after main merge

* fix(app-router): exact-match generated params

* fix(app-router): chain generated parent params

* fix(app-router): preserve generated param ownership

* fix(app-router): preserve per-result param ownership

* fix(app-router): preserve params after empty parallel generation

* fix(app-router): chain parallel static params by branch
2026-07-14 22:09:29 +01:00
MK (fengmk2) 2bf68e906b chore(deps): bump vite-plus to v0.2.2 (#2524)
Vite+ is now in beta.
https://voidzero.dev/posts/announcing-vite-plus-beta
2026-07-06 20:49:17 +01:00
James Anderson 5825c6807f fix(app-router): preserve client state during action revalidation (#2517)
* fix(app-router): preserve client state during action revalidation

* test(app-router): pin loading boundary to slot entry

* test(app-router): deploy action revalidation repro
2026-07-04 11:08:54 +01:00
James Anderson a3910e7aa9 feat(cloudflare): add unified worker entry (#2416)
* feat(cloudflare): add default pages router worker entry

* feat(cloudflare): add unified worker entry

* chore(web): use unified worker entry

* chore(examples): use built-in workers-cache entry

* chore(examples): keep hackernews on app router entry

* chore(cloudflare): rename unified entry to fetch handler

* chore(cloudflare): keep pages entry source helper test-local
2026-06-29 17:33:51 +00:00
James Anderson 89036154cc fix(pages-router): unify client asset bootstrap (#2378)
* fix(pages-router): hydrate worker-backed dev pages

* refactor(pages-router): unify client asset bootstrap

* fix(app): preinitialize bootstrap dependencies

* test(app): distinguish preinit scripts from bootstrap

* fix(app): root-anchor bootstrap preinit modules

* test(app): cover bootstrap preinitialization in browsers

* fix(app): share client assets with nested SSR modules

* test(app): verify bootstrap preinit CSP nonces

* test(app): serve CSP nonce in worker fixture

* fix(test): read CSP script nonce property

* fix(cloudflare): emit client assets beside final workers

* fix(build): resolve pages client assets from source

* fix(app): register client assets in ssr graph

* fix(pages-router): harden client bootstrap resolution

* fix(build): emit client assets for node outputs

* fix(build): package client assets in standalone output

* test(app): reference worker nonce fixture explicitly

* fix(build): preserve client assets across environments

* refactor(build): unify client asset outputs

* fix(build): stabilize custom client asset outputs

* fix(build): finalize client assets after app builds

* fix(build): reuse client assets in hybrid builds

* fix(build): scope hybrid client assets metadata

* test(e2e): colocate Cloudflare worker fixture
2026-06-26 19:48:26 +01:00
James Anderson 6e998ec114 fix(middleware): run on missing build assets (#2242)
* fix(middleware): run on missing build assets

* fix(middleware): preserve missing asset rewrites on workers
2026-06-22 23:36:49 +01:00
James Anderson 2e83e7f558 fix(edge-wasm): handle *.wasm?module imports in non-Cloudflare builds (#1877)
* fix(edge-wasm): handle \`*.wasm?module\` imports in non-Cloudflare builds (#1351)

In plain Node.js builds (deploy-suite / standalone `vinext start`),
Rolldown has no built-in handler for the `?module` query on `.wasm`
imports, so middleware and edge API routes that use:

  import wasm from './add.wasm?module'

would throw at bundle time.

Root cause: the `?module` query is a Cloudflare Workers / workerd
convention handled by `@cloudflare/vite-plugin`'s
`additionalModulesPlugin` (enforce:"pre"). When that plugin is absent
(e.g. after `vinext init` which emits a plain `vite.config.ts` with
only `vinext()`), no plugin resolves the query.

Fix: add a new `vinext:wasm-module-import` Vite plugin (enforce:"pre")
that, in the absence of @cloudflare/vite-plugin:

1. Intercepts `*.wasm?module` imports in `resolveId`.
2. Reads the WASM binary at load time and inlines it as base64.
3. Exports `await WebAssembly.compile(buffer)` via a virtual module —
   valid in Node.js (top-level await in ESM).

The `hasCloudflarePlugin` guard is checked inside the hook handler
(not as an array-spread condition) so it reflects the value set by
the `config` hook after plugin registration.

workerd forbids compiling WASM from bytes at runtime, so this path
intentionally never runs when @cloudflare/vite-plugin is present.

Fixes #1351.

* fix(wasm-module-import): suppress no-control-regex lint on virtual module regexes

Replace bare null-byte with unicode-escape in the two filter/replace regexes
and add oxlint-disable-next-line comments following the same convention used
in client-reference-dedup.ts. Fixes the Check job failure.

* fix(wasm-module-import): exclude @vercel/og from plugin scope, add resolveId tests

- Add importer guard in resolveId to skip imports originating from
  @vercel/og (resvg.wasm, yoga.wasm): vinext:og-font-patch converts
  those to dynamic imports with a .catch() disk-read fallback; if
  wasm-module-import intercepts them, the Node.js fallback never runs,
  the ~1.3 MB resvg WASM ships twice, and findEmittedWasmAsset dedup
  in vinext:og-assets breaks. Guard mirrors the pattern in the existing
  isVinextOgShimImporter() helper.
- Remove dead `return null` after `this.error()` (which throws); use
  `bytes!` non-null assertion to satisfy TypeScript.
- Add resolveId test suite: happy path, ?module query stripping, null
  resolver, og-collision guard (plain + virtual-prefix importer), and
  non-og pass-through.

Fixes ask-bonk findings on PR #1877.

* refactor(wasm-module-import): drop non-null assertion, document TLA and filter intent

* test(wasm-module-import): add build-level coverage, document runtime-gating assumption

* refactor(wasm-module-import): extract to plugins/, guard client environment, watch wasm file in dev

* fix(wasm-module-import): delegate target-owned modules

* refactor(wasm-module-import): share query stripping
2026-06-11 12:06:39 +00:00
James Anderson d504f022b0 fix(assets): default assetsDir to _next/static (Next.js parity) (#1411)
* fix(assets): default assetsDir to _next/static (Next.js parity)

Closes #1337. Supersedes #1383.

vinext's default `assetsDir` was Vite's historical `assets/`, so URL emission
(`/_next/static/...` via `resolveAssetUrlPrefix("")`) and on-disk layout
(`dist/client/assets/...`) disagreed in the empty-`assetPrefix` case. The
`build.assetsDir` and `experimental.renderBuiltUrl` Vite overrides only
applied when `assetPrefix` was configured, leaving the no-prefix branch
serving from a different path than the URL contract Next.js's client
runtime and test harness assert against.

This commit flips the default to Next.js's canonical layout:

- `resolveAssetsDir("")` returns `_next/static` (was `assets`)
- `build.assetsDir` is now set unconditionally from `resolveAssetsDir`
- Vite's default `base + assetsDir` composition produces correct URLs
  in the no-prefix case; `renderBuiltUrl` stays gated on `assetPrefix`
  because it's only needed for the configured cases
- Drops the legacy `/assets/` branches in prod-server (3 sites) and
  static-file-cache (hard cutover — see PR description for rationale)
- Updates `_headers` generation, precompress default, fonts plugin
  `DEFAULT_ASSETS_DIR` to match the new default

With the layout aligned, invalid `_next/static/*` requests naturally
return plain-text `404 + "Not Found"` from the static-file layer instead
of falling through to the page renderer (which would produce an HTML
404 with bootstrap scripts and CSS). This replaces #1383's parity
short-circuit with the natural code path:

- Node prod-server (App + Pages branches): missing asset under
  `resolveAppRouterAssetPath` returns `text/plain; charset=utf-8` 404
- Cloudflare worker entry: `isNextStaticPath` recognises asset-shape
  after ASSETS-binding misses; returns `notFoundStaticAssetResponse`

Mirrors Next.js: packages/next/src/server/lib/router-server.ts.

Adds `tests/invalid-static-asset-404.test.ts` (3 App Router + 3 Pages
Router cases — no prefix / basePath / assetPrefix) ported from Next.js
e2e suites. Updates 11 source files + 12 test files; 914+ tests pass
in the touched suites (asset-prefix, app-router, pages-router, deploy,
font-google, static-file-cache, serve-static, precompress, features,
routing, isr-cache, build-optimization, app-rsc/ssr, standalone,
middleware, shims).

Ported from Next.js:
- test/e2e/invalid-static-asset-404-app/*.test.ts
- test/e2e/invalid-static-asset-404-pages/*.test.ts

* fix(e2e): update cloudflare-pages-router hydration spec for _next/static

* PR #1411 approved. Clean fix, 2 nits.

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

* refactor(assets): derive isHashed checks from ASSET_PREFIX_URL_DIR

Replace hardcoded '_next/static' string literals in runtime code paths
with the ASSET_PREFIX_URL_DIR constant so the asset URL convention has
a single source of truth.

- server/prod-server.ts isHashed: pathname.includes(`/${ASSET_PREFIX_URL_DIR}/`)
- server/prod-server.ts public-dir guard: startsWith(`/${ASSET_PREFIX_URL_DIR}/`)
- server/static-file-cache.ts isHashed: both startsWith + includes derived
- plugins/fonts.ts DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR

Behavioural no-op — the constant is '_next/static' so the resolved
strings are identical. Centralises the URL contract so future changes
to ASSET_PREFIX_URL_DIR (or a refactor that derives it from build
config) flow through to every consumer.

Doc-comment references to '_next/static' left in place — they
describe the canonical value for human readers and are not code paths.

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-05-21 20:36:04 +00:00
James Anderson 9415a238f4 feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time

Wire `assetPrefix` from next.config through to the Vite build so every
emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors
Next.js semantics: `assetPrefix` controls asset URLs only, independent
of `basePath` (which still controls route URLs).

* Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via
  a new `normalizeAssetPrefix` helper that trims trailing slashes,
  accepts absolute URLs (`https://cdn.example.com[/sub]`) and path
  prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable
  values.
* New `packages/vinext/src/utils/asset-prefix.ts` module collects the
  shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix`
  (emitted URL prefix), `assetPrefixPathname` (path portion for runtime
  matching), and the canonical `_next/static` directory constant.
* In the Vite plugin, set `build.assetsDir` so on-disk layout matches the
  URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for
  absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's
  default `base + url` URL emission so `assetPrefix` and `basePath` stay
  independent — routes continue to anchor at `base = basePath + '/'`.
* Persist `assetPrefix` in the embedded `vinextConfig` for the Pages
  Router server and in a new `vinext-runtime-config.json` sibling for
  the App Router prod server (the RSC entry only embeds `__basePath`).

@see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix

* feat(server): serve assetPrefix-prefixed assets at runtime

Update the production server to resolve incoming asset requests under
`<assetPrefix>/_next/static/<filename>` (path-prefix form) and
`/_next/static/<filename>` (absolute-URL form) in addition to the
historical `/assets/` URLs. Both routers (Pages and App) share a new
`resolveAppRouterAssetPath` helper that returns the lookup path on disk
or `null` to fall through.

* App Router prod server loads `assetPrefix` from the new
  `vinext-runtime-config.json` sibling written by the Vite plugin.
* Pages Router prod server reads `assetPrefix` from the embedded
  `vinextConfig` and threads it through the existing
  `stripBasePath` lookup.
* StaticFileCache marks any path under `_next/static/` as content-hashed
  (alongside the existing `assets/` rule) so emitted bundles get the
  long-lived `immutable` cache headers. Same change in the slow-path
  branch of `tryServeStatic`.

Cloudflare Workers needs no code change: the ASSETS binding serves
`<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...`
because the on-disk layout already mirrors the URL. Absolute-URL
prefixes are expected to be served by the CDN, so they don't reach the
worker.

* test(asset-prefix): add unit + e2e build tests for next.config assetPrefix

Covers the surface area added in the previous two commits:

* Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL,
  trailing-slash trimming, validation), `resolveAssetsDir` /
  `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout),
  `assetPrefixPathname` (runtime matching), and
  `resolveAppRouterAssetPath` (request → on-disk lookup translation).
* End-to-end build tests that exercise `vinext build` against the
  `tests/fixtures/app-basic` app patched with each `assetPrefix` shape:
    - Path prefix (`/custom-asset-prefix`): SSR HTML references
      `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches,
      and fetching the bootstrap URL from the prod server returns 200.
    - Absolute URL (`https://cdn.example.com`): emitted asset URLs are
      fully qualified; no `/assets/` leaks.
    - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes
      stay under `/app/` while asset URLs live under `/cdn-prefix/` —
      proving the two configs stay independent.
    - Unset (default): historical `/assets/` layout is preserved.

Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts

* test(next-config): add assetPrefix field to makeResolved helper

ResolvedNextConfig requires assetPrefix; the test helper was missing
it which broke the type check.

* fix(config): replace polynomial regex with loop (CodeQL)

CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "")
as polynomial-time backtracking on uncontrolled input. Swap both for
explicit while loops with linear-time semantics. Same fix applied to
the URL pathname trim in assetPrefixPathname().

* refactor(prod-server): drop dead null fallback; tighten hashed-asset detection

- Remove the `?? null` after resolveAppRouterAssetPath; the helper
  already returns null on miss, so the fallback (and its obsolete
  comment) were no-ops.
- Add explicit startsWith("_next/static/") / startsWith("/_next/static/")
  prefix checks before the substring fallback in both static-file-cache
  and prod-server. The substring check stays so assetPrefix-prefixed
  layouts (e.g. "cdn/_next/static/") are still recognised.

* fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed

precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix`
is configured the build writes hashed assets to a different subdirectory
(`<prefix>/_next/static/` for path prefixes, `_next/static/` for
absolute URLs) and the precompression step walked an empty directory,
silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds
with assetPrefix would then serve uncompressed assets.

Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix)
at the plugin call site, default to `"assets"` for backward compat,
and add focused tests for both assetPrefix layouts.

* refactor: drop redundant isHashed startsWith; harden test cleanup against build failures

- prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")`
  check; `pathname` always has a leading slash so the existing
  `includes("/_next/static/")` already covers it.
- static-file-cache.ts: keep both `startsWith("_next/static/")` and
  `includes("/_next/static/")` because `relativePath` has no leading
  slash — the prefix and substring checks cover different layouts
  (absolute-URL prefix vs path-prefix). Clarify the comment.
- tests/asset-prefix.test.ts: register the tmp-dir cleanup with the
  shared `cleanups` array synchronously inside `buildFixtureWithConfig`,
  right after `mkdtempSync`. Previously the cleanup was only pushed
  after the build returned, so a thrown `createBuilder`/`buildApp`
  would leak the tmp dir.

* refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON

Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts)
and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry.
The runtime-config.json sidecar was unnecessary — the App Router entry
template already inlines config values as compile-time constants, so
assetPrefix should be inlined the same way.

Changes:
- Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts.
- Emit `export const __assetPrefix = <json>` from the generated RSC
  entry alongside the existing `__basePath` / `__configHeaders` /
  `__expireTime` constants. Single-line declaration to stay within the
  "thin generated entry" guideline.
- Thread `nextConfig.assetPrefix` through the generateRscEntry call site
  in index.ts.
- App Router prod-server reads `rscModule.__assetPrefix` (with a string
  fallback for older builds) instead of `vinext-runtime-config.json`.
- Delete the vinext-runtime-config.json write in index.ts. Nothing else
  in the repo reads this file (verified via rg on packages/ tests/
  examples/).

Pages Router behaviour is unchanged: it already gets assetPrefix from
the embedded `vinextConfig` exported by pages-server-entry.ts.

Addresses james-elicx review feedback on #1311.

* fix(ssr): emit bootstrap as <script src> via bootstrapModules

The App Router SSR pipeline previously passed only `bootstrapScriptContent`
to React's renderToReadableStream, producing inline
`<script>import("/path/to/entry.js")</script>` tags. The URL was therefore
inaccessible to anything that reads `script.attribs.src` — including the
Next.js asset-prefix fixture test "bundles should return 200 on served
assetPrefix" which iterates script tags looking for a `src` attribute
prefixed by the configured assetPrefix.

Extract the bootstrap URL from the plugin-rsc-generated content and pass
it as `bootstrapModules: [url]` so React emits a real
`<script type="module" src="url">` tag. The existing
`extractModulePreloadHtml` helper already did the same extraction for
the modulepreload <link>, so the URL was already exposed (just renamed
`extractBootstrapModuleUrl` and lifted out of the head-injection helper
to share between the bootstrap and the preload tags).

The inline `bootstrapScriptContent` is no longer needed — it was only
carrying the dynamic-import call that bootstrapModules now performs
natively. Passing both would load the bootstrap module twice.

`bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits
a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`)
that the browser fetches as a module from the dev server; in prod it's the
hashed bundle URL, optionally prefixed by `assetPrefix`.

Mirrors Next.js's app-render path which combines both
`bootstrapScriptContent` (for session state) and `bootstrapScripts`
(for the actual chunk URL). See
.nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx.
We use `bootstrapModules` instead of `bootstrapScripts` because vinext's
chunks are native ES modules (Vite output) — a `type="module"` tag is the
correct loader.

Tests updated where they asserted on the old `import("URL")` shape:
tests/asset-prefix.test.ts, tests/app-router.test.ts,
tests/e2e/cloudflare-workers/navigation.spec.ts.

Verified all related suites pass locally (asset-prefix, app-router,
script-nonce, script-preload, use-client-page-pathname, document,
prerender, ISR, fetch-cache, entry-templates, precompress).

* fix(build): use resolveAssetsDir for Pages Router client entry fallback scan

The Pages Router Cloudflare Workers post-build step scans the on-disk
assets directory for the client entry chunk when the SSR manifest
lookup doesn't surface one. The directory path was hardcoded to
`<clientDir>/assets/`, which is the Vite default — but when
`assetPrefix` is configured, the chunk lives under
`<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL
prefix). The fallback would silently miss the entry and the worker
would ship without its client bootstrap reference.

Use the shared `resolveAssetsDir(assetPrefix)` helper that already
drives `build.assetsDir` and the prod-server lookup, so this scan
works for every supported asset layout.

Addresses round-4 reviewer feedback on #1311 (low-priority follow-up
item — only fires when the SSR manifest lookup fails).

* feat(config): mirror Next.js basePath → assetPrefix fallback

Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured
and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531).
Without this, an app deployed under a basePath serves its routes
correctly but emits its assets from `<basePath>/assets/...` (Vite's
default `base + assetsDir` composition) rather than from the Next.js-
canonical `<basePath>/_next/static/...`.

Add the same fallback in resolveNextConfig, gated by Next.js's own
conditions:
  - basePath !== ""   (skip when basePath is unset)
  - basePath !== "/"  (Next.js rejects this earlier; we mirror the
                       guard so we never produce assetPrefix === "/")
  - assetPrefix === "" (user did not opt out)

The fallback feeds the rest of the asset-prefix pipeline (Vite's
`build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server
URL→file resolver) so no other code paths need to change.

Tests:
 - tests/next-config.test.ts: 5 unit tests for the four edge cases of
   the fallback condition.
 - tests/asset-prefix.test.ts: new end-to-end test that builds with
   `basePath: "/app"` (no assetPrefix), starts the prod-server, and
   asserts the bootstrap <script src> lands at /app/_next/static/...
   (and that the legacy /app/assets/... layout is gone).
 - tests/pages-router.test.ts: updated the "preserves basePath on
   backfilled SSR manifest entries" test to expect the new
   /docs/_next/static/ layout (was /docs/assets/). The manifest
   assertion is relaxed to startsWith("docs/") because Vite's raw
   SSR manifest now emits some duplicate-prefixed entries
   ("docs/docs/_next/static/...") alongside the backfilled ones; both
   forms anchor under basePath so the prod-server's URL→file lookup
   remains correct — and the user-visible HTML assertions in the same
   test are the source of truth.

Addresses round-4 reviewer feedback on #1311.

* fix(server): Pages Router must not strip basePath before asset lookup

The Pages Router prod-server stripped `basePath` from the request
pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke
the asset lookup whenever the assetPrefix path-prefix branch was active
with a path that happened to equal basePath.

Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's
`basePath → assetPrefix` fallback, an app configured with
`basePath: "/docs"` (and no explicit `assetPrefix`) now resolves
`assetPrefix = "/docs"`. Emitted asset URLs are
`/docs/_next/static/…` (correct). But the Pages Router branch did:

  const staticLookupPath = stripBasePath(pathname, basePath);
  // "/docs/_next/static/x.js" → "/_next/static/x.js"
  resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs");
  // path-prefix branch needs pathname to start with "/docs/" → null → 404

The App Router branch already passes the un-stripped `pathname` to the
same helper (see line ~1140). Match that behaviour. `staticLookupPath`
is still computed and used downstream by non-asset paths
(image-optimization endpoint, page routing), where the basePath-stripped
form is correct.

Test coverage:
 - tests/pages-router.test.ts: extended the existing "preserves basePath
   on backfilled SSR manifest entries" test to FETCH every emitted asset
   URL and assert HTTP 200 (previously it only inspected HTML for the
   URL string). This is the missing assertion that would have caught
   the bug the first time.
 - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test
   that builds an inline pages/ fixture with `basePath: "/docs"`,
   starts the prod-server, collects every script/link URL from the SSR
   HTML, and asserts every URL serves a 200 + correct content-type.
   Confirmed to fail on `bac8425` before this fix and pass after.

Addresses round-5 reviewer feedback on #1311.

* refactor(ssr): accept either quote style in bootstrap URL regex

plugin-rsc currently emits the bootstrap as `import("<url>")` with
double quotes, but the contract isn't documented and a future version
could switch to single quotes. Tighten the URL extraction regex to
`import(["'])([^"']+)\1` so either form works.

Addresses non-blocking round-5 review feedback on #1311.

* docs(ssr): note CSP implications of bootstrapModules switch

The round-3 commit `3a2c886` switched the App Router SSR bootstrap from
inline `bootstrapScriptContent` (`<script>import(…)</script>`) to
React's `bootstrapModules` option (`<script type="module" src=…>`).
That has user-visible CSP consequences worth documenting at the call
site so app authors hit them in code search rather than at runtime:

  - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap
    (improvement — inline imports previously required it).
  - Apps that restrict script sources need `'self'` for same-origin,
    or the CDN origin when assetPrefix is an absolute URL.
  - React still threads nonce through to the emitted <script> tag, so
    nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps
    working unchanged.

Addresses non-blocking round-5 review feedback on #1311.
2026-05-19 11:53:49 +01:00
Nathan Nguyen 9d5d1ce50d fix(app-router): validate RSC cache-busting params (#991)
* fix(app-router): validate RSC cache-busting params

App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request.

Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions.

Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration.

* fix(app-router): hide RSC cache tokens from navigation URLs

* fix(app-router): hide RSC cache token from searchParams

* fix(app-router): address RSC cache-busting review

* fix(app-router): preserve HTML redirect destination for direct .rsc navigations

Config redirects from next.config.js were unconditionally converted to
.rsc destinations for any request whose URL ended in .rsc. This broke
direct browser navigations to .rsc URLs (e.g. a bookmark or external
link) because the browser was redirected to an RSC payload it could not
render.

Client-side RSC fetches always send the RSC: 1 header, so restrict the
.rsc destination conversion to requests that carry that header. Direct
browser hits without the header continue to redirect to the canonical
HTML URL, matching the pre-PR behaviour for config redirects.
2026-05-04 18:55:43 +01:00
Steve Faulkner b678035956 fix: don't await createFromReadableStream before hydrateRoot (#695) (#700)
* fix: don't await createFromReadableStream before hydrateRoot (#695)

createFromReadableStream returns a React thenable, not a resolved tree.
Awaiting it blocked hydrateRoot until the entire RSC stream was consumed,
which prevented useEffect callbacks from ever firing in production on
Cloudflare Workers where RSC chunks arrive progressively via script tags.

Pass the thenable directly to hydrateRoot so React resolves components
lazily as stream chunks arrive -- matching the RSC contract and the
pattern already used in the SSR entry.

Adds a regression test fixture and e2e test for the Workers hydration
project that verifies useEffect fires after RSC hydration.

* fix: move effect-test page to app-router-cloudflare example

The cloudflare-workers e2e project builds examples/app-router-cloudflare,
not tests/fixtures/cf-app-basic. Move the regression test fixture page to
the correct location so the /effect-test route is actually served.
2026-03-27 22:07:53 -05:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00