cloudflare / cloudflare/vinext

Pluggable deployment adapters: support Cloudflare, Vercel, Netlify, and custom targets

Open
#80 3 comments 21 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

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:

1. **`deploy.ts`** (~780 lines): Generates wrangler.jsonc, worker entries, installs `@cloudflare/vite-plugin` and `wrangler`, builds, calls `wrangler deploy`
2. **`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
3. **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
4. **`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)

```ts
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;

/**
* 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;

/**
* 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;

/**
* 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;
}
```

### 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`:

```ts
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

1. **Define the `DeployAdapter` interface** in the vinext core package
2. **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
3. **Extract `vinext:cloudflare-build`** into the Cloudflare adapter's `postBuild()` hook
4. **Extract `deploy.ts`** into `@vinext/adapter-cloudflare` (the current deploy.ts essentially becomes the adapter)
5. **Update `cli.ts`** to load the adapter from vite config and call its `deploy()` method
6. **`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

1. Define the `DeployAdapter` interface in `packages/vinext/src/adapter.ts`
2. Create `packages/adapter-cloudflare/` and extract current Cloudflare code into it
3. Update `index.ts` to call adapter hooks instead of checking `hasCloudflarePlugin`
4. Update `cli.ts` deploy command to load adapter from config
5. Ensure all existing tests and examples still work (should be a refactor, not a behavior change)
6. Document the adapter interface for community contributors

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.