From 0034283601b415dd79792ce26e4aa9ea67e36dda Mon Sep 17 00:00:00 2001 From: Tim Buckley Date: Fri, 25 Sep 2015 16:08:54 -0600 Subject: [PATCH] Add initial Angular boilerplate files. Adds boilerplate files from `angularjs-gulp-browserify-boilerplate` to start work on Angular rewrite of the frontend. Change-Id: I54392c24f397496582f9d06d561d5c14a92ccbe6 --- .gitignore | 3 + .jshintrc | 20 +++++++ app/images/angular.png | Bin 0 -> 33138 bytes app/images/browserify.png | Bin 0 -> 24190 bytes app/images/gulp.png | Bin 0 -> 19594 bytes app/index.html | 19 +++++++ app/js/constants.js | 8 +++ app/js/controllers/_index.js | 8 +++ app/js/controllers/example.js | 18 ++++++ app/js/directives/_index.js | 8 +++ app/js/directives/example.js | 21 +++++++ app/js/main.js | 34 ++++++++++++ app/js/on_config.js | 22 ++++++++ app/js/on_run.js | 22 ++++++++ app/js/services/_index.js | 8 +++ app/js/services/example.js | 28 ++++++++++ app/styles/_typography.scss | 42 ++++++++++++++ app/styles/_vars.scss | 19 +++++++ app/styles/main.scss | 9 +++ app/views/home.html | 7 +++ gulp/LICENSE | 23 ++++++++ gulp/config.js | 59 ++++++++++++++++++++ gulp/index.js | 9 +++ gulp/tasks/browserSync.js | 17 ++++++ gulp/tasks/browserify.js | 76 ++++++++++++++++++++++++++ gulp/tasks/clean.js | 11 ++++ gulp/tasks/deploy.js | 9 +++ gulp/tasks/development.js | 14 +++++ gulp/tasks/fonts.js | 15 +++++ gulp/tasks/gzip.js | 13 +++++ gulp/tasks/images.js | 18 ++++++ gulp/tasks/lint.js | 11 ++++ gulp/tasks/production.js | 14 +++++ gulp/tasks/protractor.js | 23 ++++++++ gulp/tasks/server.js | 36 ++++++++++++ gulp/tasks/styles.js | 24 ++++++++ gulp/tasks/test.js | 10 ++++ gulp/tasks/unit.js | 21 +++++++ gulp/tasks/views.js | 21 +++++++ gulp/tasks/watch.js | 15 +++++ gulp/util/bundleLogger.js | 25 +++++++++ gulp/util/handleErrors.js | 27 +++++++++ gulp/util/scriptFilter.js | 11 ++++ gulpfile.js | 16 ++++++ package.json | 46 +++++++++++++++- test/e2e/example_spec.js | 21 +++++++ test/e2e/routes_spec.js | 12 ++++ test/karma.conf.js | 51 +++++++++++++++++ test/protractor.conf.js | 32 +++++++++++ test/unit/constants_spec.js | 27 +++++++++ test/unit/controllers/example_spec.js | 30 ++++++++++ test/unit/services/example_spec.js | 23 ++++++++ 52 files changed, 1055 insertions(+), 1 deletion(-) create mode 100644 .jshintrc create mode 100644 app/images/angular.png create mode 100644 app/images/browserify.png create mode 100644 app/images/gulp.png create mode 100644 app/index.html create mode 100644 app/js/constants.js create mode 100644 app/js/controllers/_index.js create mode 100644 app/js/controllers/example.js create mode 100644 app/js/directives/_index.js create mode 100644 app/js/directives/example.js create mode 100644 app/js/main.js create mode 100644 app/js/on_config.js create mode 100644 app/js/on_run.js create mode 100644 app/js/services/_index.js create mode 100644 app/js/services/example.js create mode 100644 app/styles/_typography.scss create mode 100644 app/styles/_vars.scss create mode 100644 app/styles/main.scss create mode 100644 app/views/home.html create mode 100644 gulp/LICENSE create mode 100644 gulp/config.js create mode 100644 gulp/index.js create mode 100644 gulp/tasks/browserSync.js create mode 100644 gulp/tasks/browserify.js create mode 100644 gulp/tasks/clean.js create mode 100644 gulp/tasks/deploy.js create mode 100644 gulp/tasks/development.js create mode 100644 gulp/tasks/fonts.js create mode 100644 gulp/tasks/gzip.js create mode 100644 gulp/tasks/images.js create mode 100644 gulp/tasks/lint.js create mode 100644 gulp/tasks/production.js create mode 100644 gulp/tasks/protractor.js create mode 100644 gulp/tasks/server.js create mode 100644 gulp/tasks/styles.js create mode 100644 gulp/tasks/test.js create mode 100644 gulp/tasks/unit.js create mode 100644 gulp/tasks/views.js create mode 100644 gulp/tasks/watch.js create mode 100644 gulp/util/bundleLogger.js create mode 100644 gulp/util/handleErrors.js create mode 100644 gulp/util/scriptFilter.js create mode 100644 gulpfile.js create mode 100644 test/e2e/example_spec.js create mode 100644 test/e2e/routes_spec.js create mode 100644 test/karma.conf.js create mode 100644 test/protractor.conf.js create mode 100644 test/unit/constants_spec.js create mode 100644 test/unit/controllers/example_spec.js create mode 100644 test/unit/services/example_spec.js 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 0000000000000000000000000000000000000000..d1dfc01c7d72466270bec827960b47a3a9301642 GIT binary patch literal 33138 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 + + + + + + + + + + + + + +
+ + + + \ 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