Gruntfile.js 15.5 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 MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\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
    // JS build configuration
    lineremover: {
      es6Import: {
        files: {
          '<%= concat.bootstrap.dest %>': '<%= concat.bootstrap.dest %>'
        },
65
        options: {
66
67
68
69
70
          exclusionPattern: /^(import|export)/g
        }
      }
    },

fat's avatar
fat committed
71
    babel: {
fat's avatar
fat committed
72
      dev: {
73
74
75
        options: {
          sourceMap: true,
          modules: 'ignore'
76
        },
fat's avatar
fat committed
77
        files: {
fat's avatar
fat committed
78
79
80
81
82
83
84
          '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
85
          'js/dist/scrollspy.js' : 'js/src/scrollspy.js',
86
          'js/dist/tab.js'       : 'js/src/tab.js',
fat's avatar
fat committed
87
88
          'js/dist/tooltip.js'   : 'js/src/tooltip.js',
          'js/dist/popover.js'   : 'js/src/popover.js'
fat's avatar
fat committed
89
        }
90
      },
91
      dist: {
XhmikosR's avatar
XhmikosR committed
92
        options: {
93
          modules: 'ignore'
XhmikosR's avatar
XhmikosR committed
94
        },
95
96
97
        files: {
          '<%= concat.bootstrap.dest %>' : '<%= concat.bootstrap.dest %>'
        }
98
      },
fat's avatar
fat committed
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
      umd: {
        options: {
          modules: 'umd'
        },
        files: {
          'dist/js/umd/util.js'      : 'js/src/util.js',
          'dist/js/umd/alert.js'     : 'js/src/alert.js',
          'dist/js/umd/button.js'    : 'js/src/button.js',
          'dist/js/umd/carousel.js'  : 'js/src/carousel.js',
          'dist/js/umd/collapse.js'  : 'js/src/collapse.js',
          'dist/js/umd/dropdown.js'  : 'js/src/dropdown.js',
          'dist/js/umd/modal.js'     : 'js/src/modal.js',
          'dist/js/umd/scrollspy.js' : 'js/src/scrollspy.js',
          'dist/js/umd/tab.js'       : 'js/src/tab.js',
          'dist/js/umd/tooltip.js'   : 'js/src/tooltip.js',
          'dist/js/umd/popover.js'   : 'js/src/popover.js'
        }
116
117
      }
    },
118

Jacob Thornton's avatar
Jacob Thornton committed
119
120
121
122
123
124
125
    eslint: {
      options: {
        configFile: 'js/.eslintrc'
      },
      target: 'js/src/*.js'
    },

Chris Rebert's avatar
Chris Rebert committed
126
127
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
128
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
129
      },
Chris Rebert's avatar
Chris Rebert committed
130
      grunt: {
fat's avatar
fat committed
131
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
132
      },
133
      core: {
fat's avatar
fat committed
134
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
135
136
      },
      test: {
fat's avatar
fat committed
137
        src: 'js/tests/unit/*.js'
138
139
      },
      assets: {
140
141
142
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
143
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
144
145
146
      }
    },

147
    stamp: {
148
      options: {
149
150
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>\n+function ($) {\n',
        footer: '\n}(jQuery);'
151
152
      },
      bootstrap: {
153
154
155
        files: {
          src: '<%= concat.bootstrap.dest %>'
        }
156
157
      }
    },
158

159
    concat: {
160
      options: {
161
        stripBanners: false
162
      },
163
      bootstrap: {
fat's avatar
fat committed
164
        src: [
165
166
167
168
169
170
171
172
173
174
175
          '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'
fat's avatar
fat committed
176
        ],
177
        dest: 'dist/js/<%= pkg.name %>.js'
fat's avatar
fat committed
178
179
180
181
182
      }
    },

    uglify: {
      options: {
fat's avatar
fat committed
183
184
        compress: {
          warnings: false
fat's avatar
fat committed
185
        },
fat's avatar
fat committed
186
        mangle: true,
fat's avatar
fat committed
187
        preserveComments: 'some'
XhmikosR's avatar
XhmikosR committed
188
      },
fat's avatar
fat committed
189
190
      core: {
        src: '<%= concat.bootstrap.dest %>',
191
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
192
193
194
195
      },
      customize: {
        src: configBridge.paths.customizerJs,
        dest: 'docs/assets/js/customize.min.js'
XhmikosR's avatar
XhmikosR committed
196
      },
197
      docsJs: {
198
        src: configBridge.paths.docsJs,
199
        dest: 'docs/assets/js/docs.min.js'
200
201
202
      }
    },

Mark Otto's avatar
Mark Otto committed
203
204
205
206
207
208
209
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

210
    // CSS build configuration
Chris Rebert's avatar
Chris Rebert committed
211
212
    scsslint: {
      options: {
Mark Otto's avatar
Mark Otto committed
213
214
215
216
        config: 'scss/.scsslint.yml',
        reporterOutput: null
      },
      src: ['scss/*.scss', '!scss/_normalize.scss']
Chris Rebert's avatar
Chris Rebert committed
217
218
    },

Chris Rebert's avatar
Chris Rebert committed
219
220
221
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
222
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
223
224
      },
      core: {
225
        src: 'dist/css/*.css'
Chris Rebert's avatar
Chris Rebert committed
226
227
228
      }
    },

