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 minimal plugin and template system #254

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft

feat: a minimal plugin and template system #254

wants to merge 29 commits into from

Conversation

ramboz
Copy link
Collaborator

@ramboz ramboz commented Sep 6, 2023

Use case

As a developer, I want to be able to easily organize my code into different files, and execute some of the logic in each phase of the page load as needed.

In particular, I want a way to:

  • easily define templates that have some JS logic
  • add plugins for more advanced features like experimentation, conversion tracking, tag management, etc.
  • load templates and plugins only when they are needed
  • control in what phase a plugin is loaded to avoid negatively impacting performances if it is not needed immediately

Technical Requirements

  • plugins and templates should use the same logic to reduce code duplication
  • plugins and templates should re-use the block loading approach to load both JS and CSS files, if applicable
  • plugins should offer a way to "override" code methods, for instance to add icon spriting on top of regular icon decoration
  • developers should be able to decide where exactly in each phase the plugins are run so they can control pre/post behavior according to the project needs
  • performance impact should be negligible
  • plugins and templates should offer a minimal API on the window.hlx.* namespace
  • the system should help in making plugins and templates easier to unit test (using mostly pure functions)

Proposal

Introduce 2 new namespaces on window.hlx.*:

  • window.hlx.templates
    • add(id, url): register a new template
    • get(id): get the template
    • includes(id): check if a template exists
  • window.hlx.plugins
    • add(id, config): add a new plugin
    • get(id): get the plugin
    • includes(id): check if a plugin exists
    • load(phase): load all plugins for the current phase (eager, lazy or delayed)
    • run(method): runs the specified method on the plugins (loadEager, loadLazy or loadDelayed)

Plugins and templates would follow the same API:

  • export default function init(document, options): logic executed immediately when the plugin/template is loaded
  • export function loadEager(document, options): logic executed in the eager phase
  • export function loadLazy(document, options): logic executed in the lazy phase
  • export function loadDelayed(document, options): logic executed in the delayed phase

where this is set to an execution context that contains most lib-franklin.js helper functions, so we can reduce imports in the plugins (makes it easier to mock in tests, and also avoids dependency cycles).

Or the slightly more verbose version below for arrow functions:

  • export default (document, options, context) => {}
  • export const loadEager = (document, options, context) => {}
  • export const loadLazy = (document, options, context) => {}
  • export const loadDelayed = (document, options, context) => {}

ℹ️ All those exports are optional and are only executed if present.

Extracting a new loadModule(name, cssPath, jsPath, args) method that both the loadBlock and the new plugin system use.

Usage

Adding Templates

Add a new template to the project:

window.hlx.templates.add('foo', '/templates/foo.js');

or the shorthand version:

window.hlx.templates.add('/templates/bar.js');

or the bulk version:

window.hlx.templates.add([
  '/templates/bar.js',
  '/templates/baz.js'
]);

or the the module approach that loads both js and css:

window.hlx.templates.add('/templates/bar');
// loads both /templates/bar/bar.css and /templates/bar/bar.js

Adding Plugins

Add a new inline plugin to the project:

window.hlx.plugins.add('inline-plugin-baz', {
  condition: () => true, // if defined, the condition is evaluated before running any code in the plugin
  loadEager: () => {  },
  loadLazy: () => {  },
  loadDelayed: () => {  },
});

Add a regular plugin to the project that will always load (no condition):

window.hlx.plugins.add('plugin-qux', { url: '/plugins/qux/src/index.js' });

or the module approach that loads both js and CSS:

window.hlx.plugins.add('plugin-qux', { url: '/plugins/qux' });

or the shorthand version:

window.hlx.plugins.add('/plugins/qux');

Add a plugin that only loads in the lazy phase and also passes some additional options:

window.hlx.plugins.add('plugin-qux', {
  url: '/plugins/qux/src/index.js',
  load: 'lazy', // can be `eager`, `lazy` or `delayed`. defaults to `eager` if omitted
  options: { corge: 'grault' },
});

