Gruntfile.js 15.7 KB
Newer Older
Chris Rebert's avatar
Chris Rebert committed
1
2
3
/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
4
 * Copyright 2013-2016 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 glob = require('glob');
Chris Rebert's avatar
Chris Rebert committed
21
  var isTravis = require('is-travis');
22
  var npmShrinkwrap = require('npm-shrinkwrap');
23
  var mq4HoverShim = require('mq4-hover-shim');
24
  var autoprefixerSettings = require('./grunt/autoprefixer-settings.js');
25
  var autoprefixer = require('autoprefixer')(autoprefixerSettings);
Mark Otto's avatar
Mark Otto committed
26

27
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
28
29
30
31
32
33
34
  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);
    });
  });
35

36
37
38
39
40
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
41
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
42
43
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
44
            ' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' +
XhmikosR's avatar
XhmikosR committed
45
            ' */\n',
46
47
48
49
50
    jqueryCheck: 'if (typeof jQuery === \'undefined\') {\n' +
                 '  throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery\')\n' +
                 '}\n',
    jqueryVersionCheck: '+function ($) {\n' +
                        '  var version = $.fn.jquery.split(\' \')[0].split(\'.\')\n' +
Chris Rebert's avatar
Chris Rebert committed
51
52
                        '  if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] >= 3)) {\n' +
                        '    throw new Error(\'Bootstrap\\\'s JavaScript requires at least jQuery v1.9.1 but less than v3.0.0\')\n' +
53
54
                        '  }\n' +
                        '}(jQuery);\n\n',
55
56
57

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

62
63
64
65
66
67
    // JS build configuration
    lineremover: {
      es6Import: {
        files: {
          '<%= concat.bootstrap.dest %>': '<%= concat.bootstrap.dest %>'
        },
68
        options: {
69
70
71
72
73
          exclusionPattern: /^(import|export)/g
        }
      }
    },

fat's avatar
fat committed
74
    babel: {
fat's avatar
fat committed
75
      dev: {
76
77
78
        options: {
          sourceMap: true,
          modules: 'ignore'
79
        },
fat's avatar
fat committed
80
        files: {
fat's avatar
fat committed
81
82
83
84
85
86
87
          'js/dist/util.js'      : 'js/src/util.js',
          'js/dist/alert.js'     : 'js/src/alert.js',
          'js/dist/button.js'    : 'js/src/button.js',
          'js/dist/carousel.js'  : 'js/src/carousel.js',
          'js/dist/collapse.js'  : 'js/src/collapse.js',
          'js/dist/dropdown.js'  : 'js/src/dropdown.js',
          'js/dist/modal.js'     : 'js/src/modal.js',
fat's avatar
tab es6    
fat committed
88
          'js/dist/scrollspy.js' : 'js/src/scrollspy.js',
89
          'js/dist/tab.js'       : 'js/src/tab.js',
fat's avatar
fat committed
90
91
          'js/dist/tooltip.js'   : 'js/src/tooltip.js',
          'js/dist/popover.js'   : 'js/src/popover.js'
fat's avatar
fat committed
92
        }
93
      },
94
      dist: {
XhmikosR's avatar
XhmikosR committed
95
        options: {
96
          modules: 'ignore'
XhmikosR's avatar
XhmikosR committed
97
        },
98
99
100
        files: {
          '<%= concat.bootstrap.dest %>' : '<%= concat.bootstrap.dest %>'
        }
101
      },
fat's avatar
fat committed
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
      umd: {
        options: {
          modules: 'umd'
        },
        files: {
          'dist/js/umd/util.js'      : 'js/src/util.js',
          'dist/js/umd/alert.js'     : 'js/src/alert.js',
          'dist/js/umd/button.js'    : 'js/src/button.js',
          'dist/js/umd/carousel.js'  : 'js/src/carousel.js',
          'dist/js/umd/collapse.js'  : 'js/src/collapse.js',
          'dist/js/umd/dropdown.js'  : 'js/src/dropdown.js',
          'dist/js/umd/modal.js'     : 'js/src/modal.js',
          'dist/js/umd/scrollspy.js' : 'js/src/scrollspy.js',
          'dist/js/umd/tab.js'       : 'js/src/tab.js',
          'dist/js/umd/tooltip.js'   : 'js/src/tooltip.js',
          'dist/js/umd/popover.js'   : 'js/src/popover.js'
        }
119
120
      }
    },
