Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time

Advanced control of the splitting logic

"Natural" bundling strategy

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)).

Splitting several classes in one bundle

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

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'), []);
	}
}

Controlled bundling

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.

Set up

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"]
  }
}

Hook format

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.

Arguments

This function will receive:

Response

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.
}

What can you change in the graph?

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.

Relationship between nodes and Haxe types

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!

Solving Reflection limitation

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');
}

Grouping multiple classes in a virtual bundle

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.

Loading with Standalone Modular

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
})

Loading with Webpack Haxe Loader

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
});