Skip to main content

Module Federation

Invent utilizes Webpack Module Federation capabilities to manage runtime-loaded "widgets".
Those are standalone applications of varying complexity, bundled in a specific way
and available from a remote host - "Invent App Store".

Read more on Module Federation here

TLDR for development

Enter the following commands inside the host app and each used widget:

npm run build_federation
npm run start_federation

Reasoning behind using Webpack Module Federation

  1. Webpack is the most popular JS app bundler and is open source. We can get support and solve our problems just by looking at GitHub issues.

  2. It offers a standard way of shared dependencies management, so we don't have to code and maintain it ourselves. "Remote apps" will reuse "host app" dependencies utilizing webpack functionality, the result is lighter bundles.

  3. As INVENT targets vendor in-house developers as well, using standard solution as a base eliminates a lot of concerns regarding platform adoption.

INVENT Platform as Host Application

Through the Module Federation Webpack plugin, the host app exposes shared dependencies for all the runtime-loaded widgets to consume. The list with required versions comes from @invent/webpack-config, and is a source of truth regarding dep versions.

  • There is a consistency problem right now, that package.json of the host app is not synced in any way with the @invent/webpack-config shared deps list.

All the code used for runtime loading/init of remote apps resides in lib/module-federation directory.

When there is a need to load a new widget - "remote module", the following steps happen:

  1. RemoteModule wrapper - a React component - is being rendered somewhere in the tree, and receives required props, like that:
<RemoteModule
url="http://some-url/remoteEntry.js"
scope="ScopeName"
module="./ModuleName"
componentProps={props}
/>

Remote modules can render other remote modules, all the tooling is passed as props and typings are available through the shared-types library. The following steps are required:

  • destructure remoteModule and generateWidgetData from props.
  • use RemoteModuleT and GenerateWidgetDataT from @invent/shared-types as typings.
  • get widget metadata from platformMeta prop.
  • generate the remote module data passing widget name and id according to the typing
  • pass generated data to the remote module:
const data = generateWidgetData(name, widgetId)

<RemoteModule
scope={data.scope}
module={data.component}
componentProps={{...}}
url={data.host}
/>
  1. The <script /> tag is being inserted into the document by the RemoteModule code, with src equal to provided url prop, and bundle code is being loaded by the browser.

  2. Webpack runtime tries to find a "namespace" according to the scope we provided, and the "module" inside - using a path from the prop. On success, it resolves the React.lazy component load and a React component instance is being created.

The host app also tracks used scopes (widgets), using the redux store, and if there aren't any currently mounted components that use a remote module code, it <script /> the tag will be deleted from the document.

Make sure that <RemoteModule /> components are not being re-mounted in the tree, it will break <Suspense> and script-loading logic, resulting in not-loaded component fallback flash, and other problems usually connected with remounts

Widget as Remote Application

Each application resides in its repository in Invent GitLab, and is prefixed with widgets-. Invent widgets also have the prefix core-, and vendors have their company name prefixes. So for a calendar widget produced by Invent, the repo name would be widgets-core-calendar, and for the Boeing notifications widget - widgets-boeing-notifications.

It can be developed as a standalone application and served as a federated module using Invent widgets webpack config

  • Please note, that widgetId used for bundle identity is a GitLab repository id, not just a random number.

Production Pipeline

We have the following understanding of the widget bundles delivery process:

  • merge in a widget "master" branch triggers CI publish.
  • CI populates build variables, giving it the URL where the bundle will be hosted.
  • after being bundled by CI, code is accessible by the URL.

So, all the widgets can be available on a single domain, like:

invent.store.com/widgets/:id