121

Jacob Thornton's avatar
Jacob Thornton committed
122
123
124
125
126
127
128
    eslint: {
      options: {
        configFile: 'js/.eslintrc'
      },
      target: 'js/src/*.js'
    },

Chris Rebert's avatar
Chris Rebert committed
129
130
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
131
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
132
      },
Chris Rebert's avatar
Chris Rebert committed
133
      grunt: {
fat's avatar
fat committed
134
        src: ['Gruntfile.js', 'grunt/*.js']
Chris Rebert's avatar
Chris Rebert committed
135
      },
136
      core: {
fat's avatar
fat committed
137
        src: 'js/src/*.js'
Chris Rebert's avatar
Chris Rebert committed
138
139
      },
      test: {
fat's avatar
fat committed
140
        src: 'js/tests/unit/*.js'
141
142
      },
      assets: {
143
144
145
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
fat's avatar
fat committed
146
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
Chris Rebert's avatar
Chris Rebert committed
147
148
149
      }
    },

150
    stamp: {
151
      options: {
152
153
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>\n+function ($) {\n',
        footer: '\n}(jQuery);'
154
155
      },
      bootstrap: {
156
157
158
        files: {
          src: '<%= concat.bootstrap.dest %>'
        }
159
160
      }
    },
161

162
    concat: {
163
      options: {
164
        stripBanners: false
165
      },
166
      bootstrap: {
fat's avatar
fat committed
167
        src: [
168
169
170
171
172
173
174
175
176
177
178
          'js/src/util.js',
          'js/src/alert.js',
          'js/src/button.js',
          'js/src/carousel.js',
          'js/src/collapse.js',
          'js/src/dropdown.js',
          'js/src/modal.js',
          'js/src/scrollspy.js',
          'js/src/tab.js',
          'js/src/tooltip.js',
          'js/src/popover.js'
fat's avatar
fat committed
179
        ],
180
        dest: 'dist/js/<%= pkg.name %>.js'
fat's avatar
fat committed
181
182
183
184
185
      }
    },

    uglify: {
      options: {
fat's avatar
fat committed
186
187
        compress: {
          warnings: false
fat's avatar
fat committed
188
        },
fat's avatar
fat committed
189
        mangle: true,
XhmikosR's avatar
XhmikosR committed
190
        preserveComments: /^!|@preserve|@license|@cc_on/i
XhmikosR's avatar
XhmikosR committed
191
      },
fat's avatar
fat committed
192
193
      core: {
        src: '<%= concat.bootstrap.dest %>',
194
        dest: 'dist/js/<%= pkg.name %>.min.js'
fat's avatar
fat committed
195
      },
196
      docsJs: {
197
        src: configBridge.paths.docsJs,
198
        dest: 'docs/assets/js/docs.min.js'
199
200
201
      }
    },

Mark Otto's avatar
Mark Otto committed
202
203
204
205
206
207
208
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

209
    // CSS build configuration
Chris Rebert's avatar
Chris Rebert committed
210
211
    scsslint: {
      options: {
212
        bundleExec: true,
nextgenthemes's avatar
nextgenthemes committed
213
        config: 'scss/.scss-lint.yml',
Mark Otto's avatar
Mark Otto committed
214
215
        reporterOutput: null
      },
Mark Otto's avatar
Mark Otto committed
216
217
218
219
      core: {
        src: ['scss/*.scss', '!scss/_normalize.scss']
      },
      docs: {
vsn4ik's avatar
vsn4ik committed
220
        src: ['docs/assets/scss/*.scss', '!docs/assets/scss/docs.scss']
Mark Otto's avatar
Mark Otto committed
221
      }
Chris Rebert's avatar
Chris Rebert committed
222
223
    },

Chris Rebert's avatar
Chris Rebert committed
224
    postcss: {
Bas Bosman's avatar
Bas Bosman committed
225
226
      core: {
        options: {
227
228
229
230
231
          map: true,
          processors: [
            mq4HoverShim.postprocessorFor({ hoverSelectorPrefix: '.bs-true-hover ' }),
            autoprefixer
          ]
Bas Bosman's avatar
Bas Bosman committed
232
        },
233
        src: 'dist/css/*.css'
Bas Bosman's avatar
Bas Bosman committed
234
235
      },
      docs: {
236
237
238
239
240
        options: {
          processors: [
            autoprefixer
          ]
        },
Mark Otto's avatar
Mark Otto committed
241
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
242
243
      },
      examples: {
244
245
246
247
248
        options: {
          processors: [
            autoprefixer
          ]
        },
Bas Bosman's avatar
Bas Bosman committed
249
250
251
252
253
254
255
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
256
    cssmin: {
XhmikosR's avatar
XhmikosR committed
257
      options: {
258
259
        // TODO: disable `zeroUnits` optimization once clean-css 3.2 is released
        //    and then simplify the fix for https://github.com/twbs/bootstrap/issues/14837 accordingly
260
        compatibility: 'ie9',
261
        keepSpecialComments: '*',
262
        sourceMap: true,
Mark Otto's avatar
Mark Otto committed
263
        advanced: false
XhmikosR's avatar
XhmikosR committed
264
      },
Mark Otto's avatar
Mark Otto committed
265
      core: {
266
267
268
269
270
271
272
273
274
        files: [
          {
            expand: true,
            cwd: 'dist/css',
            src: ['*.css', '!*.min.css'],
            dest: 'dist/css',
            ext: '.min.css'
          }
        ]
275
      },
XhmikosR's avatar
XhmikosR committed
276
      docs: {
Mark Otto's avatar
Mark Otto committed
277
        src: 'docs/assets/css/docs.min.css',
278
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
279
280
281
      }
    },

Mark Otto's avatar
Mark Otto committed
282
    copy: {
Mark Otto's avatar
Mark Otto committed
283
      docs: {
XhmikosR's avatar
XhmikosR committed
284
285
286
287
288
289
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
Mark Otto's avatar
Mark Otto committed
290
291
292
      }
    },

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

302
    jekyll: {
303
      options: {
XhmikosR's avatar
XhmikosR committed
304
        bundleExec: true,
XhmikosR's avatar
XhmikosR committed
305
306
        config: '_config.yml',
        incremental: false
307
308
309
310
311
312
313
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
314
315
    },

316
    htmllint: {
317
      options: {
318
319
        ignore: [
          'Element “img” is missing required attribute “src”.',
320
          'Attribute “autocomplete” is only allowed when the input type is “color”, “date”, “datetime”, “datetime-local”, “email”, “month”, “number”, “password”, “range”, “search”, “tel”, “text”, “time”, “url”, or “week”.',
321
322
          'Attribute “autocomplete” not allowed on element “button” at this point.',
          'Element “div” not allowed as child of element “progress” in this context. (Suppressing further errors from this subtree.)',
323
          'Consider using the “h1” element as a top-level heading only (all “h1” elements are treated as top-level headings by many screen readers and other tools).',
324
          'The “datetime” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
325
326
327
328
329
          'The “color” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
          'The “date” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
          'The “datetime-local” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
          'The “month” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
          'The “time” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
XhmikosR's avatar
XhmikosR committed
330
          'The “week” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.'
331
        ]
332
      },
333
      src: ['_gh_pages/**/*.html', 'js/tests/visual/*.html']
334
335
    },

336
337
    watch: {
      src: {
fat's avatar
fat committed
338
        files: '<%= jscs.core.src %>',
fat's avatar
fat committed
339
        tasks: ['babel:dev']
340
      },
341
342
      sass: {
        files: 'scss/**/*.scss',
Mark Otto's avatar
Mark Otto committed
343
        tasks: ['dist-css', 'docs']
Mark Otto's avatar
Mark Otto committed
344
345
346
347
      },
      docs: {
        files: 'docs/assets/scss/**/*.scss',
        tasks: ['dist-css', 'docs']
348
      }
349
350
    },

351
352
353
354
    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
355
          concurrency: 10,
356
          maxRetries: 3,
357
          maxPollRetries: 4,
358
          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
359
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
360
361
        }
      }
Chris Rebert's avatar
Chris Rebert committed
362
363
364
365
366
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
367
      }
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
    },

    buildcontrol: {
      options: {
        dir: '_gh_pages',
        commit: true,
        push: true,
        message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
      },
      pages: {
        options: {
          remote: 'git@github.com:twbs/derpstrap.git',
          branch: 'gh-pages'
        }
      }
XhmikosR's avatar
XhmikosR committed
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
    },

    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'
          }
        ]
      }
