187 Commits

Author SHA1 Message Date
Nathan Nguyen 032a2afb41 fix: preserve next.config plugin aliases for next-intl (#342)
* fix: preserve next.config plugin aliases

* refactor: probe wrapped webpack config once

* fix: tighten next.config alias warnings
v0.0.25
2026-03-08 15:30:05 +00:00
James Anderson a5d65b851f test: add a fixme for middleware invocation count tracking (#350)
* test: add a fixme for middleware invocation count tracking

* test: clarify why registerCalled uses ESM live binding not globalThis
2026-03-08 15:19:54 +00:00
James Anderson 53257a8630 fix: layout not receiving correct params (#348)
* fix: layout not receiving correct params

* fix: layout not getting correct params on 404
2026-03-08 14:59:06 +00:00
James Anderson 4e0294fc4f fix: middleware crash in dev mode with cloudflare plugin (#344)
* test: middleware with cloudflare plugin and routing bypass

* fix: use module runner for pages middleware

* add pages router dev test
2026-03-08 14:04:11 +00:00
MD YUNUS 3a4ef70c25 fix: document ESM resolution behavior for validator and similar packages (#189) (#193)
Vinext now automatically detects installed packages with broken ESM
resolution and adds them to resolve.noExternal for both the rsc and
ssr environments. This fixes 'Cannot find module' errors when using
packages like validator that have extensionless internal ESM imports.

Root cause: Vite externalizes dependencies for SSR by default, but
some packages use extensionless imports (e.g. ./util/assertString)
that fail under Node.js strict ESM resolution. Additionally, Vite 7's
Environment API means ssr.noExternal only affects the ssr environment,
not the rsc environment where server components run.

Changes:
- Add KNOWN_PROBLEMATIC_ESM_PACKAGES list (validator, date-fns)
- Add collectNoExternalPackages() to merge user config with auto-detected packages
- Propagate noExternal to both rsc and ssr environments
- Add validator ecosystem test fixture and integration tests
- Add troubleshooting documentation

Fixes #189
2026-03-08 11:14:46 +00:00
James Anderson f27796b922 fix: don't intercept requests in connect handler when cloudflare plugin is present (#343)
* test: assert App Router wins over Pages Router connect handler with cloudflare plugin

* update req.url with url
2026-03-08 10:28:09 +00:00
Nathan Nguyen 2f21bb3a7d fix: pass NextFetchEvent as second argument to middleware (#340)
* fix: pass NextFetchEvent as second argument to middleware

All 3 middleware invocation sites were calling `middlewareFn(nextRequest)`
with only one argument. The `NextMiddleware` type expects `(request, event)`
but `NextFetchEvent` was never instantiated or passed. Any middleware using
`event.waitUntil()` would throw at runtime because `event` is `undefined`.

Fix: instantiate `NextFetchEvent({ page })` at each call site, pass it as
the second argument, and drain `_waitUntilPromises` via
`Promise.allSettled()` (fire-and-forget) after middleware returns.

Call sites fixed:
- `packages/vinext/src/server/middleware.ts` (shared runner, used by prod)
- `packages/vinext/src/index.ts` (Pages Router dev codegen)
- `packages/vinext/src/server/app-dev-server.ts` (App Router dev codegen)

* address review: add drainWaitUntil(), wire ctx.waitUntil() in Workers entry, fix async test

* fix deploy tests: update assertions for ctx-aware fetch and runMiddleware signatures

* address review: fix misleading comment, update JSDoc, use queueMicrotask in test

* pass undefined explicitly to runMiddleware in prod-server (no ctx on Node.js)

---------

Co-authored-by: James <james@eli.cx>
2026-03-08 10:01:47 +00:00
Jared Stowell 397a8dceda Fix fetch cache key collisions for Request and FormData bodies (#332)
* Fix fetch cache key collisions for Request and FormData bodies

* Add additional tests

* Address codex feedback by reading incrementally with early exit on content-length check + regression test

* Add regression test for already-consumed body

* Harden fetch cache keying for Request form bodies + tests

* Preserve FormData insertion order + add file name/type into cache key payload + tests

* Fix PR feedback + additional regression tests
2026-03-08 09:34:32 +00:00
Nathan Nguyen 5d595e08cb fix: rewrite useSelectedLayoutSegment(s) to use context-based child segments (#341)
The previous depth-based approach sliced the pathname by a numeric URL
depth, which broke when route groups were present — they don't appear
in URLs but Next.js includes them in the segments array. This also
meant dynamic params were returned as raw URL strings rather than
resolved values matching the route tree.

Replace `layoutSegmentDepths: number[]` with two new fields:
- `routeSegments: string[]` — filesystem segments from app/ root
- `layoutTreePositions: number[]` — directory depth per layout

Each layout now receives pre-computed child segments through React
context (including route groups, with dynamic params resolved to
actual values and catch-all segments joined with "/").

Additional fixes found during code review:
- Pass matched params to error/fallback rendering paths so dynamic
  segments resolve correctly during error page rendering
- Skip optional catch-all segments when param is empty/undefined
- Extend routeSegments for parallel slot sub-routes
- Update stale type declaration in next-shims.d.ts

Ported from Next.js: test/e2e/app-dir/hooks/hooks.test.ts
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/hooks/hooks.test.ts
2026-03-08 09:09:28 +00:00
Jared Stowell 8a5192d1cf Fix unstable_cache undefined cache (#339) 2026-03-08 08:58:17 +00:00
Jared Stowell 49689537d0 Fix and fully implement allowedDevOrigins (#337) 2026-03-08 08:56:15 +00:00
Divanshu Chauhan (divkix) 6762472e70 fix: normalize same-origin absolute URLs for client-side navigation (#336)
* fix: normalize same-origin absolute URLs for client-side navigation

When <Link href="https://example.com/path"> is used and example.com
matches the current origin, vinext now does SPA navigation instead
of a full page reload. This matches Next.js behavior.

Adds toSameOriginPath() utility that extracts the local path from a
same-origin absolute URL. Applied consistently across:
- link.tsx handleClick() and prefetch paths
- navigation.ts navigateImpl() (App Router)
- router.ts push()/replace() (Pages Router hook + singleton)

Closes #335

* fix: handle protocol-relative URLs in toSameOriginPath()

Use window.location.origin as base for `//` URLs so they resolve
correctly for same-origin detection, while keeping `new URL(url)`
(no base) for http/https URLs to preserve existing behavior.

Addresses Copilot review comment on PR #336.

* Update tests/link.test.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: James Anderson <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-03-08 08:55:07 +00:00
17hz d471a52ed8 fix: stop stripping credential headers from rewrite proxy requests (#277)
* fix: stop stripping credential headers from rewrite proxy requests

Rewrite destinations are typically the user's own backend services,
not untrusted third-party origins. Stripping cookie, authorization,
x-api-key, and proxy-authorization headers breaks authentication
for proxied API requests.

Next.js does not strip these headers for rewrites either — it
forwards them as-is to the destination.

* test: update proxy tests to assert credential headers are forwarded

Update tests to match new behavior where credential headers (cookie,
authorization, x-api-key, proxy-authorization) are forwarded through
the rewrite proxy, matching Next.js behavior. x-middleware-* headers
are still stripped.
2026-03-07 23:31:50 +00:00
James Anderson c4c67fa5f4 fix: pages router instrumentation error reporting for api routes (#334)
* test: pages router instrumentation

* fix error handling

* add pages route to app dir

* fix api route type
2026-03-07 23:22:13 +00:00
James Anderson cb2bbc65b6 test: app router instrumentation with cloudflare plugin (#333) 2026-03-07 23:04:41 +00:00
James Anderson 80acf27933 fix: register instrumentation in rsc entry and put error handler in global scope (#330)
* test: instrumentation

* store onRequestError on globalThis

* add instrumentation registration app-dev-server

* guard against double calling runinstrumentation for appdir
2026-03-07 22:40:27 +00:00
James Anderson a7e68260f5 chore: add note about PLAYWRIGHT_PROJECT in AGENTS.md (#331) 2026-03-07 22:08:28 +00:00
James Anderson 3e0983178f fix: restrict bonk commands to repo members, collaborators, and owners (#327) 2026-03-07 17:32:22 +00:00
James Anderson c9b0a32856 fix: suppress Invalid hook call warning during layout pre-render probe (#324)
* fix: suppress Invalid hook call warning during layout pre-render probe

Layout components that call use(promise) trigger React's dev-mode
'Invalid hook call' warning when invoked outside React's render cycle.
The layout pre-render loop calls LayoutComp() directly to probe for
redirect()/notFound() throws — the same pattern the page pre-render
already handles with a console.error suppressor.

Wrap the layout pre-render loop in a try/finally that silences the
expected warning, matching the existing suppression on the page probe.

* refactor: use AsyncLocalStorage to scope hook-warning suppression per-request

Replace the per-request console.error swap/restore pattern with a single
module-level patch gated behind an AsyncLocalStorage store. Each pre-render
probe (layouts and page) now runs inside _suppressHookWarningAls.run(true)
so the suppression is scoped to that specific async execution context.

This eliminates the race window where concurrent requests could have one
request's suppression filter silently drop real errors from another.
2026-03-07 17:15:26 +00:00
James Anderson b1ce55d0c4 feat: add global.d.ts for window.__VINEXT_* and globalThis.__VINEXT_* globals (#323)
* feat: add global.d.ts for window.__VINEXT_* and globalThis.__VINEXT_* globals

Declares all 23 __VINEXT_* runtime globals in a single ambient declaration file
(packages/vinext/src/global.d.ts), eliminating the scattered (window as any) and
(globalThis as any) casts throughout the shims and server code.

* feat: declare window.__NEXT_DATA__ in global.d.ts, remove remaining (window as any) casts

- Add typed Window.__NEXT_DATA__ declaration covering all fields read by
  router.ts, link.tsx, and entry.ts (props, page, query, isFallback,
  locale/locales/defaultLocale, __vinext, __pageModule, __appModule)
- Remove all (window as any).__NEXT_DATA__ casts in router.ts (×3),
  link.tsx (×1), and entry.ts (×1)
- Drop the win = window as any alias in navigateClient() — it was only
  needed to write __NEXT_DATA__ which is now typed on Window
- Remove redundant export {} at the bottom of global.d.ts; the
  import type { Root } at the top already makes the file a module
2026-03-07 17:12:42 +00:00
Nathan Nguyen 15a4a45df9 test: add unit tests for url-safety isDangerousScheme (#317) 2026-03-07 17:12:21 +00:00
Nathan Nguyen c1ceef2aaf test: add unit tests for Pages Router api-handler (#318)
Cover body parsing (JSON, form-urlencoded, plain text, empty, malformed),
cookie parsing (single, multiple, values with =, missing header),
req/res extensions (status chaining, json, send for objects/strings/numbers/null,
redirect with default 307 and custom status codes), query string and dynamic
param merging (including array promotion for duplicate keys), MAX_BODY_SIZE
enforcement (413 for >1 MB), and error handling (missing default export,
non-function export, handler throws, ssrFixStacktrace integration).
2026-03-07 17:11:56 +00:00
Nathan Nguyen 048c49f98d test: add unit tests for next/document shim (#319)
Cover Html, Head, Main, NextScript, and default Document with
renderToString assertions for attributes, children, placeholder
content, and correct nesting order.
2026-03-07 17:10:56 +00:00
Nathan Nguyen a78266481b feat: support export const revalidate on App Router GET route handlers (#320)
* chore: add .worktrees/ to gitignore

* feat: support `export const revalidate` on App Router GET route handlers

Read the route segment config `revalidate` from route handler modules
and set `Cache-Control: s-maxage=N, stale-while-revalidate` on GET
responses, matching Next.js behavior. Only applies when the handler
does not set its own Cache-Control header.

Unskip two E2E fixme cases and add vitest coverage.

* fix: guard revalidate=0 and add proper test fixtures

- Skip Cache-Control injection when revalidate is 0 (means "never cache")
- Add /api/custom-cache fixture that sets its own Cache-Control header
- Add /api/no-cache fixture with revalidate=0
- Fix "does not override" test to use a fixture that actually sets Cache-Control
- Add revalidate=0 test case
2026-03-07 17:10:25 +00:00
James Anderson ce1d988d5c refactor(routing): extract shared compareRoutes comparator to utils (#322)
The sort comparator (routePrecedence diff + localeCompare tiebreaker)
was duplicated in three places — scanPageRoutes, scanApiRoutes, and
appRouter. Extract it as compareRoutes() in routing/utils.ts, which
already exports routePrecedence, and replace all three call sites.
2026-03-07 15:52:22 +00:00
James Anderson 57fd78f70f fix: useRouter() returns stable singleton to prevent unnecessary re-renders (#321)
Next.js returns the same router object reference on every call to
useRouter() from next/navigation. Our implementation was creating a new
object literal on each call, which breaks components that rely on
referential equality — useMemo/useEffect dependency arrays and
React.memo bailouts would fire unnecessarily on every render.

Hoist the router object to module level so useRouter() always returns
the same reference. All methods close over module-level state and carry
no per-render data, so this is safe.

Adds two regression tests: one asserting reference equality across
multiple calls, and one verifying the singleton exposes all expected
navigation methods.
2026-03-07 15:48:54 +00:00
Nathan Nguyen 8b356e4497 test: use metadata API in app-basic root layout (#316) 2026-03-07 15:14:33 +00:00
Divanshu Chauhan (divkix) 151f099089 fix: CSS imports from node_modules crash SSR (#310)
* fix: CSS imports from node_modules crash SSR (#270)

When a node_modules package imports a `.css` file, SSR crashes with
"Unknown file extension .css" because Vite externalizes node_modules
by default — causing Node's native ESM loader to handle the import,
which it can't do for non-JS files.

Set `noExternal: true` on all server environments (Pages Router SSR,
App Router RSC, App Router SSR) to force node_modules through Vite's
transform pipeline. This matches Next.js behavior of bundling
everything for SSR. Explicitly listed `external` packages (react,
satori, resvg, yoga-wasm-web) take precedence per Vite's rules.

Skipped for Cloudflare/Nitro targets which already bundle everything.

* fix: externalize better-sqlite3 and fix test hermiticity

- Add better-sqlite3 to ssr.external in better-auth fixture config.
  Native addon packages using `bindings` break when Vite transforms them
  (stack trace introspection fails). This mirrors Next.js's
  serverExternalPackages — users with native addons must externalize them.

- Fix Pages Router CSS test mutating real workspace node_modules.
  Previously symlinked tmpDir/node_modules → repo root, then created
  fake-css-lib inside the symlink target, leaking into the real workspace.
  Now creates a real node_modules dir and symlinks individual packages.

* fix: propagate user ssr.external into RSC and SSR environments

Vite 6+ treats `ssr.*` config as sugar for `environments.ssr.*` only —
it does NOT propagate to custom environments like `rsc`. This means
packages listed in `ssr.external` (e.g. better-sqlite3) were still
being transformed by Vite in the RSC environment, breaking native
addon resolution via the `bindings` package.

Read user's `ssr.external` array and merge it into both
`environments.rsc.resolve.external` and
`environments.ssr.resolve.external` so native addons are properly
externalized from all server environments.

Fixes CI failures in better-auth ecosystem tests on Linux where the
bindings package's stack trace introspection fails when the native
module is transformed by Vite.

* fix: address review comments on node-modules CSS SSR crash fix

- Fix wrong repo URL in test comment (nicepkg/vinext -> cloudflare/vinext)
- Propagate ssr.external: true into RSC and SSR environments correctly
- Move fake test packages from committed node_modules to __test_packages__
  with file: protocol dependencies, removing fragile force-tracked files
- Add CSS module (fake-css-module-lib) coverage to Pages Router test
- Narrow Pages Router test symlinks to only required packages

* fix: address bonk review comments

- Update stale comment on top-level ssr block to reflect both React
  externalization and noExternal: true behavior; document that user
  ssr.noExternal is intentionally superseded by vinext's setting
- Remove now-redundant noExternal: ['better-auth'] from better-auth
  fixture (vinext's blanket noExternal: true already covers it)
- Merge Pages Router test's two separate 'it' blocks (both hit same URL)
  and add missing fake-css-module-rendered assertion for .module.css
- Guard symlink .catch() to only swallow EEXIST errors, not all failures
- Add comment explaining appDir: tmpDir usage in Pages Router test

---------

Co-authored-by: James <james@eli.cx>
2026-03-07 15:06:04 +00:00
James Anderson 8cd07c29d9 fix: override next.config.js headers with middleware headers (#268)
* implement dangerous.middlewareHeadersOverrideNextConfigHeaders from opennext

* fix: preserve multi-value set-cookie in RSC middleware header merge, separate timing comment

* fix: accumulate Vary header in RSC middleware merge to preserve RSC, Accept

* Update packages/vinext/src/server/app-dev-server.ts
2026-03-07 15:05:02 +00:00
James Anderson 70b18536ab fix: warn when existing vite.config.ts is missing cloudflare plugin on deploy (#315)
* fix: warn when existing vite.config.ts is missing cloudflare plugin on deploy

Closes #312.

- Add `viteConfigHasCloudflarePlugin()` to deploy.ts: when `vinext deploy`
  finds an existing vite.config.ts that doesn't import @cloudflare/vite-plugin,
  it now prints an actionable warning with the exact config fix rather than
  silently proceeding to a broken build.
- Add README prerequisites section to the Cloudflare Workers deploy docs:
  covers wrangler login vs CLOUDFLARE_API_TOKEN (with required permissions)
  and how to supply account_id.
- Add 7 unit tests for `viteConfigHasCloudflarePlugin()`.

* fix: pages router warning shows cloudflare() not cloudflare(())
2026-03-07 14:50:58 +00:00
James Anderson 90771f7b00 refactor(ci): extract composite actions and improve workflow hygiene (#314)
* refactor(ci): extract composite actions and improve workflow hygiene

- Add composite actions: setup-node-pnpm, build-vinext, deploy-example, comment-preview-urls
- Cache Playwright browsers in CI to avoid ~150MB download per E2E run
- Add concurrency group to deploy-examples.yml to prevent deploy pileup
- Replace inline CJS node script in create-next-app job with portable bash
- Convert ecosystem-run.yml inline Node scripts from CJS require() to ESM
- Apply composite actions across all workflows to eliminate repeated boilerplate

* refactor(ci): revert to inline steps, keep only setup-node-pnpm composite action

Remove the build-vinext, deploy-example, and comment-preview-urls composite
actions and inline their steps back into each workflow. Keep setup-node-pnpm
as the one reusable action (pnpm install + node setup). Retain all other
improvements from the previous commit: concurrency groups, Playwright browser
cache, bash-based dev-server check, ESM node scripts in ecosystem-run.

* refactor(ci): use setup-node-pnpm composite action consistently

Every workflow that does pnpm/node setup + install now uses the
setup-node-pnpm composite action instead of the inline 3-step triplet.
Added optional registry-url input to support publish.yml's npm auth.

Exceptions (intentionally kept inline):
- bonk.yml / bigbonk.yml: issue_comment trigger, GHAS constraint
- tip.yml notify-on-failure: node-only, no pnpm install needed
2026-03-07 13:23:34 +00:00
Divanshu Chauhan (divkix) 6606006839 fix: stub node:async_hooks in client builds via virtual module (#306)
* fix: stub node:async_hooks in client builds via virtual module

Several shims (headers, cache, navigation-state, etc.) import
AsyncLocalStorage from node:async_hooks. Since resolve.alias applies
globally, these shims resolve in every environment including client.
Vite externalizes node:async_hooks to __vite-browser-external in browser
builds — an empty stub with no named exports — causing Rollup errors.

Add a vinext:async-hooks-stub plugin that intercepts node:async_hooks
in the client environment and provides a no-op AsyncLocalStorage via a
virtual module. This is semantically correct: shims already guard with
`_als.getStore() ?? fallback` patterns, so undefined store returns
produce correct client-side behavior.

Unlike the external file approach in #293, this uses a virtual module
inline in the plugin — no additional files, no build artifacts.

Closes #293

* fix: address async-hooks-stub code review feedback

- Convert load hook to object form with handler() for consistency
- Match bare `async_hooks` specifier in addition to `node:async_hooks`
- Add intentionally-minimal comment explaining stub scope
- Add regression tests for resolveId and load hook behavior

* refactor: extract async-hooks-stub plugin to plugins/ and test against real implementation

Move the vinext:async-hooks-stub plugin object from an inline definition in
index.ts to packages/vinext/src/plugins/async-hooks-stub.ts, exporting it as
asyncHooksStubPlugin. Import and use it directly in index.ts.

Update the regression test to import the real plugin via _asyncHooksStubPlugin
and invoke its resolveId/load handlers with a mock context, eliminating the
duplicated logic that the test previously used as a workaround for the plugin
being unexported and inline.

* fix: add filter to load hook, evaluate stub at runtime in test

- Add filter to load hook object form for consistency with resolveId
  and to opt into Vite/Rolldown hook optimization path
- Replace overlapping string-assertion test with one that evaluates
  the generated source via new Function, testing actual runtime
  behavior (getStore returns undefined, run/exit pass through return values)

* fix: remove control-char regex from load filter to fix lint

* fix: suppress no-control-regex for load filter via eslint-disable comment

* refactor: derive load filter regex from ASYNC_HOOKS_STUB_ID constant

* Update build-optimization.test.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

* Update index.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

* fix: widen run() type signature to accept rest args and typed callback

---------

Co-authored-by: James <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-03-07 13:00:26 +00:00
James Anderson 5421c5d526 docs: remove unnecessary -- from pnpm test examples (#313) 2026-03-07 11:49:39 +00:00
James Anderson 09d9d4cde0 refactor: extract applyMiddlewareRequestHeaders into config-matchers (#305)
* refactor: extract applyMiddlewareRequestHeaders into config-matchers

Move the x-middleware-request-* unpacking + postMwReqCtx rebuild logic
out of prod-server.ts and deploy.ts and into a shared
applyMiddlewareRequestHeaders() in config-matchers.ts, eliminating ~30
lines of duplicated code from each callsite.

* fix(test): update deploy test to reflect applyMiddlewareRequestHeaders refactor
2026-03-07 11:11:56 +00:00
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