Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STRIPES-861: Setup module federation #105

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
18 changes: 18 additions & 0 deletions consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129
const singletons = {
'@folio/stripes': '^9.3.0',
'@folio/stripes-shared-context': '^1.0.0',
'react': '~18.2',
'react-dom': '~18.2',
'react-intl': '^6.8.0',
'react-query': '^3.39.3',
'react-redux': '^8.1',
'react-router': '^5.2.0',
'react-router-dom': '^5.2.0',
'redux-observable': '^1.2.0',
'rxjs': '^6.6.3'
};

module.exports = {
singletons,
};
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^8.1.0",
"add-asset-html-webpack-plugin": "^6.0.0",
"axios": "^1.3.6",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.3",
"babel-plugin-remove-jsx-attributes": "^0.0.2",
"buffer": "^6.0.3",
"commander": "^2.9.0",
"connect-history-api-fallback": "^1.3.0",
"core-js": "^3.6.1",
"cors": "^2.8.5",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.4.0",
"csv-loader": "^3.0.3",
Expand All @@ -55,6 +57,7 @@
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.7.6",
"node-object-hash": "^1.2.0",
"portfinder": "^1.0.32",
"postcss": "^8.4.2",
"postcss-custom-media": "^9.0.1",
"postcss-import": "^15.0.1",
Expand All @@ -72,6 +75,7 @@
"typescript": "^5.3.3",
"util-ex": "^0.3.15",
"webpack-dev-middleware": "^5.2.1",
"webpack-dev-server": "^4.13.1",
"webpack-hot-middleware": "^2.25.1",
"webpack-remove-empty-scripts": "^1.0.1",
"webpack-virtual-modules": "^0.4.3"
Expand Down
9 changes: 7 additions & 2 deletions webpack.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
const { ModuleFederationPlugin } = require('webpack').container;

const { generateStripesAlias } = require('./webpack/module-paths');
const { generateStripesAlias, } = require('./webpack/module-paths');
const { processShared } = require('./webpack/utils');
const typescriptLoaderRule = require('./webpack/typescript-loader-rule');
const { isProduction } = require('./webpack/utils');
const { getTranspiledCssPaths } = require('./webpack/module-paths');
const { singletons } = require('./consts');

const shared = processShared(singletons, { singleton: true, eager: true });

// React doesn't like being included multiple times as can happen when using
// yarn link. Here we find a more specific path to it by first looking in
Expand Down Expand Up @@ -65,6 +70,7 @@ const baseConfig = {
}),
new webpack.EnvironmentPlugin(['NODE_ENV']),
new RemoveEmptyScriptsPlugin(),
new ModuleFederationPlugin({ name: 'host', shared }),
],
module: {
rules: [
Expand Down Expand Up @@ -131,7 +137,6 @@ const baseConfig = {
},
};


const buildConfig = (modulePaths) => {
const transpiledCssPaths = getTranspiledCssPaths(modulePaths);
const cssDistPathRegex = /dist[\/\\]style\.css/;
Expand Down
5 changes: 3 additions & 2 deletions webpack.config.cli.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const esbuildLoaderRule = require('./webpack/esbuild-loader-rule');
const utils = require('./webpack/utils');
const buildBaseConfig = require('./webpack.config.base');
const cli = require('./webpack.config.cli');

const StripesFederationPlugin = require('./webpack/stripes-federation-plugin');

const useBrowserMocha = () => {
return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha';
Expand Down Expand Up @@ -56,7 +56,8 @@ const buildConfig = (stripesConfig) => {
if (utils.isDevelopment) {
devConfig.plugins = devConfig.plugins.concat([
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin()
new ReactRefreshWebpackPlugin(),
new StripesFederationPlugin(stripesConfig)
]);
}

Expand Down
140 changes: 140 additions & 0 deletions webpack.config.federate.remote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { container } = webpack;
const { processExternals, processShared } = require('./webpack/utils');
const { getStripesModulesPaths } = require('./webpack/module-paths');
const esbuildLoaderRule = require('./webpack/esbuild-loader-rule');
const { singletons } = require('./consts');

const buildConfig = (metadata) => {
const { host, port, name, displayName } = metadata;
const mainEntry = path.join(process.cwd(), 'src', 'index.js');
const stripesModulePaths = getStripesModulesPaths();
const translationsPath = path.join(process.cwd(), 'translations', displayName.split('.').shift());
const iconsPath = path.join(process.cwd(), 'icons');

// yeah, yeah, soundsPath vs sound. sorry. `sound` is a legacy name.
// other paths are plural and I'm sticking with that convention.
const soundsPath = path.join(process.cwd(), 'sound');
const shared = processShared(singletons, { singleton: true });

const config = {
name,
devtool: 'inline-source-map',
mode: 'development',
entry: mainEntry,
output: {
publicPath: `${host}:${port}/`,
},
devServer: {
port: port,
open: false,
headers: {
'Access-Control-Allow-Origin': '*',
},
static: [
{
directory: translationsPath,
publicPath: '/translations'
},
{
directory: iconsPath,
publicPath: '/icons'
},
{
directory: soundsPath,
publicPath: '/sounds'
},
]
},
module: {
rules: [
esbuildLoaderRule(stripesModulePaths),
{
test: /\.(woff2?)$/,
type: 'asset/resource',
generator: {
filename: './fonts/[name].[contenthash].[ext]',
},
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[local]---[hash:base64:5]',
},
sourceMap: true,
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: path.resolve(__dirname, 'postcss.config.js'),
},
sourceMap: true,
},
},
],
},
{
test: /\.(jpg|jpeg|gif|png|ico)$/,
type: 'asset/resource',
generator: {
filename: './img/[name].[contenthash].[ext]',
},
},
// {
// test: /\.svg$/,
// use: [{
// loader: 'url-loader',
// options: {
// esModule: false,
// },
// }]
// },
{
test: /\.svg$/,
type: 'asset/inline',
resourceQuery: { not: /icon/ } // exclude built-in icons from stripes-components which are loaded as react components.
},
{
test: /\.svg$/,
resourceQuery: /icon/, // stcom icons use this query on the resource.
use: ['@svgr/webpack']
},
{
test: /\.js.map$/,
enforce: "pre",
use: ['source-map-loader'],
}
]
},
// TODO: remove this after stripes-config is gone.
externals: processExternals({ 'stripes-config': true }),
plugins: [
new MiniCssExtractPlugin({ filename: 'style.css', ignoreOrder: false }),
new container.ModuleFederationPlugin({
library: { type: 'var', name },
name,
filename: 'remoteEntry.js',
exposes: {
'./MainEntry': mainEntry,
},
shared
}),
]
};

