Gruntfile.js 15.4 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

Chris Rebert's avatar
Chris Rebert committed
119
120
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
121
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
122
      },
Chris Rebert's avatar
Chris Rebert committed
123
      grunt: {
fat's avatar
fat committed
124
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
125
      },
126
      core: {
fat's avatar
fat committed
127
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
128
129
      },
      test: {
fat's avatar
fat committed
130
        src: 'js/tests/unit/*.js'
131
132
      },
      assets: {
133
134
135
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
136
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
137
138
139
      }
    },

140
    stamp: {
141
      options: {
142
143
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>\n+function ($) {\n',
        footer: '\n}(jQuery);'
144
145
      },
      bootstrap: {
146
147
148
        files: {
          src: '<%= concat.bootstrap.dest %>'
        }
149
150
      }
    },
151

152
    concat: {
153
      options: {
154
        stripBanners: false
155
      },
156
      bootstrap: {
fat's avatar
fat committed
157
        src: [
158
159
160
161
162
163
164
165
166
167
168
          '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
169
        ],
170
        dest: 'dist/js/<%= pkg.name %>.js'
fat's avatar
fat committed
171
172
173
174
175
      }
    },

    uglify: {
      options: {
fat's avatar
fat committed
176
177
        compress: {
          warnings: false
fat's avatar
fat committed
178
        },
fat's avatar
fat committed
179
        mangle: true,
fat's avatar
fat committed
180
        preserveComments: 'some'
XhmikosR's avatar
XhmikosR committed
181
      },
fat's avatar
fat committed
182
183
      core: {
        src: '<%= concat.bootstrap.dest %>',
184
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
185
186
187
188
      },
      customize: {
        src: configBridge.paths.customizerJs,
        dest: 'docs/assets/js/customize.min.js'
XhmikosR's avatar
XhmikosR committed
189
      },
190
      docsJs: {
191
        src: configBridge.paths.docsJs,
192
        dest: 'docs/assets/js/docs.min.js'
193
194
195
      }
    },

Mark Otto's avatar
Mark Otto committed
196
197
198
199
200
201
202
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

203
    // CSS build configuration
Chris Rebert's avatar
Chris Rebert committed
204
205
    scsslint: {
      options: {
Mark Otto's avatar
Mark Otto committed
206
207
208
209
        config: 'scss/.scsslint.yml',
        reporterOutput: null
      },
      src: ['scss/*.scss', '!scss/_normalize.scss']
Chris Rebert's avatar
Chris Rebert committed
210
211
    },

Chris Rebert's avatar
Chris Rebert committed
212
213
214
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
215
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
216
217
      },
      core: {
218
        src: 'dist/css/*.css'
Chris Rebert's avatar
Chris Rebert committed
219
220
221
      }
    },

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

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

278
    usebanner: {
XhmikosR's avatar
XhmikosR committed
279
280
281
282
283
284
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
285
286
287
      }
    },

288
    csscomb: {
289
      options: {
Mark Otto's avatar
Mark Otto committed
290
        config: 'scss/.csscomb.json'
291
292
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
293
294
295
296
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
297
298
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
299
300
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
301
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
302
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
303
304
      },
      docs: {
305
306
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
307
308
309
      }
    },

Mark Otto's avatar
Mark Otto committed
310
    copy: {
Mark Otto's avatar
Mark Otto committed
311
      docs: {
XhmikosR's avatar
XhmikosR committed
312
313
314
315
316
317
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
Mark Otto's avatar
Mark Otto committed
318
319
320
      }
    },

321
322
323
324
325
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
326
        }
327
328
329
      }
    },

330
    jekyll: {
331
332
333
334
335
336
337
338
339
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
340
341
    },

342
    htmllint: {
343
      options: {
344
345
346
347
348
349
        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.)',
350
351
          '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.'
352
        ]
353
      },
354
      src: '_gh_pages/**/*.html'
355
356
    },

357
358
    watch: {
      src: {
fat's avatar
fat committed
359
        files: '<%= jscs.core.src %>',
fat's avatar
fat committed
360
        tasks: ['babel:dev']
361
      },
362
363
      sass: {
        files: 'scss/**/*.scss',
Mark Otto's avatar
Mark Otto committed
364
        tasks: ['sass-compile', 'postcss:core', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs', 'docs-css', 'clean:docs', 'copy:docs']
365
      }
366
367
368
369
370
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
371
372
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
373
374
375
376
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
377
378
379
380
381
382
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
383
          concurrency: 10,
384
          maxRetries: 3,
385
          maxPollRetries: 4,
386
          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
387
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
388
389
        }
      }
Chris Rebert's avatar
Chris Rebert committed
390
391
392
393
394
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
395
396
397
398
399
400
401
402
      },
      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
403
      }
404
405
    }
  });
406
407


408
  // These plugins provide necessary tasks.
409
410
411
  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
412
  require('time-grunt')(grunt);
413

414
  // Docs HTML validation task
415
  grunt.registerTask('validate-html', ['jekyll:docs']);
416

417
418
419
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
420
421
422
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
423

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

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

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

455
  // CSS distribution task.
456
457
458
459
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
460
461
  // grunt.registerTask('sass-compile', ['sass:core', 'sass:extras', 'sass:docs']);
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
462

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

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

468
  // Default task.
Mark Otto's avatar
Mark Otto committed
469
  grunt.registerTask('default', ['clean:dist', 'test']);
470

471
472
473
  // 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!
474
  grunt.registerTask('change-version-number', 'sed');
475

fat's avatar
fat committed
476
477
478
479
480
481
  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))
    })
482
483
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
484
485
  });

486
487
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
488
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
489
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
490
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
491

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

494
495
496
497
498
499
500
  // 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) {
501
        grunt.fail.warn(err);
502
503
504
505
506
507
508
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
509
510
511
  // 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']);
512
};