-
Notifications
You must be signed in to change notification settings - Fork 210
Description
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-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 caching- Conditional config in
index.ts: AhasCloudflarePluginboolean (detected by scanning plugin names forvite-plugin-cloudflare) that controls SSR externals, build manifest, and multi-environment behavior cloudflare/directory:kv-cache-handler.ts(implements the already-pluggableCacheHandlerinterface),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
DeployAdapterinterface in the vinext core package - Replace
hasCloudflarePlugindetection inindex.tswith adapter-driven config: the adapter'sconfigureVite()hook provides the overrides instead of vinext detecting plugins by name - Extract
vinext:cloudflare-buildinto the Cloudflare adapter'spostBuild()hook - Extract
deploy.tsinto@vinext/adapter-cloudflare(the current deploy.ts essentially becomes the adapter) - Update
cli.tsto load the adapter from vite config and call itsdeploy()method vinext buildstays generic: the adapter hooks run at config time and post-build time, sovinext builddoesn't need to know which adapter is in use
What stays the same
vinext init,vinext check,vinext dev,vinext start,vinext lintare unchanged- The
CacheHandlerinterface is already pluggable (unrelated to this work) server/prod-server.tsremains a pure Node serverserver/isr-cache.tsremains 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.tsinstead 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
DeployAdapterinterface inpackages/vinext/src/adapter.ts - Create
packages/adapter-cloudflare/and extract current Cloudflare code into it - Update
index.tsto call adapter hooks instead of checkinghasCloudflarePlugin - Update
cli.tsdeploy 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