@wagmi/cli
Version:
Manage and generate code from Ethereum ABIs
227 lines (211 loc) • 7.47 kB
text/typescript
import { execa } from 'execa'
import { fdir } from 'fdir'
import { default as fs } from 'fs-extra'
import { basename, extname, join, resolve } from 'pathe'
import pc from 'picocolors'
import type { ContractConfig, Plugin } from '../config.js'
import * as logger from '../logger.js'
import type { Compute, RequiredBy } from '../types.js'
import { getIsPackageInstalled, getPackageManager } from '../utils/packages.js'
export const hardhatDefaultExcludes = ['build-info/**', '*.dbg.json']
export type HardhatConfig = {
/**
* Project's artifacts directory.
*
* Same as your project's `artifacts` [path configuration](https://hardhat.org/hardhat-runner/docs/config#path-configuration) option.
*
* @default 'artifacts/'
*/
artifacts?: string | undefined
/** Mapping of addresses to attach to artifacts. */
deployments?: { [key: string]: ContractConfig['address'] } | undefined
/** Artifact files to exclude. */
exclude?: string[] | undefined
/** Commands to run */
commands?:
| {
/**
* Remove build artifacts and cache directories on start up.
*
* @default `${packageManger} hardhat clean`
*/
clean?: string | boolean | undefined
/**
* Build Hardhat project before fetching artifacts.
*
* @default `${packageManger} hardhat compile`
*/
build?: string | boolean | undefined
/**
* Command to run when watched file or directory is changed.
*
* @default `${packageManger} hardhat compile`
*/
rebuild?: string | boolean | undefined
}
| undefined
/** Artifact files to include. */
include?: string[] | undefined
/** Optional prefix to prepend to artifact names. */
namePrefix?: string | undefined
/** Path to Hardhat project. */
project: string
/**
* Project's artifacts directory.
*
* Same as your project's `sources` [path configuration](https://hardhat.org/hardhat-runner/docs/config#path-configuration) option.
*
* @default 'contracts/'
*/
sources?: string | undefined
}
type HardhatResult = Compute<
RequiredBy<Plugin, 'contracts' | 'validate' | 'watch'>
>
/** Resolves ABIs from [Hardhat](https://github.com/NomicFoundation/hardhat) project. */
export function hardhat(config: HardhatConfig): HardhatResult {
const {
artifacts = 'artifacts',
deployments = {},
exclude = hardhatDefaultExcludes,
commands = {},
include = ['*.json'],
namePrefix = '',
sources = 'contracts',
} = config
function getContractName(artifact: { contractName: string }) {
return `${namePrefix}${artifact.contractName}`
}
async function getContract(artifactPath: string) {
const artifact = await fs.readJSON(artifactPath)
return {
abi: artifact.abi,
address: deployments[artifact.contractName],
name: getContractName(artifact),
}
}
function getArtifactPaths(artifactsDirectory: string) {
const crawler = new fdir().withBasePath().globWithOptions(
include.map((x) => `${artifactsDirectory}/**/${x}`),
{
dot: true,
ignore: exclude.map((x) => `${artifactsDirectory}/**/${x}`),
},
)
return crawler.crawl(artifactsDirectory).withPromise()
}
const project = resolve(process.cwd(), config.project)
const artifactsDirectory = join(project, artifacts)
const sourcesDirectory = join(project, sources)
const { build = true, clean = false, rebuild = true } = commands
return {
async contracts() {
if (clean) {
const packageManager = await getPackageManager(true)
const [command, ...options] = (
typeof clean === 'boolean' ? `${packageManager} hardhat clean` : clean
).split(' ')
await execa(command!, options, { cwd: project })
}
if (build) {
const packageManager = await getPackageManager(true)
const [command, ...options] = (
typeof build === 'boolean'
? `${packageManager} hardhat compile`
: build
).split(' ')
await execa(command!, options, { cwd: project })
}
if (!fs.pathExistsSync(artifactsDirectory))
throw new Error('Artifacts not found.')
const artifactPaths = await getArtifactPaths(artifactsDirectory)
const contracts = []
for (const artifactPath of artifactPaths) {
const contract = await getContract(artifactPath)
if (!contract.abi?.length) continue
contracts.push(contract)
}
return contracts
},
name: 'Hardhat',
async validate() {
// Check that project directory exists
if (!(await fs.pathExists(project)))
throw new Error(`Hardhat project ${pc.gray(project)} not found.`)
// Check that `hardhat` is installed
const packageName = 'hardhat'
const isPackageInstalled = await getIsPackageInstalled({
packageName,
cwd: project,
})
if (isPackageInstalled) return
throw new Error(`${packageName} must be installed to use Hardhat plugin.`)
},
watch: {
command: rebuild
? async () => {
logger.log(
`${pc.blue('Hardhat')} Watching project at ${pc.gray(project)}`,
)
const [command, ...options] = (
typeof rebuild === 'boolean'
? `${await getPackageManager(true)} hardhat compile`
: rebuild
).split(' ')
const { watch } = await import('chokidar')
const watcher = watch(sourcesDirectory, {
atomic: true,
awaitWriteFinish: true,
ignoreInitial: true,
persistent: true,
})
watcher.on('all', async (event, path) => {
if (event !== 'change' && event !== 'add' && event !== 'unlink')
return
logger.log(
`${pc.blue('Hardhat')} Detected ${event} at ${basename(path)}`,
)
const subprocess = execa(command!, options, {
cwd: project,
})
subprocess.stdout?.on('data', (data) => {
process.stdout.write(`${pc.blue('Hardhat')} ${data}`)
})
})
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)
async function shutdown() {
await watcher.close()
}
}
: undefined,
paths: [
artifactsDirectory,
...include.map((x) => `${artifactsDirectory}/**/${x}`),
...exclude.map((x) => `!${artifactsDirectory}/**/${x}`),
],
async onAdd(path) {
return getContract(path)
},
async onChange(path) {
return getContract(path)
},
async onRemove(path) {
const filename = basename(path)
const extension = extname(path)
// Since we can't use `getContractName`, guess from path
const removedContractName = `${namePrefix}${filename.replace(
extension,
'',
)}`
const artifactPaths = await getArtifactPaths(artifactsDirectory)
for (const artifactPath of artifactPaths) {
const contract = await getContract(artifactPath)
// If contract with same name exists, don't remove
if (contract.name === removedContractName) return
}
return removedContractName
},
},
}
}