Gruntfile.js 15.5 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');
Mark Otto's avatar
Mark Otto committed
24

25
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
26
27
28
29
30
31
32
  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);
    });
  });
33

34
35
36
37
38
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
39
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
40
41
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
42
            ' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' +
XhmikosR's avatar
XhmikosR committed
43
            ' */\n',
44
45
46
47
48
    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' +
49
50
                        '  if (version[0] !== \'2\') {\n' +
                        '    throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery version 2.x.x\')\n' +
51
52
                        '  }\n' +
                        '}(jQuery);\n\n',
53
54
55

    // Task configuration.
    clean: {
56
57
      dist: 'dist',
      docs: 'docs/dist'
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

Jacob Thornton's avatar
Jacob Thornton committed
120
121
122
123
124
125
126
    eslint: {
      options: {
        configFile: 'js/.eslintrc'
      },
      target: 'js/src/*.js'
    },

Chris Rebert's avatar
Chris Rebert committed
127
128
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
129
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
130
      },
Chris Rebert's avatar
Chris Rebert committed
131
      grunt: {
fat's avatar
fat committed
132
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
133
      },
134
      core: {
fat's avatar
fat committed
135
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
136
137
      },
      test: {
fat's avatar
fat committed
138
        src: 'js/tests/unit/*.js'
139
140
      },
      assets: {
141
142
143
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
144
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
145
146
147
      }
    },

148
    stamp: {
149
      options: {
150
151
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>\n+function ($) {\n',
        footer: '\n}(jQuery);'
152
153
      },
      bootstrap: {
154
155
156
        files: {
          src: '<%= concat.bootstrap.dest %>'
        }
157
158
      }
    },
159

160
    concat: {
161
      options: {
162
        stripBanners: false
163
      },
164
      bootstrap: {
fat's avatar
fat committed
165
        src: [
166
167
168
169
170
171
172
173
174
175
176
          '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
177
        ],
178
        dest: 'dist/js/<%= pkg.name %>.js'
fat's avatar
fat committed
179
180
181
182
183
      }
    },

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

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

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

Chris Rebert's avatar
Chris Rebert committed
217
218
219
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
220
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
221
222
      },
      core: {
223
        src: 'dist/css/*.css'
Chris Rebert's avatar
Chris Rebert committed
224
225
226
      }
    },

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

XhmikosR's avatar
XhmikosR committed
262
    cssmin: {
XhmikosR's avatar
XhmikosR committed
263
      options: {
264
265
        // 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
266
        compatibility: 'ie9',
267
        keepSpecialComments: '*',
268
        sourceMap: true,
269
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
270
      },
Mark Otto's avatar
Mark Otto committed
271
      core: {
272
273
274
275
276
277
278
279
280
        files: [
          {
            expand: true,
            cwd: 'dist/css',
            src: ['*.css', '!*.min.css'],
            dest: 'dist/css',
            ext: '.min.css'
          }
        ]
281
      },
XhmikosR's avatar
XhmikosR committed
282
      docs: {
Mark Otto's avatar
Mark Otto committed
283
        src: 'docs/assets/css/docs.min.css',
284
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
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
      options: {
XhmikosR's avatar
XhmikosR committed
332
        bundleExec: true,
XhmikosR's avatar
XhmikosR committed
333
334
        config: '_config.yml',
        incremental: false
335
336
337
338
339
340
341
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
342
343
    },

344
    htmllint: {
345
      options: {
346
347
        ignore: [
          'Element “img” is missing required attribute “src”.',
348
          '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”.',
349
350
          '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.)',
351
352
          '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.'
353
        ]
354
      },
355
      src: '_gh_pages/**/*.html'
356
357
    },

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

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

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

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
400
      }
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
    },

    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'
        }
      }
416
417
    }
  });
418
419


420
  // These plugins provide necessary tasks.
421
422
423
  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
424
  require('time-grunt')(grunt);
425

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

429
430
431
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
432
433
434
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
435

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

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

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

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

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

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

482
  // Default task.
Mark Otto's avatar
Mark Otto committed
483
  grunt.registerTask('default', ['clean:dist', 'test']);
484

485
486
487
  // 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!
488
  grunt.registerTask('change-version-number', 'sed');
489

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

500
501
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
502
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
503
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
504
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
505

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

Mark Otto's avatar
Mark Otto committed
508
509
510
  // Publish to GitHub
  grunt.registerTask('publish', ['buildcontrol:pages']);

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