diff --git a/lib/application.js b/lib/application.js index 838b882aaae..1c949cee9a1 100644 --- a/lib/application.js +++ b/lib/application.js @@ -554,25 +554,30 @@ app.render = function render(name, options, callback) { root: this.get('views'), engines: engines }); + } - if (!view.path) { - var dirs = Array.isArray(view.root) && view.root.length > 1 - ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"' - : 'directory "' + view.root + '"' - var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs); - err.view = view; - return done(err); - } + if (!view.path) { + view.lookupMain(function (err) { + if (err) return done(err); + // prime the cache + if (renderOptions.cache) { + cache[name] = view; + } + + // render + tryRender(view, renderOptions, done); + }); + } else { // prime the cache if (renderOptions.cache) { cache[name] = view; } - } - // render - tryRender(view, renderOptions, done); -}; + // render + tryRender(view, renderOptions, done); + } +} /** * Listen for connections. diff --git a/lib/view.js b/lib/view.js index d66b4a2d89c..ef48c6a6c5d 100644 --- a/lib/view.js +++ b/lib/view.js @@ -61,15 +61,11 @@ function View(name, options) { throw new Error('No default engine was specified and no extension was provided.'); } - var fileName = name; - if (!this.ext) { // get extension from default engine name this.ext = this.defaultEngine[0] !== '.' ? '.' + this.defaultEngine : this.defaultEngine; - - fileName += this.ext; } if (!opts.engines[this.ext]) { @@ -89,26 +85,30 @@ function View(name, options) { // store loaded engine this.engine = opts.engines[this.ext]; - - // lookup path - this.path = this.lookup(fileName); } /** - * Lookup view by the given `name` + * Lookup view by the given `name` and `ext` * * @param {string} name + * @param {String} ext + * @param {Function} cb * @private */ -View.prototype.lookup = function lookup(name) { - var path; +View.prototype.lookup = function lookup(name, cb) { var roots = [].concat(this.root); debug('lookup "%s"', name); - for (var i = 0; i < roots.length && !path; i++) { - var root = roots[i]; + var ext = this.ext; + + function lookup(roots, callback) { + var root = roots.shift(); + if (!root) { + return callback(null, null); + } + debug("looking up '%s' in '%s'", name, root); // resolve the path var loc = resolve(root, name); @@ -116,10 +116,19 @@ View.prototype.lookup = function lookup(name) { var file = basename(loc); // resolve the file - path = this.resolve(dir, file); + resolveView(dir, file, ext, function (err, resolved) { + if (err) { + return callback(err); + } else if (resolved) { + return callback(null, resolved); + } else { + return lookup(roots, callback); + } + }); + } - return path; + return lookup(roots, cb); }; /** @@ -135,6 +144,8 @@ View.prototype.render = function render(options, callback) { debug('render "%s"', this.path); + if (!this.path) return fn(new Error("View has not been fully initialized yet")); + // render, normalizing sync callbacks this.engine(this.path, options, function onRender() { if (!sync) { @@ -158,48 +169,93 @@ View.prototype.render = function render(options, callback) { sync = false; }; +/** Resolve the main template for this view + * + * @param {function} cb + * @private + */ +View.prototype.lookupMain = function lookupMain(cb) { + if (this.path) return cb(); + var view = this; + var name = path.extname(this.name) === this.ext + ? this.name + : this.name + this.ext; + this.lookup(name, function (err, path) { + if (err) { + return cb(err); + } else if (!path) { + var dirs = Array.isArray(view.root) && view.root.length > 1 + ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"' + : 'directory "' + view.root + '"' + var viewError = new Error('Failed to lookup view "' + view.name + '" in views ' + dirs); + viewError.view = view; + return cb(viewError); + } else { + view.path = path; + cb(); + } + }); +}; + /** * Resolve the file within the given directory. * * @param {string} dir * @param {string} file + * @param {string} ext + * @param {function} cb * @private */ -View.prototype.resolve = function resolve(dir, file) { - var ext = this.ext; - - // . +function resolveView(dir, file, ext, cb) { var path = join(dir, file); - var stat = tryStat(path); - - if (stat && stat.isFile()) { - return path; - } - // /index. - path = join(dir, basename(file, ext), 'index' + ext); - stat = tryStat(path); + // . + limitStat(path, function (err, stat) { + if (err && err.code !== 'ENOENT') { + return cb(err); + } else if (!err && stat && stat.isFile()) { + return cb(null, path); + } - if (stat && stat.isFile()) { - return path; - } -}; + // /index. + path = join(dir, basename(file, ext), 'index' + ext); + limitStat(path, function (err, stat) { + if (err && err.code === 'ENOENT') { + return cb(null, null); + } else if (!err && stat && stat.isFile()) { + return cb(null, path); + } else { + return cb(err || new Error("error looking up '" + path + "'")); + } + }); + }); +} +var pendingStats = []; +var numPendingStats = 0; /** - * Return a stat, maybe. + * an fs.stat call that limits the number of outstanding requests to 10. * - * @param {string} path - * @return {fs.Stats} - * @private + * @param {String} path + * @param {Function} cb */ +function limitStat(path, cb) { + if (++numPendingStats > 10) { + pendingStats.push([path, cb]); + } else { + fs.stat(path, cbAndDequeue(cb)); + } -function tryStat(path) { - debug('stat "%s"', path); - - try { - return fs.statSync(path); - } catch (e) { - return undefined; + function cbAndDequeue(cb) { + return function (err, stat) { + cb(err, stat); + var next = pendingStats.shift(); + if (next) { + fs.stat(next[0], cbAndDequeue(next[1])); + } else { + numPendingStats--; + } + } } }