* Skip deploy previews for fork PRs that lack Cloudflare secrets
Fork PRs don't have access to repository secrets (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID), so deploy/smoke-test/comment steps always fail.
Add a fork detection condition to skip these steps gracefully. The build
steps still run, so example builds are still validated for fork PRs.
* fix: align Pages Router worker entry with prod-server request handling
The generated Cloudflare Worker entry for Pages Router apps was missing
several request handling steps that the Node.js production server
(prod-server.ts) already handled. This brings the two in sync:
- Run middleware (runMiddleware) before routing
- Apply next.config.js redirects, rewrites (before/after/fallback), and headers
- Handle basePath stripping and trailing slash normalization
- Merge middleware response headers with correct precedence
- Guard renderPage with typeof check
Also updates the two Pages Router examples (pages-router-cloudflare,
realworld-api-rest) and exports vinext/config/config-matchers so the
worker entry can import the shared matching utilities.
* chore: sync pnpm-lock.yaml with upstream package.json changes
* refactor: wrap Vite configuration with `defineConfig`
* refactor: remove type any
* refactor: remove as any
* refactor: remove as any
* refactor: use UserConfig type instead of defineConfig for internal config object
defineConfig is an identity function meant for user-land vite.config files.
Frameworks should use the UserConfig type annotation directly, which provides
the same type inference without the semantic mismatch.
---------
Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com>
* fix: use static import for renderToStaticMarkup to eliminate dual-import warning
The generated SSR entry imported renderToReadableStream statically from
react-dom/server.edge at the top level, but later used a dynamic
import() for renderToStaticMarkup from the same module. Rollup detects
this mixed static+dynamic import pattern and emits a warning for every
build:
"react-dom/server.edge" is imported as both a static and dynamic
import in the same module
Fix: add renderToStaticMarkup to the existing static import, removing
the redundant dynamic import(). The module was already loaded — the
dynamic import added no lazy-loading benefit since it runs
unconditionally during SSR.
* Remove comment on renderToStaticMarkup import
Removed comment about static import of renderToStaticMarkup.
* Remove unneccesary test
---------
Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com>
* fix: log errors from failed error reporting calls
Previously, error reporting calls (.catch(() => {})) silently swallowed
errors, making debugging impossible when error reporting itself failed.
This fix adds proper error logging:
- Server action error reporting failures are now logged
- Route handler error reporting failures are now logged
- Async component errors in loading boundaries are logged in dev mode
The third case is intentional (loading boundaries handle errors), but
now logs in development for visibility.
* revert: keep silent .catch for loading boundary pre-render test
The testResult.catch() suppresses expected rejections from async
components with loading boundaries — redirect/notFound errors are
handled by rscOnError during streaming. Logging here would be noisy
since this is a normal code path, not an error condition. This matches
Next.js behavior where prospective render rejections are silent by
default.
---------
Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com>
RSC navigation and prefetch fetches were missing ,
causing browsers to omit cookies from these requests. This silently broke
cookie-based authentication (Better Auth, NextAuth, etc.) during client-side
navigation: a full-page load would succeed (cookies sent normally) but any
subsequent <Link> navigation to a protected route would fail authentication
because the RSC fetch arrived without the session cookie.
Three fetch call sites are fixed:
- server/app-dev-server.ts — __VINEXT_RSC_NAVIGATE__ navigation fetch
- shims/link.tsx — <Link> prefetch on viewport intersection
- shims/navigation.ts — router.prefetch() call
* fix(navigation): cache getServerSnapshot fallback in useSearchParams
useSyncExternalStore compares snapshots with Object.is. The previous
getServerSnapshot returned `new URLSearchParams()` on every call when no
server context was available, producing a new object reference each time
and triggering an infinite re-render loop in React.
Fix: introduce a module-level `_cachedServerSearchParams` that is
allocated once and reused, matching the same referential-stability
pattern already used by `getSearchParamsSnapshot` for the client
snapshot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refactor cached search params and pathname logic
Refactor cached variables for search params and pathname for better referential stability. Consolidate logic to avoid infinite re-renders.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com>
URL.pathname returns percent-encoded strings (e.g. %20 for spaces),
which Vite cannot resolve as filesystem paths. This causes all App
Router tests to fail when the project is in a directory with spaces
(common on macOS with iCloud, Google Drive, OneDrive).
Replace .pathname with fileURLToPath() from node:url, which properly
decodes the URL to a filesystem path. This pattern is already used
elsewhere in the codebase (cloudflare/tpr.ts).
Fixes#129
Config matchers (redirects, rewrites, headers) now receive the decoded,
normalized pathname so that percent-encoded request paths like /%61bout
correctly match config rules defined for /about.
Both dev and prod servers rebuild the working URL from the decoded
pathname + original query string, ensuring all downstream consumers
operate on canonical paths.
Also adds unit and integration tests for percent-encoded path handling
in config matchers across App Router and Pages Router (dev + prod).
Strip all x-middleware-* prefixed headers from responses in both the
App Router and Pages Router dev servers before sending to clients.
These headers are internal routing signals (x-middleware-next,
x-middleware-rewrite, x-middleware-override-headers, etc.) and should
not be visible to end users.
The prod server already stripped these correctly. This brings the dev
servers into parity.
Ensures the pathname is decoded exactly once (via decodeURIComponent +
normalizePath) at the request entry point, and all downstream consumers
(middleware, config matchers, route matching) receive the same canonical
path. Previously, some code paths decoded the pathname a second time,
which could cause inconsistent matching between middleware and routing.
Also removes redundant decodeURIComponent calls from matchRoute and
matchConfigPattern catch-all branches that are no longer needed since
the caller now guarantees a pre-decoded pathname.
* fix: fetch cache key generation — handle all body types, include all headers minus blocklist
- Make buildFetchCacheKey async to support stream/blob body serialization
- Handle all BodyInit types: string, Uint8Array, ReadableStream, FormData, Blob, URLSearchParams
- Switch header strategy from allowlist (3 auth headers) to blocklist (traceparent/tracestate), matching Next.js
- Include RequestInit fields (mode, redirect, credentials, etc.) in cache key
- SHA-256 hash the cache key for compact, deterministic storage
- Fix spent stream bug: restore _ogBody in stripNextFromInit so originalFetch gets usable body
- Add 19 new tests covering body types, header inclusion, body restoration, and URLSearchParams
* Improve fetch cache key generation
Replace simple auth-header extraction with a unified header collector, add a header blocklist and cache version prefix, and introduce async cache key building. serializeBody now handles various body types (Uint8Array, streams, URLSearchParams, FormData, Blob, string), preserves the original body on init._ogBody, and body content is included in the cache key. The cache key is generated by normalizing URL, method, headers and init options and hashing them with SHA-256. buildFetchCacheKey is now async and awaited where used, and stripNextFromInit restores the preserved original body. Also add hasAuthHeaders and move auth header list into a constant.
* PR #111 approved. 52 tests pass, 3 fixes correct.
Co-authored-by: threepointone <threepointone@users.noreply.github.com>
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: threepointone <threepointone@users.noreply.github.com>
The dev server's __sanitizeDestination only collapsed leading '//' but
ignored leading backslashes (e.g. '\evil.com'). The prod version in
config-matchers.ts already used /^[\/]+/ to handle both. Align the
dev codegen to match.
Wrap all decodeURIComponent calls on user-controlled URL paths in
try/catch to return 400 Bad Request instead of throwing an uncaught
URIError that crashes the Node process.
A single request with a malformed percent-encoded path (e.g. /%E0%A4%A)
could terminate the entire server process. This affected all server
entry points: prod server (App + Pages Router), dev server middleware,
Cloudflare Worker entry, and generated RSC/middleware handlers.
Fixes:
- prod-server.ts: App Router and Pages Router request handlers
- app-router-entry.ts: Cloudflare Worker entry
- app-dev-server.ts: Generated RSC entry handler
- index.ts: Dev server connect middleware + generated middleware runner
+ generated NEXT_LOCALE cookie parser
- middleware.ts: Pages Router dev middleware runner
Includes 11 regression tests across app-router, pages-router, and
features test suites.
* fix: close security gaps in init.ts shell exec and app-dev-server external proxy
Gap 1 (GHSA-w7vc-6jjg-498h): migrate init.ts from execSync to execFileSync
with argument array, matching the pattern already applied in deploy.ts and
cli.ts. Inputs are hardcoded package names so practical risk is near zero,
but this eliminates the last instance of the flagged pattern.
Gap 2 (GHSA-pf96-3m7r-pv3j): harden the inline __proxyExternalRequest in
the generated RSC entry (app-dev-server.ts) to strip Cookie, Authorization,
x-api-key, proxy-authorization, and all x-middleware-* headers before
forwarding to external rewrite targets. Also adds a 30-second
AbortSignal.timeout. This brings the dev-server code path into parity with
the already-fixed proxyExternalRequest in config-matchers.ts.
Also fixes a missed GSP redirect path in dev-server.ts where backslash
normalization was not applied (GHSA-79rw-r8hc-qg8v — the GSSP path was
already committed).
Includes an integration test that verifies credential stripping end-to-end
via a mock HTTP server and a conditional external rewrite in next.config.ts.
* fix: unify isExternalUrl to detect all URL schemes and protocol-relative URLs
All four copies of isExternalUrl (config-matchers.ts, router.ts,
navigation.ts, app-dev-server.ts inline) now use the same RFC 3986
scheme regex: /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//')
Previously:
- config-matchers.ts and app-dev-server.ts only checked http:// and
https:// (missing // protocol-relative)
- router.ts and navigation.ts checked http://, https://, and // but
missed exotic schemes (data:, javascript:, blob:, ftp:, etc.)
Now all copies are consistent and detect any URL scheme per RFC 3986
plus protocol-relative URLs.
Adds test cases for exotic schemes, protocol-relative, hash-only, and
bare string inputs.
* fix: add shell: true to execFileSync for Windows .cmd file compat
On Windows, package managers (pnpm, npm, yarn) are .cmd batch files.
execFileSync without shell: true cannot execute them. Adding shell: true
lets the OS shell resolve .cmd files while still using the argument array
form for safer argument passing.
* Sanitize redirects/headers and use execFileSync
Replace execSync calls with execFileSync (pass argv arrays) to avoid shell interpolation and adjust package-install logic to split package manager and args. Sanitize redirect destinations in SSR paths to prevent protocol-relative open-redirects and use sanitizeDestinationLocal for Next.js redirects. Ensure middleware receives a cleaned pathname by stripping internal .rsc suffix before creating NextRequest and add a test for this behavior. Strip sensitive headers (cookie, authorization and x-middleware-*) from proxied external requests and add a test. Document cache key hazards for auth headers in fetch-cache, add a security note for dangerouslySetInnerHTML in head SSR, and set the Draft Mode cookie to include Secure in production (with tests for production/dev behavior).
* Rename middleware header to x-custom-middleware
Change middleware header name from x-middleware-test to x-custom-middleware across fixtures and tests, and clarify server comment about stripping x-middleware-* headers from responses (reserved for internal routing signals). Files updated: packages/vinext/src/server/prod-server.ts (comment tweak), tests/fixtures/pages-basic/middleware.ts (header set), and tests/pages-router.test.ts (expectations and test titles/comments). This ensures tests reflect the new public header name while preserving internal x-middleware-* semantics.
* Normalize leading slashes and strip headers
Normalize redirect/rewrite destinations by collapsing any leading slashes or backslashes to a single "/" to avoid protocol-relative redirects and handle browser-backslash behavior. Strip additional sensitive headers (x-api-key, proxy-authorization) from proxied requests and ensure all x-middleware-* internal headers are removed from responses (broadened from x-middleware-request-*). Update dev/server sanitization logic accordingly. Update tests and fixture middleware to use shortened x-mw-* header names and add tests covering backslash normalization.
Refactor chained .replace() to single-pass tokenizers in pattern matching:
- matchConfigPattern in config-matchers.ts and index.ts
- escapeHeaderSource (new) replaces inline chained escaping in matchHeaders/applyHeaders
- matchMiddlewarePattern in prod server entry template
- matchPattern in middleware.ts
Also:
- escapeHeaderSource correctly handles :param(constraint) patterns
- Fix decodeURIComponent parity gap in index.ts matchConfigPattern
- Fix redundant dot in middleware.ts tokenizer char class
- Add lgtm suppression comments for false positives in test files
- Add unit tests for escapeHeaderSource
- Add normalizePath utility that collapses consecutive slashes,
resolves . and .. segments, and ensures a leading slash
- Apply decodeURIComponent + normalizePath before middleware matching
and route matching in all entry points (dev server, prod server,
Cloudflare Worker entry, codegen entries)
- Extract shared middleware codegen module (middleware-codegen.ts) as
single source of truth for pattern matching logic, replacing
hand-copied inline implementations in index.ts and app-dev-server.ts
- Fix default middleware matcher to return true when no matcher is
configured, matching Next.js behavior
- Fix regex pattern corruption in middleware matcher: patterns containing
parentheses or backslashes are now passed to RegExp directly without
dot-escaping
- Add Content-Security-Policy and X-Content-Type-Options headers to image responses
- Validate Content-Type allowlist (reject SVG, HTML, non-image types)
- Bound width parameter to configured deviceSizes/imageSizes with 3840px max
- Add deviceSizes and imageSizes to NextConfig type
- Apply to all image paths: Workers, Node.js prod server (App + Pages Router)
- Fix benchmarks limit param NaN handling
* fix: add Vary: RSC, Accept header to prevent CDN cache poisoning
Add Vary: RSC, Accept to all App Router RSC stream and HTML SSR
responses to prevent CDN cache poisoning. Without this header, a CDN
or reverse proxy may cache an RSC stream response and serve it to a
browser expecting HTML (or vice versa), breaking the page.
Affected response types in app-dev-server.ts:
- Main RSC stream responses (client-side navigation)
- HTML SSR responses (all cache control variants)
- Not-found/forbidden/unauthorized fallback pages (RSC and HTML)
- Error boundary pages (RSC and HTML)
- Server action redirect and re-render responses
- Intercepting route RSC stream responses
Fix Vary header merging in prod-server.ts:
- sendWebResponse: merge Accept-Encoding with existing Vary values
instead of overwriting (preserves RSC, Accept from upstream)
- sendCompressed: same merge behavior for buffered responses
Scope analysis (intentionally NOT changed):
- Pages Router: no RSC/HTML content negotiation on same URL
- Route handler responses: content type is handler-controlled
- Metadata routes: serve single content type per URL
- Static assets: no content negotiation
- Error/redirect responses: not cached or content-negotiated
- Worker entry (app-router-entry.ts): passes through RSC handler
responses which already include the Vary header
Closes GHSA-jgqv-mvrh-hv36
Related: GHSA-r2fc-ccr8-96c4 (Next.js equivalent)
* remove advisory reference from code comment
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
* feat(cli): load .env files with Next.js precedence
* fix(dotenv): address review feedback on .env loading
- Document Vite double-loading interaction in loadDotenv JSDoc
- Fix escaped dollar sign bug ($100 was not unescaped when $ wasn't
followed by a valid variable name)
- Add type cast for parseEnv return value
- Expand test coverage: loadedEnv assertions, production mode
precedence, ${VAR} expansion, escaped dollars, circular refs,
missing files, empty result
- Add getDotenvFiles unit tests for all three modes
- Add 'Prefer Node.js Built-in APIs' guideline to AGENTS.md
Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
* test(dotenv): add cross-file expansion and multi-line value tests
Address review suggestions: test that .env can reference vars from
higher-priority .env.development.local, and that parseEnv handles
quoted multi-line values correctly.
---------
Co-authored-by: root <root@localhost.localdomain>
Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
* fix: replace glob npm package with native node:fs/promises glob
Migrate pagesRouter, apiRouter, and appRouter from the third-party 'glob'
package to Node.js built-in glob (node:fs/promises), available since
Node 22. This removes a dependency and fixes path handling issues on
Windows where dynamic route segments (e.g. [slug]) were incorrectly
parsed due to premature path separator normalization.
Changes:
- Use for-await with glob() from node:fs/promises
- Use exclude callbacks instead of post-filtering
- Normalize pagePath/routePath with replaceAll('\\', '/') in output
- Remove glob dependency from package.json
* fix: address review feedback on native glob migration
- Fix excludePrivate over-exclusion: the native glob exclude callback
fires at every directory depth, not just the root. Replace with
post-filtering on the first path segment to match the old glob
package's root-relative ignore behavior.
- Remove redundant .replaceAll('\\', '/') normalization from
pagePath/routePath in app-router.ts — consumers already normalize
backslashes, and this was inconsistent with pages-router.ts.
- Add engines field (node >= 22) since fs.glob requires Node 22+.
* refactor: use glob pattern arrays instead of callbacks for exclude
- app-router: replace hasSlotSegment callback with ['**/@*'] pattern
- pages-router: replace excludePrivate callback with ['api', '**/_*'] patterns
- Simplify iteration: process glob results directly instead of collecting into intermediate arrays
---------
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
- Add version bump input (patch/minor/major) to publish workflow
- Gate publish on full CI suite (lint, typecheck, vitest, e2e)
- Use npm OIDC trusted publishing instead of long-lived NPM_TOKEN
- Tag releases in git after publish
- Make ci.yml callable via workflow_call
- Fix repository URL format in package.json for OIDC validation
* fix: install react-server-dom-webpack during init and deploy for App Router
vinext init was missing react-server-dom-webpack from its dependency list,
causing 'Could not resolve react-server-dom-webpack/server.edge' errors on
fresh create-next-app projects. Additionally, react-server-dom-webpack@19.2.4
requires react@^19.2.4, but create-next-app ships react@19.2.3 -- the version
mismatch caused npm to nest rsdw inside vinext/node_modules where Vite can't
resolve it.
Fix: add react-server-dom-webpack to getInitDeps for App Router, detect when
the installed React version is too old, and upgrade react/react-dom as regular
dependencies before installing dev deps (avoiding ERESOLVE peer-dep conflicts
and keeping react in 'dependencies' rather than 'devDependencies').
Also applies the same fix to vinext deploy for parity, and adds a CI job that
scaffolds a real create-next-app project, runs vinext init, and verifies the
dev server returns HTTP 200.
Closes#22
* refactor: use createRequire for package resolution instead of direct node_modules reads
Address review feedback (PR #18 pattern): replace fs.existsSync(node_modules/...)
with createRequire-based resolution, which handles hoisting, pnpm symlinks,
monorepos, and Yarn PnP correctly.
- deploy.ts: extract isPackageResolvable() helper using createRequire, use it
in getMissingDeps for react-server-dom-webpack detection
- init.ts: refactor getReactUpgradeDeps() to use createRequire + findPackageVersion
walk-up pattern instead of reading node_modules/react/package.json directly
- tests: add isPackageResolvable unit tests, use dependency injection for
getMissingDeps rsdw test (Vitest's createRequire intercepts resolution)
* ci: add Windows to create-next-app smoke test
Run the create-next-app compatibility test on both ubuntu-latest and
windows-latest. Replace bash-specific server health check (curl, kill,
backgrounding) with a cross-platform Node script using child_process
and http. Use runner.temp instead of /tmp for portable paths.
* fix: use pathToFileURL for dynamic import() calls on Windows
On Windows, Node's ESM loader requires file:// URLs for absolute paths.
Bare paths like D:\... are rejected with ERR_UNSUPPORTED_ESM_URL_SCHEME.
Wrap all dynamic import() calls that receive absolute paths from
require.resolve() or path.join() with pathToFileURL().href:
- index.ts: RSC plugin import, RSC transforms import, PostCSS config
loading, PostCSS plugin resolution (5 call sites)
- cli.ts: Vite module import from project root
* fix: pathToFileURL for cache-runtime codegen and test imports
Additional Windows fixes found via PR #43 review:
- index.ts: use pathToFileURL for runtimeModulePath in 'use cache'
transform code generation (2 locations) — these embed absolute paths
into generated import() statements via JSON.stringify
- pages-router.test.ts: wrap 9 import(serverEntryPath) calls with
pathToFileURL so tests work on Windows
* feat(deploy): add --env support for wrangler deploy
Adds vinext deploy --env <name> and threads it through to wrangler deploy --env <name>. Keeps --preview working as shorthand for preview environment, adds unit tests for wrangler arg construction, and updates README/help examples. Refs #33.
* fix(deploy): use execFileSync and util.parseArgs for --env support
Build on #44 by liuxiaopai-ai — adds --env flag for wrangler deploy.
Security: switch from execSync(shell string) to execFileSync(binary, args)
to eliminate command injection via unsanitized --env/--name values.
Replace hand-rolled flag parsing (parseStringFlag, parseNumericFlag,
rawArgs.includes) with Node.js built-in util.parseArgs. This gives us
strict mode (throws on unknown flags), automatic --flag=value handling,
and proper error messages for missing values.
Also: buildWranglerDeployArgs now returns { args, env } directly instead
of re-parsing the args array for the log message.
---------
Co-authored-by: root <root@localhost.localdomain>
Updated the production server to handle binary responses correctly by using Buffer.from with arrayBuffer instead of text(). Added a test to ensure binary data integrity and created a new fixture for testing binary API responses.
* docs: update README — add blog link, lead with agent skill, fix examples, remove stale benchmarks
* docs: add LICENSE, sync package README from root, add bug reporting guidance
- Add MIT LICENSE to repo root
- Copy root README into package at prepack time instead of maintaining two copies
- Add 'Reporting bugs' section encouraging AI-assisted diagnosis
* fix: update LICENSE copyright to Cloudflare, Inc.
- Install vinext itself as a devDependency during init (#11)
- Remove explicit rsc() from generated configs since vinext auto-registers it (#12)
- Widen react/react-dom peer dep to >=19.2.0 for create-next-app compat (#13)
Co-authored-by: Fred K. Schott <fschott@cloudflare.com>
When a server component imports next/dynamic, the RSC serializer must
treat it as a client boundary. Without 'use client', the dynamic()
function executes inline in the RSC environment — for ssr:false, this
serializes null into the RSC payload and the client never loads the
component.
Adding 'use client' matches Next.js's App Router behavior where
next/dynamic is a client module. The RSC serializer emits a client
reference, and the client executes the full dynamic logic including
the ssr:false mount-on-client behavior.
Updated test fixtures that called dynamic() from server modules to
include 'use client' — this is required since dynamic() is now a
client export and can only be called from client modules.
Added E2E test for ssr:false from a server component page.
* fix: exclude vinext from optimizeDeps to prevent virtual module resolution errors
When vinext is installed from npm (not symlinked), esbuild's dependency
optimization scans vinext/dist/ and hits virtual:vinext-* imports that
only exist at Vite plugin resolution time. This causes build failures
in all three environments (client, rsc, ssr).
Adding optimizeDeps.exclude: ['vinext'] at the top level and in each
environment prevents esbuild from scanning vinext's dist files entirely.
Fixes#1
* test: verify optimizeDeps.exclude contains vinext for all environments