Including External JS Lib in Haxe Output

One missing feature of the new Haxe std jQuery extern from the old one is the ability to embed jQuery directly into the Haxe JS output. Although often it is better to use a CDN, which may improve page loading performance by allowing parallel downloads and increasing the chance of cache-hits (as well as other reasons), sometimes it is just handy if the Haxe compiler could embed jQuery into the JS output such that we don’t have to add another line of <script> tag in our HTML file.

Turn out I was not able to port that feature to the new extern that easily. Let’s take a look at how the old extern did it. The magic lies within the __init__ method, which has a macro function call as follows:

haxe.macro.Compiler.includeFile("js/jquery-latest.min.js");

There's a macro for that!

So, yeah, if you didn’t know, now you have learned that we have a macro named includeFile to include any external file into the JS output. The function was implemented in pure Haxe, and was quite simple, so lets go through its source code together:

public static macro function includeFile( fileName : Expr ) {
    /*
        Extract the input, `fileName`, as a String.
    */
    var str = switch( fileName.expr ) {
    case EConst(c):
        switch( c ) {
        case CString(str): str;
        default: null;
        }
    default: null;
    }
    if( str == null ) Context.error("Should be a constant string", fileName.pos);

    /*
        Load the file content into variable `f`.
    */
    var f = try sys.io.File.getContent(Context.resolvePath(str)) catch( e : Dynamic ) Context.error(Std.string(e), fileName.pos);

    /*
        Return an expression, which is in the form of `untyped __js__("file_content")`.
        Note that it can be written in a simpler way using expression reification:
        ```
        return macro untyped __js__($v{f});
        ```
    */
    var p = Context.currentPos();
    return { expr : EUntyped( { expr : ECall( { expr : EConst(CIdent("__js__")), pos : p }, [ { expr : EConst(CString(f)), pos : p } ]), pos : p } ), pos : p };
}

In short, the macro function loads the file content and injects it directly using __js__. As a function to inject simple JS code, it is simple and elegant. However it is not really suitable for including JS libs due to how Haxe generates JS code, which looks like this:

// Generated by Haxe
(function (console) { "use strict";
/*
    1. classes generation, e.g. class inheritance, methods etc. 
*/
/*
    2. __init__ of all classes
    jQuery will be embeded here
*/
/*
    3. static variables generation
*/
Test.main();
})(typeof console != "undefined" ? console : {log:function(){}});

Firstly, we notice that Haxe wraps everything in a closure by default. It avoids global variables being exposed to the outside world. Inside the closure, we enable strict mode, which improves performance and eliminates JS silent errors by changing them to throw errors. It is good for Haxe since its JS output is compatible with strict mode. But the JS lib we want to include may not. In fact, recent versions of jQuery are incompatible with strict mode. The out-dated jQuery version 1.6.4, which is included by the old extern, just happens to be strict mode compatible, ironically. That’s why this problem didn’t surface until I try to update jQuery.

Secondly, classes generation comes before the __init__s, which is the place where JS libs are embedded. It means that Haxe generated classes cannot inherit from classes of the external JS libs. It is not a problem for jQuery, since we do not often inherit from it, but it could be a problem if we want to use a JS lib that is structured using a more OOP approach.

To solve these issues, we have to find a way to place the external files before everything generated by Haxe, including the closure. And since it depends on how Haxe JS code generation works, we have to modify the compiler source. The good news is, I’ve just done that so you don’t have to :)

I’ve added a new optional argument to haxe.macro.Compiler.includeFile, named position, for us to specify the position of where the injection takes place. There is three possible values, Top (default), Closure, and Inline. Inline is the old behavior, which inject the file content at the call site of includeFile. The two new positions are illustrated as follows:

// Generated by Haxe
/*
    new include position: `Top`
*/
(function (console) { "use strict";
/*
    new include position: `Closure`
*/
/*
    1. classes generation, e.g. class inheritance, methods etc. 
*/
/*
    2. __init__ of all classes
*/
/*
    3. static variables generation
*/
Test.main();
})(typeof console != "undefined" ? console : {log:function(){}});

Problem solved! The default position, Top, is outside of the closure, not in strict mode, and it is before everything else.

As a bonus, we can now call includeFile within a hxml file, like so:

-js Main
-main Main.js
--macro includeFile('path/to/dependencies.js')

It could be useful if you use npm or bower to manage JS dependencies. We can concatenate all the dependencies into a single file and pass it to includeFile. I’m still experimenting with this approach. So, more on that later.

comments powered by Disqus