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

[WIP] Project root resolution #5517

Merged
merged 11 commits into from
Jul 23, 2024
6 changes: 6 additions & 0 deletions v-next/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@ import { HardhatRuntimeEnvironmentImplementation } from "./internal/hre.js";
* @param config - The user's Hardhat configuration.
* @param userProvidedGlobalOptions - The global options provided by the
* user.
* @param projectRoot - The root of the Hardhat project. Hardhat expects this
* to be the root of the npm project containing your config file. If none is
* provided, it will use the root of the npm project that contains the CWD.
* @param unsafeOptions - Options used to bypass some initialization, to avoid
* redoing it in the CLI. Should only be used in the official CLI.
* @returns The Hardhat Runtime Environment.
*/
export async function createBaseHardhatRuntimeEnvironment(
config: HardhatUserConfig,
userProvidedGlobalOptions: Partial<GlobalOptions> = {},
projectRoot?: string,
unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions,
): Promise<HardhatRuntimeEnvironment> {
return HardhatRuntimeEnvironmentImplementation.create(
config,
userProvidedGlobalOptions,
projectRoot,
unsafeOptions,
);
}

export { resolveProjectRoot } from "./internal/hre.js";
export { parseArgumentValue } from "./internal/arguments.js";
export { resolvePluginList } from "./internal/plugins/resolve-plugin-list.js";
export { buildGlobalOptionDefinitions } from "./internal/global-options.js";
26 changes: 19 additions & 7 deletions v-next/core/src/internal/hook-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
assertHardhatInvariant,
} from "@ignored/hardhat-vnext-errors";

import { detectPluginNpmDependencyProblems } from "./plugins/detect-plugin-npm-dependency-problems.js";

