gavinhewitt
1/22/2014 - 1:26 AM

Preliminary Gulpfile for replicating ngBoilerplate — Still a work in progress!

Preliminary Gulpfile for replicating ngBoilerplate — Still a work in progress!

{
  "author": "Phil DeJarnett",
  "name": "example",
  "version": "0.0.1",
  "homepage": "http://www.overzealous.com",
  "licenses": {
    "type": "",
    "url": ""
  },
  "bugs": "",
  "repository": {
    "type": "git",
    "url": ""
  },
  "dependencies": {},
  "devDependencies": {
    "anysort": "~0.1.1",
    "event-stream": "*",
    "gulp": "3.*",
    "gulp-autoprefixer": "*",
    "gulp-cdnizer": "*",
    "gulp-clean": "*",
    "gulp-concat": "*",
    "gulp-csso": "*",
    "gulp-debug": "*",
    "gulp-embedlr": "~0.5.2",
    "gulp-footer": "*",
    "gulp-gzip": "0.0.4",
    "gulp-header": "*",
    "gulp-if": "*",
    "gulp-inject": "*",
    "gulp-jshint": "*",
    "gulp-karma": "*",
    "gulp-less": "*",
    "gulp-livereload": "*",
    "gulp-minify-html": "*",
    "gulp-ng-html2js": "*",
    "gulp-ngmin": "*",
    "gulp-plumber": "*",
    "gulp-recess": "*",
    "gulp-rename": "*",
    "gulp-rev": "*",
    "gulp-tap": "*",
    "gulp-task-listing": "*",
    "gulp-uglify": "*",
    "gulp-util": "*",
    "gulp-watch": "*",
    "jshint-stylish": "*",
    "lazypipe": "*",
    "lodash": "~2.4.1",
    "run-sequence": "*",
    "tiny-lr": "0.0.5",
    "connect": "~2.12.0",
    "open": "0.0.4",
    "gulp-bump": "~0.1.4"
  }
}
//<editor-fold desc="Node Requires, gulp, etc">
var gulp = require('gulp'),
	autoprefixer = require('gulp-autoprefixer'),
	bump = require('gulp-bump'),
	cdnizer = require('gulp-cdnizer'),
	clean = require('gulp-clean'),
	concat = require('gulp-concat'),
	csso = require('gulp-csso'),
	debug = require('gulp-debug'),
	footer = require('gulp-footer'),
	gutil = require('gulp-util'),
	gzip = require('gulp-gzip'),
	header = require('gulp-header'),
	help = require('gulp-task-listing'),
	_if = require('gulp-if'),
	inject = require('gulp-inject'),
	jshint = require('gulp-jshint'),
	karma = require('gulp-karma'),
	less = require('gulp-less'),
	livereload = require('gulp-livereload'),
	livereloadEmbed = require('gulp-embedlr'),
	minifyHtml = require('gulp-minify-html'),
	ngHtml2js = require('gulp-ng-html2js'),
	ngmin = require('gulp-ngmin'),
	plumber = require('gulp-plumber'),
	recess = require('gulp-recess'),
	rename = require('gulp-rename'),
	rev = require('gulp-rev'),
	tap = require('gulp-tap'),
	uglify = require('gulp-uglify'),
	watch = require('gulp-watch'),
	
	_ = require('lodash'),
	anysort = require('anysort'),
	connect = require('connect'),
	es = require('event-stream'),
	fs = require('fs'),
	http = require('http'),
	lazypipe = require('lazypipe'),
	open = require('open'),
	path = require('path'),
	runSequence = require('run-sequence'),
	server = require('tiny-lr')();
//</editor-fold>

/*
 * TODO:
 *  - Banner for compressed JS/CSS
 *  - Changelog?
 */

// Load user config and package data
var cfg = require('./build.config.js');

// common variables
var concatName = cfg.pkg.name,
	embedLR = false,
	join = path.join;



//=============================================
// MAIN TASKS
//=============================================

gulp.task('default', ['watch']);

gulp.task('server-dist', ['compile'], function(cb) {
	startServer(cfg.compileDir, cb);
});

