Gruntfile.js 14.7 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
59
60
61
62
    },

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

Chris Rebert's avatar
Chris Rebert committed
83
84
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
85
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
86
      },
Chris Rebert's avatar
Chris Rebert committed
87
      grunt: {
88
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
89
      },
90
91
      core: {
        src: '<%= jshint.core.src %>'
Chris Rebert's avatar
Chris Rebert committed
92
93
      },
      test: {
94
        src: '<%= jshint.test.src %>'
95
96
      },
      assets: {
97
98
99
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
100
        src: '<%= jshint.assets.src %>'
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/util.js',
112
113
114
115
116
117
          'js/alert.js',
          'js/button.js',
          'js/carousel.js',
          'js/collapse.js',
          'js/dropdown.js',
          'js/modal.js',
fat's avatar
fat committed
118
          'js/scrollspy.js',
119
120
          'js/tooltip.js',
          'js/popover.js',
fat's avatar
fat committed
121
          'js/tab.js'
122
123
124
125
        ],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    },
126

fat's avatar
fat committed
127
128
    closureCompiler:  {

129
      options: {
fat's avatar
fat committed
130
131
132
133
        compilerFile: require('superstartup-closure-compiler').getPath(),
        checkModified: false,

        compilerOpts: {
134
           // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
fat's avatar
fat committed
135
136
137
138
139
140
141
142
           // jscomp_warning: 'reportUnknownTypes', someday - maybe we will get to 100% typed, this helps track those down
           compilation_level: 'ADVANCED_OPTIMIZATIONS',
           warning_level: 'verbose',
           summary_detail_level: 3,
           output_wrapper:
                '"<%= banner %><%= jqueryCheck %><%= jqueryVersionCheck %>'
             + '(function($){%output%})(jQuery);"',
           externs: 'js/externs/*.js'
143
           // jscs:enable requireCamelCaseOrUpperCaseIdentifiers
fat's avatar
fat committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
        },

        execOpts: {
           maxBuffer: 999999 * 1024
        },

        // [OPTIONAL] Java VM optimization options
        // see https://code.google.com/p/closure-compiler/wiki/FAQ#What_are_the_recommended_Java_VM_command-line_options?
        // Setting one of these to 'true' is strongly recommended,
        // and can reduce compile times by 50-80% depending on compilation size
        // and hardware.
        // On server-class hardware, such as with Github's Travis hook,
        // TieredCompilation should be used; on standard developer hardware,
        // d32 may be better. Set as appropriate for your environment.
        // Default for both is 'false'; do not set both to 'true'.
        d32: false, // will use 'java -client -d32 -jar compiler.jar'
        TieredCompilation: false // will use 'java -server -XX:+TieredCompilation -jar compiler.jar'
161
      },
fat's avatar
fat committed
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176

      targetName: {
        src: [
          'js/util.js',
          'js/alert.js',
          'js/button.js',
          'js/carousel.js',
          'js/collapse.js',
          'js/dropdown.js',
          'js/modal.js',
          'js/scrollspy.js',
          'js/tooltip.js',
          'js/popover.js',
          'js/tab.js'
        ],
177
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
178
179
180
181
182
183
184
      }

    },

    uglify: {
      options: {
        preserveComments: 'some'
XhmikosR's avatar
XhmikosR committed
185
      },
186
      docsJs: {
187
        src: configBridge.paths.docsJs,
188
        dest: 'docs/assets/js/docs.min.js'
189
190
191
      }
    },

Mark Otto's avatar
Mark Otto committed
192
193
194
195
196
197
198
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

Chris Rebert's avatar
Chris Rebert committed
199
200
201
202
203
204
205
206
    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
