Skip to content

Commit

Permalink
Merge pull request #34 from kevinresol/dyn_import_multi
Browse files Browse the repository at this point in the history
Support of multiple types in dynamic import
  • Loading branch information
benmerckx committed Feb 10, 2021
2 parents 73b50ab + 00e2e11 commit 8cb3526
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 20 deletions.
4 changes: 2 additions & 2 deletions readme.md
Expand Up @@ -46,9 +46,9 @@ import('./my/module/MyClass')
.then(console.log)
```

Genes expects a single-argument function declaration expression (`EFunction`) as the sole argument of `dynamicImport` and it will do 2 things:
Genes expects a function declaration expression (`EFunction`) as the sole argument of `dynamicImport` and it will do 2 things:

1. Get the argument name (e.g. "MyClass") and resolve it as a type in the current context, taking Haxe `import` statements into account. This is for preparing the relative path of the target file (e.g. `'../../MyClass.js'`).
1. For each arguments, take the argument name (e.g. "MyClass") and resolve it as a type in the current context, taking Haxe `import` statements into account. This is for preparing the relative path of the target files (e.g. `'../../MyClass.js'`).
2. Type the function body in the current context, ignoring the fact that it is a function body. Thus in the example the scope of `MyClass` is not the function argument but in current context i.e. the actual type `class MyClass {...}`. The return type is then applied as the type parameter of `js.lib.Promise`. This is for hinting the return type of the `dynamicImport(...)` call so that the compiler can do its typing job properly.

## Alternatives
Expand Down
92 changes: 78 additions & 14 deletions src/genes/Genes.hx
Expand Up @@ -7,32 +7,96 @@ import genes.util.PathUtil;
import genes.util.TypeUtil;

using haxe.macro.TypeTools;
using Lambda;

private typedef ImportedModule = {
name: String,
importExpr: Expr,
types: Array<{name: String, type: haxe.macro.Type}>
}
#end

class Genes {
macro public static function dynamicImport<T, R>(expr: ExprOf<T->
R>): ExprOf<js.lib.Promise<R>> {
final pos = Context.currentPos();

return switch expr.expr {
case EFunction(_, {args: [arg], expr: body}):
final name = arg.name;
final type = Context.getType(name);
case EFunction(_, {args: args, expr: body}):
final current = Context.getLocalClass().get().module;
final to = TypeUtil.moduleTypeName(TypeUtil.typeToModuleType(type));
final path = PathUtil.relative(current.replace('.', '/'),
to.replace('.', '/'));
final ret = Context.typeExpr(body).t.toComplexType();
final setup = {expr: EConst(CString('var $name = module.$name')),
pos: Context.currentPos()}
macro(js.Syntax.code('import({0})', $v{path})
.then(genes.Genes.ignore($v{name}, function(module) {
js.Syntax.code($setup);
$body;
})) : js.lib.Promise<$ret>);

final modules: Array<ImportedModule> = [];

for (arg in args) {
final name = arg.name;
final type = Context.getType(name);
final module = TypeUtil.moduleTypeName(TypeUtil.typeToModuleType(type));

switch modules.find(m -> m.name == module) {
case null:
modules.push({
name: module,
importExpr: {
final path = PathUtil.relative(current.replace('.', '/'),
module.replace('.', '/'));
macro js.Syntax.code('import({0})', $v{path});
},
types: [
{
name: name,
type: type
}
]
});
case module:
module.types.push({name: name, type: type});
}
}

final e = switch modules {
case [module]:
final setup = [
for (sub in module.types)
macro js.Syntax.code($v{'var ${sub.name} = module.${sub.name}'})
];

final list = [for (sub in module.types) macro $v{sub.name}];
final ignore = body -> macro genes.Genes.ignore($a{list}, $body);

final handler = ignore(macro function(module) {
@:mergeBlock $b{setup};
$body;
});

macro ${module.importExpr}.then($handler);

default:
final setup = [];
final ignores = [];

for (i in 0...modules.length) {
for (sub in modules[i].types) {
setup.push(macro js.Syntax.code($v{'var ${sub.name} = modules[$i].${sub.name}'}));
ignores.push(macro $v{sub.name});
}
}

final imports = macro $a{modules.map(module -> module.importExpr)};
macro js.lib.Promise.all($imports)
.then(genes.Genes.ignore($a{ignores}, function(modules) {
@:mergeBlock $b{setup};
$body;
}));
}

macro($e : js.lib.Promise<$ret>);

default:
Context.error('Cannot import', expr.pos);
}
}

public static function ignore<T>(name: String, res: T)
public static function ignore<T>(names: Array<String>, res: T)
return res;
}
12 changes: 12 additions & 0 deletions src/genes/es/ExprEmitter.hx
Expand Up @@ -209,6 +209,12 @@ class ExprEmitter extends Emitter {
FStatic(_.get() => {module: 'js.Syntax'}, _.get() => {name: 'code'}))
}, _) | TCall({expr: TIdent('__js__')}, _):
write(ctx.expr(e));
case TCall({
expr: TField(_,
FStatic(_.get() => {module: 'genes.Genes'},
_.get() => {name: 'ignore'}))
}, [_, body]):
emitExpr(body);
case TCall(e, params):
emitCall(e, params, false);
case TArrayDecl(el):
Expand Down Expand Up @@ -505,6 +511,12 @@ class ExprEmitter extends Emitter {
FStatic(_.get() => {module: 'js.Syntax'}, _.get() => {name: 'code'}))
}, _) | TCall({expr: TIdent('__js__')}, _):
write(ctx.value(e));
case TCall({
expr: TField(_,
FStatic(_.get() => {module: 'genes.Genes'},
_.get() => {name: 'ignore'}))
}, [_, body]):
emitValue(body);
case TCall(e, params):
emitCall(e, params, true);
case TReturn(_) | TBreak | TContinue:
Expand Down
21 changes: 17 additions & 4 deletions src/genes/util/TypeUtil.hx
Expand Up @@ -54,7 +54,9 @@ class TypeUtil {
return switch e.expr {
case TField(x, f) if (fieldName(f) == "iterator"):
switch Context.followWithAbstracts(x.t) {
case TInst(_.get() => {name: 'Array'}, _) | TInst(_.get() => {kind: KTypeParameter(_)}, _) | TAnonymous(_) | TDynamic(_) | TMono(_):
case TInst(_.get() => {name: 'Array'}, _) |
TInst(_.get() => {kind: KTypeParameter(_)}, _) | TAnonymous(_) |
TDynamic(_) | TMono(_):
true;
case _:
false;
Expand Down Expand Up @@ -95,13 +97,24 @@ class TypeUtil {
case null: [];
case {
expr: TCall(call = {
expr: TField(_, FStatic(_.get() => {module: 'genes.Genes'}, _.get() => {name: 'ignore'}))
}, [{expr: TConst(TString(name))}, func])
expr: TField(_,
FStatic(_.get() => {module: 'genes.Genes'},
_.get() => {name: 'ignore'}))
}, [{expr: TArrayDecl(texprs)}, func])
}:
final names = [
for (texpr in texprs)
switch texpr {
case {expr: TConst(TString(name))}:
name;
case _:
continue; // TODO: should error
}
];
typesInExpr(call).concat(typesInExpr(func).filter(type -> {
return switch type {
case TClassDecl(_.get() => {name: typeName}):
typeName != name;
names.indexOf(typeName) < 0;
default: true;
}
}));
Expand Down
25 changes: 25 additions & 0 deletions tests/ExternalClass2.hx
@@ -0,0 +1,25 @@
package tests;

import tests.TestCycle2.TestBase;

class ExternalSubClass2 {
public function new() {}

public static function sub() {
return 'sub2';
}
}

class ExternalClass2 extends TestBase {
public function new() {
super();
}

@:keep public function test() {
return 'ok2';
}

public static function success() {
return 'success2';
}
}
22 changes: 22 additions & 0 deletions tests/TestImportModule.hx
Expand Up @@ -2,9 +2,11 @@ package tests;

import tink.unit.AssertionBuffer;
import tests.ExternalClass;
import tests.ExternalClass2;

using tink.CoreApi;

// TODO: we should probably also make sure the static import statements are not present in the generated js
@:asserts
class TestImportModule {
public function new() {}
Expand All @@ -26,4 +28,24 @@ class TestImportModule {
return asserts.done();
}).ofJsPromise();
}

public function testImportMultiple(): Promise<AssertionBuffer> {
return genes.Genes.dynamicImport((ExternalClass, ExternalSubClass,
ExternalClass2, ExternalSubClass2) -> {
var a = new ExternalClass();
asserts.assert(Std.is(a, ExternalClass));
asserts.assert(ExternalClass.success() == 'success');
var a = new ExternalSubClass();
asserts.assert(Std.is(a, ExternalSubClass));
asserts.assert(ExternalSubClass.sub() == 'sub');
var a = new ExternalClass2();
asserts.assert(Std.is(a, ExternalClass2));
asserts.assert(ExternalClass2.success() == 'success2');
var a = new ExternalSubClass2();
asserts.assert(Std.is(a, ExternalSubClass2));
asserts.assert(ExternalSubClass2.sub() == 'sub2');
return asserts.done();
})
.ofJsPromise();
}
}

0 comments on commit 8cb3526

Please sign in to comment.