node package manager

haxe-modular

Modular Haxe-JS

Code splitting and hot-reload for Haxe-JS applications.

Haxe modular is a set of tools and support classes allowing Webpack-like code-splitting, lazy loading and hot code reloading. Without Webpack and the JS fatigue.

For complete examples using this technique you can consult:

Do not confuse with the project modular-js, which has a similar general goal but different approach based on JSPM.

This project is compatible with Haxe 3.2.1+

Context

JavaScript is one of the target platforms of Haxe, a mature, strictly typed, high level, programming language offering a powerful type system and FP language features. The compiler stays very fast, even with massive codebases.

If anything, optimised bundling is exactly was Haxe does best: Haxe offers out of the box incomparable dead-code elimination and generates very efficient JavaScript.

What Haxe lacks natively is code splitting and HMR. Haxe doesn't suggest any best practice to implement it.

The goal of this project is to propose one robust and scalable solution.

Overview of solution

  1. NPM dependencies bundling in a single libs/vendor JavaScript file

    Best practice (for speed and better caching) is to regroup all the NPM dependencies into a single JavaScript file, traditionally called vendor.js or libs.js.

  2. Haxe-JS code and source-maps splitting, and lazy-loading

    Code splitting works by identifying features which can be asynchronously loaded at run time. JS bundles can be created automatically by using the Bundle.load helper.

  3. Hot-reload

    A helper class can be used listen to a LiveReload server and reload lazy-loaded modules automatically.

Installation

You need to install both a Haxe library and a NPM module:

# code splitting and hot-reload
npm install haxe-modular --save

# Haxe support classes
haxelib install modular

Add to your HXML:

# Haxe support classes and output rewriting
-lib modular

NPM dependencies bundling

Best practice (for compilation speed and better caching) is to regroup all the NPM dependencies into a single JavaScript file, traditionally called vendor.js or libs.js.

It is absolutely required when doing a modular Haxe application sharing NPM modules, and in particular if you want to use the React hot-reload functionality.

Template

Create a src/libs.js file using the following template:

//  
// npm dependencies library 
// 
(function(scope) {
    'use-strict';
    scope.__registry__ = Object.assign({}, scope.__registry__, {
        // 
        // list npm modules required in Haxe 
        // 
        'react': require('react'),
        'react-dom': require('react-dom'),
        'redux': require('redux')
    });
    
    if (process.env.NODE_ENV !== 'production') {
        // enable React hot-reload 
        require('haxe-modular');
    }
    
})(typeof $hx_scope != "undefined" ? $hx_scope : $hx_scope = {});

As you can see we are defining a "registry" of NPM modules. It is important to correctly name the keys of the object (eg. 'react') to match the Haxe require calls (eg. @:jsRequire('react')).

For React hot-module replacement, you just have to require('haxe-modular'). Notice that this enablement is only for development mode and will be removed when doing a release build.

Tip: there is nothing forcing your to register NPM modules, you can register any valid JavaScript object here.

Building

The library must be "compiled", that is required modules should be injected, typically using Browserify (small, simple, and fast).

For development (code with sourcemaps):

browserify src/libs.js -o bin/libs.js -d

For release, optimise and minify:

cross-env NODE_ENV=production browserify src/libs.js | uglifyjs -c -m > bin/libs.js

The difference is significant: React+Redux goes from 1.8Mb for dev to 280Kb for release (and 65Kb with gzip -6).

Note:

  • NODE_ENV=production will tell UglifyJS to remove "development" code from modules,
  • -d to make source-maps, -c to compress, and -m to "mangle" (rename variables),
  • cross-env is needed to be able to set the NODE_ENV variable on Windows. Alternatively you can use envify.

Usage

If you use NPM libraries (like React and its multiple addons), you will want to create at least one library. The library MUST be loaded before your Haxe code referencing them is loaded.

Simply reference the library file in your index.html in the right order:

<script src="libs.js"></script>
<script src="index.js"></script>

You can create other libraries, and even use the same lazy loading method to load them on demand, just like you will load Haxe modules. If you have a Haxe module with its own NPM dependencies, you will load the dependencies first, then the Haxe module.

Important:

  • all the NPM dependencies have to be moved into these NPM bundles,
  • do not run Browserify on the Haxe-JS files!

Haxe-JS code splitting

Code splitting requires a bit more planning than in JavaScript, so read carefully!

Features need to have one entry point class that can be loaded asynchronously.

A good way to split is to break down your application into "routes" (cf. react-router) or reusable complex components.

How it works

  • A graph of the classes "direct references" is created,
  • The references graph is split at the entry point of bundles,
  • Each bundle will include the direct (non-split) graph of classes,
  • unless the class is present in the main bundle (it will be shared).

