Gruntfile.js 15.3 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
    // JS build configuration

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

fat's avatar
fat committed
72
    babel: {
fat's avatar
fat committed
73
      dev: {
74
75
76
        options: {
          sourceMap: true,
          modules: 'ignore'
77
        },
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
      dist: {
XhmikosR's avatar
XhmikosR committed
93
        options: {
94
          modules: 'ignore'
XhmikosR's avatar
XhmikosR committed
95
        },
96
97
98
        files: {
          '<%= concat.bootstrap.dest %>' : '<%= concat.bootstrap.dest %>'
        }
99
      },
fat's avatar
fat committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
      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'
        }
117
118
      }
    },
119

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

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

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

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

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

204
205
206

    // CSS build configuration

Chris Rebert's avatar
Chris Rebert committed
207
208
209
210
211
212
213
214
    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
215
216
217
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
218
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
219
220
221
222
223
224
      },
      core: {
        src: 'dist/css/<%= pkg.name %>.css'
      }
    },

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

XhmikosR's avatar
XhmikosR committed
255
    cssmin: {
XhmikosR's avatar
XhmikosR committed
256
      options: {
257
258
        // 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
259
        compatibility: 'ie8',
260
261
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
262
      },
Mark Otto's avatar
Mark Otto committed
263
264
265
266
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
267
      },
XhmikosR's avatar
XhmikosR committed
268
      docs: {
Mark Otto's avatar
Mark Otto committed
269
        src: 'docs/assets/css/docs.min.css',
270
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
271
272
273
      }
    },

274
    usebanner: {
XhmikosR's avatar
XhmikosR committed
275
276
277
278
279
280
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
281
282
283
      }
    },

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

Mark Otto's avatar
Mark Otto committed
306
    copy: {
Mark Otto's avatar
Mark Otto committed
307
      docs: {
XhmikosR's avatar
XhmikosR committed
308
309
310
311
312
313
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
Mark Otto's avatar
Mark Otto committed
314
315
316
      }
    },

317
318
319
320
321
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
322
        }
323
324
325
      }
    },

326
    jekyll: {
327
328
329
330
331
332
333
334
335
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
336
337
    },

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

353
354
    watch: {
      src: {
fat's avatar
fat committed
355
        files: '<%= jscs.core.src %>',
fat's avatar
fat committed
356
        tasks: ['babel:dev']
357
358
      },
      test: {
fat's avatar
fat committed
359
360
        files: '<%= jscs.test.src %>',
        tasks: ['qunit']
361
      },
362
363
364
      sass: {
        files: 'scss/**/*.scss',
        tasks: 'sass-compile'
Mark Otto's avatar
Mark Otto committed
365
366
      },
      docs: {
367
368
        files: 'docs/assets/scss/*.scss',
        tasks: 'sass:docs'
369
      }
370
371
372
373
374
    },

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

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

    exec: {
      npmUpdate: {
        command: 'npm update'
399
400
401
402
403
404
405
406
      },
      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
407
      }
408
409
    }
  });
410
411


412
  // These plugins provide necessary tasks.
413
414
415
  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
416
  require('time-grunt')(grunt);
417

418
  // Docs HTML validation task
419
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
420

421
422
423
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
424
425
426
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
427

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

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

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

459
  // CSS distribution task.
460
461
462
463
  // 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
464
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
465

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

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

471
  // Default task.
Mark Otto's avatar
Mark Otto committed
472
  grunt.registerTask('default', ['clean:dist', 'test']);
473

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

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

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

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

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