Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a (revised) minimal plugin and template system #374

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
723df25
chore: update fstab
ramboz Oct 11, 2023
6053c72
Merge branch 'adobe:main' into main
ramboz Nov 7, 2023
8e0fac5
Merge branch 'adobe:main' into main
ramboz Jun 21, 2024
804de77
fix: properly namespace events
ramboz Jun 21, 2024
70813c3
chore: cleanup PR
ramboz Jun 21, 2024
fe2f5c6
chore: cleanup PR
ramboz Jun 21, 2024
e3531d7
chore: cleanup PR
ramboz Jun 21, 2024
7880a85
chore: cleanup PR
ramboz Jun 21, 2024
6c1a4e5
chore: cleanup PR
ramboz Jun 21, 2024
69f097b
chore: cleanup PR
ramboz Jun 21, 2024
95b9a86
chore: cleanup PR
ramboz Jun 21, 2024
2381f58
chore: update to latest code
ramboz Jun 21, 2024
430287a
doc: add jsdoc
ramboz Jul 23, 2024
d4e3999
chore: update fstab
ramboz Oct 11, 2023
8b0eae8
Merge branch 'main' into plugin-system2
ramboz Jul 23, 2024
ef1e748
feat: better error tracking (#375)
kptdobe Jun 27, 2024
f6d9748
feat(lib): update scripts/aem.js to [email protected] (#378)
adobe-bot Jul 1, 2024
3b61331
feat: section loader
davidnuescheler Jul 16, 2024
12eabe1
chore(deps): update dependency stylelint to v16.7.0 (#381)
renovate[bot] Jul 16, 2024
652eee4
chore(deps): update dependency @babel/eslint-parser to v7.24.8 (#380)
renovate[bot] Jul 16, 2024
bdbc32b
chore(deps): update dependency stylelint-config-standard to v36.0.1 (…
renovate[bot] Jul 16, 2024
2629743
feat(lib): update scripts/aem.js to [email protected] (#383)
adobe-bot Jul 16, 2024
85a639a
feat: use rum js v2 (#371)
kptdobe Jul 16, 2024
4f6aecc
Merge branch 'main' into plugin-system2
ramboz Jul 23, 2024
311b856
refactor: clean up the code
ramboz Jul 23, 2024
b70f789
refactor: clean up the code
ramboz Jul 23, 2024
f0ca1c0
refactor: clean up the code
ramboz Jul 23, 2024
16f22d0
refactor: clean up the code
ramboz Jul 23, 2024
326a001
refactor: clean up the code
ramboz Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 180 additions & 45 deletions scripts/aem.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -94,33 +95,17 @@ function sampleRUM(checkpoint, data) {
}

/**
* Setup block utils.
* Dispatches a custom DOM event and awaits all listeners before returning.
* @param {String} eventName The custom event to trigger
* @param {Object} [detail] Optional detail objec to pass to the event
* @returns a promise that all async listeners have run
*/
function setup() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved further down in the file.

window.hlx = window.hlx || {};
window.hlx.RUM_MASK_URL = 'full';
window.hlx.RUM_MANUAL_ENHANCE = true;
window.hlx.codeBasePath = '';
window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on';

const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]');
if (scriptEl) {
try {
[window.hlx.codeBasePath] = new URL(scriptEl.src).pathname.split('/scripts/scripts.js');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
}

/**
* Auto initializiation.
*/

function init() {
setup();
sampleRUM();
async function dispatchAsyncEvent(eventName, detail = {}) {
const promises = [];
const event = new CustomEvent(eventName, { detail });
event.await = (p) => promises.push(p);
document.dispatchEvent(event);
return Promise.all(promises);
}

/**
Expand Down Expand Up @@ -531,6 +516,39 @@ function buildBlock(blockName, content) {
return blockEl;
}

/**
* Loads the specified module with its JS and CSS files and returns the JS API if applicable.
* @param {String} name The module name
* @param {String} cssPath A path to the CSS file to load, or null
* @param {String} jsPath A path to the JS file to load, or null
* @param {...any} args Arguments to use to call the default export on the JS file
* @returns a promsie that the module was loaded, and that returns the JS API is any
*/
async function loadModule({
name, cssPath, jsPath, el,
}) {
const cssLoaded = cssPath ? loadCSS(cssPath) : Promise.resolve();
const decorationComplete = jsPath
? new Promise((resolve) => {
(async () => {
let mod;
try {
mod = await import(jsPath);
if (mod.default) {
await mod.default(el);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`failed to load module for ${name}`, error);
}
resolve(mod);
})();
})
: Promise.resolve();
return Promise.all([cssLoaded, decorationComplete])
.then(([, api]) => api);
}

/**
* Loads JS and CSS for a block.
* @param {Element} block The block element
Expand All @@ -541,29 +559,25 @@ async function loadBlock(block) {
block.dataset.blockStatus = 'loading';
const { blockName } = block.dataset;
try {
const cssLoaded = loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted above in loadModule so we have a generic logic to use also to load the plugins

const decorationComplete = new Promise((resolve) => {
(async () => {
try {
const mod = await import(
`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.js`
);
if (mod.default) {
await mod.default(block);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`failed to load module for ${blockName}`, error);
}
resolve();
})();
const cssPath = `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`;
const jsPath = `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.js`;
const config = {
block,
blockName,
cssPath,
jsPath,
};
await dispatchAsyncEvent('aem:block:config', config);
await loadModule({
name: blockName, cssPath, jsPath, el: block,
});
await Promise.all([cssLoaded, decorationComplete]);
await dispatchAsyncEvent('aem:blockdecorated', { name: blockName, block });
} catch (error) {
// eslint-disable-next-line no-console
console.log(`failed to load block ${blockName}`, error);
}
block.dataset.blockStatus = 'loaded';
await dispatchAsyncEvent('aem:block:loaded', { name: blockName, block });
}
return block;
}
Expand Down Expand Up @@ -639,7 +653,6 @@ async function waitForFirstImage(section) {
* Loads all blocks in a section.
* @param {Element} section The section element
*/

async function loadSection(section, loadCallback) {
const status = section.dataset.sectionStatus;
if (!status || status === 'initialized') {
Expand All @@ -652,14 +665,14 @@ async function loadSection(section, loadCallback) {
if (loadCallback) await loadCallback(section);
section.dataset.sectionStatus = 'loaded';
section.style.display = null;
dispatchAsyncEvent('aem:section:loaded', { section });
}
}

/**
* Loads all sections.
* @param {Element} element The parent element of sections to load
*/

async function loadSections(element) {
const sections = [...element.querySelectorAll('div.section')];
for (let i = 0; i < sections.length; i += 1) {
Expand All @@ -668,8 +681,127 @@ async function loadSections(element) {
}
}

/**
* Parses the plugin id and config paramters and returns a proper config
*
* @param {String} id A string that idenfies the plugin, or a path to it
* @param {String|Object} [config] A string representing the path to the plugin, or a config object
* @returns an object returning the the plugin id and its config
*/
function parsePluginParams(id, config) {
const pluginId = !config
? id.split('/').splice(id.endsWith('/') ? -2 : -1, 1)[0].replace(/\.js/, '')
: id;
const pluginConfig = typeof config === 'string' || !config
? { load: 'lazy', url: (config || id).replace(/\/$/, '') }
: { load: config.eager ? 'eager' : 'lazy', ...config };
pluginConfig.options ||= {};
return { id: toClassName(pluginId), config: pluginConfig };
}

class PluginsRegistry {
#plugins;

constructor() {
this.#plugins = new Map();
}

// Register a new plugin
add(id, config) {
const { id: pluginId, config: plugin } = parsePluginParams(id, config);
this.#plugins.set(pluginId, plugin);
document.addEventListener(`aem:${plugin.load}`, (ev) => {
if (plugin.condition && !plugin.condition(document, plugin.options)) {
return;
}
if (plugin.url) {
const isJsUrl = plugin.url.endsWith('.js');
const loadPromise = loadModule({
name: pluginId,
cssPath: !isJsUrl ? `${plugin.url}/${pluginId}.css` : null,
jsPath: !isJsUrl ? `${plugin.url}/${pluginId}.js` : plugin.url,
el: document,
}).then((api = {}) => {
this.#plugins.set(pluginId, { ...plugin, ...api });
});
ev.await(loadPromise);
} else if (plugin.run) {
plugin.run(document, plugin.options);
}
['eager', 'lazy', 'delayed'].forEach((phase) => {
if (plugin[phase] && ev.type !== `aem:${phase}`) {
document.addEventListener(`aem:${phase}`, () => plugin[phase](document, plugin.options), { once: true });
} else if (plugin[phase] && ev.type === `aem:${phase}`) {
plugin[phase](document, plugin.options);
}
});
}, { once: true });
}

// Get the plugin
get(id) { return this.#plugins.get(id); }

// Check if the plugin exists
has(id) { return !!this.#plugins.has(id); }
}

class TemplatesRegistry {
// Register a new template
// eslint-disable-next-line class-methods-use-this
add(id, url) {
const { id: templateId, config: templateConfig } = parsePluginParams(id, url);
window.hlx.plugins.add(templateId, {
...templateConfig,
condition: () => toClassName(getMetadata('template')) === templateId,
load: 'eager',
});
}

// Get the template
// eslint-disable-next-line class-methods-use-this
get(id) { return window.hlx.plugins.get(id); }

// Check if the template exists
// eslint-disable-next-line class-methods-use-this
has(id) { return window.hlx.plugins.includes(id); }
}

/**
* Setup block utils.
*/
function setup() {
window.hlx = window.hlx || {};
window.hlx.RUM_MASK_URL = 'full';
window.hlx.RUM_MANUAL_ENHANCE = true;
window.hlx.codeBasePath = '';
window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on';
window.hlx.plugins = new PluginsRegistry();
window.hlx.templates = new TemplatesRegistry();

const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]');
if (scriptEl) {
try {
[window.hlx.codeBasePath] = new URL(scriptEl.src).pathname.split('/scripts/scripts.js');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
}

/**
* Auto initializiation.
*/
async function init() {
setup();
sampleRUM();
}

init();

const withPlugin = window.hlx.plugins.add.bind(window.hlx.plugins);
const withTemplate = window.hlx.templates.add.bind(window.hlx.templates);

export {
buildBlock,
createOptimizedPicture,
Expand All @@ -695,4 +827,7 @@ export {
toClassName,
waitForFirstImage,
wrapTextNodes,
withPlugin,
withTemplate,
dispatchAsyncEvent,
};
10 changes: 8 additions & 2 deletions scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
loadSections,
loadCSS,
sampleRUM,
dispatchAsyncEvent,
} from './aem.js';

/**
Expand Down Expand Up @@ -73,7 +74,7 @@ export function decorateMain(main) {
* @param {Element} doc The container element
*/
async function loadEager(doc) {
document.documentElement.lang = 'en';
doc.documentElement.lang = 'en';
decorateTemplateAndTheme();
const main = doc.querySelector('main');
if (main) {
Expand Down Expand Up @@ -119,12 +120,17 @@ async function loadLazy(doc) {
*/
function loadDelayed() {
// eslint-disable-next-line import/no-cycle
window.setTimeout(() => import('./delayed.js'), 3000);
window.setTimeout(async () => {
await dispatchAsyncEvent('aem:lazy');
import('./delayed.js');
}, 3000);
// load anything that can be postponed to the latest here
}

async function loadPage() {
await dispatchAsyncEvent('aem:eager');
await loadEager(document);
await dispatchAsyncEvent('aem:lazy');
await loadLazy(document);
loadDelayed();
}
Expand Down