-
Notifications
You must be signed in to change notification settings - Fork 362
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
ramboz
wants to merge
29
commits into
adobe:main
Choose a base branch
from
ramboz:plugin-system2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
723df25
chore: update fstab
ramboz 6053c72
Merge branch 'adobe:main' into main
ramboz 8e0fac5
Merge branch 'adobe:main' into main
ramboz 804de77
fix: properly namespace events
ramboz 70813c3
chore: cleanup PR
ramboz fe2f5c6
chore: cleanup PR
ramboz e3531d7
chore: cleanup PR
ramboz 7880a85
chore: cleanup PR
ramboz 6c1a4e5
chore: cleanup PR
ramboz 69f097b
chore: cleanup PR
ramboz 95b9a86
chore: cleanup PR
ramboz 2381f58
chore: update to latest code
ramboz 430287a
doc: add jsdoc
ramboz d4e3999
chore: update fstab
ramboz 8b0eae8
Merge branch 'main' into plugin-system2
ramboz ef1e748
feat: better error tracking (#375)
kptdobe f6d9748
feat(lib): update scripts/aem.js to [email protected] (#378)
adobe-bot 3b61331
feat: section loader
davidnuescheler 12eabe1
chore(deps): update dependency stylelint to v16.7.0 (#381)
renovate[bot] 652eee4
chore(deps): update dependency @babel/eslint-parser to v7.24.8 (#380)
renovate[bot] bdbc32b
chore(deps): update dependency stylelint-config-standard to v36.0.1 (…
renovate[bot] 2629743
feat(lib): update scripts/aem.js to [email protected] (#383)
adobe-bot 85a639a
feat: use rum js v2 (#371)
kptdobe 4f6aecc
Merge branch 'main' into plugin-system2
ramboz 311b856
refactor: clean up the code
ramboz b70f789
refactor: clean up the code
ramboz f0ca1c0
refactor: clean up the code
ramboz 16f22d0
refactor: clean up the code
ramboz 326a001
refactor: clean up the code
ramboz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
|
@@ -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() { | ||
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); | ||
} | ||
|
||
/** | ||
|
@@ -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 | ||
|
@@ -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`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted above in |
||
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; | ||
} | ||
|
@@ -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') { | ||
|
@@ -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) { | ||
|
@@ -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, | ||
|
@@ -695,4 +827,7 @@ export { | |
toClassName, | ||
waitForFirstImage, | ||
wrapTextNodes, | ||
withPlugin, | ||
withTemplate, | ||
dispatchAsyncEvent, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.