├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── LICENSE.md ├── README.md ├── app ├── images │ └── .gitkeep ├── index.html ├── js │ └── app.js └── scss │ └── main.scss ├── bower.json ├── gulpfile.js ├── package.json ├── spec └── helpers │ └── .gitkeep ├── testem.json └── vendor └── manifest.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "./vendor" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | node_modules 3 | dist 4 | .DS_Store 5 | vendor/* 6 | !vendor/manifest.js 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "newcap": true, 6 | "noarg": true, 7 | "sub": true, 8 | "boss": true, 9 | "eqnull": true, 10 | "quotmark": "single", 11 | "trailing": true, 12 | "globals": { 13 | "angular": true, 14 | "_": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2014 Jesus Rodriguez 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fox's Angular.js Gulp Workflow 2 | 3 | [![devDependency Status](https://david-dm.org/Foxandxss/fox-angular-gulp-workflow/dev-status.svg)](https://david-dm.org/Foxandxss/fox-angular-gulp-workflow#info=devDependencies) 4 | 5 | **If you would like a modern workflow using ES6, use my [webpack boilerplate](https://github.com/Foxandxss/angular-webpack-workflow)** 6 | 7 | Here is *yet another opinionated angular boilerplate* with how I work with Angular. I made it for myself but if you find that my opinions are good for you, feel free to use it and collaborate with issues or pull requests. 8 | 9 | Let's start with the `app` folder: 10 | 11 | ![App folder](http://i.imgur.com/Fppy0Ge.png) 12 | 13 | In the `app` folder you can find 3 subdirectories: 14 | 15 | * **images**: You can put there the images you need, nothing special here. 16 | * **scss**: There is a `main.scss` file there were you import all your other `.scss` files. 17 | * **js**: There is where you put your javascript code. It comes with a `app.js` file with the main `app` module created. 18 | 19 | You can also find: 20 | 21 | **index.html**: It is only the basic skeleton with the angular application loaded. It is a `lodash template` so we can have cache-busting on production. 22 | 23 | ## Structuring your Angular app in the `js` folder. 24 | 25 | We split our application per `features`, so if we have an application to manage `users`, we can decide that a page to manage those `users` is a feature and also the `settings` page is another feature. Also we need some authentication services and stuff like that. That is not a feature of our app, but something **common** to the entire app. How can we organize that? 26 | 27 | ![App structure](http://i.imgur.com/RtlhXuE.png) 28 | 29 | Looking at the image, we can see that `features` folder where we put all our features. We create a subdirectory with the feature name and then inside a `javascript` file to code that feature and also a `.tpl.html` file for its template. the `.tpl.html` is my convention, you can change that in the `gulpfile.js`. 30 | 31 | If a feature gets big enough, you can create multiple `.js` files, that is not a problem. 32 | 33 | For **common** stuff, we created a `common` folder where we can put all our `services` and `directives`. Notice how I put the `foo` directive template inside the same folder. 34 | 35 | The workflow won't force you to use this structure, the only forced convention here is to put your templates under `/js` and not under `/templates` or something like that. Also the extension being `*.tpl.html` is needed (again, easy to change in the `gulpfile.js`). Leaving that aside, you're free to code your app in the way you like. 36 | 37 | ```javascript 38 | appTemplates: 'app/js/**/*.tpl.html', 39 | ``` 40 | 41 | ## Testing your app 42 | 43 | The only convention here is to name your tests like: `*_spec.js`. Leaving that aside, you can structure it the way you like. 44 | 45 | You can do it per features like our main code or organize them per type (`controllers`, `directives`, etc.). 46 | 47 | As a test runner we are using `test'em`, `jasmine 2` as the framework of choice and `Chrome` to run the tests. You can change `jasmine` and `Chrome` in `testem.json`. 48 | 49 | ```json 50 | { 51 | "framework" : "jasmine2", 52 | "launch_in_dev" : ["Chrome"], 53 | "src_files" : [ 54 | "tmp/js/app.js", 55 | "vendor/angular-mocks/angular-mocks.js", 56 | "spec/**/*_spec.js" 57 | ] 58 | } 59 | ``` 60 | 61 | ## Talking with the backend 62 | 63 | Our angular app will run on the port `5000` and by default all the requests to the backend are going to use a `proxy` to the port `8080`. How does that work? 64 | 65 | Imagine you have a `Rails` backend (the workflow is backend agnostic) running on port `8080` and it serves some `users` information at `/api/users`. Since the `Rails` app runs on port `8080` and our `Angular` app runs on the port `5000` we would need to do something like: 66 | 67 | ```javascript 68 | $http.get('localhost:8080/api/users'); 69 | ``` 70 | And then activate `CORS` in our `Rails` app. That is not needed here, we can safely do: 71 | 72 | ```javascript 73 | $http.get('/api/users'); 74 | ``` 75 | Without any need of `CORS`. Thanks to our `proxy`, our `Angular` app will think that the backend is running in the same domain and port so if we deploy both application together (like putting our `angular` app into `Rails'` `/public` directory) we don't need to change anything in our code. 76 | 77 | ```javascript 78 | gulp.task('webserver', ['indexHtml-dev', 'images-dev'], function() { 79 | plugins.connect.server({ 80 | root: paths.tmpFolder, 81 | port: 5000, 82 | livereload: true, 83 | middleware: function(connect, o) { 84 | return [ (function() { 85 | var url = require('url'); 86 | var proxy = require('proxy-middleware'); 87 | var options = url.parse('http://localhost:8080/api'); 88 | options.route = '/api'; 89 | return proxy(options); 90 | })(), historyApiFallback ]; 91 | } 92 | }); 93 | }); 94 | ``` 95 | 96 | There you change our app port and also the port where our backend is running. Also notice that the requests that goes through the `proxy` are the ones that starts with `/api`. 97 | 98 | ## The gulp tasks 99 | 100 | To run our tasks and watch for file changes, we just need to run: 101 | 102 | ``` 103 | $ gulp 104 | ``` 105 | 106 | That will generate a `tmp` folder with all our `javascript` files concatenated in one central place. That free us of having to create a ` 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('app', []); 2 | -------------------------------------------------------------------------------- /app/scss/main.scss: -------------------------------------------------------------------------------- 1 | body {} -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fox-angular-gulp-workflow", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/Foxandxss/fox-angular-gulp-workflow", 5 | "authors": [ 6 | "Jesus Rodriguez " 7 | ], 8 | "description": "A workflow for angular", 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "/vendor/bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "~1.4.*", 20 | "angular-mocks": "~1.4.*", 21 | "lodash": "~3.10.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var fs = require('fs'); 3 | var plugins = require('gulp-load-plugins')(); 4 | var es = require('event-stream'); 5 | var del = require('del'); 6 | var historyApiFallback = require('connect-history-api-fallback'); 7 | 8 | var vendor = require('./vendor/manifest'); 9 | 10 | var paths = { 11 | appJavascript: ['app/js/app.js', 'app/js/**/*.js'], 12 | appTemplates: 'app/js/**/*.tpl.html', 13 | appMainSass: 'app/scss/main.scss', 14 | appStyles: 'app/scss/**/*.scss', 15 | appImages: 'app/images/**/*', 16 | indexHtml: 'app/index.html', 17 | vendorFonts: vendor.fonts || [], 18 | vendorJavascript: vendor.javascript || [], 19 | vendorCss: vendor.css || [], 20 | finalAppJsPath: '/js/app.js', 21 | finalAppCssPath: '/css/app.css', 22 | specFolder: ['spec/**/*_spec.js'], 23 | tmpFolder: 'tmp', 24 | tmpJavascript: 'tmp/js', 25 | tmpAppJs: 'tmp/js/app.js', 26 | tmpCss: 'tmp/css', 27 | tmpFonts: 'tmp/fonts', 28 | tmpImages: 'tmp/images', 29 | distFolder: 'dist', 30 | distJavascript: 'dist/js', 31 | distCss: 'dist/css', 32 | distFonts: 'dist/fonts', 33 | distImages: 'dist/images', 34 | distJsManifest: 'dist/js/rev-manifest.json', 35 | distCssManifest: 'dist/css/rev-manifest.json' 36 | }; 37 | 38 | gulp.task('scripts-dev', function() { 39 | return gulp.src(paths.vendorJavascript.concat(paths.appJavascript, paths.appTemplates)) 40 | .pipe(plugins.if(/html$/, buildTemplates())) 41 | .pipe(plugins.sourcemaps.init()) 42 | .pipe(plugins.concat('app.js')) 43 | .pipe(plugins.sourcemaps.write('.')) 44 | .pipe(gulp.dest(paths.tmpJavascript)) 45 | .pipe(plugins.connect.reload()); 46 | }); 47 | gulp.task('scripts-prod', function() { 48 | return gulp.src(paths.vendorJavascript.concat(paths.appJavascript, paths.appTemplates)) 49 | .pipe(plugins.if(/html$/, buildTemplates())) 50 | .pipe(plugins.concat('app.js')) 51 | .pipe(plugins.ngAnnotate()) 52 | .pipe(plugins.uglify()) 53 | .pipe(plugins.rev()) 54 | .pipe(gulp.dest(paths.distJavascript)) 55 | .pipe(plugins.rev.manifest({path: 'rev-manifest.json'})) 56 | .pipe(gulp.dest(paths.distJavascript)); 57 | }); 58 | 59 | gulp.task('styles-dev', function() { 60 | return gulp.src(paths.vendorCss.concat(paths.appMainSass)) 61 | .pipe(plugins.if(/scss$/, plugins.sass())) 62 | .pipe(plugins.concat('app.css')) 63 | .pipe(gulp.dest(paths.tmpCss)) 64 | .pipe(plugins.connect.reload()); 65 | }); 66 | 67 | gulp.task('styles-prod', function() { 68 | return gulp.src(paths.vendorCss.concat(paths.appMainSass)) 69 | .pipe(plugins.if(/scss$/, plugins.sass())) 70 | .pipe(plugins.concat('app.css')) 71 | .pipe(plugins.cleanCss()) 72 | .pipe(plugins.rev()) 73 | .pipe(gulp.dest(paths.distCss)) 74 | .pipe(plugins.rev.manifest({path: 'rev-manifest.json'})) 75 | .pipe(gulp.dest(paths.distCss)); 76 | }); 77 | 78 | gulp.task('fonts-dev', function() { 79 | return gulp.src(paths.vendorFonts) 80 | .pipe(gulp.dest(paths.tmpFonts)); 81 | }); 82 | 83 | gulp.task('fonts-prod', function() { 84 | return gulp.src(paths.vendorFonts) 85 | .pipe(gulp.dest(paths.distFonts)); 86 | }); 87 | 88 | gulp.task('images-dev', function() { 89 | return gulp.src(paths.appImages) 90 | .pipe(gulp.dest(paths.tmpImages)) 91 | .pipe(plugins.connect.reload()); 92 | }); 93 | 94 | gulp.task('images-prod', function() { 95 | return gulp.src(paths.appImages) 96 | .pipe(gulp.dest(paths.distImages)); 97 | }); 98 | 99 | gulp.task('indexHtml-dev', ['scripts-dev', 'styles-dev'], function() { 100 | var manifest = { 101 | js: paths.finalAppJsPath, 102 | css: paths.finalAppCssPath 103 | }; 104 | 105 | return gulp.src(paths.indexHtml) 106 | .pipe(plugins.template({css: manifest['css'], js: manifest['js']})) 107 | .pipe(gulp.dest(paths.tmpFolder)) 108 | .pipe(plugins.connect.reload()); 109 | }); 110 | 111 | gulp.task('indexHtml-prod', ['scripts-prod', 'styles-prod'], function() { 112 | var jsManifest = JSON.parse(fs.readFileSync(paths.distJsManifest, 'utf8')); 113 | var cssManifest = JSON.parse(fs.readFileSync(paths.distCssManifest, 'utf8')); 114 | 115 | var manifest = { 116 | js: '/js/' + jsManifest['app.js'], 117 | css: '/css/' + cssManifest['app.css'] 118 | }; 119 | 120 | return gulp.src(paths.indexHtml) 121 | .pipe(plugins.template({css: manifest['css'], js: manifest['js']})) 122 | .pipe(plugins.rename('index.html')) 123 | .pipe(gulp.dest(paths.distFolder)); 124 | }); 125 | 126 | gulp.task('lint', function() { 127 | return gulp.src(paths.appJavascript.concat(paths.specFolder)) 128 | .pipe(plugins.jshint()) 129 | .pipe(plugins.jshint.reporter('jshint-stylish')); 130 | }); 131 | 132 | gulp.task('testem', function() { 133 | return gulp.src(['']) // We don't need files, that is managed on testem.json 134 | .pipe(plugins.testem({ 135 | configFile: 'testem.json' 136 | })); 137 | }); 138 | 139 | gulp.task('clean', function(cb) { 140 | del([paths.tmpFolder, paths.distFolder], cb); 141 | }); 142 | 143 | gulp.task('watch', ['webserver'], function() { 144 | gulp.watch(paths.appJavascript, ['lint', 'scripts-dev']); 145 | gulp.watch(paths.appTemplates, ['scripts-dev']); 146 | gulp.watch(paths.vendorJavascript, ['scripts-dev']); 147 | gulp.watch(paths.appImages, ['images-dev']); 148 | gulp.watch(paths.specFolder, ['lint']); 149 | gulp.watch(paths.indexHtml, ['indexHtml-dev']); 150 | gulp.watch(paths.appStyles, ['styles-dev']); 151 | gulp.watch(paths.vendorCss, ['styles-dev']); 152 | }); 153 | 154 | gulp.task('webserver', ['indexHtml-dev', 'fonts-dev', 'images-dev'], function() { 155 | plugins.connect.server({ 156 | root: paths.tmpFolder, 157 | port: 5000, 158 | livereload: true, 159 | middleware: function(connect, o) { 160 | return [ (function() { 161 | var url = require('url'); 162 | var proxy = require('proxy-middleware'); 163 | var options = url.parse('http://localhost:8080/api'); 164 | options.route = '/api'; 165 | return proxy(options); 166 | })(), historyApiFallback() ]; 167 | } 168 | }); 169 | }); 170 | 171 | gulp.task('default', ['watch']); 172 | gulp.task('production', ['scripts-prod', 'styles-prod', 'fonts-prod', 'images-prod', 'indexHtml-prod']); 173 | 174 | function buildTemplates() { 175 | return es.pipeline( 176 | plugins.minifyHtml({ 177 | empty: true, 178 | spare: true, 179 | quotes: true 180 | }), 181 | plugins.angularTemplatecache({ 182 | module: 'app' 183 | }) 184 | ); 185 | } 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fox-angular-gulp-workflow", 3 | "version": "0.1.0", 4 | "description": "A workflow for Angular made with Gulp", 5 | "scripts": { 6 | "install": "bower install", 7 | "test": "gulp testem" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Foxandxss/fox-angular-gulp-workflow.git" 12 | }, 13 | "author": "Jesus Rodriguez", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/Foxandxss/fox-angular-gulp-workflow/issues" 17 | }, 18 | "homepage": "https://github.com/Foxandxss/fox-angular-gulp-workflow", 19 | "devDependencies": { 20 | "connect-history-api-fallback": "1.2.0", 21 | "del": "^2.2.0", 22 | "event-stream": "^3.1.7", 23 | "gulp": "^3.8.8", 24 | "gulp-angular-templatecache": "^1.3.0", 25 | "gulp-clean-css": "^2.0.5", 26 | "gulp-concat": "^2.3.4", 27 | "gulp-connect": "^3.2.2", 28 | "gulp-if": "^2.0.0", 29 | "gulp-jshint": "^2.0.0", 30 | "gulp-load-plugins": "^1.0.0-rc.1", 31 | "gulp-minify-html": "^1.0.4", 32 | "gulp-ng-annotate": "^2.0.0", 33 | "gulp-rename": "^1.2.0", 34 | "gulp-rev": "^7.0.0", 35 | "gulp-sass": "^2.0.4", 36 | "gulp-sourcemaps": "^1.2.4", 37 | "gulp-template": "^4.0.0", 38 | "gulp-testem": "0.0.1", 39 | "gulp-uglify": "^1.0.1", 40 | "gulp-util": "^3.0.1", 41 | "jshint": "^2.9.1", 42 | "jshint-stylish": "^2.0.1", 43 | "proxy-middleware": "^0.15.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxandxss/fox-angular-gulp-workflow/9bcaa12509546aca62280042a5bf0adff062d5af/spec/helpers/.gitkeep -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework" : "jasmine2", 3 | "launch_in_dev" : ["Chrome"], 4 | "src_files" : [ 5 | "tmp/js/app.js", 6 | "vendor/angular-mocks/angular-mocks.js", 7 | "spec/**/*_spec.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /vendor/manifest.js: -------------------------------------------------------------------------------- 1 | exports.javascript = [ 2 | 'vendor/angular/angular.js', 3 | 'vendor/lodash/dist/lodash.js' 4 | ]; 5 | 6 | exports.css = [ 7 | 8 | ]; 9 | 10 | exports.fonts = [ 11 | 12 | ]; 13 | --------------------------------------------------------------------------------