diff --git a/.travis.yml b/.travis.yml index 931306d..972e22b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ sudo: false language: node_js node_js: -- '4.4.4' -- '6.0.0' +- '6.1.0' +- '7.1.0' cache: directories: - node_modules diff --git a/index.js b/index.js index c0d9eb2..07225e8 100644 --- a/index.js +++ b/index.js @@ -10,17 +10,21 @@ const requestFragment = require('./lib/request-fragment'); const PIPE_DEFINITION = fs.readFileSync(path.resolve(__dirname, 'src/pipe.min.js')); const AMD_LOADER_URL = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js'; + const stripUrl = (fileUrl) => path.normalize(fileUrl.replace('file://', '')); const getPipeAttributes = (attributes) => { - const primary = (attributes.primary || attributes.primary === '') ? true : false; - return { primary, id: attributes.id }; + const { primary, id } = attributes; + return { + primary: !!(primary || primary === ''), + id + }; }; module.exports = class Tailor extends EventEmitter { constructor (options) { super(); - const amdLoaderUrl = options.amdLoaderUrl || AMD_LOADER_URL; + const { amdLoaderUrl = AMD_LOADER_URL, templatesPath } = options; let memoizedDefinition; const pipeChunk = (amdLoaderUrl, pipeInstanceName) => { if (!memoizedDefinition) { @@ -34,24 +38,27 @@ module.exports = class Tailor extends EventEmitter { } return new Buffer(`${memoizedDefinition}var ${pipeInstanceName}=${PIPE_DEFINITION}\n`); }; + const requestOptions = Object.assign({ fetchContext: () => Promise.resolve({}), fetchTemplate: fetchTemplate( - options.templatesPath || + templatesPath || path.join(process.cwd(), 'templates') ), fragmentTag: 'fragment', handledTags: [], handleTag: () => '', requestFragment, - pipeDefinition: (pipeInstanceName) => pipeChunk(amdLoaderUrl, pipeInstanceName), + pipeDefinition: pipeChunk.bind(null, amdLoaderUrl), pipeInstanceName: () => 'Pipe', - pipeAttributes: (attributes) => getPipeAttributes(attributes) + pipeAttributes: getPipeAttributes }, options); + requestOptions.parseTemplate = parseTemplate( [requestOptions.fragmentTag].concat(requestOptions.handledTags), ['script', requestOptions.fragmentTag] ); + this.requestHandler = requestHandler.bind(this, requestOptions); // To Prevent from exiting the process - https://nodejs.org/api/events.html#events_error_events this.on('error', () => {}); diff --git a/lib/fetch-template.js b/lib/fetch-template.js index 400b1be..5a89509 100644 --- a/lib/fetch-template.js +++ b/lib/fetch-template.js @@ -10,13 +10,14 @@ const TEMPLATE_NOT_FOUND = 1; class TemplateError extends Error { constructor(...args) { super(...args); + this.code = TEMPLATE_ERROR; + this.presentable = 'template error'; + const [{ code }] = args; - let code = TEMPLATE_ERROR; - if (args.length > 0 && args[0].code === 'ENOENT') { - code = TEMPLATE_NOT_FOUND; + if (code === 'ENOENT') { + this.code = TEMPLATE_NOT_FOUND; this.presentable = 'template not found'; } - this.code = code; } } @@ -25,17 +26,16 @@ class TemplateError extends Error { * * @param {string} path */ -const readFile = (path) => { - return new Promise((resolve, reject) => { +const readFile = (path) => + new Promise((resolve, reject) => { fs.readFile(path, 'utf-8', (err, data) => { if (err) { reject(new TemplateError(err)); return; - } + } resolve(data); }); }); -}; /** * Fetches the template from File System @@ -43,10 +43,11 @@ const readFile = (path) => { * @param {string} templatesPath - The path where the templates are stored * @param {function=} baseTemplateFn - Function that returns the Base template name for a given page */ -module.exports = function fetchTemplate (templatesPath, baseTemplateFn) { - return (request, parseTemplate) => { +module.exports = (templatesPath, baseTemplateFn) => + (request, parseTemplate) => { const pathname = url.parse(request.url, true).pathname; const templatePath = path.join(templatesPath, pathname) + '.html'; + return readFile(templatePath) .then((baseTemplate) => { if (typeof baseTemplateFn !== 'function') { @@ -61,10 +62,9 @@ module.exports = function fetchTemplate (templatesPath, baseTemplateFn) { const pageTemplate = baseTemplate; const baseTemplatePath = path.join(templatesPath, templateName) + '.html'; return readFile(baseTemplatePath) - .then(baseTemplate => parseTemplate(baseTemplate, pageTemplate)); + .then((baseTemplate) => parseTemplate(baseTemplate, pageTemplate)); }); }; -}; module.exports.TEMPLATE_ERROR = TEMPLATE_ERROR; module.exports.TEMPLATE_NOT_FOUND = TEMPLATE_NOT_FOUND; diff --git a/lib/filter-headers.js b/lib/filter-headers.js index 5295d35..02cc722 100644 --- a/lib/filter-headers.js +++ b/lib/filter-headers.js @@ -1,5 +1,5 @@ 'use strict'; - +const ACCEPT_HEADERS = ['accept-language', 'referer', 'user-agent']; /** * Filter the request headers that are passed to fragment request. * @@ -8,15 +8,10 @@ * @param {Object} headers - Request header object * @returns {Object} New filtered header object */ -module.exports = function filterHeaders (attributes, headers) { - const newHeaders = {}; - if (attributes.public) { - return newHeaders; - }; - ['accept-language', 'referer', 'user-agent'].forEach((key) => { - if (headers[key]) { - newHeaders[key] = headers[key]; - } - }); - return newHeaders; -}; +module.exports = ({ public: publicFragment }, headers) => + publicFragment + ? {} + : ACCEPT_HEADERS.reduce((newHeaders, key) => { + headers[key] && (newHeaders[key] = headers[key]); + return newHeaders; + }, {}); diff --git a/lib/parse-template.js b/lib/parse-template.js index caca715..1e80bfb 100644 --- a/lib/parse-template.js +++ b/lib/parse-template.js @@ -8,10 +8,9 @@ const Transform = require('./transform'); * @param {Array} insertBeforePipeTags - Pipe definition will be inserted before these tags * @returns {Promise} Promise that resolves to serialized array consisits of buffer and fragment objects */ -module.exports = function parseTemplate (handledTags, insertBeforePipeTags) { - return (baseTemplate, childTemplate) => new Promise((resolve) => { +module.exports = (handledTags, insertBeforePipeTags) => + (baseTemplate, childTemplate) => new Promise((resolve) => { const transform = new Transform(handledTags, insertBeforePipeTags); const serializedList = transform.applyTransforms(baseTemplate, childTemplate); resolve(serializedList); }); -}; diff --git a/lib/request-fragment.js b/lib/request-fragment.js index 689fc3b..ab70029 100644 --- a/lib/request-fragment.js +++ b/lib/request-fragment.js @@ -19,8 +19,8 @@ const requiredHeaders = { * @param {Object} request - HTTP request stream * @returns {Promise} Response from the fragment server */ -module.exports = function requestFragment (fragmentUrl, fragmentAttributes, request) { - return new Promise((resolve, reject) => { +module.exports = (fragmentUrl, fragmentAttributes, request) => + new Promise((resolve, reject) => { const parsedUrl = url.parse(fragmentUrl); const options = Object.assign({ headers: Object.assign( @@ -30,10 +30,11 @@ module.exports = function requestFragment (fragmentUrl, fragmentAttributes, requ keepAlive: true, timeout: fragmentAttributes.timeout }, parsedUrl); - const protocol = options.protocol === 'https:' ? https : http; + const { protocol: reqProtocol, timeout } = options; + const protocol = reqProtocol === 'https:' ? https : http; const fragmentRequest = protocol.request(options); - if (options.timeout) { - fragmentRequest.setTimeout(options.timeout, () => fragmentRequest.abort()); + if (timeout) { + fragmentRequest.setTimeout(timeout, fragmentRequest.abort); } fragmentRequest.on('response', (response) => { if (response.statusCode >= 500) { @@ -42,7 +43,6 @@ module.exports = function requestFragment (fragmentUrl, fragmentAttributes, requ resolve(response); } }); - fragmentRequest.on('error', (e) => reject(e)); + fragmentRequest.on('error', reject); fragmentRequest.end(); - }); -}; + }); \ No newline at end of file diff --git a/lib/request-handler.js b/lib/request-handler.js index e962385..7e4a81e 100644 --- a/lib/request-handler.js +++ b/lib/request-handler.js @@ -4,7 +4,7 @@ const Fragment = require('./fragment'); const StringifierStream = require('./streams/stringifier-stream'); const ContentLengthStream = require('./streams/content-length-stream'); const FRAGMENT_EVENTS = ['start', 'response', 'end', 'error', 'timeout', 'fallback', 'warn']; -const TEMPLATE_NOT_FOUND = require('./fetch-template').TEMPLATE_NOT_FOUND; +const { TEMPLATE_NOT_FOUND } = require('./fetch-template'); /** * Process the HTTP Request to the Tailor Middleware @@ -16,14 +16,10 @@ const TEMPLATE_NOT_FOUND = require('./fetch-template').TEMPLATE_NOT_FOUND; module.exports = function processRequest (options, request, response) { this.emit('start', request); - const fetchContext = options.fetchContext; - const fetchTemplate = options.fetchTemplate; - const handleTag = options.handleTag; - const parseTemplate = options.parseTemplate; - const requestFragment = options.requestFragment; - const pipeInstanceName = options.pipeInstanceName(); - const pipeDefinition = options.pipeDefinition(pipeInstanceName); - const pipeAttributes = options.pipeAttributes; + const { fetchContext, fetchTemplate, handleTag, + parseTemplate, requestFragment, fragmentTag, + pipeAttributes, pipeInstanceName, pipeDefinition} = options; + const pipeName = pipeInstanceName(); const asyncStream = new AsyncStream(); const contextPromise = fetchContext(request).catch((err) => { @@ -48,23 +44,23 @@ module.exports = function processRequest (options, request, response) { }); const resultStream = new StringifierStream((tag) => { - - if (tag.placeholder === 'pipe') { - return pipeDefinition; + const { placeholder, name, } = tag; + if (placeholder === 'pipe') { + return pipeDefinition(pipeName); } - if (tag.placeholder === 'async') { + if (placeholder === 'async') { // end of body tag return asyncStream; } - if (tag.name === options.fragmentTag) { + if (name === fragmentTag) { const fragment = new Fragment( tag, context, index++, requestFragment, - pipeInstanceName, + pipeName, pipeAttributes ); @@ -75,11 +71,13 @@ module.exports = function processRequest (options, request, response) { }); }); - if (fragment.attributes.async) { - asyncStream.write(fragment.stream); + const { attributes: { async, primary }, stream} = fragment; + + if (async) { + asyncStream.write(stream); } - if (fragment.attributes.primary && shouldWriteHead) { + if (primary && shouldWriteHead) { shouldWriteHead = false; fragment.on('response', (statusCode, headers) => { if (headers.location) { diff --git a/lib/serializer.js b/lib/serializer.js index 53f378d..6b308e2 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -55,7 +55,7 @@ module.exports = class CustomSerializer extends Serializer { * @returns {Boolean} */ _isPipeNode(node) { - return this.pipeTags.indexOf(node.name) !== -1; + return this.pipeTags.includes(node.name); } /** @@ -64,12 +64,9 @@ module.exports = class CustomSerializer extends Serializer { * @returns {Boolean} */ _isSlotNode(node) { - if (node.name === 'slot') { - return true; - } - const attribs = node.attribs; - return node.name === 'script' && - attribs && attribs.type === 'slot'; + const { attribs, name } = node; + return (name === 'slot') || + (name === 'script' && attribs && attribs.type === 'slot'); } /** @@ -78,14 +75,10 @@ module.exports = class CustomSerializer extends Serializer { * @returns {Boolean} */ _isSpecialNode(node) { - if (this.handleTags.indexOf(node.name) !== -1) { - return true; - } - const attribs = node.attribs; - if (attribs && attribs.type) { - return node.name === 'script' && this.handleTags.indexOf(attribs.type) !== -1; - } - return false; + const { attribs, name } = node; + return this.handleTags.includes(name) || + !!(attribs && attribs.type && name === 'script' + && this.handleTags.includes(attribs.type)); } /** @@ -94,13 +87,8 @@ module.exports = class CustomSerializer extends Serializer { * @returns {Boolean} */ _isLastChildOfBody(node) { - const parentNode = node.parent; - if (parentNode.name === 'body') { - if (Object.is(node, parentNode.lastChild)) { - return true; - } - } - return false; + const { parentNode: { name, lastChild } } = node; + return !!(name === 'body' && Object.is(node, lastChild)); } /** @@ -122,16 +110,13 @@ module.exports = class CustomSerializer extends Serializer { */ _serializeSpecial(node) { this.pushBuffer(); - let fragmentObj; - if (this.handleTags.indexOf(node.name) !== -1) { - fragmentObj = Object.assign({}, { name: node.name, attributes: node.attribs}); - } else { - fragmentObj = Object.assign({}, { name: node.attribs.type, attributes: node.attribs}); - } + const { name: nodeName, attribs: attributes } = node; + const name = this.handleTags.includes(nodeName) ? nodeName : attributes.type; + const fragmentObj = Object.assign({}, { name, attributes }); this.serializedList.push(fragmentObj); this._serializeChildNodes(node); this.pushBuffer(); - this.serializedList.push({ closingTag: node.name }); + this.serializedList.push({ closingTag: nodeName }); } /** @@ -143,10 +128,7 @@ module.exports = class CustomSerializer extends Serializer { const slotName = node.attribs.name; if (slotName) { const childNodes = this.treeAdapter.getChildNodes(node); - let slots = childNodes; - if (this.slotMap.has(slotName)) { - slots = this.slotMap.get(slotName); - } + const slots = this.slotMap.has(slotName) ? this.slotMap.get(slotName) : childNodes; slots && slots.forEach(this._serializeNode); } else { // Handling duplicate slots @@ -194,9 +176,7 @@ module.exports = class CustomSerializer extends Serializer { */ _serializeChildNodes(parentNode) { const childNodes = this.treeAdapter.getChildNodes(parentNode); - if (childNodes) { - childNodes.forEach(this._serializeNode); - } + childNodes && childNodes.forEach(this._serializeNode); } /** diff --git a/lib/transform.js b/lib/transform.js index 6a554cb..de11909 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -1,7 +1,7 @@ 'use strict'; const parse5 = require('parse5'); -const adapter = parse5.treeAdapters.htmlparser2; +const treeAdapter = parse5.treeAdapters.htmlparser2; const CustomSerializer = require('./serializer'); /** @@ -24,15 +24,13 @@ module.exports = class Transform { * @returns {Array} Array consiting of Buffers and Objects */ applyTransforms(baseTemplate, childTemplate) { - let rootNodes = parse5.parse(baseTemplate, { treeAdapter: adapter }); - let slotMap = new Map(); - if (childTemplate && typeof childTemplate === 'string') { - const childNodes = parse5.parseFragment(childTemplate, { treeAdapter: adapter }); - slotMap = this._groupSlots(childNodes); - } + const rootNodes = parse5.parse(baseTemplate, { treeAdapter }); + const slotMap = childTemplate && typeof childTemplate === 'string' + ? this._groupSlots(parse5.parseFragment(childTemplate, { treeAdapter })) + : new Map(); const serializerOptions = { - treeAdapter : adapter, - slotMap: slotMap, + treeAdapter, + slotMap, pipeTags: this.pipeTags, handleTags: this.handleTags }; @@ -47,25 +45,16 @@ module.exports = class Transform { * @returns {Map} Map with keys as slot attribute name and corresponding values consisting of array of matching nodes */ _groupSlots(root) { - const slotMap = new Map(); - slotMap.set('default', []); - const nodes = adapter.getChildNodes(root); + const slotMap = new Map([['default', []]]); + const nodes = treeAdapter.getChildNodes(root); nodes.forEach((node) => { - if (adapter.isTextNode(node)) { - return; - } - const attribs = node.attribs; - if (attribs && attribs.slot) { - if (slotMap.has(attribs.slot)) { - slotMap.get(attribs.slot).push(node); - } else { - slotMap.set(attribs.slot, [node]); - } - this._pushText(node.next, slotMap.get(attribs.slot)); - delete attribs.slot; - } else { - slotMap.get('default').push(node); - this._pushText(node.next, slotMap.get('default')); + if (!treeAdapter.isTextNode(node)) { + const { slot = 'default' } = node.attribs; + const slotNodes = slotMap.get(slot) || []; + const updatedSlotNodes = [...slotNodes, node]; + slotMap.set(slot, updatedSlotNodes); + this._pushText(node.next, updatedSlotNodes); + delete node.attribs.slot; } }); return slotMap; @@ -78,7 +67,7 @@ module.exports = class Transform { * @param {Array} slot - Array of matching nodes */ _pushText(nextNode, slot) { - if (nextNode && adapter.isTextNode(nextNode)) { + if (nextNode && treeAdapter.isTextNode(nextNode)) { slot.push(nextNode); } } diff --git a/package.json b/package.json index a6a15b3..814b99d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "test" ], "engines": { - "node": ">4.4.3" + "node": ">6.0.0" }, "repository": { "type": "git", diff --git a/tests/tailor.events.js b/tests/tailor.events.js index d3f2e09..ac0c153 100644 --- a/tests/tailor.events.js +++ b/tests/tailor.events.js @@ -139,7 +139,9 @@ describe('Tailor events', () => { it('emits `context:error(request, error)` event', (done) => { const onContextError = sinon.spy(); - mockContext.returns(Promise.reject('Error fetching context')); + const rejectPrm = Promise.reject('Error fetching context'); + rejectPrm.catch(() => {}); + mockContext.returns(rejectPrm); tailor.on('context:error', onContextError); http.get('http://localhost:8080/template', (response) => { const request = onContextError.args[0][0];