Gruntfile.js 13.5 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 <%= pkg.license.type %> (<%= pkg.license.url %>)\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
    },

fat's avatar
fat committed
59
    babel: {
60
      options: {
fat's avatar
fat committed
61
62
        sourceMap: true,
        modules: 'ignore'
63
      },
fat's avatar
fat committed
64
65
      dist: {
        files: {
fat's avatar
fat committed
66
67
68
69
70
71
72
          '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
73
          'js/dist/scrollspy.js' : 'js/src/scrollspy.js',
74
75
          'js/dist/tab.js'       : 'js/src/tab.js',
          'js/dist/tooltip.js'   : 'js/src/tooltip.js'
fat's avatar
fat committed
76
        }
77
78
      }
    },
79

Chris Rebert's avatar
Chris Rebert committed
80
81
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
82
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
83
      },
Chris Rebert's avatar
Chris Rebert committed
84
      grunt: {
fat's avatar
fat committed
85
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
86
      },
87
      core: {
fat's avatar
fat committed
88
89
90
91
        src: 'js/*.js'
      },
      es6: {
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
92
93
      },
      test: {
fat's avatar
fat committed
94
        src: 'js/tests/unit/*.js'
95
96
      },
      assets: {
97
98
99
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
100
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
101
102
103
      }
    },

104
105
    concat: {
      options: {
106
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
107
108
109
110
        stripBanners: false
      },
      bootstrap: {
        src: [
fat's avatar
fat committed
111
          'js/transition.js',
112
113
114
115
116
117
118
119
          'js/alert.js',
          'js/button.js',
          'js/carousel.js',
          'js/collapse.js',
          'js/dropdown.js',
          'js/modal.js',
          'js/tooltip.js',
          'js/popover.js',
fat's avatar
fat committed
120
          'js/scrollspy.js',
121
          'js/tab.js'
122
123
124
125
        ],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    },
126

fat's avatar
fat committed
127
    uglify: {
128
      options: {
fat's avatar
fat committed
129
130
        compress: {
          warnings: false
fat's avatar
fat committed
131
        },
fat's avatar
fat committed
132
133
        mangle: true,
        preserveComments: 'some'
134
      },
fat's avatar
fat committed
135
136
      core: {
        src: '<%= concat.bootstrap.dest %>',
137
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
138
139
140
141
      },
      customize: {
        src: configBridge.paths.customizerJs,
        dest: 'docs/assets/js/customize.min.js'
XhmikosR's avatar
XhmikosR committed
142
      },
143
      docsJs: {
144
        src: configBridge.paths.docsJs,
145
        dest: 'docs/assets/js/docs.min.js'
146
147
148
      }
    },

Mark Otto's avatar
Mark Otto committed
149
150
151
152
153
154
155
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

Chris Rebert's avatar
Chris Rebert committed
156
157
158
159
160
161
162
163
    scsslint: {
      scss: ['scss/*.scss', '!scss/_normalize.scss'],
      options: {
        config: 'scss/.scss-lint.yml',
        reporterOutput: 'scss-lint-report.xml'
      }
    },

Chris Rebert's avatar
Chris Rebert committed
164
165
166
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
167
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
168
169
170
171
172
173
      },
      core: {
        src: 'dist/css/<%= pkg.name %>.css'
      }
    },

Bas Bosman's avatar
Bas Bosman committed
174
175
    autoprefixer: {
      options: {
176
177
178
        browsers: [
          'Android 2.3',
          'Android >= 4',
179
          'Chrome >= 35',
180
181
          'Firefox >= 31',
          'Explorer >= 9',
182
          'iOS >= 7',
183
          'Opera >= 12',
184
          'Safari >= 7.1'
185
        ]
Bas Bosman's avatar
Bas Bosman committed
186
187
188
189
190
191
192
193
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
194
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
195
196
197
198
199
200
201
202
203
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
204
    cssmin: {
XhmikosR's avatar
XhmikosR committed
205
      options: {
206
207
        // 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
208
        compatibility: 'ie8',
209
210
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
211
      },
Mark Otto's avatar
Mark Otto committed
212
213
214
215
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
216
      },
XhmikosR's avatar
XhmikosR committed
217
      docs: {
Mark Otto's avatar
Mark Otto committed
218
        src: 'docs/assets/css/docs.min.css',
219
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
220
221
222
      }
    },

223
    usebanner: {
XhmikosR's avatar
XhmikosR committed
224
225
226
227
228
229
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
230
231
232
      }
    },

