0
0
mirror of https://github.com/twbs/bootstrap.git synced 2024-11-29 11:24:18 +01:00
Bootstrap/Gruntfile.js
2015-02-11 11:29:43 -08:00

494 lines
15 KiB
JavaScript

/*!
* Bootstrap's Gruntfile
* http://getbootstrap.com
* Copyright 2013-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
module.exports = function (grunt) {
'use strict';
// Force use of Unix newlines
grunt.util.linefeed = '\n';
RegExp.quote = function (string) {
return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
};
var fs = require('fs');
var path = require('path');
var glob = require('glob');
var npmShrinkwrap = require('npm-shrinkwrap');
var mq4HoverShim = require('mq4-hover-shim');
var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
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);
});
});
// Project configuration.
grunt.initConfig({
// Metadata.
pkg: grunt.file.readJSON('package.json'),
banner: '/*!\n' +
' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
' */\n',
jqueryCheck: 'if (typeof jQuery === \'undefined\') {\n' +
' throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery\')\n' +
'}\n',
jqueryVersionCheck: '+function ($) {\n' +
' var version = $.fn.jquery.split(\' \')[0].split(\'.\')\n' +
' if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) {\n' +
' throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery version 1.9.1 or higher\')\n' +
' }\n' +
'}(jQuery);\n\n',
// Task configuration.
clean: {
dist: 'dist',
docs: 'docs/dist'
},
jshint: {
options: {
jshintrc: 'js/.jshintrc'
},
grunt: {
options: {
jshintrc: 'grunt/.jshintrc'
},
src: ['Gruntfile.js', 'grunt/*.js']
},
core: {
src: 'js/*.js'
},
test: {
options: {
jshintrc: 'js/tests/unit/.jshintrc'
},
src: 'js/tests/unit/*.js'
},
assets: {
src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
}
},
jscs: {
options: {
config: 'js/.jscsrc'
},
grunt: {
src: '<%= jshint.grunt.src %>'
},
core: {
src: '<%= jshint.core.src %>'
},
test: {
src: '<%= jshint.test.src %>'
},
assets: {
options: {
requireCamelCaseOrUpperCaseIdentifiers: null
},
src: '<%= jshint.assets.src %>'
}
},
concat: {
options: {
banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
stripBanners: false
},
bootstrap: {
src: [
'js/util.js',
'js/alert.js',
'js/button.js',
'js/carousel.js',
'js/collapse.js',
'js/dropdown.js',
'js/modal.js',
'js/scrollspy.js',
'js/tooltip.js',
'js/popover.js',
'js/tab.js'
],
dest: 'dist/js/<%= pkg.name %>.js'
}
},
closureCompiler: {
options: {
compilerFile: require('superstartup-closure-compiler').getPath(),
checkModified: false,
compilerOpts: {
// jscomp_warning: 'reportUnknownTypes', someday - maybe we will get to 100% typed, this helps track those down
compilation_level: 'ADVANCED_OPTIMIZATIONS',
warning_level: 'verbose',
summary_detail_level: 3,
output_wrapper:
'"<%= banner %><%= jqueryCheck %><%= jqueryVersionCheck %>'
+ '(function($){%output%})(jQuery);"',
externs: 'js/externs/*.js'
},
execOpts: {
maxBuffer: 999999 * 1024
},
// [OPTIONAL] Java VM optimization options
// see https://code.google.com/p/closure-compiler/wiki/FAQ#What_are_the_recommended_Java_VM_command-line_options?
// Setting one of these to 'true' is strongly recommended,
// and can reduce compile times by 50-80% depending on compilation size
// and hardware.
// On server-class hardware, such as with Github's Travis hook,
// TieredCompilation should be used; on standard developer hardware,
// d32 may be better. Set as appropriate for your environment.
// Default for both is 'false'; do not set both to 'true'.
d32: false, // will use 'java -client -d32 -jar compiler.jar'
TieredCompilation: false // will use 'java -server -XX:+TieredCompilation -jar compiler.jar'
},
targetName: {
src: [
'js/util.js',
'js/alert.js',
'js/button.js',
'js/carousel.js',
'js/collapse.js',
'js/dropdown.js',
'js/modal.js',
'js/scrollspy.js',
'js/tooltip.js',
'js/popover.js',
'js/tab.js'
],
dest: 'dist/js/<%= pkg.name %>.min.js'
}
},
uglify: {
options: {
preserveComments: 'some'
},
docsJs: {
src: configBridge.paths.docsJs,
dest: 'docs/assets/js/docs.min.js'
}
},
qunit: {
options: {
inject: 'js/tests/unit/phantom.js'
},
files: 'js/tests/index.html'
},
scsslint: {
scss: ['scss/*.scss', '!scss/_normalize.scss'],
options: {
config: 'scss/.scss-lint.yml',
reporterOutput: 'scss-lint-report.xml'
}
},
postcss: {
options: {
map: true,
processors: [mq4HoverShim.postprocessorFor({hoverSelectorPrefix: '.bs-true-hover '})]
},
core: {
src: 'dist/css/<%= pkg.name %>.css'
}
},
autoprefixer: {
options: {
browsers: [
'Android 2.3',
'Android >= 4',
'Chrome >= 35',
'Firefox >= 31',
'Explorer >= 9',
'iOS >= 7',
'Opera >= 12',
'Safari >= 7.1'
]
},
core: {
options: {
map: true
},
src: 'dist/css/<%= pkg.name %>.css'
},
docs: {
src: 'docs/assets/css/docs.min.css'
},
examples: {
expand: true,
cwd: 'docs/examples/',
src: ['**/*.css'],
dest: 'docs/examples/'
}
},
cssmin: {
options: {
keepSpecialComments: '*',
noAdvanced: true
},
core: {
files: {
'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
}
},
docs: {
src: 'docs/assets/css/docs.min.css',
dest: 'docs/assets/css/docs.min.css'
}
},
usebanner: {
options: {
position: 'top',
banner: '<%= banner %>'
},
files: {
src: 'dist/css/*.css'
}
},
csscomb: {
options: {
config: 'scss/.csscomb.json'
},
dist: {
expand: true,
cwd: 'dist/css/',
src: ['*.css', '!*.min.css'],
dest: 'dist/css/'
},
examples: {
expand: true,
cwd: 'docs/examples/',
src: '**/*.css',
dest: 'docs/examples/'
},
docs: {
src: 'docs/assets/css/src/docs.css',
dest: 'docs/assets/css/src/docs.css'
}
},
copy: {
docs: {
src: 'dist/*/*',
dest: 'docs/'
}
},
connect: {
server: {
options: {
port: 3000,
base: '.'
}
}
},
jekyll: {
options: {
config: '_config.yml'
},
docs: {},
github: {
options: {
raw: 'github: true'
}
}
},
htmllint: {
options: {
ignore: [
'Element “img” is missing required attribute “src”.',
'Bad value “X-UA-Compatible” for attribute “http-equiv” on element “meta”.',
'Attribute “autocomplete” not allowed on element “input” at this point.',
'Attribute “autocomplete” not allowed on element “button” at this point.',
'Element “div” not allowed as child of element “progress” in this context. (Suppressing further errors from this subtree.)',
'Consider using the “h1” element as a top-level heading only (all “h1” elements are treated as top-level headings by many screen readers and other tools).'
]
},
src: '_gh_pages/**/*.html'
},
watch: {
src: {
files: '<%= jshint.core.src %>',
tasks: ['jshint:src', 'qunit', 'concat']
},
test: {
files: '<%= jshint.test.src %>',
tasks: ['jshint:test', 'qunit']
},
sass: {
files: 'scss/**/*.scss',
tasks: 'sass-compile'
},
docs: {
files: 'docs/assets/scss/*.scss',
tasks: 'sass:docs'
}
},
sed: {
versionNumber: {
pattern: (function () {
var old = grunt.option('oldver');
return old ? RegExp.quote(old) : old;
})(),
replacement: grunt.option('newver'),
recursive: true
}
},
'saucelabs-qunit': {
all: {
options: {
build: process.env.TRAVIS_JOB_ID,
concurrency: 10,
maxRetries: 3,
maxPollRetries: 4,
urls: ['http://127.0.0.1:3000/js/tests/index.html'],
browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
}
}
},
exec: {
npmUpdate: {
command: 'npm update'
},
bundleUpdate: {
command: function () {
// Update dev gems and all the test gemsets
return 'bundle update && ' + glob.sync('test-infra/gemfiles/*.gemfile').map(function (gemfile) {
return 'BUNDLE_GEMFILE=' + gemfile + ' bundle update';
}).join(' && ');
}
}
}
});
// These plugins provide necessary tasks.
require('load-grunt-tasks')(grunt, { scope: 'devDependencies',
// Exclude Sass compilers. We choose the one to load later on.
pattern: ['grunt-*', '!grunt-sass', '!grunt-contrib-sass'] });
require('time-grunt')(grunt);
// Docs HTML validation task
grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
var runSubset = function (subset) {
return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
};
var isUndefOrNonZero = function (val) {
return val === undefined || val !== '0';
};
// Test task.
var testSubtasks = [];
// Skip core tests if running a different subset of the test suite
if (runSubset('core') &&
// Skip core tests if this is a Savage build
process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
}
// Skip HTML validation if running a different subset of the test suite
if (runSubset('validate-html') &&
// Skip HTML5 validator on Travis when [skip validator] is in the commit message
isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
testSubtasks.push('validate-html');
}
// Only run Sauce Labs tests if there's a Sauce access key
if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
// Skip Sauce if running a different subset of the test suite
runSubset('sauce-js-unit') &&
// Skip Sauce on Travis when [skip sauce] is in the commit message
isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
testSubtasks.push('connect');
testSubtasks.push('saucelabs-qunit');
}
grunt.registerTask('test', testSubtasks);
grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
// JS distribution task.
grunt.registerTask('dist-js', ['concat', 'closureCompiler', 'commonjs']);
grunt.registerTask('test-scss', ['scsslint:scss']);
// CSS distribution task.
// Supported Compilers: sass (Ruby) and libsass.
(function (sassCompilerName) {
require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
})(process.env.TWBS_SASS || 'libsass');
grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
grunt.registerTask('dist-css', ['sass-compile', 'postcss:core', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
// Full distribution task.
grunt.registerTask('dist', ['clean:dist', 'dist-css', 'dist-js']);
// Default task.
grunt.registerTask('default', ['clean:dist', 'test']);
// 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!
grunt.registerTask('change-version-number', 'sed');
grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
var srcFiles = grunt.config.get('concat.bootstrap.src');
var destFilepath = 'dist/js/npm.js';
generateCommonJSModule(grunt, srcFiles, destFilepath);
});
// Docs task.
grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
grunt.registerTask('docs-js', ['uglify:docsJs']);
grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
grunt.registerTask('docs-github', ['jekyll:github']);
// 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) {
grunt.fail.warn(err);
}
var dest = 'test-infra/npm-shrinkwrap.json';
fs.renameSync('npm-shrinkwrap.json', dest);
grunt.log.writeln('File ' + dest.cyan + ' updated.');
done();
});
});
// Task for updating the cached RubyGem packages used by the Travis build (which are controlled by test-infra/Gemfile.lock).
// This task should be run and the updated file should be committed whenever Bootstrap's RubyGem dependencies change.
grunt.registerTask('update-gemfile-lock', ['exec:bundleUpdate']);
};