# 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 architecture examples using this technique you can consult:
- [Haxe React+Redux sample](https://github.com/elsassph/haxe-react-redux)
- [Haxe React+MMVC sample](https://github.com/elsassph/haxe-react-mmvc)
Notes:
- There is in fact also a [webpack-haxe-loader](https://github.com/jasononeil/webpack-haxe-loader),
based on this library.
- Do not confuse with [modular-js](https://github.com/explorigin/modular-js/network),
which has a similar general goal but a different approach based on emitting one JS file
per Haxe class (check the forks).
> This project is compatible with Haxe 3.2.1+
## Context
JavaScript is one of the target platforms of
[Haxe](http://haxe.org/documentation/introduction/language-introduction.html),
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.
4. 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:
```javascript
//
// 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 = {});
```
It hopefully is understandable that we are defining a "registry" of NPM modules.
In the browser we will have a global object `window.$hx_scope.__registry__` used by
Modular to resolve 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](http://browserify.org/) (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](https://www.npmjs.com/package/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:
```html
```
You can create other libraries, and even use the same [lazy loading](#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](https://github.com/ReactTraining/react-router/tree/master/docs)) 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.
```haxe
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`
- `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](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#getcomponentnextstate-callback):
```js
```
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:
```haxe
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`
- `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.
```haxe
#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:
```haxe
var app = ReactDOM.render(...);
#if (debug && react_hot)
ReactHMR.autoRefresh(app);
#end
```
The feature leverages [react-proxy](https://github.com/gaearon/react-proxy/tree/master) 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](https://livereload.com) API. `Require` will
set a listener for `LiveReloadConnect` and register a "reloader plugin" to handle changes.
It is recommended to simply use [livereloadx](http://nitoyon.github.io/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](http://old.haxe.org/doc/advanced/magic#initialization-magic)
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.
```haxe
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;
}
...
}
```
## Further troubleshooting
Modular recognises a few additional debugging flags:
- `-D modular_dump`: generate an additional `.graph` file showing the relationship
between classes,
- `-D modular_debugmap`: generate, for each module, an additiona; `.map.html` file
showing a visual representation of the sourcemap.