Plugins allow you to extend existing functionality without changing the original code. For that, ScandiPWA makes use of the Mosaic.js package, which introduces namespaces to easily add a unique identifier to a function or class, and provides config objects to allow applying plugins to these namespaces.

Creating a plugin function and config object depends on the type of original code you want to plug in. This can be of type function for namespaces defined above the function, member-function for functions of a class that has the namespace, member-property for properties of classes that have a namespace, or static-member for static methods of classes that define a namespace.

The plugin configuration object enables the application of multiple plugins to the same namespace, which can enhance plugin reusability and readability. It also allows you to set the priority of execution of a plugin.

ScandiPWA has some best practices for working with plugins, as well as a tool for plugin debugging.

<aside> ➡️ This guide will focus on plugins. To modify the appearance of the app, please use the override mechanism. How to override a file?

</aside>

What is a plugin?

Plugins add capabilities to an existing program without changing the original program's code.

const getData = () => {
    return ['Initial data'];
}
const data = getData();
console.log(data); // ['Initial data']

This getData function returns ['Initial data'], but how to change its functionality without changing the function directly? Plugins allow for example to change what is returned:

const plugin = (callback) => {
    const data = callback();  // ['Initial data']

    return [
        ...data,
        'Data from the plugin'
    ];
};

const newData = plugin(getData);
console.log(newData); // ['Initial data', 'Data from the plugin']

<aside> ⚠️ This is a limited example of how plugins work. They may also include the original function parameters and a class instance depending on the type, and also a mechanism to connect the plugin to the original function or property.

</aside>

This plugin is a function that accepts the original function as a callback and changes its behavior. If this plugin function is added to the getData function, which means that the original function is passed as a parameter to the plugin, it returns ['Initial data', 'Data from the plugin'].

plugin-2.png

This is why a plugin acts as a proxy between the original function or property and the caller - it intermediates between the two and adds extra functionality.

There is no limit to the number of plugins a function or property can have. However, it's important to understand the tradeoffs and best practices involved.

If need to add another plugin, the function would look like this:

const newData = plugin1(plugin2(getData));

In reality, it would be impractical to require the caller to include a plugin every time one is added. That's why a plugin mechanism is necessary to connect the caller and the original function with the plugins.

How do plugins work in ScandiPWA?

ScandiPWA is installed as an NPM module in the node_modules directory, which means that you should not modify its source directly. Instead, ScandiPWA uses the Mosaic.js package to allow you to customize the application's appearance or extend its functionality. To achieve this, you have two options:

Depiction of how third-party overrides and plugins incorporate in original source code without losing overriding it entirely or losing old code.

Depiction of how third-party overrides and plugins incorporate in original source code without losing overriding it entirely or losing old code.

In the context of ScandiPWA, a plugin is a JavaScript file that can be injected into functions, methods, static properties, and even classes.

Plugins enable you to redefine the behavior and properties of objects or functions within the application. For example:

How does ScandiPWA identify the original code?

For the plugin system to work, the concept of namespace is introduced.

Namespaces are unique identifiers that can be applied to functions or classes. They are defined directly above the code they identify.

/** @namespace <NAMESPACE> */

For example:

/** @namespace Route/Checkout/Container/mapStateToProps */
export const mapStateToProps = (state) => ({
    //...
});

/** @namespace Route/Checkout/Container */
export class CheckoutContainer extends PureComponent {
		//...
}

You can also do it for arrow functions passed as an argument:

fetch(/** ... */).then(
    /** @namespace Component/Braintree/Component/fetchThen */
    () => { /** ... */ } 
);

<aside> ✅ For this reason, it is important to add namespaces to your code. Doing so will ensure that it is extensible via plugins.

</aside>

How does ScandiPWA connect the original code with plugins?

ScandiPWA uses a configuration object in the plugin file to map plugins to the original code using namespaces. The configuration object looks as follows:

export default {
		'<NAMESPACE>': {
				'<TYPE>': <PROXY_NAME>
    },
		//...
};

export default {
    '<NAMESPACE>': {
        '<TYPE>': {
            <IDENTIFIER>: <PROXY_NAME>,
        }
    }
};

<NAMESPACE> - Object keys are the namespaces you intend to apply proxy to.

Depending on the plugin type, the config object varies.

The following example defines a config object to apply the proxy p1 to namespace Example/namespace that identifies a type function:

export default {
		'Example/Namespace': {
				'function': p1
    }
};

You can define multiple namespaces and types in the same config object:

export default {
    'Another/Example/Namespace': {
        'member-property': {
            paymentRenderMap: p1
        },
        'member-function': {
            renderPayment: p2
        }
    },
    'Another/Example/Namespace/function': {
        'function': {
            p3
        }
    }
};

<aside> ℹ️ It is possible to attach more than one plugin to the same namespace and type.

</aside>

How to create a plugin?

<aside> ✅ When working with plugins, it is important to keep in mind the best practices.

</aside>

To create a plugin you need to:

  1. Create the plugin file.

    The file should be located in the src/plugin folder. And be named <ORIGINAL NAME>.plugin.js(What are the best practices for naming and placement?).

  2. Create the plugin(proxy) function in the file. The function arguments will depend on the type of plugin you are creating. Click the link in the description of the plugin type to see the exact arguments.

    <aside> ℹ️ Consider using descriptive function names when defining the plugin function.

    </aside>

    <aside> 🚨 Avoid not calling the callback, unless really necessary! The callback is the content of the original function or other plugin.

    </aside>

  3. Export the config object as default in the file. There you should use the namespace, type of plugin, and proxy function.

The config object and proxy function arguments will vary depending on the type of plugin, which can be:

How to plug into functions?

<aside> ✅ If the original function does not have a namespace but is a class function, you can use the member function plugin.

</aside>

Configure the exported object to use the key function as follows:

const <PROXY_NAME> = (args, callback, context) => {
		// additional logic
		return callback(...args);
};

export default {
		'<NAMESPACE>': {
				function: <PROXY_NAME>
    }
};

Tutorials using plugins of type function:

Tutorial: Create a plugin to mapStateToProps and mapDispatchToProps

Tutorial: How to create a redux store in extensions?

Examples:

How to plug into class methods?

Configure the exported object to use the key member-function as follows:

const <PROXY_NAME> = (args, callback, instance) => {
		// additional logic
		return callback(...args);
};

export default {
    '<NAMESPACE>': {
        'member-function': {
            <CLASS_METHOD>: <PROXY_NAME>,
        }
    }
};

<aside> ℹ️ The <NAMESPACE> must be the class namespace.

</aside>

Tutorials using plugins of type member-function:

Tutorial: How to modify (add/remove/edit) props of a rendered component?

Tutorial: Add additional fields to a query using plugins

Examples:

How to plug into class properties?