Gruntfile.js 12.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 npmShrinkwrap = require('npm-shrinkwrap');
21
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
22
  var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
23

24
25
26
27
28
  // Project configuration.
  grunt.initConfig({

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

    // Task configuration.
    clean: {
Chris Rebert's avatar
Chris Rebert committed
39
      dist: ['dist', 'docs/dist']
40
41
42
43
44
45
    },

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
Chris Rebert's avatar
Chris Rebert committed
46
      grunt: {
47
        options: {
48
          jshintrc: 'grunt/.jshintrc'
49
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
50
        src: ['Gruntfile.js', 'grunt/*.js']
51
52
      },
      src: {
53
        src: 'js/*.js'
54
55
      },
      test: {
XhmikosR's avatar
XhmikosR committed
56
57
58
        options: {
          jshintrc: 'js/tests/unit/.jshintrc'
        },
59
        src: 'js/tests/unit/*.js'
60
61
      },
      assets: {
62
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
63
64
      }
    },
65

Chris Rebert's avatar
Chris Rebert committed
66
67
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
68
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
69
      },
Chris Rebert's avatar
Chris Rebert committed
70
      grunt: {
71
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
72
73
      },
      src: {
74
        src: '<%= jshint.src.src %>'
Chris Rebert's avatar
Chris Rebert committed
75
76
      },
      test: {
77
        src: '<%= jshint.test.src %>'
78
79
      },
      assets: {
80
81
82
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
83
        src: '<%= jshint.assets.src %>'
Chris Rebert's avatar
Chris Rebert committed
84
85
86
      }
    },

87
88
    concat: {
      options: {
Zlatan Vasović's avatar
Zlatan Vasović committed
89
        banner: '<%= banner %>\n<%= jqueryCheck %>',
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
        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'
      }
    },
110

111
    uglify: {
112
113
114
      options: {
        preserveComments: 'some'
      },
115
      bootstrap: {
116
        src: '<%= concat.bootstrap.dest %>',
117
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
118
119
120
      },
      customize: {
        src: [
121
122
123
124
125
          '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',
126
          'docs/assets/js/raw-files.min.js',
127
          'docs/assets/js/src/customizer.js'
XhmikosR's avatar
XhmikosR committed
128
        ],
129
        dest: 'docs/assets/js/customize.min.js'
130
131
      },
      docsJs: {
132
        // NOTE: This src list is duplicated in footer.html; if making changes here, be sure to update the other copy too.
133
        src: [
134
135
          'docs/assets/js/vendor/holder.js',
          'docs/assets/js/vendor/ZeroClipboard.min.js',
136
          'docs/assets/js/src/application.js'
137
138
        ],
        dest: 'docs/assets/js/docs.min.js'
139
140
141
      }
    },

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

149
    less: {
150
151
      compileCore: {
        options: {
152
          strictMath: true,
153
154
155
156
157
158
159
160
161
162
163
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
        },
        files: {
          'dist/css/<%= pkg.name %>.css': 'less/bootstrap.less'
        }
      },
      compileTheme: {
        options: {
164
          strictMath: true,
165
166
167
168
169
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
170
171
172
        files: {
          'dist/css/<%= pkg.name %>-theme.css': 'less/theme.less'
        }
173
174
175
      }
    },

Bas Bosman's avatar
Bas Bosman committed
176
177
    autoprefixer: {
      options: {
Bas Bosman's avatar
Bas Bosman committed
178
179
180
181
182
183
184
185
186
187
        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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
202
        src: 'docs/assets/css/src/docs.css'
Bas Bosman's avatar
Bas Bosman committed
203
204
205
206
207
208
209
210
211
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

Mark Otto's avatar
Mark Otto committed
212
213
214
215
216
217
218
219
220
221
222
223
224
    csslint: {
      options: {
        csslintrc: 'less/.csslintrc'
      },
      src: [
        'dist/css/bootstrap.css',
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
        'docs/examples/**/*.css'
      ],
      docs: {
        options: {
225
          ids: false,
Mark Otto's avatar
Mark Otto committed
226
227
          'overqualified-elements': false
        },
228
        src: 'docs/assets/css/src/docs.css'
Mark Otto's avatar
Mark Otto committed
229
230
231
      }
    },

XhmikosR's avatar
XhmikosR committed
232
    cssmin: {
XhmikosR's avatar
XhmikosR committed
233
      options: {
234
        compatibility: 'ie8',
235
236
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
237
      },
238
239
240
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css',
XhmikosR's avatar
XhmikosR committed
241
          'dist/css/<%= pkg.name %>-theme.min.css': 'dist/css/<%= pkg.name %>-theme.css'
242
243
        }
      },
XhmikosR's avatar
XhmikosR committed
244
      docs: {
XhmikosR's avatar
XhmikosR committed
245
        src: [
246
247
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
248
        ],
249
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
250
251
252
      }
    },

253
    usebanner: {
XhmikosR's avatar
XhmikosR committed
254
255
256
257
258
259
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
260
261
262
      }
    },

