Scalprum is a tool for building dynamic frontend UIs from a variety of different sources. Thanks to Scalprum’s dynamic nature, you can pick and choose different components that you want to pull into your UI without having to worry about rebuilding your UI each time you pull in a change. Scalprum has been built with configurability in mind - you can manage different outputs all from one configuration file, without the need to fill your code with conditionals.
Scalprum is a JavaScript micro frontend framework. It leverages webpack 5 and its module federation features to create fast and scaleable micro frontend environments.
See our roadmap to v1 to get of a future fo Scalprum.
- Ensure that you have Node.js installed.
- Environment using webpack 5 to build the final output
- Using React frontend library
The following steps outline an example webpack development setup for demo purposes. You can adjust the steps to suit your own development requirements.
- Create a working directory for the project:
mkdir scalprum-demo && cd scalprum-demo
- Use the following command to initialise a node.
npm init
- Optional: Depending on your needs, you might want to create a Git repository:
git init
- Create a webpack project to set up the development dependencies:
npm i --save-dev webpack webpack-cli webpack-dev-server swc-loader
- Install swc-loader so that the webpack can understand react:
npm i --save-dev swc-loader @swc/core
- Generate a default webpack configuration, and enter 'y' to any options you want to include:
npx webpack init
- Edit the
tsconfig.json
file to match the following configuration:
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"module": "ESNext",
"target": "ESNext",
"allowJs": true,
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
},
"include": ["src/**/*tsx", "src/**/*ts"]
}
- Install Scalprum and its dependencies:
npm i react react-dom @scalprum/core @scalprum/react-core
- Add a type definition for react:
npm i --save-dev @types/react-dom @types/react
- Edit the
index.html
file to add the root element also to the html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Webpack App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
- Edit the
webpack.config.js
file to match the following configuration:
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { ModuleFederationPlugin } = require('webpack').container;
const isProduction = process.env.NODE_ENV == "production";
const stylesHandler = MiniCssExtractPlugin.loader;
const config = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
},
devServer: {
open: true,
host: "localhost",
},
plugins: [
new HtmlWebpackPlugin({
template: "index.html",
}),
new MiniCssExtractPlugin(),
new ModuleFederationPlugin({
name: 'shell',
filename: isProduction ? 'shell-entry.[contenthash].js' : 'shell-entry.js',
shared: [
{
react: {
requiredVersion: '*',
singleton: true,
},
'react-dom': {
requiredVersion: '*',
singleton: true,
},
'@scalprum/react-core': { singleton: true, requiredVersion: '*' },
'@openshift/dynamic-plugin-sdk': { singleton: true, requiredVersion: '*' },
},
],
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
use: {
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true
}
}
}
},
exclude: ["/node_modules/"],
},
{
test: /\.css$/i,
use: [stylesHandler, "css-loader"],
},
{
test: /\.s[ac]ss$/i,
use: [stylesHandler, "css-loader", "sass-loader"],
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: "asset",
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", "..."],
},
};
module.exports = () => {
if (isProduction) {
config.mode = "production";
} else {
config.mode = "development";
}
return config;
};
- Change the
index.ts
file to match the following example:
import('./bootstrap.tsx')
Create a bootstrap.tsx
file with this code:
import React from 'react';
import { createRoot } from 'react-dom/client';
const App = () => {
return (
<div>I am an app</div>
)
}
const domNode = document.getElementById('root');
if(domNode) {
const root = createRoot(domNode);
root.render(<App />)
}
If everything is set up, you can run npm run serve
and view the example text that you entered in the <div>
of index.tsx
.
To use Scalprum, we must create a host application.
A host application is the main provider that manages module loading, routing, data sharing, and related tasks. It functions as the manager of the micro-frontend. The host application always loads first. It can be any React application as long as it has some mandatory webpack configuration.
- To create a host application - a top-level application to manage the micro frontend, in the
scalprum-demo/src
directory, create aScalprumRoot.tsx
file, and edit to match the following configuration:
import React from 'react';
import { AppsConfig } from '@scalprum/core'
import { ScalprumProvider, ScalprumComponent } from '@scalprum/react-core'
const config: AppsConfig = {
remoteModule: {
name: 'remoteModule',
manifestLocation: 'http://localhost:8003/plugin-manifest.json'
}
}
const ScalprumRoot = () => {
return (
<div>
<ScalprumProvider config={config}>
<ScalprumComponent scope="remoteModule" module="RemoteModuleComponent" />
</ScalprumProvider>
</div>
)
}
export default ScalprumRoot;
Let's take a look at what we set up in this example.
Component | Definition |
---|---|
AppsConfig module |
The AppsConfig is a set of dynamic modules from '@scalprum/core'. This is framework-agnostic and will work as long as it is in Javascript. |
@scalprum/core package |
The @scalprum/core package is responsible for managing federated modules. It provides an abstraction on low-level API to make it more developer friendly alongside additional features. Behind the scenes, it uses our exposed webpack modules, @openshift/dynamic-plugin-sdk , to manage plugins. |
ScalprumProvider module |
The ScalprumProvider is the main root component. If you want to load modules, you must ensure they are enclosed in the <ScalprumProvider /> components. ScalprumProvider has a mandatory config field. This informs ScalprumProvider of which remote modules are available to render. For the purpose of the demo, const config: AppsConfig = {} , we set up remoteModule to use in the next steps. The config requires a name , and a manifestLocation . |
@scalprum/react-core |
The @scalprum/react-core packages provide react core bindings for @scalprum/core . |
ScalprumComponent |
The ScalprumComponent is provided by the @scalprum/react-core packages. It requires two parameters: scope : the name that you set in the ScalprumProvider 's config{} . module : the actual module that you want to render. For the purposes of this demo, create RemoteModuleComponent . |
|
- Edit the
index.tsx
file and make the following changes:
import React from 'react';
import { createRoot } from 'react-dom/client';
import ScalprumRoot from './ScalprumRoot';
const App = () => {
return (
<ScalprumRoot/>
)
}
const domNode = document.getElementById('root');
if(domNode) {
const root = createRoot(domNode);
root.render(<App />)
}
- In the
webpack.config.js
file, add themoduleFederation
plugin to set up the environment for plugin applications to use the context of the host applications:
// declare shared dependencies
const moduleFederationPlugin = new ModuleFederationPlugin({
name: 'host',
filename: 'host.[contenthash].js',
shared: [
{
'@openshift/dynamic-plugin-sdk': {
singleton: true,
requiredVersion: '*',
},
},
{
'@scalprum/react-core': {
singleton: true,
requiredVersion: '*',
},
},
{ react: { singleton: true, requiredVersion: '*' } },
{ 'react-dom': { singleton: true, requiredVersion: '*'} },
// any other packages you wish to share
],
});
Note that for demo purposes, the requiredVersion
for dependencies in this example is set to *
, but you can update this to a version that suits your requirements.
- In the
webpack.config.js
file, add an entry also for themoduleFederationPlugin
to the declared plugins:
plugins: [
new HtmlWebpackPlugin({
template: "index.html",
}),
new MiniCssExtractPlugin(),
// scalprum required plugin
moduleFederationPlugin,
],
- To create the remote module, in the
scalprum-demo
directory, create a remoteModule directory. - Install the following dependencies:
npm i --save-dev @openshift/dynamic-plugin-sdk-webpack
- In the scalprum-demo/remoteModule directory, create a
webpack.config.js
file. - You can copy the contents of
webpack.config.js
file from the previous steps. - In the
webpack.config.js
file, remove theHtmlWebpackPlugin
because it breaks dynamic modules. This is a known issue that is being actively worked on. - Remove the
moduleFederationPlugin
and replace with theDynamicRemotePlugin
:
// declare shared dependencies
const { DynamicRemotePlugin } = require('@openshift/dynamic-plugin-sdk-webpack');
const sharedModules = {
'@openshift/dynamic-plugin-sdk': { singleton: true },
'@scalprum/react-core': { singleton: true },
react: { singleton: true },
'react-dom': { singleton: true },
};
const dynamicPlugin = new DynamicRemotePlugin({
extensions: [],
sharedModules,
entryScriptfilename: 'remoteModule.[contenthash].js',
pluginMetadata: {
name: 'remoteModule',
version: '1.0.0',
exposedModules: {
RemoteModuleComponent: './remoteModule/src/RemoteModuleComponent.tsx',
},
extensions: [],
},
});
- In the
webpack.config.js
file, also replace themoduleFederationPlugin
with thedynamicPlugin
plugins:
plugins: [
new MiniCssExtractPlugin(),
// scalprum required plugin
dynamicPlugin
,
],
- In the
webpack.config.js
file, also add apublicPath
:
const config = {
entry: "./src/index.tsx",
output: {
path: path.resolve(_dirname, "dist"),
publicPath:'http://localhost:8003'
},
devServer: {
open: true,
host: "localhost",
},
- In your scalprum-demo/remoteModule directory, create a src directory.
- In your scalprum-demo/remoteModule/src directory, create an
index.tsx
file. - In your scalprum-demo/remoteModule/src directory, create a
RemoteModuleComponent.tsx
file and add the following:
import React from 'react';
const RemoteModuleComponent = () => {
return (
<div>
I am a remote module component;
</div>
)
}
export default RemoteModuleComponent;
- In the
scalprum-demo/package.json
file, add the following entry and point toremoteModule/webpack.config.js
as your configuration file:
"build:plugin":"webpack --mode=production --node-env=production -c remoteModule/webpack.config.js"
To test if everything is set up correctly, enter the following command:
npm run build:plugin
Normally, your modules would be served via your own content delivery network, but for demo purposes, you can serve it locally from the dist directory that is created when you run npm run build:plugin
.
cd scalprum-demo/remoteModule/dist/ && npx http serve . -p 8003
If this action completes without error, you can view your plugin manifest at http://localhost:8003/
.
You can also run the host application via npm run serve
and check if the dynamic plugin was loaded correctly in the UI.
dependencies
npm i @openshift/dynamic-plugin-sdk @scalprum/core @scalprum/react-core
TODO: Create host webpack plugin or extensible default webpack config
// webpack.config.js
// require module federation plugin
const { ModuleFederationPlugin } = require('webpack').container;
// declare shared dependencies
const moduleFederationPlugin = new ModuleFederationPlugin({
name: 'host',
filename: 'host.[contenthash].js',
shared: [{
// These packages has to be shared and has to be marked as singleton!
{ '@openshift/dynamic-plugin-sdk', { singleton: true, }}
{ '@scalprum/react-core': { singleton: true, } },
{ react: { singleton: true, } },
{ 'react-dom': { singleton: true } },
// any other packages you wish to share
}]
})
module.exports = {
// regular react webpack config
plugins: [moduleFederationPlugin, ... /** other plugins you need */]
}
The host application requires an extra module federation plugin to be compatible with scalprum packages.
The following modules have to be shared and marked as singletons:
@scalprum/react-core
react
react-dom
If your application requires additional shared/singleton packages (eg. react-router-dom
) they can be added to the plugin configuration.
The ScalprumProvider
is a root React node that propagates necessary context to its children. It requires a configuration object that is referenced when a module is loaded.
import App from './App'
const config = {
testModuleOne: {
name: 'testModuleOne', // module name
manifestLocation: '/path/to/manifest/file.json' // metadata file with module entry script location and other information
},
testModuleTwo: {
name: 'testModuleTwo',
manifestLocation: ...
}
}
const HostRoot = () => {
return (
<ScalprumProvider
api={{
/** Custom object */
}}
config={config}
>
<App />
</ScalprumProvider>
)
}
export default HostRoot
config
The config prop contains is a module registry. The config data is used to load and initialize modules at runtime.
type Config = {
[name: string]: {
name: string;
manifestLocation: string;
}
}
api
The api
prop is an object that is available to all provider children nodes via useScalprum
hook. It is a good place to store your global context (user, auth API, etc...).
const HostRoot = () => {
return (
<ScalprumProvider
api={{
user: {
name: 'John Doe',
email: '[email protected]'
}
}}
config={config}
>
<App />
</ScalprumProvider>
)
}
import { useScalprum } from '@scalprum/react-core'
const Plugin = () => {
const { api: { user } } = useScalprum()
return (
<div>
<h2>Hello {user.name}</h2>
</div>
)
}
The ScalprumComponent
is a react binding that directly renders a module. The referenced module has to be a React component. The module also has to be present in the ScalprumProvider
config.
import { ScalprumComponent } from '@scalprum/react-core';
const RemotelyLoadedComponent = () => {
const anyProps = {
foo: 'bar'
}
return (
<ScalprumComponent
scope="testModuleOne"
module=" "
// any non scalprum props will be passed to the actual component
{...anyProps}
/>
);
}
importName
The importName
prop is a string that is used to reference the module. It is a name of the exported component from the module. Should be used if other than default
export is required.
// Remote module definition
export const NamedComponent = () => {
return (
<div>
<h2>Named component</h2>
</div>
)
}
// Consumer
<ScalprumComponent {...props} importName="NamedComponent">
One module can have multiple exports.
fallback
Similar to React.Suspense. This component will be loaded before the module is ready.
<ScalprumComponent {...props} fallback={<Spinner />}>
ErrorComponent
A node that is rendered if a module encountered a runtime error or failed to load.
// error type cannot be strict and depends on type of error and on application
const ErrorComponent = ({ error, errorInfo }) => {
useEffect(() => {
// handle the error (report to logging service)
}, [])
return (
<div>
<h2>Error rendering component</h2>
</div>
)
}
<ScalprumComponent {...props} ErrorComponent={<ErrorComponent />}>