402
    }
XhmikosR's avatar
XhmikosR committed
403

404
  });
405
406


407
  // These plugins provide necessary tasks.
408
409
410
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies',
    // Exclude Sass compilers. We choose the one to load later on.
    pattern: ['grunt-*', '!grunt-sass', '!grunt-contrib-sass'] });
XhmikosR's avatar
XhmikosR committed
411
  require('time-grunt')(grunt);
412

413
  // Docs HTML validation task
XhmikosR's avatar
XhmikosR committed
414
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
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
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
427
    // Skip core tests if this is a Savage build
428
429
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
430
431
  }
  // Skip HTML validation if running a different subset of the test suite
432
  if (runSubset('validate-html') &&
Chris Rebert's avatar
Chris Rebert committed
433
434
      isTravis &&
      // Skip HTML5 validator when [skip validator] is in the commit message
435
      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
    testSubtasks.push('babel:dev');
445
446
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
447
448
  }
  grunt.registerTask('test', testSubtasks);
Jacob Thornton's avatar
Jacob Thornton committed
449
  grunt.registerTask('test-js', ['eslint', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
450

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

Mark Otto's avatar
Mark Otto committed
454
  grunt.registerTask('test-scss', ['scsslint:core']);
Chris Rebert's avatar
Chris Rebert committed
455

456
  // CSS distribution task.
457
458
459
460
  // Supported Compilers: sass (Ruby) and libsass.
  (function (sassCompilerName) {
    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
  })(process.env.TWBS_SASS || 'libsass');
461
462
  // grunt.registerTask('sass-compile', ['sass:core', 'sass:extras', 'sass:docs']);
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
463

464
  grunt.registerTask('dist-css', ['sass-compile', 'postcss:core', 'cssmin:core', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
465

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

469
  // Default task.
Mark Otto's avatar
Mark Otto committed
470
  grunt.registerTask('default', ['clean:dist', 'test']);
471

fat's avatar
fat committed
472
473
474
475
476
477
  grunt.registerTask('commonjs', ['babel:umd', 'npm-js']);

  grunt.registerTask('npm-js', 'Generate npm-js entrypoint module in dist dir.', function () {
    var srcFiles = Object.keys(grunt.config.get('babel.umd.files')).map(function (filename) {
      return './' + path.join('umd', path.basename(filename))
    })
478
479
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
480
481
  });

482
  // Docs task.
483
  grunt.registerTask('docs-css', ['postcss:docs', 'postcss:examples', 'cssmin:docs']);
484
  grunt.registerTask('lint-docs-css', ['scsslint:docs']);
Mark Otto's avatar
Mark Otto committed
485
  grunt.registerTask('docs-js', ['uglify:docsJs']);
fat's avatar
fat committed
486
  grunt.registerTask('lint-docs-js', ['jscs:assets']);
487
  grunt.registerTask('docs', ['lint-docs-css', 'docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
488
  grunt.registerTask('docs-github', ['jekyll:github']);
489

490
  grunt.registerTask('prep-release', ['dist', 'docs', 'docs-github', 'compress']);
491

Mark Otto's avatar
Mark Otto committed
492
493
494
  // Publish to GitHub
  grunt.registerTask('publish', ['buildcontrol:pages']);

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