Gruntfile.js 14.2 KB
Newer Older
Chris Rebert's avatar
Chris Rebert committed
1
2
3
4
5
6
/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
 * Copyright 2013-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */
7

Chris Rebert's avatar
Chris Rebert committed
8
module.exports = function (grunt) {
XhmikosR's avatar
XhmikosR committed
9
  'use strict';
10

11
12
13
  // Force use of Unix newlines
  grunt.util.linefeed = '\n';

Zlatan Vasović's avatar
Zlatan Vasović committed
14
  RegExp.quote = function (string) {
Chris Rebert's avatar
Chris Rebert committed
15
16
    return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
  };
17

Chris Rebert's avatar
Chris Rebert committed
18
  var fs = require('fs');
19
  var path = require('path');
20
  var glob = require('glob');
21
  var npmShrinkwrap = require('npm-shrinkwrap');
22
  var mq4HoverShim = require('mq4-hover-shim');
Mark Otto's avatar
Mark Otto committed
23

24
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
25
26
27
28
29
30
31
  var configBridge = grunt.file.readJSON('./grunt/configBridge.json', { encoding: 'utf8' });

  Object.keys(configBridge.paths).forEach(function (key) {
    configBridge.paths[key].forEach(function (val, i, arr) {
      arr[i] = path.join('./docs/assets', val);
    });
  });
32

33
34
35
36
37
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
38
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
39
40
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
41
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
42
            ' */\n',
43
44
45
46
47
48
49
50
51
    jqueryCheck: 'if (typeof jQuery === \'undefined\') {\n' +
                 '  throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery\')\n' +
                 '}\n',
    jqueryVersionCheck: '+function ($) {\n' +
                        '  var version = $.fn.jquery.split(\' \')[0].split(\'.\')\n' +
                        '  if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) {\n' +
                        '    throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery version 1.9.1 or higher\')\n' +
                        '  }\n' +
                        '}(jQuery);\n\n',
52
53
54

    // Task configuration.
    clean: {
55
56
      dist: 'dist',
      docs: 'docs/dist'
57
58
    },

59
60
61
62
63
64
65
66
67
68
69
70
71
    // JS build configuration

    lineremover: {
      es6Import: {
        files: {
          '<%= concat.bootstrap.dest %>': '<%= concat.bootstrap.dest %>'
        },
        options: {
          exclusionPattern: /^(import|export)/g
        }
      }
    },

fat's avatar
fat committed
72
    babel: {
73
74
75
76
77
      src: {
        options: {
          sourceMap: true,
          modules: 'ignore'
        },
fat's avatar
fat committed
78
        files: {
fat's avatar
fat committed
79
80
81
82
83
84
85
          'js/dist/util.js'      : 'js/src/util.js',
          'js/dist/alert.js'     : 'js/src/alert.js',
          'js/dist/button.js'    : 'js/src/button.js',
          'js/dist/carousel.js'  : 'js/src/carousel.js',
          'js/dist/collapse.js'  : 'js/src/collapse.js',
          'js/dist/dropdown.js'  : 'js/src/dropdown.js',
          'js/dist/modal.js'     : 'js/src/modal.js',
fat's avatar
tab es6    
fat committed
86
          'js/dist/scrollspy.js' : 'js/src/scrollspy.js',
87
          'js/dist/tab.js'       : 'js/src/tab.js',
fat's avatar
fat committed
88
89
          'js/dist/tooltip.js'   : 'js/src/tooltip.js',
          'js/dist/popover.js'   : 'js/src/popover.js'
fat's avatar
fat committed
90
        }
91
92
93
94
95
96
97
98
      },
      dist: {
        options: {
          modules: 'ignore'
        },
        files: {
          '<%= concat.bootstrap.dest %>' : '<%= concat.bootstrap.dest %>'
        }
99
100
      }
    },
101

Chris Rebert's avatar
Chris Rebert committed
102
103
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
104
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
105
      },
Chris Rebert's avatar
Chris Rebert committed
106
      grunt: {
fat's avatar
fat committed
107
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
108
      },
109
      core: {
fat's avatar
fat committed
110
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
111
112
      },
      test: {
fat's avatar
fat committed
113
        src: 'js/tests/unit/*.js'
114
115
      },
      assets: {
116
117
118
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
119
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
120
121
122
      }
    },

