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/anchor.css', '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 // TODO: disable `zeroUnits` optimization once clean-css 3.2 is released
227 // and then simplify the fix for https://github.com/twbs/bootstrap/issues/14837 accordingly
228 compatibility: 'ie8',
229 keepSpecialComments: '*',
233 src: 'dist/css/<%= pkg.name %>.css',
234 dest: 'dist/css/<%= pkg.name %>.min.css'
237 src: 'dist/css/<%= pkg.name %>-theme.css',
238 dest: 'dist/css/<%= pkg.name %>-theme.min.css'
242 'docs/assets/css/src/pygments-manni.css',
243 'docs/assets/css/src/anchor.css',
244 'docs/assets/css/src/docs.css'
247 dest: 'docs/assets/css/docs.min.css'
254 banner: '<%= banner %>'
257 src: 'dist/css/*.css'
263 config: 'less/.csscomb.json'
268 src: ['*.css', '!*.min.css'],
273 cwd: 'docs/examples/',
275 dest: 'docs/examples/'
278 src: 'docs/assets/css/src/docs.css',
279 dest: 'docs/assets/css/src/docs.css'
310 config: '_config.yml'
323 data: getLessVarsData
326 src: 'docs/_jade/customizer-variables.jade',
327 dest: 'docs/_includes/customizer-variables.html'
330 src: 'docs/_jade/customizer-nav.jade',
331 dest: 'docs/_includes/nav/customize.html'
338 'Attribute "autocomplete" not allowed on element "button" at this point.',
339 'Attribute "autocomplete" not allowed on element "input" at this point.',
340 'Element "img" is missing required attribute "src".'
343 src: '_gh_pages/**/*.html'
348 files: '<%= jshint.core.src %>',
349 tasks: ['jshint:src', 'qunit', 'concat']
352 files: '<%= jshint.test.src %>',
353 tasks: ['jshint:test', 'qunit']
356 files: 'less/**/*.less',
363 pattern: (function () {
364 var old = grunt.option('oldver');
365 return old ? RegExp.quote(old) : old;
367 replacement: grunt.option('newver'),
375 build: process.env.TRAVIS_JOB_ID,
379 urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
380 browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
387 command: 'npm update'
394 archive: 'bootstrap-<%= pkg.version %>-dist.zip',
404 dest: 'bootstrap-<%= pkg.version %>-dist'
413 // These plugins provide necessary tasks.
414 require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
415 require('time-grunt')(grunt);
417 // Docs HTML validation task
418 grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
420 var runSubset = function (subset) {
421 return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
423 var isUndefOrNonZero = function (val) {
424 return val === undefined || val !== '0';
428 var testSubtasks = [];
429 // Skip core tests if running a different subset of the test suite
430 if (runSubset('core') &&
431 // Skip core tests if this is a Savage build
432 process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
433 testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
435 // Skip HTML validation if running a different subset of the test suite
436 if (runSubset('validate-html') &&
437 // Skip HTML5 validator on Travis when [skip validator] is in the commit message
438 isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
439 testSubtasks.push('validate-html');
441 // Only run Sauce Labs tests if there's a Sauce access key
442 if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
443 // Skip Sauce if running a different subset of the test suite
444 runSubset('sauce-js-unit') &&
445 // Skip Sauce on Travis when [skip sauce] is in the commit message
446 isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
447 testSubtasks.push('connect');
448 testSubtasks.push('saucelabs-qunit');
450 grunt.registerTask('test', testSubtasks);
451 grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
453 // JS distribution task.
454 grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
456 // CSS distribution task.
457 grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
458 grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
460 // Full distribution task.
461 grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
464 grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
466 // Version numbering task.
467 // grunt change-version-number --oldver=A.B.C --newver=X.Y.Z
468 // This can be overzealous, so its changes should always be manually reviewed!
469 grunt.registerTask('change-version-number', 'sed');
471 grunt.registerTask('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); });
473 // task for building customizer
474 grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
475 grunt.registerTask('build-customizer-html', 'jade');
476 grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
477 var banner = grunt.template.process('<%= banner %>');
478 generateRawFiles(grunt, banner);
481 grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
482 var srcFiles = grunt.config.get('concat.bootstrap.src');
483 var destFilepath = 'dist/js/npm.js';
484 generateCommonJSModule(grunt, srcFiles, destFilepath);
488 grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
489 grunt.registerTask('lint-docs-css', ['csslint:docs', 'csslint:examples']);
490 grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
491 grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
492 grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);
494 grunt.registerTask('prep-release', ['jekyll:github', 'compress']);
496 // Task for updating the cached npm packages used by the Travis build (which are controlled by test-infra/npm-shrinkwrap.json).
497 // This task should be run and the updated file should be committed whenever Bootstrap's dependencies change.
498 grunt.registerTask('update-shrinkwrap', ['exec:npmUpdate', '_update-shrinkwrap']);
499 grunt.registerTask('_update-shrinkwrap', function () {
500 var done = this.async();
501 npmShrinkwrap({ dev: true, dirname: __dirname }, function (err) {
503 grunt.fail.warn(err);
505 var dest = 'test-infra/npm-shrinkwrap.json';
506 fs.renameSync('npm-shrinkwrap.json', dest);
507 grunt.log.writeln('File ' + dest.cyan + ' updated.');