/** * Special concat/build task to handle various jQuery build requirements * Concats AMD modules, removes their definitions, * and includes/excludes specified modules */ "use strict"; module.exports = function( grunt ) { var fs = require( "fs" ), requirejs = require( "requirejs" ), Insight = require( "insight" ), pkg = require( "../../package.json" ), srcFolder = __dirname + "/../../src/", rdefineEnd = /\}\s*?\);[^}\w]*$/, read = function( fileName ) { return grunt.file.read( srcFolder + fileName ); }, // Catch `// @CODE` and subsequent comment lines event if they don't start // in the first column. wrapper = read( "wrapper.js" ).split( /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/ ), config = { baseUrl: "src", name: "jquery", // Allow strict mode useStrict: true, // 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( /\/\*\s*eslint(?: |-).*\s*\*\/\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\(\s*(["'])[\w\W]*?\1[\w\W]*?return/, "var " + ( /var\/([\w-]+)/.exec( name )[ 1 ] ) + " =" ) .replace( rdefineEnd, "" ); } 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\([^{]*?{\s*(?:("|')use strict\1(?:;|))?/, "" ) .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]+$/, "" ); } // AMD Name if ( ( amdName = grunt.option( "amd" ) ) != null && /^exports\/amd$/.test( name ) ) { if ( amdName ) { grunt.log.writeln( "Naming jQuery with AMD name: " + amdName ); } else { grunt.log.writeln( "AMD name now anonymous" ); } // Remove the comma for anonymous defines contents = contents .replace( /(\s*)"jquery"(\,\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, optIn = 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 additional, 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 ); } } additional = removeWith[ module ]; // Check removeWith list if ( additional ) { excludeList( additional.remove || additional ); if ( additional.include ) { included = included.concat( additional.include ); grunt.log.writeln( "+" + additional.include ); } } } else { grunt.log.error( "Module \"" + module + "\" is a minimum requirement." ); } } else { grunt.log.writeln( flag ); included.push( module ); } }; // Filename can be passed to the command line using // command line options // e.g. grunt build --filename=jquery-custom.js 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 // > 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 ); } // Replace exports/global with a noop noConflict if ( ( index = excluded.indexOf( "exports/global" ) ) > -1 ) { config.rawText[ "exports/global" ] = "define(['../core']," + "function( jQuery ) {\njQuery.noConflict = function() {};\n});"; excluded.splice( index, 1 ); } 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 ); }; // Turn off opt-in if necessary if ( !optIn ) { // Overwrite the default inclusions with the explicit ones provided config.rawText.jquery = "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 = this.args, modules = args.length ? args[ 0 ].replace( /,/g, ":" ) : "", done = this.async(), insight = new Insight( { trackingCode: "UA-1076265-4", pkg: pkg } ); function exec( trackingAllowed ) { var tracks = args.length ? args[ 0 ].split( "," ) : []; var defaultPath = [ "build", "custom" ]; tracks = tracks.map( function( track ) { return track.replace( /\//g, "+" ); } ); if ( trackingAllowed ) { // Track individuals tracks.forEach( function( module ) { var path = defaultPath.concat( [ "individual" ], module ); insight.track.apply( insight, path ); } ); // Track full command insight.track.apply( insight, defaultPath.concat( [ "full" ], tracks ) ); } grunt.task.run( [ "build:*:*" + ( modules ? ":" + modules : "" ), "uglify", "dist" ] ); done(); } grunt.log.writeln( "Creating custom build...\n" ); // Ask for permission the first time if ( insight.optOut === undefined ) { insight.askPermission( null, function( _error, result ) { exec( result ); } ); } else { exec( !insight.optOut ); } } ); };