Gruntfile.js 13.7 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
23
24
25
26
27
  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() };
  };
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: {
152
153
      compileCore: {
        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
163
164
      },
      compileTheme: {
        options: {
165
          strictMath: true,
166
167
168
169
170
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
171
172
        src: 'less/theme.less',
        dest: 'dist/css/<%= pkg.name %>-theme.css'
173
174
175
      }
    },

Bas Bosman's avatar
Bas Bosman committed
176
177
    autoprefixer: {
      options: {
178
        browsers: configBridge.config.autoprefixerBrowsers
Bas Bosman's avatar
Bas Bosman committed
179
180
181
182
183
184
185
186
187
188
189
190
191
192
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
193
        src: 'docs/assets/css/src/docs.css'
Bas Bosman's avatar
Bas Bosman committed
194
195
196
197
198
199
200
201
202
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

Mark Otto's avatar
Mark Otto committed
203
204
205
206
    csslint: {
      options: {
        csslintrc: 'less/.csslintrc'
      },
207
      dist: [
Mark Otto's avatar
Mark Otto committed
208
209
210
211
212
213
214
215
        'dist/css/bootstrap.css',
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
        'docs/examples/**/*.css'
      ],
      docs: {
        options: {
216
          ids: false,
Mark Otto's avatar
Mark Otto committed
217
218
          'overqualified-elements': false
        },
219
        src: 'docs/assets/css/src/docs.css'
Mark Otto's avatar
Mark Otto committed
220
221
222
      }
    },

XhmikosR's avatar
XhmikosR committed
223
    cssmin: {
XhmikosR's avatar
XhmikosR committed
224
      options: {
225
        compatibility: 'ie8',
226
227
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
228
      },
229
230
231
232
233
234
235
      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'
236
      },
XhmikosR's avatar
XhmikosR committed
237
      docs: {
XhmikosR's avatar
XhmikosR committed
238
        src: [
239
240
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
241
        ],
242
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
243
244
245
      }
    },

246
    usebanner: {
XhmikosR's avatar
XhmikosR committed
247
248
249
250
251
252
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
253
254
255
      }
    },

256
    csscomb: {
257
258
259
260
      options: {
        config: 'less/.csscomb.json'
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
261
262
263
264
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
265
266
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
267
268
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
269
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
270
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
271
272
      },
      docs: {
273
274
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
275
276
277
      }
    },

Mark Otto's avatar
Mark Otto committed
278
279
    copy: {
      fonts: {
280
        src: 'fonts/*',
Mark Otto's avatar
Mark Otto committed
281
        dest: 'dist/'
282
      },
Mark Otto's avatar
Mark Otto committed
283
      docs: {
vsn4ik's avatar
vsn4ik committed
284
285
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
286
287
288
      }
    },

289
290
291
292
293
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
294
        }
295
296
297
      }
    },

298
    jekyll: {
299
300
301
302
303
304
305
306
307
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
308
309
    },

310
    jade: {
311
312
313
314
315
316
317
318
319
320
321
      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'
322
323
324
      }
    },

325
326
    validation: {
      options: {
327
328
        charset: 'utf-8',
        doctype: 'HTML5',
329
        failHard: true,
330
331
        reset: true,
        relaxerror: [
332
          'Element img is missing required attribute src.',
333
          'Attribute autocomplete not allowed on element input at this point.',
334
335
          'Attribute autocomplete not allowed on element button at this point.',
          'Bad value separator for attribute role on element li.'
336
        ]
337
338
      },
      files: {
339
        src: '_gh_pages/**/*.html'
340
341
342
      }
    },

343
344
    watch: {
      src: {
345
        files: '<%= jshint.core.src %>',
346
        tasks: ['jshint:src', 'qunit', 'concat']
347
348
349
350
351
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
352
      less: {
353
        files: 'less/**/*.less',
354
        tasks: 'less'
355
      }
356
357
358
359
360
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
361
362
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
363
364
365
366
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
367
368
369
370
371
372
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
373
          throttled: 10,
374
          maxRetries: 3,
375
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
376
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
377
378
        }
      }
Chris Rebert's avatar
Chris Rebert committed
379
380
381
382
383
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
384
      }
XhmikosR's avatar
XhmikosR committed
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
    },

    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'
          }
        ]
      }
404
    }
XhmikosR's avatar
XhmikosR committed
405

406
  });
407
408


409
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
410
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
411
  require('time-grunt')(grunt);
412

413
  // Docs HTML validation task
414
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
415

416
417
418
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
419
420
421
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
422

423
  // Test task.
424
425
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
426
427
428
  if (runSubset('core') &&
      // Skip core tests if this is a Savage build
      process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
Chris Rebert's avatar
Chris Rebert committed
429
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
430
431
  }
  // Skip HTML validation if running a different subset of the test suite
432
433
434
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
435
436
    testSubtasks.push('validate-html');
  }
437
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
438
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
439
      // Skip Sauce if running a different subset of the test suite
440
441
442
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
443
444
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
445
446
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
447
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
448

449
  // JS distribution task.
450
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
451

452
  // CSS distribution task.
453
  grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
454
  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
Mark Otto's avatar
Mark Otto committed
455

456
  // Full distribution task.
457
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
458

459
  // Default task.
460
  grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
461

462
463
464
  // 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!
465
  grunt.registerTask('change-version-number', 'sed');
466

467
  // task for building customizer
468
469
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
470
471
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
472
    generateRawFiles(grunt, banner);
473
  });
Chris Rebert's avatar
Chris Rebert committed
474

475
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
476
477
478
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
479
480
  });

481
482
483
484
485
  // 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']);
486
  grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-customizer']);
487

XhmikosR's avatar
XhmikosR committed
488
  grunt.registerTask('prep-release', ['jekyll:github', 'compress']);
489

490
491
492
493
494
495
496
  // 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) {
497
        grunt.fail.warn(err);
498
499
500
501
502
503
504
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
505
};