Volto add-ons
Contents
Volto add-ons¶
There are several advanced scenarios where we might want to have more control and flexibility beyond using the plain Volto project to build a site.
We can build Volto add-on products and make them available as generic Javascript packages that can be included in any Volto project. By doing so we can provide code and component reutilization across projects and, of course, benefit from open source collaboration.
Note
By declaring a Javascript package as a "Volto addon", Volto provides several integration features: language features (so they can be transpiled by Babel), whole-process customization via razzle.extend.js and integration with Volto's configuration registry.
The addon can be published to an NPM registry or directly installed from github by Yarn. By using mrs-develop, it's possible to have a workflow similar to zc.buildout's mr.developer, where you can "checkout" an addon for development.
An addon can be almost anything that a Volto project can be. They can:
provide additional views and blocks
override or extend Volto's builtin views, blocks, settings
shadow (customize) Volto's (or another addon's) modules
register custom routes
provide custom Redux actions and reducers
register custom Express middleware for Volto's server process
tweak Volto's Webpack configuration, load custom Razzle and Webpack plugins
even provide a custom theme, just like a regular Volto project does.
Configuring a Volto project to use an addon¶
You can install a Volto addon just like any other JS package:
yarn add name-of-addon
If the addon is not published on NPM, you can retrieve it directly from Github:
yarn add collective/volto-dropdownmenu
Next, you'll need to add the addon (identified by its JS package name) to the
addons
key of your Volto project's package.json
. More details in the next
section.
Loading addon configuration¶
As a convenience, an addon can export configuration functions that can mutate, in-place, the overall Volto configuration registry. An addon can export multiple configurations methods, making it possible to selectively choose which specific addon functionality you want to load.
In your Volto project's package.json
you can allow the addon to alter the
global configuration by adding, in the addons
key, a list of volto addon
package names, like:
{
"name": "my-nice-volto-project",
...
"addons": [
"acme-volto-foo-addon",
"@plone/some-addon",
"collective-another-volto-addon"
],
...
}
Warning
Adding the addon package to the addons
key is obligatory! It allows Volto
to treat that package properly and provide it with BabelJS language
features. In Plone terminology, it is like including a Python egg to the
zcml
section of zc.buildout.
Some addons might choose to allow the Volto project to selectively load some of
their configuration, so they may offer additional configuration functions,
which you can load by overloading the addon name in the addons
package.json
key, like so:
{
"name": "my-nice-volto-project",
"addons": [
"acme-volto-foo-addon:loadOptionalBlocks,overrideSomeDefaultBlock",
"volto-ga"
],
}
Note
The additional comma-separated names should be exported from the addon
package's index.js
. The main configuration function should be exported as
the default. An addon's default configuration method will always be loaded.
If for some reason, you want to manually load the addon, you could always do,
in your project's config.js
module:
import loadExampleAddon, { enableOptionalBlocks } from 'volto-example-addon';
import * as voltoConfig from '@plone/volto/config';
const config = enableOptionalBlocks(loadExampleAddon(voltoConfig));
export blocks = {
...config.blocks,
}
...
As this is a common operation, Volto provides a helper method for this:
import { applyConfig } from '@plone/volto/helpers';
import * as voltoConfig from '@plone/volto/config';
const config = applyConfig([
enableOptionalBlocks,
loadExampleAddon
], voltoConfig);
export blocks = {
...config.blocks,
}
The applyConfig
helper ensures that each configuration methods returns the
config object, avoiding odd and hard to track errors when developing addons.
Creating addons¶
Volto addon packages are just CommonJS packages. The only requirement is that
they point the main
key of their package.json
to a module that exports, as
a default function that acts as a Volto configuration loader.
Although you could simply use npm init
to generate an addon initial code,
we now have a nice
Yeoman-based generator that you can use:
npm install -g @plone/generator-volto
yo @plone/volto:addon [<addonName>] [options]
Volto will automatically provide aliases for your (unreleased) package, so that
once you've released it, you don't need to change import paths, since you can
use the final ones from the very beginning. This means that you can use imports
such as import { Something } from '@plone/my-volto-addon'
without any extra
configuration.
Use mrs-developer to manage the development cycle¶
mrs.developer.json¶
This is the configuration file that instructs mrs-developer
from where it has
to pull the packages. So, create mrs.developer.json
and add:
{
"acme-volto-foo-addon": {
"package": "@acme/volto-foo-addon",
"url": "git@github.com:acme/my-volto-addon.git",
"path": "src"
}
}
Then run:
yarn develop
Now the addon is found in src/addons/
.
Note
package
property is optional, set it up only if your package has a scope.
src
is required if the content of your addon is located in the src
directory (but, as that is the convention recommended for all Volto add-on
packages, you will always include it)
If you want to know more about mrs-developer
config options, please refer to
its npm page.
tsconfig.json / jsconfig.json¶
mrs-developer
automatically creates this file for you, but if you choose not
to use mrs-developer, you'll have to add something like this to your
tsconfig.json
or jsconfig.json
file in the Volto project root:
{
"compilerOptions": {
"paths": {
"acme-volto-foo-addon": [
"addons/acme-volto-foo-addon/src"
]
},
"baseUrl": "src"
}
}
Warning
Please note that both paths
and baseUrl
are required to match your
project layout.
Tip
You should use the src
path inside your package and point the main
key
in package.json
to the index.js
file in src/index.js
.
Customizations¶
Addon packages can include customization folders, just like the Volto projects.
The customizations are resolved in the order: addons (as sorted in the addons
key of your project's package.json
) then the customizations in the Volto
project, last one wins.
Tip
See the Advanced customization scenarios section on how to enhance this pattern and how to include customizations inside addons.
Providing addon configuration¶
The default export of your addon main index.js
file should be a function with
the signature config => config
.
That is, it should take the global
configuration object and return it,
possibly mutated or changed. So your main index.js
will look like:
export default function applyConfig(config) {
config.blocks.blocksConfig.faq_viewer = {
id: 'faq_viewer',
title: 'FAQ Viewer',
edit: FAQBlockEdit,
view: FAQBlockView,
icon: chartIcon,
group: 'common',
restricted: false,
mostUsed: true,
sidebarTab: 1,
security: {
addPermission: [],
view: [],
},
};
return config;
}
And the package.json
file of your addon:
{
"main": "src/index.js",
}
Warning
An addon's default configuration method will always be loaded.
Multiple addon configurations¶
You can export additional configuration functions from your addon's main
index.js
.
import applyConfig, {loadOptionalBlocks,overrideSomeDefaultBlock} from './config';
export { loadOptionalBlocks, overrideSomeDefaultBlock };
export default applyConfig;
Add third-party dependencies to your addon¶
If you're developing the addon and you wish to add an external dependency, you'll have to switch your project to be a Yarn Workspaces root.
So you'll need to add, in your Volto project's package.json
:
"private": true,
"workspaces": [],
Then populate the workspaces
key with the path to your development addons:
"workspaces": [
"src/addons/my-volto-addon"
]
You'll have to manage the addon dependencies via the workspace root (your Volto project). For example, to add a new dependency:
yarn workspace @plone/my-volto-addon add some-third-party-package
You can run yarn workspaces info
to see a list of workspaces defined.
In case you want to add new dependencies to the Volto project, now you'll have
to run the yarn add
command with the -W
switch:
yarn add -W some-dependency
Extending Razzle from an addon¶
Just like you can extend Razzle's configuration from the project, you can do so
with an addon, as well. You should provide a razzle.extend.js
file in your
addon root folder. An example of such file where the theme.config alias is
changed, to enable a custom Semantic theme inside the addon:
const analyzerPlugin = {
name: 'bundle-analyzer',
options: {
analyzerHost: '0.0.0.0',
analyzerMode: 'static',
generateStatsFile: true,
statsFilename: 'stats.json',
reportFilename: 'reports.html',
openAnalyzer: false,
},
};
const plugins = (defaultPlugins) => {
return defaultPlugins.concat([analyzerPlugin]);
};
const modify = (config, { target, dev }, webpack) => {
const themeConfigPath = `${__dirname}/theme/theme.config`;
config.resolve.alias['../../theme.config$'] = themeConfigPath;
return config;
};
module.exports = {
plugins,
modify,
};
Addon dependencies¶
Sometimes your addon depends on another addon. You can declare addon dependency
in your addon's addons
key, just like you do in your project. By doing so,
that other addon's configuration loader is executed first, so you can depend on
the configuration being already applied. Another benefit is that you'll have
to declare only the "top level" addon in your project, the dependencies will be
discovered and automatically treated as Volto addons. For example, volto-slate
depends on volto-object-widget's configuration being already applied, so
volto-slate can declare in its package.json:
{
"name": "volto-slate",
...
"addons": ['@eeacms/volto-object-widget']
}
And of course, the dependency addon can depend, on its turn, on other addons which will be loaded as well. Circular dependencies should be avoided.
Testing addons¶
We should let jest know about our aliases and make them available to it to
resolve them, so in package.json
:
"jest": {
"moduleNameMapper": {
"@plone/volto/(.*)$": "<rootDir>/node_modules/@plone/volto/src/$1",
"@package/(.*)$": "<rootDir>/src/$1",
"@plone/some-volto-addon/(.*)$": "<rootDir>/src/addons/@plone/some-volto-addon/src/$1",
"my-volto-addon/(.*)$": "<rootDir>/src/addons/my-volto-addon/src/$1",
"~/(.*)$": "<rootDir>/src/$1"
},
Tip
We're in the process of moving the default scaffolding generators to
provide a jest.config.js
file in Volto, making this step unneeded.
You can use yarn test src/addons/addon-name
to run tests.
Code linting¶
If you have generated your Volto project recently (after the summer of 2020),
you don't have to do anything to have automatic integration with ESLint,
otherwise make sure to upgrade your project's .eslintrc
to the .eslintrc.js
version, according to the Upgrade Guide.