diff --git a/build.hxml b/build.hxml index 3c302a7..5e7c2ab 100644 --- a/build.hxml +++ b/build.hxml @@ -1,4 +1,6 @@ --interp -main hxextern.Main -cp src/ +-lib hscript -lib hxargs +-lib minject diff --git a/haxelib.json b/haxelib.json index 26227d3..6e5d9d1 100644 --- a/haxelib.json +++ b/haxelib.json @@ -9,7 +9,8 @@ "classPath": "src/", "contributors": ["fantoine"], "dependencies": { + "hscript": "", "hxargs": "", - "hxssl": "" + "minject": "" } } diff --git a/src/hxextern/Cli.hx b/src/hxextern/Cli.hx index cdb428e..e64e3f9 100644 --- a/src/hxextern/Cli.hx +++ b/src/hxextern/Cli.hx @@ -2,7 +2,8 @@ package hxextern; import hxargs.Args; import hxextern.command.*; -import hxextern.service.*; +import hxextern.service.Console; +import minject.Injector; typedef ArgsHandler = { function getDoc() : String; @@ -12,33 +13,39 @@ typedef ArgsHandler = { class Cli { + @inject + public var injector(default, null) : Injector; + + @inject + public var console(default, null) : Console; + private var handler : ArgsHandler; public function new() { this.handler = Args.generate([ + @doc('Generate extern code') + 'generate' => function(?path : String) : Void { + this.runCommand(GenerateCommand, [path]); + }, + @doc('List available externs') 'list' => function(?target : String) : Void { - this.runCommand(new ListCommand(target)); + this.runCommand(ListCommand, [target]); }, @doc('Search for an extern') 'search' => function(name : String, ?target : String) : Void { - this.runCommand(new SearchCommand(name, target)); - }, - - @doc('Generate extern code') - 'generate' => function(?path : String) : Void { - trace('Not implemented'); + this.runCommand(SearchCommand, [name, target]); }, @doc('Show this help') 'help' => function() : Void { - this.runCommand(new HelpCommand(this.handler.getDoc())); + this.runCommand(HelpCommand, [this.handler.getDoc()]); }, _ => function(arg : String) : Void { - this.runCommand(new HelpCommand(this.handler.getDoc())); + this.runCommand(HelpCommand, [this.handler.getDoc()]); }, ]); } @@ -51,16 +58,17 @@ class Cli try { this.handler.parse(args); } catch (e : Dynamic) { - Console.instance.error(Std.string(e)); + this.console.error(Std.string(e)); } } - private function runCommand(command : ICommand) : Void + private function runCommand(commandType : Class, ?args : Array) : Void { try { - command.run(); + var command = this.injector.instantiate(commandType); + command.run(null == args ? [] : args); } catch (e : Dynamic) { - Console.instance.error(Std.string(e)); + this.console.error(Std.string(e)); } } } diff --git a/src/hxextern/Macro.hx b/src/hxextern/Macro.hx new file mode 100644 index 0000000..92c56cb --- /dev/null +++ b/src/hxextern/Macro.hx @@ -0,0 +1,107 @@ +package hxextern; + +import haxe.io.Path; +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; +import haxe.macro.TypeTools; +import sys.FileSystem; + +using StringTools; + +class Macro +{ + private static inline var STEP_META : String = ':step'; + + public static macro function listSteps(injector : Expr) : Expr + { + var injections = []; + var modules = Macro.listTypes('hxextern.step'); + for (module in modules) { + var type = switch (module) { + case TInst(inst, _): inst.get(); + case _: null; + } + if (null == type) { + continue; + } + + if (type.isExtern || type.isInterface || !type.meta.has(Macro.STEP_META) || !Macro.doesImplement(type, 'hxextern.step.IStep')) { + continue; + } + var step = switch (type.meta.extract(Macro.STEP_META)) { + case [{ params: [{ expr: EConst(CString(name)) }] }]: name; + case _: null; + } + if (null == step) { + continue; + } + + var moduleName = type.name; + injections.push(macro $injector.mapClass(hxextern.step.IStep, hxextern.step.$moduleName, $v{step})); + } + return macro { $a{injections} }; + } + + public static macro function listCommands(injector : Expr) : Expr + { + var injections = []; + var modules = Macro.listTypes('hxextern.step'); + for (module in modules) { + var type = switch (module) { + case TInst(inst, _): inst.get(); + case _: null; + } + if (null == type) { + continue; + } + + if (type.isExtern || type.isInterface || !Macro.doesImplement(type, 'hxextern.command.ICommand')) { + continue; + } + + var moduleName = type.name; + injections.push(macro $injector.mapClass(hxextern.command.$moduleName, hxextern.command.$moduleName)); + injections.push(macro $injector.mapClass(hxextern.command.ICommand, hxextern.command.$moduleName, $v{moduleName})); + } + return macro { $a{injections} }; + } + +#if macro + private static function listTypes(pack : String) : Array + { + var types = []; + for (cp in Context.getClassPath()) { + var directory = Path.addTrailingSlash(cp) + pack.replace('.', '/'); + if (!FileSystem.exists(directory) || !FileSystem.isDirectory(directory)) { + continue; + } + + for (file in FileSystem.readDirectory(directory)) { + if (!file.endsWith('.hx')) { + continue; + } + + var moduleName = file.substr(0, -3); + var module = try Context.getType('${pack}.${moduleName}') catch(e : String) null; + if (null == module) { + continue; + } + types.push(module); + } + } + return types; + } + + private static function doesImplement(type : ClassType, interfaceType : String) : Bool + { + for (entry in type.interfaces) { + if (entry.t.toString() == interfaceType || Macro.doesImplement(entry.t.get(), interfaceType)) { + return true; + } + } + + return (null != type.superClass ? Macro.doesImplement(type.superClass.t.get(), interfaceType) : false); + } +#end +} diff --git a/src/hxextern/Main.hx b/src/hxextern/Main.hx index a7d3bb3..5cb9481 100644 --- a/src/hxextern/Main.hx +++ b/src/hxextern/Main.hx @@ -1,15 +1,30 @@ package hxextern; +import hxextern.service.*; +import minject.Injector; + class Main { public static function main() : Void { // Get arguments var args = Sys.args(); - args.pop(); + var cwd = args.pop(); + + // Prepare injector + var injector = new Injector(); + injector.mapValue(Injector, injector); + injector.mapSingleton(Console); + injector.mapSingleton(Haxelib); + injector.mapSingleton(Process); + injector.mapSingleton(Repository); + Macro.listSteps(injector); + + // Update cwd + Sys.setCwd(cwd); // Run - var cli = new Cli(); + var cli = injector.instantiate(Cli); cli.run(args); } } diff --git a/src/hxextern/command/AbstractListCommand.hx b/src/hxextern/command/AbstractListCommand.hx index 91b177d..9828020 100644 --- a/src/hxextern/command/AbstractListCommand.hx +++ b/src/hxextern/command/AbstractListCommand.hx @@ -5,12 +5,18 @@ import hxextern.service.Repository; class AbstractListCommand implements ICommand { + @inject + public var console(default, null) : Console; + + @inject + public var repository(default, null) : Repository; + public function new() { // Nothing to do } - public function run() : Void + public function run(args : Array) : Void { throw 'Must be overriden'; } @@ -18,15 +24,17 @@ class AbstractListCommand implements ICommand private function printList(infos : Array) : Void { if (0 == infos.length) { - Console.instance.info('No results found'); + this.console.info('No results found'); return; } - Console.instance.success('Found ${infos.length} result(s)'); + this.console.success('Found ${infos.length} result(s)'); for (info in infos) { - Console.instance.message(' * ', false); - Console.instance.info('${info.name} (${info.target})', false); - Console.instance.message(' : ${info.description}'); + this.console + .message(' * ', false) + .info('${info.name} (${info.target})', false) + .message(' : ${info.description}') + ; } } } diff --git a/src/hxextern/command/GenerateCommand.hx b/src/hxextern/command/GenerateCommand.hx new file mode 100644 index 0000000..1d7f4a6 --- /dev/null +++ b/src/hxextern/command/GenerateCommand.hx @@ -0,0 +1,62 @@ +package hxextern.command; + +import hxextern.service.Haxelib; +import hxextern.step.*; +import hxextern.step.IStep; +import sys.FileSystem; + +using StringTools; + +class GenerateCommand implements ICommand +{ + @inject + public var haxelib(default, null) : Haxelib; + + public function new() + { + /* + this.path = path; + this.steps = new Map(); + + var types : Array> = [ScriptStep, NpmStep]; + for (type in types) { + var instance = Type.createInstance(type, []); + this.steps[instance.type] = instance; + } + */ + } + + public function run(args : Array) : Void + { + var path : Null = args[0]; + + // Find haxelib file + var file = (null != path ? + this.haxelib.findFile(path) : + this.haxelib.findFromPath(Sys.getCwd()) + ); + + // Extract datas + var data = this.haxelib.extract(file); + this.executeSteps(data.steps); + } + + private function executeSteps(steps : Array) : Void + { + /* + var definitions = new TypeDefinitionMap(); + for (step in steps) { + // Get step name + var name = step.type.trim().toLowerCase(); + if (!this.steps.exists(name)) { + throw 'Step "${step.type}" has not been found'; + } + + // Run step + var instance = this.steps[name]; + definitions = instance.run(definitions, step.options); + } + trace(definitions); + */ + } +} diff --git a/src/hxextern/command/HelpCommand.hx b/src/hxextern/command/HelpCommand.hx index 5e1b323..031faa1 100644 --- a/src/hxextern/command/HelpCommand.hx +++ b/src/hxextern/command/HelpCommand.hx @@ -1,16 +1,25 @@ package hxextern.command; +import hxextern.service.Console; + class HelpCommand implements ICommand { - private var doc : String; + @inject + public var console(default, null) : Console; - public function new(doc : String) + public function new() { - this.doc = doc; + } - public function run() : Void + public function run(args : Array) : Void { - Sys.println(this.doc); + var doc : String = args[0]; + + this.console + .info('HxExtern Manager 1.0.0') + .message(' Usage: hxextern [command] ') + .message(' ' + doc.split('\n').join('\n ')) + ; } } diff --git a/src/hxextern/command/ICommand.hx b/src/hxextern/command/ICommand.hx index 52e941b..42c246b 100644 --- a/src/hxextern/command/ICommand.hx +++ b/src/hxextern/command/ICommand.hx @@ -2,5 +2,5 @@ package hxextern.command; interface ICommand { - public function run() : Void; + public function run(args : Array) : Void; } diff --git a/src/hxextern/command/ListCommand.hx b/src/hxextern/command/ListCommand.hx index 7c1a596..f4698ad 100644 --- a/src/hxextern/command/ListCommand.hx +++ b/src/hxextern/command/ListCommand.hx @@ -4,20 +4,16 @@ import hxextern.service.Repository; class ListCommand extends AbstractListCommand { - private var target : String; - - public function new(?target : String) + public function new() { super(); - - this.target = target; } - public override function run() : Void + public override function run(args : Array) : Void { - var target = (null == this.target ? null : Target.fromString(this.target)); + var target : Null = (null == args[0] ? null : Target.fromString(args[0])); - var infos = Repository.instance.list(target); + var infos = this.repository.list(target); this.printList(infos); } } diff --git a/src/hxextern/command/SearchCommand.hx b/src/hxextern/command/SearchCommand.hx index 4d3690d..82540c1 100644 --- a/src/hxextern/command/SearchCommand.hx +++ b/src/hxextern/command/SearchCommand.hx @@ -4,22 +4,17 @@ import hxextern.service.Repository; class SearchCommand extends AbstractListCommand { - private var name : String; - private var target : String; - - public function new(name : String, ?target : String) + public function new() { super(); - - this.name = name; - this.target = target; } - public override function run() : Void + public override function run(args : Array) : Void { - var target = (null == this.target ? null : Target.fromString(this.target)); + var name : String = args[0]; + var target : Null = (null == args[1] ? null : Target.fromString(args[1])); - var infos = Repository.instance.find(this.name, target); + var infos = this.repository.find(name, target); this.printList(infos); } } diff --git a/src/hxextern/service/Console.hx b/src/hxextern/service/Console.hx index 8b3d5fc..16ac492 100644 --- a/src/hxextern/service/Console.hx +++ b/src/hxextern/service/Console.hx @@ -5,61 +5,53 @@ import hxextern.utils.Colors; class Console { - @:isVar - public static var instance(get, null) : Console; - private static function get_instance() : Console - { - if (null == Console.instance) { - Console.instance = new Console(); - } - return Console.instance; - } - private var stdout : Output; private var stderr : Output; - private function new() + public function new() { this.stdout = Sys.stdout(); this.stderr = Sys.stderr(); } - public function message(message : String, newline : Bool = true) : Void + public function message(message : String, newline : Bool = true) : Console { - this.print(this.stdout, message, newline); + return this.print(this.stdout, message, newline); } - public function debug(message : String, newline : Bool = true) : Void + public function debug(message : String, newline : Bool = true) : Console { - this.print(this.stdout, Colors.magenta('${message}'), newline); + return this.print(this.stdout, Colors.magenta(message), newline); } - public function info(message : String, newline : Bool = true) : Void + public function info(message : String, newline : Bool = true) : Console { - this.print(this.stdout, Colors.cyan('${message}'), newline); + return this.print(this.stdout, Colors.cyan(message), newline); } - public function success(message : String, newline : Bool = true) : Void + public function success(message : String, newline : Bool = true) : Console { - this.print(this.stdout, Colors.green('${message}'), newline); + return this.print(this.stdout, Colors.green(message), newline); } - public function warning(message : String, newline : Bool = true) : Void + public function warning(message : String, newline : Bool = true) : Console { - this.print(this.stderr, Colors.yellow('${message}'), newline); + return this.print(this.stderr, Colors.yellow(message), newline); } - public function error(message : String, newline : Bool = true) : Void + public function error(message : String, newline : Bool = true) : Console { - this.print(this.stderr, Colors.red('${message}'), newline); + return this.print(this.stderr, Colors.red(message), newline); } - private function print(output : Output, message : String, newline : Bool = true) : Void + private function print(output : Output, message : String, newline : Bool = true) : Console { output.writeString(message); if (newline) { output.writeString('\n'); } output.flush(); + + return this; } } diff --git a/src/hxextern/service/Haxelib.hx b/src/hxextern/service/Haxelib.hx new file mode 100644 index 0000000..4ea3f0e --- /dev/null +++ b/src/hxextern/service/Haxelib.hx @@ -0,0 +1,83 @@ +package hxextern.service; + +import haxe.DynamicAccess; +import haxe.io.Path; +import haxe.Json; +import sys.FileSystem; +import sys.io.File; + +typedef HaxelibHxExternStep = { + var type : String; + var options : Null; +}; + +typedef HaxelibHxExtern = { + var steps : Array; +}; + +class Haxelib +{ + private static inline var FILENAME : String = 'haxelib.json'; + + public function new() + { + // Nothing to do + } + + public function findFile(file : String) : String + { + if (!FileSystem.exists(file) || FileSystem.isDirectory(file)) { + throw 'File not found "${file}"'; + } + + return FileSystem.absolutePath(file); + } + + public function findFromPath(path : String) : String + { + var directory = Path.removeTrailingSlashes(FileSystem.absolutePath(path)); + while (FileSystem.exists(directory) && FileSystem.isDirectory(directory)) { + var filename = '${directory}/${Haxelib.FILENAME}'; + if (FileSystem.exists(filename)) { + return filename; + } + + // Go in upper directory + directory = Path.directory(directory); + } + + throw 'Cannot find "${Haxelib.FILENAME}" file from path "${path}"'; + return null; + } + + public function extract(file : String) : HaxelibHxExtern + { + // Get file + var filename = this.findFile(file); + var content = File.getContent(filename); + + // Get json + var json = Json.parse(content); + var hxextern = this.extractField(json, 'hxextern'); + var steps = this.extractField(hxextern, 'steps'); + if (!Std.is(steps, Array)) { + throw 'Field "steps" should be an array'; + } + + // Return validated object + return { + steps: [ for (step in (steps : Array>)) { + type: this.extractField(step, 'type'), + options: (step.exists('options') ? step['options'] : null), + } ], + }; + } + + private function extractField(object : DynamicAccess, field : String) : Dynamic + { + if (!object.exists(field)) { + throw 'Required "${field}" field not found'; + } + return object[field]; + } +} diff --git a/src/hxextern/service/Process.hx b/src/hxextern/service/Process.hx index 3dfe597..08c99f6 100644 --- a/src/hxextern/service/Process.hx +++ b/src/hxextern/service/Process.hx @@ -12,24 +12,17 @@ typedef ProcessOutput = { class Process { - @:isVar - public static var instance(get, null) : Process; - private static function get_instance() : Process - { - if (null == Process.instance) { - Process.instance = new Process(); - } - return Process.instance; - } + @inject + public var console(default, null) : Console; - private function new() + public function new() { - + // Nothing to do } public function execute(command : String, ?args : Array) : ProcessOutput { - Console.instance.debug('Calling command "${command}' + (null != args && args.length > 0 ? ' ' + args.join(' ') : '') + '"'); + this.console.debug('Calling command "${command}' + (null != args && args.length > 0 ? ' ' + args.join(' ') : '') + '"'); var process = new HxProcess(command, args); var output = process.stdout.readAll().toString(); diff --git a/src/hxextern/service/Repository.hx b/src/hxextern/service/Repository.hx index 936da29..2d399ad 100644 --- a/src/hxextern/service/Repository.hx +++ b/src/hxextern/service/Repository.hx @@ -17,15 +17,8 @@ class Repository private static var REPO_PATTERN : EReg = ~/extern-([a-z]+)-([a-z0-9_-]+)/ig; - @:isVar - public static var instance(get, null) : Repository; - private static function get_instance() : Repository - { - if (null == Repository.instance) { - Repository.instance = new Repository(); - } - return Repository.instance; - } + @inject + public var process(default, null) : Process; private var repositories : Array; @@ -43,7 +36,7 @@ class Repository this.repositories = []; // Get json from Github - var result = Process.instance.execute('curl', ['-sSf', Repository.REPOS_URL]); + var result = this.process.execute('curl', ['-sSf', Repository.REPOS_URL]); if (!result.valid) { throw result.error; } diff --git a/src/hxextern/step/AbstractCommandStep.hx b/src/hxextern/step/AbstractCommandStep.hx new file mode 100644 index 0000000..1b2f799 --- /dev/null +++ b/src/hxextern/step/AbstractCommandStep.hx @@ -0,0 +1,17 @@ +package hxextern.step; + +import hxextern.step.IStep; + +@:skip +class AbstractCommandStep extends AbstractStep +{ + public function new(type : String) + { + super(type); + } + + public override function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + { + return definitions; + } +} diff --git a/src/hxextern/step/AbstractStep.hx b/src/hxextern/step/AbstractStep.hx new file mode 100644 index 0000000..b7f0509 --- /dev/null +++ b/src/hxextern/step/AbstractStep.hx @@ -0,0 +1,21 @@ +package hxextern.step; + +import hxextern.step.IStep; + +@:skip +class AbstractStep implements IStep +{ + public var type(default, null) : String; + + public function new(type : String) + { + this.type = type; + } + + public function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + { + throw 'Must be overriden'; + + return definitions; + } +} diff --git a/src/hxextern/step/IStep.hx b/src/hxextern/step/IStep.hx new file mode 100644 index 0000000..9383b4e --- /dev/null +++ b/src/hxextern/step/IStep.hx @@ -0,0 +1,12 @@ +package hxextern.step; + +import haxe.macro.Expr; + +typedef TypeDefinitionMap = Map; + +interface IStep +{ + public var type(default, null) : String; + + public function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap; +} diff --git a/src/hxextern/step/NpmStep.hx b/src/hxextern/step/NpmStep.hx new file mode 100644 index 0000000..0aa15e6 --- /dev/null +++ b/src/hxextern/step/NpmStep.hx @@ -0,0 +1,37 @@ +package hxextern.step; + +import hxextern.service.Console; +import hxextern.service.Process; +import hxextern.step.IStep; + +@:step('npm') +class NpmStep extends AbstractCommandStep +{ + @inject + var console(default, null) : Console; + + @inject + var process(default, null) : Process; + + public function new() + { + super('npm'); + } + + public override function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + { + var module = options.module; + + var result = this.process.execute('npm', ['install', module, '--silent']); + if (0 == result.code) { + this.console.success('NPM module "${module}" installed'); + } else { + this.console + .error('NPM module "${module}" has not been installed') + .error(result.error) + ; + } + + return definitions; + } +} diff --git a/src/hxextern/step/ScriptStep.hx b/src/hxextern/step/ScriptStep.hx new file mode 100644 index 0000000..c04dcb0 --- /dev/null +++ b/src/hxextern/step/ScriptStep.hx @@ -0,0 +1,23 @@ +package hxextern.step; + +import haxe.macro.Compiler; +import haxe.macro.Context; +import hxextern.step.IStep; +import sys.FileSystem; +import haxe.macro.Expr; + +@:step('script') +class ScriptStep extends AbstractStep +{ + public function new() + { + super('script'); + } + + public override function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + { + var classPath = FileSystem.absolutePath(options.classPath); + + return definitions; + } +}