263
    csscomb: {
264
265
266
267
      options: {
        config: 'less/.csscomb.json'
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
268
269
270
271
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
272
273
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
274
275
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
276
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
277
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
278
279
280
      },
      docs: {
        files: {
281
          'docs/assets/css/src/docs.css': 'docs/assets/css/src/docs.css'
XhmikosR's avatar
XhmikosR committed
282
        }
283
284
285
      }
    },

Mark Otto's avatar
Mark Otto committed
286
287
288
    copy: {
      fonts: {
        expand: true,
289
        src: 'fonts/*',
Mark Otto's avatar
Mark Otto committed
290
        dest: 'dist/'
291
      },
Mark Otto's avatar
Mark Otto committed
292
      docs: {
293
        expand: true,
Mark Otto's avatar
Mark Otto committed
294
295
        cwd: './dist',
        src: [
XhmikosR's avatar
XhmikosR committed
296
297
          'css/*',
          'js/*',
Mark Otto's avatar
Mark Otto committed
298
299
300
          'fonts/*'
        ],
        dest: 'docs/dist'
Mark Otto's avatar
Mark Otto committed
301
302
303
      }
    },

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

313
314
315
316
    jekyll: {
      docs: {}
    },

317
318
319
320
321
322
    jade: {
      compile: {
        options: {
          pretty: true,
          data: function () {
            var filePath = path.join(__dirname, 'less/variables.less');
XhmikosR's avatar
XhmikosR committed
323
            var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
324
            var parser = new BsLessdocParser(fileContent);
XhmikosR's avatar
XhmikosR committed
325
            return { sections: parser.parseFile() };
326
327
328
          }
        },
        files: {
329
330
          'docs/_includes/customizer-variables.html': 'docs/_jade/customizer-variables.jade',
          'docs/_includes/nav/customize.html': 'docs/_jade/customizer-nav.jade'
331
332
333
334
        }
      }
    },

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

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

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

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

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
394
      }
395
396
    }
  });
397
398


399
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
400
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
401
  require('time-grunt')(grunt);
402

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

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

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

436
437
  // JS distribution task.
  grunt.registerTask('dist-js', ['concat', 'uglify']);
438

439
  // CSS distribution task.
440
  grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
441
  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer', 'usebanner', 'csscomb', 'cssmin']);
442

Mark Otto's avatar
derp    
Mark Otto committed
443
  // Docs distribution task.
444
  grunt.registerTask('dist-docs', 'copy:docs');
Mark Otto's avatar
Mark Otto committed
445

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

449
  // Default task.
450
  grunt.registerTask('default', ['test', 'dist', 'build-customizer']);
451

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

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

465
466
467
468
469
470
471
  // 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) {
472
        grunt.fail.warn(err);
473
474
475
476
477
478
479
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
480
};