gulp.task('server', ['watch'], function(cb) {
	startServer(cfg.buildDir, cb);
});

gulp.task('build', function(cb) {
	runSequence(['build-assets', 'build-scripts', 'build-styles'], ['build-html', 'test'], cb);
});
gulp.task('watch', ['lr-server', 'build', 'test-watch'], function() {
	watch({glob: cfg.watchFiles.js, emitOnGlob: false, name: 'JS'})
					.pipe(plumber())
					.pipe(jsBuildTasks())
					.pipe(livereload(server));
	
	watch({glob: cfg.watchFiles.tpl, emitOnGlob: false, name: 'Templates'})
					.pipe(plumber())
					.pipe(tplBuildTasks())
					.pipe(livereload(server));
	
	watch({glob: cfg.watchFiles.html, emitOnGlob: false, name: 'HTML'}, function() {
		return buildHTML();
	});
	
	watch({glob: cfg.watchFiles.less, emitOnGlob: false, name: 'Styles'}, function() {
		// run this way to ensure that a failed pipe doesn't break the watcher.
		return buildStyles();
	});
	
	watch({glob: cfg.watchFiles.assets, emitOnGlob: false, name: 'Assets'})
		.pipe(gulp.dest(join(cfg.buildDir, cfg.assetsDir)));
});

gulp.task('compile', function(cb) {
	runSequence(
		'compile-clean',
		['compile-assets', 'compile-scripts', 'compile-styles'],
		'compile-html',
		cb
	);
});

gulp.task('clean', ['compile-clean', 'build-clean']);

gulp.task('help', help);




//=============================================
// UTILITIES
//=============================================

function readFile(filename) {
	return fs.existsSync(filename) ? fs.readFileSync(filename, {encoding: 'utf8'}) : '';
}

function insertRevGlob(f) {
	// allows for rev'd filenames (which end with /-[0-9a-f]{8}.ext/
	return f.replace(/(\..*)$/, '?(-????????)$1');
}

function startServer(root, cb) {
	var devApp, devServer, devAddress, devHost, url, log=gutil.log, colors=gutil.colors;
	
	devApp = connect();
	if(cfg.server.log) {
		devApp.use(connect.logger(cfg.server.log===true ? 'dev' : cfg.server.log));
	}
	devApp.use(connect.static(root));
	
	devServer = http.createServer(devApp).listen(cfg.server.port, cfg.server.host||undefined);
	
	devServer.on('error', function(error) {
		log(colors.underline(colors.red('ERROR'))+' Unable to start server!');
		cb(error);
	});
	
	devServer.on('listening', function() {
		devAddress = devServer.address();
		devHost = devAddress.address === '0.0.0.0' ? 'localhost' : devAddress.address;
		url = 'http://' + devHost + ':' + devAddress.port + join('/', cfg.indexFile);
		
		log('');
		log('Started dev server at '+colors.magenta(url));
		var openByDefault = cfg.server.openByDefault;
		if(gutil.env.open || (openByDefault && gutil.env.open !== false)) {
			log('Opening dev server URL in browser');
			if(openByDefault) {
				log(colors.gray('(Run with --no-open to prevent automatically opening URL)'));
			}
			// Open the URL in the browser at this point.
			open(url);
		} else if(!openByDefault) {
			log(colors.gray('(Run with --open to automatically open URL on startup)'));
		}
		log('');
		cb();
	});
}

//=============================================
// SUB TASKS
//=============================================


//---------------------------------------------
// HTML
//---------------------------------------------

var buildHTML = function() {
	var htmlFile = readFile(join(cfg.buildDir, cfg.indexFile));
	return gulp.src([join(cfg.buildDir, '/**/*.*'), '!' + join(cfg.buildDir, cfg.indexFile)], {read: false})
					.pipe(plumber())
					.pipe(inject(cfg.appFiles.html, {
							addRootSlash: false,
							sort: fileSorter, // see below
							ignorePath: join('/',cfg.buildDir,'/')
						}))
					.pipe(_if(embedLR, livereloadEmbed({port: cfg.server.lrPort})))
					.pipe(gulp.dest(cfg.buildDir))
					.pipe(tap(function(file) {
						var newHtmlFile = file.contents.toString();
						if(newHtmlFile !== htmlFile) {
							htmlFile = newHtmlFile;
							gulp.src(file.path).pipe(livereload(server));
						}
					}));
};

