diff --git a/.changeset/autoconfig-bump-next-and-auto-upgrade.md b/.changeset/autoconfig-bump-next-and-auto-upgrade.md new file mode 100644 index 00000000000..094113e59d8 --- /dev/null +++ b/.changeset/autoconfig-bump-next-and-auto-upgrade.md @@ -0,0 +1,14 @@ +--- +"@cloudflare/autoconfig": minor +"wrangler": minor +--- + +Bump Next.js minimum versions and provide an automatic upgrade path + +`@opennextjs/cloudflare` declares a Next.js peer range of `>=15.5.21 <16 || >=16.2.11`, so projects on earlier 15.x or 16.x releases sit outside the versions the adapter supports. + +Autoconfig previously ran `@opennextjs/cloudflare migrate --force-install`. That flag just passes `--force` to the package manager. The peer dependency error becomes a warning buried in the install output, Next.js stays on its unsupported version, and setup finishes with a success message. + +Autoconfig now recognises the supported floors and offers to update an unsupported project in place, staying within its existing major version. The update is listed in the setup summary before you confirm, and is applied before any other project changes are made. `--force-install` is no longer passed, so a real dependency conflict is reported rather than forced. + +Two cases are not updated automatically and ask you to update Next.js yourself. Next.js 14 now falls outside the adapter's peer range entirely, and Next.js 15.0.x cannot be updated in place because `create-next-app` pinned React to a 19 prerelease before Next.js 15.1, which no supported Next.js version accepts as a peer. diff --git a/packages/autoconfig/package.json b/packages/autoconfig/package.json index 0785de05e0b..b20e03a7502 100644 --- a/packages/autoconfig/package.json +++ b/packages/autoconfig/package.json @@ -38,11 +38,13 @@ "@netlify/build-info": "^10.5.1", "@types/esprima": "^4.0.3", "@types/node": "catalog:default", + "@types/semver": "^7.5.1", "chalk": "catalog:default", "empathic": "^2.0.0", "esprima": "4.0.1", "recast": "0.23.11", "semiver": "^1.1.0", + "semver": "^7.7.1", "ts-dedent": "^2.2.0", "tsup": "8.3.0", "typescript": "catalog:default", diff --git a/packages/autoconfig/src/frameworks/all-frameworks.ts b/packages/autoconfig/src/frameworks/all-frameworks.ts index 7770d9fe39f..b5195fd06e7 100644 --- a/packages/autoconfig/src/frameworks/all-frameworks.ts +++ b/packages/autoconfig/src/frameworks/all-frameworks.ts @@ -75,10 +75,19 @@ 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", + // 15.5.21 is the earliest version of Next.js officially supported by open-next + // see: https://github.com/opennextjs/opennextjs-cloudflare/pull/1313 + minimumVersion: "15.5.21", maximumKnownMajorVersion: "16", + // Next.js 15 and 16 installations that OpenNext doesn't support are upgraded in + // place, staying within their existing major version. + // 15.0.x is deliberately excluded: `create-next-app` pinned React to a 19 prerelease + // until Next.js 15.1, which no supported Next.js version accepts as a peer, so the + // upgrade cannot succeed without the user changing React first. + upgradeRequired: { + ">=15.1.0 <15.5.21": "15.5.21", + ">=16 <16.2.11": "16.2.11", + }, }, supported: true, }, diff --git a/packages/autoconfig/src/frameworks/framework-class.ts b/packages/autoconfig/src/frameworks/framework-class.ts index 93f51529e88..712df374bbc 100644 --- a/packages/autoconfig/src/frameworks/framework-class.ts +++ b/packages/autoconfig/src/frameworks/framework-class.ts @@ -1,5 +1,6 @@ import assert from "node:assert"; import semiver from "semiver"; +import semverSatisfies from "semver/functions/satisfies.js"; import { AutoConfigFrameworkConfigurationError } from "../errors"; import { getInstalledPackageVersion } from "./utils/packages"; import type { AutoConfigFrameworkPackageInfo, FrameworkInfo } from "."; @@ -35,22 +36,44 @@ export abstract class Framework { configurationDescription?: string; + /** + * Upgrades the framework to a version that autoconfig supports. + * + * Only called for installed versions matched by the package's `upgradeRequired` ranges, so + * frameworks that don't declare any never need to implement this. + * + * @param _options - The installed version, the version to upgrade to, the package + * manager, and the workspace root flag. + */ + upgradeFrameworkVersion( + _options: FrameworkVersionUpgradeOptions + ): Promise { + throw new AutoConfigFrameworkConfigurationError( + `${this.name} requires an upgrade before your project can be automatically configured.`, + { telemetryMessage: "autoconfig framework version upgrade unavailable" } + ); + } + /** * Validates the installed framework version against the supported range and * stores it for later access via the `frameworkVersion` getter. * Warns via the context logger if the version exceeds `maximumKnownMajorVersion`. * + * Versions matched by `upgradeRequired` are returned as an upgrade for the caller to apply, + * rather than being rejected for being below `minimumVersion`. + * * @param projectPath - Path to the project root used to resolve the installed version. * @param frameworkPackageInfo - Package metadata including name and version bounds. * @param context - The autoconfig context providing logger and other dependencies. * @throws {AssertionError} If the installed version cannot be determined. - * @throws {AutoConfigFrameworkConfigurationError} If the version is below `minimumVersion`. + * @throws {AutoConfigFrameworkConfigurationError} If the version is below `minimumVersion` and no upgrade is available. + * @returns The upgrade needed to reach a supported version, or `undefined` if none is needed. */ validateFrameworkVersion( projectPath: string, frameworkPackageInfo: AutoConfigFrameworkPackageInfo, context: AutoConfigContext - ) { + ): FrameworkVersionUpgrade | undefined { const frameworkVersion = getInstalledPackageVersion( frameworkPackageInfo.name, projectPath @@ -61,6 +84,19 @@ export abstract class Framework { `Unable to detect the version of the \`${frameworkPackageInfo.name}\` package` ); + for (const [versionRange, upgradeTo] of Object.entries( + frameworkPackageInfo.upgradeRequired ?? {} + )) { + if ( + semverSatisfies(frameworkVersion, versionRange, { + includePrerelease: true, + }) + ) { + this.#frameworkVersion = frameworkVersion; + return { installedVersion: frameworkVersion, upgradeTo }; + } + } + if (semiver(frameworkVersion, frameworkPackageInfo.minimumVersion) < 0) { throw new AutoConfigFrameworkConfigurationError( `The version of ${this.name} used in the project (${JSON.stringify( @@ -89,6 +125,14 @@ export abstract class Framework { } } +export type FrameworkVersionUpgrade = { + installedVersion: string; + upgradeTo: string; +}; + +export type FrameworkVersionUpgradeOptions = FrameworkVersionUpgrade & + Pick; + export type ConfigurationOptions = { outputDir: string; projectPath: string; diff --git a/packages/autoconfig/src/frameworks/index.ts b/packages/autoconfig/src/frameworks/index.ts index 9e90c2aab48..6e294aee1fa 100644 --- a/packages/autoconfig/src/frameworks/index.ts +++ b/packages/autoconfig/src/frameworks/index.ts @@ -80,4 +80,9 @@ export type AutoConfigFrameworkPackageInfo = { minimumVersion: string; /** The latest major version of the package/framework that autoconfig supports */ maximumKnownMajorVersion: string; + /** + * Maps ranges of installed versions that autoconfig can upgrade in place to the version each + * range should be upgraded to. Ranges are checked in order, and the first match wins. + */ + upgradeRequired?: Record; }; diff --git a/packages/autoconfig/src/frameworks/next.ts b/packages/autoconfig/src/frameworks/next.ts index 70af2173cb1..d60ca98f613 100644 --- a/packages/autoconfig/src/frameworks/next.ts +++ b/packages/autoconfig/src/frameworks/next.ts @@ -1,11 +1,26 @@ +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; import { runCommand } from "@cloudflare/cli-shared-helpers/command"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; import { Framework } from "./framework-class"; import type { ConfigurationOptions, ConfigurationResults, + FrameworkVersionUpgradeOptions, } from "./framework-class"; export class NextJs extends Framework { + async upgradeFrameworkVersion({ + upgradeTo, + packageManager, + isWorkspaceRoot, + }: FrameworkVersionUpgradeOptions): Promise { + await installPackages(packageManager.type, [`next@${upgradeTo}`], { + isWorkspaceRoot, + startText: `Updating Next.js to ${upgradeTo}`, + doneText: `${brandColor("updated")} ${dim(`Next.js to ${upgradeTo}`)}`, + }); + } + async configure({ dryRun, projectPath, @@ -14,20 +29,9 @@ export class NextJs extends Framework { const { npx, dlx } = packageManager; if (!dryRun) { - 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", - ], - { - cwd: projectPath, - } - ); + await runCommand([...dlx, "@opennextjs/cloudflare", "migrate"], { + cwd: projectPath, + }); } return { diff --git a/packages/autoconfig/src/run.ts b/packages/autoconfig/src/run.ts index 58b045be990..c7bb8060b7c 100644 --- a/packages/autoconfig/src/run.ts +++ b/packages/autoconfig/src/run.ts @@ -17,6 +17,7 @@ import { confirmAutoConfigDetails, displayAutoConfigDetails, } from "./details"; +import { AutoConfigFrameworkConfigurationError } from "./errors"; import { isFrameworkSupported, isKnownFramework, @@ -110,13 +111,13 @@ export async function runAutoConfig( const frameworkPackageInfo = getFrameworkPackageInfo( autoConfigDetails.framework.id ); - if (frameworkPackageInfo) { - autoConfigDetails.framework.validateFrameworkVersion( - autoConfigDetails.projectPath, - frameworkPackageInfo, - context - ); - } + const frameworkVersionUpgrade = frameworkPackageInfo + ? autoConfigDetails.framework.validateFrameworkVersion( + autoConfigDetails.projectPath, + frameworkPackageInfo, + context + ) + : undefined; const dryRunConfigurationResults = await autoConfigDetails.framework.configure({ @@ -151,7 +152,12 @@ export async function runAutoConfig( `${npx} wrangler versions upload`, }, context, - dryRunConfigurationResults.packageJsonScriptsOverrides + dryRunConfigurationResults.packageJsonScriptsOverrides, + frameworkVersionUpgrade + ? `Upgrade ${autoConfigDetails.framework.name} from ${JSON.stringify( + frameworkVersionUpgrade.installedVersion + )} to ${JSON.stringify(frameworkVersionUpgrade.upgradeTo)}` + : undefined ); if ( @@ -174,6 +180,34 @@ export async function runAutoConfig( return autoConfigSummary; } + if (frameworkVersionUpgrade) { + await autoConfigDetails.framework.upgradeFrameworkVersion({ + ...frameworkVersionUpgrade, + packageManager, + isWorkspaceRoot, + }); + + assert(frameworkPackageInfo); + // Hold the newly installed version to the same bounds as any other supported version + const remainingUpgrade = + autoConfigDetails.framework.validateFrameworkVersion( + autoConfigDetails.projectPath, + frameworkPackageInfo, + context + ); + + if (remainingUpgrade) { + throw new AutoConfigFrameworkConfigurationError( + `${autoConfigDetails.framework.name} was updated to ${JSON.stringify( + frameworkVersionUpgrade.upgradeTo + )}, but the version installed in the project is still ${JSON.stringify( + remainingUpgrade.installedVersion + )}. Update it manually and try again.`, + { telemetryMessage: "autoconfig framework version upgrade incomplete" } + ); + } + } + logger.debug( `Running autoconfig with:\n${JSON.stringify(autoConfigDetails, null, 2)}...` ); @@ -320,6 +354,7 @@ async function saveWranglerJsonc( * @param projectCommands - The build, deploy, and version commands for the project. * @param context - The autoconfig context providing logger and other dependencies. * @param packageJsonScriptsOverrides - Optional overrides for package.json script entries. + * @param frameworkVersionUpgradeDescription - Optional framework upgrade shown before other setup operations. * @returns A summary object describing all planned operations. */ export async function buildOperationsSummary( @@ -333,7 +368,8 @@ export async function buildOperationsSummary( version?: string; }, context: AutoConfigContext, - packageJsonScriptsOverrides?: PackageJsonScriptsOverrides + packageJsonScriptsOverrides?: PackageJsonScriptsOverrides, + frameworkVersionUpgradeDescription?: string ): Promise { const { logger } = context; logger.log(""); @@ -411,6 +447,12 @@ export async function buildOperationsSummary( logger.log(""); } + if (frameworkVersionUpgradeDescription) { + summary.frameworkVersionUpgrade = frameworkVersionUpgradeDescription; + logger.log(`⬆️ ${summary.frameworkVersionUpgrade}`); + logger.log(""); + } + if ( autoConfigDetails.framework && !(autoConfigDetails.framework instanceof Static) && diff --git a/packages/autoconfig/src/types.ts b/packages/autoconfig/src/types.ts index 3cd97d09e3d..0a5deff9b6f 100644 --- a/packages/autoconfig/src/types.ts +++ b/packages/autoconfig/src/types.ts @@ -72,6 +72,7 @@ export type AutoConfigSummary = { wranglerInstall: boolean; wranglerConfig?: RawConfig; frameworkConfiguration?: string; + frameworkVersionUpgrade?: string; outputDir: string; frameworkId?: string; buildCommand?: string; diff --git a/packages/autoconfig/tests/frameworks/next.test.ts b/packages/autoconfig/tests/frameworks/next.test.ts new file mode 100644 index 00000000000..0d20475ea7a --- /dev/null +++ b/packages/autoconfig/tests/frameworks/next.test.ts @@ -0,0 +1,127 @@ +import assert from "node:assert"; +import { runCommand } from "@cloudflare/cli-shared-helpers/command"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { describe, it, vi } from "vitest"; +import { AutoConfigFrameworkConfigurationError } from "../../src/errors"; +import { getFrameworkPackageInfo } from "../../src/frameworks/all-frameworks"; +import { NextJs } from "../../src/frameworks/next"; +import { getInstalledPackageVersion } from "../../src/frameworks/utils/packages"; +import { createMockContext } from "../helpers/mock-context"; + +vi.mock("@cloudflare/cli-shared-helpers/command"); +vi.mock("@cloudflare/cli-shared-helpers/packages"); +vi.mock("../../src/frameworks/utils/packages"); + +describe("NextJs", () => { + const context = createMockContext(); + + it("selects upgrades for unsupported Next.js versions", ({ expect }) => { + const cases = [ + ["15.1.0", "15.5.21"], + ["15.5.7", "15.5.21"], + ["15.5.20", "15.5.21"], + ["15.5.21", undefined], + ["16.0.7", "16.2.11"], + ["16.2.6", "16.2.11"], + ["16.2.10", "16.2.11"], + ["16.2.11-canary.3", "16.2.11"], + ["16.2.11", undefined], + ["16.3.0", undefined], + ] as const; + const packageInfo = getFrameworkPackageInfo("next"); + assert(packageInfo); + + for (const [installedVersion, upgradeTo] of cases) { + vi.mocked(getInstalledPackageVersion).mockReturnValue(installedVersion); + const framework = new NextJs({ id: "next", name: "Next.js" }); + + expect( + framework.validateFrameworkVersion("/project", packageInfo, context) + ?.upgradeTo + ).toBe(upgradeTo); + } + }); + + it("rejects versions below the minimum that cannot be upgraded", ({ + expect, + }) => { + const packageInfo = getFrameworkPackageInfo("next"); + assert(packageInfo); + + // 15.0.x is excluded from the upgrade ranges: `create-next-app` pinned React to a 19 + // prerelease before Next.js 15.1, so the upgrade could not resolve + for (const installedVersion of ["13.5.11", "14.2.35", "15.0.0", "15.0.4"]) { + vi.mocked(getInstalledPackageVersion).mockReturnValue(installedVersion); + const framework = new NextJs({ id: "next", name: "Next.js" }); + + expect(() => + framework.validateFrameworkVersion("/project", packageInfo, context) + ).toThrow(AutoConfigFrameworkConfigurationError); + } + }); + + it("installs only the targeted Next.js version", async ({ expect }) => { + const cases = [ + ["15.5.20", "15.5.21"], + ["16.2.10", "16.2.11"], + ] as const; + + for (const [installedVersion, upgradeTo] of cases) { + vi.mocked(installPackages).mockResolvedValue(); + const framework = new NextJs({ id: "next", name: "Next.js" }); + + await framework.upgradeFrameworkVersion({ + installedVersion, + upgradeTo, + packageManager: NpmPackageManager, + isWorkspaceRoot: true, + }); + + expect(installPackages).toHaveBeenCalledWith( + "npm", + [`next@${upgradeTo}`], + expect.objectContaining({ isWorkspaceRoot: true }) + ); + } + }); + + it("propagates package manager failures unchanged", async ({ expect }) => { + const installError = new Error( + "npm error ERESOLVE unable to resolve dependency tree" + ); + vi.mocked(installPackages).mockRejectedValue(installError); + const framework = new NextJs({ id: "next", name: "Next.js" }); + + await expect( + framework.upgradeFrameworkVersion({ + installedVersion: "15.5.20", + upgradeTo: "15.5.21", + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + }) + ).rejects.toBe(installError); + }); + + it("runs OpenNext migration without forcing dependency installation", async ({ + expect, + }) => { + vi.mocked(runCommand).mockResolvedValue(""); + const framework = new NextJs({ id: "next", name: "Next.js" }); + + await framework.configure({ + projectPath: "/project", + outputDir: ".open-next", + workerName: "next-app", + dryRun: false, + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + context, + }); + + expect(runCommand).toHaveBeenCalledWith( + ["npx", "@opennextjs/cloudflare", "migrate"], + { cwd: "/project" } + ); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts index 440aec9d23f..5846d082d4e 100644 --- a/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts +++ b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts @@ -25,6 +25,15 @@ const PACKAGE_INFO: AutoConfigFrameworkPackageInfo = { maximumKnownMajorVersion: "4", }; +const PACKAGE_INFO_WITH_UPGRADES: AutoConfigFrameworkPackageInfo = { + ...PACKAGE_INFO, + upgradeRequired: { + ">=1.5 <2": "2.0.0", + ">=2 <2.5.0": "2.5.0", + ">=3 <3.1.0": "3.1.0", + }, +}; + describe("Framework.validateFrameworkVersion()", () => { const std = mockConsoleMethods(); const context = createMockContext(); @@ -137,6 +146,63 @@ describe("Framework.validateFrameworkVersion()", () => { expect(std.warn).toContain("is not officially supported"); }); + it("returns the target version when the installed version requires an upgrade", ({ + expect, + }) => { + const cases = [ + ["1.5.5", "2.0.0"], + ["2.0.0", "2.5.0"], + ["2.5.0-beta.1", "2.5.0"], + ["3.0.9", "3.1.0"], + ] as const; + + for (const [installedVersion, upgradeTo] of cases) { + vi.mocked(getInstalledPackageVersion).mockReturnValue(installedVersion); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect( + framework.validateFrameworkVersion( + "/project", + PACKAGE_INFO_WITH_UPGRADES, + context + ) + ).toEqual({ installedVersion, upgradeTo }); + // The version is recorded even though it still needs upgrading + expect(framework.frameworkVersion).toBe(installedVersion); + } + }); + + it("does not return an upgrade at the target-version boundary", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("2.5.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect( + framework.validateFrameworkVersion( + "/project", + PACKAGE_INFO_WITH_UPGRADES, + context + ) + ).toBeUndefined(); + }); + + it("returns an upgrade instead of rejecting a below-minimum version", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("1.5.5"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect( + framework.validateFrameworkVersion( + "/project", + PACKAGE_INFO_WITH_UPGRADES, + context + ) + ).toEqual({ installedVersion: "1.5.5", upgradeTo: "2.0.0" }); + expect(std.warn).toBe(""); + }); + it("throws an AssertionError when frameworkVersion getter is accessed before validateFrameworkVersion is called", ({ expect, }) => { diff --git a/packages/autoconfig/tests/run-upgrade.test.ts b/packages/autoconfig/tests/run-upgrade.test.ts new file mode 100644 index 00000000000..47ca59ab435 --- /dev/null +++ b/packages/autoconfig/tests/run-upgrade.test.ts @@ -0,0 +1,149 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { NextJs } from "../src/frameworks/next"; +import { getInstalledPackageVersion } from "../src/frameworks/utils/packages"; +import { runAutoConfig } from "../src/run"; +import { createMockContext } from "./helpers/mock-context"; +import type { AutoConfigContext } from "../src/context"; + +vi.mock("../src/frameworks/utils/packages"); + +describe("autoconfig framework upgrades", () => { + runInTempDir(); + + let context: AutoConfigContext; + let framework: NextJs; + + beforeEach(() => { + context = createMockContext(); + framework = new NextJs({ id: "next", name: "Next.js" }); + }); + + function run( + options: { dryRun?: boolean; skipConfirmations?: boolean } = {} + ) { + return runAutoConfig( + { + projectPath: process.cwd(), + workerName: "my-worker", + configured: false, + outputDir: ".open-next", + framework, + packageManager: NpmPackageManager, + }, + { + context, + runBuild: false, + enableWranglerInstallation: false, + ...options, + } + ); + } + + it("upgrades after confirmation and revalidates before configuring", async ({ + expect, + }) => { + vi.mocked(context.dialogs.confirm) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + let installedVersion = "15.5.20"; + vi.mocked(getInstalledPackageVersion).mockImplementation( + () => installedVersion + ); + const callOrder: string[] = []; + vi.spyOn(framework, "upgradeFrameworkVersion").mockImplementation( + async () => { + callOrder.push("upgrade"); + installedVersion = "15.5.21"; + } + ); + vi.spyOn(framework, "configure").mockImplementation(({ dryRun }) => { + callOrder.push(dryRun ? "configure:dry-run" : "configure"); + return Promise.resolve({ wranglerConfig: null }); + }); + + const summary = await run(); + + expect(callOrder).toEqual(["configure:dry-run", "upgrade", "configure"]); + expect(summary.frameworkVersionUpgrade).toContain( + 'Next.js from "15.5.20" to "15.5.21"' + ); + }); + + it("does not upgrade when setup is declined", async ({ expect }) => { + vi.mocked(context.dialogs.confirm) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false); + vi.mocked(getInstalledPackageVersion).mockReturnValue("15.5.20"); + const upgradeSpy = vi + .spyOn(framework, "upgradeFrameworkVersion") + .mockImplementation(async () => {}); + vi.spyOn(framework, "configure").mockResolvedValue({ + wranglerConfig: null, + }); + + await expect(run()).rejects.toThrow("Setup cancelled"); + expect(upgradeSpy).not.toHaveBeenCalled(); + }); + + it("reports but does not apply an upgrade during a dry run", async ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("16.2.10"); + const upgradeSpy = vi + .spyOn(framework, "upgradeFrameworkVersion") + .mockImplementation(async () => {}); + vi.spyOn(framework, "configure").mockResolvedValue({ + wranglerConfig: null, + }); + + const summary = await run({ dryRun: true }); + + expect(summary.frameworkVersionUpgrade).toContain( + 'Next.js from "16.2.10" to "16.2.11"' + ); + expect(upgradeSpy).not.toHaveBeenCalled(); + }); + + it("applies an upgrade when confirmations are explicitly skipped", async ({ + expect, + }) => { + let installedVersion = "15.5.20"; + vi.mocked(getInstalledPackageVersion).mockImplementation( + () => installedVersion + ); + const upgradeSpy = vi + .spyOn(framework, "upgradeFrameworkVersion") + .mockImplementation(async () => { + installedVersion = "15.5.21"; + }); + vi.spyOn(framework, "configure").mockResolvedValue({ + wranglerConfig: null, + }); + + await run({ skipConfirmations: true }); + + expect(context.dialogs.confirm).not.toHaveBeenCalled(); + expect(upgradeSpy).toHaveBeenCalledOnce(); + }); + + it("fails when the version is still unsupported after upgrading", async ({ + expect, + }) => { + // The upgrade reports success without changing the installed version + vi.mocked(getInstalledPackageVersion).mockReturnValue("15.5.20"); + vi.spyOn(framework, "upgradeFrameworkVersion").mockImplementation( + async () => {} + ); + const configureSpy = vi + .spyOn(framework, "configure") + .mockResolvedValue({ wranglerConfig: null }); + + await expect(run({ skipConfirmations: true })).rejects.toThrow( + 'but the version installed in the project is still "15.5.20"' + ); + // The dry run builds the summary, but the project is never configured for real + expect(configureSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/wrangler/src/__tests__/autoconfig/run.test.ts b/packages/wrangler/src/__tests__/autoconfig/run.test.ts index f9bf7a55260..411d220014d 100644 --- a/packages/wrangler/src/__tests__/autoconfig/run.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/run.test.ts @@ -910,6 +910,7 @@ describe("autoconfig (deploy)", () => { const callOrder: string[] = []; vi.spyOn(framework, "validateFrameworkVersion").mockImplementation(() => { callOrder.push("validateFrameworkVersion"); + return undefined; }); vi.spyOn(framework, "configure").mockImplementation(async () => { callOrder.push("configure"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f15f4ec7e3..d87233ebe0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1666,6 +1666,9 @@ importers: '@types/node': specifier: 22.15.17 version: 22.15.17 + '@types/semver': + specifier: ^7.5.1 + version: 7.7.1 chalk: specifier: catalog:default version: 5.3.0 @@ -1681,6 +1684,9 @@ importers: semiver: specifier: ^1.1.0 version: 1.1.0 + semver: + specifier: ^7.7.1 + version: 7.8.5 ts-dedent: specifier: ^2.2.0 version: 2.2.0 @@ -14504,11 +14510,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.2: - resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -16976,7 +16977,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.3 + semver: 7.8.5 '@changesets/assemble-release-plan@6.0.9': dependencies: @@ -16985,7 +16986,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.7.3 + semver: 7.8.5 '@changesets/changelog-git@0.2.1': dependencies: @@ -17051,7 +17052,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.3 + semver: 7.8.5 '@changesets/get-github-info@0.6.0(encoding@0.1.13)': dependencies: @@ -18763,7 +18764,7 @@ snapshots: find-up: 7.0.0 minimatch: 10.2.5 read-pkg: 9.0.1 - semver: 7.7.3 + semver: 7.8.5 yaml: 2.8.1 yargs: 17.7.2 @@ -19243,7 +19244,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.7.3 + semver: 7.8.5 tar-fs: 3.1.0 yargs: 17.7.2 transitivePeerDependencies: @@ -19256,7 +19257,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.7.3 + semver: 7.8.5 tar-fs: 3.1.0 unbzip2-stream: 1.4.3 yargs: 17.7.2 @@ -24011,7 +24012,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.2 + semver: 7.8.5 jsprim@2.0.2: dependencies: @@ -24401,7 +24402,7 @@ snapshots: pkg-types: 1.3.1 postcss: 8.5.14 postcss-nested: 7.0.2(postcss@8.5.14) - semver: 7.8.2 + semver: 7.8.5 tinyglobby: 0.2.17 optionalDependencies: typescript: 5.8.3 @@ -24851,7 +24852,7 @@ snapshots: ky: 1.7.5 registry-auth-token: 5.0.2 registry-url: 6.0.1 - semver: 7.7.3 + semver: 7.8.5 package-manager-detector@0.2.9: {} @@ -26079,7 +26080,7 @@ snapshots: sembear@0.7.0: dependencies: - semver: 7.7.3 + semver: 7.8.5 semiver@1.1.0: {} @@ -26097,8 +26098,6 @@ snapshots: semver@7.7.3: {} - semver@7.8.2: {} - semver@7.8.5: {} send@0.19.0: @@ -26850,7 +26849,7 @@ snapshots: hookable: 5.5.3 rolldown: 1.0.0-beta.44(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) rolldown-plugin-dts: 0.16.12(rolldown@1.0.0-beta.44(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1))(typescript@5.8.3) - semver: 7.7.3 + semver: 7.8.5 tinyexec: 1.0.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 @@ -26877,7 +26876,7 @@ snapshots: hookable: 5.5.3 rolldown: 1.0.0-beta.44(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) rolldown-plugin-dts: 0.16.12(rolldown@1.0.0-beta.44(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1))(typescript@5.9.3) - semver: 7.7.3 + semver: 7.8.5 tinyexec: 1.0.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 @@ -27670,7 +27669,7 @@ snapshots: dependencies: '@volar/typescript': 2.4.0-alpha.18 '@vue/language-core': 2.0.29(typescript@5.8.3) - semver: 7.8.2 + semver: 7.8.5 typescript: 5.8.3 w3c-keyname@2.2.8: {}