Gruntfile.js 14.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');
Chris Rebert's avatar
Chris Rebert committed
20
  var isTravis = require('is-travis');
21
  var mq4HoverShim = require('mq4-hover-shim');
22
  var autoprefixerSettings = require('./grunt/autoprefixer-settings.js');
23
  var autoprefixer = require('autoprefixer')(autoprefixerSettings);
Mark Otto's avatar
Mark Otto committed
24

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

34
35
36
37
38
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
39
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
40
41
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
42
            ' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' +
XhmikosR's avatar
XhmikosR committed
43
            ' */\n',
44
45
46
47
48
    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
49
50
                        '  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' +
51
52
                        '  }\n' +
                        '}(jQuery);\n\n',
53
54
55

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

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

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

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

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

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

160
    concat: {
161
      options: {
162
        stripBanners: false
163
      },
164
      bootstrap: {
fat's avatar
fat committed
165
        src: [
166
167
168
169
170
171
172
173
174
175
176
          '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
177
        ],
178
        dest: 'dist/js/<%= pkg.name %>.js'
fat's avatar
fat committed
179
180
181
182
183
      }
    },

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

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

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

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

XhmikosR's avatar
XhmikosR committed
254
    cssmin: {
XhmikosR's avatar
XhmikosR committed
255
      options: {
256
257
        // 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
258
        compatibility: 'ie9',
259
        keepSpecialComments: '*',
260
        sourceMap: true,
Mark Otto's avatar
Mark Otto committed
261
        advanced: false
XhmikosR's avatar
XhmikosR committed
262
      },
Mark Otto's avatar
Mark Otto committed
263
      core: {
264
265
266
267
268
269
270
271
272
        files: [
          {
            expand: true,
            cwd: 'dist/css',
            src: ['*.css', '!*.min.css'],
            dest: 'dist/css',
            ext: '.min.css'
          }
        ]
273
      },
XhmikosR's avatar
XhmikosR committed
274
      docs: {
Mark Otto's avatar
Mark Otto committed
275
        src: 'docs/assets/css/docs.min.css',
276
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
277
278
279
      }
    },

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

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

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

314
    htmllint: {
315
      options: {
316
317
        ignore: [
          'Element “img” is missing required attribute “src”.',
318
          '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”.',
319
320
          '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.)',
321
          '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).',
322
          'The “datetime” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.',
323
324
325
326
327
          '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
328
          'The “week” input type is not supported in all browsers. Please be sure to test, and consider using a polyfill.'
329
        ]
330
      },
331
      src: ['_gh_pages/**/*.html', 'js/tests/visual/*.html']
332
333
    },

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

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

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

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

    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'
          }
        ]
      }
400
    }
XhmikosR's avatar
XhmikosR committed
401

402
  });
403
404


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

411
  // Docs HTML validation task
XhmikosR's avatar
XhmikosR committed
412
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);
413

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

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

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

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

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

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

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

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

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

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

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

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