Gruntfile.js 12.5 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
21
22
23
  var generateGlyphiconsData = require('./grunt/bs-glyphicons-data-generator.js');
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
  var generateRawFilesJs = require('./grunt/bs-raw-files-generator.js');
  var updateShrinkwrap = require('./grunt/shrinkwrap.js');
24

25
26
27
28
29
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
30
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
31
32
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
33
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
34
            ' */\n',
35
    jqueryCheck: 'if (typeof jQuery === \'undefined\') { throw new Error(\'Bootstrap\\\'s JavaScript requires jQuery\') }\n\n',
36
37
38

    // Task configuration.
    clean: {
Chris Rebert's avatar
Chris Rebert committed
39
      dist: ['dist', 'docs/dist']
40
41
42
43
44
45
    },

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
Chris Rebert's avatar
Chris Rebert committed
46
      grunt: {
47
        options: {
48
          jshintrc: 'grunt/.jshintrc'
49
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
50
        src: ['Gruntfile.js', 'grunt/*.js']
51
52
      },
      src: {
53
        src: 'js/*.js'
54
55
      },
      test: {
56
        src: 'js/tests/unit/*.js'
57
58
      },
      assets: {
Zlatan Vasović's avatar
Zlatan Vasović committed
59
        src: 'docs/assets/js/src/*.js'
60
61
      }
    },
62

Chris Rebert's avatar
Chris Rebert committed
63
64
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
65
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
66
      },
Chris Rebert's avatar
Chris Rebert committed
67
      grunt: {
XhmikosR's avatar
XhmikosR committed
68
        options: {
XhmikosR's avatar
XhmikosR committed
69
70
          requireCamelCaseOrUpperCaseIdentifiers: null,
          requireParenthesesAroundIIFE: true
XhmikosR's avatar
XhmikosR committed
71
        },
72
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
73
74
      },
      src: {
75
        src: '<%= jshint.src.src %>'
Chris Rebert's avatar
Chris Rebert committed
76
77
      },
      test: {
78
        src: '<%= jshint.test.src %>'
79
80
      },
      assets: {
81
        src: '<%= jshint.assets.src %>'
Chris Rebert's avatar
Chris Rebert committed
82
83
84
      }
    },

85
86
    concat: {
      options: {
Zlatan Vasović's avatar
Zlatan Vasović committed
87
        banner: '<%= banner %>\n<%= jqueryCheck %>',
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
        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'
      }
    },
108

109
    uglify: {
110
111
112
      options: {
        report: 'min'
      },
113
      bootstrap: {
114
        options: {
115
          banner: '<%= banner %>'
116
        },
117
        src: '<%= concat.bootstrap.dest %>',
118
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
119
120
      },
      customize: {
121
        options: {
122
          preserveComments: 'some'
123
        },
XhmikosR's avatar
XhmikosR committed
124
        src: [
125
          'docs/assets/js/vendor/less.min.js',
XhmikosR's avatar
XhmikosR committed
126
          'docs/assets/js/vendor/jszip.min.js',
127
128
129
          'docs/assets/js/vendor/uglify.min.js',
          'docs/assets/js/vendor/blob.js',
          'docs/assets/js/vendor/filesaver.js',
130
          'docs/assets/js/raw-files.min.js',
Zlatan Vasović's avatar
Zlatan Vasović committed
131
          'docs/assets/js/src/customizer.js'
XhmikosR's avatar
XhmikosR committed
132
        ],
133
        dest: 'docs/assets/js/customize.min.js'
134
135
136
      },
      docsJs: {
        options: {
137
          preserveComments: 'some'
138
139
        },
        src: [
140
          'docs/assets/js/vendor/holder.js',
Zlatan Vasović's avatar
Zlatan Vasović committed
141
          'docs/assets/js/src/application.js'
142
143
        ],
        dest: 'docs/assets/js/docs.min.js'
144
145
146
      }
    },

Mark Otto's avatar
Mark Otto committed
147
148
149
150
151
152
153
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

