Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
bc644ce
STRIPES-861: Setup module federation
mkuklis Apr 25, 2023
b9431f1
Cleanup
mkuklis Apr 25, 2023
467f390
Cleanup
mkuklis Apr 25, 2023
7c3576a
cleanup
mkuklis Apr 25, 2023
2deed9e
Use shutdown hook
mkuklis Apr 25, 2023
49189bd
Cleanup
mkuklis Apr 26, 2023
12cc324
Cleanup
mkuklis Apr 26, 2023
60b5dae
Cleanup
mkuklis Apr 26, 2023
97ba155
Add required version to shared singletons
mkuklis Apr 27, 2023
8c47f56
Start remotes automatically
mkuklis May 1, 2023
84c0199
Expose icons via public endpoint
mkuklis May 10, 2023
54db9c9
react-query provides a context, so must be a singleton
zburke May 16, 2023
ecaa7a6
STCOR-726 map sounds directory for remote applications
zburke Jun 8, 2023
42cac16
current versions
zburke Dec 4, 2024
12613ed
separate handling of stripes-components and application icons
zburke Dec 4, 2024
0c73e79
resolve conflicts
JohnC-80 Oct 31, 2025
626a7c8
provide registry url in stripes-config
JohnC-80 Nov 5, 2025
b399047
resolve conflict
JohnC-80 Nov 5, 2025
08068ed
collect translations from 'StripesDeps' modules in built translations
JohnC-80 Nov 18, 2025
8889e08
get the app's main entry from package.json
JohnC-80 Nov 18, 2025
a8b5fb0
ensure there are aliases before member access
JohnC-80 Nov 18, 2025
fddfec2
expand the singletons for the platform
JohnC-80 Nov 18, 2025
915fa56
migrate mf plugin/libraries to @module-federation/enhanced
JohnC-80 Dec 1, 2025
e729149
bump webpack-dev-server version
JohnC-80 Feb 5, 2026
68e17c5
add community Module-federation plugin
JohnC-80 Feb 5, 2026
3646be8
restore stripes-config-plugin from main
JohnC-80 Feb 5, 2026
019d185
turn off hot reloading in module-level dev server
JohnC-80 Feb 10, 2026
5a580c7
turn off hot reloading in devtools for now
JohnC-80 Feb 10, 2026
b44d8a5
use default discoveryUrl for local
JohnC-80 Feb 11, 2026
18f969b
fussing with attempting to override shares with host dependencies
JohnC-80 Feb 13, 2026
70e0922
re-add test folder
JohnC-80 Feb 13, 2026
0e3218c
sync with STCOR STRWEB-139 changes
JohnC-80 Feb 19, 2026
319461f
add runtimePlugin for host override
JohnC-80 Feb 19, 2026
027d41e
remove auto version from modules
JohnC-80 Feb 19, 2026
09a1b0e
single runtime from host
JohnC-80 Feb 20, 2026
16e713b
Merge branch 'STRWEB-139' of github.com:folio-org/stripes-webpack int…
JohnC-80 Feb 20, 2026
fec245e
set external runtime to true for modules
JohnC-80 Feb 20, 2026
15874db
exclude host-shares from module bundle
JohnC-80 Feb 20, 2026
0fc851e
add further optimization to module bundle, remove runtime plugins
JohnC-80 Feb 20, 2026
3b6f2c0
add remote runtime plugin to absorb plugins of host at runtime
JohnC-80 Feb 20, 2026
e66662c
push host plugins to remote runtime
JohnC-80 Feb 21, 2026
6f9812a
collect debug info, cleanup logging in runtime plugins
JohnC-80 Feb 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ const getHostAppSingletons = () => {
return platformSingletons;
}

const defaultEntitlementUrl = 'http://localhost:3001/registry';
const defaultDiscoveryUrl = 'http://localhost:3001/registry';

