diff --git a/src/hxextern/Main.hx b/src/hxextern/Main.hx index 5cb9481..c8fbec4 100644 --- a/src/hxextern/Main.hx +++ b/src/hxextern/Main.hx @@ -14,15 +14,14 @@ class Main // Prepare injector var injector = new Injector(); injector.mapValue(Injector, injector); + injector.mapValue(String, cwd, 'cwd'); // current working directory + injector.mapValue(String, Sys.getCwd(), 'hwd'); // hxextern working directory injector.mapSingleton(Console); injector.mapSingleton(Haxelib); injector.mapSingleton(Process); injector.mapSingleton(Repository); Macro.listSteps(injector); - // Update cwd - Sys.setCwd(cwd); - // Run var cli = injector.instantiate(Cli); cli.run(args); diff --git a/src/hxextern/Tools.hx b/src/hxextern/Tools.hx new file mode 100644 index 0000000..ae7a47b --- /dev/null +++ b/src/hxextern/Tools.hx @@ -0,0 +1,9 @@ +package hxextern; + +class Tools +{ + public static function escapeEReg(value : String) : String + { + return ~/([-[\]{}()*+?.,\\^$|#\s])/g.replace(value, '\\$1'); + } +} diff --git a/src/hxextern/command/GenerateCommand.hx b/src/hxextern/command/GenerateCommand.hx index 1d7f4a6..2db2807 100644 --- a/src/hxextern/command/GenerateCommand.hx +++ b/src/hxextern/command/GenerateCommand.hx @@ -1,29 +1,32 @@ package hxextern.command; +import haxe.io.Path; +import hxextern.service.Console; import hxextern.service.Haxelib; -import hxextern.step.*; import hxextern.step.IStep; +import hxextern.step.StepContext; +import minject.Injector; import sys.FileSystem; using StringTools; class GenerateCommand implements ICommand { + @inject + public var injector(default, null) : Injector; + @inject public var haxelib(default, null) : Haxelib; + + @inject + public var console(default, null) : Console; + + @inject('cwd') + public var cwd(default, null) : String; 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 @@ -33,30 +36,48 @@ class GenerateCommand implements ICommand // Find haxelib file var file = (null != path ? this.haxelib.findFile(path) : - this.haxelib.findFromPath(Sys.getCwd()) + this.haxelib.findFromPath(this.cwd) ); // Extract datas var data = this.haxelib.extract(file); - this.executeSteps(data.steps); + var context = new StepContext(); + + // Update to haxelib.json file + Sys.setCwd(Path.directory(file)); + + // Run steps + this.executeSteps(context, data.steps); + this.generateCode(context); } - private function executeSteps(steps : Array) : Void + private function executeSteps(context : StepContext, steps : Array) : Void { - /* - var definitions = new TypeDefinitionMap(); - for (step in steps) { + // Run steps + for (i in 0...steps.length) { + var step = steps[i]; + // Get step name var name = step.type.trim().toLowerCase(); - if (!this.steps.exists(name)) { - throw 'Step "${step.type}" has not been found'; + + // Get step + var instance = try this.injector.getInstance(IStep, name) catch(e : Dynamic) null; + if (null == instance) { + throw 'Step ${step.type} has not been found'; } + // Prepare context + context.setup(name); + // Run step - var instance = this.steps[name]; - definitions = instance.run(definitions, step.options); + this.console.info('(${i + 1}) Running step "${name}"'); + instance.initialize(step.options); + instance.run(context); } - trace(definitions); - */ + } + + private function generateCode(context : StepContext) : Void + { + } } diff --git a/src/hxextern/service/Haxelib.hx b/src/hxextern/service/Haxelib.hx index 4ea3f0e..dc78237 100644 --- a/src/hxextern/service/Haxelib.hx +++ b/src/hxextern/service/Haxelib.hx @@ -67,8 +67,21 @@ class Haxelib // Return validated object return { steps: [ for (step in (steps : Array>)) { - type: this.extractField(step, 'type'), - options: (step.exists('options') ? step['options'] : null), + var type = this.extractField(step, 'type'); + var options = null; + for (field in step.keys()) { + if ('type' == field) { + continue; + } + if (null == options) { + options = {}; + } + Reflect.setField(options, field, step[field]); + } + { + type: type, + options: options, + }; } ], }; } diff --git a/src/hxextern/service/Process.hx b/src/hxextern/service/Process.hx index 08c99f6..f08f833 100644 --- a/src/hxextern/service/Process.hx +++ b/src/hxextern/service/Process.hx @@ -42,4 +42,13 @@ class Process return result; } + + public function checkCommand(command : String) : Void + { + // Check if command exists + var result = this.execute('which', [command]); + if (0 != result.code) { + throw 'Command "${command}" not available'; + } + } } diff --git a/src/hxextern/service/Repository.hx b/src/hxextern/service/Repository.hx index 2d399ad..b2eb68d 100644 --- a/src/hxextern/service/Repository.hx +++ b/src/hxextern/service/Repository.hx @@ -4,6 +4,8 @@ import haxe.Json; import hxextern.service.Process; import hxextern.Target; +using hxextern.Tools; + typedef RepositoryInfo = { var name : String; var target : Target; @@ -89,9 +91,7 @@ class Repository { this.preload(); - var escapedName = ~/([-[\]{}()*+?.,\\^$|#\s])/g.replace(name, '\\$1'); - var ereg = new EReg(escapedName, 'ig'); - + var ereg = new EReg(name.escapeEReg(), 'ig'); return [ for (info in this.repositories) if (ereg.match(info.name) && (null == target || info.target == target)) diff --git a/src/hxextern/step/AbstractCommandStep.hx b/src/hxextern/step/AbstractCommandStep.hx index 1b2f799..d6ae623 100644 --- a/src/hxextern/step/AbstractCommandStep.hx +++ b/src/hxextern/step/AbstractCommandStep.hx @@ -1,17 +1,26 @@ package hxextern.step; +import hxextern.service.Process; import hxextern.step.IStep; -@:skip class AbstractCommandStep extends AbstractStep { - public function new(type : String) + @inject + public var process(default, null) : Process; + + public function new() + { + super(); + } + + public override function run(context : StepContext) : Void { - super(type); + super.run(context); } - public override function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + private function exec(command : String, ?args : Array) : ProcessOutput { - return definitions; + this.process.checkCommand(command); + return this.process.execute(command, args); } } diff --git a/src/hxextern/step/AbstractStep.hx b/src/hxextern/step/AbstractStep.hx index b7f0509..00c28cf 100644 --- a/src/hxextern/step/AbstractStep.hx +++ b/src/hxextern/step/AbstractStep.hx @@ -1,21 +1,42 @@ package hxextern.step; +import haxe.DynamicAccess; +import hxextern.service.Console; import hxextern.step.IStep; -@:skip class AbstractStep implements IStep { - public var type(default, null) : String; + @inject + public var console(default, null) : Console; - public function new(type : String) + public function new() { - this.type = type; + // ... } - public function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + public function initialize(options : DynamicAccess) : Void { - throw 'Must be overriden'; + // ... + } + + public function run(context : StepContext) : Void + { + // ... + } + + private function getOption(options : DynamicAccess, field : String, type : Dynamic, ?defaultValue : Dynamic) : Dynamic + { + if (!options.exists(field)) { + if (null != defaultValue) { + return defaultValue; + } + throw 'Missing field "${field}" in step options'; + } - return definitions; + var value = options[field]; + if (!Std.is(value, type)) { + throw 'Invalid type for field "${field}". Expected ${type}, but got ${Type.typeof(value)}'; + } + return value; } } diff --git a/src/hxextern/step/AbstractTextStep.hx b/src/hxextern/step/AbstractTextStep.hx new file mode 100644 index 0000000..f3bd531 --- /dev/null +++ b/src/hxextern/step/AbstractTextStep.hx @@ -0,0 +1,58 @@ +package hxextern.step; + +import haxe.DynamicAccess; +import hxextern.step.IStep; +import sys.FileSystem; +import sys.io.File; + +class AbstractTextStep extends AbstractStep +{ + private var file : String; + + public function new() + { + super(); + } + + public override function initialize(options : DynamicAccess) : Void + { + super.initialize(options); + + this.file = FileSystem.absolutePath(this.getOption(options, 'file', String)); + } + + public override function run(context : StepContext) : Void + { + super.run(context); + + // Ensure that file exists + if (!FileSystem.exists(this.file)) { + throw 'Cannot find file "${this.file}"'; + } + } + + private function getLines() : Array + { + var input = File.read(this.file); + var lines = []; + try { + while (true) { + lines.push(input.readLine()); + } + } catch (e : haxe.io.Eof) {} + input.close(); + + return lines; + } + + private function setLines(lines : Array) : Void + { + var output = File.write(this.file); + for (line in lines) { + output.writeString(line); + output.writeString('\n'); + } + output.flush(); + output.close(); + } +} diff --git a/src/hxextern/step/CommandNpmStep.hx b/src/hxextern/step/CommandNpmStep.hx new file mode 100644 index 0000000..03496e5 --- /dev/null +++ b/src/hxextern/step/CommandNpmStep.hx @@ -0,0 +1,61 @@ +package hxextern.step; + +import haxe.DynamicAccess; +import hxextern.service.Process; +import hxextern.step.IStep; + +using hxextern.Tools; +using StringTools; + +@:step('command:npm') +class CommandNpmStep extends AbstractCommandStep +{ + private var module : String; + private var version : String; + + public function new() + { + super(); + } + + public override function initialize(options : DynamicAccess) : Void + { + super.initialize(options); + + this.module = this.getOption(options, 'module', String); + this.version = this.getOption(options, 'version', String, ''); + } + + public override function run(context : StepContext) : Void + { + super.run(context); + + // Prepare package name + var packageName = this.module; + if (this.version.length > 0) { + packageName += '@${this.version}'; + } + + // Install module + var result = this.exec('npm', ['install', packageName, '--silent']); + if (0 == result.code) { + this.console.success('NPM module "${packageName}" installed'); + + // Extract installed npm module version + var ereg = new EReg('${this.module.escapeEReg()}@([0-9.]+)', 'ig'); + if (ereg.match(result.output)) { + context.registerVariable('npm', { + module: { + name: this.module, + version: ereg.matched(1), + }, + }); + } + } else { + this.console + .error('NPM module "${this.module}" has not been installed') + .error(result.error) + ; + } + } +} diff --git a/src/hxextern/step/IStep.hx b/src/hxextern/step/IStep.hx index 9383b4e..421e389 100644 --- a/src/hxextern/step/IStep.hx +++ b/src/hxextern/step/IStep.hx @@ -1,12 +1,10 @@ package hxextern.step; -import haxe.macro.Expr; - -typedef TypeDefinitionMap = Map; +import haxe.DynamicAccess; interface IStep { - public var type(default, null) : String; + public function initialize(options : DynamicAccess) : Void; - public function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap; + public function run(context : StepContext) : Void; } diff --git a/src/hxextern/step/NpmStep.hx b/src/hxextern/step/NpmStep.hx deleted file mode 100644 index 0aa15e6..0000000 --- a/src/hxextern/step/NpmStep.hx +++ /dev/null @@ -1,37 +0,0 @@ -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 index c04dcb0..a768a79 100644 --- a/src/hxextern/step/ScriptStep.hx +++ b/src/hxextern/step/ScriptStep.hx @@ -1,23 +1,78 @@ package hxextern.step; +import haxe.DynamicAccess; +import haxe.io.Path; import haxe.macro.Compiler; import haxe.macro.Context; +import haxe.macro.Expr; +import hscript.Interp; +import hscript.Parser; import hxextern.step.IStep; import sys.FileSystem; -import haxe.macro.Expr; +import sys.io.File; @:step('script') class ScriptStep extends AbstractStep { + private var script : String; + private var classPath : String; + public function new() { - super('script'); + super(); } - public override function run(definitions : TypeDefinitionMap, options : Null) : TypeDefinitionMap + public override function initialize(options : DynamicAccess) : Void { - var classPath = FileSystem.absolutePath(options.classPath); + super.initialize(options); - return definitions; + this.script = this.getOption(options, 'script', String); + this.classPath = Path.addTrailingSlash(FileSystem.absolutePath(this.getOption(options, 'classPath', String, './'))); + } + + public override function run(context : StepContext) : Void + { + // Setup interpreter + var interpreter = this.createInterpreter(context); + + // Run main script + var result = this.loadScript(interpreter, this.script); + trace(result); + } + + private function createInterpreter(context : StepContext) : Interp + { + // Set default interpreter + var interpreter = new Interp(); + interpreter.variables.set('context', context); + interpreter.variables.set('import', function(script : String) : Dynamic { + return this.loadScript(interpreter, script); // TODO: more security on files loading + }); + + // Bind Haxe types + var types : Array = [ + // root level + Array, Date, DateTools, EReg, Lambda, List, Math, Reflect, Std, StringBuf, StringTools, Sys, Type, Xml, + ]; + for (type in types) { + interpreter.variables.set(Type.getClassName(type), type); + } + + return interpreter; + } + + private function loadScript(interpreter : Interp, script : String) : Dynamic + { + var scriptPath = this.classPath + script; + if (!FileSystem.exists(scriptPath)) { + throw 'Cannot find script "${script}"'; + } + + var parser = new Parser(); + parser.allowTypes = true; + var program = parser.parse(File.read(scriptPath)); + + this.console.debug('Running script "${script}"'); + return interpreter.execute(program); } } diff --git a/src/hxextern/step/StepContext.hx b/src/hxextern/step/StepContext.hx new file mode 100644 index 0000000..d00ee20 --- /dev/null +++ b/src/hxextern/step/StepContext.hx @@ -0,0 +1,54 @@ +package hxextern.step; + +import haxe.DynamicAccess; +import haxe.macro.Expr; + +class StepContext +{ + public var currentStep(default, null) : String; + + public var options(default, null) : Dynamic; + + public var definitions(default, null) : Map; + + public var variables(default, null) : DynamicAccess; + + public function new(?options : Dynamic) + { + this.options = options; + this.definitions = new Map(); + this.variables = {}; + } + + public function setup(step : String) : Void + { + this.currentStep = step; + } + + public function registerVariable(name : String, value : Dynamic) : StepContext + { + this.variables[name] = value; + return this; + } + + public function applyVariables(text : String) : String + { + return ~/\${([a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*)}/ig.map(text, function(ereg : EReg) : String { + return this.resolveVariable(ereg.matched(1)); + }); + } + + public function resolveVariable(field : String) : Dynamic + { + var parts = field.split('.'); + var data : Dynamic = this.variables; + while (parts.length > 0) { + var part = parts.shift(); + if (!Reflect.hasField(data, part)) { + throw 'Field "${part}" not found'; + } + data = Reflect.field(data, part); + } + return data; + } +} diff --git a/src/hxextern/step/TextReplaceStep.hx b/src/hxextern/step/TextReplaceStep.hx new file mode 100644 index 0000000..8644070 --- /dev/null +++ b/src/hxextern/step/TextReplaceStep.hx @@ -0,0 +1,74 @@ +package hxextern.step; + +import haxe.DynamicAccess; +import hxextern.step.IStep; +import sys.FileSystem; + +@:step('text:replace') +class TextReplaceStep extends AbstractTextStep +{ + private var from : String; + private var to : String; + private var regex : Bool; + + public function new() + { + super(); + } + + public override function initialize(options : DynamicAccess) : Void + { + super.initialize(options); + + this.from = this.getOption(options, 'from', String); + this.to = this.getOption(options, 'to', String); + this.regex = this.getOption(options, 'regex', Bool, true); + } + + public override function run(context : StepContext) : Void + { + super.run(context); + + // Prepare matcher + var matcher = (this.regex ? + function (line : String) : Bool { + return new EReg(this.from, 'g').match(line); + } : + function (line : String) : Bool { + return (line.indexOf(this.from) > -1); + } + ); + + // Works on lines + var lines = this.getLines(); + var matched = false; + for (i in 0...lines.length) { + var line = lines[i]; + + if (matcher(line)) { + var newLine = context.applyVariables(this.to); + if (line == newLine) { + continue; + } + + // Update content + matched = true; + lines[i] = newLine; + + this.console + .message('Replaced line') + .success(' ${line}') + .message('with line') + .success(' ${newLine}') + ; + } + } + + // Dump updates lines + if (matched) { + this.setLines(lines); + } else { + this.console.success('No changes done'); + } + } +}