• Gleb Mazovetskiy's avatar
    Compile with Ruby Sass or libsass. · 8a51daf2
    Gleb Mazovetskiy authored
    * Separate configs for libsass and sass.
    * Sass compiler selected based on `process.env.TWBS_SASS`.
    * Travis:
      * Use Gemfile to manage ruby dependencies.
      * Run core tests with both Sass compilers.
      * Only install/cache ruby gems required by the test subset.
    * Grunt: `update-gemfile-lock` task a la `update-shrinkwrap`.
    8a51daf2
Gruntfile.js 12.43 KiB
/*!
 * 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 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'
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' }, docsJs: { src: configBridge.paths.docsJs, dest: 'docs/assets/js/docs.min.js' } },
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
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' } }, autoprefixer: { options: { browsers: [ 'Android 2.3', 'Android >= 4', 'Chrome >= 20', 'Firefox >= 31', 'Explorer >= 9', 'iOS >= 7', 'Opera >= 12', 'Safari >= 6' ] }, 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' } },
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
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' } } }, 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.', 'Element div not allowed as child of element progress in this context.', 'Element thead not allowed as child of element table in this context.', 'Bad value tablist for attribute role on element nav.' ] }, files: { src: '_gh_pages/**/*.html' } },
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
watch: { src: { files: '<%= jshint.core.src %>', tasks: ['jshint:src', 'qunit', 'concat'] }, test: { files: '<%= jshint.test.src %>', tasks: ['jshint:test', 'qunit'] }, less: { files: 'less/**/*.less', tasks: 'less' }, docs: { files: 'docs/assets/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, concurrency: 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' }, 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', 'validation']); var runSubset = function (subset) { return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
}; 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', 'uglify:core', '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', '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']);
421422423424425426427428429430431432433434435436437438439440
// 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']); };