Skip to content

Commit

Permalink
chore: Add export-deployments package
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista committed Feb 13, 2024
1 parent b215fef commit 0b2de13
Show file tree
Hide file tree
Showing 38 changed files with 2,981 additions and 2 deletions.
96 changes: 96 additions & 0 deletions packages/devtools-evm-hardhat-export-deployments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<p align="center">
<a href="https://layerzero.network">
<img alt="LayerZero" style="max-width: 500px" src="https://d3a2dpnnrypp5h.cloudfront.net/bridge-app/lz.png"/>
</a>
</p>

<h1 align="center">@layerzerolabs/devtools-evm-hardhat-export-deployments</h1>

<!-- The badges section -->
<p align="center">
<!-- Shields.io NPM published package version -->
<a href="https://www.npmjs.com/package/@layerzerolabs/devtools-evm-hardhat-export-deployments"><img alt="NPM Version" src="https://img.shields.io/npm/v/@layerzerolabs/devtools-evm-hardhat-export-deployments"/></a>
<!-- Shields.io NPM downloads -->
<a href="https://www.npmjs.com/package/@layerzerolabs/devtools-evm-hardhat-export-deployments"><img alt="Downloads" src="https://img.shields.io/npm/dm/@layerzerolabs/devtools-evm-hardhat-export-deployments"/></a>
<!-- Shields.io license badge -->
<a href="https://www.npmjs.com/package/@layerzerolabs/devtools-evm-hardhat-export-deployments"><img alt="NPM License" src="https://img.shields.io/npm/l/@layerzerolabs/devtools-evm-hardhat-export-deployments"/></a>
</p>

## Installation

```bash
yarn add @layerzerolabs/devtools-evm-hardhat-export-deployments

pnpm add @layerzerolabs/devtools-evm-hardhat-export-deployments

npm install @layerzerolabs/devtools-evm-hardhat-export-deployments
```

## Usage

### CLI

This package comes with a CLI interface and registers an executable called `@layerzerolabs/devtools-evm-hardhat-export-deployments`:

```bash
# When installed locally
@layerzerolabs/devtools-evm-hardhat-export-deployments --help

# Or using npx, preferred
npx @layerzerolabs/devtools-evm-hardhat-export-deployments
```

### Programatic usage

```typescript
// generateSafe is an error-safe function that returns an Either<Error, OutputFile[]> object
import { generateSafe } from "@layerzerolabs/devtools-evm-hardhat-export-deployments";

// if throwing an error is desired, generate is a better option
import { generate } from "@layerzerolabs/devtools-evm-hardhat-export-deployments";

generateSafe({
deploymentsDir: "./my/deployments",
outDir: "./generated",
});
```

If filtering of networks is necessary, `createIncludeDirent` utility can be used to construct a quick filtering function:

```typescript
import {
createIncludeDirent,
generateSafe,
} from "@layerzerolabs/devtools-evm-hardhat-export-deployments";

const includedNetworks = ["arbitrum-mainnet"];
const excludedNetworks = ["telos-testnet"];

generateSafe({
deploymentsDir: "./my/deployments",
outDir: "./generated",
includeNetworkDir: createIncludeDirent(includedNetworks, excludedNetworks),
});
```

Similar goes for deployment files:

```typescript
import {
createIncludeDirent,
generateSafe,
} from "@layerzerolabs/devtools-evm-hardhat-export-deployments";

// createIncludeDirent will handle the json extension internally
const includedContracts = ["MyContract", "OtherContract.json"];
const excludedContracts = ["TopSecret"];

generateSafe({
deploymentsDir: "./my/deployments",
outDir: "./generated",
includeDeploymentFile: createIncludeDirent(
includedContracts,
excludedContracts,
),
});
```
3 changes: 3 additions & 0 deletions packages/devtools-evm-hardhat-export-deployments/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node

