Gruntfile.js 12.4 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');
21
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
22
  var getLessVarsData = function () {
Mark Otto's avatar
Mark Otto committed
23
    var filePath = path.join(__dirname, 'less/_variables.less');
24
25
26
27
    var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
    var parser = new BsLessdocParser(fileContent);
    return { sections: parser.parseFile() };
  };
28
  var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
29
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
30
31
32
33
34
35
36
  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);
    });
  });
37

38
39
40
41
42
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
43
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
44
45
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
46
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
47
            ' */\n',
48
49
    jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
    jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\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
      },
      customize: {
135
        src: configBridge.paths.customizerJs,
136
        dest: 'docs/assets/js/customize.min.js'
137
138
      },
      docsJs: {
139
        src: configBridge.paths.docsJs,
140
        dest: 'docs/assets/js/docs.min.js'
141
142
143
      }
    },

Mark Otto's avatar
Mark Otto committed
144
145
146
147
148
149
150
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

151
    less: {
Mark Otto's avatar
Mark Otto committed
152
      core: {
153
        options: {
154
          strictMath: true,
155
156
157
158
159
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
        },
160
161
        src: 'less/bootstrap.less',
        dest: 'dist/css/<%= pkg.name %>.css'
162
      },
Mark Otto's avatar
Mark Otto committed
163
      docs: {
164
        options: {
Mark Otto's avatar
Mark Otto committed
165
          strictMath: true
166
        },
Mark Otto's avatar
Mark Otto committed
167
168
169
        files: {
          'docs/assets/css/docs.min.css': 'docs/assets/less/docs.less'
        }
170
171
172
      }
    },

Bas Bosman's avatar
Bas Bosman committed
173
174
    autoprefixer: {
      options: {
175
        browsers: configBridge.config.autoprefixerBrowsers
Bas Bosman's avatar
Bas Bosman committed
176
177
178
179
180
181
182
183
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
184
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
185
186
187
188
189
190
191
192
193
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
194
    cssmin: {
XhmikosR's avatar
XhmikosR committed
195
      options: {
196
        compatibility: 'ie8',
197
198
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
199
      },
Mark Otto's avatar
Mark Otto committed
200
201
202
203
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
204
      },
XhmikosR's avatar
XhmikosR committed
205
      docs: {
Mark Otto's avatar
Mark Otto committed
206
        src: 'docs/assets/css/docs.min.css',
207
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
208
209
210
      }
    },

211
    usebanner: {
XhmikosR's avatar
XhmikosR committed
212
213
214
215
216
217
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
218
219
220
      }
    },

221
    csscomb: {
222
223
224
225
      options: {
        config: 'less/.csscomb.json'
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
226
227
228
229
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
230
231
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
232
233
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
234
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
235
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
236
237
      },
      docs: {
238
239
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
240
241
242
      }
    },

Mark Otto's avatar
Mark Otto committed
243
    copy: {
Mark Otto's avatar
Mark Otto committed
244
      docs: {
vsn4ik's avatar
vsn4ik committed
245
246
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
247
248
249
      }
    },

250
251
252
253
254
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
255
        }
256
257
258
      }
    },

259
    jekyll: {
260
261
262
263
264
265
266
267
268
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
269
270
    },

271
    jade: {
272
273
274
275
276
277
278
279
280
281
282
      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'
283
284
285
      }
    },

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

306
307
    watch: {
      src: {
308
        files: '<%= jshint.core.src %>',
309
        tasks: ['jshint:src', 'qunit', 'concat']
310
311
312
313
314
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
315
      less: {
316
        files: 'less/**/*.less',
317
        tasks: 'less'
Mark Otto's avatar
Mark Otto committed
318
319
320
321
      },
      docs: {
        files: 'docs/assets/less/*.less',
        tasks: 'less'
322
      }
323
324
325
326
327
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
328
329
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
330
331
332
333
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
334
335
336
337
338
339
    },

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

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
351
      }
352
353
    }
  });
354
355


356
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
357
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
358
  require('time-grunt')(grunt);
359

360
  // Docs HTML validation task
361
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
362

363
364
365
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
366
367
368
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
369

370
  // Test task.
371
372
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
373
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
374
375
    // 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-js', 'docs']);
376
377
  }
  // Skip HTML validation if running a different subset of the test suite
378
379
380
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
381
382
    testSubtasks.push('validate-html');
  }
383
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
384
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
385
      // Skip Sauce if running a different subset of the test suite
386
387
388
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
389
390
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
391
392
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
393
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
394

395
  // JS distribution task.
396
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
397

398
  // CSS distribution task.
Mark Otto's avatar
Mark Otto committed
399
400
  grunt.registerTask('less-compile', ['less:core', 'less:docs']);
  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
401

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

405
  // Default task.
Mark Otto's avatar
Mark Otto committed
406
  grunt.registerTask('default', ['clean:dist', 'test']);
407

408
409
410
  // 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!
411
  grunt.registerTask('change-version-number', 'sed');
412

413
  // task for building customizer
414
415
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
416
417
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
418
    generateRawFiles(grunt, banner);
419
  });
Chris Rebert's avatar
Chris Rebert committed
420

421
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
422
423
424
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
425
426
  });

427
428
429
430
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
  grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
Mark Otto's avatar
Mark Otto committed
431
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-customizer']);
432

433
434
  grunt.registerTask('docs-github', ['jekyll:github']);

435
436
437
438
439
440
441
  // 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) {
442
        grunt.fail.warn(err);
443
444
445
446
447
448
449
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
450
};