Gruntfile.js 15.9 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
420
421
422
423
424
425
426
427
428
429
    },

    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'
        }
      }
430
431
    }
  });
432
433


434
  // These plugins provide necessary tasks.
435
436
437
  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
438
  require('time-grunt')(grunt);
439

440
  // Docs HTML validation task
441
  grunt.registerTask('validate-html', ['jekyll:docs']);
442

443
444
445
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
446
447
448
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
449

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

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

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

481
  // CSS distribution task.
482
483
484
485
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
486
487
  // grunt.registerTask('sass-compile', ['sass:core', 'sass:extras', 'sass:docs']);
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
488

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

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

494
  // Default task.
Mark Otto's avatar
Mark Otto committed
495
  grunt.registerTask('default', ['clean:dist', 'test']);
496

497
498
499
  // 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!
500
  grunt.registerTask('change-version-number', 'sed');
501

fat's avatar
fat committed
502
503
504
505
506
507
  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))
    })
508
509
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
510
511
  });

512
513
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
514
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
515
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
516
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
517

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

Mark Otto's avatar
Mark Otto committed
520
521
522
  // Publish to GitHub
  grunt.registerTask('publish', ['buildcontrol:pages']);

523
524
525
526
527
528
529
  // 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) {
530
        grunt.fail.warn(err);
531
532
533
534
535
536
537
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
538
539
540
  // 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']);
541
};