diff --git a/.changeset/fifty-radios-nail.md b/.changeset/fifty-radios-nail.md
new file mode 100644
index 000000000000..41c9e522d132
--- /dev/null
+++ b/.changeset/fifty-radios-nail.md
@@ -0,0 +1,9 @@
+---
+"wrangler": patch
+---
+
+Fix autoconfig package installation always failing at workspace roots
+
+When running autoconfig at the root of a monorepo workspace, package installation commands now include the appropriate workspace root flags (`--workspace-root` for pnpm, `-W` for yarn). This prevents errors like "Running this command will add the dependency to the workspace root" that previously occurred when configuring projects at the workspace root.
+
+Additionally, autoconfig now allows running at the workspace root if the root directory itself is listed as a workspace package (e.g., `workspaces: ["packages/*", "."]`).
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 fdc4be0a7ed5..ce405e7cb767 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
@@ -118,10 +118,46 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => {
await expect(
details.getDetailsForAutoConfig()
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: The Wrangler application detection logic has been run in the root of a workspace, this is not supported. Change your working directory to one of the applications in the workspace and try again.]`
+ `[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.]`
);
});
+ it("should not bail when run in the root of a workspace if the root is included as a workspace package", async ({
+ expect,
+ }) => {
+ await seed({
+ "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n - '.'\n",
+ "package.json": JSON.stringify({
+ name: "my-workspace",
+ workspaces: ["packages/*", "."],
+ }),
+ "index.html": "
Hello World
",
+ "packages/my-app/package.json": JSON.stringify({ name: "my-app" }),
+ "packages/my-app/index.html": "Hello World
",
+ });
+
+ const result = await details.getDetailsForAutoConfig();
+
+ expect(result.isWorkspaceRoot).toBe(true);
+ expect(result.framework?.id).toBe("static");
+ });
+
+ it("should set isWorkspaceRoot to false for non-workspace projects", async ({
+ expect,
+ }) => {
+ await seed({
+ "package.json": JSON.stringify({
+ name: "my-app",
+ }),
+ "package-lock.json": JSON.stringify({ lockfileVersion: 3 }),
+ "index.html": "Hello World
",
+ });
+
+ const result = await details.getDetailsForAutoConfig();
+
+ expect(result.isWorkspaceRoot).toBe(false);
+ });
+
it("should warn when no lock file is detected (project may be inside a workspace)", async ({
expect,
}) => {
diff --git a/packages/wrangler/src/autoconfig/c3-vendor/packages.ts b/packages/wrangler/src/autoconfig/c3-vendor/packages.ts
index da25ac19c4c4..408d5ffa2c6a 100644
--- a/packages/wrangler/src/autoconfig/c3-vendor/packages.ts
+++ b/packages/wrangler/src/autoconfig/c3-vendor/packages.ts
@@ -11,6 +11,7 @@ type InstallConfig = {
doneText?: string;
dev?: boolean;
force?: boolean;
+ isWorkspaceRoot?: boolean;
};
/**
@@ -30,6 +31,7 @@ export const installPackages = async (
) => {
const { type } = packageManager;
const { force, dev, startText, doneText } = config;
+ const isWorkspaceRoot = !!config.isWorkspaceRoot;
if (packages.length === 0) {
let cmd;
@@ -50,8 +52,10 @@ export const installPackages = async (
...packages,
...(type === "pnpm" ? ["--no-frozen-lockfile"] : []),
...(force === true ? ["--force"] : []),
+ ...getWorkspaceInstallRootFlag(type, isWorkspaceRoot),
],
{
+ cwd: process.cwd(),
startText,
doneText,
silent: true,
@@ -74,7 +78,6 @@ export const installPackages = async (
saveFlag = dev ? "--save-dev" : "";
break;
}
-
await runCommand(
[
type,
@@ -82,6 +85,7 @@ export const installPackages = async (
...(saveFlag ? [saveFlag] : []),
...packages,
...(force === true ? ["--force"] : []),
+ ...getWorkspaceInstallRootFlag(type, isWorkspaceRoot),
],
{
startText,
@@ -113,15 +117,47 @@ export const installPackages = async (
}
};
+/**
+ * Returns the potential flag(/s) that need to be added to a package manager's install command when it is
+ * run at the root of a workspace.
+ *
+ * @param packageManagerType The type of package manager
+ * @param isWorkspaceRoot Flag indicating whether the install command is being run at the root of a workspace
+ * @returns Either an empty array, or an array containing the flag(/s) to use.
+ */
+const getWorkspaceInstallRootFlag = (
+ packageManagerType: PackageManager["type"],
+ isWorkspaceRoot: boolean
+): string[] => {
+ if (!isWorkspaceRoot) {
+ return [];
+ }
+
+ switch (packageManagerType) {
+ case "pnpm":
+ return ["--workspace-root"];
+ case "yarn":
+ return ["-W"];
+ case "npm":
+ case "bun":
+ // npm and bun don't have the workspace check
+ return [];
+ }
+};
+
/**
* Installs the latest version of wrangler in the project directory if it isn't already.
*/
-export const installWrangler = async (packageManager: PackageManager) => {
+export const installWrangler = async (
+ packageManager: PackageManager,
+ isWorkspaceRoot: boolean
+) => {
const { type } = packageManager;
// Even if Wrangler is already installed, make sure we install the latest version, as some framework CLIs are pinned to an older version
await installPackages(packageManager, [`wrangler@latest`], {
dev: true,
+ isWorkspaceRoot,
startText: `Installing wrangler ${dim(
"A command line tool for building Cloudflare Workers"
)}`,
diff --git a/packages/wrangler/src/autoconfig/details.ts b/packages/wrangler/src/autoconfig/details.ts
index d69ce0e61974..01a690e055ec 100644
--- a/packages/wrangler/src/autoconfig/details.ts
+++ b/packages/wrangler/src/autoconfig/details.ts
@@ -206,6 +206,7 @@ async function detectFramework(
): Promise<{
detectedFramework: DetectedFramework | undefined;
packageManager: PackageManager;
+ isWorkspaceRoot?: boolean;
}> {
const fs = new NodeFS();
@@ -220,10 +221,20 @@ async function detectFramework(
const buildSettings = await project.getBuildSettings();
- if (project.workspace?.isRoot) {
- throw new UserError(
- "The Wrangler application detection logic has been run in the root of a workspace, this is not supported. Change your working directory to one of the applications in the workspace and try again."
+ 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);
@@ -259,7 +270,7 @@ async function detectFramework(
};
}
- return { detectedFramework, packageManager };
+ return { detectedFramework, packageManager, isWorkspaceRoot };
}
/**
@@ -402,10 +413,8 @@ export async function getDetailsForAutoConfig({
};
}
- const { detectedFramework, packageManager } = await detectFramework(
- projectPath,
- wranglerConfig
- );
+ const { detectedFramework, packageManager, isWorkspaceRoot } =
+ await detectFramework(projectPath, wranglerConfig);
const framework = getFramework(detectedFramework?.framework?.id);
const packageJsonPath = resolve(projectPath, "package.json");
@@ -456,6 +465,7 @@ export async function getDetailsForAutoConfig({
return {
...baseDetails,
configured: true,
+ isWorkspaceRoot,
};
}
@@ -498,6 +508,7 @@ export async function getDetailsForAutoConfig({
...baseDetails,
outputDir,
configured: false,
+ isWorkspaceRoot,
};
}
diff --git a/packages/wrangler/src/autoconfig/frameworks/angular.ts b/packages/wrangler/src/autoconfig/frameworks/angular.ts
index 193ae77d33fb..55999929bed8 100644
--- a/packages/wrangler/src/autoconfig/frameworks/angular.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/angular.ts
@@ -15,11 +15,12 @@ export class Angular extends Framework {
outputDir,
dryRun,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
if (!dryRun) {
await updateAngularJson(workerName);
await overrideServerFile();
- await installAdditionalDependencies(packageManager);
+ await installAdditionalDependencies(packageManager, isWorkspaceRoot);
}
return {
wranglerConfig: {
@@ -80,11 +81,15 @@ async function overrideServerFile() {
);
}
-async function installAdditionalDependencies(packageManager: PackageManager) {
+async function installAdditionalDependencies(
+ packageManager: PackageManager,
+ isWorkspaceRoot: boolean
+) {
await installPackages(packageManager, ["xhr2"], {
dev: true,
startText: "Installing additional dependencies",
doneText: `${brandColor("installed")}`,
+ isWorkspaceRoot,
});
}
diff --git a/packages/wrangler/src/autoconfig/frameworks/index.ts b/packages/wrangler/src/autoconfig/frameworks/index.ts
index 447c889b464e..d6cdfe58e9e9 100644
--- a/packages/wrangler/src/autoconfig/frameworks/index.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/index.ts
@@ -8,6 +8,7 @@ export type ConfigurationOptions = {
workerName: string;
dryRun: boolean;
packageManager: PackageManager;
+ isWorkspaceRoot: boolean;
};
export type PackageJsonScriptsOverrides = {
diff --git a/packages/wrangler/src/autoconfig/frameworks/nuxt.ts b/packages/wrangler/src/autoconfig/frameworks/nuxt.ts
index c0bc99ccab13..d26a84124a18 100644
--- a/packages/wrangler/src/autoconfig/frameworks/nuxt.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/nuxt.ts
@@ -56,12 +56,14 @@ export class Nuxt extends Framework {
dryRun,
projectPath,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
if (!dryRun) {
await installPackages(packageManager, ["nitro-cloudflare-dev"], {
dev: true,
startText: "Installing the Cloudflare dev module",
doneText: `${brandColor(`installed`)} ${dim("nitro-cloudflare-dev")}`,
+ isWorkspaceRoot,
});
updateNuxtConfig(projectPath);
}
diff --git a/packages/wrangler/src/autoconfig/frameworks/sveltekit.ts b/packages/wrangler/src/autoconfig/frameworks/sveltekit.ts
index 94705ec50f28..ef7f7b05f93c 100644
--- a/packages/wrangler/src/autoconfig/frameworks/sveltekit.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/sveltekit.ts
@@ -9,6 +9,7 @@ export class SvelteKit extends Framework {
async configure({
dryRun,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
const { dlx } = packageManager;
if (!dryRun) {
@@ -34,6 +35,7 @@ export class SvelteKit extends Framework {
await installPackages(packageManager, [], {
startText: "Installing packages",
doneText: `${brandColor("installed")}`,
+ isWorkspaceRoot,
});
}
return {
diff --git a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts b/packages/wrangler/src/autoconfig/frameworks/tanstack.ts
index 55bc2c46cc5f..ae15b88ab654 100644
--- a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/tanstack.ts
@@ -9,12 +9,14 @@ export class TanstackStart extends Framework {
dryRun,
projectPath,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
if (!dryRun) {
await installPackages(packageManager, ["@cloudflare/vite-plugin"], {
dev: true,
startText: "Installing the Cloudflare Vite plugin",
doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`,
+ isWorkspaceRoot,
});
transformViteConfig(projectPath, { viteEnvironmentName: "ssr" });
diff --git a/packages/wrangler/src/autoconfig/frameworks/vike.ts b/packages/wrangler/src/autoconfig/frameworks/vike.ts
index b3b0b3b8be70..1c5898a0f0ff 100644
--- a/packages/wrangler/src/autoconfig/frameworks/vike.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/vike.ts
@@ -18,6 +18,7 @@ export class Vike extends Framework {
projectPath,
dryRun,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
const vikeServerIsInstalled = isPackageInstalled(
"vike-server",
@@ -41,10 +42,12 @@ export class Vike extends Framework {
{
startText: "Installing vike-photon and @photonjs/cloudflare",
doneText: `${brandColor(`installed`)} photon packages`,
+ isWorkspaceRoot,
}
);
await installPackages(packageManager, ["@cloudflare/vite-plugin"], {
dev: true,
+ isWorkspaceRoot,
});
addVikePhotonToConfigFile(projectPath);
diff --git a/packages/wrangler/src/autoconfig/frameworks/vite.ts b/packages/wrangler/src/autoconfig/frameworks/vite.ts
index a6e69464615e..cbabbeae035f 100644
--- a/packages/wrangler/src/autoconfig/frameworks/vite.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/vite.ts
@@ -16,12 +16,14 @@ export class Vite extends Framework {
dryRun,
projectPath,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
if (!dryRun) {
await installPackages(packageManager, ["@cloudflare/vite-plugin"], {
dev: true,
startText: "Installing the Cloudflare Vite plugin",
doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`,
+ isWorkspaceRoot,
});
transformViteConfig(projectPath);
diff --git a/packages/wrangler/src/autoconfig/frameworks/waku.ts b/packages/wrangler/src/autoconfig/frameworks/waku.ts
index e0c98ac79836..dd2e5bf3d39c 100644
--- a/packages/wrangler/src/autoconfig/frameworks/waku.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/waku.ts
@@ -23,6 +23,7 @@ export class Waku extends Framework {
dryRun,
projectPath,
packageManager,
+ isWorkspaceRoot,
}: ConfigurationOptions): Promise {
validateMinimumWakuVersion(projectPath);
@@ -34,6 +35,7 @@ export class Waku extends Framework {
dev: true,
startText: "Installing additional dependencies",
doneText: `${brandColor("installed")}`,
+ isWorkspaceRoot,
}
);
diff --git a/packages/wrangler/src/autoconfig/run.ts b/packages/wrangler/src/autoconfig/run.ts
index 89b2b496246e..60e603177040 100644
--- a/packages/wrangler/src/autoconfig/run.ts
+++ b/packages/wrangler/src/autoconfig/run.ts
@@ -103,11 +103,14 @@ export async function runAutoConfig(
const { packageManager } = autoConfigDetails;
+ const isWorkspaceRoot = autoConfigDetails.isWorkspaceRoot ?? false;
+
const dryRunConfigurationResults =
await autoConfigDetails.framework.configure({
outputDir: autoConfigDetails.outputDir,
projectPath: autoConfigDetails.projectPath,
workerName: autoConfigDetails.workerName,
+ isWorkspaceRoot,
dryRun: true,
packageManager,
});
@@ -163,13 +166,14 @@ export async function runAutoConfig(
);
if (autoConfigSummary.wranglerInstall && enableWranglerInstallation) {
- await installWrangler(packageManager);
+ await installWrangler(packageManager, isWorkspaceRoot);
}
const configurationResults = await autoConfigDetails.framework.configure({
outputDir: autoConfigDetails.outputDir,
projectPath: autoConfigDetails.projectPath,
workerName: autoConfigDetails.workerName,
+ isWorkspaceRoot,
dryRun: false,
packageManager,
});
diff --git a/packages/wrangler/src/autoconfig/types.ts b/packages/wrangler/src/autoconfig/types.ts
index f49e1db394c1..600d0061f1f7 100644
--- a/packages/wrangler/src/autoconfig/types.ts
+++ b/packages/wrangler/src/autoconfig/types.ts
@@ -20,6 +20,8 @@ type AutoConfigDetailsBase = {
outputDir: string;
/** The detected package manager for the project */
packageManager: PackageManager;
+ /** Whether the current path is at the root of a workspace */
+ isWorkspaceRoot?: boolean;
};
export type AutoConfigDetailsForConfiguredProject = Optional<