233
    csscomb: {
234
      options: {
Mark Otto's avatar
Mark Otto committed
235
        config: 'scss/.csscomb.json'
236
237
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
238
239
240
241
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
242
243
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
244
245
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
246
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
247
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
248
249
      },
      docs: {
250
251
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
252
253
254
      }
    },

Mark Otto's avatar
Mark Otto committed
255
    copy: {
Mark Otto's avatar
Mark Otto committed
256
      docs: {
XhmikosR's avatar
XhmikosR committed
257
258
259
260
261
262
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
Mark Otto's avatar
Mark Otto committed
263
264
265
      }
    },

266
267
268
269
270
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
271
        }
272
273
274
      }
    },

275
    jekyll: {
276
277
278
279
280
281
282
283
284
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
285
286
    },

287
    htmllint: {
288
      options: {
289
290
291
292
293
294
295
        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.)',
          '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).'
296
        ]
297
      },
298
      src: '_gh_pages/**/*.html'
299
300
    },

301
302
    watch: {
      src: {
fat's avatar
fat committed
303
304
        files: '<%= jscs.core.src %>',
        tasks: ['qunit', 'concat']
305
306
      },
      test: {
fat's avatar
fat committed
307
308
        files: '<%= jscs.test.src %>',
        tasks: ['qunit']
309
      },
310
311
312
      sass: {
        files: 'scss/**/*.scss',
        tasks: 'sass-compile'
Mark Otto's avatar
Mark Otto committed
313
314
      },
      docs: {
315
316
        files: 'docs/assets/scss/*.scss',
        tasks: 'sass:docs'
317
      }
318
319
320
321
322
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
323
324
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
325
326
327
328
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
329
330
331
332
333
334
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
335
          concurrency: 10,
336
          maxRetries: 3,
337
          maxPollRetries: 4,
338
          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
339
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
340
341
        }
      }
Chris Rebert's avatar
Chris Rebert committed
342
343
344
345
346
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
347
348
349
350
351
352
353
354
      },
      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
355
      }
356
357
    }
  });
358
359


360
  // These plugins provide necessary tasks.
361
362
363
  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
364
  require('time-grunt')(grunt);
365

366
  // Docs HTML validation task
367
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
368

369
370
371
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
372
373
374
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
375

376
  // Test task.
377
378
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
379
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
380
    // Skip core tests if this is a Savage build
381
382
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
383
384
  }
  // Skip HTML validation if running a different subset of the test suite
385
386
387
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
388
389
    testSubtasks.push('validate-html');
  }
390
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
391
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
392
      // Skip Sauce if running a different subset of the test suite
393
394
395
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
396
397
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
398
399
  }
  grunt.registerTask('test', testSubtasks);
fat's avatar
fat committed
400
  grunt.registerTask('test-js', ['jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
401

402
  // JS distribution task.
fat's avatar
fat committed
403
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
404

Chris Rebert's avatar
Chris Rebert committed
405
406
  grunt.registerTask('test-scss', ['scsslint:scss']);

407
  // CSS distribution task.
408
409
410
411
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
Mark Otto's avatar
Mark Otto committed
412
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
413

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

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

419
  // Default task.
Mark Otto's avatar
Mark Otto committed
420
  grunt.registerTask('default', ['clean:dist', 'test']);
421

422
423
424
  // 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!
425
  grunt.registerTask('change-version-number', 'sed');
426

427
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
428
429
430
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
431
432
  });

433
434
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
435
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
436
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
Mark Otto's avatar
Mark Otto committed
437
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
438

439
440
  grunt.registerTask('docs-github', ['jekyll:github']);

441
442
443
444
445
446
447
  // 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) {
448
        grunt.fail.warn(err);
449
450
451
452
453
454
455
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
456
457
458
  // 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']);
459
};