Gruntfile.js 13.6 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 npmShrinkwrap = require('npm-shrinkwrap');
21
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
22
23
24
25
26
27
  var getLessVarsData = function () {
    var filePath = path.join(__dirname, 'less/variables.less');
    var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
    var parser = new BsLessdocParser(fileContent);
    return { sections: parser.parseFile() };
  };
28
  var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
29
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
30

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

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
36
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
37
38
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
39
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
40
            ' */\n',
41
    // NOTE: This jqueryCheck code is duplicated in customizer.js; if making changes here, be sure to update the other copy too.
42
    jqueryCheck: 'if (typeof jQuery === \'undefined\') { throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery\') }\n\n',
43
44
45

    // Task configuration.
    clean: {
46
47
      dist: 'dist',
      docs: 'docs/dist'
48
49
50
51
52
53
    },

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
Chris Rebert's avatar
Chris Rebert committed
54
      grunt: {
55
        options: {
56
          jshintrc: 'grunt/.jshintrc'
57
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
58
        src: ['Gruntfile.js', 'grunt/*.js']
59
      },
60
      core: {
61
        src: 'js/*.js'
62
63
      },
      test: {
XhmikosR's avatar
XhmikosR committed
64
65
66
        options: {
          jshintrc: 'js/tests/unit/.jshintrc'
        },
67
        src: 'js/tests/unit/*.js'
68
69
      },
      assets: {
70
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
71
72
      }
    },
73

Chris Rebert's avatar
Chris Rebert committed
74
75
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
76
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
77
      },
Chris Rebert's avatar
Chris Rebert committed
78
      grunt: {
79
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
80
      },
81
82
      core: {
        src: '<%= jshint.core.src %>'
Chris Rebert's avatar
Chris Rebert committed
83
84
      },
      test: {
85
        src: '<%= jshint.test.src %>'
86
87
      },
      assets: {
88
89
90
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
91
        src: '<%= jshint.assets.src %>'
Chris Rebert's avatar
Chris Rebert committed
92
93
94
      }
    },

95
96
    concat: {
      options: {
Zlatan Vasović's avatar
Zlatan Vasović committed
97
        banner: '<%= banner %>\n<%= jqueryCheck %>',
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
        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'
      }
    },
118

119
    uglify: {
120
121
122
      options: {
        preserveComments: 'some'
      },
123
      core: {
124
        src: '<%= concat.bootstrap.dest %>',
125
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
126
127
128
      },
      customize: {
        src: [
129
130
131
132
133
          'docs/assets/js/vendor/less.min.js',
          'docs/assets/js/vendor/jszip.min.js',
          'docs/assets/js/vendor/uglify.min.js',
          'docs/assets/js/vendor/blob.js',
          'docs/assets/js/vendor/filesaver.js',
134
          'docs/assets/js/raw-files.min.js',
135
          'docs/assets/js/src/customizer.js'
XhmikosR's avatar
XhmikosR committed
136
        ],
137
        dest: 'docs/assets/js/customize.min.js'
138
139
      },
      docsJs: {
140
        // NOTE: This src list is duplicated in footer.html; if making changes here, be sure to update the other copy too.
141
        src: [
142
143
          'docs/assets/js/vendor/holder.js',
          'docs/assets/js/vendor/ZeroClipboard.min.js',
144
          'docs/assets/js/src/application.js'
145
146
        ],
        dest: 'docs/assets/js/docs.min.js'
147
148
149
      }
    },

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

