From c618a4ed881fca99c514ba026dc881f83f6ca946 Mon Sep 17 00:00:00 2001 From: Adam Avramov Date: Sat, 4 Nov 2023 20:07:29 +0200 Subject: [PATCH] wip 40: 62.74% coverage, 27 type errors --- fadroma.test.ts | 7 +- fadroma.ts | 101 +++++++++----------- ops/project.ts | 245 +++++++++++++++++++++++------------------------- ops/tools.ts | 6 +- 4 files changed, 166 insertions(+), 193 deletions(-) diff --git a/fadroma.test.ts b/fadroma.test.ts index bc3cbe18098..f9385e60aa3 100644 --- a/fadroma.test.ts +++ b/fadroma.test.ts @@ -5,8 +5,6 @@ import { Suite } from '@hackbg/ensuite' import { ProjectCommands } from './fadroma' -new ProjectCommands() - export default new Suite([ ['agent', () => import('./agent/agent.test')], ['build', () => import('./ops/build.test')], @@ -16,4 +14,9 @@ export default new Suite([ ['stores', () => import('./ops/stores.test')], ['tools', () => import('./ops/tools.test')], ['connect', () => import('./connect/connect.test')], + ['cli', testCLI] ]) + +export async function testCLI () { + const cli = new ProjectCommands() +} diff --git a/fadroma.ts b/fadroma.ts index 0cb8e76db84..9dd2fd634b4 100644 --- a/fadroma.ts +++ b/fadroma.ts @@ -37,65 +37,54 @@ export class ProjectCommands extends CommandContext { readonly project: Project = new Project('project', process.env.FADROMA_PROJECT || process.cwd()) ) { super() - this - .addCommand( - 'run', 'execute a script', - (script: string, ...args: string[]) => runScript({ project: this.project, script, args })) - .addCommand( - 'repl', 'open a project REPL (optionally executing a script first)', - (script: string, ...args: string[]) => runRepl({ project: this.project, script, args })) - .addCommand( - 'status', 'show the status of the project', - () => Prompts.logProjectStatus(this.getProject())) - .addCommand( - 'create', 'create a new project', - Project.create) - + this.command('run', 'execute a script', + (script: string, ...args: string[]) => runScript({ project: this.project, script, args })) + this.command('repl', 'open an interactive Fadroma shell', + (script: string, ...args: string[]) => runRepl({ project: this.project, script, args })) + this.command('status', 'show the status of the project', + () => console.log(JSON.stringify(this.project, null, 2))) + this.command('create', 'create a new project', + Project.create) if (this.project) { - this - .addCommand('build', 'build the project or specific contracts from it', - (...names: string[]) => this.getProject().getDeployment().build({ - compiler: Compilers.getCompiler(), })) - .addCommand('rebuild', 'rebuild the project or specific contracts from it', - (...names: string[]) => this.getProject().getDeployment().build({ - rebuild: true, - compiler: Compilers.getCompiler(), })) - .addCommand('upload', 'upload the project or specific contracts from it', - (...names: string[]) => this.getProject().getDeployment().upload({ - compiler: Compilers.getCompiler(), - uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), })) - .addCommand('reupload', 'reupload the project or specific contracts from it', - (...names: string[]) => this.getProject().getDeployment().upload({ - compiler: Compilers.getCompiler(), - uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), reupload: true })) - .addCommand('deploy', 'deploy this project or continue an interrupted deployment', - (...args: string[]) => this.getProject().getDeployment().deploy({ - compiler: Compilers.getCompiler(), - uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), - deployStore: Stores.getDeployStore(), deployer: this.getAgent(), - deployment: this.getProject().getDeployment() })) - .addCommand('redeploy', 'redeploy this project from scratch', - (...args: string[]) => this.getProject().getDeployment().deploy({ - compiler: Compilers.getCompiler(), - uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), - deployStore: Stores.getDeployStore(), deployer: this.getAgent(), - deployment: this.getProject().createDeployment() })) - .addCommand('select', `activate another deployment`, - async (name?: string): Promise => selectDeployment( - this.project.root, name)) - .addCommand('export', `export current deployment to JSON`, - async (path?: string) => exportDeployment( - this.project.root, await this.getProject().getDeployment(), path)) - .addCommand('reset', 'stop and erase running devnets', - (...ids: ChainId[]) => Devnets.Container.deleteMany( - this.project.root, ids)) + this.command('build', 'build the project or specific contracts from it', + (...names: string[]) => this.project.getDeployment().build({ + compiler: Compilers.getCompiler(), })) + this.command('rebuild', 'rebuild the project or specific contracts from it', + (...names: string[]) => this.project.getDeployment().build({ + rebuild: true, + compiler: Compilers.getCompiler(), })) + this.command('upload', 'upload the project or specific contracts from it', + (...names: string[]) => this.project.getDeployment().upload({ + compiler: Compilers.getCompiler(), + uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), })) + this.command('reupload', 'reupload the project or specific contracts from it', + (...names: string[]) => this.project.getDeployment().upload({ + compiler: Compilers.getCompiler(), + uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), reupload: true })) + this.command('deploy', 'deploy this project or continue an interrupted deployment', + (...args: string[]) => this.project.getDeployment().deploy({ + compiler: Compilers.getCompiler(), + uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), + deployStore: Stores.getDeployStore(), deployer: this.getAgent(), + deployment: this.project.getDeployment() })) + this.command('redeploy', 'redeploy this project from scratch', + (...args: string[]) => this.project.getDeployment().deploy({ + compiler: Compilers.getCompiler(), + uploadStore: Stores.getUploadStore(), uploader: this.getAgent(), + deployStore: Stores.getDeployStore(), deployer: this.getAgent(), + deployment: this.project.createDeployment() })) + this.command('select', `activate another deployment`, + async (name?: string): Promise => selectDeployment( + this.project.root, name)) + this.command('export', `export current deployment to JSON`, + async (path?: string) => exportDeployment( + this.project.root, await this.project.getDeployment(), path)) + this.command('reset', 'stop and erase running devnets', + (...ids: ChainId[]) => Devnets.Container.deleteMany( + this.project.root, ids)) } } - getProject (): Project { - return new Project('name_from_package_json', this.root) - } - getAgent (): Agent { } } @@ -164,7 +153,7 @@ export function exportDeployment ( // If passed a directory, generate file name let file = $(path) if (file.isDirectory()) { - file = file.in(`${name}_@_${timestamp()}.json`) + file = file.in(`${deployment.name}_@_${timestamp()}.json`) } // Serialize and write the deployment. const state = deployment.toReceipt() diff --git a/ops/project.ts b/ops/project.ts index 9b1033052d0..dc73b9250b4 100644 --- a/ops/project.ts +++ b/ops/project.ts @@ -107,57 +107,16 @@ export class ProjectRoot { } export class Project extends ProjectRoot { - // warning: do not convert static create methods - // to arrow functions or inheritance will break - static async create (properties: Parameters[0] = {}) { - properties.tools ??= new Tools.SystemTools() - properties.interactive ??= properties.tools.interactive - const project = await super.create(properties) as Project - const name = await Promise.resolve(properties.interactive ? this.askName() : undefined) - throw new Error('bang') - if (!name) { - throw new Error("missing project name") - } - project.readme - .save(Tools.generateReadme(name)) - project.packageJson - .save(Tools.generatePackageJson(name)) - project.gitIgnore - .save(Tools.generateGitIgnore()) - project.envFile - .save(Tools.generateEnvFile()) - project.shellNix - .save(Tools.generateShellNix(name)) - project.apiIndex - .save(Tools.generateApiIndex(name, {})) - project.projectIndex - .save(Tools.generateProjectIndex(name)) - project.testIndex - .save(Tools.generateTestIndex(name)) - Tools.runNPMInstall(project, properties.tools) - return project - } - - stateDir = $(this.root, 'state') - .as(OpaqueDirectory) - wasmDir = $(this.root, 'wasm') - .as(OpaqueDirectory) - envFile = $(this.root, '.env') - .as(TextFile) - gitIgnore = $(this.root, '.gitignore') - .as(TextFile) - packageJson = $(this.root, 'package.json') - .as(JSONFile) - readme = $(this.root, 'README.md') - .as(TextFile) - shellNix = $(this.root, 'shell.nix') - .as(TextFile) - apiIndex = $(this.root, 'index.ts') - .as(TextFile) - projectIndex = $(this.root, 'fadroma.config.ts') - .as(TextFile) - testIndex = $(this.root, 'test.ts') - .as(TextFile) + stateDir = $(this.root, 'state').as(OpaqueDirectory) + wasmDir = $(this.root, 'wasm').as(OpaqueDirectory) + envFile = $(this.root, '.env').as(TextFile) + gitIgnore = $(this.root, '.gitignore').as(TextFile) + packageJson = $(this.root, 'package.json').as(JSONFile) + readme = $(this.root, 'README.md').as(TextFile) + shellNix = $(this.root, 'shell.nix').as(TextFile) + apiIndex = $(this.root, 'index.ts').as(TextFile) + projectIndex = $(this.root, 'fadroma.config.ts').as(TextFile) + testIndex = $(this.root, 'test.ts').as(TextFile) logStatus () { return super.logStatus().br() @@ -168,60 +127,81 @@ export class Project extends ProjectRoot { } createDeployment (): Deployment { + console.warn('createDeployment: not implemented') + return new Deployment() } getDeployment (): Deployment { + console.warn('getDeployment: not implemented') + return new Deployment() } + // warning: do not convert static create methods + // to arrow functions or inheritance will break + static async create (properties: Parameters[0] = {}) { + properties.tools ??= new Tools.SystemTools() + properties.interactive ??= properties.tools.interactive + const project = await super.create(properties) as Project + properties.name ??= await Promise.resolve(properties.interactive ? this.askName() : undefined) + if (!properties.name) { + throw new Error("missing project name") + } + project.readme.save(Tools.generateReadme(properties.name)) + project.packageJson.save(Tools.generatePackageJson(properties.name)) + project.gitIgnore.save(Tools.generateGitIgnore()) + project.envFile.save(Tools.generateEnvFile()) + project.shellNix.save(Tools.generateShellNix(properties.name)) + project.apiIndex.save(Tools.generateApiIndex(properties.name, {})) + project.projectIndex.save(Tools.generateProjectIndex(properties.name)) + project.testIndex.save(Tools.generateTestIndex(properties.name)) + Tools.runNPMInstall(project, properties.tools) + return project + } static async askTemplates (name: string): Promise>> { - return Prompts.askUntilDone({}, (state) => Prompts.askSelect([ `Project ${name} contains ${Object.keys(state).length} contract(s):\n`, ` ${Object.keys(state).join(',\n ')}` ].join(''), [ - { title: `Add contract template to the project`, value: defineContract }, - { title: `Remove contract template`, value: undefineContract }, - { title: `Rename contract template`, value: renameContract }, + { title: `Add contract template to the project`, value: this.defineContract }, + { title: `Remove contract template`, value: this.undefineContract }, + { title: `Rename contract template`, value: this.renameContract }, { title: `(done)`, value: null }, ])) - - async function defineContract (state: Record) { - let crate - crate = await Prompts.askText('Enter a name for the new contract (lowercase a-z, 0-9, dash, underscore):')??'' - if (!isNaN(crate[0] as any)) { - console.info('Contract name cannot start with a digit.') - crate = '' - } - if (crate) { - state[crate] = { crate } - } + } + protected static async defineContract (state: Record) { + let crate + crate = await Prompts.askText('Enter a name for the new contract (lowercase a-z, 0-9, dash, underscore):')??'' + if (!isNaN(crate[0] as any)) { + console.info('Contract name cannot start with a digit.') + crate = '' } - - async function undefineContract (state: Record) { - const name = await Prompts.askSelect(`Select contract to remove from project scope:`, [ - ...Object.keys(state).map(contract=>({ title: contract, value: contract })), - { title: `(done)`, value: null }, - ]) - if (name === null) return - delete state[name] + if (crate) { + state[crate] = { crate } } - - async function renameContract (state: Record) { - const name = await Prompts.askSelect(`Select contract to rename:`, [ - ...Object.keys(state).map(contract=>({ title: contract, value: contract })), - { title: `(done)`, value: null }, - ]) - if (name === null) return - const newName = await Prompts.askText(`Enter a new name for ${name} (a-z, 0-9, dash/underscore):`) - if (newName) { - state[newName] = Object.assign(state[name], { name: newName }) - delete state[name] - } + } + protected static async undefineContract (state: Record) { + const name = await Prompts.askSelect(`Select contract to remove from project scope:`, [ + ...Object.keys(state).map(contract=>({ title: contract, value: contract })), + { title: `(done)`, value: null }, + ]) + if (name === null) return + delete state[name] + } + protected static async renameContract (state: Record) { + const name = await Prompts.askSelect(`Select contract to rename:`, [ + ...Object.keys(state).map(contract=>({ title: contract, value: contract })), + { title: `(done)`, value: null }, + ]) + if (name === null) return + const newName = await Prompts.askText(`Enter a new name for ${name} (a-z, 0-9, dash/underscore):`) + if (newName) { + state[newName] = Object.assign(state[name], { name: newName }) + delete state[name] } - } } +/** A NPM-only project that contains only scripts, no Rust crates. */ export class ScriptProject extends Project { logStatus () { return super.logStatus().br() @@ -229,11 +209,29 @@ export class ScriptProject extends Project { } } +/** Base class for project that contains a Cargo crate or workspace. */ export class CargoProject extends Project { + cargoToml = $(this.root, 'Cargo.toml') + .as(TOMLFile) + cargoUpdate () { + return Tools.runShellCommands(this.root.path, ['cargo update']) + } + writeContractCrate ({ path = '.', name, features = [] }: { + name: string, features?: string[], path?: string + }) { + $(this.root, path, 'Cargo.toml').as(TextFile) + .save(Tools.generateCargoToml(name, features)) + $(this.root, path, 'src').as(OpaqueDirectory) + .make() + $(this.root, path, 'src/lib.rs').as(TextFile) + .save(Tools.generateContractEntrypoint()) + return this + } static async create (properties: Parameters[0] = {}) { properties.tools ??= new Tools.SystemTools() + properties.interactive ??= properties.tools.interactive const project = await super.create(properties) as Project - if (properties.tools?.interactive) { + if (properties.interactive) { switch (await this.askCompiler(properties?.tools)) { case 'podman': project.envFile.save(`${project.envFile.load()}\nFADROMA_BUILD_PODMAN=1`) @@ -250,7 +248,6 @@ export class CargoProject extends Project { Tools.gitCommitUpdatedLockfiles(project, properties.tools) return project } - static async askCompiler ({ isLinux, cargo = Tools.NOT_INSTALLED, @@ -274,46 +271,48 @@ export class CargoProject extends Project { isLinux ? [ ...engines, buildRaw ] : [ buildRaw, ...engines ] return await Prompts.askSelect(`Select build isolation mode:`, options) } - - static writeCrate (path: string|Path, name: string, features?: string[]) { - $(path, 'Cargo.toml') - .as(TextFile) - .save(Tools.generateCargoToml(name, features)) - $(path, 'src') - .as(OpaqueDirectory) - .make() - $(path, 'src/lib.rs') - .as(TextFile) - .save(Tools.generateContractEntrypoint()) - } - } +/** Project that consists of scripts plus a single crate. */ export class CrateProject extends CargoProject { + logStatus () { + return super.logStatus().br() + .info('This project contains a single source crate:') + .warn('TODO') + } + /** Create a project, writing a single crate. */ static async create (properties?: Partial<{ tools?: Tools.SystemTools - name?: string - root?: string|Path, - crateFeatures?: string[] - }>) { - const project = await super.create(properties) as CrateProject - this.writeCrate(project.root.path, project.name, properties?.crateFeatures) - return project + name?: string + root?: string|Path, + features?: string[] + }>) { // don't change to arrow function (or all hell will break loose) + return (await super.create(properties) as CrateProject) + .writeContractCrate({ + path: '.', + name: properties?.name || 'untitled', + features: properties?.features || [] + }) } +} - cargoToml = $(this.root, 'Cargo.toml') - .as(TOMLFile) - srcDir = $(this.root, 'lib') - .as(TOMLFile) +/** Project that consists of scripts plus multiple crates in a Cargo workspace. */ +export class WorkspaceProject extends CargoProject { + /** The root file of the workspace */ + cargoToml = $(this.root, 'Cargo.toml').as(TOMLFile) + /** Directory where deployable crates live. */ + contractsDir = $(this.root, 'contracts').as(OpaqueDirectory) + /** Directory where non-deployable crates live. */ + librariesDir = $(this.root, 'libraries').as(OpaqueDirectory) logStatus () { return super.logStatus().br() - .info('This project contains a single source crate:') + .info('This project contains the following source crates:') + .warn('TODO') + .info('This project contains the following library crates:') .warn('TODO') } -} -export class WorkspaceProject extends CargoProject { static async create (properties?: Partial<{ tools?: Tools.SystemTools name?: string @@ -322,20 +321,6 @@ export class WorkspaceProject extends CargoProject { const project = await super.create(properties) as Project return project } - - cargoToml = $(this.root, 'Cargo.toml') - .as(TOMLFile) - contractsDir = $(this.root, 'contracts') - .as(OpaqueDirectory) - librariesDir = $(this.root, 'libraries') - .as(OpaqueDirectory) - - logStatus () { - return console.br() - .info('This project contains the following source crates:') - .warn('TODO') - } - static writeCrates ({ cargoToml, wasmDir, crates }: { cargoToml: Path, wasmDir: Path, diff --git a/ops/tools.ts b/ops/tools.ts index e9620c9a0f4..2f1fe1f8be3 100644 --- a/ops/tools.ts +++ b/ops/tools.ts @@ -63,10 +63,6 @@ export class SystemTools { homebrew = this.isMac ? this.checkTool('homebrew ', 'brew --version') : undefined } -export function cargoUpdate (cwd: string) { - return runShellCommands(cwd, ['cargo update']) -} - /** Run one or more external commands in the project root. */ export function runShellCommands (cwd: string, cmds: string[]) { return cmds.map(cmd=>execSync(cmd, { cwd, stdio: 'inherit' })) @@ -79,7 +75,7 @@ export function createGitRepo (cwd: string, tools: SystemTools): { if (tools.git) { try { runShellCommands(cwd, [ - 'git --no-pager init', + 'git --no-pager init -b main', ]) $(cwd).at('.gitignore').as(TextFile).save(generateGitIgnore()) runShellCommands(cwd, [