Problem
vinext deploy is currently hard-coded to Cloudflare Workers. The entire deploy pipeline (config generation, dependency installation, build adjustments, and the deploy command itself) lives in a single deploy.ts file that only knows about wrangler, Workers, and KV.
As vinext grows, people want to deploy to other targets (Vercel, Netlify, generic Node servers). We need a way for deployment providers to plug in without forking the core.
Current state
The good news: the separation is already cleaner than it looks. Six of seven CLI commands are fully provider-agnostic today:
| Command |
Provider-specific? |
vinext init |
No, generates a plain vite.config.ts with just vinext() |
vinext check |
No, pure Next.js compatibility scanner |
vinext dev |
No, just starts Vite dev server |
vinext build |
No, standard Vite build (with conditional codepath if CF plugin is detected) |
vinext start |
No, pure Node.js HTTP server |
vinext lint |
No |
vinext deploy |
Yes, entirely Cloudflare |
The Cloudflare-specific code lives in four places:
deploy.ts (~780 lines): Generates wrangler.jsonc, worker entries, installs @cloudflare/vite-plugin and wrangler, builds, calls wrangler deploy
vinext:cloudflare-build plugin in index.ts (lines 3058-3204): Post-build hook that injects __VINEXT_SSR_MANIFEST__ and __VINEXT_LAZY_CHUNKS__ globals into the worker entry, generates _headers file for immutable asset caching
- Conditional config in
index.ts: A hasCloudflarePlugin boolean (detected by scanning plugin names for vite-plugin-cloudflare) that controls SSR externals, build manifest, and multi-environment behavior
cloudflare/ directory: kv-cache-handler.ts (implements the already-pluggable CacheHandler interface), tpr.ts (Traffic-aware Pre-Rendering via Cloudflare analytics API)
The cache system (CacheHandler interface in shims/cache.ts) is already fully pluggable. The ISR layer (server/isr-cache.ts) is generic. The production Node server (server/prod-server.ts) has zero Cloudflare code.
Proposal: deployment adapters
Following the same pattern used by SvelteKit (@sveltejs/adapter-cloudflare, @sveltejs/adapter-vercel, etc.) and Astro (@astrojs/cloudflare, @astrojs/vercel, etc.), we introduce a deployment adapter interface.
Adapter interface (rough sketch)
interface DeployAdapter {
/** Human-readable name, e.g. "Cloudflare Workers" */
name: string;
/**
* Vite config adjustments for this target.
* Called during vinext's config hook.
*
* Examples:
* - Cloudflare: disable SSR externals, enable build manifest
* - Vercel: set output directory for serverless functions
* - Node: no changes needed
*/
configureVite?: (ctx: AdapterContext) => ViteConfigOverrides;
/**
* Additional Vite plugins required by this adapter.
* Returned plugins are merged into the plugin array.
*
* Examples:
* - Cloudflare: returns @cloudflare/vite-plugin
* - Vercel: returns vercel's vite plugin or preset
*/
vitePlugins?: (ctx: AdapterContext) => Plugin[];
/**
* Post-build hook. Runs after all Vite environments are built.
* Use this for injecting globals, rewriting output, generating
* platform-specific files, etc.
*
* Examples:
* - Cloudflare: inject __VINEXT_SSR_MANIFEST__, generate _headers
* - Vercel: generate .vercel/output config
*/
postBuild?: (ctx: BuildContext) => Promise<void>;
/**
* Generate platform-specific config files if they don't exist.
* Called during `vinext deploy` before the build step.
*
* Examples:
* - Cloudflare: generate wrangler.jsonc, worker/index.ts
* - Vercel: generate vercel.json
* - Netlify: generate netlify.toml
*/
generateConfig?: (ctx: AdapterContext) => Promise<GeneratedFile[]>;
/**
* Generate the server entry point for this target.
*
* Examples:
* - Cloudflare: Workers fetch() handler
* - Vercel: serverless function handler
* - Node: already covered by vinext start, may be a no-op
*/
generateServerEntry?: (ctx: AdapterContext) => Promise<GeneratedFile | null>;
/**
* npm packages this adapter needs installed.
*/
dependencies?: () => { name: string; dev?: boolean }[];
/**
* Run the actual deployment.
* Called during `vinext deploy` after the build completes.
*
* Examples:
* - Cloudflare: exec `wrangler deploy`
* - Vercel: exec `vercel deploy` or `vercel --prod`
* - Netlify: exec `netlify deploy --prod`
*/
deploy: (ctx: DeployContext) => Promise<void>;
}
Package structure
Separate packages in this monorepo:
packages/
vinext/ # Core (unchanged package name)
adapter-cloudflare/ # @vinext/adapter-cloudflare
adapter-vercel/ # @vinext/adapter-vercel (future)
adapter-netlify/ # @vinext/adapter-netlify (future)
adapter-node/ # @vinext/adapter-node (future, wraps vinext start)
User-facing config
The adapter is specified in vite.config.ts, similar to how SvelteKit does it in svelte.config.js:
import vinext from "vinext";
import cloudflare from "@vinext/adapter-cloudflare";
export default defineConfig({
plugins: [vinext({ adapter: cloudflare() })],
});
Then vinext deploy reads the adapter from the resolved config. No CLI argument needed in the common case. If no adapter is configured, vinext deploy prints an error telling the user to pick one.
What changes in core
- Define the
DeployAdapter interface in the vinext core package
- Replace
hasCloudflarePlugin detection in index.ts with adapter-driven config: the adapter's configureVite() hook provides the overrides instead of vinext detecting plugins by name
- Extract
vinext:cloudflare-build into the Cloudflare adapter's postBuild() hook
- Extract
deploy.ts into @vinext/adapter-cloudflare (the current deploy.ts essentially becomes the adapter)
- Update
cli.ts to load the adapter from vite config and call its deploy() method
vinext build stays generic: the adapter hooks run at config time and post-build time, so vinext build doesn't need to know which adapter is in use
What stays the same
vinext init, vinext check, vinext dev, vinext start, vinext lint are unchanged
- The
CacheHandler interface is already pluggable (unrelated to this work)
server/prod-server.ts remains a pure Node server
server/isr-cache.ts remains generic
Open questions
How should adapters interact with existing Vite platform plugins?
Cloudflare, Vercel, and Netlify all have their own Vite plugins. Should the adapter:
- (a) Wrap and re-export the platform's Vite plugin (adapter owns the full pipeline)
- (b) Coexist alongside the platform's Vite plugin (adapter only handles deploy, platform plugin handles build)
- (c) Both, with the adapter detecting whether the platform plugin is already configured
Option (c) is probably the most pragmatic. The adapter can provide the platform plugin via vitePlugins() if the user hasn't already added it manually, but also work alongside a manually configured plugin.
Should vinext start be documented as the "deploy anywhere" solution?
For targets that don't have a dedicated deploy platform (Docker, fly.io, Railway, any VPS), vinext start already gives you a Node HTTP server. Rather than building adapters for every hosting provider, we could document vinext start as the universal escape hatch and provide guidance on running it in Docker, systemd, etc.
Adapter-specific CLI flags
The current vinext deploy has Cloudflare-specific flags (--experimental-tpr, --tpr-coverage, etc.). Should adapter-specific flags be:
- (a) Namespaced:
vinext deploy --cloudflare-tpr
- (b) Passed through:
vinext deploy -- --experimental-tpr (everything after -- goes to the adapter)
- (c) Configured in
vite.config.ts instead of CLI flags: cloudflare({ tpr: { coverage: 90 } })
Option (c) feels cleanest since the adapter is already configured in vite.config.ts.
Implementation plan
- Define the
DeployAdapter interface in packages/vinext/src/adapter.ts
- Create
packages/adapter-cloudflare/ and extract current Cloudflare code into it
- Update
index.ts to call adapter hooks instead of checking hasCloudflarePlugin
- Update
cli.ts deploy command to load adapter from config
- Ensure all existing tests and examples still work (should be a refactor, not a behavior change)
- Document the adapter interface for community contributors
Problem
vinext deployis currently hard-coded to Cloudflare Workers. The entire deploy pipeline (config generation, dependency installation, build adjustments, and the deploy command itself) lives in a singledeploy.tsfile that only knows about wrangler, Workers, and KV.As vinext grows, people want to deploy to other targets (Vercel, Netlify, generic Node servers). We need a way for deployment providers to plug in without forking the core.
Current state
The good news: the separation is already cleaner than it looks. Six of seven CLI commands are fully provider-agnostic today:
vinext initvinext()vinext checkvinext devvinext buildvinext startvinext lintvinext deployThe Cloudflare-specific code lives in four places:
deploy.ts(~780 lines): Generates wrangler.jsonc, worker entries, installs@cloudflare/vite-pluginandwrangler, builds, callswrangler deployvinext:cloudflare-buildplugin inindex.ts(lines 3058-3204): Post-build hook that injects__VINEXT_SSR_MANIFEST__and__VINEXT_LAZY_CHUNKS__globals into the worker entry, generates_headersfile for immutable asset cachingindex.ts: AhasCloudflarePluginboolean (detected by scanning plugin names forvite-plugin-cloudflare) that controls SSR externals, build manifest, and multi-environment behaviorcloudflare/directory:kv-cache-handler.ts(implements the already-pluggableCacheHandlerinterface),tpr.ts(Traffic-aware Pre-Rendering via Cloudflare analytics API)The cache system (
CacheHandlerinterface inshims/cache.ts) is already fully pluggable. The ISR layer (server/isr-cache.ts) is generic. The production Node server (server/prod-server.ts) has zero Cloudflare code.Proposal: deployment adapters
Following the same pattern used by SvelteKit (
@sveltejs/adapter-cloudflare,@sveltejs/adapter-vercel, etc.) and Astro (@astrojs/cloudflare,@astrojs/vercel, etc.), we introduce a deployment adapter interface.Adapter interface (rough sketch)
Package structure
Separate packages in this monorepo:
User-facing config
The adapter is specified in
vite.config.ts, similar to how SvelteKit does it insvelte.config.js:Then
vinext deployreads the adapter from the resolved config. No CLI argument needed in the common case. If no adapter is configured,vinext deployprints an error telling the user to pick one.What changes in core
DeployAdapterinterface in the vinext core packagehasCloudflarePlugindetection inindex.tswith adapter-driven config: the adapter'sconfigureVite()hook provides the overrides instead of vinext detecting plugins by namevinext:cloudflare-buildinto the Cloudflare adapter'spostBuild()hookdeploy.tsinto@vinext/adapter-cloudflare(the current deploy.ts essentially becomes the adapter)cli.tsto load the adapter from vite config and call itsdeploy()methodvinext buildstays generic: the adapter hooks run at config time and post-build time, sovinext builddoesn't need to know which adapter is in useWhat stays the same
vinext init,vinext check,vinext dev,vinext start,vinext lintare unchangedCacheHandlerinterface is already pluggable (unrelated to this work)server/prod-server.tsremains a pure Node serverserver/isr-cache.tsremains genericOpen questions
How should adapters interact with existing Vite platform plugins?
Cloudflare, Vercel, and Netlify all have their own Vite plugins. Should the adapter:
Option (c) is probably the most pragmatic. The adapter can provide the platform plugin via
vitePlugins()if the user hasn't already added it manually, but also work alongside a manually configured plugin.Should
vinext startbe documented as the "deploy anywhere" solution?For targets that don't have a dedicated deploy platform (Docker, fly.io, Railway, any VPS),
vinext startalready gives you a Node HTTP server. Rather than building adapters for every hosting provider, we could documentvinext startas the universal escape hatch and provide guidance on running it in Docker, systemd, etc.Adapter-specific CLI flags
The current
vinext deployhas Cloudflare-specific flags (--experimental-tpr,--tpr-coverage, etc.). Should adapter-specific flags be:vinext deploy --cloudflare-tprvinext deploy -- --experimental-tpr(everything after--goes to the adapter)vite.config.tsinstead of CLI flags:cloudflare({ tpr: { coverage: 90 } })Option (c) feels cleanest since the adapter is already configured in vite.config.ts.
Implementation plan
DeployAdapterinterface inpackages/vinext/src/adapter.tspackages/adapter-cloudflare/and extract current Cloudflare code into itindex.tsto call adapter hooks instead of checkinghasCloudflarePlugincli.tsdeploy command to load adapter from config