UNPKG

22.4 kBJavaScriptView Raw
1/*
2 * Copyright (c) 2011-2013, Yahoo! Inc. All rights reserved.
3 * Copyrights licensed under the New BSD License.
4 * See the accompanying LICENSE file for terms.
5 */
6
7
8/*jslint anon:true, sloppy:true, nomen:true, node:true*/
9
10'use strict';
11
12// ----------------------------------------------------------------------------
13// Prerequisites
14// ----------------------------------------------------------------------------
15
16
17var express = require('express'), // TODO: [Issue 80] go back to connect?
18 http = require('http'),
19 store = require('./store'),
20 OutputHandler = require('./output-handler.server'),
21 libpath = require('path'),
22 requestCounter = 0, // used to scope logs per request
23 Mojito;
24
25// ----------------------------------------------------------------------------
26// Mojito Global
27// ----------------------------------------------------------------------------
28
29
30/**
31 * Shared global object, which isn't named 'mojito' because 'mojito' is a module
32 * name defined in mojito.common.js and required via Y.use.
33 */
34// TODO: Merge what we put on this object with the 'mojito' module/namespace.
35global._mojito = {};
36
37
38// ----------------------------------------------------------------------------
39// MojitoServer
40// ----------------------------------------------------------------------------
41
42
43/**
44 * The primary Mojito server type. Invoking the constructor returns an instance
45 * which is not yet running. Use listen to run the server once you have made
46 * any adjustments to its configuration.
47 * @param {{port: number,
48 * dir: string,
49 * context: Object,
50 * appConfig: Object,
51 * verbose: boolean}} options An object containing server options. The
52 * default port is process.env.PORT or port 8666 if no port is given.
53 * Verbose is false by default. Dir is cwd() by default. Both the
54 * context and appConfig will default to empty objects.
55 * @constructor
56 * @return {MojitoServer}
57 */
58function MojitoServer(options) {
59 var appConfig;
60
61 this._options = options || {};
62 this._options.port = this._options.port || process.env.PORT || 8666;
63 this._options.dir = this._options.dir || process.cwd();
64 this._options.context = this._options.context || {};
65 this._options.mojitoRoot = __dirname;
66
67 // TODO: Note we could pass some options to the express server instance.
68 this._app = express.createServer();
69
70 appConfig = store.getAppConfig(this._options.dir, this._options.context);
71 this._options.Y = this._createYUIInstance(this._options, appConfig);
72 this._configureLogger(this._options.Y);
73 this._app.store = store.createStore(this._options);
74 this._configureAppInstance(this._app, this._options, appConfig);
75 this._app.store.optimizeForEnvironment();
76
77 return this;
78}
79
80
81// ---------
82// Constants
83// ---------
84
85/**
86 * An ordered list of the middleware module names to load for a standard Mojito
87 * server instance.
88 * @type {Array.<string>}
89 */
90MojitoServer.MOJITO_MIDDLEWARE = [
91 'mojito-handler-static',
92 'mojito-parser-body',
93 'mojito-parser-cookies',
94 'mojito-contextualizer',
95 'mojito-handler-tunnel',
96 'mojito-router',
97 'mojito-handler-dispatcher',
98 'mojito-handler-error'
99];
100
101
102// ------------------
103// Private Attributes
104// ------------------
105
106
107/**
108 * The Express application (server) instance.
109 * @type {Object}
110 */
111MojitoServer.prototype._app = null;
112
113
114/**
115 * The formatting function for the server's associated logger.
116 * @type {function(string, number, string, Date, Object, number)}
117 */
118MojitoServer.prototype._logFormatter = null;
119
120
121/**
122 * The publisher function for the server's associated logger.
123 * @type {function(string, number, string, Date, number)}
124 */
125MojitoServer.prototype._logPublisher = null;
126
127
128/**
129 * The write function for the server's associated logger.
130 * @type {function(function(string, number, string, Date, Object, number))}
131 */
132MojitoServer.prototype._logWriter = null;
133
134
135/**
136 * The server options container. Common option keys are listed.
137 * @type {{port: number,
138 * dir: string,
139 * context: Object,
140 * appConfig: Object,
141 * verbose: boolean}}
142 */
143MojitoServer.prototype._options = null;
144
145
146/**
147 * The server startup time. This value is used to both provide startup/uptime
148 * information and as a signifier that the server has been configured/started.
149 * @type {number}
150 */
151MojitoServer.prototype._startupTime = null;
152
153
154// ---------------
155// Private Methods
156// ---------------
157
158/**
159 * A utility function for compiling a list of middleware
160 * @method _makeMiddewareList
161 * @private
162 * @param {array} app_mw Middleware list specified by the app's applicatioon.json
163 * @param {array} mojito_mw Middeware list specified Mojito, in this file, by the
164 * MojitoServer.MOJITO_MIDDLEWARE property
165 * @return {array} Complete and ordered list of middleware to load
166 */
167MojitoServer.prototype._makeMiddewareList = function (app_mw, mojito_mw) {
168 var m,
169 hasMojito = false,
170 midName,
171 middleware = [];
172
173 // computing middleware pieces
174 if (app_mw && app_mw.length) {
175 for (m = 0; m < app_mw.length; m += 1) {
176 midName = app_mw[m];
177 if (0 === midName.indexOf('mojito-')) {
178 hasMojito = true;
179 break;
180 }
181 }
182
183 if (hasMojito) {
184 // User has specified at least one of mojito's middleware, so
185 // we assume that they have specified all that they need.
186 middleware = app_mw;
187 } else {
188 // backwards compatibility mode:
189 // middlware = user's, then mojito's
190 middleware = app_mw.concat(mojito_mw);
191 }
192
193 } else {
194 middleware = mojito_mw;
195 }
196
197 return middleware;
198};
199
200/**
201 * A utility function to require middleware code, configure it, and tell express
202 * to use() it.
203 * @method _useMiddleware
204 * @private
205 * @param {object} app Express app instance.
206 * @param {function} dispatcher Dispatcher function wrapper, special case middleware.
207 * @param {string} midDir Directory of user-specified middleware, if any.
208 * @param {object} midConfig Configuration object.
209 * @param {array} middleware Middleware names, or pathnames.
210 */
211MojitoServer.prototype._useMiddleware = function (app, dispatcher, midDir, midConfig, middleware) {
212 var m,
213 midName,
214 midPath,
215 midBase,
216 midFactory;
217
218 for (m = 0; m < middleware.length; m += 1) {
219 midName = middleware[m];
220 if (0 === midName.indexOf('mojito-')) {
221 // one special one, since it might be difficult to move to a
222 // separate file
223 if (midName === 'mojito-handler-dispatcher') {
224 //console.log("======== MIDDLEWARE mojito -- " +
225 // "builtin mojito-handler-dispatcher");
226 app.use(dispatcher);
227 } else {
228 midPath = libpath.join(__dirname, 'app', 'middleware', midName);
229 //console.log("======== MIDDLEWARE mojito " + midPath);
230 midFactory = require(midPath);
231 app.use(midFactory(midConfig));
232 }
233 } else {
234 // backwards-compatibility: user-provided middleware is
235 // specified by path
236 midPath = libpath.join(midDir, midName);
237 //console.log("======== MIDDLEWARE user " + midPath);
238 midBase = libpath.basename(midPath);
239 if (0 === midBase.indexOf('mojito-')) {
240 // Same as above (case of Mojito's special middlewares)
241 // Gives a user-provided middleware access to the YUI
242 // instance, resource store, logger, context, etc.
243 app.use(require(midPath)(midConfig));
244 } else {
245 app.use(require(midPath));
246 }
247 }
248 }
249};
250
251/**
252 * Creates the YUI instance.
253 * @private
254 * @method _createYUIInstance
255 * @param {Object} options The options as passed to the constructor.
256 * @param {Object} appConfig The static application configuration.
257 * @return {Object} The YUI instances.
258 */
259MojitoServer.prototype._createYUIInstance = function(options, appConfig) {
260 var yuiConfig = (appConfig.yui && appConfig.yui.config) || {},
261 Y;
262
263 // redefining "combine" and/or "base" in the server side have side effects
264 // and might try to load yui from CDN, so we bypass them.
265 // TODO: report bug.
266 // is there a better alternative for this delete?
267 // maybe not, but it might introduce a perf penalty
268 // in v8 engine, and we can't use the undefined trick
269 // because loader is doing hasOwnProperty :(
270 delete yuiConfig.combine;
271 delete yuiConfig.base;
272
273 // in case we want to collect some performance metrics,
274 // we can do that by defining the "perf" object in:
275 // application.json (master)
276 // You can also use the option --perf path/filename.log when
277 // running mojito start to dump metrics to disk.
278 if (appConfig.perf) {
279 yuiConfig.perf = appConfig.perf;
280 yuiConfig.perf.logFile = options.perf || yuiConfig.perf.logFile;
281 }
282
283 // getting yui module, or yui/debug if needed, and applying
284 // the default configuration from application.json->yui-config
285 Y = require('yui' + (yuiConfig.filter === 'debug' ? '/debug' : '')).YUI(yuiConfig, {
286 useSync: true
287 });
288
289 return Y;
290};
291
292/**
293 * Adds Mojito framework components to the Express application instance.
294 * @private
295 * @method _configureAppInstance
296 * @param {Object} app The Express application instance to Mojito-enable.
297 * @param {{port: number,
298 * dir: string,
299 * context: Object,
300 * appConfig: Object,
301 * verbose: boolean}} options An object containing server options.
302 * @param {Object} appConfig The static application configuration.
303 */
304MojitoServer.prototype._configureAppInstance = function(app, options, appConfig) {
305 var store = app.store,
306 Y = options.Y,
307 modules = [],
308 middleware,
309 midConfig;
310
311 modules = this._configureYUI(Y, store);
312
313 // attaching all modules available for this application for the server side
314 Y.applyConfig({ useSync: true });
315 Y.use.apply(Y, modules);
316 Y.applyConfig({ useSync: false });
317
318 middleware = this._makeMiddewareList(appConfig.middleware, MojitoServer.MOJITO_MIDDLEWARE);
319
320 midConfig = {
321 Y: Y,
322 store: store,
323 logger: {
324 log: Y.log
325 },
326 context: options.context
327 };
328
329 function dispatcher(req, res, next) {
330 var command = req.command,
331 outputHandler,
332 context = req.context || {};
333
334 if (!command) {
335 next();
336 return;
337 }
338
339 outputHandler = new OutputHandler(req, res, next);
340 outputHandler.setLogger({
341 log: Y.log
342 });
343
344 // storing the static app config as well as contextualized app config per request
345 outputHandler.page.staticAppConfig = store.getStaticAppConfig();
346 outputHandler.page.appConfig = store.getAppConfig(context);
347 // compute routes once per request
348 outputHandler.page.routes = store.getRoutes(context);
349
350 // HookSystem::StartBlock
351 // enabling perf group
352 if (appConfig.perf) {
353 // in case another middleware has enabled hooks before
354 outputHandler.hook = req.hook || {};
355 Y.mojito.hooks.enableHookGroup(outputHandler.hook, 'mojito-perf');
356 }
357 // HookSystem::EndBlock
358
359 // HookSystem::StartBlock
360 Y.mojito.hooks.hook('AppDispatch', outputHandler.hook, req, res);
361 // HookSystem::EndBlock
362
363 Y.mojito.Dispatcher.init(store).dispatch(command, outputHandler);
364 }
365
366 // attach middleware pieces
367 this._useMiddleware(app, dispatcher, options.dir, midConfig, middleware);
368
369 Y.log('Mojito HTTP Server initialized in ' +
370 ((new Date().getTime()) - Mojito.MOJITO_INIT) + 'ms.');
371};
372
373/*
374 * Configures YUI logger to honor the logLevel and logLevelOrder
375 * TODO: this should be done at the low level in YUI.
376 */
377MojitoServer.prototype._configureLogger = function(Y) {
378 var logLevel = (Y.config.logLevel || 'debug').toLowerCase(),
379 logLevelOrder = Y.config.logLevelOrder || [],
380 defaultLogLevel = logLevelOrder[0] || 'info',
381 isatty = process.stdout.isTTY;
382
383 function log(c, msg, cat, src) {
384 var f,
385 m = (src) ? src + ': ' + msg : msg;
386
387 // if stdout is bound to the tty, we should try to
388 // use the fancy logs implemented by 'yui-log-nodejs'.
389 // TODO: eventually YUI should take care of this piece.
390 if (isatty && Y.Lang.isFunction(c.logFn)) {
391 c.logFn.call(Y, msg, cat, src);
392 } else if ((typeof console !== 'undefined') && console.log) {
393 f = (cat && console[cat]) ? cat : 'log';
394 console[f](msg);
395 }
396 }
397
398 // one more hack: we need to make sure that base is attached
399 // to be able to listen for Y.on.
400 Y.use('base');
401
402 if (Y.config.debug) {
403
404 logLevel = (logLevelOrder.indexOf(logLevel) >= 0 ? logLevel : logLevelOrder[0]);
405
406 // logLevel index defines the begining of the logLevelOrder structure
407 // e.g: ['foo', 'bar', 'baz'], and logLevel 'bar' should produce: ['bar', 'baz']
408 logLevelOrder = (logLevel ? logLevelOrder.slice(logLevelOrder.indexOf(logLevel)) : []);
409
410 Y.applyConfig({
411 useBrowserConsole: false,
412 logLevel: logLevel,
413 logLevelOrder: logLevelOrder
414 });
415
416 // listening for low level log events to filter some of them.
417 Y.on('yui:log', function (e) {
418 var c = Y.config,
419 cat = e && e.cat && e.cat.toLowerCase();
420
421 // this covers the case Y.log(msg) without category
422 // by using the low priority category from logLevelOrder.
423 cat = cat || defaultLogLevel;
424
425 // applying logLevel filters
426 if (cat && ((c.logLevel === cat) || (c.logLevelOrder.indexOf(cat) >= 0))) {
427 log(c, e.msg, cat, e.src);
428 }
429 return true;
430 });
431 }
432
433};
434
435/**
436 * Configures YUI with both the Mojito framework and all the YUI modules in the
437 * application.
438 * @private
439 * @method _configureYUI
440 * @param {object} Y YUI object to configure
441 * @param {object} store Resource Store which knows what to load
442 * @return {array} array of YUI module names
443 */
444MojitoServer.prototype._configureYUI = function(Y, store) {
445 var modules,
446 load,
447 lang;
448
449 modules = store.yui.getModulesConfig('server', false);
450 Y.applyConfig(modules);
451
452 load = Object.keys(modules.modules);
453
454 // NOTE: Not all of these module names are guaranteed to be valid,
455 // but the loader tolerates them anyways.
456 for (lang in store.yui.langs) {
457 if (store.yui.langs.hasOwnProperty(lang) && lang) {
458 load.push('lang/datatype-date-format_' + lang);
459 }
460 }
461
462 return load;
463};
464
465
466// --------------
467// Public Methods
468// --------------
469
470
471/**
472 * Closes (shuts down) the server port and stops the server.
473 */
474MojitoServer.prototype.close = function() {
475 if (this._options.verbose) {
476 console.warn('Closing Mojito Application');
477 }
478
479 this._app.close();
480};
481
482
483/**
484 * Returns the instance of http.Server (or a subtype) which is the true server.
485 * @return {http.Server} The node.js http.Server (or subtype) instance.
486 */
487MojitoServer.prototype.getHttpServer = function() {
488 return this._app;
489};
490
491
492/**
493 * Begin listening for inbound connections.
494 * @param {Number} port The port number. Defaults to the server's value for
495 * options.port (which defaults to process.env.PORT followed by 8666).
496 * @param {String} host Optional hostname or IP address in string form.
497 * @param {Function} callback Optional callback to get notified when the
498 * server is ready to server traffic.
499 */
500MojitoServer.prototype.listen = function(port, host, callback) {
501
502 var app = this._app,
503 p = port || this._options.port,
504 h = host || this._options.host,
505 listenArgs = [p];
506
507 // Track startup time and use it to ensure we don't try to listen() twice.
508 if (this._startupTime) {
509 if (this._options.verbose) {
510 console.warn('Mojito Application Already Running');
511 }
512 return;
513 }
514 this._startupTime = new Date().getTime();
515
516 if (this._options.verbose) {
517 console.warn('Starting Mojito Application');
518 }
519
520 if (h) {
521 listenArgs.push(h);
522 }
523
524 // close on app for callback
525 function handler(err) {
526 callback(err, app);
527 }
528
529 if (callback) {
530 app.on('error', handler);
531 listenArgs.push(handler);
532 }
533
534 app.listen.apply(app, listenArgs);
535};
536
537
538/**
539 * Invokes a callback function with the content of the requested url.
540 * @param {string} url A url to fetch.
541 * @param {{host: string, port: number, method: string}|function} opts A list of
542 * options, or a callback function (See @param for cb). When providing
543 * options note that the list here is not exhaustive. Any valid http.request
544 * object option may be provided. See documentation for http.request.
545 * @param {function(Error, string, string)} cb A function called on request
546 * completion. Parameters are any optional Error, the original URL, and the
547 * content of that URL.
548 */
549MojitoServer.prototype.getWebPage = function(url, opts, cb) {
550 var buffer = '',
551 callback,
552 options = {
553 host: '127.0.0.1',
554 port: this._options.port,
555 path: url,
556 method: 'get'
557 };
558
559 // Options block is optional, no pun intended. When it's a function we'll
560 // use that as our callback function.
561 if (typeof opts === 'function') {
562 callback = opts;
563 } else {
564 // Don't assume we got a real callback function.
565 callback = cb || Mojito.NOOP;
566
567 // Map provided options into our request options object.
568 Object.keys(opts).forEach(function(k) {
569 if (opts.hasOwnProperty(k)) {
570 options[k] = opts[k];
571 }
572 });
573 }
574
575 http.request(options, function(res) {
576 res.setEncoding('utf8');
577 res.on('data', function(chunk) {
578 buffer += chunk;
579 });
580 res.on('end', function() {
581 // TODO: 200 isn't the only success code. Support 304 etc.
582 if (res.statusCode !== 200) {
583 callback('Could not get web page: status code: ' +
584 res.statusCode + '\n' + buffer, url);
585 } else {
586 callback(null, url, buffer);
587 }
588 });
589 }).on('error', function(err) {
590 callback(err, url);
591 }).end();
592};
593
594
595/**
596 * Invokes a callback function with the content of each url requested.
597 * @param {Array.<string>} urls A list of urls to fetch.
598 * @param {function(Error, string, string)} cb A function called once for each
599 * url in the urls list. Parameters are any optional Error, the original URL
600 * and the URL's content.
601 */
602MojitoServer.prototype.getWebPages = function(urls, cb) {
603 var server = this,
604 callback,
605 count,
606 len,
607 initOne;
608
609 // If no array, or an empty array, just exit.
610 if (!urls || urls.length === 0) {
611 return;
612 }
613
614 // NOTE we could just say this is an error condition. No callback, what's
615 // the point of doing the work?
616 callback = cb || Mojito.NOOP;
617
618 len = urls.length;
619 count = 0;
620
621 // Create a function to call getWebPage with an individual URL shifted from
622 // the list. When the list is empty we can stop.
623 initOne = function() {
624 if (count < len) {
625 server.getWebPage(urls[count], function(err, url, data) {
626 count += 1;
627 try {
628 callback(err, url, data);
629 } finally {
630 initOne();
631 }
632 });
633 }
634 };
635
636 // Start the ball rolling :).
637 initOne();
638};
639
640// ----------------------------------------------------------------------------
641// Mojito
642// ----------------------------------------------------------------------------
643
644/**
645 * The Mojito object is the primary server construction interface for Mojito.
646 * This object is used to create new server instances but given that the raw
647 * Express application object is expected/returned there's no need for a true
648 * constructor since there are no true instances of a Mojito server object.
649 */
650// TODO: Merge what we put on this object with the 'mojito' module/namespace.
651Mojito = {};
652
653
654// ---------
655// Constants
656// ---------
657
658/**
659 * The date/time the Mojito object was initialized.
660 * @type {Date}
661 */
662Mojito.MOJITO_INIT = new Date().getTime();
663
664
665/**
666 * A placeholder function used to avoid overhead checking for callbacks.
667 * @type {function()}
668 */
669Mojito.NOOP = function() {};
670
671
672// --------------
673// Public Methods
674// --------------
675
676
677/**
678 * Creates a properly configured MojitoServer instance and returns it.
679 * @method createServer
680 * @param {Object} options Options for starting the app.
681 * @return {Object} Express application.
682 */
683Mojito.createServer = function(options) {
684 // NOTE that we use the exported name here. This allows us to mock that
685 // object during testing.
686 return new Mojito.Server(options);
687};
688
689
690/**
691 * Allows the bin/mojito command to leverage the current module's relative path
692 * for initial startup loading.
693 * @method include
694 * @param {string} path The path used to locate resources.
695 * @return {Object} The return value of require() for the adjusted path.
696 */
697Mojito.include = function(path) {
698 return require('./' + path);
699};
700
701
702// ----------------------------------------------------------------------------
703// EXPORT(S)
704// ----------------------------------------------------------------------------
705
706/**
707 * Export Mojito as the return value for any require() calls.
708 * @type {Mojito}
709 */
710module.exports = Mojito;
711
712/**
713 * Export Mojito.Server to support unit testing of the server type. With this
714 * approach the slot for the server can be replaced with a mock, but the actual
715 * MojitoServer type remains private to the module.
716 * @type {MojitoServer}
717 */
718module.exports.Server = MojitoServer;
719