Lars Roettig

Getting started with PWA Studio Extensibility

By Lars Roettig2 min

The 7.0 release improves on the extensibility framework. I want to prove if we can already build a real PWA extension or where we run in limitation. This blogpost contains two parts. The first part is about the technical strategies behind the Extensibility Apis. In the second part, we will write a small extension.

Technical strategies

The new extensibility Apis allows extending Build pack, Peregrine, and Venia UI library components. The main goal is developers can make changes in storefront projects or standalone extensions without duplicating or maintaining the PWA-Studio code. The PageBuilder is integrated via targets. It is the first Magento Content plugin. Also, it is an excellent example of your plugins. For replacing or extend already existing ui components, we need a workaround currently. Replacing a part can be dangerous. It is not a compound change, so if two extensions both want to replace the same file, it can lead to errors. Currently, the Core Team doesn't want to support a fallback structure by default. From my perspective, we need a way how a plugin can register its new components to already existing ui components. PWA Studio packages should declare which of their ui-components are safe to replace and make them replaceable. A reliable replacement strategy will help that vendors can build Standalone Marketplace Extensions.

Venia UI extension points

Currently it is allowed to add new routs or create a new richContentRenderers to create new blog plugin based on CMS like NEOS. The richContentRenderers are used to integrated the Magento Page Builder Plugin in PWA Studio.

Dokumentation Link: https://github.com/magento/pwa-studio/blob/release/7.0/packages/venia-ui/lib/targets/venia-ui-declare.js

Peregrine extension points

We can now wrap any individual Peregrine talons when Peregrine talons are invoked, and/or to modify the behavior and output of those talons. Like the around plugin concept knowen as interceptor pattern of Magento.

1targets.of('@magento/peregrine').talons.tap(talons => {
2 talons.App.useApp.wrapWith('./log-wrapper');
3})
log-wrapper.js
1export default function wrapUseApp(original) {
2 return function useApp(...args) {
3 console.log('calling useApp with', ...args);
4 return original(...args);
5 }
6}

Build pack extension points

This extension points is based on the Webpack Compiler Hooks. This the Webpack API is very well documented and often used to build plugins.

Dokumentation Link

How to create a PWA Studio Extension

In this tutorial, we create a new Info TopBar component.

Magento Frontent with own TopBar

The story contains the following acceptance criteria:

As a Customer, I want to show a TopBar to inform customers about holidays and longer delivery time.

UAK:

  • TopBar Informs customers
  • Topbar styled with customer brand colors.

Command to create a new Project:

1yarn create @magento/pwa

Create_PWA

Create SSL Cerficate:

1cd <your_pwa_studio>
2yarn install
3yarn buildpack create-custom-origin ./

Create your own Module

1cd ../
2mkdir -p larsroettig/top-bar-plugin
3cd larsroettig/top-bar-plugin
package.json
1{
2 "name": "@larsroettig/top-bar-plugin",
3 "version": "1.0.0",
4 "description": "",
5 "author": "Lars Roettig <l.roettig@techdivision.com>",
6 "license": "MIT",
7 "main": "./lib/components/TopBar/index.js"
8 "pwa-studio": {
9 "targets": {
10 "intercept": "intercept.js"
11 }
12 },
13 "peerDependencies": {
14 "@magento/pwa-buildpack": "*",
15 "@magento/venia-ui": "*",
16 "react": "~16.9.0",
17 "react-router-dom": "~5.1.0"
18 }
19}
intercept.js
1const moduleOverridePlugin = require('./moduleOverrideWebpackPlugin');
2const componentOverrideMapping = {
3 '@magento/venia-ui/lib/components/Main/main.js': './lib/overwrites/components/Main/main.js',
4};
5
6module.exports = targets => {
7 targets.of('@magento/pwa-buildpack').specialFeatures.tap(flags => {
8 /**
9 * Wee need to actived esModules and cssModules to allow build pack to load our extension
10 * {@link https://magento.github.io/pwa-studio/pwa-buildpack/reference/configure-webpack/#special-flags}.
11 */
12 flags[targets.name] = {esModules: true, cssModules: true};
13 });
14
15 targets.of('@magento/pwa-buildpack').webpackCompiler.tap(compiler => {
16 // registers our own overwrite plugin for webpack
17 new moduleOverridePlugin(componentOverrideMapping).apply(compiler);
18 })
19}

Create Webpack Overwrite Plugin

The Webpack has ships NormalModuleReplacementPlugin allows you to replace resources that match resourceRegExp with newResource. This very helpful for replacing one file. But I want to show you how we can write our Plugin very essay. From my point of view, this plugin has a better developer experience, because we can overwrite multiple files and don't need to use a regex.