require('./dist/cli/index.js');
11 changes: 11 additions & 0 deletions packages/devtools-evm-hardhat-export-deployments/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: 'node',
testTimeout: 15000,
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
};
48 changes: 48 additions & 0 deletions packages/devtools-evm-hardhat-export-deployments/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@layerzerolabs/devtools-evm-hardhat-export-deployments",
"version": "0.0.1",
"description": "Export hardhat deploy deployments file into typescript",
"repository": {
"type": "git",
"url": "git+https://github.com/LayerZero-Labs/devtools.git",
"directory": "packages/devtools-evm-hardhat-export-deployments"
},
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": "./cli.js",
"files": [
"./cli.js",
"./dist"
],
"scripts": {
"prebuild": "tsc -noEmit",
"build": "$npm_execpath tsup",
"clean": "rm -rf dist",
"dev": "$npm_execpath tsup --watch",
"lint": "$npm_execpath eslint '**/*.{js,ts,json}'",
"lint:fix": "eslint --fix '**/*.{js,ts,json}'",
"start": "node ./cli.js",
"test": "jest"
},
"dependencies": {
"chalk": "^4.1.2",
"fp-ts": "^2.16.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@layerzerolabs/io-devtools": "~0.1.2",
"@swc/core": "^1.4.0",
"@swc/jest": "^0.2.36",
"@types/jest": "^29.5.12",
"@types/node": "~18.18.14",
"commander": "^11.1.0",
"jest": "^29.6.2",
"tsup": "~8.0.1",
"typescript": "^5.1.6"
},
"publishConfig": {
"access": "public"
}
}
84 changes: 84 additions & 0 deletions packages/devtools-evm-hardhat-export-deployments/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Command } from 'commander'
import {
deploymentFilesOption,
deploymentsPathOption,
generatorOption,
logLevelOption,
networksOption,
outputPathOption,
} from '@/cli/options'
import { COLORS, SUCCESS_SYMBOL } from '@/common/logger'
import { createIncludeDirent, generateSafe } from '@/index'
import { pipe } from 'fp-ts/lib/function'
import * as E from 'fp-ts/Either'
import type { CodeGenerator } from '@/generator/types'
import { type LogLevel, createLogger, pluralizeNoun } from '@layerzerolabs/io-devtools'
import { version } from '../package.json'

// Ensure we exit with zero code on SIGTERM/SIGINT
//
// This is important for prompts
const handleSigTerm = () => process.exit(0)
process.on('SIGINT', handleSigTerm)
process.on('SIGTERM', handleSigTerm)

new Command('devtools-evm-hardhat-export-deployments')
.version(version)
.addOption(deploymentsPathOption)
.addOption(outputPathOption)
.addOption(logLevelOption)
.addOption(networksOption)
.addOption(deploymentFilesOption)
.addOption(generatorOption)
.action(
async (args: {
deployments: string
files?: string[]
networks?: string[]
logLevel: LogLevel
outDir: string
generator: CodeGenerator
}) => {
const defaultLogger = createLogger(args.logLevel)

defaultLogger.debug(COLORS.default`Exporting deployments from ${args.deployments} to ${args.outDir}`)

pipe(
generateSafe({
deploymentsDir: args.deployments,
outDir: args.outDir,
includeDeploymentFile: createIncludeDirent(args.files),
includeNetworkDir: createIncludeDirent(args.networks),
generator: args.generator,
}),
E.foldW(
(error) => {
defaultLogger.error(COLORS.error`Got an error during code generation: ${error}`)

process.exit(1)
},
(outputFiles) => {
const numGeneratedFiles = outputFiles.length

if (numGeneratedFiles === 0) {
defaultLogger.info(COLORS.default`No files generated, exiting`)
} else {
const message = pluralizeNoun(
numGeneratedFiles,
`Generated 1 file:`,
`Generated ${numGeneratedFiles} files:`
)

defaultLogger.info(COLORS.default`${SUCCESS_SYMBOL} ${message}`)
for (const { path } of outputFiles) {
defaultLogger.info(COLORS.default`\t${path}`)
}
}

process.exit(0)
}
)
)
}
)
.parseAsync()
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { InvalidOptionArgumentError, Option } from 'commander'
import { generatorMarkdown, generatorTypeScript } from '..'

export const logLevelOption = new Option('-l,--log-level <level>', 'Log level')
.choices(['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'])
.default('info')

export const deploymentsPathOption = new Option('-d,--deployments <path>', 'Path to the deployments folder').default(
'./deployments'
)

