const marked = require('marked'); const fs = require('fs'); const _ = require('lodash'); module.exports = function(grunt) { require('load-grunt-tasks')(grunt); // Project configuration. grunt.util.linefeed = '\n'; grunt.initConfig({ ngversion: '1.6.1', bsversion: '4.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 %>', ' */' ].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', '!src/**/index.js'], 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/' } ] }, docs: { files: [ { expand: true, src: ['**/*', '*/*'], cwd: 'dist/', dest: 'docs/' } ] } }, 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: '.', rename: function(moduleName) { return `uib/${moduleName}`; } }, files: [ { expand: true, src: ['template/**/*.html'], ext: '.html.js' } ] } }, eslint: { files: ['Gruntfile.js', 'src/**/*.js'] }, 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 evaluate it at run-time, because <%= pkg.version %> //is only evaluated once 'release-prepare': [ 'git add docs' ], 'release-complete': [ 'git commit -a -m "chore(release): v%version%"', 'git tag v%version%', 'git push --follow-tags' ], 'publish': [ 'rm -rf dist/*', 'cp ./docs/ui-bootstrap-%version%.js ./dist/ui-bootstrap.js', 'cp ./docs/ui-bootstrap-%version%-csp.css ./dist/ui-bootstrap-csp.css', 'cp ./docs/ui-bootstrap-tpls-%version%.js ./dist/ui-bootstrap-tpls.js', 'npm publish' ] }, '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', [/*'ddescribe-iit',*/ 'eslint', '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']); // Build docs grunt.registerTask('docs', ['before-test', 'after-test', 'copy:docs']); //Common ui.bootstrap module containing all modules for src and templates //findModule: Adds a given module to config const 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}"`; } function enquoteUibDir(str) { return enquote(`uib/${str}`); } const module = { name: name, moduleName: enquote(`ui.bootstrap.${name}`), displayName: ucwords(breakup(name, ' ')), srcFiles: grunt.file.expand([`src/${name}/*.js`, `!src/${name}/index.js`, `!src/${name}/index-nocss.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(enquoteUibDir), dependencies: dependenciesForModule(name), docs: { md: grunt.file.expand(`src/${name}/docs/*.md`) .map(grunt.file.read).map((str) => marked(str)).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') } }; const styles = { css: [], js: [] }; module.cssFiles.forEach(processCSS.bind(null, module.name, 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) { let deps = []; grunt.file.expand([`src/${name}/*.js`, `!src/${name}/index.js`, `!src/${name}/index-nocss.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 const moduleDeclIndex = contents.indexOf('angular.module('); const depArrayStart = contents.indexOf('[', moduleDeclIndex); const depArrayEnd = contents.indexOf(']', depArrayStart); const dependencies = contents.substring(depArrayStart + 1, depArrayEnd); dependencies.split(',').forEach(function(dep) { if(dep.indexOf('ui.bootstrap.') > -1) { const 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() { const dir = this.args[0]; if(dir) { grunt.config('dist', dir); } }); grunt.registerTask('build', 'Create bootstrap build files', function() { const _ = 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((dir) => { findModule(dir.split('/')[1]); }); } const modules = grunt.config('modules'); grunt.config('srcModules', _.pluck(modules, 'moduleName')); grunt.config('tplModules', _.pluck(modules, 'tplModules').filter((tpls) => tpls.length > 0)); grunt.config('demoModules', modules .filter((module) => module.docs.md && module.docs.js && module.docs.html) .sort((a, b) => { if(a.name < b.name) { return -1; } if(a.name > b.name) { return 1; } return 0; }) ); const cssStrings = _.flatten(_.compact(_.pluck(modules, 'css'))); const 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'); } const moduleFileMapping = _.clone(modules, true); moduleFileMapping.forEach((module) => delete module.docs); grunt.config('moduleFileMapping', moduleFileMapping); const srcFiles = _.pluck(modules, 'srcFiles'); const 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 { const isToRunJenkinsTask = !!this.args.length; if(grunt.option('coverage')) { const 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() { const _ = grunt.util._; const moduleMappingJs = 'dist/assets/module-mapping.json'; const moduleMappings = grunt.config('moduleFileMapping'); const moduleMappingsMap = _.object(_.pluck(moduleMappings, 'name'), moduleMappings); const jsContent = JSON.stringify(moduleMappingsMap); grunt.file.write(moduleMappingJs, jsContent); grunt.log.writeln('File ' + moduleMappingJs.cyan + ' created.'); }); grunt.registerTask('makeRawFilesJs', function() { const _ = grunt.util._; const jsFilename = 'dist/assets/raw-files.json'; const 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() { const done = this.async(); const exec = require('child_process').exec; grunt.log.writeln(`Mapping file skipped till someone wants to fix.`); return; // const versionsMappingFile = 'dist/versions-mapping.json'; // exec('git tag --sort -version:refname', function(error, stdout, stderr) { // // Let's remove the oldest 56 versions. // const versions = stdout.split('\n').slice(0, -56); // let jsContent = versions.map(function(version) { // version = version.replace(/^v/, ''); // return { // version: version, // url: `/ui-bootstrap4/versioned-docs/${version}/index.html` // }; // }); // jsContent = _.sortBy(jsContent, 'version').reverse(); // jsContent.unshift({ // version: 'Current', // url: '/ui-bootstrap4/index.html' // }); // 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(moduleName, state, minify, file) { let 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.module('ui.bootstrap.${moduleName}').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uib${_.capitalize( moduleName)}Css && angular.element(document).find('head').prepend(''); angular.$$uib${_.capitalize( moduleName)}Css = true; });`; state.js.push(js); return state; } function setVersion(type, suffix) { const file = 'package.json'; const VERSION_REGEX = /([\'|\"]version[\'|\"][ ]*:[ ]*[\'|\"])([\d|.]*)(-\w+)*([\'|\"])/; let contents = grunt.file.read(file); let 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() { const self = this; const sh = require('shelljs'); self.data.forEach(function(cmd) { cmd = cmd.replace('%version%', grunt.file.readJSON('package.json').version); grunt.log.ok(cmd); const result = sh.exec(cmd, { silent: true }); if(result.code !== 0) { grunt.fatal(result.output); } }); }); //------------------------------------------------------------------------------------------------------------------ // New Release System //------------------------------------------------------------------------------------------------------------------ grunt.registerTask('release', function(version) { // Step 1, we change package.json grunt.config.set('pkg.version', version); grunt.file.write('./package.json', JSON.stringify(grunt.config('pkg'), null, 2)); // Step 2, we queue up additional tasks grunt.task.run([ 'conventionalChangelog', 'html2js', 'build', 'copy', 'shell:release-prepare', 'shell:release-complete', 'shell:publish' ]); }); //------------------------------------------------------------------------------------------------------------------ return grunt; };