From eb9595fef4cde2d5d8a2631605c66e8835319750 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Thu, 11 Sep 2025 13:46:36 +0000 Subject: [PATCH 01/12] Turn into a monorepo --- .gitignore | 16 ----- TODO.md | 4 -- package.json | 57 +----------------- packages/example-project/.gitignore | 12 ++++ packages/example-project/README.md | 1 + .../example-project/hardhat.config.ts | 2 +- packages/example-project/package.json | 15 +++++ packages/example-project/tsconfig.json | 20 +++++++ packages/plugin/.gitignore | 12 ++++ .../plugin/.prettierignore | 0 LICENSE => packages/plugin/LICENSE | 0 README.md => packages/plugin/README.md | 0 .../plugin/eslint.config.js | 0 packages/plugin/package.json | 58 +++++++++++++++++++ {src => packages/plugin/src}/hooks/network.ts | 4 +- {src => packages/plugin/src}/index.ts | 6 +- {test => packages/plugin/test}/index.ts | 0 .../plugin/tsconfig.json | 1 + pnpm-lock.yaml | 37 +++++++----- pnpm-workspace.yaml | 2 + 20 files changed, 151 insertions(+), 96 deletions(-) create mode 100644 packages/example-project/.gitignore create mode 100644 packages/example-project/README.md rename hardhat.config.ts => packages/example-project/hardhat.config.ts (72%) create mode 100644 packages/example-project/package.json create mode 100644 packages/example-project/tsconfig.json create mode 100644 packages/plugin/.gitignore rename .prettierignore => packages/plugin/.prettierignore (100%) rename LICENSE => packages/plugin/LICENSE (100%) rename README.md => packages/plugin/README.md (100%) rename eslint.config.js => packages/plugin/eslint.config.js (100%) create mode 100644 packages/plugin/package.json rename {src => packages/plugin/src}/hooks/network.ts (84%) rename {src => packages/plugin/src}/index.ts (86%) rename {test => packages/plugin/test}/index.ts (100%) rename tsconfig.json => packages/plugin/tsconfig.json (94%) create mode 100644 pnpm-workspace.yaml diff --git a/.gitignore b/.gitignore index cac68ea..2ff0549 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,2 @@ # Node modules /node_modules - -# Compilation output -/dist - -# pnpm deploy output -/bundle - -# test coverage output -coverage - -# all the tmp folders in the fixture projects -/test/fixture-projects/tmp/ - -# Hardhat project files -/artifacts -/cache diff --git a/TODO.md b/TODO.md index 09104ca..ae43d52 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1 @@ -[] package.json: Repository? -[] package.json: Homepage? -[] package.json: Author? [] dependencies: hardhat-test-utils? hardhat-plugin-testing-utils? -[] tsconfig: "isolatedModules": true, "isolatedDeclarations": true,? too pedantic? diff --git a/package.json b/package.json index 142fda4..71f31b1 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,4 @@ { - "name": "hardhat-plugin-template", - "version": "1.0.0", - "description": "Hardhat 3 plugin template", - "license": "MIT", - "type": "module", - "types": "dist/src/index.d.ts", - "exports": { - ".": "./dist/src/index.js" - }, - "keywords": [ - "ethereum", - "smart-contracts", - "hardhat", - "hardhat3", - "hardhat-plugin" - ], - "scripts": { - "lint": "pnpm prettier --check && pnpm eslint", - "lint:fix": "pnpm prettier --write && pnpm eslint --fix", - "eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", - "prettier": "prettier \"**/*.{ts,js,md,json}\"", - "test": "node --import tsx/esm --test --test-reporter=@nomicfoundation/hardhat-node-test-reporter \"test/*.ts\" \"test/!(fixture-projects|helpers)/**/*.ts\"", - "test:only": "node --import tsx/esm --test --test-only --test-reporter=@nomicfoundation/hardhat-node-test-reporter \"test/*.ts\" \"test/!(fixture-projects|helpers)/**/*.ts\"", - "test:coverage": "c8 --reporter html --reporter text --all --exclude test --exclude \"src/**/{types,type-extensions}.ts\" --src src node --import tsx/esm --test --test-reporter=@nomicfoundation/hardhat-node-test-reporter \"test/*.ts\" \"test/!(fixture-projects|helpers)/**/*.ts\"", - "pretest": "pnpm build", - "pretest:only": "pnpm build", - "build": "tsc --build .", - "prepublishOnly": "pnpm build", - "clean": "rimraf dist" - }, - "files": [ - "dist/src/", - "src/", - "CHANGELOG.md", - "LICENSE", - "README.md" - ], - "devDependencies": { - "@eslint/js": "^9.35.0", - "@nomicfoundation/hardhat-node-test-reporter": "^3.0.0", - "@tsconfig/node22": "^22.0.2", - "@types/node": "^20.14.9", - "c8": "^9.1.0", - "eslint": "^9.35.0", - "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.32.0", - "prettier": "3.2.5", - "rimraf": "^5.0.5", - "tsx": "^4.19.3", - "typescript": "~5.8.0", - "typescript-eslint": "^8.43.0" - }, - "peerDependencies": { - "hardhat": "^3.0.6" - } + "name": "hardhat3-plugin-template", + "private": true } diff --git a/packages/example-project/.gitignore b/packages/example-project/.gitignore new file mode 100644 index 0000000..b10ecff --- /dev/null +++ b/packages/example-project/.gitignore @@ -0,0 +1,12 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# test coverage output +/coverage + +# Hardhat files +/artifacts +/cache diff --git a/packages/example-project/README.md b/packages/example-project/README.md new file mode 100644 index 0000000..3ea0be5 --- /dev/null +++ b/packages/example-project/README.md @@ -0,0 +1 @@ +# Example project that uses your plugin diff --git a/hardhat.config.ts b/packages/example-project/hardhat.config.ts similarity index 72% rename from hardhat.config.ts rename to packages/example-project/hardhat.config.ts index 82d0b37..085fb0a 100644 --- a/hardhat.config.ts +++ b/packages/example-project/hardhat.config.ts @@ -1,5 +1,5 @@ import { HardhatUserConfig } from "hardhat/config"; -import myPlugin from "./src/index.js"; +import myPlugin from "hardhat-plugin-template"; export default { plugins: [myPlugin], diff --git a/packages/example-project/package.json b/packages/example-project/package.json new file mode 100644 index 0000000..a3530d0 --- /dev/null +++ b/packages/example-project/package.json @@ -0,0 +1,15 @@ +{ + "name": "example-project", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc --build" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "hardhat": "^3.0.6", + "hardhat-plugin-template": "workspace:*", + "typescript": "~5.8.0" + } +} diff --git a/packages/example-project/tsconfig.json b/packages/example-project/tsconfig.json new file mode 100644 index 0000000..f0ea66b --- /dev/null +++ b/packages/example-project/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "outDir": "${configDir}/dist", + "declaration": true, + "declarationMap": true, + "forceConsistentCasingInFileNames": true, + "noEmitOnError": true, + "noImplicitOverride": true, + "noUncheckedSideEffectImports": true, + "skipDefaultLibCheck": true, + "sourceMap": true, + "composite": true, + "incremental": true, + "isolatedModules": true, + "typeRoots": ["${configDir}/node_modules/@types"] + }, + "exclude": ["${configDir}/dist", "${configDir}/node_modules"], + "references": [{ "path": "../plugin" }] +} diff --git a/packages/plugin/.gitignore b/packages/plugin/.gitignore new file mode 100644 index 0000000..b10ecff --- /dev/null +++ b/packages/plugin/.gitignore @@ -0,0 +1,12 @@ +# Node modules +/node_modules + +# Compilation output +/dist + +# test coverage output +/coverage + +# Hardhat files +/artifacts +/cache diff --git a/.prettierignore b/packages/plugin/.prettierignore similarity index 100% rename from .prettierignore rename to packages/plugin/.prettierignore diff --git a/LICENSE b/packages/plugin/LICENSE similarity index 100% rename from LICENSE rename to packages/plugin/LICENSE diff --git a/README.md b/packages/plugin/README.md similarity index 100% rename from README.md rename to packages/plugin/README.md diff --git a/eslint.config.js b/packages/plugin/eslint.config.js similarity index 100% rename from eslint.config.js rename to packages/plugin/eslint.config.js diff --git a/packages/plugin/package.json b/packages/plugin/package.json new file mode 100644 index 0000000..b635ae3 --- /dev/null +++ b/packages/plugin/package.json @@ -0,0 +1,58 @@ +{ + "name": "hardhat-plugin-template", + "version": "1.0.0", + "description": "Hardhat 3 plugin template", + "license": "MIT", + "type": "module", + "types": "dist/src/index.d.ts", + "exports": { + ".": "./dist/src/index.js" + }, + "keywords": [ + "ethereum", + "smart-contracts", + "hardhat", + "hardhat3", + "hardhat-plugin" + ], + "scripts": { + "lint": "pnpm prettier --check && pnpm eslint", + "lint:fix": "pnpm prettier --write && pnpm eslint --fix", + "eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", + "prettier": "prettier \"**/*.{ts,js,md,json}\"", + "test": "node --import tsx/esm --test --test-reporter=@nomicfoundation/hardhat-node-test-reporter \"test/*.ts\" \"test/!(fixture-projects|helpers)/**/*.ts\"", + "test:only": "node --import tsx/esm --test --test-only --test-reporter=@nomicfoundation/hardhat-node-test-reporter \"test/*.ts\" \"test/!(fixture-projects|helpers)/**/*.ts\"", + "test:coverage": "c8 --reporter html --reporter text --all --exclude test --exclude \"src/**/{types,type-extensions}.ts\" --src src node --import tsx/esm --test --test-reporter=@nomicfoundation/hardhat-node-test-reporter \"test/*.ts\" \"test/!(fixture-projects|helpers)/**/*.ts\"", + "pretest": "pnpm build", + "pretest:only": "pnpm build", + "build": "tsc --build .", + "prepublishOnly": "pnpm build", + "clean": "rimraf dist" + }, + "files": [ + "dist/src/", + "src/", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "devDependencies": { + "@eslint/js": "^9.35.0", + "@nomicfoundation/hardhat-node-test-reporter": "^3.0.0", + "@tsconfig/node22": "^22.0.2", + "@types/node": "^20.14.9", + "c8": "^9.1.0", + "eslint": "^9.35.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "hardhat": "^3.0.6", + "prettier": "3.2.5", + "rimraf": "^5.0.5", + "tsx": "^4.19.3", + "typescript": "~5.8.0", + "typescript-eslint": "^8.43.0" + }, + "peerDependencies": { + "hardhat": "^3.0.6" + } +} diff --git a/src/hooks/network.ts b/packages/plugin/src/hooks/network.ts similarity index 84% rename from src/hooks/network.ts rename to packages/plugin/src/hooks/network.ts index e06d959..ee68a17 100644 --- a/src/hooks/network.ts +++ b/packages/plugin/src/hooks/network.ts @@ -5,9 +5,7 @@ export default async (): Promise> => { const handlers: Partial = { async newConnection( context: HookContext, - next: ( - nextContext: HookContext, - ) => Promise>, + next: (nextContext: HookContext) => Promise> ): Promise> { const connection = await next(context); diff --git a/src/index.ts b/packages/plugin/src/index.ts similarity index 86% rename from src/index.ts rename to packages/plugin/src/index.ts index a23c017..ae23b06 100644 --- a/src/index.ts +++ b/packages/plugin/src/index.ts @@ -1,6 +1,6 @@ import type { HardhatPlugin } from "hardhat/types/plugins"; -export default { +const plugin: HardhatPlugin = { id: "hardhat-plugin-template", // The `npmPackage` field is only necessary if the `id` is not the npm package @@ -11,4 +11,6 @@ export default { hookHandlers: { network: () => import("./hooks/network.js"), }, -} satisfies HardhatPlugin; +}; + +export default plugin; diff --git a/test/index.ts b/packages/plugin/test/index.ts similarity index 100% rename from test/index.ts rename to packages/plugin/test/index.ts diff --git a/tsconfig.json b/packages/plugin/tsconfig.json similarity index 94% rename from tsconfig.json rename to packages/plugin/tsconfig.json index 6cb7371..124103e 100644 --- a/tsconfig.json +++ b/packages/plugin/tsconfig.json @@ -12,6 +12,7 @@ "sourceMap": true, "composite": true, "incremental": true, + "isolatedModules": true, "typeRoots": ["${configDir}/node_modules/@types"] }, "exclude": ["${configDir}/dist", "${configDir}/node_modules"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5276c1c..b8efccd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,24 @@ settings: importers: - .: - dependencies: + .: {} + + packages/example-project: + devDependencies: + '@tsconfig/node22': + specifier: ^22.0.2 + version: 22.0.2 hardhat: specifier: ^3.0.6 version: 3.0.6 + hardhat-plugin-template: + specifier: workspace:* + version: link:../plugin + typescript: + specifier: ~5.8.0 + version: 5.8.3 + + packages/plugin: devDependencies: '@eslint/js': specifier: ^9.35.0 @@ -36,9 +49,9 @@ importers: eslint-plugin-import: specifier: ^2.32.0 version: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.35.0) - expect-type: - specifier: ^1.2.1 - version: 1.2.2 + hardhat: + specifier: ^3.0.6 + version: 3.0.6 prettier: specifier: 3.2.5 version: 3.2.5 @@ -314,8 +327,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.30': - resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -982,10 +995,6 @@ packages: ethereum-cryptography@2.2.1: resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} - engines: {node: '>=12.0.0'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2002,7 +2011,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.30': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 @@ -2822,8 +2831,6 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - expect-type@1.2.2: {} - fast-deep-equal@3.1.3: {} fast-equals@5.2.2: {} @@ -3677,7 +3684,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..9c9be4b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/**" From 0173eebdf29a14d2f05e59edbe24a25dd532f0c8 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:55:40 +0000 Subject: [PATCH 02/12] Most of the initial structure in place --- README.md | 0 packages/example-project/hardhat.config.ts | 3 ++ packages/example-project/package.json | 3 +- packages/plugin/eslint.config.js | 8 ++++ packages/plugin/package.json | 3 +- packages/plugin/src/hooks/config.ts | 49 ++++++++++++++++++++++ packages/plugin/src/hooks/network.ts | 15 ++++++- packages/plugin/src/index.ts | 22 +++++++--- packages/plugin/src/tasks/my-task.ts | 12 ++++++ packages/plugin/src/type-extensions.ts | 12 ++++++ packages/plugin/src/types.ts | 7 ++++ 11 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 README.md create mode 100644 packages/plugin/src/hooks/config.ts create mode 100644 packages/plugin/src/tasks/my-task.ts create mode 100644 packages/plugin/src/type-extensions.ts create mode 100644 packages/plugin/src/types.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/example-project/hardhat.config.ts b/packages/example-project/hardhat.config.ts index 085fb0a..6497a9c 100644 --- a/packages/example-project/hardhat.config.ts +++ b/packages/example-project/hardhat.config.ts @@ -3,4 +3,7 @@ import myPlugin from "hardhat-plugin-template"; export default { plugins: [myPlugin], + myConfig: { + greeting: "", + }, } satisfies HardhatUserConfig; diff --git a/packages/example-project/package.json b/packages/example-project/package.json index a3530d0..805b655 100644 --- a/packages/example-project/package.json +++ b/packages/example-project/package.json @@ -4,7 +4,8 @@ "private": true, "type": "module", "scripts": { - "build": "tsc --build" + "build": "tsc --build", + "watch": "tsc --build . --watch" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", diff --git a/packages/plugin/eslint.config.js b/packages/plugin/eslint.config.js index 91efc02..9263af9 100644 --- a/packages/plugin/eslint.config.js +++ b/packages/plugin/eslint.config.js @@ -29,6 +29,14 @@ export default defineConfig( }, eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, + { + files: ["src/**/*.ts", "test/**/*.ts", "integration-tests/**/*.ts"], + rules: { + // Disable two rules that conflict with the patterns that we use + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-redundant-type-constituents": "off", + }, + }, { files: ["src/**/*.ts"], rules: { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index b635ae3..c840763 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -27,7 +27,8 @@ "pretest:only": "pnpm build", "build": "tsc --build .", "prepublishOnly": "pnpm build", - "clean": "rimraf dist" + "clean": "rimraf dist", + "watch": "tsc --build . --watch" }, "files": [ "dist/src/", diff --git a/packages/plugin/src/hooks/config.ts b/packages/plugin/src/hooks/config.ts new file mode 100644 index 0000000..24bff29 --- /dev/null +++ b/packages/plugin/src/hooks/config.ts @@ -0,0 +1,49 @@ +import type { ConfigHooks } from "hardhat/types/hooks"; + +export default async (): Promise> => { + const handlers: Partial = { + async validateUserConfig(userConfig) { + if (userConfig.myConfig === undefined) { + return []; + } + + if (typeof userConfig.myConfig !== "object") { + return [ + { + path: ["myConfig"], + message: "Expected an object with an optional greeting.", + }, + ]; + } + + const greeting = userConfig.myConfig?.greeting; + if (greeting === undefined) { + return []; + } + + if (typeof greeting !== "string" || greeting.length === 0) { + return [ + { + path: ["myConfig", "greeting"], + message: "Expected a non-empty string.", + }, + ]; + } + + return []; + }, + async resolveUserConfig(userConfig, resolveConfigurationVariable, next) { + const resolved = await next(userConfig, resolveConfigurationVariable); + + const greeting = userConfig.myConfig?.greeting ?? "Hello"; + const myConfig = { greeting }; + + return { + ...resolved, + myConfig, + }; + }, + }; + + return handlers; +}; diff --git a/packages/plugin/src/hooks/network.ts b/packages/plugin/src/hooks/network.ts index ee68a17..2c1c690 100644 --- a/packages/plugin/src/hooks/network.ts +++ b/packages/plugin/src/hooks/network.ts @@ -2,10 +2,16 @@ import type { HookContext, NetworkHooks } from "hardhat/types/hooks"; import { ChainType, NetworkConnection } from "hardhat/types/network"; export default async (): Promise> => { + console.log( + "An instance of Hardhat is using the network hooks for the first time", + ); + const handlers: Partial = { async newConnection( context: HookContext, - next: (nextContext: HookContext) => Promise> + next: ( + nextContext: HookContext, + ) => Promise>, ): Promise> { const connection = await next(context); @@ -13,6 +19,13 @@ export default async (): Promise> => { return connection; }, + async onRequest(context, networkConnection, jsonRpcRequest, next) { + console.log( + `Request from connection ${networkConnection.id} is being processed — Method: ${jsonRpcRequest.method}`, + ); + + return next(context, networkConnection, jsonRpcRequest); + }, }; return handlers; diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index ae23b06..699dc8e 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -1,16 +1,26 @@ +import { task } from "hardhat/config"; +import { ArgumentType } from "hardhat/types/arguments"; import type { HardhatPlugin } from "hardhat/types/plugins"; +import "./type-extensions.js"; + const plugin: HardhatPlugin = { id: "hardhat-plugin-template", - - // The `npmPackage` field is only necessary if the `id` is not the npm package - // name. This is useful when shipping multiple npm plugins in the same - // package. - // npmPackage: "hardhat-plugin-template", - hookHandlers: { + config: () => import("./hooks/config.js"), network: () => import("./hooks/network.js"), }, + tasks: [ + task("my-task", "Prints a greeting.") + .addOption({ + name: "who", + description: "Who is receiving the greeting.", + type: ArgumentType.STRING, + defaultValue: "Hardhat", + }) + .setAction(() => import("./tasks/my-task.js")) + .build(), + ], }; export default plugin; diff --git a/packages/plugin/src/tasks/my-task.ts b/packages/plugin/src/tasks/my-task.ts new file mode 100644 index 0000000..b09a5b2 --- /dev/null +++ b/packages/plugin/src/tasks/my-task.ts @@ -0,0 +1,12 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +interface MyTaskTaskArguments { + who: string; +} + +export default async function ( + taskArguments: MyTaskTaskArguments, + hre: HardhatRuntimeEnvironment, +) { + console.log(`${hre.config.myConfig.greeting}, ${taskArguments.who}!`); +} diff --git a/packages/plugin/src/type-extensions.ts b/packages/plugin/src/type-extensions.ts new file mode 100644 index 0000000..1888e77 --- /dev/null +++ b/packages/plugin/src/type-extensions.ts @@ -0,0 +1,12 @@ +import { MyPluginConfig, MyPluginUserConfig } from "./types.js"; + +import "hardhat/types/config"; +declare module "hardhat/types/config" { + interface HardhatUserConfig { + myConfig?: MyPluginUserConfig; + } + + interface HardhatConfig { + myConfig: MyPluginConfig; + } +} diff --git a/packages/plugin/src/types.ts b/packages/plugin/src/types.ts new file mode 100644 index 0000000..8b23adf --- /dev/null +++ b/packages/plugin/src/types.ts @@ -0,0 +1,7 @@ +export interface MyPluginUserConfig { + greeting?: string; +} + +export interface MyPluginConfig { + greeting: string; +} From 473bc1017b0f305a8238edf812b0b848f11e5634 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 20:44:39 +0000 Subject: [PATCH 03/12] Complete initial structure --- .github/workflows/ci.yml | 38 +++++ README.md | 59 ++++++++ TODO.md | 1 - package.json | 10 +- packages/example-project/README.md | 39 ++++- .../example-project/contracts/Counter.sol | 19 +++ .../example-project/contracts/Counter.t.sol | 32 ++++ packages/example-project/hardhat.config.ts | 3 +- packages/example-project/package.json | 4 +- .../example-project/scripts/example-script.ts | 17 +++ packages/example-project/tsconfig.json | 3 +- packages/plugin/README.md | 27 +++- packages/plugin/eslint.config.js | 8 + packages/plugin/package.json | 2 +- packages/plugin/src/config.ts | 69 +++++++++ packages/plugin/src/hooks/config.ts | 43 +----- packages/plugin/src/hooks/network.ts | 2 +- packages/plugin/test/config.ts | 143 ++++++++++++++++++ packages/plugin/test/example-tests.ts | 64 ++++++++ .../base-project/hardhat.config.ts | 8 + .../base-project/package.json | 6 + .../plugin/test/helpers/fixture-projects.ts | 44 ++++++ packages/plugin/test/index.ts | 20 --- pnpm-lock.yaml | 24 ++- 24 files changed, 614 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 TODO.md create mode 100644 packages/example-project/contracts/Counter.sol create mode 100644 packages/example-project/contracts/Counter.t.sol create mode 100644 packages/example-project/scripts/example-script.ts create mode 100644 packages/plugin/src/config.ts create mode 100644 packages/plugin/test/config.ts create mode 100644 packages/plugin/test/example-tests.ts create mode 100644 packages/plugin/test/fixture-projects/base-project/hardhat.config.ts create mode 100644 packages/plugin/test/fixture-projects/base-project/package.json create mode 100644 packages/plugin/test/helpers/fixture-projects.ts delete mode 100644 packages/plugin/test/index.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..df66f60 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +concurrency: + group: ${{github.workflow}}-${{github.ref}} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22, 24] + steps: + - uses: actions/checkout@v5 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "pnpm" + - name: Install dependencies + run: pnpm install --frozen-lockfile --prefer-offline + - name: Build + run: pnpm build + - name: Test + run: pnpm test + - name: Lint + run: pnpm lint diff --git a/README.md b/README.md index e69de29..67bd212 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,59 @@ +# Hardhat 3 plugin template + +This repository is a template for creating a Hardhat 3 plugin. + +## Getting started + +> This repository is structured as a pnpm monorepo, so make sure you have pnpm installed first + +To get started, clone the respository and run: + +```sh +pnpm install +pnpm build +``` + +This will install all the dependencies and build the plugin. + +You can now run the tests of the plugin with: + +```sh +pnpm test +``` + +And try the plugin out in `packages/example-project` with: + +```sh +cd packages/example-project +pnpm hardhat my-task +``` + +which should print `Hola, Hardhat!`. + +## Understanding the repository structure + +This repository is structured as a pnpm monorepo with the following packages: + +- `packages/plugin`: The plugin itself. +- `packages/example-project`: An example Hardhat 3 project that uses the plugin. + +All the development will happen in the `packages/plugin` directory, while `packages/example-project` is a playground to experiment with your plugin, and manually test it. + +## Github Actions setup + +This repository is setup with a Github Actions workflow. You don't need to do anything to set it up, it runs your on every push to `main`, on pull requests, and when manual triggered. + +The workflow is equivalent to running this steps in the root of the repository: + +```sh +pnpm install +pnpm build +pnpm test +pnpm lint +``` + +It runs using Node.js versions 22 and 24, on an `ubuntu-latest` runner. + +## Development tips + +- We recommend leaving a terminal with `pnpm watch` running in the root of the repository. That way, things will normally be rebuilt by the time you try them out in `packages/example-project`. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index ae43d52..0000000 --- a/TODO.md +++ /dev/null @@ -1 +0,0 @@ -[] dependencies: hardhat-test-utils? hardhat-plugin-testing-utils? diff --git a/package.json b/package.json index 71f31b1..85e3173 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,12 @@ { "name": "hardhat3-plugin-template", - "private": true + "private": true, + "scripts": { + "build": "pnpm --recursive build", + "clean": "pnpm --recursive clean", + "lint": "pnpm --recursive lint", + "lint:fix": "pnpm --recursive lint:fix", + "test": "pnpm --recursive test", + "watch": "pnpm --filter ./packages/plugin watch" + } } diff --git a/packages/example-project/README.md b/packages/example-project/README.md index 3ea0be5..3b1ab75 100644 --- a/packages/example-project/README.md +++ b/packages/example-project/README.md @@ -1 +1,38 @@ -# Example project that uses your plugin +# A Hardhat 3 project that uses your plugin + +This is an example project that uses your plugin. + +## Getting started + +To run this project, you need to install the dependencies and build the plugin: + +```sh +pnpm install +pnpm build +``` + +Then, you can run hardhat with: + +```sh +pnpm hardhat my-task +``` + +You can also run an example script with: + +```sh +pnpm hardhat run scripts/example-script.ts +``` + +And the project's solidity tests with: + +```sh +pnpm hardhat test +``` + +## What's inside the project? + +This project is similar to what you get when initializing a Hardhat 3 project with `npx hardhat --init`, but without any of the Hardhat toolboxes. + +This means that you don't have `ethers,` `viem`, `mochar`, nor the Node.js test runner plugins. + +Please install whichever dependency or plugin you need in here. This package won't be published, so you have complete freedom to do whatever you want. diff --git a/packages/example-project/contracts/Counter.sol b/packages/example-project/contracts/Counter.sol new file mode 100644 index 0000000..24b8149 --- /dev/null +++ b/packages/example-project/contracts/Counter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +contract Counter { + uint public x; + + event Increment(uint by); + + function inc() public { + x++; + emit Increment(1); + } + + function incBy(uint by) public { + require(by > 0, "incBy: increment should be positive"); + x += by; + emit Increment(by); + } +} diff --git a/packages/example-project/contracts/Counter.t.sol b/packages/example-project/contracts/Counter.t.sol new file mode 100644 index 0000000..1899814 --- /dev/null +++ b/packages/example-project/contracts/Counter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_InitialValue() public view { + require(counter.x() == 0, "Initial value should be 0"); + } + + function testFuzz_Inc(uint8 x) public { + for (uint8 i = 0; i < x; i++) { + counter.inc(); + } + require( + counter.x() == x, + "Value after calling inc x times should be x" + ); + } + + function test_IncByZero() public { + vm.expectRevert(); + counter.incBy(0); + } +} diff --git a/packages/example-project/hardhat.config.ts b/packages/example-project/hardhat.config.ts index 6497a9c..0f63602 100644 --- a/packages/example-project/hardhat.config.ts +++ b/packages/example-project/hardhat.config.ts @@ -3,7 +3,8 @@ import myPlugin from "hardhat-plugin-template"; export default { plugins: [myPlugin], + solidity: "0.8.29", myConfig: { - greeting: "", + greeting: "Hola", }, } satisfies HardhatUserConfig; diff --git a/packages/example-project/package.json b/packages/example-project/package.json index 805b655..455a1be 100644 --- a/packages/example-project/package.json +++ b/packages/example-project/package.json @@ -9,8 +9,10 @@ }, "devDependencies": { "@tsconfig/node22": "^22.0.2", + "@types/node": "^22.11.0", "hardhat": "^3.0.6", "hardhat-plugin-template": "workspace:*", - "typescript": "~5.8.0" + "typescript": "~5.8.0", + "forge-std": "github:foundry-rs/forge-std#v1.9.4" } } diff --git a/packages/example-project/scripts/example-script.ts b/packages/example-project/scripts/example-script.ts new file mode 100644 index 0000000..145af86 --- /dev/null +++ b/packages/example-project/scripts/example-script.ts @@ -0,0 +1,17 @@ +import { network } from "hardhat"; + +console.log("Running example script"); +const { provider } = await network.connect(); + +const accounts = await provider.send("eth_accounts", []); + +console.log("Accounts:", accounts); + +console.log(`Sending 1wei form ${accounts[0]} to ${accounts[1]}...`); + +const tx = await provider.request({ + method: "eth_sendTransaction", + params: [{ from: accounts[0], to: accounts[1], value: "0x1" }], +}); + +console.log(`Successfully sent transaction with hash ${tx}`); diff --git a/packages/example-project/tsconfig.json b/packages/example-project/tsconfig.json index f0ea66b..124103e 100644 --- a/packages/example-project/tsconfig.json +++ b/packages/example-project/tsconfig.json @@ -15,6 +15,5 @@ "isolatedModules": true, "typeRoots": ["${configDir}/node_modules/@types"] }, - "exclude": ["${configDir}/dist", "${configDir}/node_modules"], - "references": [{ "path": "../plugin" }] + "exclude": ["${configDir}/dist", "${configDir}/node_modules"] } diff --git a/packages/plugin/README.md b/packages/plugin/README.md index aed734f..0c583c0 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -1 +1,26 @@ -# Hardhat 3 plugin template +# My Hardhat 3 plugin + +This is an example project that uses your plugin. + +## Getting started + +To run this project, you need to install the dependencies and build the plugin: + +```sh +pnpm install +pnpm build +``` + +Then, you can run hardhat with: + +```sh +pnpm hardhat my-task +``` + +## What's inside the project? + +This project is similar to what you get when initializing a Hardhat 3 project with `npx hardhat --init`, but without any of the Hardhat toolboxes. + +This means that you don't have `ethers,` `viem`, `mochar`, nor the Node.js test runner plugins. + +Please install whichever dependency or plugin you need in here. This package won't be published, so you have complete freedom to do whatever you want. diff --git a/packages/plugin/eslint.config.js b/packages/plugin/eslint.config.js index 9263af9..17c5e28 100644 --- a/packages/plugin/eslint.config.js +++ b/packages/plugin/eslint.config.js @@ -78,4 +78,12 @@ export default defineConfig( ], }, }, + { + // This is a set of more opinionated rules. Feel free to adapt to your style. + files: ["src/**/*.ts", "test/**/*.ts", "integration-tests/**/*.ts"], + ignores: ["test/**/fixture-projects/**"], + rules: { + "import/order": "error", + }, + }, ); diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c840763..27fc19d 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -41,7 +41,7 @@ "@eslint/js": "^9.35.0", "@nomicfoundation/hardhat-node-test-reporter": "^3.0.0", "@tsconfig/node22": "^22.0.2", - "@types/node": "^20.14.9", + "@types/node": "^22.11.0", "c8": "^9.1.0", "eslint": "^9.35.0", "eslint-import-resolver-typescript": "^4.4.4", diff --git a/packages/plugin/src/config.ts b/packages/plugin/src/config.ts new file mode 100644 index 0000000..9014146 --- /dev/null +++ b/packages/plugin/src/config.ts @@ -0,0 +1,69 @@ +import { HardhatUserConfig } from "hardhat/config"; +import { HardhatConfig } from "hardhat/types/config"; +import { HardhatUserConfigValidationError } from "hardhat/types/hooks"; + +/** + * This function validates the parts of the HardhatUserConfig that are relevant + * to the plugin. + * + * This function is called from the `validateUserConfig` hook handler. + * + * @param userConfig The HardhatUserConfig, as exported in the config file. + * @returns An array of validation errors, or an empty array if valid. + */ +export async function validatePluginConfig( + userConfig: HardhatUserConfig, +): Promise { + if (userConfig.myConfig === undefined) { + return []; + } + + if (typeof userConfig.myConfig !== "object") { + return [ + { + path: ["myConfig"], + message: "Expected an object with an optional greeting.", + }, + ]; + } + + const greeting = userConfig.myConfig?.greeting; + if (greeting === undefined) { + return []; + } + + if (typeof greeting !== "string" || greeting.length === 0) { + return [ + { + path: ["myConfig", "greeting"], + message: "Expected a non-empty string.", + }, + ]; + } + + return []; +} + +/** + * Resolves the plugin config, based on an already validated HardhatUserConfig, + * and a partially resolved HardhatConfig. + * + * This function is called from the `resolveUserConfig` hook handler. + * + * @param userConfig The HardhatUserConfig. + * @param partiallyResolvedConfig The partially resolved HardhatConfig, which is + * generated by calling `next` in the `validateUserConfig` hook handler. + * @returns The resolved HardhatConfig. + */ +export async function resolvePluginConfig( + userConfig: HardhatUserConfig, + partiallyResolvedConfig: HardhatConfig, +): Promise { + const greeting = userConfig.myConfig?.greeting ?? "Hello"; + const myConfig = { greeting }; + + return { + ...partiallyResolvedConfig, + myConfig, + }; +} diff --git a/packages/plugin/src/hooks/config.ts b/packages/plugin/src/hooks/config.ts index 24bff29..9beb1b3 100644 --- a/packages/plugin/src/hooks/config.ts +++ b/packages/plugin/src/hooks/config.ts @@ -1,47 +1,18 @@ import type { ConfigHooks } from "hardhat/types/hooks"; +import { resolvePluginConfig, validatePluginConfig } from "../config.js"; export default async (): Promise> => { const handlers: Partial = { async validateUserConfig(userConfig) { - if (userConfig.myConfig === undefined) { - return []; - } - - if (typeof userConfig.myConfig !== "object") { - return [ - { - path: ["myConfig"], - message: "Expected an object with an optional greeting.", - }, - ]; - } - - const greeting = userConfig.myConfig?.greeting; - if (greeting === undefined) { - return []; - } - - if (typeof greeting !== "string" || greeting.length === 0) { - return [ - { - path: ["myConfig", "greeting"], - message: "Expected a non-empty string.", - }, - ]; - } - - return []; + return validatePluginConfig(userConfig); }, async resolveUserConfig(userConfig, resolveConfigurationVariable, next) { - const resolved = await next(userConfig, resolveConfigurationVariable); - - const greeting = userConfig.myConfig?.greeting ?? "Hello"; - const myConfig = { greeting }; + const partiallyResolvedConfig = await next( + userConfig, + resolveConfigurationVariable, + ); - return { - ...resolved, - myConfig, - }; + return resolvePluginConfig(userConfig, partiallyResolvedConfig); }, }; diff --git a/packages/plugin/src/hooks/network.ts b/packages/plugin/src/hooks/network.ts index 2c1c690..28f7fa0 100644 --- a/packages/plugin/src/hooks/network.ts +++ b/packages/plugin/src/hooks/network.ts @@ -3,7 +3,7 @@ import { ChainType, NetworkConnection } from "hardhat/types/network"; export default async (): Promise> => { console.log( - "An instance of Hardhat is using the network hooks for the first time", + "An instance of the HRE is using the network hooks for the first time", ); const handlers: Partial = { diff --git a/packages/plugin/test/config.ts b/packages/plugin/test/config.ts new file mode 100644 index 0000000..746b08d --- /dev/null +++ b/packages/plugin/test/config.ts @@ -0,0 +1,143 @@ +import { describe, it } from "node:test"; + +import assert from "node:assert/strict"; +import { HardhatConfig, HardhatUserConfig } from "hardhat/types/config"; +import { resolvePluginConfig, validatePluginConfig } from "../src/config.js"; +import { MyPluginUserConfig } from "../src/types.js"; + +describe("MyPlugin config", () => { + describe("Config validation", () => { + describe("Valid cases", () => { + it("Should consider an empty config as valid", async () => { + const validationErrors = await validatePluginConfig({}); + + assert.equal(validationErrors.length, 0); + }); + + it("Should ignore errors in other parts of the config", async () => { + const validationErrors = await validatePluginConfig({ + networks: { + foo: { + type: "http", + url: "INVALID URL", + }, + }, + }); + + assert.equal(validationErrors.length, 0); + }); + + it("Should accept an empty myConfig object", async () => { + const validationErrors = await validatePluginConfig({ + myConfig: {}, + }); + + assert.equal(validationErrors.length, 0); + }); + + it("Should accept an non-empty greeting", async () => { + const validationErrors = await validatePluginConfig({ + myConfig: { + greeting: "Hola", + }, + }); + + assert.equal(validationErrors.length, 0); + }); + }); + + describe("Invalid cases", () => { + // Many invalid cases are type-unsafe, as we have to trick TypeScript into + // allowing something that is invalid + it("Should reject a myConfig field with an invalid type", async () => { + const validationErrors = await validatePluginConfig({ + myConfig: "INVALID" as MyPluginUserConfig, + }); + + assert.deepEqual(validationErrors, [ + { + path: ["myConfig"], + message: "Expected an object with an optional greeting.", + }, + ]); + }); + + it("Should reject a myConfig field with an invalid greeting", async () => { + const validationErrors = await validatePluginConfig({ + myConfig: { + greeting: 123 as unknown as string, + }, + }); + + assert.deepEqual(validationErrors, [ + { + path: ["myConfig", "greeting"], + message: "Expected a non-empty string.", + }, + ]); + }); + + it("Should reject a myConfig field with an empty greeting", async () => { + const validationErrors = await validatePluginConfig({ + myConfig: { + greeting: "", + }, + }); + + assert.deepEqual(validationErrors, [ + { + path: ["myConfig", "greeting"], + message: "Expected a non-empty string.", + }, + ]); + }); + }); + }); + + describe("Config resolution", () => { + // The config resolution is always type-unsafe, as your plugin is extending + // the HardhatConfig type, but the partially resolved config isn't aware of + // your plugin's extensions. You are responsible for ensuring that they are + // defined correctly during the resolution process. + // + // We recommend testing using an artificial partially resolved config, as + // we do here, but taking care that the fields that your resolution logic + // depends on are defined and valid. + + it("Should resolve a config without a myConfig field", async () => { + const userConfig: HardhatUserConfig = {}; + const partiallyResolvedConfig = {} as HardhatConfig; + + const resolvedConfig = await resolvePluginConfig( + userConfig, + partiallyResolvedConfig, + ); + + assert.deepEqual(resolvedConfig.myConfig, { greeting: "Hello" }); + }); + + it("Should resolve a config with an empty myConfig field", async () => { + const userConfig: HardhatUserConfig = { myConfig: {} }; + const partiallyResolvedConfig = {} as HardhatConfig; + + const resolvedConfig = await resolvePluginConfig( + userConfig, + partiallyResolvedConfig, + ); + + assert.deepEqual(resolvedConfig.myConfig, { greeting: "Hello" }); + }); + + it("Should resolve a config using the provided greeting", async () => { + const userConfig: HardhatUserConfig = { myConfig: { greeting: "Hola" } }; + const partiallyResolvedConfig = {} as HardhatConfig; + + const resolvedConfig = await resolvePluginConfig( + userConfig, + partiallyResolvedConfig, + ); + + assert.deepEqual(resolvedConfig.myConfig, { greeting: "Hola" }); + }); + }); +}); diff --git a/packages/plugin/test/example-tests.ts b/packages/plugin/test/example-tests.ts new file mode 100644 index 0000000..38b04ff --- /dev/null +++ b/packages/plugin/test/example-tests.ts @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import path from "node:path"; +import { createHardhatRuntimeEnvironment } from "hardhat/hre"; +import MyPlugin from "../src/index.js"; +import { createFixtureProjectHRE } from "./helpers/fixture-projects.js"; + +describe("MyPlugin tests", () => { + describe("Test using a fixture project", async () => { + it("Should define my-task", async () => { + const hre = await createFixtureProjectHRE("base-project"); + + const myTask = hre.tasks.getTask("my-task"); + assert.notEqual( + myTask, + undefined, + "my-task should be defined because we loaded the plugin", + ); + + // You can use any feature of Hardhat to build your tests, for example, + // running the task and connecting to a new edr-simulated network + await myTask.run(); + + const conn = await hre.network.connect(); + assert.equal( + await conn.provider.request({ method: "eth_blockNumber" }), + "0x0", + "The simulated chain is new, so it should be empty", + ); + }); + }); + + describe("Test creting a new HRE with an inline config", async () => { + it("Should be able to load the plugin", async () => { + // You can also create a new HRE without a fixture project, including + // a custom config. + // + // In this case we don't provide a fixture project, nor a config path, just + // a config object. + // + // You can customize the config object here, including adding new plugins. + const hre = await createHardhatRuntimeEnvironment({ + plugins: [MyPlugin], + myConfig: { + greeting: "Hola", + }, + }); + + assert.equal(hre.config.myConfig.greeting, "Hola"); + + // The config path is undefined because we didn't provide it to + // createHardhatRuntimeEnvironment. See its documentation for more info. + assert.equal(hre.config.paths.config, undefined); + + // The root path is the directory containing the closest package.json to + // the CWD, if none is provided. + assert.equal( + hre.config.paths.root, + path.resolve(import.meta.dirname, ".."), + ); + }); + }); +}); diff --git a/packages/plugin/test/fixture-projects/base-project/hardhat.config.ts b/packages/plugin/test/fixture-projects/base-project/hardhat.config.ts new file mode 100644 index 0000000..6d2d235 --- /dev/null +++ b/packages/plugin/test/fixture-projects/base-project/hardhat.config.ts @@ -0,0 +1,8 @@ +import { HardhatUserConfig } from "hardhat/config"; +import MyPlugin from "../../../src/index.js"; + +const config: HardhatUserConfig = { + plugins: [MyPlugin], +}; + +export default config; diff --git a/packages/plugin/test/fixture-projects/base-project/package.json b/packages/plugin/test/fixture-projects/base-project/package.json new file mode 100644 index 0000000..fd91b66 --- /dev/null +++ b/packages/plugin/test/fixture-projects/base-project/package.json @@ -0,0 +1,6 @@ +{ + "name": "base-project", + "version": "1.0.0", + "type": "module", + "private": true +} diff --git a/packages/plugin/test/helpers/fixture-projects.ts b/packages/plugin/test/helpers/fixture-projects.ts new file mode 100644 index 0000000..98d00c0 --- /dev/null +++ b/packages/plugin/test/helpers/fixture-projects.ts @@ -0,0 +1,44 @@ +import path from "node:path"; + +import { + createHardhatRuntimeEnvironment, + importUserConfig, + resolveHardhatConfigPath, +} from "hardhat/hre"; +import { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +/** + * Creates a new Hardhat Runtime Environment based on a fixture project. + * + * A fixture project is a Hardhat project located in `../fixtures-projects` that + * has its own hardhat.config.ts and package.json (not part of the monorepo). + * + * Note: This function doesn't modify the global environment, the global + * instance of the HRE, nor the CWD. + * + * @param fixtureProjectName The name of the fixture project to use. + * e.g. `base-project` + * @returns A new HRE with the fixture project folder as root. + */ +export async function createFixtureProjectHRE( + fixtureProjectName: string, +): Promise { + const fixtureProjectRoot = path.resolve( + import.meta.dirname, + `../fixture-projects/${fixtureProjectName}`, + ); + + const configPath = await resolveHardhatConfigPath( + path.join(fixtureProjectRoot, "hardhat.config.ts"), + ); + + const userConfig = await importUserConfig(configPath); + + return createHardhatRuntimeEnvironment( + userConfig, + { + config: configPath, + }, + fixtureProjectRoot, + ); +} diff --git a/packages/plugin/test/index.ts b/packages/plugin/test/index.ts deleted file mode 100644 index 8ddb860..0000000 --- a/packages/plugin/test/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -// import assert from "node:assert/strict"; -// import { describe, it } from "node:test"; - -// describe("Example tests", () => { -// it("foo", function () { -// assert.equal(foo(), "foo"); -// }); - -// it("bar", function () { -// assert.equal(bar(), "bar"); -// }); - -// it("foobar", function () { -// assert.equal(foobar(), "foobar"); -// }); - -// it("should return the right types", function () { -// expectTypeOf(foo()).toMatchTypeOf(); -// }); -// }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8efccd..6207d67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,12 @@ importers: '@tsconfig/node22': specifier: ^22.0.2 version: 22.0.2 + '@types/node': + specifier: ^22.11.0 + version: 22.18.4 + forge-std: + specifier: github:foundry-rs/forge-std#v1.9.4 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262 hardhat: specifier: ^3.0.6 version: 3.0.6 @@ -35,8 +41,8 @@ importers: specifier: ^22.0.2 version: 22.0.2 '@types/node': - specifier: ^20.14.9 - version: 20.19.13 + specifier: ^22.11.0 + version: 22.18.4 c8: specifier: ^9.1.0 version: 9.1.0 @@ -68,6 +74,8 @@ importers: specifier: ^8.43.0 version: 8.43.0(eslint@9.35.0)(typescript@5.8.3) + packages/plugin/test/fixture-projects/base-project: {} + packages: '@actions/core@1.11.1': @@ -488,8 +496,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.19.13': - resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==} + '@types/node@22.18.4': + resolution: {integrity: sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==} '@typescript-eslint/eslint-plugin@8.43.0': resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} @@ -1051,6 +1059,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262: + resolution: {tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262} + version: 1.9.4 + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2187,7 +2199,7 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@20.19.13': + '@types/node@22.18.4': dependencies: undici-types: 6.21.0 @@ -2884,6 +2896,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forge-std@https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: From 5a0229d822802d0a8e618bc58099f6e80121a25d Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 20:46:20 +0000 Subject: [PATCH 04/12] Small improvement to the README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67bd212..d417540 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This repository is a template for creating a Hardhat 3 plugin. ## Getting started -> This repository is structured as a pnpm monorepo, so make sure you have pnpm installed first +> This repository is structured as a pnpm monorepo, so make sure you have [`pnpm`](https://pnpm.io/) installed first To get started, clone the respository and run: From 2114da1f914b6f28ad84b779b025f87744fecf83 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 20:49:38 +0000 Subject: [PATCH 05/12] Readme improvements --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d417540..cab687e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ which should print `Hola, Hardhat!`. ## Understanding the repository structure +### Monorepo structure + This repository is structured as a pnpm monorepo with the following packages: - `packages/plugin`: The plugin itself. @@ -39,7 +41,7 @@ This repository is structured as a pnpm monorepo with the following packages: All the development will happen in the `packages/plugin` directory, while `packages/example-project` is a playground to experiment with your plugin, and manually test it. -## Github Actions setup +### Github Actions setup This repository is setup with a Github Actions workflow. You don't need to do anything to set it up, it runs your on every push to `main`, on pull requests, and when manual triggered. From 45f471ffcabcec64adcdc6503d168d9c9dc3b13c Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:54:26 -0300 Subject: [PATCH 06/12] Update packages/plugin/test/helpers/fixture-projects.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/plugin/test/helpers/fixture-projects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/test/helpers/fixture-projects.ts b/packages/plugin/test/helpers/fixture-projects.ts index 98d00c0..4cef1cf 100644 --- a/packages/plugin/test/helpers/fixture-projects.ts +++ b/packages/plugin/test/helpers/fixture-projects.ts @@ -10,7 +10,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types/hre"; /** * Creates a new Hardhat Runtime Environment based on a fixture project. * - * A fixture project is a Hardhat project located in `../fixtures-projects` that + * A fixture project is a Hardhat project located in `../fixture-projects` that * has its own hardhat.config.ts and package.json (not part of the monorepo). * * Note: This function doesn't modify the global environment, the global From b1b70d23bab5b8906d15ec9041e3074b3722a8da Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:54:47 -0300 Subject: [PATCH 07/12] Update packages/plugin/test/example-tests.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/plugin/test/example-tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/test/example-tests.ts b/packages/plugin/test/example-tests.ts index 38b04ff..395b156 100644 --- a/packages/plugin/test/example-tests.ts +++ b/packages/plugin/test/example-tests.ts @@ -31,7 +31,7 @@ describe("MyPlugin tests", () => { }); }); - describe("Test creting a new HRE with an inline config", async () => { + describe("Test creating a new HRE with an inline config", async () => { it("Should be able to load the plugin", async () => { // You can also create a new HRE without a fixture project, including // a custom config. From 6a4ab87bd6558c2dd36e5b6b167ac2408040e0ff Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:55:13 -0300 Subject: [PATCH 08/12] Update packages/example-project/scripts/example-script.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/example-project/scripts/example-script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/example-project/scripts/example-script.ts b/packages/example-project/scripts/example-script.ts index 145af86..7c0c079 100644 --- a/packages/example-project/scripts/example-script.ts +++ b/packages/example-project/scripts/example-script.ts @@ -7,7 +7,7 @@ const accounts = await provider.send("eth_accounts", []); console.log("Accounts:", accounts); -console.log(`Sending 1wei form ${accounts[0]} to ${accounts[1]}...`); +console.log(`Sending 1wei from ${accounts[0]} to ${accounts[1]}...`); const tx = await provider.request({ method: "eth_sendTransaction", From 550b3ca6900f13edd82b3775e6fd446c5100c566 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:55:20 -0300 Subject: [PATCH 09/12] Update packages/example-project/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/example-project/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/example-project/README.md b/packages/example-project/README.md index 3b1ab75..0e7bd67 100644 --- a/packages/example-project/README.md +++ b/packages/example-project/README.md @@ -33,6 +33,6 @@ pnpm hardhat test This project is similar to what you get when initializing a Hardhat 3 project with `npx hardhat --init`, but without any of the Hardhat toolboxes. -This means that you don't have `ethers,` `viem`, `mochar`, nor the Node.js test runner plugins. +This means that you don't have `ethers,` `viem`, `mocha`, nor the Node.js test runner plugins. Please install whichever dependency or plugin you need in here. This package won't be published, so you have complete freedom to do whatever you want. From 3bc9dc347dce952dbef92241014a144541656507 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:55:35 -0300 Subject: [PATCH 10/12] Update packages/plugin/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/README.md b/packages/plugin/README.md index 0c583c0..558992a 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -21,6 +21,6 @@ pnpm hardhat my-task This project is similar to what you get when initializing a Hardhat 3 project with `npx hardhat --init`, but without any of the Hardhat toolboxes. -This means that you don't have `ethers,` `viem`, `mochar`, nor the Node.js test runner plugins. +This means that you don't have `ethers,` `viem`, `mocha`, nor the Node.js test runner plugins. Please install whichever dependency or plugin you need in here. This package won't be published, so you have complete freedom to do whatever you want. From 0a9338e09f50101728bd2eef7ae202e660385cdd Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 17:57:03 -0300 Subject: [PATCH 11/12] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cab687e..7964dce 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository is a template for creating a Hardhat 3 plugin. > This repository is structured as a pnpm monorepo, so make sure you have [`pnpm`](https://pnpm.io/) installed first -To get started, clone the respository and run: +To get started, clone the repository and run: ```sh pnpm install From 504ad5b815180de622f2671b4799ef8dd10c2ea7 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 15 Sep 2025 21:05:00 +0000 Subject: [PATCH 12/12] Add a README to the plugin --- packages/plugin/README.md | 57 +++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/plugin/README.md b/packages/plugin/README.md index 558992a..5550369 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -1,26 +1,55 @@ -# My Hardhat 3 plugin +# `hardhat-plugin-template` -This is an example project that uses your plugin. +This is an example plugin that adds a task that prints a greeting. -## Getting started +## Installation -To run this project, you need to install the dependencies and build the plugin: +To install this plugin, run the following command: -```sh -pnpm install -pnpm build +```bash +npm install --save-dev hardhat-plugin-template ``` -Then, you can run hardhat with: +In your `hardhat.config.ts` file, import the plugin and add it to the `plugins` array: -```sh -pnpm hardhat my-task +```ts +import hardhatPluginTemplate from "hardhat-plugin-template"; + +export default { + plugins: [hardhatPluginTemplate], +}; +``` + +## Usage + +The plugin adds a new task called `my-task`. To run it, use the following command: + +```bash +npx hardhat my-task +``` + +You should see the following output: + +``` +Hello, Hardhat! ``` -## What's inside the project? +### Configuration -This project is similar to what you get when initializing a Hardhat 3 project with `npx hardhat --init`, but without any of the Hardhat toolboxes. +You can configure the greeting that's printed by using the `myConfig` field in your Hardhat config. For example, you can have the following configuration: + +```ts +import hardhatPluginTemplate from "hardhat-plugin-template"; + +export default { + plugins: [hardhatPluginTemplate], + myConfig: { + greeting: "Hola", + }, + //... +}; +``` -This means that you don't have `ethers,` `viem`, `mocha`, nor the Node.js test runner plugins. +### Network logs -Please install whichever dependency or plugin you need in here. This package won't be published, so you have complete freedom to do whatever you want. +This plugin also adds some example code to log different network events. To see it in action, all you need to do is run your Hardhat tests, deployment, or a script.