From 7c8b8839332ae62ef95b0f7aaefae2ee74a2d40f Mon Sep 17 00:00:00 2001 From: Armando Andini Date: Thu, 27 Feb 2025 15:17:49 -0300 Subject: [PATCH 1/4] Fix: bug in semver check on installProjectDependencies --- v-next/hardhat/src/internal/cli/init/init.ts | 4 +- v-next/hardhat/test/internal/cli/init/init.ts | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/v-next/hardhat/src/internal/cli/init/init.ts b/v-next/hardhat/src/internal/cli/init/init.ts index 72a5544241..6ca875c3a4 100644 --- a/v-next/hardhat/src/internal/cli/init/init.ts +++ b/v-next/hardhat/src/internal/cli/init/init.ts @@ -484,8 +484,8 @@ export async function installProjectDependencies( const workspaceVersion = workspaceDependencies[name]; return ( workspaceVersion !== undefined && - !semver.satisfies(version, workspaceVersion) && - !semver.intersects(version, workspaceVersion) + !semver.satisfies(workspaceVersion, version) && + !semver.intersects(workspaceVersion, version) ); }) .map(([name, version]) => `${name}@${version}`); diff --git a/v-next/hardhat/test/internal/cli/init/init.ts b/v-next/hardhat/test/internal/cli/init/init.ts index 212dabfb4c..5a57ea5939 100644 --- a/v-next/hardhat/test/internal/cli/init/init.ts +++ b/v-next/hardhat/test/internal/cli/init/init.ts @@ -1,3 +1,4 @@ +import type { Template } from "../../../../src/internal/cli/init/template.js"; import type { PackageJson } from "@nomicfoundation/hardhat-utils/package"; import assert from "node:assert/strict"; @@ -311,6 +312,72 @@ describe("installProjectDependencies", async () => { } }, ); + + it( + "should not update dependencies if they are up-to-date and the user opts-in to the update (specific version)", + { + skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true", + }, + async () => { + const template: Template = { + name: "test", + packageJson: { + name: "test", + version: "0.0.1", + devDependencies: { hardhat: "^3.0.0-next.0" }, // <-- required version + }, + path: process.cwd(), + files: [], + }; + + await writeUtf8File( + "package.json", + JSON.stringify({ + type: "module", + devDependencies: { hardhat: "3.0.0-next.0" }, // <-- specific version + }), + ); + await installProjectDependencies(process.cwd(), template, false, true); + + assert.ok( + !(await exists("node_modules")), + "no modules should have been installed", + ); + }, + ); + + it( + "should not update dependencies if they are up-to-date and the user opts-in to the update (version range)", + { + skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true", + }, + async () => { + const template: Template = { + name: "test", + packageJson: { + name: "test", + version: "0.0.1", + devDependencies: { hardhat: "^3.0.0-next.0" }, // <-- required version + }, + path: process.cwd(), + files: [], + }; + + await writeUtf8File( + "package.json", + JSON.stringify({ + type: "module", + devDependencies: { hardhat: ">= 3.0.0-next.0" }, // <-- version range + }), + ); + await installProjectDependencies(process.cwd(), template, false, true); + + assert.ok( + !(await exists("node_modules")), + "no modules should have been installed", + ); + }, + ); }); describe("initHardhat", async () => { From 2e1422f3f46f192acb01e1a22c9395975cebdcd5 Mon Sep 17 00:00:00 2001 From: John Kane Date: Tue, 4 Mar 2025 14:21:13 +0000 Subject: [PATCH 2/4] test: tweak test example Use an example that clarifies the reason for the test, unconnected with any implication around the hardhat package. --- v-next/hardhat/test/internal/cli/init/init.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v-next/hardhat/test/internal/cli/init/init.ts b/v-next/hardhat/test/internal/cli/init/init.ts index 5a57ea5939..a5b52d0b6b 100644 --- a/v-next/hardhat/test/internal/cli/init/init.ts +++ b/v-next/hardhat/test/internal/cli/init/init.ts @@ -324,7 +324,7 @@ describe("installProjectDependencies", async () => { packageJson: { name: "test", version: "0.0.1", - devDependencies: { hardhat: "^3.0.0-next.0" }, // <-- required version + devDependencies: { "fake-dependency": "^1.2.3" }, // <-- required version }, path: process.cwd(), files: [], @@ -334,7 +334,7 @@ describe("installProjectDependencies", async () => { "package.json", JSON.stringify({ type: "module", - devDependencies: { hardhat: "3.0.0-next.0" }, // <-- specific version + devDependencies: { "fake-dependency": "1.2.3" }, // <-- specific version }), ); await installProjectDependencies(process.cwd(), template, false, true); @@ -357,7 +357,7 @@ describe("installProjectDependencies", async () => { packageJson: { name: "test", version: "0.0.1", - devDependencies: { hardhat: "^3.0.0-next.0" }, // <-- required version + devDependencies: { "fake-dependency": "^1.2.3" }, // <-- required version }, path: process.cwd(), files: [], @@ -367,7 +367,7 @@ describe("installProjectDependencies", async () => { "package.json", JSON.stringify({ type: "module", - devDependencies: { hardhat: ">= 3.0.0-next.0" }, // <-- version range + devDependencies: { "fake-dependency": ">= 1.2.3" }, // <-- version range }), ); await installProjectDependencies(process.cwd(), template, false, true); From e5d4453d36de64a657ddd9251b7142f85a24bd45 Mon Sep 17 00:00:00 2001 From: John Kane Date: Tue, 4 Mar 2025 14:24:56 +0000 Subject: [PATCH 3/4] chore: add a changeset for the fix --- .changeset/cool-waves-wonder.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cool-waves-wonder.md diff --git a/.changeset/cool-waves-wonder.md b/.changeset/cool-waves-wonder.md new file mode 100644 index 0000000000..7b1bfbfba5 --- /dev/null +++ b/.changeset/cool-waves-wonder.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Fix unnecessary re-install of hardhat during init ([#6323](https://github.com/NomicFoundation/hardhat/issues/6323)) From 83c0156049f23c50afc746394ae8898d48ea14bf Mon Sep 17 00:00:00 2001 From: Piotr Galar Date: Tue, 4 Mar 2025 17:03:46 +0100 Subject: [PATCH 4/4] fix: test the shouldUpdateDependency helper thoroughly (#6446) --- v-next/hardhat/src/internal/cli/init/init.ts | 46 ++++- v-next/hardhat/test/internal/cli/init/init.ts | 168 +++++++++++++++++- 2 files changed, 204 insertions(+), 10 deletions(-) diff --git a/v-next/hardhat/src/internal/cli/init/init.ts b/v-next/hardhat/src/internal/cli/init/init.ts index 6ca875c3a4..a333a6a505 100644 --- a/v-next/hardhat/src/internal/cli/init/init.ts +++ b/v-next/hardhat/src/internal/cli/init/init.ts @@ -3,7 +3,10 @@ import type { PackageJson } from "@nomicfoundation/hardhat-utils/package"; import path from "node:path"; -import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { + assertHardhatInvariant, + HardhatError, +} from "@nomicfoundation/hardhat-errors"; import { copy, ensureDir, @@ -480,13 +483,9 @@ export async function installProjectDependencies( // Finding the installed dependencies that have an incompatible version const dependenciesToUpdate = templateDependencyEntries - .filter(([name, version]) => { - const workspaceVersion = workspaceDependencies[name]; - return ( - workspaceVersion !== undefined && - !semver.satisfies(workspaceVersion, version) && - !semver.intersects(workspaceVersion, version) - ); + .filter(([dependencyName, templateVersion]) => { + const workspaceVersion = workspaceDependencies[dependencyName]; + return shouldUpdateDependency(workspaceVersion, templateVersion); }) .map(([name, version]) => `${name}@${version}`); @@ -529,3 +528,34 @@ function showStarOnGitHubMessage() { console.log(); console.log(chalk.cyan(" https://github.com/NomicFoundation/hardhat")); } + +// NOTE: This function is exported for testing purposes only. +export function shouldUpdateDependency( + workspaceVersion: string | undefined, + templateVersion: string, +): boolean { + // We should not update the dependency if it is not yet installed in the workspace. + if (workspaceVersion === undefined) { + return false; + } + // NOTE: a specific version also a valid range that includes itself only + const workspaceRange = semver.validRange(workspaceVersion, { + includePrerelease: true, + }); + const templateRange = semver.validRange(templateVersion, { + includePrerelease: true, + }); + assertHardhatInvariant( + templateRange !== null, + "All dependencies of the template should have valid versions", + ); + // We should update the dependency if the workspace version could not be parsed. + if (workspaceRange === null) { + return true; + } + // We should update the dependency if the workspace range (or, in particular, a specific version) is not + // a strict subset of the template range/does not equal the template version. + return !semver.subset(workspaceRange, templateRange, { + includePrerelease: true, + }); +} diff --git a/v-next/hardhat/test/internal/cli/init/init.ts b/v-next/hardhat/test/internal/cli/init/init.ts index a5b52d0b6b..5275f3ed15 100644 --- a/v-next/hardhat/test/internal/cli/init/init.ts +++ b/v-next/hardhat/test/internal/cli/init/init.ts @@ -29,6 +29,7 @@ import { printWelcomeMessage, relativeTemplateToWorkspacePath, relativeWorkspaceToTemplatePath, + shouldUpdateDependency, } from "../../../../src/internal/cli/init/init.js"; import { getTemplates } from "../../../../src/internal/cli/init/template.js"; @@ -357,7 +358,7 @@ describe("installProjectDependencies", async () => { packageJson: { name: "test", version: "0.0.1", - devDependencies: { "fake-dependency": "^1.2.3" }, // <-- required version + devDependencies: { "fake-dependency": ">=1.2.3" }, // <-- required version }, path: process.cwd(), files: [], @@ -367,7 +368,7 @@ describe("installProjectDependencies", async () => { "package.json", JSON.stringify({ type: "module", - devDependencies: { "fake-dependency": ">= 1.2.3" }, // <-- version range + devDependencies: { "fake-dependency": "^1.2.3" }, // <-- version range }), ); await installProjectDependencies(process.cwd(), template, false, true); @@ -414,3 +415,166 @@ describe("initHardhat", async () => { ); } }); + +describe("shouldUpdateDependency", () => { + const testCases = [ + { + workspaceVersion: "1.0.0", + templateVersion: "1.0.0", + expectedResult: false, + }, + { + workspaceVersion: "1.0.0", + templateVersion: "1.2.3", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3", + templateVersion: "1.0.0", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3", + templateVersion: "^1.2.3", + expectedResult: false, + }, + { + workspaceVersion: "^1.2.3", + templateVersion: "1.2.3", + expectedResult: true, + }, + { + workspaceVersion: ">= 1.2.3", + templateVersion: "^1.2.3", + expectedResult: true, + }, + { + workspaceVersion: "^1.2.3", + templateVersion: ">= 1.2.3", + expectedResult: false, + }, + { + workspaceVersion: "1.0.0-dev", + templateVersion: "1.0.0-dev", + expectedResult: false, + }, + { + workspaceVersion: "1.0.0-dev", + templateVersion: "1.2.3-dev", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3-dev", + templateVersion: "1.0.0-dev", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3-dev", + templateVersion: "^1.2.3-dev", + expectedResult: false, + }, + { + workspaceVersion: "^1.2.3-dev", + templateVersion: "1.2.3-dev", + expectedResult: true, + }, + { + workspaceVersion: ">= 1.2.3-dev", + templateVersion: "^1.2.3-dev", + expectedResult: true, + }, + { + workspaceVersion: "^1.2.3-dev", + templateVersion: ">= 1.2.3-dev", + expectedResult: false, + }, + { + workspaceVersion: "1.0.0", + templateVersion: "1.0.0-dev", + expectedResult: true, + }, + { + workspaceVersion: "1.0.0-dev", + templateVersion: "1.0.0", + expectedResult: true, + }, + { + workspaceVersion: "1.0.0-dev", + templateVersion: "1.2.3", + expectedResult: true, + }, + { + workspaceVersion: "1.0.0", + templateVersion: "1.2.3-dev", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3", + templateVersion: "1.0.0-dev", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3-dev", + templateVersion: "1.0.0", + expectedResult: true, + }, + { + workspaceVersion: "1.2.3", + templateVersion: "^1.2.3-dev", + expectedResult: false, + }, + { + workspaceVersion: "1.2.3-dev", + templateVersion: "^1.2.3", + expectedResult: true, + }, + { + workspaceVersion: "^1.2.3", + templateVersion: "1.2.3-dev", + expectedResult: true, + }, + { + workspaceVersion: "^1.2.3-dev", + templateVersion: "1.2.3", + expectedResult: true, + }, + { + workspaceVersion: ">= 1.2.3", + templateVersion: "^1.2.3-dev", + expectedResult: true, + }, + { + workspaceVersion: ">= 1.2.3-dev", + templateVersion: "^1.2.3", + expectedResult: true, + }, + { + workspaceVersion: "^1.2.3", + templateVersion: ">= 1.2.3-dev", + expectedResult: false, + }, + { + workspaceVersion: "^1.2.3-dev", + templateVersion: ">= 1.2.3", + expectedResult: true, + }, + { + workspaceVersion: "3.0.0-next.0", + templateVersion: "^3.0.0-next.0", + expectedResult: false, + }, + ]; + + for (const { + workspaceVersion, + templateVersion, + expectedResult, + } of testCases) { + it(`should return ${expectedResult} when workspace version is ${workspaceVersion} and template version is ${templateVersion}`, () => { + assert.equal( + shouldUpdateDependency(workspaceVersion, templateVersion), + expectedResult, + ); + }); + } +});