Migrate Next.js to vinext
vinext reimplements the Next.js API surface on Vite. Existing app/, pages/, and next.config.js work as-is — migration is a package swap, config generation, and ESM conversion. No changes to application code required.
FIRST: Verify Next.js Project
Confirm next is in dependencies or devDependencies in package.json. If not found, STOP — this skill does not apply.
Detect the package manager from the lockfile:
| Lockfile | Manager | Install | Uninstall |
|---|
pnpm-lock.yaml | pnpm | pnpm add | pnpm remove |
yarn.lock | yarn | yarn add | yarn remove |
bun.lockb / bun.lock | bun | bun add | bun remove |
package-lock.json or none | npm | npm install | npm uninstall |
Detect the router: if an app/ directory exists at root or under src/, it's App Router. If only pages/ exists, it's Pages Router. Both can coexist.
Quick Reference
| Command | Purpose |
|---|
vinext check | Scan project for compatibility issues, produce scored report |
vinext init | Automated migration — installs deps, generates config, converts to ESM |
vinext dev | Development server with HMR |
vinext build | Production build (multi-environment for App Router) |
vinext start | Local production server |
vinext deploy | Build and deploy to Cloudflare Workers |
Phase 1: Check Compatibility
Run vinext check (install vinext first if needed via npx vinext check). Review the scored report. If critical incompatibilities exist, inform the user before proceeding.
See references/compatibility.md for supported/unsupported features and ecosystem library status.
Phase 2: Automated Migration (Recommended)
Run vinext init. This command:
- Runs
vinext check for a compatibility report
- Installs
vite as a devDependency (and @vitejs/plugin-rsc for App Router)
- Adds
"type": "module" to package.json
- Renames CJS config files (e.g.,
postcss.config.js → .cjs) to avoid ESM conflicts
- Adds
dev:vinext and build:vinext scripts to package.json
- Generates a minimal
vite.config.ts
This is non-destructive — the existing Next.js setup continues to work alongside vinext. Use the dev:vinext script to test before fully switching over.
If vinext init succeeds, skip to Phase 4 (Verify). If it fails or the user prefers manual control, continue to Phase 3.
Phase 3: Manual Migration
Use this as a fallback when vinext init doesn't work or the user wants full control.
3a. Replace packages
# Example with npm:
npm uninstall next
npm install vinext
npm install -D vite
# App Router only:
npm install -D @vitejs/plugin-rsc
3b. Update scripts
Replace all next commands in package.json scripts:
| Before | After | Notes |
|---|
next dev | vinext dev | Dev server with HMR |
next build | vinext build | Production build |
next start | vinext start | Local production server |
next lint | vinext lint | Delegates to eslint/oxlint |
Preserve flags: next dev --port 3001 → vinext dev --port 3001.
3c. Convert to ESM
Add "type": "module" to package.json. Rename any CJS config files:
postcss.config.js → postcss.config.cjs
tailwind.config.js → tailwind.config.cjs
- Any other
.js config that uses module.exports
3d. Generate vite.config.ts
See references/config-examples.md for config variants per router and deployment target.
If the project already has custom Vite config, prefer Vite 8-native keys when editing it: oxc, optimizeDeps.rolldownOptions, and build.rolldownOptions. Older esbuild and build.rollupOptions settings still work for now but are migration targets.
Pages Router (minimal):
import vinext from "vinext";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [vinext()] });
App Router (minimal):
import vinext from "vinext";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [vinext()] });
vinext auto-registers @vitejs/plugin-rsc for App Router when the rsc option is not explicitly false. No manual RSC plugin config needed for local development.
Phase 4: Deployment (Optional)
Option A: Cloudflare Workers (recommended for Cloudflare)
If the user wants to deploy to Cloudflare Workers, use vinext deploy. It auto-generates wrangler.jsonc, worker entry, and Vite config if missing, installs @cloudflare/vite-plugin and wrangler, then builds and deploys.
For manual setup or custom worker entries, see references/config-examples.md.
Cloudflare Bindings (D1, R2, KV, AI, etc.)
To access Cloudflare bindings (D1, R2, KV, AI, Queues, Durable Objects, etc.), use import { env } from "cloudflare:workers" in any server component, route handler, or server action:
import { env } from "cloudflare:workers";
export default async function Page() {
const result = await env.DB.prepare("SELECT * FROM posts").all();
return <div>{JSON.stringify(result)}</div>;
}
This works because @cloudflare/vite-plugin runs server environments in workerd, where cloudflare:workers is a native module. No custom worker entry, no getPlatformProxy(), no special configuration needed. Just import and use.
Bindings must be defined in wrangler.jsonc. For TypeScript types, run wrangler types.
IMPORTANT: Do not use getPlatformProxy(), getRequestContext(), or custom worker entries with fetch(request, env) to access bindings. These are older patterns. cloudflare:workers is the recommended approach and works out of the box with vinext.
For deploying to Vercel, Netlify, AWS, Deno Deploy, or any other Nitro-supported platform, add the Nitro Vite plugin:
npm install nitro
// vite.config.ts
import { defineConfig } from "vite";
import vinext from "vinext";
import { nitro } from "nitro/vite";
export default defineConfig({
plugins: [vinext(), nitro()],
});
Build and deploy:
NITRO_PRESET=vercel npx vite build # Vercel
NITRO_PRESET=netlify npx vite build # Netlify
NITRO_PRESET=deno_deploy npx vite build # Deno Deploy
NITRO_PRESET=node npx vite build # Node.js server
Nitro auto-detects the platform in most CI/CD environments, so the preset is often unnecessary.
Note: For Cloudflare Workers, Nitro works but the native integration (vinext deploy / @cloudflare/vite-plugin) is recommended for the best developer experience with cloudflare:workers bindings, KV caching, and one-command deploys.
Phase 5: Verify
- Run
vinext dev to start the development server
- Confirm the server starts without errors
- Navigate key routes and check functionality
- Report the result to the user — if errors occur, share full output
See references/troubleshooting.md for common migration errors.
Known Limitations
| Feature | Status |
|---|
next/image optimization | Remote images via @unpic; no build-time optimization |
next/font/google | CDN-loaded, not self-hosted |
| Domain-based i18n | Not supported; path-prefix i18n works |
next/jest | Not supported; use Vitest |
| Turbopack/webpack config | Ignored; use Vite plugins instead |
runtime / preferredRegion | Route segment configs ignored |
| PPR (Partial Prerendering) | Use "use cache" directive instead (Next.js 16 approach) |
Anti-patterns
- Do not modify
app/, pages/, or application code. vinext shims all next/* imports — no import rewrites needed.
- Do not rewrite
next/* imports to vinext/* in application code. Imports like next/image, next/link, next/server resolve automatically.
- Do not copy webpack/Turbopack config into Vite config. Use Vite-native plugins instead.
- Do not skip the compatibility check. Run
vinext check before migration to surface issues early.
- Do not remove
next.config.js unless replacing it with next.config.ts or .mjs. vinext reads it for redirects, rewrites, headers, basePath, i18n, images, and env config.
- Do not use
getPlatformProxy() or custom worker entries for bindings. Use import { env } from "cloudflare:workers" instead. This is the modern pattern and works out of the box with vinext and @cloudflare/vite-plugin.
- For Cloudflare Workers, prefer the native integration over Nitro.
vinext deploy / @cloudflare/vite-plugin provides the best experience with cloudflare:workers bindings, KV caching, and image optimization. Nitro works for Cloudflare but the native setup is recommended.