diff --git a/consts.js b/consts.js index f0978a7..ffc4519 100644 --- a/consts.js +++ b/consts.js @@ -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 }; diff --git a/package.json b/package.json index f07752b..26ffb08 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" @@ -92,4 +94,4 @@ "react-dom": "^18.2.0", "webpack": "^5.58.1" } -} +} \ No newline at end of file diff --git a/webpack.config.base.js b/webpack.config.base.js index ecee96e..c587736 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -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 @@ -65,7 +70,6 @@ const baseConfig = { }), new webpack.EnvironmentPlugin(['NODE_ENV']), new RemoveEmptyScriptsPlugin(), - new webpack.ManifestPlugin({ entrypoints: true }), ], module: { rules: [ @@ -132,7 +136,6 @@ const baseConfig = { }, }; - const buildConfig = (modulePaths) => { const transpiledCssPaths = getTranspiledCssPaths(modulePaths); const cssDistPathRegex = /dist[\/\\]style\.css/; diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 8942fdc..6136a41 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -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 = () => { @@ -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 diff --git a/webpack.config.cli.prod.js b/webpack.config.cli.prod.js index 6675680..b4d8fc6 100644 --- a/webpack.config.cli.prod.js +++ b/webpack.config.cli.prod.js @@ -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'); @@ -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)); diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 795fed6..7bcfa3f 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -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'); @@ -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: @@ -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 }, @@ -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')], }), ] }; @@ -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': '*', }, diff --git a/webpack/federate.js b/webpack/federate.js index b188f32..fe7a2a1 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -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) { @@ -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) { @@ -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(); diff --git a/webpack/host-override-share-plugin.js b/webpack/host-override-share-plugin.js new file mode 100644 index 0000000..5d0ee0e --- /dev/null +++ b/webpack/host-override-share-plugin.js @@ -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; \ No newline at end of file diff --git a/webpack/remote-runtime-plugin.js b/webpack/remote-runtime-plugin.js new file mode 100644 index 0000000..f02832e --- /dev/null +++ b/webpack/remote-runtime-plugin.js @@ -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; \ No newline at end of file diff --git a/webpack/utils.js b/webpack/utils.js index 7133f8d..5727e3e 100644 --- a/webpack/utils.js +++ b/webpack/utils.js @@ -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 };