154
    less: {
155
156
      compileCore: {
        options: {
157
          strictMath: true,
158
159
160
161
162
163
164
165
166
167
168
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
        },
        files: {
          'dist/css/<%= pkg.name %>.css': 'less/bootstrap.less'
        }
      },
      compileTheme: {
        options: {
169
          strictMath: true,
170
171
172
173
174
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
175
176
177
        files: {
          'dist/css/<%= pkg.name %>-theme.css': 'less/theme.less'
        }
178
      },
179
180
181
182
183
184
185
      minify: {
        options: {
          cleancss: true,
          report: 'min'
        },
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css',
186
          'dist/css/<%= pkg.name %>-rtl.min.css': 'dist/css/<%= pkg.name %>-rtl.css',
Mark Otto's avatar
Mark Otto committed
187
          'dist/css/<%= pkg.name %>-theme.min.css': 'dist/css/<%= pkg.name %>-theme.css'
188
189
190
191
        }
      }
    },

Bas Bosman's avatar
Bas Bosman committed
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
    autoprefixer: {
      options: {
        browsers: ['last 2 versions', 'ie 8', 'ie 9', 'android 2.3', 'android 4', 'opera 12']
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
        src: 'docs/assets/css/docs.css'
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

219
    css_flip: {
220
221
      rtl: {
        files: {
222
          'dist/css/<%= pkg.name %>-rtl.css': 'dist/css/<%= pkg.name %>.css'
223
224
        }
      }
225
226
    },

Mark Otto's avatar
Mark Otto committed
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
    csslint: {
      options: {
        csslintrc: 'less/.csslintrc'
      },
      src: [
        'dist/css/bootstrap.css',
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
        'docs/examples/**/*.css'
      ],
      docs: {
        options: {
          'ids': false,
          'overqualified-elements': false
        },
        src: 'docs/assets/css/src/docs.css'
      }
    },

XhmikosR's avatar
XhmikosR committed
247
248
249
250
    cssmin: {
      compress: {
        options: {
          keepSpecialComments: '*',
251
          noAdvanced: true, // turn advanced optimizations off until the issue is fixed in clean-css
XhmikosR's avatar
XhmikosR committed
252
          report: 'min',
XhmikosR's avatar
XhmikosR committed
253
          compatibility: 'ie8'
XhmikosR's avatar
XhmikosR committed
254
255
        },
        src: [
Zlatan Vasović's avatar
Zlatan Vasović committed
256
257
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
258
        ],
259
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
260
261
262
      }
    },

263
264
    usebanner: {
      dist: {
265
        options: {
266
267
          position: 'top',
          banner: '<%= banner %>'
268
        },
269
        files: {
270
271
          src: [
            'dist/css/<%= pkg.name %>.css',
272
            'dist/css/<%= pkg.name %>-rtl.css',
273
            'dist/css/<%= pkg.name %>.min.css',
274
            'dist/css/<%= pkg.name %>-rtl.min.css',
275
            'dist/css/<%= pkg.name %>-theme.css',
276
            'dist/css/<%= pkg.name %>-theme.min.css'
277
          ]
278
279
280
281
        }
      }
    },

282
    csscomb: {
283
284
285
286
      options: {
        config: 'less/.csscomb.json'
      },
      dist: {
287
        files: {
288
          'dist/css/<%= pkg.name %>.css': 'dist/css/<%= pkg.name %>.css',
289
          'dist/css/<%= pkg.name %>-rtl.css': 'dist/css/<%= pkg.name %>-rtl.css',
290
          'dist/css/<%= pkg.name %>-theme.css': 'dist/css/<%= pkg.name %>-theme.css'
291
        }
292
293
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
294
295
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
296
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
297
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
298
299
300
      },
      docs: {
        files: {
Zlatan Vasović's avatar
Zlatan Vasović committed
301
          'docs/assets/css/src/docs.css': 'docs/assets/css/src/docs.css'
XhmikosR's avatar
XhmikosR committed
302
        }
303
304
305
      }
    },

Mark Otto's avatar
Mark Otto committed
306
307
308
    copy: {
      fonts: {
        expand: true,
309
        src: 'fonts/*',
Mark Otto's avatar
Mark Otto committed
310
        dest: 'dist/'
311
      },
Mark Otto's avatar
Mark Otto committed
312
      docs: {
313
        expand: true,
Mark Otto's avatar
Mark Otto committed
314
315
316
        cwd: './dist',
        src: [
          '{css,js}/*.min.*',
Chris Rebert's avatar
Chris Rebert committed
317
          'css/*.map',
Mark Otto's avatar
Mark Otto committed
318
319
320
          'fonts/*'
        ],
        dest: 'docs/dist'
Mark Otto's avatar
Mark Otto committed
321
322
323
      }
    },

324
325
326
327
328
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
329
        }
330
331
332
      }
    },

