diff --git a/app/index.js b/app/index.js index 6dc1f23a3..9bd0697d1 100644 --- a/app/index.js +++ b/app/index.js @@ -429,9 +429,16 @@ Generator.prototype.addHtmlViews = function addHtmlViews() { Generator.prototype.packageFiles = function () { this.coffee = this.env.options.coffee; + this.jade = this.env.options.jade; + this.template('../../templates/common/_bower.json', 'bower.json'); this.template('../../templates/common/_package.json', 'package.json'); - this.template('../../templates/common/Gruntfile.js', 'Gruntfile.js'); + + if (this.coffee) { + this.template('../../templates/common/Gruntfile.coffee', 'Gruntfile.coffee'); + } else { + this.template('../../templates/common/Gruntfile.js', 'Gruntfile.js'); + } }; Generator.prototype.imageFiles = function () { @@ -470,30 +477,47 @@ Generator.prototype._injectDependencies = function _injectDependencies() { }; Generator.prototype.serverFiles = function () { - this.template('../../templates/express/server.js', 'server.js'); - this.copy('../../templates/express/jshintrc', 'lib/.jshintrc'); - this.template('../../templates/express/controllers/api.js', 'lib/controllers/api.js'); - this.template('../../templates/express/controllers/index.js', 'lib/controllers/index.js'); - this.template('../../templates/express/routes.js', 'lib/routes.js'); - this.template('../../templates/express/test/thing/api.js', 'test/server/thing/api.js'); - - this.template('../../templates/express/config/express.js', 'lib/config/express.js'); - this.template('../../templates/express/config/config.js', 'lib/config/config.js'); - this.template('../../templates/express/config/env/all.js', 'lib/config/env/all.js'); - this.template('../../templates/express/config/env/development.js', 'lib/config/env/development.js'); - this.template('../../templates/express/config/env/production.js', 'lib/config/env/production.js'); - this.template('../../templates/express/config/env/test.js', 'lib/config/env/test.js'); + var sourceRoot = '../../templates/express', + scriptSuffix = '.js'; + + if (this.env.options.coffee) { + sourceRoot = '../../templates/express-coffeescript'; + scriptSuffix = '.coffee'; + } + + if (!this.env.options.coffee) { + this.copy(sourceRoot + '/jshintrc', 'lib/.jshintrc'); + } + this.template(sourceRoot + '/server' + scriptSuffix, 'server' + scriptSuffix); + this.template(sourceRoot + '/controllers/api' + scriptSuffix, 'lib/controllers/api' + scriptSuffix); + this.template(sourceRoot + '/controllers/index' + scriptSuffix, 'lib/controllers/index' + scriptSuffix); + this.template(sourceRoot + '/routes' + scriptSuffix, 'lib/routes' + scriptSuffix); + this.template(sourceRoot + '/test/thing/api' + scriptSuffix, 'test/server/thing/api' + scriptSuffix); + + this.template(sourceRoot + '/config/express' + scriptSuffix, 'lib/config/express' + scriptSuffix); + this.template(sourceRoot + '/config/config' + scriptSuffix, 'lib/config/config' + scriptSuffix); + this.template(sourceRoot + '/config/env/all' + scriptSuffix, 'lib/config/env/all' + scriptSuffix); + this.template(sourceRoot + '/config/env/development' + scriptSuffix, 'lib/config/env/development' + scriptSuffix); + this.template(sourceRoot + '/config/env/production' + scriptSuffix, 'lib/config/env/production' + scriptSuffix); + this.template(sourceRoot + '/config/env/test' + scriptSuffix, 'lib/config/env/test' + scriptSuffix); }; Generator.prototype.mongoFiles = function () { + var sourceRoot = '../../templates/express', + scriptSuffix = '.js'; + + if (this.env.options.coffee) { + sourceRoot = '../../templates/express-coffeescript'; + scriptSuffix = '.coffee'; + } if (!this.mongo) { return; // Skip if disabled. } this.env.options.mongo = this.mongo; - this.template('../../templates/express/config/dummydata.js', 'lib/config/dummydata.js'); - this.template('../../templates/express/models/thing.js', 'lib/models/thing.js'); + this.template(sourceRoot + '/config/dummydata' + scriptSuffix, 'lib/config/dummydata' + scriptSuffix); + this.template(sourceRoot + '/models/thing' + scriptSuffix, 'lib/models/thing' + scriptSuffix); if(!this.mongoPassportUser) { return; // Skip if disabled. @@ -512,14 +536,14 @@ Generator.prototype.mongoFiles = function () { copyScriptWithEnvOptions(this, 'directives/mongooseError', 'app/scripts/'); // middleware - this.template('../../templates/express/middleware.js', 'lib/middleware.js'); + this.template(sourceRoot + '/middleware' + scriptSuffix, 'lib/middleware' + scriptSuffix); // config - this.template('../../templates/express/config/passport.js', 'lib/config/passport.js'); + this.template(sourceRoot + '/config/passport' + scriptSuffix, 'lib/config/passport' + scriptSuffix); // models - this.template('../../templates/express/models/user.js', 'lib/models/user.js'); + this.template(sourceRoot + '/models/user' + scriptSuffix, 'lib/models/user' + scriptSuffix); // controllers - this.template('../../templates/express/controllers/session.js', 'lib/controllers/session.js'); - this.template('../../templates/express/controllers/users.js', 'lib/controllers/users.js'); + this.template(sourceRoot + '/controllers/session' + scriptSuffix, 'lib/controllers/session' + scriptSuffix); + this.template(sourceRoot + '/controllers/users' + scriptSuffix, 'lib/controllers/users' + scriptSuffix); // tests - this.template('../../templates/express/test/user/model.js', 'test/server/user/model.js'); + this.template(sourceRoot + '/test/user/model' + scriptSuffix, 'test/server/user/model' + scriptSuffix); }; diff --git a/templates/common/Gruntfile.coffee b/templates/common/Gruntfile.coffee new file mode 100644 index 000000000..9878dfe70 --- /dev/null +++ b/templates/common/Gruntfile.coffee @@ -0,0 +1,514 @@ +# Generated on <%= (new Date).toISOString().split('T')[0] %> using <%= pkg.name %> <%= pkg.version %> +'use strict' + +# # Globbing +# for performance reasons we're only matching one level down: +# 'test/spec/{,*/}*.js' +# use this if you want to recursively match all subfolders: +# 'test/spec/**/*.js' + +module.exports = (grunt) -> + + # Load grunt tasks automatically + require('load-grunt-tasks') grunt + + # Time how long tasks take. Can help when optimizing build times + require('time-grunt') grunt + + # Define the configuration for all the tasks + grunt.initConfig + + # Project settings + yeoman: + # configurable paths + app: require('./bower.json').appPath ? 'app' + dist: 'dist' + lib: 'lib' + tmp: '.tmp' + + express: + options: + port: process.env.PORT ? 9000 + dev: + options: + script: '<%%= yeoman.tmp %>/server.js' + debug: true + prod: + options: + script: '<%%= yeoman.dist %>/server.js' + node_env: 'production' + + open: + server: + url: 'http://localhost:<%%= express.options.port %>' + + watch: + coffee: + files: [ + '<%%= yeoman.app %>/scripts/{,*/}*.{coffee,litcoffee,coffee.md}' + '<%%= yeoman.lib %>/{,*/}*.{coffee,litcoffee,coffee.md}' + ] + tasks: ['newer:coffee:dist'] + coffeeTest: + files: ['test/spec/{,*/}*.{coffee,litcoffee,coffee.md}'] + tasks: ['newer:coffee:test', 'karma'] + <% if (compass) { %> + compass: + files: ['<%%= yeoman.app %>/styles/{,*/}*.{scss,sass}'] + tasks: ['compass:server', 'autoprefixer'] + <% } else { %> + styles: + files: ['<%%= yeoman.app %>/styles/{,*/}*.css'] + tasks: ['newer:copy:styles', 'autoprefixer'] + <% } %> + gruntfile: + files: ['Gruntfile.coffee'] + livereload: + files: [ + '<%%= yeoman.app %>/views/{,*#*}*.{html,jade}' + '{<%%= yeoman.tmp %>,<%%= yeoman.app %>}/styles/{,*#*}*.css' + '{<%%= yeoman.tmp %>,<%%= yeoman.app %>}/scripts/{,*#*}*.js' + '<%%= yeoman.app %>/images/{,*#*}*.{png,jpg,jpeg,gif,webp,svg}' + ] + options: + livereload: true + express: + files: [ + '<%%= yeoman.tmp %>/server.js' + '<%%= yeoman.tmp %>/lib/**/*.{js,json}' + ] + tasks: ['express:dev', 'wait'] + options: + livereload: true + nospawn: true #Without this option specified express won't be reloaded + + # Empties folders to start fresh + clean: + dist: + files: [ + dot: true + src: [ + '<%%= yeoman.tmp %>' + '<%%= yeoman.dist %>/*' + '!<%%= yeoman.dist %>/.git*' + '!<%%= yeoman.dist %>/Procfile' + ] + ] + heroku: + files: [ + dot: true + src: [ + 'heroku/*' + '!heroku/.git*' + '!heroku/Procfile' + ] + ] + server: '<%%= yeoman.tmp %>' + + # Add vendor prefixed styles + autoprefixer: + options: + browsers: ['last 1 version'] + dist: + files: [ + expand: true + cwd: '<%%= yeoman.tmp %>/styles/' + src: '{,*/}*.css' + dest: '<%%= yeoman.tmp %>/styles/' + ] + + # Debugging with node inspector + 'node-inspector': + custom: + options: + 'web-host': 'localhost' + + # Use nodemon to run server in debug mode with an initial breakpoint + nodemon: + debug: + script: '<%%= yeoman.tmp %>/server.js', + options: + nodeArgs: ['--debug-brk'] + env: + PORT: process.env.PORT || 9000 + callback: (nodemon) -> + nodemon.on 'log', (event) -> + console.log event.colour + + # opens browser on initial server start + nodemon.on 'config:update', -> + setTimeout -> + require('open') 'http://localhost:8080/debug?port=5858' + , 500 + + # Automatically inject Bower components into the app + 'bower-install': + app: <% if (jade) { %> + html: '<%%= yeoman.app %>/views/index.jade'<% } else { %> + html: '<%%= yeoman.app %>/views/index.html'<% } %> + ignorePath: '<%%= yeoman.app %>/'<% if (compass && bootstrap) { %> + exclude: ['bootstrap-sass']<% } %> + + # Compiles CoffeeScript to JavaScript + coffee: + options: + sourceMap: true + sourceRoot: '' + dist: + files: [ + expand: true + cwd: '<%%= yeoman.app %>/scripts' + src: '**/*.coffee' + dest: '<%%= yeoman.tmp %>/app/scripts' + ext: '.js' + , + expand: true + cwd: '<%%= yeoman.lib %>' + src: '**/*.coffee' + dest: '<%%= yeoman.tmp %>/lib' + ext: '.js' + , + expand: true + cwd: '.' + src: 'server.coffee' + dest: '<%%= yeoman.tmp %>' + ext: '.js' + ] + test: + files: [ + expand: true + cwd: 'test/client/spec' + src: '{,*/}*.coffee' + dest: '<%%= yeoman.tmp %>/client/spec' + ext: '.js' + ] + <% if (compass) { %> + + # Compiles Sass to CSS and generates necessary files if requested + compass: + options: + sassDir: '<%%= yeoman.app %>/styles' + cssDir: '<%%= yeoman.tmp %>/styles' + generatedImagesDir: '<%%= yeoman.tmp %>/images/generated' + imagesDir: '<%%= yeoman.app %>/images' + javascriptsDir: '<%%= yeoman.app %>/scripts' + fontsDir: '<%%= yeoman.app %>/styles/fonts' + importPath: '<%%= yeoman.app %>/bower_components' + httpImagesPath: '/images' + httpGeneratedImagesPath: '/images/generated' + httpFontsPath: '/styles/fonts' + relativeAssets: false + assetCacheBuster: false + raw: 'Sass::Script::Number.precision = 10\n' + dist: + options: + generatedImagesDir: '<%%= yeoman.dist %>/public/images/generated' + server: + options: + debugInfo: true + <% } %> + + # Renames files for browser caching purposes + rev: + dist: + files: + src: [ + '<%%= yeoman.dist %>/public/scripts/{,*/}*.js' + '<%%= yeoman.dist %>/public/styles/{,*/}*.css' + '<%%= yeoman.dist %>/public/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' + '<%%= yeoman.dist %>/public/styles/fonts/*' + ] + + # Reads HTML for usemin blocks to enable smart builds that automatically + # concat, minify and revision files. Creates configurations in memory so + # additional tasks can operate on them + useminPrepare: + html: ['<%%= yeoman.app %>/views/index.html' + '<%%= yeoman.app %>/views/index.jade'] + options: + dest: '<%%= yeoman.dist %>/public' + + # Performs rewrites based on rev and the useminPrepare configuration + usemin: + html: ['<%%= yeoman.dist %>/views/{,*/}*.html' + '<%%= yeoman.dist %>/views/{,*/}*.jade'] + css: ['<%%= yeoman.dist %>/public/styles/{,*/}*.css'] + options: + assetsDirs: ['<%%= yeoman.dist %>/public'] + + # The following *-min tasks produce minified files in the dist folder + imagemin: + options: + cache: false + dist: + files: [ + expand: true + cwd: '<%%= yeoman.app %>/images' + src: '{,*/}*.{png,jpg,jpeg,gif}' + dest: '<%%= yeoman.dist %>/public/images' + ] + + svgmin: + dist: + files: [ + expand: true + cwd: '<%%= yeoman.app %>/images' + src: '{,*/}*.svg' + dest: '<%%= yeoman.dist %>/public/images' + ] + + htmlmin: + dist: + # options: + #collapseWhitespace: true, + #collapseBooleanAttributes: true, + #removeCommentsFromCDATA: true, + #removeOptionalTags: true + files: [ + expand: true + cwd: '<%%= yeoman.app %>/views' + src: ['*.html', 'partials/**/*.html'] + dest: '<%%= yeoman.dist %>/views' + ] + + # Allow the use of non-minsafe AngularJS files. Automatically makes it + # minsafe compatible so Uglify does not destroy the ng references + ngmin: + dist: + files: [ + expand: true + cwd: '<%%= yeoman.tmp %>/concat/scripts' + src: '*.js' + dest: '<%%= yeoman.tmp %>/concat/scripts' + ] + + # Replace Google CDN references + cdnify: + dist: + html: ['<%%= yeoman.dist %>/views/*.html'] + + # Copies remaining files to places other tasks can use + copy: + dist: + files: [ + expand: true + dot: true + cwd: '<%%= yeoman.app %>' + dest: '<%%= yeoman.dist %>/public' + src: [ + '*.{ico,png,txt}' + '.htaccess' + 'bower_components/**/*' + 'images/{,*/}*.{webp}' + 'fonts/**/*' + ] + , + expand: true + dot: true + cwd: '<%%= yeoman.app %>/views' + dest: '<%%= yeoman.dist %>/views' + src: '**/*.jade' + , + expand: true + cwd: '<%%= yeoman.tmp %>/images' + dest: '<%%= yeoman.dist %>/public/images' + src: ['generated/*'] + , + expand: true + cwd: '<%%= yeoman.tmp %>' + dest: '<%%= yeoman.dist %>' + src: [ + 'server.js' + 'lib/**/*' + ] + , + expand: true + dest: '<%%= yeoman.dist %>' + src: ['package.json'] + ] + app: + files: [ + expand: true + cwd: '<%%= yeoman.app %>/bower_components' + dest: '<%%= yeoman.tmp %>/app/bower_components/' + src: '**/*' + , + expand: true + cwd: '<%%= yeoman.app %>/styles' + dest: '<%%= yeoman.tmp %>/app/styles/' + src: '{,*/}*.css' + ,<% if (jade) { %> + expand: true + cwd: '<%%= yeoman.app %>/views' + dest: '<%%= yeoman.tmp %>/app/views/' + src: '**/*.jade' + ,<% } else { %> + expand: true + cwd: '<%%= yeoman.app %>/views' + dest: '<%%= yeoman.tmp %>/app/views/' + src: '**/*.html' + ,<% } %> + expand: true + cwd: '<%%= yeoman.app %>/fonts' + dest: '<%%= yeoman.tmp %>/app/fonts/' + src: '**/*' + , + expand: true + cwd: '<%%= yeoman.app %>/images' + dest: '<%%= yeoman.tmp %>/app/images/' + src: '**/*' + , + expand: true + cwd: '<%%= yeoman.app %>' + dest: '<%%= yeoman.tmp %>/app/' + src: '*' + ] + + # Run some tasks in parallel to speed up the build process + concurrent: + server: [ + 'coffee:dist'<% if (compass) { %> + 'compass:server'<% } else { %> + 'copy:app'<% } %> + ] + test: [ + 'coffee'<% if (compass) { %> + 'compass'<% } else { %> + 'copy:app'<% } %> + ] + debug: + tasks: [ + 'nodemon' + 'node-inspector' + ] + options: + logConcurrentOutput: true + dist: [ + 'coffee'<% if (compass) { %> + 'compass:dist'<% } else { %> + 'copy:app'<% } %> + 'imagemin' + 'svgmin' + 'htmlmin' + ] + + # By default, your `index.html`'s will take care of + # minification. These next options are pre-configured if you do not wish + # to use the Usemin blocks. + # cssmin: + # dist: + # files: + # '<%%= yeoman.dist %>/styles/main.css': [ + # '<%%= yeoman.tmp %>/styles/{,*/}*.css' + # '<%%= yeoman.app %>/styles/{,*/}*.css' + # ] + # uglify: + # dist: + # files: + # '<%%= yeoman.dist %>/scripts/scripts.js': [ + # '<%%= yeoman.dist %>/scripts/scripts.js' + # ] + # concat: + # dist: {} + + # Test settings + karma: + unit: + configFile: 'karma.conf.coffee' + singleRun: true + + mochaTest: + options: + reporter: 'spec' + require: 'coffee-script/register' + src: ['test/server/**/*.coffee'] + + env: + test: + NODE_ENV: 'test' + + # Used for delaying livereload until after server has restarted + grunt.registerTask 'wait', -> + grunt.log.ok 'Waiting for server reload...' + + done = this.async() + + setTimeout -> + grunt.log.writeln 'Done waiting!' + done() + , 500 + + grunt.registerTask 'express-keepalive', 'Keep grunt running', -> + this.async() + + grunt.registerTask 'serve', (target) -> + if target is 'dist' + return grunt.task.run ['build', 'express:prod', 'open', 'express-keepalive'] + + if target is 'debug' + return grunt.task.run [ + 'clean:server' + 'bower-install' + 'concurrent:server' + 'autoprefixer' + 'concurrent:debug' + ] + + grunt.task.run [ + 'clean:server' + 'bower-install' + 'concurrent:server' + 'autoprefixer' + 'express:dev' + 'open' + 'watch' + ] + + grunt.registerTask 'server', -> + grunt.log.warn 'The `server` task has been deprecated. Use `grunt serve` to start a server.' + grunt.task.run ['serve'] + + grunt.registerTask 'test', (target) -> + if target is 'server' + return grunt.task.run [ + 'env:test' + 'mochaTest' + ] + + else if target is 'client' + return grunt.task.run [ + 'clean:server' + 'concurrent:test' + 'autoprefixer' + 'karma' + ] + + else grunt.task.run [ + 'test:server' + 'test:client' + ] + + grunt.registerTask 'build', [ + 'clean:dist' + 'bower-install' + 'useminPrepare' + 'concurrent:dist' + 'autoprefixer' + 'concat' + 'ngmin' + 'copy:dist' + 'cdnify' + 'cssmin' + 'uglify' + 'rev' + 'usemin' + ] + + grunt.registerTask 'heroku', -> + grunt.log.warn 'The `heroku` task has been deprecated. Use `grunt build` to build for deployment.' + grunt.task.run ['build'] + + grunt.registerTask 'default', [ + 'test' + 'build' + ] diff --git a/templates/common/Gruntfile.js b/templates/common/Gruntfile.js index 235f64e90..e2808de97 100644 --- a/templates/common/Gruntfile.js +++ b/templates/common/Gruntfile.js @@ -22,7 +22,8 @@ module.exports = function (grunt) { yeoman: { // configurable paths app: require('./bower.json').appPath || 'app', - dist: 'dist' + dist: 'dist', + lib: 'lib' }, express: { options: { @@ -46,15 +47,7 @@ module.exports = function (grunt) { url: 'http://localhost:<%%= express.options.port %>' } }, - watch: {<% if (coffee) { %> - coffee: { - files: ['<%%= yeoman.app %>/scripts/{,*/}*.{coffee,litcoffee,coffee.md}'], - tasks: ['newer:coffee:dist'] - }, - coffeeTest: { - files: ['test/spec/{,*/}*.{coffee,litcoffee,coffee.md}'], - tasks: ['newer:coffee:test', 'karma'] - },<% } else { %> + watch: { js: { files: ['<%%= yeoman.app %>/scripts/{,*/}*.js'], tasks: ['newer:jshint:all'], @@ -69,7 +62,7 @@ module.exports = function (grunt) { jsTest: { files: ['test/client/spec/{,*/}*.js'], tasks: ['newer:jshint:test', 'karma'] - },<% } %><% if (compass) { %> + },<% if (compass) { %> compass: { files: ['<%%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], tasks: ['compass:server', 'autoprefixer'] @@ -118,15 +111,12 @@ module.exports = function (grunt) { }, src: [ 'lib/{,*/}*.js'] }, - all: [<% if (!coffee) { %> - '<%%= yeoman.app %>/scripts/{,*/}*.js'<% } %> - ]<% if (!coffee) { %>, test: { options: { jshintrc: 'test/client/.jshintrc' }, src: ['test/client/spec/{,*/}*.js'] - }<% } %> + } }, // Empties folders to start fresh @@ -212,33 +202,7 @@ module.exports = function (grunt) { ignorePath: '<%%= yeoman.app %>/'<% if (compass && bootstrap) { %>, exclude: ['bootstrap-sass']<% } %> } - },<% if (coffee) { %> - - // Compiles CoffeeScript to JavaScript - coffee: { - options: { - sourceMap: true, - sourceRoot: '' - }, - dist: { - files: [{ - expand: true, - cwd: '<%%= yeoman.app %>/scripts', - src: '{,*/}*.coffee', - dest: '.tmp/scripts', - ext: '.js' - }] - }, - test: { - files: [{ - expand: true, - cwd: 'test/client/spec', - src: '{,*/}*.coffee', - dest: '.tmp/client/spec', - ext: '.js' - }] - } - },<% } %><% if (compass) { %> + },<% if (compass) { %> // Compiles Sass to CSS and generates necessary files if requested compass: { @@ -413,13 +377,11 @@ module.exports = function (grunt) { // Run some tasks in parallel to speed up the build process concurrent: { - server: [<% if (coffee) { %> - 'coffee:dist',<% } %><% if (compass) { %> + server: [<% if (compass) { %> 'compass:server'<% } else { %> 'copy:styles'<% } %> ], - test: [<% if (coffee) { %> - 'coffee',<% } %><% if (compass) { %> + test: [<% if (compass) { %> 'compass'<% } else { %> 'copy:styles'<% } %> ], @@ -432,8 +394,7 @@ module.exports = function (grunt) { logConcurrentOutput: true } }, - dist: [<% if (coffee) { %> - 'coffee',<% } %><% if (compass) { %> + dist: [<% if (compass) { %> 'compass:dist',<% } else { %> 'copy:styles',<% } %> 'imagemin', diff --git a/templates/common/_package.json b/templates/common/_package.json index 9694565b2..337ed3c58 100644 --- a/templates/common/_package.json +++ b/templates/common/_package.json @@ -66,7 +66,8 @@ "grunt-env": "~0.4.1", "grunt-node-inspector": "~0.1.3", "grunt-nodemon": "~0.2.0", - "open": "~0.0.4" + "open": "~0.0.4"<% if (coffee) { %>, + "coffee-script": "^1.7.1"<% } %> }, "engines": { "node": ">=0.10.0" diff --git a/templates/express-coffeescript/config/config.coffee b/templates/express-coffeescript/config/config.coffee new file mode 100644 index 000000000..ccbf17d4c --- /dev/null +++ b/templates/express-coffeescript/config/config.coffee @@ -0,0 +1,8 @@ +'use strict' + +_ = require 'lodash' + +### + Load environment configuration +### +module.exports = _.merge require('./env/all'), (require("./env/#{process.env.NODE_ENV}") ? {}) \ No newline at end of file diff --git a/templates/express-coffeescript/config/dummydata.coffee b/templates/express-coffeescript/config/dummydata.coffee new file mode 100644 index 000000000..dd4c3fd60 --- /dev/null +++ b/templates/express-coffeescript/config/dummydata.coffee @@ -0,0 +1,46 @@ +'use strict' + +mongoose = require 'mongoose'<% if(mongo && mongoPassportUser) { %> +User = mongoose.model 'User'<% } %> +Thing = mongoose.model 'Thing' + +### + Populate database with sample application data +### + +# Clear old things, then add things in +Thing.find({}).remove -> + Thing.create + name : 'HTML5 Boilerplate' + info : 'HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.' + awesomeness: 10 + , + name : 'AngularJS' + info : 'AngularJS is a toolset for building the framework most suited to your application development.' + awesomeness: 10 + , + name : 'Karma' + info : 'Spectacular Test Runner for JavaScript.' + awesomeness: 10 + , + name : 'Express' + info : 'Flexible and minimalist web application framework for node.js.' + awesomeness: 10 + , + name : 'MongoDB + Mongoose' + info : 'An excellent document database. Combined with Mongoose to simplify adding validation and business logic.' + awesomeness: 10 + , -> + console.log 'finished populating things' + +<% if(mongo && mongoPassportUser) { %> +# Clear old users, then add a default user +User.find({}).remove -> + User.create + provider: 'local' + name: 'Test User' + email: 'test@test.com' + password: 'test' + , -> + console.log 'finished populating users' +<% } %> \ No newline at end of file diff --git a/templates/express-coffeescript/config/env/all.coffee b/templates/express-coffeescript/config/env/all.coffee new file mode 100644 index 000000000..071efc2d5 --- /dev/null +++ b/templates/express-coffeescript/config/env/all.coffee @@ -0,0 +1,15 @@ +'use strict' + +path = require 'path' + +rootPath = "#{path.normalize(__dirname)}/../../.." + +module.exports = + root: rootPath + ip: '0.0.0.0' + port: process.env.PORT || 9000<% if (mongo) { %> + mongo: + options: + db: + safe: true + <% } %> diff --git a/templates/express-coffeescript/config/env/development.coffee b/templates/express-coffeescript/config/env/development.coffee new file mode 100644 index 000000000..84863e371 --- /dev/null +++ b/templates/express-coffeescript/config/env/development.coffee @@ -0,0 +1,7 @@ +'use strict' + +module.exports = + env: 'development'<% if (mongo) { %> + mongo: + uri: 'mongodb://localhost/fullstack-dev' + <% } %> diff --git a/templates/express-coffeescript/config/env/production.coffee b/templates/express-coffeescript/config/env/production.coffee new file mode 100644 index 000000000..0af6281a6 --- /dev/null +++ b/templates/express-coffeescript/config/env/production.coffee @@ -0,0 +1,16 @@ +'use strict' + +module.exports = + env: 'production' + ip: process.env.OPENSHIFT_NODEJS_IP ? + process.env.IP ? + '0.0.0.0' + port: process.env.OPENSHIFT_NODEJS_PORT ? + process.env.PORT ? + 8080<% if (mongo) { %> + mongo: + uri: process.env.MONGOLAB_URI ? + process.env.MONGOHQ_URL ? + process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME ? + 'mongodb://localhost/fullstack' + <% } %> diff --git a/templates/express-coffeescript/config/env/test.coffee b/templates/express-coffeescript/config/env/test.coffee new file mode 100644 index 000000000..65e2308c7 --- /dev/null +++ b/templates/express-coffeescript/config/env/test.coffee @@ -0,0 +1,7 @@ +'use strict' + +module.exports = + env: 'test'<% if (mongo) { %> + mongo: + uri: 'mongodb://localhost/fullstack-test' + <% } %> diff --git a/templates/express-coffeescript/config/express.coffee b/templates/express-coffeescript/config/express.coffee new file mode 100644 index 000000000..395791b53 --- /dev/null +++ b/templates/express-coffeescript/config/express.coffee @@ -0,0 +1,69 @@ +'use strict' + +express = require 'express' +favicon = require 'static-favicon' +morgan = require 'morgan' +compression = require 'compression' +bodyParser = require 'body-parser' +methodOverride = require 'method-override' +cookieParser = require 'cookie-parser' +session = require 'express-session' +errorHandler = require 'errorhandler' +path = require 'path' +config = require './config'<% if (mongoPassportUser) { %> +passport = require 'passport' +mongoStore = require('connect-mongo')(session)<% } %> + +### + Express configuration +### +module.exports = (app) -> + env = app.get 'env' + + if env is 'development' + app.use require('connect-livereload')() + + # Disable caching of scripts for easier testing + app.use (req, res, next) -> + if req.url.indexOf('/scripts/') is 0 + res.header 'Cache-Control', 'no-cache, no-store, must-revalidate' + res.header 'Pragma', 'no-cache' + res.header 'Expires', 0 + + next() + + app.use express.static(path.join(config.root, '.tmp')) + app.use express.static(path.join(config.root, 'app')) + app.set 'views', path.join(config.root, 'app', 'views') + + if env is 'production' + app.use compression() + app.use favicon(path.join(config.root, 'public', 'favicon.ico')) + app.use express.static(path.join(config.root, 'public')) + app.set 'views', path.join(config.root, 'views') + +<% if (!jade) { %> + app.engine 'html', require('ejs').renderFile + app.set 'view engine', 'html'<% } %><% if (jade) { %> + app.set 'view engine', 'jade'<% } %> + app.use morgan('dev') + app.use bodyParser() + app.use methodOverride()<% if(mongoPassportUser) { %> + app.use cookieParser() + + # Persist sessions with mongoStore + app.use session + secret: 'angular-fullstack secret' + store: new mongoStore + url: config.mongo.uri + collection: 'sessions' + , -> + console.log 'db connection open' + + # Use passport session + app.use passport.initialize() + app.use passport.session()<% } %> + + # Error handler - has to be last + if env is 'development' + app.use errorHandler() diff --git a/templates/express-coffeescript/config/passport.coffee b/templates/express-coffeescript/config/passport.coffee new file mode 100644 index 000000000..a4746357c --- /dev/null +++ b/templates/express-coffeescript/config/passport.coffee @@ -0,0 +1,37 @@ +'use strict' + +mongoose = require 'mongoose' +User = mongoose.model 'User' +passport = require 'passport' +LocalStrategy = require('passport-local').Strategy + +### + Passport configuration +### +passport.serializeUser (user, done) -> + done null, user.id +passport.deserializeUser (id, done) -> + User.findOne + _id: id + , '-salt -hashedPassword', (err, user) -> # don't ever give out the password or salt + done err, user + +# add other strategies for more authentication flexibility +passport.use new LocalStrategy + usernameField: 'email' + passwordField: 'password' # this is the virtual field on the model + , (email, password, done) -> + User.findOne + email: email.toLowerCase() + , (err, user) -> + return done(err) if err + + unless user + return done null, false, message: 'This email is not registered.' + + unless user.authenticate password + return done null, false, message: 'This password is not correct.' + + done null, user + +module.exports = passport diff --git a/templates/express-coffeescript/controllers/api.coffee b/templates/express-coffeescript/controllers/api.coffee new file mode 100644 index 000000000..37117606d --- /dev/null +++ b/templates/express-coffeescript/controllers/api.coffee @@ -0,0 +1,30 @@ +'use strict' +<% if (mongo) { %> +mongoose = require 'mongoose' +Thing = mongoose.model 'Thing' +<% } %> +### + Get awesome things +### +exports.awesomeThings = (req, res) -><% if (mongo) { %> + Thing.find (err, things) -> + return res.send(err) if err + res.json things + <% } %><% if (!mongo) { %> + res.json [ + name : 'HTML5 Boilerplate' + info : 'HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.' + awesomeness: 10 + , + name : 'AngularJS' + info : 'AngularJS is a toolset for building the framework most suited to your application development.' + awesomeness: 10 + , + name : 'Karma' + info : 'Spectacular Test Runner for JavaScript.' + awesomeness: 10 + , + name : 'Express' + info : 'Flexible and minimalist web application framework for node.js.' + awesomeness: 10 + ]<% } %> diff --git a/templates/express-coffeescript/controllers/index.coffee b/templates/express-coffeescript/controllers/index.coffee new file mode 100644 index 000000000..89e9bb765 --- /dev/null +++ b/templates/express-coffeescript/controllers/index.coffee @@ -0,0 +1,22 @@ +'use strict' + +path = require 'path' + +### + Send partial, or 404 if it doesn't exist +### +exports.partials = (req, res) -> + stripped = req.url.split('.')[0] + requestedView = path.join './', stripped + res.render requestedView, (err, html) -> + if err + console.log "Error rendering partial '#{requestedView}':\n", err + return res.send 404 + + res.send html + +### + Send our single page app +### +exports.index = (req, res) -> + res.render 'index' diff --git a/templates/express-coffeescript/controllers/session.coffee b/templates/express-coffeescript/controllers/session.coffee new file mode 100644 index 000000000..2a8060bd4 --- /dev/null +++ b/templates/express-coffeescript/controllers/session.coffee @@ -0,0 +1,24 @@ +'use strict' + +mongoose = require 'mongoose' +passport = require 'passport' + +### + Logout +### +exports.logout = (req, res) -> + req.logout() + res.send 200 + +### + Login +### +exports.login = (req, res, next) -> + passport.authenticate('local', (err, user, info) -> + error = err ? info + return res.json(401, error) if error + + req.logIn user, (err) -> + return res.send(err) if (err) + res.json req.user.userInfo + )(req, res, next) diff --git a/templates/express-coffeescript/controllers/users.coffee b/templates/express-coffeescript/controllers/users.coffee new file mode 100644 index 000000000..f4062fccf --- /dev/null +++ b/templates/express-coffeescript/controllers/users.coffee @@ -0,0 +1,52 @@ +'use strict'; + +mongoose = require 'mongoose' +User = mongoose.model 'User' +passport = require 'passport' + +### + Create user +### +exports.create = (req, res, next) -> + newUser = new User req.body + newUser.provider = 'local' + newUser.save (err) -> + return res.json(400, err) if err + + req.logIn newUser, (err) -> + return next(err) if (err) + res.json req.user.userInfo + +### + Get profile of specified user +### +exports.show = (req, res, next) -> + userId = req.params.id + + User.findById userId, (err, user) -> + return next(err) if err + return res.send(404) unless user + + res.json profile: user.profile + +### + Change password +### +exports.changePassword = (req, res, next) -> + userId = req.user._id + oldPass = String req.body.oldPassword + newPass = String req.body.newPassword + + User.findById userId, (err, user) -> + return res.send(403) unless user.authenticate(oldPass) + + user.password = newPass + user.save (err) -> + return res.send(400) if err + res.send 200 + +### + Get current user +### +exports.me = (req, res) -> + res.json req.user ? null diff --git a/templates/express-coffeescript/middleware.coffee b/templates/express-coffeescript/middleware.coffee new file mode 100644 index 000000000..8f8d3e8a6 --- /dev/null +++ b/templates/express-coffeescript/middleware.coffee @@ -0,0 +1,22 @@ +'use strict' + +### + Custom middleware used by the application +### +module.exports = + + ### + Protect routes on your api from unauthenticated access + ### + auth: (req, res, next) -> + return res.send(401) unless req.isAuthenticated() + next() + + ### + Set a cookie for angular so it knows we have an http session + ### + setUserCookie: (req, res, next) -> + if req.user + res.cookie 'user', JSON.stringify(req.user.userInfo) + + next() diff --git a/templates/express-coffeescript/models/thing.coffee b/templates/express-coffeescript/models/thing.coffee new file mode 100644 index 000000000..90abe1c3d --- /dev/null +++ b/templates/express-coffeescript/models/thing.coffee @@ -0,0 +1,21 @@ +'use strict' + +mongoose = require 'mongoose' +Schema = mongoose.Schema + +### + Thing Schema +### +ThingSchema = new Schema + name: String + info: String + awesomeness: Number + +### + Validations +### +ThingSchema.path('awesomeness').validate (num) -> + num >= 1 && num <= 10; +, 'Awesomeness must be between 1 and 10' + +mongoose.model 'Thing', ThingSchema diff --git a/templates/express-coffeescript/models/user.coffee b/templates/express-coffeescript/models/user.coffee new file mode 100644 index 000000000..2e430c19c --- /dev/null +++ b/templates/express-coffeescript/models/user.coffee @@ -0,0 +1,138 @@ +'use strict' + +mongoose = require 'mongoose' +Schema = mongoose.Schema +crypto = require 'crypto' + +authTypes = ['github', 'twitter', 'facebook', 'google'] + +### + User Schema +### +UserSchema = new Schema + name: String + email: + type: String + lowercase: true + role: + type: String + default: 'user' + hashedPassword: String + provider: String + salt: String + facebook: {} + twitter: {} + github: {} + google: {} + +### + Virtuals +### +UserSchema + .virtual('password') + .set (password) -> + @_password = password + @salt = @makeSalt() + @hashedPassword = @encryptPassword password + .get -> + @_password + +# Basic info to identify the current authenticated user in the app +UserSchema + .virtual('userInfo') + .get -> + name: @name + role: @role + provider: @provider + +# Public profile information +UserSchema + .virtual('profile') + .get -> + name: @name + role: @role + +### + Validations +### + +# Validate empty email +UserSchema + .path('email') + .validate (email) -> + # if you are authenticating by any of the oauth strategies, don't validate + return true if (authTypes.indexOf(@provider) isnt -1) + email.length + , 'Email cannot be blank' + +# Validate empty password +UserSchema + .path('hashedPassword') + .validate (hashedPassword) -> + # if you are authenticating by any of the oauth strategies, don't validate + return true if (authTypes.indexOf(@provider) isnt -1) + hashedPassword.length + , 'Password cannot be blank' + +# Validate email is not taken +UserSchema + .path('email') + .validate (value, respond) -> + @constructor.findOne email: value, (err, user) => + throw err if err + return respond(true) unless user + + respond(@id is user.id) + , 'The specified email address is already in use.' + +validatePresenceOf = (value) -> + value && value.length + +### + Pre-save hook +### +UserSchema + .pre 'save', (next) -> + return next() unless @isNew + + if !validatePresenceOf(@hashedPassword) and authTypes.indexOf(@provider) is -1 + next new Error('Invalid password') + else + next() + +### + Methods +### +UserSchema.methods = + ### + Authenticate - check if the passwords are the same + + @param {String} plainText + @return {Boolean} + @api public + ### + authenticate: (plainText) -> + @encryptPassword(plainText) is @hashedPassword + + ### + Make salt + + @return {String} + @api public + ### + makeSalt: () -> + crypto.randomBytes(16).toString 'base64' + + ### + Encrypt password + + @param {String} password + @return {String} + @api public + ### + encryptPassword: (password) -> + return '' unless password and @salt + salt = new Buffer @salt, 'base64' + crypto.pbkdf2Sync(password, salt, 10000, 64).toString 'base64' + +module.exports = mongoose.model 'User', UserSchema diff --git a/templates/express-coffeescript/routes.coffee b/templates/express-coffeescript/routes.coffee new file mode 100644 index 000000000..b7b0c5aa5 --- /dev/null +++ b/templates/express-coffeescript/routes.coffee @@ -0,0 +1,39 @@ +'use strict' + +api = require './controllers/api' +index = require './controllers'<% if(mongoPassportUser) { %> +users = require './controllers/users' +session = require './controllers/session' +middleware = require './middleware'<% } %> + +### + Application routes +### +module.exports = (app) -> + + # Server API Routes + app.route('/api/awesomeThings') + .get(api.awesomeThings) + <% if(mongoPassportUser) { %> + app.route('/api/users') + .post(users.create) + .put(users.changePassword) + app.route('/api/users/me') + .get(users.me) + app.route('/api/users/:id') + .get(users.show) + + app.route('/api/session') + .post(session.login) + .delete(session.logout)<% } %> + + # All undefined api routes should return a 404 + app.route('/api/*') + .get (req, res) -> + res.send 404 + + # All other routes to use Angular routing in app/scripts/app.js + app.route('/partials/*') + .get(index.partials) + app.route('/*') + .get(<% if(mongoPassportUser) { %> middleware.setUserCookie,<% } %> index.index) diff --git a/templates/express-coffeescript/server.coffee b/templates/express-coffeescript/server.coffee new file mode 100644 index 000000000..f45db2fcb --- /dev/null +++ b/templates/express-coffeescript/server.coffee @@ -0,0 +1,40 @@ +'use strict' + +express = require 'express'<% if (mongo) { %> +path = require 'path' +fs = require 'fs' +mongoose = require 'mongoose'<% } %> + +### + Main application file +### + +# Set default node environment to development +process.env.NODE_ENV = process.env.NODE_ENV ? 'development' + +config = require './lib/config/config'<% if (mongo) { %> +db = mongoose.connect config.mongo.uri, config.mongo.options + +# Bootstrap models +modelsPath = path.join __dirname, 'lib/models' +fs.readdirSync(modelsPath).forEach (file) -> + if /(.*)\.(js$|coffee$)/.test file + require modelsPath + '/' + file + +# Populate empty DB with sample data +require './lib/config/dummydata'<% } %><% if(mongoPassportUser) { %> + +# Passport Configuration +passport = require './lib/config/passport'<% } %> + +# Setup Express +app = express() +require('./lib/config/express') app +require('./lib/routes') app + +# Start server +app.listen config.port, config.ip, -> + console.log 'Express server listening on %s:%d, in %s mode', config.ip, config.port, app.get('env') + +# Expose app +exports = module.exports = app diff --git a/templates/express-coffeescript/test/thing/api.coffee b/templates/express-coffeescript/test/thing/api.coffee new file mode 100644 index 000000000..6af9f2c57 --- /dev/null +++ b/templates/express-coffeescript/test/thing/api.coffee @@ -0,0 +1,17 @@ +'use strict' + +should = require 'should' +app = require '../../../server' +request = require 'supertest' + +describe 'GET /api/awesomeThings', -> + + it 'should respond with JSON array', (done) -> + request(app) + .get('/api/awesomeThings') + .expect(200) + .expect('Content-Type', /json/) + .end (err, res) -> + return done(err) if err + res.body.should.be.instanceof Array + done() diff --git a/templates/express-coffeescript/test/user/model.coffee b/templates/express-coffeescript/test/user/model.coffee new file mode 100644 index 000000000..e29595a94 --- /dev/null +++ b/templates/express-coffeescript/test/user/model.coffee @@ -0,0 +1,43 @@ +'use strict' + +should = require 'should' +mongoose = require 'mongoose' +User = mongoose.model 'User' + +describe 'User Model', -> + before (done) -> + @user = new User + provider: 'local' + name: 'Fake User' + email: 'test@test.com' + password: 'password' + + # Clear users before testing + User.remove().exec done + + afterEach (done) -> + User.remove().exec done + + it 'should begin with no users', (done) -> + User.find {}, (err, users) -> + users.should.have.length 0 + done() + + it 'should fail when saving a duplicate user', (done) -> + @user.save() + userDup = new User @user + userDup.save (err) -> + should.exist err + done() + + it 'should fail when saving without an email', (done) -> + @user.email = '' + @user.save (err) -> + should.exist err + done() + + it 'should authenticate user if password is valid', -> + @user.authenticate('password').should.be.true + + it 'should not authenticate user if password is invalid', -> + @user.authenticate('blah').should.not.be.true