Gruntfile.js 11.8 KB
Newer Older
Chris Rebert's avatar
Chris Rebert committed
1
2
3
4
5
6
/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
 * Copyright 2013-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */
7

Chris Rebert's avatar
Chris Rebert committed
8
module.exports = function (grunt) {
XhmikosR's avatar
XhmikosR committed
9
  'use strict';
10

11
12
13
  // Force use of Unix newlines
  grunt.util.linefeed = '\n';

Zlatan Vasović's avatar
Zlatan Vasović committed
14
  RegExp.quote = function (string) {
Chris Rebert's avatar
Chris Rebert committed
15
16
    return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
  };
17

Chris Rebert's avatar
Chris Rebert committed
18
  var fs = require('fs');
19
  var path = require('path');
20
  var npmShrinkwrap = require('npm-shrinkwrap');
Mark Otto's avatar
Mark Otto committed
21

22
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
23
24
25
26
27
28
29
  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);
    });
  });
30

31
32
33
34
35
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
36
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
37
38
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
39
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
40
            ' */\n',
41
42
43
44
45
46
47
48
49
    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',
50
51
52

    // Task configuration.
    clean: {
53
54
      dist: 'dist',
      docs: 'docs/dist'
55
56
57
58
59
60
    },

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
Chris Rebert's avatar
Chris Rebert committed
61
      grunt: {
62
        options: {
63
          jshintrc: 'grunt/.jshintrc'
64
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
65
        src: ['Gruntfile.js', 'grunt/*.js']
66
      },
67
      core: {
68
        src: 'js/*.js'
69
70
      },
      test: {
XhmikosR's avatar
XhmikosR committed
71
72
73
        options: {
          jshintrc: 'js/tests/unit/.jshintrc'
        },
74
        src: 'js/tests/unit/*.js'
75
76
      },
      assets: {
77
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
78
79
      }
    },
80

Chris Rebert's avatar
Chris Rebert committed
81
82
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
83
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
84
      },
Chris Rebert's avatar
Chris Rebert committed
85
      grunt: {
86
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
87
      },
88
89
      core: {
        src: '<%= jshint.core.src %>'
Chris Rebert's avatar
Chris Rebert committed
90
91
      },
      test: {
92
        src: '<%= jshint.test.src %>'
93
94
      },
      assets: {
95
96
97
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
98
        src: '<%= jshint.assets.src %>'
Chris Rebert's avatar
Chris Rebert committed
99
100
101
      }
    },

102
103
    concat: {
      options: {
104
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        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'
      }
    },
125

126
    uglify: {
127
128
129
      options: {
        preserveComments: 'some'
      },
130
      core: {
131
        src: '<%= concat.bootstrap.dest %>',
132
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
133
      },
134
      docsJs: {
135
        src: configBridge.paths.docsJs,
136
        dest: 'docs/assets/js/docs.min.js'
137
138
139
      }
    },

Mark Otto's avatar
Mark Otto committed
140
141
142
143
144
145
146
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

Mark Otto's avatar
Mark Otto committed
147
148
149
150
    sass: {
      options: {
        includePaths: ['scss'],
        precision: 6,
Gleb Mazovetskiy's avatar
Gleb Mazovetskiy committed
151
        sourceComments: false,
Mark Otto's avatar
Mark Otto committed
152
153
        sourceMap: true
      },
Mark Otto's avatar
Mark Otto committed
154
      core: {
Mark Otto's avatar
Mark Otto committed
155
156
157
        files: {
          'dist/css/<%= pkg.name %>.css': 'scss/<%= pkg.name %>.scss'
        }
158
      },
Mark Otto's avatar
Mark Otto committed
159
160
      docs: {
        files: {
Mark Otto's avatar
Mark Otto committed
161
          'docs/assets/css/docs.min.css': 'docs/assets/scss/docs.scss'
Mark Otto's avatar
Mark Otto committed
162
        }
163
164
165
      }
    },

Chris Rebert's avatar
Chris Rebert committed
166
167
168
169
170
171
172
173
    scsslint: {
      scss: ['scss/*.scss', '!scss/_normalize.scss'],
      options: {
        config: 'scss/.scss-lint.yml',
        reporterOutput: 'scss-lint-report.xml'
      }
    },

