Gruntfile.js 15.9 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: {
220
        src: ['docs/assets/scss/*.scss', '!scss/_normalize.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.',
330
331
          'The “week” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
          'Attribute “integrity” not allowed on element “script” at this point.' // Until https://github.com/jzaefferer/grunt-html/issues/86 gets fixed
332
        ]
333
      },
334
      src: ['_gh_pages/**/*.html', 'js/tests/visual/*.html']
335
336
    },

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

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

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

    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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
    },

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

405
  });
406
407


408
  // These plugins provide necessary tasks.
409
410
411
  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
412
  require('time-grunt')(grunt);
413

414
  // Docs HTML validation task
XhmikosR's avatar
XhmikosR committed
415
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
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
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
428
    // Skip core tests if this is a Savage build
429
430
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
431
432
  }
  // Skip HTML validation if running a different subset of the test suite
433
  if (runSubset('validate-html') &&
Chris Rebert's avatar
Chris Rebert committed
434
435
      isTravis &&
      // Skip HTML5 validator when [skip validator] is in the commit message
436
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
437
438
    testSubtasks.push('validate-html');
  }
439
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
440
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
441
      // Skip Sauce if running a different subset of the test suite
442
443
444
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
445
    testSubtasks.push('babel:dev');
446
447
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
448
449
  }
  grunt.registerTask('test', testSubtasks);
Jacob Thornton's avatar
Jacob Thornton committed
450
  grunt.registerTask('test-js', ['eslint', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
451

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

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

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

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

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

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

fat's avatar
fat committed
473
474
475
476
477
478
  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))
    })
479
480
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
481
482
  });

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

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

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

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
      var dest = 'grunt/npm-shrinkwrap.json';
506
507
508
509
510
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
511
};