157
    less: {
158
159
      compileCore: {
        options: {
160
          strictMath: true,
161
162
163
164
165
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
        },
166
167
        src: 'less/bootstrap.less',
        dest: 'dist/css/<%= pkg.name %>.css'
168
169
170
      },
      compileTheme: {
        options: {
171
          strictMath: true,
172
173
174
175
176
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
177
178
        src: 'less/theme.less',
        dest: 'dist/css/<%= pkg.name %>-theme.css'
179
180
181
      }
    },

Bas Bosman's avatar
Bas Bosman committed
182
183
    autoprefixer: {
      options: {
Bas Bosman's avatar
Bas Bosman committed
184
185
186
187
188
189
190
191
192
193
        browsers: [
          'Android 2.3',
          'Android >= 4',
          'Chrome >= 20',
          'Firefox >= 24', // Firefox 24 is the latest ESR
          'Explorer >= 8',
          'iOS >= 6',
          'Opera >= 12',
          'Safari >= 6'
        ]
Bas Bosman's avatar
Bas Bosman committed
194
195
196
197
198
199
200
201
202
203
204
205
206
207
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
208
        src: 'docs/assets/css/src/docs.css'
Bas Bosman's avatar
Bas Bosman committed
209
210
211
212
213
214
215
216
217
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

Mark Otto's avatar
Mark Otto committed
218
219
220
221
    csslint: {
      options: {
        csslintrc: 'less/.csslintrc'
      },
222
      dist: [
Mark Otto's avatar
Mark Otto committed
223
224
225
226
227
228
229
230
        'dist/css/bootstrap.css',
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
        'docs/examples/**/*.css'
      ],
      docs: {
        options: {
231
          ids: false,
Mark Otto's avatar
Mark Otto committed
232
233
          'overqualified-elements': false
        },
234
        src: 'docs/assets/css/src/docs.css'
Mark Otto's avatar
Mark Otto committed
235
236
237
      }
    },

XhmikosR's avatar
XhmikosR committed
238
    cssmin: {
XhmikosR's avatar
XhmikosR committed
239
      options: {
240
        compatibility: 'ie8',
241
242
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
243
      },
244
245
246
247
248
249
250
      minifyCore: {
        src: 'dist/css/<%= pkg.name %>.css',
        dest: 'dist/css/<%= pkg.name %>.min.css'
      },
      minifyTheme: {
        src: 'dist/css/<%= pkg.name %>-theme.css',
        dest: 'dist/css/<%= pkg.name %>-theme.min.css'
251
      },
XhmikosR's avatar
XhmikosR committed
252
      docs: {
XhmikosR's avatar
XhmikosR committed
253
        src: [
254
255
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
256
        ],
257
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
258
259
260
      }
    },

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

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

Mark Otto's avatar
Mark Otto committed
293
294
295
    copy: {
      fonts: {
        expand: true,
296
        src: 'fonts/*',
Mark Otto's avatar
Mark Otto committed
297
        dest: 'dist/'
298
      },
Mark Otto's avatar
Mark Otto committed
299
      docs: {
300
        expand: true,
Mark Otto's avatar
Mark Otto committed
301
302
        cwd: './dist',
        src: [
XhmikosR's avatar
XhmikosR committed
303
304
          'css/*',
          'js/*',
Mark Otto's avatar
Mark Otto committed
305
306
307
          'fonts/*'
        ],
        dest: 'docs/dist'
Mark Otto's avatar
Mark Otto committed
308
309
310
      }
    },

311
312
313
314
315
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
316
        }
317
318
319
      }
    },

320
321
322
323
    jekyll: {
      docs: {}
    },

324
    jade: {
325
326
327
328
329
330
331
332
333
334
335
      options: {
        pretty: true,
        data: getLessVarsData
      },
      customizerVars: {
        src: 'docs/_jade/customizer-variables.jade',
        dest: 'docs/_includes/customizer-variables.html'
      },
      customizerNav: {
        src: 'docs/_jade/customizer-nav.jade',
        dest: 'docs/_includes/nav/customize.html'
336
337
338
      }
    },

339
340
    validation: {
      options: {
341
342
        charset: 'utf-8',
        doctype: 'HTML5',
343
        failHard: true,
344
345
        reset: true,
        relaxerror: [
XhmikosR's avatar
XhmikosR committed
346
          'Bad value X-UA-Compatible for attribute http-equiv on element meta.',
347
          'Element img is missing required attribute src.',
348
349
          'Attribute autocomplete not allowed on element input at this point.',
          'Attribute autocomplete not allowed on element button at this point.'
350
        ]
351
352
      },
      files: {
353
        src: '_gh_pages/**/*.html'
354
355
356
      }
    },

357
358
    watch: {
      src: {
359
        files: '<%= jshint.core.src %>',
360
        tasks: ['jshint:src', 'qunit', 'concat']
361
362
363
364
365
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
366
      less: {
367
        files: 'less/**/*.less',
368
        tasks: 'less'
369
      }
370
371
372
373
374
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
375
376
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
377
378
379
380
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
381
382
383
384
385
386
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
387
          concurrency: 10,
388
          maxRetries: 3,
389
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
390
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
391
392
        }
      }
Chris Rebert's avatar
Chris Rebert committed
393
394
395
396
397
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
398
      }
399
400
    }
  });
401
402


403
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
404
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
405
  require('time-grunt')(grunt);
406

407
  // Docs HTML validation task
408
  grunt.registerTask('validate-html', ['jekyll', 'validation']);
409

410
411
412
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
413
414
415
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
416

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

440
  // JS distribution task.
441
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
442

443
  // CSS distribution task.
444
  grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
445
  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
Mark Otto's avatar
Mark Otto committed
446

447
  // Full distribution task.
448
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
449

450
  // Default task.
451
  grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
452

453
454
455
  // 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!
456
  grunt.registerTask('change-version-number', 'sed');
457

458
  // task for building customizer
459
460
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
461
462
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
463
    generateRawFiles(grunt, banner);
464
  });
Chris Rebert's avatar
Chris Rebert committed
465

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

472
473
474
475
476
477
478
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
  grunt.registerTask('lint-docs-css', ['csslint:docs', 'csslint:examples']);
  grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
  grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-customizer']);

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