This guide walks through the process of adding new features to vinext.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/vinext/llms.txt
Use this file to discover all available pages before exploring further.
Development Workflow
Adding a New Feature
When adding a new feature to vinext:- Check if Next.js has it — look at Next.js source to understand expected behavior
- Add tests first — put test cases in the appropriate
tests/*.test.tsfile - Implement in shims or server — most features are either a shim (
next/*module) or server-side logic - Add fixture pages if needed —
tests/fixtures/has test apps for integration testing - Run the full test suite before committing
Fixing Bugs
Always check dev and prod server parity. Request handling logic exists in multiple places that must stay in sync:server/app-dev-server.ts— App Router dev (generates the RSC entry)server/dev-server.ts— Pages Router devserver/prod-server.ts— Pages Router production (handles middleware, routing, SSR directly)cloudflare/worker-entry.ts— Cloudflare Workers entry
app-dev-server.ts. But the Pages Router production server has its own middleware/routing/SSR logic that must be updated separately.
Architecture Patterns
Understanding these architectural patterns is critical for implementing features correctly.RSC and SSR Are Separate Vite Environments
This is the single most important architectural detail. The RSC environment and the SSR environment are separate Vite module graphs with separate module instances. If you set state in a module in the RSC environment (e.g.,setNavigationContext() in next/navigation), the SSR environment’s copy of that module is unaffected.
Per-request state (pathname, searchParams, params, headers, cookies) must be explicitly passed from the RSC entry to the SSR entry via the handleSsr(rscStream, navContext) call. The SSR entry calls the setter before rendering and cleans up afterward.
Rule of thumb: Any per-request state that "use client" components need during SSR must be passed across the environment boundary. They don’t share module state.
What @vitejs/plugin-rsc Does vs What vinext Does
The RSC plugin handles:
- Bundler transforms for
"use client"/"use server"directives - RSC stream serialization (wraps
react-server-dom-webpack) - Multi-environment builds (RSC/SSR/Client)
- CSS code-splitting and auto-injection
- HMR for server components
- Bootstrap script injection for client hydration
- File-system routing (scanning
app/andpages/directories) - Request lifecycle (middleware, headers, redirects, rewrites, then route handling)
- Layout nesting and React tree construction
- Client-side navigation and prefetching
- Caching (ISR,
"use cache", fetch cache) - All
next/*module shims
default export is the request handler. The plugin calls it for every request; vinext does route matching, builds the React tree, renders to RSC stream, and delegates to the SSR entry for HTML.
Production Builds Require createBuilder
You must use createBuilder() + builder.buildApp() for production builds, not build() directly. Calling build() from the Vite JS API doesn’t trigger the RSC plugin’s multi-environment build pipeline. buildApp() runs the 5-step RSC/SSR/client build sequence in the correct order.
Virtual Module Resolution Quirks
- Build-time root prefix: Vite prefixes virtual module IDs with the project root path when resolving SSR build entries. The
resolveIdhook must handle bothvirtual:vinext-server-entryand<root>/virtual:vinext-server-entry. \0prefix in client environment: When the RSC plugin generates its browser entry, it imports virtual modules using the already-resolved\0-prefixed ID. Vite’simport-analysisplugin can’t resolve this. Fix: strip the\0prefix before matching inresolveId.- Absolute paths required: Virtual modules have no real file location, so all imports within them must use absolute paths.
Next.js 15+ Thenable Params
Next.js 15 changedparams and searchParams to Promises. For backward compatibility with pre-15 code, vinext creates “thenable objects”:
await params (new style) and params.id (old style). The same pattern applies to generateMetadata and generateViewport.
ISR Architecture
The ISR cache layer sits aboveCacheHandler, not inside it. CacheHandler (matching Next.js 16’s interface) is a simple key-value store. ISR semantics live in a separate isr-cache.ts module:
- Stale-while-revalidate: Returns stale entries (not null) while background regeneration runs
- Dedup: A
Map<string, Promise>keyed by cache key ensures only one regeneration per key at a time - Revalidate tracking: A side map stores revalidate durations by cache key (populated on MISS, read on HIT/STALE)
- Tag invalidation: Tag-invalidated entries are hard-deleted (return null), unlike time-expired entries which return stale
setCacheHandler(). KV is the default for Cloudflare Workers. The ISR logic works automatically with any backend.
Adding Examples
Theexamples/ directory contains real-world Next.js apps ported to run on vinext. When adding a new example:
Adding a New Example
- Create a directory under
examples/with apackage.json(use"vinext": "workspace:*") - Add a
vite.config.tswithvinext()andcloudflare()plugins - Add a
wrangler.jsonc— for simple apps use"main": "vinext/server/app-router-entry"(no custom worker entry needed) - Add the example to the deploy matrix in
.github/workflows/deploy-examples.yml:- Add to
matrix.examplearray (withname,project,wrangler_config) - Add to the
examplesarray in the PR comment step
- Add to
- Add a smoke test entry in
scripts/smoke-test.sh— add a line to theCHECKSarray: - Run
./scripts/smoke-test.shlocally to verify after deploying
Porting Strategy
The examples in.github/repos.json are the ecosystem of Next.js apps we want to support. When porting one:
- Use App Router unless the original app specifically requires Pages Router
- Keep the same content — the goal is to prove the app works on vinext, not to rewrite it
- Use
@mdx-js/rollupfor MDX support (vinext auto-detects and injects it, or you can register it manually invite.config.ts) - File issues for anything that requires workarounds — missing shims, unsupported config options, etc.
- Don’t depend on the original framework’s build plugins — e.g., Nextra’s webpack plugin won’t work; port the content and build a lightweight equivalent
Ecosystem Library Compatibility
When adding support for third-party Next.js libraries:next/navigation.js(with .js extension): Libraries likenuqsimport with the.jsextension. Vite’sresolve.aliasdoes exact matching, so aresolveIdhook strips.jsfromnext/*imports and redirects through the shim map.- next-themes: Works out of the box. ThemeProvider,
useTheme, SSR script injection all function correctly. - next-intl: Requires deep integration. It expects a plugin from
next.config.ts(createNextIntlPlugin) that injects config at build time. Simply installing and importing doesn’t work. - General pattern: Libraries that only import from
next/*public APIs tend to work. Libraries that depend on Next.js build plugins or internal APIs need custom shimming.