feat: introduce yargs architecture
parent
bb844bcb46
commit
6b32132ec0
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
42
src/index.ts
42
src/index.ts
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export class Engine {
|
||||
public static unity = 'unity';
|
||||
}
|
||||
Loading…
Reference in New Issue