diff --git a/package.json b/package.json index d006df1..1adc236 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "commander": "^11.0.0", "node-fetch": "^2.6.1", "prettier": "^2.2.1", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "eslint": "^8.48.0" }, "resolutions": { "@wharfkit/antelope": "^1.0.0" diff --git a/src/commands/contract/helpers.ts b/src/commands/contract/helpers.ts index fec47a4..705e1a4 100644 --- a/src/commands/contract/helpers.ts +++ b/src/commands/contract/helpers.ts @@ -34,34 +34,35 @@ export function getCoreImports(abi: ABI.Def) { const {type} = findAbiType(field.type, abi) - if (type.includes(' | ')) { - coreImports.push('Variant') + const coreClass = findCoreClassImport(type) - type.split(' | ').forEach((typeString) => { - const coreType = findCoreClassImport(typeString) - - if (coreType) { - coreTypes.push(coreType) - } - }) - } else { - const coreClass = findCoreClassImport(type) + if (coreClass) { + coreImports.push(coreClass) + } - if (coreClass) { - coreImports.push(coreClass) - } + // We don't need to add action types unless the struct is an action param + if (!structIsActionParams) { + continue + } - // We don't need to add action types unless the struct is an action param - if (!structIsActionParams) { - continue - } + const coreType = findCoreType(type) - const coreType = findCoreType(type) + if (coreType) { + coreTypes.push(coreType) + } + } + } - if (coreType) { - coreTypes.push(coreType) + if (abi.variants.length != 0) { + coreImports.push('Variant') + for (const variant of abi.variants) { + variant.types.forEach((typeString) => { + const {type: abiType} = findAbiType(typeString, abi) + const coreClass = findCoreClassImport(abiType) + if (coreClass) { + coreImports.push(coreClass) } - } + }) } } @@ -180,6 +181,7 @@ export function findInternalType( ): string { const {type: typeString, decorator} = findType(type, abi, typeNamespace) + // TODO: inside findType, namespace is prefixed, but format internal is doing the same return formatInternalType(typeString, typeNamespace, abi, decorator) } @@ -189,7 +191,7 @@ function formatInternalType( abi: ABI.Def, decorator = '' ): string { - const structNames = abi.structs.map((struct) => struct.name.toLowerCase()) + const structNames = [...abi.structs, ...abi.variants].map((struct) => struct.name.toLowerCase()) let type @@ -209,36 +211,10 @@ function findAliasType(typeString: string, abi: ABI.Def): string | undefined { return alias?.type && `${alias?.type}${decorator || ''}` } -function findVariantType( - typeString: string, - abi: ABI.Def, - typeNamespace: string, - context: string -): string | undefined { - const abiVariant = abi.variants.find( - (variant) => variant.name.toLowerCase() === typeString.toLowerCase() - ) - - if (!abiVariant) { - return - } - - return abiVariant.types - .map((type) => { - if (context === 'external') { - return parseType(findExternalType(type, typeNamespace, abi)) - } else { - return parseType(findInternalType(type, typeNamespace, abi)) - } - }) - .join(' | ') -} - export function findAbiType( type: string, abi: ABI.Def, - typeNamespace = '', - context = 'internal' + typeNamespace = '' ): {type: string; decorator?: string} { let typeString = parseType(trim(type)) @@ -252,13 +228,9 @@ export function findAbiType( typeString = extractDecoratorResponse.type const decorator = extractDecoratorResponse.decorator - const variantType = findVariantType(typeString, abi, typeNamespace, context) - - if (variantType) { - return {type: variantType, decorator} - } - - const abiType = abi.structs.find((abiType) => abiType.name === typeString)?.name + const abiType = [...abi.structs, ...abi.variants].find( + (abiType) => abiType.name === typeString + )?.name if (abiType) { return {type: `${typeNamespace}${formatClassName(abiType)}`, decorator} @@ -268,13 +240,13 @@ export function findAbiType( } export function findExternalType(type: string, typeNamespace = '', abi: ABI.Def): string { - const {type: typeString, decorator} = findType(type, abi, typeNamespace, 'external') + const {type: typeString, decorator} = findType(type, abi, typeNamespace) return `${findCoreType(typeString) || capitalize(typeString)}${decorator === '[]' ? '[]' : ''}` } -function findType(type: string, abi: ABI.Def, typeNamespace?: string, context = 'internal') { - return findAbiType(type, abi, typeNamespace, context) +function findType(type: string, abi: ABI.Def, typeNamespace?: string) { + return findAbiType(type, abi, typeNamespace) } const decorators = ['?', '[]'] diff --git a/src/commands/contract/index.ts b/src/commands/contract/index.ts index 4fa6a77..d9237d7 100644 --- a/src/commands/contract/index.ts +++ b/src/commands/contract/index.ts @@ -1,17 +1,19 @@ +import * as eslint from 'eslint' +import * as fs from 'fs' import * as prettier from 'prettier' import * as ts from 'typescript' -import * as fs from 'fs' +import type {ABI} from '@wharfkit/session' import type {ABIDef} from '@wharfkit/antelope' import {abiToBlob, ContractKit} from '@wharfkit/contract' +import {log, makeClient} from '../../utils' import {generateContractClass} from './class' import {generateImportStatement, getCoreImports} from './helpers' import {generateActionNamesInterface, generateActionsNamespace} from './interfaces' import {generateTableMap, generateTableTypesInterface} from './maps' import {generateNamespace} from './namespace' import {generateStructClasses} from './structs' -import {log, makeClient} from '../../utils' import {generateActionsTypeAlias, generateRowType, generateTablesTypeAlias} from './types' const printer = ts.createPrinter() @@ -20,9 +22,13 @@ interface CommandOptions { url: string file?: string json?: string + eslintrc?: string } -export async function generateContractFromCommand(contractName, {url, file, json}: CommandOptions) { +export async function generateContractFromCommand( + contractName, + {url, file, json, eslintrc}: CommandOptions +) { let abi: ABIDef | undefined if (json) { @@ -52,7 +58,7 @@ export async function generateContractFromCommand(contractName, {url, file, json const contract = await contractKit.load(contractName) log(`Generating Contract helpers for ${contractName}...`) - const contractCode = await generateContract(contractName, contract.abi) + const contractCode = await generateContract(contractName, contract.abi, eslintrc) log(`Generated Contract helper class for ${contractName}...`) if (file) { @@ -64,7 +70,7 @@ export async function generateContractFromCommand(contractName, {url, file, json } } -export async function generateContract(contractName, abi) { +export async function generateContract(contractName: string, abi: ABI, eslintrc?: string) { try { const {classes, types} = getCoreImports(abi) @@ -181,7 +187,7 @@ export async function generateContract(contractName, abi) { ts.NodeFlags.None ) - return runPrettier(printer.printFile(sourceFile)) + return runPrettier(printer.printFile(sourceFile), eslintrc) } catch (e) { // eslint-disable-next-line no-console console.error(`An error occurred while generating the contract code: ${e}`) @@ -189,8 +195,9 @@ export async function generateContract(contractName, abi) { } } -function runPrettier(codeText: string) { - return prettier.format(codeText, { +async function runPrettier(codeText: string, eslintrc?: string): Promise { + // First prettier and then eslint fix, cause prettier result cann't pass eslint check + const prettiered = prettier.format(codeText, { arrowParens: 'always', bracketSpacing: false, endOfLine: 'lf', @@ -201,6 +208,15 @@ function runPrettier(codeText: string) { trailingComma: 'es5', parser: 'typescript', }) + + const linter = new eslint.ESLint({ + useEslintrc: false, + fix: true, + baseConfig: {}, + overrideConfigFile: eslintrc ? eslintrc : null, + }) + const results = await linter.lintText(prettiered) + return results[0].output ? results[0].output : prettiered } function cleanupImports(imports: string[]) { diff --git a/src/commands/contract/structs.ts b/src/commands/contract/structs.ts index 50fb134..cca69f9 100644 --- a/src/commands/contract/structs.ts +++ b/src/commands/contract/structs.ts @@ -13,6 +13,7 @@ interface FieldType { interface StructData { structName: string fields: FieldType[] + variant: boolean } interface TypeAlias { @@ -27,14 +28,34 @@ export function generateStructClasses(abi) { const structMembers: ts.ClassDeclaration[] = [] for (const struct of orderedStructs) { - structMembers.push(generateStruct(struct, abi, true)) + if (struct.variant) { + structMembers.push(generateVariant(struct, abi, true)) + } else { + structMembers.push(generateStruct(struct, abi, true)) + } } return structMembers } export function getActionFieldFromAbi(abi: any): StructData[] { - const structTypes: {structName: string; fields: FieldType[]}[] = [] + const structTypes: StructData[] = [] + + if (abi && abi.variants) { + for (const variant of abi.variants) { + structTypes.push({ + structName: variant.name, + fields: variant.types.map((t) => { + return { + name: 'value', + type: t, + optional: false, + } + }), + variant: true, + }) + } + } if (abi && abi.structs) { for (const struct of abi.structs) { @@ -48,13 +69,65 @@ export function getActionFieldFromAbi(abi: any): StructData[] { }) } - structTypes.push({structName: struct.name, fields}) + structTypes.push({structName: struct.name, fields, variant: false}) } } return structTypes } +export function generateVariant(variant, abi: any, isExport = false): ts.ClassDeclaration { + const decoratorArguments: (ts.ObjectLiteralExpression | ts.StringLiteral | ts.Identifier)[] = + variant.fields.map((field) => findVariantStructType(field.type, undefined, abi)) + + const decorators = [ + ts.factory.createDecorator( + ts.factory.createCallExpression( + ts.factory.createIdentifier('Variant.type'), + undefined, + [ + ts.factory.createStringLiteral(variant.structName), + ts.factory.createArrayLiteralExpression(decoratorArguments), + ] + ) + ), + ] + + const valueField = ts.factory.createPropertyDeclaration( + [], + ts.factory.createIdentifier('value'), + ts.factory.createToken(ts.SyntaxKind.ExclamationToken), + ts.factory.createUnionTypeNode( + variant.fields.map((field) => { + return ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier( + findFieldStructTypeString(field.type, undefined, abi) + ), + undefined + ) + }) + ), + undefined + ) + + return ts.factory.createClassDeclaration( + isExport + ? [...decorators, ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)] + : decorators, + ts.factory.createIdentifier(formatClassName(variant.structName)), + undefined, // typeParameters + [ + ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ + ts.factory.createExpressionWithTypeArguments( + ts.factory.createIdentifier('Struct'), + [] + ), + ]), + ], // heritageClauses + [valueField] + ) +} + export function generateStruct(struct, abi, isExport = false): ts.ClassDeclaration { const decorators = [ ts.factory.createDecorator( @@ -139,24 +212,11 @@ export function generateField( ), ] - let typeReferenceNode: ts.TypeReferenceNode | ts.UnionTypeNode - const structTypeString = findFieldStructTypeString(field.type, namespace, abi) - if (structTypeString.includes(' | ')) { - typeReferenceNode = ts.factory.createUnionTypeNode( - structTypeString.split(' | ').map((type) => { - return ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(type), - undefined - ) - }) - ) - } else { - typeReferenceNode = ts.factory.createTypeReferenceNode( - extractDecorator(structTypeString).type - ) - } + const typeReferenceNode = ts.factory.createTypeReferenceNode( + extractDecorator(structTypeString).type + ) let typeNode: ts.TypeNode @@ -229,6 +289,39 @@ function findDependencies( return dependencies } +function findVariantStructType( + typeString: string, + namespace: string | undefined, + abi: ABI.Def +): ts.Identifier | ts.StringLiteral | ts.ObjectLiteralExpression { + const variantTypeString = findFieldStructTypeString(typeString, namespace, abi) + + if (['string', 'string[]', 'boolean', 'boolean[]'].includes(variantTypeString.toLowerCase())) { + return ts.factory.createStringLiteral(formatFieldString(variantTypeString)) + } + + const isArray = variantTypeString.endsWith('[]') + if (isArray) { + const optionsProps: ts.ObjectLiteralElementLike[] = [] + optionsProps.push( + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('type'), + ts.factory.createIdentifier(extractDecorator(variantTypeString).type) + ) + ) + optionsProps.push( + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('array'), + ts.factory.createTrue() + ) + ) + + const optionsObject = ts.factory.createObjectLiteralExpression(optionsProps) + return optionsObject + } else { + return ts.factory.createIdentifier(variantTypeString) + } +} function findFieldStructType( typeString: string, namespace: string | undefined, @@ -238,10 +331,6 @@ function findFieldStructType( findFieldStructTypeString(typeString, namespace, abi) ).type - if (fieldTypeString.includes(' | ')) { - return ts.factory.createIdentifier('Variant') - } - if (['string', 'string[]', 'boolean', 'boolean[]'].includes(fieldTypeString.toLowerCase())) { return ts.factory.createStringLiteral(formatFieldString(fieldTypeString)) } diff --git a/src/index.ts b/src/index.ts index f4a072c..a0e371e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ program .argument('', 'The account name of the contract (e.g. "eosio.token")') .option('-f, --file [filename]', 'The path where the generated file will be saved') .option('-j, --json [json]', 'The path to a JSON file containing the contract ABI') + .option('-e, --eslintrc [eslintrc]', 'The eslintrc file to use') .requiredOption( '-u, --url ', 'The URL of the API to connect with (e.g. "https://jungle4.greymass.com")', diff --git a/test/data/contracts/mock-atomicassets.ts b/test/data/contracts/mock-atomicassets.ts index 3345089..23723d5 100644 --- a/test/data/contracts/mock-atomicassets.ts +++ b/test/data/contracts/mock-atomicassets.ts @@ -1,15 +1,9 @@ import type { Action, AssetType, - Bytes, - Float32, Float64Type, - Int16, Int32Type, - Int64, - Int8, NameType, - UInt16, UInt32Type, UInt64Type, } from '@wharfkit/antelope' @@ -17,10 +11,16 @@ import { ABI, Asset, Blob, + Bytes, + Float32, Float64, + Int16, Int32, + Int64, + Int8, Name, Struct, + UInt16, UInt32, UInt64, UInt8, @@ -283,6 +283,58 @@ export namespace ActionParams { } } export namespace Types { + @Variant.type( + 'variant_int8_int16_int32_int64_uint8_uint16_uint32_uint64_float32_float64_string_INT8_VEC_INT16_VEC_INT32_VEC_INT64_VEC_UINT8_VEC_UINT16_VEC_UINT32_VEC_UINT64_VEC_FLOAT_VEC_DOUBLE_VEC_STRING_VEC', + [ + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float32, + Float64, + 'string', + Bytes, + {type: Int16, array: true}, + {type: Int32, array: true}, + {type: Int64, array: true}, + {type: UInt8, array: true}, + {type: UInt16, array: true}, + {type: UInt32, array: true}, + {type: UInt64, array: true}, + {type: Float32, array: true}, + {type: Float64, array: true}, + 'string[]', + ] + ) + export class variant_int8_int16_int32_int64_uint8_uint16_uint32_uint64_float32_float64_string_INT8_VEC_INT16_VEC_INT32_VEC_INT64_VEC_UINT8_VEC_UINT16_VEC_UINT32_VEC_UINT64_VEC_FLOAT_VEC_DOUBLE_VEC_STRING_VEC extends Struct { + value!: + | Int8 + | Int16 + | Int32 + | Int64 + | UInt8 + | UInt16 + | UInt32 + | UInt64 + | Float32 + | Float64 + | string + | Bytes + | Int16[] + | Int32[] + | Int64[] + | UInt8[] + | UInt16[] + | UInt32[] + | UInt64[] + | Float32[] + | Float64[] + | string[] + } @Struct.type('FORMAT') export class FORMAT extends Struct { @Struct.field('string') @@ -418,30 +470,10 @@ export namespace Types { export class pair_string_ATOMIC_ATTRIBUTE extends Struct { @Struct.field('string') key!: string - @Struct.field(Variant) - value!: - | Int8 - | Int16 - | Int32 - | Int64 - | UInt8 - | UInt16 - | UInt32 - | UInt64 - | Float32 - | Float64 - | string - | Bytes - | Int16[] - | Int32[] - | Int64[] - | UInt8[] - | UInt16[] - | UInt32[] - | UInt64[] - | Float32[] - | Float64[] - | string[] + @Struct.field( + variant_int8_int16_int32_int64_uint8_uint16_uint32_uint64_float32_float64_string_INT8_VEC_INT16_VEC_INT32_VEC_INT64_VEC_UINT8_VEC_UINT16_VEC_UINT32_VEC_UINT64_VEC_FLOAT_VEC_DOUBLE_VEC_STRING_VEC + ) + value!: variant_int8_int16_int32_int64_uint8_uint16_uint32_uint64_float32_float64_string_INT8_VEC_INT16_VEC_INT32_VEC_INT64_VEC_UINT8_VEC_UINT16_VEC_UINT32_VEC_UINT64_VEC_FLOAT_VEC_DOUBLE_VEC_STRING_VEC } @Struct.type('createcol') export class createcol extends Struct { diff --git a/test/data/contracts/mock-boid.ts b/test/data/contracts/mock-boid.ts index e0f3cb1..81e9048 100644 --- a/test/data/contracts/mock-boid.ts +++ b/test/data/contracts/mock-boid.ts @@ -1,11 +1,7 @@ import type { Action, BytesType, - Float64, - Int16, Int32Type, - Int64, - Int8, NameType, PublicKeyType, SignatureType, @@ -20,7 +16,11 @@ import { Blob, Bytes, Float32, + Float64, + Int16, Int32, + Int64, + Int8, Name, PublicKey, Signature, @@ -349,6 +349,55 @@ export namespace ActionParams { } } export namespace Types { + @Variant.type('AtomicValue', [ + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float32, + Float64, + 'string', + {type: Int8, array: true}, + {type: Int16, array: true}, + {type: Int32, array: true}, + {type: Int64, array: true}, + Bytes, + {type: UInt16, array: true}, + {type: UInt32, array: true}, + {type: UInt64, array: true}, + {type: Float32, array: true}, + {type: Float64, array: true}, + 'string[]', + ]) + export class AtomicValue extends Struct { + value!: + | Int8 + | Int16 + | Int32 + | Int64 + | UInt8 + | UInt16 + | UInt32 + | UInt64 + | Float32 + | Float64 + | string + | Int8[] + | Int16[] + | Int32[] + | Int64[] + | Bytes + | UInt16[] + | UInt32[] + | UInt64[] + | Float32[] + | Float64[] + | string[] + } @Struct.type('AccountAuth') export class AccountAuth extends Struct { @Struct.field(PublicKey, {array: true}) @@ -468,30 +517,8 @@ export namespace Types { export class AtomicAttribute extends Struct { @Struct.field('string') key!: string - @Struct.field(Variant) - value!: - | Int8 - | Int16 - | Int32 - | Int64 - | UInt8 - | UInt16 - | UInt32 - | UInt64 - | Float32 - | Float64 - | string - | Int8[] - | Int16[] - | Int32[] - | Int64[] - | Bytes - | UInt16[] - | UInt32[] - | UInt64[] - | Float32[] - | Float64[] - | string[] + @Struct.field(AtomicValue) + value!: AtomicValue } @Struct.type('AtomicFormat') export class AtomicFormat extends Struct { diff --git a/test/data/contracts/mock-eosio.ts b/test/data/contracts/mock-eosio.ts index e558cb8..df4c94d 100644 --- a/test/data/contracts/mock-eosio.ts +++ b/test/data/contracts/mock-eosio.ts @@ -32,6 +32,7 @@ import { UInt32, UInt64, UInt8, + Variant, VarUInt, } from '@wharfkit/antelope' import type {ActionOptions, ContractArgs, PartialBy, Table} from '@wharfkit/contract' @@ -265,7 +266,7 @@ export namespace ActionParams { } export interface Regproducer2 { producer: NameType - producer_authority: Types.block_signing_authority_v0 + producer_authority: Types.variant_block_signing_authority_v0 url: string location: UInt16Type } @@ -402,6 +403,24 @@ export namespace ActionParams { } } export namespace Types { + @Struct.type('key_weight') + export class key_weight extends Struct { + @Struct.field(PublicKey) + key!: PublicKey + @Struct.field(UInt16) + weight!: UInt16 + } + @Struct.type('block_signing_authority_v0') + export class block_signing_authority_v0 extends Struct { + @Struct.field(UInt32) + threshold!: UInt32 + @Struct.field(key_weight, {array: true}) + keys!: key_weight[] + } + @Variant.type('variant_block_signing_authority_v0', [block_signing_authority_v0]) + export class variant_block_signing_authority_v0 extends Struct { + value!: block_signing_authority_v0 + } @Struct.type('abi_hash') export class abi_hash extends Struct { @Struct.field(Name) @@ -414,13 +433,6 @@ export namespace Types { @Struct.field(Checksum256) feature_digest!: Checksum256 } - @Struct.type('key_weight') - export class key_weight extends Struct { - @Struct.field(PublicKey) - key!: PublicKey - @Struct.field(UInt16) - weight!: UInt16 - } @Struct.type('permission_level') export class permission_level extends Struct { @Struct.field(Name) @@ -518,13 +530,6 @@ export namespace Types { @Struct.field(TimePoint) block_timestamp!: TimePoint } - @Struct.type('block_signing_authority_v0') - export class block_signing_authority_v0 extends Struct { - @Struct.field(UInt32) - threshold!: UInt32 - @Struct.field(key_weight, {array: true}) - keys!: key_weight[] - } @Struct.type('blockchain_parameters') export class blockchain_parameters extends Struct { @Struct.field(UInt64) @@ -989,8 +994,8 @@ export namespace Types { last_claim_time!: TimePoint @Struct.field(UInt16) location!: UInt16 - @Struct.field(block_signing_authority_v0, {optional: true}) - producer_authority?: block_signing_authority_v0 + @Struct.field(variant_block_signing_authority_v0, {optional: true}) + producer_authority?: variant_block_signing_authority_v0 } @Struct.type('producer_info2') export class producer_info2 extends Struct { @@ -1032,8 +1037,8 @@ export namespace Types { export class regproducer2 extends Struct { @Struct.field(Name) producer!: Name - @Struct.field(block_signing_authority_v0) - producer_authority!: block_signing_authority_v0 + @Struct.field(variant_block_signing_authority_v0) + producer_authority!: variant_block_signing_authority_v0 @Struct.field('string') url!: string @Struct.field(UInt16) diff --git a/test/tests/contract-command.ts b/test/tests/contract-command.ts index f6d6759..23f4f9e 100644 --- a/test/tests/contract-command.ts +++ b/test/tests/contract-command.ts @@ -82,4 +82,34 @@ suite('generateContractFromCommand', () => { 'process.stdout.write should be called with generated contract code' ) }) + + test('--eslintrc option uses the provided eslintrc', async function () { + try { + await generateContractFromCommand('someContractThatDoesntExist', { + url: 'http://eos.example-api.com', + json: './test/data/abis/rewards.gm.json', + eslintrc: './.eslintrc', + }) + } catch (error) { + assert.fail( + `Error should not be thrown when valid eslintrc file is passed. Error: ${error}` + ) + } + }) + + test("--eslintrc option throws an error when file doesn't exist", async function () { + try { + await generateContractFromCommand('someContract', { + url: 'http://eos.example-api.com', + json: './test/data/abis/rewards.gm.json', + eslintrc: './.does-not-exist-eslintrc', + }) + assert.fail('Error should be thrown when eslintrc file does not exist.') + } catch (error) { + assert( + (error as unknown as Error).message.includes('no such file or directory'), + 'Error should be thrown when eslintrc file does not exist.' + ) + } + }) }) diff --git a/test/utils/codegen.ts b/test/utils/codegen.ts index ecc0a39..1d93d96 100644 --- a/test/utils/codegen.ts +++ b/test/utils/codegen.ts @@ -9,7 +9,7 @@ export async function generateCodegenContract(contractName: string) { const abi = new ABI(JSON.parse(abiJson)) // Generate the code - const generatedCode = await generateContract(contractName, abi) + const generatedCode = await generateContract(contractName, abi, './.eslintrc') // Create the tmp directory under the test directory if it does not exist if (!fs.existsSync('test/tmp')) {