Gruntfile.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. module.exports = function( grunt ) {
  2. "use strict";
  3. var distpaths = [
  4. "dist/jquery.js",
  5. "dist/jquery.min.map",
  6. "dist/jquery.min.js"
  7. ],
  8. gzip = require("gzip-js"),
  9. readOptionalJSON = function( filepath ) {
  10. var data = {};
  11. try {
  12. data = grunt.file.readJSON( filepath );
  13. } catch(e) {}
  14. return data;
  15. },
  16. srcHintOptions = readOptionalJSON("src/.jshintrc");
  17. // The concatenated file won't pass onevar
  18. // But our modules can
  19. delete srcHintOptions.onevar;
  20. grunt.initConfig({
  21. pkg: grunt.file.readJSON("package.json"),
  22. dst: readOptionalJSON("dist/.destination.json"),
  23. compare_size: {
  24. files: [ "dist/jquery.js", "dist/jquery.min.js" ],
  25. options: {
  26. compress: {
  27. gz: function( contents ) {
  28. return gzip.zip( contents, {} ).length;
  29. }
  30. },
  31. cache: "dist/.sizecache.json"
  32. }
  33. },
  34. selector: {
  35. destFile: "src/selector-sizzle.js",
  36. apiFile: "src/sizzle-jquery.js",
  37. srcFile: "src/sizzle/dist/sizzle.js"
  38. },
  39. build: {
  40. all: {
  41. dest: "dist/jquery.js",
  42. src: [
  43. "src/intro.js",
  44. "src/core.js",
  45. { flag: "sizzle", src: "src/selector-sizzle.js", alt: "src/selector-native.js" },
  46. "src/callbacks.js",
  47. "src/deferred.js",
  48. "src/support.js",
  49. "src/data.js",
  50. "src/queue.js",
  51. "src/attributes.js",
  52. "src/event.js",
  53. "src/traversing.js",
  54. "src/manipulation.js",
  55. { flag: "wrap", src: "src/wrap.js" },
  56. { flag: "css", src: "src/css.js" },
  57. "src/serialize.js",
  58. { flag: "event-alias", src: "src/event-alias.js" },
  59. { flag: "ajax", src: "src/ajax.js" },
  60. { flag: "ajax/script", src: "src/ajax/script.js", needs: ["ajax"] },
  61. { flag: "ajax/jsonp", src: "src/ajax/jsonp.js", needs: [ "ajax", "ajax/script" ] },
  62. { flag: "ajax/xhr", src: "src/ajax/xhr.js", needs: ["ajax"] },
  63. { flag: "effects", src: "src/effects.js", needs: ["css"] },
  64. { flag: "offset", src: "src/offset.js", needs: ["css"] },
  65. { flag: "dimensions", src: "src/dimensions.js", needs: ["css"] },
  66. { flag: "deprecated", src: "src/deprecated.js" },
  67. "src/exports.js",
  68. "src/outro.js"
  69. ]
  70. }
  71. },
  72. jshint: {
  73. dist: {
  74. src: [ "dist/jquery.js" ],
  75. options: srcHintOptions
  76. },
  77. grunt: {
  78. src: [ "Gruntfile.js" ],
  79. options: {
  80. jshintrc: ".jshintrc"
  81. }
  82. },
  83. tests: {
  84. // TODO: Once .jshintignore is supported, use that instead.
  85. // issue located here: https://github.com/gruntjs/grunt-contrib-jshint/issues/1
  86. src: [ "test/data/{test,testinit,testrunner}.js", "test/unit/**/*.js" ],
  87. options: {
  88. jshintrc: "test/.jshintrc"
  89. }
  90. }
  91. },
  92. testswarm: {
  93. tests: "ajax attributes callbacks core css data deferred dimensions effects event manipulation offset queue selector serialize support traversing Sizzle".split(" ")
  94. },
  95. watch: {
  96. files: [ "<%= jshint.grunt.src %>", "<%= jshint.tests.src %>", "src/**/*.js" ],
  97. tasks: "dev"
  98. },
  99. "pre-uglify": {
  100. all: {
  101. files: {
  102. "dist/jquery.pre-min.js": [ "dist/jquery.js" ]
  103. },
  104. options: {
  105. banner: "\n\n\n\n\n\n\n\n\n\n" + // banner line size must be preserved
  106. "/*! jQuery v<%= pkg.version %> | " +
  107. "(c) 2005, 2013 jQuery Foundation, Inc. | " +
  108. "jquery.org/license\n" +
  109. "//@ sourceMappingURL=jquery.min.map\n" +
  110. "*/\n"
  111. }
  112. }
  113. },
  114. uglify: {
  115. all: {
  116. files: {
  117. "dist/jquery.min.js": [ "dist/jquery.pre-min.js" ]
  118. },
  119. options: {
  120. // Keep our hard-coded banner
  121. preserveComments: "some",
  122. sourceMap: "dist/jquery.min.map",
  123. sourceMappingURL: "jquery.min.map",
  124. report: "min",
  125. beautify: {
  126. ascii_only: true
  127. },
  128. compress: {
  129. hoist_funs: false,
  130. join_vars: false,
  131. loops: false,
  132. unused: false
  133. },
  134. mangle: {
  135. // saves some bytes when gzipped
  136. except: [ "undefined" ]
  137. }
  138. }
  139. }
  140. },
  141. "post-uglify": {
  142. all: {
  143. files: {
  144. "dist/jquery.min.map.tmp": [ "dist/jquery.min.map" ],
  145. "dist/jquery.min.js.tmp": [ "dist/jquery.min.js" ]
  146. },
  147. options: {
  148. tempFiles: [ "dist/jquery.min.map.tmp", "dist/jquery.min.js.tmp", "dist/jquery.pre-min.js" ]
  149. }
  150. }
  151. }
  152. });
  153. grunt.registerTask( "testswarm", function( commit, configFile ) {
  154. var jobName,
  155. testswarm = require( "testswarm" ),
  156. runs = {},
  157. done = this.async(),
  158. pull = /PR-(\d+)/.exec( commit ),
  159. config = grunt.file.readJSON( configFile ).jquery,
  160. tests = grunt.config([ this.name, "tests" ]);
  161. if ( pull ) {
  162. jobName = "jQuery pull <a href='https://github.com/jquery/jquery/pull/" +
  163. pull[ 1 ] + "'>#" + pull[ 1 ] + "</a>";
  164. } else {
  165. jobName = "jQuery commit #<a href='https://github.com/jquery/jquery/commit/" +
  166. commit + "'>" + commit.substr( 0, 10 ) + "</a>";
  167. }
  168. tests.forEach(function( test ) {
  169. runs[test] = config.testUrl + commit + "/test/index.html?module=" + test;
  170. });
  171. // TODO: create separate job for git/git2 so we can do different browsersets
  172. testswarm.createClient( {
  173. url: config.swarmUrl,
  174. pollInterval: 10000,
  175. timeout: 1000 * 60 * 30
  176. } )
  177. .addReporter( testswarm.reporters.cli )
  178. .auth( {
  179. id: config.authUsername,
  180. token: config.authToken
  181. })
  182. .addjob(
  183. {
  184. name: jobName,
  185. runs: runs,
  186. runMax: config.runMax,
  187. browserSets: "popular-no-old-ie"
  188. }, function( err, passed ) {
  189. if ( err ) {
  190. grunt.log.error( err );
  191. }
  192. done( passed );
  193. }
  194. );
  195. });
  196. grunt.registerTask( "selector", "Build Sizzle-based selector module", function() {
  197. var cfg = grunt.config("selector"),
  198. name = cfg.destFile,
  199. sizzle = {
  200. api: grunt.file.read( cfg.apiFile ),
  201. src: grunt.file.read( cfg.srcFile )
  202. },
  203. compiled, parts;
  204. /**
  205. sizzle-jquery.js -> sizzle between "EXPOSE" blocks,
  206. replace define & window.Sizzle assignment
  207. // EXPOSE
  208. if ( typeof define === "function" && define.amd ) {
  209. define(function() { return Sizzle; });
  210. } else {
  211. window.Sizzle = Sizzle;
  212. }
  213. // EXPOSE
  214. Becomes...
  215. Sizzle.attr = jQuery.attr;
  216. jQuery.find = Sizzle;
  217. jQuery.expr = Sizzle.selectors;
  218. jQuery.expr[":"] = jQuery.expr.pseudos;
  219. jQuery.unique = Sizzle.uniqueSort;
  220. jQuery.text = Sizzle.getText;
  221. jQuery.isXMLDoc = Sizzle.isXML;
  222. jQuery.contains = Sizzle.contains;
  223. */
  224. // Break into 3 pieces
  225. parts = sizzle.src.split("// EXPOSE");
  226. // Replace the if/else block with api
  227. parts[1] = sizzle.api;
  228. // Rejoin the pieces
  229. compiled = parts.join("");
  230. grunt.verbose.writeln("Injected " + cfg.apiFile + " into " + cfg.srcFile);
  231. // Write concatenated source to file, and ensure newline-only termination
  232. grunt.file.write( name, compiled.replace( /\x0d\x0a/g, "\x0a" ) );
  233. // Fail task if errors were logged.
  234. if ( this.errorCount ) {
  235. return false;
  236. }
  237. // Otherwise, print a success message.
  238. grunt.log.writeln( "File '" + name + "' created." );
  239. });
  240. // Special "alias" task to make custom build creation less grawlix-y
  241. grunt.registerTask( "custom", function() {
  242. var done = this.async(),
  243. args = [].slice.call(arguments),
  244. modules = args.length ? args[0].replace(/,/g, ":") : "";
  245. // Translation example
  246. //
  247. // grunt custom:+ajax,-dimensions,-effects,-offset
  248. //
  249. // Becomes:
  250. //
  251. // grunt build:*:*:+ajax:-dimensions:-effects:-offset
  252. grunt.log.writeln( "Creating custom build...\n" );
  253. grunt.util.spawn({
  254. grunt: true,
  255. args: [ "build:*:*:" + modules, "pre-uglify", "uglify", "dist" ]
  256. }, function( err, result ) {
  257. if ( err ) {
  258. grunt.verbose.error();
  259. done( err );
  260. return;
  261. }
  262. grunt.log.writeln( result.stdout.replace("Done, without errors.", "") );
  263. done();
  264. });
  265. });
  266. // Special concat/build task to handle various jQuery build requirements
  267. grunt.registerMultiTask(
  268. "build",
  269. "Concatenate source (include/exclude modules with +/- flags), embed date/version",
  270. function() {
  271. // Concat specified files.
  272. var compiled = "",
  273. modules = this.flags,
  274. optIn = !modules["*"],
  275. explicit = optIn || Object.keys(modules).length > 1,
  276. name = this.data.dest,
  277. src = this.data.src,
  278. deps = {},
  279. excluded = {},
  280. version = grunt.config( "pkg.version" ),
  281. excluder = function( flag, needsFlag ) {
  282. // optIn defaults implicit behavior to weak exclusion
  283. if ( optIn && !modules[ flag ] && !modules[ "+" + flag ] ) {
  284. excluded[ flag ] = false;
  285. }
  286. // explicit or inherited strong exclusion
  287. if ( excluded[ needsFlag ] || modules[ "-" + flag ] ) {
  288. excluded[ flag ] = true;
  289. // explicit inclusion overrides weak exclusion
  290. } else if ( excluded[ needsFlag ] === false &&
  291. ( modules[ flag ] || modules[ "+" + flag ] ) ) {
  292. delete excluded[ needsFlag ];
  293. // ...all the way down
  294. if ( deps[ needsFlag ] ) {
  295. deps[ needsFlag ].forEach(function( subDep ) {
  296. modules[ needsFlag ] = true;
  297. excluder( needsFlag, subDep );
  298. });
  299. }
  300. }
  301. };
  302. // append commit id to version
  303. if ( process.env.COMMIT ) {
  304. version += " " + process.env.COMMIT;
  305. }
  306. // figure out which files to exclude based on these rules in this order:
  307. // dependency explicit exclude
  308. // > explicit exclude
  309. // > explicit include
  310. // > dependency implicit exclude
  311. // > implicit exclude
  312. // examples:
  313. // * none (implicit exclude)
  314. // *:* all (implicit include)
  315. // *:*:-css all except css and dependents (explicit > implicit)
  316. // *:*:-css:+effects same (excludes effects because explicit include is trumped by explicit exclude of dependency)
  317. // *:+effects none except effects and its dependencies (explicit include trumps implicit exclude of dependency)
  318. src.forEach(function( filepath ) {
  319. var flag = filepath.flag;
  320. if ( flag ) {
  321. excluder(flag);
  322. // check for dependencies
  323. if ( filepath.needs ) {
  324. deps[ flag ] = filepath.needs;
  325. filepath.needs.forEach(function( needsFlag ) {
  326. excluder( flag, needsFlag );
  327. });
  328. }
  329. }
  330. });
  331. // append excluded modules to version
  332. if ( Object.keys( excluded ).length ) {
  333. version += " -" + Object.keys( excluded ).join( ",-" );
  334. // set pkg.version to version with excludes, so minified file picks it up
  335. grunt.config.set( "pkg.version", version );
  336. }
  337. // conditionally concatenate source
  338. src.forEach(function( filepath ) {
  339. var flag = filepath.flag,
  340. specified = false,
  341. omit = false,
  342. messages = [];
  343. if ( flag ) {
  344. if ( excluded[ flag ] !== undefined ) {
  345. messages.push([
  346. ( "Excluding " + flag ).red,
  347. ( "(" + filepath.src + ")" ).grey
  348. ]);
  349. specified = true;
  350. omit = !filepath.alt;
  351. if ( !omit ) {
  352. flag += " alternate";
  353. filepath.src = filepath.alt;
  354. }
  355. }
  356. if ( excluded[ flag ] === undefined ) {
  357. messages.push([
  358. ( "Including " + flag ).green,
  359. ( "(" + filepath.src + ")" ).grey
  360. ]);
  361. // If this module was actually specified by the
  362. // builder, then set the flag to include it in the
  363. // output list
  364. if ( modules[ "+" + flag ] ) {
  365. specified = true;
  366. }
  367. }
  368. filepath = filepath.src;
  369. // Only display the inclusion/exclusion list when handling
  370. // an explicit list.
  371. //
  372. // Additionally, only display modules that have been specified
  373. // by the user
  374. if ( explicit && specified ) {
  375. messages.forEach(function( message ) {
  376. grunt.log.writetableln( [ 27, 30 ], message );
  377. });
  378. }
  379. }
  380. if ( !omit ) {
  381. compiled += grunt.file.read( filepath );
  382. }
  383. });
  384. // Embed Version
  385. // Embed Date
  386. compiled = compiled.replace( /@VERSION/g, version )
  387. // yyyy-mm-ddThh:mmZ
  388. .replace( /@DATE/g, ( new Date() ).toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );
  389. // Write concatenated source to file
  390. grunt.file.write( name, compiled );
  391. // Fail task if errors were logged.
  392. if ( this.errorCount ) {
  393. return false;
  394. }
  395. // Otherwise, print a success message.
  396. grunt.log.writeln( "File '" + name + "' created." );
  397. });
  398. // Process files for distribution
  399. grunt.registerTask( "dist", function() {
  400. var stored, flags, paths, fs, nonascii;
  401. // Check for stored destination paths
  402. // ( set in dist/.destination.json )
  403. stored = Object.keys( grunt.config( "dst" ) );
  404. // Allow command line input as well
  405. flags = Object.keys( this.flags );
  406. // Combine all output target paths
  407. paths = [].concat( stored, flags ).filter(function( path ) {
  408. return path !== "*";
  409. });
  410. // Ensure the dist files are pure ASCII
  411. fs = require( "fs" );
  412. nonascii = false;
  413. distpaths.forEach(function( filename ) {
  414. var i, c,
  415. text = fs.readFileSync( filename, "utf8" );
  416. // Ensure files use only \n for line endings, not \r\n
  417. if ( /\x0d\x0a/.test( text ) ) {
  418. grunt.log.writeln( filename + ": Incorrect line endings (\\r\\n)" );
  419. nonascii = true;
  420. }
  421. // Ensure only ASCII chars so script tags don't need a charset attribute
  422. if ( text.length !== Buffer.byteLength( text, "utf8" ) ) {
  423. grunt.log.writeln( filename + ": Non-ASCII characters detected:" );
  424. for ( i = 0; i < text.length; i++ ) {
  425. c = text.charCodeAt( i );
  426. if ( c > 127 ) {
  427. grunt.log.writeln( "- position " + i + ": " + c );
  428. grunt.log.writeln( "-- " + text.substring( i - 20, i + 20 ) );
  429. break;
  430. }
  431. }
  432. nonascii = true;
  433. }
  434. // Modify map/min so that it points to files in the same folder;
  435. // see https://github.com/mishoo/UglifyJS2/issues/47
  436. if ( /\.map$/.test( filename ) ) {
  437. text = text.replace( /"dist\//g, "\"" );
  438. fs.writeFileSync( filename, text, "utf-8" );
  439. // Use our hard-coded sourceMap directive instead of the autogenerated one (#13274; #13776)
  440. } else if ( /\.min\.js$/.test( filename ) ) {
  441. i = 0;
  442. text = text.replace( /(?:\/\*|)\n?\/\/@\s*sourceMappingURL=.*(\n\*\/|)/g,
  443. function( match ) {
  444. if ( i++ ) {
  445. return "";
  446. }
  447. return match;
  448. });
  449. fs.writeFileSync( filename, text, "utf-8" );
  450. }
  451. // Optionally copy dist files to other locations
  452. paths.forEach(function( path ) {
  453. var created;
  454. if ( !/\/$/.test( path ) ) {
  455. path += "/";
  456. }
  457. created = path + filename.replace( "dist/", "" );
  458. grunt.file.write( created, text );
  459. grunt.log.writeln( "File '" + created + "' created." );
  460. });
  461. });
  462. return !nonascii;
  463. });
  464. // Work around grunt-contrib-uglify sourceMap issues (jQuery #13776)
  465. grunt.registerMultiTask( "pre-uglify", function() {
  466. var banner = this.options().banner;
  467. this.files.forEach(function( mapping ) {
  468. // Join src
  469. var input = mapping.src.map(function( file ) {
  470. var contents = grunt.file.read( file );
  471. // Strip banners
  472. return contents
  473. // Remove the main jQuery banner, it'll be replaced by the new banner anyway.
  474. .replace( /^\/\*!(?:.|\n)*?\*\/\n?/g, "" )
  475. // Strip other banners preserving line count.
  476. .replace( /^\/\*!(?:.|\n)*?\*\/\n?/gm, function ( match ) {
  477. return match.replace( /[^\n]/gm, "" );
  478. });
  479. }).join("\n");
  480. // Write temp file (with optional banner)
  481. grunt.file.write( mapping.dest, ( banner || "" ) + input );
  482. });
  483. });
  484. // Change the map file to point back to jquery.js instead of jquery.pre-min.js.
  485. // The problem is caused by the pre-uglify task.
  486. // Also, remove temporary files.
  487. grunt.registerMultiTask( "post-uglify", function() {
  488. var fs = require( "fs" );
  489. this.files.forEach(function( mapping ) {
  490. var mapFileName = mapping.src[ 0 ];
  491. // Rename the file to a temporary name.
  492. fs.renameSync( mapFileName, mapping.dest);
  493. grunt.file.write( mapFileName, grunt.file.read( mapping.dest )
  494. // The uglify task erroneously prepends dist/ to file names.
  495. .replace( /"dist\//g, "\"" )
  496. // Refer to the source jquery.js, not the temporary jquery.pre-min.js.
  497. .replace( /\.pre-min\./g, "." )
  498. // There's already a pragma at the beginning of the file, remove the one at the end.
  499. .replace( /\/\/@ sourceMappingURL=jquery\.min\.map$/g, "" ));
  500. });
  501. // Remove temporary files.
  502. this.options().tempFiles.forEach(function( fileName ) {
  503. fs.unlink( fileName );
  504. });
  505. });
  506. // Load grunt tasks from NPM packages
  507. grunt.loadNpmTasks("grunt-compare-size");
  508. grunt.loadNpmTasks("grunt-git-authors");
  509. grunt.loadNpmTasks("grunt-update-submodules");
  510. grunt.loadNpmTasks("grunt-contrib-watch");
  511. grunt.loadNpmTasks("grunt-contrib-jshint");
  512. grunt.loadNpmTasks("grunt-contrib-uglify");
  513. // Default grunt
  514. grunt.registerTask( "default", [ "update_submodules", "selector", "build:*:*", "jshint", "pre-uglify", "uglify", "post-uglify", "dist:*", "compare_size" ] );
  515. // Short list as a high frequency watch task
  516. grunt.registerTask( "dev", [ "selector", "build:*:*", "jshint" ] );
  517. };