gulp.task('build-html', function() {
	// NOTE: this task does NOT depend on buildScripts and buildStyles,
	// therefore, it may incorrectly create the HTML file if called
	// directly.
	return buildHTML();
});

gulp.task('compile-html', function() {
	// NOTE: this task does NOT depend on compileScripts and compileStyles,
	// therefore, it may incorrectly create the HTML file if called
	// directly.
	return gulp.src([join(cfg.compileDir, '/**/*.*'), '!' + join(cfg.compileDir, cfg.indexFile)], {read: false})
					.pipe(inject(cfg.appFiles.html, {
							addRootSlash: false,
							sort: fileSorter, // see below
							ignorePath: join('/', cfg.compileDir, '/')
						}))
					.pipe(cdnizer(cfg.cdn))
					.pipe(minifyHtml({empty:true,spare:true,quotes:true}))
					.pipe(gulp.dest(cfg.compileDir))
					.pipe(gzip())
					.pipe(gulp.dest(cfg.compileDir))
});

// used by build-html to ensure correct file order during builds
var fileSorter = (function(){
	var as = anysort(_.flatten([
		// JS files are sorted by original vendor order, common, app, then everything else
		cfg.vendorFiles.js.map(function(f){ return join(cfg.jsDir, insertRevGlob(f)); }),
		cfg.vendorFiles.jsConcat.map(function(f){ return join(cfg.jsDir, insertRevGlob(f)); }),
		join(cfg.jsDir, 'common/**/*.js'),	   
		join(cfg.jsDir, 'app/**/!(app)*.js'), // exclude app.js so it comes last
		join(cfg.jsDir, '**/*.js'),
		// CSS order should be maintained via Less includes
		join(cfg.cssDir, '**/*.css')
	]));
	return function(a,b){ return as(a.filepath, b.filepath) || a.filepath < b.filepath };
})();



//---------------------------------------------
// JavaScript
//---------------------------------------------

var jsFiles = function() { return gulp.src(cfg.appFiles.js); },
	jsBaseTasks = lazypipe()
					.pipe(plumber)  // jshint won't render parse errors without plumber 
					.pipe(function() {
						return jshint(_.clone(cfg.taskOptions.jshint));
					})
					.pipe(jshint.reporter, 'jshint-stylish'),
	jsBuildTasks = jsBaseTasks
					.pipe(gulp.dest, join(cfg.buildDir, cfg.jsDir)),
	tplFiles = function() { return gulp.src(cfg.appFiles.tpl); },
	tplBuildTasks = lazypipe()
					.pipe(ngHtml2js, {moduleName: 'templates'})
	                .pipe(gulp.dest, join(cfg.buildDir, cfg.jsDir, cfg.templatesDir));

//noinspection FunctionWithInconsistentReturnsJS
gulp.task('build-scripts-vendor', function() {
	var vendorFiles = [].concat(cfg.vendorFiles.js||[], cfg.vendorFiles.jsConcat||[]);
	if(vendorFiles.length) {
		return gulp.src(vendorFiles, {base: cfg.vendorDir})
					.pipe(gulp.dest(join(cfg.buildDir, cfg.jsDir, cfg.vendorDir)))
	}
});
gulp.task('build-scripts-app', function() {
	return jsFiles().pipe(jsBuildTasks());
});
gulp.task('build-scripts-templates', function() {
	return tplFiles().pipe(tplBuildTasks());
});
gulp.task('build-scripts', ['build-scripts-vendor', 'build-scripts-app', 'build-scripts-templates']);