module.exports = {
defaultEntitlementUrl,
defaultDiscoveryUrl,
singletons,
getHostAppSingletons
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
"@cerner/duplicate-package-checker-webpack-plugin": "~2.1.0",
"@csstools/postcss-global-data": "^3.0.0",
"@csstools/postcss-relative-color-syntax": "^3.0.7",
"@module-federation/enhanced": "^2.0.0",
"@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",
"buffer": "^6.0.3",
Expand Down Expand Up @@ -73,7 +75,7 @@
"util-ex": "^0.3.15",
"validate-npm-package-name": "^6.0.2",
"webpack-dev-middleware": "^5.2.1",
"webpack-dev-server": "^4.13.1",
"webpack-dev-server": "^5.2.3",
"webpack-hot-middleware": "^2.25.1",
"webpack-remove-empty-scripts": "^1.0.1",
"webpack-virtual-modules": "^0.4.3"
Expand All @@ -92,4 +94,4 @@
"react-dom": "^18.2.0",
"webpack": "^5.58.1"
}
}
}
9 changes: 6 additions & 3 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('@module-federation/enhanced/webpack');

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,7 +70,6 @@ const baseConfig = {
}),
new webpack.EnvironmentPlugin(['NODE_ENV']),
new RemoveEmptyScriptsPlugin(),
new webpack.ManifestPlugin({ entrypoints: true }),
],
module: {
rules: [
Expand Down Expand Up @@ -132,7 +136,6 @@ const baseConfig = {
},
};


const buildConfig = (modulePaths) => {
const transpiledCssPaths = getTranspiledCssPaths(modulePaths);
const cssDistPathRegex = /dist[\/\\]style\.css/;
Expand Down
16 changes: 14 additions & 2 deletions webpack.config.cli.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const utils = require('./webpack/utils');
const buildBaseConfig = require('./webpack.config.base');
const cli = require('./webpack.config.cli');
const { getHostAppSingletons } = require('./consts');
const { ModuleFederationPlugin } = require('webpack').container;
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
const { processShared } = require('./webpack/utils');

const useBrowserMocha = () => {
Expand Down Expand Up @@ -64,9 +64,21 @@ const buildConfig = (stripesConfig) => {

// Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules.
if (stripesConfig.okapi.discoveryUrl) {
devConfig.cache = false;
const hostAppSingletons = getHostAppSingletons();
const shared = processShared(hostAppSingletons, { singleton: true, eager: true });
devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared }));
devConfig.plugins.push(new ModuleFederationPlugin({
experiments: {
provideExternalRuntime: true,
optimization: {
target: 'web',
}
},
name: 'host',
shared,
shareStrategy: 'loaded-first',
runtimePlugins: [require.resolve('./webpack/host-override-share-plugin')],
}));
}

// This alias avoids a console warning for react-dom patch
Expand Down
21 changes: 11 additions & 10 deletions webpack.config.cli.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const cli = require('./webpack.config.cli');
const esbuildLoaderRule = require('./webpack/esbuild-loader-rule');
const { getModulesPaths, getStripesModulesPaths, getTranspiledModules } = require('./webpack/module-paths');
const { processShared } = require('./webpack/utils');
const { ModuleFederationPlugin } = require('webpack').container;
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
const { getHostAppSingletons } = require('./consts');


Expand Down Expand Up @@ -65,17 +65,18 @@ const buildConfig = (stripesConfig, options = {}) => {
prodConfig.plugins.push(
new ModuleFederationPlugin({ name: 'host', shared })
);
} else {
prodConfig.optimization = {
mangleWasmImports: false,
minimizer: [
new EsbuildPlugin({
css: true,
}),
],
splitChunks,
}
}

prodConfig.optimization = {
mangleWasmImports: false,
minimizer: [
new EsbuildPlugin({
css: true,
}),
],
splitChunks,
}

prodConfig.module.rules.push(esbuildLoaderRule(allModulePaths));

Expand Down
22 changes: 18 additions & 4 deletions webpack.config.federate.remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyPlugin = require("copy-webpack-plugin");
const StripesTranslationsPlugin = require('./webpack/stripes-translations-plugin');
const { container } = webpack;
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
const { processExternals, processShared } = require('./webpack/utils');
const { getStripesModulesPaths } = require('./webpack/module-paths');
const esbuildLoaderRule = require('./webpack/esbuild-loader-rule');
Expand Down Expand Up @@ -36,7 +36,7 @@ const buildConfig = (metadata, options) => {
// For dependencies that are configured as singletons, only a single version will be loaded from the host app.
// If a version is semver incompatible, a console warning will be emitted.
const configSingletons = getHostAppSingletons();
const shared = processShared(configSingletons, { singleton: true });
const shared = processShared(configSingletons, { singleton: true, eager: false, import: false }, true);

// general webpack config.
// Some noteworthy settings:
Expand All @@ -55,6 +55,7 @@ const buildConfig = (metadata, options) => {
publicPath: 'auto', // webpack will determine publicPath of script at runtime.
path: options.outputPath ? path.resolve(options.outputPath) : undefined
},
cache: false,
stats: {
errorDetails: true
},
Expand Down Expand Up @@ -139,14 +140,22 @@ const buildConfig = (metadata, options) => {
// 2. remote entry requires its own set of chunks, determining location of those chunks (publicPath: 'auto' logic).
// 3. The above are stored in a 'container' (webpack/mod-fed term) - a global variable by the 'name' field.
// The host app 'imports' the app via container.get('MainEntry') from the loaded code.
new container.ModuleFederationPlugin({
new ModuleFederationPlugin({
library: { type: 'var', name },
name,
filename: 'remoteEntry.js',
exposes: {
'./MainEntry': mainEntry,
},
shared
shared,
experiments: {
externalRuntime: true,
optimization: {
target: 'web',
}
},
shareStrategy: 'loaded-first',
runtimePlugins: [require.resolve('./webpack/remote-runtime-plugin')],
}),
]
};
Expand All @@ -164,9 +173,14 @@ const buildConfig = (metadata, options) => {
} else {
// in development mode, setup the devserver...
config.devtool = 'inline-source-map';
// turning off hot reloading and overlay since we're using the dev sever for hosting static files rather than actual dev work.
config.devServer = {
hot: false,
port: port,
open: false,
client: {
overlay: false,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
Expand Down
8 changes: 6 additions & 2 deletions webpack/federate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { snakeCase } = require('lodash');
const buildConfig = require('../webpack.config.federate.remote');
const { tryResolve } = require('./module-paths');
const logger = require('./logger')();
const { defaultDiscoveryUrl } = require('../consts');

// Function to check if a port is free
function isPortFree(port) {
Expand All @@ -33,7 +34,9 @@ async function findFreePort(startPort) {

module.exports = async function federate(stripesConfig, options = {}, callback = () => { }) {
logger.log('starting federation...');
const { discoveryUrl } = stripesConfig.okapi;
const { discoveryUrl: configDiscoveryUrl } = stripesConfig.okapi;
const discoveryUrl = configDiscoveryUrl || defaultDiscoveryUrl;

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

if (!packageJsonPath) {
Expand Down Expand Up @@ -81,7 +84,8 @@ module.exports = async function federate(stripesConfig, options = {}, callback =
method: 'POST',
headers: requestHeader,
body: JSON.stringify(metadata),
})
});
console.log(`Module registered with local discovery at ${discoveryUrl}`);
} catch (err) {
console.error(`Local discovery not found for module registration. Please check ${discoveryUrl}:${err}`);
process.exit();
Expand Down
41 changes: 41 additions & 0 deletions webpack/host-override-share-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// This file can be used to attach lifecycle hooks to the module federation runtime logic.
// See https://module-federation.io/guide/runtime/runtime-hooks.html for possible entries.

const HostOverrideSharePlugin = () => {
return {
name: 'host-override-share-plugin',
// collect requests for shares that are different versions from the host app's provided versions for debugging purposes.
// accessible in the console via the __DEBUG_MISSED_DEPS__ global variable.
async beforeLoadShare(args) {
if (!globalThis.__DEBUG_MISSED_DEPS__) {
globalThis.__DEBUG_MISSED_DEPS__ = [];
}

const hostInstance = __FEDERATION__.__INSTANCES__[0];
if (!hostInstance) {
return args;
}

const { origin, shareInfo, pkgName } = args;

const hostShared = hostInstance.options.shared[pkgName][0];

if (!hostShared) {
return args;
}

let hostVersion = hostShared.version;


if (shareInfo.shareConfig.requiredVersion !== hostVersion) {
if (globalThis.__DEBUG_MISSED_DEPS__.findIndex(s => s.pkgName === pkgName && s.remoteApp === origin.name) === -1) {
globalThis.__DEBUG_MISSED_DEPS__.push({ pkgName, hostVersion: hostVersion, remoteApp: origin.name, ...shareInfo });
}
}

return args;
},
};
};

export default HostOverrideSharePlugin;
29 changes: 29 additions & 0 deletions webpack/remote-runtime-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Remote containers include runtime plugins from their own build configuration, but not those of the host.
// In order for the remote's runtime plugins to be updated without a rebuild,
// we need to apply the host's plugins to the remote's container.
// This plugin sets a lifecycle method Before initialization of the remote's ModuleFederation instance
// to pass the host's runtime plugins to the remote container.

const RemoteRuntimePlugin = () => ({
name: 'remote-runtime-plugin',
beforeInit(args) {

// get override plugin from host instance...
const hostInstance = __FEDERATION__.__INSTANCES__[0];
if (!hostInstance) {
return args;
}
const hostOverridePlugin = hostInstance.options.plugins.find(plugin => plugin.name === 'host-override-share-plugin');
if (!hostOverridePlugin) {
return args;
}

// injects it into new instance at runtime.
const { origin } = args;
origin.registerPlugin(hostOverridePlugin);

return args;
},
});

export default RemoteRuntimePlugin;
4 changes: 2 additions & 2 deletions webpack/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ const processExternals = (peerDeps) => {
// and applies additional options for the module federation configuration,
// like setting the shared items as singletons, or using the 'eager' consumption
// setting (chunks are included in the initial bundle whether than split out/lazy loaded)
const processShared = (shared, options = {}) => {
const processShared = (shared, options = {}, remote) => {
return Object.keys(shared).reduce((acc, name) => {
acc[name] = {
acc[name] = remote ? options : {
requiredVersion: shared[name],
...options
};
Expand Down
Loading