module.exports = function( grunt ) {
"use strict";
var distpaths = [
"dist/jquery.js",
"dist/jquery.min.map",
"dist/jquery.min.js"
],
gzip = require("gzip-js"),
readOptionalJSON = function( filepath ) {
var data = {};
try {
data = grunt.file.readJSON( filepath );
} catch(e) {}
return data;
},
srcHintOptions = readOptionalJSON("src/.jshintrc");
// The concatenated file won't pass onevar
// But our modules can
delete srcHintOptions.onevar;
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
dst: readOptionalJSON("dist/.destination.json"),
compare_size: {
files: [ "dist/jquery.js", "dist/jquery.min.js" ],
options: {
compress: {
gz: function( contents ) {
return gzip.zip( contents, {} ).length;
}
},
cache: "dist/.sizecache.json"
}
},
selector: {
destFile: "src/selector-sizzle.js",
apiFile: "src/sizzle-jquery.js",
srcFile: "src/sizzle/dist/sizzle.js"
},
build: {
all: {
dest: "dist/jquery.js",
src: [
"src/intro.js",
"src/core.js",
{ flag: "sizzle", src: "src/selector-sizzle.js", alt: "src/selector-native.js" },
"src/callbacks.js",
"src/deferred.js",
"src/support.js",
"src/data.js",
"src/queue.js",
"src/attributes.js",
"src/event.js",
"src/traversing.js",
"src/manipulation.js",
{ flag: "wrap", src: "src/wrap.js" },
{ flag: "css", src: "src/css.js" },
"src/serialize.js",
{ flag: "event-alias", src: "src/event-alias.js" },
{ flag: "ajax", src: "src/ajax.js" },
{ flag: "ajax/script", src: "src/ajax/script.js", needs: ["ajax"] },
{ flag: "ajax/jsonp", src: "src/ajax/jsonp.js", needs: [ "ajax", "ajax/script" ] },
{ flag: "ajax/xhr", src: "src/ajax/xhr.js", needs: ["ajax"] },
{ flag: "effects", src: "src/effects.js", needs: ["css"] },
{ flag: "offset", src: "src/offset.js", needs: ["css"] },
{ flag: "dimensions", src: "src/dimensions.js", needs: ["css"] },
{ flag: "deprecated", src: "src/deprecated.js" },
"src/exports.js",
"src/outro.js"
]
}
},
jshint: {
dist: {
src: [ "dist/jquery.js" ],
options: srcHintOptions
},
grunt: {
src: [ "Gruntfile.js" ],
options: {
jshintrc: ".jshintrc"
}
},
tests: {
// TODO: Once .jshintignore is supported, use that instead.
// issue located here: https://github.com/gruntjs/grunt-contrib-jshint/issues/1
src: [ "test/data/{test,testinit,testrunner}.js", "test/unit/**/*.js" ],
options: {
jshintrc: "test/.jshintrc"
}
}
},
testswarm: {
tests: "ajax attributes callbacks core css data deferred dimensions effects event manipulation offset queue selector serialize support traversing Sizzle".split(" ")
},
watch: {
files: [ "<%= jshint.grunt.src %>", "<%= jshint.tests.src %>", "src/**/*.js" ],
tasks: "dev"
},
"pre-uglify": {
all: {
files: {
"dist/jquery.pre-min.js": [ "dist/jquery.js" ]
},
options: {
banner: "\n\n\n\n\n\n\n\n\n\n" + // banner line size must be preserved
"/*! jQuery v<%= pkg.version %> | " +
"(c) 2005, 2013 jQuery Foundation, Inc. | " +
"jquery.org/license\n" +
"//@ sourceMappingURL=jquery.min.map\n" +
"*/\n"
}
}
},
uglify: {
all: {
files: {
"dist/jquery.min.js": [ "dist/jquery.pre-min.js" ]
},
options: {
// Keep our hard-coded banner
preserveComments: "some",
sourceMap: "dist/jquery.min.map",
sourceMappingURL: "jquery.min.map",
report: "min",
beautify: {
ascii_only: true
},
compress: {
hoist_funs: false,
join_vars: false,
loops: false,
unused: false
},
mangle: {
// saves some bytes when gzipped
except: [ "undefined" ]
}
}
}
},
"post-uglify": {
all: {
files: {
"dist/jquery.min.map.tmp": [ "dist/jquery.min.map" ],
"dist/jquery.min.js.tmp": [ "dist/jquery.min.js" ]
},
options: {
tempFiles: [ "dist/jquery.min.map.tmp", "dist/jquery.min.js.tmp", "dist/jquery.pre-min.js" ]
}
}
}
});
grunt.registerTask( "testswarm", function( commit, configFile ) {
var jobName,
testswarm = require( "testswarm" ),
runs = {},
done = this.async(),
pull = /PR-(\d+)/.exec( commit ),
config = grunt.file.readJSON( configFile ).jquery,
tests = grunt.config([ this.name, "tests" ]);
if ( pull ) {
jobName = "jQuery pull #" + pull[ 1 ] + "";
} else {
jobName = "jQuery commit #" + commit.substr( 0, 10 ) + "";
}
tests.forEach(function( test ) {
runs[test] = config.testUrl + commit + "/test/index.html?module=" + test;
});
// TODO: create separate job for git/git2 so we can do different browsersets
testswarm.createClient( {
url: config.swarmUrl,
pollInterval: 10000,
timeout: 1000 * 60 * 30
} )
.addReporter( testswarm.reporters.cli )
.auth( {
id: config.authUsername,
token: config.authToken
})
.addjob(
{
name: jobName,
runs: runs,
runMax: config.runMax,
browserSets: "popular-no-old-ie"
}, function( err, passed ) {
if ( err ) {
grunt.log.error( err );
}
done( passed );
}
);
});
grunt.registerTask( "selector", "Build Sizzle-based selector module", function() {
var cfg = grunt.config("selector"),
name = cfg.destFile,
sizzle = {
api: grunt.file.read( cfg.apiFile ),
src: grunt.file.read( cfg.srcFile )
},
compiled, parts;
/**
sizzle-jquery.js -> sizzle between "EXPOSE" blocks,
replace define & window.Sizzle assignment
// EXPOSE
if ( typeof define === "function" && define.amd ) {
define(function() { return Sizzle; });
} else {
window.Sizzle = Sizzle;
}
// EXPOSE
Becomes...
Sizzle.attr = jQuery.attr;
jQuery.find = Sizzle;
jQuery.expr = Sizzle.selectors;
jQuery.expr[":"] = jQuery.expr.pseudos;
jQuery.unique = Sizzle.uniqueSort;
jQuery.text = Sizzle.getText;
jQuery.isXMLDoc = Sizzle.isXML;
jQuery.contains = Sizzle.contains;
*/
// Break into 3 pieces
parts = sizzle.src.split("// EXPOSE");
// Replace the if/else block with api
parts[1] = sizzle.api;
// Rejoin the pieces
compiled = parts.join("");
grunt.verbose.writeln("Injected " + cfg.apiFile + " into " + cfg.srcFile);
// Write concatenated source to file, and ensure newline-only termination
grunt.file.write( name, compiled.replace( /\x0d\x0a/g, "\x0a" ) );
// Fail task if errors were logged.
if ( this.errorCount ) {
return false;
}
// Otherwise, print a success message.
grunt.log.writeln( "File '" + name + "' created." );
});
// Special "alias" task to make custom build creation less grawlix-y
grunt.registerTask( "custom", function() {
var done = this.async(),
args = [].slice.call(arguments),
modules = args.length ? args[0].replace(/,/g, ":") : "";
// Translation example
//
// grunt custom:+ajax,-dimensions,-effects,-offset
//
// Becomes:
//
// grunt build:*:*:+ajax:-dimensions:-effects:-offset
grunt.log.writeln( "Creating custom build...\n" );
grunt.util.spawn({
grunt: true,
args: [ "build:*:*:" + modules, "pre-uglify", "uglify", "dist" ]
}, function( err, result ) {
if ( err ) {
grunt.verbose.error();
done( err );
return;
}
grunt.log.writeln( result.stdout.replace("Done, without errors.", "") );
done();
});
});
// Special concat/build task to handle various jQuery build requirements
grunt.registerMultiTask(
"build",
"Concatenate source (include/exclude modules with +/- flags), embed date/version",
function() {
// Concat specified files.
var compiled = "",
modules = this.flags,
optIn = !modules["*"],
explicit = optIn || Object.keys(modules).length > 1,
name = this.data.dest,
src = this.data.src,
deps = {},
excluded = {},
version = grunt.config( "pkg.version" ),
excluder = function( flag, needsFlag ) {
// optIn defaults implicit behavior to weak exclusion
if ( optIn && !modules[ flag ] && !modules[ "+" + flag ] ) {
excluded[ flag ] = false;
}
// explicit or inherited strong exclusion
if ( excluded[ needsFlag ] || modules[ "-" + flag ] ) {
excluded[ flag ] = true;
// explicit inclusion overrides weak exclusion
} else if ( excluded[ needsFlag ] === false &&
( modules[ flag ] || modules[ "+" + flag ] ) ) {
delete excluded[ needsFlag ];
// ...all the way down
if ( deps[ needsFlag ] ) {
deps[ needsFlag ].forEach(function( subDep ) {
modules[ needsFlag ] = true;
excluder( needsFlag, subDep );
});
}
}
};
// 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)
src.forEach(function( filepath ) {
var flag = filepath.flag;
if ( flag ) {
excluder(flag);
// check for dependencies
if ( filepath.needs ) {
deps[ flag ] = filepath.needs;
filepath.needs.forEach(function( needsFlag ) {
excluder( flag, needsFlag );
});
}
}
});
// append excluded modules to version
if ( Object.keys( excluded ).length ) {
version += " -" + Object.keys( excluded ).join( ",-" );
// set pkg.version to version with excludes, so minified file picks it up
grunt.config.set( "pkg.version", version );
}
// conditionally concatenate source
src.forEach(function( filepath ) {
var flag = filepath.flag,
specified = false,
omit = false,
messages = [];
if ( flag ) {
if ( excluded[ flag ] !== undefined ) {
messages.push([
( "Excluding " + flag ).red,
( "(" + filepath.src + ")" ).grey
]);
specified = true;
omit = !filepath.alt;
if ( !omit ) {
flag += " alternate";
filepath.src = filepath.alt;
}
}
if ( excluded[ flag ] === undefined ) {
messages.push([
( "Including " + flag ).green,
( "(" + filepath.src + ")" ).grey
]);
// If this module was actually specified by the
// builder, then set the flag to include it in the
// output list
if ( modules[ "+" + flag ] ) {
specified = true;
}
}
filepath = filepath.src;
// Only display the inclusion/exclusion list when handling
// an explicit list.
//
// Additionally, only display modules that have been specified
// by the user
if ( explicit && specified ) {
messages.forEach(function( message ) {
grunt.log.writetableln( [ 27, 30 ], message );
});
}
}
if ( !omit ) {
compiled += grunt.file.read( filepath );
}
});
// Embed Version
// Embed Date
compiled = compiled.replace( /@VERSION/g, version )
// 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 );
// Fail task if errors were logged.
if ( this.errorCount ) {
return false;
}
// Otherwise, print a success message.
grunt.log.writeln( "File '" + name + "' created." );
});
// Process files for distribution
grunt.registerTask( "dist", function() {
var stored, flags, paths, fs, nonascii;
// Check for stored destination paths
// ( set in dist/.destination.json )
stored = Object.keys( grunt.config( "dst" ) );
// Allow command line input as well
flags = Object.keys( this.flags );
// Combine all output target paths
paths = [].concat( stored, flags ).filter(function( path ) {
return path !== "*";
});
// Ensure the dist files are pure ASCII
fs = require( "fs" );
nonascii = false;
distpaths.forEach(function( filename ) {
var i, c,
text = fs.readFileSync( filename, "utf8" );
// Ensure files use only \n for line endings, not \r\n
if ( /\x0d\x0a/.test( text ) ) {
grunt.log.writeln( filename + ": Incorrect line endings (\\r\\n)" );
nonascii = true;
}
// Ensure only ASCII chars so script tags don't need a charset attribute
if ( text.length !== Buffer.byteLength( text, "utf8" ) ) {
grunt.log.writeln( filename + ": Non-ASCII characters detected:" );
for ( i = 0; i < text.length; i++ ) {
c = text.charCodeAt( i );
if ( c > 127 ) {
grunt.log.writeln( "- position " + i + ": " + c );
grunt.log.writeln( "-- " + text.substring( i - 20, i + 20 ) );
break;
}
}
nonascii = true;
}
// Modify map/min so that it points to files in the same folder;
// see https://github.com/mishoo/UglifyJS2/issues/47
if ( /\.map$/.test( filename ) ) {
text = text.replace( /"dist\//g, "\"" );
fs.writeFileSync( filename, text, "utf-8" );
// Use our hard-coded sourceMap directive instead of the autogenerated one (#13274; #13776)
} else if ( /\.min\.js$/.test( filename ) ) {
i = 0;
text = text.replace( /(?:\/\*|)\n?\/\/@\s*sourceMappingURL=.*(\n\*\/|)/g,
function( match ) {
if ( i++ ) {
return "";
}
return match;
});
fs.writeFileSync( filename, text, "utf-8" );
}
// Optionally copy dist files to other locations
paths.forEach(function( path ) {
var created;
if ( !/\/$/.test( path ) ) {
path += "/";
}
created = path + filename.replace( "dist/", "" );
grunt.file.write( created, text );
grunt.log.writeln( "File '" + created + "' created." );
});
});
return !nonascii;
});
// Work around grunt-contrib-uglify sourceMap issues (jQuery #13776)
grunt.registerMultiTask( "pre-uglify", function() {
var banner = this.options().banner;
this.files.forEach(function( mapping ) {
// Join src
var input = mapping.src.map(function( file ) {
var contents = grunt.file.read( file );
// Strip banners
return contents
// Remove the main jQuery banner, it'll be replaced by the new banner anyway.
.replace( /^\/\*!(?:.|\n)*?\*\/\n?/g, "" )
// Strip other banners preserving line count.
.replace( /^\/\*!(?:.|\n)*?\*\/\n?/gm, function ( match ) {
return match.replace( /[^\n]/gm, "" );
});
}).join("\n");
// Write temp file (with optional banner)
grunt.file.write( mapping.dest, ( banner || "" ) + input );
});
});
// Change the map file to point back to jquery.js instead of jquery.pre-min.js.
// The problem is caused by the pre-uglify task.
// Also, remove temporary files.
grunt.registerMultiTask( "post-uglify", function() {
var fs = require( "fs" );
this.files.forEach(function( mapping ) {
var mapFileName = mapping.src[ 0 ];
// Rename the file to a temporary name.
fs.renameSync( mapFileName, mapping.dest);
grunt.file.write( mapFileName, grunt.file.read( mapping.dest )
// The uglify task erroneously prepends dist/ to file names.
.replace( /"dist\//g, "\"" )
// Refer to the source jquery.js, not the temporary jquery.pre-min.js.
.replace( /\.pre-min\./g, "." )
// There's already a pragma at the beginning of the file, remove the one at the end.
.replace( /\/\/@ sourceMappingURL=jquery\.min\.map$/g, "" ));
});
// Remove temporary files.
this.options().tempFiles.forEach(function( fileName ) {
fs.unlink( fileName );
});
});
// Load grunt tasks from NPM packages
grunt.loadNpmTasks("grunt-compare-size");
grunt.loadNpmTasks("grunt-git-authors");
grunt.loadNpmTasks("grunt-update-submodules");
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-contrib-jshint");
grunt.loadNpmTasks("grunt-contrib-uglify");
// Default grunt
grunt.registerTask( "default", [ "update_submodules", "selector", "build:*:*", "jshint", "pre-uglify", "uglify", "post-uglify", "dist:*", "compare_size" ] );
// Short list as a high frequency watch task
grunt.registerTask( "dev", [ "selector", "build:*:*", "jshint" ] );
};