333
334
335
336
    jekyll: {
      docs: {}
    },

337
338
339
340
341
342
343
344
345
346
347
348
    jade: {
      compile: {
        options: {
          pretty: true,
          data: 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()};
          }
        },
        files: {
349
350
          'docs/_includes/customizer-variables.html': 'docs/jade/customizer-variables.jade',
          'docs/_includes/nav-customize.html': 'docs/jade/customizer-nav.jade'
351
352
353
354
        }
      }
    },

355
356
    validation: {
      options: {
357
358
        charset: 'utf-8',
        doctype: 'HTML5',
359
        failHard: true,
360
361
        reset: true,
        relaxerror: [
XhmikosR's avatar
XhmikosR committed
362
363
          'Bad value X-UA-Compatible for attribute http-equiv on element meta.',
          'Element img is missing required attribute src.'
364
        ]
365
366
      },
      files: {
367
        src: '_gh_pages/**/*.html'
368
369
370
      }
    },

371
372
373
374
375
376
377
378
379
    watch: {
      src: {
        files: '<%= jshint.src.src %>',
        tasks: ['jshint:src', 'qunit']
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
380
      less: {
381
        files: 'less/*.less',
382
        tasks: 'less'
383
      }
384
385
386
387
388
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
389
390
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
391
392
393
394
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
395
396
397
398
399
400
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
401
          concurrency: 10,
402
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
403
          browsers: grunt.file.readYAML('test-infra/sauce_browsers.yml')
404
405
        }
      }
Chris Rebert's avatar
Chris Rebert committed
406
407
408
409
410
411
412
413
414
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
      },
      npmShrinkWrap: {
        command: 'npm shrinkwrap --dev'
      }
415
416
    }
  });
417
418


419
  // These plugins provide necessary tasks.
Tobias Lindig's avatar
Tobias Lindig committed
420
  require('load-grunt-tasks')(grunt, {scope: 'devDependencies'});
XhmikosR's avatar
XhmikosR committed
421
  require('time-grunt')(grunt);
422

423
  // Docs HTML validation task
424
  grunt.registerTask('validate-html', ['jekyll', 'validation']);
425

426
  // Test task.
427
428
429
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
  if (!process.env.TWBS_TEST || process.env.TWBS_TEST === 'core') {
430
    testSubtasks = testSubtasks.concat(['dist-css', 'csslint', 'jshint', 'jscs', 'qunit', 'build-customizer-html']);
431
432
433
434
435
  }
  // Skip HTML validation if running a different subset of the test suite
  if (!process.env.TWBS_TEST || process.env.TWBS_TEST === 'validate-html') {
    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
Chris Rebert's avatar
Chris Rebert committed
439
      (!process.env.TWBS_TEST || process.env.TWBS_TEST === 'sauce-js-unit')) {
440
441
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
442
443
  }
  grunt.registerTask('test', testSubtasks);
444

445
446
  // JS distribution task.
  grunt.registerTask('dist-js', ['concat', 'uglify']);
447

448
  // CSS distribution task.
449
  grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
Bas Bosman's avatar
Bas Bosman committed
450
  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer', 'css_flip', 'csscomb', 'less:minify', 'cssmin', 'usebanner']);
451

Mark Otto's avatar
derp    
Mark Otto committed
452
  // Docs distribution task.
453
  grunt.registerTask('dist-docs', 'copy:docs');
Mark Otto's avatar
Mark Otto committed
454

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

458
  // Default task.
Chris Rebert's avatar
Chris Rebert committed
459
  grunt.registerTask('default', ['test', 'dist', 'build-glyphicons-data', 'build-customizer', 'update-shrinkwrap']);
460

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

466
  grunt.registerTask('build-glyphicons-data', generateGlyphiconsData);
467

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

  // Task for updating the npm packages used by the Travis build.
  grunt.registerTask('update-shrinkwrap', ['exec:npmUpdate', 'exec:npmShrinkWrap', '_update-shrinkwrap']);
  grunt.registerTask('_update-shrinkwrap', function () { updateShrinkwrap.call(this, grunt); });
479
};