Skip to content

Commit 02fac13

Browse files
authored
Merge pull request #5517 from NomicFoundation/projext-root
[WIP] Project root resolution
2 parents 02730b0 + 4dc69d2 commit 02fac13

17 files changed

+284
-124
lines changed

v-next/core/src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,28 @@ import { HardhatRuntimeEnvironmentImplementation } from "./internal/hre.js";
1515
* @param config - The user's Hardhat configuration.
1616
* @param userProvidedGlobalOptions - The global options provided by the
1717
* user.
18+
* @param projectRoot - The root of the Hardhat project. Hardhat expects this
19+
* to be the root of the npm project containing your config file. If none is
20+
* provided, it will use the root of the npm project that contains the CWD.
1821
* @param unsafeOptions - Options used to bypass some initialization, to avoid
1922
* redoing it in the CLI. Should only be used in the official CLI.
2023
* @returns The Hardhat Runtime Environment.
2124
*/
2225
export async function createBaseHardhatRuntimeEnvironment(
2326
config: HardhatUserConfig,
2427
userProvidedGlobalOptions: Partial<GlobalOptions> = {},
28+
projectRoot?: string,
2529
unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions,
2630
): Promise<HardhatRuntimeEnvironment> {
2731
return HardhatRuntimeEnvironmentImplementation.create(
2832
config,
2933
userProvidedGlobalOptions,
34+
projectRoot,
3035
unsafeOptions,
3136
);
3237
}
3338

39+
export { resolveProjectRoot } from "./internal/hre.js";
3440
export { parseArgumentValue } from "./internal/arguments.js";
3541
export { resolvePluginList } from "./internal/plugins/resolve-plugin-list.js";
3642
export { buildGlobalOptionDefinitions } from "./internal/global-options.js";

v-next/core/src/internal/hook-manager.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import {
1717
assertHardhatInvariant,
1818
} from "@ignored/hardhat-vnext-errors";
1919

