Gruntfile.js 13.4 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
73
          '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',
          'js/dist/scrollspy.js' : 'js/src/scrollspy.js'
fat's avatar
fat committed
74
        }
75
76
      }
    },
77

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

102
103
    concat: {
      options: {
104
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
105
106
107
108
        stripBanners: false
      },
      bootstrap: {
        src: [
fat's avatar
fat committed
109
          'js/transition.js',
110
111
112
113
114
115
116
117
          '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
118
          'js/scrollspy.js',
119
          'js/tab.js'
120
121
122
123
        ],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    },
124

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

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

Chris Rebert's avatar
Chris Rebert committed
154
155
156
157
158
159
160
161
    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
162
163
164
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
165
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
166
167
168
169
170
171
      },
      core: {
        src: 'dist/css/<%= pkg.name %>.css'
      }
    },

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

405
  // CSS distribution task.
406
407
408
409
  // 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
410
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
411

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

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

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

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

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

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

437
438
  grunt.registerTask('docs-github', ['jekyll:github']);

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