Skip to content

Commit d414eda

Browse files
committed
feat: conditional dependencies
1 parent 3022259 commit d414eda

File tree

4 files changed

+251
-3
lines changed

4 files changed

+251
-3
lines changed

.changeset/lemon-days-compare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Support for conditional dependencies on plugins ([#7424](https://github.com/NomicFoundation/hardhat/issues/7424))

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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async function reverseTopologicalSort(
2828
plugins: HardhatPlugin[],
2929
): Promise<HardhatPlugin[]> {
3030
const visitedPlugins: Map<string, HardhatPlugin> = new Map();
31-
const result: HardhatPlugin[] = [];
31+
const resolvedPlugins: HardhatPlugin[] = [];
3232

3333
async function dfs(plugin: HardhatPlugin) {
3434
const visited = visitedPlugins.get(plugin.id);
@@ -69,12 +69,49 @@ async function reverseTopologicalSort(
6969
}
7070
}
7171

72-
result.push(plugin);
72+
resolvedPlugins.push(plugin);
7373
}
7474

7575
for (const plugin of plugins) {
7676
await dfs(plugin);
7777
}
7878

79-
return result;
79+
// Resolve conditional dependencies iteratively
80+
let lastResolvedCount = -1;
81+
82+
while (resolvedPlugins.length !== lastResolvedCount) {
83+
lastResolvedCount = resolvedPlugins.length;
84+
85+
for (const plugin of resolvedPlugins) {
86+
if (plugin.conditionalDependencies === undefined) {
87+
continue;
88+
}
89+
90+
for (const conditionalDependency of plugin.conditionalDependencies) {
91+
try {
92+
// Check all condition plugins are installed
93+
const conditionModules = await Promise.all(
94+
conditionalDependency.condition(),
95+
);
96+
97+
// Check all condition plugins are loaded
98+
if (
99+
conditionModules.some(
100+
(conditionPlugin) =>
101+
!resolvedPlugins.includes(conditionPlugin.default),
102+
)
103+
) {
104+
continue;
105+
}
106+
107+
// Load the conditional dependency
108+
const pluginModule = await conditionalDependency.plugin();
109+
110+
await dfs(pluginModule.default);
111+
} catch (_error) {}
112+
}
113+
}
114+
}
115+
116+
return resolvedPlugins;
80117
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export interface HardhatPlugin {
3939
*/
4040
dependencies?: () => Array<Promise<{ default: HardhatPlugin }>>;
4141

42+
conditionalDependencies?: Array<{
43+
condition: () => Array<Promise<{ default: HardhatPlugin }>>;
44+
plugin: () => Promise<{ default: HardhatPlugin }>;
45+
}>;
46+
4247
/**
4348
* An object with the different hook handlers that this plugin defines.
4449
*

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

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable prefer-const -- test*/
12
import type { HardhatPlugin } from "../../../../src/types/plugins.js";
23

34
import assert from "node:assert/strict";
@@ -244,3 +245,203 @@ describe("Plugins - resolve plugin list", () => {
244245
});
245246
});
246247
});
248+
249+
describe("Plugins - resolve plugin list - conditional dependencies", () => {
250+
// Helper function that simulates loading a module that returns a plugin
251+
async function mod(plugin: HardhatPlugin) {
252+
return { default: plugin };
253+
}
254+
255+
let A: HardhatPlugin;
256+
let B: HardhatPlugin;
257+
let C: HardhatPlugin;
258+
let D: HardhatPlugin;
259+
let E: HardhatPlugin;
260+
let F: HardhatPlugin;
261+
let G: HardhatPlugin;
262+
let H: HardhatPlugin;
263+
let I: HardhatPlugin;
264+
let J: HardhatPlugin;
265+
let K: HardhatPlugin;
266+
let L: HardhatPlugin;
267+
let M: HardhatPlugin;
268+
269+
A = {
270+
id: "A",
271+
conditionalDependencies: [
272+
{
273+
condition: () => [mod(B)],
274+
plugin: () => mod(D),
275+
},
276+
],
277+
};
278+
279+
B = {
280+
id: "B",
281+
conditionalDependencies: [
282+
{
283+
condition: () => [mod(E), mod(G)],
284+
plugin: () => mod(F),
285+
},
286+
],
287+
};
288+
289+
C = {
290+
id: "C",
291+
dependencies: () => {
292+
return [mod(B)];
293+
// return [];
294+
},
295+
};
296+
297+
D = {
298+
id: "D",
299+
dependencies: () => {
300+
return [mod(E)];
301+
},
302+
};
303+
304+
E = {
305+
id: "E",
306+
dependencies: () => {
307+
return [];
308+
},
309+
conditionalDependencies: [
310+
{
311+
condition: () => [mod(D)],
312+
plugin: () => mod(G),
313+
},
314+
],
315+
};
316+
317+
F = {
318+
id: "F",
319+
conditionalDependencies: [
320+
{
321+
condition: () => [import(`${"nonexistant"}`)],
322+
plugin: () => mod(G),
323+
},
324+
],
325+
};
326+
327+
G = {
328+
id: "G",
329+
};
330+
331+
H = {
332+
id: "H",
333+
conditionalDependencies: [
334+
{
335+
condition: () => [mod(G)],
336+
plugin: () => mod(F),
337+
},
338+
],
339+
};
340+
341+
I = {
342+
id: "I",
343+
conditionalDependencies: [
344+
{
345+
condition: () => [mod(B)],
346+
plugin: () => mod(G),
347+
},
348+
],
349+
};
350+
351+
J = {
352+
id: "J",
353+
conditionalDependencies: [
354+
{
355+
condition: () => [mod(G)],
356+
plugin: () => mod(C),
357+
},
358+
{
359+
condition: () => [mod(B)],
360+
plugin: () => mod(F),
361+
},
362+
],
363+
};
364+
365+
K = {
366+
id: "K",
367+
conditionalDependencies: [
368+
{
369+
condition: () => [mod(G)],
370+
plugin: () => mod(L),
371+
},
372+
{
373+
condition: () => [mod(L)],
374+
plugin: () => mod(M),
375+
},
376+
{
377+
condition: () => [mod(M)],
378+
plugin: () => mod(F),
379+
},
380+
],
381+
};
382+
383+
L = {
384+
id: "L",
385+
conditionalDependencies: [
386+
{
387+
condition: () => [mod(B)],
388+
plugin: () => mod(G),
389+
},
390+
],
391+
};
392+
393+
M = {
394+
id: "M",
395+
conditionalDependencies: [
396+
{
397+
condition: () => [mod(B)],
398+
plugin: () => mod(G),
399+
},
400+
],
401+
};
402+
403+
// Helper function to get plugin ids
404+
function ids(plugins: HardhatPlugin[]) {
405+
return plugins.map((p) => p.id);
406+
}
407+
408+
it("doesn't load a conditional dependency if the conditions are not met", async () => {
409+
const resolvedPlugins = await resolvePluginList(process.cwd(), [A]);
410+
assert.deepEqual(resolvedPlugins, [A]);
411+
});
412+
413+
it("doesn't load a conditional dependency if only one of the conditions are not met", async () => {
414+
const resolvedPlugins = await resolvePluginList(process.cwd(), [B, G]);
415+
assert.deepEqual(resolvedPlugins, [B, G]);
416+
});
417+
418+
it("doesn't load a conditional dependency if one of the conditions is not installed", async () => {
419+
const resolvedPlugins = await resolvePluginList(process.cwd(), [F]);
420+
assert.deepEqual(resolvedPlugins, [F]);
421+
});
422+
423+
it("loads a conditional dependency if the condition is met, on the initial plugin list", async () => {
424+
const resolvedPlugins = await resolvePluginList(process.cwd(), [D, E]);
425+
assert.deepEqual(ids(resolvedPlugins), ["E", "D", "G"]); // D is loaded initially, E conditionally loads G
426+
});
427+
428+
it("loads a conditional dependency if the condition is met, from a dependency from the initial list", async () => {
429+
const resolvedPlugins = await resolvePluginList(process.cwd(), [C, I]);
430+
assert.deepEqual(ids(resolvedPlugins), ["B", "C", "I", "G"]); // C loads B, I conditionally loads G because of B
431+
});
432+
433+
it("loads a conditional dependency if the condition is met, from a conditional dependency of a plugin on the initial list", async () => {
434+
const resolvedPlugins = await resolvePluginList(process.cwd(), [B, I, H]);
435+
assert.deepEqual(ids(resolvedPlugins), ["B", "I", "H", "G", "F"]); // I conditionally loads G because of B, then H conditionally loads F because of G
436+
});
437+
438+
it("loads a conditional dependency if the condition is met, from a dependency of a conditional dependency", async () => {
439+
const resolvedPlugins = await resolvePluginList(process.cwd(), [J, G]);
440+
assert.deepEqual(ids(resolvedPlugins), ["J", "G", "B", "C", "F"]); // J conditionally loads C because of G. C loads B, J loads F because of B
441+
});
442+
443+
it("loads a conditional dependency if the condition is met, from a conditional dependency of a conditional dependency", async () => {
444+
const resolvedPlugins = await resolvePluginList(process.cwd(), [K, G]);
445+
assert.deepEqual(ids(resolvedPlugins), ["K", "G", "L", "M", "F"]); // K loads L because of G, then M because of L, then F because of M
446+
});
447+
});

0 commit comments

Comments
 (0)