Gruntfile.js 15.7 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');
Chris Rebert's avatar
Chris Rebert committed
21
  var isTravis = require('is-travis');
22
  var npmShrinkwrap = require('npm-shrinkwrap');
23
  var mq4HoverShim = require('mq4-hover-shim');
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  var autoprefixer = require('autoprefixer')({
    browsers: [
      'Android 2.3',
      'Android >= 4',
      'Chrome >= 35',
      'Firefox >= 31',
      // Note: Edge versions in Autoprefixer & Can I Use refer to the EdgeHTML rendering engine version,
      // NOT the Edge app version shown in Edge's "About" screen.
      // For example, at the time of writing, Edge 20 on an up-to-date system uses EdgeHTML 12.
      // See also https://github.com/Fyrd/caniuse/issues/1928
      'Edge >= 12',
      'Explorer >= 9',
      'iOS >= 7',
      'Opera >= 12',
      'Safari >= 7.1'
    ]
  });
Mark Otto's avatar
Mark Otto committed
41

42
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
43
44
45
46
47
48
49
  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);
    });
  });
50

51
52
53
54
55
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
56
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
57
58
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
59
            ' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' +
XhmikosR's avatar
XhmikosR committed
60
            ' */\n',
61
62
63
64
65
    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' +
Chris Rebert's avatar
Chris Rebert committed
66
67
                        '  if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] >= 3)) {\n' +
                        '    throw new Error(\'Bootstrap\\\'s JavaScript requires at least jQuery v1.9.1 but less than v3.0.0\')\n' +
68
69
                        '  }\n' +
                        '}(jQuery);\n\n',
70
71
72

    // Task configuration.
    clean: {
73
74
      dist: 'dist',
      docs: 'docs/dist'
75
76
    },

77
78
79
80
81
82
    // JS build configuration
    lineremover: {
      es6Import: {
        files: {
          '<%= concat.bootstrap.dest %>': '<%= concat.bootstrap.dest %>'
        },
83
        options: {
84
85
86
87
88
          exclusionPattern: /^(import|export)/g
        }
      }
    },

fat's avatar
fat committed
89
    babel: {
fat's avatar
fat committed
90
      dev: {
91
92
93
        options: {
          sourceMap: true,
          modules: 'ignore'
94
        },
fat's avatar
fat committed
95
        files: {
fat's avatar
fat committed
96
97
98
99
100
101
102
          '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
103
          'js/dist/scrollspy.js' : 'js/src/scrollspy.js',
104
          'js/dist/tab.js'       : 'js/src/tab.js',
fat's avatar
fat committed
105
106
          'js/dist/tooltip.js'   : 'js/src/tooltip.js',
          'js/dist/popover.js'   : 'js/src/popover.js'
fat's avatar
fat committed
107
        }
108
      },
109
      dist: {
XhmikosR's avatar
XhmikosR committed
110
        options: {
111
          modules: 'ignore'
XhmikosR's avatar
XhmikosR committed
112
        },
113
114
115
        files: {
          '<%= concat.bootstrap.dest %>' : '<%= concat.bootstrap.dest %>'
        }
116
      },
fat's avatar
fat committed
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
      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'
        }
134
135
      }
    },
136

Jacob Thornton's avatar
Jacob Thornton committed
137
138
139
140
141
142
143
    eslint: {
      options: {
        configFile: 'js/.eslintrc'
      },
      target: 'js/src/*.js'
    },

Chris Rebert's avatar
Chris Rebert committed
144
145
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
146
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
147
      },
Chris Rebert's avatar
Chris Rebert committed
148
      grunt: {
fat's avatar
fat committed
149
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
150
      },
151
      core: {
fat's avatar
fat committed
152
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
153
154
      },
      test: {
fat's avatar
fat committed
155
        src: 'js/tests/unit/*.js'
156
157
      },
      assets: {
158
159
160
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
161
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
162
163
164
      }
    },

