Skip to content

Commit 6fe4d5a

Browse files
committed
[autoconfig] Bump Next.js minimum versions and provide an automatic upgrade path
1 parent 76e6014 commit 6fe4d5a

13 files changed

Lines changed: 511 additions & 48 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@cloudflare/autoconfig": minor
3+
"wrangler": minor
4+
---
5+
6+
Bump Next.js minimum versions and provide an automatic upgrade path
7+
8+
`@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.
9+
10+
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.
11+
12+
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.
13+
14+
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.

packages/autoconfig/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
"@netlify/build-info": "^10.5.1",
3939
"@types/esprima": "^4.0.3",
4040
"@types/node": "catalog:default",
41+
"@types/semver": "^7.5.1",
4142
"chalk": "catalog:default",
4243
"empathic": "^2.0.0",
4344
"esprima": "4.0.1",
4445
"recast": "0.23.11",
4546
"semiver": "^1.1.0",
47+
"semver": "^7.7.1",
4648
"ts-dedent": "^2.2.0",
4749
"tsup": "8.3.0",
4850
"typescript": "catalog:default",

packages/autoconfig/src/frameworks/all-frameworks.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,19 @@ export const allKnownFrameworks = [
7575
class: NextJs,
7676
frameworkPackageInfo: {
7777
name: "next",
78-
// 14.2.35 is the earliest version of Next.js officially supported by open-next
79-
// see: https://github.com/cloudflare/workers-sdk/pull/11704#discussion_r2634519440
80-
minimumVersion: "14.2.35",
78+
// 15.5.21 is the earliest version of Next.js officially supported by open-next
79+
// see: https://github.com/opennextjs/opennextjs-cloudflare/pull/1313
80+
minimumVersion: "15.5.21",
8181
maximumKnownMajorVersion: "16",
82+
// Next.js 15 and 16 installations that OpenNext doesn't support are upgraded in
83+
// place, staying within their existing major version.
84+
// 15.0.x is deliberately excluded: `create-next-app` pinned React to a 19 prerelease
85+
// until Next.js 15.1, which no supported Next.js version accepts as a peer, so the
86+
// upgrade cannot succeed without the user changing React first.
87+
upgradeRequired: {
88+
">=15.1.0 <15.5.21": "15.5.21",
89+
">=16 <16.2.11": "16.2.11",
90+
},
8291
},
8392
supported: true,
8493
},

packages/autoconfig/src/frameworks/framework-class.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert";
22
import semiver from "semiver";
3+
import semverSatisfies from "semver/functions/satisfies.js";
34
import { AutoConfigFrameworkConfigurationError } from "../errors";
45
import { getInstalledPackageVersion } from "./utils/packages";
56
import type { AutoConfigFrameworkPackageInfo, FrameworkInfo } from ".";
@@ -35,22 +36,44 @@ export abstract class Framework {
3536

3637
configurationDescription?: string;
3738

39+
/**
40+
* Upgrades the framework to a version that autoconfig supports.
41+
*
42+
* Only called for installed versions matched by the package's `upgradeRequired` ranges, so
43+
* frameworks that don't declare any never need to implement this.
44+
*
45+
* @param _options - The installed version, the version to upgrade to, the package
46+
* manager, and the workspace root flag.
47+
*/
48+
upgradeFrameworkVersion(
49+
_options: FrameworkVersionUpgradeOptions
50+
): Promise<void> {
51+
throw new AutoConfigFrameworkConfigurationError(
52+
`${this.name} requires an upgrade before it can be automatically configured, but autoconfig cannot upgrade it automatically.`,
53+
{ telemetryMessage: "autoconfig framework version upgrade unavailable" }
54+
);
55+
}
56+
3857
/**
3958
* Validates the installed framework version against the supported range and
4059
* stores it for later access via the `frameworkVersion` getter.
4160
* Warns via the context logger if the version exceeds `maximumKnownMajorVersion`.
4261
*
62+
* Versions matched by `upgradeRequired` are returned as an upgrade for the caller to apply,
63+
* rather than being rejected for being below `minimumVersion`.
64+
*
4365
* @param projectPath - Path to the project root used to resolve the installed version.
4466
* @param frameworkPackageInfo - Package metadata including name and version bounds.
4567
* @param context - The autoconfig context providing logger and other dependencies.
4668
* @throws {AssertionError} If the installed version cannot be determined.
47-
* @throws {AutoConfigFrameworkConfigurationError} If the version is below `minimumVersion`.
69+
* @throws {AutoConfigFrameworkConfigurationError} If the version is below `minimumVersion` and no upgrade is available.
70+
* @returns The upgrade needed to reach a supported version, or `undefined` if none is needed.
4871
*/
4972
validateFrameworkVersion(
5073
projectPath: string,
5174
frameworkPackageInfo: AutoConfigFrameworkPackageInfo,
5275
context: AutoConfigContext
53-
) {
76+
): FrameworkVersionUpgrade | undefined {
5477
const frameworkVersion = getInstalledPackageVersion(
5578
frameworkPackageInfo.name,
5679
projectPath
@@ -61,6 +84,19 @@ export abstract class Framework {
6184
`Unable to detect the version of the \`${frameworkPackageInfo.name}\` package`
6285
);
6386

