Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ If a Node built-in does the job, use it. Only reach for a dependency when the bu

## Git Workflow

- **Refresh main before assuming repo state.** At the start of every task, run `git fetch origin` and check `git status --branch` so you know whether the local branch and `origin/main` are current. Do not assume the cached `origin/main` ref is up to date.

- **Only fast-forward automatically when safe.** If you are on `main` and the worktree is clean, update with `git pull --ff-only` before doing substantial work. If the worktree is dirty or you are on another branch, do not auto-pull; note the divergence and avoid mutating the user's branch history without an explicit request.

- **NEVER push directly to main.** Always create a feature branch and open a PR, even for small fixes. This ensures CI runs before changes are merged and provides a review checkpoint.

- **Branch protection is enabled on main.** Required checks: Lint, Typecheck, Vitest, Playwright E2E. Pushing directly to main bypasses these protections and can introduce regressions.
Expand Down
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Options: `-p / --port <port>`, `-H / --hostname <host>`, `--turbopack` (accepted

Run `npm create next-app@latest` to create a new Next.js project, and then follow these instructions to migrate it to vinext.

In the future, we will have a proper `npm create vinext` new project workflow.
In the future, we will have a proper `npm create vinext` new project workflow.

### Migrating an existing Next.js project

Expand Down Expand Up @@ -152,7 +152,7 @@ Both. File-system routing, SSR, client hydration, and deployment to Cloudflare W
Next.js 16.x. No support for deprecated APIs from older versions.

**Can I deploy to AWS/Netlify/other platforms?**
Yes. Add the [Nitro](https://v3.nitro.build/) Vite plugin alongside vinext, and you can deploy to Vercel, Netlify, AWS Amplify, Deno Deploy, Azure, and [many more](https://v3.nitro.build/deploy). See [Other platforms (via Nitro)](#other-platforms-via-nitro) for setup. For Cloudflare Workers, the native integration (`vinext deploy`) gives you the smoothest experience. Native adapters for more platforms are [planned](https://github.com/cloudflare/vinext/issues/80).
Yes. Add the [Nitro](https://v3.nitro.build/) Vite plugin alongside vinext, and you can deploy to Vercel, Netlify, AWS Amplify, Deno Deploy, Azure, Node.js, and [many more](https://v3.nitro.build/deploy). `vinext deploy` itself is Cloudflare-only; for other targets, use Nitro. See [Other platforms (via Nitro)](#other-platforms-via-nitro) for setup. For Cloudflare Workers, the native integration (`vinext deploy`) gives you the smoothest experience. Native adapters for more platforms are [planned](https://github.com/cloudflare/vinext/issues/80).

**What happens when Next.js releases a new feature?**
We track the public Next.js API surface and add support for new stable features. Experimental or unstable Next.js features are lower priority. The plan is to add commit-level tracking of the Next.js repo so we can stay current as new versions are released.
Expand All @@ -169,12 +169,12 @@ Before running `vinext deploy` for the first time you need to authenticate with

**Authentication — pick one:**

- **`wrangler login`** (recommended for local development) — opens a browser window to authenticate. Run it once and wrangler caches the token.
- **`CLOUDFLARE_API_TOKEN` env var** (CI / non-interactive) — create a token at [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens) using the **Edit Cloudflare Workers** template. That template grants all the permissions `vinext deploy` needs.
- **`wrangler login`** (recommended for local development) — opens a browser window to authenticate. In an interactive terminal, `vinext deploy` will reuse your saved Wrangler session and prompt you through `wrangler login` if needed. Docs: <https://developers.cloudflare.com/workers/wrangler/commands/#login>
- **`CLOUDFLARE_API_TOKEN` env var** (CI / non-interactive) — use Cloudflare's **Edit Cloudflare Workers** template. Docs: <https://developers.cloudflare.com/fundamentals/api/get-started/create-token/> and <https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/>

**Account ID:**

wrangler needs to know which Cloudflare account to deploy to. Add your account ID to `wrangler.jsonc`:
wrangler needs to know which Cloudflare account to deploy to. Set `CLOUDFLARE_ACCOUNT_ID`, or add `account_id` to `wrangler.jsonc` / `wrangler.toml` either at the top level or inside the selected env block:

```jsonc
{
Expand All @@ -183,9 +183,7 @@ wrangler needs to know which Cloudflare account to deploy to. Add your account I
}
```

Find your account ID in the Cloudflare dashboard URL (`dash.cloudflare.com/<account-id>`) or by running `wrangler whoami` after logging in.

Alternatively, set the `CLOUDFLARE_ACCOUNT_ID` environment variable instead of hardcoding it in the config file.
Find your account ID in the Cloudflare dashboard URL (`dash.cloudflare.com/<account-id>`) or by running `wrangler whoami` after logging in. Account ID docs: <https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/>

`vinext deploy` auto-generates the necessary configuration files (`vite.config.ts`, `wrangler.jsonc`, `worker/index.ts`) if they don't exist, builds the application, and deploys to Workers.

Expand Down Expand Up @@ -260,7 +258,7 @@ setCacheHandler(new KVCacheHandler(env.MY_KV_NAMESPACE));

#### Custom Vite configuration

If you need to customize the Vite config, create a `vite.config.ts`. vinext will merge its config with yours. This is required for Cloudflare Workers deployment with the App Router (RSC needs explicit plugin configuration):
If you need to customize the Vite config, create a `vite.config.ts`. vinext will merge its config with yours. For Cloudflare Workers, adding `@cloudflare/vite-plugin` yourself is recommended when you want Cloudflare-specific local behavior such as `cloudflare:workers` bindings in dev. If an existing config is missing the plugin, `vinext deploy` can inject it for the deploy build:

```ts
import { defineConfig } from "vite";
Expand Down Expand Up @@ -289,7 +287,11 @@ See the [examples](#live-examples) for complete working configurations.

### Other platforms (via Nitro)

For deploying to platforms other than Cloudflare, vinext works with [Nitro](https://v3.nitro.build/) as a Vite plugin. Add `nitro` alongside `vinext` in your Vite config and deploy to any [Nitro-supported platform](https://v3.nitro.build/deploy).
`vinext deploy` is Cloudflare-only. For other targets, use vinext with the [Nitro](https://v3.nitro.build/) Vite plugin and deploy to any [Nitro-supported platform](https://v3.nitro.build/deploy).

```bash
npm install nitro
```

```ts
import { defineConfig } from "vite";
Expand All @@ -301,19 +303,16 @@ export default defineConfig({
});
```

```bash
npm install nitro
```

Nitro auto-detects the deployment platform in most CI/CD environments (Vercel, Netlify, AWS Amplify, Azure, and others), so you typically don't need to set a preset. For local builds, set the `NITRO_PRESET` environment variable:

```bash
NITRO_PRESET=vercel npx vite build
NITRO_PRESET=netlify npx vite build
NITRO_PRESET=deno_deploy npx vite build
NITRO_PRESET=node npx vite build
```

> **Deploying to Cloudflare?** You can use Nitro, but the native integration (`vinext deploy` / `@cloudflare/vite-plugin`) is recommended. It provides the best developer experience with `cloudflare:workers` bindings, KV caching, image optimization, and one-command deploys.
> **Deploying to Cloudflare?** You can use Nitro, but the native integration (`vinext deploy` / `@cloudflare/vite-plugin`) is recommended. It provides the best developer experience with Wrangler auth, `cloudflare:workers` bindings, KV caching, image optimization, and one-command deploys.

<details>
<summary>Vercel</summary>
Expand Down
76 changes: 62 additions & 14 deletions packages/vinext/src/cloudflare/tpr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,18 @@ interface WranglerConfig {
/**
* Parse wrangler config (JSONC or TOML) to extract the fields TPR needs:
* account_id, VINEXT_CACHE KV namespace ID, and custom domain.
*
* If `targetEnv` is provided, env-specific values override top-level config.
*/
export function parseWranglerConfig(root: string): WranglerConfig | null {
export function parseWranglerConfig(root: string, targetEnv?: string): WranglerConfig | null {
// Try JSONC / JSON first
for (const filename of ["wrangler.jsonc", "wrangler.json"]) {
const filepath = path.join(root, filename);
if (fs.existsSync(filepath)) {
const content = fs.readFileSync(filepath, "utf-8");
try {
const json = JSON.parse(stripJsonComments(content));
return extractFromJSON(json);
return extractFromJSON(json, targetEnv);
} catch {
continue;
}
Expand All @@ -100,7 +102,7 @@ export function parseWranglerConfig(root: string): WranglerConfig | null {
const tomlPath = path.join(root, "wrangler.toml");
if (fs.existsSync(tomlPath)) {
const content = fs.readFileSync(tomlPath, "utf-8");
return extractFromTOML(content);
return extractFromTOML(content, targetEnv);
}

return null;
Expand Down Expand Up @@ -179,17 +181,23 @@ function stripJsonComments(str: string): string {
return result;
}

function extractFromJSON(config: Record<string, unknown>): WranglerConfig {
function extractFromJSON(config: Record<string, unknown>, targetEnv?: string): WranglerConfig {
const result: WranglerConfig = {};
const envConfig = getJSONEnvConfig(config, targetEnv);

// account_id
if (typeof config.account_id === "string") {
if (typeof envConfig?.account_id === "string") {
result.accountId = envConfig.account_id;
} else if (typeof config.account_id === "string") {
result.accountId = config.account_id;
}

// KV namespace ID for VINEXT_CACHE
if (Array.isArray(config.kv_namespaces)) {
const vinextKV = config.kv_namespaces.find(
const kvNamespaces = Array.isArray(envConfig?.kv_namespaces)
? envConfig.kv_namespaces
: config.kv_namespaces;
if (Array.isArray(kvNamespaces)) {
const vinextKV = kvNamespaces.find(
(ns: Record<string, unknown>) =>
ns && typeof ns === "object" && ns.binding === "VINEXT_CACHE",
);
Expand All @@ -203,12 +211,32 @@ function extractFromJSON(config: Record<string, unknown>): WranglerConfig {
}

// Custom domain — check routes[] and custom_domains[]
const domain = extractDomainFromRoutes(config.routes) ?? extractDomainFromCustomDomains(config);
const domain = extractDomainFromRoutes(envConfig?.routes)
?? extractDomainFromRoutes(config.routes)
?? extractDomainFromCustomDomains(envConfig)
?? extractDomainFromCustomDomains(config);
if (domain) result.customDomain = domain;

return result;
}

function getJSONEnvConfig(
config: Record<string, unknown>,
targetEnv?: string,
): Record<string, unknown> | null {
if (!targetEnv || !config.env || typeof config.env !== "object") {
return null;
}

const envs = config.env as Record<string, unknown>;
const envConfig = envs[targetEnv];
if (!envConfig || typeof envConfig !== "object") {
return null;
}

return envConfig as Record<string, unknown>;
}

function extractDomainFromRoutes(routes: unknown): string | null {
if (!Array.isArray(routes)) return null;

Expand All @@ -233,7 +261,9 @@ function extractDomainFromRoutes(routes: unknown): string | null {
return null;
}

function extractDomainFromCustomDomains(config: Record<string, unknown>): string | null {
function extractDomainFromCustomDomains(config: Record<string, unknown> | null): string | null {
if (!config) return null;

// Workers Custom Domains: "custom_domains": ["example.com"]
if (Array.isArray(config.custom_domains)) {
for (const d of config.custom_domains) {
Expand All @@ -259,16 +289,18 @@ function cleanDomain(raw: string): string | null {
* Simple extraction of specific fields from wrangler.toml content.
* Not a full TOML parser — just enough for the fields we need.
*/
function extractFromTOML(content: string): WranglerConfig {
function extractFromTOML(content: string, targetEnv?: string): WranglerConfig {
const result: WranglerConfig = {};
const envBlock = getTOMLEnvBlock(content, targetEnv);

// account_id = "..."
const accountMatch = content.match(/^account_id\s*=\s*"([^"]+)"/m);
const accountMatch = envBlock?.match(/^account_id\s*=\s*"([^"]+)"/m)
?? content.match(/^account_id\s*=\s*"([^"]+)"/m);
if (accountMatch) result.accountId = accountMatch[1];

// KV namespace with binding = "VINEXT_CACHE"
// Look for [[kv_namespaces]] blocks
const kvBlocks = content.split(/\[\[kv_namespaces\]\]/);
const kvBlocks = (envBlock ?? content).split(/\[\[kv_namespaces\]\]/);
for (let i = 1; i < kvBlocks.length; i++) {
const block = kvBlocks[i].split(/\[\[/)[0]; // Take until next section
const bindingMatch = block.match(/binding\s*=\s*"([^"]+)"/);
Expand All @@ -284,7 +316,8 @@ function extractFromTOML(content: string): WranglerConfig {

// routes — both string and table forms
// route = "example.com/*"
const routeMatch = content.match(/^route\s*=\s*"([^"]+)"/m);
const routeMatch = envBlock?.match(/^route\s*=\s*"([^"]+)"/m)
?? content.match(/^route\s*=\s*"([^"]+)"/m);
if (routeMatch) {
const domain = cleanDomain(routeMatch[1]);
if (domain && !domain.includes("workers.dev")) {
Expand All @@ -294,7 +327,7 @@ function extractFromTOML(content: string): WranglerConfig {

// [[routes]] blocks
if (!result.customDomain) {
const routeBlocks = content.split(/\[\[routes\]\]/);
const routeBlocks = (envBlock ?? content).split(/\[\[routes\]\]/);
for (let i = 1; i < routeBlocks.length; i++) {
const block = routeBlocks[i].split(/\[\[/)[0];
const patternMatch = block.match(/pattern\s*=\s*"([^"]+)"/);
Expand All @@ -311,6 +344,21 @@ function extractFromTOML(content: string): WranglerConfig {
return result;
}

function getTOMLEnvBlock(content: string, targetEnv?: string): string | null {
if (!targetEnv) return null;

const sectionHeader = `[env.${targetEnv}]`;
const start = content.indexOf(sectionHeader);
if (start === -1) return null;

const nextSection = content.slice(start + sectionHeader.length).search(/^\[/m);
if (nextSection === -1) {
return content.slice(start + sectionHeader.length);
}

return content.slice(start + sectionHeader.length, start + sectionHeader.length + nextSection);
}

// ─── Cloudflare API ──────────────────────────────────────────────────────────

/** Resolve zone ID from a domain name via the Cloudflare API. */
Expand Down
Loading
Loading