diff --git a/v-next/core/src/index.ts b/v-next/core/src/index.ts index d81b73ceee..48e0b0bc9a 100644 --- a/v-next/core/src/index.ts +++ b/v-next/core/src/index.ts @@ -15,6 +15,9 @@ 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. @@ -22,15 +25,18 @@ import { HardhatRuntimeEnvironmentImplementation } from "./internal/hre.js"; export async function createBaseHardhatRuntimeEnvironment( config: HardhatUserConfig, userProvidedGlobalOptions: Partial = {}, + projectRoot?: string, unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions, ): Promise { 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"; diff --git a/v-next/core/src/internal/hook-manager.ts b/v-next/core/src/internal/hook-manager.ts index d29c3ff231..c4997ec995 100644 --- a/v-next/core/src/internal/hook-manager.ts +++ b/v-next/core/src/internal/hook-manager.ts @@ -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[]; /** @@ -44,7 +48,8 @@ export class HookManagerImplementation implements HookManager { Array> > = new Map(); - constructor(plugins: HardhatPlugin[]) { + constructor(projectRoot: string, plugins: HardhatPlugin[]) { + this.#projectRoot = projectRoot; this.#pluginsInReverseOrder = plugins.toReversed(); } @@ -252,7 +257,7 @@ export class HookManagerImplementation implements HookManager { if (typeof hookHandlerCategoryFactory === "string") { hookCategory = await this.#loadHookCategoryFactory( - plugin.id, + plugin, hookCategoryName, hookHandlerCategoryFactory, ); @@ -288,7 +293,7 @@ export class HookManagerImplementation implements HookManager { } async #loadHookCategoryFactory( - pluginId: string, + plugin: HardhatPlugin, hookCategoryName: HookCategoryNameT, path: string, ): Promise> { @@ -296,27 +301,34 @@ export class HookManagerImplementation implements HookManager { 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(this.#projectRoot, plugin); + 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; diff --git a/v-next/core/src/internal/hre.ts b/v-next/core/src/internal/hre.ts index 399aae6ba4..b6ae2c0193 100644 --- a/v-next/core/src/internal/hre.ts +++ b/v-next/core/src/internal/hre.ts @@ -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 { @@ -32,28 +33,24 @@ export class HardhatRuntimeEnvironmentImplementation public static async create( inputUserConfig: HardhatUserConfig, userProvidedGlobalOptions: Partial, + projectRoot?: string, unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions, ): Promise { - // 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 @@ -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, }; @@ -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 { + return findClosestPackageRoot(absolutePathWithinProject ?? process.cwd()); +} + async function runUserConfigExtensions( hooks: HookManager, config: HardhatUserConfig, @@ -169,6 +186,7 @@ async function validateUserConfig( } async function resolveUserConfig( + projectRoot: string, hooks: HookManager, sortedPlugins: HardhatPlugin[], config: HardhatUserConfig, @@ -176,6 +194,9 @@ async function resolveUserConfig( const initialResolvedConfig: HardhatConfig = { plugins: sortedPlugins, tasks: config.tasks ?? [], + paths: { + root: projectRoot, + }, }; return hooks.runHandlerChain( diff --git a/v-next/core/src/internal/plugins/detect-plugin-npm-dependency-problems.ts b/v-next/core/src/internal/plugins/detect-plugin-npm-dependency-problems.ts index 01d07fd45e..366428e041 100644 --- a/v-next/core/src/internal/plugins/detect-plugin-npm-dependency-problems.ts +++ b/v-next/core/src/internal/plugins/detect-plugin-npm-dependency-problems.ts @@ -8,26 +8,30 @@ import { HardhatError } from "@ignored/hardhat-vnext-errors"; import semver from "semver"; /** - * Validate that a plugin is installed and that its peer dependencies are installed and satisfy the version constraints. + * Validate that a plugin is installed and that its peer dependencies are + * installed and satisfy the version constraints. * - * @param plugin - the plugin to be validated - * @param basePathForNpmResolution - the directory path to use for node module resolution, defaulting to `process.cwd()` + * @param basePathForNpmResolution the dir path for node module resolution + * @param plugin the plugin to be validated * @throws {HardhatError} with descriptor: - * - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_NOT_INSTALLED} if the plugin is not installed as an npm package - * - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_MISSING_DEPENDENCY} if the plugin's package peer dependency is not installed - * - {@link HardhatError.ERRORS.PLUGINS.DEPENDENCY_VERSION_MISMATCH} if the plugin's package peer dependency is installed but has the wrong version + * - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_NOT_INSTALLED} if the plugin is + * not installed as an npm package + * - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_MISSING_DEPENDENCY} if the + * plugin's package peer dependency is not installed + * - {@link HardhatError.ERRORS.PLUGINS.DEPENDENCY_VERSION_MISMATCH} if the + * plugin's package peer dependency is installed but has the wrong version */ export async function detectPluginNpmDependencyProblems( + basePathForNpmResolution: string, plugin: HardhatPlugin, - basePathForNpmResolution: string = process.cwd(), ): Promise { if (plugin.npmPackage === undefined) { return; } const pluginPackageResult = readPackageJsonViaNodeRequire( - plugin.npmPackage, basePathForNpmResolution, + plugin.npmPackage, ); if (pluginPackageResult === undefined) { @@ -46,8 +50,8 @@ export async function detectPluginNpmDependencyProblems( pluginPackageJson.peerDependencies, )) { const dependencyPackageJsonResult = readPackageJsonViaNodeRequire( - dependencyName, packagePath, + dependencyName, ); if (dependencyPackageJsonResult === undefined) { @@ -77,16 +81,16 @@ export async function detectPluginNpmDependencyProblems( } /** - * Read the package.json of a named package resolved through the node - * require system. + * Read the package.json of a named package resolved through the node require + * system. * - * @param packageName - the package name i.e. "@nomiclabs/hardhat-waffle" - * @param baseRequirePath - the directory path to use for resolution, defaults to `process.cwd()` + * @param packageName the package name i.e. "@nomiclabs/hardhat-waffle" + * @param baseRequirePath the dir path for node module resolution * @returns the package.json object or undefined if the package is not found */ function readPackageJsonViaNodeRequire( - packageName: string, baseRequirePath: string, + packageName: string, ): { packageJson: PackageJson; packagePath: string } | undefined { try { const require = createRequire(baseRequirePath); diff --git a/v-next/core/src/internal/plugins/resolve-plugin-list.ts b/v-next/core/src/internal/plugins/resolve-plugin-list.ts index 640795922c..937376acb7 100644 --- a/v-next/core/src/internal/plugins/resolve-plugin-list.ts +++ b/v-next/core/src/internal/plugins/resolve-plugin-list.ts @@ -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 { - return reverseTopologicalSort(userConfigPluginList, basePathForNpmResolution); + return reverseTopologicalSort(projectRoot, userConfigPluginList); } /** @@ -24,8 +24,8 @@ export async function resolvePluginList( * @returns The ordered plugins. */ async function reverseTopologicalSort( + projectRoot: string, plugins: HardhatPlugin[], - basePathForNpmResolution: string, ): Promise { const visitedPlugins: Map = new Map(); const result: HardhatPlugin[] = []; @@ -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); } @@ -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, - basePathForNpmResolution: string, ): Promise { try { return await loadFn(); } catch (error) { ensureError(error); - await detectPluginNpmDependencyProblems(plugin, basePathForNpmResolution); + await detectPluginNpmDependencyProblems(projectRoot, plugin); throw new HardhatError( HardhatError.ERRORS.PLUGINS.PLUGIN_DEPENDENCY_FAILED_LOAD, diff --git a/v-next/core/src/internal/tasks/resolved-task.ts b/v-next/core/src/internal/tasks/resolved-task.ts index 61e6c69bd6..20112e47d7 100644 --- a/v-next/core/src/internal/tasks/resolved-task.ts +++ b/v-next/core/src/internal/tasks/resolved-task.ts @@ -266,7 +266,10 @@ export class ResolvedTask implements Task { `Plugin with id ${actionPluginId} not found.`, ); - await detectPluginNpmDependencyProblems(plugin); + await detectPluginNpmDependencyProblems( + this.#hre.config.paths.root, + plugin, + ); } throw new HardhatError( diff --git a/v-next/core/src/types/config.ts b/v-next/core/src/types/config.ts index 50b4ef860f..dce20bc624 100644 --- a/v-next/core/src/types/config.ts +++ b/v-next/core/src/types/config.ts @@ -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; +} diff --git a/v-next/core/test/internal/configuration-variables/configuration-variables.ts b/v-next/core/test/internal/configuration-variables/configuration-variables.ts index 509b47eac0..081fa9e3b9 100644 --- a/v-next/core/test/internal/configuration-variables/configuration-variables.ts +++ b/v-next/core/test/internal/configuration-variables/configuration-variables.ts @@ -4,6 +4,7 @@ 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"; @@ -11,8 +12,10 @@ import { UserInterruptionManagerImplementation } from "../../../src/internal/use 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, ); @@ -21,6 +24,9 @@ describe("ResolvedConfigurationVariable", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, hooks: hookManager, globalOptions: {}, diff --git a/v-next/core/test/internal/hook-manager.ts b/v-next/core/test/internal/hook-manager.ts index 67753c77c6..826ba02103 100644 --- a/v-next/core/test/internal/hook-manager.ts +++ b/v-next/core/test/internal/hook-manager.ts @@ -19,16 +19,23 @@ import type { Task, TaskManager } from "../../src/types/tasks.js"; import type { UserInterruptionManager } from "../../src/types/user-interruptions.js"; import assert from "node:assert/strict"; -import { describe, it, beforeEach } from "node:test"; +import { describe, it, beforeEach, before } from "node:test"; import { HardhatError } from "@ignored/hardhat-vnext-errors"; import { ensureError } from "@ignored/hardhat-vnext-utils/error"; import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; +import { resolveProjectRoot } from "../../src/index.js"; import { HookManagerImplementation } from "../../src/internal/hook-manager.js"; import { UserInterruptionManagerImplementation } from "../../src/internal/user-interruptions.js"; describe("HookManager", () => { + let projectRoot: string; + + before(async () => { + projectRoot = await resolveProjectRoot(process.cwd()); + }); + describe("plugin hooks", () => { describe("running", () => { let hookManager: HookManager; @@ -83,7 +90,9 @@ describe("HookManager", () => { }, }; - const manager = new HookManagerImplementation([examplePlugin]); + const manager = new HookManagerImplementation(projectRoot, [ + examplePlugin, + ]); const userInterruptionsManager = new UserInterruptionManagerImplementation(hookManager); @@ -92,6 +101,9 @@ describe("HookManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, hooks: hookManager, globalOptions: {}, @@ -157,6 +169,9 @@ describe("HookManager", () => { const originalConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; hookManager.registerHandlers("config", { @@ -207,9 +222,14 @@ describe("HookManager", () => { const expectedConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; - const manager = new HookManagerImplementation([examplePlugin]); + const manager = new HookManagerImplementation(projectRoot, [ + examplePlugin, + ]); const validationResult = await manager.runSequentialHandlers( "config", @@ -235,7 +255,9 @@ describe("HookManager", () => { }, }; - const manager = new HookManagerImplementation([examplePlugin]); + const manager = new HookManagerImplementation(projectRoot, [ + examplePlugin, + ]); try { await manager.runHandlerChain( @@ -264,7 +286,9 @@ describe("HookManager", () => { }, }; - const manager = new HookManagerImplementation([examplePlugin]); + const manager = new HookManagerImplementation(projectRoot, [ + examplePlugin, + ]); await assertRejectsWithHardhatError( async () => @@ -292,7 +316,7 @@ describe("HookManager", () => { let hookManager: HookManager; beforeEach(() => { - const manager = new HookManagerImplementation([]); + const manager = new HookManagerImplementation(projectRoot, []); const userInterruptionsManager = new UserInterruptionManagerImplementation(hookManager); @@ -301,6 +325,9 @@ describe("HookManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, hooks: hookManager, globalOptions: {}, @@ -316,6 +343,9 @@ describe("HookManager", () => { const defaultImplementationVersionOfConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; const resultConfig = await hookManager.runHandlerChain( @@ -397,6 +427,9 @@ describe("HookManager", () => { const expectedConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; hookManager.registerHandlers("config", { @@ -506,7 +539,7 @@ describe("HookManager", () => { let hookManager: HookManager; beforeEach(() => { - const manager = new HookManagerImplementation([]); + const manager = new HookManagerImplementation(projectRoot, []); const userInterruptionsManager = new UserInterruptionManagerImplementation(hookManager); @@ -515,6 +548,9 @@ describe("HookManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, hooks: hookManager, globalOptions: {}, @@ -525,7 +561,10 @@ describe("HookManager", () => { }); it("Should return the empty set if no handlers are registered", async () => { - const mockHre = buildMockHardhatRuntimeEnvironment(hookManager); + const mockHre = buildMockHardhatRuntimeEnvironment( + projectRoot, + hookManager, + ); const resultHre = await hookManager.runSequentialHandlers( "hre", @@ -601,6 +640,9 @@ describe("HookManager", () => { const expectedConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; hookManager.registerHandlers("config", { @@ -631,7 +673,7 @@ describe("HookManager", () => { let hookManager: HookManager; beforeEach(() => { - const manager = new HookManagerImplementation([]); + const manager = new HookManagerImplementation(projectRoot, []); const userInterruptionsManager = new UserInterruptionManagerImplementation(hookManager); @@ -640,6 +682,9 @@ describe("HookManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, hooks: hookManager, globalOptions: {}, @@ -653,6 +698,9 @@ describe("HookManager", () => { const originalConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; const results = await hookManager.runParallelHandlers( @@ -668,6 +716,9 @@ describe("HookManager", () => { const originalConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; hookManager.registerHandlers("config", { @@ -719,7 +770,10 @@ describe("HookManager", () => { }); it("Should pass the context to the handler (for non-config)", async () => { - const mockHre = buildMockHardhatRuntimeEnvironment(hookManager); + const mockHre = buildMockHardhatRuntimeEnvironment( + projectRoot, + hookManager, + ); hookManager.registerHandlers("hre", { created: async ( @@ -745,6 +799,9 @@ describe("HookManager", () => { const expectedConfig: HardhatConfig = { plugins: [], tasks: [], + paths: { + root: projectRoot, + }, }; const validationError = { @@ -775,7 +832,7 @@ describe("HookManager", () => { let hookManager: HookManager; beforeEach(() => { - const manager = new HookManagerImplementation([]); + const manager = new HookManagerImplementation(projectRoot, []); const userInterruptionsManager = new UserInterruptionManagerImplementation(hookManager); @@ -784,6 +841,9 @@ describe("HookManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, hooks: hookManager, globalOptions: {}, @@ -913,6 +973,7 @@ describe("HookManager", () => { }); function buildMockHardhatRuntimeEnvironment( + projectRoot: string, hookManager: HookManager, ): HardhatRuntimeEnvironment { const mockInteruptionManager: UserInterruptionManager = { @@ -939,6 +1000,9 @@ function buildMockHardhatRuntimeEnvironment( config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, tasks: mockTaskManager, globalOptions: {}, diff --git a/v-next/core/test/internal/plugins/detect-plugin-npm-dependency-problems.ts b/v-next/core/test/internal/plugins/detect-plugin-npm-dependency-problems.ts index be29066be5..b07ea365e6 100644 --- a/v-next/core/test/internal/plugins/detect-plugin-npm-dependency-problems.ts +++ b/v-next/core/test/internal/plugins/detect-plugin-npm-dependency-problems.ts @@ -18,13 +18,10 @@ describe("Plugins - detect npm dependency problems", () => { "./fixture-projects/peer-dep-with-wrong-version", ); - await detectPluginNpmDependencyProblems( - { - ...plugin, - npmPackage: undefined, - }, - peerDepWithWrongVersionFixture, - ); + await detectPluginNpmDependencyProblems(peerDepWithWrongVersionFixture, { + ...plugin, + npmPackage: undefined, + }); }); describe("when the plugin has no peer deps", () => { @@ -34,8 +31,8 @@ describe("Plugins - detect npm dependency problems", () => { ); await detectPluginNpmDependencyProblems( - plugin, installedPackageProjectFixture, + plugin, ); }); @@ -47,8 +44,8 @@ describe("Plugins - detect npm dependency problems", () => { await assertRejectsWithHardhatError( async () => detectPluginNpmDependencyProblems( - plugin, nonInstalledPackageProjectFixture, + plugin, ), HardhatError.ERRORS.PLUGINS.PLUGIN_NOT_INSTALLED, { @@ -66,8 +63,8 @@ describe("Plugins - detect npm dependency problems", () => { ); await detectPluginNpmDependencyProblems( - plugin, installedPeerDepsFixture, + plugin, ); }); @@ -77,7 +74,7 @@ describe("Plugins - detect npm dependency problems", () => { ); await assertRejectsWithHardhatError( - detectPluginNpmDependencyProblems(plugin, notInstalledPeerDepFixture), + detectPluginNpmDependencyProblems(notInstalledPeerDepFixture, plugin), HardhatError.ERRORS.PLUGINS.PLUGIN_MISSING_DEPENDENCY, { pluginId: "example-plugin", peerDependencyName: "peer2" }, ); @@ -91,8 +88,8 @@ describe("Plugins - detect npm dependency problems", () => { ); await detectPluginNpmDependencyProblems( - plugin, installedPeerDepsFixture, + plugin, ); }); }); @@ -107,8 +104,8 @@ describe("Plugins - detect npm dependency problems", () => { await assertRejectsWithHardhatError( async () => detectPluginNpmDependencyProblems( - plugin, peerDepWithWrongVersionFixture, + plugin, ), HardhatError.ERRORS.PLUGINS.DEPENDENCY_VERSION_MISMATCH, { diff --git a/v-next/core/test/internal/plugins/resolve-plugin-list.ts b/v-next/core/test/internal/plugins/resolve-plugin-list.ts index 25ca50089a..163f423f41 100644 --- a/v-next/core/test/internal/plugins/resolve-plugin-list.ts +++ b/v-next/core/test/internal/plugins/resolve-plugin-list.ts @@ -14,12 +14,12 @@ describe("Plugins - resolve plugin list", () => { ); it("should return empty on an empty plugin list", async () => { - assert.deepEqual(await resolvePluginList([], installedPackageFixture), []); + assert.deepEqual(await resolvePluginList(installedPackageFixture, []), []); }); it("should return empty on an undefined plugin list", async () => { assert.deepEqual( - await resolvePluginList(undefined, installedPackageFixture), + await resolvePluginList(installedPackageFixture, undefined), [], ); }); @@ -30,7 +30,7 @@ describe("Plugins - resolve plugin list", () => { }; assert.deepEqual( - await resolvePluginList([plugin], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [plugin]), [plugin], ); }); @@ -43,7 +43,7 @@ describe("Plugins - resolve plugin list", () => { const expected = [c, b, a]; assert.deepEqual( - await resolvePluginList([a], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [a]), expected, ); }); @@ -56,7 +56,7 @@ describe("Plugins - resolve plugin list", () => { const expected = [a, b, c]; assert.deepEqual( - await resolvePluginList([a, b, c], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [a, b, c]), expected, ); }); @@ -74,7 +74,7 @@ describe("Plugins - resolve plugin list", () => { const expected = [b, c, a]; assert.deepEqual( - await resolvePluginList([a], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [a]), expected, ); }); @@ -89,7 +89,7 @@ describe("Plugins - resolve plugin list", () => { const expected = [c, a, b]; assert.deepEqual( - await resolvePluginList([a, b], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [a, b]), expected, ); }); @@ -110,7 +110,7 @@ describe("Plugins - resolve plugin list", () => { const expected = [d, b, c, a]; assert.deepEqual( - await resolvePluginList([a], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [a]), expected, ); }); @@ -141,7 +141,7 @@ describe("Plugins - resolve plugin list", () => { const expected = [i, g, c, d, a, h, e, f, b]; assert.deepEqual( - await resolvePluginList([a, b], installedPackageFixture), + await resolvePluginList(installedPackageFixture, [a, b]), expected, ); }); @@ -151,7 +151,7 @@ describe("Plugins - resolve plugin list", () => { const copy = { id: "dup" }; await assertRejectsWithHardhatError( - async () => resolvePluginList([a, copy], installedPackageFixture), + async () => resolvePluginList(installedPackageFixture, [a, copy]), HardhatError.ERRORS.GENERAL.DUPLICATED_PLUGIN_ID, { id: "dup", @@ -172,7 +172,7 @@ describe("Plugins - resolve plugin list", () => { }; await assertRejectsWithHardhatError( - async () => resolvePluginList([plugin], installedPackageFixture), + async () => resolvePluginList(installedPackageFixture, [plugin]), HardhatError.ERRORS.PLUGINS.PLUGIN_DEPENDENCY_FAILED_LOAD, { pluginId: plugin.id }, ); @@ -194,7 +194,7 @@ describe("Plugins - resolve plugin list", () => { }; await assertRejectsWithHardhatError( - async () => resolvePluginList([plugin], notInstalledPackageFixture), + async () => resolvePluginList(notInstalledPackageFixture, [plugin]), HardhatError.ERRORS.PLUGINS.PLUGIN_NOT_INSTALLED, { pluginId: "example", diff --git a/v-next/core/test/internal/user-interruptions/user-interruptions-manager.ts b/v-next/core/test/internal/user-interruptions/user-interruptions-manager.ts index ce1ad6c1a3..29b5edefc7 100644 --- a/v-next/core/test/internal/user-interruptions/user-interruptions-manager.ts +++ b/v-next/core/test/internal/user-interruptions/user-interruptions-manager.ts @@ -1,15 +1,22 @@ import type { UserInterruptionHooks } from "../../../src/types/hooks.js"; import assert from "node:assert/strict"; -import { describe, it } from "node:test"; +import { before, describe, it } from "node:test"; +import { resolveProjectRoot } from "../../../src/index.js"; import { HookManagerImplementation } from "../../../src/internal/hook-manager.js"; import { UserInterruptionManagerImplementation } from "../../../src/internal/user-interruptions.js"; describe("UserInterruptionManager", () => { + let projectRoot: string; + + before(async () => { + projectRoot = await resolveProjectRoot(process.cwd()); + }); + describe("displayMessage", () => { it("Should call a dynamic handler with a given message from an interruptor", async () => { - const hookManager = new HookManagerImplementation([]); + const hookManager = new HookManagerImplementation(projectRoot, []); const userInterruptionManager = new UserInterruptionManagerImplementation( hookManager, ); @@ -21,6 +28,9 @@ describe("UserInterruptionManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, globalOptions: {}, hooks: hookManager, @@ -54,7 +64,7 @@ describe("UserInterruptionManager", () => { describe("requestInput", () => { it("Should call a dynamic handler with a given input description from an interruptor", async () => { - const hookManager = new HookManagerImplementation([]); + const hookManager = new HookManagerImplementation(projectRoot, []); const userInterruptionManager = new UserInterruptionManagerImplementation( hookManager, ); @@ -62,6 +72,9 @@ describe("UserInterruptionManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, globalOptions: {}, hooks: hookManager, @@ -97,7 +110,7 @@ describe("UserInterruptionManager", () => { describe("requestSecretInput", () => { it("Should call a dynamic handler with a given input description from an interruptor", async () => { - const hookManager = new HookManagerImplementation([]); + const hookManager = new HookManagerImplementation(projectRoot, []); const userInterruptionManager = new UserInterruptionManagerImplementation( hookManager, ); @@ -105,6 +118,9 @@ describe("UserInterruptionManager", () => { config: { tasks: [], plugins: [], + paths: { + root: projectRoot, + }, }, globalOptions: {}, hooks: hookManager, diff --git a/v-next/hardhat-utils/src/package.ts b/v-next/hardhat-utils/src/package.ts index 244fe6e981..57fc710df1 100644 --- a/v-next/hardhat-utils/src/package.ts +++ b/v-next/hardhat-utils/src/package.ts @@ -29,39 +29,39 @@ export interface PackageJson { * Searches for the nearest `package.json` file, starting from the directory of * the provided file path or url string and moving up the directory tree. * - * @param filePathOrUrl The file path or url string from which to start the - * search. The url must be a file url. This is useful when you want to find - * the nearest `package.json` file relative to the current module, as you - * can use `import.meta.url`. + * @param pathOrUrl A path or url string from which to start the search. The url + * must be a file url. This is useful when you want to find the nearest + * `package.json` file relative to the current module, as you can use + * `import.meta.url`. * @returns The absolute path to the nearest `package.json` file. * @throws PackageJsonNotFoundError If no `package.json` file is found. */ export async function findClosestPackageJson( - filePathOrUrl: string, + pathOrUrl: string, ): Promise { - const filePath = getFilePath(filePathOrUrl); + const filePath = getFilePath(pathOrUrl); if (filePath === undefined) { - throw new PackageJsonNotFoundError(filePathOrUrl); + throw new PackageJsonNotFoundError(pathOrUrl); } - const packageJsonPath = await findUp("package.json", path.dirname(filePath)); + const packageJsonPath = await findUp("package.json", filePath); if (packageJsonPath === undefined) { - throw new PackageJsonNotFoundError(filePathOrUrl); + throw new PackageJsonNotFoundError(pathOrUrl); } return packageJsonPath; } /** - * Reads the nearest `package.json` file, starting from the directory of the - * provided file path or url string and moving up the directory tree. + * Reads the nearest `package.json` file, starting from provided path or url + * string and moving up the directory tree. * - * @param filePathOrUrl The file path or url string from which to start the - * search. The url must be a file url. This is useful when you want to find - * the nearest `package.json` file relative to the current module, as you - * can use `import.meta.url`. + * @param pathOrUrl A path or url string from which to start the search. The url + * must be a file url. This is useful when you want to find the nearest + * `package.json` file relative to the current module, as you can use + * `import.meta.url`. * @returns The contents of the nearest `package.json` file, parsed as a * {@link PackageJson} object. * @throws PackageJsonNotFoundError If no `package.json` file is found. @@ -69,9 +69,9 @@ export async function findClosestPackageJson( * be read. */ export async function readClosestPackageJson( - filePathOrUrl: string, + pathOrUrl: string, ): Promise { - const packageJsonPath = await findClosestPackageJson(filePathOrUrl); + const packageJsonPath = await findClosestPackageJson(pathOrUrl); try { return await readJsonFile(packageJsonPath); } catch (e) { @@ -81,16 +81,16 @@ export async function readClosestPackageJson( } /** - * Finds the root directory of the nearest package, starting from the directory - * of the provided file path or url string and moving up the directory tree. + * Finds the root directory of the nearest package, starting from the provided + * path or url string and moving up the directory tree. * * This function uses `findClosestPackageJson` to find the nearest `package.json` * file and then returns the directory that contains that file. * - * @param filePathOrUrl The file path or url string from which to start the - * search. The url must be a file url. This is useful when you want to find - * the nearest `package.json` file relative to the current module, as you - * can use `import.meta.url`. + * @param pathOrUrl A path or url string from which to start the search. The url + * must be a file url. This is useful when you want to find the nearest + * `package.json` file relative to the current module, as you can use + * `import.meta.url`. * @returns The absolute path of the root directory of the nearest package. */ export async function findClosestPackageRoot( diff --git a/v-next/hardhat-utils/test/package.ts b/v-next/hardhat-utils/test/package.ts index 0ceb4cfb93..e507902d3b 100644 --- a/v-next/hardhat-utils/test/package.ts +++ b/v-next/hardhat-utils/test/package.ts @@ -48,6 +48,16 @@ describe("package", () => { assert.equal(actualPath, expectedPath); }); + it("Should find the closest package.json relative to a directory when its within that directory", async () => { + const expectedPath = path.join(getTmpDir(), "package.json"); + const fromPath = getTmpDir(); + await createFile(expectedPath); + + const actualPath = await findClosestPackageJson(fromPath); + + assert.equal(actualPath, expectedPath); + }); + it("Should throw PackageJsonNotFoundError if no package.json is found", async () => { const fromPath = path.join(getTmpDir(), "subdir", "subsubdir"); diff --git a/v-next/hardhat/src/hre.ts b/v-next/hardhat/src/hre.ts index 37f0298334..b488fe1606 100644 --- a/v-next/hardhat/src/hre.ts +++ b/v-next/hardhat/src/hre.ts @@ -8,6 +8,7 @@ import { // eslint-disable-next-line no-restricted-imports -- This is the one place where we allow it createBaseHardhatRuntimeEnvironment, resolvePluginList, + resolveProjectRoot, } from "@ignored/hardhat-vnext-core"; import { BUILTIN_GLOBAL_OPTIONS_DEFINITIONS } from "./internal/builtin-global-options.js"; @@ -19,21 +20,25 @@ import { builtinPlugins } from "./internal/builtin-plugins/index.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. * @returns The Hardhat Runtime Environment. */ export async function createHardhatRuntimeEnvironment( config: HardhatUserConfig, userProvidedGlobalOptions: Partial = {}, + projectRoot?: string, unsafeOptions: UnsafeHardhatRuntimeEnvironmentOptions = {}, ): Promise { + const resolvedProjectRoot = await resolveProjectRoot(projectRoot); + if (unsafeOptions.resolvedPlugins === undefined) { const plugins = [...builtinPlugins, ...(config.plugins ?? [])]; - // We resolve the plugins within npm modules relative to the current working - const basePathForNpmResolution = process.cwd(); const resolvedPlugins = await resolvePluginList( + resolvedProjectRoot, plugins, - basePathForNpmResolution, ); unsafeOptions.resolvedPlugins = resolvedPlugins; @@ -54,6 +59,7 @@ export async function createHardhatRuntimeEnvironment( return createBaseHardhatRuntimeEnvironment( config, userProvidedGlobalOptions, + resolvedProjectRoot, unsafeOptions, ); } diff --git a/v-next/hardhat/src/index.ts b/v-next/hardhat/src/index.ts index 247d7cfcd0..67e1b3b110 100644 --- a/v-next/hardhat/src/index.ts +++ b/v-next/hardhat/src/index.ts @@ -5,6 +5,8 @@ import type { HardhatRuntimeEnvironment } from "./types/hre.js"; import type { TaskManager } from "./types/tasks.js"; import type { UserInterruptionManager } from "./types/user-interruptions.js"; +import { resolveProjectRoot } from "@ignored/hardhat-vnext-core"; + import { resolveHardhatConfigPath } from "./config.js"; import { createHardhatRuntimeEnvironment } from "./hre.js"; import { @@ -19,9 +21,10 @@ let maybeHre: HardhatRuntimeEnvironment | undefined = if (maybeHre === undefined) { /* eslint-disable no-restricted-syntax -- Allow top-level await here */ const configPath = await resolveHardhatConfigPath(); + const projectRoot = await resolveProjectRoot(configPath); const userConfig = await importUserConfig(configPath); - maybeHre = await createHardhatRuntimeEnvironment(userConfig); + maybeHre = await createHardhatRuntimeEnvironment(userConfig, {}, projectRoot); /* eslint-enable no-restricted-syntax */ setGlobalHardhatRuntimeEnvironment(maybeHre); diff --git a/v-next/hardhat/src/internal/cli/main.ts b/v-next/hardhat/src/internal/cli/main.ts index 4474b84703..bd1aa818e3 100644 --- a/v-next/hardhat/src/internal/cli/main.ts +++ b/v-next/hardhat/src/internal/cli/main.ts @@ -18,6 +18,7 @@ import { buildGlobalOptionDefinitions, parseArgumentValue, resolvePluginList, + resolveProjectRoot, } from "@ignored/hardhat-vnext-core"; import { ArgumentType } from "@ignored/hardhat-vnext-core/types/arguments"; import { @@ -25,6 +26,7 @@ import { assertHardhatInvariant, } from "@ignored/hardhat-vnext-errors"; import { isCi } from "@ignored/hardhat-vnext-utils/ci"; +import { getRealPath } from "@ignored/hardhat-vnext-utils/fs"; import { kebabToCamelCase } from "@ignored/hardhat-vnext-utils/string"; import { resolveHardhatConfigPath } from "../../config.js"; @@ -68,16 +70,17 @@ export async function main( builtinGlobalOptions.configPath = await resolveHardhatConfigPath(); } + const projectRoot = await resolveProjectRoot( + await getRealPath(builtinGlobalOptions.configPath), + ); + const userConfig = await importUserConfig(builtinGlobalOptions.configPath); const configPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : []; const plugins = [...builtinPlugins, ...configPlugins]; - const resolvedPlugins = await resolvePluginList( - plugins, - builtinGlobalOptions.configPath, - ); + const resolvedPlugins = await resolvePluginList(projectRoot, plugins); const pluginGlobalOptionDefinitions = buildGlobalOptionDefinitions(resolvedPlugins); @@ -94,6 +97,7 @@ export async function main( const hre = await createHardhatRuntimeEnvironment( userConfig, { ...builtinGlobalOptions, ...userProvidedGlobalOptions }, + projectRoot, { resolvedPlugins, globalOptionDefinitions }, );