Gruntfile.js 14 KB
Newer Older
Chris Rebert's avatar
Chris Rebert committed
1
2
3
/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
Zlatan Vasović's avatar
Zlatan Vasović committed
4
 * Copyright 2013-2015 Twitter, Inc.
Chris Rebert's avatar
Chris Rebert committed
5
6
 * 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 generateGlyphiconsData = require('./grunt/bs-glyphicons-data-generator.js');
22
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
23
24
25
26
27
28
  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() };
  };
29
  var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
30
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
31
32
33
34
35
36
37
  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);
    });
  });
38

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

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
44
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
45
46
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
47
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
48
            ' */\n',
49
50
    jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
    jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\n'),
51
52
53

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

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

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

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

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

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

152
    less: {
153
154
      compileCore: {
        options: {
155
          strictMath: true,
156
157
158
159
160
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
        },
161
162
        src: 'less/bootstrap.less',
        dest: 'dist/css/<%= pkg.name %>.css'
163
164
165
      },
      compileTheme: {
        options: {
166
          strictMath: true,
167
168
169
170
171
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
172
173
        src: 'less/theme.less',
        dest: 'dist/css/<%= pkg.name %>-theme.css'
174
175
176
      }
    },

Bas Bosman's avatar
Bas Bosman committed
177
178
    autoprefixer: {
      options: {
179
        browsers: configBridge.config.autoprefixerBrowsers
Bas Bosman's avatar
Bas Bosman committed
180
181
182
183
184
185
186
187
188
189
190
191
192
193
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
XhmikosR's avatar
XhmikosR committed
194
        src: ['docs/assets/css/anchor.css', 'docs/assets/css/src/docs.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/'
      }
    },

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

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

XhmikosR's avatar
XhmikosR committed
244
        ],
245
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
246
247
248
      }
    },

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

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

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

292
293
294
295
296
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
297
        }
298
299
300
      }
    },

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

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

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

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

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

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

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

    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'
          }
        ]
      }
408
    }
XhmikosR's avatar
XhmikosR committed
409

410
  });
411
412


413
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
414
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
415
  require('time-grunt')(grunt);
416

417
  // Docs HTML validation task
418
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
419

420
421
422
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
423
424
425
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
426

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

453
  // JS distribution task.
454
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
455

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

460
  // Full distribution task.
461
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
462

463
  // Default task.
464
  grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
465

466
467
468
  // 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!
469
  grunt.registerTask('change-version-number', 'sed');
470

471
472
  grunt.registerTask('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); });

473
  // task for building customizer
474
475
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
476
477
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
478
    generateRawFiles(grunt, banner);
479
  });
Chris Rebert's avatar
Chris Rebert committed
480

481
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
482
483
484
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
485
486
  });

487
488
489
490
491
  // 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']);
492
  grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);
493

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

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