hardhat 3.4.1 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/src/cli.js +5 -5
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/internal/builtin-plugins/artifacts/artifact-manager.d.ts.map +1 -1
  5. package/dist/src/internal/builtin-plugins/artifacts/artifact-manager.js +10 -3
  6. package/dist/src/internal/builtin-plugins/artifacts/artifact-manager.js.map +1 -1
  7. package/dist/src/internal/builtin-plugins/coverage/exports.d.ts +1 -1
  8. package/dist/src/internal/builtin-plugins/coverage/exports.d.ts.map +1 -1
  9. package/dist/src/internal/builtin-plugins/coverage/exports.js +1 -1
  10. package/dist/src/internal/builtin-plugins/coverage/exports.js.map +1 -1
  11. package/dist/src/internal/builtin-plugins/coverage/helpers/accessors.d.ts +7 -0
  12. package/dist/src/internal/builtin-plugins/coverage/helpers/accessors.d.ts.map +1 -0
  13. package/dist/src/internal/builtin-plugins/coverage/helpers/accessors.js +24 -0
  14. package/dist/src/internal/builtin-plugins/coverage/helpers/accessors.js.map +1 -0
  15. package/dist/src/internal/builtin-plugins/coverage/helpers/compat.d.ts +4 -0
  16. package/dist/src/internal/builtin-plugins/coverage/helpers/compat.d.ts.map +1 -0
  17. package/dist/src/internal/builtin-plugins/coverage/helpers/compat.js +27 -0
  18. package/dist/src/internal/builtin-plugins/coverage/helpers/compat.js.map +1 -0
  19. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/clean.js +1 -1
  20. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/clean.js.map +1 -1
  21. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/hre.d.ts.map +1 -1
  22. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/hre.js +18 -15
  23. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/hre.js.map +1 -1
  24. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.js +1 -1
  25. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.js.map +1 -1
  26. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/test.js +1 -1
  27. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/test.js.map +1 -1
  28. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/accessors.d.ts.map +1 -1
  29. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/accessors.js +10 -4
  30. package/dist/src/internal/builtin-plugins/gas-analytics/helpers/accessors.js.map +1 -1
  31. package/dist/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.d.ts.map +1 -1
  32. package/dist/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.js +18 -14
  33. package/dist/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.js.map +1 -1
  34. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.d.ts.map +1 -1
  35. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js +2 -7
  36. package/dist/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.js.map +1 -1
  37. package/dist/src/internal/builtin-plugins/network-manager/config-resolution.js +1 -1
  38. package/dist/src/internal/builtin-plugins/network-manager/config-resolution.js.map +1 -1
  39. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.d.ts +14 -0
  40. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.d.ts.map +1 -0
  41. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.js +40 -0
  42. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-constants.js.map +1 -0
  43. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.d.ts +1 -12
  44. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.d.ts.map +1 -1
  45. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.js +0 -39
  46. package/dist/src/internal/builtin-plugins/network-manager/edr/edr-provider.js.map +1 -1
  47. package/dist/src/internal/builtin-plugins/network-manager/edr/types/hardfork.d.ts.map +1 -1
  48. package/dist/src/internal/builtin-plugins/network-manager/edr/types/hardfork.js +2 -5
  49. package/dist/src/internal/builtin-plugins/network-manager/edr/types/hardfork.js.map +1 -1
  50. package/dist/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.js +1 -1
  51. package/dist/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.js.map +1 -1
  52. package/dist/src/internal/builtin-plugins/node/helpers.js +1 -1
  53. package/dist/src/internal/builtin-plugins/node/helpers.js.map +1 -1
  54. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.d.ts.map +1 -1
  55. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.js +50 -20
  56. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.js.map +1 -1
  57. package/dist/src/internal/builtin-plugins/solidity/hook-handlers/hre.d.ts.map +1 -1
  58. package/dist/src/internal/builtin-plugins/solidity/hook-handlers/hre.js +6 -1
  59. package/dist/src/internal/builtin-plugins/solidity/hook-handlers/hre.js.map +1 -1
  60. package/dist/src/internal/builtin-plugins/solidity-test/task-action.js +1 -1
  61. package/dist/src/internal/builtin-plugins/solidity-test/task-action.js.map +1 -1
  62. package/dist/src/internal/builtin-plugins/test/task-action.js +1 -1
  63. package/dist/src/internal/builtin-plugins/test/task-action.js.map +1 -1
  64. package/dist/src/internal/cli/init/init.d.ts.map +1 -1
  65. package/dist/src/internal/cli/init/init.js +17 -8
  66. package/dist/src/internal/cli/init/init.js.map +1 -1
  67. package/dist/src/internal/cli/init/template.d.ts.map +1 -1
  68. package/dist/src/internal/cli/init/template.js +5 -14
  69. package/dist/src/internal/cli/init/template.js.map +1 -1
  70. package/dist/src/internal/cli/node-version.d.ts +1 -1
  71. package/dist/src/internal/cli/node-version.d.ts.map +1 -1
  72. package/dist/src/internal/cli/node-version.js +16 -9
  73. package/dist/src/internal/cli/node-version.js.map +1 -1
  74. package/dist/src/internal/core/hook-manager.d.ts.map +1 -1
  75. package/dist/src/internal/core/hook-manager.js +194 -57
  76. package/dist/src/internal/core/hook-manager.js.map +1 -1
  77. package/dist/src/internal/core/hre.js +2 -2
  78. package/dist/src/internal/core/hre.js.map +1 -1
  79. package/dist/src/internal/core/lazy-user-interruptions.d.ts +11 -0
  80. package/dist/src/internal/core/lazy-user-interruptions.d.ts.map +1 -0
  81. package/dist/src/internal/core/lazy-user-interruptions.js +39 -0
  82. package/dist/src/internal/core/lazy-user-interruptions.js.map +1 -0
  83. package/package.json +2 -2
  84. package/src/cli.ts +5 -5
  85. package/src/internal/builtin-plugins/artifacts/artifact-manager.ts +12 -5
  86. package/src/internal/builtin-plugins/coverage/exports.ts +1 -1
  87. package/src/internal/builtin-plugins/coverage/helpers/accessors.ts +44 -0
  88. package/src/internal/builtin-plugins/coverage/helpers/compat.ts +37 -0
  89. package/src/internal/builtin-plugins/coverage/hook-handlers/clean.ts +1 -1
  90. package/src/internal/builtin-plugins/coverage/hook-handlers/hre.ts +26 -16
  91. package/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts +1 -1
  92. package/src/internal/builtin-plugins/coverage/hook-handlers/test.ts +1 -1
  93. package/src/internal/builtin-plugins/gas-analytics/helpers/accessors.ts +12 -5
  94. package/src/internal/builtin-plugins/gas-analytics/hook-handlers/hre.ts +29 -17
  95. package/src/internal/builtin-plugins/gas-analytics/snapshot-cheatcodes.ts +2 -6
  96. package/src/internal/builtin-plugins/network-manager/config-resolution.ts +1 -1
  97. package/src/internal/builtin-plugins/network-manager/edr/edr-constants.ts +61 -0
  98. package/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts +0 -59
  99. package/src/internal/builtin-plugins/network-manager/edr/types/hardfork.ts +3 -9
  100. package/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts +1 -1
  101. package/src/internal/builtin-plugins/node/helpers.ts +1 -1
  102. package/src/internal/builtin-plugins/solidity/build-system/build-system.ts +69 -43
  103. package/src/internal/builtin-plugins/solidity/hook-handlers/hre.ts +13 -4
  104. package/src/internal/builtin-plugins/solidity-test/task-action.ts +1 -1
  105. package/src/internal/builtin-plugins/test/task-action.ts +1 -1
  106. package/src/internal/cli/init/init.ts +31 -13
  107. package/src/internal/cli/init/template.ts +22 -27
  108. package/src/internal/cli/node-version.ts +19 -11
  109. package/src/internal/core/hook-manager.ts +265 -101
  110. package/src/internal/core/hre.ts +2 -2
  111. package/src/internal/core/lazy-user-interruptions.ts +75 -0
  112. package/templates/hardhat-3/01-node-test-runner-viem/package.json +2 -2
  113. package/templates/hardhat-3/02-mocha-ethers/package.json +2 -2
  114. package/templates/hardhat-3/03-minimal/package.json +1 -1
  115. package/dist/src/internal/builtin-plugins/coverage/helpers.d.ts +0 -15
  116. package/dist/src/internal/builtin-plugins/coverage/helpers.d.ts.map +0 -1
  117. package/dist/src/internal/builtin-plugins/coverage/helpers.js +0 -35
  118. package/dist/src/internal/builtin-plugins/coverage/helpers.js.map +0 -1
  119. package/src/internal/builtin-plugins/coverage/helpers.ts +0 -63
