For most project it should be sufficient to let Modular work automatically using the natural relationship of your code:
-
Every explicit dependency will appear in the code and allow Modular to build a graph of the types of your project.
-
When splitting a class, it is equivalent to breaking the graph into separate graphs; one for your main entry point, and one for each of your modules.
Reminder: casting IS an explicit reference (e.g. cast(a, A)
, Std.is(a, A)
).
Normally splitting the class is done on a single class, and the bundle will include all its dependencies.
The recommended approach to bundle several classes is creating a factory:
class A {...}
class B {...}
class ABFactory {
static function createA():A {
return new A();
}
static function createB():B {
return new B();
}
}
load(ABFactory).then(function(_) {
var a:A = ABFactory.createA();
})
Using reflection (e.g. Type.resolveClass
) is an interesting edge case:
-
Using reflection, no visible relationship is available to Modular to attribute the class to a specific part of the graph.
-
By default, non-attributed classes (and their dependencies) will land in the main bundle. It often can be undesirable.
A simple solution is to somehow add this explicit relationship through code:
class A {...}
class B {
// Not very elegant, but explicit
@:keep
static private var reflected = [A];
function new() {
// the string 'A' is not enough to establish the relationship
var a = Type.createInstance(Type.resolveClass('A'), []);
}
}
The natural bundling strategy may have limitations that you don't want to use workaround for, or maybe your project is very large and highly dynamic.
Modular (both standalone or Webpack Haxe Loader) allow advanced users to finely control the bundling process.
The approach is to allow you to provide a "hook" script which will have all freedom to modify the dependency graph.
Hooks are declared in your project's package.json
, as a single file
or a list of files.
// package.json
{
"name": "My Project",
...
"config": {
"haxe-split-hook": "script/my_hook.js"
}
}
// package.json
{
"name": "My Project",
...
"config": {
"haxe-split-hook": ["script/my_hook.js", "script/my_other_hook.js"]
}
}
A hook is a simple JavaScript file which should export a function.
The hook runs on node.js, so you have access to the entire library of node modules.
This function will receive:
- the graph of your project (see Graphlib documentation),
- the name of the entry point class.
This function can return:
- nothing, or
- an additional list of nodes which should be split.
// my_hook.js
// Setup code goes here.
/**
* This is your hook
* @param {graphlib.Graph} graph Identifiers graph
* @param {String} root Root node
* @return {String[]} Additional modules to split (identifiers)
*/
module.exports = function(graph, root) {
console.log(
'Hook called with', graph.nodes().length,
'nodes and "' + root + '" entry point');
// Modify the graph here.
}
The hook can significantly change the graph; you can:
- create nodes (
graph.setNode(n)
); new nodes can be used to virtually regroup classes, - create edges (
graph.setEdge(fron, to)
; add directional dependencies between classes, - delete edges (
graph.unlink(from, to)
); remove a direction dependency between classes. - lookup orphan nodes (
graph.sources()
); this will give you the entry class and all the classes without explicit reference (e.g. reflection).
Modular will automatically "unlink" all the modules after the hooks have run.
Each Haxe type will appear as a node; in the case of a module, every type in the module will be a distinct node.
Type names are transformed in the compilation process:
- underscores become
_$
- dots become underscores
_
com.foo.Bar -> com_foo_Bar
Pascal_case -> Pascal_$case
Even__worse -> Even_$_$worse
If you have any doubt, look inside the generated source code!
Now we can solve the missing attribution due to reflection and create the link between the dynamicall created class and some class of the module it should belong:
module.exports = function(graph, root) {
// link the reflected class to its parent
graph.setEdge('module1_ReflectedClass', 'module1_App');
}
If you're going to use reflection anyway, you can as well go and group these classes together in a single bundle without having to create a factory class.
Attention: classes split this way
- CAN be used in type annotations (
var a:A
), - CAN be used in soft cast (
var a:A = cast o
), - can NOT be used in type inspection (
cast(a, A)
,Std.is(a, A)
), - MUST be referenced through reflection (
Type.resolveClass('A')
), - MUST be dynamically instantiated (
Type.createInstance
),
module.exports = function(graph, root) {
// create a node for the bundle
graph.setNode('DynBundle');
// assign nodes to this bundle
graph.setEdge('foo_ReflectedClass1', 'DynBundle');
graph.setEdge('foo_ReflectedClass2', 'DynBundle');
// register the bundle
return ['DynBundle'];
}
Modular will split out the 2 classes and emit DynBundle.js
.
To load this bundle, use Modular's Require.module
API
(or any mechanism to load the JS file after the main JS):
Require.module('DynBundle').then(function(_) {
var rc1:ReflectClass1 = Type.createInstance(Type.resolveClass('foo.ReflectedClass1'), []);
// notice that `resolveClass` needs the normal qualified Haxe type name
// notice that we can type the variable
})
With Webpack it is required to know the virtual name of the bundle. A helper function is available:
Webpack.loadModule('DynBundle').then(function(bundleExports) {
// Argument is the (optional) @:exposed declarations
var rc1:ReflectClass1 = Type.createInstance(Type.resolveClass('foo.ReflectedClass1'), []);
// notice that `resolveClass` needs the normal qualified Haxe type name
// notice that we can type the variable
});