Gruntfile.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. /*!
  2. * Bootstrap's Gruntfile
  3. * http://getbootstrap.com
  4. * Copyright 2013-2015 Twitter, Inc.
  5. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  6. */
  7. module.exports = function (grunt) {
  8. 'use strict';
  9. // Force use of Unix newlines
  10. grunt.util.linefeed = '\n';
  11. RegExp.quote = function (string) {
  12. return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
  13. };
  14. var fs = require('fs');
  15. var path = require('path');
  16. var npmShrinkwrap = require('npm-shrinkwrap');
  17. var generateGlyphiconsData = require('./grunt/bs-glyphicons-data-generator.js');
  18. var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
  19. var getLessVarsData = function () {
  20. var filePath = path.join(__dirname, 'less/variables.less');
  21. var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
  22. var parser = new BsLessdocParser(fileContent);
  23. return { sections: parser.parseFile() };
  24. };
  25. var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
  26. var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
  27. var configBridge = grunt.file.readJSON('./grunt/configBridge.json', { encoding: 'utf8' });
  28. Object.keys(configBridge.paths).forEach(function (key) {
  29. configBridge.paths[key].forEach(function (val, i, arr) {
  30. arr[i] = path.join('./docs/assets', val);
  31. });
  32. });
  33. // Project configuration.
  34. grunt.initConfig({
  35. // Metadata.
  36. pkg: grunt.file.readJSON('package.json'),
  37. banner: '/*!\n' +
  38. ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
  39. ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
  40. ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' +
  41. ' */\n',
  42. jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
  43. jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\n'),
  44. // Task configuration.
  45. clean: {
  46. dist: 'dist',
  47. docs: 'docs/dist'
  48. },
  49. jshint: {
  50. options: {
  51. jshintrc: 'js/.jshintrc'
  52. },
  53. grunt: {
  54. options: {
  55. jshintrc: 'grunt/.jshintrc'
  56. },
  57. src: ['Gruntfile.js', 'grunt/*.js']
  58. },
  59. core: {
  60. src: 'js/*.js'
  61. },
  62. test: {
  63. options: {
  64. jshintrc: 'js/tests/unit/.jshintrc'
  65. },
  66. src: 'js/tests/unit/*.js'
  67. },
  68. assets: {
  69. src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
  70. }
  71. },
  72. jscs: {
  73. options: {
  74. config: 'js/.jscsrc'
  75. },
  76. grunt: {
  77. src: '<%= jshint.grunt.src %>'
  78. },
  79. core: {
  80. src: '<%= jshint.core.src %>'
  81. },
  82. test: {
  83. src: '<%= jshint.test.src %>'
  84. },
  85. assets: {
  86. options: {
  87. requireCamelCaseOrUpperCaseIdentifiers: null
  88. },
  89. src: '<%= jshint.assets.src %>'
  90. }
  91. },
  92. concat: {
  93. options: {
  94. banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
  95. stripBanners: false
  96. },
  97. bootstrap: {
  98. src: [
  99. 'js/transition.js',
  100. 'js/alert.js',
  101. 'js/button.js',
  102. 'js/carousel.js',
  103. 'js/collapse.js',
  104. 'js/dropdown.js',
  105. 'js/modal.js',
  106. 'js/tooltip.js',
  107. 'js/popover.js',
  108. 'js/scrollspy.js',
  109. 'js/tab.js',
  110. 'js/affix.js'
  111. ],
  112. dest: 'dist/js/<%= pkg.name %>.js'
  113. }
  114. },
  115. uglify: {
  116. options: {
  117. preserveComments: 'some'
  118. },
  119. core: {
  120. src: '<%= concat.bootstrap.dest %>',
  121. dest: 'dist/js/<%= pkg.name %>.min.js'
  122. },
  123. customize: {
  124. src: configBridge.paths.customizerJs,
  125. dest: 'docs/assets/js/customize.min.js'
  126. },
  127. docsJs: {
  128. src: configBridge.paths.docsJs,
  129. dest: 'docs/assets/js/docs.min.js'
  130. }
  131. },
  132. qunit: {
  133. options: {
  134. inject: 'js/tests/unit/phantom.js'
  135. },
  136. files: 'js/tests/index.html'
  137. },
  138. less: {
  139. compileCore: {
  140. options: {
  141. strictMath: true,
  142. sourceMap: true,
  143. outputSourceFiles: true,
  144. sourceMapURL: '<%= pkg.name %>.css.map',
  145. sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
  146. },
  147. src: 'less/bootstrap.less',
  148. dest: 'dist/css/<%= pkg.name %>.css'
  149. },
  150. compileTheme: {
  151. options: {
  152. strictMath: true,
  153. sourceMap: true,
  154. outputSourceFiles: true,
  155. sourceMapURL: '<%= pkg.name %>-theme.css.map',
  156. sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
  157. },
  158. src: 'less/theme.less',
  159. dest: 'dist/css/<%= pkg.name %>-theme.css'
  160. }
  161. },
  162. autoprefixer: {
  163. options: {
  164. browsers: configBridge.config.autoprefixerBrowsers
  165. },
  166. core: {
  167. options: {
  168. map: true
  169. },
  170. src: 'dist/css/<%= pkg.name %>.css'
  171. },
  172. theme: {
  173. options: {
  174. map: true
  175. },
  176. src: 'dist/css/<%= pkg.name %>-theme.css'
  177. },
  178. docs: {
  179. src: 'docs/assets/css/src/docs.css'
  180. },
  181. examples: {
  182. expand: true,
  183. cwd: 'docs/examples/',
  184. src: ['**/*.css'],
  185. dest: 'docs/examples/'
  186. }
  187. },
  188. csslint: {
  189. options: {
  190. csslintrc: 'less/.csslintrc'
  191. },
  192. dist: [
  193. 'dist/css/bootstrap.css',
  194. 'dist/css/bootstrap-theme.css'
  195. ],
  196. examples: [
  197. 'docs/examples/**/*.css'
  198. ],
  199. docs: {
  200. options: {
  201. ids: false,
  202. 'overqualified-elements': false
  203. },
  204. src: 'docs/assets/css/src/docs.css'
  205. }
  206. },
  207. cssmin: {
  208. options: {
  209. compatibility: 'ie8',
  210. keepSpecialComments: '*',
  211. advanced: false
  212. },
  213. minifyCore: {
  214. src: 'dist/css/<%= pkg.name %>.css',
  215. dest: 'dist/css/<%= pkg.name %>.min.css'
  216. },
  217. minifyTheme: {
  218. src: 'dist/css/<%= pkg.name %>-theme.css',
  219. dest: 'dist/css/<%= pkg.name %>-theme.min.css'
  220. },
  221. docs: {
  222. src: [
  223. 'docs/assets/css/src/docs.css',
  224. 'docs/assets/css/src/pygments-manni.css'
  225. ],
  226. dest: 'docs/assets/css/docs.min.css'
  227. }
  228. },
  229. usebanner: {
  230. options: {
  231. position: 'top',
  232. banner: '<%= banner %>'
  233. },
  234. files: {
  235. src: 'dist/css/*.css'
  236. }
  237. },
  238. csscomb: {
  239. options: {
  240. config: 'less/.csscomb.json'
  241. },
  242. dist: {
  243. expand: true,
  244. cwd: 'dist/css/',
  245. src: ['*.css', '!*.min.css'],
  246. dest: 'dist/css/'
  247. },
  248. examples: {
  249. expand: true,
  250. cwd: 'docs/examples/',
  251. src: '**/*.css',
  252. dest: 'docs/examples/'
  253. },
  254. docs: {
  255. src: 'docs/assets/css/src/docs.css',
  256. dest: 'docs/assets/css/src/docs.css'
  257. }
  258. },
  259. copy: {
  260. fonts: {
  261. src: 'fonts/*',
  262. dest: 'dist/'
  263. },
  264. docs: {
  265. src: 'dist/*/*',
  266. dest: 'docs/'
  267. }
  268. },
  269. connect: {
  270. server: {
  271. options: {
  272. port: 3000,
  273. base: '.'
  274. }
  275. }
  276. },
  277. jekyll: {
  278. options: {
  279. config: '_config.yml'
  280. },
  281. docs: {},
  282. github: {
  283. options: {
  284. raw: 'github: true'
  285. }
  286. }
  287. },
  288. jade: {
  289. options: {
  290. pretty: true,
  291. data: getLessVarsData
  292. },
  293. customizerVars: {
  294. src: 'docs/_jade/customizer-variables.jade',
  295. dest: 'docs/_includes/customizer-variables.html'
  296. },
  297. customizerNav: {
  298. src: 'docs/_jade/customizer-nav.jade',
  299. dest: 'docs/_includes/nav/customize.html'
  300. }
  301. },
  302. validation: {
  303. options: {
  304. charset: 'utf-8',
  305. doctype: 'HTML5',
  306. failHard: true,
  307. reset: true,
  308. relaxerror: [
  309. 'Element img is missing required attribute src.',
  310. 'Attribute autocomplete not allowed on element input at this point.',
  311. 'Attribute autocomplete not allowed on element button at this point.',
  312. 'Bad value separator for attribute role on element li.'
  313. ]
  314. },
  315. files: {
  316. src: '_gh_pages/**/*.html'
  317. }
  318. },
  319. watch: {
  320. src: {
  321. files: '<%= jshint.core.src %>',
  322. tasks: ['jshint:src', 'qunit', 'concat']
  323. },
  324. test: {
  325. files: '<%= jshint.test.src %>',
  326. tasks: ['jshint:test', 'qunit']
  327. },
  328. less: {
  329. files: 'less/**/*.less',
  330. tasks: 'less'
  331. }
  332. },
  333. sed: {
  334. versionNumber: {
  335. pattern: (function () {
  336. var old = grunt.option('oldver');
  337. return old ? RegExp.quote(old) : old;
  338. })(),
  339. replacement: grunt.option('newver'),
  340. recursive: true
  341. }
  342. },
  343. 'saucelabs-qunit': {
  344. all: {
  345. options: {
  346. build: process.env.TRAVIS_JOB_ID,
  347. throttled: 10,
  348. maxRetries: 3,
  349. maxPollRetries: 4,
  350. urls: ['http://127.0.0.1:3000/js/tests/index.html'],
  351. browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
  352. }
  353. }
  354. },
  355. exec: {
  356. npmUpdate: {
  357. command: 'npm update'
  358. }
  359. },
  360. compress: {
  361. main: {
  362. options: {
  363. archive: 'bootstrap-<%= pkg.version %>-dist.zip',
  364. mode: 'zip',
  365. level: 9,
  366. pretty: true
  367. },
  368. files: [
  369. {
  370. expand: true,
  371. cwd: 'dist/',
  372. src: ['**'],
  373. dest: 'bootstrap-<%= pkg.version %>-dist'
  374. }
  375. ]
  376. }
  377. }
  378. });
  379. // These plugins provide necessary tasks.
  380. require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
  381. require('time-grunt')(grunt);
  382. // Docs HTML validation task
  383. grunt.registerTask('validate-html', ['jekyll:docs', 'validation']);
  384. var runSubset = function (subset) {
  385. return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  386. };
  387. var isUndefOrNonZero = function (val) {
  388. return val === undefined || val !== '0';
  389. };
  390. // Test task.
  391. var testSubtasks = [];
  392. // Skip core tests if running a different subset of the test suite
  393. if (runSubset('core') &&
  394. // Skip core tests if this is a Savage build
  395. process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
  396. testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
  397. }
  398. // Skip HTML validation if running a different subset of the test suite
  399. if (runSubset('validate-html') &&
  400. // Skip HTML5 validator on Travis when [skip validator] is in the commit message
  401. isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
  402. testSubtasks.push('validate-html');
  403. }
  404. // Only run Sauce Labs tests if there's a Sauce access key
  405. if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
  406. // Skip Sauce if running a different subset of the test suite
  407. runSubset('sauce-js-unit') &&
  408. // Skip Sauce on Travis when [skip sauce] is in the commit message
  409. isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
  410. testSubtasks.push('connect');
  411. testSubtasks.push('saucelabs-qunit');
  412. }
  413. grunt.registerTask('test', testSubtasks);
  414. grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);
  415. // JS distribution task.
  416. grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);
  417. // CSS distribution task.
  418. grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
  419. grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
  420. // Full distribution task.
  421. grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
  422. // Default task.
  423. grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);
  424. // Version numbering task.
  425. // grunt change-version-number --oldver=A.B.C --newver=X.Y.Z
  426. // This can be overzealous, so its changes should always be manually reviewed!
  427. grunt.registerTask('change-version-number', 'sed');
  428. grunt.registerTask('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); });
  429. // task for building customizer
  430. grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  431. grunt.registerTask('build-customizer-html', 'jade');
  432. grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
  433. var banner = grunt.template.process('<%= banner %>');
  434. generateRawFiles(grunt, banner);
  435. });
  436. grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
  437. var srcFiles = grunt.config.get('concat.bootstrap.src');
  438. var destFilepath = 'dist/js/npm.js';
  439. generateCommonJSModule(grunt, srcFiles, destFilepath);
  440. });
  441. // Docs task.
  442. grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
  443. grunt.registerTask('lint-docs-css', ['csslint:docs', 'csslint:examples']);
  444. grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
  445. grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
  446. grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);
  447. grunt.registerTask('prep-release', ['jekyll:github', 'compress']);
  448. // Task for updating the cached npm packages used by the Travis build (which are controlled by test-infra/npm-shrinkwrap.json).
  449. // This task should be run and the updated file should be committed whenever Bootstrap's dependencies change.
  450. grunt.registerTask('update-shrinkwrap', ['exec:npmUpdate', '_update-shrinkwrap']);
  451. grunt.registerTask('_update-shrinkwrap', function () {
  452. var done = this.async();
  453. npmShrinkwrap({ dev: true, dirname: __dirname }, function (err) {
  454. if (err) {
  455. grunt.fail.warn(err);
  456. }
  457. var dest = 'test-infra/npm-shrinkwrap.json';
  458. fs.renameSync('npm-shrinkwrap.json', dest);
  459. grunt.log.writeln('File ' + dest.cyan + ' updated.');
  460. done();
  461. });
  462. });
  463. };