What is a direct reference?

  • new A()
  • A.b / A.c()
  • Std.is(o, A)
  • cast(o, A)
  • ...

Difference between Debug and Release builds

Debug builds are optimised for "hot-reload":

  • Enums are compiled in the main bundle, otherwise you may load several incompatible instances of the enum definitions.
  • Transitive dependencies will be duplicated (eg. sub-components of views may be included in several routes) so you can hot-reload these sub-components.

Release builds are optimised for size:

  • All classes (and their dependencies) used in more than one bundle will be included in the main bundle.

Bundling

The Bundle class provides the module extraction functionality which then translates into the regular "Lazy loading" API.

import myapp.view.MyAppView;
...
Bundle.load(MyAppView).then(function(_) {
    // Class myapp.view.MyAppView can be safely used from now on.
    // It's time to render the view.
    new MyAppView();
}); 

API

Bundle.load(module:Class, loadCss:Bool = false):Promise<String>

  • module: the entry point class reference,
  • loadCss: optionally load a CSS file of the same name.
  • returns a Promise providing the name of the loaded module

(API is identical generally to the "Lazy loading" feature below)

React-router usage

Bundle.loadRoute(MyAppView) generates a wrapper function to satisfy React-router's async routes API using getComponent:

<Route getComponent=${Bundle.loadRoute(MyAppView)} />

Magic! MyAppView will be extracted in its own bundle and loaded lazily when the route is activated.

Lazy loading

The Require class provides Promise-based lazy-loading functionality for JS files:

Require.module('view').then(function(_) {
    // 'view.js' was loaded and evaluated.
}); 

Require.module returns the same Promise for a same module unless it failed, otherwise, calling the function again will attempt to reload the failed script.

API

Require.module(module:String, loadCss:Bool = false):Promise<String>

  • module: the name of the JS file to load (Haxe-JS module or library),
  • loadCss: optionally load a CSS file of the same name.
  • returns a Promise providing the name of the loaded module

Require.jsPath: relative path to JS files (defaults to ./)

Require.cssPath: relative path to CSS files (defaults to ./)

Hot-reload

Hot-reload functionality is based on the lazy-loading feature.

Calling Require.hot will set up a LiveReload hook. When a JS file loaded using Require.module will change, it will be automatically reloaded and the callbacks will be triggered to allow the application to handle the change.

#if debug
Require.hot(function(_) {
    // Some lazy-loaded module has been reloaded (eg. 'view.js').
    // Class myapp.view.MyAppView reference has now been updated,
    // and new instances will use the newly loaded code!
    // It's time to re-render the view.
    new MyAppView();
});
#end

Important: hot-reload does NOT update code in existing instances - you must create new instances of reloaded classes to use the new code.

API

Require.hot(?handler:String -> Void, ?forModule:String):Void

  • handler: a callback to be notified of modules having reloaded
  • forModule: if provided, only be notified of a specific module changes

React hot-reload wrapper

When using hot-reload for React views you will want to use the handy autoRefresh wrapper:

var app = ReactDOM.render(...);
 
#if (debug && react_hot)
ReactHMR.autoRefresh(app);
#end

The feature leverages react-proxy and needs to be enabled by calling require('haxe-modular'), preferably in your NPM modules bundle.

Note: you must compile with -D react_hot. The feature is only enabled in debug mode.

LiveReload server

The feature is based on the LiveReload API. Require will set a listener for LiveReloadConnect and register a "reloader plugin" to handle changes.

It is recommended to simply use livereloadx. The static mode dynamically injects the livereload client API script in HTML pages served:

npm install livereloadx -g
livereloadx -s bin
open http://localhost:35729

The reloader plugin will prevent page reloading when JS files change, and if the JS file corresponds to a lazy-loaded module, it is reloaded and re-evaluated.

The feature is simply based on filesystem changes, so you just have to rebuild the Haxe-JS application and let LiveReload inform our running application to reload some of the JavaScript files.

PS: stylesheets and static images will be normally live reloaded.

Known issues

Problem with init

If you don't know what __init__ is, don't worry :) If your're curious

When using __init__ you may generate code that will not be moved to the right bundle:

  • assume that __init__ code will be duplicated in all the bundles,
  • unless you generate calls to static methods.
class MyComponent 
{
    static function __init__(
    {
        // these lines will go in all the bundles
        var foo = 42;
        untyped window.something = function() {...}
        
        // these lines will go in the bundle containing MyComponent
        MyComponent.doSomething();
        if (...) MyComponent.anotherThing();
        
        // this line will go in the bundle containing OtherComponent
        OtherComponent.someProp = 42;
    }
    ...
}