diff --git a/lib/fragment.js b/lib/fragment.js index 6d1bdb0..2bd54fd 100644 --- a/lib/fragment.js +++ b/lib/fragment.js @@ -1,10 +1,10 @@ 'use strict'; -const ContentLengthStream = require('./streams/content-length-stream'); const EventEmitter = require('events').EventEmitter; const PassThrough = require('stream').PassThrough; -const LinkHeader = require('http-link-header'); const zlib = require('zlib'); +const ContentLengthStream = require('./streams/content-length-stream'); +const parseLinkHeader = require('./parse-link-header'); const { globalTracer, Tags } = require('opentracing'); const tracer = globalTracer(); @@ -16,6 +16,20 @@ const hasValue = value => { return false; }; +const getFragmentAssetUris = (refs, assetSize) => { + const scriptUris = []; + const styleUris = []; + + for (const ref of refs) { + if (ref.rel === 'fragment-script') { + scriptUris.push(ref.uri); + } else if (ref.rel === 'stylesheet') { + styleUris.push(ref.uri); + } + } + return [scriptUris.slice(0, assetSize), styleUris.slice(0, assetSize)]; +}; + /** * Merge the attributes based on the fragment tag attributes and context * @@ -84,6 +98,8 @@ module.exports = class Fragment extends EventEmitter { this.requestFragment = requestFragment; this.pipeInstanceName = pipeInstanceName; this.stream = new PassThrough(); + this.scriptRefs = []; + this.styleRefs = []; } /** @@ -165,22 +181,21 @@ module.exports = class Fragment extends EventEmitter { onResponse(response, isFallback, span) { const { statusCode, headers } = response; - if (!isFallback) { - this.emit('response', statusCode, headers); - } // Extract the assets from fragment link headers. - const { refs } = LinkHeader.parse( + const refs = parseLinkHeader( [headers.link, headers['x-amz-meta-link']].join(',') ); - this.scriptRefs = refs - .filter(ref => ref.rel === 'fragment-script') - .slice(0, this.maxAssetLinks) - .map(ref => ref.uri); - this.styleRefs = refs - .filter(ref => ref.rel === 'stylesheet') - .slice(0, this.maxAssetLinks) - .map(ref => ref.uri); + if (refs.length > 0) { + [this.scriptRefs, this.styleRefs] = getFragmentAssetUris( + refs, + this.maxAssetLinks + ); + } + + if (!isFallback) { + this.emit('response', statusCode, headers); + } this.insertStart(); diff --git a/lib/parse-link-header.js b/lib/parse-link-header.js new file mode 100644 index 0000000..a671a2f --- /dev/null +++ b/lib/parse-link-header.js @@ -0,0 +1,51 @@ +'use strict'; +/** + * Parse link headers + * '; rel="fragment-script"' + * + * [ + * { + * rel: "fragment-script", + * uri: "http://localhost:8080/script.js" + * } + * ] + * + * Based on code from parse-link-header! + * https://github.com/thlorenz/parse-link-header/blob/master/index.js + */ +module.exports = function parseLinkHeader(linkHeader) { + const assets = linkHeader + .split(/,\s* { + const match = link.match(/]*)>(.*)/); + if (!match) { + return null; + } + const linkUrl = match[1]; + const parts = match[2].split(';'); + parts.shift(); + return { + uri: linkUrl, + rel: getRelValue(parts[0]) + }; + }) + .filter(v => v && v.rel != null) + .reduce((acc, curr) => { + return acc.concat(curr); + }, []); + + return assets; +}; + +/** + * Get the value of rel attribute + * + * rel="fragment-script" -> ["rel", "fragment-script"] + */ +function getRelValue(parts) { + const m = parts.match(/\s*(.+)\s*=\s*"?([^"]+)"?/); + if (!m) { + return null; + } + return m[2]; +} diff --git a/lib/request-handler.js b/lib/request-handler.js index 54723a5..12a1e05 100644 --- a/lib/request-handler.js +++ b/lib/request-handler.js @@ -127,7 +127,8 @@ module.exports = function processRequest(options, request, response) { // Make resources early discoverable while processing HTML const assetsToPreload = getFragmentAssetsToPreload( - headers, + fragment.styleRefs, + fragment.scriptRefs, request.headers ); @@ -218,9 +219,7 @@ module.exports = function processRequest(options, request, response) { resultStream.once('error', handleError); - parsedTemplate.forEach(item => { - resultStream.write(item); - }); + parsedTemplate.forEach(item => resultStream.write(item)); resultStream.end(); }) .catch(err => { diff --git a/lib/utils.js b/lib/utils.js index 1aafbb4..5f8f906 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,7 +1,5 @@ 'use strict'; -const LinkHeader = require('http-link-header'); - const getCrossOrigin = (url = '', host = '') => { if (url.includes(`://${host}`)) { return ''; @@ -39,21 +37,11 @@ const getLoaderScript = (amdLoaderUrl, { host } = {}) => { }; // Early preloading of primary fragments assets to improve Performance -const getFragmentAssetsToPreload = (headers, { host } = {}) => { +const getFragmentAssetsToPreload = (styleRefs, scriptRefs, { host } = {}) => { let assetsToPreload = []; - const { refs = [] } = LinkHeader.parse( - [headers.link, headers['x-amz-meta-link']].join(',') - ); - const scriptRefs = refs - .filter(ref => ref.rel === 'fragment-script') - .map(ref => ref.uri); - const styleRefs = refs - .filter(ref => ref.rel === 'stylesheet') - .map(ref => ref.uri); - // Handle Server rendered fragments without depending on assets - if (!scriptRefs[0] && !styleRefs[0]) { + if (scriptRefs.length === 0 && styleRefs.length === 0) { return assetsToPreload; } diff --git a/package.json b/package.json index f0a1dc2..3c3e2b0 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "author": "Andrey Kuzmin", "license": "MIT", "dependencies": { - "http-link-header": "^0.8.0", "opentracing": "^0.14.3", "parse5": "^3.0.3", "util.promisify": "^1.0.0" diff --git a/tests/parse-link-header.js b/tests/parse-link-header.js new file mode 100644 index 0000000..552ac2f --- /dev/null +++ b/tests/parse-link-header.js @@ -0,0 +1,49 @@ +'use strict'; + +const parseLinkHeader = require('../lib/parse-link-header'); +const assert = require('assert'); + +describe('Parse Link Header', () => { + it('returns uri and rel of the passed header', () => { + const linkHeader = + '; rel="script",; rel="stylesheet"'; + + assert.deepStrictEqual(parseLinkHeader(linkHeader), [ + { uri: 'http://a.com/app.js', rel: 'script' }, + { uri: 'http://a.com/app.css', rel: 'stylesheet' } + ]); + }); + + it('ignore attributes other than rel and uri', () => { + const linkHeader = + '; rel="script"; crossorigin="anonymous"'; + + assert.deepStrictEqual(parseLinkHeader(linkHeader), [ + { uri: 'http://a.com/app.js', rel: 'script' } + ]); + }); + + it('filters invalid header links', () => { + const linkHeader = 'http://a.com/app.js; rel="script"'; + + assert.deepStrictEqual(parseLinkHeader(linkHeader), []); + }); + + it('filters invalid rel attributes', () => { + const linkHeader = + '; rel="script";, ; rel="stylesheet", ;, ; rel=""'; + + assert.deepStrictEqual(parseLinkHeader(linkHeader), [ + { uri: 'http://a.com/app.js', rel: 'script' }, + { uri: 'http://a.com/app1.css', rel: 'stylesheet' } + ]); + }); + + it('do not modify query parms in link urls', () => { + const linkHeader = '; rel="script";'; + + assert.deepStrictEqual(parseLinkHeader(linkHeader), [ + { uri: 'http://a.com/app.js?nocache=1', rel: 'script' } + ]); + }); +}); diff --git a/tests/tailor.js b/tests/tailor.js index beb7351..9a4dbe2 100644 --- a/tests/tailor.js +++ b/tests/tailor.js @@ -388,7 +388,6 @@ describe('Tailor', () => { mockTemplate.reset(); withFile.close(done); }); - it('should preload external module loader if fragment is present', done => { nock('https://fragment') .get('/1') diff --git a/yarn.lock b/yarn.lock index 3ad97d1..42573b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -878,10 +878,6 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" -http-link-header@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-0.8.0.tgz#a22b41a0c9b1e2d8fac1bf1b697c6bd532d5f5e4" - http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"