UNPKG

dataflo.ws

Version:

Zero-code JSON config-based dataflow engine for Node, PhoneGap and browser.

392 lines (320 loc) 9.13 kB
var task = require ('./base'), path = require ('path'), fs = require ('fs'), util = require ('util'), stream = require('stream'); // TODO: write a message var presenters = {}, defaultTemplateDir = (project && project.config && project.config.templateDir) || 'share/presentation', isWatched = (project && project.config && project.config.debug); /** * @class task.presenterTask * @extends task.task * * This is a type of task that sends a rendered template as an HTTP response. * * Implementation specific by definition. */ var presenterTask = module.exports = function (config) { this.headers = {}; this.init (config); if (config.headers) util.extend (this.headers, config.headers); }; util.inherits (presenterTask, task); var cache = {}; util.extend (presenterTask.prototype, { defaultTemplateDir: defaultTemplateDir, /** * @private */ readTemplate: function (templateIO, cb) { templateIO.readFile (function (err, data) { cb.call (this, err, data); }); }, isInStaticDir: function (filePath) { var httpStatic; try { httpStatic = project.config.initiator.http.static.root.path || project.config.initiator.http.static.root; } catch (e) {} if (httpStatic) { var rootPath = project.root.path; httpStatic = path.resolve(rootPath, httpStatic); var dirName = filePath; while (dirName != rootPath) { dirName = path.dirname(dirName); if (dirName == httpStatic) { return true; break; } } } return false; }, getAbsPath: function () { return path.resolve( project.root.path, this.defaultTemplateDir, this.file ); }, getTemplateIO: function (callback) { var self = this; var defTemplate = this.getAbsPath(); var rootTemplate = path.resolve(project.root.path, this.file); function isTemplateOk (templateIO) { // warn if file is in static HTTP directory if (self.isInStaticDir(templateIO)) { throw new Error( 'Publicly accessible template file at '+templateIO+'!' ); } callback (project.root.fileIO (templateIO)); } fs.exists(defTemplate, function (exists) { if (exists) { isTemplateOk (defTemplate); } else { fs.exists(rootTemplate, function (exists) { if (exists) { isTemplateOk (rootTemplate); } else { var statusCode = self.response.statusCode; self.response.statusCode = (statusCode >= 200 && statusCode <= 300) ? 500 : statusCode; // if we have good response, but no template file, this is failure. // redirects, 4xx and 5xx codes seems ok without template // self.response.writeHead ((statusCode >= 200 && statusCode <= 300) ? 500 : statusCode); self.renderResult (null, "failure"); self.emit ("error", "template not found in '" + defTemplate + "' or '" + rootTemplate + "'"); // self.failed (); } }); } }); }, renderFile: function () { var self = this; this.getTemplateIO(function (templateIO) { templateIO.readFile(function (err, data) { if (err) { // self.response.writeHead (self.response.statusCode); self.renderResult (null, "failure"); self.emit ("error", "template error: can't access file " + templateIO.path); } else { self.renderResult(data.toString()); } }); }); }, /** * @private */ // TODO: add cache management renderCompile: function() { var self = this; if (self.file in cache && cache[self.file]) { self.renderProcess(cache[self.file]); return; } var templateIO = this.getTemplateIO(function (templateIO) { self.readTemplate (templateIO, function (err, tpl) { if (err) { console.error ("can't access file %s", templateIO.path); // process.kill (); // bad idea return; } var tplStr = tpl.toString(); // compile class method must return function. we call // this function with presentation data. if your renderer // doesn't have such function, you must extend renderer // via renderer.prototype.compile var compileMethod = self.compileMethod || 'compile'; if (!presenters[self.type]) presenters[self.type] = require (self.moduleName || self.type); if (!presenters[self.type][compileMethod]) { console.error ( 'renderer \"' + self.type + '\" doesn\'t have a template' + 'compilation method named \"' + compileMethod + '\"' ); } if (isWatched) { self.emit ('log', 'setting up watch for presentation file'); fs.watch (self.getAbsPath(), function () { self.emit ('log', 'presentation file is changed'); delete cache[self.file]; }); } cache[self.file] = presenters[self.type][compileMethod]( tplStr, self.compileParams || {} ); if (self.renderMethod) { self.renderProcess(cache[self.file][self.renderMethod].bind(cache[self.file])) } else { self.renderProcess(cache[self.file]); } }); }); }, /** * @private */ renderProcess: function(render) { var responseData; try { responseData = render (this.vars); } catch (e) { this.emit ('error', e); // console.log (e); } this.renderResult ( responseData ); }, /** * @private */ renderResult: function(result, failure) { if (this.headers) { for (var key in this.headers) { this.response.setHeader(key, this.headers[key]); } } this.headers.connection = 'close'; if (this.verbose) console.log (this.headers, result); if (!result) { this.response.end(); } else if (result instanceof stream) { result.pipe(this.response); } else { this.response.end(result); } if (!failure) { this.completed(); } else { this.failed(); } }, /** * @method run * Renders the template from {@link #file} and sends the result * as the content of the {@link #response}. */ run: function () { var self = this; /** * @cfg {String} file The template file name. */ /** * @cfg {String} type Template type. Tries to guess the type * by the {@link #file} extension. * * Possible values: * * - `ejs`, EJS template * - `json`, JSON string * - `asis`, plain text. * - `fileAsIs`, file from disk (please provide `file` param) */ /** * @cfg {http.ClientResponse} response (required) The response object. * * This task doesn't populate the {@link #produce} * field of the dataflow. Instead, it sends the result via HTTP. */ /** * @cfg {String} contentType The MIME type of the response content. * * Default values depend on the template {@link #type}. */ /** * @cfg {Object} headers http headers to send * */ /** * @cfg {String} code http status code to overrride current one * */ if (self.code) { self.response.statusCode = self.code; } if (!self.type) { if (self.file) { // guess on file name self.type = self.file.substring (self.file.lastIndexOf ('.') + 1);; self.emit ('log', 'guessed ' + self.type + ' presenter type from filename: ' + self.file); } else { // if (self.response.statusCode > 200) { self.renderResult (); return; // } // TODO: throw error in case of 2xx response code } } // TODO: lowercase all headers switch (self.type.toLowerCase()) { case 'html': self.setContentType('text/html; charset=utf-8'); self.renderFile(); break; case 'ejs': if (!self.compileParams) { // compileParams can be defined in task initConfig self.compileParams = {filename: path.resolve( project.root.path, self.defaultTemplateDir, self.file )}; } case 'jade': case 'mustache': case 'handlebars': self.setContentType(self.headers['content-type'] || 'text/html; charset=utf-8'); self.renderCompile(); break; case 'hogan': self.setContentType(self.headers['content-type'] || 'text/html; charset=utf-8'); self.moduleName = 'hogan.js'; self.renderMethod = 'render'; self.renderCompile(); break; case 'json': self.setContentType('application/json; charset=utf-8'); self.renderResult ( JSON.stringify (self.vars) ); break; case 'fileasis': var mmm; try { mmm = require ('mmmagic'); } catch (e) { console.error ("module 'mmmagic' not found.", "this module required if you plan to use fileAsIs presenter type"); process.kill(); } var Magic = mmm.Magic; var magic = new Magic(mmm.MAGIC_MIME_TYPE); magic.detectFile(self.file, function(err, contentType) { if (err) throw err; self.setContentType(contentType); var fileStream = fs.createReadStream(self.file); self.renderResult (fileStream); }); break; case 'asis': default: if (!self.headers['content-type']) { var contentType = (self.contentType) ? self.contentType : 'text/plain'; if (!self.noUTF8 || contentType.indexOf ('application/') != 0) { contentType += '; charset=utf-8'; } self.setContentType(contentType); } self.renderResult (self.vars); break; } }, setContentType: function(value) { this.headers['content-type'] = value; } });