feat: introduce yargs architecture

pull/413/head
Webber 2022-08-28 00:57:51 +02:00
parent bb844bcb46
commit 6b32132ec0
11 changed files with 210 additions and 84 deletions

122
src/cli.ts 100644
View File

@ -0,0 +1,122 @@
import yargs from 'https://deno.land/x/yargs@v17.5.1-deno/deno.ts';
import { default as getHomeDir } from 'https://deno.land/x/dir@1.5.1/home_dir/mod.ts';
import { engineDetection } from './middleware/engine-detection/index.ts';
import { CommandInterface } from './command/command-interface.ts';
import { configureLogger } from './middleware/logger-verbosity/index.ts';
import { CommandFactory } from './command/command-factory.ts';
export class Cli {
private readonly yargs: yargs.Argv;
private readonly cliStorageAbsolutePath: string;
private readonly cliStorageCanonicalPath: string;
private readonly configFileName: string;
private command: CommandInterface;
constructor() {
this.cliStorageAbsolutePath = `${getHomeDir()}/.game-ci`;
this.cliStorageCanonicalPath = '~/.game-ci';
this.configFileName = 'config.json';
this.yargs = yargs(Deno.args);
}
public async validateAndParseArguments() {
this.globalSettings();
this.configureLogger();
this.globalOptions();
await this.registerBuildCommand();
await this.parse();
return {
command: this.command,
options: this.options,
};
}
private globalSettings() {
const defaultCanonicalPath = `${this.cliStorageCanonicalPath}/${this.configFileName}`;
const defaultAbsolutePath = `${this.cliStorageAbsolutePath}/${this.configFileName}`;
this.yargs
.config('config', `default: ${defaultCanonicalPath}`, async (override) => {
const configPath = override || defaultAbsolutePath;
return JSON.parse(await Deno.readTextFile(configPath));
})
.parserConfiguration({
'dot-notation': false,
'duplicate-arguments-array': false,
'negation-prefix': false,
'strip-aliased': true,
'strip-dashed': true,
});
// Todo - enable `.env()` after this is merged: https://github.com/yargs/yargs/pull/2231
// this.yargs.env();
}
private configureLogger() {
this.yargs
.options('quiet', {
alias: 'q',
default: false,
description: 'Suppress all output',
type: 'boolean',
})
.options('verbose', {
alias: 'v',
default: false,
description: 'Enable verbose logging',
type: 'boolean',
})
.options('veryVerbose', {
alias: 'vv',
default: false,
description: 'Enable very verbose logging',
type: 'boolean',
})
.options('maxVerbose', {
alias: 'vvv',
default: false,
description: 'Enable debug logging',
})
.middleware([configureLogger], true);
}
private globalOptions() {
this.yargs
.epilogue('for more information, find our manual at https://game.ci/docs/cli')
.middleware([])
.showHelpOnFail(true)
.strict(true);
}
private async registerBuildCommand() {
this.yargs.command('build [projectPath]', 'Builds a project that you want to build', async (yargs) => {
yargs
.positional('projectPath', {
describe: 'Path to the project',
type: 'string',
})
.middleware([
engineDetection, // Command is engine specific
async (args) => {
await this.registerCommand(args, yargs);
},
]);
});
}
private async registerCommand(args: yargs.Arguments, yargs) {
const { engine, engineVersion, _: command } = args;
this.command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(command);
await this.command.configureOptions(yargs);
}
private async parse() {
this.options = await this.yargs.parseAsync();
}
}

View File