Bas Bosman's avatar
Bas Bosman committed
229
230
    autoprefixer: {
      options: {
231
232
233
        browsers: [
          'Android 2.3',
          'Android >= 4',
234
          'Chrome >= 35',
235
236
          'Firefox >= 31',
          'Explorer >= 9',
237
          'iOS >= 7',
238
          'Opera >= 12',
239
          'Safari >= 7.1'
240
        ]
Bas Bosman's avatar
Bas Bosman committed
241
242
243
244
245
      },
      core: {
        options: {
          map: true
        },
246
        src: 'dist/css/*.css'
Bas Bosman's avatar
Bas Bosman committed
247
248
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
249
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
250
251
252
253
254
255
256
257
258
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
259
    cssmin: {
XhmikosR's avatar
XhmikosR committed
260
      options: {
261
262
        // 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
263
        compatibility: 'ie8',
264
        keepSpecialComments: '*',
265
        sourceMap: true,
266
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
267
      },
Mark Otto's avatar
Mark Otto committed
268
      core: {
269
270
271
272
273
274
275
276
277
        files: [
          {
            expand: true,
            cwd: 'dist/css',
            src: ['*.css', '!*.min.css'],
            dest: 'dist/css',
            ext: '.min.css'
          }
        ]
278
      },
XhmikosR's avatar
XhmikosR committed
279
      docs: {
Mark Otto's avatar
Mark Otto committed
280
        src: 'docs/assets/css/docs.min.css',
281
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
282
283
284
      }
    },

285
    usebanner: {
XhmikosR's avatar
XhmikosR committed
286
287
288
289
290
291
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
292
293
294
      }
    },

295
    csscomb: {
296
      options: {
Mark Otto's avatar
Mark Otto committed
297
        config: 'scss/.csscomb.json'
298
299
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
300
301
302
303
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
304
305
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
306
307
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
308
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
309
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
310
311
      },
      docs: {
312
313
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
314
315
316
      }
    },

Mark Otto's avatar
Mark Otto committed
317
    copy: {
Mark Otto's avatar
Mark Otto committed
318
      docs: {
XhmikosR's avatar
XhmikosR committed
319
320
321
322
323
324
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
Mark Otto's avatar
Mark Otto committed
325
326
327
      }
    },

328
329
330
331
332
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
333
        }
334
335
336
      }
    },

337
    jekyll: {
338
339
340
341
342
343
344
345
346
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
347
348
    },

349
    htmllint: {
350
      options: {
351
352
353
354
355
356
        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.)',
357
358
          '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).',
          'The “datetime” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.'
359
        ]
360
      },
361
      src: '_gh_pages/**/*.html'
362
363
    },

364
365
    watch: {
      src: {
fat's avatar
fat committed
366
        files: '<%= jscs.core.src %>',
fat's avatar
fat committed
367
        tasks: ['babel:dev']
368
      },
369
370
      sass: {
        files: 'scss/**/*.scss',
Mark Otto's avatar
Mark Otto committed
371
        tasks: ['dist-css', 'docs']
Mark Otto's avatar
Mark Otto committed
372
373
374
375
      },
      docs: {
        files: 'docs/assets/scss/**/*.scss',
        tasks: ['dist-css', 'docs']
376
      }
377
378
379
380
381
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
382
383
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
384
385
386
387
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
388
389
390
391
392
393
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
394
          concurrency: 10,
395
          maxRetries: 3,
396
          maxPollRetries: 4,
397
          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
398
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
399
400
        }
      }
Chris Rebert's avatar
Chris Rebert committed
401
402
403
404
405
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
406
407
408
409
410
411
412
413
      },
      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
414
      }
415
416
    }
  });
417
418


419
  // These plugins provide necessary tasks.
420
421
422
  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
423
  require('time-grunt')(grunt);
424

425
  // Docs HTML validation task
426
  grunt.registerTask('validate-html', ['jekyll:docs']);
427

428
429
430
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
431
432
433
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
434

435
  // Test task.
436
437
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
438
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
439
    // Skip core tests if this is a Savage build
440
441
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
442
443
  }
  // Skip HTML validation if running a different subset of the test suite
444
445
446
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
447
448
    testSubtasks.push('validate-html');
  }
449
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
450
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
451
      // Skip Sauce if running a different subset of the test suite
452
453
454
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
455
456
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
457
458
  }
  grunt.registerTask('test', testSubtasks);
Jacob Thornton's avatar
Jacob Thornton committed
459
  grunt.registerTask('test-js', ['eslint', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
460

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

Mark Otto's avatar
Mark Otto committed
464
  grunt.registerTask('test-scss', ['scsslint']);
Chris Rebert's avatar
Chris Rebert committed
465

466
  // CSS distribution task.
467
468
469
470
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
471
472
  // grunt.registerTask('sass-compile', ['sass:core', 'sass:extras', 'sass:docs']);
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
473

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

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

479
  // Default task.
Mark Otto's avatar
Mark Otto committed
480
  grunt.registerTask('default', ['clean:dist', 'test']);
481

482
483
484
  // 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!
485
  grunt.registerTask('change-version-number', 'sed');
486

fat's avatar
fat committed
487
488
489
490
491
492
  grunt.registerTask('commonjs', ['babel:umd', 'npm-js']);

  grunt.registerTask('npm-js', 'Generate npm-js entrypoint module in dist dir.', function () {
    var srcFiles = Object.keys(grunt.config.get('babel.umd.files')).map(function (filename) {
      return './' + path.join('umd', path.basename(filename))
    })
493
494
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
495
496
  });

497
498
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
499
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
500
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
501
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
502

503
  grunt.registerTask('prep-release', ['dist', 'docs', 'jekyll:github', 'htmlmin', 'compress']);
504

505
506
507
508
509
510
511
  // 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) {
512
        grunt.fail.warn(err);
513
514
515
516
517
518
519
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
520
521
522
  // 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']);
523
};