From 8b1a5d3d87964cee5c5f87a4615e786880cdc2c4 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Tue, 18 Oct 2022 09:09:15 +0200 Subject: [PATCH 01/28] feat: basic plugin system PoC --- scripts/delayed.js | 6 +- scripts/lib-franklin.js | 138 +++++++------------------------- scripts/plugins/placeholders.js | 46 +++++++++++ scripts/plugins/rum.js | 86 ++++++++++++++++++++ scripts/scripts.js | 29 +++---- 5 files changed, 178 insertions(+), 127 deletions(-) create mode 100644 scripts/plugins/placeholders.js create mode 100644 scripts/plugins/rum.js diff --git a/scripts/delayed.js b/scripts/delayed.js index 920b4ad83..253878834 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -1,7 +1,3 @@ // eslint-disable-next-line import/no-cycle -import { sampleRUM } from './lib-franklin.js'; -// Core Web Vitals RUM collection -sampleRUM('cwv'); - -// add more delayed functionality here +console.log('Delayed script.'); diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index bd0635779..6ee06d872 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -10,69 +10,6 @@ * governing permissions and limitations under the License. */ -/** - * log RUM if part of the sample. - * @param {string} checkpoint identifies the checkpoint in funnel - * @param {Object} data additional data for RUM sample - */ -export function sampleRUM(checkpoint, data = {}) { - sampleRUM.defer = sampleRUM.defer || []; - const defer = (fnname) => { - sampleRUM[fnname] = sampleRUM[fnname] - || ((...args) => sampleRUM.defer.push({ fnname, args })); - }; - sampleRUM.drain = sampleRUM.drain - || ((dfnname, fn) => { - sampleRUM[dfnname] = fn; - sampleRUM.defer - .filter(({ fnname }) => dfnname === fnname) - .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); - }); - sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; - defer('observe'); - defer('cwv'); - try { - window.hlx = window.hlx || {}; - if (!window.hlx.rum) { - const usp = new URLSearchParams(window.location.search); - const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. - // eslint-disable-next-line no-bitwise - const hashCode = (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); - const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; - const random = Math.random(); - const isSelected = (random * weight < 1); - // eslint-disable-next-line object-curly-newline - window.hlx.rum = { weight, id, random, isSelected, sampleRUM }; - } - const { weight, id } = window.hlx.rum; - if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { - const sendPing = (pdata = data) => { - // eslint-disable-next-line object-curly-newline, max-len, no-use-before-define - const body = JSON.stringify({ weight, id, referer: window.location.href, generation: window.hlx.RUM_GENERATION, checkpoint, ...data }); - const url = `https://rum.hlx.page/.rum/${weight}`; - // eslint-disable-next-line no-unused-expressions - navigator.sendBeacon(url, body); - // eslint-disable-next-line no-console - console.debug(`ping:${checkpoint}`, pdata); - }; - sampleRUM.cases = sampleRUM.cases || { - cwv: () => sampleRUM.cwv(data) || true, - lazy: () => { - // use classic script to avoid CORS issues - const script = document.createElement('script'); - script.src = 'https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js'; - document.head.appendChild(script); - return true; - }, - }; - sendPing(data); - if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } - } - } catch (error) { - // something went wrong - } -} - /** * Loads a CSS file. * @param {string} href The path to the CSS file @@ -148,37 +85,6 @@ export function decorateIcons(element = document) { }); } -/** - * Gets placeholders object - * @param {string} prefix - */ -export async function fetchPlaceholders(prefix = 'default') { - window.placeholders = window.placeholders || {}; - const loaded = window.placeholders[`${prefix}-loaded`]; - if (!loaded) { - window.placeholders[`${prefix}-loaded`] = new Promise((resolve, reject) => { - try { - fetch(`${prefix === 'default' ? '' : prefix}/placeholders.json`) - .then((resp) => resp.json()) - .then((json) => { - const placeholders = {}; - json.data.forEach((placeholder) => { - placeholders[toCamelCase(placeholder.Key)] = placeholder.Text; - }); - window.placeholders[prefix] = placeholders; - resolve(); - }); - } catch (error) { - // error loading placeholders - window.placeholders[prefix] = {}; - reject(); - } - }); - } - await window.placeholders[`${prefix}-loaded`]; - return window.placeholders[prefix]; -} - /** * Decorates a block. * @param {Element} block The block element @@ -546,11 +452,39 @@ export function loadFooter(footer) { return loadBlock(footerBlock); } +export const plugins = {}; + +export async function loadPage(options = {}) { + const pluginsList = Object.values(plugins); + if (options.loadEager) { + await options.loadEager(document); + } + await Promise.all(pluginsList.map((p) => p.withEager && p.withEager.call(null, p.options, plugins))); + + if (options.loadLazy) { + await options.loadLazy(document); + } + await Promise.all(pluginsList.map((p) => p.withLazy && p.withLazy.call(null, p.options, plugins))); + + window.setTimeout(() => { + if (options.loadDelayed) { + options.loadDelayed(); + } + Promise.all(pluginsList.map((p) => p.withDelayed && p.withDelayed.call(null, p.options, plugins))); + }, options.delayedDuration || 3000); +} + +export async function withPlugin(path, options = {}) { + const pluginName = path.split('/').pop().replace('.js', ''); + const plugin = await import(path); + plugins[pluginName] = { ...plugin, options }; +} + /** * init block utils */ -function init() { +export function init(options) { window.hlx = window.hlx || {}; window.hlx.codeBasePath = ''; @@ -564,17 +498,5 @@ function init() { } } - sampleRUM('top'); - - window.addEventListener('load', () => sampleRUM('load')); - - window.addEventListener('unhandledrejection', (event) => { - sampleRUM('error', { source: event.reason.sourceURL, target: event.reason.line }); - }); - - window.addEventListener('error', (event) => { - sampleRUM('error', { source: event.filename, target: event.lineno }); - }); + loadPage(options); } - -init(); diff --git a/scripts/plugins/placeholders.js b/scripts/plugins/placeholders.js new file mode 100644 index 000000000..931b27602 --- /dev/null +++ b/scripts/plugins/placeholders.js @@ -0,0 +1,46 @@ +let placeholders = {}; + +/** + * Gets placeholders object + * @param {string} prefix + */ +async function fetchPlaceholders(prefix = 'default') { + const placeholders = {}; + const loaded = placeholders[`${prefix}-loaded`]; + if (!loaded) { + placeholders[`${prefix}-loaded`] = new Promise(async (resolve, reject) => { + try { + const response = await fetch(`${prefix === 'default' ? '' : prefix}/placeholders.json`); + if (!response.ok) { + reject(); + } + const json = await resp.json(); + placeholders = json.data.reduce((results, placeholder) => { + results[toCamelCase(placeholder.Key)] = placeholder.Text; + return results; + }, {}); + resolve(placeholders); + } catch (error) { + reject(); + } + }); + } + await placeholders[`${prefix}-loaded`]; + return placeholders[prefix]; +} + +function getPlaceholders() { + return placeholders; +} + +export const api = { + getPlaceholders +} + +export async function withLazy(document, options) { + try { + placeholders = await fetchPlaceholders(options.prefix); + } catch (err) { + placeholders = {}; + } +} diff --git a/scripts/plugins/rum.js b/scripts/plugins/rum.js new file mode 100644 index 000000000..1de9f9389 --- /dev/null +++ b/scripts/plugins/rum.js @@ -0,0 +1,86 @@ +/** + * log RUM if part of the sample. + * @param {string} checkpoint identifies the checkpoint in funnel + * @param {Object} data additional data for RUM sample + */ + function sampleRUM(checkpoint, data = {}) { + sampleRUM.defer = sampleRUM.defer || []; + const defer = (fnname) => { + sampleRUM[fnname] = sampleRUM[fnname] + || ((...args) => sampleRUM.defer.push({ fnname, args })); + }; + sampleRUM.drain = sampleRUM.drain + || ((dfnname, fn) => { + sampleRUM[dfnname] = fn; + sampleRUM.defer + .filter(({ fnname }) => dfnname === fnname) + .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); + }); + sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; + defer('observe'); + defer('cwv'); + try { + window.hlx = window.hlx || {}; + if (!window.hlx.rum) { + const usp = new URLSearchParams(window.location.search); + const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. + // eslint-disable-next-line no-bitwise + const hashCode = (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); + const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; + const random = Math.random(); + const isSelected = (random * weight < 1); + // eslint-disable-next-line object-curly-newline + window.hlx.rum = { weight, id, random, isSelected, sampleRUM }; + } + const { weight, id } = window.hlx.rum; + if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { + const sendPing = (pdata = data) => { + // eslint-disable-next-line object-curly-newline, max-len, no-use-before-define + const body = JSON.stringify({ weight, id, referer: window.location.href, generation: window.hlx.RUM_GENERATION, checkpoint, ...data }); + const url = `https://rum.hlx.page/.rum/${weight}`; + // eslint-disable-next-line no-unused-expressions + navigator.sendBeacon(url, body); + // eslint-disable-next-line no-console + console.debug(`ping:${checkpoint}`, pdata); + }; + sampleRUM.cases = sampleRUM.cases || { + cwv: () => sampleRUM.cwv(data) || true, + lazy: () => { + // use classic script to avoid CORS issues + const script = document.createElement('script'); + script.src = 'https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js'; + document.head.appendChild(script); + sendPing(data); + return true; + }, + }; + sendPing(data); + if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } + } + } catch (error) { + // something went wrong + } +} + +export const api = { + sampleRUM +}; + +export async function withEager(options) { + window.hlx.RUM_GENERATION = options.rumGeneration || 'project-1'; // add your RUM generation information here + sampleRUM('top'); + + window.addEventListener('load', () => sampleRUM('load')); + + window.addEventListener('unhandledrejection', (event) => { + sampleRUM('error', { source: event.reason.sourceURL, target: event.reason.line }); + }); + + window.addEventListener('error', (event) => { + sampleRUM('error', { source: event.filename, target: event.lineno }); + }); +} + +export async function withDelayed(options) { + sampleRUM('cwv'); +} \ No newline at end of file diff --git a/scripts/scripts.js b/scripts/scripts.js index c7fadb471..efb11f187 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -1,5 +1,4 @@ import { - sampleRUM, buildBlock, loadHeader, loadFooter, @@ -8,13 +7,15 @@ import { decorateSections, decorateBlocks, decorateTemplateAndTheme, - waitForLCP, + init, loadBlocks, loadCSS, + plugins, + waitForLCP, + withPlugin, } from './lib-franklin.js'; const LCP_BLOCKS = []; // add your LCP blocks to the list -window.hlx.RUM_GENERATION = 'project-1'; // add your RUM generation information here function buildHeroBlock(main) { const h1 = main.querySelector('h1'); @@ -100,9 +101,9 @@ async function loadLazy(doc) { loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); addFavIcon(`${window.hlx.codeBasePath}/styles/favicon.svg`); - sampleRUM('lazy'); - sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); - sampleRUM.observe(main.querySelectorAll('picture > img')); + plugins.rum.api.sampleRUM('lazy'); + plugins.rum.api.sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); + plugins.rum.api.sampleRUM.observe(main.querySelectorAll('picture > img')); } /** @@ -110,15 +111,15 @@ async function loadLazy(doc) { * the user experience. */ function loadDelayed() { - // eslint-disable-next-line import/no-cycle - window.setTimeout(() => import('./delayed.js'), 3000); // load anything that can be postponed to the latest here + import('./delayed.js'); } -async function loadPage() { - await loadEager(document); - await loadLazy(document); - loadDelayed(); -} +withPlugin('./plugins/rum.js'); +withPlugin('./plugins/placeholders.js'); -loadPage(); +init({ + loadEager, + loadLazy, + loadDelayed, +}); From 2a75982c09b236a928f6450c0ac1a32df42ed948 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Tue, 18 Oct 2022 09:36:58 +0200 Subject: [PATCH 02/28] feat: basic plugin system PoC --- scripts/lib-franklin.js | 23 ++++++++++++---------- scripts/plugins/placeholders.js | 34 ++++++++++++++++++--------------- scripts/plugins/rum.js | 8 ++++---- scripts/scripts.js | 13 ++++++------- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 6ee06d872..74d9d11c4 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -452,34 +452,37 @@ export function loadFooter(footer) { return loadBlock(footerBlock); } -export const plugins = {}; +const plugins = {}; +export async function withPlugin(path, options = {}) { + const pluginName = path.split('/').pop().replace('.js', ''); + const plugin = await import(path); + plugins[pluginName] = { ...plugin, options }; + return plugin.api || null; +} export async function loadPage(options = {}) { const pluginsList = Object.values(plugins); if (options.loadEager) { await options.loadEager(document); } - await Promise.all(pluginsList.map((p) => p.withEager && p.withEager.call(null, p.options, plugins))); + await Promise.all(pluginsList.map((p) => p.withEager + && p.withEager.call(null, p.options, plugins))); if (options.loadLazy) { await options.loadLazy(document); } - await Promise.all(pluginsList.map((p) => p.withLazy && p.withLazy.call(null, p.options, plugins))); + await Promise.all(pluginsList.map((p) => p.withLazy + && p.withLazy.call(null, p.options, plugins))); window.setTimeout(() => { if (options.loadDelayed) { options.loadDelayed(); } - Promise.all(pluginsList.map((p) => p.withDelayed && p.withDelayed.call(null, p.options, plugins))); + Promise.all(pluginsList.map((p) => p.withDelayed + && p.withDelayed.call(null, p.options, plugins))); }, options.delayedDuration || 3000); } -export async function withPlugin(path, options = {}) { - const pluginName = path.split('/').pop().replace('.js', ''); - const plugin = await import(path); - plugins[pluginName] = { ...plugin, options }; -} - /** * init block utils */ diff --git a/scripts/plugins/placeholders.js b/scripts/plugins/placeholders.js index 931b27602..ed0b98514 100644 --- a/scripts/plugins/placeholders.js +++ b/scripts/plugins/placeholders.js @@ -1,3 +1,7 @@ +import { + toCamelCase, +} from '../lib-franklin.js'; + let placeholders = {}; /** @@ -5,22 +9,22 @@ let placeholders = {}; * @param {string} prefix */ async function fetchPlaceholders(prefix = 'default') { - const placeholders = {}; const loaded = placeholders[`${prefix}-loaded`]; if (!loaded) { - placeholders[`${prefix}-loaded`] = new Promise(async (resolve, reject) => { + placeholders[`${prefix}-loaded`] = new Promise((resolve, reject) => { try { - const response = await fetch(`${prefix === 'default' ? '' : prefix}/placeholders.json`); - if (!response.ok) { - reject(); - } - const json = await resp.json(); - placeholders = json.data.reduce((results, placeholder) => { - results[toCamelCase(placeholder.Key)] = placeholder.Text; - return results; - }, {}); - resolve(placeholders); + fetch(`${prefix === 'default' ? '' : prefix}/placeholders.json`) + .then((resp) => resp.json()) + .then((json) => { + placeholders = json.data.reduce((results, placeholder) => { + results[toCamelCase(placeholder.Key)] = placeholder.Text; + return results; + }, {}); + resolve(placeholders); + }); } catch (error) { + // error loading placeholders + placeholders[prefix] = {}; reject(); } }); @@ -34,10 +38,10 @@ function getPlaceholders() { } export const api = { - getPlaceholders -} + getPlaceholders, +}; -export async function withLazy(document, options) { +export async function withLazy(options) { try { placeholders = await fetchPlaceholders(options.prefix); } catch (err) { diff --git a/scripts/plugins/rum.js b/scripts/plugins/rum.js index 1de9f9389..70f8ee8a7 100644 --- a/scripts/plugins/rum.js +++ b/scripts/plugins/rum.js @@ -3,7 +3,7 @@ * @param {string} checkpoint identifies the checkpoint in funnel * @param {Object} data additional data for RUM sample */ - function sampleRUM(checkpoint, data = {}) { +function sampleRUM(checkpoint, data = {}) { sampleRUM.defer = sampleRUM.defer || []; const defer = (fnname) => { sampleRUM[fnname] = sampleRUM[fnname] @@ -63,7 +63,7 @@ } export const api = { - sampleRUM + sampleRUM, }; export async function withEager(options) { @@ -81,6 +81,6 @@ export async function withEager(options) { }); } -export async function withDelayed(options) { +export async function withDelayed() { sampleRUM('cwv'); -} \ No newline at end of file +} diff --git a/scripts/scripts.js b/scripts/scripts.js index efb11f187..3c75573f4 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -10,13 +10,15 @@ import { init, loadBlocks, loadCSS, - plugins, waitForLCP, withPlugin, } from './lib-franklin.js'; const LCP_BLOCKS = []; // add your LCP blocks to the list +const rum = await withPlugin('./plugins/rum.js'); +withPlugin('./plugins/placeholders.js'); + function buildHeroBlock(main) { const h1 = main.querySelector('h1'); const picture = main.querySelector('picture'); @@ -101,9 +103,9 @@ async function loadLazy(doc) { loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); addFavIcon(`${window.hlx.codeBasePath}/styles/favicon.svg`); - plugins.rum.api.sampleRUM('lazy'); - plugins.rum.api.sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); - plugins.rum.api.sampleRUM.observe(main.querySelectorAll('picture > img')); + rum.sampleRUM('lazy'); + rum.sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); + rum.sampleRUM.observe(main.querySelectorAll('picture > img')); } /** @@ -115,9 +117,6 @@ function loadDelayed() { import('./delayed.js'); } -withPlugin('./plugins/rum.js'); -withPlugin('./plugins/placeholders.js'); - init({ loadEager, loadLazy, From 7c895d006a9a39700f29945f846c040aadbf4cc5 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Tue, 18 Oct 2022 10:39:21 +0200 Subject: [PATCH 03/28] feat: basic plugin system PoC --- scripts/lib-franklin.js | 23 +++++++++++++++-------- scripts/plugins/placeholders.js | 2 +- scripts/plugins/rum.js | 6 +++--- scripts/scripts.js | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 74d9d11c4..a7f2b5484 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -462,24 +462,31 @@ export async function withPlugin(path, options = {}) { export async function loadPage(options = {}) { const pluginsList = Object.values(plugins); + + await Promise.all(pluginsList.map((p) => p.preEager + && p.preEager.call(null, p.options, plugins))); if (options.loadEager) { await options.loadEager(document); } - await Promise.all(pluginsList.map((p) => p.withEager - && p.withEager.call(null, p.options, plugins))); + await Promise.all(pluginsList.map((p) => p.postEager + && p.postEager.call(null, p.options, plugins))); + await Promise.all(pluginsList.map((p) => p.preLazy + && p.preLazy.call(null, p.options, plugins))); if (options.loadLazy) { await options.loadLazy(document); } - await Promise.all(pluginsList.map((p) => p.withLazy - && p.withLazy.call(null, p.options, plugins))); + await Promise.all(pluginsList.map((p) => p.postLazy + && p.postLazy.call(null, p.options, plugins))); - window.setTimeout(() => { + window.setTimeout(async () => { + await Promise.all(pluginsList.map((p) => p.preDelayed + && p.preDelayed.call(null, p.options, plugins))); if (options.loadDelayed) { - options.loadDelayed(); + await options.loadDelayed(); } - Promise.all(pluginsList.map((p) => p.withDelayed - && p.withDelayed.call(null, p.options, plugins))); + Promise.all(pluginsList.map((p) => p.postDelayed + && p.postDelayed.call(null, p.options, plugins))); }, options.delayedDuration || 3000); } diff --git a/scripts/plugins/placeholders.js b/scripts/plugins/placeholders.js index ed0b98514..936b1b0c3 100644 --- a/scripts/plugins/placeholders.js +++ b/scripts/plugins/placeholders.js @@ -41,7 +41,7 @@ export const api = { getPlaceholders, }; -export async function withLazy(options) { +export async function preLazy(options) { try { placeholders = await fetchPlaceholders(options.prefix); } catch (err) { diff --git a/scripts/plugins/rum.js b/scripts/plugins/rum.js index 70f8ee8a7..7839b639a 100644 --- a/scripts/plugins/rum.js +++ b/scripts/plugins/rum.js @@ -66,8 +66,8 @@ export const api = { sampleRUM, }; -export async function withEager(options) { - window.hlx.RUM_GENERATION = options.rumGeneration || 'project-1'; // add your RUM generation information here +export async function preEager(options) { + window.hlx.RUM_GENERATION = options.projectName; // add your RUM generation information here sampleRUM('top'); window.addEventListener('load', () => sampleRUM('load')); @@ -81,6 +81,6 @@ export async function withEager(options) { }); } -export async function withDelayed() { +export async function preDelayed() { sampleRUM('cwv'); } diff --git a/scripts/scripts.js b/scripts/scripts.js index 3c75573f4..d0834fa44 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -16,7 +16,7 @@ import { const LCP_BLOCKS = []; // add your LCP blocks to the list -const rum = await withPlugin('./plugins/rum.js'); +const rum = await withPlugin('./plugins/rum.js', { projectName: 'project-1' }); withPlugin('./plugins/placeholders.js'); function buildHeroBlock(main) { From e8831faf8c7486dea3c45bc3137f1213e4188fb2 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Tue, 18 Oct 2022 17:02:55 +0200 Subject: [PATCH 04/28] feat: basic plugin system PoC --- scripts/lib-franklin.js | 22 ++++++++++------- scripts/plugins/placeholders.js | 3 +++ scripts/scripts.js | 1 - test/blocks/footer/footer.test.js | 5 +++- test/blocks/header/header.test.js | 7 ++++-- test/scripts/block-utils.test.js | 40 +++++++++++++++++-------------- 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index a7f2b5484..7fc4c0bfb 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -456,44 +456,48 @@ const plugins = {}; export async function withPlugin(path, options = {}) { const pluginName = path.split('/').pop().replace('.js', ''); const plugin = await import(path); - plugins[pluginName] = { ...plugin, options }; + plugins[toCamelCase(pluginName)] = { ...plugin, options }; return plugin.api || null; } export async function loadPage(options = {}) { - const pluginsList = Object.values(plugins); + const pluginsList = []; + const pluginsApis = {}; + Object.entries(plugins).forEach(([key, value]) => { + pluginsList.push(value); + pluginsApis[key] = plugins[key].api; + }); await Promise.all(pluginsList.map((p) => p.preEager - && p.preEager.call(null, p.options, plugins))); + && p.preEager.call(null, p.options, pluginsApis))); if (options.loadEager) { await options.loadEager(document); } await Promise.all(pluginsList.map((p) => p.postEager - && p.postEager.call(null, p.options, plugins))); + && p.postEager.call(null, p.options, pluginsApis))); await Promise.all(pluginsList.map((p) => p.preLazy - && p.preLazy.call(null, p.options, plugins))); + && p.preLazy.call(null, p.options, pluginsApis))); if (options.loadLazy) { await options.loadLazy(document); } await Promise.all(pluginsList.map((p) => p.postLazy - && p.postLazy.call(null, p.options, plugins))); + && p.postLazy.call(null, p.options, pluginsApis))); window.setTimeout(async () => { await Promise.all(pluginsList.map((p) => p.preDelayed - && p.preDelayed.call(null, p.options, plugins))); + && p.preDelayed.call(null, p.options, pluginsApis))); if (options.loadDelayed) { await options.loadDelayed(); } Promise.all(pluginsList.map((p) => p.postDelayed - && p.postDelayed.call(null, p.options, plugins))); + && p.postDelayed.call(null, p.options, pluginsApis))); }, options.delayedDuration || 3000); } /** * init block utils */ - export function init(options) { window.hlx = window.hlx || {}; window.hlx.codeBasePath = ''; diff --git a/scripts/plugins/placeholders.js b/scripts/plugins/placeholders.js index 936b1b0c3..419508f7f 100644 --- a/scripts/plugins/placeholders.js +++ b/scripts/plugins/placeholders.js @@ -21,6 +21,9 @@ async function fetchPlaceholders(prefix = 'default') { return results; }, {}); resolve(placeholders); + }) + .catch((err) => { + reject(err); }); } catch (error) { // error loading placeholders diff --git a/scripts/scripts.js b/scripts/scripts.js index d0834fa44..b37dcc88f 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -17,7 +17,6 @@ import { const LCP_BLOCKS = []; // add your LCP blocks to the list const rum = await withPlugin('./plugins/rum.js', { projectName: 'project-1' }); -withPlugin('./plugins/placeholders.js'); function buildHeroBlock(main) { const h1 = main.querySelector('h1'); diff --git a/test/blocks/footer/footer.test.js b/test/blocks/footer/footer.test.js index 2c183ad8e..4f3c37365 100644 --- a/test/blocks/footer/footer.test.js +++ b/test/blocks/footer/footer.test.js @@ -6,7 +6,10 @@ import { expect } from '@esm-bundle/chai'; document.body.innerHTML = await readFile({ path: '../../scripts/dummy.html' }); -const { buildBlock, decorateBlock, loadBlock } = await import('../../../scripts/lib-franklin.js'); +const { + buildBlock, decorateBlock, init, loadBlock, +} = await import('../../../scripts/lib-franklin.js'); +init(); document.body.innerHTML = await readFile({ path: '../../scripts/body.html' }); diff --git a/test/blocks/header/header.test.js b/test/blocks/header/header.test.js index 833224e7a..563f6be50 100644 --- a/test/blocks/header/header.test.js +++ b/test/blocks/header/header.test.js @@ -1,12 +1,15 @@ /* eslint-disable no-unused-expressions */ -/* global describe it */ +/* global describe before it */ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; document.body.innerHTML = await readFile({ path: '../../scripts/dummy.html' }); -const { buildBlock, decorateBlock, loadBlock } = await import('../../../scripts/lib-franklin.js'); +const { + buildBlock, decorateBlock, init, loadBlock, +} = await import('../../../scripts/lib-franklin.js'); +init(); document.body.innerHTML = await readFile({ path: '../../scripts/body.html' }); diff --git a/test/scripts/block-utils.test.js b/test/scripts/block-utils.test.js index b18a04107..ef8a212fb 100644 --- a/test/scripts/block-utils.test.js +++ b/test/scripts/block-utils.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-expressions */ -/* global describe before it */ +/* global describe before beforeEach it */ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; @@ -16,6 +16,10 @@ describe('Utils methods', () => { document.body.innerHTML = await readFile({ path: './body.html' }); }); + beforeEach(() => { + blockUtils.init(); + }); + it('Sanitizes class name', async () => { expect(blockUtils.toClassName('Hello world')).to.equal('hello-world'); expect(blockUtils.toClassName(null)).to.equal(''); @@ -47,27 +51,27 @@ describe('Utils methods', () => { expect(error).to.equal('error'); }); - it('Collects RUM data', async () => { - const sendBeacon = sinon.stub(navigator, 'sendBeacon'); - // turn on RUM - window.history.pushState({}, '', `${window.location.href}&rum=on`); - delete window.hlx; + // it('Collects RUM data', async () => { + // const sendBeacon = sinon.stub(navigator, 'sendBeacon'); + // // turn on RUM + // window.history.pushState({}, '', `${window.location.href}&rum=on`); + // delete window.hlx; - // sends checkpoint beacon - await blockUtils.sampleRUM('test', { foo: 'bar' }); - expect(sendBeacon.called).to.be.true; - sendBeacon.resetHistory(); + // // sends checkpoint beacon + // await blockUtils.sampleRUM('test', { foo: 'bar' }); + // expect(sendBeacon.called).to.be.true; + // sendBeacon.resetHistory(); - // sends cwv beacon - await blockUtils.sampleRUM('cwv', { foo: 'bar' }); - expect(sendBeacon.called).to.be.true; + // // sends cwv beacon + // await blockUtils.sampleRUM('cwv', { foo: 'bar' }); + // expect(sendBeacon.called).to.be.true; - // test error handling - sendBeacon.throws(); - await blockUtils.sampleRUM('error', { foo: 'bar' }); + // // test error handling + // sendBeacon.throws(); + // await blockUtils.sampleRUM('error', { foo: 'bar' }); - sendBeacon.restore(); - }); + // sendBeacon.restore(); + // }); it('Creates optimized picture', async () => { const $picture = blockUtils.createOptimizedPicture('/test/scripts/mock.png'); From feb0808bd90d487a5267f61df9de81c2c3869f3b Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Tue, 18 Oct 2022 17:20:58 +0200 Subject: [PATCH 05/28] test: add/fix unit tests --- scripts/lib-franklin.js | 25 +++++++++-------- test/scripts/block-utils.test.js | 4 +-- test/scripts/plugins/rum.test.js | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 test/scripts/plugins/rum.test.js diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 7fc4c0bfb..116bffc25 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -484,21 +484,24 @@ export async function loadPage(options = {}) { await Promise.all(pluginsList.map((p) => p.postLazy && p.postLazy.call(null, p.options, pluginsApis))); - window.setTimeout(async () => { - await Promise.all(pluginsList.map((p) => p.preDelayed - && p.preDelayed.call(null, p.options, pluginsApis))); - if (options.loadDelayed) { - await options.loadDelayed(); - } - Promise.all(pluginsList.map((p) => p.postDelayed - && p.postDelayed.call(null, p.options, pluginsApis))); - }, options.delayedDuration || 3000); + return new Promise((resolve) => { + window.setTimeout(async () => { + await Promise.all(pluginsList.map((p) => p.preDelayed + && p.preDelayed.call(null, p.options, pluginsApis))); + if (options.loadDelayed) { + await options.loadDelayed(); + } + await Promise.all(pluginsList.map((p) => p.postDelayed + && p.postDelayed.call(null, p.options, pluginsApis))); + resolve(); + }, options.delayedDuration || 3000); + }) } /** * init block utils */ -export function init(options) { +export async function init(options) { window.hlx = window.hlx || {}; window.hlx.codeBasePath = ''; @@ -512,5 +515,5 @@ export function init(options) { } } - loadPage(options); + return loadPage(options); } diff --git a/test/scripts/block-utils.test.js b/test/scripts/block-utils.test.js index ef8a212fb..a95a70c75 100644 --- a/test/scripts/block-utils.test.js +++ b/test/scripts/block-utils.test.js @@ -16,8 +16,8 @@ describe('Utils methods', () => { document.body.innerHTML = await readFile({ path: './body.html' }); }); - beforeEach(() => { - blockUtils.init(); + beforeEach(async () => { + await blockUtils.init({ delayedDuration: 10 }); }); it('Sanitizes class name', async () => { diff --git a/test/scripts/plugins/rum.test.js b/test/scripts/plugins/rum.test.js new file mode 100644 index 000000000..c958af792 --- /dev/null +++ b/test/scripts/plugins/rum.test.js @@ -0,0 +1,46 @@ +/* eslint-disable no-unused-expressions */ +/* global describe before beforeEach it */ + +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; + +let blockUtils; +let rum; + +document.body.innerHTML = await readFile({ path: '../dummy.html' }); +document.head.innerHTML = await readFile({ path: '../head.html' }); + +describe('Utils methods', () => { + before(async () => { + blockUtils = await import('../../../scripts/lib-franklin.js'); + rum = await blockUtils.withPlugin('../../../scripts/plugins/rum.js'); + document.body.innerHTML = await readFile({ path: '../body.html' }); + }); + + beforeEach(async () => { + await blockUtils.init({ delayedDuration: 10 }); + }); + + it('Collects RUM data', async () => { + const sendBeacon = sinon.stub(navigator, 'sendBeacon'); + // turn on RUM + window.history.pushState({}, '', `${window.location.href}&rum=on`); + delete window.hlx; + + // sends checkpoint beacon + await rum.sampleRUM('test', { foo: 'bar' }); + expect(sendBeacon.called).to.be.true; + sendBeacon.resetHistory(); + + // sends cwv beacon + await rum.sampleRUM('cwv', { foo: 'bar' }); + expect(sendBeacon.called).to.be.true; + + // test error handling + sendBeacon.throws(); + await rum.sampleRUM('error', { foo: 'bar' }); + + sendBeacon.restore(); + }); +}); From b2cd24774a4437bcddc0d219486ace26a5a14298 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Wed, 19 Oct 2022 07:17:18 +0200 Subject: [PATCH 06/28] feat: preload plugins --- head.html | 1 + 1 file changed, 1 insertion(+) diff --git a/head.html b/head.html index 55e6751fc..6dab990fd 100644 --- a/head.html +++ b/head.html @@ -1,4 +1,5 @@ + From 58b8119254afd2f9dadda6a3a7b702ca1a049079 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Wed, 19 Oct 2022 07:22:40 +0200 Subject: [PATCH 07/28] feat: preload plugins --- head.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/head.html b/head.html index 6dab990fd..9ac2816e0 100644 --- a/head.html +++ b/head.html @@ -1,5 +1,5 @@ - + From fa0054f4ffc635efbc66990be5aa200ac163faea Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Wed, 19 Oct 2022 07:27:11 +0200 Subject: [PATCH 08/28] chore: fix linting issues --- scripts/lib-franklin.js | 2 +- test/blocks/header/header.test.js | 2 +- test/scripts/block-utils.test.js | 1 - test/scripts/plugins/rum.test.js | 6 ++++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 116bffc25..8a2a83a02 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -495,7 +495,7 @@ export async function loadPage(options = {}) { && p.postDelayed.call(null, p.options, pluginsApis))); resolve(); }, options.delayedDuration || 3000); - }) + }); } /** diff --git a/test/blocks/header/header.test.js b/test/blocks/header/header.test.js index 563f6be50..741b3b4a9 100644 --- a/test/blocks/header/header.test.js +++ b/test/blocks/header/header.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-expressions */ -/* global describe before it */ +/* global describe it */ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; diff --git a/test/scripts/block-utils.test.js b/test/scripts/block-utils.test.js index a95a70c75..d2b897ae0 100644 --- a/test/scripts/block-utils.test.js +++ b/test/scripts/block-utils.test.js @@ -3,7 +3,6 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -import sinon from 'sinon'; let blockUtils; diff --git a/test/scripts/plugins/rum.test.js b/test/scripts/plugins/rum.test.js index c958af792..0ffbbfe47 100644 --- a/test/scripts/plugins/rum.test.js +++ b/test/scripts/plugins/rum.test.js @@ -43,4 +43,10 @@ describe('Utils methods', () => { sendBeacon.restore(); }); + + // it('Reports errors as RUM metrics', async () => { + // const sendBeacon = sinon.stub(navigator, 'sendBeacon'); + // window.history.pushState({}, '', `${window.location.href}&rum=on`); + // window.dispatchEvent('error', ); + // }); }); From f99a97bbf841795481453653d498425b984465f4 Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Fri, 21 Oct 2022 10:42:08 +0200 Subject: [PATCH 09/28] feat: extract decorator plugin --- blocks/footer/footer.js | 6 +- blocks/header/header.js | 8 +- head.html | 1 + scripts/lib-franklin.js | 338 ++++++++++------------------------- scripts/plugins/decorator.js | 151 ++++++++++++++++ scripts/scripts.js | 38 +++- 6 files changed, 287 insertions(+), 255 deletions(-) create mode 100644 scripts/plugins/decorator.js diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index fa0093819..462941411 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -1,11 +1,11 @@ -import { readBlockConfig, decorateIcons } from '../../scripts/lib-franklin.js'; +import { readBlockConfig } from '../../scripts/lib-franklin.js'; /** * loads and decorates the footer * @param {Element} block The header block element */ -export default async function decorate(block) { +export default async function decorate(block, plugins) { const cfg = readBlockConfig(block); block.textContent = ''; @@ -14,6 +14,6 @@ export default async function decorate(block) { const html = await resp.text(); const footer = document.createElement('div'); footer.innerHTML = html; - await decorateIcons(footer); + await plugins.decorator.decorateIcons(footer); block.append(footer); } diff --git a/blocks/header/header.js b/blocks/header/header.js index bce97e9c6..738b81ec9 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,4 +1,4 @@ -import { readBlockConfig, decorateIcons } from '../../scripts/lib-franklin.js'; +import { readBlockConfig } from '../../scripts/lib-franklin.js'; /** * collapses all open nav sections @@ -16,7 +16,7 @@ function collapseAllNavSections(sections) { * @param {Element} block The header block element */ -export default async function decorate(block) { +export default async function decorate(block, plugins) { const cfg = readBlockConfig(block); block.textContent = ''; @@ -29,7 +29,7 @@ export default async function decorate(block) { // decorate nav DOM const nav = document.createElement('nav'); nav.innerHTML = html; - decorateIcons(nav); + plugins.decorator.decorateIcons(nav); const classes = ['brand', 'sections', 'tools']; classes.forEach((e, j) => { @@ -60,7 +60,7 @@ export default async function decorate(block) { }); nav.prepend(hamburger); nav.setAttribute('aria-expanded', 'false'); - decorateIcons(nav); + plugins.decorator.decorateIcons(nav); block.append(nav); } } diff --git a/head.html b/head.html index 9ac2816e0..fab0e7389 100644 --- a/head.html +++ b/head.html @@ -1,4 +1,5 @@ + diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 8a2a83a02..7dce7e2c7 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -60,127 +60,16 @@ export function toCamelCase(name) { return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } -/** - * Replace icons with inline SVG and prefix with codeBasePath. - * @param {Element} element - */ -export function decorateIcons(element = document) { - element.querySelectorAll('span.icon').forEach(async (span) => { - if (span.classList.length < 2 || !span.classList[1].startsWith('icon-')) { - return; - } - const icon = span.classList[1].substring(5); - // eslint-disable-next-line no-use-before-define - const resp = await fetch(`${window.hlx.codeBasePath}/icons/${icon}.svg`); - if (resp.ok) { - const iconHTML = await resp.text(); - if (iconHTML.match(/