207
208
209
    postcss: {
      options: {
        map: true,
Chris Rebert's avatar
Chris Rebert committed
210
        processors: [mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' })]
Chris Rebert's avatar
Chris Rebert committed
211
212
213
214
215
216
      },
      core: {
        src: 'dist/css/<%= pkg.name %>.css'
      }
    },

Bas Bosman's avatar
Bas Bosman committed
217
218
    autoprefixer: {
      options: {
219
220
221
        browsers: [
          'Android 2.3',
          'Android >= 4',
222
          'Chrome >= 35',
223
224
          'Firefox >= 31',
          'Explorer >= 9',
225
          'iOS >= 7',
226
          'Opera >= 12',
227
          'Safari >= 7.1'
228
        ]
Bas Bosman's avatar
Bas Bosman committed
229
230
231
232
233
234
235
236
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
237
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
238
239
240
241
242
243
244
245
246
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
247
    cssmin: {
XhmikosR's avatar
XhmikosR committed
248
      options: {
249
250
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
251
      },
Mark Otto's avatar
Mark Otto committed
252
253
254
255
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
256
      },
XhmikosR's avatar
XhmikosR committed
257
      docs: {
Mark Otto's avatar
Mark Otto committed
258
        src: 'docs/assets/css/docs.min.css',
259
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
260
261
262
      }
    },

263
    usebanner: {
XhmikosR's avatar
XhmikosR committed
264
265
266
267
268
269
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
270
271
272
      }
    },

273
    csscomb: {
274
      options: {
Mark Otto's avatar
Mark Otto committed
275
        config: 'scss/.csscomb.json'
276
277
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
278
279
280
281
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
282
283
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
284
285
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
286
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
287
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
288
289
      },
      docs: {
290
291
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
292
293
294
      }
    },

Mark Otto's avatar
Mark Otto committed
295
    copy: {
Mark Otto's avatar
Mark Otto committed
296
      docs: {
vsn4ik's avatar
vsn4ik committed
297
298
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
299
300
301
      }
    },

302
303
304
305
306
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
307
        }
308
309
310
      }
    },

311
    jekyll: {
312
313
314
315
316
317
318
319
320
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
321
322
    },

323
    htmllint: {
324
      options: {
325
326
327
328
329
330
331
        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).'
332
        ]
333
      },
334
      src: '_gh_pages/**/*.html'
335
336
    },

337
338
    watch: {
      src: {
339
        files: '<%= jshint.core.src %>',
340
        tasks: ['jshint:src', 'qunit', 'concat']
341
342
343
344
345
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
346
347
348
      sass: {
        files: 'scss/**/*.scss',
        tasks: 'sass-compile'
Mark Otto's avatar
Mark Otto committed
349
350
      },
      docs: {
351
352
        files: 'docs/assets/scss/*.scss',
        tasks: 'sass:docs'
353
      }
354
355
356
357
358
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
359
360
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
361
362
363
364
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
365
366
367
368
369
370
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
371
          concurrency: 10,
372
          maxRetries: 3,
373
          maxPollRetries: 4,
374
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
375
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
376
377
        }
      }
Chris Rebert's avatar
Chris Rebert committed
378
379
380
381
382
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
383
384
385
386
387
388
389
390
      },
      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
391
      }
392
393
    }
  });
394
395


396
  // These plugins provide necessary tasks.
397
398
399
  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
400
  require('time-grunt')(grunt);
401

402
  // Docs HTML validation task
403
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
404

405
406
407
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
408
409
410
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
411

412
  // Test task.
413
414
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
415
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
416
    // Skip core tests if this is a Savage build
417
418
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
419
420
  }
  // Skip HTML validation if running a different subset of the test suite
421
422
423
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
424
425
    testSubtasks.push('validate-html');
  }
426
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
427
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
428
      // Skip Sauce if running a different subset of the test suite
429
430
431
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
432
433
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
434
435
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
436
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
437

438
  // JS distribution task.
fat's avatar
fat committed
439
  grunt.registerTask('dist-js', ['concat', 'closureCompiler', 'commonjs']);
440

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

443
  // CSS distribution task.
444
445
446
447
  // 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
448
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
449

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

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

455
  // Default task.
Mark Otto's avatar
Mark Otto committed
456
  grunt.registerTask('default', ['clean:dist', 'test']);
457

458
459
460
  // 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!
461
  grunt.registerTask('change-version-number', 'sed');
462

463
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
464
465
466
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
467
468
  });

469
470
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
471
  grunt.registerTask('docs-js', ['uglify:docsJs']);
472
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
Mark Otto's avatar
Mark Otto committed
473
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
474

475
476
  grunt.registerTask('docs-github', ['jekyll:github']);

477
478
479
480
481
482
483
  // 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) {
484
        grunt.fail.warn(err);
485
486
487
488
489
490
491
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
492
493
494
  // 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']);
495
};