PhotonJS.Build (now with added Grunt)
There are currently a few different JavaScript module formats out in the wild, and it's true to say that there are mixed opinions on the various approaches. In addition to this, it now seems that ECMA (Harmony) support for modules will be postponed till a future version.
The module patterns shown in many examples confuse dependency injection with packaging, and do not address the needs of modern large scale enterprise applications (hopefully I'll have time to elaborate on this in a blog sometime). This does not mean that module handling libraries such as RequireJS, etc. should be avoided, it just means we need to think hard about what is the right level of granularity for our module, and the source files from which it is comprised.
Regardless where you stand on these issues, the follow is probably true:
- We don't want make our code too dependent on one particular format, unless we can help it.
- We want an easy way convert existing libraries to work with our module format of choice.
- We want to easily include it in our build process.
PhotonJS.Build hopes to address these issues.
The PhotonJS.Build module builder can be used to build module files supporting multiple module formats, including 'amd' and 'global'. Module packaging information is maintained in a JavaScript Module (.jsm) file. By placing module packaging information in a separate file developers do not have to pollute their source code with module specific semantics, they can focus on writing clear maintainable code which can be deployed easily to a variety of module formats.
Example module file:
({
name:'photon.examples.module',
/**
* Module version number.
*/
version:'0.7.0.2',
/**
* An ordered list of the files that make up the module.
*/
files:[
'file01.js',
'file02.js'
],
/**
* Module dependencies
*/
dependencies:{
/**
* '$' The variable that will be used to reference the dependency
*/
'$':{
/**
* The AMD dependency
*/
amd:'jquery',
/**
* The global dependency, resolved as window.jQuery
*/
global:'jQuery'
}
},
environment:{
/**
* Optional environment dependencies
*/
dependencies:[
/**
* Reference window using a parameter named 'window'
*/
'window',
/**
* Reference document using a parameter named 'doc'.
*/
{
alias:'doc',
name:'document'
}]
},
/**
* Configuration information
*/
configuration:{
debug:{
srcOutput:'../output/%module%-debug.js',
mapOutput:'../output/%module%-debug.js.map'
}
}
});
Building via the command line:
node build-js.js --jsm Examples/Example1/module.jsm --add-source-map-directive
--configuration debug
Output:
(function(window, doc){
(function(factory) {
if (typeof define === 'function' && define.amd]) {
define(['exports', 'jquery'], factory);
} else if (window) {
var nsi = 'photon.examples.module'.split('.'), ns = window;
for (var i= 0, n=nsi.length; i<n; i++) {
ns = ns[nsi[i]] = ns[nsi[i]] || {};
}
factory(ns, window.jQuery);
}
})(function(module, $) {
/**
* Gets a message from file 1
* @return {String}
*/
module.getMessage1 = function () {
return "Message from file 1.";
}
/**
* Gets a message from file 2
* @return {String}
*/
module.getMessage2 = function() {
return "Message from file 2.";
}
});
})(window, document);
//@ sourceMappingURL=photon.examples.module-debug.js.map
Source maps are automatically created for the module. Source maps allow you to debug the module as if you had deployed the individual files it is comprised of (currently only supported in chrome). For more information on source maps including how to enable them, check out http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/
When developing it can be useful to have you modules rebuilt automatically whenever you change any of its files. To enable this, simply specify the --monitor option.
Example:
node build-js.js --jsm Examples/Example1/module.jsm --add-source-map-directive
--configuration debug --monitor
During development files may get renamed, deleted, etc. because of this it may be useful to specify how missing module files should be handled. There are three different options:
- IGNORE: Ignore errors
- THROW: An exception it thrown
- TODO: A '// TODO: ' comment is output in the module file which provides details of the missing file.
Example:
node build-js.js --jsm Examples/Example1/module.jsm --add-source-map-directive
--configuration debug --monitor --error-strategy TODO
By default, the module produced will support all available formats (current 'amd' & 'global'). To manually specify which formats the module should support use the --formats command line option.
Example:
node build-js.js --jsm Examples/Example1/module.jsm --add-source-map-directive
--configuration debug --formats amd
Modules can be versioned by specifying a version property in the '.jsm' file, or using the '--version' command line switch. If both are specified the command line '--version' wins.
Example:
node build-js.js --jsm Examples/Example1/module.jsm --add-source-map-directive
--configuration debug --formats global --version 0.7.0.2
({
name : 'myModule',
version: 0.7.0.1
})
Generates:
(function(factory) {
if (window) {
var ns = window.myModule = window.myModule || {};
factory(ns);
}
})(function(myModule) {
myModule.version = '0.7.0.2';
});
In AMD shims can be used to import modules that add themselves to existing global namespaces. For example, jquery.ui.core does not directly export anything, it simply adds itself to jquery/$. The 'amd' dependencies section supports this mechanism in two ways.
dependencies : {
'$' : {
// multiple dependencies are pulled in and exposed via the '$' factory parameter.
amd : ['jquery', 'jquery.ui.core']
}
}
dependencies: {
// define dependency without exposing it as a factory parameter.
'<<anonymous>>' : {
amd : ['jquery', 'jquery.ui.core']
}
}
Grunt support is provided via the 'module' task. The example below demonstrates how to configure the grunt.js file to build a module. It also shows how the 'watch' task can be used to trigger builds automatically whenever a file changes.
module.exports = function (grunt) {
grunt.initConfig({
module:{
photon:{
jsm:'../source/core/photon.jsm',
options:{
configuration:'debug',
version:'0.7.0.1'
}
}
},
watch: {
module : {
files: ['../source/core/**/*.js', '../source/core/**/*.jsm'],
tasks: 'module'
}
},
lint: {
files: ['grunt.js', '../output/photon-debug.js']
}
});
// default task.
grunt.registerTask('default', 'module lint');
// load module-grunt tasks
grunt.loadTasks('../../Build/node_modules/module-grunt');
};
Example:
node ./grunt/grunt/bin/grunt module watch:module
A Dynamic module is a .jsm file that returns a function that in turn returns a module. The advantage of dynamic modules is that they can be passed properties which can be used to alter the module. For example this feature could be used to easily compile a custom module based on a subset of required 'features'.
Example:
// Example file that shows dynamic packaging dependent on the features requested
(function(properties) {
var result =
{
name:'photon.examples.module',
files :[]
}
var features = properties.features;
function isFeatureIncluded(feature) {
return !features || !features.length ? true : features.indexOf(feature) !== -1;
}
if (isFeatureIncluded('AwesomeFeature')) {
result.files.push('awesomeFile1.js', 'awesomeFile2.js');
}
if (isFeatureIncluded('AdequateFeature')) {
result.files.push('adequateFile1.js', 'adequateFile2.js');F
}
return result;
});
// example showing hard coded grunt configuration, grunt properties could also be used...
module:{
photon:{
jsm:'../source/core/photon.jsm',
options:{
properties:{
features:['AwesomeFeature', 'AdequateFeature']
}
}
}
}
Grunt support the Google Closure Compiler is supported via the 'closureCompiler' task.
The snippet below shows how to configure the 'closureCompiler' task.
closureCompiler: {
photon : {
js : ['../output/photon-debug.js'],
jsOutputFile : '../output/photon-min.js',
closurePath : './tools/Closure',
options: {
'compilation_level' : 'SIMPLE_OPTIMIZATIONS'
}
}
}