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