Once plugins have been loaded via window.hlx.plugins.load(phase), we can then run individual methods as required via window.hlx.plugins.run('loadEager'), etc.

Plugin Template

/plugins/qux.js

// document: to keep it consistent with the loadEager in the scripts.js file
// options: additional options passed to the plugin when it was added
// context: passes a plugin context which gives access to the main lib-franklin.js APIs and avoids cyclic dependencies
//          it also turns all of those into pure functions that are easier to unit test
export default (document, options, context) => {
  console.log('plugin qux: init', options, context);
};

export const loadEager = (document, options, context) => {
  console.log('plugin qux: eager', options, context);
};

export const loadLazy = (document, options, context) => {
  console.log('plugin qux: lazy', options, context);
};

export const loadDelayed = (document, options, context) => {
  console.log('plugin qux: delayed', options, context);
};

When using proper functions, the this variable is set to the context, so we can ignore the last parameter. This is mostly for backward compatibility with existing approaches that were passing it down that way until now, but also offers a slightly shorter API.
Or with proper functions:
/plugins/qux.js

// document: to keep it consistent with the loadEager in the scripts.js file
// options: additional options passed to the plugin when it was added
// context: passes a plugin context which gives access to the main lib-franklin.js APIs and avoids cyclic dependencies
//          it also turns all of those into pure functions that are easier to unit test
export default function init(document, options, context) {
  console.log('plugin qux: init', options, context);
};

export function loadEager(document, options, context) {
  console.log('plugin qux: eager', options, context);
};

export function loadLazy(document, options, context) {
  console.log('plugin qux: lazy', options, context);
};

export function loadDelayed(document, options, context) {
  console.log('plugin qux: delayed', options, context);
};

Examples

A barebone demo site built for this PR:

We have a few sites that are being instrumented with this:

We also do have a few plugins that already follow this API:

As well as a few template mechanisms that could leverage it:

Those could all be aligned on a consistent system.

Test URLs:

@aem-code-sync
Copy link

aem-code-sync bot commented Sep 6, 2023

Hello, I'm Franklin Bot and I will run some test suites that validate the page speed.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-run PSI Checks

@aem-code-sync
Copy link

aem-code-sync bot commented Sep 6, 2023

Page Scores Audits Google
/ PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI

@ramboz ramboz changed the title feat: a minimal plugin and template system [WIP] feat: a minimal plugin and template system Sep 6, 2023
@ramboz ramboz requested a review from kptdobe September 15, 2023 16:18
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 15, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 26, 2023
@adobe adobe deleted a comment from aem-code-sync bot Sep 26, 2023
@aem-code-sync
Copy link

aem-code-sync bot commented Sep 26, 2023

Page Scores Audits Google
/ PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
/?0.7093454368913195 PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
/?0.5814969076944914 PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI

@aem-code-sync
Copy link

aem-code-sync bot commented Sep 26, 2023

Page Scores Audits Google
/ PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
/?0.5814969076944914 PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
/?0.7093454368913195 PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI

Copy link

@chicharr chicharr left a comment

Choose a reason for hiding this comment

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

This plugin system is great, and much needed.
It provides a formal way to introduce add-ons to Franklin/EDS, in an optimal, easy, and non-intrusive way.
There are many features that would benefit from it:

  • experimentation
  • rum conversion tracking
  • Martech integration (neutrino)
  • omnivore
  • etc...

*/
function runFunctionWithContext(fn, args, context) {
return fn.toString().startsWith('function')
? fn.call(context, ...args, context)
Copy link

Choose a reason for hiding this comment

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

is it worth to use .call?
It feels a bit strange to have methods which are defined in aem-lib (or lib-franklin) in the this object of a plugin function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The idea behind it was:

  • simplify imports and given immediate access to all "core" functions
  • allow for "overriding" core functions by modifying the context if needed, as opposed to imports which are read-only (think icons decoration via svg spriting for instance that could replace the regular decorateIcons)
  • simplify unit testing, since you don't have to inject mocked dependencies anymore

}

