/** * Special concat/build task to handle build requirements * Concats AMD modules, removes their definitions, * and includes/excludes specified modules */ module.exports = function( grunt ) { "use strict"; var fs = require( "fs" ), requirejs = require( "requirejs" ), pkg = require( "../package.json" ), srcFolder = __dirname + "/../src/", rdefineEnd = /\r?\n\}\s*?\);[^}\w]*$/, read = function( fileName ) { return grunt.file.read( srcFolder + fileName ); }, wrapper = read( "wrapper.js" ).split( /\/\/ \@CODE\r?\n\/\/[^\n]+\n/ ), config = { baseUrl: "src", name: pkg.name, paths: { jquery : 'https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min' }, // We have multiple minify steps optimize: "none", // Include dependencies loaded with require findNestedDependencies: true, // Avoid inserting define() placeholder skipModuleInsertion: true, // Avoid breaking semicolons inserted by r.js skipSemiColonInsertion: true, wrap: { start: wrapper[ 0 ].replace( /\/\*jshint .* \*\/\r?\n/, "" ), end: wrapper[ 1 ] }, rawText: {}, 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 ) { var amdName; // Convert var modules if ( /.\/var\//.test( path.replace( process.cwd(), "" ) ) ) { contents = contents .replace( /define\([\w\W]*?return undefined\s*;/, " var " + ( /var\/([\w-]+)/.exec( name )[ 1 ] ) + ";" ) .replace( /define\([\w\W]*?return/, " var " + ( /var\/([\w-]+)/.exec( name )[ 1 ] ) + " =" ) .replace( rdefineEnd, "" ); } else if ( /.\/function\//.test( path.replace( process.cwd(), "" ) ) ) { contents = contents .replace( /define\([\w\W]*?return function/, " function " + ( /function\/([\w-]+)/.exec( name )[ 1 ] ) + "") .replace( rdefineEnd, "" ); // Remove empty definitions contents = contents .replace( /define\(\[[^\]]*\]\)[\W\r\n]+$/, "" ); } else { contents = contents .replace( /\s*return\s+[^\}]+(\}\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\r?\n\r?[\w\W]*?\r?\n\r?/ig, "" ); // Remove empty definitions contents = contents .replace( /define\(\[[^\]]*\]\)[\W\r\n]+$/, "" ); } // AMD Name if ( ( amdName = grunt.option( "amd" ) ) != null && /^exports\/amd$/.test( name ) ) { // Remove the comma for anonymous defines contents = contents .replace( new RegExp('/(\s*)"' + pkg.name + '"(\,\s*)/'), amdName ? "$1\"" + amdName + "\"$2" : "" ); } return contents; } grunt.registerMultiTask( "build", "Concatenate source, remove sub AMD definitions, " + "(include/exclude modules with +/- flags), embed date/version", function() { var flag, index, done = this.async(), flags = this.flags, name = grunt.option( "filename" ), minimum = this.data.minimum, removeWith = this.data.removeWith, excluded = [], included = [], 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 */ 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 */ excluder = function( flag ) { var m = /^(\+|\-|)([\w\/-]+)$/.exec( flag ), exclude = m[ 1 ] === "-", module = m[ 2 ]; if ( exclude ) { // Can't exclude certain modules if ( minimum.indexOf( module ) === -1 ) { // Add to excluded if ( excluded.indexOf( module ) === -1 ) { grunt.log.writeln( flag ); 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 minimum requirement." ); if ( module === "selector" ) { grunt.log.error( "If you meant to replace Sizzle, use -sizzle instead." ); } } } else { grunt.log.writeln( flag ); included.push( module ); } }; // Filename can be passed to the command line using // command line options name = name ? ( "dist/" + name ) : this.data.dest; // 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 delete flags[ "*" ]; for ( flag in flags ) { excluder( flag ); } 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 Version .replace( /@VERSION/g, version ) // Embed Date // yyyy-mm-ddThh:mmZ .replace( /@DATE/g, ( new Date() ).toISOString().replace( /:\d+\.\d+Z$/, "Z" ) ); // Write concatenated source to file grunt.file.write( name, compiled ); }; // 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 ); } ); } ); };