Gruntfile.js 13.2 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
334
          'Attribute autocomplete not allowed on element input at this point.',
          'Attribute autocomplete not allowed on element button at this point.'
335
        ]
336
337
      },
      files: {
338
        src: '_gh_pages/**/*.html'
339
340
341
      }
    },

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

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

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

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
383
      }
384
385
    }
  });
386
387


388
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
389
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
390
  require('time-grunt')(grunt);
391

392
  // Docs HTML validation task
393
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
394

395
396
397
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
398
399
400
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
401

402
  // Test task.
403
404
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
405
406
407
  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
408
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
409
410
  }
  // Skip HTML validation if running a different subset of the test suite
411
412
413
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
414
415
    testSubtasks.push('validate-html');
  }
416
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
417
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
418
      // Skip Sauce if running a different subset of the test suite
419
420
421
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
422
423
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
424
425
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
426
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
427

428
  // JS distribution task.
429
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
430

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

435
  // Full distribution task.
436
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
437

438
  // Default task.
439
  grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
440

441
442
443
  // 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!
444
  grunt.registerTask('change-version-number', 'sed');
445

446
  // task for building customizer
447
448
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
449
450
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
451
    generateRawFiles(grunt, banner);
452
  });
Chris Rebert's avatar
Chris Rebert committed
453

454
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
455
456
457
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
458
459
  });

460
461
462
463
464
  // 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']);
465
  grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-customizer']);
466

467
468
  grunt.registerTask('docs-github', ['jekyll:github']);

469
470
471
472
473
474
475
  // 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) {
476
        grunt.fail.warn(err);
477
478
479
480
481
482
483
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
484
};