From a4170f24db71dd428dcad4e7313a17f8de97f755 Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Fri, 12 Aug 2022 14:29:29 -0500 Subject: [PATCH 1/7] [STRF-9950] paper-handlebars - stylesheet helper early-hints flag new param --- helpers/lib/resourceHints.js | 64 ++++++++++++++ helpers/stylesheet.js | 20 +++-- index.js | 11 ++- spec/helpers/lib/resourceHints.js | 140 ++++++++++++++++++++++++++++++ spec/helpers/stylesheet.js | 79 ++++++++++++----- 5 files changed, 281 insertions(+), 33 deletions(-) create mode 100644 helpers/lib/resourceHints.js create mode 100644 spec/helpers/lib/resourceHints.js diff --git a/helpers/lib/resourceHints.js b/helpers/lib/resourceHints.js new file mode 100644 index 0000000..5cac419 --- /dev/null +++ b/helpers/lib/resourceHints.js @@ -0,0 +1,64 @@ +/* eslint-disable curly */ +const _ = require("lodash"); +const utils = require("handlebars-utils"); + +const resourceHintsLimit = 50; + +const defaultResourceHintSate = 'preload'; + +const resourceHintStates = [defaultResourceHintSate, 'preconnect', 'prefetch']; + +const resourceHintFontType = 'font'; +const resourceHintStyleType = 'style'; +const resourceHintScriptType = 'script'; +const resourceHintAllowedTypes = [resourceHintStyleType, resourceHintFontType, resourceHintScriptType]; + +/** + * @param {string} path - The uri to the resource. + * @param {string} state - 'preload' or 'preconnect' + * @param {string} type - any of [style, font, script] + */ +function addResourceHint(globals, path, state, type) { + + path = _.trim(path); + if (path === '') { + return; + } + + state = _.trim(state); + if (!resourceHintStates.includes(state)) { + return; + } + + if (typeof globals.resourceHints === 'undefined') { + globals.resourceHints = []; + } + + path = utils.escapeExpression(path); + let index = globals.resourceHints.findIndex(({src}) => path === src); + if (index >= 0) { + return; + } + + if (globals.resourceHints.length >= resourceHintsLimit) { + return; + } + + let value = Object.create({}); + Object.defineProperty(value, 'src', {value: path, writable: false}); + Object.defineProperty(value, 'state', {value: state, writable: false}); + + type = _.trim(type); + if (type !== '' && _.includes(resourceHintAllowedTypes, type)) { + Object.defineProperty(value, 'type', {value: type, writable: false}); + } + + globals.resourceHints.push(value); +} + +module.exports = { + resourceHintsLimit, + defaultResourceHintSate, + addResourceHint, + resourceHintAllowedTypes: {resourceHintStyleType, resourceHintFontType, resourceHintScriptType} +}; diff --git a/helpers/stylesheet.js b/helpers/stylesheet.js index 1c3e5ef..bacaf3b 100644 --- a/helpers/stylesheet.js +++ b/helpers/stylesheet.js @@ -2,9 +2,10 @@ const _ = require('lodash'); const buildCDNHelper = require('./lib/cdnify'); +const ResourceHints = require('./lib/resourceHints'); const factory = globals => { - return function(assetPath) { + return function (assetPath) { const cdnify = buildCDNHelper(globals); const siteSettings = globals.getSiteSettings(); const configId = siteSettings.theme_config_id; @@ -18,12 +19,19 @@ const factory = globals => { const url = cdnify(path); - let attrs = { rel: 'stylesheet' }; - + if (_.has(options.hash, 'resourceHint')) { + ResourceHints.addResourceHint( + globals, + url, + options.hash['resourceHint'], + ResourceHints.resourceHintAllowedTypes.resourceHintStyleType + ); + delete options.hash.resourceHint; + } + + let attrs = {rel: 'stylesheet'}; Object.assign(attrs, options.hash); - - attrs = _.map(attrs, (value, key) => `${key}="${value}"`).join( ' '); - + attrs = _.map(attrs, (value, key) => `${key}="${value}"`).join(' '); return ``; }; }; diff --git a/index.js b/index.js index 47dc05c..a868c2f 100644 --- a/index.js +++ b/index.js @@ -66,6 +66,7 @@ class HandlebarsRenderer { getTranslator: this.getTranslator.bind(this), getContent: this.getContent.bind(this), storage: {}, // global storage used by helpers to keep state + resourceHints: [] }; // Register helpers with Handlebars @@ -75,6 +76,10 @@ class HandlebarsRenderer { } } + getResourceHints() { + return this.helperContext.resourceHints; + } + /** * Set the paper.Translator instance used to translate strings in helpers. * @@ -293,7 +298,7 @@ class HandlebarsRenderer { * * @param {String} template * @param {Object} context - * @return {String} + * @return {Promise} * @throws [CompileError|RenderError] */ renderString(template, context) { @@ -342,8 +347,8 @@ class HandlebarsRenderer { } /** - * - * @param {String} level + * + * @param {String} level */ setLoggerLevel(level) { this.handlebars.logger.level = level; diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js new file mode 100644 index 0000000..737b18f --- /dev/null +++ b/spec/helpers/lib/resourceHints.js @@ -0,0 +1,140 @@ +const _ = require('lodash'); +const Code = require('code'), + expect = Code.expect; +const Sinon = require('sinon'); +const Lab = require('lab'), + lab = exports.lab = Lab.script(), + describe = lab.experiment, + it = lab.it, + beforeEach = lab.beforeEach, + afterEach = lab.afterEach; +const ResourceHints = require('../../../helpers/lib/resourceHints'); + +describe('resource hints', function () { + + describe('addResourceHint', function () { + + let sandbox; + + beforeEach(done => { + sandbox = Sinon.createSandbox(); + done(); + }); + + afterEach(done => { + sandbox.restore(); + done(); + }); + + it("does work as expected with valid params", (done) => { + + let globals = {resourceHints: []}; + sandbox.spy(globals.resourceHints, 'push'); + + let src = '/my/styles.css'; + let type = 'style'; + let state = 'preload'; + let expected = {src, state, type}; + + ResourceHints.addResourceHint(globals, src, state, type); + + expect(globals.resourceHints.push.called).to.equals(true); + expect(globals.resourceHints).to.have.length(1); + let result = _.head(globals.resourceHints); + expect(result).to.equals(expected); + + done(); + }); + + it('does not add a hint when provided path is invalid', (done) => { + let globals = {resourceHints: []}; + sandbox.spy(globals.resourceHints, 'push'); + + [undefined, null, ''].map(s => { + ResourceHints.addResourceHint( + global, + s, + ResourceHints.defaultResourceHintSate, + ResourceHints.resourceHintAllowedTypes.resourceHintStyleType + ); + }); + + expect(globals.resourceHints.push.notCalled).to.equals(true); + + done(); + }); + + it('does create a hint when no type is provided', (done) => { + let globals = {resourceHints: []}; + sandbox.spy(globals.resourceHints, 'push'); + + ResourceHints.addResourceHint( + globals, + 'https://my.asset.css', + ResourceHints.defaultResourceHintSate + ); + + expect(globals.resourceHints.push.calledOnce).to.equals(true); + let hint = _.head(globals.resourceHints); + expect(hint.type).to.not.exist(); + + done(); + }); + + it('does not create a hint when provided state param is not supported', (done) => { + let globals = {resourceHints: []}; + sandbox.spy(globals.resourceHints, 'push'); + + ResourceHints.addResourceHint( + globals, + '/styles.css', + 'not-supported' + ); + + expect(globals.resourceHints.push.notCalled).to.equals(true); + + done(); + }); + + it('does not create duplicate, by src, hints', (done) => { + let globals = {resourceHints: []}; + sandbox.spy(globals.resourceHints, 'push'); + + let src = '/my/styles.css'; + let type = 'style'; + let state = 'preload'; + + for (let i = 0; i < 5; i++) { + ResourceHints.addResourceHint( + globals, + src, + state, + type + ); + } + + expect(globals.resourceHints.push.calledOnce).to.equals(true); + expect(globals.resourceHints).to.have.length(1); + + done(); + }); + + it('does not create any hint when the limit of allowed hints was reached', (done) => { + let filled = _.fill(Array(50), 1); + let globals = {resourceHints: filled}; + sandbox.spy(globals.resourceHints, 'push'); + + ResourceHints.addResourceHint( + globals, + '/my/styles.css', + 'style', + 'preload' + ); + + expect(globals.resourceHints.push.notCalled).to.equals(true); + + done(); + }); + + }); +}); diff --git a/spec/helpers/stylesheet.js b/spec/helpers/stylesheet.js index badd4d4..79fb115 100644 --- a/spec/helpers/stylesheet.js +++ b/spec/helpers/stylesheet.js @@ -1,8 +1,11 @@ +const _ = require('lodash'); const Lab = require('lab'), - lab = exports.lab = Lab.script(), - describe = lab.experiment, - it = lab.it, - testRunner = require('../spec-helpers').testRunner; + lab = exports.lab = Lab.script(), + describe = lab.experiment, + it = lab.it; +const {buildRenderer, testRunner} = require("../spec-helpers"); +const {expect} = require("code"); +const resourceHintStyleType = require("../../helpers/lib/resourceHints").resourceHintAllowedTypes.resourceHintStyleType; describe('stylesheet helper', () => { const siteSettings = { @@ -14,12 +17,27 @@ describe('stylesheet helper', () => { const runTestCases = testRunner({siteSettings}); it('should render a link tag with the cdn ulr and stencil-stylesheet data tag', done => { - runTestCases([ - { - input: '{{{stylesheet "assets/css/style.css"}}}', - output: '', - }, - ], done); + + const template = '{{{stylesheet "assets/css/style.css" resourceHint="preload"}}}'; + const expected = ''; + + const renderer = buildRenderer(siteSettings); + renderer.renderString(template, {}) + .then(r => { + expect(r).to.equals(expected); + + const hints = renderer.getResourceHints(); + expect(hints.length).to.equals(1); + + _.each(hints, function ({src, state, type}) { + expect(src).to.startWith(siteSettings.cdn_url); + expect(type).to.equals(resourceHintStyleType); + expect(state).to.equals('preload'); + }); + + done(); + }) + .catch(done); }); it('should render a link tag and all extra attributes with no cdn url', done => { @@ -32,23 +50,36 @@ describe('stylesheet helper', () => { ], done); }); - it('should render a link with empty href', done => { - runTestCases([ - { - input: '{{{stylesheet "" }}}', - output: '', - }, - ], done); + it('should render a link with empty href and no resource hint', done => { + const template = '{{{stylesheet "" }}}'; + + const renderer = buildRenderer(siteSettings); + renderer.renderString(template, {}) + .then(r => { + expect(r).to.equals(''); + const hints = renderer.getResourceHints(); + expect(hints).to.have.length(0); + done(); + }) + .catch(done); }); it('should add configId to the filename', done => { - runTestCases([ - { - input: '{{{stylesheet "assets/css/style.css" }}}', - output: '', - siteSettings: { theme_config_id: 'foo' }, - }, - ], done); + const template = '{{{stylesheet "assets/css/style.css" resourceHint="preconnect" }}}'; + const src = '/assets/css/style-foo.css'; + const output = ``; + + const handlebarsRenderer = buildRenderer({theme_config_id: 'foo'}); + handlebarsRenderer.renderString(template, {}) + .then(r => { + expect(r).to.equals(output); + const hint = _.head(handlebarsRenderer.getResourceHints()); + expect(hint.src).to.equals(src); + expect(hint.state).to.equals('preconnect'); + + done(); + }) + .catch(done); }); it('should not append configId if the file is not in assets/css/ directory', done => { From 22936839ab80025d56a2238418b0cc71afe774f3 Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Mon, 15 Aug 2022 12:20:54 -0500 Subject: [PATCH 2/7] [STRF-9950] paper-handlebars - stylesheet helper early-hints flag new param PR fixes --- helpers/lib/resourceHints.js | 33 ++++++++------ helpers/stylesheet.js | 6 +-- spec/helpers/lib/resourceHints.js | 71 ++++++++++++++++++------------- spec/helpers/stylesheet.js | 70 +++++++++++++++--------------- 4 files changed, 97 insertions(+), 83 deletions(-) diff --git a/helpers/lib/resourceHints.js b/helpers/lib/resourceHints.js index 5cac419..a4564af 100644 --- a/helpers/lib/resourceHints.js +++ b/helpers/lib/resourceHints.js @@ -1,12 +1,10 @@ -/* eslint-disable curly */ -const _ = require("lodash"); const utils = require("handlebars-utils"); const resourceHintsLimit = 50; -const defaultResourceHintSate = 'preload'; +const defaultResourceHintState = 'preload'; -const resourceHintStates = [defaultResourceHintSate, 'preconnect', 'prefetch']; +const resourceHintStates = [defaultResourceHintState, 'preconnect', 'prefetch']; const resourceHintFontType = 'font'; const resourceHintStyleType = 'style'; @@ -20,12 +18,18 @@ const resourceHintAllowedTypes = [resourceHintStyleType, resourceHintFontType, r */ function addResourceHint(globals, path, state, type) { - path = _.trim(path); + if (typeof path !== 'string') { + throw new Error('resourceHint attribute require a valid URI'); + } + path = path.trim(); if (path === '') { - return; + throw new Error('resourceHint received an empty path'); } - state = _.trim(state); + if (typeof state !== 'string') { + throw new Error(`resourceHint attribute received invalid value. Valid values are: ${resourceHintStates}`); + } + state = state.trim(); if (!resourceHintStates.includes(state)) { return; } @@ -44,13 +48,14 @@ function addResourceHint(globals, path, state, type) { return; } - let value = Object.create({}); - Object.defineProperty(value, 'src', {value: path, writable: false}); - Object.defineProperty(value, 'state', {value: state, writable: false}); + let value = {src: path, state}; - type = _.trim(type); - if (type !== '' && _.includes(resourceHintAllowedTypes, type)) { - Object.defineProperty(value, 'type', {value: type, writable: false}); + if (typeof type !== 'string') { + type = ''; + } + type = type.trim(); + if (type !== '' && resourceHintAllowedTypes.includes(type)) { + value.type = type; } globals.resourceHints.push(value); @@ -58,7 +63,7 @@ function addResourceHint(globals, path, state, type) { module.exports = { resourceHintsLimit, - defaultResourceHintSate, + defaultResourceHintSate: defaultResourceHintState, addResourceHint, resourceHintAllowedTypes: {resourceHintStyleType, resourceHintFontType, resourceHintScriptType} }; diff --git a/helpers/stylesheet.js b/helpers/stylesheet.js index bacaf3b..9de2a10 100644 --- a/helpers/stylesheet.js +++ b/helpers/stylesheet.js @@ -2,7 +2,7 @@ const _ = require('lodash'); const buildCDNHelper = require('./lib/cdnify'); -const ResourceHints = require('./lib/resourceHints'); +const {addResourceHint, resourceHintAllowedTypes} = require('./lib/resourceHints'); const factory = globals => { return function (assetPath) { @@ -20,11 +20,11 @@ const factory = globals => { const url = cdnify(path); if (_.has(options.hash, 'resourceHint')) { - ResourceHints.addResourceHint( + addResourceHint( globals, url, options.hash['resourceHint'], - ResourceHints.resourceHintAllowedTypes.resourceHintStyleType + resourceHintAllowedTypes.resourceHintStyleType ); delete options.hash.resourceHint; } diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js index 737b18f..ec57ffe 100644 --- a/spec/helpers/lib/resourceHints.js +++ b/spec/helpers/lib/resourceHints.js @@ -8,7 +8,11 @@ const Lab = require('lab'), it = lab.it, beforeEach = lab.beforeEach, afterEach = lab.afterEach; -const ResourceHints = require('../../../helpers/lib/resourceHints'); +const { + addResourceHint, + defaultResourceHintSate, + resourceHintAllowedTypes +} = require('../../../helpers/lib/resourceHints'); describe('resource hints', function () { @@ -28,35 +32,42 @@ describe('resource hints', function () { it("does work as expected with valid params", (done) => { - let globals = {resourceHints: []}; + const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); - let src = '/my/styles.css'; - let type = 'style'; - let state = 'preload'; - let expected = {src, state, type}; + const src = '/my/styles.css'; + const type = 'style'; + const state = 'preload'; + const expected = {src, state, type}; - ResourceHints.addResourceHint(globals, src, state, type); + addResourceHint(globals, src, state, type); expect(globals.resourceHints.push.called).to.equals(true); expect(globals.resourceHints).to.have.length(1); - let result = _.head(globals.resourceHints); + const result = _.head(globals.resourceHints); expect(result).to.equals(expected); done(); }); it('does not add a hint when provided path is invalid', (done) => { - let globals = {resourceHints: []}; + const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); - [undefined, null, ''].map(s => { - ResourceHints.addResourceHint( - global, - s, - ResourceHints.defaultResourceHintSate, - ResourceHints.resourceHintAllowedTypes.resourceHintStyleType - ); + const throws = [undefined, null, ''].map(s => { + const f = () => { + addResourceHint( + global, + s, + defaultResourceHintSate, + resourceHintAllowedTypes.resourceHintStyleType + ); + } + return f; + }); + + throws.forEach(t => { + expect(t).to.throw(); }); expect(globals.resourceHints.push.notCalled).to.equals(true); @@ -65,27 +76,27 @@ describe('resource hints', function () { }); it('does create a hint when no type is provided', (done) => { - let globals = {resourceHints: []}; + const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); - ResourceHints.addResourceHint( + addResourceHint( globals, 'https://my.asset.css', - ResourceHints.defaultResourceHintSate + defaultResourceHintSate ); expect(globals.resourceHints.push.calledOnce).to.equals(true); - let hint = _.head(globals.resourceHints); + const hint = _.head(globals.resourceHints); expect(hint.type).to.not.exist(); done(); }); it('does not create a hint when provided state param is not supported', (done) => { - let globals = {resourceHints: []}; + const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); - ResourceHints.addResourceHint( + addResourceHint( globals, '/styles.css', 'not-supported' @@ -97,15 +108,15 @@ describe('resource hints', function () { }); it('does not create duplicate, by src, hints', (done) => { - let globals = {resourceHints: []}; + const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); - let src = '/my/styles.css'; - let type = 'style'; - let state = 'preload'; + const src = '/my/styles.css'; + const type = 'style'; + const state = 'preload'; for (let i = 0; i < 5; i++) { - ResourceHints.addResourceHint( + addResourceHint( globals, src, state, @@ -120,11 +131,11 @@ describe('resource hints', function () { }); it('does not create any hint when the limit of allowed hints was reached', (done) => { - let filled = _.fill(Array(50), 1); - let globals = {resourceHints: filled}; + const filled = _.fill(Array(50), 1); + const globals = {resourceHints: filled}; sandbox.spy(globals.resourceHints, 'push'); - ResourceHints.addResourceHint( + addResourceHint( globals, '/my/styles.css', 'style', diff --git a/spec/helpers/stylesheet.js b/spec/helpers/stylesheet.js index 79fb115..a86a64c 100644 --- a/spec/helpers/stylesheet.js +++ b/spec/helpers/stylesheet.js @@ -1,11 +1,10 @@ -const _ = require('lodash'); const Lab = require('lab'), lab = exports.lab = Lab.script(), describe = lab.experiment, it = lab.it; const {buildRenderer, testRunner} = require("../spec-helpers"); const {expect} = require("code"); -const resourceHintStyleType = require("../../helpers/lib/resourceHints").resourceHintAllowedTypes.resourceHintStyleType; +const {resourceHintStyleType} = require("../../helpers/lib/resourceHints").resourceHintAllowedTypes; describe('stylesheet helper', () => { const siteSettings = { @@ -17,27 +16,23 @@ describe('stylesheet helper', () => { const runTestCases = testRunner({siteSettings}); it('should render a link tag with the cdn ulr and stencil-stylesheet data tag', done => { - - const template = '{{{stylesheet "assets/css/style.css" resourceHint="preload"}}}'; - const expected = ''; - const renderer = buildRenderer(siteSettings); - renderer.renderString(template, {}) - .then(r => { - expect(r).to.equals(expected); - - const hints = renderer.getResourceHints(); - expect(hints.length).to.equals(1); - - _.each(hints, function ({src, state, type}) { - expect(src).to.startWith(siteSettings.cdn_url); - expect(type).to.equals(resourceHintStyleType); - expect(state).to.equals('preload'); - }); - - done(); - }) - .catch(done); + const runner = testRunner({renderer}); + runner([ + { + input: '{{{stylesheet "assets/css/style.css" resourceHint="preload"}}}', + output: '', + }, + ], () => { + const hints = renderer.getResourceHints(); + expect(hints.length).to.equals(1); + hints.forEach(({src, type, state}) => { + expect(src).to.startWith(siteSettings.cdn_url); + expect(type).to.equals(resourceHintStyleType); + expect(state).to.equals('preload'); + }); + done(); + }); }); it('should render a link tag and all extra attributes with no cdn url', done => { @@ -65,21 +60,24 @@ describe('stylesheet helper', () => { }); it('should add configId to the filename', done => { - const template = '{{{stylesheet "assets/css/style.css" resourceHint="preconnect" }}}'; + const siteSettings = {theme_config_id: 'foo'}; + const renderer = buildRenderer(siteSettings); + const runner = testRunner({renderer}); const src = '/assets/css/style-foo.css'; - const output = ``; - - const handlebarsRenderer = buildRenderer({theme_config_id: 'foo'}); - handlebarsRenderer.renderString(template, {}) - .then(r => { - expect(r).to.equals(output); - const hint = _.head(handlebarsRenderer.getResourceHints()); - expect(hint.src).to.equals(src); - expect(hint.state).to.equals('preconnect'); - - done(); - }) - .catch(done); + runner([ + { + input: '{{{stylesheet "assets/css/style.css" resourceHint="preconnect"}}}', + output: ``, + siteSettings: siteSettings, + }, + ], () => { + const hints = renderer.getResourceHints(); + expect(hints.length).to.equals(1); + const hint = hints[0]; + expect(hint.src).to.equals(src); + expect(hint.state).to.equals('preconnect'); + done(); + }); }); it('should not append configId if the file is not in assets/css/ directory', done => { From d45ae07492872ffcfaeb6446d7f6d757ffaa5a71 Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Wed, 17 Aug 2022 12:37:52 -0500 Subject: [PATCH 3/7] [STRF-9950] paper-handlebars - stylesheet helper early-hints flag new param PR fixes --- helpers/lib/resourceHints.js | 15 ++++----------- helpers/stylesheet.js | 16 +++++++++------- spec/helpers/lib/resourceHints.js | 32 +++++++++++++------------------ 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/helpers/lib/resourceHints.js b/helpers/lib/resourceHints.js index a4564af..011ec2b 100644 --- a/helpers/lib/resourceHints.js +++ b/helpers/lib/resourceHints.js @@ -18,21 +18,14 @@ const resourceHintAllowedTypes = [resourceHintStyleType, resourceHintFontType, r */ function addResourceHint(globals, path, state, type) { - if (typeof path !== 'string') { - throw new Error('resourceHint attribute require a valid URI'); + if (!utils.isString(path)) { + throw new Error('Invalid path provided. path should be a non empty string'); } path = path.trim(); - if (path === '') { - throw new Error('resourceHint received an empty path'); - } - if (typeof state !== 'string') { + if (!utils.isString(state) || !resourceHintStates.includes(state)) { throw new Error(`resourceHint attribute received invalid value. Valid values are: ${resourceHintStates}`); } - state = state.trim(); - if (!resourceHintStates.includes(state)) { - return; - } if (typeof globals.resourceHints === 'undefined') { globals.resourceHints = []; @@ -63,7 +56,7 @@ function addResourceHint(globals, path, state, type) { module.exports = { resourceHintsLimit, - defaultResourceHintSate: defaultResourceHintState, + defaultResourceHintState, addResourceHint, resourceHintAllowedTypes: {resourceHintStyleType, resourceHintFontType, resourceHintScriptType} }; diff --git a/helpers/stylesheet.js b/helpers/stylesheet.js index 9de2a10..f4afc7b 100644 --- a/helpers/stylesheet.js +++ b/helpers/stylesheet.js @@ -1,6 +1,5 @@ 'use strict'; -const _ = require('lodash'); const buildCDNHelper = require('./lib/cdnify'); const {addResourceHint, resourceHintAllowedTypes} = require('./lib/resourceHints'); @@ -19,20 +18,23 @@ const factory = globals => { const url = cdnify(path); - if (_.has(options.hash, 'resourceHint')) { + if (options.hash.resourceHint) { addResourceHint( globals, url, - options.hash['resourceHint'], + options.hash.resourceHint, resourceHintAllowedTypes.resourceHintStyleType ); delete options.hash.resourceHint; } - let attrs = {rel: 'stylesheet'}; - Object.assign(attrs, options.hash); - attrs = _.map(attrs, (value, key) => `${key}="${value}"`).join(' '); - return ``; + const attrs = Object.assign({rel: 'stylesheet'}, options.hash); + const keyValuePairs = []; + for (const attrsKey in attrs) { + keyValuePairs.push(`${attrsKey}="${attrs[attrsKey]}"`); + } + + return ``; }; }; diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js index ec57ffe..d9c1cdf 100644 --- a/spec/helpers/lib/resourceHints.js +++ b/spec/helpers/lib/resourceHints.js @@ -1,4 +1,3 @@ -const _ = require('lodash'); const Code = require('code'), expect = Code.expect; const Sinon = require('sinon'); @@ -10,7 +9,7 @@ const Lab = require('lab'), afterEach = lab.afterEach; const { addResourceHint, - defaultResourceHintSate, + defaultResourceHintState, resourceHintAllowedTypes } = require('../../../helpers/lib/resourceHints'); @@ -42,10 +41,8 @@ describe('resource hints', function () { addResourceHint(globals, src, state, type); - expect(globals.resourceHints.push.called).to.equals(true); - expect(globals.resourceHints).to.have.length(1); - const result = _.head(globals.resourceHints); - expect(result).to.equals(expected); + expect(globals.resourceHints.push.calledOnce).to.equals(true); + expect(globals.resourceHints[0]).to.equals(expected); done(); }); @@ -55,15 +52,14 @@ describe('resource hints', function () { sandbox.spy(globals.resourceHints, 'push'); const throws = [undefined, null, ''].map(s => { - const f = () => { + return () => { addResourceHint( global, s, - defaultResourceHintSate, + defaultResourceHintState, resourceHintAllowedTypes.resourceHintStyleType ); - } - return f; + }; }); throws.forEach(t => { @@ -82,12 +78,11 @@ describe('resource hints', function () { addResourceHint( globals, 'https://my.asset.css', - defaultResourceHintSate + defaultResourceHintState ); expect(globals.resourceHints.push.calledOnce).to.equals(true); - const hint = _.head(globals.resourceHints); - expect(hint.type).to.not.exist(); + expect(globals.resourceHints[0].type).to.not.exist(); done(); }); @@ -96,13 +91,12 @@ describe('resource hints', function () { const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); - addResourceHint( + const f = () => addResourceHint( globals, '/styles.css', 'not-supported' ); - - expect(globals.resourceHints.push.notCalled).to.equals(true); + expect(f).to.throw(); done(); }); @@ -131,15 +125,15 @@ describe('resource hints', function () { }); it('does not create any hint when the limit of allowed hints was reached', (done) => { - const filled = _.fill(Array(50), 1); + const filled = Array(50).fill(1); const globals = {resourceHints: filled}; sandbox.spy(globals.resourceHints, 'push'); addResourceHint( globals, '/my/styles.css', - 'style', - 'preload' + 'preload', + 'style' ); expect(globals.resourceHints.push.notCalled).to.equals(true); From 7fe597550d4f2bc1c904aacdcf72522426b572a9 Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Wed, 17 Aug 2022 12:40:41 -0500 Subject: [PATCH 4/7] [STRF-9950] paper-handlebars - stylesheet helper early-hints flag new param PR fixes --- spec/helpers/lib/resourceHints.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js index d9c1cdf..7580249 100644 --- a/spec/helpers/lib/resourceHints.js +++ b/spec/helpers/lib/resourceHints.js @@ -29,7 +29,7 @@ describe('resource hints', function () { done(); }); - it("does work as expected with valid params", (done) => { + it("creates resource hints when valid params are provided", (done) => { const globals = {resourceHints: []}; sandbox.spy(globals.resourceHints, 'push'); From b78d74ee2adba20c34543e0576dd2214342033d6 Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Wed, 17 Aug 2022 12:52:48 -0500 Subject: [PATCH 5/7] [STRF-9950] paper-handlebars - stylesheet helper early-hints flag new param PR fixes --- spec/helpers/lib/resourceHints.js | 34 ++++++------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js index 7580249..43f78b5 100644 --- a/spec/helpers/lib/resourceHints.js +++ b/spec/helpers/lib/resourceHints.js @@ -1,12 +1,9 @@ const Code = require('code'), expect = Code.expect; -const Sinon = require('sinon'); const Lab = require('lab'), lab = exports.lab = Lab.script(), describe = lab.experiment, - it = lab.it, - beforeEach = lab.beforeEach, - afterEach = lab.afterEach; + it = lab.it; const { addResourceHint, defaultResourceHintState, @@ -17,22 +14,8 @@ describe('resource hints', function () { describe('addResourceHint', function () { - let sandbox; - - beforeEach(done => { - sandbox = Sinon.createSandbox(); - done(); - }); - - afterEach(done => { - sandbox.restore(); - done(); - }); - it("creates resource hints when valid params are provided", (done) => { - const globals = {resourceHints: []}; - sandbox.spy(globals.resourceHints, 'push'); const src = '/my/styles.css'; const type = 'style'; @@ -41,7 +24,7 @@ describe('resource hints', function () { addResourceHint(globals, src, state, type); - expect(globals.resourceHints.push.calledOnce).to.equals(true); + expect(globals.resourceHints).to.have.length(1); expect(globals.resourceHints[0]).to.equals(expected); done(); @@ -49,7 +32,6 @@ describe('resource hints', function () { it('does not add a hint when provided path is invalid', (done) => { const globals = {resourceHints: []}; - sandbox.spy(globals.resourceHints, 'push'); const throws = [undefined, null, ''].map(s => { return () => { @@ -66,14 +48,13 @@ describe('resource hints', function () { expect(t).to.throw(); }); - expect(globals.resourceHints.push.notCalled).to.equals(true); + expect(globals.resourceHints).to.have.length(0); done(); }); it('does create a hint when no type is provided', (done) => { const globals = {resourceHints: []}; - sandbox.spy(globals.resourceHints, 'push'); addResourceHint( globals, @@ -81,7 +62,7 @@ describe('resource hints', function () { defaultResourceHintState ); - expect(globals.resourceHints.push.calledOnce).to.equals(true); + expect(globals.resourceHints).to.have.length(1); expect(globals.resourceHints[0].type).to.not.exist(); done(); @@ -89,13 +70,13 @@ describe('resource hints', function () { it('does not create a hint when provided state param is not supported', (done) => { const globals = {resourceHints: []}; - sandbox.spy(globals.resourceHints, 'push'); const f = () => addResourceHint( globals, '/styles.css', 'not-supported' ); + expect(f).to.throw(); done(); @@ -103,7 +84,6 @@ describe('resource hints', function () { it('does not create duplicate, by src, hints', (done) => { const globals = {resourceHints: []}; - sandbox.spy(globals.resourceHints, 'push'); const src = '/my/styles.css'; const type = 'style'; @@ -118,7 +98,6 @@ describe('resource hints', function () { ); } - expect(globals.resourceHints.push.calledOnce).to.equals(true); expect(globals.resourceHints).to.have.length(1); done(); @@ -127,7 +106,6 @@ describe('resource hints', function () { it('does not create any hint when the limit of allowed hints was reached', (done) => { const filled = Array(50).fill(1); const globals = {resourceHints: filled}; - sandbox.spy(globals.resourceHints, 'push'); addResourceHint( globals, @@ -136,7 +114,7 @@ describe('resource hints', function () { 'style' ); - expect(globals.resourceHints.push.notCalled).to.equals(true); + expect(globals.resourceHints).to.have.length(filled.length); done(); }); From ee2c3c0eebd35d8e1a2bbd83db5206e7db7e0557 Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Thu, 18 Aug 2022 12:09:44 -0500 Subject: [PATCH 6/7] [STRF-9948] paper-handlebars - resourceHints helper add new early-hints flag param --- helpers/getFontsCollection.js | 10 +++++- helpers/lib/fonts.js | 21 +++++++++++-- helpers/lib/resourceHints.js | 49 +++++++++++++++++++----------- helpers/resourceHints.js | 35 +++++++++++++++------ spec/helpers/getFontsCollection.js | 41 +++++++++++++++++-------- spec/helpers/lib/resourceHints.js | 3 +- spec/helpers/resourceHints.js | 28 +++++++++++++---- 7 files changed, 138 insertions(+), 49 deletions(-) diff --git a/helpers/getFontsCollection.js b/helpers/getFontsCollection.js index 85e3b96..324ad2d 100644 --- a/helpers/getFontsCollection.js +++ b/helpers/getFontsCollection.js @@ -1,12 +1,20 @@ 'use strict'; const getFonts = require('./lib/fonts'); +const utils = require('handlebars-utils'); const factory = globals => { return function() { const options = arguments[arguments.length - 1]; const fontDisplay = options.hash['font-display']; - return getFonts('linkElements', globals.getThemeSettings(), globals.handlebars, {fontDisplay}); + + const getFontsOptions = utils.isString(options.hash.resourceHint) ? { + globals, + state: options.hash.resourceHint, + fontDisplay + } : {fontDisplay}; + + return getFonts('linkElements', globals.getThemeSettings(), globals.handlebars, getFontsOptions); }; }; diff --git a/helpers/lib/fonts.js b/helpers/lib/fonts.js index 48f3329..889200d 100644 --- a/helpers/lib/fonts.js +++ b/helpers/lib/fonts.js @@ -2,6 +2,8 @@ const _ = require('lodash'); +const {resourceHintAllowedTypes, addResourceHint} = require('../lib/resourceHints'); + const fontProviders = { 'Google': { /** @@ -68,6 +70,18 @@ const fontProviders = { } }; }, + + generateResourceHints: function (globals, state, fonts, fontDisplay) { + const displayTypes = ['auto', 'block', 'swap', 'fallback', 'optional']; + fontDisplay = displayTypes.includes(fontDisplay) ? fontDisplay : 'swap'; + const path = `https://fonts.googleapis.com/css?family=${fonts.join('|')}&display=${fontDisplay}`; + addResourceHint( + globals, + path, + state, + resourceHintAllowedTypes.resourceHintFontType + ); + } }, }; @@ -84,7 +98,7 @@ const fontProviders = { * @returns {Object.|string} */ module.exports = function(format, themeSettings, handlebars, options) { - + const collectedFonts = {}; _.each(themeSettings, function(value, key) { //check that -font is on end of string but not start of string @@ -115,8 +129,11 @@ module.exports = function(format, themeSettings, handlebars, options) { // Format output based on requested format switch(format) { case 'linkElements': - + const formattedFonts = _.mapValues(parsedFonts, function(value, key) { + if (options.globals && options.state) { + fontProviders[key].generateResourceHints(options.globals, options.state, value, options.fontDisplay); + } return fontProviders[key].buildLink(value, options.fontDisplay); }); return new handlebars.SafeString(_.values(formattedFonts).join('')); diff --git a/helpers/lib/resourceHints.js b/helpers/lib/resourceHints.js index 011ec2b..c9d3a31 100644 --- a/helpers/lib/resourceHints.js +++ b/helpers/lib/resourceHints.js @@ -2,21 +2,29 @@ const utils = require("handlebars-utils"); const resourceHintsLimit = 50; -const defaultResourceHintState = 'preload'; - -const resourceHintStates = [defaultResourceHintState, 'preconnect', 'prefetch']; +const preloadResourceHintState = 'preload'; +const preconnectResourceHintState = 'preconnect'; +const prerenderResourceHintState = 'prerender'; +const dnsPrefetchResourceHintState = 'dns-prefetch'; +const resourceHintStates = [preloadResourceHintState, preconnectResourceHintState, prerenderResourceHintState, dnsPrefetchResourceHintState]; const resourceHintFontType = 'font'; const resourceHintStyleType = 'style'; const resourceHintScriptType = 'script'; -const resourceHintAllowedTypes = [resourceHintStyleType, resourceHintFontType, resourceHintScriptType]; +const resourceHintTypes = [resourceHintStyleType, resourceHintFontType, resourceHintScriptType]; + +const noCors = 'no'; +const anonymousCors = 'anonymous'; +const useCredentialsCors = 'use-credentials'; +const allowedCors = [noCors, anonymousCors, useCredentialsCors]; /** * @param {string} path - The uri to the resource. - * @param {string} state - 'preload' or 'preconnect' - * @param {string} type - any of [style, font, script] + * @param {string} state - any of [preload, preconnect, prerender, dns-prefetch] + * @param {string} type? - any of [style, font, script] If an invalid value is provided, property won't be included + * @param {string} cors? - any of [no, anonymous, use-credentials] defaults to no */ -function addResourceHint(globals, path, state, type) { +function addResourceHint(globals, path, state, type, cors) { if (!utils.isString(path)) { throw new Error('Invalid path provided. path should be a non empty string'); @@ -31,7 +39,6 @@ function addResourceHint(globals, path, state, type) { globals.resourceHints = []; } - path = utils.escapeExpression(path); let index = globals.resourceHints.findIndex(({src}) => path === src); if (index >= 0) { return; @@ -41,22 +48,30 @@ function addResourceHint(globals, path, state, type) { return; } - let value = {src: path, state}; + let hint = {src: path, state}; - if (typeof type !== 'string') { - type = ''; + if (utils.isString(type) && resourceHintTypes.includes(type)) { + hint.type = type; } - type = type.trim(); - if (type !== '' && resourceHintAllowedTypes.includes(type)) { - value.type = type; + + if (!utils.isString(cors) || !allowedCors.includes(cors)) { + cors = noCors; } + hint.cors = cors; - globals.resourceHints.push(value); + globals.resourceHints.push(hint); } module.exports = { resourceHintsLimit, - defaultResourceHintState, + defaultResourceHintState: preloadResourceHintState, addResourceHint, - resourceHintAllowedTypes: {resourceHintStyleType, resourceHintFontType, resourceHintScriptType} + resourceHintAllowedTypes: {resourceHintStyleType, resourceHintFontType, resourceHintScriptType}, + resourceHintAllowedCors: {noCors, anonymousCors, useCredentialsCors}, + resourceHintAllowedStates: { + preloadResourceHintState, + preconnectResourceHintState, + prerenderResourceHintState, + dnsPrefetchResourceHintState + } }; diff --git a/helpers/resourceHints.js b/helpers/resourceHints.js index 1f24eb7..e534929 100644 --- a/helpers/resourceHints.js +++ b/helpers/resourceHints.js @@ -1,7 +1,13 @@ 'use strict'; -const _ = require('lodash'); +const utils = require('handlebars-utils'); const getFonts = require('./lib/fonts'); +const { + addResourceHint, + resourceHintAllowedStates, + resourceHintAllowedTypes, + resourceHintAllowedCors +} = require('./lib/resourceHints'); const fontResources = { 'Google': [ @@ -10,27 +16,38 @@ const fontResources = { ], }; +function format(host) { + return ``; +} + const factory = globals => { - return function() { - function format(host) { - return ``; - } + return function () { - var hosts = []; + let hosts = []; // Add cdn const siteSettings = globals.getSiteSettings(); const cdnUrl = siteSettings['cdn_url'] || ''; - if (cdnUrl != '') { + if (utils.isString(cdnUrl)) { hosts.push(cdnUrl); } // Add font providers - const fontProviders = _.keys(getFonts('providerLists', globals.getThemeSettings(), globals.handlebars)); - _.each(fontProviders, function(provider) { + const fonts = getFonts('providerLists', globals.getThemeSettings(), globals.handlebars); + for (const provider in fonts) { if (typeof fontResources[provider] !== 'undefined') { hosts = hosts.concat(fontResources[provider]); } + } + + hosts.forEach(host => { + addResourceHint( + globals, + host, + resourceHintAllowedStates.dnsPrefetchResourceHintState, + resourceHintAllowedTypes.resourceHintFontType, + resourceHintAllowedCors.noCors + ); }); return new globals.handlebars.SafeString(hosts.map(host => format(host)).join('')); diff --git a/spec/helpers/getFontsCollection.js b/spec/helpers/getFontsCollection.js index 35a2858..8028dcb 100644 --- a/spec/helpers/getFontsCollection.js +++ b/spec/helpers/getFontsCollection.js @@ -1,8 +1,10 @@ const Lab = require('lab'), - lab = exports.lab = Lab.script(), - describe = lab.experiment, - it = lab.it, - testRunner = require('../spec-helpers').testRunner; + lab = exports.lab = Lab.script(), + describe = lab.experiment, + it = lab.it, + {testRunner, buildRenderer} = require('../spec-helpers'); +const {expect} = require("code"); +const {resourceHintAllowedTypes, resourceHintAllowedStates} = require('../../helpers/lib/resourceHints'); describe('getFontsCollection', function () { it('should return a font link with fonts from theme settings and &display=swap when no font-display value is passed', function (done) { @@ -17,14 +19,22 @@ describe('getFontsCollection', function () { 'test8-font': 'Google_Crimson+Text_400,700_sans', 'random-property': 'not a font' }; - - const runTestCases = testRunner({themeSettings}); + const renderer = buildRenderer({}, themeSettings); + const runTestCases = testRunner({renderer}); + const href = "https://fonts.googleapis.com/css?family=Open+Sans:,400italic,700|Karla:700|Lora:400|Volkron:|Droid:400,700|Crimson+Text:400,700&display=swap"; runTestCases([ { - input: '{{getFontsCollection}}', - output: '', + input: '{{getFontsCollection resourceHint="preload"}}', + output: ``, }, - ], done); + ], () => { + const hints = renderer.getResourceHints(); + expect(hints).to.have.length(1); + expect(hints[0].src).to.equals(href); + expect(hints[0].state).to.equals(resourceHintAllowedStates.preloadResourceHintState); + expect(hints[0].type).to.equals(resourceHintAllowedTypes.resourceHintFontType); + done(); + }); }); it('should return a font link with fonts from theme settings and &display=swap when font-display value passed is invalid', function (done) { const themeSettings = { @@ -74,14 +84,19 @@ describe('getFontsCollection', function () { 'test2-font': 'Google_', 'test3-font': 'Google' }; - - const runTestCases = testRunner({themeSettings}); + const renderer = buildRenderer({}, themeSettings); + const runTestCases = testRunner({renderer}); runTestCases([ { - input: '{{getFontsCollection}}', + input: '{{getFontsCollection resourceHint="preconnect"}}', output: '', }, - ], done); + ], () => { + const hints = renderer.getResourceHints(); + expect(hints).to.have.length(1); + expect(hints[0].state).to.equals(resourceHintAllowedStates.preconnectResourceHintState); + done(); + }); }); it('should not crash if a malformed Google font is passed when valid font-display value is passed', function (done) { const themeSettings = { diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js index 43f78b5..2d49b13 100644 --- a/spec/helpers/lib/resourceHints.js +++ b/spec/helpers/lib/resourceHints.js @@ -20,7 +20,8 @@ describe('resource hints', function () { const src = '/my/styles.css'; const type = 'style'; const state = 'preload'; - const expected = {src, state, type}; + const cors = 'no'; + const expected = {src, state, type, cors}; addResourceHint(globals, src, state, type); diff --git a/spec/helpers/resourceHints.js b/spec/helpers/resourceHints.js index 36f5359..39c534b 100644 --- a/spec/helpers/resourceHints.js +++ b/spec/helpers/resourceHints.js @@ -1,8 +1,14 @@ const Lab = require('lab'), - lab = exports.lab = Lab.script(), - describe = lab.experiment, - it = lab.it, - testRunner = require('../spec-helpers').testRunner; + lab = exports.lab = Lab.script(), + describe = lab.experiment, + it = lab.it, + {testRunner, buildRenderer} = require('../spec-helpers'); +const {expect} = require("code"); +const { + resourceHintAllowedCors, + resourceHintAllowedStates, + resourceHintAllowedTypes +} = require('../../helpers/lib/resourceHints'); describe('resourceHints', function () { it('should return the expected resource links', function (done) { @@ -18,13 +24,23 @@ describe('resourceHints', function () { 'random-property': 'not a font' }; - const runTestCases = testRunner({themeSettings}); + const renderer = buildRenderer({}, themeSettings); + const runTestCases = testRunner({renderer}); runTestCases([ { input: '{{resourceHints}}', output: '', }, - ], done); + ], () => { + const hints = renderer.getResourceHints(); + expect(hints).to.have.length(2); + hints.forEach(hint => { + expect(hint.cors).to.equals(resourceHintAllowedCors.noCors); + expect(hint.type).to.equals(resourceHintAllowedTypes.resourceHintFontType); + expect(hint.state).to.equals(resourceHintAllowedStates.dnsPrefetchResourceHintState); + }); + done(); + }); }); }); From 1ea9dbe9f23e9b2a81bd235100b24c1f88513a7f Mon Sep 17 00:00:00 2001 From: Rafa Avila Date: Fri, 19 Aug 2022 11:16:04 -0500 Subject: [PATCH 7/7] [STRF-9948] paper-handlebars - resourceHints helper add new early-hints flag param pr fixes --- helpers/lib/resourceHints.js | 6 ++++-- spec/helpers/lib/resourceHints.js | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/helpers/lib/resourceHints.js b/helpers/lib/resourceHints.js index c9d3a31..652192b 100644 --- a/helpers/lib/resourceHints.js +++ b/helpers/lib/resourceHints.js @@ -22,7 +22,7 @@ const allowedCors = [noCors, anonymousCors, useCredentialsCors]; * @param {string} path - The uri to the resource. * @param {string} state - any of [preload, preconnect, prerender, dns-prefetch] * @param {string} type? - any of [style, font, script] If an invalid value is provided, property won't be included - * @param {string} cors? - any of [no, anonymous, use-credentials] defaults to no + * @param {string} cors? - any of [no, anonymous, use-credentials] defaults to no when no value is provided */ function addResourceHint(globals, path, state, type, cors) { @@ -54,7 +54,9 @@ function addResourceHint(globals, path, state, type, cors) { hint.type = type; } - if (!utils.isString(cors) || !allowedCors.includes(cors)) { + if (utils.isString(cors) && !allowedCors.includes(cors)) { + throw new Error(`Invalid cors value provided. Valid values are: ${allowedCors}`); + } else if (!utils.isString(cors)) { cors = noCors; } hint.cors = cors; diff --git a/spec/helpers/lib/resourceHints.js b/spec/helpers/lib/resourceHints.js index 2d49b13..8a60b87 100644 --- a/spec/helpers/lib/resourceHints.js +++ b/spec/helpers/lib/resourceHints.js @@ -20,9 +20,25 @@ describe('resource hints', function () { const src = '/my/styles.css'; const type = 'style'; const state = 'preload'; - const cors = 'no'; + const cors = 'anonymous'; const expected = {src, state, type, cors}; + addResourceHint(globals, src, state, type, cors); + + expect(globals.resourceHints).to.have.length(1); + expect(globals.resourceHints[0]).to.equals(expected); + + done(); + }); + + it("creates resource hints when valid params are provided defaulting cors to no when no provided", (done) => { + const globals = {}; + + const src = '/my/styles.css'; + const type = 'style'; + const state = 'preload'; + const expected = {src, state, type, cors: 'no'}; + addResourceHint(globals, src, state, type); expect(globals.resourceHints).to.have.length(1); @@ -120,5 +136,10 @@ describe('resource hints', function () { done(); }); + it('should throw when invalid cors value is provided', done => { + const f = () => addResourceHint({}, '/theme.css', 'style', 'preload', 'invalid-cors'); + expect(f).to.throw(); + done(); + }); }); });