Gruntfile.js 15.2 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: {
209
        bundleExec: true,
Mark Otto's avatar
Mark Otto committed
210
211
212
213
        config: 'scss/.scsslint.yml',
        reporterOutput: null
      },
      src: ['scss/*.scss', '!scss/_normalize.scss']
Chris Rebert's avatar
Chris Rebert committed
214
215
    },

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

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

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

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

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

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

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

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

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

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

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

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

    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'
        }
      }
409
410
    }
  });
411
412


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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