Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fifty-radios-nail.md
Original file line number Diff line number Diff line change
@@ -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/*", "."]`).
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<h1>Hello World</h1>",
"packages/my-app/package.json": JSON.stringify({ name: "my-app" }),
"packages/my-app/index.html": "<h1>Hello World</h1>",
});

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": "<h1>Hello World</h1>",
});

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,
}) => {
Expand Down
40 changes: 38 additions & 2 deletions packages/wrangler/src/autoconfig/c3-vendor/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type InstallConfig = {
doneText?: string;
dev?: boolean;
force?: boolean;
isWorkspaceRoot?: boolean;
};

/**
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -74,14 +78,14 @@ export const installPackages = async (
saveFlag = dev ? "--save-dev" : "";
break;
}

await runCommand(
[
type,
cmd,
...(saveFlag ? [saveFlag] : []),
...packages,
...(force === true ? ["--force"] : []),
...getWorkspaceInstallRootFlag(type, isWorkspaceRoot),
],
{
startText,
Expand Down Expand Up @@ -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"
)}`,
Expand Down
27 changes: 19 additions & 8 deletions packages/wrangler/src/autoconfig/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ async function detectFramework(
): Promise<{
detectedFramework: DetectedFramework | undefined;
packageManager: PackageManager;
isWorkspaceRoot?: boolean;
}> {
const fs = new NodeFS();

Expand All @@ -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);
Expand Down Expand Up @@ -259,7 +270,7 @@ async function detectFramework(
};
}

return { detectedFramework, packageManager };
return { detectedFramework, packageManager, isWorkspaceRoot };
}

/**
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -456,6 +465,7 @@ export async function getDetailsForAutoConfig({
return {
...baseDetails,
configured: true,
isWorkspaceRoot,
};
}

Expand Down Expand Up @@ -498,6 +508,7 @@ export async function getDetailsForAutoConfig({
...baseDetails,
outputDir,
configured: false,
isWorkspaceRoot,
};
}

Expand Down
9 changes: 7 additions & 2 deletions packages/wrangler/src/autoconfig/frameworks/angular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ export class Angular extends Framework {
outputDir,
dryRun,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
if (!dryRun) {
await updateAngularJson(workerName);
await overrideServerFile();
await installAdditionalDependencies(packageManager);
await installAdditionalDependencies(packageManager, isWorkspaceRoot);
}
return {
wranglerConfig: {
Expand Down Expand Up @@ -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,
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/autoconfig/frameworks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ConfigurationOptions = {
workerName: string;
dryRun: boolean;
packageManager: PackageManager;
isWorkspaceRoot: boolean;
};

export type PackageJsonScriptsOverrides = {
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/autoconfig/frameworks/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ export class Nuxt extends Framework {
dryRun,
projectPath,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
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);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/autoconfig/frameworks/sveltekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class SvelteKit extends Framework {
async configure({
dryRun,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
const { dlx } = packageManager;
if (!dryRun) {
Expand All @@ -34,6 +35,7 @@ export class SvelteKit extends Framework {
await installPackages(packageManager, [], {
startText: "Installing packages",
doneText: `${brandColor("installed")}`,
isWorkspaceRoot,
});
}
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/autoconfig/frameworks/tanstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ export class TanstackStart extends Framework {
dryRun,
projectPath,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
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" });
Expand Down
3 changes: 3 additions & 0 deletions packages/wrangler/src/autoconfig/frameworks/vike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class Vike extends Framework {
projectPath,
dryRun,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
const vikeServerIsInstalled = isPackageInstalled(
"vike-server",
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/autoconfig/frameworks/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export class Vite extends Framework {
dryRun,
projectPath,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
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);
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/autoconfig/frameworks/waku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class Waku extends Framework {
dryRun,
projectPath,
packageManager,
isWorkspaceRoot,
}: ConfigurationOptions): Promise<ConfigurationResults> {
validateMinimumWakuVersion(projectPath);

Expand All @@ -34,6 +35,7 @@ export class Waku extends Framework {
dev: true,
startText: "Installing additional dependencies",
doneText: `${brandColor("installed")}`,
isWorkspaceRoot,
}
);

Expand Down
6 changes: 5 additions & 1 deletion packages/wrangler/src/autoconfig/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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,
});
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/autoconfig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Loading