@ -5,33 +5,37 @@ import MacBuilder from '../../model/mac-builder.ts';
import { CommandBase } from '../command-base.ts';
export class UnityBuildCommand extends CommandBase implements CommandInterface {
public async validate() {
await super.validate();
}
public async execute(): Promise<boolean> {
public async execute(options): Promise<boolean> {
try {
const { workspace, actionFolder } = Action;
const { parameters, env } = this.options;
Action.checkCompatibility();
Cache.verify();
const baseImage = new ImageTag(parameters);
log.debug('baseImage', baseImage);
await PlatformSetup.setup(parameters, actionFolder);
if (env.getOS() === 'darwin') {
MacBuilder.run(actionFolder, workspace, parameters);
} else {
await Docker.run(baseImage, { workspace, actionFolder, ...parameters });
}
// Set output
await Output.setBuildVersion(parameters.buildVersion);
// Todo - rework this without needing this.options, use parameters from cli instead.
// const { workspace, actionFolder } = Action;
// const { parameters, env } = this.options;
//
// Action.checkCompatibility();
// Cache.verify();
//
// const baseImage = new ImageTag(options);
// log.debug('baseImage', baseImage);
//
// await PlatformSetup.setup(parameters, actionFolder);
// if (env.getOS() === 'darwin') {
// MacBuilder.run(actionFolder, workspace, parameters);
// } else {
// await Docker.run(baseImage, { workspace, actionFolder, ...parameters });
// }
//
// // Set output
// await Output.setBuildVersion(parameters.buildVersion);
} catch (error) {
log.error(error);
Deno.exit(1);
}
}
public async configureOptions(instance): Promise<void> {
instance.option('buildName', {
description: 'Name of the build',
type: 'string',
});
}
}

View File

@ -1,8 +1,9 @@
import { Options } from '../config/options.ts';
import { Input } from '../model/index.ts';
import Parameters from '../model/parameters.ts';
import { CommandInterface } from './command-interface.ts';
export class CommandBase {
export class CommandBase implements CommandInterface {
public readonly name: string;
private options: Options;
@ -10,20 +11,6 @@ export class CommandBase {
this.name = name;
}
public configure(options: Options): this {
this.options = options;
return this;
}
public async validate(): Promise<this> {
return this;
}
public async parseParameters(input: Input, parameters: Parameters): Promise<Partial<Parameters>> {
return {};
}
public async execute(): Promise<boolean> {
throw new Error('Method not implemented.');
}

View File

@ -2,6 +2,7 @@ import { NonExistentCommand } from './null/non-existent-command.ts';
import { UnityBuildCommand } from './build/unity-build-command.ts';
import { CommandInterface } from './command-interface.ts';
import { UnityRemoteBuildCommand } from './remote/unity-remote-build-command.ts';
import { Engine } from '../model/engine.ts';
export class CommandFactory {
constructor() {}
@ -13,9 +14,12 @@ export class CommandFactory {
return this;
}
public createCommand(commandName: string): CommandInterface {
public createCommand(command: string[]): CommandInterface {
// Structure looks like: _: [ "build" ],
const commandName = command[0];
switch (this.engine) {
case 'unity':
case Engine.unity:
return this.createUnityCommand(commandName);
default:
throw new Error(`Engine ${this.engine} is not yet supported.`);
@ -26,10 +30,11 @@ export class CommandFactory {
switch (commandName) {
case 'build':
return new UnityBuildCommand(commandName);
case 'build-remote':
return new UnityRemoteBuildCommand(commandName);
default:
return new NonExistentCommand(commandName);
// case 'remote-build':
// return new UnityRemoteBuildCommand(commandName);
// default:
// return new NonExistentCommand(commandName);
}
}
}

View File

@ -1,11 +1,8 @@
import { Options } from '../config/options.ts';
import Parameters from '../model/parameters.ts';
import { Input } from '../model/index.ts';
import { yargs } from '../dependencies.ts';
export interface CommandInterface {
name: string;
execute: (options: Options) => Promise<boolean>;
parseParameters: (input: Input, parameters: Parameters) => Promise<Partial<Parameters>>;
configure(options: Options): this;
validate(): Promise<this>;
configureOptions: (instance: yargs.Argv) => Promise<void>;
}

View File

@ -20,6 +20,8 @@ import { Command } from 'https://deno.land/x/cmd@v1.2.0/commander/index.ts';
import { getUnityChangeset as getUnityChangeSet } from 'https://deno.land/x/unity_changeset@2.0.0/src/index.ts';
import { Buffer } from 'https://deno.land/std@0.151.0/io/buffer.ts';
import { config, configSync } from 'https://deno.land/std@0.151.0/dotenv/mod.ts';
import yargs from 'https://deno.land/x/yargs@v17.4.0-deno/deno.ts';
import * as yargsTypes from 'https://deno.land/x/yargs@v17.4.0-deno/deno-types.ts';
// Internally managed packages
import waitUntil from './module/wait-until.ts';
@ -71,4 +73,6 @@ export {
waitUntil,
Writable,
yaml,
yargs,
yargsTypes,
};

View File

@ -1,41 +1,13 @@
import { configSync } from './dependencies.ts';
import { configureLogger } from './core/logger/index.ts';
import { Options } from './config/options.ts';
import { CommandFactory } from './command/command-factory.ts';
import { ArgumentsParser } from './core/cli/arguments-parser.ts';
import { Environment } from './core/env/environment.ts';
import { EngineDetector } from './core/engine/engine-detector.ts';
import { Cli } from './cli.ts';
export class GameCI {
private readonly env: Environment;
constructor() {
this.env = new Environment(Deno.env, configSync());
this.args = Deno.args;
}
public async run() {
class GameCI {
public static async run() {
try {
// Infallible configuration
const { commandName, subCommands, args, verbosity } = new ArgumentsParser().parse(this.args);
await configureLogger(verbosity);
const { command, options } = await new Cli().validateAndParseArguments();
// Todo - set default values for parameters to restore functionality.
// Todo - investigate how fitting a CLI lib will be for us
// (now that things are starting to be more separated)
const success = await command.execute(options);
// Determine which command to run
const { engine, engineVersion } = await new EngineDetector(subCommands, args).detect();
const command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(commandName, subCommands);
// Provide the command with options
const options = await new Options(command, this.env).generateParameters(args);
await command.configure(options).validate();
// Execute
if (log.isVerbose) log.info('Executing', command.name);
const success = await command.execute();
if (!success) log.info(`Command ${command.name} failed.`);
if (!success) throw new Error(`${command.name} failed.`);
} catch (error) {
log.error(error);
Deno.exit(1);
@ -43,4 +15,4 @@ export class GameCI {
}
}
await new GameCI().run();
await GameCI.run();

View File

@ -0,0 +1,12 @@
import { EngineDetector } from './engine-detector.ts';
export const engineDetection = async (argv) => {
const { projectPath } = argv;
if (!projectPath) throw new Error('Unable to detect engine. No project path provided.');
const { engine, engineVersion } = await new EngineDetector(projectPath).detect();
argv.engine = engine;
argv.engineVersion = engineVersion;
};

View File

@ -0,0 +1,20 @@
import { configureLogger as createLoggerAndSetVerbosity } from '../../core/logger/index.ts';
export const configureLogger = async (argv) => {
const { quiet, verbose, veryVerbose, maxVerbose } = argv;
let verbosity;
if (maxVerbose) {
verbosity = 3;
} else if (veryVerbose) {
verbosity = 2;
} else if (verbose) {
verbosity = 1;
} else if (quiet) {
verbosity = -1;
} else {
verbosity = 0;
}
await createLoggerAndSetVerbosity(verbosity);
};

View File

@ -0,0 +1,3 @@
export class Engine {
public static unity = 'unity';
}