Gruntfile.js 13.89 KiB
/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
 * Copyright 2013-2015 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 npmShrinkwrap = require('npm-shrinkwrap');
  var generateGlyphiconsData = require('./grunt/bs-glyphicons-data-generator.js');
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
  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() };
  var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
  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: configBridge.config.jqueryCheck.join('\n'),
    jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\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'
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
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/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' } }, uglify: { options: { preserveComments: 'some' }, core: { src: '<%= concat.bootstrap.dest %>', dest: 'dist/js/<%= pkg.name %>.min.js' }, customize: { src: configBridge.paths.customizerJs, dest: 'docs/assets/js/customize.min.js' }, docsJs: { src: configBridge.paths.docsJs,
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
dest: 'docs/assets/js/docs.min.js' } }, qunit: { options: { inject: 'js/tests/unit/phantom.js' }, files: 'js/tests/index.html' }, less: { compileCore: { options: { strictMath: true, sourceMap: true, outputSourceFiles: true, sourceMapURL: '<%= pkg.name %>.css.map', sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map' }, src: 'less/bootstrap.less', dest: 'dist/css/<%= pkg.name %>.css' }, compileTheme: { options: { strictMath: true, sourceMap: true, outputSourceFiles: true, sourceMapURL: '<%= pkg.name %>-theme.css.map', sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map' }, src: 'less/theme.less', dest: 'dist/css/<%= pkg.name %>-theme.css' } }, autoprefixer: { options: { browsers: configBridge.config.autoprefixerBrowsers }, core: { options: { map: true }, src: 'dist/css/<%= pkg.name %>.css' }, theme: { options: { map: true }, src: 'dist/css/<%= pkg.name %>-theme.css' }, docs: { src: 'docs/assets/css/src/docs.css' }, examples: { expand: true, cwd: 'docs/examples/', src: ['**/*.css'], dest: 'docs/examples/' } }, csslint: { options: { csslintrc: 'less/.csslintrc' }, dist: [ 'dist/css/bootstrap.css', 'dist/css/bootstrap-theme.css'
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
], examples: [ 'docs/examples/**/*.css' ], docs: { options: { ids: false, 'overqualified-elements': false }, src: 'docs/assets/css/src/docs.css' } }, cssmin: { options: { compatibility: 'ie8', keepSpecialComments: '*', advanced: false }, 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' }, docs: { src: [ 'docs/assets/css/src/docs.css', 'docs/assets/css/src/pygments-manni.css' ], dest: 'docs/assets/css/docs.min.css' } }, usebanner: { options: { position: 'top', banner: '<%= banner %>' }, files: { src: 'dist/css/*.css' } }, csscomb: { options: { config: 'less/.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: { fonts: {
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
src: 'fonts/*', dest: 'dist/' }, docs: { src: 'dist/*/*', dest: 'docs/' } }, connect: { server: { options: { port: 3000, base: '.' } } }, jekyll: { options: { config: '_config.yml' }, docs: {}, github: { options: { raw: 'github: true' } } }, jade: { 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' } }, validation: { options: { charset: 'utf-8', doctype: 'HTML5', failHard: true, reset: true, relaxerror: [ 'Element img is missing required attribute src.', 'Attribute autocomplete not allowed on element input at this point.', 'Attribute autocomplete not allowed on element button at this point.', 'Bad value separator for attribute role on element li.' ] }, files: { src: '_gh_pages/**/*.html' } }, watch: { src: { files: '<%= jshint.core.src %>', tasks: ['jshint:src', 'qunit', 'concat'] }, test: { files: '<%= jshint.test.src %>',
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
tasks: ['jshint:test', 'qunit'] }, less: { files: 'less/**/*.less', tasks: 'less' } }, 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, throttled: 10, maxRetries: 3, urls: ['http://127.0.0.1:3000/js/tests/index.html'], browsers: grunt.file.readYAML('grunt/sauce_browsers.yml') } } }, exec: { npmUpdate: { command: 'npm update' } }, compress: { main: { options: { archive: 'bootstrap-<%= pkg.version %>-dist.zip', mode: 'zip', level: 9, pretty: true }, files: [ { expand: true, cwd: 'dist/', src: ['**'], dest: 'bootstrap-<%= pkg.version %>-dist' } ] } } }); // These plugins provide necessary tasks. require('load-grunt-tasks')(grunt, { scope: 'devDependencies' }); require('time-grunt')(grunt); // Docs HTML validation task grunt.registerTask('validate-html', ['jekyll:docs', 'validation']); var runSubset = function (subset) { return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset; }; var isUndefOrNonZero = function (val) {
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
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', 'csslint:dist', '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', 'uglify:core', 'commonjs']); // CSS distribution task. grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']); grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']); // Full distribution task. grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']); // Default task. grunt.registerTask('default', ['clean:dist', 'copy:fonts', '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('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); }); // task for building customizer grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']); grunt.registerTask('build-customizer-html', 'jade'); grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () { var banner = grunt.template.process('<%= banner %>'); generateRawFiles(grunt, banner); }); 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('lint-docs-css', ['csslint:docs', 'csslint:examples']); grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']); grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']); grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);
491492493494495496497498499500501502503504505506507508509
grunt.registerTask('prep-release', ['jekyll:github', 'compress']); // 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(); }); }); };