153 Commits

Author SHA1 Message Date
Stephen Zhou f0d5f85112 feat: add react plugin for client components (#267)
* feat: add react plugin for client components

* fix: improve react plugin JSDoc and update stale HMR comment

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
v0.0.24
2026-03-07 01:39:38 +00:00
harris.dev c03cb3f4b4 fix(deploy): monorepo-aware lock file and node_modules resolution (#122)
* fix(deploy): monorepo-aware lock file and node_modules resolution

vinext deploy failed in monorepos in two ways:

1. Wrong package manager detected — detectPackageManager only checked for
   bun.lockb (legacy binary format), missing bun.lock (text format, Bun
   v1.0+). It also only searched the immediate project directory, so any
   monorepo where the lock file lives at the workspace root fell through to
   npm, causing a confusing "npm error Invalid Version:" failure.

2. Dependency detection and wrangler binary path hard-coded to the app
   directory — hasCloudflarePlugin, hasRscPlugin, hasWrangler, hasMdxRollup
   all checked {root}/node_modules only, producing false negatives for
   hoisted packages. runWranglerDeploy then crashed with ENOENT because
   the wrangler binary was at the workspace root, not the app root.

Changes:
- Add walkUpUntil<T>(start, check) to utils/project.ts — a shared primitive
  that walks ancestor directories until check() returns non-null. Both the
  lock-file and node_modules walkers are built on top of this.
- Rewrite detectPackageManager / detectPackageManagerName using walkUpUntil;
  add bun.lock (text) check alongside existing bun.lockb (binary).
- Add findInNodeModules(start, subPath) using walkUpUntil — walks up to find
  node_modules/{subPath}, returning the absolute path or null.
- Use findInNodeModules in detectProject (hasCloudflarePlugin, hasRscPlugin,
  hasWrangler), getMissingDeps (hasMdxRollup), and runWranglerDeploy.

Tests (12 new):
- detectPackageManager: pnpm, yarn, bun.lock, bun.lockb, npm fallback,
  monorepo root traversal, closest-wins precedence
- findInNodeModules: package lookup, binary lookup, null fallback,
  monorepo root traversal, closest-wins precedence

* fix(test): clear npm_config_user_agent in npm fallback test

On CI, pnpm sets process.env.npm_config_user_agent which causes
detectPackageManagerName to return 'pnpm' instead of falling through
to the npm default. Save and restore the env var around the assertion.

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-07 01:37:59 +00:00
Divanshu Chauhan (divkix) dc8e953f07 fix(instrumentation): defer ssrLoadModule to post-middleware hook (#289)
* fix(instrumentation): defer ssrLoadModule call to post-middleware hook

Vite 7's SSRCompatModuleRunner requires the SSR environment's transport
channel to be initialized before ssrLoadModule() can be called. During
configureServer(), the channel is not yet ready, causing a TypeError
when instrumentation.ts exists.

Move the runInstrumentation() call from the configureServer body into
the returned post-middleware function, where environments are fully
initialized.

Fixes #167

* fix(instrumentation): address review comments on deferred ssrLoadModule

- Remove unreachable .catch() on runInstrumentation() since internal
  try/catch already handles errors and the function never rejects
- Mock console.error in transport error test to suppress noisy output
  and assert the expected error message
- Rename misleading test to accurately describe what it verifies

* test: address instrumentation review feedback
2026-03-06 19:33:12 -06:00
tomasgferreira 4f827efbb1 fix(ci): use env guard for tip email notification step (#172)
Co-authored-by: tomoconstrutor <tomoconstrutor@gmail.com>
2026-03-07 01:27:30 +00:00
MumuTW 8318ae12c0 fix: strip content-encoding and content-length from proxied responses (#272)
* fix: strip content-encoding and content-length from proxied responses (#263)

Node.js fetch() auto-decompresses responses (gzip, br, etc.), but the
proxy was forwarding the original content-encoding and content-length
headers unchanged. This caused the browser to attempt a second
decompression on the already-decoded body, resulting in
ERR_CONTENT_DECODING_FAILED.

Fix applied in both proxyExternalRequest (config-matchers.ts) and the
inline __proxyExternalRequest (app-dev-server.ts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: gate proxy header stripping to Node runtime

* fix: guard content-encoding stripping to Node runtime in config-matchers

proxyExternalRequest is shared between Node.js and Workers. Workers
fetch() preserves wire encoding, so unconditionally stripping
content-encoding/content-length would break responses on Workers.
Apply the same __isNodeRuntime guard used in the generated dev server.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-07 00:55:19 +00:00
ask-bonk[bot] a5edf26627 fix: set consumer: 'client' on client environment to fix Nitro ERR_UNSUPPORTED_ESM_URL_SCHEME (#296)
When Nitro's configEnvironment hook runs, Vite hasn't yet applied the
default consumer based on environment name. Without an explicit
consumer: 'client', Nitro incorrectly treats the client environment as
a server-side service, creating a FetchableDevEnvironment that loads
the browser entry (virtual:vinext-app-browser-entry) in a Node.js
worker thread. The browser entry imports @vitejs/plugin-rsc/browser,
which is externalized and loaded natively by Node — but it contains
`import * as clientReferences from 'virtual:vite-rsc/client-references'`,
which fails because Node's ESM loader doesn't support the virtual: scheme.

Fixes #260

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
2026-03-06 18:35:47 -06:00
Steve Faulkner 33824fc4e2 docs: update CONTRIBUTING.md to reflect current review workflow and merge philosophy (#309) 2026-03-07 00:18:50 +00:00
Nathan Nguyen af0a839a67 fix: preserve multiple Set-Cookie headers in response merging (#297)
* fix: preserve multiple Set-Cookie headers in prod-server and worker entry

The response header merging in prod-server.ts and the generated Cloudflare
worker entry used Record<string, string> which flattened multiple Set-Cookie
headers — the last value won, or cookies with Expires dates got corrupted
by comma-joining.

Extract mergeResponseHeaders() helper that uses getSetCookie() to preserve
array-valued Set-Cookie headers. Apply the same fix to the worker entry
template in deploy.ts using the Headers API.

Fixes #295

* fix: address review feedback on Set-Cookie header preservation

- Add `as string` cast in deploy.ts worker entry for x-middleware-request-*
  header unpacking (matches prod-server.ts)
- Normalize Vary header with Array.isArray check in sendCompressed to handle
  the widened type correctly
- Add edge-case test for middleware cookie as plain string (not array)

* fix: apply Set-Cookie fix to example workers, remove redundant toLowerCase

- Fix the same Set-Cookie flattening bug in hand-written example worker
  entries (pages-router-cloudflare, realworld-api-rest)
- Remove 4 redundant .toLowerCase() calls — Headers.forEach() always
  yields lowercase keys

* fix: add missing as string cast in deploy.ts middleware header collection

* refactor: extract mergeHeaders to shared vinext/server/worker-utils

The mergeHeaders function was duplicated in both example worker entries
(pages-router-cloudflare, realworld-api-rest) and the deploy.ts template.
Extract it to a shared module that example workers import. The deploy.ts
template still inlines it (code generation constraint).

Reduces 3 copies to 2 (shared module + generated template).

* fix: array-accumulate Set-Cookie in config headers, fix JSDoc, add behavioral test

- deploy.ts template: config headers section was comma-joining Set-Cookie values
  using += which corrupts cookies with Expires dates and loses all but the last
  cookie when multiple config rules match. Matches the array-accumulation pattern
  already used in prod-server.ts (lines 899-907) and the middleware section above.
- worker-utils.ts + deploy.ts template JSDoc: 'Response headers take precedence'
  was misleading — Set-Cookie is additive, not overriding. Clarify both docs.
- tests/deploy.test.ts: add behavioral test for mergeHeaders via worker-utils import
  (same function inlined in the generated template), replacing reliance on
  string-contains assertions alone.

* fix: indentation in deploy.ts template, array-accumulate Set-Cookie in example workers

- deploy.ts: fix extra leading space in config headers block (lines 636-659)
  and JSDoc lines 740-743 introduced in previous commit
- pages-router-cloudflare, realworld-api-rest: config headers section was
  doing a plain assignment (middlewareHeaders[lk] = h.value) which overwrites
  array-valued Set-Cookie built up by the middleware section above; use the
  same array-accumulation pattern as prod-server.ts and the deploy.ts template

* fix: remove as any casts in prod-server.ts, add Vary handling to example workers

- prod-server.ts lines 904/906: middlewareHeaders is now Record<string, string | string[]>
  so the as any casts are unnecessary; replace with as string (consistent with the
  middleware collection section above at line 848)
- pages-router-cloudflare, realworld-api-rest: add Vary comma-joining to config
  headers section to match prod-server.ts (line 908) and deploy.ts template (line 653)

---------

Co-authored-by: James <james@eli.cx>
2026-03-06 23:53:23 +00:00
James Anderson 287923ebee chore: add out-e2e to .gitignore (#304) 2026-03-06 22:35:55 +00:00
James Anderson 619e5c0f73 fix: rebuild reqCtx for rewrites after middleware (#303)
* fix: rebuild reqCtx for afterFiles/fallback rewrites after middleware

afterFiles and fallback rewrites run after middleware in the App Router
execution order. Their has/missing conditions should evaluate against
middleware-modified headers, not the original pre-middleware request.

Introduces postMwReqCtx (prod-server, deploy.ts) and __postMwReqCtx
(app-dev-server) built after x-middleware-request-* headers are unpacked.
headers, redirects, and beforeFiles rewrites keep using the original
pre-middleware reqCtx, matching Next.js execution order.

Adds getHeadersContext() to the headers shim so app-dev-server can read
the ALS-mutated headers context for the post-middleware snapshot.

Test: afterFiles rewrite with has:[cookie:mw-user] is skipped without
middleware injection and matches after middleware injects the cookie.

* fix: also use postMwReqCtx for beforeFiles rewrites in deploy.ts

In App Router execution order, beforeFiles rewrites run after middleware
(step 4 vs step 3). deploy.ts processes them after middleware runs, so
they should use postMwReqCtx like afterFiles and fallback.

prod-server.ts (Pages Router) correctly keeps reqCtx for beforeFiles —
the Pages Router order has no middleware step before beforeFiles.

* fix: resolve merge conflict - keep Vary header test and afterFiles rewrite test

* fix: move beforeFiles rewrites after middleware in app-dev-server; add has/missing regression tests

- Move beforeFiles rewrite block after middleware execution so it sees
  middleware-injected cookies/headers (matches Next.js App Router order:
  middleware → beforeFiles → filesystem → afterFiles → fallback)
- Fix Map→Record bug in __buildPostMwRequestContext: ctx.cookies is a
  Map<string,string> but RequestContext.cookies must be Record<string,string>;
  convert with Object.fromEntries() so cookie lookups work correctly
- Update .rsc-suffix stripping test to reflect new code path (cleanPathname
  is now used directly by beforeFiles rather than a separate __rewritePathname)
- Add regression tests: beforeFiles and fallback rewrite has/missing conditions
  see middleware-injected cookies (app-router); fixtures wire up mw-gated-before
  and mw-gated-fallback routes + middleware cookie injection

* Apply suggestion from @james-elicx

* fix: use postMwReqCtx for beforeFiles in prod-server (Pages Router)

Per Next.js docs, the execution order is: headers → redirects → Middleware
→ beforeFiles → filesystem → afterFiles → fallback. This applies to both
App Router and Pages Router. prod-server.ts was incorrectly using the
pre-middleware reqCtx for beforeFiles matching; switch to postMwReqCtx.

Add regression test: beforeFiles has/missing conditions on the production
Pages Router server now correctly see middleware-injected cookies.
2026-03-06 22:06:09 +00:00
James Anderson 19e3a208c4 Revert "Rebuild request context after middleware modifies headers" (#301)
* Revert "Rebuild request context after middleware modifies headers (#288)"

This reverts commit 8f89555cd0.

* change test into a regression test
v0.0.23
2026-03-06 15:05:17 -06:00
James Anderson 6011fb1abb opt out of opencode dev (#302) 2026-03-06 20:58:06 +00:00
Steve Faulkner 256e0ad5e5 Use append for multi-value headers in config header application (#291)
* Apply same append logic for multi-value headers in index.ts and deploy.ts

Address review feedback: the Pages Router dev server (applyHeaders in
index.ts) and Cloudflare Worker entry (deploy.ts) had the same overwrite
bug for Vary and Set-Cookie config headers. Apply the same append
semantics used in prod-server.ts and app-dev-server.ts.

* Add append logic for multi-value config headers in prod-server.ts and app-dev-server.ts

The original fixes for these two files were lost during a rebase.
Re-apply: use append() for Vary and Set-Cookie headers instead of
set()/overwrite in both the Pages Router prod server and the App
Router RSC entry config header application.

All four config header sites now use append semantics for multi-value
headers: app-dev-server.ts, prod-server.ts, index.ts, deploy.ts.
2026-03-06 14:41:22 -06:00
Steve Faulkner 150f2222ea Fix unstable_cache serving stale entries and incorrect revalidate default (#294)
* Fix unstable_cache serving stale entries and incorrect revalidate default

Two bugs in unstable_cache:

1. Stale entries served indefinitely: unstable_cache did not check
   cacheState when reading from cache, so time-expired entries were
   returned without re-fetching. The "use cache" runtime correctly
   checks cacheState !== "stale" before returning cached values.
   Added the same check to unstable_cache.

2. Incorrect revalidate default: when no revalidate option was provided
   (undefined) or false was passed, the stored cache value had
   revalidate: 0. The MemoryCacheHandler interprets 0 as "revalidate
   at current time" (immediately stale on next set), not "never
   revalidate". Changed to store false for indefinite caching, which
   correctly leaves revalidateAt as null.

* Update IncrementalCacheValue.revalidate type in next-shims.d.ts

Keep the type declaration in sync with the CachedFetchValue interface
in cache.ts: revalidate is now number | false.
2026-03-06 14:36:36 -06:00
Divanshu Chauhan (divkix) 04d0086731 fix: inject default viewport meta (width=device-width, initial-scale=1) (#298)
* fix: inject default viewport meta (width=device-width, initial-scale=1)

mergeViewport() started from an empty object, so when a user exported
a viewport config with only themeColor (no width/initialScale), the
merged result was truthy but missing viewport dimensions. ViewportHead
then skipped the <meta name="viewport"> tag entirely, breaking mobile
rendering.

Seed mergeViewport with DEFAULT_VIEWPORT matching Next.js's
createDefaultViewport() defaults: { width: "device-width", initialScale: 1 }.
User-provided values still override these defaults.

* refactor: export DEFAULT_VIEWPORT and reuse at call sites

Export DEFAULT_VIEWPORT from metadata shim and replace the duplicated
inline literals in app-dev-server.ts to keep the default in one place.

* refactor: simplify mergeViewport call sites to always apply defaults

Since mergeViewport() always seeds from DEFAULT_VIEWPORT (even for
empty lists), the `viewportList.length > 0` guards and `?? DEFAULT_VIEWPORT`
fallbacks at both call sites were redundant. Remove them so the default
viewport is applied unconditionally via mergeViewport itself.

Addresses review feedback from james-elicx on PR #298.
2026-03-06 20:24:45 +00:00
Steve Faulkner 9ab08ff49f Default API route Content-Type to application/octet-stream (#285)
* Default API route Content-Type to application/octet-stream

When an API route handler calls res.end() without setting a Content-Type
header, the production server was defaulting to text/html. This could
cause browsers to render response bodies as HTML when the developer
intended to return plain data.

Change the fallback for API routes to application/octet-stream, which
is the safe default for arbitrary binary/text content. The SSR page
rendering path still correctly defaults to text/html.

* Improve test comment for Content-Type assertion

Explain why we use a negative assertion (not text/html) rather than
a positive one: when the handler passes a string to res.end(), the
Response constructor auto-sets text/plain, so the octet-stream
fallback only triggers for non-string bodies.
2026-03-06 13:50:46 -06:00
Steve Faulkner 5b1ffa178c Merge middleware response headers on all App Router response paths (#290)
* Merge middleware response headers on all App Router response paths

Middleware response headers were only merged into RSC streaming
responses and HTML rendering responses, but were dropped on route
handlers, server actions, metadata routes, image optimization, error
pages, and other response paths (~30 out of ~37 return points).

Move middleware header merging from individual response paths to the
handler() wrapper, which runs after _handleRequest() returns. This
ensures all response paths receive middleware headers. The previous
per-path merges in the RSC response and attachMiddlewareContext() are
replaced by a single merge point at the top level.

* Replace module-level middleware state with per-request container

Module-level __pendingMiddlewareHeaders and __pendingMiddlewareRewriteStatus
variables could race between concurrent requests on Cloudflare Workers.
Replace with a per-request _mwCtx object passed from handler() into
_handleRequest(). Each request gets its own container, eliminating the
shared mutable state.
2026-03-06 13:50:08 -06:00
Steve Faulkner 705075d878 fix: strip server-only data-fetching exports from client bundles (#282)
* fix: strip getServerSideProps/getStaticProps from client bundles

Pages Router page modules export getServerSideProps, getStaticProps,
and getStaticPaths for server-side use. These functions often import
server-only modules (database drivers, fs, etc.) that would break or
bloat the client bundle.

Added a new Vite transform plugin (vinext:strip-server-exports) that
replaces these exports with no-op stubs when the module is being
processed for the client environment. The plugin only applies to
files under the pages/ directory and skips API routes, _app,
_document, and _error files.

The transform handles multiple export patterns:
- export (async) function declarations
- export const arrow functions
- export const simple references
- TypeScript type annotations in parameters

Extracted the core logic into a standalone stripServerExports()
function for testability. Added 11 unit tests covering all patterns
including edge cases with nested braces, string literals, TypeScript
types, and multiple server exports in one file.

* Rewrite stripServerExports to use AST instead of regex

Replace the regex-based approach with parseAst (Rollup/acorn) and
MagicString. The AST approach correctly handles all export patterns:

- Function declarations (export function / export async function)
- Variable declarations with any initializer (arrow functions,
  function expressions, async named function expressions, simple refs)
- Re-export syntax (export { getServerSideProps })
- Mixed re-exports (export { getServerSideProps, config })

The old regex approach had bugs with function expressions, arrow
functions with TS return type annotations, re-exports, and regex
literals in function bodies. Since this transform runs after Vite's
JSX/TS compilation, parseAst always receives plain JavaScript.

Updated tests to use post-compiled JS (no JSX/TS) matching what the
transform actually receives. Added 6 new test cases for the patterns
that broke the regex approach.
2026-03-06 10:39:42 -06:00
Steve Faulkner 8f89555cd0 Rebuild request context after middleware modifies headers (#288)
* Rebuild request context after middleware modifies headers

The production server built the RequestContext for has/missing condition
matching before middleware ran. When middleware modified request headers
(e.g. injecting cookies via x-middleware-request-cookie), config rules
for redirects, rewrites, and custom headers evaluated against the
original pre-middleware values instead of the updated ones.

Move the reqCtx construction to after middleware header unpacking so
that config conditions see the current request state.

* Fix request context parity in dev server and deploy entry

Address review feedback: the dev server (index.ts) was not unpacking
x-middleware-request-* headers into req.headers before building reqCtx,
so middleware-injected cookies were invisible to config has/missing
conditions. The Cloudflare Worker entry (deploy.ts) had the same stale
reqCtx as prod-server.ts: built once before middleware, never rebuilt.

Dev server: unpack x-middleware-request-* headers into req.headers and
strip x-middleware-* internal headers from the response.

Deploy entry: change const to let, rebuild reqCtx after middleware
header unpacking.
2026-03-06 10:39:33 -06:00
Nathan Nguyen d056e50467 fix(config): pass correct Next.js phase to function-form configs + instrumentation test coverage (#273)
* fix(config): pass correct Next.js phase to function-form configs

unwrapConfig() hardcoded "phase-development-server" regardless of
whether vinext was running in dev or build mode. Apps that branch on
the phase argument in next.config.ts received wrong config during
production builds.

Map Vite's env.command to the correct Next.js phase constant:
- "serve" → "phase-development-server"
- "build" → "phase-production-build"

* test: add instrumentation module test coverage

The instrumentation module had zero tests. Add 11 tests covering:

- findInstrumentationFile: file discovery across root and src/,
  priority ordering, null when absent
- runInstrumentation: register() lifecycle, onRequestError handler
  storage, graceful handling of empty modules, error isolation
  when ssrLoadModule fails
- reportRequestError: handler invocation with correct args,
  no-op without handler, error isolation when handler throws

* fix(test): use shared helper for App Router static export test

The static-export test had its own startFixtureServer that loaded the
fixture's vite.config.ts via configFile. This caused RSC plugin timing
issues on Node 25 / macOS — the App Router dev server returned 404 for
all pages. The Pages Router section (same approach) worked because it
doesn't use the RSC plugin.

Switch the App Router section to use the shared startFixtureServerHelper
from helpers.ts, which uses configFile: false with explicit plugin
registration. This matches how app-router.test.ts (193 passing tests)
creates its dev server and avoids the RSC timing issue.

* fix: address PR review feedback

- Import PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants
  from shims/constants.ts instead of hardcoding string literals in
  index.ts and config/next-config.ts
- Add afterEach cleanup for temp directories in instrumentation tests

* refactor: use phase constants in tests, simplify pure function test setup

- Replace hardcoded phase strings in next-config.test.ts with
  PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants
- Remove unnecessary vi.resetModules() from findInstrumentationFile
  tests — it's a pure function with no module-level state to reset

* refactor: use shared startFixtureServer helper in static export tests

Remove the local startFixtureServer function — the shared helper from
tests/helpers.ts does the same thing. The Pages Router fixture's
vite.config.ts is just `vinext()` with no special options, so the
shared helper (which uses configFile: false + vinext plugin) works
identically.
2026-03-06 16:31:16 +00:00
Divanshu Chauhan (divkix) bd19a5ffaa fix: ensure clientDir exists before writing _headers file (#292)
The `closeBundle` hook writes `_headers` to `clientDir` (`dist/client/`)
without creating the directory first. Only `distDir` (`dist/`) is checked
earlier. When build cache is invalidated, `dist/client/` may not exist yet,
causing an ENOENT error on `writeFileSync`.

Add `fs.mkdirSync(clientDir, { recursive: true })` before the write.
`recursive: true` is a no-op if the directory already exists, so this is
safe on warm builds.
2026-03-06 13:47:33 +00:00
Dhruv Sharma e25da317bb feat(routing): respect pageExtensions for app/pages route discovery (#261)
* feat(routing): respect pageExtensions for app/pages route discovery

- read next.config pageExtensions into resolved config
- add shared file matcher for extension-aware scanning
- apply matcher to App Router and Pages/API route scanners
- wire pageExtensions through plugin route generation and file watching
- add unit tests for matcher behavior and mdx positive/negative routing cases

* fix(routing): normalize pageExtensions input, share ValidFileMatcher, fix static-export parity
2026-03-06 13:36:57 +00:00
Steve Faulkner 1f36a9cee0 Escape closing tags in inline script/style SSR output (#284)
The HTML parser treats </script> (and </style>) as the end of the
containing element regardless of JavaScript/CSS string context. When
dangerouslySetInnerHTML content contained these sequences, the SSR
output could produce malformed HTML where the tag boundary was broken.

Add escapeInlineContent() that replaces </script and </style with
<\/script and <\/style (case-insensitive). Apply it in:
- next/script: beforeInteractive SSR path
- next/head: reactElementToHTML for script and style tags

Includes 14 tests covering the escaping helper, Script component SSR,
and Head component SSR.
v0.0.22
2026-03-06 06:48:32 -06:00
James Anderson 49aaa2abac chore: improvements to various ci workflows (#300)
* chore: improvements to various ci workflows

* fix(ci): gate publish and preview-release on repository_owner == cloudflare
2026-03-06 12:33:01 +00:00
Steve Faulkner b0961a1974 Block access to .vite/ build metadata in production server (#287)
The .vite/ directory in the client build output contains build manifests
(ssr-manifest.json, manifest.json) that should not be publicly
accessible. The tryServeStatic() function now rejects requests to
.vite/ paths after URL decoding, which also handles encoded variants
like /%2Evite/.
2026-03-05 22:23:06 -06:00
Steve Faulkner 1a468dd0cd fix: apply headers, cookies, and status set by getServerSideProps to the response (#281)
* fix: apply headers, cookies, and status set by getServerSideProps to the response

getServerSideProps receives a res object that supports setHeader(),
statusCode, and end(). Previously, any headers, cookies, or status
codes set on this object were silently discarded in the final HTML
response.

This fixes two code paths:

1. Production (index.ts renderPage): Capture the res object from
   createReqRes and merge its accumulated headers, cookies, and
   status into the final Response. Also handle the short-circuit
   case where gSSP calls res.end() directly.

2. Dev server (dev-server.ts): Preserve res.statusCode set by gSSP
   so it flows through to streamPageToResponse. Also detect when
   gSSP calls res.end() to avoid writing to an already-ended stream.

Added getHeaders() and headersSent to the mock res object in
createReqRes so callers can read accumulated response metadata.

Test: Added fixture pages (ssr-headers.tsx, ssr-res-end.tsx) and
integration tests verifying custom headers, cookies, status codes,
and res.end() short-circuit behavior.

* refactor: explicitly forward gSSP headers in dev server

Address review feedback:
- Capture headers newly set by gSSP via snapshot/diff and forward them
  explicitly through extraHeaders, instead of relying on Node.js
  writeHead() implicitly merging setHeader() calls. This prevents
  silent header loss if streamPageToResponse is refactored.
- Support array-valued headers (e.g. multiple Set-Cookie) in
  streamPageToResponse by using res.setHeader() for arrays.
- Add comments explaining the middleware header behavior when gSSP
  calls res.end() (dev includes them via the shared res object,
  prod merges them via the worker entry).
2026-03-05 22:20:21 -06:00
Steve Faulkner c088fa7ff0 docs: add targeted test running guidance for agents (#283)
The full Vitest suite runs serially (~2 min) due to Vite deps optimizer
cache races between test files. Agents rarely need the full suite during
development. This adds a Running Tests section with a mapping from source
files to relevant test files, so agents run only what they need (seconds
instead of minutes).
2026-03-05 22:14:49 -06:00
Steve Faulkner 9217154223 fix: sanitize redirect destination in generated worker entry (#280)
The generated Pages Router worker entry was not wrapping the final
redirect destination in sanitizeDestination(), unlike the equivalent
code path in prod-server.ts. This ensures protocol-relative URLs
like //evil.com are normalized to /evil.com before being set as
the Location header.
2026-03-05 21:17:14 -06:00
Steve Faulkner f31ed8f379 fix: throw when dynamic request APIs are called inside cache scopes (#279)
* fix: throw when headers()/cookies()/connection() called inside cache scope

Next.js throws errors when dynamic request APIs (headers(), cookies(),
connection()) are called inside "use cache" or unstable_cache() scopes.
This is because cached results are shared across requests, so reading
request-specific data inside a cache scope would freeze one user's data
into the cache and return it to all subsequent callers.

This change adds the same guards:

- headers() and cookies() in next/headers now check if the current
  execution is inside a "use cache" or unstable_cache() ALS scope
- connection() in next/server does the same check
- unstable_cache() wraps its callback in an ALS scope so the guard
  can detect it
- The "use cache" runtime's cacheContextStorage is stored on globalThis
  via a well-known Symbol so headers.ts can read it without circular
  imports

Includes 9 regression tests covering all combinations of APIs and
cache scope types, plus a test verifying normal operation outside
cache scopes.

* fix: add draftMode() cache scope guard, remove dead code, add nested scope test

Address review feedback:
- Add throwIfInsideCacheScope() guard to draftMode() since it reads
  per-request cookies and mutates state via enable()/disable()
- Remove dead isInsideCacheScope export from cache-runtime.ts (never
  imported, headers.ts re-implements via globalThis Symbol lookup)
- Add tests for draftMode() in both "use cache" and unstable_cache scopes
- Add nested scope test (unstable_cache inside "use cache")
2026-03-05 21:14:18 -06:00
Steve Faulkner b5dcc76eb0 fix: validate request origin in dev server before Vite serves content (#278)
* fix: validate request origin in dev server before Vite serves content

The dev server origin check middleware was registered after Vite's
built-in file-serving middleware, so requests to internal paths
(/@*, /__vite*, /node_modules*) were served before any validation.

- Register origin check directly in configureServer body (pre-middleware)
  so it runs before Vite serves source files or internal endpoints
- Restrict CORS origin to localhost variants (127.0.0.1, [::1], *.localhost)
- Add Sec-Fetch-Site: cross-site check as defense-in-depth
- Block image endpoint redirects to internal Vite paths (/@*, /__vite*,
  /node_modules*)
- Add regression tests for both App Router and Pages Router dev servers

* refactor: use shared validateDevRequest() in pre-Vite middleware

Address review feedback:
- Replace inline origin-check logic with validateDevRequest() call to
  maintain a single source of truth (includes x-forwarded-host,
  allowedDevOrigins, and *.localhost support)
- Fix misleading Sec-Fetch-Site test in app-router.test.ts: use node:http
  instead of fetch() which strips Sec-Fetch-* forbidden headers
- Add defense-in-depth comment on the redundant Pages Router check
2026-03-05 20:34:25 -06:00
James Anderson cf60485fe3 fix: static-prefix routes not outranking dynamic catch-alls in route scoring (#275)
* fix: static-prefix routes not outranking dynamic catch-alls in route scoring

* share route precedence logic between app and pages router

* fix(routing): static-prefix boost collapsed infix-static ordering to a tie

* extra comment
2026-03-06 00:25:54 +00:00
James Anderson 534c0012d7 feat: log incoming requests in dev server (#271)
* feat: log incoming requests in dev server

* whitespace, undefined check, sentinel improvement

* bonk comments

* bonk comments
2026-03-05 23:50:25 +00:00
James Anderson af30c7438e fix: config redirects not applying to rsc requests (#255)
* fix: config redirects not applying to rsc requests

* fix poorly written test

* add better fix for soft nav url diffs

* escape the .

* fix: strip .rsc from beforeFiles rewrites; fix redirect recursion and double pushState
2026-03-05 22:41:44 +00:00
James Anderson df44bfb83f remove checked-in dist file (#269) 2026-03-05 21:35:24 +00:00
James Anderson 4fe92e9d79 fix: apps with both app + pages router returning 404s (#252)
* fix: apps with both app + pages router returning 404s

* apply same fix for api routes

* test for pages dir route

* always filter x-middleware headers, and preserve others on redirect
v0.0.21
2026-03-04 20:46:27 -06:00
Liu Xiaopai 5f593983bd fix(app-router): avoid React 19 dev Flight decode crash in SSR (#118)
* fix(app-router): avoid React 19 dev Flight decode crash in SSR (#50)

* fix(app-router): avoid React 19 dev Flight decode crash in SSR (#50)

* test(ecosystem): harden fixture cleanup and isolate better-auth signup data

* chore: address review nits on React 19 dev Flight decode fix

- Convert standalone JSDoc block to plain // comment to avoid confusion
  with the adjacent createRscEmbedTransform JSDoc
- Add note that VinextFlightRoot returns a thenable (not ReactNode),
  relying on React 19's internal thenable-as-child behavior
- Add comment noting the 'Switched to client rendering' assertion
  depends on React internal message text
- Clarify error.tsx fixture exists for realistic route structure, not
  because the error boundary fires during SSR
- Merge main into branch

---------

Co-authored-by: root <root@localhost.localdomain>
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-04 18:24:30 -06:00
Kidus Abebe d3b80ca247 feat: add Nix flake for reproducible dev environment (#238)
* feat: add Nix flake for reproducible dev environment

- flake.nix with devShell (Node.js 24, pnpm, oxlint, Playwright, gh)
- nix/checks.nix for hermetic lint/typecheck/test via nix flake check
- shell.nix for backward compat, .envrc for direnv integration

* fix: use corepack for exact pnpm version pinning
- corepack reads packageManager field from package.json
- --install-directory to writable .corepack/bin (Nix store is read-only)
- COREPACK_ENABLE_DOWNLOAD_PROMPT=0 for non-interactive downloads
- Remove checks.nix (sandboxed pnpm install infeasible)
- Fix stale check to use .modules.yaml
- Remove Playwright from Nix (let pnpm manage)
- Pin shell.nix to flake-compat rev from flake.lock

* chore: address review feedback on Nix dev shell

- Remove unused lib and stdenv function arguments
- Replace no-op PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD env var with comments
  documenting why we don't use Nix-provided browsers and the NixOS
  system library limitation
- Make pnpm install non-fatal in shellHook so the shell still opens
  cleanly if the lockfile is stale
- Improve staleness check readability (use -f guard instead of
  2>/dev/null redirect)

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-04 23:42:42 +00:00
milkboy2564 4ed5c89346 fix(rsc): emit actionable hint when non-plain object crosses server-client boundary (#250)
* fix(rsc): emit actionable hint when non-plain object crosses server-client boundary

  When a Server Component passes a class instance, ESM module namespace
  object, or null-prototype object as a prop to a Client Component,
  React's RSC serializer throws "Only plain objects, and a few built-ins,
  can be passed to Client Components from Server Components." In dev mode
  this error previously surfaced in the browser with no indication of
  which component or prop triggered it, making it hard to debug in large
  apps.

  Add a console.error in rscOnError that detects this specific error and
  prints an actionable guide explaining the four common causes, including
  the key ESM vs webpack difference: Vite produces true ESM module
  namespace objects (non-serializable), while Next.js webpack produces
  plain __esModule objects (serializable) — causing code to silently work
  in plain Next.js but correctly fail in vinext.

* test(rsc): add unit tests for rscOnError non-plain object dev hint

  Verify that the generated RSC entry code produced by generateRscEntry
  includes the dev-mode hint for issue #237. Tests cover:
  - Detection string for "Only plain objects" error is present
  - Hint is guarded by NODE_ENV !== "production"
  - Actionable guidance for module namespace objects is included
  - Actionable guidance for class instances is included
  - Existing digest return path for navigation errors is unaffected

* fix: escape newlines in rscOnError hint and add runtime tests

- Double-escape \n in the console.error hint strings so the generated
  JavaScript output contains valid string literals (the single-escaped
  \n was resolved to literal newlines by the template literal, producing
  unterminated strings that broke production builds).
- Add explicit 'return undefined' after the hint console.error block.
- Add runtime tests that extract rscOnError from the generated code via
  new Function() and verify: digest passthrough for navigation errors,
  console.error hint for RSC serialization errors, and undefined return
  for generic dev errors.

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-04 23:30:58 +00:00
Nathan Nguyen 706f40be0d fix: register MDX plugin at top level to fix transform ordering (#160)
* fix: register MDX plugin at top level to fix transform ordering

Plugins returned from the config() hook run after vite:import-analysis,
so @mdx-js/rollup transforms never fire. Move MDX to a top-level proxy
plugin with enforce:"pre" that delegates to the lazily-created MDX
instance once config() populates it.

Fixes #77

* address review: add config proxy, remove dead resolveId, fix comment separation

- Replace resolveId proxy with config proxy: @mdx-js/rollup has no
  resolveId hook, but its config() hook initializes format-aware
  processors with the Vite mode (development vs production). Without
  forwarding config(), MDX always compiles in production mode.
- Remove dead resolveId proxy (dead code, @mdx-js/rollup has no such hook)
- Separate MDX proxy comment from vinext:react-canary comment
- Update test to verify config and transform hooks on the proxy

* add E2E test for MDX import transform ordering

A regular page.tsx imports content.mdx as a React component and
renders it. This verifies the vinext:mdx proxy plugin transforms
.mdx files before vite:import-analysis runs (the core fix from #77).

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-04 22:47:28 +00:00
MD YUNUS b89afe0736 fix: wire up RouterContext.Provider in Pages Router (#239) (#249)
Add wrapWithRouterContext() function to the next/router shim that wraps
any React element in a <RouterContext.Provider>. This is the root-cause
fix: it builds the router value object directly from the SSR context
(server) or window.location + Router singleton (client) — no hooks, no
duplicate state, no extra component.

This provides a single source of truth for the router context value,
matching how Next.js provides RouterContext at the framework level.

Wrapping sites:
- dev-server.ts: main render, ISR re-render, error page render
- index.ts virtual module: production renderPage and ISR re-render
- Hydration script: client-side hydration entry
- navigateClient: client-side navigation re-renders

Tests:
- useRouter returns null without provider (default/App Router)
- useRouter returns full router when wrapWithRouterContext wraps tree
- useRouter reflects SSR context (pathname, query, asPath)
- wrapWithRouterContext is exported from next/router

Closes #239
v0.0.20
2026-03-04 22:12:29 +00:00
Stephen Zhou a7fa81f190 fix: tree shaking for google fonts (#245) 2026-03-04 22:01:12 +00:00
Stephen Zhou 5b9d7756b2 fix: set correct NODE_ENV for build (#247)
* fix: set NODE_ENV

* address review feedback: readability, test coverage, and nextConfig.env guard

- Replace nested ternary with if/else for NODE_ENV resolution
- Add comment explaining why beforeEach/afterEach is file-scoped
- Add test cases: serve (development), build without mode, user override
- Skip NODE_ENV key in nextConfig.env loop to prevent silent override
- Extract setupTmpProject helper to reduce test boilerplate

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-04 21:59:40 +00:00
harris.dev df3b2c3e37 fix(shims): add missing URL setters to NextURL (#125)
* fix(shims): add missing URL setters to NextURL

The NextURL class only had setters for pathname, search, and hash, leaving
port, host, hostname, protocol, href, username, and password as getter-only.

Libraries like next-intl's getAlternateLinksHeaderValue clone nextUrl then set
.port and .host to normalise the URL for alternate-link headers. In Cloudflare
Workers, x-forwarded-host is always present, so this code path is always
triggered, producing:

  TypeError: Cannot set property port of [object Object] which has only a getter

Add setters for all writable URL properties so NextURL behaves identically to
the platform URL class.

Fixes runtime middleware errors when using next-intl in production on Workers.

* revert build artifact to main (remove local machine paths)

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-04 17:57:40 +00:00
Abdel d21d541fbc fix typo in README.md (#248) 2026-03-04 17:23:03 +00:00
gagipro 702367da9b fix: normalize null-prototype params from matchPattern() for RSC serialization (#243)
matchPattern() uses Object.create(null), producing objects without
Object.prototype. The RSC serializer rejects these during dynamic route
rendering, causing 500 errors.

The fix spreads params ({...obj}) to restore a normal prototype, then
assigns properties onto the Promise to preserve synchronous access
(params.id, params.slug) for pre-Next.js 15 code. This satisfies both
constraints:
1. RSC serializer accepts objects with Object.prototype
2. Existing components can access params synchronously without await

Fixes #241
2026-03-03 19:02:29 -06:00
MD YUNUS 567f0ddf1d fix: export ServerInsertedHTMLContext from next/navigation shim (#145) (#151)
Wire ServerInsertedHTMLContext into the SSR pipeline so libraries like
Apollo Client, styled-components, and emotion that use
useContext(ServerInsertedHTMLContext) get a working callback registration
function during SSR.

Changes:
- Add ServerInsertedHTMLContext export to navigation.ts (single shared
  context, RSC-safe with null fallback)
- Wrap SSR rendered tree with ServerInsertedHTMLContext.Provider in
  handleSsr() — the provider value is useServerInsertedHTML itself, so
  both direct callers and useContext() callers push to the same
  ALS-backed callback array
- Add TypeScript declaration in next-shims.d.ts
- Add 8 integration tests verifying the actual Apollo Client pattern
  (useContext returns a usable callback, callbacks are flushed correctly)

Addresses review feedback from #151:
- Single context (no dead getServerInsertedHTMLContext)
- Provider wired into SSR pipeline (not disconnected)
- Tests verify real integration behavior, not React internals
2026-03-03 18:56:35 -06:00
Stringer fe0a48dde4 fix: lowercase package name before sanitizing Worker name (#159)
* fix: lowercase package name before sanitizing Worker name

* test: assert exact sanitized project name value

Strengthen the sanitization test to assert the exact expected value
('my-app-v2') instead of just matching a regex pattern. The regex
assertion passed even when the bug was present.

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-03 17:35:54 +00:00
Justin Finnegan 8058c34761 fix: use detected package manager in install error/hint messages (#187)
* fix: use detected package manager in error/hint messages

Error messages and install hints in index.ts, check.ts, and cli.ts
were hardcoded to use `npm install`. This PR uses the existing
`detectPackageManager()` / `detectPackageManagerName()` utilities
(already used correctly in deploy.ts) so that Yarn, pnpm, and Bun
users see the right install command.

Before (on a Yarn project):
  Run: npm install -D @vitejs/plugin-rsc

After:
  Run: yarn add -D @vitejs/plugin-rsc

Fixes #186

* fix: use detectPackageManager() in cli.ts per review feedback

Replace detectPackageManagerName() + manual 'add -D' with
detectPackageManager() which already returns the correct install
command. This avoids producing 'npm add -D' (undocumented alias)
for npm users and stays consistent with check.ts and index.ts.

---------

Co-authored-by: Homiebot <homiebot@homielab.tech>
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-03 17:13:41 +00:00
Liu Xiaopai d5e1f0029d fix(init): detect Bun and invoking package manager reliably (#139)
* fix(init): detect bun and invoking package manager reliably

* refactor: address review feedback on detectPackageManagerName

- Combine bun.lock/bun.lockb checks into block form for consistency
  with the npm lockfile check
- Accept optional env parameter (defaults to process.env) so the
  function can be unit-tested without mutating global state

---------

Co-authored-by: root <root@localhost.localdomain>
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-03 17:01:05 +00:00
mounir eefee6d849 fix: resolve conditional React Hook calls in Link and Script shims (#223)
* fix: resolve conditional React Hook calls in Link and Script shims

Move early returns after all hook calls to satisfy the Rules of Hooks.

- link.tsx: the dangerous-scheme early return was before useState/useRef/
  useEffect/useCallback/useMemo — moved it to after all hooks
- script.tsx: the SSR early return was before useEffect — moved useEffect
  before the check since it never runs during SSR anyway

* fix: skip prefetch setup for dangerous href schemes

---------

Co-authored-by: Mounir Haddou <mhaddou@c1r2p4.1337.ma>
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-03 16:40:17 +00:00