Gruntfile.js 12.2 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
  var npmShrinkwrap = require('npm-shrinkwrap');
21
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
22
  var getLessVarsData = function () {
Mark Otto's avatar
Mark Otto committed
23
    var filePath = path.join(__dirname, 'scss/_variables.scss');
24
25
26
27
    var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
    var parser = new BsLessdocParser(fileContent);
    return { sections: parser.parseFile() };
  };
Mark Otto's avatar
Mark Otto committed
28
  // var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
29
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
30
31
32
33
34
35
36
  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);
    });
  });
37

38
39
40
41
42
  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
43
    banner: '/*!\n' +
XhmikosR's avatar
XhmikosR committed
44
45
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
46
            ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
XhmikosR's avatar
XhmikosR committed
47
            ' */\n',
48
49
    jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
    jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\n'),
50
51
52

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

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
Chris Rebert's avatar
Chris Rebert committed
61
      grunt: {
62
        options: {
63
          jshintrc: 'grunt/.jshintrc'
64
        },
Zlatan Vasović's avatar
Zlatan Vasović committed
65
        src: ['Gruntfile.js', 'grunt/*.js']
66
      },
67
      core: {
68
        src: 'js/*.js'
69
70
      },
      test: {
XhmikosR's avatar
XhmikosR committed
71
72
73
        options: {
          jshintrc: 'js/tests/unit/.jshintrc'
        },
74
        src: 'js/tests/unit/*.js'
75
76
      },
      assets: {
77
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
78
79
      }
    },
80

Chris Rebert's avatar
Chris Rebert committed
81
82
    jscs: {
      options: {
XhmikosR's avatar
XhmikosR committed
83
        config: 'js/.jscsrc'
Chris Rebert's avatar
Chris Rebert committed
84
      },
Chris Rebert's avatar
Chris Rebert committed
85
      grunt: {
86
        src: '<%= jshint.grunt.src %>'
Chris Rebert's avatar
Chris Rebert committed
87
      },
88
89
      core: {
        src: '<%= jshint.core.src %>'
Chris Rebert's avatar
Chris Rebert committed
90
91
      },
      test: {
92
        src: '<%= jshint.test.src %>'
93
94
      },
      assets: {
95
96
97
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
98
        src: '<%= jshint.assets.src %>'
Chris Rebert's avatar
Chris Rebert committed
99
100
101
      }
    },

102
103
    concat: {
      options: {
104
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        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'
      }
    },
125

126
    uglify: {
127
128
129
      options: {
        preserveComments: 'some'
      },
130
      core: {
131
        src: '<%= concat.bootstrap.dest %>',
132
        dest: 'dist/js/<%= pkg.name %>.min.js'
XhmikosR's avatar
XhmikosR committed
133
134
      },
      customize: {
135
        src: configBridge.paths.customizerJs,
136
        dest: 'docs/assets/js/customize.min.js'
137
138
      },
      docsJs: {
139
        src: configBridge.paths.docsJs,
140
        dest: 'docs/assets/js/docs.min.js'
141
142
143
      }
    },

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

Mark Otto's avatar
Mark Otto committed
151
152
153
154
    sass: {
      options: {
        includePaths: ['scss'],
        precision: 6,
Gleb Mazovetskiy's avatar
Gleb Mazovetskiy committed
155
        sourceComments: false,
Mark Otto's avatar
Mark Otto committed
156
157
        sourceMap: true
      },
Mark Otto's avatar
Mark Otto committed
158
      core: {
Mark Otto's avatar
Mark Otto committed
159
160
161
        files: {
          'dist/css/<%= pkg.name %>.css': 'scss/<%= pkg.name %>.scss'
        }
162
      },
Mark Otto's avatar
Mark Otto committed
163
164
      docs: {
        files: {
Mark Otto's avatar
Mark Otto committed
165
          'docs/assets/css/docs.min.css': 'docs/assets/scss/docs.scss'
Mark Otto's avatar
Mark Otto committed
166
        }
167
168
169
      }
    },

Bas Bosman's avatar
Bas Bosman committed
170
171
    autoprefixer: {
      options: {
172
        browsers: configBridge.config.autoprefixerBrowsers
Bas Bosman's avatar
Bas Bosman committed
173
174
175
176
177
178
179
180
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      docs: {
Mark Otto's avatar
Mark Otto committed
181
        src: 'docs/assets/css/docs.min.css'
Bas Bosman's avatar
Bas Bosman committed
182
183
184
185
186
187
188
189
190
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

XhmikosR's avatar
XhmikosR committed
191
    cssmin: {
XhmikosR's avatar
XhmikosR committed
192
      options: {
193
        compatibility: 'ie8',
194
195
        keepSpecialComments: '*',
        noAdvanced: true
XhmikosR's avatar
XhmikosR committed
196
      },
Mark Otto's avatar
Mark Otto committed
197
198
199
200
      core: {
        files: {
          'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css'
        }
201
      },
XhmikosR's avatar
XhmikosR committed
202
      docs: {
Mark Otto's avatar
Mark Otto committed
203
        src: 'docs/assets/css/docs.min.css',
204
        dest: 'docs/assets/css/docs.min.css'
XhmikosR's avatar
XhmikosR committed
205
206
207
      }
    },

208
    usebanner: {
XhmikosR's avatar
XhmikosR committed
209
210
211
212
213
214
      options: {
        position: 'top',
        banner: '<%= banner %>'
      },
      files: {
        src: 'dist/css/*.css'
215
216
217
      }
    },

218
    csscomb: {
219
      options: {
Mark Otto's avatar
Mark Otto committed
220
        config: 'scss/.csscomb.json'
221
222
      },
      dist: {
XhmikosR's avatar
XhmikosR committed
223
224
225
226
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
227
228
      },
      examples: {
Chris Rebert's avatar
Chris Rebert committed
229
230
        expand: true,
        cwd: 'docs/examples/',
Zlatan Vasović's avatar
Zlatan Vasović committed
231
        src: '**/*.css',
Chris Rebert's avatar
Chris Rebert committed
232
        dest: 'docs/examples/'
XhmikosR's avatar
XhmikosR committed
233
234
      },
      docs: {
235
236
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
237
238
239
      }
    },

Mark Otto's avatar
Mark Otto committed
240
    copy: {
Mark Otto's avatar
Mark Otto committed
241
      docs: {
vsn4ik's avatar
vsn4ik committed
242
243
        src: 'dist/*/*',
        dest: 'docs/'
Mark Otto's avatar
Mark Otto committed
244
245
246
      }
    },

247
248
249
250
251
    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
252
        }
253
254
255
      }
    },

