Flatten class fields like module-level fields?

70 Views Asked by At

I'm trying to use Haxe to generate a Javascript file in which each static function/variable is named like a module-level field (package_ClassName_fieldName rather than package_ClassName.fieldName). I'd also like to be able to use build macros though, and as far as I know, those aren't usable with actual module-level fields. Is there some way to get the best of both worlds?

I've tried using build macros to change the class type:

import haxe.macro.Context;
import haxe.macro.Expr; // Field
import haxe.macro.Type; // ClassKind, ClassType

class Macro {
    public static macro function convertClassToModuleLevelFields() : Array<Field> {
        var ct:ClassType = Context.getLocalClass().get();
        ct.kind = ClassKind.KModuleFields(ct.module);
        return Context.getBuildFields();
    }
}

...but it didn't seem to change anything. Context.defineModule and Context.defineType seemed good for redefining the static class fields as module-level fields, but it doesn't seem like I can convert a class's ClassType to an equivalent TypeDefinition. Compiler.setCustomJSGenerator sounded promising too, but JSGenApi.generateValue always uses dots. Is there any way to do this?

1

There are 1 best solutions below

0
YellowAfterlife On

If this is for purposes of interfacing with the code, you could give fields an @:expose metadata with a macro:

import haxe.macro.Compiler;
import haxe.macro.Context;
import haxe.macro.Expr;

class Macro {
    public static macro function flatten():Array<Field> {
        var lt = Context.getLocalType();
        var pos = Context.currentPos();
        var pack = switch (lt) {
            case TInst(_.get() => ct, _): ct.pack.concat([ct.name]);
            default: return null;
        }
        var fields = Context.getBuildFields();
        var changed = false;
        for (field in fields) {
            var acc = field.access;
            if (acc == null || !acc.contains(AStatic)) continue;
            if (field.meta == null) field.meta = [];
            if (field.meta.filter(m -> m.name == ":expose").length > 0) continue;
            var name = field.name;
            var nativeMeta = field.meta.filter(m -> m.name == ":native")[0];
            if (nativeMeta != null
                && nativeMeta.params != null
                && nativeMeta.params.length > 0
            ) switch (nativeMeta.params[0].expr) {
                case EConst(CString(s)): name = s; // @:native("newName") static ...
                default:
            }
            field.meta.push({
                name: ":expose",
                params: [macro $v{pack.concat([name]).join("_")}],
                pos: pos,
            });
            changed = true;
        }
        return changed ? fields : null;
    }
}

and then

@:build(Macro.flatten())
@:keep class Test {
    static function one() {
        
    }
    static function two() {
        
    }
    static function main() {
        Browser.console.log("hi!");
    }
}

would produce

var Test = function() { };
Test.one = $hx_exports["Test_one"] = function() {
};
Test.two = $hx_exports["Test_two"] = function() {
};
Test.main = $hx_exports["Test_main"] = function() {
    $global.console.log("hi!");
};

and you'd be able to access your functions via window.Test_one or global.Test_one (depending on where JS is running).

You can also use

--macro addGlobalMetadata("mypackage", "@:build(Macro.flatten())")

to add a @:build to all classes in a package, though note that doing so includes them in the build even if DCE would usually consume them (bug? feature?).

If this is for purposes of optimizing the generated code for Closure (github issue), you'd want to use CustomJSGenerator but write your own printer for some/all expressions instead of using JSGenApi.generateValue. I used to maintain a generator for this, but eventually gave up on it since the gains weren't as noticeable in larger projects.