├── app ├── images │ ├── .keep │ └── image.jpg ├── views │ ├── about.jade │ ├── main.jade │ └── index.jade ├── scripts │ ├── vendor.js │ ├── app.controller.js │ ├── about.controller.js │ └── entry.js ├── package.json └── styles │ └── app.scss ├── .gitattributes ├── config-example.json ├── .gitignore ├── test ├── e2e │ ├── main.po.js │ └── main.spec.js └── protractor.config.js ├── .jshintrc ├── .editorconfig ├── README.md ├── package.json └── gulpfile.js /app/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /app/views/about.jade: -------------------------------------------------------------------------------- 1 | div(ng-controller="AboutController") 2 | p.lead {{text}} 3 | -------------------------------------------------------------------------------- /app/images/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhpoet/nwjs-boilerplate/HEAD/app/images/image.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .build 3 | .tmp 4 | packages 5 | cache 6 | .sass-cache 7 | config.json 8 | 9 | -------------------------------------------------------------------------------- /app/views/main.jade: -------------------------------------------------------------------------------- 1 | p.lead Happiness lies in the joy of achievement and the thrill of creative effort. 2 | -------------------------------------------------------------------------------- /app/scripts/vendor.js: -------------------------------------------------------------------------------- 1 | window.jQuery = window.$ = require('jquery'); 2 | 3 | require('angular'); 4 | require('angular-ui-router'); 5 | require('bootstrap'); 6 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontendboilerplate", 3 | "version": "0.0.1", 4 | "main": "index.html", 5 | "window": { 6 | "frame": true, 7 | "toolbar": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/scripts/app.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular 4 | .module('app') 5 | .controller('AppController', AppController); 6 | 7 | AppController.$inject = ['$rootScope']; 8 | 9 | function AppController ($scope) 10 | { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/scripts/about.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular 4 | .module('app') 5 | .controller('AboutController', AboutController); 6 | 7 | AboutController.$inject = ['$scope']; 8 | 9 | function AboutController ($scope) 10 | { 11 | $scope.text = 'This could be a dynamic text' 12 | } 13 | -------------------------------------------------------------------------------- /test/e2e/main.po.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file uses the Page Object pattern to define the main page for tests 3 | * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var MainPage = function() { 9 | 10 | this.text = element(by.css('.text')); 11 | }; 12 | 13 | module.exports = new MainPage(); 14 | -------------------------------------------------------------------------------- /test/e2e/main.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('The main view', function () { 4 | var page; 5 | 6 | beforeEach(function () { 7 | browser.get('http://localhost:3000/index.html'); 8 | page = require('./main.po'); 9 | }); 10 | 11 | it('should include the right text', function() { 12 | expect(page.text.getText()).toBe('Oh hey!lo world'); 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /test/protractor.config.js: -------------------------------------------------------------------------------- 1 | // An example configuration file. 2 | exports.config = { 3 | // The address of a running selenium server. 4 | //seleniumAddress: 'http://localhost:4444/wd/hub', 5 | //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json 6 | 7 | // Capabilities to be passed to the webdriver instance. 8 | capabilities: { 9 | 'browserName': 'chrome' 10 | }, 11 | 12 | // Spec patterns are relative to the current working directly when 13 | // protractor is called. 14 | specs: ['e2e/**/*.js'], 15 | 16 | // Options to be passed to Jasmine-node. 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /app/scripts/entry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var appDependencies = [ 4 | 'ng', 5 | 'ui.router' 6 | ]; 7 | 8 | angular 9 | .module('app', appDependencies) 10 | .config(appConfig) 11 | .constant('config', require('../../config.json')); 12 | 13 | require('./app.controller'); 14 | require('./about.controller'); 15 | 16 | appConfig.$inject = ['$stateProvider', '$urlRouterProvider']; 17 | 18 | function appConfig ($stateProvider, $urlRouterProvider) { 19 | var routes = [ 20 | { 21 | name: 'main', 22 | path: '' 23 | }, 24 | { 25 | name: 'about', 26 | path: 'about' 27 | } 28 | ]; 29 | 30 | routes.forEach(function(route){ 31 | $stateProvider.state(route.name, { 32 | url: "/" + route.path, 33 | views: { 34 | guest: { templateUrl: 'views/' + route.name + '.html' } 35 | } 36 | }); 37 | }); 38 | 39 | $urlRouterProvider.otherwise("/"); 40 | } 41 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | body { 6 | margin-bottom: 60px; 7 | } 8 | 9 | h1 { 10 | margin-top: 100px; 11 | color: #fff; 12 | font-size: 40px; 13 | } 14 | 15 | header { 16 | width: 100%; 17 | height: 200px; 18 | position: absolute; 19 | top: 0; 20 | z-index: -1; 21 | 22 | background: url("../images/image.jpg"); 23 | background-position: center; 24 | background-repeat: no-repeat; 25 | -webkit-filter: brightness(40%); 26 | filter: brightness(40%); 27 | } 28 | 29 | footer { 30 | position: absolute; 31 | bottom: 0; 32 | width: 100%; 33 | height: 60px; 34 | background-color: #f5f5f5; 35 | } 36 | 37 | .container { 38 | width: auto; 39 | max-width: 680px; 40 | padding: 0 15px; 41 | } 42 | .container .text-muted { 43 | margin: 20px 0; 44 | } 45 | .menu { 46 | margin-top: 80px; 47 | a { 48 | margin: 0 10px 0 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [nw.js](https://github.com/nwjs/nw.js/) version of the [frontend-boilerplate](https://github.com/vhpoet/frontend-boilerplate) 2 | 3 | No fancy generators, just clone this repo and build your app on top of it. 4 | 5 | ### Package 6 | 7 | - Uses nw.js, AngularJS, Gulp, Jade, Sass (with sourcemaps), browserSync. 8 | - Respects [angularjs-styleguide](https://github.com/johnpapa/angularjs-styleguide). 9 | 10 | ### Setup 11 | 12 | - Setup [nw.js](https://github.com/nwjs/nw.js/). 13 | - `$ git clone https://github.com/vhpoet/nwjs-boilerplate` 14 | - Find and replace `frontendboilerplate` to `yourappname` in `./` 15 | - `$ npm install` 16 | - `$ npm install -g gulp` 17 | - `$ gem install sass` 18 | - `$ cp config-example.json config.json` 19 | - `$ gulp` 20 | - `$ /path/to/nw .` 21 | 22 | ### Deployment 23 | 24 | Run `$ gulp packages` for the production ready packages in `build/packages`. 25 | 26 | ### Contributing 27 | 28 | I'm open for contributions via pull-requests, and please open an issue for anything you don't like. 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontendboilerplate", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": { 6 | "name": "Vahe Hovhannisyan", 7 | "email": "vhpoet@gmail.com", 8 | "url": "http://vahehovhannisyan.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/vhpoet/frontend-boilerplate.git" 13 | }, 14 | "node-remote": "http://localhost:3000", 15 | "main": "http://localhost:3000", 16 | "window": { 17 | "frame": true, 18 | "toolbar": true 19 | }, 20 | "readmeFilename": "README", 21 | "devDependencies": { 22 | "browser-sync": "^1.8.3", 23 | "connect-modrewrite": "^0.7.9", 24 | "del": "^1.1.1", 25 | "gulp": "~3.8.10", 26 | "gulp-csso": "^0.2.9", 27 | "gulp-if": "^1.2.5", 28 | "gulp-jade": "^1.0.0", 29 | "gulp-jade-find-affected": "^0.2.1", 30 | "gulp-livereload": "~2.1.1", 31 | "gulp-load-plugins": "^0.8.0", 32 | "gulp-minify-html": "^0.1.8", 33 | "gulp-ng-annotate": "^0.4.3", 34 | "gulp-protractor": "0.0.12", 35 | "gulp-rev": "^2.0.1", 36 | "gulp-rev-replace": "^0.3.1", 37 | "gulp-ruby-sass": "~1.0.0-alpha", 38 | "gulp-size": "^1.2.0", 39 | "gulp-sourcemaps": "^1.3.0", 40 | "gulp-uglify": "~1.0.1", 41 | "gulp-useref": "^1.1.0", 42 | "gulp-watch": "^4.2.4", 43 | "gulp-zip": "^3.0.2", 44 | "jade": "^1.9.2", 45 | "json-loader": "^0.5.1", 46 | "merge-stream": "^1.0.0", 47 | "nw-builder": "^2.0.2", 48 | "protractor": "^1.5.0", 49 | "webpack-stream": "~2.1.0", 50 | "wiredep": "^2.2.2" 51 | }, 52 | "dependencies": { 53 | "angular": "^1.4.4", 54 | "angular-ui-router": "^0.2.15", 55 | "bootstrap": "^3.3.5", 56 | "jquery": "^2.1.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/views/index.jade: -------------------------------------------------------------------------------- 1 | html(ng-app='app', ng-controller='AppController') 2 | head 3 | meta(charset='utf-8') 4 | title frontendboilerplate 5 | meta(name='description', content='') 6 | meta(property='og:description', content='') 7 | 8 | meta(name='apple-mobile-web-app-capable', content='yes') 9 | meta(name='apple-mobile-web-app-title', content='frontendboilerplate') 10 | meta(name='mobile-web-app-capable', content='yes') 11 | meta(name='mobile-web-app-title', content='frontendboilerplate') 12 | 13 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 14 | 15 | 16 | 17 | link(rel='stylesheet', href='../node_modules/bootstrap/dist/css/bootstrap.css') 18 | 19 | 20 | 21 | 22 | 23 | link(rel='stylesheet', href='styles/app.css') 24 | 25 | 26 | 27 | // FAVICON 28 | link(rel='shortcut icon', href='images/favicon.ico', type='image/x-icon') 29 | link(rel='icon', href='images/favicon.ico', type='image/x-icon') 30 | body 31 | header 32 | 33 | .container 34 | .page-header 35 | h1 Frontend Boilerplate 36 | .menu 37 | a(ui-sref="main") Home 38 | a(ui-sref="about") About 39 | 40 | div(ui-view="guest") 41 | 42 | footer 43 | .container 44 | p.text-muted Made with love. 45 | 46 | 47 | 48 | script(src='scripts/vendor.js') 49 | 50 | 51 | 52 | 53 | 54 | script(src='scripts/app.js') 55 | 56 | 57 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | jade = require('jade'), 5 | modRewrite = require('connect-modrewrite'), 6 | webpack = require('webpack-stream'), 7 | NwBuilder = require('nw-builder'), 8 | merge = require('merge-stream'); 9 | 10 | var $ = require('gulp-load-plugins')({ 11 | pattern: ['gulp-*', 'del', 'browser-sync'] 12 | }); 13 | 14 | // Constants 15 | var BUILD_DIR = '.build/'; 16 | var TMP_DIR = '.tmp/'; 17 | var PACKAGES_DIR = 'packages/'; 18 | 19 | var meta = require('./package.json'); 20 | 21 | // Webpack 22 | gulp.task('webpack:vendor', function() { 23 | return gulp.src('app/scripts/vendor.js') 24 | .pipe(webpack({ 25 | output: { 26 | filename: 'vendor.js' 27 | }, 28 | target: 'node-webkit' 29 | })) 30 | .pipe(gulp.dest(TMP_DIR + 'scripts/')) 31 | }); 32 | 33 | gulp.task('webpack', function() { 34 | return gulp.src('app/scripts/entry.js') 35 | .pipe(webpack({ 36 | module: { 37 | loaders: [ 38 | { test: /\.json$/, loader: 'json-loader' } 39 | ] 40 | }, 41 | output: { 42 | filename: 'app.js' 43 | }, 44 | target: 'node-webkit' 45 | })) 46 | .pipe(gulp.dest(TMP_DIR + 'scripts/')) 47 | .pipe($.browserSync.reload({stream:true})); 48 | }); 49 | 50 | // Views 51 | gulp.task('jade:dev', function(){ 52 | return gulp.src('app/views/**/*.jade') 53 | .pipe($.jade({ 54 | jade: jade, 55 | pretty: true 56 | })) 57 | .pipe(gulp.dest(TMP_DIR + 'views')); 58 | }); 59 | 60 | gulp.task('jade:dist', function(){ 61 | return gulp.src('app/views/**/*.jade') 62 | .pipe($.jade({ 63 | jade: jade, 64 | pretty: true 65 | })) 66 | .pipe(gulp.dest(BUILD_DIR + 'views')); 67 | }); 68 | 69 | // Html 70 | gulp.task('html:dev', ['jade:dev'], function() { 71 | return gulp.src(TMP_DIR + 'views/index.html') 72 | .pipe(gulp.dest(TMP_DIR)) 73 | .pipe($.browserSync.reload({stream:true})); 74 | }); 75 | 76 | gulp.task('html:dist', ['jade:dist'], function() { 77 | return gulp.src(BUILD_DIR + 'views/index.html') 78 | .pipe(gulp.dest(BUILD_DIR)) 79 | }); 80 | 81 | // Sass 82 | gulp.task('sass', function () { 83 | return $.rubySass('app/styles/app.scss', { sourcemap: true }) 84 | .on('error', function (err) { 85 | console.error('Error!', err.message); 86 | }) 87 | .pipe($.sourcemaps.write()) 88 | .pipe(gulp.dest(TMP_DIR + 'styles')) 89 | .pipe($.browserSync.reload({stream:true})); 90 | }); 91 | 92 | // Images 93 | gulp.task('images:dist', function () { 94 | return gulp.src('app/images/**/*') 95 | .pipe(gulp.dest(BUILD_DIR + 'images/')) 96 | }); 97 | 98 | gulp.task('images:dev', function () { 99 | return gulp.src('app/images/**/*') 100 | .pipe(gulp.dest(TMP_DIR + 'images/')) 101 | .pipe($.browserSync.reload({stream:true})); 102 | }); 103 | 104 | // package.json 105 | gulp.task('packagejson', function () { 106 | return gulp.src('app/package.json') 107 | .pipe(gulp.dest(BUILD_DIR)) 108 | }); 109 | 110 | // .htaccess 111 | gulp.task('htaccess', function () { 112 | return gulp.src('.htaccess') 113 | .pipe(gulp.dest(BUILD_DIR)); 114 | }); 115 | 116 | // Static server 117 | gulp.task('serve:dev', ['dev'], function() { 118 | $.browserSync({ 119 | open: false, 120 | server: { 121 | baseDir: [".",TMP_DIR,"app"], 122 | middleware: [ 123 | modRewrite([ 124 | '!\\.html|\\.js|\\.css|\\.png|\\.jpg|\\.gif|\\.svg|\\.txt$ /index.html [L]' 125 | ]) 126 | ] 127 | } 128 | }); 129 | }); 130 | 131 | gulp.task('serve:dist', function() { 132 | $.browserSync({ 133 | open: false, 134 | server: { 135 | baseDir: [BUILD_DIR], 136 | middleware: [ 137 | modRewrite([ 138 | '!\\.html|\\.js|\\.css|\\.png|\\.jpg|\\.gif|\\.svg|\\.txt$ /index.html [L]' 139 | ]) 140 | ] 141 | } 142 | }); 143 | }); 144 | 145 | // e2e tests 146 | gulp.task('webdriver-update', $.protractor.webdriver_update); 147 | gulp.task('protractor', ['webdriver-update'], function () { 148 | gulp.src(['test/e2e/**/*.js']) 149 | .pipe($.protractor.protractor({ 150 | configFile: "test/protractor.config.js", 151 | args: ['--baseUrl', 'http://localhost:3000'] 152 | })) 153 | .on('error', function (e) { 154 | throw e 155 | }); 156 | }); 157 | 158 | // Clean 159 | gulp.task('clean', function () { 160 | $.del.sync([ 161 | TMP_DIR + 'scripts', 162 | TMP_DIR + 'views', 163 | TMP_DIR + 'styles', 164 | TMP_DIR + 'index.html', 165 | BUILD_DIR + '*' 166 | ]); 167 | }); 168 | 169 | // Development 170 | gulp.task('dev', ['clean', 'webpack', 'webpack:vendor', 'sass', 'html:dev', 'images:dev']); 171 | 172 | // Default Task (Dev environment) 173 | gulp.task('default', ['serve:dev'], function() { 174 | // Scripts 175 | gulp.watch(['config.json', 'app/scripts/**/*.js'], ['webpack']); 176 | 177 | // Views 178 | $.watch('app/views/**/*.jade') 179 | .pipe($.jadeFindAffected()) 180 | .pipe($.jade({jade: jade, pretty: true})) 181 | .pipe(gulp.dest(TMP_DIR + 'views')); 182 | 183 | // Htmls 184 | gulp.watch(TMP_DIR + 'views/**/*.html', ['html:dev']); 185 | 186 | // Styles 187 | gulp.watch('app/styles/**/*.scss', ['sass']); 188 | }); 189 | 190 | gulp.task('deps', ['html:dist'], function () { 191 | var assets = $.useref.assets(); 192 | 193 | return gulp.src([BUILD_DIR + 'index.html']) 194 | // Concatenates asset files from the build blocks inside the HTML 195 | .pipe(assets) 196 | // Appends hash to extracted files app.css → app-098f6bcd.css 197 | .pipe($.rev()) 198 | // Adds AngularJS dependency injection annotations 199 | .pipe($.if('*.js', $.ngAnnotate())) 200 | // Uglifies js files 201 | .pipe($.if('*.js', $.uglify())) 202 | // Minifies css files 203 | .pipe($.if('*.css', $.csso())) 204 | // Brings back the previously filtered HTML files 205 | .pipe(assets.restore()) 206 | // Parses build blocks in html to replace references to non-optimized scripts or stylesheets 207 | .pipe($.useref()) 208 | // Rewrites occurences of filenames which have been renamed by rev 209 | .pipe($.revReplace()) 210 | // Minifies html 211 | .pipe($.if('*.html', $.minifyHtml({ 212 | empty: true, 213 | spare: true, 214 | quotes: true 215 | }))) 216 | // Creates the actual files 217 | .pipe(gulp.dest(BUILD_DIR)) 218 | // Print the file sizes 219 | .pipe($.size({ title: BUILD_DIR, showFiles: true })); 220 | }); 221 | 222 | // Distribution 223 | gulp.task('prepare', ['dev', 'images:dist', 'htaccess', 'packagejson']); 224 | gulp.task('dist', ['prepare', 'deps']); 225 | 226 | // Build packages 227 | gulp.task('build', ['dist'], function() { 228 | var nw = new NwBuilder({ 229 | files: [BUILD_DIR + '**/**'], // use the glob format 230 | platforms: ['win', 'osx', 'linux'], 231 | // TODO: Use these instead of the nested app/package.json values 232 | //appName: meta.name, 233 | //appVersion: meta.version, 234 | buildDir: PACKAGES_DIR, 235 | macZip: true, 236 | cacheDir: TMP_DIR, 237 | version: '0.12.3' 238 | // TODO: timestamped versions 239 | // TODO: macIcns 240 | // TODO: winIco 241 | }); 242 | 243 | return nw.build() 244 | .catch(function (error) { 245 | console.error(error); 246 | }); 247 | }); 248 | 249 | // Zip packages 250 | gulp.task('zip', ['build'], function() { 251 | // Zip the packages 252 | var linux32 = gulp.src(PACKAGES_DIR + meta.name + '/linux32/**/*') 253 | .pipe($.zip('linux32.zip')) 254 | .pipe(gulp.dest(PACKAGES_DIR + meta.name)); 255 | 256 | var linux64 = gulp.src(PACKAGES_DIR + meta.name + '/linux64/**/*') 257 | .pipe($.zip('linux64.zip')) 258 | .pipe(gulp.dest(PACKAGES_DIR + meta.name)); 259 | 260 | var osx32 = gulp.src(PACKAGES_DIR + meta.name + '/osx32/**/*') 261 | .pipe($.zip('osx32.zip')) 262 | .pipe(gulp.dest(PACKAGES_DIR + meta.name)); 263 | 264 | var osx64 = gulp.src(PACKAGES_DIR + meta.name + '/osx64/**/*') 265 | .pipe($.zip('osx64.zip')) 266 | .pipe(gulp.dest(PACKAGES_DIR + meta.name)); 267 | 268 | var win32 = gulp.src(PACKAGES_DIR + meta.name + '/win32/**/*') 269 | .pipe($.zip('win32.zip')) 270 | .pipe(gulp.dest(PACKAGES_DIR + meta.name)); 271 | 272 | var win64 = gulp.src(PACKAGES_DIR + meta.name + '/win64/**/*') 273 | .pipe($.zip('win64.zip')) 274 | .pipe(gulp.dest(PACKAGES_DIR + meta.name)); 275 | 276 | return merge(linux32, linux64, osx32, osx64, win32, win64); 277 | }); 278 | 279 | // Final product 280 | gulp.task('packages', ['build', 'zip']); 281 | 282 | --------------------------------------------------------------------------------