165
    stamp: {
166
      options: {
167
168
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>\n+function ($) {\n',
        footer: '\n}(jQuery);'
169
170
      },
      bootstrap: {
171
172
173
        files: {
          src: '<%= concat.bootstrap.dest %>'
        }
174
175
      }
    },
176

177
    concat: {
178
      options: {
179
        stripBanners: false
180
      },
181
      bootstrap: {
fat's avatar
fat committed
182
        src: [
183
184
185
186
187
188
189
190
191
192
193
          '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
194
        ],
195
        dest: 'dist/js/<%= pkg.name %>.js'
fat's avatar
fat committed
196
197
198
199
200
      }
    },

    uglify: {
      options: {
fat's avatar
fat committed
201
202
        compress: {
          warnings: false
fat's avatar
fat committed
203
        },
fat's avatar
fat committed
204
        mangle: true,
fat's avatar
fat committed
205
        preserveComments: 'some'
XhmikosR's avatar
XhmikosR committed
206
      },
fat's avatar
fat committed
207
208
      core: {
        src: '<%= concat.bootstrap.dest %>',
209
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
210
      },
211
      docsJs: {
212
        src: configBridge.paths.docsJs,
213
        dest: 'docs/assets/js/docs.min.js'
214
215
216
      }
    },

Mark Otto's avatar
Mark Otto committed
217
218
219
220
221
222
223
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

224
    // CSS build configuration
Chris Rebert's avatar
Chris Rebert committed
225
226
    scsslint: {
      options: {
227
        bundleExec: true,
nextgenthemes's avatar
nextgenthemes committed
228
        config: 'scss/.scss-lint.yml',
Mark Otto's avatar
Mark Otto committed
229
230
231
        reporterOutput: null
      },
      src: ['scss/*.scss', '!scss/_normalize.scss']
Chris Rebert's avatar
Chris Rebert committed
232
233
    },

Chris Rebert's avatar
Chris Rebert committed
234
    postcss: {
Bas Bosman's avatar
Bas Bosman committed
235
236
      core: {
        options: {
237
238
239
240
241
          map: true,
          processors: [
            mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' }),
            autoprefixer
          ]
Bas Bosman's avatar
Bas Bosman committed
242
        },
243
        src: 'dist/css/*.css'
Bas Bosman's avatar
Bas Bosman committed
244
245
      },
      docs: {
246
247
248
249
250
        options: {
          processors: [
            autoprefixer
          ]
        },
Mark Otto's avatar
Mark Otto committed
251
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
252
253
      },
      examples: {
254
255
256
257
258
        options: {
          processors: [
            autoprefixer
          ]
        },
Bas Bosman's avatar
Bas Bosman committed
259
260
261
262
263
264
265
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
266
    cssmin: {
XhmikosR's avatar
XhmikosR committed
267
      options: {
268
269
        // 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
270
        compatibility: 'ie9',
271
        keepSpecialComments: '*',
272
        sourceMap: true,
273
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
274
      },
Mark Otto's avatar
Mark Otto committed
275
      core: {
276
277
278
279
280
281
282
283
284
        files: [
          {
            expand: true,
            cwd: 'dist/css',
            src: ['*.css', '!*.min.css'],
            dest: 'dist/css',
            ext: '.min.css'
          }
        ]
285
      },
XhmikosR's avatar
XhmikosR committed
286
      docs: {
Mark Otto's avatar
Mark Otto committed
287
        src: 'docs/assets/css/docs.min.css',
288
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
289
290
291
      }
    },

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

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

325
326
327
328
329
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
330
        }
331
332
333
      }
    },

334
    jekyll: {
335
      options: {
XhmikosR's avatar
XhmikosR committed
336
        bundleExec: true,
XhmikosR's avatar
XhmikosR committed
337
338
        config: '_config.yml',
        incremental: false
339
340
341
342
343
344
345
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
346
347
    },

348
    htmllint: {
349
      options: {
350
351
        ignore: [
          'Element “img” is missing required attribute “src”.',
352
          'Attribute “autocomplete” is only allowed when the input type is “color”, “date”, “datetime”, “datetime-local”, “email”, “month”, “number”, “password”, “range”, “search”, “tel”, “text”, “time”, “url”, or “week”.',
353
354
          '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.)',
355
356
          '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.'
357
        ]
358
      },
359
      src: '_gh_pages/**/*.html'
360
361
    },

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

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

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

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
404
      }
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
    },

    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'
        }
      }
420
421
    }
  });
422
423


424
  // These plugins provide necessary tasks.
425
426
427
  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
428
  require('time-grunt')(grunt);
429

430
  // Docs HTML validation task
XhmikosR's avatar
XhmikosR committed
431
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
432

433
434
435
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
436
437
438
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
439

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

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

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

473
  // CSS distribution task.
474
475
476
477
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
478
479
  // grunt.registerTask('sass-compile', ['sass:core', 'sass:extras', 'sass:docs']);
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
480

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

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

486
  // Default task.
Mark Otto's avatar
Mark Otto committed
487
  grunt.registerTask('default', ['clean:dist', 'test']);
488

489
490
491
  // 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!
492
  grunt.registerTask('change-version-number', 'sed');
493

fat's avatar
fat committed
494
495
496
497
498
499
  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))
    })
500
501
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
502
503
  });

504
  // Docs task.
505
  grunt.registerTask('docs-css', ['postcss:docs', 'postcss:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
506
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
507
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
508
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
509

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

Mark Otto's avatar
Mark Otto committed
512
513
514
  // Publish to GitHub
  grunt.registerTask('publish', ['buildcontrol:pages']);

515
516
517
518
519
520
521
  // 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) {
522
        grunt.fail.warn(err);
523
      }
524
      var dest = 'grunt/npm-shrinkwrap.json';
525
526
527
528
529
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
530
};