UNPKG

gulp-vash-static

Version:

Gulp plugin for converting Vash razor templates to static html

390 lines (313 loc) 13.7 kB
// through2 is a thin wrapper around node transform streams var through = require('through2') , gutil = require("gulp-util") , watch = require('gulp-watch') , PluginError = gutil.PluginError , File = gutil.File , path = require('path') , replaceExt = require('replace-ext') , vashStatic = require('vash-static') , runSequence = require('run-sequence') , fs = require("fs") , _ = require("lodash") , slash = require("slash") , sav = require('log-saviour') // Consts const PLUGIN_NAME = 'gulp-vash-static'; const NS = "gulpVashStatic"; var argFunctionOverride; // Function to override the functionality of 'getAllArgs'. sav.setNameSpace(PLUGIN_NAME) // Tells JSHint that these guys can be globals /*global warn:true, warnArr:true, log:true, logArr:true*/ sav.setNameSpace(NS) warn = sav.warn warnArr = sav.warnArr log = sav.log logArr = sav.logArr function prefixStream(prefixText) { var stream = through(); stream.write(prefixText); return stream; } /** * Escapes common special characters in regexes * @param {string} str - String to escape * @returns {string} String made safe for regex. */ function regSlash(str) { if (str.indexOf("?") !== -1) str = str.split("?").join("\\?"); if (str.indexOf("*") !== -1) str = str.split("*").join("\\*"); return str; } /** * Gets the all argument from the command-line that starts with '--', omitting the '--' and finishing at the next space. * @param {string[]} [args] - Optionally pass in custom args, say from a child process when testing. * @returns {string[]} Argument values without the '--' prefix. */ function getAllArgs(args) { // if 'argFunctionOverride' exists, use that instead if(argFunctionOverride) return argFunctionOverride(args); args = args || process.argv; var argVals = []; args.forEach(function (arg) { if (arg.indexOf("--") === 0) { var propArr = arg.split(" "); var argVal = propArr[0].substr(2); // allows shortcut for styleguide pages, starting with 'SG_' if(argVal && argVal.indexOf("SG_") === 0) argVal = "styleGuide/" + argVal; argVals.push( argVal ); } }); return argVals; } /** * Allows you to optionally override the functionality of 'getAllArgs', so you can manipulate arguments. First param should be the function and it should return a manipulated string containing the page name. * @param {Function} fun - Function to override 'getAllArgs'. */ function overrideGetAllArgs(fun) { if(typeof fun !== "function") { warn(NS, 'overrideGetAllArgs', "Arg 'fun' must be a function. Type was " + (typeof fun) ); return; } argFunctionOverride = fun; } /** * Checks a file path exists and warns with error specific to vash pages if doesn't exist * @param {string} pageFilePath - File path to the vash template. * @returns {boolean} Success/fail */ function validatePageTemplate(pageFilePath) { if(!fs.existsSync(pageFilePath)) { warn(NS, "validatePageTemplate", "Seems like that page doesn't have a Vash template.", pageFilePath) return false } return true } /** * Gets details relevant to Vash Static from a vinyl file * @param {Object} vinyl - Vinyl file. * @param {string[]} dirTypes - Array of 'directory types', such as 'pg', 'wg', 'glb'. * @returns {Object} Object literal with details */ function getVinylDetails(vinyl, dirTypes) { var type = vashStatic.getDirTypeFromPath(vinyl.path, dirTypes); return { type: type, moduleName: vashStatic.getModuleName(vinyl.path, type), fileName: vashStatic.getFileName( vinyl.path, true ) } } /** * Precompiles a vash templates and generates a json file with the template's name and contents in it. * @param {object} opts - Options for the function: * @param {boolean} [opts.debugMode] - Production usage should pass false to keep file size down. Development should use true, for useful debugging info. Defaults to false. * @param {string[]} [opts.dirTypes] - List of types. This type will be searched for in the filePath and is expected to be a full directory name. If not given, only page type will be used. * @param {string} [opts.modelsPath] - File path to the combined models js file, which can prepend your templates to provide model data. If not given, no models will be added. * @param {string} [cacheFileName] - Name of the json file output. Defaults to 'precompiled-vash.json'. */ function precompileTemplateCache(opts) { // sets default options opts = opts || {}; opts.debugMode = opts.debugMode || false; opts.dirTypes = opts.dirTypes || false; opts.modelsPath = opts.modelsPath || null; opts.cacheFileName = opts.cacheFileName || "precompiled-vash.json"; var tplcache = {} , count = 0; var bufferContents = function(file, enc, cb) { if (file.isNull()) { // return empty file return cb(null, file); } // we don't do streams if (file.isStream()) { this.emit('error', new PluginError(PLUGIN_NAME, 'Streaming not supported')); cb(); return; } opts.file = file.path; vashStatic.precompileTemplateCache(opts, function(success, data) { if(!success) { this.emit('error', new PluginError(PLUGIN_NAME, 'Problem with "Vash Static".', data.msg)); } else { tplcache[data.name] = data.contents; count++; } cb(); }); }; var endStream = function(cb) { var cwd = process.cwd(); var file = new gutil.File({ base: cwd, cwd: cwd, path: path.join(cwd, opts.cacheFileName) }); if (!count) { this.emit('error', new PluginError(PLUGIN_NAME, 'No files were precompiled!')); cb(); return; } file.contents = new Buffer(JSON.stringify(tplcache)) this.push(file); cb(); } // Creating a stream through which each file will pass var stream = through.obj(bufferContents, endStream); return stream; } /** * Renders a 'page' vash template by name, which should be stored in the cacheDest, with optional helpers. * @param {object} opts - Options for the function: * @param {string} opts.cacheDest - Path to the JSON file containing the vash template cache. * @param {string} [opts.omitSubDir] - If given, will remove the first occurance of a sub-directory with this name when generating the template name. * @param {string[]} [helpers] - Array of paths to use as Vash helpers. Defaults will be used, unless overridden by name. Otherwise both lists will be used. */ function renderPage(opts) { // sets default options opts = opts || {}; opts.cacheDest = opts.cacheDest || "./"; // Creating a stream through which each file will pass var stream = through.obj(function(file, enc, cb) { if (file.isNull()) { // return empty file return cb(null, file); } if (file.isStream()) throw new PluginError(PLUGIN_NAME, 'Plugin does not support streams!'); var pgTmplName = vashStatic.getModuleName(file.path, vashStatic.getPageDirType(), true) , renderCnf = vashStatic.renderPage(opts.cacheDest, pgTmplName, opts.helpers) if(renderCnf.success) { file.contents = new Buffer(renderCnf.contents) } else { throw new PluginError(PLUGIN_NAME, renderCnf.contents.join(", ") ); } // make sure we're changing the extention to an .html file, instead of .vash file.path = replaceExt(file.path, ".html") if(opts.omitSubDir) { opts.omitSubDir = opts.omitSubDir.split("/").join(""); file.path = slash(file.path).replace("/" + opts.omitSubDir + "/", "/"); //console.log("file.path", file.path) } // make sure the file goes through the next gulp plugin this.push(file); // tell the stream engine that we are done with this file cb(); }); return stream; } /** * Gets the correct schema for the 'template path', so it's always just got 1 source of truth. * @param {string} type - Type of module, such as 'pg', 'wg', 'glb'. * @param {string} moduleName - Name of the module (eg a page 'HomePage'' or a widget 'SiteFooter') * @param {string} [fileName] - File name of the template. Defaults to 'Index.vash' if not supplied. */ function getTemplatePathConfig(type, moduleName, fileName) { return { type: type , moduleName: moduleName , fileName: fileName || "Index.vash" } } /** * Convenience function for watching models and templates. All properties are mandatory. * @param {object} opts - Options for the function: * @param {object} opts.gulp - instance of gulp * @param {string} opts.vashSrc - vash templates to watch (accepts globbing) * @param {string} opts.modelSrc - models to watch (accepts globbing) * @param {string} opts.modelsDest - path to the models JS destination (once they are combined) * @param {string} opts.cacheDest - path to the template cache destination * @param {boolean} opts.debugMode - If this is for production, should be false * @param {string[]} opts.dirTypes - Module types (eg "pg", "wg", glb), which correspond to parent directory name of template modules. * @param {string} opts.pageTemplatePath - String for determining the path of the page-level template, where only the page name is known (eg "<%= type %>/<%= moduleName %>/tmpl/<%= fileName %>"). * @param {string} opts.combineModelsTask - Gulp task name for combining your models * @param {string} opts.precompileTask - Gulp task name for pre-compiling your vash templates * @param {string} opts.pageRenderTask - Gulp task name for rendering a page */ function watchModelsAndTemplates(opts) { var runSeq = runSequence.use(opts.gulp) var preSeq = []; if(opts.combineModelsTask) preSeq.push(opts.combineModelsTask); if(opts.precompileTask) preSeq.push(opts.precompileTask); if(opts.pageRenderTask) preSeq.push(opts.pageRenderTask); // just compiles everything once first before watching for changes if(preSeq.length) runSeq.apply(this, preSeq); // runs a watch task (using gulp-watch) that looks for changes to models and vash files var queue = [] , isUpdating = false; return watch(opts.vashSrc.concat(opts.modelSrc), function(vinyl) { var cacheAndRender = function(type, moduleName, contents, fileName) { // if currently updating the cache, put items in a queue for later if(isUpdating) { queue.push({ type:type, moduleName:moduleName, contents:contents, fileName:fileName }); return } isUpdating = true; vashStatic.updateCache({ type: type , tmplPath: _.template(opts.pageTemplatePath)(getTemplatePathConfig(type, moduleName, fileName)) , contents: contents , modelsPath: opts.modelsDest , cacheDest: opts.cacheDest , debugMode: opts.debugMode , cb: function() { isUpdating = false; // goes throug queue until none left if(queue.length) { var firstItem = queue.shift(); cacheAndRender(firstItem.type, firstItem.moduleName, firstItem.contents, firstItem.fileName); } else { // console.log("opts.pageRenderTask") runSeq(opts.pageRenderTask) } } }) } // checks if changed file was a vash object or a model if(vinyl.path.indexOf(".vash") !== -1) { // gets the details needed about the changed vash file from Vinyl object, then updates the template cache and page html var cnf = getVinylDetails(vinyl, opts.dirTypes) cacheAndRender(cnf.type, cnf.moduleName, vinyl.contents, cnf.fileName) } else { var pgNames = getAllArgs(); // gets the page name from the expected command-line argument and cancels task if invalid if(!pgNames.length) { warn(NS, "watchModelsAndTemplates", 'You need to pass page name in a flag like this "--home".') return false } var type = vashStatic.getPageDirType(); runSeq(opts.combineModelsTask, function() { pgNames.forEach(function(pgName) { // allows you to specify a fileName within the module var fileName = "Index.vash"; if(pgName.indexOf("/") !== -1) { fileName = pgName.split("/")[1] + ".vash"; pgName = pgName.split("/")[0]; } var pageFilePath = _.template(opts.pageTemplatePath)(getTemplatePathConfig(type, pgName, fileName)) if( !validatePageTemplate(pageFilePath) ) return // refreshes models.js by combining all models again, then updates the template cache and page html cacheAndRender(vashStatic.getPageDirType(), pgName, false, fileName); }); }) } }) } // Exporting the plugin main function module.exports = { suppressWarnings: sav.suppressWarnings , renderPage: renderPage , precompile: precompileTemplateCache , setPageDirType: vashStatic.setPageDirType , watchModelsAndTemplates: watchModelsAndTemplates , getAllArgs: getAllArgs , overrideGetAllArgs: overrideGetAllArgs , restoreGetAllArgs: function() { argFunctionOverride = null; } , testable: { regSlash: regSlash , validatePageTemplate: validatePageTemplate , getVinylDetails: getVinylDetails } }