Skip to content

Commit 0305823

Browse files
authored
v-next/lazy-plugin-loading (#5273)
Switch to lazy loading of Hardhat plugin dependencies and move the validation logic for plugin installs into the plugin resolution. The use of plugin validation within the plugin resolution function requires exposing the base npm resolution folder as a parameter to `resolvePluginList`. Tests have been added to the plugin resolution that test failed dependency loads fall back to plugin install validation logic.
1 parent 7c5b185 commit 0305823

File tree

10 files changed

+228
-104
lines changed

10 files changed

+228
-104
lines changed

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

-9
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ import {
99
import { HardhatPlugin } from "../types/plugins.js";
1010
import { LastParameter, Return } from "../types/utils.js";
1111

12-
import { validatePluginNpmDependencies } from "./plugins/plugin-validation.js";
13-
1412
export class HookManagerImplementation implements HookManager {
15-
readonly #validatedPlugins = new Set<string>();
16-
1713
readonly #pluginsInReverseOrder: HardhatPlugin[];
1814

1915
/**
@@ -244,11 +240,6 @@ export class HookManagerImplementation implements HookManager {
244240
return;
245241
}
246242

247-
if (!this.#validatedPlugins.has(plugin.id)) {
248-
await validatePluginNpmDependencies(plugin);
249-
this.#validatedPlugins.add(plugin.id);
250-
}
251-
252243
let hookCategory: Partial<HardhatHooks[HookCategoryNameT]>;
253244

254245
if (typeof hookHandlerCategoryFactory === "string") {

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,15 @@ export class HardhatRuntimeEnvironmentImplementation
3737
// TODO: Or maybe don't clone at all
3838
const clonedUserConfig = inputUserConfig;
3939

40+
// Resolve plugins from node modules relative to the current working directory
41+
const basePathForNpmResolution = process.cwd();
42+
4043
const resolvedPlugins =
4144
unsafeOptions?.resolvedPlugins ??
42-
resolvePluginList(clonedUserConfig.plugins);
45+
(await resolvePluginList(
46+
clonedUserConfig.plugins,
47+
basePathForNpmResolution,
48+
));
4349

4450
const hooks = new HookManagerImplementation(resolvedPlugins);
4551

v-next/core/src/internal/plugins/plugin-validation.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createRequire } from "node:module";
22
import path from "node:path";
3-
import process from "node:process";
43

54
import { HardhatError } from "@nomicfoundation/hardhat-errors";
65
import { PackageJson } from "@nomicfoundation/hardhat-utils/package";
@@ -20,11 +19,7 @@ import { HardhatPlugin } from "../../types/plugins.js";
2019
*/
2120
export async function validatePluginNpmDependencies(
2221
plugin: HardhatPlugin,
23-
// TODO: When all loads where based on a hardhat config file
24-
// on disk we used the config file's directory as the base path
25-
// We need to decide if the current working directory is the right
26-
// option for module resolution now
27-
basePathForNpmResolution?: string,
22+
basePathForNpmResolution: string,
2823
): Promise<void> {
2924
if (plugin.npmPackage === undefined) {
3025
return;
@@ -91,10 +86,10 @@ export async function validatePluginNpmDependencies(
9186
*/
9287
function readPackageJsonViaNodeRequire(
9388
packageName: string,
94-
baseRequirePath?: string,
89+
baseRequirePath: string,
9590
): { packageJson: PackageJson; packagePath: string } | undefined {
9691
try {
97-
const require = createRequire(baseRequirePath ?? process.cwd());
92+
const require = createRequire(baseRequirePath);
9893

9994
const packagePath = require.resolve(path.join(packageName, "package.json"));
10095

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

+52-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import type { HardhatPlugin } from "../../types/plugins.js";
2+
13
import { HardhatError } from "@nomicfoundation/hardhat-errors";
4+
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
25

3-
import { HardhatPlugin } from "../../types/plugins.js";
6+
import { validatePluginNpmDependencies } from "./plugin-validation.js";
47

58
/**
69
* Resolves the plugin list, returning them in the right order.
710
*/
8-
export function resolvePluginList(
11+
export async function resolvePluginList(
912
userConfigPluginList: HardhatPlugin[] = [],
10-
): HardhatPlugin[] {
11-
return reverseTopologicalSort(userConfigPluginList);
13+
basePathForNpmResolution: string,
14+
): Promise<HardhatPlugin[]> {
15+
return reverseTopologicalSort(userConfigPluginList, basePathForNpmResolution);
1216
}
1317

1418
/**
@@ -19,13 +23,14 @@ export function resolvePluginList(
1923
* @param plugins The plugins.
2024
* @returns The ordered plugins.
2125
*/
22-
export function reverseTopologicalSort(
26+
async function reverseTopologicalSort(
2327
plugins: HardhatPlugin[],
24-
): HardhatPlugin[] {
28+
basePathForNpmResolution: string,
29+
): Promise<HardhatPlugin[]> {
2530
const visitedPlugins: Map<string, HardhatPlugin> = new Map();
2631
const result: HardhatPlugin[] = [];
2732

28-
function dfs(plugin: HardhatPlugin) {
33+
async function dfs(plugin: HardhatPlugin) {
2934
const visited = visitedPlugins.get(plugin.id);
3035

3136
if (visited !== undefined) {
@@ -42,17 +47,54 @@ export function reverseTopologicalSort(
4247
visitedPlugins.set(plugin.id, plugin);
4348

4449
if (plugin.dependencies !== undefined) {
45-
for (const dependency of plugin.dependencies) {
46-
dfs(dependency);
50+
for (const loadFn of plugin.dependencies) {
51+
const dependency = await loadDependency(
52+
plugin,
53+
loadFn,
54+
basePathForNpmResolution,
55+
);
56+
57+
await dfs(dependency);
4758
}
4859
}
4960

5061
result.push(plugin);
5162
}
5263

5364
for (const plugin of plugins) {
54-
dfs(plugin);
65+
await dfs(plugin);
5566
}
5667

5768
return result;
5869
}
70+
71+
/**
72+
* Attempt to load a plugins dependency. If there is an error,
73+
* first try and validate the npm dependencies of the plugin.
74+
*
75+
* @param plugin - the plugin has the dependency
76+
* @param loadFn - the load function for the dependency
77+
* @param basePathForNpmResolution - the directory path to use for node module resolution
78+
* @returns the loaded plugin
79+
*/
80+
async function loadDependency(
81+
plugin: HardhatPlugin,
82+
loadFn: () => Promise<HardhatPlugin>,
83+
basePathForNpmResolution: string,
84+
): Promise<HardhatPlugin> {
85+
try {
86+
return await loadFn();
87+
} catch (error) {
88+
ensureError(error);
89+
90+
await validatePluginNpmDependencies(plugin, basePathForNpmResolution);
91+
92+
throw new HardhatError(
93+
HardhatError.ERRORS.PLUGINS.PLUGIN_DEPENDENCY_FAILED_LOAD,
94+
{
95+
pluginId: plugin.id,
96+
},
97+
error,
98+
);
99+
}
100+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface HardhatPlugin {
3333
/**
3434
* An arary of plugins that this plugins depends on.
3535
*/
36-
dependencies?: HardhatPlugin[];
36+
dependencies?: Array<() => Promise<HardhatPlugin>>;
3737

3838
/**
3939
* An object with the different hook handlers that this plugin defines.

v-next/core/test/plugins/plugin-validation.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ describe("Plugins - plugin validation", () => {
1111
};
1212

1313
it("should skip validation if the plugin is not from an npm package", async () => {
14+
const peerDepWithWrongVersionFixture = import.meta.resolve(
15+
"./fixture-projects/peer-dep-with-wrong-version",
16+
);
17+
1418
await assert.doesNotReject(async () =>
15-
validatePluginNpmDependencies({
16-
...plugin,
17-
npmPackage: undefined,
18-
}),
19+
validatePluginNpmDependencies(
20+
{
21+
...plugin,
22+
npmPackage: undefined,
23+
},
24+
peerDepWithWrongVersionFixture,
25+
),
1926
);
2027
});
2128

0 commit comments

Comments
 (0)