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

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

32
33
34
35
36
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
37
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
38
39
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
40
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
41
            ' */\n',
42
43
44
45
46
47
48
49
50
    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',
51
52
53

    // Task configuration.
    clean: {
54
55
      dist: 'dist',
      docs: 'docs/dist'
56
57
58
59
60
61
    },

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
Chris Rebert's avatar
Chris Rebert committed
62
      grunt: {
63
        options: {
64
          jshintrc: 'grunt/.jshintrc'
65
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
66
        src: ['Gruntfile.js', 'grunt/*.js']
67
      },
68
      core: {
69
        src: 'js/*.js'
70
71
      },
      test: {
XhmikosR's avatar
XhmikosR committed
72
73
74
        options: {
          jshintrc: 'js/tests/unit/.jshintrc'
        },
75
        src: 'js/tests/unit/*.js'
76
77
      },
      assets: {
78
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
79
80
      }
    },
81

Chris Rebert's avatar
Chris Rebert committed
82
83
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
84
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
85
      },
Chris Rebert's avatar
Chris Rebert committed
86
      grunt: {
87
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
88
      },
89
90
      core: {
        src: '<%= jshint.core.src %>'
Chris Rebert's avatar
Chris Rebert committed
91
92
      },
      test: {
93
        src: '<%= jshint.test.src %>'
94
95
      },
      assets: {
96
97
98
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
99
        src: '<%= jshint.assets.src %>'
Chris Rebert's avatar
Chris Rebert committed
100
101
102
      }
    },

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

127
    uglify: {
128
129
130
      options: {
        preserveComments: 'some'
      },
131
      core: {
132
        src: '<%= concat.bootstrap.dest %>',
133
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
134
      },
135
      docsJs: {
136
        src: configBridge.paths.docsJs,
137
        dest: 'docs/assets/js/docs.min.js'
138
139
140
      }
    },

Mark Otto's avatar
Mark Otto committed
141
142
143
144
145
146
147
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

Chris Rebert's avatar
Chris Rebert committed
148
149
150
151
152
153
154
155
    scsslint: {
      scss: ['scss/*.scss', '!scss/_normalize.scss'],
      options: {
        config: 'scss/.scss-lint.yml',
        reporterOutput: 'scss-lint-report.xml'
      }
    },

Bas Bosman's avatar
Bas Bosman committed
156
157
    autoprefixer: {
      options: {
158
159
160
        browsers: [
          'Android 2.3',
          'Android >= 4',
161
          'Chrome >= 35',
162
163
          'Firefox >= 31',
          'Explorer >= 9',
164
          'iOS >= 7',
165
          'Opera >= 12',
166
          'Safari >= 7.1'
167
        ]
Bas Bosman's avatar
Bas Bosman committed
168
169
170
171
172
173
174
175
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
176
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
177
178
179
180
181
182
183
184
185
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
186
    cssmin: {
XhmikosR's avatar
XhmikosR committed
187
      options: {
188
189
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
190
      },
Mark Otto's avatar
Mark Otto committed
191
192
193
194
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
195
      },
XhmikosR's avatar
XhmikosR committed
196
      docs: {
Mark Otto's avatar
Mark Otto committed
197
        src: 'docs/assets/css/docs.min.css',
198
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
199
200
201
      }
    },

202
    usebanner: {
XhmikosR's avatar
XhmikosR committed
203
204
205
206
207
208
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
209
210
211
      }
    },

212
    csscomb: {
213
      options: {
Mark Otto's avatar
Mark Otto committed
214
        config: 'scss/.csscomb.json'
215
216
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
217
218
219
220
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
221
222
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
223
224
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
225
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
226
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
227
228
      },
      docs: {
229
230
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
231
232
233
      }
    },

Mark Otto's avatar
Mark Otto committed
234
    copy: {
Mark Otto's avatar
Mark Otto committed
235
      docs: {
vsn4ik's avatar
vsn4ik committed
236
237
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
238
239
240
      }
    },

241
242
243
244
245
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
246
        }
247
248
249
      }
    },

250
    jekyll: {
251
252
253
254
255
256
257
258
259
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
260
261
262
263
    },

    validation: {
      options: {
264
265
        charset: 'utf-8',
        doctype: 'HTML5',
266
        failHard: true,
267
268
        reset: true,
        relaxerror: [
269
          'Element img is missing required attribute src.',
270
          'Attribute autocomplete not allowed on element input at this point.',
Mark Otto's avatar
Mark Otto committed
271
272
273
274
          'Attribute autocomplete not allowed on element button at this point.',
          'Element div not allowed as child of element progress in this context.',
          'Element thead not allowed as child of element table in this context.',
          'Bad value tablist for attribute role on element nav.'
275
        ]
276
277
      },
      files: {
278
        src: '_gh_pages/**/*.html'
279
280
281
      }
    },

282
283
    watch: {
      src: {
284
        files: '<%= jshint.core.src %>',
285
        tasks: ['jshint:src', 'qunit', 'concat']
286
287
288
289
290
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
291
      less: {
292
        files: 'less/**/*.less',
293
        tasks: 'less'
Mark Otto's avatar
Mark Otto committed
294
295
296
297
      },
      docs: {
        files: 'docs/assets/less/*.less',
        tasks: 'less'
298
      }
299
300
301
302
303
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
304
305
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
306
307
308
309
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
310
311
312
313
314
315
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
316
          concurrency: 10,
317
          maxRetries: 3,
318
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
319
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
320
321
        }
      }
Chris Rebert's avatar
Chris Rebert committed
322
323
324
325
326
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
327
328
329
330
331
332
333
334
      },
      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
335
      }
336
337
    }
  });
338
339


340
  // These plugins provide necessary tasks.
341
342
343
  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
344
  require('time-grunt')(grunt);
345

346
  // Docs HTML validation task
347
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
348

349
350
351
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
352
353
354
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
355

356
  // Test task.
357
358
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
359
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
360
    // Skip core tests if this is a Savage build
361
362
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
363
364
  }
  // Skip HTML validation if running a different subset of the test suite
365
366
367
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
368
369
    testSubtasks.push('validate-html');
  }
370
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
371
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
372
      // Skip Sauce if running a different subset of the test suite
373
374
375
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
376
377
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
378
379
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
380
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
381

382
  // JS distribution task.
383
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
384

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

387
  // CSS distribution task.
388
389
390
391
  // 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
392
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
393

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

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

399
  // Default task.
Mark Otto's avatar
Mark Otto committed
400
  grunt.registerTask('default', ['clean:dist', 'test']);
401

402
403
404
  // 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!
405
  grunt.registerTask('change-version-number', 'sed');
406

407
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
408
409
410
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
411
412
  });

413
414
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
415
  grunt.registerTask('docs-js', ['uglify:docsJs']);
416
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
Mark Otto's avatar
Mark Otto committed
417
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
418

419
420
  grunt.registerTask('docs-github', ['jekyll:github']);

421
422
423
424
425
426
427
  // 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) {
428
        grunt.fail.warn(err);
429
430
431
432
433
434
435
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
436
437
438
  // 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']);
439
};