├── .gitignore ├── README.md ├── bin └── frodo ├── lib ├── config.js ├── files.js ├── index.js ├── pluralize.js ├── routes.js └── templates │ ├── .gitignore │ ├── dynamic │ ├── controller │ └── model │ ├── index.js │ └── static │ ├── app │ ├── assets │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ └── welcome_controller.js │ └── views │ │ ├── layouts │ │ └── application.pug │ │ └── welcome │ │ └── index.pug │ ├── config │ ├── application.js │ ├── database.js │ ├── globals.js │ └── routes.js │ ├── db │ ├── connection.js │ └── util.js │ └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frodo 2 | 3 | Frodo is a Rails-like app generator for Node and Express. It was created to simplify working 4 | with projects based on the Express framework. Nowadays, many developers are familiar with the Ruby on Rails 5 | framework, which helps you generate a structure for your app. An app generated with Frodo will help you in the same way. 6 | 7 | **Motivation:** 8 | * I use Rails a lot and I love how Rails apps are organized 9 | * It can be difficult to start with a micro-framework like Express without a predefined structure. 10 | Frodo will help developers using Node/Express to create a new app as fast as possible. 11 | 12 | 13 | ## Documentation: 14 | 15 | #### Installation 16 | 17 | ```shell 18 | $ npm install -g frodojs 19 | ``` 20 | 21 | Done. Now you can easily invoke the Frodo command line tool. 22 | 23 | #### Usage: 24 | Frodo has a command line interface like Rails, but, for now, Frodo has support for a very limited set of actions. You are able to: 25 | ``` 26 | - create a new application 27 | - generate controller/model or scaffold 28 | ``` 29 | 30 | To create a new application use 'new' followed by application name: 31 | ```shell 32 | $ frodo new blog 33 | ``` 34 | 35 | This command will create a new project with a predefined folders structure. 36 | 37 | If you want to build an API, and you don't need views or static files, use the `--skipViews` optional argument. 38 | ```shell 39 | $ frodo new api --skipViews 40 | ``` 41 | 42 | **Generators** 43 | 44 | Like in Rails, Frodo can create a controller, a model, or a combination of both with a scaffold. 45 | 46 | Generating a controller 47 | ```shell 48 | $ frodo generate controller user [methods] 49 | ``` 50 | This command creates an empty controller in the app/controllers folder, which 51 | should look like this: 52 | ```javascript 53 | // users_controller.js 54 | var UsersController = (function () { 55 | return { 56 | } 57 | }()); 58 | 59 | module.exports = UsersController; 60 | ``` 61 | and corresponding static files users.css and users.js in app/assests/stylesheets and app/assets/javascripts respectively. 62 | 63 | You can also write a list of methods separated by whitespaces. 64 | ```shell 65 | $ frodo generate controller user new show 66 | ``` 67 | this produces: 68 | ```javascript 69 | // users_controller.js 70 | var UsersController = (function () { 71 | return { 72 | new: function (req, res) {}, 73 | show: function (req, res) {} 74 | } 75 | }()); 76 | ``` 77 | and creates corresponding the views. 78 | 79 | Generating a model: 80 | ```shell 81 | $ frodo generate model user name age:number 82 | ``` 83 | produces: 84 | ```javascript 85 | var mongoose = require('mongoose'), 86 | ObjectId = mongoose.Schema.Types.ObjectId, 87 | Mixed = Schema.Types.Mixed; 88 | 89 | var UserSchema = mongoose.Schema({ 90 | name: {type: String, required: true}, 91 | age: {type: Number, required: true}, 92 | created_at: {type: Number, required: true, default: new Date().getTime()}, 93 | updated_at: {type: Number, required: true, default: new Date().getTime()} 94 | }); 95 | 96 | var User = mongoose.model('User', UserSchema); 97 | 98 | module.exports = User; 99 | ``` 100 | 101 | Each property in a model generator may consist of 3 parts separated by a semicolon - 'name:type:required' 102 | ```shell 103 | $ frodo generate model user email:string:true 104 | ``` 105 | where property name is email, type string, and it is required field. Type and required are not mandatory 106 | parts and can be ommited. 107 | 108 | Available types: 109 | * String 110 | * Number 111 | * Date 112 | * Buffer 113 | * Boolean 114 | * Mixed 115 | * Objectid 116 | * Array 117 | 118 | At the moment, Frodo supports only mongoose for defining models. 119 | -------------------------------------------------------------------------------- /bin/frodo: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var frodo = require('../lib/index.js'); -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | appname: null, 3 | 4 | set: function (props) { 5 | for (var key in props) { 6 | this[key] = props[key]; 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /lib/files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * [exports description] 3 | * @type {Object} 4 | */ 5 | 6 | var fs = require('fs'), 7 | os = {path: require('path')}, 8 | config = require('./config'); 9 | 10 | 11 | /** 12 | * 13 | * @param {String} path [description] 14 | * @param {Object} locals 15 | * 16 | * @return {Boolean} 17 | */ 18 | function useTemplate(path, locals) { 19 | var fileName = path.split(config.appname).pop().replace('/', ''), 20 | template = os.path.join(__dirname, 'templates/static', fileName); 21 | 22 | if (fs.existsSync(template)) { 23 | fs.createReadStream(template).pipe(fs.createWriteStream(path)); 24 | return true; 25 | } 26 | }; 27 | 28 | 29 | module.exports = { 30 | createFile: function (path, content) { 31 | console.log('Creating file', path); 32 | 33 | if (useTemplate(path)) { 34 | return; 35 | } 36 | 37 | if (content) { 38 | fs.writeFileSync(path, content); 39 | } else { 40 | file = fs.openSync(path, 'w'); 41 | fs.closeSync(file); 42 | } 43 | }, 44 | 45 | /** 46 | * 47 | * @param {[type]} files [description] 48 | * @param {[type]} path [description] 49 | */ 50 | createFiles: function (files, path) { 51 | var file = null, 52 | self = this; 53 | 54 | for (var i = 0; i < files.length; i++) { 55 | file = files[i]; 56 | self.createFile(os.path.join(path, file)); 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Frodo.js is a app generator for Express-based aplications. 3 | * Easily set structure in a json file and pass path for the file as command line argument 4 | */ 5 | 6 | String.prototype.capitalize = function() { 7 | return this.charAt(0).toUpperCase() + this.slice(1); 8 | } 9 | 10 | var fs = require('fs'), 11 | exec = require('child_process').execSync, 12 | /* path is actively used, so I made Python like name */ 13 | os = {path: require('path')}, 14 | pluralize = require('./pluralize'), 15 | generateRoutes = require('./routes'), 16 | config = require('./config'), 17 | files = require('./files'), 18 | templates = require('./templates'); 19 | 20 | 21 | var $WORKING_DIR = process.cwd(), 22 | $SKIP_VIEWS = false, 23 | $SKIP_ASSETS = false, 24 | $SCAFFOLD = false, 25 | $APP = os.path.join($WORKING_DIR, 'app'), 26 | $ASSETS = os.path.join($APP, 'assets'), 27 | $CONTROLLERS = os.path.join($APP, 'controllers'), 28 | $MODELS = os.path.join($APP, 'models'); 29 | $VIEWS = os.path.join($APP, 'views'), 30 | $VERSION = '0.6.0', 31 | $PREPROCESSORS = { 32 | views: 'pug', 33 | stylesheets: 'css', 34 | javascripts: 'js' 35 | }, 36 | $SCHEMA_TYPES = ['String', 'Number', 'Date', 'Buffer', 'Boolean', 'Mixed', 'ObjectId', 'Array']; 37 | 38 | 39 | var skipViewsItems = ['assets', 'views', 'vendor', 'public']; 40 | 41 | 42 | /** 43 | * Project structure 44 | */ 45 | var skeleton = { 46 | files: ['index.js', '.gitignore'], 47 | app: { 48 | assets: { 49 | images: {files: ['.keep']}, 50 | javascripts: {files: ['application.js']}, 51 | stylesheets: {files: ['application.css']} 52 | }, 53 | controllers: {files: ['welcome_controller.js']}, 54 | models: {files: ['.keep']}, 55 | views: { 56 | layouts: {files: ['application.pug']}, 57 | welcome: {files: ['index.pug']} 58 | }, 59 | helpers: {files: ['.keep']} 60 | }, 61 | bin: {files: ['.keep']}, 62 | config: { 63 | environments: {files: ['development.js', 'test.js', 'production.js']}, 64 | files: ['application.js', 'environment.js', 'database.js', 'routes.js', 'globals.js'] 65 | }, 66 | db: {files: ['.keep', 'connection.js', 'util.js']}, 67 | lib: {files: ['.keep']}, 68 | log: {files: ['.keep']}, 69 | middleware: { 70 | files: ['.keep'] 71 | }, 72 | public: { 73 | files: ['404.pug', '500.pug', 'favicon.ico', 'robots.txt'] 74 | }, 75 | test: { 76 | controllers: {files: ['.keep']}, 77 | models: {files: ['.keep']}, 78 | helpers: {files: ['.keep']} 79 | }, 80 | tmp: {files: ['.keep']}, 81 | vendor: { 82 | javascripts: {files: ['.keep']}, 83 | stylesheets: {files: ['.keep']} 84 | } 85 | }; 86 | 87 | 88 | /** 89 | * Checks if argument is object 90 | * 91 | * @method isObject 92 | * @param {Any} obj value to be checked 93 | * @return Boolean 94 | */ 95 | function isObject(obj) { 96 | return Object.prototype.toString.call(obj) === '[object Object]'; 97 | } 98 | 99 | 100 | /** 101 | * Generates project folders structure 102 | */ 103 | function generateSkeleton(skeleton, path) { 104 | for (key in skeleton) { 105 | if (skeleton.hasOwnProperty(key)) { 106 | if ($SKIP_VIEWS && skipViewsItems.indexOf(key) >= 0) { 107 | continue; 108 | } 109 | 110 | if (isObject(skeleton[key])) { 111 | console.log('Creating folder', os.path.join(path, key)); 112 | fs.mkdirSync(os.path.join(path, key)); 113 | generateSkeleton(skeleton[key], os.path.join(path, key)); 114 | } else if (key === 'files') { 115 | files.createFiles(skeleton.files, path); 116 | } 117 | } 118 | } 119 | } 120 | 121 | 122 | function generatePackageJson(projectPath, appName) { 123 | var packageJson = '{\n'+ 124 | ' "name": "'+appName+'",\n'+ 125 | ' "version": "0.0.1",\n'+ 126 | ' "description": "",\n'+ 127 | ' "main": "index.js",\n'+ 128 | ' "license": "ISC"\n'+ 129 | '}'; 130 | 131 | files.createFile(os.path.join(projectPath, 'package.json'), packageJson) 132 | } 133 | 134 | 135 | function npmInit(projectPath) { 136 | console.log('Installing dependencies'); 137 | process.chdir(projectPath); 138 | console.log('Running: npm install express --save'); 139 | exec('npm install express --save'); 140 | console.log('Running: npm install body-parser --save'); 141 | exec('npm install body-parser --save'); 142 | console.log('Running: npm install mongoose --save'); 143 | exec('npm install mongoose --save'); 144 | console.log('Running: npm install async --save'); 145 | exec('npm install async --save'); 146 | 147 | if (!$SKIP_VIEWS) { 148 | console.log('Running: npm install pug --save'); 149 | exec('npm install pug --save'); 150 | } 151 | } 152 | 153 | 154 | /** 155 | * 156 | */ 157 | function getControllerContents (name, methods) { 158 | return templates.render('dynamic/controller', {methods: methods}); 159 | } 160 | 161 | 162 | /** 163 | * Generates a new controller. If $SKIP_VIEWS is false, generateViews will be executed. 164 | * Views folder name is first argument of the generateController function, views names the same as 165 | * methods. 166 | * 167 | * ! For now routes would not be generated automatically. This feature is on the road map 168 | * 169 | * @method generateController 170 | * @param {String} name controller name 171 | * @param {Array} methods a list of methods to be added to the controller 172 | */ 173 | function generateController(name, methods) { 174 | var pluralName = pluralize(name), 175 | fullName = pluralName+'_controller.js', 176 | contents = getControllerContents(pluralName, methods); 177 | 178 | files.createFile(os.path.join($CONTROLLERS, fullName), contents); 179 | 180 | if (!$SKIP_VIEWS && methods) { 181 | generateViews(pluralName, methods); 182 | } 183 | 184 | if (!$SKIP_ASSETS) { 185 | generateAssets(pluralName); 186 | } 187 | 188 | generateRoutes(pluralName, fullName, methods, $SCAFFOLD); 189 | } 190 | 191 | 192 | /** 193 | * Generates javascript and stylesheet files. 194 | * 195 | * @method generateAssets 196 | * 197 | * @param {String} name name of file as is. 198 | */ 199 | function generateAssets(name) { 200 | var js = os.path.join($ASSETS, 'javascripts', name+'.'+$PREPROCESSORS.javascripts), 201 | css = os.path.join($ASSETS, 'stylesheets', name+'.'+$PREPROCESSORS.stylesheets); 202 | 203 | files.createFile(js); 204 | files.createFile(css); 205 | } 206 | 207 | 208 | function getDefaultViewContent (viewPath) { 209 | return "extends ../layouts/application.pug\n\n" + 210 | "block content\n"+ 211 | " p You can find this view template at "+viewPath; 212 | } 213 | 214 | 215 | /** 216 | * Generates new views 217 | * 218 | * @method generateViews 219 | * 220 | * @param {String} dirName a directory for new views 221 | * @param {Array} methods a list of views to be generated 222 | */ 223 | function generateViews(dirName, views) { 224 | var viewsPath = os.path.join($VIEWS, dirName), 225 | view = null; 226 | 227 | /* If views directory does not exist - skip this step */ 228 | if (!fs.existsSync($VIEWS)) { 229 | return; 230 | } 231 | 232 | if (!fs.existsSync(os.path.join($VIEWS, dirName))) { 233 | fs.mkdirSync(os.path.join($VIEWS, dirName)); 234 | } 235 | 236 | for (var i = 0; i < views.length; i++) { 237 | /* Don't create views in the array if scaffold has been called */ 238 | if ($SCAFFOLD && ['create', 'update', 'delete'].indexOf(views[i]) >= 0 ) { 239 | continue; 240 | } 241 | 242 | view = views[i]+'.'+$PREPROCESSORS.views; 243 | 244 | var content = getDefaultViewContent(os.path.join(viewsPath, view)); 245 | files.createFile(os.path.join(viewsPath, view), content); 246 | } 247 | } 248 | 249 | 250 | /** 251 | * Generates mongoose compatible schema object. 252 | * 253 | * @method createMongooseSchema 254 | * 255 | * @param {Array} props - an array of model properties. Each item is a string 'name:type:required' 256 | * where only 'name' is required part. If type is omitted, then the default 257 | * value (String) will be used. 258 | */ 259 | function createMongooseSchema(options) { 260 | var modelName = options.name, 261 | nameCap = modelName.capitalize(), 262 | schemaName = nameCap+"Schema", 263 | propsRaw = options.props, 264 | props = [], 265 | splitted = null; 266 | 267 | propsRaw.forEach(function (val, i) { 268 | splitted = val.split(':'); 269 | var name = splitted[0], 270 | type = splitted[1], 271 | required = splitted[2]; 272 | 273 | if (splitted.length === 2 && type === 'true') { 274 | type = null; 275 | required = 'true'; 276 | } 277 | 278 | if (!type) { 279 | type = 'String'; 280 | } 281 | 282 | if ($SCHEMA_TYPES.indexOf(type.capitalize()) < 0) { 283 | console.error('Unknown type', type, 'for', name); 284 | process.exit(1); 285 | } 286 | 287 | props.push({name: name, type: type.capitalize(), required: required}); 288 | }); 289 | 290 | return templates.render('dynamic/model', {name: modelName, nameCap: nameCap, 291 | schemaName: schemaName, props: props}); 292 | } 293 | 294 | 295 | /** 296 | * Generates a new model 297 | * 298 | * @method generateModel 299 | * 300 | * @param {String} name model name 301 | * @prop {Array} props an array of properties. A property of collon seperated strings. 302 | * propName:propType:required:defaultValue. Example: login:string:required, 303 | * created_at:date:required:now 304 | * 305 | */ 306 | function generateModel(name, props) { 307 | var schema = createMongooseSchema({name: name, props: props}); 308 | 309 | /* If models directory does not exist - skip this step */ 310 | /* TODO: add logging here */ 311 | if (!fs.existsSync($MODELS)) { 312 | return; 313 | } 314 | 315 | files.createFile(os.path.join($MODELS, name+'.js'), schema); 316 | } 317 | 318 | 319 | /** 320 | * ------ THIS FEATURE NOT AVAILABLE -------- 321 | * Use your favorite preprocessors with frodo generators. In order to configure preprocessors 322 | * add module.exports.preprocessors into config/application. If preprocessors are not set frodo 323 | * will use default values: jade for views, css for stylesheets, js for scripts. If any value is 324 | * missed, frodo will use default for this as well. 325 | * 326 | * /!\ These settigns do not set your assets pipeline. Such a feature is on the roadmap. Don't forget 327 | * to set preprocessing of your assets. 328 | * 329 | * Example ('config/application.js'): 330 | * module.exports.preprocessors = { 331 | * views: 'jade', 332 | * stylesheets: 'scss', 333 | * javascripts: 'coffee' 334 | * } 335 | */ 336 | function setPreprocessors() { 337 | var preprocessors = null; 338 | 339 | try { 340 | preprocessors = require(os.path.join($WORKING_DIR, 'config/application')).preprocessors; 341 | } catch (e) { 342 | // console.log("Can't find config/application.js"); 343 | } 344 | 345 | if (!preprocessors) { 346 | return; 347 | } else { 348 | if (preprocessors.views) { 349 | $PREPROCESSORS.views = preprocessors.views; 350 | } 351 | 352 | if (preprocessors.stylesheets) { 353 | $PREPROCESSORS.stylesheets = preprocessors.stylesheets; 354 | } 355 | 356 | if (preprocessors.javascripts) { 357 | $PREPROCESSORS.javascripts = preprocessors.javascripts; 358 | } 359 | } 360 | } 361 | 362 | 363 | /** 364 | * Parses command line arguments and if they are correct, call required action. 365 | * 366 | * @method main 367 | */ 368 | function main () { 369 | var argv = process.argv; 370 | 371 | if (!fs.existsSync($ASSETS)) { 372 | $SKIP_ASSETS = true; 373 | } 374 | 375 | setPreprocessors(); 376 | 377 | if (argv.length === 2) { 378 | console.log('too few arguments'); 379 | return; 380 | } else if (argv[2] === 'new') { 381 | if (argv.length === 3) { 382 | console.log('Usage: frodo new project_name. Example: frodo new blog'); 383 | } else { 384 | config.set({appname: argv[3]}); 385 | var projectPath = os.path.join($WORKING_DIR, argv[3]); 386 | fs.mkdirSync(projectPath); 387 | 388 | if (argv.indexOf('--skipViews') >= 0) { 389 | $SKIP_VIEWS = true; 390 | } 391 | 392 | generateSkeleton(skeleton, projectPath); 393 | generatePackageJson(projectPath, argv[3]); 394 | npmInit(projectPath); 395 | } 396 | } else if (argv[2] === 'generate') { 397 | config.set({appname: process.cwd().split('/').pop()}); 398 | 399 | if (argv.length < 5) { 400 | console.log('Usage: frodo generate controller controller_name.'+ 401 | 'Example: frodo generate controller users'); 402 | return; 403 | } 404 | if (argv[3] === 'controller') { 405 | generateController(argv[4], argv.slice(5)); 406 | } else if (argv[3] === 'model') { 407 | generateModel(argv[4], argv.slice(5)); 408 | } else if (argv[3] === 'scaffold') { 409 | $SCAFFOLD = true; 410 | generateController(argv[4], ['index', 'show', 'new', 'edit', 'create', 'update', 'delete']); 411 | generateModel(argv[4], argv.slice(5)); 412 | } 413 | } else if (argv[2] === 'server') { 414 | try { 415 | exec('node index.js', {stdio: 'inherit'}); 416 | } catch (e) { 417 | /* TODO: add logging */ 418 | return; 419 | } 420 | } else { 421 | console.log('=> Unknown command "' + argv[2] + '"'); 422 | } 423 | } 424 | 425 | main(); 426 | -------------------------------------------------------------------------------- /lib/pluralize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a pluralized or singularized word based on the input string. 3 | * Author: Blake Embrey 4 | * Repository: https://github.com/blakeembrey/pluralize 5 | */ 6 | 7 | /* global define */ 8 | 9 | (function (root, pluralize) { 10 | /* istanbul ignore else */ 11 | if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { 12 | // Node. 13 | module.exports = pluralize() 14 | } else if (typeof define === 'function' && define.amd) { 15 | // AMD, registers as an anonymous module. 16 | define(function () { 17 | return pluralize() 18 | }) 19 | } else { 20 | // Browser global. 21 | root.pluralize = pluralize() 22 | } 23 | })(this, function () { 24 | // Rule storage - pluralize and singularize need to be run sequentially, 25 | // while other rules can be optimized using an object for instant lookups. 26 | var pluralRules = [] 27 | var singularRules = [] 28 | var uncountables = {} 29 | var irregularPlurals = {} 30 | var irregularSingles = {} 31 | 32 | /** 33 | * Title case a string. 34 | * 35 | * @param {string} str 36 | * @return {string} 37 | */ 38 | function toTitleCase (str) { 39 | return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase() 40 | } 41 | 42 | /** 43 | * Sanitize a pluralization rule to a usable regular expression. 44 | * 45 | * @param {(RegExp|string)} rule 46 | * @return {RegExp} 47 | */ 48 | function sanitizeRule (rule) { 49 | if (typeof rule === 'string') { 50 | return new RegExp('^' + rule + '$', 'i') 51 | } 52 | 53 | return rule 54 | } 55 | 56 | /** 57 | * Pass in a word token to produce a function that can replicate the case on 58 | * another word. 59 | * 60 | * @param {string} word 61 | * @param {string} token 62 | * @return {Function} 63 | */ 64 | function restoreCase (word, token) { 65 | // Upper cased words. E.g. "HELLO". 66 | if (word === word.toUpperCase()) { 67 | return token.toUpperCase() 68 | } 69 | 70 | // Title cased words. E.g. "Title". 71 | if (word[0] === word[0].toUpperCase()) { 72 | return toTitleCase(token) 73 | } 74 | 75 | // Lower cased words. E.g. "test". 76 | return token.toLowerCase() 77 | } 78 | 79 | /** 80 | * Interpolate a regexp string. 81 | * 82 | * @param {string} str 83 | * @param {Array} args 84 | * @return {string} 85 | */ 86 | function interpolate (str, args) { 87 | return str.replace(/\$(\d{1,2})/g, function (match, index) { 88 | return args[index] || '' 89 | }) 90 | } 91 | 92 | /** 93 | * Sanitize a word by passing in the word and sanitization rules. 94 | * 95 | * @param {String} word 96 | * @param {Array} collection 97 | * @return {String} 98 | */ 99 | function sanitizeWord (word, collection) { 100 | // Empty string or doesn't need fixing. 101 | if (!word.length || uncountables.hasOwnProperty(word)) { 102 | return word 103 | } 104 | 105 | var len = collection.length 106 | 107 | // Iterate over the sanitization rules and use the first one to match. 108 | while (len--) { 109 | var rule = collection[len] 110 | 111 | // If the rule passes, return the replacement. 112 | if (rule[0].test(word)) { 113 | return word.replace(rule[0], function (match, index, word) { 114 | var result = interpolate(rule[1], arguments) 115 | 116 | if (match === '') { 117 | return restoreCase(word[index - 1], result) 118 | } 119 | 120 | return restoreCase(match, result) 121 | }) 122 | } 123 | } 124 | 125 | return word 126 | } 127 | 128 | /** 129 | * Replace a word with the updated word. 130 | * 131 | * @param {Object} replaceMap 132 | * @param {Object} keepMap 133 | * @param {Array} rules 134 | * @return {Function} 135 | */ 136 | function replaceWord (replaceMap, keepMap, rules) { 137 | return function (word) { 138 | // Get the correct token and case restoration functions. 139 | var token = word.toLowerCase() 140 | 141 | // Check against the keep object map. 142 | if (keepMap.hasOwnProperty(token)) { 143 | return restoreCase(word, token) 144 | } 145 | 146 | // Check against the replacement map for a direct word replacement. 147 | if (replaceMap.hasOwnProperty(token)) { 148 | return restoreCase(word, replaceMap[token]) 149 | } 150 | 151 | // Run all the rules against the word. 152 | return sanitizeWord(word, rules) 153 | } 154 | } 155 | 156 | /** 157 | * Pluralize or singularize a word based on the passed in count. 158 | * 159 | * @param {String} word 160 | * @param {Number} count 161 | * @param {Boolean} inclusive 162 | * @return {String} 163 | */ 164 | function pluralize (word, count, inclusive) { 165 | var pluralized = count === 1 ? 166 | pluralize.singular(word) : pluralize.plural(word) 167 | 168 | return (inclusive ? count + ' ' : '') + pluralized 169 | } 170 | 171 | /** 172 | * Pluralize a word. 173 | * 174 | * @type {Function} 175 | */ 176 | pluralize.plural = replaceWord( 177 | irregularSingles, irregularPlurals, pluralRules 178 | ) 179 | 180 | /** 181 | * Singularize a word. 182 | * 183 | * @type {Function} 184 | */ 185 | pluralize.singular = replaceWord( 186 | irregularPlurals, irregularSingles, singularRules 187 | ) 188 | 189 | /** 190 | * Add a pluralization rule to the collection. 191 | * 192 | * @param {(string|RegExp)} rule 193 | * @param {string} replacement 194 | */ 195 | pluralize.addPluralRule = function (rule, replacement) { 196 | pluralRules.push([sanitizeRule(rule), replacement]) 197 | } 198 | 199 | /** 200 | * Add a singularization rule to the collection. 201 | * 202 | * @param {(string|RegExp)} rule 203 | * @param {string} replacement 204 | */ 205 | pluralize.addSingularRule = function (rule, replacement) { 206 | singularRules.push([sanitizeRule(rule), replacement]) 207 | } 208 | 209 | /** 210 | * Add an uncountable word rule. 211 | * 212 | * @param {(string|RegExp)} word 213 | */ 214 | pluralize.addUncountableRule = function (word) { 215 | if (typeof word === 'string') { 216 | uncountables[word.toLowerCase()] = true 217 | return 218 | } 219 | 220 | // Set singular and plural references for the word. 221 | pluralize.addPluralRule(word, '$0') 222 | pluralize.addSingularRule(word, '$0') 223 | } 224 | 225 | /** 226 | * Add an irregular word definition. 227 | * 228 | * @param {String} single 229 | * @param {String} plural 230 | */ 231 | pluralize.addIrregularRule = function (single, plural) { 232 | plural = plural.toLowerCase() 233 | single = single.toLowerCase() 234 | 235 | irregularSingles[single] = plural 236 | irregularPlurals[plural] = single 237 | } 238 | 239 | /** 240 | * Irregular rules. 241 | */ 242 | ;[ 243 | // Pronouns. 244 | ['I', 'we'], 245 | ['me', 'us'], 246 | ['he', 'they'], 247 | ['she', 'they'], 248 | ['them', 'them'], 249 | ['myself', 'ourselves'], 250 | ['yourself', 'yourselves'], 251 | ['itself', 'themselves'], 252 | ['herself', 'themselves'], 253 | ['himself', 'themselves'], 254 | ['themself', 'themselves'], 255 | ['this', 'these'], 256 | ['that', 'those'], 257 | // Words ending in with a consonant and `o`. 258 | ['echo', 'echoes'], 259 | ['dingo', 'dingoes'], 260 | ['volcano', 'volcanoes'], 261 | ['tornado', 'tornadoes'], 262 | ['torpedo', 'torpedoes'], 263 | // Ends with `us`. 264 | ['genus', 'genera'], 265 | ['viscus', 'viscera'], 266 | // Ends with `ma`. 267 | ['stigma', 'stigmata'], 268 | ['stoma', 'stomata'], 269 | ['dogma', 'dogmata'], 270 | ['lemma', 'lemmata'], 271 | ['schema', 'schemata'], 272 | ['anathema', 'anathemata'], 273 | // Other irregular rules. 274 | ['ox', 'oxen'], 275 | ['axe', 'axes'], 276 | ['die', 'dice'], 277 | ['yes', 'yeses'], 278 | ['foot', 'feet'], 279 | ['eave', 'eaves'], 280 | ['goose', 'geese'], 281 | ['tooth', 'teeth'], 282 | ['quiz', 'quizzes'], 283 | ['human', 'humans'], 284 | ['proof', 'proofs'], 285 | ['carve', 'carves'], 286 | ['valve', 'valves'], 287 | ['thief', 'thieves'], 288 | ['genie', 'genies'], 289 | ['groove', 'grooves'], 290 | ['pickaxe', 'pickaxes'], 291 | ['whiskey', 'whiskies'] 292 | ].forEach(function (rule) { 293 | return pluralize.addIrregularRule(rule[0], rule[1]) 294 | }) 295 | 296 | /** 297 | * Pluralization rules. 298 | */ 299 | ;[ 300 | [/s?$/i, 's'], 301 | [/([^aeiou]ese)$/i, '$1'], 302 | [/(ax|test)is$/i, '$1es'], 303 | [/(alias|[^aou]us|tlas|gas|ris)$/i, '$1es'], 304 | [/(e[mn]u)s?$/i, '$1s'], 305 | [/([^l]ias|[aeiou]las|[emjzr]as|[iu]am)$/i, '$1'], 306 | [/(alumn|syllab|octop|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1i'], 307 | [/(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae'], 308 | [/(seraph|cherub)(?:im)?$/i, '$1im'], 309 | [/(her|at|gr)o$/i, '$1oes'], 310 | [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i, '$1a'], 311 | [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i, '$1a'], 312 | [/sis$/i, 'ses'], 313 | [/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves'], 314 | [/([^aeiouy]|qu)y$/i, '$1ies'], 315 | [/([^ch][ieo][ln])ey$/i, '$1ies'], 316 | [/(x|ch|ss|sh|zz)$/i, '$1es'], 317 | [/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices'], 318 | [/(m|l)(?:ice|ouse)$/i, '$1ice'], 319 | [/(pe)(?:rson|ople)$/i, '$1ople'], 320 | [/(child)(?:ren)?$/i, '$1ren'], 321 | [/eaux$/i, '$0'], 322 | [/m[ae]n$/i, 'men'], 323 | ['thou', 'you'] 324 | ].forEach(function (rule) { 325 | return pluralize.addPluralRule(rule[0], rule[1]) 326 | }) 327 | 328 | /** 329 | * Singularization rules. 330 | */ 331 | ;[ 332 | [/s$/i, ''], 333 | [/(ss)$/i, '$1'], 334 | [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(?:sis|ses)$/i, '$1sis'], 335 | [/(^analy)(?:sis|ses)$/i, '$1sis'], 336 | [/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i, '$1fe'], 337 | [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'], 338 | [/([^aeiouy]|qu)ies$/i, '$1y'], 339 | [/(^[pl]|zomb|^(?:neck)?t|[aeo][lt]|cut)ies$/i, '$1ie'], 340 | [/([^c][eor]n|smil)ies$/i, '$1ey'], 341 | [/(m|l)ice$/i, '$1ouse'], 342 | [/(seraph|cherub)im$/i, '$1'], 343 | [/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|tlas|gas|(?:her|at|gr)o|ris)(?:es)?$/i, '$1'], 344 | [/(e[mn]u)s?$/i, '$1'], 345 | [/(movie|twelve)s$/i, '$1'], 346 | [/(cris|test|diagnos)(?:is|es)$/i, '$1is'], 347 | [/(alumn|syllab|octop|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1us'], 348 | [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i, '$1um'], 349 | [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i, '$1on'], 350 | [/(alumn|alg|vertebr)ae$/i, '$1a'], 351 | [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'], 352 | [/(matr|append)ices$/i, '$1ix'], 353 | [/(pe)(rson|ople)$/i, '$1rson'], 354 | [/(child)ren$/i, '$1'], 355 | [/(eau)x?$/i, '$1'], 356 | [/men$/i, 'man'] 357 | ].forEach(function (rule) { 358 | return pluralize.addSingularRule(rule[0], rule[1]) 359 | }) 360 | 361 | /** 362 | * Uncountable rules. 363 | */ 364 | ;[ 365 | // Singular words with no plurals. 366 | 'advice', 367 | 'agenda', 368 | 'bison', 369 | 'bream', 370 | 'buffalo', 371 | 'carp', 372 | 'chassis', 373 | 'cod', 374 | 'cooperation', 375 | 'corps', 376 | 'digestion', 377 | 'debris', 378 | 'diabetes', 379 | 'energy', 380 | 'equipment', 381 | 'elk', 382 | 'excretion', 383 | 'expertise', 384 | 'flounder', 385 | 'gallows', 386 | 'garbage', 387 | 'graffiti', 388 | 'headquarters', 389 | 'health', 390 | 'herpes', 391 | 'highjinks', 392 | 'homework', 393 | 'information', 394 | 'jeans', 395 | 'justice', 396 | 'kudos', 397 | 'labour', 398 | 'machinery', 399 | 'mackerel', 400 | 'media', 401 | 'mews', 402 | 'moose', 403 | 'news', 404 | 'pike', 405 | 'plankton', 406 | 'pliers', 407 | 'pollution', 408 | 'premises', 409 | 'rain', 410 | 'rice', 411 | 'salmon', 412 | 'scissors', 413 | 'series', 414 | 'sewage', 415 | 'shambles', 416 | 'shrimp', 417 | 'species', 418 | 'staff', 419 | 'swine', 420 | 'trout', 421 | 'tuna', 422 | 'whiting', 423 | 'wildebeest', 424 | 'wildlife', 425 | 'you', 426 | 'hello', 427 | 'auth', 428 | // Regexes. 429 | /pox$/i, // "chickpox", "smallpox" 430 | /ois$/i, 431 | /deer$/i, // "deer", "reindeer" 432 | /fish$/i, // "fish", "blowfish", "angelfish" 433 | /sheep$/i, 434 | /measles$/i, 435 | /[^aeiou]ese$/i // "chinese", "japanese" 436 | ].forEach(pluralize.addUncountableRule) 437 | 438 | return pluralize 439 | }) 440 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * config/routes.js manipulations 3 | */ 4 | 5 | var fs = require('fs'), 6 | path = require('path'); 7 | 8 | var $ROUTES_FILE = path.join(process.cwd(), 'config/routes.js'); 9 | 10 | 11 | function findPlaceForRequire (lines, linesNum) { 12 | var lastRequireLineNum = 0, 13 | line = null; 14 | 15 | for (var i = 0; i < linesNum; i++) { 16 | line = lines[i]; 17 | 18 | if (line.indexOf('controller') >= 0) { 19 | lastRequireLineNum = i; 20 | } 21 | } 22 | 23 | return lastRequireLineNum; 24 | } 25 | 26 | 27 | function addNewRequire (lines, lastRequireLineNum, name, fullName) { 28 | var newLine = " "+name+" = require('../app/controllers/"+ fullName +"');" 29 | lines[lastRequireLineNum] = lines[lastRequireLineNum].replace(";", ","); 30 | lines.splice(lastRequireLineNum+1, 0, newLine); 31 | } 32 | 33 | 34 | function addRoutes (lines, controller, methods) { 35 | var method = null; 36 | 37 | for (var i = 0; i < methods.length; i++) { 38 | method = methods[i]; 39 | lines.push("app.get('/"+controller+"/"+method+"', "+controller+"."+method+");"); 40 | } 41 | } 42 | 43 | 44 | function addScaffoldRoutes (lines, controller) { 45 | lines.push("app.get('/"+controller+"', "+controller+".index);"); 46 | lines.push("app.get('/"+controller+"/new', "+controller+".new);"); 47 | lines.push("app.post('/"+controller+"/create', "+controller+".create);"); 48 | lines.push("app.get('/"+controller+"/:id/edit', "+controller+".edit);"); 49 | lines.push("app.get('/"+controller+"/:id', "+controller+".show);"); 50 | lines.push("app.put('/"+controller+"/:id', "+controller+".update);"); 51 | lines.push("app.delete('/"+controller+"/:id', "+controller+".delete);"); 52 | } 53 | 54 | 55 | module.exports = function (name, fullName, methods, scaffold) { 56 | var lines = fs.readFileSync($ROUTES_FILE, {encoding: 'utf-8'}).split('\n'), 57 | linesNum = lines.length; 58 | 59 | var lastRequireLineNum = findPlaceForRequire(lines, linesNum); 60 | 61 | /* Prevents addition of routes in any routes for a controller exists */ 62 | for (var i = 0; i < lines.length; i++) { 63 | if (lines[i].indexOf(fullName) >= 0) { 64 | return; 65 | } 66 | } 67 | 68 | addNewRequire(lines, lastRequireLineNum, name, fullName); 69 | 70 | if (scaffold) { 71 | addScaffoldRoutes(lines, name); 72 | } else if (methods) { 73 | addRoutes(lines, name, methods); 74 | } 75 | 76 | fs.writeFileSync($ROUTES_FILE, lines.join('\n'), {encoding: 'utf-8'}); 77 | } 78 | -------------------------------------------------------------------------------- /lib/templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all logfiles and tempfiles. 2 | node_modules/ 3 | /log/* 4 | !/log/.keep 5 | /tmp 6 | !/tmp/.keep 7 | -------------------------------------------------------------------------------- /lib/templates/dynamic/controller: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | {{#methods}} 4 | {{.}}: (req, res) => { 5 | // your code goes here 6 | }, 7 | {{/methods}} 8 | }; 9 | -------------------------------------------------------------------------------- /lib/templates/dynamic/model: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), 2 | ObjectId = mongoose.Schema.Types.ObjectId, 3 | Mixed = mongoose.Schema.Types.Mixed; 4 | 5 | var {{schemaName}} = mongoose.Schema({ 6 | {{#props}} 7 | {{name}}: {type: {{type}}}, 8 | {{/props}} 9 | created_at: {type: Number, required: true, default: new Date().getTime()}, 10 | updated_at: {type: Number, required: true, default: new Date().getTime()} 11 | }); 12 | 13 | var {{nameCap}} = mongoose.model('{{nameCap}}', {{schemaName}}); 14 | 15 | module.exports = {{nameCap}}; 16 | -------------------------------------------------------------------------------- /lib/templates/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | Mustache = require('mustache'); 4 | 5 | 6 | /** 7 | * 8 | * @param {Object} options [description] 9 | * @return {String} [description] 10 | */ 11 | function renderController (options) { 12 | var _path = path.join(__dirname, 'dynamic/controller'), 13 | text = fs.readFileSync(_path, {encoding: 'utf8'}); 14 | 15 | return Mustache.render(text, {methods: options.methods}); 16 | } 17 | 18 | 19 | /** 20 | * 21 | * @param {Object} options [description] 22 | * @return {String} [description] 23 | */ 24 | function renderModel (options) { 25 | var _path = path.join(__dirname, 'dynamic/model'), 26 | text = fs.readFileSync(_path, {encoding: 'utf8'}); 27 | 28 | return Mustache.render(text, options); 29 | } 30 | 31 | 32 | 33 | module.exports = { 34 | /** 35 | * [render description] 36 | * @param {String} template [description] 37 | * @param {Object} options [description] 38 | * @return {String} [description] 39 | */ 40 | render(template, options) { 41 | switch (template) { 42 | case 'dynamic/controller': 43 | return renderController(options); 44 | break; 45 | case 'dynamic/model': 46 | return renderModel(options); 47 | break; 48 | default: 49 | console.log('Unknown template ' + template); 50 | return; 51 | } 52 | }, 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /lib/templates/static/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | div#content {font-size: 16px;} 2 | 3 | section {margin-bottom: 20px;} 4 | section:last-child {margin-bottom: 0;} 5 | -------------------------------------------------------------------------------- /lib/templates/static/app/controllers/welcome_controller.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | index: function (req, res) { 4 | res.render('welcome/index'); 5 | } 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /lib/templates/static/app/views/layouts/application.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Welcome! 5 | 6 | link(href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" type="text/css" rel="stylesheet") 7 | link(href="/assets/stylesheets/application.css" type="text/css" rel="stylesheet") 8 | script(src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js") 9 | script(src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js") 10 | script(src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js") 11 | script(src="/assets/javascripts/application.js") 12 | body 13 | div#header 14 | 15 | div#content 16 | block content 17 | 18 | div#footer 19 | -------------------------------------------------------------------------------- /lib/templates/static/app/views/welcome/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/application.pug 2 | 3 | block content 4 | div.fluid_container 5 | div.row 6 | div.col-md-8.col-md-offset-2 7 | h1.text-center Welcome to the NodeJS world! 8 | section#intro 9 | | This page has been automaticaly created by 10 | a(href="https://www.npmjs.com/package/frodojs") frodojs 11 | |. Frodo is a Rails-like app generator for 12 | a(href="https://nodejs.org") Node 13 | | and 14 | a(href="http://expressjs.com/") Express 15 | | . It was created to simplify working with projects based on the Express framework. 16 | 17 | section#docs 18 | | First thing first - frodojs is not a framework, it is a tool to help you organize your 19 | | Node apps similar to Rails (if you want it:)). Frodo uses Express as the framework, 20 | | moreover it is pure Express withou any modifications, so you will be able to upgrage 21 | | instantly. See available documentation on the 22 | a(href="https://github.com/leemalmac/frodo") Github Page 23 | -------------------------------------------------------------------------------- /lib/templates/static/config/application.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | express = require('express'), 3 | app = express(); 4 | 5 | 6 | global.ENV = process.env.NODE_ENV || 'development'; 7 | console.log('=> NODE_ENV:', ENV); 8 | 9 | 10 | app.set('view engine', 'pug'); 11 | app.set('views', path.join(ROOT, 'app/views')); 12 | app.use('/assets', express.static(path.join(ROOT, 'app/assets'))); 13 | 14 | /** 15 | * Add middleware here 16 | * 17 | * Example: 18 | * app.use(bodyParser.json()); 19 | * app.use('/public', express.static('public')); 20 | */ 21 | 22 | module.exports = app; 23 | -------------------------------------------------------------------------------- /lib/templates/static/config/database.js: -------------------------------------------------------------------------------- 1 | /* Database configuration file */ 2 | 3 | module.exports = { 4 | development: { 5 | servers: [['127.0.0.1', 27017]], 6 | database: '', 7 | user: '', 8 | password: '', 9 | replicaSet: null, 10 | } 11 | } -------------------------------------------------------------------------------- /lib/templates/static/config/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define useful global veriables here 3 | */ 4 | 5 | global.ROOT = process.cwd(); 6 | -------------------------------------------------------------------------------- /lib/templates/static/config/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Require controllers and bound it to routes. 3 | * 4 | * Example: 5 | * var app = require('./application'), 6 | * users = require('../app/controllers/users_controller'); 7 | * 8 | * app.get('/users', users.index); 9 | * app.post('/users', users.create); 10 | * app.put('/users/:id', users.update); 11 | * app.delete('/users/:id, users.delete'); 12 | * 13 | */ 14 | 15 | var app = require('./application'), 16 | welcome = require('../app/controllers/welcome_controller'); 17 | 18 | app.get('/', welcome.index); 19 | -------------------------------------------------------------------------------- /lib/templates/static/db/connection.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | /** 4 | * Require databse configuration depending on environment 5 | */ 6 | var conf = require('../config/database.js')[ENV], 7 | util = require('./util'), 8 | options = {useMongoClient: true}; 9 | 10 | 11 | mongoose.Promise = Promise; 12 | 13 | 14 | var connectionString = util.createConnectionString(conf); 15 | 16 | 17 | if (conf.replicaSet) { 18 | options.replset = conf.replicaSet; 19 | } 20 | 21 | 22 | mongoose.connect(connectionString, options); 23 | -------------------------------------------------------------------------------- /lib/templates/static/db/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates mongodb's connection string 3 | */ 4 | var createConnectionString = function (conf) { 5 | 'mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]'; 6 | var str = 'mongodb://'; 7 | 8 | if (conf.user && conf.password) 9 | str = str+conf.user + ':' + conf.password + '@'; 10 | 11 | conf.servers.forEach(function (v, i, a) { 12 | var host = v[0], 13 | port = v[1]; 14 | 15 | str = str+host+':'+port; 16 | 17 | if (i < conf.servers.length-1) { 18 | str=str+','; 19 | } 20 | }); 21 | 22 | if (!conf.database || typeof conf.database !== "string" || conf.database.length === 0) { 23 | console.error('=> Database name should be a nonempty string'); 24 | process.exit(1); 25 | } 26 | 27 | str = str + '/' + conf.database; 28 | 29 | return str; 30 | }; 31 | 32 | 33 | module.exports = { 34 | createConnectionString: createConnectionString 35 | } 36 | -------------------------------------------------------------------------------- /lib/templates/static/index.js: -------------------------------------------------------------------------------- 1 | require('./config/globals'); 2 | var app = require('./config/application'); 3 | require('./db/connection'); 4 | require('./config/routes'); 5 | 6 | 7 | var server = app.listen(7000, function () { 8 | var host = server.address().address; 9 | var port = server.address().port; 10 | console.log('=> Express application starting on http://%s:%s', host, port); 11 | console.log('=> Ctrl-C to shutdown server'); 12 | }); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frodojs", 3 | "version": "0.6.1", 4 | "description": "Frodo is a Rails-like app generator for Node and Express. It was created to simplify work with projects based on Express framework. Nowadays, many developers are familiar with Ruby on Rails framework, so app generated with Frodo will help you to kickstart faster.", 5 | "main": "frodo", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/leemalmac/frodo.git" 12 | }, 13 | "keywords": [ 14 | "node", 15 | "mean", 16 | "javascript", 17 | "rails" 18 | ], 19 | "author": "Nodari Lipartiya ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/leemalmac/frodo/issues" 23 | }, 24 | "homepage": "https://github.com/leemalmac/frodo#readme", 25 | "dependencies": { 26 | "jade": "^1.11.0", 27 | "mustache": "^2.3.0" 28 | }, 29 | "bin": { 30 | "frodo": "./bin/frodo" 31 | } 32 | } 33 | --------------------------------------------------------------------------------