@@ -12,7 +12,6 @@ import {
12
12
  copy,
13
13
  ensureDir,
14
14
  exists,
15
- getAllFilesMatching,
16
15
  isDirectory,
17
16
  mkdir,
18
17
  readJsonFile,
@@ -435,17 +434,36 @@ export async function copyProjectFiles(
435
434
  template: Template,
436
435
  force?: boolean,
437
436
  ): Promise<void> {
438
- // Find all the files in the workspace that would have been overwritten by the template files
439
- const matchingRelativeWorkspacePaths = await getAllFilesMatching(
440
- workspace,
441
- (file) => {
442
- const relativeWorkspacePath = path.relative(workspace, file);
443
- const relativeTemplatePath = relativeWorkspaceToTemplatePath(
444
- relativeWorkspacePath,
445
- );
446
- return template.files.includes(relativeTemplatePath);
447
- },
448
- ).then((files) => files.map((f) => path.relative(workspace, f)));
437
+ // Find all the paths in the template that clash with an existing one in the
438
+ // workspace
439
+ const matchingRelativeWorkspacePaths = (
440
+ await Promise.all(
441
+ template.files.map(async (relativeTemplatePath) => {
442
+ const relativeWorkspacePath =
443
+ relativeTemplateToWorkspacePath(relativeTemplatePath);
444
+
445
+ const absoluteWorkspacePath = path.join(
446
+ workspace,
447
+ relativeWorkspacePath,
448
+ );
449
+
450
+ if (!(await exists(absoluteWorkspacePath))) {
451
+ return undefined;
452
+ }
453
+
454
+ // We ignore directories in this clash detection
455
+ if (await isDirectory(absoluteWorkspacePath)) {
456
+ return undefined;
457
+ }
458
+
459
+ return relativeWorkspacePath;
460
+ }),
461
+ )
462
+ ).filter((relativeWorkspacePath) => relativeWorkspacePath !== undefined);
463
+
464
+ const matchingRelativeWorkspacePathsSet = new Set(
465
+ matchingRelativeWorkspacePaths,
466
+ );
449
467
 
450
468
  // Ask the user for permission to overwrite existing files if needed
451
469
  if (matchingRelativeWorkspacePaths.length !== 0) {
@@ -461,7 +479,7 @@ export async function copyProjectFiles(
461
479
 
462
480
  if (
463
481
  force === false &&
464
- matchingRelativeWorkspacePaths.includes(relativeWorkspacePath)
482
+ matchingRelativeWorkspacePathsSet.has(relativeWorkspacePath)
465
483
  ) {
466
484
  continue;
467
485
  }
@@ -5,7 +5,7 @@ import {
5
5
  exists,
6
6
  getAllFilesMatching,
7
7
  isDirectory,
8
- readdir,
8
+ readdirOrEmpty,
9
9
  readJsonFile,
10
10
  } from "@nomicfoundation/hardhat-utils/fs";
11
11
  import {
@@ -40,11 +40,7 @@ export async function getTemplates(
40
40
  const packageRoot = await findClosestPackageRoot(import.meta.url);
41
41
  const pathToTemplates = path.join(packageRoot, "templates", templatesDir);
42
42
 
43
- if (!(await exists(pathToTemplates))) {
44
- return [];
45
- }
46
-
47
- const pathsToTemplates = await readdir(pathToTemplates);
43
+ const pathsToTemplates = await readdirOrEmpty(pathToTemplates);
48
44
  pathsToTemplates.sort();
49
45
 
50
46
  const templates = await Promise.all(
@@ -65,27 +61,26 @@ export async function getTemplates(
65
61
 
66
62
  const packageJson: PackageJson =
67
63
  await readJsonFile<PackageJson>(pathToPackageJson);
68
- const files = await getAllFilesMatching(pathToTemplate, (f) => {
69
- // Ignore the package.json file because it is handled separately
70
- if (f === pathToPackageJson) {
71
- return false;
72
- }
73
- // .gitignore files are expected to be called gitignore in the templates
74
- // because npm ignores .gitignore files during npm pack (see https://github.com/npm/npm/issues/3763)
75
- if (path.basename(f) === ".gitignore") {
76
- return false;
77
- }
78
- // We should ignore all the files according to the .gitignore rules
79
- // However, for simplicity, we just ignore the node_modules folder
80
- // If we needed to implement a more complex ignore logic, we could
81
- // use recently introduced glob from node:fs/promises
82
- if (
83
- path.relative(pathToTemplate, f).split(path.sep)[0] === "node_modules"
84
- ) {
85
- return false;
86
- }
87
- return true;
88
- }).then((fs) => fs.map((f) => path.relative(pathToTemplate, f)));
64
+
65
+ const matchingFiles = await getAllFilesMatching(
66
+ pathToTemplate,
67
+ (f) => {
68
+ // Ignore the package.json file because it is handled separately
69
+ if (f === pathToPackageJson) {
70
+ return false;
71
+ }
72
+
73
+ // .gitignore files are expected to be called gitignore in the templates
74
+ // because npm ignores .gitignore files during npm pack (see https://github.com/npm/npm/issues/3763)
75
+ if (path.basename(f) === ".gitignore") {
76
+ return false;
77
+ }
78
+ return true;
79
+ },
80
+ (dir) => path.basename(dir) !== "node_modules",
81
+ );
82
+
83
+ const files = matchingFiles.map((f) => path.relative(pathToTemplate, f));
89
84
 
90
85
  return {
91
86
  name,
@@ -2,9 +2,13 @@
2
2
  // is always run during the initialization of the CLI.
3
3
  //
4
4
  // NOTE: This file shouldn't import any non-builtin dependency, as it's imported
5
- // before enabling source maps support. TODO: Change chalk to util.styleText
5
+ // before enabling source maps support.
6
+ //
7
+ // EXCEPTION: we share `getRuntimeInfo` with the rest of the codebase instead
8
+ // of duplicating it. The helper has no transitive dependencies, so the risk of
9
+ // an unreadable stack trace from its import graph is negligible.
6
10
 
7
- import chalk from "chalk";
11
+ import { getRuntimeInfo } from "@nomicfoundation/hardhat-utils/runtime";
8
12
 
9
13
  export const MIN_SUPPORTED_NODE_VERSION: number[] = [22, 10, 0];
10
14
 
@@ -15,10 +19,6 @@ export function isNodeVersionSupported(): boolean {
15
19
  const minor = parseInt(minorStr, 10);
16
20
  const patch = parseInt(patchStr, 10);
17
21
 
18
- if (major % 2 === 1) {
19
- return false;
20
- }
21
-
22
22
  if (major < MIN_SUPPORTED_NODE_VERSION[0]) {
23
23
  return false;
24
24
  } else if (major > MIN_SUPPORTED_NODE_VERSION[0]) {
@@ -42,12 +42,20 @@ export function isNodeVersionSupported(): boolean {
42
42
  return true;
43
43
  }
44
44
 
45
- export function printNodeJsVersionWarningIfNecessary(): void {
45
+ export function exitIfNodeVersionNotSupported(): void {
46
+ // Only enforce the Node.js version when we're actually running on Node.js.
47
+ // Bun and Deno emulate `process.versions.node`, so checking it there would
48
+ // incorrectly reject users on those runtimes.
49
+ if (getRuntimeInfo()?.runtime !== "node") {
50
+ return;
51
+ }
52
+
46
53
  if (!isNodeVersionSupported()) {
47
- console.log(
48
- chalk.bold(`\n${chalk.yellow("WARNING:")} You are using Node.js ${process.versions.node} which is not supported by Hardhat.
49
- Please upgrade to ${MIN_SUPPORTED_NODE_VERSION.join(".")} or a later LTS version (even major version number)\n`),
54
+ console.error(
55
+ `\nERROR: You are using Node.js ${process.versions.node} which is not supported by Hardhat.\n` +
56
+ `Please upgrade to Node.js ${MIN_SUPPORTED_NODE_VERSION.join(".")} or later.\n`,
50
57
  );
51
- return;
58
+
59
+ process.exit(1);
52
60
  }
53
61
  }
@@ -23,35 +23,94 @@ export class HookManagerImplementation implements HookManager {
23
23
 
24
24
  readonly #projectRoot: string;
25
25
 
26
- readonly #pluginsInReverseOrder: HardhatPlugin[];
27
-
28
26
  /**
27
+ * The context passed to hook handlers, except to the `config` ones, to break
28
+ * a circular dependency between the config and the hook handler.
29
+ *
29
30
  * Initially `undefined` to be able to run the config hooks during
30
31
  * initialization.
31
32
  */
32
33
  #context: HookContext | undefined;
33
34
 
34
35
  /**
35
- * The initialized handler categories for each plugin.
36
+ * Plugins that provide hook handlers for each category, in reverse order.
37
+ *
38
+ * Precomputed from the plugin list at construction.
39
+ */
40
+ readonly #pluginsByHookCategory: Map<keyof HardhatHooks, HardhatPlugin[]> =
41
+ new Map();
42
+
43
+ /**
44
+ * Cached resolved category objects per hook category in reverse plugin
45
+ * order.
46
+ *
47
+ * Only written by #getStaticHookHandlerCategories, which uses a mutex to
48
+ * ensure that every Hook Category Factory is run once per HookManager
49
+ * instance.
36
50
  */
37
- readonly #staticHookHandlerCategories: Map<
38
- string,
39
- Map<keyof HardhatHooks, Partial<HardhatHooks[keyof HardhatHooks]>>
51
+ readonly #resolvedStaticCategories: Map<
52
+ keyof HardhatHooks,
53
+ Array<Partial<HardhatHooks[keyof HardhatHooks]>>
40
54
  > = new Map();
41
55
 
42
56
  /**
43
57
  * A map of the dynamically registered handler categories.
44
58
  *
45
59
  * Each array is a list of categories, in reverse order of registration.
60
+ *
61
+ * Written by registerHandlers and unregisterHandlers.
46
62
  */
47
63
  readonly #dynamicHookHandlerCategories: Map<
48
64
  keyof HardhatHooks,
49
65
  Array<Partial<HardhatHooks[keyof HardhatHooks]>>
50
66
  > = new Map();
51
67
 
68
+ /**
69
+ * Cached combined (dynamic + static) handlers per (category, hook name) in
70
+ * chained running order.
71
+ *
72
+ * Only written by #getHandlersInChainedRunningOrder, and invalidated
73
+ * per-category on dynamic handlers register/unregister.
74
+ */
75
+ readonly #chainedHandlers: Map<keyof HardhatHooks, Map<string, any[]>> =
76
+ new Map();
77
+
78
+ /**
79
+ * Cached combined handlers per (category, hook name) in sequential running
80
+ * order (reverse of chained).
81
+ *
82
+ * Only written by #getHandlersInSequentialRunningOrder, and invalidated
83
+ * per-category on dynamic handlers register/unregister.
84
+ */
85
+ readonly #sequentialHandlers: Map<keyof HardhatHooks, Map<string, any[]>> =
86
+ new Map();
87
+
52
88
  constructor(projectRoot: string, plugins: HardhatPlugin[]) {
53
89
  this.#projectRoot = projectRoot;
54
- this.#pluginsInReverseOrder = plugins.toReversed();
90
+
91
+ for (const plugin of plugins.toReversed()) {
92
+ if (plugin.hookHandlers === undefined) {
93
+ continue;
94
+ }
95
+
96
+ for (const hookCategoryName of Object.keys(plugin.hookHandlers) as Array<
97
+ keyof HardhatHooks
98
+ >) {
99
+ if (plugin.hookHandlers[hookCategoryName] === undefined) {
100
+ continue;
101
+ }
102
+
103
+ let pluginsForCategory =
104
+ this.#pluginsByHookCategory.get(hookCategoryName);
105
+
106
+ if (pluginsForCategory === undefined) {
107
+ pluginsForCategory = [];
108
+ this.#pluginsByHookCategory.set(hookCategoryName, pluginsForCategory);
109
+ }
110
+
111
+ pluginsForCategory.push(plugin);
112
+ }
113
+ }
55
114
  }
56
115
 
57
116
  public setContext(context: HookContext): void {
@@ -69,6 +128,8 @@ export class HookManagerImplementation implements HookManager {
69
128
  }
70
129
 
71
130
  categories.unshift(hookHandlerCategory);
131
+
132
+ this.#invalidateResolvedHandlersCache(hookCategoryName);
72
133
  }
73
134
 
74
135
  public unregisterHandlers<HookCategoryNameT extends keyof HardhatHooks>(
@@ -84,6 +145,8 @@ export class HookManagerImplementation implements HookManager {
84
145
  hookCategoryName,
85
146
  categories.filter((c) => c !== hookHandlerCategory),
86
147
  );
148
+
149
+ this.#invalidateResolvedHandlersCache(hookCategoryName);
87
150
  }
88
151
 
89
152
  public async runHandlerChain<
@@ -96,10 +159,23 @@ export class HookManagerImplementation implements HookManager {
96
159
  params: InitialChainedHookParams<HookCategoryNameT, HookT>,
97
160
  defaultImplementation: LastParameter<HookT>,
98
161
  ): Promise<Awaited<Return<HardhatHooks[HookCategoryNameT][HookNameT]>>> {
99
- const handlers = await this.#getHandlersInChainedRunningOrder(
100
- hookCategoryName,
101
- hookName,
102
- );
162
+ // Synchronous fast path for already cached handlers. This duplicates
163
+ // the check inside #getHandlersInChainedRunningOrder on purpose:
164
+ // calling that async method introduces a microtask tick even on a
165
+ // cache hit, whereas a direct Map lookup stays on the current tick.
166
+ // That tick matters here because runHandlerChain is on every hook's
167
+ // hot path, and this path pairs with the empty-handlers shortcut
168
+ // below to dispatch straight to defaultImplementation with no awaits.
169
+ const cachedHandlers = this.#chainedHandlers
170
+ .get(hookCategoryName)
171
+ ?.get(hookName as string);
172
+
173
+ const handlers =
174
+ cachedHandlers ??
175
+ (await this.#getHandlersInChainedRunningOrder(
176
+ hookCategoryName,
177
+ hookName,
178
+ ));
103
179
 
104
180
  let handlerParams: Parameters<typeof defaultImplementation>;
105
181
  if (hookCategoryName !== "config") {
@@ -113,6 +189,13 @@ export class HookManagerImplementation implements HookManager {
113
189
  handlerParams = params as any;
114
190
  }
115
191
 
192
+ // Fast path for the common case of no registered handlers: skip building
193
+ // handlerParams and the `next` closure, and call the default implementation
194
+ // directly.
195
+ if (handlers.length === 0) {
196
+ return (await defaultImplementation(...handlerParams)) as any;
197
+ }
198
+
116
199
  const numberOfHandlers = handlers.length;
117
200
  let index = 0;
118
201
  const next = async (...nextParams: typeof handlerParams) => {
@@ -220,14 +303,48 @@ export class HookManagerImplementation implements HookManager {
220
303
  hookCategoryName: HookCategoryNameT,
221
304
  hookName: HookNameT,
222
305
  ): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
223
- const pluginHooks = await this.#getPluginHooks(hookCategoryName, hookName);
306
+ let handlersByName = this.#chainedHandlers.get(hookCategoryName);
307
+ if (handlersByName === undefined) {
308
+ handlersByName = new Map();
309
+ this.#chainedHandlers.set(hookCategoryName, handlersByName);
310
+ }
224
311
 
225
- const dynamicHooks = await this.#getDynamicHooks(
312
+ const cached = handlersByName.get(hookName as string);
313
+ if (cached !== undefined) {
314
+ return cached;
315
+ }
316
+
317
+ const staticCategories =
318
+ await this.#getStaticHookHandlerCategories(hookCategoryName);
319
+
320
+ // IMPORTANT NOTE: Accessing the dynamic hook handlers MUST happen
321
+ // after awaiting the static ones. See
322
+ // #invalidateResolvedHandlersCache for more info.
323
+ const dynamicCategories = this.#dynamicHookHandlerCategories.get(
226
324
  hookCategoryName,
227
- hookName,
228
- );
325
+ ) as Array<Partial<HardhatHooks[HookCategoryNameT]>> | undefined;
326
+
327
+ const handlers: Array<HardhatHooks[HookCategoryNameT][HookNameT]> = [];
229
328
 
230
- return [...dynamicHooks, ...pluginHooks];
329
+ if (dynamicCategories !== undefined) {
330
+ for (const category of dynamicCategories) {
331
+ const handler = category[hookName];
332
+ if (handler !== undefined) {
333
+ handlers.push(handler as HardhatHooks[HookCategoryNameT][HookNameT]);
334
+ }
335
+ }
336
+ }
337
+
338
+ for (const category of staticCategories) {
339
+ const handler = category[hookName];
340
+ if (handler !== undefined) {
341
+ handlers.push(handler as HardhatHooks[HookCategoryNameT][HookNameT]);
342
+ }
343
+ }
344
+
345
+ handlersByName.set(hookName as string, handlers);
346
+
347
+ return handlers;
231
348
  }
232
349
 
233
350
  async #getHandlersInSequentialRunningOrder<
@@ -237,111 +354,158 @@ export class HookManagerImplementation implements HookManager {
237
354
  hookCategoryName: HookCategoryNameT,
238
355
  hookName: HookNameT,
239
356
  ): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
240
- const handlersInChainedOrder = await this.#getHandlersInChainedRunningOrder(
357
+ let handlersByName = this.#sequentialHandlers.get(hookCategoryName);
358
+ if (handlersByName === undefined) {
359
+ handlersByName = new Map();
360
+ this.#sequentialHandlers.set(hookCategoryName, handlersByName);
361
+ }
362
+
363
+ const cached = handlersByName.get(hookName as string);
364
+ if (cached !== undefined) {
365
+ return cached;
366
+ }
367
+
368
+ const chained = await this.#getHandlersInChainedRunningOrder(
241
369
  hookCategoryName,
242
370
  hookName,
243
371
  );
244
372
 
245
- return handlersInChainedOrder.reverse();
373
+ const sequential = chained.toReversed();
374
+
375
+ handlersByName.set(hookName as string, sequential);
376
+
377
+ return sequential;
246
378
  }
247
379
 
248
- async #getDynamicHooks<
380
+ async #getStaticHookHandlerCategories<
249
381
  HookCategoryNameT extends keyof HardhatHooks,
250
- HookNameT extends keyof HardhatHooks[HookCategoryNameT],
251
382
  >(
252
383
  hookCategoryName: HookCategoryNameT,
253
- hookName: HookNameT,
254
- ): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
255
- const categories = this.#dynamicHookHandlerCategories.get(
256
- hookCategoryName,
257
- ) as Array<Partial<HardhatHooks[HookCategoryNameT]>> | undefined;
384
+ ): Promise<Array<Partial<HardhatHooks[HookCategoryNameT]>>> {
385
+ const cached = this.#resolvedStaticCategories.get(hookCategoryName) as
386
+ | Array<Partial<HardhatHooks[HookCategoryNameT]>>
387
+ | undefined;
258
388
 
259
- if (categories === undefined) {
389
+ if (cached !== undefined) {
390
+ return cached;
391
+ }
392
+
393
+ const plugins = this.#pluginsByHookCategory.get(hookCategoryName);
394
+
395
+ // We don't need to get the mutex to resolve this case, as it will always
396
+ // be an empty array, and won't execute any factory.
397
+ if (plugins === undefined) {
398
+ this.#resolvedStaticCategories.set(hookCategoryName, []);
260
399
  return [];
261
400
  }
262
401
 
263
- return categories.flatMap((hookCategory) => {
264
- return (hookCategory[hookName] ?? []) as Array<
265
- HardhatHooks[HookCategoryNameT][HookNameT]
266
- >;
402
+ return await this.#mutex.exclusiveRun(async () => {
403
+ // Re-check under the mutex in case another caller just populated it.
404
+ const recheck = this.#resolvedStaticCategories.get(hookCategoryName) as
405
+ | Array<Partial<HardhatHooks[HookCategoryNameT]>>
406
+ | undefined;
407
+
408
+ if (recheck !== undefined) {
409
+ return recheck;
410
+ }
411
+
412
+ const resolved = await Promise.all(
413
+ plugins.map(
414
+ async (plugin) =>
415
+ await this.#getPluginStaticHookCategory(plugin, hookCategoryName),
416
+ ),
417
+ );
418
+
419
+ this.#resolvedStaticCategories.set(hookCategoryName, resolved);
420
+
421
+ return resolved;
267
422
  });
268
423
  }
269
424
 
270
- async #getPluginHooks<
425
+ /**
426
+ * Returns the hook category object for a plugin that has the hook category
427
+ * defined.
428
+ *
429
+ * @param plugin A plugin that MUST have the given hook category defined.
430
+ * @param hookCategoryName The name of the hook category.
431
+ * @returns The hook category object.
432
+ */
433
+ async #getPluginStaticHookCategory<
271
434
  HookCategoryNameT extends keyof HardhatHooks,
272
- HookNameT extends keyof HardhatHooks[HookCategoryNameT],
273
435
  >(
436
+ plugin: HardhatPlugin,
274
437
  hookCategoryName: HookCategoryNameT,
275
- hookName: HookNameT,
276
- ): Promise<Array<HardhatHooks[HookCategoryNameT][HookNameT]>> {
277
- const categories: Array<
278
- Partial<HardhatHooks[HookCategoryNameT]> | undefined
279
- > = await this.#mutex.exclusiveRun(async () => {
280
- return await Promise.all(
281
- this.#pluginsInReverseOrder.map(async (plugin) => {
282
- const existingCategory = this.#staticHookHandlerCategories
283
- .get(plugin.id)
284
- ?.get(hookCategoryName);
285
-
286
- if (existingCategory !== undefined) {
287
- return existingCategory as Partial<HardhatHooks[HookCategoryNameT]>;
288
- }
289
-
290
- const hookHandlerCategoryFactory =
291
- plugin.hookHandlers?.[hookCategoryName];
292
-
293
- if (hookHandlerCategoryFactory === undefined) {
294
- return;
295
- }
296
-
297
- let factory;
298
- try {
299
- factory = (await hookHandlerCategoryFactory()).default;
300
- } catch (error) {
301
- ensureError(error);
302
-
303
- await detectPluginNpmDependencyProblems(
304
- this.#projectRoot,
305
- plugin,
306
- error,
307
- );
308
-
309
- throw error;
310
- }
311
-
312
- assertHardhatInvariant(
313
- typeof factory === "function",
314
- `Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName}`,
315
- );
316
-
317
- const hookCategory = await factory();
318
-
319
- assertHardhatInvariant(
320
- hookCategory !== null && typeof hookCategory === "object",
321
- `Plugin ${plugin.id} doesn't export a valid factory for category ${hookCategoryName}, it didn't return an object`,
322
- );
323
-
324
- if (!this.#staticHookHandlerCategories.has(plugin.id)) {
325
- this.#staticHookHandlerCategories.set(plugin.id, new Map());
326
- }
327
-
328
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Defined right above
329
- this.#staticHookHandlerCategories
330
- .get(plugin.id)!
331
- .set(hookCategoryName, hookCategory);
332
-
333
- return hookCategory;
334
- }),
335
- );
336
- });
438
+ ): Promise<Partial<HardhatHooks[HookCategoryNameT]>> {
439
+ const hookHandlerCategoryFactory = plugin.hookHandlers?.[hookCategoryName];
337
440
 
338
- return categories.flatMap((category) => {
339
- const handler = category?.[hookName];
340
- if (handler === undefined) {
341
- return [];
342
- }
441
+ assertHardhatInvariant(
442
+ hookHandlerCategoryFactory !== undefined,
443
+ "#pluginsByHookCategory only contains plugins with this hook category",
444
+ );
343
445
 
344
- return handler as HardhatHooks[HookCategoryNameT][HookNameT];
345
- });
446
+ let factory;
447
+ try {
448
+ factory = (await hookHandlerCategoryFactory()).default;
449
+ } catch (error) {
450
+ ensureError(error);
451
+
452
+ await detectPluginNpmDependencyProblems(this.#projectRoot, plugin, error);
453
+
454
+ throw error;
455
+ }
456
+
457
+ assertHardhatInvariant(
458
+ typeof factory === "function",
459
+ `Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName}`,
460
+ );
461
+
462
+ const hookCategory = await factory();
463
+
464
+ assertHardhatInvariant(
465
+ hookCategory !== null && typeof hookCategory === "object",
466
+ `Plugin ${plugin.id} doesn't export a valid factory for category ${hookCategoryName}, it didn't return an object`,
467
+ );
468
+
469
+ return hookCategory;
470
+ }
471
+
472
+ #invalidateResolvedHandlersCache<
473
+ HookCategoryNameT extends keyof HardhatHooks,
474
+ >(hookCategoryName: HookCategoryNameT) {
475
+ // Invalidation deletes the outer entry rather than clearing the inner
476
+ // map. This matters under concurrency.
477
+ //
478
+ // A reader of #getHandlersInChainedRunningOrder (or its sequential
479
+ // sibling) captures a reference to the inner map before awaiting the
480
+ // static categories, and writes its computed array back after the
481
+ // await. If invalidation runs during that await, deleting the outer
482
+ // entry leaves the reader's inner map orphaned: its write lands in a
483
+ // map no longer reachable from #chainedHandlers/#sequentialHandlers,
484
+ // so it cannot poison the shared cache. The next reader sees
485
+ // `undefined`, installs a fresh inner map, and rebuilds from the
486
+ // current dynamic state.
487
+ //
488
+ // Two distinct properties make this safe, guaranteed by two different
489
+ // things:
490
+ //
491
+ // 1. The in-flight reader's own return value is correct. This is
492
+ // because #getHandlersInChainedRunningOrder reads
493
+ // #dynamicHookHandlerCategories *after* awaiting the static
494
+ // categories. Any invalidation that happened during the await is
495
+ // visible to the reader when it resumes, so the array it builds
496
+ // reflects the current dynamic state.
497
+ //
498
+ // 2. The shared cache never holds a stale array. This is guaranteed
499
+ // by the orphaning-by-delete described above: a reader that
500
+ // started before the invalidation can only write into an
501
+ // unreachable inner map.
502
+ //
503
+ // Property 1 depends on the ordering of the dynamic handlers read relative
504
+ // to the await. If that read ever moved *before* the await, a reader
505
+ // could build a stale array and return it to its caller — the cache
506
+ // would still be protected by property 2, but the reader's caller
507
+ // would see the stale result.
508
+ this.#chainedHandlers.delete(hookCategoryName);
509
+ this.#sequentialHandlers.delete(hookCategoryName);
346
510
  }
347
511
  }
@@ -40,9 +40,9 @@ import {
40
40
  resolveGlobalOptions,
41
41
  } from "./global-options.js";
42
42
  import { HookManagerImplementation } from "./hook-manager.js";
43
+ import { LazyUserInterruptionManager } from "./lazy-user-interruptions.js";
43
44
  import { resolvePluginList } from "./plugins/resolve-plugin-list.js";
44
45
  import { TaskManagerImplementation } from "./tasks/task-manager.js";
45
- import { UserInterruptionManagerImplementation } from "./user-interruptions.js";
46
46
 
47
47
  export class HardhatRuntimeEnvironmentImplementation
48
48
  implements HardhatRuntimeEnvironment
@@ -134,7 +134,7 @@ export class HardhatRuntimeEnvironmentImplementation
134
134
 
135
135
  // Set the HookContext in the hook manager so that non-config hooks can
136
136
  // use it
137
- const interruptions = new UserInterruptionManagerImplementation(hooks);
137
+ const interruptions = new LazyUserInterruptionManager(hooks);
138
138
 
139
139
  const hre = new HardhatRuntimeEnvironmentImplementation(
140
140
  extendedUserConfig,