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