UNPKG

rocket

Version:

The rapid development framework for node.js

468 lines (314 loc) 13.1 kB
/****************************************************************************** * Module dependencies */ var express = require('express') , __ = require('express-resource') , async = require('async') , path = require('path') , _ = require('underscore') , lingo = require('lingo') ; //project related dependencies var asyncFs = require('./util/async_fs') , dadt = require('./util/dadt') , config = require('./config') , CONTROLLER_NAME_REGEXP = config.CONTROLLER_NAME_REGEXP ; /****************************************************************************** * Rocket template name space */ var rocket_tmpl = {}; rocket_tmpl.header = function() { var args = Array.prototype.slice.call(arguments) , controller_name = args.shift().replace(/[.,-;\s]+/g, '_') , header = [] , require_configuration = global.require_configuration || {} ; return header.join('\n'); }; rocket_tmpl.footer = function() { var args = Array.prototype.slice.call(arguments) , controller_name = args.shift().replace(/[.,-;\s]+/g, '_') , footer = [] , require_configuration = global.require_configuration || {} ; footer.push("<script>"); footer.push("var _b = " + JSON.stringify(args) + "; "); footer.push("var require = " + JSON.stringify(require_configuration) + ";"); footer.push("require.deps || (require.deps = []);"); footer.push("require.deps.push('" + controller_name + "_client" + "')"); footer.push("require.callback = " + "function() { require(['" + controller_name + "_client" + "'], function(m) { m && m.init && m.init.apply(m, _b); })}"); footer.push("</script>"); footer.push("<script src=\"//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.1/require.min.js\"></script>"); footer.push("<script>window.define||document.write('<script src=\"/js/rocket/vendors/require.min.js\"><\\/script>')</script>") return footer.join('\n'); }; /****************************************************************************** * Destroy all the routes associated with a given controller * * @param app {Object} The `app` object as returned by express, connect, or * rocket. * * @param routes_dadt {Object} The root of routes' dadt data structure. * * @param controller_name {String} The name of the controller */ function destroyControllerRoutes(app, routes_dadt, controller_name) { var routes = routes_dadt.get(controller_name).get('routes') ; if (routes) { for (var route in routes.values) { app.remove(route); } } delete app.resources[controller_info.name]; controller_info.set('routes', dadt.createNode()); } /****************************************************************************** * Builds a wrapper function sending raw json as a response if no view is found * or a rendered view * * @param controller_info {dadtNode} The dadtNode containing the controller * info. * * @param method_func {Function} The method function. */ function buildWrapper(controller_info, method_func) { return function(req, res, next) { var view_file = controller_info.get('view') , layout_node = controller_info.parent.get('layout') , __ = !layout_node && controller_info.parent.parent && (layout_node = controller_info.parent.parent.get('layout')) , layout = (typeof layout_node !== 'undefined' ? layout_node.get('view') : true) , oSend = res.send , oRender = res.render ; res._boot_options || (res._boot_options = {}); //check if we have a view & if that we do not have an XHR-Request. //If the view is missing or if we have an XHR-Request, skip view rendering. if (!req.xhr && view_file) { //restore the original `res.send` function when `res.render` is called. res.render = function() { res.send = oSend; return oRender.apply(res, arguments); }; //replace the original send function to be able to automatically render //the wanted view. res.send = function(obj) { obj = obj || {}; obj.layout = layout obj.controller = obj.controller || controller_info.parent.get('name'); obj.rocket = obj.rocket || rocket_tmpl; res.send = oSend; obj.boot_options = _.extend(res._boot_options, obj.boot_options); res.render(view_file, obj); }; } method_func(req, res, next); }; } /****************************************************************************** * Builds and set up the routes of the given controller. Removing the previous * routes if necessary. * * @param app {Object} The app object as returned by express, connect, or * rocket. * * @param controller_info {Object} The dadt node containing the controller info. * * @param controller {Object} The controller object to route. */ function buildRoutes(app, controller_info, controller) { var wrapped_funcs = {} , split = [] , resource , load , name = controller_info.get('name') , controller_path = controller_info.get('path') ; for (var key in controller) { if (key.substr(0,1) === '_') { if (key === '_load' && typeof controller[key] === 'function') { //if the method name is `_load` and it is a function ... save it. loader = controller[key]; } //ignore keys beginning with a `_` unless it is `_load` continue; } controller_info.setIfNone(key, dadt.createNode()); innerNode = controller_info.get(key); if (typeof controller[key] === 'object' && controller[key] !== null) { var verbs = _.functions(controller[key]) , route = (name === 'root' ? path.join('/', key) : path.join('/', name, key)) , param = ':' + lingo.en.singularize(name) , route_param = path.join(route, param) ; innerNode.set('name', [name, key].join('.')); for (var i = 0, ii = verbs.length; i < ii; i++) { var current_verb = verbs[i] , method = controller[key][current_verb] ; if (['get', 'post', 'put', 'del'].indexOf(current_verb) !== -1) { innerNode.setIfNone(current_verb, dadt.createNode()); var wrapped = undefined , current_verb_node = innerNode.get(current_verb) ; wrapped = buildWrapper(current_verb_node, method); current_verb_node.set('handler', wrapped); require.cache[controller_path].exports[key][current_verb] = wrapped; app[current_verb](route, wrapped); app[current_verb](route_param, wrapped); controller_info.get('routes').setIfNone(route, true); controller_info.get('routes').setIfNone(route_param, true); } } } else if(typeof controller[key] === 'function') { var wrapped = undefined , key_node = controller_info.get(key) , route = (name === 'root' ? path.join('/', key) : path.join('/', name, key)) , param = ':' + lingo.en.singularize(name) , route_param = path.join(route, param) ; wrapped = buildWrapper(innerNode, controller[key]); innerNode.set('handler', wrapped); require.cache[controller_path].exports[key] = wrapped; //if the function is a custom controller method ... if (['index', 'show', 'new', 'create', 'edit', 'update', 'destroy'].indexOf(key) === -1) { app.all(route, wrapped); app.all(route_param, wrapped); controller_info.get('routes').setIfNone(route, true); controller_info.get('routes').setIfNone(route_param, true); } else { app.remove(route); app.remove(route_param); wrapped_funcs[key] = wrapped; } } } if(name === 'root') { resource = app.resource(wrapped_funcs); } else { resource = app.resource(name, wrapped_funcs); } if(typeof load !== 'undefined') { resource.load(load); } for (var verb in resource.routes) { for (var url in resource.routes[verb]) { controller_info.get('routes').setIfNone(url, true); } } } /****************************************************************************** * Function called either when a new controller file is found or when a change * is detected. It takes care of clearing the `require` cache and creating the * controllers dadt node. * * * @param app {Object} The app object as returned by express, connect, or * rocket. * * @param controller_info {Object} The dadt node containing the controller info. * * @param controller_path {Object} The path of the current * controller file. * * @param callback {Function} A callback of the form `function(err)`. * */ function loadController() { var args = Array.prototype.slice.call(arguments) , app = args.shift() , controller_info = args.shift() , controller_path = args.shift() , callback = args.pop() , controller ; controller = require(controller_path); buildRoutes(app, controller_info, controller); if (callback) { callback(null); } } /****************************************************************************** * Sets up the listeners on the `controllers` directory and calls * `loadController`. * * This function is also called each time a new controller file is found. * * * @param app {Object} The app object as returned by express, connect, or * rocket. * * @param routes_dadt {Object} The root of routes' dadt data structure. * * @param controller_file {Object} The FileEventEmitter object of the current * controller file. * * @param matches {Array} Array of the matched patterns of the RegExp selecting * this controller file. * * * @param callback {Function} A callback of the form `function(err)` * */ function setupController(app, routes_dadt, controller_file, matches, callback) { var controller_name = matches[1] , controller_path = controller_file.path , reload ; routes_dadt.setIfNone(controller_name, dadt.createNode()); controller_info = routes_dadt.get(controller_name); controller_info.set('routes', dadt.createNode()); controller_name = lingo.camelcase(controller_name.replace(/_/g, ' ')); controller_info.set('name', controller_name); controller_info.set('path', controller_path); reload = async.apply( loadController , app , controller_info , controller_path ) controller_file.on('change', function(curr, prev) { if (curr.nlink > 0) { if (require.cache[controller_path]) { delete require.cache[controller_path]; } destroyControllerRoutes(app, routes_dadt, controller_name); reload(); } }); controller_file.on('destroy', function() { controller_file.unwatch(); destroyControllerRoutes(app, routes_dadt, controller_name) }); reload(callback); } /****************************************************************************** * Setups the controllers, also watches the `controllers` directory for new * files and calls `setupController` accordingly. * * @param app {Object} The app object as returned by express, connect, or * rocket. * * @param routes_dadt {Object} The root of routes' dadt data structure. * * @param controllers {Object} The FileEventEmitter object of the `controllers` * folder. * * @param callback {Function} A callback of the form `function(err)` */ exports.setup = function setup_controller() { var args = Array.prototype.slice.call(arguments) , app = args.shift() , routes_dadt = args.shift() , controllers = args.shift() , callback = args.pop() ; app.rocket_routes = routes_dadt; app.use(express.methodOverride()); asyncFs.mapDir( controllers.path , [ CONTROLLER_NAME_REGEXP , async.apply(setupController, app, routes_dadt) ] , callback ); }