123
124
125
126
127
128
129
130
131
132
133
134
    stamp: {
      options: {
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>\n+function ($) {\n',
        footer: '\n}(jQuery);'
      },
      bootstrap: {
        files: {
          src: '<%= concat.bootstrap.dest %>'
        }
      }
    },

135
136
137
138
139
140
    concat: {
      options: {
        stripBanners: false
      },
      bootstrap: {
        src: [
141
142
143
144
145
146
147
148
149
150
151
          'js/src/util.js',
          'js/src/alert.js',
          'js/src/button.js',
          'js/src/carousel.js',
          'js/src/collapse.js',
          'js/src/dropdown.js',
          'js/src/modal.js',
          'js/src/scrollspy.js',
          'js/src/tab.js',
          'js/src/tooltip.js',
          'js/src/popover.js'
152
153
154
155
        ],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    },
156

fat's avatar
fat committed
157
    uglify: {
158
      options: {
fat's avatar
fat committed
159
160
        compress: {
          warnings: false
fat's avatar
fat committed
161
        },
fat's avatar
fat committed
162
163
        mangle: true,
        preserveComments: 'some'
164
      },
fat's avatar
fat committed
165
166
      core: {
        src: '<%= concat.bootstrap.dest %>',
167
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
168
169
170
171
      },
      customize: {
        src: configBridge.paths.customizerJs,
        dest: 'docs/assets/js/customize.min.js'
XhmikosR's avatar
XhmikosR committed
172
      },
173
      docsJs: {
174
        src: configBridge.paths.docsJs,
175
        dest: 'docs/assets/js/docs.min.js'
176
177
178
      }
    },

Mark Otto's avatar
Mark Otto committed
179
180
181
182
183
184
185
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

186
187
188

    // CSS build configuration

Chris Rebert's avatar
Chris Rebert committed
189
190
191
192
193
194
195
196
    scsslint: {
      scss: ['scss/*.scss', '!scss/_normalize.scss'],
      options: {
        config: 'scss/.scss-lint.yml',
        reporterOutput: 'scss-lint-report.xml'
      }
    },

Chris Rebert's avatar
Chris Rebert committed
197
198
199
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
200
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
201
202
203
204
205
206
      },
      core: {
        src: 'dist/css/<%= pkg.name %>.css'
      }
    },

Bas Bosman's avatar
Bas Bosman committed
207
208
    autoprefixer: {
      options: {
209
210
211
        browsers: [
          'Android 2.3',
          'Android >= 4',
212
          'Chrome >= 35',
213
214
          'Firefox >= 31',
          'Explorer >= 9',
215
          'iOS >= 7',
216
          'Opera >= 12',
217
          'Safari >= 7.1'
218
        ]
Bas Bosman's avatar
Bas Bosman committed
219
220
221
222
223
224
225
226
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
227
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
228
229
230
231
232
233
234
235
236
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
237
    cssmin: {
XhmikosR's avatar
XhmikosR committed
238
      options: {
239
240
        // TODO: disable `zeroUnits` optimization once clean-css 3.2 is released
        //    and then simplify the fix for https://github.com/twbs/bootstrap/issues/14837 accordingly
241
        compatibility: 'ie8',
242
243
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
244
      },
Mark Otto's avatar
Mark Otto committed
245
246
247
248
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
249
      },
XhmikosR's avatar
XhmikosR committed
250
      docs: {
Mark Otto's avatar
Mark Otto committed
251
        src: 'docs/assets/css/docs.min.css',
252
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
253
254
255
      }
    },

256
    usebanner: {
XhmikosR's avatar
XhmikosR committed
257
258
259
260
261
262
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
263
264
265
      }
    },

266
    csscomb: {
267
      options: {
Mark Otto's avatar
Mark Otto committed
268
        config: 'scss/.csscomb.json'
269
270
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
271
272
273
274
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
275
276
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
277
278
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
279
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
280
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
281
282
      },
      docs: {
283
284
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
285
286
287
      }
    },

Mark Otto's avatar
Mark Otto committed
288
    copy: {
Mark Otto's avatar
Mark Otto committed
289
      docs: {
XhmikosR's avatar
XhmikosR committed
290
291
292
293
294
295
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
Mark Otto's avatar
Mark Otto committed
296
297
298
      }
    },

299
300
301
302
303
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
304
        }
305
306
307
      }
    },

308
    jekyll: {
309
310
311
312
313
314
315
316
317
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
318
319
    },

320
    htmllint: {
321
      options: {
322
323
324
325
326
327
328
        ignore: [
          'Element “img” is missing required attribute “src”.',
          'Bad value “X-UA-Compatible” for attribute “http-equiv” on element “meta”.',
          'Attribute “autocomplete” not allowed on element “input” at this point.',
          'Attribute “autocomplete” not allowed on element “button” at this point.',
          'Element “div” not allowed as child of element “progress” in this context. (Suppressing further errors from this subtree.)',
          'Consider using the “h1” element as a top-level heading only (all “h1” elements are treated as top-level headings by many screen readers and other tools).'
329
        ]
330
      },
331
      src: '_gh_pages/**/*.html'
332
333
    },

334
335
    watch: {
      src: {
fat's avatar
fat committed
336
337
        files: '<%= jscs.core.src %>',
        tasks: ['qunit', 'concat']
338
339
      },
      test: {
fat's avatar
fat committed
340
341
        files: '<%= jscs.test.src %>',
        tasks: ['qunit']
342
      },
343
344
345
      sass: {
        files: 'scss/**/*.scss',
        tasks: 'sass-compile'
Mark Otto's avatar
Mark Otto committed
346
347
      },
      docs: {
348
349
        files: 'docs/assets/scss/*.scss',
        tasks: 'sass:docs'
350
      }
351
352
353
354
355
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
356
357
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
358
359
360
361
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
362
363
364
365
366
367
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
368
          concurrency: 10,
369
          maxRetries: 3,
370
          maxPollRetries: 4,
371
          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
372
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
373
374
        }
      }
Chris Rebert's avatar
Chris Rebert committed
375
376
377
378
379
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
380
381
382
383
384
385
386
387
      },
      bundleUpdate: {
        command: function () {
          // Update dev gems and all the test gemsets
          return 'bundle update && ' + glob.sync('test-infra/gemfiles/*.gemfile').map(function (gemfile) {
            return 'BUNDLE_GEMFILE=' + gemfile + ' bundle update';
          }).join(' && ');
        }
Mark Otto's avatar
Mark Otto committed
388
      }
389
390
    }
  });
391
392


393
  // These plugins provide necessary tasks.
394
395
396
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies',
    // Exclude Sass compilers. We choose the one to load later on.
    pattern: ['grunt-*', '!grunt-sass', '!grunt-contrib-sass'] });
XhmikosR's avatar
XhmikosR committed
397
  require('time-grunt')(grunt);
398

399
  // Docs HTML validation task
400
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
401

402
403
404
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
405
406
407
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
408

409
  // Test task.
410
411
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
412
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
413
    // Skip core tests if this is a Savage build
414
415
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
416
417
  }
  // Skip HTML validation if running a different subset of the test suite
418
419
420
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
421
422
    testSubtasks.push('validate-html');
  }
423
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
424
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
425
      // Skip Sauce if running a different subset of the test suite
426
427
428
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
429
430
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
431
432
  }
  grunt.registerTask('test', testSubtasks);
fat's avatar
fat committed
433
  grunt.registerTask('test-js', ['jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
434

435
  // JS distribution task.
436
  grunt.registerTask('dist-js', ['concat', 'lineremover', 'babel:dist', 'stamp', 'uglify:core', 'commonjs']);
437

Chris Rebert's avatar
Chris Rebert committed
438
439
  grunt.registerTask('test-scss', ['scsslint:scss']);

440
  // CSS distribution task.
441
442
443
444
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
Mark Otto's avatar
Mark Otto committed
445
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
446

Chris Rebert's avatar
Chris Rebert committed
447
  grunt.registerTask('dist-css', ['sass-compile', 'postcss:core', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
448

449
  // Full distribution task.
Mark Otto's avatar
Mark Otto committed
450
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'dist-js']);
451

452
  // Default task.
Mark Otto's avatar
Mark Otto committed
453
  grunt.registerTask('default', ['clean:dist', 'test']);
454

455
456
457
  // Version numbering task.
  // grunt change-version-number --oldver=A.B.C --newver=X.Y.Z
  // This can be overzealous, so its changes should always be manually reviewed!
458
  grunt.registerTask('change-version-number', 'sed');
459

460
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
461
462
463
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
464
465
  });

466
467
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
468
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
469
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
470
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
471

472
473
  grunt.registerTask('docs-github', ['jekyll:github']);

474
475
476
477
478
479
480
  // Task for updating the cached npm packages used by the Travis build (which are controlled by test-infra/npm-shrinkwrap.json).
  // This task should be run and the updated file should be committed whenever Bootstrap's dependencies change.
  grunt.registerTask('update-shrinkwrap', ['exec:npmUpdate', '_update-shrinkwrap']);
  grunt.registerTask('_update-shrinkwrap', function () {
    var done = this.async();
    npmShrinkwrap({ dev: true, dirname: __dirname }, function (err) {
      if (err) {
481
        grunt.fail.warn(err);
482
483
484
485
486
487
488
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
489
490
491
  // Task for updating the cached RubyGem packages used by the Travis build (which are controlled by test-infra/Gemfile.lock).
  // This task should be run and the updated file should be committed whenever Bootstrap's RubyGem dependencies change.
  grunt.registerTask('update-gemfile-lock', ['exec:bundleUpdate']);
492
};