Gruntfile.js 13.9 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: {
194
        src: '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: [
240
241
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
242
        ],
243
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
244
245
246
      }
    },

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

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

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

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

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

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

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

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

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

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

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

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

407
  });
408
409


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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