export class HookManagerImplementation implements HookManager {
readonly #projectRoot: string;

readonly #pluginsInReverseOrder: HardhatPlugin[];

/**
Expand All @@ -44,7 +48,8 @@ export class HookManagerImplementation implements HookManager {
Array<Partial<HardhatHooks[keyof HardhatHooks]>>
> = new Map();

constructor(plugins: HardhatPlugin[]) {
constructor(projectRoot: string, plugins: HardhatPlugin[]) {
this.#projectRoot = projectRoot;
this.#pluginsInReverseOrder = plugins.toReversed();
}

Expand Down Expand Up @@ -252,7 +257,7 @@ export class HookManagerImplementation implements HookManager {

if (typeof hookHandlerCategoryFactory === "string") {
hookCategory = await this.#loadHookCategoryFactory(
plugin.id,
plugin,
hookCategoryName,
hookHandlerCategoryFactory,
);
Expand Down Expand Up @@ -288,35 +293,42 @@ export class HookManagerImplementation implements HookManager {
}

async #loadHookCategoryFactory<HookCategoryNameT extends keyof HardhatHooks>(
pluginId: string,
plugin: HardhatPlugin,
hookCategoryName: HookCategoryNameT,
path: string,
): Promise<Partial<HardhatHooks[HookCategoryNameT]>> {
if (!path.startsWith("file://")) {
throw new HardhatError(
HardhatError.ERRORS.HOOKS.INVALID_HOOK_FACTORY_PATH,
{
pluginId,
pluginId: plugin.id,
hookCategoryName,
path,
},
);
}

const mod = await import(path);
let mod;

try {
mod = await import(path);
} catch (error) {
await detectPluginNpmDependencyProblems(plugin, this.#projectRoot);
throw error;
}

const factory = mod.default;

assertHardhatInvariant(
typeof factory === "function",
`Plugin ${pluginId} doesn't export a hook factory for category ${hookCategoryName} in ${path}`,
`Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName} in ${path}`,
);

const category = await factory();

assertHardhatInvariant(
category !== null && typeof category === "object",
`Plugin ${pluginId} doesn't export a valid factory for category ${hookCategoryName} in ${path}, it didn't return an object`,
`Plugin ${plugin.id} doesn't export a valid factory for category ${hookCategoryName} in ${path}, it didn't return an object`,
);

return category;
Expand Down
49 changes: 35 additions & 14 deletions v-next/core/src/internal/hre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { TaskManager } from "../types/tasks.js";
import type { UserInterruptionManager } from "../types/user-interruptions.js";

import { HardhatError } from "@ignored/hardhat-vnext-errors";
import { findClosestPackageRoot } from "@ignored/hardhat-vnext-utils/package";

import { ResolvedConfigurationVariableImplementation } from "./configuration-variables.js";
import {
Expand All @@ -32,28 +33,24 @@ export class HardhatRuntimeEnvironmentImplementation
public static async create(
inputUserConfig: HardhatUserConfig,
userProvidedGlobalOptions: Partial<GlobalOptions>,
projectRoot?: string,
unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions,
): Promise<HardhatRuntimeEnvironmentImplementation> {
// TODO: Clone with lodash or https://github.com/davidmarkclements/rfdc
// TODO: Or maybe don't clone at all
const clonedUserConfig = inputUserConfig;

// Resolve plugins from node modules relative to the current working directory
const basePathForNpmResolution = process.cwd();
const resolvedProjectRoot = await resolveProjectRoot(projectRoot);

const resolvedPlugins =
unsafeOptions?.resolvedPlugins ??
(await resolvePluginList(
clonedUserConfig.plugins,
basePathForNpmResolution,
));
(await resolvePluginList(resolvedProjectRoot, inputUserConfig.plugins));

const hooks = new HookManagerImplementation(resolvedPlugins);
const hooks = new HookManagerImplementation(
resolvedProjectRoot,
resolvedPlugins,
);

// extend user config:
const extendedUserConfig = await runUserConfigExtensions(
hooks,
clonedUserConfig,
inputUserConfig,
);

// validate config
Expand All @@ -76,14 +73,20 @@ export class HardhatRuntimeEnvironmentImplementation
// Resolve config

const resolvedConfig = await resolveUserConfig(
resolvedProjectRoot,
hooks,
resolvedPlugins,
inputUserConfig,
);

// We override the plugins, as we want to prevent plugins from changing this
const config = {
// We override the plugins and the proejct root, as we want to prevent
// the plugins from changing them
const config: HardhatConfig = {
...resolvedConfig,
paths: {
...resolvedConfig.paths,
root: resolvedProjectRoot,
},
plugins: resolvedPlugins,
};

Expand Down Expand Up @@ -138,6 +141,20 @@ export class HardhatRuntimeEnvironmentImplementation
}
}

/**
* Resolves the project root of a Hardhat project based on the config file or
* another path within the project. If not provided, it will be resolved from
* the current working directory.
*
* @param absolutePathWithinProject An absolute path within the project, usually
* the config file.
*/
export async function resolveProjectRoot(
absolutePathWithinProject: string | undefined,
): Promise<string> {
return findClosestPackageRoot(absolutePathWithinProject ?? process.cwd());
}

async function runUserConfigExtensions(
hooks: HookManager,
config: HardhatUserConfig,
Expand Down Expand Up @@ -169,13 +186,17 @@ async function validateUserConfig(
}

async function resolveUserConfig(
projectRoot: string,
hooks: HookManager,
sortedPlugins: HardhatPlugin[],
config: HardhatUserConfig,
): Promise<HardhatConfig> {
const initialResolvedConfig: HardhatConfig = {
plugins: sortedPlugins,
tasks: config.tasks ?? [],
paths: {
root: projectRoot,
},
};

return hooks.runHandlerChain(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import semver from "semver";
*/
export async function detectPluginNpmDependencyProblems(
plugin: HardhatPlugin,
basePathForNpmResolution: string = process.cwd(),
basePathForNpmResolution: string,
): Promise<void> {
if (plugin.npmPackage === undefined) {
return;
Expand Down
18 changes: 7 additions & 11 deletions v-next/core/src/internal/plugins/resolve-plugin-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { detectPluginNpmDependencyProblems } from "./detect-plugin-npm-dependenc
* Resolves the plugin list, returning them in the right order.
*/
export async function resolvePluginList(
projectRoot: string,
userConfigPluginList: HardhatPlugin[] = [],
basePathForNpmResolution: string,
): Promise<HardhatPlugin[]> {
return reverseTopologicalSort(userConfigPluginList, basePathForNpmResolution);
return reverseTopologicalSort(projectRoot, userConfigPluginList);
}

/**
Expand All @@ -24,8 +24,8 @@ export async function resolvePluginList(
* @returns The ordered plugins.
*/
async function reverseTopologicalSort(
projectRoot: string,
plugins: HardhatPlugin[],
basePathForNpmResolution: string,
): Promise<HardhatPlugin[]> {
const visitedPlugins: Map<string, HardhatPlugin> = new Map();
const result: HardhatPlugin[] = [];
Expand All @@ -48,11 +48,7 @@ async function reverseTopologicalSort(

if (plugin.dependencies !== undefined) {
for (const loadFn of plugin.dependencies) {
const dependency = await loadDependency(
plugin,
loadFn,
basePathForNpmResolution,
);
const dependency = await loadDependency(projectRoot, plugin, loadFn);

await dfs(dependency);
}
Expand All @@ -72,22 +68,22 @@ async function reverseTopologicalSort(
* Attempt to load a plugins dependency. If there is an error,
* first try and validate the npm dependencies of the plugin.
*
* @param projectRoot - The root of the Hardhat project.
* @param plugin - the plugin has the dependency
* @param loadFn - the load function for the dependency
* @param basePathForNpmResolution - the directory path to use for node module resolution
* @returns the loaded plugin
*/
async function loadDependency(
projectRoot: string,
plugin: HardhatPlugin,
loadFn: () => Promise<HardhatPlugin>,
basePathForNpmResolution: string,
): Promise<HardhatPlugin> {
try {
return await loadFn();
} catch (error) {
ensureError(error);

await detectPluginNpmDependencyProblems(plugin, basePathForNpmResolution);
await detectPluginNpmDependencyProblems(plugin, projectRoot);

throw new HardhatError(
HardhatError.ERRORS.PLUGINS.PLUGIN_DEPENDENCY_FAILED_LOAD,
Expand Down
5 changes: 4 additions & 1 deletion v-next/core/src/internal/tasks/resolved-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,10 @@ export class ResolvedTask implements Task {
`Plugin with id ${actionPluginId} not found.`,
);

await detectPluginNpmDependencyProblems(plugin);
await detectPluginNpmDependencyProblems(
plugin,
this.#hre.config.paths.root,
);
}

throw new HardhatError(
Expand Down
20 changes: 16 additions & 4 deletions v-next/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,23 @@ export type SensitiveString = string | ConfigurationVariable;
* The user's Hardhat configuration, as exported in their
* config file.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- Used through module aumentation
export interface HardhatUserConfig {}
export interface HardhatUserConfig {
paths?: ProjectPathsUserConfig;
}

/**
* The different paths that conform a Hardhat project.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- TODO: add the paths
export interface ProjectPathsUserConfig {}

/**
* The resolved Hardhat configuration.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- Used through module aumentation
export interface HardhatConfig {}
export interface HardhatConfig {
paths: ProjectPathsConfig;
}

export interface ProjectPathsConfig {
root: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { before, describe, it } from "node:test";
import { HardhatError } from "@ignored/hardhat-vnext-errors";
import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils";

import { resolveProjectRoot } from "../../../src/index.js";
import { ResolvedConfigurationVariableImplementation } from "../../../src/internal/configuration-variables.js";
import { HookManagerImplementation } from "../../../src/internal/hook-manager.js";
import { UserInterruptionManagerImplementation } from "../../../src/internal/user-interruptions.js";

describe("ResolvedConfigurationVariable", () => {
let hookManager: HookManagerImplementation;

before(() => {
hookManager = new HookManagerImplementation([]);
before(async () => {
const projectRoot = await resolveProjectRoot(process.cwd());

hookManager = new HookManagerImplementation(projectRoot, []);
const userInterruptionsManager = new UserInterruptionManagerImplementation(
hookManager,
);
Expand All @@ -21,6 +24,9 @@ describe("ResolvedConfigurationVariable", () => {
config: {
tasks: [],
plugins: [],
paths: {
root: projectRoot,
},
},
hooks: hookManager,
globalOptions: {},
Expand Down
Loading