2 * Bootstrap's Gruntfile
3 * http://getbootstrap.com
4 * Copyright 2013-2015 Twitter, Inc.
5 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8 module.exports = function (grunt) {
11 // Force use of Unix newlines
12 grunt.util.linefeed = '\n';
14 RegExp.quote = function (string) {
15 return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
18 var fs = require('fs');
19 var path = require('path');
20 var npmShrinkwrap = require('npm-shrinkwrap');
21 var generateGlyphiconsData = require('./grunt/bs-glyphicons-data-generator.js');
22 var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
23 var getLessVarsData = function () {
24 var filePath = path.join(__dirname, 'less/variables.less');
25 var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
26 var parser = new BsLessdocParser(fileContent);
27 return { sections: parser.parseFile() };
29 var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
30 var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
31 var configBridge = grunt.file.readJSON('./grunt/configBridge.json', { encoding: 'utf8' });
33 Object.keys(configBridge.paths).forEach(function (key) {
34 configBridge.paths[key].forEach(function (val, i, arr) {
35 arr[i] = path.join('./docs/assets', val);
39 // Project configuration.
43 pkg: grunt.file.readJSON('package.json'),
45 ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
46 ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
47 ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
49 jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
50 jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\n'),
52 // Task configuration.
60 jshintrc: 'js/.jshintrc'
64 jshintrc: 'grunt/.jshintrc'
66 src: ['Gruntfile.js', 'grunt/*.js']
73 jshintrc: 'js/tests/unit/.jshintrc'
75 src: 'js/tests/unit/*.js'
78 src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
87 src: '<%= jshint.grunt.src %>'
90 src: '<%= jshint.core.src %>'
93 src: '<%= jshint.test.src %>'
97 requireCamelCaseOrUpperCaseIdentifiers: null
99 src: '<%= jshint.assets.src %>'
105 banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
123 dest: 'dist/js/<%= pkg.name %>.js'
129 preserveComments: 'some'
132 src: '<%= concat.bootstrap.dest %>',
133 dest: 'dist/js/<%= pkg.name %>.min.js'
136 src: configBridge.paths.customizerJs,
137 dest: 'docs/assets/js/customize.min.js'
140 src: configBridge.paths.docsJs,
141 dest: 'docs/assets/js/docs.min.js'
147 inject: 'js/tests/unit/phantom.js'
149 files: 'js/tests/index.html'
157 outputSourceFiles: true,
158 sourceMapURL: '<%= pkg.name %>.css.map',
159 sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
161 src: 'less/bootstrap.less',
162 dest: 'dist/css/<%= pkg.name %>.css'
168 outputSourceFiles: true,
169 sourceMapURL: '<%= pkg.name %>-theme.css.map',
170 sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
172 src: 'less/theme.less',
173 dest: 'dist/css/<%= pkg.name %>-theme.css'
179 browsers: configBridge.config.autoprefixerBrowsers
185 src: 'dist/css/<%= pkg.name %>.css'
191 src: 'dist/css/<%= pkg.name %>-theme.css'
194 src: 'docs/assets/css/src/docs.css'
198 cwd: 'docs/examples/',
200 dest: 'docs/examples/'
206 csslintrc: 'less/.csslintrc'
209 'dist/css/bootstrap.css',
210 'dist/css/bootstrap-theme.css'
213 'docs/examples/**/*.css'
218 'overqualified-elements': false
220 src: 'docs/assets/css/src/docs.css'
226 compatibility: 'ie8',
227 keepSpecialComments: '*',
231 src: 'dist/css/<%= pkg.name %>.css',
232 dest: 'dist/css/<%= pkg.name %>.min.css'
235 src: 'dist/css/<%= pkg.name %>-theme.css',
236 dest: 'dist/css/<%= pkg.name %>-theme.min.css'
240 'docs/assets/css/src/docs.css',
241 'docs/assets/css/src/pygments-manni.css'
243 dest: 'docs/assets/css/docs.min.css'
250 banner: '<%= banner %>'
253 src: 'dist/css/*.css'
259 config: 'less/.csscomb.json'
264 src: ['*.css', '!*.min.css'],
269 cwd: 'docs/examples/',
271 dest: 'docs/examples/'
274 src: 'docs/assets/css/src/docs.css',
275 dest: 'docs/assets/css/src/docs.css'
301 config: '_config.yml'
314 data: getLessVarsData
317 src: 'docs/_jade/customizer-variables.jade',
318 dest: 'docs/_includes/customizer-variables.html'
321 src: 'docs/_jade/customizer-nav.jade',
322 dest: 'docs/_includes/nav/customize.html'
333 'Element img is missing required attribute src.',
334 'Attribute autocomplete not allowed on element input at this point.',
335 'Attribute autocomplete not allowed on element button at this point.',
336 'Bad value separator for attribute role on element li.'
340 src: '_gh_pages/**/*.html'
346 files: '<%= jshint.core.src %>',
347 tasks: ['jshint:src', 'qunit', 'concat']
350 files: '<%= jshint.test.src %>',
351 tasks: ['jshint:test', 'qunit']
354 files: 'less/**/*.less',
361 pattern: (function () {
362 var old = grunt.option('oldver');
363 return old ? RegExp.quote(old) : old;
365 replacement: grunt.option('newver'),
373 build: process.env.TRAVIS_JOB_ID,
377 urls: ['http://127.0.0.1:3000/js/tests/index.html'],
378 browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
385 command: 'npm update'
392 archive: 'bootstrap-<%= pkg.version %>-dist.zip',
402 dest: 'bootstrap-<%= pkg.version %>-dist'
411 // These plugins provide necessary tasks.
412 require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
413 require('time-grunt')(grunt);
415 // Docs HTML validation task
416 grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
418 var runSubset = function (subset) {
419 return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
421 var isUndefOrNonZero = function (val) {
422 return val === undefined || val !== '0';
426 var testSubtasks = [];
427 // Skip core tests if running a different subset of the test suite
428 if (runSubset('core') &&
429 // Skip core tests if this is a Savage build
430 process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
431 testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
433 // Skip HTML validation if running a different subset of the test suite
434 if (runSubset('validate-html') &&
435 // Skip HTML5 validator on Travis when [skip validator] is in the commit message
436 isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
437 testSubtasks.push('validate-html');
439 // Only run Sauce Labs tests if there's a Sauce access key
440 if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
441 // Skip Sauce if running a different subset of the test suite
442 runSubset('sauce-js-unit') &&
443 // Skip Sauce on Travis when [skip sauce] is in the commit message
444 isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
445 testSubtasks.push('connect');
446 testSubtasks.push('saucelabs-qunit');
448 grunt.registerTask('test', testSubtasks);
449 grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
451 // JS distribution task.
452 grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
454 // CSS distribution task.
455 grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
456 grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
458 // Full distribution task.
459 grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
462 grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
464 // Version numbering task.
465 // grunt change-version-number --oldver=A.B.C --newver=X.Y.Z
466 // This can be overzealous, so its changes should always be manually reviewed!
467 grunt.registerTask('change-version-number', 'sed');
469 grunt.registerTask('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); });
471 // task for building customizer
472 grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
473 grunt.registerTask('build-customizer-html', 'jade');
474 grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
475 var banner = grunt.template.process('<%= banner %>');
476 generateRawFiles(grunt, banner);
479 grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
480 var srcFiles = grunt.config.get('concat.bootstrap.src');
481 var destFilepath = 'dist/js/npm.js';
482 generateCommonJSModule(grunt, srcFiles, destFilepath);
486 grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
487 grunt.registerTask('lint-docs-css', ['csslint:docs', 'csslint:examples']);
488 grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
489 grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
490 grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);
492 grunt.registerTask('prep-release', ['jekyll:github', 'compress']);
494 // Task for updating the cached npm packages used by the Travis build (which are controlled by test-infra/npm-shrinkwrap.json).
495 // This task should be run and the updated file should be committed whenever Bootstrap's dependencies change.
496 grunt.registerTask('update-shrinkwrap', ['exec:npmUpdate', '_update-shrinkwrap']);
497 grunt.registerTask('_update-shrinkwrap', function () {
498 var done = this.async();
499 npmShrinkwrap({ dev: true, dirname: __dirname }, function (err) {
501 grunt.fail.warn(err);
503 var dest = 'test-infra/npm-shrinkwrap.json';
504 fs.renameSync('npm-shrinkwrap.json', dest);
505 grunt.log.writeln('File ' + dest.cyan + ' updated.');