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<