diff --git a/.changeset/floppy-brooms-cheat.md b/.changeset/floppy-brooms-cheat.md new file mode 100644 index 00000000000..735516dd1fb --- /dev/null +++ b/.changeset/floppy-brooms-cheat.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Fix package manager detection diff --git a/v-next/hardhat/src/internal/cli/init/init.ts b/v-next/hardhat/src/internal/cli/init/init.ts index d8a10549737..0b2f092ef76 100644 --- a/v-next/hardhat/src/internal/cli/init/init.ts +++ b/v-next/hardhat/src/internal/cli/init/init.ts @@ -327,7 +327,7 @@ export async function validatePackageJson( await writeJsonFile(absolutePathToPackageJson, packageJson); } - const packageManager = await getPackageManager(workspace); + const packageManager = getPackageManager(); // We know this works with npm, pnpm, but not with yarn. If, so we use // pnpm or npm exclusively. @@ -491,7 +491,7 @@ export async function installProjectDependencies( pathToWorkspacePackageJson, ); - const packageManager = await getPackageManager(workspace); + const packageManager = getPackageManager(); // Find the template dev dependencies that are not already installed const templateDependencies = template.packageJson.devDependencies ?? {}; diff --git a/v-next/hardhat/src/internal/cli/init/package-manager.ts b/v-next/hardhat/src/internal/cli/init/package-manager.ts index b0ee61696da..75c7c6702aa 100644 --- a/v-next/hardhat/src/internal/cli/init/package-manager.ts +++ b/v-next/hardhat/src/internal/cli/init/package-manager.ts @@ -1,35 +1,98 @@ import { execSync } from "node:child_process"; -import path from "node:path"; -import { exists } from "@nomicfoundation/hardhat-utils/fs"; import semver from "semver"; -type PackageManager = "npm" | "yarn" | "pnpm"; +type PackageManager = "npm" | "yarn" | "pnpm" | "bun" | "deno"; /** - * getPackageManager returns the name of the package manager used in the workspace. - * It determines this by checking the presence of package manager specific lock files. + * getPackageManager returns the name of the package manager used to run Hardhat * - * @param workspace The path to the workspace where the package manager should be checked. - * @returns The name of the package manager used in the workspace. + * This logic is based on the env variable `npm_config_user_agent`, which is set + * by all major package manager, both when running a package that has been + * installed, and when it hasn't. + * + * Here's how to reproduce it, with the value of the env var: + * + * npm: + * + * uninstalled: npx -y print-environment + * "npm/11.6.1 node/v24.10.0 linux arm64 workspaces/false" + * + * installed: npm init -y && npm i print-environment && npx print-environment + * "npm/11.6.1 node/v24.10.0 linux arm64 workspaces/false" + * + * + * pnpm: + * + * uninstalled: pnpm dlx print-environment + * "pnpm/10.18.3 npm/? node/v24.10.0 linux arm64" + * + * installed: pnpm init && pnpm add print-environment && pnpm print-environment + * "pnpm/10.18.3 npm/? node/v24.10.0 linux arm64" + * + * + * yarn classic: + * uninstalled: unsupported + * + * installed: yarn init -y && yarn add print-environment && yarn print-environment + * "yarn/1.22.22 npm/? node/v24.10.0 linux arm64" + * + * yarn berry: + * + * uninstalled: yarn set version berry && yarn dlx print-environment + * "yarn/4.10.3 npm/? node/v24.10.0 linux arm64" + * + * installed: yarn set version berry && yarn add print-environment && yarn print-environment + * "yarn/4.10.3 npm/? node/v24.10.0 linux arm64" + * + * bun: + * + * uninstalled: bunx print-environment + * "bun/1.3.1 npm/? node/v24.3.0 linux arm64" + * + * installed: bun init -y && bun add print-environment && bun print-environment + * "bun/1.3.1 npm/? node/v24.3.0 linux arm64" + * + * deno: + * + * uninstalled: deno run -A npm:print-environment + * "deno/2.5.6 npm/? deno/2.5.6 linux aarch64" + * + * installed: deno init && deno add npm:print-environment && deno --allow-env print-environment + * "deno/2.5.6 npm/? deno/2.5.6 linux aarch64" + * + * @returns The name of the package manager used to run hardhat. */ -export async function getPackageManager( - workspace: string, -): Promise { - const pathToYarnLock = path.join(workspace, "yarn.lock"); - const pathToPnpmLock = path.join(workspace, "pnpm-lock.yaml"); +export function getPackageManager(): PackageManager { + const DEFAULT = "npm"; - const invokedFromPnpm = (process.env.npm_config_user_agent ?? "").includes( - "pnpm", - ); + const userAgent = process.env.npm_config_user_agent; - if (await exists(pathToYarnLock)) { - return "yarn"; + if (userAgent === undefined) { + return DEFAULT; } - if ((await exists(pathToPnpmLock)) || invokedFromPnpm) { - return "pnpm"; + + const firstSlashIndex = userAgent.indexOf("/"); + if (firstSlashIndex === -1) { + return DEFAULT; + } + + const packageManager = userAgent.substring(0, firstSlashIndex); + + switch (packageManager) { + case "npm": + return "npm"; + case "yarn": + return "yarn"; + case "pnpm": + return "pnpm"; + case "bun": + return "bun"; + case "deno": + return "deno"; + default: + return DEFAULT; } - return "npm"; } /** @@ -49,11 +112,21 @@ export function getDevDependenciesInstallationCommand( npm: ["npm", "install", "--save-dev"], yarn: ["yarn", "add", "--dev"], pnpm: ["pnpm", "add", "--save-dev"], + deno: ["deno", "add"], + bun: ["bun", "add", "--dev"], }; const command = packageManagerToCommand[packageManager]; // We quote all the dependency identifiers so that they can be run on a shell // without semver symbols interfering with the command - command.push(...dependencies.map((d) => `"${d}"`)); + command.push( + ...dependencies.map((d) => { + if (packageManager === "deno") { + return `"npm:${d}"`; + } + + return `"${d}"`; + }), + ); return command; } @@ -118,6 +191,15 @@ export async function installsPeerDependenciesByDefault( } } return false; + case "bun": + // Bun has installed peer dependencies for over 2 years, so that's fine + // https://github.com/oven-sh/bun/releases/tag/bun-v1.0.5 + // This can be disabled, and there's no easy way to check that, so we + // assume true for now + return true; + case "deno": + // Deno doesn't autoinstall peers + return false; } } diff --git a/v-next/hardhat/test/internal/cli/init/package-manager.ts b/v-next/hardhat/test/internal/cli/init/package-manager.ts index eb24a9bca1a..89bebb8f0ca 100644 --- a/v-next/hardhat/test/internal/cli/init/package-manager.ts +++ b/v-next/hardhat/test/internal/cli/init/package-manager.ts @@ -1,9 +1,6 @@ import assert from "node:assert/strict"; import { afterEach, beforeEach, describe, it } from "node:test"; -import { useTmpDir } from "@nomicfoundation/hardhat-test-utils"; -import { writeUtf8File } from "@nomicfoundation/hardhat-utils/fs"; - import { getDevDependenciesInstallationCommand, getPackageManager, @@ -11,40 +8,52 @@ import { } from "../../../../src/internal/cli/init/package-manager.js"; describe("getPackageManager", () => { - useTmpDir("getPackageManager"); - let originalUserAgent: string | undefined; beforeEach(() => { originalUserAgent = process.env.npm_config_user_agent; - process.env.npm_config_user_agent = "npm"; // assume we are running npm by default }); afterEach(() => { process.env.npm_config_user_agent = originalUserAgent; }); - it("should return pnpm if pnpm-lock.yaml exists", async () => { - await writeUtf8File("pnpm-lock.yaml", ""); - assert.equal(await getPackageManager(process.cwd()), "pnpm"); - }); + const fixtureValues = [ + { + agentString: "npm/11.6.1 node/v24.10.0 linux arm64 workspaces/false", + expected: "npm", + }, + { + agentString: "pnpm/10.18.3 npm/? node/v24.10.0 linux arm64", + expected: "pnpm", + }, + { + agentString: "yarn/1.22.22 npm/? node/v24.10.0 linux arm64", + expected: "yarn", + }, + { + agentString: "yarn/4.10.3 npm/? node/v24.10.0 linux arm64", + expected: "yarn", + }, + { + agentString: "bun/1.3.1 npm/? node/v24.3.0 linux arm64", + expected: "bun", + }, + { + agentString: "deno/2.5.6 npm/? deno/2.5.6 linux aarch64", + expected: "deno", + }, + ]; - it("should return pnpm if invoked from pnpx", async () => { - assert.equal(await getPackageManager(process.cwd()), "npm"); - process.env.npm_config_user_agent = - "pnpm/10.4.1 npm/? node/v23.2.0 linux x64"; - assert.equal(await getPackageManager(process.cwd()), "pnpm"); - }); + it("Should work for all the fixture values", () => { + for (const fixture of fixtureValues) { + process.env.npm_config_user_agent = fixture.agentString; - it("should return npm if package-lock.json exists", async () => { - await writeUtf8File("package-lock.json", ""); - assert.equal(await getPackageManager(process.cwd()), "npm"); - }); - it("should return yarn if yarn.lock exists", async () => { - await writeUtf8File("yarn.lock", ""); - assert.equal(await getPackageManager(process.cwd()), "yarn"); - }); - it("should return npm if no lock file exists", async () => { - assert.equal(await getPackageManager(process.cwd()), "npm"); + assert.equal( + getPackageManager(), + fixture.expected, + "Incorrect package manager for " + fixture.expected, + ); + } }); }); @@ -150,6 +159,26 @@ describe("installsPeerDependenciesByDefault", () => { assert.equal(actual, false); }); }); + + describe("for bun", () => { + it("should always return true", async () => { + const actual = await installsPeerDependenciesByDefault( + process.cwd(), + "bun", + ); + assert.equal(actual, true); + }); + }); + + describe("for deno", () => { + it("should always return false", async () => { + const actual = await installsPeerDependenciesByDefault( + process.cwd(), + "deno", + ); + assert.equal(actual, false); + }); + }); }); describe("getDevDependenciesInstallationCommand", () => { @@ -157,12 +186,24 @@ describe("getDevDependenciesInstallationCommand", () => { const command = getDevDependenciesInstallationCommand("pnpm", ["a", "b"]); assert.equal(command.join(" "), 'pnpm add --save-dev "a" "b"'); }); + it("should return the correct command for npm", async () => { const command = getDevDependenciesInstallationCommand("npm", ["a", "b"]); assert.equal(command.join(" "), 'npm install --save-dev "a" "b"'); }); + it("should return the correct command for yarn", async () => { const command = getDevDependenciesInstallationCommand("yarn", ["a", "b"]); assert.equal(command.join(" "), 'yarn add --dev "a" "b"'); }); + + it("should return the correct command for bun", async () => { + const command = getDevDependenciesInstallationCommand("bun", ["a", "b"]); + assert.equal(command.join(" "), 'bun add --dev "a" "b"'); + }); + + it("should return the correct command for deno", async () => { + const command = getDevDependenciesInstallationCommand("deno", ["a", "b"]); + assert.equal(command.join(" "), 'deno add "npm:a" "npm:b"'); + }); });