// Register a new plugin
add(id, config) {
Copy link

Choose a reason for hiding this comment

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

I was thinking that plugins could register themselves, instead of being explicitly registered in the scripts.js as suggested in the docs. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For a plugin to register itself, it means we need to load the plugin unconditionally and then let it decide itself if it triggers or not. I think this goes against the performance-first paradigm.

If you have a hefty preview overlay plugin for instance, and the plugin registers itself, that means you have to load a large JS file just to see the plugin not activate when the conditions aren't met (i.e. not preview, and likely lazy loaded)

@ramboz
Copy link
Collaborator Author

ramboz commented Oct 5, 2023

@davidnuescheler @rofe @kptdobe @fkakatie Would love to have your thoughts on the current proposal to see how to fine-tune this.

@kptdobe
Copy link
Contributor

kptdobe commented Oct 16, 2023

The current is:

@ramboz
Copy link
Collaborator Author

ramboz commented Oct 25, 2023

And we also agreed to remove the this context hack on proper functions in favor of a more consistent use of the context last parameter on the main exports

@ramboz
Copy link
Collaborator Author

ramboz commented Oct 26, 2023

@ramboz ramboz marked this pull request as draft October 26, 2023 09:05
ramboz added a commit to adobe/aem-experimentation that referenced this pull request Oct 27, 2023
Refactor the engine to adopt the new plugin system proposal in
adobe/aem-boilerplate#254.

BREAKING:
- The new plugin API changes the high-level signature of the exported
methods. All methods now follow a `*(document, options, context)`
signature, and do not pass a `this` context anymore

Test URLs:
-
https://main--aem-experience-decisioning-demo--ramboz.hlx.page/audiences/
-
https://main--aem-experience-decisioning-demo--ramboz.hlx.page/campaigns/
-
https://main--aem-experience-decisioning-demo--ramboz.hlx.page/experiments/
ramboz added a commit to hlxsites/mammotome that referenced this pull request Nov 1, 2023
The PR adds the plugin system proposed in adobe/aem-boilerplate#254.
It also updates the 2 used plugins to their latest versions and adds some of the missing lib-franklin.js methods that are expected to be there.

Test URLs:
- Before: https://main--mammotome--hlxsites.hlx.page/
- After: https://plugin-system--mammotome--hlxsites.hlx.page/
ramboz added a commit to hlxsites/moleculardevices that referenced this pull request Nov 6, 2023
The PR adds the plugin system proposed in adobe/aem-boilerplate#254.
It also updates a few lib-franklin.js methods that were missing, along with the blocks that are impacted, and the template loading is converted to the new system.

Test URLs:
- Before: https://main--moleculardevices--hlxsites.hlx.page/
- After: https://plugin-system--moleculardevices--hlxsites.hlx.page/
ramboz added a commit to adobe/helix-website that referenced this pull request Nov 6, 2023
As discussed with @trieloff during our recent workshop, I'm adding the plugin system and the experimentation plugin to the website.

## Description

1. Adds the minimal plugin system proposal from adobe/aem-boilerplate#254
2. Adds the experimentation plugin from https://github.com/adobe/aem-experience-decisioning/tree/plugin-api
3. Instrument the existing `performance.js` as a plugin
ramboz added a commit to hlxsites/bitdefender that referenced this pull request Nov 7, 2023
The PR adds the plugin system proposed in adobe/aem-boilerplate#254 and updates the rum conversion instrumentation to leverage it.

Test URLs:
- Before: https://main--bitdefender--hlxsites.hlx.page/solutions/
- After: https://plugin-system--bitdefender--hlxsites.hlx.page/solutions/
@rofe
Copy link
Contributor

rofe commented May 25, 2024

@ramboz any update on this?

@ramboz
Copy link
Collaborator Author

ramboz commented May 26, 2024

@rofe We've tried it on a dozen websites so far, and it's working fine. But I remember discussions with @trieloff to try to simplify this and leverage more the CustomEvent API. This would also align better with how the RUM library has evolved.
I'm planning on having a revised proposal accordingly… but it's not exactly at the top of my priority list at the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants