Gruntfile.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /* jshint node: true */
  2. var markdown = require('node-markdown').Markdown;
  3. var fs = require('fs');
  4. module.exports = function(grunt) {
  5. grunt.loadNpmTasks('grunt-contrib-watch');
  6. grunt.loadNpmTasks('grunt-contrib-concat');
  7. grunt.loadNpmTasks('grunt-contrib-copy');
  8. grunt.loadNpmTasks('grunt-contrib-jshint');
  9. grunt.loadNpmTasks('grunt-contrib-uglify');
  10. grunt.loadNpmTasks('grunt-html2js');
  11. grunt.loadNpmTasks('grunt-karma');
  12. grunt.loadNpmTasks('grunt-conventional-changelog');
  13. grunt.loadNpmTasks('grunt-ddescribe-iit');
  14. // Project configuration.
  15. grunt.util.linefeed = '\n';
  16. grunt.initConfig({
  17. ngversion: '1.4.7',
  18. bsversion: '3.1.1',
  19. modules: [],//to be filled in by build task
  20. pkg: grunt.file.readJSON('package.json'),
  21. dist: 'dist',
  22. filename: 'ui-bootstrap',
  23. filenamecustom: '<%= filename %>-custom',
  24. meta: {
  25. modules: 'angular.module("ui.bootstrap", [<%= srcModules %>]);',
  26. tplmodules: 'angular.module("ui.bootstrap.tpls", [<%= tplModules %>]);',
  27. all: 'angular.module("ui.bootstrap", ["ui.bootstrap.tpls", <%= srcModules %>]);',
  28. cssInclude: '',
  29. cssFileBanner: '/* Include this file in your html if you are using the CSP mode. */\n\n',
  30. cssFileDest: '<%= dist %>/<%= filename %>-<%= pkg.version %>-csp.css',
  31. banner: ['/*',
  32. ' * <%= pkg.name %>',
  33. ' * <%= pkg.homepage %>\n',
  34. ' * Version: <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
  35. ' * License: <%= pkg.license %>',
  36. ' */\n'].join('\n')
  37. },
  38. delta: {
  39. docs: {
  40. files: ['misc/demo/index.html'],
  41. tasks: ['after-test']
  42. },
  43. html: {
  44. files: ['template/**/*.html'],
  45. tasks: ['html2js', 'karma:watch:run']
  46. },
  47. js: {
  48. files: ['src/**/*.js'],
  49. //we don't need to jshint here, it slows down everything else
  50. tasks: ['karma:watch:run']
  51. }
  52. },
  53. concat: {
  54. dist: {
  55. options: {
  56. banner: '<%= meta.banner %><%= meta.modules %>\n',
  57. footer: '<%= meta.cssInclude %>'
  58. },
  59. src: [], //src filled in by build task
  60. dest: '<%= dist %>/<%= filename %>-<%= pkg.version %>.js'
  61. },
  62. dist_tpls: {
  63. options: {
  64. banner: '<%= meta.banner %><%= meta.all %>\n<%= meta.tplmodules %>\n',
  65. footer: '<%= meta.cssInclude %>'
  66. },
  67. src: [], //src filled in by build task
  68. dest: '<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.js'
  69. }
  70. },
  71. copy: {
  72. demohtml: {
  73. options: {
  74. //process html files with gruntfile config
  75. processContent: grunt.template.process
  76. },
  77. files: [{
  78. expand: true,
  79. src: ['**/*.html'],
  80. cwd: 'misc/demo/',
  81. dest: 'dist/'
  82. }]
  83. },
  84. demoassets: {
  85. files: [{
  86. expand: true,
  87. //Don't re-copy html files, we process those
  88. src: ['**/**/*', '!**/*.html'],
  89. cwd: 'misc/demo',
  90. dest: 'dist/'
  91. }]
  92. }
  93. },
  94. uglify: {
  95. options: {
  96. banner: '<%= meta.banner %>'
  97. },
  98. dist:{
  99. src:['<%= concat.dist.dest %>'],
  100. dest:'<%= dist %>/<%= filename %>-<%= pkg.version %>.min.js'
  101. },
  102. dist_tpls:{
  103. src:['<%= concat.dist_tpls.dest %>'],
  104. dest:'<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.min.js'
  105. }
  106. },
  107. html2js: {
  108. dist: {
  109. options: {
  110. module: null, // no bundle module for all the html2js templates
  111. base: '.'
  112. },
  113. files: [{
  114. expand: true,
  115. src: ['template/**/*.html'],
  116. ext: '.html.js'
  117. }]
  118. }
  119. },
  120. jshint: {
  121. files: ['Gruntfile.js','src/**/*.js'],
  122. options: {
  123. jshintrc: '.jshintrc'
  124. }
  125. },
  126. karma: {
  127. options: {
  128. configFile: 'karma.conf.js'
  129. },
  130. watch: {
  131. background: true
  132. },
  133. continuous: {
  134. singleRun: true
  135. },
  136. jenkins: {
  137. singleRun: true,
  138. autoWatch: false,
  139. colors: false,
  140. reporters: ['dots', 'junit'],
  141. browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh']
  142. },
  143. travis: {
  144. singleRun: true,
  145. autoWatch: false,
  146. reporters: ['dots'],
  147. browsers: ['Firefox']
  148. },
  149. coverage: {
  150. preprocessors: {
  151. 'src/*/*.js': 'coverage'
  152. },
  153. reporters: ['progress', 'coverage']
  154. }
  155. },
  156. conventionalChangelog: {
  157. options: {
  158. changelogOpts: {
  159. preset: 'angular'
  160. },
  161. templateFile: 'misc/changelog.tpl.md'
  162. },
  163. release: {
  164. src: 'CHANGELOG.md'
  165. }
  166. },
  167. shell: {
  168. //We use %version% and evluate it at run-time, because <%= pkg.version %>
  169. //is only evaluated once
  170. 'release-prepare': [
  171. 'grunt before-test after-test',
  172. 'grunt version', //remove "-SNAPSHOT"
  173. 'grunt conventionalChangelog'
  174. ],
  175. 'release-complete': [
  176. 'git commit CHANGELOG.md package.json -m "chore(release): v%version%"',
  177. 'git tag %version%'
  178. ],
  179. 'release-start': [
  180. 'grunt version:minor:"SNAPSHOT"',
  181. 'git commit package.json -m "chore(release): Starting v%version%"'
  182. ]
  183. },
  184. 'ddescribe-iit': {
  185. files: [
  186. 'src/**/*.spec.js'
  187. ]
  188. }
  189. });
  190. //register before and after test tasks so we've don't have to change cli
  191. //options on the google's CI server
  192. grunt.registerTask('before-test', ['enforce', 'ddescribe-iit', 'jshint', 'html2js']);
  193. grunt.registerTask('after-test', ['build', 'copy']);
  194. //Rename our watch task to 'delta', then make actual 'watch'
  195. //task build things, then start test server
  196. grunt.renameTask('watch', 'delta');
  197. grunt.registerTask('watch', ['before-test', 'after-test', 'karma:watch', 'delta']);
  198. // Default task.
  199. grunt.registerTask('default', ['before-test', 'test', 'after-test']);
  200. grunt.registerTask('enforce', 'Install commit message enforce script if it doesn\'t exist', function() {
  201. if (!grunt.file.exists('.git/hooks/commit-msg')) {
  202. grunt.file.copy('misc/validate-commit-msg.js', '.git/hooks/commit-msg');
  203. require('fs').chmodSync('.git/hooks/commit-msg', '0755');
  204. }
  205. });
  206. //Common ui.bootstrap module containing all modules for src and templates
  207. //findModule: Adds a given module to config
  208. var foundModules = {};
  209. function findModule(name) {
  210. if (foundModules[name]) { return; }
  211. foundModules[name] = true;
  212. function breakup(text, separator) {
  213. return text.replace(/[A-Z]/g, function (match) {
  214. return separator + match;
  215. });
  216. }
  217. function ucwords(text) {
  218. return text.replace(/^([a-z])|\s+([a-z])/g, function ($1) {
  219. return $1.toUpperCase();
  220. });
  221. }
  222. function enquote(str) {
  223. return '"' + str + '"';
  224. }
  225. var module = {
  226. name: name,
  227. moduleName: enquote('ui.bootstrap.' + name),
  228. displayName: ucwords(breakup(name, ' ')),
  229. srcFiles: grunt.file.expand('src/'+name+'/*.js'),
  230. cssFiles: grunt.file.expand('src/'+name+'/*.css'),
  231. tplFiles: grunt.file.expand('template/'+name+'/*.html'),
  232. tpljsFiles: grunt.file.expand('template/'+name+'/*.html.js'),
  233. tplModules: grunt.file.expand('template/'+name+'/*.html').map(enquote),
  234. dependencies: dependenciesForModule(name),
  235. docs: {
  236. md: grunt.file.expand('src/'+name+'/docs/*.md')
  237. .map(grunt.file.read).map(markdown).join('\n'),
  238. js: grunt.file.expand('src/'+name+'/docs/*.js')
  239. .map(grunt.file.read).join('\n'),
  240. html: grunt.file.expand('src/'+name+'/docs/*.html')
  241. .map(grunt.file.read).join('\n')
  242. }
  243. };
  244. var styles = {
  245. css: [],
  246. js: []
  247. };
  248. module.cssFiles.forEach(processCSS.bind(null, styles, true));
  249. if (styles.css.length) {
  250. module.css = styles.css.join('\n');
  251. module.cssJs = styles.js.join('\n');
  252. }
  253. module.dependencies.forEach(findModule);
  254. grunt.config('modules', grunt.config('modules').concat(module));
  255. }
  256. function dependenciesForModule(name) {
  257. var deps = [];
  258. grunt.file.expand('src/' + name + '/*.js')
  259. .map(grunt.file.read)
  260. .forEach(function(contents) {
  261. //Strategy: find where module is declared,
  262. //and from there get everything inside the [] and split them by comma
  263. var moduleDeclIndex = contents.indexOf('angular.module(');
  264. var depArrayStart = contents.indexOf('[', moduleDeclIndex);
  265. var depArrayEnd = contents.indexOf(']', depArrayStart);
  266. var dependencies = contents.substring(depArrayStart + 1, depArrayEnd);
  267. dependencies.split(',').forEach(function(dep) {
  268. if (dep.indexOf('ui.bootstrap.') > -1) {
  269. var depName = dep.trim().replace('ui.bootstrap.','').replace(/['"]/g,'');
  270. if (deps.indexOf(depName) < 0) {
  271. deps.push(depName);
  272. //Get dependencies for this new dependency
  273. deps = deps.concat(dependenciesForModule(depName));
  274. }
  275. }
  276. });
  277. });
  278. return deps;
  279. }
  280. grunt.registerTask('dist', 'Override dist directory', function() {
  281. var dir = this.args[0];
  282. if (dir) { grunt.config('dist', dir); }
  283. });
  284. grunt.registerTask('build', 'Create bootstrap build files', function() {
  285. var _ = grunt.util._;
  286. //If arguments define what modules to build, build those. Else, everything
  287. if (this.args.length) {
  288. this.args.forEach(findModule);
  289. grunt.config('filename', grunt.config('filenamecustom'));
  290. } else {
  291. grunt.file.expand({
  292. filter: 'isDirectory', cwd: '.'
  293. }, 'src/*').forEach(function(dir) {
  294. findModule(dir.split('/')[1]);
  295. });
  296. }
  297. var modules = grunt.config('modules');
  298. grunt.config('srcModules', _.pluck(modules, 'moduleName'));
  299. grunt.config('tplModules', _.pluck(modules, 'tplModules').filter(function(tpls) { return tpls.length > 0;} ));
  300. grunt.config('demoModules', modules
  301. .filter(function(module) {
  302. return module.docs.md && module.docs.js && module.docs.html;
  303. })
  304. .sort(function(a, b) {
  305. if (a.name < b.name) { return -1; }
  306. if (a.name > b.name) { return 1; }
  307. return 0;
  308. })
  309. );
  310. var cssStrings = _.flatten(_.compact(_.pluck(modules, 'css')));
  311. var cssJsStrings = _.flatten(_.compact(_.pluck(modules, 'cssJs')));
  312. if (cssStrings.length) {
  313. grunt.config('meta.cssInclude', cssJsStrings.join('\n'));
  314. grunt.file.write(grunt.config('meta.cssFileDest'), grunt.config('meta.cssFileBanner') +
  315. cssStrings.join('\n'));
  316. grunt.log.writeln('File ' + grunt.config('meta.cssFileDest') + ' created');
  317. }
  318. var moduleFileMapping = _.clone(modules, true);
  319. moduleFileMapping.forEach(function (module) {
  320. delete module.docs;
  321. });
  322. grunt.config('moduleFileMapping', moduleFileMapping);
  323. var srcFiles = _.pluck(modules, 'srcFiles');
  324. var tpljsFiles = _.pluck(modules, 'tpljsFiles');
  325. //Set the concat task to concatenate the given src modules
  326. grunt.config('concat.dist.src', grunt.config('concat.dist.src')
  327. .concat(srcFiles));
  328. //Set the concat-with-templates task to concat the given src & tpl modules
  329. grunt.config('concat.dist_tpls.src', grunt.config('concat.dist_tpls.src')
  330. .concat(srcFiles).concat(tpljsFiles));
  331. grunt.task.run(['concat', 'uglify', 'makeModuleMappingFile', 'makeRawFilesJs', 'makeVersionsMappingFile']);
  332. });
  333. grunt.registerTask('test', 'Run tests on singleRun karma server', function () {
  334. //this task can be executed in 3 different environments: local, Travis-CI and Jenkins-CI
  335. //we need to take settings for each one into account
  336. if (process.env.TRAVIS) {
  337. grunt.task.run('karma:travis');
  338. } else {
  339. var isToRunJenkinsTask = !!this.args.length;
  340. if(grunt.option('coverage')) {
  341. var karmaOptions = grunt.config.get('karma.options'),
  342. coverageOpts = grunt.config.get('karma.coverage');
  343. grunt.util._.extend(karmaOptions, coverageOpts);
  344. grunt.config.set('karma.options', karmaOptions);
  345. }
  346. grunt.task.run(this.args.length ? 'karma:jenkins' : 'karma:continuous');
  347. }
  348. });
  349. grunt.registerTask('makeModuleMappingFile', function () {
  350. var _ = grunt.util._;
  351. var moduleMappingJs = 'dist/assets/module-mapping.json';
  352. var moduleMappings = grunt.config('moduleFileMapping');
  353. var moduleMappingsMap = _.object(_.pluck(moduleMappings, 'name'), moduleMappings);
  354. var jsContent = JSON.stringify(moduleMappingsMap);
  355. grunt.file.write(moduleMappingJs, jsContent);
  356. grunt.log.writeln('File ' + moduleMappingJs.cyan + ' created.');
  357. });
  358. grunt.registerTask('makeRawFilesJs', function () {
  359. var _ = grunt.util._;
  360. var jsFilename = 'dist/assets/raw-files.json';
  361. var genRawFilesJs = require('./misc/raw-files-generator');
  362. genRawFilesJs(grunt, jsFilename, _.flatten(grunt.config('concat.dist_tpls.src')),
  363. grunt.config('meta.banner'), grunt.config('meta.cssFileBanner'));
  364. });
  365. grunt.registerTask('makeVersionsMappingFile', function () {
  366. var done = this.async();
  367. var exec = require('child_process').exec;
  368. var versionsMappingFile = 'dist/versions-mapping.json';
  369. exec('git tag --sort -version:refname', function(error, stdout, stderr) {
  370. // Let's remove the oldest 14 versions.
  371. var versions = stdout.split('\n').slice(0, -14);
  372. var jsContent = versions.map(function(version) {
  373. return {
  374. version: version,
  375. url: '/bootstrap/versioned-docs/' + version
  376. };
  377. });
  378. jsContent[0] = {
  379. version: 'Current',
  380. url: '/bootstrap'
  381. };
  382. grunt.file.write(versionsMappingFile, JSON.stringify(jsContent));
  383. grunt.log.writeln('File ' + versionsMappingFile.cyan + ' created.');
  384. done();
  385. });
  386. });
  387. /**
  388. * Logic from AngularJS
  389. * https://github.com/angular/angular.js/blob/36831eccd1da37c089f2141a2c073a6db69f3e1d/lib/grunt/utils.js#L121-L145
  390. */
  391. function processCSS(state, minify, file) {
  392. /* jshint quotmark: false */
  393. var css = fs.readFileSync(file).toString(),
  394. js;
  395. state.css.push(css);
  396. if(minify){
  397. css = css
  398. .replace(/\r?\n/g, '')
  399. .replace(/\/\*.*?\*\//g, '')
  400. .replace(/:\s+/g, ':')
  401. .replace(/\s*\{\s*/g, '{')
  402. .replace(/\s*\}\s*/g, '}')
  403. .replace(/\s*\,\s*/g, ',')
  404. .replace(/\s*\;\s*/g, ';');
  405. }
  406. //escape for js
  407. css = css
  408. .replace(/\\/g, '\\\\')
  409. .replace(/'/g, "\\'")
  410. .replace(/\r?\n/g, '\\n');
  411. js = "!angular.$$csp() && angular.element(document).find('head').prepend('<style type=\"text/css\">" + css + "</style>');";
  412. state.js.push(js);
  413. return state;
  414. }
  415. function setVersion(type, suffix) {
  416. var file = 'package.json';
  417. var VERSION_REGEX = /([\'|\"]version[\'|\"][ ]*:[ ]*[\'|\"])([\d|.]*)(-\w+)*([\'|\"])/;
  418. var contents = grunt.file.read(file);
  419. var version;
  420. contents = contents.replace(VERSION_REGEX, function(match, left, center) {
  421. version = center;
  422. if (type) {
  423. version = require('semver').inc(version, type);
  424. }
  425. //semver.inc strips our suffix if it existed
  426. if (suffix) {
  427. version += '-' + suffix;
  428. }
  429. return left + version + '"';
  430. });
  431. grunt.log.ok('Version set to ' + version.cyan);
  432. grunt.file.write(file, contents);
  433. return version;
  434. }
  435. grunt.registerTask('version', 'Set version. If no arguments, it just takes off suffix', function() {
  436. setVersion(this.args[0], this.args[1]);
  437. });
  438. grunt.registerMultiTask('shell', 'run shell commands', function() {
  439. var self = this;
  440. var sh = require('shelljs');
  441. self.data.forEach(function(cmd) {
  442. cmd = cmd.replace('%version%', grunt.file.readJSON('package.json').version);
  443. grunt.log.ok(cmd);
  444. var result = sh.exec(cmd,{silent:true});
  445. if (result.code !== 0) {
  446. grunt.fatal(result.output);
  447. }
  448. });
  449. });
  450. return grunt;
  451. };