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
      }
    },

XhmikosR's avatar
XhmikosR committed
85
86
    csslint: {
      options: {
87
        csslintrc: 'less/.csslintrc'
XhmikosR's avatar
XhmikosR committed
88
      },
89
90
      src: [
        'dist/css/bootstrap.css',
XhmikosR's avatar
XhmikosR committed
91
92
93
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
94
        'docs/examples/**/*.css'
XhmikosR's avatar
XhmikosR committed
95
      ],
96
97
      docs: {
        options: {
XhmikosR's avatar
XhmikosR committed
98
          ids: false,
99
100
          'overqualified-elements': false
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
101
        src: 'docs/assets/css/src/docs.css'
102
      }
XhmikosR's avatar
XhmikosR committed
103
104
    },

105
106
    concat: {
      options: {
Zlatan Vasović's avatar
Zlatan Vasović committed
107
        banner: '<%= banner %>\n<%= jqueryCheck %>',
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
        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'
      }
    },
128

129
    uglify: {
130
131
132
      options: {
        report: 'min'
      },
133
      bootstrap: {
134
        options: {
135
          banner: '<%= banner %>'
136
        },
137
        src: '<%= concat.bootstrap.dest %>',
138
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
139
140
      },
      customize: {
141
        options: {
142
          preserveComments: 'some'
143
        },
XhmikosR's avatar
XhmikosR committed
144
        src: [
145
          'docs/assets/js/vendor/less.min.js',
XhmikosR's avatar
XhmikosR committed
146
          'docs/assets/js/vendor/jszip.min.js',
147
148
149
          'docs/assets/js/vendor/uglify.min.js',
          'docs/assets/js/vendor/blob.js',
          'docs/assets/js/vendor/filesaver.js',
150
          'docs/assets/js/raw-files.min.js',
Zlatan Vasović's avatar
Zlatan Vasović committed
151
          'docs/assets/js/src/customizer.js'
XhmikosR's avatar
XhmikosR committed
152
        ],
153
        dest: 'docs/assets/js/customize.min.js'
154
155
156
      },
      docsJs: {
        options: {
157
          preserveComments: 'some'
158
159
        },
        src: [
160
          'docs/assets/js/vendor/holder.js',
Zlatan Vasović's avatar
Zlatan Vasović committed
161
          'docs/assets/js/src/application.js'
162
163
        ],
        dest: 'docs/assets/js/docs.min.js'
164
165
166
      }
    },

167
    less: {
168
169
      compileCore: {
        options: {
170
          strictMath: true,
171
172
173
174
175
176
177
178
179
180
181
          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: {
182
          strictMath: true,
183
184
185
186
187
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
188
189
190
        files: {
          'dist/css/<%= pkg.name %>-theme.css': 'less/theme.less'
        }
191
      },
192
193
194
195
196
197
198
      minify: {
        options: {
          cleancss: true,
          report: 'min'
        },
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css',
199
          'dist/css/<%= pkg.name %>-rtl.min.css': 'dist/css/<%= pkg.name %>-rtl.css',
Mark Otto's avatar
Mark Otto committed
200
          'dist/css/<%= pkg.name %>-theme.min.css': 'dist/css/<%= pkg.name %>-theme.css'
201
202
203
204
        }
      }
    },

Bas Bosman's avatar
Bas Bosman committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
    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/'
      }
    },

232
    css_flip: {
233
234
      rtl: {
        files: {
235
          'dist/css/<%= pkg.name %>-rtl.css': 'dist/css/<%= pkg.name %>.css'
236
237
        }
      }
238
239
    },

XhmikosR's avatar
XhmikosR committed
240
241
242
243
    cssmin: {
      compress: {
        options: {
          keepSpecialComments: '*',
244
          noAdvanced: true, // turn advanced optimizations off until the issue is fixed in clean-css
XhmikosR's avatar
XhmikosR committed
245
          report: 'min',
XhmikosR's avatar
XhmikosR committed
246
          compatibility: 'ie8'
XhmikosR's avatar
XhmikosR committed
247
248
        },
        src: [
Zlatan Vasović's avatar
Zlatan Vasović committed
249
250
          'docs/assets/css/src/docs.css',
          'docs/assets/css/src/pygments-manni.css'
XhmikosR's avatar
XhmikosR committed
251
        ],
252
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
253
254
255
      }
    },

256
257
    usebanner: {
      dist: {
258
        options: {
259
260
          position: 'top',
          banner: '<%= banner %>'
261
        },
262
        files: {
263
264
          src: [
            'dist/css/<%= pkg.name %>.css',
265
            'dist/css/<%= pkg.name %>-rtl.css',
266
            'dist/css/<%= pkg.name %>.min.css',
267
            'dist/css/<%= pkg.name %>-rtl.min.css',
268
            'dist/css/<%= pkg.name %>-theme.css',
269
            'dist/css/<%= pkg.name %>-theme.min.css'
270
          ]
271
272
273
274
        }
      }
    },

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

Mark Otto's avatar
Mark Otto committed
299
300
301
    copy: {
      fonts: {
        expand: true,
302
        src: 'fonts/*',
Mark Otto's avatar
Mark Otto committed
303
        dest: 'dist/'
304
      },
Mark Otto's avatar
Mark Otto committed
305
      docs: {
306
        expand: true,
Mark Otto's avatar
Mark Otto committed
307
308
309
        cwd: './dist',
        src: [
          '{css,js}/*.min.*',
Chris Rebert's avatar
Chris Rebert committed
310
          'css/*.map',
Mark Otto's avatar
Mark Otto committed
311
312
313
          'fonts/*'
        ],
        dest: 'docs/dist'
Mark Otto's avatar
Mark Otto committed
314
315
316
      }
    },

317
318
319
320
    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
321
      files: 'js/tests/index.html'
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
};