export const networksOption = new Option(
'-n,--networks <network names>',
'Comma separated list of network names to verify'
).argParser((value: string) => value.trim().split(/\s*,\s*/))

export const outputPathOption = new Option('-o,--out-dir <path>', 'Path to the output directory').default('./generated')

export const contractNamesOption = new Option(
'-c,--contracts <contract names>',
'Comma-separated list of case-sensitive contract names to verify'
).argParser((value: string) => value.trim().split(/\s*,\s*/))

export const deploymentFilesOption = new Option(
'-f,--files <deployment file name>',
'Comma-separated list of case-sensitive deployment file names to verify'
).argParser((value: string) => value.trim().split(/\s*,\s*/))

export const generatorOption = new Option('-g,--generator <generator type>', 'Type of generator to use')
.choices(['markdown', 'typescript'])
.argParser((value: string) => {
switch (value) {
case 'markdown':
return generatorMarkdown

case 'typescript':
return generatorTypeScript

default:
throw new InvalidOptionArgumentError(`Invalid generator specified: ${value}`)
}
})
.default(generatorTypeScript)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFileSyncSafe, mkdirSafe } from './fs'
import * as E from 'fp-ts/Either'
import { readFileSync, mkdirSync } from 'fs'

jest.mock('fs')

describe('common/fs', () => {
describe('readFileSyncSafe()', () => {
const readFileSyncMock = readFileSync as jest.Mock

it('should return a left either wrapped in error if readFileSync fails', () => {
readFileSyncMock.mockImplementation(() => {
throw 'borken'
})

expect(readFileSyncSafe('filename')).toEqual(E.left(new Error('borken')))
})

it('should return a right either with utf8 contents if readFileSync succeeds', () => {
readFileSyncMock.mockReturnValue('contents')

expect(readFileSyncSafe('filename')).toEqual(E.right('contents'))
})
})

describe('mkdirSafe()', () => {
const mkdirSyncMock = mkdirSync as jest.Mock

it('should return a left either if mkdirSync fails', () => {
mkdirSyncMock.mockImplementation(() => {
throw 'borken'
})

expect(mkdirSafe('some/dir/name')).toEqual(E.left(new Error('borken')))
})

it('should return a right either with the path if mkdirSync returns undefined', () => {
mkdirSyncMock.mockReturnValue(undefined)

expect(mkdirSafe('some/dir/name')).toEqual(E.right('some/dir/name'))
})

it('should return a right either with the path if mkdirSync returns a string', () => {
mkdirSyncMock.mockReturnValue('some/dir')

expect(mkdirSafe('some/dir/name')).toEqual(E.right('some/dir/name'))
})
})
})
38 changes: 38 additions & 0 deletions packages/devtools-evm-hardhat-export-deployments/src/common/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { type Dirent, lstatSync, mkdirSync, opendirSync, readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path'
import * as E from 'fp-ts/Either'
import { type Predicate } from 'fp-ts/lib/Predicate'
import { type OutputFile } from '../generator/types'

export const readFileSyncSafe = E.tryCatchK((path: string) => readFileSync(path, 'utf8'), E.toError)

export const isDirectorySafe = E.tryCatchK((path: string) => lstatSync(path).isDirectory(), E.toError)

export const mkdirSafe = E.tryCatchK((path: string): string => (mkdirSync(path, { recursive: true }), path), E.toError)

export const writeFileSyncSafe = E.tryCatchK(({ content, path }: OutputFile) => writeFileSync(path, content), E.toError)

/**
* Lists all the directory entries that pass the predicate function. The returned array
* will contain absolute paths of the directory entries.
*
* @param predicate `(dir: Dirent) => boolean`
* @returns `(path: string) => Either<Error, string[]>`
*/
export const listEntriesSafe = (predicate: Predicate<Dirent>) =>
E.tryCatchK((path: string) => {
const dir = opendirSync(path)

try {
let subDirectory: Dirent | null
const subDirectories: string[] = []

while ((subDirectory = dir.readSync())) {
if (predicate(subDirectory)) subDirectories.push(resolve(path, subDirectory.name))
}

return subDirectories
} finally {
dir.closeSync()
}
}, E.toError)
Loading

0 comments on commit 0b2de13

Please sign in to comment.