build/tasks/build.js (145 lines of code) (raw):
/**
* Special concat/build task to handle various jQuery build requirements
* Concats AMD modules, removes their definitions, and includes/excludes specified modules
*/
module.exports = function( grunt ) {
"use strict";
var fs = require( "fs" );
var requirejs = require("requirejs");
var srcFolder = __dirname + "/../../xf/";
var rdefineEnd = /\}\);[^}\w]*$/;
var config = {
baseUrl: "xf/src",
name: "xf.framework",
out: "js/xf.js",
// We have multiple minify steps
optimize: "none",
// Include dependencies loaded with require
findNestedDependencies: true,
// Avoid breaking semicolons inserted by r.js
skipSemiColonInsertion: true,
wrap: {
start: '/*! X-Framework @DATE */\n;(function (window, $, BB) {',
end: '}).call(this, window, $, Backbone); \n\n'
},
rawText: {},
paths: {
jquery: 'empty:',
underscore: 'empty:',
backbone: 'empty:'
},
onBuildWrite: convert
};
/**
* Strip all definitions generated by requirejs
* Convert "var" modules to var declarations
* "var module" means the module only contains a return statement that should be converted to a var declaration
* This is indicated by including the file in any "var" folder
* @param {String} name
* @param {String} path
* @param {String} contents The contents to be written (including their AMD wrappers)
*/
function convert( name, path, contents ) {
// Convert var modules.
if ( /.\/var\//.test( path ) ) {
contents = contents
.replace( /define\([\w\W]*?return/, "var " + (/var\/([\w-]+)/.exec(name)[1]) + " =" )
.replace( rdefineEnd, "" );
} else {
// Ignore exports, XF is exported when it is declared in xf.core.js.
contents = contents
.replace( /\s*return\s+[^\}]+(\}\);[^\w\}]*)$/, "$1" )
// Multiple exports
.replace( /\s*exports\.\w+\s*=\s*\w+;/g, "" );
// Remove define wrappers, closure ends, and empty declarations.
contents = contents
.replace( /define\([^{]*?{/, "" )
.replace( rdefineEnd, "" );
// Remove anything wrapped with
// /* ExcludeStart */ /* ExcludeEnd */
// or a single line directly after a // BuildExclude comment
contents = contents
.replace( /\/\*\s*ExcludeStart\s*\*\/[\w\W]*?\/\*\s*ExcludeEnd\s*\*\//ig, "" )
.replace( /\/\/\s*BuildExclude\n\r?[\w\W]*?\n\r?/ig, "" );
// Remove empty definitions
contents = contents
.replace( /define\(\[[^\]]+\]\)[\W\n]+$/, "" );
}
return contents;
}
grunt.registerMultiTask(
"build",
"Concatenate source, remove sub AMD definitions, (include/exclude modules with +/- flags), embed date/version",
function() {
var flag;
var index;
var done = this.async();
var flags = this.flags;
var optIn = flags[ "*" ];
var name = this.data.dest;
var minimum = ['core'];
var removeWith = [];
var excluded = [];
var included = [];
var version = grunt.config("pkg.version");
/**
* Recursively calls the excluder to remove on all modules in the list.
* @param {Array} list
* @param {String} [prepend] Prepend this to the module name. Indicates we're walking a directory.
*/
var excludeList = function (list, prepend) {
if (list) {
prepend = prepend ? prepend + "/" : "";
list.forEach(function (module) {
// Exclude var modules as well
if (module === "var") {
excludeList(fs.readdirSync(srcFolder + prepend + module), prepend + module);
return;
}
if (prepend) {
// Skip if this is not a js file and we're walking files in a dir.
if (!(module = /([\w-\/]+)\.js$/.exec(module))) {
return;
}
// Prepend folder name if passed.
// Remove .js extension.
module = prepend + module[1];
}
// Avoid infinite recursion.
if (excluded.indexOf(module) === -1) {
excluder("-" + module);
}
});
}
};
/**
* Adds the specified module to the excluded or included list, depending on the flag
* @param {String} flag A module path relative to the src directory starting with + or - to indicate whether it should included or excluded
*/
var excluder = function (flag) {
var m = /^(\+|\-|)([\w\/\.-]+)$/.exec(flag);
var exclude = m[ 1 ] === "-";
var module = m[ 2 ];
if (exclude) {
// Can't exclude certain modules
if (minimum.indexOf(module) === -1) {
// Add to excluded
if (excluded.indexOf(module) === -1) {
excluded.push(module);
// Exclude all files in the folder of the same name
// These are the removable dependencies
// It's fine if the directory is not there
try {
excludeList(fs.readdirSync(srcFolder + module), module);
} catch (e) {
grunt.verbose.writeln(e);
}
}
// Check removeWith list
excludeList(removeWith[ module ]);
} else {
grunt.log.error("Module \"" + module + "\" is a mimimum requirement.");
}
} else {
grunt.log.writeln(flag);
included.push(module);
}
};
// append commit id to version
if ( process.env.COMMIT ) {
version += " " + process.env.COMMIT;
}
// figure out which files to exclude based on these rules in this order:
// dependency explicit exclude
// > explicit exclude
// > explicit include
// > dependency implicit exclude
// > implicit exclude
// examples:
// * none (implicit exclude)
// *:* all (implicit include)
// *:*:-css all except css and dependents (explicit > implicit)
// *:*:-css:+effects same (excludes effects because explicit include is trumped by explicit exclude of dependency)
// *:+effects none except effects and its dependencies (explicit include trumps implicit exclude of dependency)
delete flags[ "*" ];
for ( flag in flags ) {
excluder( flag );
}
// Exclude second inclusion of xf.* modules, required by xf.ui components.
// TODO(jauhen): Move xf/ui folder to proper location to avoid this hack.
['-../src/xf.core', '-../src/xf.utils', '-../src/xf.device', '-../src/dom/dom'].forEach(function (module) {
excluder(module);
});
grunt.verbose.writeflags( excluded, "Excluded" );
grunt.verbose.writeflags( included, "Included" );
// Append excluded modules to version.
if ( excluded.length ) {
version += " -" + excluded.join( ",-" );
// set pkg.version to version with excludes, so minified file picks it up
grunt.config.set( "pkg.version", version );
grunt.verbose.writeln( "Version changed to " + version );
// Have to use shallow or core will get excluded since it is a dependency
config.excludeShallow = excluded;
}
config.include = included;
/**
* Handle Final output from the optimizer.
* @param {String} compiled
*/
config.out = function( compiled ) {
compiled = compiled
// Embed Date
.replace( /@DATE/g, grunt.template.today("dd-mm-yyyy") );
// Write concatenated source to file
grunt.file.write( name, compiled );
};
// Turn off opt-in if necessary
if ( !optIn ) {
// Overwrite the default inclusions with the explicit ones provided
config.rawText.xf = "define([" + (included.length ? included.join(",") : "") + "]);";
}
// Trace dependencies and concatenate files
requirejs.optimize( config, function( response ) {
grunt.verbose.writeln( response );
grunt.log.ok( "File '" + name + "' created." );
done();
}, function( err ) {
done( err );
});
});
// Special "alias" task to make custom build creation less grawlix-y
// Translation example
//
// grunt custom:+ajax,-dimensions,-effects,-offset
//
// Becomes:
//
// grunt build:*:*:+ajax:-dimensions:-effects:-offset
grunt.registerTask( "custom", function() {
var args = [].slice.call( arguments ),
modules = args.length ? args[ 0 ].replace( /,/g, ":" ) : "";
grunt.log.writeln( "Creating custom build...\n" );
grunt.task.run([ "build:*:*:" + modules, "uglify", "dist" ]);
});
};