Commit Graph

1925 Commits

Author SHA1 Message Date
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
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