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
6 changes: 6 additions & 0 deletions .changeset/next-vinext-autoconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudflare/autoconfig": minor
"wrangler": minor
---

Configure and deploy Next.js 16 projects with vinext instead of OpenNext during Wrangler automatic configuration.
5 changes: 2 additions & 3 deletions packages/autoconfig/src/frameworks/all-frameworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@ export const allKnownFrameworks = [
class: NextJs,
frameworkPackageInfo: {
name: "next",
// 14.2.35 is the earliest version of Next.js officially supported by open-next
// see: https://github.com/cloudflare/workers-sdk/pull/11704#discussion_r2634519440
minimumVersion: "14.2.35",
// vinext targets the latest major version of Next.js.
minimumVersion: "16.0.0",
maximumKnownMajorVersion: "16",
},
supported: true,
Expand Down
31 changes: 17 additions & 14 deletions packages/autoconfig/src/frameworks/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ export class NextJs extends Framework {
await runCommand(
[
...dlx,
"@opennextjs/cloudflare",
"migrate",
// Note: we force-install so that even if an incompatible version of
// Next.js is used this installation still succeeds, moving users
// (hopefully) in right direction (instead of failing at this step)
"--force-install",
"vinext",
"init",
"--platform=cloudflare",
"--cdn-cache=workers-cache",
"--data-cache=none",
"--image-optimization=cloudflare-images",
"--no-prerender",
"--no-experimental-warm-cdn-cache",
],
{
cwd: projectPath,
Expand All @@ -31,18 +33,19 @@ export class NextJs extends Framework {
}

return {
// `@opennextjs/cloudflare migrate` creates the wrangler config file
wranglerConfig: {},
// `vinext init` creates the Wrangler and Vite configuration files.
wranglerConfig: null,
packageJsonScriptsOverrides: {
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
preview:
"vinext build && wrangler dev --config dist/server/wrangler.json",
deploy: "vinext-cloudflare deploy --config dist/server/wrangler.json",
},
buildCommandOverride: `${npx} opennextjs-cloudflare build`,
deployCommandOverride: `${npx} opennextjs-cloudflare deploy`,
versionCommandOverride: `${npx} opennextjs-cloudflare upload`,
buildCommandOverride: `${npx} vinext build`,
deployCommandOverride: `${npx} vinext-cloudflare deploy`,
versionCommandOverride: `${npx} wrangler versions upload --config dist/server/wrangler.json`,
};
}

configurationDescription =
"Configuring project for Next.js with OpenNext by running `@opennextjs/cloudflare migrate`";
"Configuring project for Next.js with vinext by running `vinext init`";
}
68 changes: 68 additions & 0 deletions packages/autoconfig/tests/frameworks/next.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { runCommand } from "@cloudflare/cli-shared-helpers/command";
import { NpmPackageManager } from "@cloudflare/workers-utils";
import { beforeEach, describe, it, vi } from "vitest";
import { NextJs } from "../../src/frameworks/next";
import { createMockContext } from "../helpers/mock-context";

vi.mock("@cloudflare/cli-shared-helpers/command");

const context = createMockContext();
const BASE_OPTIONS = {
projectPath: "/next-app",
workerName: "next-app",
outputDir: "dist/",
dryRun: false,
packageManager: NpmPackageManager,
isWorkspaceRoot: false,
context,
};

describe("Next.js framework configure()", () => {
beforeEach(() => {
vi.mocked(runCommand).mockReset();
});

it("configures Cloudflare deployment with vinext", async ({ expect }) => {
const framework = new NextJs({ id: "next", name: "Next.js" });

const result = await framework.configure(BASE_OPTIONS);

expect(runCommand).toHaveBeenCalledWith(
[
"npx",
"vinext",
"init",
"--platform=cloudflare",
"--cdn-cache=workers-cache",
"--data-cache=none",
"--image-optimization=cloudflare-images",
"--no-prerender",
"--no-experimental-warm-cdn-cache",
],
{ cwd: "/next-app" }
);
expect(result).toEqual({
wranglerConfig: null,
packageJsonScriptsOverrides: {
preview:
"vinext build && wrangler dev --config dist/server/wrangler.json",
deploy: "vinext-cloudflare deploy --config dist/server/wrangler.json",
},
buildCommandOverride: "npx vinext build",
deployCommandOverride: "npx vinext-cloudflare deploy",
versionCommandOverride:
"npx wrangler versions upload --config dist/server/wrangler.json",
});
expect(framework.configurationDescription).toBe(
"Configuring project for Next.js with vinext by running `vinext init`"
);
});

it("does not run vinext init during a dry run", async ({ expect }) => {
const framework = new NextJs({ id: "next", name: "Next.js" });

await framework.configure({ ...BASE_OPTIONS, dryRun: true });

expect(runCommand).not.toHaveBeenCalled();
});
});
140 changes: 140 additions & 0 deletions packages/wrangler/src/__tests__/deploy/vinext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { writeFile } from "node:fs/promises";
import { getInstalledPackageVersion } from "@cloudflare/autoconfig";
import { runCommand } from "@cloudflare/cli-shared-helpers/command";
import { runInTempDir } from "@cloudflare/workers-utils/test-helpers";
import { afterEach, beforeEach, describe, it, vi } from "vitest";
import {
getVinextDeployArguments,
maybeDelegateToVinextDeployCommand,
} from "../../deploy/vinext";
import { getPackageManager } from "../../package-manager";

vi.mock("@cloudflare/autoconfig", async (importOriginal) => ({
...(await importOriginal()),
getInstalledPackageVersion: vi.fn(),
}));
vi.mock("@cloudflare/cli-shared-helpers/command");
vi.mock("../../package-manager", async (importOriginal) => ({
...(await importOriginal()),
getPackageManager: vi.fn(),
}));

describe("vinext deploy delegation", () => {
runInTempDir();
const originalArgv = process.argv;

beforeEach(async () => {
process.argv = ["node", "wrangler", "deploy", "--name", "my-worker"];
vi.mocked(getPackageManager).mockResolvedValue({
type: "npm",
npx: "npx",
dlx: ["npx"],
lockFiles: ["package-lock.json"],
});
vi.mocked(getInstalledPackageVersion).mockReturnValue("1.0.0-beta.4");
await writeFile(
"vite.config.ts",
'import vinext from "vinext";\nimport { cloudflare } from "@cloudflare/vite-plugin";\nexport default { plugins: [vinext(), cloudflare()] };\n'
);
await writeFile(
"wrangler.jsonc",
JSON.stringify({
main: "vinext/server/fetch-handler",
assets: {
directory: "dist/client",
not_found_handling: "none",
binding: "ASSETS",
},
})
);
});

afterEach(() => {
process.argv = originalArgv;
vi.unstubAllEnvs();
vi.clearAllMocks();
});

it("delegates to the vinext Cloudflare deploy command", async ({
expect,
}) => {
await expect(
maybeDelegateToVinextDeployCommand(process.cwd(), { skipBuild: true })
).resolves.toBe(true);
expect(runCommand).toHaveBeenCalledWith(
[
"npx",
"vinext-cloudflare",
"deploy",
"--skip-build",
"--name",
"my-worker",
],
{ env: { VINEXT_CLOUDFLARE_DEPLOY: "true" } }
);
});

it("does not delegate the nested Wrangler deploy", async ({ expect }) => {
vi.stubEnv("VINEXT_CLOUDFLARE_DEPLOY", "true");

await expect(
maybeDelegateToVinextDeployCommand(process.cwd())
).resolves.toBe(false);
expect(runCommand).not.toHaveBeenCalled();
});

it("does not replace an active OpenNext deployment", async ({ expect }) => {
vi.stubEnv("OPEN_NEXT_DEPLOY", "true");

await expect(
maybeDelegateToVinextDeployCommand(process.cwd())
).resolves.toBe(false);
expect(runCommand).not.toHaveBeenCalled();
});

it("does not delegate without the vinext Cloudflare packages", async ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined);

await expect(
maybeDelegateToVinextDeployCommand(process.cwd())
).resolves.toBe(false);
expect(runCommand).not.toHaveBeenCalled();
});

it("translates supported Wrangler deploy arguments", ({ expect }) => {
expect(
getVinextDeployArguments([
"--autoconfig=true",
"--name=my-worker",
"-e=staging",
])
).toEqual(["--name", "my-worker", "--env", "staging"]);
});

it("recognizes CommonJS Vite configuration", async ({ expect }) => {
await writeFile(
"vite.config.ts",
'const vinext = require("vinext");\nconst { cloudflare } = require("@cloudflare/vite-plugin");\nmodule.exports = { plugins: [vinext(), cloudflare()] };\n'
);

await expect(
maybeDelegateToVinextDeployCommand(process.cwd())
).resolves.toBe(true);
});

it("requires vinext Wrangler scaffolding", async ({ expect }) => {
await writeFile("wrangler.jsonc", "{}\n");

await expect(
maybeDelegateToVinextDeployCommand(process.cwd())
).resolves.toBe(false);
});

it("rejects Wrangler arguments that vinext cannot preserve", ({ expect }) => {
expect(() => getVinextDeployArguments(["--keep-vars"])).toThrow(
'option "--keep-vars" cannot be forwarded to vinext'
);
});
});
8 changes: 5 additions & 3 deletions packages/wrangler/src/deploy/autoconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export async function maybeRunAutoConfig<Args extends AutoConfigArgs>(
args: Args,
config: Config,
options: { skipConfirmations?: boolean } = {}
): Promise<{ config: Config; aborted: boolean }> {
): Promise<{ config: Config; aborted: boolean; configured: boolean }> {
let configured = false;
const shouldRunAutoConfig =
args.autoconfig &&
// If there is a positional parameter, an assets directory specified via --assets, or an
Expand Down Expand Up @@ -132,14 +133,15 @@ export async function maybeRunAutoConfig<Args extends AutoConfigArgs>(
command: "wrangler deploy",
dryRun: !!args.dryRun,
});
return { config, aborted: true };
return { config, aborted: true, configured };
}
} else if (!details.configured) {
const autoConfigSummary = await runAutoConfigLogic(details, {
context: autoConfigContext,
dryRun: !!args.dryRun,
skipConfirmations: options.skipConfirmations === true,
});
configured = !args.dryRun;

writeOutput({
type: "autoconfig",
Expand Down Expand Up @@ -171,7 +173,7 @@ export async function maybeRunAutoConfig<Args extends AutoConfigArgs>(
});
}

return { config, aborted: false };
return { config, aborted: false, configured };
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/wrangler/src/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { detectAgent } from "../utils/detect-agent";
import { getScriptName } from "../utils/getScriptName";
import { maybeRunAutoConfig, promptForMissingDeployConfig } from "./autoconfig";
import { maybeDelegateToOpenNextDeployCommand } from "./open-next";
import { maybeDelegateToVinextDeployCommand } from "./vinext";
import type { Config } from "@cloudflare/workers-utils";

export const deployCommand = createCommand({
Expand Down Expand Up @@ -165,15 +166,18 @@ export async function runDeployCommandHandler(
);
}

// Needs to happen after auto-config logic to capture newly auto-configured open-next apps.
// Needs to happen after auto-config logic to capture newly auto-configured framework apps.
// As a precaution we're gating the feature under the autoconfig flag for the time being.
// If the user explicitly provided a --config path, they are targeting a specific Worker config and we should not delegate
if (
!pagesToWorkersDelegation &&
args.autoconfig &&
!args.config &&
!args.dryRun &&
(await maybeDelegateToOpenNextDeployCommand(process.cwd()))
((await maybeDelegateToVinextDeployCommand(process.cwd(), {
skipBuild: autoConfigResult.configured && !args.env,
})) ||
(await maybeDelegateToOpenNextDeployCommand(process.cwd())))
) {
return;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/wrangler/src/deploy/open-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { runCommand } from "@cloudflare/cli-shared-helpers/command";
import { getOpenNextDeployFromEnv } from "@cloudflare/workers-utils";
import { logger } from "../logger";
import { getPackageManager } from "../package-manager";
import { getVinextDeployFromEnv } from "./vinext";

/**
* If appropriate (when `wrangler deploy` is run in an OpenNext project without setting the `OPEN_NEXT_DEPLOY` environment variable)
Expand All @@ -16,6 +17,10 @@ import { getPackageManager } from "../package-manager";
export async function maybeDelegateToOpenNextDeployCommand(
projectRoot: string
): Promise<boolean> {
if (getVinextDeployFromEnv()) {
return false;
}

if (await isOpenNextProject(projectRoot)) {
const openNextDeploy = getOpenNextDeployFromEnv();
if (!openNextDeploy) {
Expand Down
Loading
Loading