256
    jekyll: {
257
258
259
260
261
262
263
264
265
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
266
267
    },

268
    jade: {
269
270
271
272
273
274
275
276
277
278
279
      options: {
        pretty: true,
        data: getLessVarsData
      },
      customizerVars: {
        src: 'docs/_jade/customizer-variables.jade',
        dest: 'docs/_includes/customizer-variables.html'
      },
      customizerNav: {
        src: 'docs/_jade/customizer-nav.jade',
        dest: 'docs/_includes/nav/customize.html'
280
281
282
      }
    },

283
284
    validation: {
      options: {
285
286
        charset: 'utf-8',
        doctype: 'HTML5',
287
        failHard: true,
288
289
        reset: true,
        relaxerror: [
290
          'Element img is missing required attribute src.',
291
          'Attribute autocomplete not allowed on element input at this point.',
Mark Otto's avatar
Mark Otto committed
292
293
294
295
          'Attribute autocomplete not allowed on element button at this point.',
          'Element div not allowed as child of element progress in this context.',
          'Element thead not allowed as child of element table in this context.',
          'Bad value tablist for attribute role on element nav.'
296
        ]
297
298
      },
      files: {
299
        src: '_gh_pages/**/*.html'
300
301
302
      }
    },

303
304
    watch: {
      src: {
305
        files: '<%= jshint.core.src %>',
306
        tasks: ['jshint:src', 'qunit', 'concat']
307
308
309
310
311
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
312
      less: {
313
        files: 'less/**/*.less',
314
        tasks: 'less'
Mark Otto's avatar
Mark Otto committed
315
316
317
318
      },
      docs: {
        files: 'docs/assets/less/*.less',
        tasks: 'less'
319
      }
320
321
322
323
324
    },

    sed: {
      versionNumber: {
        pattern: (function () {
Chris Rebert's avatar
Chris Rebert committed
325
326
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
327
328
329
330
        })(),
        replacement: grunt.option('newver'),
        recursive: true
      }
331
332
333
334
335
336
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
Chris Rebert's avatar
Chris Rebert committed
337
          concurrency: 10,
338
          maxRetries: 3,
339
          urls: ['http://127.0.0.1:3000/js/tests/index.html'],
340
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
341
342
        }
      }
Chris Rebert's avatar
Chris Rebert committed
343
344
345
346
347
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
Mark Otto's avatar
Mark Otto committed
348
      }
349
350
    }
  });
351
352


353
  // These plugins provide necessary tasks.
XhmikosR's avatar
XhmikosR committed
354
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
XhmikosR's avatar
XhmikosR committed
355
  require('time-grunt')(grunt);
356

357
  // Docs HTML validation task
358
  grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
359

360
361
362
  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
363
364
365
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };
366

367
  // Test task.
368
369
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
370
  if (runSubset('core') &&
Mark Otto's avatar
Mark Otto committed
371
372
    // Skip core tests if this is a Savage build
    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-js', 'docs']);
373
374
  }
  // Skip HTML validation if running a different subset of the test suite
375
376
377
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
378
379
    testSubtasks.push('validate-html');
  }
380
  // Only run Sauce Labs tests if there's a Sauce access key
Chris Rebert's avatar
Chris Rebert committed
381
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
382
      // Skip Sauce if running a different subset of the test suite
383
384
385
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
386
387
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
388
389
  }
  grunt.registerTask('test', testSubtasks);
Chris Rebert's avatar
Chris Rebert committed
390
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
391

392
  // JS distribution task.
393
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
394

395
  // CSS distribution task.
Mark Otto's avatar
Mark Otto committed
396
397
  grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
  grunt.registerTask('dist-css', ['sass-compile', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
Mark Otto's avatar
Mark Otto committed
398

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

402
  // Default task.
Mark Otto's avatar
Mark Otto committed
403
  grunt.registerTask('default', ['clean:dist', 'test']);
404

405
406
407
  // 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!
408
  grunt.registerTask('change-version-number', 'sed');
409

410
  // task for building customizer
Mark Otto's avatar
Mark Otto committed
411
412
413
414
415
416
  // grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  // grunt.registerTask('build-customizer-html', 'jade');
  // grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
  //   var banner = grunt.template.process('<%= banner %>');
  //   generateRawFiles(grunt, banner);
  // });
Chris Rebert's avatar
Chris Rebert committed
417

418
  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
419
420
421
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
422
423
  });

424
425
426
427
  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
  grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
Mark Otto's avatar
Mark Otto committed
428
  grunt.registerTask('docs', ['docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs']);
429

430
431
  grunt.registerTask('docs-github', ['jekyll:github']);

432
433
434
435
436
437
438
  // 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) {
439
        grunt.fail.warn(err);
440
441
442
443
444
445
446
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
447
};