diff --git a/.changeset/twelve-signs-fry.md b/.changeset/twelve-signs-fry.md
new file mode 100644
index 0000000000..ea7081f4e7
--- /dev/null
+++ b/.changeset/twelve-signs-fry.md
@@ -0,0 +1,7 @@
+---
+"wrangler": patch
+---
+
+Skip lock file warning for static projects during autoconfig
+
+Previously, running autoconfig on a static project (one with no framework detected) would emit a misleading warning about a missing lock file, suggesting the project might be in a workspace. Since static projects don't require a lock file, this warning is now suppressed for them.
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/basic-framework-detection.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/basic-framework-detection.test.ts
new file mode 100644
index 0000000000..11e72791c6
--- /dev/null
+++ b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/basic-framework-detection.test.ts
@@ -0,0 +1,48 @@
+import { writeFile } from "node:fs/promises";
+import { seed } from "@cloudflare/workers-utils/test-helpers";
+import { describe, it } from "vitest";
+import { detectFramework } from "../../../../autoconfig/details/framework-detection";
+import { runInTempDir } from "../../../helpers/run-in-tmp";
+
+describe("detectFramework() / basic framework detection", () => {
+ runInTempDir();
+
+ it("defaults to the static framework when no framework is detected", async ({
+ expect,
+ }) => {
+ await writeFile(
+ "package-lock.json",
+ JSON.stringify({ lockfileVersion: 3 })
+ );
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework.framework.id).toBe("static");
+ });
+
+ it("detects astro when astro is in dependencies", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("astro");
+ expect(result.detectedFramework?.framework.name).toBe("Astro");
+ });
+
+ it("includes buildCommand in detectedFramework when available", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.buildCommand).toBeDefined();
+ expect(result.detectedFramework?.buildCommand).toContain("astro build");
+ });
+});
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/lock-file-warning.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/lock-file-warning.test.ts
new file mode 100644
index 0000000000..eeb1d9352f
--- /dev/null
+++ b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/lock-file-warning.test.ts
@@ -0,0 +1,46 @@
+import { seed } from "@cloudflare/workers-utils/test-helpers";
+import { describe, it } from "vitest";
+import { detectFramework } from "../../../../autoconfig/details/framework-detection";
+import { mockConsoleMethods } from "../../../helpers/mock-console";
+import { runInTempDir } from "../../../helpers/run-in-tmp";
+
+const noLockFileWarning =
+ "No lock file has been detected in the current working directory. This might indicate that the project is part of a workspace.";
+
+describe("detectFramework() / lock file warning", () => {
+ runInTempDir();
+ const std = mockConsoleMethods();
+
+ it("warns when no lock file is detected", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ });
+
+ await detectFramework(process.cwd());
+
+ expect(std.warn).toContain(noLockFileWarning);
+ });
+
+ it("does not warn for static projects without a lock file", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({}),
+ });
+
+ await detectFramework(process.cwd());
+
+ expect(std.warn).not.toContain(noLockFileWarning);
+ });
+
+ it("does not warn when a lock file exists", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ await detectFramework(process.cwd());
+
+ expect(std.warn).not.toContain(noLockFileWarning);
+ });
+});
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/multiple-frameworks-detected.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/multiple-frameworks-detected.test.ts
new file mode 100644
index 0000000000..ef54e79c38
--- /dev/null
+++ b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/multiple-frameworks-detected.test.ts
@@ -0,0 +1,151 @@
+import { seed } from "@cloudflare/workers-utils/test-helpers";
+import { afterEach, beforeEach, describe, it, vi } from "vitest";
+import { detectFramework } from "../../../../autoconfig/details/framework-detection";
+import * as isInteractiveModule from "../../../../is-interactive";
+import { runInTempDir } from "../../../helpers/run-in-tmp";
+import type { MockInstance } from "vitest";
+
+describe("detectFramework() / multiple frameworks detected", () => {
+ runInTempDir();
+ let isNonInteractiveOrCISpy: MockInstance;
+
+ beforeEach(() => {
+ isNonInteractiveOrCISpy = vi
+ .spyOn(isInteractiveModule, "isNonInteractiveOrCI")
+ .mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ isNonInteractiveOrCISpy.mockRestore();
+ });
+
+ describe("non-CI environment", () => {
+ beforeEach(() => {
+ isNonInteractiveOrCISpy.mockReturnValue(false);
+ });
+
+ it("returns the known framework when multiple are detected but only one is known", async ({
+ expect,
+ }) => {
+ // gatsby is not in allKnownFrameworks, only astro is
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { astro: "5", gatsby: "5" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("astro");
+ });
+
+ it("filters out Vite and returns the other known framework", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { next: "14", vite: "5" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("next");
+ });
+
+ it("returns Waku (not Hono) when both Waku and Hono are detected", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { waku: "0.21", hono: "4" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("waku");
+ });
+
+ it("returns first framework without throwing when multiple unknown frameworks are detected", async ({
+ expect,
+ }) => {
+ // Both gatsby and gridsome are unknown to wrangler
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { gatsby: "5", gridsome: "1" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ // Should not throw even with multiple unknowns in non-CI mode
+ await expect(detectFramework(process.cwd())).resolves.toBeDefined();
+ });
+ });
+
+ describe("CI environment", () => {
+ beforeEach(() => {
+ isNonInteractiveOrCISpy.mockReturnValue(true);
+ });
+
+ it("throws MultipleFrameworksCIError when multiple known frameworks are detected", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { astro: "5", nuxt: "3" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ await expect(
+ detectFramework(process.cwd())
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `
+ [Error: Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found: Astro, Nuxt.
+
+ To fix this issue either:
+ - check your project's configuration to make sure that the target framework
+ is the only configured one and try again
+ - run \`wrangler setup\` locally to get an interactive user experience where
+ you can specify what framework you want to target
+ ]
+ `
+ );
+ });
+
+ it("throws MultipleFrameworksCIError when multiple unknown frameworks are detected", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { gatsby: "5", gridsome: "1" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ await expect(detectFramework(process.cwd())).rejects.toThrowError(
+ /Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found/
+ );
+ });
+
+ it("does not throw when Vite and another known framework are detected (Vite is filtered out)", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({
+ dependencies: { astro: "5", vite: "5" },
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("astro");
+ });
+ });
+});
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/package-manager-detection.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/package-manager-detection.test.ts
new file mode 100644
index 0000000000..8ebb32fa38
--- /dev/null
+++ b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/package-manager-detection.test.ts
@@ -0,0 +1,70 @@
+import { seed } from "@cloudflare/workers-utils/test-helpers";
+import { describe, it } from "vitest";
+import { detectFramework } from "../../../../autoconfig/details/framework-detection";
+import {
+ BunPackageManager,
+ NpmPackageManager,
+ PnpmPackageManager,
+ YarnPackageManager,
+} from "../../../../package-manager";
+import { runInTempDir } from "../../../helpers/run-in-tmp";
+
+describe("detectFramework() / package manager detection", () => {
+ runInTempDir();
+
+ it("detects npm when package-lock.json is present", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.packageManager).toStrictEqual(NpmPackageManager);
+ });
+
+ it("detects pnpm when pnpm-lock.yaml is present", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "pnpm-lock.yaml": "lockfileVersion: '6.0'\n",
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.packageManager).toStrictEqual(PnpmPackageManager);
+ });
+
+ it("detects yarn when yarn.lock is present", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "yarn.lock": "# yarn lockfile v1\n",
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.packageManager).toStrictEqual(YarnPackageManager);
+ });
+
+ it("detects bun when bun.lock is present", async ({ expect }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "bun.lock": "",
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.packageManager).toStrictEqual(BunPackageManager);
+ });
+
+ it("falls back to npm when no package manager lock file is present", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.packageManager).toStrictEqual(NpmPackageManager);
+ });
+});
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/pages-project-detection.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/pages-project-detection.test.ts
new file mode 100644
index 0000000000..19ed3dc3b1
--- /dev/null
+++ b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/pages-project-detection.test.ts
@@ -0,0 +1,123 @@
+import { join } from "node:path";
+import { seed } from "@cloudflare/workers-utils/test-helpers";
+import { afterEach, beforeEach, describe, it, vi } from "vitest";
+import { detectFramework } from "../../../../autoconfig/details/framework-detection";
+import * as configCache from "../../../../config-cache";
+import * as isInteractiveModule from "../../../../is-interactive";
+import { PAGES_CONFIG_CACHE_FILENAME } from "../../../../pages/constants";
+import { mockConfirm } from "../../../helpers/mock-dialogs";
+import { runInTempDir } from "../../../helpers/run-in-tmp";
+import type { Config } from "@cloudflare/workers-utils";
+import type { MockInstance } from "vitest";
+
+describe("detectFramework() / Pages project detection", () => {
+ runInTempDir();
+ let isNonInteractiveOrCISpy: MockInstance;
+
+ beforeEach(() => {
+ isNonInteractiveOrCISpy = vi
+ .spyOn(isInteractiveModule, "isNonInteractiveOrCI")
+ .mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ isNonInteractiveOrCISpy.mockRestore();
+ });
+
+ it("returns Cloudflare Pages framework when pages_build_output_dir is set in wrangler config", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({}),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd(), {
+ pages_build_output_dir: "./dist",
+ } as Config);
+
+ expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages");
+ expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages");
+ expect(result.detectedFramework?.dist).toBe("./dist");
+ });
+
+ it("returns Cloudflare Pages framework when the pages cache file exists", async ({
+ expect,
+ }) => {
+ const cacheFolder = join(process.cwd(), ".cache");
+ await seed({
+ "package.json": JSON.stringify({}),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ [join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME)]: JSON.stringify({
+ account_id: "test-account",
+ }),
+ });
+
+ const getCacheFolderSpy = vi
+ .spyOn(configCache, "getCacheFolder")
+ .mockReturnValue(cacheFolder);
+
+ try {
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages");
+ expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages");
+ } finally {
+ getCacheFolderSpy.mockRestore();
+ }
+ });
+
+ it("returns Cloudflare Pages when a functions/ directory exists, no framework is detected, and the user confirms", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({}),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ "functions/hello.js": `export function onRequest() { return new Response("hi"); }`,
+ });
+
+ mockConfirm({
+ text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?",
+ result: true,
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages");
+ expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages");
+ });
+
+ it("does not return Cloudflare Pages when the user denies the functions/ directory prompt", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({}),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ "functions/hello.js": `export function onRequest() { return new Response("hi"); }`,
+ });
+
+ mockConfirm({
+ text: "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?",
+ result: false,
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.detectedFramework?.framework.id).not.toBe("cloudflare-pages");
+ });
+
+ it("does not return Cloudflare Pages when a functions/ directory exists alongside a detected framework", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({ dependencies: { astro: "5" } }),
+ "functions/hello.js": `export function onRequest() { return new Response("hi"); }`,
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ // Astro is detected, so Pages detection via functions/ is skipped
+ expect(result.detectedFramework?.framework.id).not.toBe("cloudflare-pages");
+ expect(result.detectedFramework?.framework.id).toBe("astro");
+ });
+});
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/workspace-root-handling.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/workspace-root-handling.test.ts
new file mode 100644
index 0000000000..7a978a4b05
--- /dev/null
+++ b/packages/wrangler/src/__tests__/autoconfig/details/framework-detection/workspace-root-handling.test.ts
@@ -0,0 +1,58 @@
+import { seed } from "@cloudflare/workers-utils/test-helpers";
+import { describe, it } from "vitest";
+import { detectFramework } from "../../../../autoconfig/details/framework-detection";
+import { runInTempDir } from "../../../helpers/run-in-tmp";
+
+describe("detectFramework() / workspace root handling", () => {
+ runInTempDir();
+
+ it("sets isWorkspaceRoot to false for regular (non-monorepo) projects", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({ name: "my-app" }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.isWorkspaceRoot).toBe(false);
+ });
+
+ it("sets isWorkspaceRoot to true when the workspace root is itself a workspace package", async ({
+ expect,
+ }) => {
+ await seed({
+ "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n - '.'\n",
+ "package.json": JSON.stringify({
+ name: "my-workspace",
+ workspaces: ["packages/*", "."],
+ }),
+ "packages/my-app/package.json": JSON.stringify({ name: "my-app" }),
+ });
+
+ const result = await detectFramework(process.cwd());
+
+ expect(result.isWorkspaceRoot).toBe(true);
+ });
+
+ it("throws UserError when run from a workspace root that does not include the root as a package", async ({
+ expect,
+ }) => {
+ await seed({
+ "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n",
+ "package.json": JSON.stringify({
+ name: "my-workspace",
+ workspaces: ["packages/*"],
+ }),
+ "packages/my-app/package.json": JSON.stringify({ name: "my-app" }),
+ "packages/my-app/index.html": "
Hello World
",
+ });
+
+ await expect(
+ detectFramework(process.cwd())
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `[Error: The Wrangler application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]`
+ );
+ });
+});
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts
index ce405e7cb7..17f47f09ad 100644
--- a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts
+++ b/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts
@@ -166,7 +166,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => {
await seed({
"package.json": JSON.stringify({
name: "my-app",
- dependencies: {},
+ dependencies: { astro: "6" },
}),
"index.html": "Hello World
",
});
diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework.test.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework.test.ts
index 036475bf35..730e3fa063 100644
--- a/packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework.test.ts
+++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/get-framework.test.ts
@@ -2,15 +2,6 @@ import { describe, it } from "vitest";
import { getFramework } from "../../../autoconfig/frameworks/get-framework";
describe("getFramework()", () => {
- it("should return a Static framework when frameworkId is undefined", ({
- expect,
- }) => {
- const framework = getFramework(undefined);
-
- expect(framework.id).toBe("static");
- expect(framework.name).toBe("Static");
- });
-
it("should return a Static framework when frameworkId is unknown", ({
expect,
}) => {
diff --git a/packages/wrangler/src/autoconfig/details/framework-detection.ts b/packages/wrangler/src/autoconfig/details/framework-detection.ts
new file mode 100644
index 0000000000..9f0c57385b
--- /dev/null
+++ b/packages/wrangler/src/autoconfig/details/framework-detection.ts
@@ -0,0 +1,330 @@
+import assert from "node:assert";
+import { existsSync, statSync } from "node:fs";
+import { join, resolve } from "node:path";
+import { FatalError, UserError } from "@cloudflare/workers-utils";
+import { Project } from "@netlify/build-info";
+import { NodeFS } from "@netlify/build-info/node";
+import { captureException } from "@sentry/node";
+import chalk from "chalk";
+import dedent from "ts-dedent";
+import { getCacheFolder } from "../../config-cache";
+import { confirm } from "../../dialogs";
+import { isNonInteractiveOrCI } from "../../is-interactive";
+import { logger } from "../../logger";
+import {
+ BunPackageManager,
+ NpmPackageManager,
+ PnpmPackageManager,
+ YarnPackageManager,
+} from "../../package-manager";
+import { PAGES_CONFIG_CACHE_FILENAME } from "../../pages/constants";
+import {
+ allKnownFrameworksIds,
+ staticFramework,
+} from "../frameworks/get-framework";
+import type { PackageManager } from "../../package-manager";
+import type { KnownFrameworkId } from "../frameworks/get-framework";
+import type { Config } from "@cloudflare/workers-utils";
+import type { Settings } from "@netlify/build-info";
+
+/**
+ * Detects the framework used by the project at the given path.
+ *
+ * Uses `@netlify/build-info` to analyze build settings and identify the
+ * framework, then maps the detected package manager to wrangler's own type.
+ *
+ * If the project is identified as a Cloudflare Pages project the function
+ * returns early with a synthetic "Cloudflare Pages" framework entry.
+ *
+ * @param projectPath Path to the project root
+ * @param wranglerConfig Optional parsed wrangler config for the project
+ * @returns An object containing:
+ * - `detectedFramework`: The matched framework together with its build
+ * command and output directory.
+ * - `packageManager`: The package manager detected in the project.
+ * - `isWorkspaceRoot`: `true` when the project path is the root of a
+ * monorepo workspace (only present when relevant).
+ * @throws {UserError} When called from a workspace root that does not itself
+ * contain the targeted project path.
+ * @throws {MultipleFrameworksCIError} (via `findDetectedFramework`) in CI /
+ * non-interactive environments when multiple known frameworks are detected
+ * and no clear winner can be determined.
+ */
+export async function detectFramework(
+ projectPath: string,
+ wranglerConfig?: Config
+): Promise<{
+ detectedFramework: DetectedFramework;
+ packageManager: PackageManager;
+ isWorkspaceRoot?: boolean;
+}> {
+ const fs = new NodeFS();
+
+ fs.logger = logger;
+
+ const project = new Project(fs, projectPath, projectPath)
+ .setEnvironment(process.env)
+ .setNodeVersion(process.version)
+ .setReportFn((err) => {
+ captureException(err);
+ });
+
+ const buildSettings = await project.getBuildSettings();
+
+ const isWorkspaceRoot = !!project.workspace?.isRoot;
+
+ if (isWorkspaceRoot) {
+ const resolvedProjectPath = resolve(projectPath);
+
+ const workspaceRootIncludesProject = project.workspace?.packages.some(
+ (pkg) => resolve(pkg.path) === resolvedProjectPath
+ );
+
+ if (!workspaceRootIncludesProject) {
+ throw new UserError(
+ "The Wrangler application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again."
+ );
+ }
+ }
+
+ // Convert the package manager detected by @netlify/build-info to our PackageManager type.
+ // This is populated after getBuildSettings() runs, which triggers the full detection chain.
+ const packageManager = convertDetectedPackageManager(project.packageManager);
+
+ const lockFileExists = packageManager.lockFiles.some((lockFile) =>
+ existsSync(join(projectPath, lockFile))
+ );
+
+ const maybeDetectedFramework = maybeFindDetectedFramework(buildSettings);
+
+ if (
+ await isPagesProject(projectPath, wranglerConfig, maybeDetectedFramework)
+ ) {
+ return {
+ detectedFramework: {
+ framework: {
+ name: "Cloudflare Pages",
+ id: "cloudflare-pages",
+ },
+ dist: wranglerConfig?.pages_build_output_dir,
+ },
+ packageManager,
+ };
+ }
+
+ const detectedFramework = maybeDetectedFramework ?? {
+ framework: {
+ id: staticFramework.id,
+ name: staticFramework.name,
+ },
+ };
+
+ if (
+ !lockFileExists &&
+ detectedFramework.framework.id !== staticFramework.id
+ ) {
+ logger.warn(
+ "No lock file has been detected in the current working directory." +
+ " This might indicate that the project is part of a workspace. Auto-configuration of " +
+ `projects inside workspaces is limited. See ${chalk.hex("#3B818D")(
+ "https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/#workspaces"
+ )}`
+ );
+ }
+
+ return {
+ detectedFramework,
+ packageManager,
+ isWorkspaceRoot,
+ };
+}
+
+/**
+ * Converts the package manager detected by @netlify/build-info to our PackageManager type.
+ * Falls back to npm if no package manager was detected.
+ *
+ * @param pkgManager The package manager detected by @netlify/build-info (from project.packageManager)
+ * @returns A PackageManager object compatible with wrangler's package manager utilities
+ */
+function convertDetectedPackageManager(
+ pkgManager: { name: string } | null
+): PackageManager {
+ if (!pkgManager) {
+ return NpmPackageManager;
+ }
+
+ switch (pkgManager?.name) {
+ case "pnpm":
+ return PnpmPackageManager;
+ case "yarn":
+ return YarnPackageManager;
+ case "bun":
+ return BunPackageManager;
+ case "npm":
+ default:
+ return NpmPackageManager;
+ }
+}
+
+class MultipleFrameworksCIError extends FatalError {
+ constructor(frameworks: string[]) {
+ super(
+ dedent`Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found: ${frameworks.join(
+ ", "
+ )}.
+
+ To fix this issue either:
+ - check your project's configuration to make sure that the target framework
+ is the only configured one and try again
+ - run \`wrangler setup\` locally to get an interactive user experience where
+ you can specify what framework you want to target
+
+ `,
+ 1,
+ { telemetryMessage: true }
+ );
+ }
+}
+
+function throwMultipleFrameworksNonInteractiveError(
+ settings: Settings[]
+): never {
+ throw new MultipleFrameworksCIError(settings.map((b) => b.name));
+}
+
+type DetectedFramework = {
+ framework: {
+ name: string;
+ id: string;
+ };
+ buildCommand?: string | undefined;
+ dist?: string;
+};
+
+async function isPagesProject(
+ projectPath: string,
+ wranglerConfig: Config | undefined,
+ detectedFramework?: DetectedFramework | undefined
+): Promise {
+ if (wranglerConfig?.pages_build_output_dir) {
+ // The `pages_build_output_dir` is set only for Pages projects
+ return true;
+ }
+
+ const cacheFolder = getCacheFolder();
+ if (cacheFolder) {
+ const pagesConfigCache = join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME);
+ if (existsSync(pagesConfigCache)) {
+ // If there is a cached pages.json we can safely assume that the project
+ // is a Pages one
+ return true;
+ }
+ }
+
+ if (detectedFramework === undefined) {
+ const functionsPath = join(projectPath, "functions");
+ if (existsSync(functionsPath)) {
+ const functionsStat = statSync(functionsPath);
+ if (functionsStat.isDirectory()) {
+ const pagesConfirmed = await confirm(
+ "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?",
+ {
+ defaultValue: true,
+ // In CI we do want to fallback to `false` so that we can proceed with the autoconfig flow
+ fallbackValue: false,
+ }
+ );
+ return pagesConfirmed;
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Selects the most appropriate framework from a list of detected framework settings.
+ *
+ * When there is a good level of confidence that the selected framework is correct or
+ * the process is running locally (where the user can choose a different framework or
+ * abort the process) the framework is returned. If there is no such confidence and the
+ * process is running in non interactive mode (where the user doesn't have the option to
+ * change the detected framework) an error is instead thrown.
+ *
+ * @param settings The array of framework settings
+ * @returns The selected framework settings, or `undefined` if none provided
+ * @throws {MultipleFrameworksCIError} In CI environments when multiple known frameworks
+ * are detected and no clear winner can be determined
+ */
+function maybeFindDetectedFramework(
+ settings: Settings[]
+): DetectedFramework | undefined {
+ if (settings.length === 0) {
+ return undefined;
+ }
+
+ if (settings.length === 1) {
+ return settings[0];
+ }
+
+ const settingsForOnlyKnownFrameworks = settings.filter(({ framework }) =>
+ allKnownFrameworksIds.has(framework.id as KnownFrameworkId)
+ );
+
+ if (settingsForOnlyKnownFrameworks.length === 0) {
+ if (isNonInteractiveOrCI()) {
+ // If we're in a non interactive session (e.g. CI) let's throw to be on the safe side
+ throwMultipleFrameworksNonInteractiveError(settings);
+ }
+ // Locally we can just return the first one since the user can anyways choose a different
+ // framework or abort the process anyways
+ return settings[0];
+ }
+
+ if (settingsForOnlyKnownFrameworks.length === 1) {
+ // If there is a single known framework it's quite safe to assume that that's the
+ // one we care about
+ return settingsForOnlyKnownFrameworks[0];
+ }
+
+ if (settingsForOnlyKnownFrameworks.length === 2) {
+ const frameworkIdsFound = new Set(
+ settings.map(({ framework }) => framework.id as KnownFrameworkId)
+ );
+
+ const viteId = "vite";
+
+ if (frameworkIdsFound.has(viteId)) {
+ const knownNonViteSettings = settingsForOnlyKnownFrameworks.find(
+ ({ framework }) => framework.id !== viteId
+ );
+
+ // Here knownNonViteSettings should always be defined, it only could be undefined if the detected frameworks are both Vite.
+ if (knownNonViteSettings) {
+ // If we've found a known framework and Vite, then most likely the Vite is there only either as an extra build tool
+ // or as part of a Vitest installation, so it's pretty safe to ignore it in this case
+ return knownNonViteSettings;
+ }
+ }
+
+ if (frameworkIdsFound.has("waku") && frameworkIdsFound.has("hono")) {
+ // The waku framework has a tight integration with hono, so it's likely that hono can also
+ // be detected in waku projects, if that's the case let's filter hono out
+ const wakuSettings = settingsForOnlyKnownFrameworks.find(
+ ({ framework }) => framework.id === "waku"
+ );
+ assert(wakuSettings);
+ return wakuSettings;
+ }
+ }
+
+ // If we've detected multiple frameworks, and we're in a non interactive session (e.g. CI) let's stay on the safe side and error
+ // (otherwise we just pick the first one as the user is always able to choose a different framework or terminate the process anyways)
+ if (isNonInteractiveOrCI()) {
+ throw new MultipleFrameworksCIError(
+ settingsForOnlyKnownFrameworks.map((b) => b.name)
+ );
+ }
+
+ return settingsForOnlyKnownFrameworks[0];
+}
diff --git a/packages/wrangler/src/autoconfig/details.ts b/packages/wrangler/src/autoconfig/details/index.ts
similarity index 58%
rename from packages/wrangler/src/autoconfig/details.ts
rename to packages/wrangler/src/autoconfig/details/index.ts
index 5e8879659b..155ca62836 100644
--- a/packages/wrangler/src/autoconfig/details.ts
+++ b/packages/wrangler/src/autoconfig/details/index.ts
@@ -1,5 +1,5 @@
import assert from "node:assert";
-import { existsSync, statSync } from "node:fs";
+import { statSync } from "node:fs";
import { readdir, stat } from "node:fs/promises";
import { basename, join, relative, resolve } from "node:path";
import { brandColor } from "@cloudflare/cli/colors";
@@ -8,70 +8,24 @@ import {
getCIOverrideName,
parsePackageJSON,
readFileSync,
- UserError,
} from "@cloudflare/workers-utils";
-import { Project } from "@netlify/build-info";
-import { NodeFS } from "@netlify/build-info/node";
-import { captureException } from "@sentry/node";
-import chalk from "chalk";
-import dedent from "ts-dedent";
-import { getCacheFolder } from "../config-cache";
-import { getErrorType } from "../core/handle-errors";
-import { confirm, prompt, select } from "../dialogs";
-import { isNonInteractiveOrCI } from "../is-interactive";
-import { logger } from "../logger";
-import { sendMetricsEvent } from "../metrics";
-import {
- BunPackageManager,
- NpmPackageManager,
- PnpmPackageManager,
- YarnPackageManager,
-} from "../package-manager";
-import { PAGES_CONFIG_CACHE_FILENAME } from "../pages/constants";
-import {
- allKnownFrameworks,
- allKnownFrameworksIds,
- getFramework,
-} from "./frameworks/get-framework";
+import { getErrorType } from "../../core/handle-errors";
+import { confirm, prompt, select } from "../../dialogs";
+import { logger } from "../../logger";
+import { sendMetricsEvent } from "../../metrics";
+import { NpmPackageManager } from "../../package-manager";
+import { allKnownFrameworks, getFramework } from "../frameworks/get-framework";
import {
getAutoConfigId,
getAutoConfigTriggerCommand,
-} from "./telemetry-utils";
-import type { PackageManager } from "../package-manager";
-import type { KnownFrameworkId } from "./frameworks/get-framework";
+} from "../telemetry-utils";
+import { detectFramework } from "./framework-detection";
+import type { PackageManager } from "../../package-manager";
import type {
AutoConfigDetails,
AutoConfigDetailsForNonConfiguredProject,
-} from "./types";
+} from "../types";
import type { Config, PackageJSON } from "@cloudflare/workers-utils";
-import type { Settings } from "@netlify/build-info";
-
-/**
- * Converts the package manager detected by @netlify/build-info to our PackageManager type.
- * Falls back to npm if no package manager was detected.
- *
- * @param pkgManager The package manager detected by @netlify/build-info (from project.packageManager)
- * @returns A PackageManager object compatible with wrangler's package manager utilities
- */
-function convertDetectedPackageManager(
- pkgManager: { name: string } | null
-): PackageManager {
- if (!pkgManager) {
- return NpmPackageManager;
- }
-
- switch (pkgManager?.name) {
- case "pnpm":
- return PnpmPackageManager;
- case "yarn":
- return YarnPackageManager;
- case "bun":
- return BunPackageManager;
- case "npm":
- default:
- return NpmPackageManager;
- }
-}
/**
* Asserts that the current project being targeted for autoconfig is not already configured.
@@ -87,33 +41,6 @@ export function assertNonConfigured(
);
}
-class MultipleFrameworksCIError extends FatalError {
- constructor(frameworks: string[]) {
- super(
- dedent`Wrangler was unable to automatically configure your project to work with Cloudflare, since multiple frameworks were found: ${frameworks.join(
- ", "
- )}.
-
- To fix this issue either:
- - check your project's configuration to make sure that the target framework
- is the only configured one and try again
- - run \`wrangler setup\` locally to get an interactive user experience where
- you can specify what framework you want to target
-
- `,
- 1,
- { telemetryMessage: true }
- );
- }
-}
-
-function throwMultipleFrameworksNonInteractiveError(
- settings: Settings[]
-): never {
- // assert(isNonInteractiveOrCI());
- throw new MultipleFrameworksCIError(settings.map((b) => b.name));
-}
-
async function hasIndexHtml(dir: string): Promise {
const children = await readdir(dir);
for (const child of children) {
@@ -161,205 +88,6 @@ type DetectedFramework = {
dist?: string;
};
-async function isPagesProject(
- projectPath: string,
- wranglerConfig: Config | undefined,
- detectedFramework?: DetectedFramework | undefined
-): Promise {
- if (wranglerConfig?.pages_build_output_dir) {
- // The `pages_build_output_dir` is set only for Pages projects
- return true;
- }
-
- const cacheFolder = getCacheFolder();
- if (cacheFolder) {
- const pagesConfigCache = join(cacheFolder, PAGES_CONFIG_CACHE_FILENAME);
- if (existsSync(pagesConfigCache)) {
- // If there is a cached pages.json we can safely assume that the project
- // is a Pages one
- return true;
- }
- }
-
- if (detectedFramework === undefined) {
- const functionsPath = join(projectPath, "functions");
- if (existsSync(functionsPath)) {
- const functionsStat = statSync(functionsPath);
- if (functionsStat.isDirectory()) {
- const pagesConfirmed = await confirm(
- "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?",
- {
- defaultValue: true,
- // In CI we do want to fallback to `false` so that we can proceed with the autoconfig flow
- fallbackValue: false,
- }
- );
- return pagesConfirmed;
- }
- }
- }
-
- return false;
-}
-
-async function detectFramework(
- projectPath: string,
- wranglerConfig?: Config
-): Promise<{
- detectedFramework: DetectedFramework | undefined;
- packageManager: PackageManager;
- isWorkspaceRoot?: boolean;
-}> {
- const fs = new NodeFS();
-
- fs.logger = logger;
-
- const project = new Project(fs, projectPath, projectPath)
- .setEnvironment(process.env)
- .setNodeVersion(process.version)
- .setReportFn((err) => {
- captureException(err);
- });
-
- const buildSettings = await project.getBuildSettings();
-
- const isWorkspaceRoot = !!project.workspace?.isRoot;
-
- if (isWorkspaceRoot) {
- const resolvedProjectPath = resolve(projectPath);
-
- const workspaceRootIncludesProject = project.workspace?.packages.some(
- (pkg) => resolve(pkg.path) === resolvedProjectPath
- );
-
- if (!workspaceRootIncludesProject) {
- throw new UserError(
- "The Wrangler application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again."
- );
- }
- }
-
- const detectedFramework = findDetectedFramework(buildSettings);
-
- // Convert the package manager detected by @netlify/build-info to our PackageManager type.
- // This is populated after getBuildSettings() runs, which triggers the full detection chain.
- const packageManager = convertDetectedPackageManager(project.packageManager);
-
- const lockFileExists = packageManager.lockFiles.some((lockFile) =>
- existsSync(join(projectPath, lockFile))
- );
-
- if (!lockFileExists) {
- logger.warn(
- "No lock file has been detected in the current working directory." +
- " This might indicate that the project is part of a workspace. Auto-configuration of " +
- `projects inside workspaces is limited. See ${chalk.hex("#3B818D")(
- "https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/#workspaces"
- )}`
- );
- }
-
- if (await isPagesProject(projectPath, wranglerConfig, detectedFramework)) {
- return {
- detectedFramework: {
- framework: {
- name: "Cloudflare Pages",
- id: "cloudflare-pages",
- },
- dist: wranglerConfig?.pages_build_output_dir,
- },
- packageManager,
- };
- }
-
- return { detectedFramework, packageManager, isWorkspaceRoot };
-}
-
-/**
- * Selects the most appropriate framework from a list of detected framework settings.
- *
- * When there is a good level of confidence that the selected framework is correct or
- * the process is running locally (where the user can choose a different framework or
- * abort the process) the framework is returned. If there is no such confidence and the
- * process is running in non interactive mode (where the user doesn't have the option to
- * change the detected framework) an error is instead thrown.
- *
- * @param settings The array of framework settings
- * @returns The selected framework settings, or `undefined` if none provided
- * @throws {MultipleFrameworksCIError} In CI environments when multiple known frameworks
- * are detected and no clear winner can be determined
- */
-function findDetectedFramework(
- settings: Settings[]
-): DetectedFramework | undefined {
- if (settings.length === 0) {
- return undefined;
- }
-
- if (settings.length === 1) {
- return settings[0];
- }
-
- const settingsForOnlyKnownFrameworks = settings.filter(({ framework }) =>
- allKnownFrameworksIds.has(framework.id as KnownFrameworkId)
- );
-
- if (settingsForOnlyKnownFrameworks.length === 0) {
- if (isNonInteractiveOrCI()) {
- // If we're in a non interactive session (e.g. CI) let's throw to be on the safe side
- throwMultipleFrameworksNonInteractiveError(settings);
- }
- // Locally we can just return the first one since the user can anyways choose a different
- // framework or abort the process anyways
- return settings[0];
- }
-
- if (settingsForOnlyKnownFrameworks.length === 1) {
- // If there is a single known framework it's quite safe to assume that that's the
- // one we care about
- return settingsForOnlyKnownFrameworks[0];
- }
-
- if (settingsForOnlyKnownFrameworks.length === 2) {
- const frameworkIdsFound = new Set(
- settings.map(({ framework }) => framework.id as KnownFrameworkId)
- );
-
- const viteId = "vite";
-
- if (frameworkIdsFound.has(viteId)) {
- const knownNonViteSettings = settingsForOnlyKnownFrameworks.find(
- ({ framework }) => framework.id !== viteId
- );
-
- // Here knownNonViteSettings should always be defined, it only could be undefined if the detected frameworks are both Vite.
- if (knownNonViteSettings) {
- // If we've found a known framework and Vite, then most likely the Vite is there only either as an extra build tool
- // or as part of a Vitest installation, so it's pretty safe to ignore it in this case
- return knownNonViteSettings;
- }
- }
-
- if (frameworkIdsFound.has("waku") && frameworkIdsFound.has("hono")) {
- // The waku framework has a tight integration with hono, so it's likely that hono can also
- // be detected in waku projects, if that's the case let's filter hono out
- return settingsForOnlyKnownFrameworks.find(
- ({ framework }) => framework.id === "waku"
- );
- }
- }
-
- // If we've detected multiple frameworks, and we're in a non interactive session (e.g. CI) let's stay on the safe side and error
- // (otherwise we just pick the first one as the user is always able to choose a different framework or terminate the process anyways)
- if (isNonInteractiveOrCI()) {
- throw new MultipleFrameworksCIError(
- settingsForOnlyKnownFrameworks.map((b) => b.name)
- );
- }
-
- return settingsForOnlyKnownFrameworks[0];
-}
-
/**
* Derives a valid worker name from a project directory.
*
diff --git a/packages/wrangler/src/autoconfig/frameworks/get-framework.ts b/packages/wrangler/src/autoconfig/frameworks/get-framework.ts
index 6a8053b943..5a4c7da879 100644
--- a/packages/wrangler/src/autoconfig/frameworks/get-framework.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/get-framework.ts
@@ -22,7 +22,7 @@ export type FrameworkInfo = {
class: typeof Framework;
};
-const staticFramework = {
+export const staticFramework = {
id: "static",
name: "Static",
class: Static,
@@ -53,7 +53,7 @@ export const allKnownFrameworksIds = new Set(
allKnownFrameworks.map(({ id }) => id)
);
-export function getFramework(frameworkId?: FrameworkInfo["id"]): Framework {
+export function getFramework(frameworkId: FrameworkInfo["id"]): Framework {
const targetedFramework = allKnownFrameworks.find(
(framework) => framework.id === frameworkId
);
diff --git a/packages/wrangler/src/autoconfig/run.ts b/packages/wrangler/src/autoconfig/run.ts
index be68fbfb1d..ed24469562 100644
--- a/packages/wrangler/src/autoconfig/run.ts
+++ b/packages/wrangler/src/autoconfig/run.ts
@@ -164,7 +164,11 @@ export async function runAutoConfig(
}
logger.debug(
- `Running autoconfig with:\n${JSON.stringify(autoConfigDetails, null, 2)}...`
+ `Running autoconfig with:\n${JSON.stringify(
+ autoConfigDetails,
+ null,
+ 2
+ )}...`
);
if (autoConfigSummary.wranglerInstall && enableWranglerInstallation) {
diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts
index c1cc519cc6..d77170b5b7 100644
--- a/packages/wrangler/src/deploy/index.ts
+++ b/packages/wrangler/src/deploy/index.ts
@@ -417,7 +417,9 @@ export const deployCommand = createCommand({
if (args.latest) {
logger.warn(
- `Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your ${configFileName(config.configPath)} file.`
+ `Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your ${configFileName(
+ config.configPath
+ )} file.`
);
}
@@ -597,7 +599,9 @@ export async function handleMaybeAssetsDeployment(
// Ask if user wants to write config file
const writeConfig = await confirm(
- `Do you want Wrangler to write a wrangler.json config file to store this configuration?\n${chalk.dim("This will allow you to simply run `wrangler deploy` on future deployments.")}`
+ `Do you want Wrangler to write a wrangler.json config file to store this configuration?\n${chalk.dim(
+ "This will allow you to simply run `wrangler deploy` on future deployments."
+ )}`
);
if (writeConfig) {
@@ -614,7 +618,9 @@ export async function handleMaybeAssetsDeployment(
writeFileSync(configPath, jsonString);
logger.log(`Wrote \n${jsonString}\n to ${chalk.bold(configPath)}.`);
logger.log(
- `Please run ${chalk.bold("`wrangler deploy`")} instead of ${chalk.bold(`\`wrangler deploy ${args.assets}\``)} next time. Wrangler will automatically use the configuration saved to wrangler.jsonc.`
+ `Please run ${chalk.bold("`wrangler deploy`")} instead of ${chalk.bold(
+ `\`wrangler deploy ${args.assets}\``
+ )} next time. Wrangler will automatically use the configuration saved to wrangler.jsonc.`
);
} else {
logger.log(
diff --git a/packages/wrangler/src/setup.ts b/packages/wrangler/src/setup.ts
index b30ae5727d..a3b7ce1f83 100644
--- a/packages/wrangler/src/setup.ts
+++ b/packages/wrangler/src/setup.ts
@@ -107,7 +107,9 @@ export const setupCommand = createCommand({
if (!args.dryRun) {
const { type } = await getPackageManager();
logCompletionMessage(
- `You can now deploy with ${brandColor(details.packageJson ? `${type} run deploy` : "wrangler deploy")}`
+ `You can now deploy with ${brandColor(
+ details.packageJson ? `${type} run deploy` : "wrangler deploy"
+ )}`
);
}
},