return config;
}

module.exports = buildConfig;
66 changes: 66 additions & 0 deletions webpack/federate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const path = require('path');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const axios = require('axios');
const { snakeCase } = require('lodash');
const portfinder = require('portfinder');

const buildConfig = require('../webpack.config.federate.remote');
const { tryResolve } = require('./module-paths');
const logger = require('./logger')();

// Remotes will be serve starting from port 3002
portfinder.setBasePort(3002);

module.exports = async function federate(options = {}) {
logger.log('starting federation...');

const packageJsonPath = tryResolve(path.join(process.cwd(), 'package.json'));

if (!packageJsonPath) {
console.error('package.json not found');
process.exit();
}

const port = options.port ?? await portfinder.getPortPromise();
const host = `http://localhost`;
const url = `${host}:${port}/remoteEntry.js`;

const { name: packageName, version, description, stripes } = require(packageJsonPath);
const { permissionSets: _, ...stripesRest } = stripes;
const name = snakeCase(packageName);
const metadata = {
module: packageName,
version,
description,
host,
port,
url,
name,
...stripesRest,
};

const config = buildConfig(metadata);

// TODO: allow for configuring registryUrl via env var or stripes config
const registryUrl = 'http://localhost:3001/registry';

// update registry
axios.post(registryUrl, metadata).catch(error => {
console.error(`Registry not found. Please check ${registryUrl}`);
process.exit();
});

const compiler = webpack(config);
const server = new WebpackDevServer(config.devServer, compiler);
console.log(`Starting remote server on port ${port}`);
server.start();

compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => {
try {
await axios.delete(registryUrl, { data: metadata });
} catch (error) {
console.error(`registry not found. Please check ${registryUrl}`);
}
});
};
1 change: 1 addition & 0 deletions webpack/module-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,5 @@ module.exports = {
getNonTranspiledModules,
getTranspiledModules,
getTranspiledCssPaths,
locatePackageJsonPath,
};
44 changes: 44 additions & 0 deletions webpack/registryServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const express = require('express');
const cors = require('cors');

// Registry data
const registry = { remotes: {} };

const registryServer = {
start: () => {
const app = express();

app.use(express.json());
app.use(cors());

// add/update remote to registry
app.post('/registry', (req, res) => {
const metadata = req.body;
const { name } = metadata;

registry.remotes[name] = metadata;
res.status(200).send(`Remote ${name} metadata updated`);

Check failure

Code scanning / SonarCloud

Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks High

Change this code to not reflect user-controlled data. See more on SonarQube Cloud
});

// return entire registry for machines
app.get('/registry', (_, res) => res.json(registry));

// return entire registry for humans
app.get('/code', (_, res) => res.send(`<pre>${JSON.stringify(registry, null, 2)}</pre>`));

Check failure

Code scanning / SonarCloud

Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks High

Change this code to not reflect user-controlled data. See more on SonarQube Cloud

app.delete('/registry', (req, res) => {
const metadata = req.body;
const { name } = metadata;

delete registry.remotes[name];

res.status(200).send(`Remote ${name} removed`);

Check failure

Code scanning / SonarCloud

Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks High

Change this code to not reflect user-controlled data. See more on SonarQube Cloud
});

app.listen(3001, () => {
console.log('Starting registry server at http://localhost:3001');
});
}
};

module.exports = registryServer;
9 changes: 9 additions & 0 deletions webpack/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const logger = require('./logger')();
const buildConfig = require('../webpack.config.cli.dev');
const sharedStylesConfig = require('../webpack.config.cli.shared.styles');
const buildServiceWorkerConfig = require('../webpack.config.service.worker');
const registryServer = require('./registryServer');

const cwd = path.resolve();
const platformModulePath = path.join(cwd, 'node_modules');
Expand All @@ -32,6 +33,14 @@ module.exports = function serve(stripesConfig, options) {
serviceWorkerConfig.resolve = { modules: ['node_modules', platformModulePath, coreModulePath] };
serviceWorkerConfig.resolveLoader = { modules: ['node_modules', platformModulePath, coreModulePath] };

// stripes module registry
try {
registryServer.start();
}
catch (e) {
console.error(e)
}

let config = buildConfig(stripesConfig);

config = sharedStylesConfig(config, {});
Expand Down
Loading
Loading