20+
import { detectPluginNpmDependencyProblems } from "./plugins/detect-plugin-npm-dependency-problems.js";
21+
2022
export class HookManagerImplementation implements HookManager {
23+
readonly #projectRoot: string;
24+
2125
readonly #pluginsInReverseOrder: HardhatPlugin[];
2226

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

47-
constructor(plugins: HardhatPlugin[]) {
51+
constructor(projectRoot: string, plugins: HardhatPlugin[]) {
52+
this.#projectRoot = projectRoot;
4853
this.#pluginsInReverseOrder = plugins.toReversed();
4954
}
5055

@@ -252,7 +257,7 @@ export class HookManagerImplementation implements HookManager {
252257

253258
if (typeof hookHandlerCategoryFactory === "string") {
254259
hookCategory = await this.#loadHookCategoryFactory(
255-
plugin.id,
260+
plugin,
256261
hookCategoryName,
257262
hookHandlerCategoryFactory,
258263
);
@@ -288,35 +293,42 @@ export class HookManagerImplementation implements HookManager {
288293
}
289294

290295
async #loadHookCategoryFactory<HookCategoryNameT extends keyof HardhatHooks>(
291-
pluginId: string,
296+
plugin: HardhatPlugin,
292297
hookCategoryName: HookCategoryNameT,
293298
path: string,
294299
): Promise<Partial<HardhatHooks[HookCategoryNameT]>> {
295300
if (!path.startsWith("file://")) {
296301
throw new HardhatError(
297302
HardhatError.ERRORS.HOOKS.INVALID_HOOK_FACTORY_PATH,
298303
{
299-
pluginId,
304+
pluginId: plugin.id,
300305
hookCategoryName,
301306
path,
302307
},
303308
);
304309
}
305310

306-
const mod = await import(path);
311+
let mod;
312+
313+
try {
314+
mod = await import(path);
315+
} catch (error) {
316+
await detectPluginNpmDependencyProblems(this.#projectRoot, plugin);
317+
throw error;
318+
}
307319

308320
const factory = mod.default;
309321

310322
assertHardhatInvariant(
311323
typeof factory === "function",
312-
`Plugin ${pluginId} doesn't export a hook factory for category ${hookCategoryName} in ${path}`,
324+
`Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName} in ${path}`,
313325
);
314326

315327
const category = await factory();
316328

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

322334
return category;

v-next/core/src/internal/hre.ts

+35-14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { TaskManager } from "../types/tasks.js";
1515
import type { UserInterruptionManager } from "../types/user-interruptions.js";
1616

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

1920
import { ResolvedConfigurationVariableImplementation } from "./configuration-variables.js";
2021
import {
@@ -32,28 +33,24 @@ export class HardhatRuntimeEnvironmentImplementation
3233
public static async create(
3334
inputUserConfig: HardhatUserConfig,
3435
userProvidedGlobalOptions: Partial<GlobalOptions>,
36+
projectRoot?: string,
3537
unsafeOptions?: UnsafeHardhatRuntimeEnvironmentOptions,
3638
): Promise<HardhatRuntimeEnvironmentImplementation> {
37-
// TODO: Clone with lodash or https://github.com/davidmarkclements/rfdc
38-
// TODO: Or maybe don't clone at all
39-
const clonedUserConfig = inputUserConfig;
40-
41-
// Resolve plugins from node modules relative to the current working directory
42-
const basePathForNpmResolution = process.cwd();
39+
const resolvedProjectRoot = await resolveProjectRoot(projectRoot);
4340

4441
const resolvedPlugins =
4542
unsafeOptions?.resolvedPlugins ??
46-
(await resolvePluginList(
47-
clonedUserConfig.plugins,
48-
basePathForNpmResolution,
49-
));
43+
(await resolvePluginList(resolvedProjectRoot, inputUserConfig.plugins));
5044

51-
const hooks = new HookManagerImplementation(resolvedPlugins);
45+
const hooks = new HookManagerImplementation(
46+
resolvedProjectRoot,
47+
resolvedPlugins,
48+
);
5249

5350
// extend user config:
5451
const extendedUserConfig = await runUserConfigExtensions(
5552
hooks,
56-
clonedUserConfig,
53+
inputUserConfig,
5754
);
5855

5956
// validate config
@@ -76,14 +73,20 @@ export class HardhatRuntimeEnvironmentImplementation
7673
// Resolve config
7774

7875
const resolvedConfig = await resolveUserConfig(
76+
resolvedProjectRoot,
7977
hooks,
8078
resolvedPlugins,
8179
inputUserConfig,
8280
);
8381

84-
// We override the plugins, as we want to prevent plugins from changing this
85-
const config = {
82+
// We override the plugins and the proejct root, as we want to prevent
83+
// the plugins from changing them
84+
const config: HardhatConfig = {
8685
...resolvedConfig,
86+
paths: {
87+
...resolvedConfig.paths,
88+
root: resolvedProjectRoot,
89+
},
8790
plugins: resolvedPlugins,
8891
};
8992

@@ -138,6 +141,20 @@ export class HardhatRuntimeEnvironmentImplementation
138141
}
139142
}
140143

144+
/**
145+
* Resolves the project root of a Hardhat project based on the config file or
146+
* another path within the project. If not provided, it will be resolved from
147+
* the current working directory.
148+
*
149+
* @param absolutePathWithinProject An absolute path within the project, usually
150+
* the config file.
151+
*/
152+
export async function resolveProjectRoot(
153+
absolutePathWithinProject: string | undefined,
154+
): Promise<string> {
155+
return findClosestPackageRoot(absolutePathWithinProject ?? process.cwd());
156+
}
157+
141158
async function runUserConfigExtensions(
142159
hooks: HookManager,
143160
config: HardhatUserConfig,
@@ -169,13 +186,17 @@ async function validateUserConfig(
169186
}
170187

171188
async function resolveUserConfig(
189+
projectRoot: string,
172190
hooks: HookManager,
173191
sortedPlugins: HardhatPlugin[],
174192
config: HardhatUserConfig,
175193
): Promise<HardhatConfig> {
176194
const initialResolvedConfig: HardhatConfig = {
177195
plugins: sortedPlugins,
178196
tasks: config.tasks ?? [],
197+
paths: {
198+
root: projectRoot,
199+
},
179200
};
180201

181202
return hooks.runHandlerChain(

v-next/core/src/internal/plugins/detect-plugin-npm-dependency-problems.ts

+18-14
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,30 @@ import { HardhatError } from "@ignored/hardhat-vnext-errors";
88
import semver from "semver";
99

1010
/**
11-
* Validate that a plugin is installed and that its peer dependencies are installed and satisfy the version constraints.
11+
* Validate that a plugin is installed and that its peer dependencies are
12+
* installed and satisfy the version constraints.
1213
*
13-
* @param plugin - the plugin to be validated
14-
* @param basePathForNpmResolution - the directory path to use for node module resolution, defaulting to `process.cwd()`
14+
* @param basePathForNpmResolution the dir path for node module resolution
15+
* @param plugin the plugin to be validated
1516
* @throws {HardhatError} with descriptor:
16-
* - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_NOT_INSTALLED} if the plugin is not installed as an npm package
17-
* - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_MISSING_DEPENDENCY} if the plugin's package peer dependency is not installed
18-
* - {@link HardhatError.ERRORS.PLUGINS.DEPENDENCY_VERSION_MISMATCH} if the plugin's package peer dependency is installed but has the wrong version
17+
* - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_NOT_INSTALLED} if the plugin is
18+
* not installed as an npm package
19+
* - {@link HardhatError.ERRORS.PLUGINS.PLUGIN_MISSING_DEPENDENCY} if the
20+
* plugin's package peer dependency is not installed
21+
* - {@link HardhatError.ERRORS.PLUGINS.DEPENDENCY_VERSION_MISMATCH} if the
22+
* plugin's package peer dependency is installed but has the wrong version
1923
*/
2024
export async function detectPluginNpmDependencyProblems(
25+
basePathForNpmResolution: string,
2126
plugin: HardhatPlugin,
22-
basePathForNpmResolution: string = process.cwd(),
2327
): Promise<void> {
2428
if (plugin.npmPackage === undefined) {
2529
return;
2630
}
2731

2832
const pluginPackageResult = readPackageJsonViaNodeRequire(
29-
plugin.npmPackage,
3033
basePathForNpmResolution,
34+
plugin.npmPackage,
3135
);
3236

3337
if (pluginPackageResult === undefined) {
@@ -46,8 +50,8 @@ export async function detectPluginNpmDependencyProblems(
4650
pluginPackageJson.peerDependencies,
4751
)) {
4852
const dependencyPackageJsonResult = readPackageJsonViaNodeRequire(
49-
dependencyName,
5053
packagePath,
54+
dependencyName,
5155
);
5256

5357
if (dependencyPackageJsonResult === undefined) {
@@ -77,16 +81,16 @@ export async function detectPluginNpmDependencyProblems(
7781
}
7882

7983
/**
80-
* Read the package.json of a named package resolved through the node
81-
* require system.
84+
* Read the package.json of a named package resolved through the node require
85+
* system.
8286
*
83-
* @param packageName - the package name i.e. "@nomiclabs/hardhat-waffle"
84-
* @param baseRequirePath - the directory path to use for resolution, defaults to `process.cwd()`
87+
* @param packageName the package name i.e. "@nomiclabs/hardhat-waffle"
88+
* @param baseRequirePath the dir path for node module resolution
8589
* @returns the package.json object or undefined if the package is not found
8690
*/
8791
function readPackageJsonViaNodeRequire(
88-
packageName: string,
8992
baseRequirePath: string,
93+
packageName: string,
9094
): { packageJson: PackageJson; packagePath: string } | undefined {
9195
try {
9296
const require = createRequire(baseRequirePath);

v-next/core/src/internal/plugins/resolve-plugin-list.ts

+7-11
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { detectPluginNpmDependencyProblems } from "./detect-plugin-npm-dependenc
99
* Resolves the plugin list, returning them in the right order.
1010
*/
1111
export async function resolvePluginList(
12+
projectRoot: string,
1213
userConfigPluginList: HardhatPlugin[] = [],
13-
basePathForNpmResolution: string,
1414
): Promise<HardhatPlugin[]> {
15-
return reverseTopologicalSort(userConfigPluginList, basePathForNpmResolution);
15+
return reverseTopologicalSort(projectRoot, userConfigPluginList);
1616
}
1717

1818
/**
@@ -24,8 +24,8 @@ export async function resolvePluginList(
2424
* @returns The ordered plugins.
2525
*/
2626
async function reverseTopologicalSort(
27+
projectRoot: string,
2728
plugins: HardhatPlugin[],
28-
basePathForNpmResolution: string,
2929
): Promise<HardhatPlugin[]> {
3030
const visitedPlugins: Map<string, HardhatPlugin> = new Map();
3131
const result: HardhatPlugin[] = [];
@@ -48,11 +48,7 @@ async function reverseTopologicalSort(
4848

4949
if (plugin.dependencies !== undefined) {
5050
for (const loadFn of plugin.dependencies) {
51-
const dependency = await loadDependency(
52-
plugin,
53-
loadFn,
54-
basePathForNpmResolution,
55-
);
51+
const dependency = await loadDependency(projectRoot, plugin, loadFn);
5652

5753
await dfs(dependency);
5854
}
@@ -72,22 +68,22 @@ async function reverseTopologicalSort(
7268
* Attempt to load a plugins dependency. If there is an error,
7369
* first try and validate the npm dependencies of the plugin.
7470
*
71+
* @param projectRoot - The root of the Hardhat project.
7572
* @param plugin - the plugin has the dependency
7673
* @param loadFn - the load function for the dependency
77-
* @param basePathForNpmResolution - the directory path to use for node module resolution
7874
* @returns the loaded plugin
7975
*/
8076
async function loadDependency(
77+
projectRoot: string,
8178
plugin: HardhatPlugin,
8279
loadFn: () => Promise<HardhatPlugin>,
83-
basePathForNpmResolution: string,
8480
): Promise<HardhatPlugin> {
8581
try {
8682
return await loadFn();
8783
} catch (error) {
8884
ensureError(error);
8985

90-
await detectPluginNpmDependencyProblems(plugin, basePathForNpmResolution);
86+
await detectPluginNpmDependencyProblems(projectRoot, plugin);
9187

9288
throw new HardhatError(
9389
HardhatError.ERRORS.PLUGINS.PLUGIN_DEPENDENCY_FAILED_LOAD,

v-next/core/src/internal/tasks/resolved-task.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ export class ResolvedTask implements Task {
235235
`Plugin with id ${actionPluginId} not found.`,
236236
);
237237

238-
await detectPluginNpmDependencyProblems(plugin);
238+
await detectPluginNpmDependencyProblems(
239+
this.#hre.config.paths.root,
240+
plugin,
241+
);
239242
}
240243

241244
throw new HardhatError(

v-next/core/src/types/config.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,23 @@ export type SensitiveString = string | ConfigurationVariable;
4545
* The user's Hardhat configuration, as exported in their
4646
* config file.
4747
*/
48-
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- Used through module aumentation
49-
export interface HardhatUserConfig {}
48+
export interface HardhatUserConfig {
49+
paths?: ProjectPathsUserConfig;
50+
}
51+
52+
/**
53+
* The different paths that conform a Hardhat project.
54+
*/
55+
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- TODO: add the paths
56+
export interface ProjectPathsUserConfig {}
5057

5158
/**
5259
* The resolved Hardhat configuration.
5360
*/
54-
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- Used through module aumentation
55-
export interface HardhatConfig {}
61+
export interface HardhatConfig {
62+
paths: ProjectPathsConfig;
63+
}
64+
65+
export interface ProjectPathsConfig {
66+
root: string;
67+
}

0 commit comments

Comments
 (0)