diff --git a/.gitignore b/.gitignore
index 41e63c6..1c8fe82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
# Project-specific ignores
.idea
stackviz/static/components/*
+node_modules
+build
+app/js/templates.js
*.py[cod]
# C extensions
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..b93054e
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,20 @@
+{
+ "node": true,
+ "jasmine": true,
+ "browser": true,
+ "esnext": true,
+ "bitwise": true,
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "indent": 2,
+ "latedef": true,
+ "noarg": true,
+ "regexp": true,
+ "undef": true,
+ "unused": true,
+ "strict": true,
+ "trailing": true,
+ "smarttabs": true,
+ "newcap": false
+}
diff --git a/app/images/angular.png b/app/images/angular.png
new file mode 100644
index 0000000..d1dfc01
Binary files /dev/null and b/app/images/angular.png differ
diff --git a/app/images/browserify.png b/app/images/browserify.png
new file mode 100644
index 0000000..b77ba45
Binary files /dev/null and b/app/images/browserify.png differ
diff --git a/app/images/gulp.png b/app/images/gulp.png
new file mode 100644
index 0000000..7994592
Binary files /dev/null and b/app/images/gulp.png differ
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..4e0ed26
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/js/constants.js b/app/js/constants.js
new file mode 100644
index 0000000..837f328
--- /dev/null
+++ b/app/js/constants.js
@@ -0,0 +1,8 @@
+'use strict';
+
+var AppSettings = {
+ appTitle: 'Example Application',
+ apiUrl: '/api/v1'
+};
+
+module.exports = AppSettings;
\ No newline at end of file
diff --git a/app/js/controllers/_index.js b/app/js/controllers/_index.js
new file mode 100644
index 0000000..5dae729
--- /dev/null
+++ b/app/js/controllers/_index.js
@@ -0,0 +1,8 @@
+'use strict';
+
+var angular = require('angular');
+var bulk = require('bulk-require');
+
+module.exports = angular.module('app.controllers', []);
+
+bulk(__dirname, ['./**/!(*_index|*.spec).js']);
\ No newline at end of file
diff --git a/app/js/controllers/example.js b/app/js/controllers/example.js
new file mode 100644
index 0000000..d74059a
--- /dev/null
+++ b/app/js/controllers/example.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var controllersModule = require('./_index');
+
+/**
+ * @ngInject
+ */
+function ExampleCtrl() {
+
+ // ViewModel
+ var vm = this;
+
+ vm.title = 'AngularJS, Gulp, and Browserify!';
+ vm.number = 1234;
+
+}
+
+controllersModule.controller('ExampleCtrl', ExampleCtrl);
\ No newline at end of file
diff --git a/app/js/directives/_index.js b/app/js/directives/_index.js
new file mode 100644
index 0000000..689cbb9
--- /dev/null
+++ b/app/js/directives/_index.js
@@ -0,0 +1,8 @@
+'use strict';
+
+var angular = require('angular');
+var bulk = require('bulk-require');
+
+module.exports = angular.module('app.directives', []);
+
+bulk(__dirname, ['./**/!(*_index|*.spec).js']);
\ No newline at end of file
diff --git a/app/js/directives/example.js b/app/js/directives/example.js
new file mode 100644
index 0000000..fafd0ae
--- /dev/null
+++ b/app/js/directives/example.js
@@ -0,0 +1,21 @@
+'use strict';
+
+var directivesModule = require('./_index.js');
+
+/**
+ * @ngInject
+ */
+function exampleDirective() {
+
+ return {
+ restrict: 'EA',
+ link: function(scope, element) {
+ element.on('click', function() {
+ console.log('element clicked');
+ });
+ }
+ };
+
+}
+
+directivesModule.directive('exampleDirective', exampleDirective);
\ No newline at end of file
diff --git a/app/js/main.js b/app/js/main.js
new file mode 100644
index 0000000..1f2d02f
--- /dev/null
+++ b/app/js/main.js
@@ -0,0 +1,34 @@
+'use strict';
+
+var angular = require('angular');
+
+// angular modules
+require('angular-ui-router');
+require('./templates');
+require('./controllers/_index');
+require('./services/_index');
+require('./directives/_index');
+
+// create and bootstrap application
+angular.element(document).ready(function() {
+
+ var requires = [
+ 'ui.router',
+ 'templates',
+ 'app.controllers',
+ 'app.services',
+ 'app.directives'
+ ];
+
+ // mount on window for testing
+ window.app = angular.module('app', requires);
+
+ angular.module('app').constant('AppSettings', require('./constants'));
+
+ angular.module('app').config(require('./on_config'));
+
+ angular.module('app').run(require('./on_run'));
+
+ angular.bootstrap(document, ['app']);
+
+});
\ No newline at end of file
diff --git a/app/js/on_config.js b/app/js/on_config.js
new file mode 100644
index 0000000..b2adcb3
--- /dev/null
+++ b/app/js/on_config.js
@@ -0,0 +1,22 @@
+'use strict';
+
+/**
+ * @ngInject
+ */
+function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) {
+
+ $locationProvider.html5Mode(true);
+
+ $stateProvider
+ .state('Home', {
+ url: '/',
+ controller: 'ExampleCtrl as home',
+ templateUrl: 'home.html',
+ title: 'Home'
+ });
+
+ $urlRouterProvider.otherwise('/');
+
+}
+
+module.exports = OnConfig;
\ No newline at end of file
diff --git a/app/js/on_run.js b/app/js/on_run.js
new file mode 100644
index 0000000..e9c0122
--- /dev/null
+++ b/app/js/on_run.js
@@ -0,0 +1,22 @@
+'use strict';
+
+/**
+ * @ngInject
+ */
+function OnRun($rootScope, AppSettings) {
+
+ // change page title based on state
+ $rootScope.$on('$stateChangeSuccess', function(event, toState) {
+ $rootScope.pageTitle = '';
+
+ if ( toState.title ) {
+ $rootScope.pageTitle += toState.title;
+ $rootScope.pageTitle += ' \u2014 ';
+ }
+
+ $rootScope.pageTitle += AppSettings.appTitle;
+ });
+
+}
+
+module.exports = OnRun;
\ No newline at end of file
diff --git a/app/js/services/_index.js b/app/js/services/_index.js
new file mode 100644
index 0000000..c72fc15
--- /dev/null
+++ b/app/js/services/_index.js
@@ -0,0 +1,8 @@
+'use strict';
+
+var angular = require('angular');
+var bulk = require('bulk-require');
+
+module.exports = angular.module('app.services', []);
+
+bulk(__dirname, ['./**/!(*_index|*.spec).js']);
\ No newline at end of file
diff --git a/app/js/services/example.js b/app/js/services/example.js
new file mode 100644
index 0000000..310ce12
--- /dev/null
+++ b/app/js/services/example.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var servicesModule = require('./_index.js');
+
+/**
+ * @ngInject
+ */
+function ExampleService($q, $http) {
+
+ var service = {};
+
+ service.get = function() {
+ var deferred = $q.defer();
+
+ $http.get('apiPath').success(function(data) {
+ deferred.resolve(data);
+ }).error(function(err, status) {
+ deferred.reject(err, status);
+ });
+
+ return deferred.promise;
+ };
+
+ return service;
+
+}
+
+servicesModule.service('ExampleService', ExampleService);
\ No newline at end of file
diff --git a/app/styles/_typography.scss b/app/styles/_typography.scss
new file mode 100644
index 0000000..8cc7a16
--- /dev/null
+++ b/app/styles/_typography.scss
@@ -0,0 +1,42 @@
+p {
+ margin-bottom: 1em;
+}
+
+.heading {
+ margin-bottom: 0.618em;
+
+ &.-large {
+ font-size: $font-size--lg;
+ font-weight: bold;
+ line-height: $half-space * 3 / 2;
+ }
+
+ &.-medium {
+ font-size: $font-size--md;
+ font-weight: normal;
+ line-height: $half-space;
+ }
+
+ &.-small {
+ font-size: $font-size--sm;
+ font-weight: bold;
+ line-height: $half-space * 2 / 3;
+ }
+
+ &.-smallest {
+ font-size: $font-size--xs;
+ font-weight: bold;
+ }
+}
+
+h1 {
+ @extend .heading.-large;
+}
+
+h2 {
+ @extend .heading.-medium;
+}
+
+h3 {
+ @extend .heading.-small;
+}
\ No newline at end of file
diff --git a/app/styles/_vars.scss b/app/styles/_vars.scss
new file mode 100644
index 0000000..d0678ad
--- /dev/null
+++ b/app/styles/_vars.scss
@@ -0,0 +1,19 @@
+// colors
+$font-color--dark: #333;
+$font-color--light: #fff;
+$background--light: #eee;
+$background--dark: #222;
+$blue: #1f8de2;
+$green: #1fe27b;
+$red: #e21f3f;
+
+// spacing
+$full-space: 40px;
+$half-space: 20px;
+
+// font sizing
+$font-size--xs: 10px;
+$font-size--sm: 12px;
+$font-size--md: 16px;
+$font-size--lg: 24px;
+$font-size--xl: 32px;
\ No newline at end of file
diff --git a/app/styles/main.scss b/app/styles/main.scss
new file mode 100644
index 0000000..4a2d9f9
--- /dev/null
+++ b/app/styles/main.scss
@@ -0,0 +1,9 @@
+@import 'vars';
+@import 'typography';
+
+body {
+ font-family: Helvetica, sans-serif;
+ color: $font-color--dark;
+ background-color: $background--light;
+ padding: $half-space;
+}
\ No newline at end of file
diff --git a/app/views/home.html b/app/views/home.html
new file mode 100644
index 0000000..e28eb3f
--- /dev/null
+++ b/app/views/home.html
@@ -0,0 +1,7 @@
+{{ home.title }}
+
+Here is a fancy number served up courtesy of Angular: {{ home.number }}
+
+
+
+
\ No newline at end of file
diff --git a/gulp/LICENSE b/gulp/LICENSE
new file mode 100644
index 0000000..a609691
--- /dev/null
+++ b/gulp/LICENSE
@@ -0,0 +1,23 @@
+Imported from https://github.com/jakemmarsh/angularjs-gulp-browserify-boilerplate :
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Jake Marsh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/gulp/config.js b/gulp/config.js
new file mode 100644
index 0000000..32f0aa2
--- /dev/null
+++ b/gulp/config.js
@@ -0,0 +1,59 @@
+'use strict';
+
+module.exports = {
+
+ 'browserPort' : 3000,
+ 'UIPort' : 3001,
+ 'serverPort' : 3002,
+
+ 'styles': {
+ 'src' : 'app/styles/**/*.scss',
+ 'dest': 'build/css'
+ },
+
+ 'scripts': {
+ 'src' : 'app/js/**/*.js',
+ 'dest': 'build/js'
+ },
+
+ 'images': {
+ 'src' : 'app/images/**/*',
+ 'dest': 'build/images'
+ },
+
+ 'fonts': {
+ 'src' : ['app/fonts/**/*'],
+ 'dest': 'build/fonts'
+ },
+
+ 'views': {
+ 'watch': [
+ 'app/index.html',
+ 'app/views/**/*.html'
+ ],
+ 'src': 'app/views/**/*.html',
+ 'dest': 'app/js'
+ },
+
+ 'gzip': {
+ 'src': 'build/**/*.{html,xml,json,css,js,js.map}',
+ 'dest': 'build/',
+ 'options': {}
+ },
+
+ 'dist': {
+ 'root' : 'build'
+ },
+
+ 'browserify': {
+ 'entries' : ['./app/js/main.js'],
+ 'bundleName': 'main.js',
+ 'sourcemap' : true
+ },
+
+ 'test': {
+ 'karma': 'test/karma.conf.js',
+ 'protractor': 'test/protractor.conf.js'
+ }
+
+};
diff --git a/gulp/index.js b/gulp/index.js
new file mode 100644
index 0000000..35fe83d
--- /dev/null
+++ b/gulp/index.js
@@ -0,0 +1,9 @@
+'use strict';
+
+var fs = require('fs');
+var onlyScripts = require('./util/scriptFilter');
+var tasks = fs.readdirSync('./gulp/tasks/').filter(onlyScripts);
+
+tasks.forEach(function(task) {
+ require('./tasks/' + task);
+});
\ No newline at end of file
diff --git a/gulp/tasks/browserSync.js b/gulp/tasks/browserSync.js
new file mode 100644
index 0000000..1fb5f57
--- /dev/null
+++ b/gulp/tasks/browserSync.js
@@ -0,0 +1,17 @@
+'use strict';
+
+var config = require('../config');
+var browserSync = require('browser-sync');
+var gulp = require('gulp');
+
+gulp.task('browserSync', function() {
+
+ browserSync({
+ port: config.browserPort,
+ ui: {
+ port: config.UIPort
+ },
+ proxy: 'localhost:' + config.serverPort
+ });
+
+});
diff --git a/gulp/tasks/browserify.js b/gulp/tasks/browserify.js
new file mode 100644
index 0000000..0d670b3
--- /dev/null
+++ b/gulp/tasks/browserify.js
@@ -0,0 +1,76 @@
+'use strict';
+
+var config = require('../config');
+var gulp = require('gulp');
+var gulpif = require('gulp-if');
+var gutil = require('gulp-util');
+var source = require('vinyl-source-stream');
+var sourcemaps = require('gulp-sourcemaps');
+var buffer = require('vinyl-buffer');
+var streamify = require('gulp-streamify');
+var watchify = require('watchify');
+var browserify = require('browserify');
+var babelify = require('babelify');
+var uglify = require('gulp-uglify');
+var handleErrors = require('../util/handleErrors');
+var browserSync = require('browser-sync');
+var debowerify = require('debowerify');
+var ngAnnotate = require('browserify-ngannotate');
+
+// Based on: http://blog.avisi.nl/2014/04/25/how-to-keep-a-fast-build-with-browserify-and-reactjs/
+function buildScript(file) {
+
+ var bundler = browserify({
+ entries: config.browserify.entries,
+ debug: true,
+ cache: {},
+ packageCache: {},
+ fullPaths: true
+ }, watchify.args);
+
+ if ( !global.isProd ) {
+ bundler = watchify(bundler);
+ bundler.on('update', function() {
+ rebundle();
+ });
+ }
+
+ var transforms = [
+ babelify,
+ debowerify,
+ ngAnnotate,
+ 'brfs',
+ 'bulkify'
+ ];
+
+ transforms.forEach(function(transform) {
+ bundler.transform(transform);
+ });
+
+ function rebundle() {
+ var stream = bundler.bundle();
+ var createSourcemap = global.isProd && config.browserify.sourcemap;
+
+ gutil.log('Rebundle...');
+
+ return stream.on('error', handleErrors)
+ .pipe(source(file))
+ .pipe(gulpif(createSourcemap, buffer()))
+ .pipe(gulpif(createSourcemap, sourcemaps.init()))
+ .pipe(gulpif(global.isProd, streamify(uglify({
+ compress: { drop_console: true }
+ }))))
+ .pipe(gulpif(createSourcemap, sourcemaps.write('./')))
+ .pipe(gulp.dest(config.scripts.dest))
+ .pipe(browserSync.reload({ stream: true, once: true }));
+ }
+
+ return rebundle();
+
+}
+
+gulp.task('browserify', function() {
+
+ return buildScript('main.js');
+
+});
diff --git a/gulp/tasks/clean.js b/gulp/tasks/clean.js
new file mode 100644
index 0000000..6db4a7a
--- /dev/null
+++ b/gulp/tasks/clean.js
@@ -0,0 +1,11 @@
+'use strict';
+
+var config = require('../config');
+var gulp = require('gulp');
+var del = require('del');
+
+gulp.task('clean', function(cb) {
+
+ del([config.dist.root], cb);
+
+});
diff --git a/gulp/tasks/deploy.js b/gulp/tasks/deploy.js
new file mode 100644
index 0000000..72bf210
--- /dev/null
+++ b/gulp/tasks/deploy.js
@@ -0,0 +1,9 @@
+'use strict';
+
+var gulp = require('gulp');
+
+gulp.task('deploy', ['prod'], function() {
+
+ // Any deployment logic should go here
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/development.js b/gulp/tasks/development.js
new file mode 100644
index 0000000..cd00e83
--- /dev/null
+++ b/gulp/tasks/development.js
@@ -0,0 +1,14 @@
+'use strict';
+
+var gulp = require('gulp');
+var runSequence = require('run-sequence');
+
+gulp.task('dev', ['clean'], function(cb) {
+
+ cb = cb || function() {};
+
+ global.isProd = false;
+
+ runSequence(['styles', 'images', 'fonts', 'views', 'browserify'], 'watch', cb);
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/fonts.js b/gulp/tasks/fonts.js
new file mode 100644
index 0000000..a13c15e
--- /dev/null
+++ b/gulp/tasks/fonts.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var config = require('../config');
+var changed = require('gulp-changed');
+var gulp = require('gulp');
+var browserSync = require('browser-sync');
+
+gulp.task('fonts', function() {
+
+ return gulp.src(config.fonts.src)
+ .pipe(changed(config.fonts.dest)) // Ignore unchanged files
+ .pipe(gulp.dest(config.fonts.dest))
+ .pipe(browserSync.reload({ stream: true, once: true }));
+
+});
diff --git a/gulp/tasks/gzip.js b/gulp/tasks/gzip.js
new file mode 100644
index 0000000..a48fc66
--- /dev/null
+++ b/gulp/tasks/gzip.js
@@ -0,0 +1,13 @@
+'use strict';
+
+var gulp = require('gulp');
+var gzip = require('gulp-gzip');
+var config = require('../config');
+
+gulp.task('gzip', function() {
+
+ return gulp.src(config.gzip.src)
+ .pipe(gzip(config.gzip.options))
+ .pipe(gulp.dest(config.gzip.dest));
+
+});
diff --git a/gulp/tasks/images.js b/gulp/tasks/images.js
new file mode 100644
index 0000000..6c54c34
--- /dev/null
+++ b/gulp/tasks/images.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var config = require('../config');
+var changed = require('gulp-changed');
+var gulp = require('gulp');
+var gulpif = require('gulp-if');
+//var imagemin = require('gulp-imagemin');
+var browserSync = require('browser-sync');
+
+gulp.task('images', function() {
+
+ return gulp.src(config.images.src)
+ .pipe(changed(config.images.dest)) // Ignore unchanged files
+ //.pipe(gulpif(global.isProd, imagemin())) // Optimize
+ .pipe(gulp.dest(config.images.dest))
+ .pipe(browserSync.reload({ stream: true, once: true }));
+
+});
diff --git a/gulp/tasks/lint.js b/gulp/tasks/lint.js
new file mode 100644
index 0000000..730595b
--- /dev/null
+++ b/gulp/tasks/lint.js
@@ -0,0 +1,11 @@
+'use strict';
+
+var config = require('../config');
+var gulp = require('gulp');
+var jshint = require('gulp-jshint');
+
+gulp.task('lint', function() {
+ return gulp.src([config.scripts.src, '!app/js/templates.js'])
+ .pipe(jshint())
+ .pipe(jshint.reporter('jshint-stylish'));
+});
\ No newline at end of file
diff --git a/gulp/tasks/production.js b/gulp/tasks/production.js
new file mode 100644
index 0000000..c70d1d7
--- /dev/null
+++ b/gulp/tasks/production.js
@@ -0,0 +1,14 @@
+'use strict';
+
+var gulp = require('gulp');
+var runSequence = require('run-sequence');
+
+gulp.task('prod', ['clean'], function(cb) {
+
+ cb = cb || function() {};
+
+ global.isProd = true;
+
+ runSequence(['styles', 'images', 'fonts', 'views', 'browserify'], 'gzip', cb);
+
+});
diff --git a/gulp/tasks/protractor.js b/gulp/tasks/protractor.js
new file mode 100644
index 0000000..ffeeef7
--- /dev/null
+++ b/gulp/tasks/protractor.js
@@ -0,0 +1,23 @@
+'use strict';
+
+var gulp = require('gulp');
+var protractor = require('gulp-protractor').protractor;
+var webdriver = require('gulp-protractor').webdriver;
+var webdriverUpdate = require('gulp-protractor').webdriver_update;
+var config = require('../config');
+
+gulp.task('webdriver-update', webdriverUpdate);
+gulp.task('webdriver', webdriver);
+
+gulp.task('protractor', ['webdriver-update', 'webdriver', 'server'], function() {
+
+ return gulp.src('test/e2e/**/*.js')
+ .pipe(protractor({
+ configFile: config.test.protractor
+ }))
+ .on('error', function(err) {
+ // Make sure failed tests cause gulp to exit non-zero
+ throw err;
+ });
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/server.js b/gulp/tasks/server.js
new file mode 100644
index 0000000..08250c9
--- /dev/null
+++ b/gulp/tasks/server.js
@@ -0,0 +1,36 @@
+'use strict';
+
+var config = require('../config');
+var http = require('http');
+var express = require('express');
+var gulp = require('gulp');
+var gutil = require('gulp-util');
+var morgan = require('morgan');
+
+gulp.task('server', function() {
+
+ var server = express();
+
+ // log all requests to the console
+ server.use(morgan('dev'));
+ server.use(express.static(config.dist.root));
+
+ // Serve index.html for all routes to leave routing up to Angular
+ server.all('/*', function(req, res) {
+ res.sendFile('index.html', { root: 'build' });
+ });
+
+ // Start webserver if not already running
+ var s = http.createServer(server);
+ s.on('error', function(err){
+ if(err.code === 'EADDRINUSE'){
+ gutil.log('Development server is already started at port ' + config.serverPort);
+ }
+ else {
+ throw err;
+ }
+ });
+
+ s.listen(config.serverPort);
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/styles.js b/gulp/tasks/styles.js
new file mode 100644
index 0000000..1fe1538
--- /dev/null
+++ b/gulp/tasks/styles.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var config = require('../config');
+var gulp = require('gulp');
+var sass = require('gulp-sass');
+var gulpif = require('gulp-if');
+var handleErrors = require('../util/handleErrors');
+var browserSync = require('browser-sync');
+var autoprefixer = require('gulp-autoprefixer');
+
+gulp.task('styles', function () {
+
+ return gulp.src(config.styles.src)
+ .pipe(sass({
+ sourceComments: global.isProd ? 'none' : 'map',
+ sourceMap: 'sass',
+ outputStyle: global.isProd ? 'compressed' : 'nested'
+ }))
+ .pipe(autoprefixer("last 2 versions", "> 1%", "ie 8"))
+ .on('error', handleErrors)
+ .pipe(gulp.dest(config.styles.dest))
+ .pipe(browserSync.reload({ stream: true }));
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/test.js b/gulp/tasks/test.js
new file mode 100644
index 0000000..a385729
--- /dev/null
+++ b/gulp/tasks/test.js
@@ -0,0 +1,10 @@
+'use strict';
+
+var gulp = require('gulp');
+var runSequence = require('run-sequence');
+
+gulp.task('test', ['server'], function() {
+
+ return runSequence('unit', 'protractor');
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/unit.js b/gulp/tasks/unit.js
new file mode 100644
index 0000000..25cca6c
--- /dev/null
+++ b/gulp/tasks/unit.js
@@ -0,0 +1,21 @@
+'use strict';
+
+var gulp = require('gulp');
+var karma = require('gulp-karma');
+var config = require('../config');
+
+gulp.task('unit', ['views'], function() {
+
+ // Nonsensical source to fall back to files listed in karma.conf.js,
+ // see https://github.com/lazd/gulp-karma/issues/9
+ return gulp.src('./thisdoesntexist')
+ .pipe(karma({
+ configFile: config.test.karma,
+ action: 'run'
+ }))
+ .on('error', function(err) {
+ // Make sure failed tests cause gulp to exit non-zero
+ throw err;
+ });
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/views.js b/gulp/tasks/views.js
new file mode 100644
index 0000000..e82d391
--- /dev/null
+++ b/gulp/tasks/views.js
@@ -0,0 +1,21 @@
+'use strict';
+
+var config = require('../config');
+var gulp = require('gulp');
+var templateCache = require('gulp-angular-templatecache');
+
+// Views task
+gulp.task('views', function() {
+
+ // Put our index.html in the dist folder
+ gulp.src('app/index.html')
+ .pipe(gulp.dest(config.dist.root));
+
+ // Process any other view files from app/views
+ return gulp.src(config.views.src)
+ .pipe(templateCache({
+ standalone: true
+ }))
+ .pipe(gulp.dest(config.views.dest));
+
+});
\ No newline at end of file
diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js
new file mode 100644
index 0000000..36aa72c
--- /dev/null
+++ b/gulp/tasks/watch.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var config = require('../config');
+var gulp = require('gulp');
+
+gulp.task('watch', ['browserSync', 'server'], function() {
+
+ // Scripts are automatically watched and rebundled by Watchify inside Browserify task
+ gulp.watch(config.scripts.src, ['lint']);
+ gulp.watch(config.styles.src, ['styles']);
+ gulp.watch(config.images.src, ['images']);
+ gulp.watch(config.fonts.src, ['fonts']);
+ gulp.watch(config.views.watch, ['views']);
+
+});
\ No newline at end of file
diff --git a/gulp/util/bundleLogger.js b/gulp/util/bundleLogger.js
new file mode 100644
index 0000000..cf50dd0
--- /dev/null
+++ b/gulp/util/bundleLogger.js
@@ -0,0 +1,25 @@
+'use strict';
+
+/* bundleLogger
+ * ------------
+ * Provides gulp style logs to the bundle method in browserify.js
+ */
+
+var gutil = require('gulp-util');
+var prettyHrtime = require('pretty-hrtime');
+var startTime;
+
+module.exports = {
+
+ start: function() {
+ startTime = process.hrtime();
+ gutil.log('Running', gutil.colors.green('\'bundle\'') + '...');
+ },
+
+ end: function() {
+ var taskTime = process.hrtime(startTime);
+ var prettyTime = prettyHrtime(taskTime);
+ gutil.log('Finished', gutil.colors.green('\'bundle\''), 'in', gutil.colors.magenta(prettyTime));
+ }
+
+};
\ No newline at end of file
diff --git a/gulp/util/handleErrors.js b/gulp/util/handleErrors.js
new file mode 100644
index 0000000..3cf398a
--- /dev/null
+++ b/gulp/util/handleErrors.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var notify = require('gulp-notify');
+
+module.exports = function(error) {
+
+ if( !global.isProd ) {
+
+ var args = Array.prototype.slice.call(arguments);
+
+ // Send error to notification center with gulp-notify
+ notify.onError({
+ title: 'Compile Error',
+ message: '<%= error.message %>'
+ }).apply(this, args);
+
+ // Keep gulp from hanging on this task
+ this.emit('end');
+
+ } else {
+ // Log the error and stop the process
+ // to prevent broken code from building
+ console.log(error);
+ process.exit(1);
+ }
+
+};
\ No newline at end of file
diff --git a/gulp/util/scriptFilter.js b/gulp/util/scriptFilter.js
new file mode 100644
index 0000000..dec95d7
--- /dev/null
+++ b/gulp/util/scriptFilter.js
@@ -0,0 +1,11 @@
+'use strict';
+
+var path = require('path');
+
+// Filters out non .js files. Prevents
+// accidental inclusion of possible hidden files
+module.exports = function(name) {
+
+ return /(\.(js)$)/i.test(path.extname(name));
+
+};
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..ad3074c
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,16 @@
+'use strict';
+
+/*
+ * gulpfile.js
+ * ===========
+ * Rather than manage one giant configuration file responsible
+ * for creating multiple tasks, each task has been broken out into
+ * its own file in gulp/tasks. Any file in that folder gets automatically
+ * required by the loop in ./gulp/index.js (required below).
+ *
+ * To add a new task, simply add a new task file to gulp/tasks.
+ */
+
+global.isProd = false;
+
+require('./gulp');
diff --git a/package.json b/package.json
index df73910..fcb6829 100644
--- a/package.json
+++ b/package.json
@@ -6,18 +6,62 @@
"repository": "none",
"license": "Apache 2.0",
"devDependencies": {
+ "angular": "^1.3.15",
+ "angular-mocks": "^1.3.15",
+ "angular-ui-router": "^0.2.13",
+ "babelify": "^5.0.4",
+ "brfs": "^1.2.0",
+ "browser-sync": "^2.7.6",
+ "browserify": "^5.10.0",
+ "browserify-istanbul": "^0.2.0",
+ "browserify-ngannotate": "^0.1.0",
+ "bulk-require": "^0.2.1",
+ "bulkify": "^1.1.1",
+ "debowerify": "^1.2.0",
+ "del": "^0.1.3",
"eslint": "^0.23.0",
"eslint-config-openstack": "1.2.0",
+ "express": "^4.7.2",
+ "gulp": "^3.8.8",
+ "gulp-angular-templatecache": "^1.3.0",
+ "gulp-autoprefixer": "^2.0.0",
+ "gulp-changed": "^1.0.0",
+ "gulp-gzip": "^0.0.8",
+ "gulp-if": "^1.2.5",
+ "gulp-imagemin": "^1.1.0",
+ "gulp-jshint": "^1.8.3",
+ "gulp-karma": "0.0.4",
+ "gulp-notify": "^2.0.0",
+ "gulp-protractor": "0.0.11",
+ "gulp-rename": "^1.2.0",
+ "gulp-sass": "^1.3.3",
+ "gulp-sourcemaps": "^1.3.0",
+ "gulp-streamify": "0.0.5",
+ "gulp-uglify": "^1.0.1",
+ "gulp-util": "^3.0.1",
+ "isparta": "^3.0.3",
"jasmine-ajax": "^3.1.1",
"jasmine-core": "^2.3.4",
"jasmine-fixture": "^1.3.2",
+ "jshint-stylish": "^1.0.0",
"karma": "^0.13.4",
+ "karma-babel-preprocessor": "^4.0.1",
+ "karma-browserify": "^4.0.0",
"karma-chrome-launcher": "0.1.8",
"karma-cli": "0.0.4",
"karma-coverage": "0.3.1",
"karma-jasmine": "^0.3.6",
"karma-phantomjs-launcher": "0.2.0",
- "phantomjs": "1.9.17"
+ "morgan": "^1.6.1",
+ "phantomjs": "1.9.17",
+ "pretty-hrtime": "^1.0.0",
+ "protractor": "^2.2.0",
+ "run-sequence": "^1.1.2",
+ "tiny-lr": "^0.1.6",
+ "uglifyify": "^3.0.1",
+ "vinyl-buffer": "^1.0.0",
+ "vinyl-source-stream": "^1.1.0",
+ "watchify": "^3.3.1"
},
"scripts": {
"postinstall": "if [ ! -d .venv ]; then tox -epy27 --notest; fi",
diff --git a/test/e2e/example_spec.js b/test/e2e/example_spec.js
new file mode 100644
index 0000000..d68cb4a
--- /dev/null
+++ b/test/e2e/example_spec.js
@@ -0,0 +1,21 @@
+/*global browser, by */
+
+'use strict';
+
+describe('E2E: Example', function() {
+
+ beforeEach(function() {
+ browser.get('/');
+ browser.waitForAngular();
+ });
+
+ it('should route correctly', function() {
+ expect(browser.getLocationAbsUrl()).toMatch('/');
+ });
+
+ it('should show the number defined in the controller', function() {
+ var element = browser.findElement(by.css('.number-example'));
+ expect(element.getText()).toEqual('1234');
+ });
+
+});
\ No newline at end of file
diff --git a/test/e2e/routes_spec.js b/test/e2e/routes_spec.js
new file mode 100644
index 0000000..3cedaca
--- /dev/null
+++ b/test/e2e/routes_spec.js
@@ -0,0 +1,12 @@
+/*global browser */
+
+'use strict';
+
+describe('E2E: Routes', function() {
+
+ it('should have a working home route', function() {
+ browser.get('#/');
+ expect(browser.getLocationAbsUrl()).toMatch('/');
+ });
+
+});
\ No newline at end of file
diff --git a/test/karma.conf.js b/test/karma.conf.js
new file mode 100644
index 0000000..358c44a
--- /dev/null
+++ b/test/karma.conf.js
@@ -0,0 +1,51 @@
+'use strict';
+
+var istanbul = require('browserify-istanbul');
+var isparta = require('isparta');
+
+module.exports = function(config) {
+
+ config.set({
+
+ basePath: '../',
+ frameworks: ['jasmine', 'browserify'],
+ preprocessors: {
+ 'app/js/**/*.js': ['browserify', 'babel', 'coverage']
+ },
+ browsers: ['Chrome'],
+ reporters: ['progress', 'coverage'],
+
+ autoWatch: true,
+
+ browserify: {
+ debug: true,
+ transform: [
+ 'bulkify',
+ istanbul({
+ instrumenter: isparta,
+ ignore: ['**/node_modules/**', '**/test/**']
+ })
+ ]
+ },
+
+ proxies: {
+ '/': 'http://localhost:9876/'
+ },
+
+ urlRoot: '/__karma__/',
+
+ files: [
+ // 3rd-party resources
+ 'node_modules/angular/angular.min.js',
+ 'node_modules/angular-mocks/angular-mocks.js',
+
+ // app-specific code
+ 'app/js/main.js',
+
+ // test files
+ 'test/unit/**/*.js'
+ ]
+
+ });
+
+};
diff --git a/test/protractor.conf.js b/test/protractor.conf.js
new file mode 100644
index 0000000..406cfba
--- /dev/null
+++ b/test/protractor.conf.js
@@ -0,0 +1,32 @@
+'use strict';
+
+var gulpConfig = require('../gulp/config');
+
+exports.config = {
+
+ allScriptsTimeout: 11000,
+
+ baseUrl: 'http://localhost:' + gulpConfig.serverPort + '/',
+
+ directConnect: true,
+
+ capabilities: {
+ browserName: 'chrome',
+ version: '',
+ platform: 'ANY'
+ },
+
+ framework: 'jasmine',
+
+ jasmineNodeOpts: {
+ isVerbose: false,
+ showColors: true,
+ includeStackTrace: true,
+ defaultTimeoutInterval: 30000
+ },
+
+ specs: [
+ 'e2e/**/*.js'
+ ]
+
+};
\ No newline at end of file
diff --git a/test/unit/constants_spec.js b/test/unit/constants_spec.js
new file mode 100644
index 0000000..8fb2a5b
--- /dev/null
+++ b/test/unit/constants_spec.js
@@ -0,0 +1,27 @@
+/*global angular */
+
+'use strict';
+
+describe('Unit: Constants', function() {
+
+ var constants;
+
+ beforeEach(function() {
+ // instantiate the app module
+ angular.mock.module('app');
+
+ // mock the directive
+ angular.mock.inject(function(AppSettings) {
+ constants = AppSettings;
+ });
+ });
+
+ it('should exist', function() {
+ expect(constants).toBeDefined();
+ });
+
+ it('should have an application name', function() {
+ expect(constants.appTitle).toEqual('Example Application');
+ });
+
+});
\ No newline at end of file
diff --git a/test/unit/controllers/example_spec.js b/test/unit/controllers/example_spec.js
new file mode 100644
index 0000000..56b9f2a
--- /dev/null
+++ b/test/unit/controllers/example_spec.js
@@ -0,0 +1,30 @@
+/*global angular */
+
+'use strict';
+
+describe('Unit: ExampleCtrl', function() {
+
+ var ctrl;
+
+ beforeEach(function() {
+ // instantiate the app module
+ angular.mock.module('app');
+
+ angular.mock.inject(function($controller) {
+ ctrl = $controller('ExampleCtrl');
+ });
+ });
+
+ it('should exist', function() {
+ expect(ctrl).toBeDefined();
+ });
+
+ it('should have a number variable equal to 1234', function() {
+ expect(ctrl.number).toEqual(1234);
+ });
+
+ it('should have a title variable equal to \'AngularJS, Gulp, and Browserify!\'', function() {
+ expect(ctrl.title).toEqual('AngularJS, Gulp, and Browserify!');
+ });
+
+});
\ No newline at end of file
diff --git a/test/unit/services/example_spec.js b/test/unit/services/example_spec.js
new file mode 100644
index 0000000..9648bba
--- /dev/null
+++ b/test/unit/services/example_spec.js
@@ -0,0 +1,23 @@
+/*global angular */
+
+'use strict';
+
+describe('Unit: ExampleService', function() {
+
+ var service;
+
+ beforeEach(function() {
+ // instantiate the app module
+ angular.mock.module('app');
+
+ // mock the service
+ angular.mock.inject(function(ExampleService) {
+ service = ExampleService;
+ });
+ });
+
+ it('should exist', function() {
+ expect(service).toBeDefined();
+ });
+
+});
\ No newline at end of file