gulp.task('compile-scripts', function() {
	var appFiles, templates, files, concatFiles, vendorFiles;
	appFiles = jsFiles()
					.pipe(jsBaseTasks())
					.pipe(concat('appFiles.js')) // not used
					.pipe(ngmin())
					.pipe(header(cfg.moduleWrapper.header))
					.pipe(footer(cfg.moduleWrapper.footer));

	templates = tplFiles()
					.pipe(minifyHtml({empty: true, spare: true, quotes: true}))
					.pipe(ngHtml2js({moduleName: 'templates'}))
					.pipe(concat('templates.min.js')); // not used
	
	files = [appFiles, templates];
	if(cfg.vendorFiles.jsConcat.length) {
		files.unshift(gulp.src(cfg.vendorFiles.jsConcat));
	}
	
	concatFiles = es.concat.apply(es, files)
					.pipe(concat(concatName + '.js'))
					.pipe(uglify(cfg.taskOptions.uglify))
					.pipe(rev())
					.pipe(gulp.dest(join(cfg.compileDir, cfg.jsDir)))
					.pipe(gzip())
					.pipe(gulp.dest(join(cfg.compileDir, cfg.jsDir)));
	
	if(cfg.vendorFiles.js.length) {
		vendorFiles = gulp.src(cfg.vendorFiles.js, {base: cfg.vendorDir})
					.pipe(uglify(cfg.taskOptions.uglify))
					.pipe(rev())
					.pipe(gulp.dest(join(cfg.compileDir, cfg.jsDir, cfg.vendorDir)))
					.pipe(gzip())
					.pipe(gulp.dest(join(cfg.compileDir, cfg.jsDir, cfg.vendorDir)));
		
		return es.concat(vendorFiles, concatFiles);
	} else {
		return concatFiles;
	}
});



//---------------------------------------------
// Less / CSS Styles
//---------------------------------------------

var styleFiles = function() { return gulp.src(cfg.appFiles.less); },
	styleBaseTasks = lazypipe()
					.pipe(recess, cfg.taskOptions.recess)
					.pipe(less, cfg.taskOptions.less)
					.pipe(autoprefixer),
	buildStyles = function() {
		return styleFiles()
					.pipe(
						styleBaseTasks()
						// need to manually catch errors on recess/less :-(
						.on('error', function() {
							gutil.log(gutil.colors.red('Error')+' processing Less files.');
						})
					)
					.pipe(gulp.dest(join(cfg.buildDir, cfg.cssDir)))
					.pipe(livereload(server))
	};

gulp.task('build-styles', function() {
	return buildStyles();
});
gulp.task('compile-styles', function() {
	return styleFiles()
					.pipe(styleBaseTasks())
					.pipe(rename(concatName + '.css'))
					.pipe(csso(cfg.taskOptions.csso))
					.pipe(rev())
					.pipe(gulp.dest(join(cfg.compileDir, cfg.cssDir)))
					.pipe(gzip())
					.pipe(gulp.dest(join(cfg.compileDir, cfg.cssDir)))
});



//---------------------------------------------
// Unit Testing
//---------------------------------------------

var testFiles = function() {
	return gulp.src(_.flatten([cfg.vendorFiles.js, cfg.vendorFiles.jsConcat, cfg.testFiles.js, cfg.appFiles.jsunit]));
};

gulp.task('test', ['build-scripts'], function() {
	return testFiles()
					.pipe(karma({
						configFile: cfg.testFiles.config
					}))
});
gulp.task('test-watch', ['build-scripts', 'test'], function() {
	// NOT returned on purpose!
	testFiles()
					.pipe(karma({
						configFile: cfg.testFiles.config,
						action: 'watch'
					}))
});



//---------------------------------------------
// Assets
//---------------------------------------------

// If you want to automate image compression, or font creation,
// this is the place to do it!
//noinspection FunctionWithInconsistentReturnsJS
gulp.task('build-assets-vendor', function() {
	if(cfg.vendorFiles.assets.length) {
		return gulp.src(cfg.vendorFiles.assets, {base: cfg.vendorDir})
					.pipe(gulp.dest(join(cfg.buildDir, cfg.assetsDir, cfg.vendorDir)))
	}
});
gulp.task('build-assets', ['build-assets-vendor'], function() {
	return gulp.src(cfg.appFiles.assets)
					.pipe(gulp.dest(join(cfg.buildDir, cfg.assetsDir)))
});

//noinspection FunctionWithInconsistentReturnsJS
gulp.task('compile-assets-vendor', function() {
	if(cfg.vendorFiles.assets.length) {
		return gulp.src(cfg.vendorFiles.assets, {base: cfg.vendorDir})
					.pipe(gulp.dest(join(cfg.compileDir, cfg.assetsDir, cfg.vendorDir)))
	}
});
gulp.task('compile-assets', ['compile-assets-vendor'], function() {
	return gulp.src(cfg.appFiles.assets)
					.pipe(gulp.dest(join(cfg.compileDir, cfg.assetsDir)))
});



//---------------------------------------------
// Miscellaneous Tasks
//---------------------------------------------

gulp.task('lr-server', function() {
	embedLR = true;
	server.listen(cfg.server.lrPort, function(err) {
		if(err) {
			console.log(err);
		}
		gutil.log('Started LiveReload server');
	});
});

gulp.task('build-clean', function() {
	return gulp.src(cfg.buildDir, {read: false}).pipe(clean());
});

gulp.task('compile-clean', function() {
	return gulp.src(cfg.compileDir, {read: false}).pipe(clean());
});

gulp.task('bump', function() {
	var version = gutil.env['set-version'],
		type = gutil.env.type,
		msg = version ? ('Setting version to '+gutil.colors.yellow(version)) : ('Bumping '+gutil.colors.yellow(type||'patch') + ' version');
	
	gutil.log('');
	gutil.log(msg);
	gutil.log(gutil.colors.gray('(You can set a specific version using --set-version <version>,'));
	gutil.log(gutil.colors.gray('  or a specific type using --type <type>)'));
	gutil.log('');
	
	return gulp.src(['./package.json', cfg.bowerJSON])
					.pipe(bump({type:type, version:version}))
					.pipe(gulp.dest('./'));
});
/**
 * This file/module contains all configuration for the build process.
 */

/**
 * Load requires and directory resources
 */
var join = require('path').join,
	bowerrc = JSON.parse(require('fs').readFileSync('./.bowerrc', {encoding: 'utf8'})),
	bowerJSON = bowerrc.json.replace(/^\.?\/?/, './'),
	bower = require(bowerJSON),
	pkg = require('./package.json'),
	
	/**
	 * The `buildDir` folder is where our projects are compiled during
	 * development and the `compileDir` folder is where our app resides once it's
	 * completely built.
	 */	
	buildDir = 'build',
	compileDir = 'compile',
	vendorDir = bowerrc.directory,
	templatesDir = 'templates',
	indexFile = 'index.html',
	jsDir = 'js',
	cssDir = 'css',
	assetsDir = 'assets';

module.exports = {
	buildDir: buildDir,
	compileDir: compileDir,
	
	// Relative paths to core files and folders for input and output
	indexFile: indexFile,
	jsDir: jsDir,
	cssDir: cssDir,
	assetsDir: assetsDir,
	vendorDir: vendorDir,
	templatesDir: templatesDir,
	
	// allows settings reuse from package.json and bower.json
	bowerJSON: bowerJSON,
	bower: bower,
	pkg: pkg,

	/**
	 * This code is wrapped around the application code.  It is used to "protect"
	 * the application code from the global scope.
	 */
	moduleWrapper: {
		header: '\n(function ( window, angular, undefined ) {\n\n',
		footer: '\n})( window, window.angular );'
	},
	
	/**
	 * Settings for the server task
	 * When run, this task will start a connect server on
	 * your build directory, great for livereload
	 */
	server: {
		port: 8081, // 0 = random port
		host: null, // null/falsy means listen to all, but will auto open localhost
		
		// Enable disable default auto open
		// false: run with --open to open
		// true: run with --no-open to not open, recommended if port is 0
		openByDefault: false,
		
		// set to false to prevent request logging
		// set to any non-`true` value to configure the logger
		log: false,
		
		// Live Reload server port
		lrPort: 35729
	},

	/**
	 * Options passed into the various tasks.
	 * These are usually passed directly into the individual gulp plugins
	 */
	taskOptions: {
		csso: false, // set to true to prevent structural modifications
		jshint: {
			eqeqeq: true,
			camelcase: true,
			freeze: true,
			immed: true,
			latedef: true,
			newcap: true,
			undef: true,
			unused: true,
			browser: true,
			globals: {
				angular: false,
				console: false
			}
		},
		less: {},
		recess: {
			strictPropertyOrder: false,
			noOverqualifying: false,
			noUniversalSelectors: false
		},
		uglify: {}
	},
	
	/**
	 * This is a collection of file patterns that refer to our app code (the
	 * stuff in `src/`). These file paths are used in the configuration of
	 * build tasks.
	 * 
	 * js - All project javascript, less tests
	 * jsunit - All the JS needed to run tests (in this setup, it uses the build results)
	 * tpl - contains our various templates
	 * html - just our main HTML file
	 * less - our main stylesheet
	 * assets - the rest of the files that are copied directly to the build
	 */
	appFiles: {
		js: [ 'src/**/!(app)*.js', 'src/**/*.js', '!src/**/*.spec.js', '!src/assets/**/*.js' ],
		jsunit: [ join(buildDir, '/**/*.js'), 'src/**/*.spec.js', '!'+join(buildDir,'/assets/**/*.js'), '!'+join(buildDir, vendorDir, '**/*.js') ],

		tpl: [ 'src/app/**/*.tpl.html', 'src/common/**/*.tpl.html' ],

		html: join('src', indexFile),
		less: 'src/less/main.less',
		assets: join('src', assetsDir, '**/*.*')
	},
	
	/**
	 * Similar to above, except this is the pattern of files to watch
	 * for live build and reloading.
	 */
	watchFiles: {
		js: [ 'src/**/*.js', '!src/**/*.spec.js', '!src/assets/**/*.js' ],
		//jsunit: [ 'src/**/*.spec.js' ], // watch is handled by the karma plugin!

		tpl: [ 'src/app/**/*.tpl.html', 'src/common/**/*.tpl.html' ],
		
		html: [ join(buildDir, '**/*'), '!'+join(buildDir,indexFile), join('src',indexFile) ],
		less: [ 'src/**/*.less' ],
		assets: join('src',assetsDir,'**/*.*')
	},

	/**
	 * This is a collection of files used during testing only.
	 */
	testFiles: {
		config: 'karma/karma.conf.js',
		js: [
			'vendor/angular-mocks/angular-mocks.js'
		]
	},

	/**
	 * This contains files that are provided via bower.
	 * Vendor files under `js` are copied and minified, but not concatenated into the application
	 * js file.
	 * Vendor files under `jsConcat` are included in the application js file.
	 * Vendor files under `assets` are simply copied into the assets directory.
	 */
	vendorFiles: {
		js: [
			'vendor/angular/angular.js',
			'vendor/firebase/firebase.js',
			'vendor/angularfire/angularfire.js'
		],
		jsConcat: [
			'vendor/angular-bootstrap/ui-bootstrap-tpls.min.js',
			'vendor/angular-ui-router/release/angular-ui-router.js',
			'vendor/angular-ui-utils/modules/route/route.js'
		],
		assets: [
		]
	},

	/**
	 * This contains details about files stored on a CDN, using gulp-cdnizer.
	 * file: glob or filename to match for replacement
	 * package: used to look up the version info of a bower package
	 * test: if provided, this will be used to fallback to the local file if the CDN fails to load
	 * cdn: template for the CDN filename
	 */
	cdn: [
		{
			file: 'js/vendor/angular/angular.js',
			package: 'angular',
			test: 'window.angular',
			cdn: '//ajax.googleapis.com/ajax/libs/angularjs/${ major }.${ minor }.${ patch }/angular.min.js'
		}
	]
};