Skip to content
10 changes: 10 additions & 0 deletions .changeset/chatty-comics-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"wrangler": patch
---

Add minimum and maximum version checks for frameworks during auto-configuration

When Wrangler automatically configures a project, it now validates the installed version of the detected framework before proceeding:

- If the version is below the minimum known-good version, the command exits with an error asking the user to upgrade the framework.
- If the version is above the maximum known major version, a warning is emitted to let the user know the framework version has not been officially tested with this feature, and the command continues.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"buildCommand": "npm run build",
"configured": false,
"framework": Static {
"autoConfigSupported": true,
"configurationDescription": undefined,
"id": "static",
"name": "Static",
Expand Down Expand Up @@ -108,7 +107,6 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"buildCommand": "npm run app:build",
"configured": false,
"framework": Static {
"autoConfigSupported": true,
"configurationDescription": undefined,
"id": "static",
"name": "Static",
Expand Down Expand Up @@ -170,7 +168,6 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"buildCommand": "npm run build",
"configured": false,
"framework": Astro {
"autoConfigSupported": true,
"configurationDescription": "Configuring project for Astro with "astro add cloudflare"",
"id": "astro",
"name": "Astro",
Expand Down Expand Up @@ -254,7 +251,6 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"buildCommand": "npm run build",
"configured": false,
"framework": Static {
"autoConfigSupported": true,
"configurationDescription": undefined,
"id": "static",
"name": "Static",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ describe("autoconfig details - displayAutoConfigDetails()", () => {
({
wranglerConfig: {},
}) satisfies ReturnType<Framework["configure"]>,
autoConfigSupported: true,
},
} as unknown as Framework,
buildCommand: "astro build",
outputDir: "dist",
packageManager: NpmPackageManager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, it } from "vitest";
import { getFrameworkClass } from "../../../autoconfig/frameworks";
import { NextJs } from "../../../autoconfig/frameworks/next";
import { Static } from "../../../autoconfig/frameworks/static";

describe("getFrameworkClass()", () => {
it("should return a Static framework when frameworkId is unknown", ({
expect,
}) => {
const framework = getFrameworkClass("unknown-framework");

expect(framework).toBeInstanceOf(Static);
expect(framework.id).toBe("static");
expect(framework.name).toBe("Static");
});

it("should return a target framework when frameworkId is known", ({
expect,
}) => {
const framework = getFrameworkClass("next");

expect(framework).toBeInstanceOf(NextJs);
expect(framework.id).toBe("next");
expect(framework.name).toBe("Next.js");
});
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it } from "vitest";
import { isFrameworkSupported } from "../../../autoconfig/frameworks";

describe("isFrameworkSupported()", () => {
it("should return true for a supported framework id", ({ expect }) => {
expect(isFrameworkSupported("astro")).toBe(true);
});

it("should return false for a known but unsupported framework id", ({
expect,
}) => {
expect(isFrameworkSupported("hono")).toBe(false);
});

it("should throw for an unknown framework id", ({ expect }) => {
expect(() => isFrameworkSupported("unknown-framework")).toThrow(
'Unexpected unknown framework id: "unknown-framework"'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it } from "vitest";
import { isKnownFramework } from "../../../autoconfig/frameworks";

describe("isKnownFramework()", () => {
it("should return true for a known supported framework id", ({ expect }) => {
expect(isKnownFramework("astro")).toBe(true);
});

it("should return true for a known but unsupported framework id", ({
expect,
}) => {
expect(isKnownFramework("hono")).toBe(true);
});

it('should return true for the "static" framework id', ({ expect }) => {
expect(isKnownFramework("static")).toBe(true);
});

it("should return false for an unknown framework id", ({ expect }) => {
expect(isKnownFramework("unknown-framework")).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, it, vi } from "vitest";
import { AutoConfigFrameworkConfigurationError } from "../../../autoconfig/errors";
import { Framework } from "../../../autoconfig/frameworks/framework-class";
import { getInstalledPackageVersion } from "../../../autoconfig/frameworks/utils/packages";
import { mockConsoleMethods } from "../../helpers/mock-console";
import type { AutoConfigFrameworkPackageInfo } from "../../../autoconfig/frameworks";
import type {
ConfigurationOptions,
ConfigurationResults,
} from "../../../autoconfig/frameworks/framework-class";

vi.mock("../../../autoconfig/frameworks/utils/packages");

/** Minimal concrete subclass so we can instantiate the abstract Framework */
class TestFramework extends Framework {
configure(_options: ConfigurationOptions): ConfigurationResults {
return { wranglerConfig: null };
}
}

const PACKAGE_INFO: AutoConfigFrameworkPackageInfo = {
name: "some-pkg",
minimumVersion: "2.0.0",
maximumKnownMajorVersion: "4",
};

describe("Framework.validateFrameworkVersion()", () => {
const std = mockConsoleMethods();

it("throws an AssertionError when the package version cannot be determined", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined);
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).toThrow("Unable to detect the version of the `some-pkg` package");
});

it("throws AutoConfigFrameworkConfigurationError when installed version is below minimum", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("1.0.0");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).toThrow(AutoConfigFrameworkConfigurationError);

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).toThrowErrorMatchingInlineSnapshot(
`[Error: The version of Test used in the project ("1.0.0") cannot be automatically configured. Please update the Test version to at least "2.0.0" and try again.]`
);
});

it("does not throw and sets frameworkVersion when installed version equals minimumVersion", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("2.0.0");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).not.toThrow();
expect(framework.frameworkVersion).toBe("2.0.0");
expect(std.warn).toBe("");
});

it("does not throw and sets frameworkVersion when installed version is within known range", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("3.0.0");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).not.toThrow();
expect(framework.frameworkVersion).toBe("3.0.0");
expect(std.warn).toBe("");
});

it("does not throw and does not warn when installed version equals maximumKnownMajorVersion", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("4.0.0");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).not.toThrow();
expect(framework.frameworkVersion).toBe("4.0.0");
expect(std.warn).toBe("");
});

it("does not throw and does not warn when installed version is a minor/patch update within the known major", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("4.5.0");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).not.toThrow();
expect(framework.frameworkVersion).toBe("4.5.0");
expect(std.warn).toBe("");
});

it("does not throw nor warn when the installed version is an update within the known major", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("4.3.2");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).not.toThrow();
expect(framework.frameworkVersion).toBe("4.3.2");
expect(std.warn).toBe("");
});

it("does not throw but warns when installed version exceeds maximumKnownMajorVersion", ({
expect,
}) => {
vi.mocked(getInstalledPackageVersion).mockReturnValue("5.0.0");
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() =>
framework.validateFrameworkVersion("/project", PACKAGE_INFO)
).not.toThrow();
expect(framework.frameworkVersion).toBe("5.0.0");
expect(std.warn).toContain('"5.0.0"');
expect(std.warn).toContain("Test");
expect(std.warn).toContain("is not officially supported");
});

it("throws an AssertionError when frameworkVersion getter is accessed before validateFrameworkVersion is called", ({
expect,
}) => {
const framework = new TestFramework({ id: "test", name: "Test" });

expect(() => framework.frameworkVersion).toThrow(
'The version for "Test" is unexpectedly missing'
);
});
});
Loading
Loading