moduleOverrideWebpackPlugin.js
1const path = require('path');
2const glob = require('glob');
3
4module.exports = class NormalModuleOverridePlugin {
5 constructor(moduleOverrideMap) {
6 this.name = 'NormalModuleOverridePlugin';
7 this.moduleOverrideMap = moduleOverrideMap;
8 }
9
10 requireResolveIfCan(id, options = undefined) {
11 try {
12 return require.resolve(id, options);
13 } catch (e) {
14 return undefined;
15 }
16 }
17 resolveModulePath(context, request) {
18 const filePathWithoutExtension = path.resolve(context, request);
19 const files = glob.sync(`${filePathWithoutExtension}@(|.*)`);
20 if (files.length === 0) {
21 throw new Error(`There is no file '${filePathWithoutExtension}'`);
22 }
23 if (files.length > 1) {
24 throw new Error(
25 `There is more than one file '${filePathWithoutExtension}'`
26 );
27 }
28
29 return require.resolve(files[0]);
30 }
31
32 resolveModuleOverrideMap(context, map) {
33 return Object.keys(map).reduce(
34 (result, x) => ({
35 ...result,
36 [require.resolve(x)]:
37 this.requireResolveIfCan(map[x]) ||
38 this.resolveModulePath(context, map[x]),
39 }),
40 {}
41 );
42 }
43
44 apply(compiler) {
45 if (Object.keys(this.moduleOverrideMap).length === 0) {
46 return;
47 }
48
49 const moduleMap = this.resolveModuleOverrideMap(
50 compiler.context,
51 this.moduleOverrideMap
52 );
53
54 compiler.hooks.normalModuleFactory.tap(this.name, (nmf) => {
55 nmf.hooks.beforeResolve.tap(this.name, (resolve) => {
56 if (!resolve) {
57 return;
58 }
59
60 const moduleToReplace = this.requireResolveIfCan(resolve.request, {
61 paths: [resolve.context],
62 });
63 if (moduleToReplace && moduleMap[moduleToReplace]) {
64 resolve.request = moduleMap[moduleToReplace];
65 }
66
67 return resolve;
68 });
69 });
70 }
71};

Create your own PWA Component

lib/components/TopBar/topbar.css
1.root {
2 background-color: rgb(var(--venia-teal));
3 padding: 0.5rem;
4 color: #fff;
5 display: grid;
6 grid-template-columns: 25% 25%;
7 justify-content: center;
8 align-items: center;
9}
10
11.moreText {
12 text-align: end;
13}
14
15.moreButton {
16 color: #fff;
17 margin-left: 0.5rem;
18 display: inline-block;
19 padding: 0.75rem 2rem;
20 border: 1px solid #fff;
21 border-radius: 3px;
22 outline: none;
23}
lib/components/TopBar/topbar.js
1import React from 'react';
2import {mergeClasses} from '@magento/venia-ui/lib/classify';
3import Button from '@magento/venia-ui/lib/components/Button/button';
4import defaultClasses from './topbar.css';
5
6const TopBar = () => {
7 const classes = mergeClasses(defaultClasses);
8
9 return (
10 <div className={classes.root}>
11 <div className={classes.moreText}> Your first custom react component</div>
12 <div>
13 <Button className={classes.moreButton}>
14 {'More'}
15 </Button>
16 </div>
17 </div>
18 )
19};
20
21export default TopBar;
lib/components/TopBar/index.js
1export { default } from './topbar';

Replace Main PWA Component

1mkdir lib/overwrites/components/Main
2cp node_modules/@magento/venia-ui/lib/components/Main/main.js lib/overwrites/components/Main/main.js
lib/overwrites/components/Main/main.js
1import React from 'react';
2import {bool, shape, string} from 'prop-types';
3import {useScrollLock} from '@magento/peregrine';
4
5import {mergeClasses} from '@magento/venia-ui/lib/classify';
6
7import Footer from '@magento/venia-ui/lib/components/Footer';
8import Header from '@magento/venia-ui/lib/components/Header';
9import defaultClasses from '@magento/venia-ui/lib/components/Main/main.css';
10import TopBar from '@larsroettig/top-bar-plugin/lib/components/TopBar';
11
12const Main = props => {
13 const {children, isMasked} = props;
14 const classes = mergeClasses(defaultClasses, props.classes);
15
16 const rootClass = isMasked ? classes.root_masked : classes.root;
17 const pageClass = isMasked ? classes.page_masked : classes.page;
18
19 useScrollLock(isMasked);
20
21 return (
22 <main className={rootClass}>
23 <TopBar/>
24 <Header/>
25 <div className={pageClass}>{children}</div>
26 <Footer/>
27 </main>
28);
29};
30
31export default Main;
32
33Main.propTypes = {
34 classes: shape({
35 page: string,
36 page_masked: string,
37 root: string,
38 root_masked: string
39 }),
40 isMasked: bool
41};

Open cd <your_pwa_studio>/package.json add following line for lokal testing later you install your extension via yarn install --force

1"devDependencies": {
2 "@larsroettig/top-bar-plugin": "file:<file_path>/larsroettig/top-bar-plugin",
3 }

Final_Module.jpg

How do can add static routs as Extension

Tipps

Edit post on GitHub

Written by
Lars Roettig
Software Engineer at TechDivision GmbH and Maintainer of the Community Engineering Team at Magento. He has 8 years of professional SoftwareEngineering experience. Lars is passionate about Magento and Open Source.
You may also like

Useful? Share it!

Lars Roettig
Legal
  • Imprint
  • Privacy Statement
Employee at: