diff --git a/packages/hardhat/README.md b/packages/hardhat/README.md index 1a4e77efe..c1b289fce 100644 --- a/packages/hardhat/README.md +++ b/packages/hardhat/README.md @@ -90,10 +90,18 @@ hardhat typechain # always regenerates typings to all files ## Configuration -This plugin extends the `hardhatConfig` optional `typechain` object. The object contains two fields, `outDir` and -`target`. `outDir` is the output directory of the artifacts that TypeChain creates (defaults to `typechain`). `target` -is one of the targets specified by the TypeChain [docs](https://github.com/ethereum-ts/TypeChain#cli) (defaults to -`ethers`). +This plugin extends the `hardhatConfig` optional `typechain` object. The object contains the following (optional) +fields: + +- `target`: one of the targets specified by the TypeChain [docs](https://github.com/ethereum-ts/TypeChain#cli) (defaults + to `ethers`) +- `outDir`: the output directory of the artifacts that TypeChain creates (defaults to `typechain`). +- `artifacts`: glob pattern that defines for which build artifacts to generate types (defaults to all artifacts + generated in compile step) +- `alwaysGenerateOverloads`: some targets won't generate unnecessary types for overloaded functions by default, this + option forces to always generate them +- `externalArtifacts`: array of glob patterns with external artifacts to process (e.g external libs from `node_modules`) +- `dontOverrideCompile`: boolean disabling automatic inclusion of type generation in compile task. This is an example of how to set it: @@ -102,9 +110,10 @@ module.exports = { typechain: { outDir: 'src/types', target: 'ethers-v5', - alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? - externalArtifacts: ['externalArtifacts/*.json'], // optional array of glob patterns with external artifacts to process (for example external libs from node_modules) - dontOverrideCompile: false // defaults to false + artifacts: '**/TestContract.json', + alwaysGenerateOverloads: false, + externalArtifacts: ['externalArtifacts/*.json'], + dontOverrideCompile: false, }, } ``` diff --git a/packages/hardhat/src/config.ts b/packages/hardhat/src/config.ts index 23a3f7f19..f5eb9b414 100644 --- a/packages/hardhat/src/config.ts +++ b/packages/hardhat/src/config.ts @@ -6,6 +6,7 @@ export function getDefaultTypechainConfig(config: HardhatConfig): TypechainConfi const defaultConfig: TypechainConfig = { outDir: 'typechain-types', target: 'ethers-v5', + artifacts: `${config.paths.artifacts}/!(build-info)/**/+([a-zA-Z0-9_]).json`, alwaysGenerateOverloads: false, discriminateTypes: false, tsNocheck: false, diff --git a/packages/hardhat/src/index.ts b/packages/hardhat/src/index.ts index 8dcad299f..701d36d58 100644 --- a/packages/hardhat/src/index.ts +++ b/packages/hardhat/src/index.ts @@ -10,6 +10,13 @@ import { glob, PublicConfig as RunTypeChainConfig, runTypeChain } from 'typechai import { getDefaultTypechainConfig } from './config' import { TASK_TYPECHAIN, TASK_TYPECHAIN_GENERATE_TYPES } from './constants' +function intersect(a: Array, b: Array): Array { + var setA = new Set(a) + var setB = new Set(b) + var intersection = new Set([...setA].filter((x) => setB.has(x))) + return Array.from(intersection) +} + const taskArgsStore: { noTypechain: boolean; fullRebuild: boolean } = { noTypechain: false, fullRebuild: false } extendConfig((config) => { @@ -38,7 +45,7 @@ subtask(TASK_TYPECHAIN_GENERATE_TYPES) .addFlag('quiet', 'Makes the process less verbose') .setAction(async ({ compileSolOutput, quiet }, { config, artifacts }) => { const artifactFQNs: string[] = getFQNamesFromCompilationOutput(compileSolOutput) - const artifactPaths = uniq(artifactFQNs.map((fqn) => artifacts.formArtifactPathFromFullyQualifiedName(fqn))) + let artifactPaths = uniq(artifactFQNs.map((fqn) => artifacts.formArtifactPathFromFullyQualifiedName(fqn))) if (taskArgsStore.noTypechain) { return compileSolOutput @@ -46,7 +53,20 @@ subtask(TASK_TYPECHAIN_GENERATE_TYPES) // RUN TYPECHAIN TASK const typechainCfg = config.typechain - if (artifactPaths.length === 0 && !taskArgsStore.fullRebuild && !typechainCfg.externalArtifacts) { + + const cwd = config.paths.root + const allFiles = glob(cwd, [typechainCfg.artifacts]) + if (typechainCfg.externalArtifacts) { + allFiles.push(...glob(cwd, typechainCfg.externalArtifacts, false)) + } + + // incremental generation is only supported in 'ethers-v5' + // @todo: probably targets should specify somehow if then support incremental generation this won't work with custom targets + const needsFullRebuild = taskArgsStore.fullRebuild || typechainCfg.target !== 'ethers-v5' + artifactPaths = intersect(allFiles, artifactPaths) + const filesToProcess = needsFullRebuild ? allFiles : glob(cwd, artifactPaths) + + if (filesToProcess.length === 0 && !typechainCfg.externalArtifacts) { if (!quiet) { // eslint-disable-next-line no-console console.log('No need to generate any newer typings.') @@ -55,21 +75,12 @@ subtask(TASK_TYPECHAIN_GENERATE_TYPES) return compileSolOutput } - // incremental generation is only supported in 'ethers-v5' - // @todo: probably targets should specify somehow if then support incremental generation this won't work with custom targets - const needsFullRebuild = taskArgsStore.fullRebuild || typechainCfg.target !== 'ethers-v5' if (!quiet) { // eslint-disable-next-line no-console console.log( `Generating typings for: ${artifactPaths.length} artifacts in dir: ${typechainCfg.outDir} for target: ${typechainCfg.target}`, ) } - const cwd = config.paths.root - - const allFiles = glob(cwd, [`${config.paths.artifacts}/!(build-info)/**/+([a-zA-Z0-9_]).json`]) - if (typechainCfg.externalArtifacts) { - allFiles.push(...glob(cwd, typechainCfg.externalArtifacts, false)) - } const typechainOptions: Omit = { cwd, @@ -84,14 +95,16 @@ subtask(TASK_TYPECHAIN_GENERATE_TYPES) }, } - const result = await runTypeChain({ - ...typechainOptions, - filesToProcess: needsFullRebuild ? allFiles : glob(cwd, artifactPaths), // only process changed files if not doing full rebuild - }) + if (filesToProcess.length > 0) { + const result = await runTypeChain({ + ...typechainOptions, + filesToProcess, // only process changed files if not doing full rebuild + }) - if (!quiet) { - // eslint-disable-next-line no-console - console.log(`Successfully generated ${result.filesGenerated} typings!`) + if (!quiet) { + // eslint-disable-next-line no-console + console.log(`Successfully generated ${result.filesGenerated} typings!`) + } } // if this is not full rebuilding, always re-generate types for external artifacts diff --git a/packages/hardhat/src/types.ts b/packages/hardhat/src/types.ts index f16f47191..05e83d540 100644 --- a/packages/hardhat/src/types.ts +++ b/packages/hardhat/src/types.ts @@ -1,6 +1,7 @@ export interface TypechainConfig { outDir: string target: string + artifacts: string alwaysGenerateOverloads: boolean discriminateTypes: boolean tsNocheck: boolean diff --git a/packages/hardhat/test/project.test.ts b/packages/hardhat/test/project.test.ts index ae785ec4e..95bdc3a58 100644 --- a/packages/hardhat/test/project.test.ts +++ b/packages/hardhat/test/project.test.ts @@ -129,6 +129,86 @@ describe('Typechain x Hardhat', function () { expect(consoleLogMock).toHaveBeenCalledWith(['Successfully generated 14 typings for external artifacts!']) }) }) + + describe('when setting custom artifact glob', () => { + let oldArtifactGlob: string + beforeEach(function () { + oldArtifactGlob = this.hre.config.typechain.artifacts + }) + afterEach(function () { + this.hre.config.typechain.artifacts = oldArtifactGlob + }) + ;[true, false].forEach((forcedCompilation) => { + describe(`when type generation is ${forcedCompilation ? '' : 'not'} forced`, () => { + let subject: () => Promise + beforeEach(async function () { + if (forcedCompilation) { + await this.hre.run('compile', { noTypechain: true }) + } + subject = () => { + if (forcedCompilation) { + return this.hre.run('typechain') + } else { + return this.hre.run('compile') + } + } + }) + + describe('when glob matches some files', () => { + beforeEach(function () { + this.hre.config.typechain.artifacts = '**/EdgeCases.json' + }) + + it('includes build artifacts that match the glob', async function () { + const exists = existsSync(this.hre.config.typechain.outDir) + expect(exists).toEqual(false) + + await subject() + + const dir = await readdir(this.hre.config.typechain.outDir) + expect(dir.includes('EdgeCases.ts')).toEqual(true) + }) + + it('excludes build artifacts that do not match the glob', async function () { + const exists = existsSync(this.hre.config.typechain.outDir) + expect(exists).toEqual(false) + + await subject() + + const dir = await readdir(this.hre.config.typechain.outDir) + expect(dir.includes('TestContract.ts')).toEqual(false) + expect(dir.includes('TestContract1.ts')).toEqual(false) + }) + }) + describe('when glob matches no files', () => { + beforeEach(function () { + this.hre.config.typechain.artifacts = '**/THISDOESNTMATCHANYTHING.json' + }) + + describe('when no external artifacts are specified', () => { + it('does not generate any types', async function () { + const exists = existsSync(this.hre.config.typechain.outDir) + expect(exists).toEqual(false) + + await subject() + expect(existsSync(this.hre.config.typechain.outDir)).toEqual(false) + }) + }) + + describe('when external artifacts are specified', () => { + it('only generates types for external artifacts', async function () { + const exists = existsSync(this.hre.config.typechain.outDir) + expect(exists).toEqual(false) + + this.hre.config.typechain.externalArtifacts = ['externalArtifacts/*.json'] + await subject() + expect(existsSync(this.hre.config.typechain.outDir)).toEqual(true) + }) + }) + }) + }) + }) + }) }) describe('dontOverrideCompile', function () {