Skip to content

Creating a new plugin app

ray-millward-tessella edited this page Apr 4, 2019 · 30 revisions

Creating a new plugin app

The first step in creating a new plugin for the DAaaS frontend is to create a new Git repository. You will also need the same tooling as for the parent app specified here

Once you have this then you can run create react app to make a new app and push to you new repo.

To make a new react app with Typescript support run:

npx create-react-app my-app --typescript

Modifying the app to be a plugin

For an example of the code for a plugin see https://github.com/ral-facilities/daaas-frontend-demo-plugin, the modifications made to that app to make it a plugin are described below.

Building as a library

To modify a react app to work as a plugin then the webpack config needs to be modified to change the target to be a library. To do this we need to install react-scripts-rewired; this allows us to modify the webpack config without doing a full blown eject on the react app.

npm install --save-dev react-scripts-rewired

Then add a webpack.config.extend.js to the root of the codebase with the code:

module.exports = (webpackConfig, env, { paths }) => {
  webpackConfig.externals = {
    'react': 'React', // Case matters here 
    'react-dom': 'ReactDOM', // Case matters here
  }

  if (env == "production") {
      webpackConfig.output.library = "demo_plugin"
      webpackConfig.output.libraryTarget = "window"

      webpackConfig.output.filename = '[name].js'
      webpackConfig.output.chunkFilename = '[name].chunk.js'

      delete webpackConfig.optimization.splitChunks
      webpackConfig.optimization.runtimeChunk = false
  }

  return webpackConfig
}

where demo_plugin should be changed for the name of your plugin. Note the webpack externals - this will mean that the plugin uses external versions of React and ReactDOM. Add the following to your index.html:

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

inside the body tag. If you need a dev version then you can change these to react.development.js and react-dom.developement.js temporarily.

Also add a webpackDevServer.config.extend.js file with the contents:

module.exports = (webpackDevServerConfig, env, { paths }) => {
    return webpackDevServerConfig
}

React scripts rewired has slightly older requirements for libraries and some may need to be downgraded; this will very much depend on the version of react-scripts-rewired at the time of making a plugin. Therefore, to work out what to downgrade run npm run build and see what fails due to a library version.

For example, when run for the demo plugin then the following had to be downgraded:

Adding Single-SPA hooks

The parent app runs Single-SPA and so expects certain hooks to be able to load a plugin (i.e. a bootstrap, mount and unmount method).

If the plugin uses React then you can install single-spa-react:

npm install single-spa-react @types/single-spa-react

and modify index.tsx to have the required hooks:

{ other imports }
import singleSpaReact from 'single-spa-react';
...

function domElementGetter(): HTMLElement {
  // Make sure there is a div for us to render into
  let el = document.getElementById('demo_plugin');
  if (!el) {
    el = document.createElement('div');
    el.id = 'demo_plugin';
    document.body.appendChild(el);
  }

  return el;
}

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: App,
  domElementGetter,
});

/* eslint-disable @typescript-eslint/no-explicit-any */
// Single-SPA bootstrap methods have no idea what type of inputs may be
// pushed down from the parent app
export function bootstrap(props: any): Promise<void> {
  return reactLifecycles.bootstrap(props);
}

export function mount(props: any): Promise<void> {
  return reactLifecycles.mount(props);
}

export function unmount(props: any): Promise<void> {
  return reactLifecycles.unmount(props);
}
/* eslint-enable @typescript-eslint/no-explicit-any */

where demo_plugin should be swapped for the name of your plugin. Normally, as part of the library we would also want to remove the ReactDOM.render(...) line in index.tsx but we still want to be able to develop the plugin locally and in isolation, therefore we should only render to the screen if in development mode:

if (process.env.NODE_ENV === `development`) {
  ReactDOM.render(<App />, document.getElementById('demo_plugin'));
}

This also means updating the line with <div id="root"></div> in index.html to

<div id="demo_plugin"></div>

where again demo_plugin is the name of your plugin. This simulates how it will be mounted by the parent app (i.e. in to a div with the corresponding ID).

Additionally, you will want to implement the componentDidCatch lifecycle method in your root component (App in this case). This ensures that if the plugin throws an unhandled error, this method is called and the plugin is not unmounted due to single-spa not being able to deal with the error. Additionally, you can use this method to log the error and use it to display a fallback UI (in the example this is done by setting a state variable hasError). An example implementation of this is shown below:

public componentDidCatch(error: Error | null): void {
    this.setState({ hasError: true });
    log.error(`demo_plugin failed with error: ${error}`);
}
Clone this wiki locally