Gruntfile.js 15 KB
Newer Older
Chris Rebert's avatar
Chris Rebert committed
1
2
3
/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
XhmikosR's avatar
XhmikosR committed
4
 * Copyright 2013-2015 Twitter, Inc.
Chris Rebert's avatar
Chris Rebert committed
5
6
 * 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
      docsJs: {
194
        src: configBridge.paths.docsJs,
195
        dest: 'docs/assets/js/docs.min.js'
196
197
198
      }
    },

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

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

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
      },
      core: {
221
        src: 'dist/css/*.css'
Chris Rebert's avatar
Chris Rebert committed
222
223
224
      }
    },

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

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

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

314
315
316
317
318
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
319
        }
320
321
322
      }
    },

323
    jekyll: {
324
      options: {
XhmikosR's avatar
XhmikosR committed
325
        bundleExec: true,
326
327
328
329
330
331
332
333
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
334
335
    },

336
    htmllint: {
337
      options: {
338
339
340
341
342
        ignore: [
          'Element “img” is missing required attribute “src”.',
          '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.)',
343
344
          '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.'
345
        ]
346
      },
347
      src: '_gh_pages/**/*.html'
348
349
    },

350
351
    watch: {
      src: {
fat's avatar
fat committed
352
        files: '<%= jscs.core.src %>',
fat's avatar
fat committed
353
        tasks: ['babel:dev']
354
      },
355
356
      sass: {
        files: 'scss/**/*.scss',
Mark Otto's avatar
Mark Otto committed
357
        tasks: ['dist-css', 'docs']
Mark Otto's avatar
Mark Otto committed
358
359
360
361
      },
      docs: {
        files: 'docs/assets/scss/**/*.scss',
        tasks: ['dist-css', 'docs']
362
      }
363
364
365
366
367
    },

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

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

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
392
      }
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    },

    buildcontrol: {
      options: {
        dir: '_gh_pages',
        commit: true,
        push: true,
        message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
      },
      pages: {
        options: {
          remote: 'git@github.com:twbs/derpstrap.git',
          branch: 'gh-pages'
        }
      }
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
XhmikosR's avatar
XhmikosR committed
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
    testSubtasks.push('babel:dev');
449
450
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
451
452
  }
  grunt.registerTask('test', testSubtasks);
Jacob Thornton's avatar
Jacob Thornton committed
453
  grunt.registerTask('test-js', ['eslint', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
454

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

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

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

468
  grunt.registerTask('dist-css', ['sass-compile', 'postcss:core', 'autoprefixer:core', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
469

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

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

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

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

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

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

Mark Otto's avatar
Mark Otto committed
499
500
501
  // Publish to GitHub
  grunt.registerTask('publish', ['buildcontrol:pages']);

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