blob: 147adb47abec7fb332e229e373316697e7da16df [file] [log] [blame]
/* jshint node: true */
var markdown = require('node-markdown').Markdown;
var fs = require('fs');
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-html2js');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-conventional-changelog');
grunt.loadNpmTasks('grunt-ddescribe-iit');
// Project configuration.
grunt.util.linefeed = '\n';
grunt.initConfig({
ngversion: '1.4.7',
bsversion: '3.1.1',
modules: [],//to be filled in by build task
pkg: grunt.file.readJSON('package.json'),
dist: 'dist',
filename: 'ui-bootstrap',
filenamecustom: '<%= filename %>-custom',
meta: {
modules: 'angular.module("ui.bootstrap", [<%= srcModules %>]);',
tplmodules: 'angular.module("ui.bootstrap.tpls", [<%= tplModules %>]);',
all: 'angular.module("ui.bootstrap", ["ui.bootstrap.tpls", <%= srcModules %>]);',
cssInclude: '',
cssFileBanner: '/* Include this file in your html if you are using the CSP mode. */\n\n',
cssFileDest: '<%= dist %>/<%= filename %>-<%= pkg.version %>-csp.css',
banner: ['/*',
' * <%= pkg.name %>',
' * <%= pkg.homepage %>\n',
' * Version: <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
' * License: <%= pkg.license %>',
' */\n'].join('\n')
},
delta: {
docs: {
files: ['misc/demo/index.html'],
tasks: ['after-test']
},
html: {
files: ['template/**/*.html'],
tasks: ['html2js', 'karma:watch:run']
},
js: {
files: ['src/**/*.js'],
//we don't need to jshint here, it slows down everything else
tasks: ['karma:watch:run']
}
},
concat: {
dist: {
options: {
banner: '<%= meta.banner %><%= meta.modules %>\n',
footer: '<%= meta.cssInclude %>'
},
src: [], //src filled in by build task
dest: '<%= dist %>/<%= filename %>-<%= pkg.version %>.js'
},
dist_tpls: {
options: {
banner: '<%= meta.banner %><%= meta.all %>\n<%= meta.tplmodules %>\n',
footer: '<%= meta.cssInclude %>'
},
src: [], //src filled in by build task
dest: '<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.js'
}
},
copy: {
demohtml: {
options: {
//process html files with gruntfile config
processContent: grunt.template.process
},
files: [{
expand: true,
src: ['**/*.html'],
cwd: 'misc/demo/',
dest: 'dist/'
}]
},
demoassets: {
files: [{
expand: true,
//Don't re-copy html files, we process those
src: ['**/**/*', '!**/*.html'],
cwd: 'misc/demo',
dest: 'dist/'
}]
}
},
uglify: {
options: {
banner: '<%= meta.banner %>'
},
dist:{
src:['<%= concat.dist.dest %>'],
dest:'<%= dist %>/<%= filename %>-<%= pkg.version %>.min.js'
},
dist_tpls:{
src:['<%= concat.dist_tpls.dest %>'],
dest:'<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.min.js'
}
},
html2js: {
dist: {
options: {
module: null, // no bundle module for all the html2js templates
base: '.'
},
files: [{
expand: true,
src: ['template/**/*.html'],
ext: '.html.js'
}]
}
},
jshint: {
files: ['Gruntfile.js','src/**/*.js'],
options: {
jshintrc: '.jshintrc'
}
},
karma: {
options: {
configFile: 'karma.conf.js'
},
watch: {
background: true
},
continuous: {
singleRun: true
},
jenkins: {
singleRun: true,
autoWatch: false,
colors: false,
reporters: ['dots', 'junit'],
browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh']
},
travis: {
singleRun: true,
autoWatch: false,
reporters: ['dots'],
browsers: ['Firefox']
},
coverage: {
preprocessors: {
'src/*/*.js': 'coverage'
},
reporters: ['progress', 'coverage']
}
},
conventionalChangelog: {
options: {
changelogOpts: {
preset: 'angular'
},
templateFile: 'misc/changelog.tpl.md'
},
release: {
src: 'CHANGELOG.md'
}
},
shell: {
//We use %version% and evluate it at run-time, because <%= pkg.version %>
//is only evaluated once
'release-prepare': [
'grunt before-test after-test',
'grunt version', //remove "-SNAPSHOT"
'grunt conventionalChangelog'
],
'release-complete': [
'git commit CHANGELOG.md package.json -m "chore(release): v%version%"',
'git tag %version%'
],
'release-start': [
'grunt version:minor:"SNAPSHOT"',
'git commit package.json -m "chore(release): Starting v%version%"'
]
},
'ddescribe-iit': {
files: [
'src/**/*.spec.js'
]
}
});
//register before and after test tasks so we've don't have to change cli
//options on the google's CI server
grunt.registerTask('before-test', ['enforce', 'ddescribe-iit', 'jshint', 'html2js']);
grunt.registerTask('after-test', ['build', 'copy']);
//Rename our watch task to 'delta', then make actual 'watch'
//task build things, then start test server
grunt.renameTask('watch', 'delta');
grunt.registerTask('watch', ['before-test', 'after-test', 'karma:watch', 'delta']);
// Default task.
grunt.registerTask('default', ['before-test', 'test', 'after-test']);
grunt.registerTask('enforce', 'Install commit message enforce script if it doesn\'t exist', function() {
if (!grunt.file.exists('.git/hooks/commit-msg')) {
grunt.file.copy('misc/validate-commit-msg.js', '.git/hooks/commit-msg');
require('fs').chmodSync('.git/hooks/commit-msg', '0755');
}
});
//Common ui.bootstrap module containing all modules for src and templates
//findModule: Adds a given module to config
var foundModules = {};
function findModule(name) {
if (foundModules[name]) { return; }
foundModules[name] = true;
function breakup(text, separator) {
return text.replace(/[A-Z]/g, function (match) {
return separator + match;
});
}
function ucwords(text) {
return text.replace(/^([a-z])|\s+([a-z])/g, function ($1) {
return $1.toUpperCase();
});
}
function enquote(str) {
return '"' + str + '"';
}
var module = {
name: name,
moduleName: enquote('ui.bootstrap.' + name),
displayName: ucwords(breakup(name, ' ')),
srcFiles: grunt.file.expand('src/'+name+'/*.js'),
cssFiles: grunt.file.expand('src/'+name+'/*.css'),
tplFiles: grunt.file.expand('template/'+name+'/*.html'),
tpljsFiles: grunt.file.expand('template/'+name+'/*.html.js'),
tplModules: grunt.file.expand('template/'+name+'/*.html').map(enquote),
dependencies: dependenciesForModule(name),
docs: {
md: grunt.file.expand('src/'+name+'/docs/*.md')
.map(grunt.file.read).map(markdown).join('\n'),
js: grunt.file.expand('src/'+name+'/docs/*.js')
.map(grunt.file.read).join('\n'),
html: grunt.file.expand('src/'+name+'/docs/*.html')
.map(grunt.file.read).join('\n')
}
};
var styles = {
css: [],
js: []
};
module.cssFiles.forEach(processCSS.bind(null, styles, true));
if (styles.css.length) {
module.css = styles.css.join('\n');
module.cssJs = styles.js.join('\n');
}
module.dependencies.forEach(findModule);
grunt.config('modules', grunt.config('modules').concat(module));
}
function dependenciesForModule(name) {
var deps = [];
grunt.file.expand('src/' + name + '/*.js')
.map(grunt.file.read)
.forEach(function(contents) {
//Strategy: find where module is declared,
//and from there get everything inside the [] and split them by comma
var moduleDeclIndex = contents.indexOf('angular.module(');
var depArrayStart = contents.indexOf('[', moduleDeclIndex);
var depArrayEnd = contents.indexOf(']', depArrayStart);
var dependencies = contents.substring(depArrayStart + 1, depArrayEnd);
dependencies.split(',').forEach(function(dep) {
if (dep.indexOf('ui.bootstrap.') > -1) {
var depName = dep.trim().replace('ui.bootstrap.','').replace(/['"]/g,'');
if (deps.indexOf(depName) < 0) {
deps.push(depName);
//Get dependencies for this new dependency
deps = deps.concat(dependenciesForModule(depName));
}
}
});
});
return deps;
}
grunt.registerTask('dist', 'Override dist directory', function() {
var dir = this.args[0];
if (dir) { grunt.config('dist', dir); }
});
grunt.registerTask('build', 'Create bootstrap build files', function() {
var _ = grunt.util._;
//If arguments define what modules to build, build those. Else, everything
if (this.args.length) {
this.args.forEach(findModule);
grunt.config('filename', grunt.config('filenamecustom'));
} else {
grunt.file.expand({
filter: 'isDirectory', cwd: '.'
}, 'src/*').forEach(function(dir) {
findModule(dir.split('/')[1]);
});
}
var modules = grunt.config('modules');
grunt.config('srcModules', _.pluck(modules, 'moduleName'));
grunt.config('tplModules', _.pluck(modules, 'tplModules').filter(function(tpls) { return tpls.length > 0;} ));
grunt.config('demoModules', modules
.filter(function(module) {
return module.docs.md && module.docs.js && module.docs.html;
})
.sort(function(a, b) {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
})
);
var cssStrings = _.flatten(_.compact(_.pluck(modules, 'css')));
var cssJsStrings = _.flatten(_.compact(_.pluck(modules, 'cssJs')));
if (cssStrings.length) {
grunt.config('meta.cssInclude', cssJsStrings.join('\n'));
grunt.file.write(grunt.config('meta.cssFileDest'), grunt.config('meta.cssFileBanner') +
cssStrings.join('\n'));
grunt.log.writeln('File ' + grunt.config('meta.cssFileDest') + ' created');
}
var moduleFileMapping = _.clone(modules, true);
moduleFileMapping.forEach(function (module) {
delete module.docs;
});
grunt.config('moduleFileMapping', moduleFileMapping);
var srcFiles = _.pluck(modules, 'srcFiles');
var tpljsFiles = _.pluck(modules, 'tpljsFiles');
//Set the concat task to concatenate the given src modules
grunt.config('concat.dist.src', grunt.config('concat.dist.src')
.concat(srcFiles));
//Set the concat-with-templates task to concat the given src & tpl modules
grunt.config('concat.dist_tpls.src', grunt.config('concat.dist_tpls.src')
.concat(srcFiles).concat(tpljsFiles));
grunt.task.run(['concat', 'uglify', 'makeModuleMappingFile', 'makeRawFilesJs', 'makeVersionsMappingFile']);
});
grunt.registerTask('test', 'Run tests on singleRun karma server', function () {
//this task can be executed in 3 different environments: local, Travis-CI and Jenkins-CI
//we need to take settings for each one into account
if (process.env.TRAVIS) {
grunt.task.run('karma:travis');
} else {
var isToRunJenkinsTask = !!this.args.length;
if(grunt.option('coverage')) {
var karmaOptions = grunt.config.get('karma.options'),
coverageOpts = grunt.config.get('karma.coverage');
grunt.util._.extend(karmaOptions, coverageOpts);
grunt.config.set('karma.options', karmaOptions);
}
grunt.task.run(this.args.length ? 'karma:jenkins' : 'karma:continuous');
}
});
grunt.registerTask('makeModuleMappingFile', function () {
var _ = grunt.util._;
var moduleMappingJs = 'dist/assets/module-mapping.json';
var moduleMappings = grunt.config('moduleFileMapping');
var moduleMappingsMap = _.object(_.pluck(moduleMappings, 'name'), moduleMappings);
var jsContent = JSON.stringify(moduleMappingsMap);
grunt.file.write(moduleMappingJs, jsContent);
grunt.log.writeln('File ' + moduleMappingJs.cyan + ' created.');
});
grunt.registerTask('makeRawFilesJs', function () {
var _ = grunt.util._;
var jsFilename = 'dist/assets/raw-files.json';
var genRawFilesJs = require('./misc/raw-files-generator');
genRawFilesJs(grunt, jsFilename, _.flatten(grunt.config('concat.dist_tpls.src')),
grunt.config('meta.banner'), grunt.config('meta.cssFileBanner'));
});
grunt.registerTask('makeVersionsMappingFile', function () {
var done = this.async();
var exec = require('child_process').exec;
var versionsMappingFile = 'dist/versions-mapping.json';
exec('git tag --sort -version:refname', function(error, stdout, stderr) {
// Let's remove the oldest 14 versions.
var versions = stdout.split('\n').slice(0, -14);
var jsContent = versions.map(function(version) {
return {
version: version,
url: '/bootstrap/versioned-docs/' + version
};
});
jsContent[0] = {
version: 'Current',
url: '/bootstrap'
};
grunt.file.write(versionsMappingFile, JSON.stringify(jsContent));
grunt.log.writeln('File ' + versionsMappingFile.cyan + ' created.');
done();
});
});
/**
* Logic from AngularJS
* https://github.com/angular/angular.js/blob/36831eccd1da37c089f2141a2c073a6db69f3e1d/lib/grunt/utils.js#L121-L145
*/
function processCSS(state, minify, file) {
/* jshint quotmark: false */
var css = fs.readFileSync(file).toString(),
js;
state.css.push(css);
if(minify){
css = css
.replace(/\r?\n/g, '')
.replace(/\/\*.*?\*\//g, '')
.replace(/:\s+/g, ':')
.replace(/\s*\{\s*/g, '{')
.replace(/\s*\}\s*/g, '}')
.replace(/\s*\,\s*/g, ',')
.replace(/\s*\;\s*/g, ';');
}
//escape for js
css = css
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\r?\n/g, '\\n');
js = "!angular.$$csp() && angular.element(document).find('head').prepend('<style type=\"text/css\">" + css + "</style>');";
state.js.push(js);
return state;
}
function setVersion(type, suffix) {
var file = 'package.json';
var VERSION_REGEX = /([\'|\"]version[\'|\"][ ]*:[ ]*[\'|\"])([\d|.]*)(-\w+)*([\'|\"])/;
var contents = grunt.file.read(file);
var version;
contents = contents.replace(VERSION_REGEX, function(match, left, center) {
version = center;
if (type) {
version = require('semver').inc(version, type);
}
//semver.inc strips our suffix if it existed
if (suffix) {
version += '-' + suffix;
}
return left + version + '"';
});
grunt.log.ok('Version set to ' + version.cyan);
grunt.file.write(file, contents);
return version;
}
grunt.registerTask('version', 'Set version. If no arguments, it just takes off suffix', function() {
setVersion(this.args[0], this.args[1]);
});
grunt.registerMultiTask('shell', 'run shell commands', function() {
var self = this;
var sh = require('shelljs');
self.data.forEach(function(cmd) {
cmd = cmd.replace('%version%', grunt.file.readJSON('package.json').version);
grunt.log.ok(cmd);
var result = sh.exec(cmd,{silent:true});
if (result.code !== 0) {
grunt.fatal(result.output);
}
});
});
return grunt;
};