var gulp = require('gulp'); var pkg = require('./package.json'); var semver = require('semver'); var through = require('through'); var argv = require('minimist')(process.argv.slice(2)); var _ = require('lodash'); var buildConfig = require('./config/build.config.js'); var changelog = require('conventional-changelog'); var connect = require('connect'); var dgeni = require('dgeni'); var es = require('event-stream'); var htmlparser = require('htmlparser2'); var irc = require('ircb'); var lunr = require('lunr'); var marked = require('marked'); var mkdirp = require('mkdirp'); var twitter = require('node-twitter-api'); var yaml = require('js-yaml'); var http = require('http'); var cp = require('child_process'); var fs = require('fs'); var concat = require('gulp-concat'); var footer = require('gulp-footer'); var gulpif = require('gulp-if'); var header = require('gulp-header'); var jshint = require('gulp-jshint'); var minifyCss = require('gulp-minify-css'); var rename = require('gulp-rename'); var sass = require('gulp-sass'); var stripDebug = require('gulp-strip-debug'); var template = require('gulp-template'); var uglify = require('gulp-uglify'); var gutil = require('gulp-util'); var banner = _.template(buildConfig.banner, { pkg: pkg }); var IS_RELEASE_BUILD = !!argv.release; if (IS_RELEASE_BUILD) { gutil.log( gutil.colors.red('--release:'), 'Building release version (minified, debugs stripped)...' ); } gulp.task('default', ['build']); gulp.task('build', ['bundle', 'sass']); gulp.task('docs', function(done) { var docVersion = argv['doc-version']; if (docVersion != 'nightly' && !semver.valid(docVersion)) { console.log('Usage: gulp docs --doc-version=(nightly|versionName)'); return process.exit(1); } process.env.DOC_VERSION = docVersion; return dgeni(__dirname + '/docs/docs.config.js').generateDocs().then(function() { gutil.log('Docs for', gutil.colors.cyan(docVersion), 'generated!'); }); }); var IS_WATCH = false; gulp.task('watch', ['build'], function() { IS_WATCH = true; gulp.watch('js/**/*.js', ['bundle']); gulp.watch('scss/**/*.scss', ['sass']); }); gulp.task('changelog', function(done) { var file = argv.standalone ? '' : __dirname + '/CHANGELOG.md'; var subtitle = argv.subtitle || '"' + pkg.codename + '"'; var toHtml = !!argv.html; var dest = argv.dest || 'CHANGELOG.md'; var from = argv.from; changelog({ repository: 'https://github.com/driftyco/ionic', version: pkg.version, subtitle: subtitle, file: file, from: from }, function(err, data) { if (err) return done(err); if (toHtml) { data = marked(data, { gfm: true }); } fs.writeFileSync(dest, data); done(); }); }); gulp.task('bundle', [ 'scripts', 'scripts-ng', 'vendor', 'version', ], function() { IS_RELEASE_BUILD && gulp.src(buildConfig.ionicBundleFiles.map(function(src) { return src.replace(/.js$/, '.min.js'); })) .pipe(header(buildConfig.bundleBanner)) .pipe(concat('ionic.bundle.min.js')) .pipe(gulp.dest(buildConfig.distJs)); return gulp.src(buildConfig.ionicBundleFiles) .pipe(header(buildConfig.bundleBanner)) .pipe(concat('ionic.bundle.js')) .pipe(gulp.dest(buildConfig.distJs)); }); gulp.task('jshint', function() { return gulp.src(['js/**/*.js', 'test/**/*.js']) .pipe(jshint('.jshintrc')) .pipe(jshint.reporter('jshint-stylish')); }); gulp.task('ddescribe-iit', function() { return gulp.src(['test/**/*.js', 'js/**/*.js']) .pipe(notContains([ 'ddescribe', 'iit', 'xit', 'xdescribe' ])); }); gulp.task('vendor', function() { return gulp.src(buildConfig.vendorFiles, { cwd: 'config/lib/', base: 'config/lib/' }) .pipe(gulp.dest(buildConfig.dist)); }); gulp.task('scripts', function() { return gulp.src(buildConfig.ionicFiles) .pipe(gulpif(IS_RELEASE_BUILD, stripDebug())) .pipe(template({ pkg: pkg })) .pipe(concat('ionic.js')) .pipe(header(buildConfig.closureStart)) .pipe(footer(buildConfig.closureEnd)) .pipe(header(banner)) .pipe(gulp.dest(buildConfig.distJs)) .pipe(gulpif(IS_RELEASE_BUILD, uglify())) .pipe(rename({ extname: '.min.js' })) .pipe(header(banner)) .pipe(gulp.dest(buildConfig.distJs)); }); gulp.task('scripts-ng', function() { return gulp.src(buildConfig.angularIonicFiles) .pipe(gulpif(IS_RELEASE_BUILD, stripDebug())) .pipe(concat('ionic-angular.js')) .pipe(header(buildConfig.closureStart)) .pipe(footer(buildConfig.closureEnd)) .pipe(header(banner)) .pipe(gulp.dest(buildConfig.distJs)) .pipe(gulpif(IS_RELEASE_BUILD, uglify())) .pipe(rename({ extname: '.min.js' })) .pipe(header(banner)) .pipe(gulp.dest(buildConfig.distJs)); }); gulp.task('sass', function(done) { gulp.src('scss/ionic.scss') .pipe(header(banner)) .pipe(sass({ onError: function(err) { //If we're watching, don't exit on error if (IS_WATCH) { console.log(gutil.colors.red(err)); } else { done(err); } } })) .pipe(concat('ionic.css')) .pipe(gulp.dest(buildConfig.distCss)) // .pipe(gulpif(IS_RELEASE_BUILD, minifyCss())) .pipe(rename({ extname: '.min.css' })) .pipe(gulp.dest(buildConfig.distCss)) .on('end', done); }); gulp.task('version', function() { var d = new Date(); var date = d.toISOString().substring(0,10); var time = pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()); return gulp.src('config/version.template.json') .pipe(template({ pkg: pkg, date: date, time: time })) .pipe(rename('version.json')) .pipe(gulp.dest('dist')); }); gulp.task('release-tweet', function(done) { var oauth = { consumerKey: process.env.TWITTER_CONSUMER_KEY, consumerSecret: process.env.TWITTER_CONSUMER_SECRET, accessToken: process.env.TWITTER_ACCESS_TOKEN, accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET }; var client = new twitter(oauth); client.statuses( 'update', { status: buildConfig.releaseMessage() }, oauth.accessToken, oauth.accessTokenSecret, done ); }); gulp.task('release-irc', function(done) { var client = irc({ host: 'irc.freenode.net', secure: true, nick: 'ionitron', username: 'ionitron', realName: 'ionitron', channels: ['#ionic'] }, function() { client.say('#ionic', buildConfig.releaseMessage(), function() { client.quit('', done); }); }); }); gulp.task('docs-index', function() { var idx = lunr(function() { this.field('path'); this.field('title', {boost: 10}); this.field('body'); this.ref('id'); }); var ref = {}; var refId = 0; function addToIndex(path, title, layout, body) { // Add the data to the indexer and ref object idx.add({'path': path, 'body': body, 'title': title, id: refId}); ref[refId] = {'p': path, 't': title, 'l': layout}; refId++; } return gulp.src([ 'tmp/ionic-site/docs/{components,guide,api,overview}/**/*.{md,html,markdown}', 'tmp/ionic-site/docs/index.html', 'tmp/ionic-site/getting-started/index.html', 'tmp/ionic-site/tutorials/**/*.{md,html,markdown}', 'tmp/ionic-site/_posts/**/*.{md,html,markdown}' ]) .pipe(es.map(function(file, callback) { //docs for gulp file objects: https://github.com/wearefractal/vinyl var contents = file.contents.toString(); //was buffer // Grab relative path from ionic-site root var relpath = file.path.replace(/^.*?tmp\/ionic-site\//, ''); // Read out the yaml portion of the Jekyll file var yamlStartIndex = contents.indexOf('---'); if (yamlStartIndex === -1) { return callback(); } // read Jekyll's page yaml variables at the top of the file var yamlEndIndex = contents.indexOf('---', yamlStartIndex+3); //starting from start var yamlRaw = contents.substring(yamlStartIndex+3, yamlEndIndex); var pageData = yaml.safeLoad(yamlRaw); if(!pageData.title || !pageData.layout) { return callback(); } // manually set to not be searchable, or for a blog post, manually set to be searchable if(pageData.searchable === false || (pageData.layout == 'post' && pageData.searchable !== true)) { return callback(); } // clean up some content so code variables are searchable too contents = contents.substring(yamlEndIndex+3); contents = contents.replace(//gi, ''); contents = contents.replace(/<\/code>/gi, ''); contents = contents.replace(/<\/code>/gi, ''); contents = contents.replace(/``/gi, ''); // create a clean path to the URL var path = '/' + relpath.replace('index.md', '') .replace('index.html', '') .replace('.md', '.html') .replace('.markdown', '.html'); if(pageData.layout == 'post') { path = '/blog/' + path.substring(19).replace('.html', '/'); } var parser; if(pageData.search_sections === true) { // each section within the content should be its own search result var section = { body: '', title: '' }; var isTitleOpen = false; parser = new htmlparser.Parser({ ontext: function(text){ if(isTitleOpen) { section.title += text; // get the title of this section } else { section.body += text.replace(/{%.*%}/, '', 'g'); // Ignore any Jekyll expressions } }, onopentag: function(name, attrs) { if(name == 'section' && attrs.id) { // start building new section data section = { body: '', path: path + '#' + attrs.id, title: '' }; } else if( (name == 'h1' || name == 'h2' || name == 'h3') && attrs.class == 'title') { isTitleOpen = true; // the next text will be this sections title } }, onclosetag: function(name) { if(name == 'section') { // section closed, index this section then clear it out addToIndex(section.path, section.title, pageData.layout, section.body); section = { body: '', title: '' }; } else if( (name == 'h1' || name == 'h2' || name == 'h3') && isTitleOpen) { isTitleOpen = false; } } }); parser.write(contents); parser.end(); } else { // index the entire page var body = ''; parser = new htmlparser.Parser({ ontext: function(text){ body += text.replace(/{%.*%}/, '', 'g'); // Ignore any Jekyll expressions } }); parser.write(contents); parser.end(); addToIndex(path, pageData.title, pageData.layout, body); } callback(); })).on('end', function() { // Write out as one json file mkdirp.sync('tmp/ionic-site/data'); fs.writeFileSync( 'tmp/ionic-site/data/index.json', JSON.stringify({'ref': ref, 'index': idx.toJSON()}) ); }); }); gulp.task('sauce-connect', sauceConnect); gulp.task('cloudtest', ['protractor-sauce'], function(cb) { sauceDisconnect(cb); }); gulp.task('karma', function(cb) { return karma(cb, [__dirname + '/config/karma.conf.js', '--single-run=true']); }); gulp.task('karma-watch', function(cb) { return karma(cb, [__dirname + '/config/karma.conf.js']); }); var connectServer; gulp.task('connect-server', function() { var app = connect().use(connect.static(__dirname)); connectServer = http.createServer(app).listen(8765); }); gulp.task('protractor', ['connect-server'], function(cb) { return protractor(cb, ['config/protractor.conf.js']); }); gulp.task('protractor-sauce', ['sauce-connect', 'connect-server'], function(cb) { return protractor(cb, ['config/protractor-sauce.conf.js']); }); function karma(cb, args) { if (argv.browsers) { args.push('--browsers='+argv.browsers.trim()); } if (argv.reporters) { args.push('--reporters='+argv.reporters.trim()); } cp.spawn('node', [ __dirname + '/node_modules/karma/bin/karma', 'start' ].concat(args), { stdio: 'inherit' }) .on('exit', function(code) { if (code) return cb('Karma test(s) failed. Exit code: ' + code); cb(); }); } function pad(n) { if (n<10) { return '0' + n; } return n; } function protractor(cb, args) { cp.spawn('protractor', args, { stdio: 'inherit' }) .on('exit', function(code) { connectServer && connectServer.close(); if (code) return cb('Protector test(s) failed. Exit code: ' + code); cb(); }); } var sauceInstance; function sauceConnect(cb) { require('sauce-connect-launcher')({ username: process.env.SAUCE_USER, accessKey: process.env.SAUCE_KEY, verbose: true, tunnelIdentifier: process.env.TRAVIS_BUILD_NUMBER }, function(err, instance) { if (err) return cb('Failed to launch sauce connect!'); sauceInstance = instance; cb(); }); } function sauceDisconnect(cb) { if (sauceInstance) { return sauceInstance.close(cb); } cb(); } function notContains(disallowed) { disallowed = disallowed || []; return through(function(file) { var error; var contents = file.contents.toString(); disallowed.forEach(function(str) { var idx = disallowedIndex(contents, str); if (idx !== -1) { error = error || file.path + ' contains ' + str + ' on line ' + contents.substring(0, idx, str).split('\n').length + '!'; } }); if (error) { throw new Error(error); } else { this.emit('data', file); } }); function disallowedIndex(content, disallowedString) { var notFunctionName = '[^A-Za-z0-9$_]'; var regex = new RegExp('(^|' + notFunctionName + ')(' + disallowedString + ')' + notFunctionName + '*\\(', 'gm'); var match = regex.exec(content); // Return the match accounting for the first submatch length. return match !== null ? match.index + match[1].length : -1; } }