Gruntfile.js 12.9 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
36
  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);
    });
  });
37

38
39
40
41
42
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
43
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
44
45
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
46
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
47
            ' */\n',
48
49
    jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
    jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\n'),
50
51
52

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

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

Chris Rebert's avatar
Chris Rebert committed
81
82
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
83
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
84
      },
Chris Rebert's avatar
Chris Rebert committed
85
      grunt: {
86
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
87
      },
88
89
      core: {
        src: '<%= jshint.core.src %>'
Chris Rebert's avatar
Chris Rebert committed
90
91
      },
      test: {
92
        src: '<%= jshint.test.src %>'
93
94
      },
      assets: {
95
96
97
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
98
        src: '<%= jshint.assets.src %>'
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        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'
      }
    },
125

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

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

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

Bas Bosman's avatar
Bas Bosman committed
176
177
    autoprefixer: {
      options: {
178
        browsers: configBridge.config.autoprefixerBrowsers
Bas Bosman's avatar
Bas Bosman committed
179
180
181
182
183
184
185
186
187
188
189
190
191
192
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
193
        src: 'docs/assets/css/src/docs.css'
Bas Bosman's avatar
Bas Bosman committed
194
195
196
197
198
199
200
201
202
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

Mark Otto's avatar
Mark Otto committed
203
204
205
206
    csslint: {
      options: {
        csslintrc: 'less/.csslintrc'
      },
207
      dist: [
Mark Otto's avatar
Mark Otto committed
208
209
210
211
212
213
214
215
        'dist/css/bootstrap.css',
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
        'docs/examples/**/*.css'
      ],
      docs: {
        options: {
216
          ids: false,
Mark Otto's avatar
Mark Otto committed
217
218
          'overqualified-elements': false
        },
219
        src: 'docs/assets/css/src/docs.css'
Mark Otto's avatar
Mark Otto committed
220
221
222
      }
    },

XhmikosR's avatar
XhmikosR committed
223
    cssmin: {
XhmikosR's avatar
XhmikosR committed
224
      options: {
225
        compatibility: 'ie8',
226
227
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
228
      },
229
230
231
232
233
234
235
      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'
236
      },
XhmikosR's avatar
XhmikosR committed
237
      docs: {
XhmikosR's avatar
XhmikosR committed
238
        src: [
239
240
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
241
        ],
242
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
243
244
245
      }
    },

246
    usebanner: {
XhmikosR's avatar
XhmikosR committed
247
248
249
250
251
252
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
253
254
255
      }
    },

256
    csscomb: {
257
258
259
260
      options: {
        config: 'less/.csscomb.json'
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
261
262
263
264
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
265
266
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
267
268
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
269
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
270
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
271
272
      },
      docs: {
273
274
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
275
276
277
      }
    },

Mark Otto's avatar
Mark Otto committed
278
279
    copy: {
      fonts: {
280
        src: 'fonts/*',
Mark Otto's avatar
Mark Otto committed
281
        dest: 'dist/'
282
      },
Mark Otto's avatar
Mark Otto committed
283
      docs: {
vsn4ik's avatar
vsn4ik committed
284
285
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
286
287
288
      }
    },

289
290
291
292
293
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
294
        }
295
296
297
      }
    },

298
299
300
301
    jekyll: {
      docs: {}
    },

302
    jade: {
303
304
305
306
307
308
309
310
311
312
313
      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'
314
315
316
      }
    },

317
318
    validation: {
      options: {
319
320
        charset: 'utf-8',
        doctype: 'HTML5',
321
        failHard: true,
322
323
        reset: true,
        relaxerror: [
324
          'Element img is missing required attribute src.',
325
326
          'Attribute autocomplete not allowed on element input at this point.',
          'Attribute autocomplete not allowed on element button at this point.'
327
        ]
328
329
      },
      files: {
330
        src: '_gh_pages/**/*.html'
331
332
333
      }
    },

334
335
    watch: {
      src: {
336
        files: '<%= jshint.core.src %>',
337
        tasks: ['jshint:src', 'qunit', 'concat']
338
339
340
341
342
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
343
      less: {
344
        files: 'less/**/*.less',
345
        tasks: 'less'
346
      }
347
348
349
350
351
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
352
353
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
354
355
356
357
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
358
359
360
361
362
363
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
364
          concurrency: 10,
365
          maxRetries: 3,
366
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
367
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
368
369
        }
      }
Chris Rebert's avatar
Chris Rebert committed
370
371
372
373
374
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
375
      }
376
377
    }
  });
378
379


380
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
381
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
382
  require('time-grunt')(grunt);
383

384
  // Docs HTML validation task
385
  grunt.registerTask('validate-html', ['jekyll', 'validation']);
386

387
388
389
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
390
391
392
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
393

394
  // Test task.
395
396
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
397
  if (runSubset('core')) {
Chris Rebert's avatar
Chris Rebert committed
398
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
399
400
  }
  // Skip HTML validation if running a different subset of the test suite
401
402
403
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
404
405
    testSubtasks.push('validate-html');
  }
406
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
407
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
408
      // Skip Sauce if running a different subset of the test suite
409
410
411
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
412
413
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
414
415
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
416
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
417

418
  // JS distribution task.
419
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
420

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

425
  // Full distribution task.
426
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
427

428
  // Default task.
429
  grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
430

431
432
433
  // 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!
434
  grunt.registerTask('change-version-number', 'sed');
435

436
  // task for building customizer
437
438
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
439
440
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
441
    generateRawFiles(grunt, banner);
442
  });
Chris Rebert's avatar
Chris Rebert committed
443

444
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
445
446
447
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
448
449
  });

450
451
452
453
454
  // 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']);
455
  grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-customizer']);
456

457
458
459
460
461
462
463
  // 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) {
464
        grunt.fail.warn(err);
465
466
467
468
469
470
471
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
472
};