87+
for (const [versionRange, upgradeTo] of Object.entries(
88+
frameworkPackageInfo.upgradeRequired ?? {}
89+
)) {
90+
if (
91+
semverSatisfies(frameworkVersion, versionRange, {
92+
includePrerelease: true,
93+
})
94+
) {
95+
this.#frameworkVersion = frameworkVersion;
96+
return { installedVersion: frameworkVersion, upgradeTo };
97+
}
98+
}
99+
64100
if (semiver(frameworkVersion, frameworkPackageInfo.minimumVersion) < 0) {
65101
throw new AutoConfigFrameworkConfigurationError(
66102
`The version of ${this.name} used in the project (${JSON.stringify(
@@ -89,6 +125,14 @@ export abstract class Framework {
89125
}
90126
}
91127

128+
export type FrameworkVersionUpgrade = {
129+
installedVersion: string;
130+
upgradeTo: string;
131+
};
132+
133+
export type FrameworkVersionUpgradeOptions = FrameworkVersionUpgrade &
134+
Pick<ConfigurationOptions, "isWorkspaceRoot" | "packageManager">;
135+
92136
export type ConfigurationOptions = {
93137
outputDir: string;
94138
projectPath: string;

packages/autoconfig/src/frameworks/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,9 @@ export type AutoConfigFrameworkPackageInfo = {
8080
minimumVersion: string;
8181
/** The latest major version of the package/framework that autoconfig supports */
8282
maximumKnownMajorVersion: string;
83+
/**
84+
* Maps ranges of installed versions that autoconfig can upgrade in place to the version each
85+
* range should be upgraded to. Ranges are checked in order, and the first match wins.
86+
*/
87+
upgradeRequired?: Record<string, string>;
8388
};

packages/autoconfig/src/frameworks/next.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1+
import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors";
12
import { runCommand } from "@cloudflare/cli-shared-helpers/command";
3+
import { installPackages } from "@cloudflare/cli-shared-helpers/packages";
24
import { Framework } from "./framework-class";
35
import type {
46
ConfigurationOptions,
57
ConfigurationResults,
8+
FrameworkVersionUpgradeOptions,
69
} from "./framework-class";
710

811
export class NextJs extends Framework {
12+
async upgradeFrameworkVersion({
13+
upgradeTo,
14+
packageManager,
15+
isWorkspaceRoot,
16+
}: FrameworkVersionUpgradeOptions): Promise<void> {
17+
await installPackages(packageManager.type, [`next@${upgradeTo}`], {
18+
isWorkspaceRoot,
19+
startText: `Updating Next.js to ${upgradeTo}`,
20+
doneText: `${brandColor("updated")} ${dim(`Next.js to ${upgradeTo}`)}`,
21+
});
22+
}
23+
924
async configure({
1025
dryRun,
1126
projectPath,
@@ -14,20 +29,9 @@ export class NextJs extends Framework {
1429
const { npx, dlx } = packageManager;
1530

1631
if (!dryRun) {
17-
await runCommand(
18-
[
19-
...dlx,
20-
"@opennextjs/cloudflare",
21-
"migrate",
22-
// Note: we force-install so that even if an incompatible version of
23-
// Next.js is used this installation still succeeds, moving users
24-
// (hopefully) in right direction (instead of failing at this step)
25-
"--force-install",
26-
],
27-
{
28-
cwd: projectPath,
29-
}
30-
);
32+
await runCommand([...dlx, "@opennextjs/cloudflare", "migrate"], {
33+
cwd: projectPath,
34+
});
3135
}
3236

3337
return {

packages/autoconfig/src/run.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
confirmAutoConfigDetails,
1818
displayAutoConfigDetails,
1919
} from "./details";
20+
import { AutoConfigFrameworkConfigurationError } from "./errors";
2021
import {
2122
isFrameworkSupported,
2223
isKnownFramework,
@@ -110,13 +111,13 @@ export async function runAutoConfig(
110111
const frameworkPackageInfo = getFrameworkPackageInfo(
111112
autoConfigDetails.framework.id
112113
);
113-
if (frameworkPackageInfo) {
114-
autoConfigDetails.framework.validateFrameworkVersion(
115-
autoConfigDetails.projectPath,
116-
frameworkPackageInfo,
117-
context
118-
);
119-
}
114+
const frameworkVersionUpgrade = frameworkPackageInfo
115+
? autoConfigDetails.framework.validateFrameworkVersion(
116+
autoConfigDetails.projectPath,
117+
frameworkPackageInfo,
118+
context
119+
)
120+
: undefined;
120121

121122
const dryRunConfigurationResults =
122123
await autoConfigDetails.framework.configure({
@@ -151,7 +152,12 @@ export async function runAutoConfig(
151152
`${npx} wrangler versions upload`,
152153
},
153154
context,
154-
dryRunConfigurationResults.packageJsonScriptsOverrides
155+
dryRunConfigurationResults.packageJsonScriptsOverrides,
156+
frameworkVersionUpgrade
157+
? `Upgrade ${autoConfigDetails.framework.name} from ${JSON.stringify(
158+
frameworkVersionUpgrade.installedVersion
159+
)} to ${JSON.stringify(frameworkVersionUpgrade.upgradeTo)}`
160+
: undefined
155161
);
156162

157163
if (
@@ -174,6 +180,34 @@ export async function runAutoConfig(
174180
return autoConfigSummary;
175181
}
176182

183+
if (frameworkVersionUpgrade) {
184+
await autoConfigDetails.framework.upgradeFrameworkVersion({
185+
...frameworkVersionUpgrade,
186+
packageManager,
187+
isWorkspaceRoot,
188+
});
189+
190+
assert(frameworkPackageInfo);
191+
// Hold the newly installed version to the same bounds as any other supported version
192+
const remainingUpgrade =
193+
autoConfigDetails.framework.validateFrameworkVersion(
194+
autoConfigDetails.projectPath,
195+
frameworkPackageInfo,
196+
context
197+
);
198+
199+
if (remainingUpgrade) {
200+
throw new AutoConfigFrameworkConfigurationError(
201+
`${autoConfigDetails.framework.name} was updated to ${JSON.stringify(
202+
frameworkVersionUpgrade.upgradeTo
203+
)}, but the version installed in the project is still ${JSON.stringify(
204+
remainingUpgrade.installedVersion
205+
)}. Update it manually and try again.`,
206+
{ telemetryMessage: "autoconfig framework version upgrade incomplete" }
207+
);
208+
}
209+
}
210+
177211
logger.debug(
178212
`Running autoconfig with:\n${JSON.stringify(autoConfigDetails, null, 2)}...`
179213
);
@@ -320,6 +354,7 @@ async function saveWranglerJsonc(
320354
* @param projectCommands - The build, deploy, and version commands for the project.
321355
* @param context - The autoconfig context providing logger and other dependencies.
322356
* @param packageJsonScriptsOverrides - Optional overrides for package.json script entries.
357+
* @param frameworkVersionUpgradeDescription - Optional framework upgrade shown before other setup operations.
323358
* @returns A summary object describing all planned operations.
324359
*/
325360
export async function buildOperationsSummary(
@@ -333,7 +368,8 @@ export async function buildOperationsSummary(
333368
version?: string;
334369
},
335370
context: AutoConfigContext,
336-
packageJsonScriptsOverrides?: PackageJsonScriptsOverrides
371+
packageJsonScriptsOverrides?: PackageJsonScriptsOverrides,
372+
frameworkVersionUpgradeDescription?: string
337373
): Promise<AutoConfigSummary> {
338374
const { logger } = context;
339375
logger.log("");
@@ -411,6 +447,12 @@ export async function buildOperationsSummary(
411447
logger.log("");
412448
}
413449

450+
if (frameworkVersionUpgradeDescription) {
451+
summary.frameworkVersionUpgrade = frameworkVersionUpgradeDescription;
452+
logger.log(`⬆️ ${summary.frameworkVersionUpgrade}`);
453+
logger.log("");
454+
}
455+
414456
if (
415457
autoConfigDetails.framework &&
416458
!(autoConfigDetails.framework instanceof Static) &&

packages/autoconfig/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type AutoConfigSummary = {
7272
wranglerInstall: boolean;
7373
wranglerConfig?: RawConfig;
7474
frameworkConfiguration?: string;
75+
frameworkVersionUpgrade?: string;
7576
outputDir: string;
7677
frameworkId?: string;
7778
buildCommand?: string;

0 commit comments

Comments
 (0)