diff --git a/.changeset/tricky-colts-hear.md b/.changeset/tricky-colts-hear.md new file mode 100644 index 00000000..1bc7b2c7 --- /dev/null +++ b/.changeset/tricky-colts-hear.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/cli": patch +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/test-ts": patch +--- + +Refactor codegen, compile, build, deploy and test commands and their parameters diff --git a/packages/cli/src/commands/authenticate.ts b/packages/cli/src/commands/authenticate.ts index e1ec083f..1385b687 100644 --- a/packages/cli/src/commands/authenticate.ts +++ b/packages/cli/src/commands/authenticate.ts @@ -2,6 +2,9 @@ import { Command, Flags } from '@oclif/core' import { CredentialsManager, ProfileCredentials } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' + +export type AuthenticateFlags = FlagsType export default class Authenticate extends Command { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -28,7 +31,7 @@ export default class Authenticate extends Command { }), } - protected authenticate(flags: { profile?: string; 'api-key'?: string }): ProfileCredentials { + public static authenticate(cmd: Command, flags: AuthenticateFlags): ProfileCredentials { let apiKey = flags['api-key'] if (!apiKey) { try { @@ -36,7 +39,7 @@ export default class Authenticate extends Command { apiKey = credentials.apiKey } catch (error) { if (error instanceof Error) { - this.error(error.message, { + cmd.error(error.message, { code: 'AuthenticationRequired', suggestions: [ `Run ${log.highlightText('mimic login')} to authenticate`, diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index c4eac755..a9b617ce 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,37 +1,31 @@ -import { Command, Flags } from '@oclif/core' +import { Command } from '@oclif/core' + +import { FlagsType } from '../types' import Codegen from './codegen' import Compile from './compile' +export type BuildFlags = FlagsType + export default class Build extends Command { static override description = 'Runs code generation and then compiles the function' static override examples = [ - '<%= config.bin %> <%= command.id %> --manifest ./manifest.yaml --function src/function.ts --output ./build --types ./src/types', + '<%= config.bin %> <%= command.id %> --manifest ./manifest.yaml --function src/function.ts --build-directory ./build --types-directory ./src/types', ] static override flags = { - manifest: Flags.string({ char: 'm', description: 'manifest to use', default: 'manifest.yaml' }), - function: Flags.string({ char: 'f', description: 'function to compile', default: 'src/function.ts' }), - output: Flags.string({ char: 'o', description: 'output directory for build artifacts', default: './build' }), - types: Flags.string({ char: 'y', description: 'output directory for generated types', default: './src/types' }), - clean: Flags.boolean({ - char: 'c', - description: 'remove existing generated types before generating new files', - default: false, - }), + ...Codegen.flags, + ...Compile.flags, } public async run(): Promise { const { flags } = await this.parse(Build) - const { manifest, function: functionFile, output, types, clean } = flags - - const codegenArgs: string[] = ['--manifest', manifest, '--output', types] - if (clean) codegenArgs.push('--clean') - - await Codegen.run(codegenArgs) + await Build.build(this, flags) + } - const compileArgs: string[] = ['--function', functionFile, '--manifest', manifest, '--output', output] - await Compile.run(compileArgs) + public static async build(cmd: Command, flags: BuildFlags): Promise { + await Codegen.codegen(cmd, flags) + await Compile.compile(cmd, flags) } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 01d1deca..75bf856a 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -5,16 +5,24 @@ import { join } from 'path' import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler } from '../lib' import log from '../log' -import { Manifest } from '../types' +import { FlagsType, Manifest } from '../types' + +export type CodegenFlags = FlagsType export default class Codegen extends Command { static override description = 'Generates typed interfaces for declared inputs and ABIs from your manifest.yaml file' - static override examples = ['<%= config.bin %> <%= command.id %> --manifest ./manifest.yaml --output ./types'] + static override examples = [ + '<%= config.bin %> <%= command.id %> --manifest ./manifest.yaml --types-directory ./types', + ] static override flags = { manifest: Flags.string({ char: 'm', description: 'Specify a custom manifest file path', default: 'manifest.yaml' }), - output: Flags.string({ char: 'o', description: 'Ouput directory for generated types', default: './src/types' }), + 'types-directory': Flags.string({ + char: 't', + description: 'Output directory for generated types', + default: './src/types', + }), clean: Flags.boolean({ char: 'c', description: 'Remove existing generated types before generating new files', @@ -24,21 +32,24 @@ export default class Codegen extends Command { public async run(): Promise { const { flags } = await this.parse(Codegen) - const { manifest: manifestDir, output: outputDir, clean } = flags - const manifest = ManifestHandler.load(this, manifestDir) + await Codegen.codegen(this, flags) + } + public static async codegen(cmd: Command, flags: CodegenFlags): Promise { + const { manifest: manifestDir, 'types-directory': typesDir, clean } = flags + const manifest = ManifestHandler.load(cmd, manifestDir) if (clean) { const shouldDelete = await confirm({ - message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(outputDir)}. This action is ${log.warnText('irreversible')}`, + message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(typesDir)}. This action is ${log.warnText('irreversible')}`, default: false, }) if (!shouldDelete) { console.log('You can remove the --clean flag from your command') console.log('Stopping initialization...') - this.exit(0) + cmd.exit(0) } - log.startAction(`Deleting contents of ${outputDir}`) - if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true }) + log.startAction(`Deleting contents of ${typesDir}`) + if (fs.existsSync(typesDir)) fs.rmSync(typesDir, { recursive: true, force: true }) } log.startAction('Generating code') @@ -47,23 +58,22 @@ export default class Codegen extends Command { return } - if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) - - generateAbisCode(manifest, outputDir, manifestDir) - generateInputsCode(manifest, outputDir) + if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true }) + generateAbisCode(manifest, typesDir, manifestDir) + generateInputsCode(manifest, typesDir) log.stopAction() } } -function generateAbisCode(manifest: Manifest, outputDir: string, manifestDir: string) { +function generateAbisCode(manifest: Manifest, typesDir: string, manifestDir: string) { for (const [contractName, path] of Object.entries(manifest.abis)) { const abi = JSON.parse(fs.readFileSync(join(manifestDir, '../', path), 'utf-8')) const abiInterface = AbisInterfaceGenerator.generate(abi, contractName) - if (abiInterface.length > 0) fs.writeFileSync(`${outputDir}/${contractName}.ts`, abiInterface) + if (abiInterface.length > 0) fs.writeFileSync(`${typesDir}/${contractName}.ts`, abiInterface) } } -function generateInputsCode(manifest: Manifest, outputDir: string) { +function generateInputsCode(manifest: Manifest, typesDir: string) { const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) - if (inputsInterface.length > 0) fs.writeFileSync(`${outputDir}/index.ts`, inputsInterface) + if (inputsInterface.length > 0) fs.writeFileSync(`${typesDir}/index.ts`, inputsInterface) } diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 381c99d8..ef6130ca 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -5,29 +5,39 @@ import * as path from 'path' import ManifestHandler from '../lib/ManifestHandler' import { execBinCommand } from '../lib/packageManager' import log from '../log' +import { FlagsType } from '../types' + +export type CompileFlags = FlagsType export default class Compile extends Command { static override description = 'Compiles function' - static override examples = ['<%= config.bin %> <%= command.id %> --function src/function.ts --output ./output'] + static override examples = [ + '<%= config.bin %> <%= command.id %> --function src/function.ts --build-directory ./build', + ] static override flags = { - function: Flags.string({ char: 'f', description: 'function to compile', default: 'src/function.ts' }), - manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: 'manifest.yaml' }), - output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), + function: Flags.string({ char: 'f', description: 'Function to compile', default: 'src/function.ts' }), + manifest: Flags.string({ char: 'm', description: 'Manifest to validate', default: 'manifest.yaml' }), + 'build-directory': Flags.string({ char: 'b', description: 'Output directory for compilation', default: './build' }), } public async run(): Promise { const { flags } = await this.parse(Compile) - const { function: functionFile, output: outputDir, manifest: manifestDir } = flags + await Compile.compile(this, flags) + } - const absFunctionFile = path.resolve(functionFile) - const absOutputDir = path.resolve(outputDir) + public static async compile( + cmd: Command, + { function: functionDir, 'build-directory': buildDir, manifest: manifestDir }: CompileFlags + ): Promise { + const absFunctionFile = path.resolve(functionDir) + const absBuildDir = path.resolve(buildDir) - if (!fs.existsSync(absOutputDir)) fs.mkdirSync(absOutputDir, { recursive: true }) + if (!fs.existsSync(absBuildDir)) fs.mkdirSync(absBuildDir, { recursive: true }) log.startAction('Verifying Manifest') - const manifest = ManifestHandler.load(this, manifestDir) + const manifest = ManifestHandler.load(cmd, manifestDir) log.startAction('Compiling') const ascArgs = [ @@ -35,7 +45,7 @@ export default class Compile extends Command { '--target', 'release', '--outFile', - path.join(absOutputDir, 'function.wasm'), + path.join(absBuildDir, 'function.wasm'), '--optimize', '--exportRuntime', '--transform', @@ -44,7 +54,7 @@ export default class Compile extends Command { const result = execBinCommand('asc', ascArgs, process.cwd()) if (result.status !== 0) { - this.error('AssemblyScript compilation failed', { + cmd.error('AssemblyScript compilation failed', { code: 'BuildError', suggestions: ['Check the AssemblyScript file'], }) @@ -52,8 +62,8 @@ export default class Compile extends Command { log.startAction('Saving files') - fs.writeFileSync(path.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) + fs.writeFileSync(path.join(absBuildDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) log.stopAction() - console.log(`Build complete! Artifacts in ${outputDir}/`) + console.log(`Build complete! Artifacts in ${absBuildDir}/`) } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index d1224ccc..2f98c97b 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -1,4 +1,4 @@ -import { Flags } from '@oclif/core' +import { Command, Flags } from '@oclif/core' import axios, { AxiosError } from 'axios' import FormData from 'form-data' import * as fs from 'fs' @@ -6,61 +6,63 @@ import { join, resolve } from 'path' import { GENERIC_SUGGESTION } from '../errors' import { ProfileCredentials } from '../lib/CredentialsManager' -import { execBinCommand } from '../lib/packageManager' import log from '../log' +import { FlagsType } from '../types' import Authenticate from './authenticate' +import Build from './build' const MIMIC_REGISTRY_DEFAULT = 'https://api-protocol.mimic.fi' -export default class Deploy extends Authenticate { +export type DeployFlags = FlagsType + +export default class Deploy extends Command { static override description = 'Uploads your compiled function artifacts to IPFS and registers it into the Mimic Registry' static override examples = [ - '<%= config.bin %> <%= command.id %> --input ./dist --output ./dist', + '<%= config.bin %> <%= command.id %> --build-directory ./build', '<%= config.bin %> <%= command.id %> --profile staging', - '<%= config.bin %> <%= command.id %> --api-key MY_KEY --input ./dist --output ./dist', + '<%= config.bin %> <%= command.id %> --api-key MY_KEY --build-directory ./build', ] static override flags = { ...Authenticate.flags, - input: Flags.string({ char: 'i', description: 'Directory containing the compiled artifacts', default: './build' }), - output: Flags.string({ char: 'o', description: 'Output directory for deployment CID', default: './build' }), + ...Build.flags, + 'build-directory': Flags.string({ + char: 'b', + description: 'Output directory for compilation, or input directory for deployment when --skip-build is used', + default: './build', + }), url: Flags.string({ char: 'u', description: `Mimic Registry base URL`, default: MIMIC_REGISTRY_DEFAULT }), - 'skip-compile': Flags.boolean({ description: 'Skip codegen and compile steps before uploading', default: false }), + 'skip-build': Flags.boolean({ description: 'Skip codegen and compile steps before uploading', default: false }), } public async run(): Promise { const { flags } = await this.parse(Deploy) - const { input: inputDir, output: outputDir, 'skip-compile': skipCompile, url: registryUrl } = flags - const fullInputDir = resolve(inputDir) - const fullOutputDir = resolve(outputDir) + await this.deploy(this, flags) + } - let credentials = this.authenticate(flags) + public async deploy(cmd: Command, flags: DeployFlags): Promise { + const { 'build-directory': buildDir, 'skip-build': skipBuild, url: registryUrl } = flags + const absBuildDir = resolve(buildDir) - if (!skipCompile) { - const codegen = execBinCommand('mimic', ['codegen'], process.cwd()) - if (codegen.status !== 0) - this.error('Code generation failed', { code: 'CodegenError', suggestions: ['Fix manifest and ABI files'] }) + let credentials = Authenticate.authenticate(cmd, flags) - const compile = execBinCommand('mimic', ['compile', '--output', fullInputDir], process.cwd()) - if (compile.status !== 0) - this.error('Compilation failed', { code: 'BuildError', suggestions: ['Check the function source code'] }) - } + if (!skipBuild) await Build.build(cmd, flags) log.startAction('Validating') - if (!fs.existsSync(fullInputDir)) - this.error(`Directory ${log.highlightText(fullInputDir)} does not exist`, { + if (!fs.existsSync(absBuildDir) && skipBuild) + cmd.error(`Directory ${log.highlightText(absBuildDir)} does not exist`, { code: 'Directory Not Found', - suggestions: ['Use the --input flag to specify the correct path'], + suggestions: ['Use the --build-directory flag to specify the correct path'], }) - const neededFiles = ['manifest.json', 'function.wasm'].map((file) => join(fullInputDir, file)) + const neededFiles = ['manifest.json', 'function.wasm'].map((file) => join(absBuildDir, file)) for (const file of neededFiles) { if (!fs.existsSync(file)) - this.error(`Could not find ${file}`, { + cmd.error(`Could not find ${file}`, { code: 'File Not Found', suggestions: [`Use ${log.highlightText('mimic compile')} to generate the needed files`], }) @@ -71,9 +73,8 @@ export default class Deploy extends Authenticate { console.log(`IPFS CID: ${log.highlightText(CID)}`) log.stopAction() - if (!fs.existsSync(fullOutputDir)) fs.mkdirSync(fullOutputDir, { recursive: true }) - fs.writeFileSync(join(fullOutputDir, 'CID.json'), JSON.stringify({ CID }, null, 2)) - console.log(`CID saved at ${log.highlightText(fullOutputDir)}`) + fs.writeFileSync(join(absBuildDir, 'CID.json'), JSON.stringify({ CID }, null, 2)) + console.log(`CID saved at ${log.highlightText(absBuildDir)}`) console.log(`Function deployed!`) } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 1ca41ca5..5daf54c4 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -24,14 +24,14 @@ export default class Init extends Command { const { args, flags } = await this.parse(Init) const { directory } = args const { force } = flags - const fullDirectory = path.resolve(directory) + const absDir = path.resolve(directory) - if (force && fs.existsSync(fullDirectory) && fs.readdirSync(fullDirectory).length > 0) { + if (force && fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) { const shouldDelete = process.env.NODE_ENV === 'test' ? true : await confirm({ - message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(fullDirectory)}. This action is ${log.warnText('irreversible')}`, + message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(absDir)}. This action is ${log.warnText('irreversible')}`, default: false, }) if (!shouldDelete) { @@ -39,19 +39,19 @@ export default class Init extends Command { console.log('Stopping initialization...') this.exit(0) } - log.startAction(`Deleting contents of ${fullDirectory}`) + log.startAction(`Deleting contents of ${absDir}`) // Delete files individually instead of removing the entire directory to preserve // the directory reference. This prevents issues when the directory is the current // working directory, as removing it would cause the reference to be lost. - for (const file of fs.readdirSync(fullDirectory)) { - fs.rmSync(path.join(fullDirectory, file), { recursive: true, force: true }) + for (const file of fs.readdirSync(absDir)) { + fs.rmSync(path.join(absDir, file), { recursive: true, force: true }) } } log.startAction('Creating files') - if (fs.existsSync(fullDirectory) && fs.readdirSync(fullDirectory).length > 0) { - this.error(`Directory ${log.highlightText(fullDirectory)} is not empty`, { + if (fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) { + this.error(`Directory ${log.highlightText(absDir)} is not empty`, { code: 'DirectoryNotEmpty', suggestions: [ 'You can specify the directory as a positional argument', @@ -60,32 +60,32 @@ export default class Init extends Command { }) } - if (!fs.existsSync(fullDirectory)) { - fs.mkdirSync(fullDirectory, { recursive: true }) + if (!fs.existsSync(absDir)) { + fs.mkdirSync(absDir, { recursive: true }) } try { - await simpleGit().clone('https://github.com/mimic-protocol/init-template.git', fullDirectory) + await simpleGit().clone('https://github.com/mimic-protocol/init-template.git', absDir) - const gitDir = path.join(fullDirectory, '.git') + const gitDir = path.join(absDir, '.git') if (fs.existsSync(gitDir)) fs.rmSync(gitDir, { recursive: true, force: true }) } catch (error) { this.error(`Failed to clone template repository. Details: ${error}`) } - this.installDependencies(fullDirectory) - this.runCodegen(fullDirectory) + this.installDependencies(absDir) + this.runCodegen(absDir) log.stopAction() console.log('New project initialized!') } - installDependencies(fullDirectory: string) { + installDependencies(absDir: string) { if (process.env.NODE_ENV === 'test') return - installDependencies(fullDirectory) + installDependencies(absDir) } - runCodegen(fullDirectory: string) { + runCodegen(absDir: string) { if (process.env.NODE_ENV === 'test') return - execBinCommand('mimic', ['codegen'], fullDirectory) + execBinCommand('mimic', ['codegen'], absDir) } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 03018ec7..8157b578 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -2,31 +2,36 @@ import { Command, Flags } from '@oclif/core' import * as path from 'path' import { execBinCommand } from '../lib/packageManager' +import { FlagsType } from '../types' + +import Build from './build' + +export type TestFlags = FlagsType export default class Test extends Command { static override description = 'Runs function tests' - static override examples = ['<%= config.bin %> <%= command.id %> --directory ./'] + static override examples = ['<%= config.bin %> <%= command.id %> --directory ./tests'] static override flags = { - directory: Flags.string({ char: 'd', description: 'function directory', default: './' }), - 'skip-compile': Flags.boolean({ description: 'skip codegen and compile steps' }), + ...Build.flags, + directory: Flags.string({ char: 'd', description: 'Path to the testing directory', default: './tests' }), + 'skip-build': Flags.boolean({ description: 'Skip build before testing', default: false }), } public async run(): Promise { const { flags } = await this.parse(Test) - const { directory, 'skip-compile': skipCompile } = flags - const baseDir = path.resolve(directory) - const testPath = path.join(baseDir, 'tests') + await this.test(this, flags) + } + + public async test(cmd: Command, flags: TestFlags): Promise { + const { directory, 'skip-build': skipBuild } = flags + const baseDir = path.resolve('./') + const testPath = path.join(baseDir, directory) - if (!skipCompile) { - const cg = execBinCommand('mimic', ['codegen'], baseDir) - if (cg.status !== 0) this.exit(cg.status ?? 1) - const cp = execBinCommand('mimic', ['compile'], baseDir) - if (cp.status !== 0) this.exit(cp.status ?? 1) - } + if (!skipBuild) await Build.build(this, flags) const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', `${testPath}/**/*.spec.ts`], baseDir) - this.exit(result.status ?? 1) + cmd.exit(result.status ?? 1) } } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index e710ac0a..a3da84f8 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,3 +1,4 @@ +import { Command, Interfaces } from '@oclif/core' import { z } from 'zod' import { ManifestValidator } from './validators' @@ -48,3 +49,5 @@ export enum AssemblyPrimitiveTypes { } export type AssemblyTypes = `${LibTypes}` | `${AssemblyPrimitiveTypes}` | 'unknown' + +export type FlagsType = Interfaces.InferredFlags diff --git a/packages/cli/tests/commands/build.spec.ts b/packages/cli/tests/commands/build.spec.ts index 7b5306b4..bb24b090 100644 --- a/packages/cli/tests/commands/build.spec.ts +++ b/packages/cli/tests/commands/build.spec.ts @@ -9,11 +9,11 @@ describe('build', () => { const basePath = `${__dirname}/../fixtures` const functionPath = `${basePath}/functions/function.ts` const manifestPath = `${basePath}/manifests/manifest.yaml` - const outputDir = `${basePath}/output` + const buildDir = `${basePath}/output` const typesDir = `${basePath}/src/types` afterEach('cleanup generated files', () => { - if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true }) + if (fs.existsSync(buildDir)) fs.rmSync(buildDir, { recursive: true }) if (fs.existsSync(typesDir)) fs.rmSync(typesDir, { recursive: true }) }) @@ -24,30 +24,36 @@ describe('build', () => { const withCommonFlags = ( manifest: string, functionPath: string, - output: string, - types: string, + buildDirectory: string, + typesDirectory: string, extra: string[] = [] ) => { - return [`--manifest ${manifest}`, `--function ${functionPath}`, `--output ${output}`, `--types ${types}`, ...extra] + return [ + `--manifest ${manifest}`, + `--function ${functionPath}`, + `--build-directory ${buildDirectory}`, + `--types-directory ${typesDirectory}`, + ...extra, + ] } const itBuildsAndGeneratesTypes = (manifest: string, expectedInputs: object) => { it('generates types and build artifacts', async () => { - const command = buildCommand(withCommonFlags(manifest, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(manifest, functionPath, buildDir, typesDir)) const { stdout, error } = await runCommand(command) expect(error).to.be.undefined expect(stdout).to.include('Build complete!') // build artifacts - expect(fs.existsSync(path.join(outputDir, 'function.wasm'))).to.be.true - expect(fs.existsSync(path.join(outputDir, 'manifest.json'))).to.be.true + expect(fs.existsSync(path.join(buildDir, 'function.wasm'))).to.be.true + expect(fs.existsSync(path.join(buildDir, 'manifest.json'))).to.be.true // generated types expect(fs.existsSync(path.join(typesDir, 'index.ts'))).to.be.true expect(fs.existsSync(path.join(typesDir, 'ERC20.ts'))).to.be.true - const manifestJson = JSON.parse(fs.readFileSync(path.join(outputDir, 'manifest.json'), 'utf-8')) + const manifestJson = JSON.parse(fs.readFileSync(path.join(buildDir, 'manifest.json'), 'utf-8')) expect(manifestJson.inputs).to.be.deep.equal(expectedInputs) }) } @@ -84,7 +90,7 @@ describe('build', () => { context('when the function fails to compile', () => { const invalidFunctionPath = `${basePath}/functions/invalid-function.ts` - const command = buildCommand(withCommonFlags(manifestPath, invalidFunctionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(manifestPath, invalidFunctionPath, buildDir, typesDir)) itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) }) @@ -96,7 +102,7 @@ describe('build', () => { }) it('generates types without requiring clean', async () => { - const command = buildCommand(withCommonFlags(manifestPath, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(manifestPath, functionPath, buildDir, typesDir)) const { error } = await runCommand(command) expect(error).to.be.undefined @@ -109,28 +115,28 @@ describe('build', () => { context('when the manifest is not valid', () => { context('when the manifest has invalid fields', () => { const invalidManifest = `${basePath}/manifests/invalid-manifest.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(invalidManifest, functionPath, buildDir, typesDir)) itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) }) context('when the manifest has repeated fields', () => { const invalidManifest = `${basePath}/manifests/invalid-manifest-repeated.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(invalidManifest, functionPath, buildDir, typesDir)) itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) }) context('when the manifest is incomplete', () => { const invalidManifest = `${basePath}/manifests/incomplete-manifest.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(invalidManifest, functionPath, buildDir, typesDir)) itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) }) context('when the manifest is empty', () => { const invalidManifest = `${basePath}/manifests/empty-manifest.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(invalidManifest, functionPath, buildDir, typesDir)) itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) }) @@ -139,7 +145,7 @@ describe('build', () => { context('when the manifest does not exist', () => { const inexistentManifest = `${manifestPath}-none` - const command = buildCommand(withCommonFlags(inexistentManifest, functionPath, outputDir, typesDir)) + const command = buildCommand(withCommonFlags(inexistentManifest, functionPath, buildDir, typesDir)) itThrowsACliError(command, `Could not find ${inexistentManifest}`, 'FileNotFound', 1) }) diff --git a/packages/cli/tests/commands/codegen.spec.ts b/packages/cli/tests/commands/codegen.spec.ts index 8d6fefe3..e93a8a8f 100644 --- a/packages/cli/tests/commands/codegen.spec.ts +++ b/packages/cli/tests/commands/codegen.spec.ts @@ -8,48 +8,52 @@ import { itThrowsACliError } from '../helpers' describe('codegen', () => { const basePath = `${__dirname}/../fixtures` const manifestPath = `${basePath}/manifests/manifest.yaml` - const outputDir = `${basePath}/src/types` + const typesDirectory = `${basePath}/src/types` afterEach('delete generated files', () => { - if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true }) + if (fs.existsSync(typesDirectory)) fs.rmSync(typesDirectory, { recursive: true }) }) context('when the manifest exists', () => { context('when clean flag is not passed', () => { - const command = ['codegen', `--manifest ${manifestPath}`, `--output ${outputDir}`] + const command = ['codegen', `--manifest ${manifestPath}`, `--types-directory ${typesDirectory}`] context('when there are inputs and abis', () => { it('generates correctly', async () => { const { error } = await runCommand(command) expect(error).to.be.undefined - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true - expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true + expect(fs.existsSync(`${typesDirectory}/ERC20.ts`)).to.be.true + expect(fs.existsSync(`${typesDirectory}/index.ts`)).to.be.true }) }) context('when there are no inputs or abis', () => { - const command = ['codegen', `--manifest ${basePath}/manifests/simple-manifest.yaml`, `--output ${outputDir}`] + const command = [ + 'codegen', + `--manifest ${basePath}/manifests/simple-manifest.yaml`, + `--types-directory ${typesDirectory}`, + ] it('generates nothing', async () => { const { error } = await runCommand(command) expect(error).to.be.undefined - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.false - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.false - expect(fs.existsSync(`${outputDir}`)).to.be.false + expect(fs.existsSync(`${typesDirectory}/ERC20.ts`)).to.be.false + expect(fs.existsSync(`${typesDirectory}/ERC20.ts`)).to.be.false + expect(fs.existsSync(`${typesDirectory}`)).to.be.false }) }) }) }) context('when the manifest does not exist', () => { - const command = ['codegen', `--manifest ${manifestPath}fake`, `--output ${outputDir}`] + const command = ['codegen', `--manifest ${manifestPath}fake`, `--types-directory ${typesDirectory}`] itThrowsACliError(command, `Could not find ${manifestPath}fake`, 'FileNotFound', 1) }) context('when clean flag is passed', () => { let userResponse - const command = ['codegen', `--manifest ${manifestPath}`, `--output ${outputDir}`, '--clean'] + const command = ['codegen', `--manifest ${manifestPath}`, `--types-directory ${typesDirectory}`, '--clean'] context('when the user accepts the confirmation', () => { beforeEach('stub user input', () => { @@ -58,14 +62,14 @@ describe('codegen', () => { context('when the directory exists', () => { beforeEach('create directory', () => { - fs.mkdirSync(outputDir, { recursive: true }) + fs.mkdirSync(typesDirectory, { recursive: true }) }) it("deletes the folder and it's contents", async () => { const { status } = runCommandWithUserInput(command, userResponse) expect(status).to.be.equal(0) - expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + expect(fs.existsSync(`${typesDirectory}/index.ts`)).to.be.true + expect(fs.existsSync(`${typesDirectory}/ERC20.ts`)).to.be.true }) }) @@ -73,8 +77,8 @@ describe('codegen', () => { it("deletes the folder and it's contents", async () => { const { status } = runCommandWithUserInput(command, userResponse) expect(status).to.be.equal(0) - expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + expect(fs.existsSync(`${typesDirectory}/index.ts`)).to.be.true + expect(fs.existsSync(`${typesDirectory}/ERC20.ts`)).to.be.true }) }) }) diff --git a/packages/cli/tests/commands/compile.spec.ts b/packages/cli/tests/commands/compile.spec.ts index 31848a70..483f6345 100644 --- a/packages/cli/tests/commands/compile.spec.ts +++ b/packages/cli/tests/commands/compile.spec.ts @@ -16,7 +16,7 @@ describe('compile', () => { }) const buildCommand = (manifestPath: string, functionPath: string, outputDir: string) => { - return ['compile', `--function ${functionPath}`, `--manifest ${manifestPath}`, `--output ${outputDir}`] + return ['compile', `--function ${functionPath}`, `--manifest ${manifestPath}`, `--build-directory ${outputDir}`] } const itCreatesFilesCorrectly = (manifestPath: string, expectedInputs: object) => { diff --git a/packages/cli/tests/commands/deploy.spec.ts b/packages/cli/tests/commands/deploy.spec.ts index 43aa138a..a6cefe39 100644 --- a/packages/cli/tests/commands/deploy.spec.ts +++ b/packages/cli/tests/commands/deploy.spec.ts @@ -9,8 +9,7 @@ import { CredentialsManager } from '../../src/lib/CredentialsManager' import { backupCredentials, itThrowsACliError, restoreCredentials } from '../helpers' describe('deploy', () => { - const inputDir = join(__dirname, 'deploy-directory') - let outputDir = inputDir + const buildDir = join(__dirname, 'deploy-directory') context('when the default profile exists', () => { let credentialsManager: CredentialsManager @@ -31,163 +30,175 @@ describe('deploy', () => { credentialsManager.saveProfile('default', defaultKey) }) - const command = ['deploy', `-i ${inputDir}`, `-o ${outputDir}`, '--skip-compile'] + context('when uploading to registry is successful', () => { + const CID = '123' + let axiosMock: MockAdapter - context('when input directory exists', () => { - beforeEach('create input directory', () => { - fs.mkdirSync(inputDir, { recursive: true }) + beforeEach('create axios mock', () => { + axiosMock = new MockAdapter(axios) }) - afterEach('delete generated files', () => { - if (fs.existsSync(inputDir)) fs.rmSync(inputDir, { recursive: true }) + afterEach('restore axios mock', () => { + axiosMock.restore() }) - const createFile = (name: string) => { - fs.writeFileSync(`${inputDir}/${name}`, '') - } - - context('when the directory contains necessary files', () => { - let axiosMock: MockAdapter + beforeEach('mock registry response', () => { + axiosMock.onPost(/.*\/functions/gm).reply(200, { CID }) + }) - beforeEach('create files', () => { - ;['manifest.json', 'function.wasm'].map(createFile) - }) + context('when build directory exists', () => { + const command = ['deploy', `-b ${buildDir}`, '--skip-build'] - beforeEach('create axios mock', () => { - axiosMock = new MockAdapter(axios) + beforeEach('create build directory', () => { + fs.mkdirSync(buildDir, { recursive: true }) }) - afterEach('restore axios mock', () => { - axiosMock.restore() + afterEach('delete generated files', () => { + if (fs.existsSync(buildDir)) fs.rmSync(buildDir, { recursive: true }) }) - context('when uploading to registry is successful', () => { - const CID = '123' + const createFile = (name: string) => { + fs.writeFileSync(`${buildDir}/${name}`, '') + } - beforeEach('mock registry response', () => { - axiosMock.onPost(/.*\/functions/gm).reply(200, { CID }) + context('when the directory contains necessary files', () => { + beforeEach('create files', () => { + ;['manifest.json', 'function.wasm'].map(createFile) }) - context('when output directory exists', () => { - context('when the api key is provided', () => { - const apiKey = '456' - const apiKeyCommand = [...command, '--api-key', apiKey] + context('when the api key is provided', () => { + const apiKey = '456' + const apiKeyCommand = [...command, '--api-key', apiKey] - it('deploys successfully with the api key', async () => { - await runCommand(apiKeyCommand) + it('deploys successfully with the api key', async () => { + await runCommand(apiKeyCommand) - const requests = axiosMock.history.post - expect(requests).to.have.lengthOf(1) - expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) - }) + const requests = axiosMock.history.post + expect(requests).to.have.lengthOf(1) + expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) }) + }) - context('when a profile is provided', () => { - const apiKey = '789' - const profile = 'custom-profile' - const profileCommand = [...command, '--profile', profile] + context('when a profile is provided', () => { + const apiKey = '789' + const profile = 'custom-profile' + const profileCommand = [...command, '--profile', profile] - beforeEach('create profile', () => { - credentialsManager.saveProfile(profile, apiKey) - }) + beforeEach('create profile', () => { + credentialsManager.saveProfile(profile, apiKey) + }) - it('deploys successfully with the custom profile', async () => { - await runCommand(profileCommand) + it('deploys successfully with the custom profile', async () => { + await runCommand(profileCommand) - const requests = axiosMock.history.post - expect(requests).to.have.lengthOf(1) - expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) - }) + const requests = axiosMock.history.post + expect(requests).to.have.lengthOf(1) + expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) }) + }) - it('saves the CID on a file', async () => { - await runCommand(command) - const json = JSON.parse(fs.readFileSync(`${outputDir}/CID.json`, 'utf-8')) - expect(json.CID).to.be.equal(CID) - }) + it('saves the CID on a file', async () => { + await runCommand(command) + const json = JSON.parse(fs.readFileSync(`${buildDir}/CID.json`, 'utf-8')) + expect(json.CID).to.be.equal(CID) }) + }) - context('when output directory does not exist', () => { - const noOutDir = `${outputDir}/does-not-exist` - const noOutDirCommand = ['deploy', `-i ${inputDir}`, `-o ${noOutDir}`, '--skip-compile'] + context('when the directory does not contain the necessary files', () => { + context('when the directory contains no files', () => { + itThrowsACliError(command, `Could not find ${buildDir}/manifest.json`, 'File Not Found', 1) + }) - it('saves the CID on a file', async () => { - await runCommand(noOutDirCommand) - const json = JSON.parse(fs.readFileSync(`${noOutDir}/CID.json`, 'utf-8')) - expect(json.CID).to.be.equal(CID) + context('when the directory contains only one file', () => { + beforeEach('create file', () => { + createFile('manifest.json') }) + + itThrowsACliError(command, `Could not find ${buildDir}/function.wasm`, 'File Not Found', 1) }) }) + }) - context('when uploading to registry is not successful', () => { - context('when there is a bad request failure', () => { - context('when the error message is present', () => { - const message = 'Function with same name and version already exists' + context('when build directory does not exist', () => { + context('when the build is skipped', () => { + const command = ['deploy', `-b ${buildDir}`, '--skip-build'] - beforeEach('mock response', () => { - axiosMock.onPost(/.*\/functions/gm).reply(400, { content: { message } }) - }) + itThrowsACliError(command, `Directory ${buildDir} does not exist`, 'Directory Not Found', 1) + }) + }) + }) - itThrowsACliError(command, message, 'Bad Request', 1) - }) + context('when uploading to registry is not successful', () => { + const command = ['deploy', `-b ${buildDir}`, '--skip-build'] + let axiosMock: MockAdapter - context('when the error message is not present', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*\/functions/gm).reply(400, { content: { errors: ['some error'] } }) - }) + beforeEach('create build directory', () => { + fs.mkdirSync(buildDir, { recursive: true }) + }) - itThrowsACliError(command, 'Failed to upload to registry', 'Bad Request', 1) - }) - }) + afterEach('delete generated files', () => { + if (fs.existsSync(buildDir)) fs.rmSync(buildDir, { recursive: true }) + }) - context('when there is an authorization failure', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*/).reply(401) - }) + const createFile = (name: string) => { + fs.writeFileSync(`${buildDir}/${name}`, '') + } - itThrowsACliError(command, 'Failed to upload to registry', 'Unauthorized', 1) - }) + beforeEach('create files', () => { + ;['manifest.json', 'function.wasm'].map(createFile) + }) - context('when there is an authentication failure', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*/).reply(403) - }) + beforeEach('create axios mock', () => { + axiosMock = new MockAdapter(axios) + }) + + afterEach('restore axios mock', () => { + axiosMock.restore() + }) + + context('when there is a bad request failure', () => { + context('when the error message is present', () => { + const message = 'Function with same name and version already exists' - itThrowsACliError(command, 'Failed to upload to registry', 'Invalid api key', 1) + beforeEach('mock response', () => { + axiosMock.onPost(/.*\/functions/gm).reply(400, { content: { message } }) }) - context('when there is a generic error', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*/).reply(501) - }) + itThrowsACliError(command, message, 'Bad Request', 1) + }) - itThrowsACliError( - command, - 'Failed to upload to registry - Request failed with status code 501', - '501 Error', - 1 - ) + context('when the error message is not present', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*\/functions/gm).reply(400, { content: { errors: ['some error'] } }) }) + + itThrowsACliError(command, 'Failed to upload to registry', 'Bad Request', 1) }) }) - context('when the directory does not contain the necessary files', () => { - context('when the directory contains no files', () => { - itThrowsACliError(command, `Could not find ${inputDir}/manifest.json`, 'File Not Found', 1) + context('when there is an authorization failure', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*/).reply(401) }) - context('when the directory contains only one file', () => { - beforeEach('create file', () => { - createFile('manifest.json') - }) + itThrowsACliError(command, 'Failed to upload to registry', 'Unauthorized', 1) + }) - itThrowsACliError(command, `Could not find ${inputDir}/function.wasm`, 'File Not Found', 1) + context('when there is an authentication failure', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*/).reply(403) }) + + itThrowsACliError(command, 'Failed to upload to registry', 'Invalid api key', 1) }) - }) - context('when input directory does not exist', () => { - itThrowsACliError(command, `Directory ${inputDir} does not exist`, 'Directory Not Found', 1) + context('when there is a generic error', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*/).reply(501) + }) + + itThrowsACliError(command, 'Failed to upload to registry - Request failed with status code 501', '501 Error', 1) + }) }) }) diff --git a/packages/integration/tests/integration.spec.ts b/packages/integration/tests/integration.spec.ts index 6308bde6..47c36d7c 100644 --- a/packages/integration/tests/integration.spec.ts +++ b/packages/integration/tests/integration.spec.ts @@ -26,32 +26,35 @@ async function runTestCase(testCase: string): Promise { const path = join(__dirname, testCase) const manifestPath = join(path, 'manifest.yaml') const functionPath = join(path, 'src', 'function.ts') - const outputPath = join(path, 'build') + const buildDirectory = join(path, 'build') let compilationSuccessful = true before('build function', () => { const typesOutputPath = join(path, 'src', 'types') - const resultCodegen = spawnSync('yarn', ['mimic', 'codegen', '-m', manifestPath, '-o', typesOutputPath]) + const resultBuild = spawnSync('yarn', [ + 'mimic', + 'build', + '-m', + manifestPath, + '-t', + typesOutputPath, + '-b', + buildDirectory, + '-f', + functionPath, + ]) - if (resultCodegen.status !== 0) { + if (resultBuild.status !== 0) { compilationSuccessful = false - console.error(`Codegen error in test case '${testCase}':`) - console.error(resultCodegen.stderr.toString()) + console.error(`Build error in test case '${testCase}':`) + console.error(resultBuild.stderr.toString()) return } - - const result = spawnSync('yarn', ['mimic', 'compile', '-m', manifestPath, '-f', functionPath, '-o', outputPath]) - - if (result.status !== 0) { - compilationSuccessful = false - console.error(`Compilation error in test case '${testCase}':`) - console.error(result.stderr.toString()) - } }) after('delete artifacts', () => { - fs.rmSync(outputPath, { recursive: true }) + fs.rmSync(buildDirectory, { recursive: true }) fs.rmSync(join(path, 'test.log')) }) @@ -60,7 +63,7 @@ async function runTestCase(testCase: string): Promise { throw new Error(`Unable to run test case '${testCase}' due to compilation errors`) } - const mockRunner = new RunnerMock(outputPath, path) + const mockRunner = new RunnerMock(buildDirectory, path) mockRunner.run() const expectedLogs = loadLogs(join(path, 'expected.log')) const testLogs = loadLogs(join(path, 'test.log'))