Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

address the follow-up tasks related to --init #5959

Open
wants to merge 4 commits into
base: v-next
Choose a base branch
from
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
115 changes: 87 additions & 28 deletions v-next/hardhat/src/internal/cli/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import {
} from "@ignored/hardhat-vnext-utils/fs";
import { resolveFromRoot } from "@ignored/hardhat-vnext-utils/path";
import chalk from "chalk";
import * as semver from "semver";

import { findClosestHardhatConfig } from "../../config-loading.js";
import { getHardhatVersion } from "../../utils/package.js";
import { ensureTelemetryConsent } from "../telemetry/telemetry-permissions.js";

import { HARDHAT_NAME } from "./constants.js";
import {
Expand All @@ -29,6 +31,7 @@ import {
promptForForce,
promptForInstall,
promptForTemplate,
promptForUpdate,
promptForWorkspace,
} from "./prompt.js";
import { spawn } from "./subprocess.js";
Expand Down Expand Up @@ -59,23 +62,29 @@ export interface InitHardhatOptions {
* The flow is as follows:
* 1. Print the ascii logo.
* 2. Print the welcome message.
* 3. Optionally, ask the user for the workspace to initialize the project in.
* 4. Optionally, ask the user for the template to use for the project initialization.
* 5. Create the package.json file if it does not exist.
* 6. Validate that the package.json file is an esm package.
* 7. Optionally, ask the user if files should be overwritten.
* 8. Copy the template files to the workspace.
* 3. Ensure telemetry consent.
* 4. Optionally, ask the user for the workspace to initialize the project in.
* 5. Optionally, ask the user for the template to use for the project initialization.
* 6. Create the package.json file if it does not exist.
* 7. Validate that the package.json file is an esm package.
* 8. Optionally, ask the user if files should be overwritten.
* 9. Copy the template files to the workspace.
* 10. Print the commands to install the project dependencies.
* 11. Optionally, ask the user if the project dependencies should be installed.
* 12. Optionally, run the commands to install the project dependencies.
* 13. Print a message to star the project on GitHub.
* 13. Ensure telemetry consent.
* 14. Print a message to star the project on GitHub.
*/
export async function initHardhat(options?: InitHardhatOptions): Promise<void> {
try {
printAsciiLogo();

await printWelcomeMessage();

// Ensure telemetry consent first so that we are allowed to also track
// the unfinished init flows
await ensureTelemetryConsent();

// Ask the user for the workspace to initialize the project in
// if it was not provided, and validate that it is not already initialized
const workspace = await getWorkspace(options?.workspace);
Expand Down Expand Up @@ -315,11 +324,13 @@ export async function copyProjectFiles(
* @param workspace The path to the workspace to initialize the project in.
* @param template The template to use for the project initialization.
* @param install Whether to install the project dependencies.
* @param update Whether to update the project dependencies.
*/
export async function installProjectDependencies(
workspace: string,
template: Template,
install?: boolean,
update?: boolean,
): Promise<void> {
const pathToWorkspacePackageJson = path.join(workspace, "package.json");

Expand All @@ -340,42 +351,42 @@ export async function installProjectDependencies(
templateDependencies[name] = version;
}
}
const workspaceDependencies = workspacePkg.devDependencies ?? {};
const dependenciesToInstall = Object.entries(templateDependencies)

// Checking both workspace dependencies and dev dependencies in case the user
// installed a dev dependency as a dependency
const workspaceDependencies = {
...(workspacePkg.dependencies ?? {}),
...(workspacePkg.devDependencies ?? {}),
};

// We need to strip the optional workspace prefix from template dependency versions
const templateDependencyEntries = Object.entries(templateDependencies).map(
([name, version]) => [name, version.replace(/^workspace:/, "")],
);

// Finding the dependencies that are not already installed
const dependenciesToInstall = templateDependencyEntries
.filter(([name]) => workspaceDependencies[name] === undefined)
.map(([name, version]) => {
// Strip the workspace: prefix from the version
return `${name}@${version.replace(/^workspace:/, "")}`;
});
.map(([name, version]) => `${name}@${version}`);

// Try to install the missing dependencies if there are any
if (Object.keys(dependenciesToInstall).length !== 0) {
// Retrieve the package manager specific installation command
let command = getDevDependenciesInstallationCommand(
const command = getDevDependenciesInstallationCommand(
packageManager,
dependenciesToInstall,
);
const commandString = command.join(" ");

// We quote all the dependency identifiers to that it can be run on a shell
// without semver symbols interfering with the command
command = [
command[0],
command[1],
command[2],
...command.slice(3).map((arg) => `"${arg}"`),
];

const formattedCommand = command.join(" ");

// Ask the user for permission to install the project dependencies and install them if needed
// Ask the user for permission to install the project dependencies
if (install === undefined) {
install = await promptForInstall(formattedCommand);
install = await promptForInstall(commandString);
}

// If the user grants permission to install the dependencies, run the installation command
if (install) {
console.log();
console.log(formattedCommand);
console.log(commandString);

await spawn(command[0], command.slice(1), {
cwd: workspace,
Expand All @@ -389,6 +400,54 @@ export async function installProjectDependencies(
console.log(`✨ ${chalk.cyan(`Dependencies installed`)} ✨`);
}
}

// NOTE: Even though the dependency updates are very similar to pure
// installations, they are kept separate to allow the user to skip one while
// proceeding with the other, and to allow us to design handling of these
// two processes independently.

// Finding the installed dependencies that have an incompatible version
const dependenciesToUpdate = templateDependencyEntries
.filter(([name, version]) => {
const workspaceVersion = workspaceDependencies[name];
return (
workspaceVersion !== undefined &&
!semver.satisfies(version, workspaceVersion) &&
!semver.intersects(version, workspaceVersion)
);
})
.map(([name, version]) => `${name}@${version}`);

// Try to update the missing dependencies if there are any.
if (dependenciesToUpdate.length !== 0) {
// Retrieve the package manager specific installation command
const command = getDevDependenciesInstallationCommand(
packageManager,
dependenciesToUpdate,
);
const commandString = command.join(" ");

// Ask the user for permission to update the project dependencies
if (update === undefined) {
update = await promptForUpdate(commandString);
}

if (update) {
console.log();
console.log(commandString);

await spawn(command[0], command.slice(1), {
cwd: workspace,
// We need to run with `shell: true` for this to work on powershell, but
// we already enclosed every dependency identifier in quotes, so this
// is safe.
shell: true,
stdio: "inherit",
});

console.log(`✨ ${chalk.cyan(`Dependencies updated`)} ✨`);
}
}
}

function showStarOnGitHubMessage() {
Expand Down
7 changes: 5 additions & 2 deletions v-next/hardhat/src/internal/cli/init/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export async function getPackageManager(

/**
* getDevDependenciesInstallationCommand returns the command to install the given dependencies
* as dev dependencies using the given package manager.
* as dev dependencies using the given package manager. The returned command should
* be safe to run on the command line.
*
* @param packageManager The package manager to use.
* @param dependencies The dependencies to install.
Expand All @@ -46,7 +47,9 @@ export function getDevDependenciesInstallationCommand(
pnpm: ["pnpm", "add", "--save-dev"],
};
const command = packageManagerToCommand[packageManager];
command.push(...dependencies);
// 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}"`));
return command;
}

Expand Down
22 changes: 21 additions & 1 deletion v-next/hardhat/src/internal/cli/init/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Template } from "./template.js";

import { HardhatError } from "@ignored/hardhat-vnext-errors";
import chalk from "chalk";

export async function promptForWorkspace(): Promise<string> {
ensureTTY();
Expand Down Expand Up @@ -71,14 +72,33 @@ export async function promptForInstall(
{
name: "install",
type: "confirm",
message: `You need to install the project dependencies using the following command:\n${safelyFormattedCommand}\n\nDo you want to run it now?`,
message: `You need to install the following dependencies using the following command:\n${chalk.italic(safelyFormattedCommand)}\n\nDo you want to run it now?`,
initial: true,
},
]);

return installResponse.install;
}

export async function promptForUpdate(
safelyFormattedCommand: string,
): Promise<boolean> {
ensureTTY();

const { default: enquirer } = await import("enquirer");

const updateResponse = await enquirer.prompt<{ update: boolean }>([
{
name: "update",
type: "confirm",
message: `You need to update the following dependencies using the following command:\n${chalk.italic(safelyFormattedCommand)}\n\nDo you want to run it now?`,
initial: true,
},
]);

return updateResponse.update;
}

/**
* ensureTTY checks if the process is running in a TTY (i.e. a terminal).
* If it is not, it throws and error.
Expand Down
Loading