In this section, we will explore the creation of plugins for Hardhat, which are the key component for integrating other tools and extending the built-in functionality.
Plugins are bits of reusable configuration. Anything that you can do in a plugin, can also be done in your config file. You can test your ideas in a config file, and move them into a plugin when ready.
When developing a plugin the main tools available to integrate new functionality are extending the Hardhat Runtime Environment, extending the Hardhat config, defining new tasks and overriding existing ones, which are all configuration actions achieved through code.
Some examples of things you could achieve by creating a plugin are running a linter when
the check
task runs, using different compiler versions for different files or
generating an UML diagram for your contracts.
Let’s go through the process of creating a plugin that adds new functionality to the Hardhat Runtime Environment. By doing this, we make sure our new feature is available everywhere. This means your plugin users can access it from tasks, tests, scripts, and the Hardhat console.
The Hardhat Runtime Environment (HRE) is configured through a queue of extension functions
that you can add to using the extendEnvironment()
function. It receives one parameter which is a callback which will be executed
after the HRE is initialized. If extendEnvironment
is called multiple times, its
callbacks will be executed in order.
For example, adding the following to hardhat.config.js
:
extendEnvironment((hre) => {
hre.hi = "Hello, Hardhat!";
});
Will make hi
available everywhere where the environment is accessible.
extendEnvironment((hre) => {
hre.hi = "Hello, Hardhat!";
});
task("envtest", (args, hre) => {
console.log(hre.hi);
});
module.exports = {};
Will yield:
$ npx hardhat envtest
Hello, Hardhat!
This is literally all it takes to put together a plugin for Hardhat. Now hi
is available to be used in
the Hardhat console, your tasks, tests and other plugins.
For a complete example of a plugin you can take a look at the Hardhat TypeScript plugin boilerplate project.
Plugins don't need to be written in TypeScript, but we recommend doing it, as many of our users use it. Creating a plugin in JavaScript can lead to a subpar experience for them.
To learn how to successfully extend the HRE in TypeScript, and to give your users type information about your extension, take a look at src/index.ts
in the boilerplate repo and read the Extending the HRE documentation.
Make sure to keep the type extension in your main file, as that convention is used across different plugins.
The boilerplate project also has an example on how to extend the Hardhat config.
We strongly recommend doing this in TypeScript and properly extending the config types.
An example on how to add fields to the Hardhat config can be found in src/index.ts
.
To show better stack traces to your users when an error is meant to interrupt a task's execution, please consider throwing HardhatPluginError
errors, which can be found in hardhat/plugins
.
If your error originated in your user's code, like a test or script calling one of your functions, you shouldn't use HardhatPluginError
.
Keeping startup time short is vital to give a good user experience.
To do so, Hardhat and its plugins delay any slow import or initialization until the very last moment. To do so, you can use lazyObject
, and lazyFunction
from hardhat/plugins
.
An example on how to use them is present in src/index.ts
.
Knowing when to use a dependency
or a peerDependency
can be tricky. We recommend these articles to learn about their distinctions.
If you are still in doubt, these can be helpful:
Rule of thumb #1: Hardhat MUST be a peer dependency.
Rule of thumb #2: If your plugin P depends on another plugin P2, P2 should be a peer dependency of P, and P2's peer dependencies should be peer dependencies of P.
Rule of thumb #3: If you have a non-Hardhat dependency that your users may require()
, it should be a peer dependency.
Rule of thumb #4: Every peerDependency
should also be a devDependency
.
To integrate into your users' existing workflow, we recommend plugin authors to override built-in tasks whenever it makes sense.
Examples of suggested overrides are:
compile
subtasks.check
task.clean
task.For a list of all the built-in tasks and subtasks please take a look at task-names.ts