Bas Bosman's avatar
Bas Bosman committed
174
175
    autoprefixer: {
      options: {
176
177
178
179
180
181
182
183
184
185
        browsers: [
          'Android 2.3',
          'Android >= 4',
          'Chrome >= 20',
          'Firefox >= 31',
          'Explorer >= 9',
          'iOS >= 6',
          'Opera >= 12',
          'Safari >= 6'
        ]
Bas Bosman's avatar
Bas Bosman committed
186
187
188
189
190
191
192
193
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
194
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
195
196
197
198
199
200
201
202
203
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
204
    cssmin: {
XhmikosR's avatar
XhmikosR committed
205
      options: {
206
207
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
208
      },
Mark Otto's avatar
Mark Otto committed
209
210
211
212
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
213
      },
XhmikosR's avatar
XhmikosR committed
214
      docs: {
Mark Otto's avatar
Mark Otto committed
215
        src: 'docs/assets/css/docs.min.css',
216
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
217
218
219
      }
    },

220
    usebanner: {
XhmikosR's avatar
XhmikosR committed
221
222
223
224
225
226
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
227
228
229
      }
    },

230
    csscomb: {
231
      options: {
Mark Otto's avatar
Mark Otto committed
232
        config: 'scss/.csscomb.json'
233
234
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
235
236
237
238
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
239
240
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
241
242
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
243
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
244
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
245
246
      },
      docs: {
247
248
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
249
250
251
      }
    },

Mark Otto's avatar
Mark Otto committed
252
    copy: {
Mark Otto's avatar
Mark Otto committed
253
      docs: {
vsn4ik's avatar
vsn4ik committed
254
255
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
256
257
258
      }
    },

259
260
261
262
263
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
264
        }
265
266
267
      }
    },

268
    jekyll: {
269
270
271
272
273
274
275
276
277
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
278
279
280
281
    },

    validation: {
      options: {
282
283
        charset: 'utf-8',
        doctype: 'HTML5',
284
        failHard: true,
285
286
        reset: true,
        relaxerror: [
287
          'Element img is missing required attribute src.',
288
          'Attribute autocomplete not allowed on element input at this point.',
Mark Otto's avatar
Mark Otto committed
289
290
291
292
          '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.'
293
        ]
294
295
      },
      files: {
296
        src: '_gh_pages/**/*.html'
297
298
299
      }
    },

300
301
    watch: {
      src: {
302
        files: '<%= jshint.core.src %>',
303
        tasks: ['jshint:src', 'qunit', 'concat']
304
305
306
307
308
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
309
      less: {
310
        files: 'less/**/*.less',
311
        tasks: 'less'
Mark Otto's avatar
Mark Otto committed
312
313
314
315
      },
      docs: {
        files: 'docs/assets/less/*.less',
        tasks: 'less'
316
      }
317
318
319
320
321
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
322
323
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
324
325
326
327
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
328
329
330
331
332
333
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
334
          concurrency: 10,
335
          maxRetries: 3,
336
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
337
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
338
339
        }
      }
Chris Rebert's avatar
Chris Rebert committed
340
341
342
343
344
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
345
      }
346
347
    }
  });
348
349


350
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
351
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
352
  require('time-grunt')(grunt);
353

354
  // Docs HTML validation task
355
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
356

357
358
359
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
360
361
362
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
363

364
  // Test task.
365
366
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
367
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
368
    // Skip core tests if this is a Savage build
Chris Rebert's avatar
Chris Rebert committed
369
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
370
371
  }
  // Skip HTML validation if running a different subset of the test suite
372
373
374
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
375
376
    testSubtasks.push('validate-html');
  }
377
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
378
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
379
      // Skip Sauce if running a different subset of the test suite
380
381
382
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
383
384
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
385
386
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
387
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
388

389
  // JS distribution task.
390
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
391

Chris Rebert's avatar
Chris Rebert committed
392
393
  grunt.registerTask('test-scss', ['scsslint:scss']);

394
  // CSS distribution task.
Mark Otto's avatar
Mark Otto committed
395
396
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
  grunt.registerTask('dist-css', ['sass-compile', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
397

398
  // Full distribution task.
Mark Otto's avatar
Mark Otto committed
399
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'dist-js']);
400

401
  // Default task.
Mark Otto's avatar
Mark Otto committed
402
  grunt.registerTask('default', ['clean:dist', 'test']);
403

404
405
406
  // 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!
407
  grunt.registerTask('change-version-number', 'sed');
408

409
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
410
411
412
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
413
414
  });

415
416
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
417
  grunt.registerTask('docs-js', ['uglify:docsJs']);
418
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
Mark Otto's avatar
Mark Otto committed
419
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
420

421
422
  grunt.registerTask('docs-github', ['jekyll:github']);

423
424
425
426
427
428
429
  // 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) {
430
        grunt.fail.warn(err);
431
432
433
434
435
436
437
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
438
};