From bc644ceb35d405c1f19a30fa1cdb5d8f9cca8857 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 09:41:17 -0400 Subject: [PATCH 01/60] STRIPES-861: Setup module federation --- consts.js | 18 +++++ package.json | 4 ++ webpack.config.base.js | 9 ++- webpack.config.cli.dev.js | 1 - webpack.config.federate.remote.js | 115 ++++++++++++++++++++++++++++++ webpack/federate.js | 64 +++++++++++++++++ webpack/module-paths.js | 1 + webpack/registryServer.js | 40 +++++++++++ webpack/serve.js | 4 ++ webpack/stripes-config-plugin.js | 16 +++-- webpack/stripes-module-parser.js | 7 +- webpack/stripes-node-api.js | 2 + webpack/utils.js | 8 +++ 13 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 consts.js create mode 100644 webpack.config.federate.remote.js create mode 100644 webpack/federate.js create mode 100644 webpack/registryServer.js diff --git a/consts.js b/consts.js new file mode 100644 index 0000000..041f7a9 --- /dev/null +++ b/consts.js @@ -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.0.0', + '@folio/stripes-shared-context': '^1.0.0', + 'react': '^18.2', + 'react-dom': '^18.2', + 'react-intl': '^6.4.4', + 'react-query': '^3.39.3', + 'react-redux': '^8.0.5', + 'react-router': '^5.2.0', + 'react-router-dom': '^5.2.0', + 'redux-observable': '^1.2.0', + 'rxjs': '^6.6.3' +}; + +module.exports = { + singletons, +}; diff --git a/package.json b/package.json index 6721220..ba20aa8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@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", @@ -41,6 +42,7 @@ "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", @@ -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", @@ -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" diff --git a/webpack.config.base.js b/webpack.config.base.js index 0be1037..3889973 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('webpack').container; -const { generateStripesAlias } = require('./webpack/module-paths'); +const { generateStripesAlias, locatePackageJsonPath } = 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,6 +70,7 @@ const baseConfig = { }), new webpack.EnvironmentPlugin(['NODE_ENV']), new RemoveEmptyScriptsPlugin(), + new ModuleFederationPlugin({ name: 'host', shared }), ], module: { rules: [ @@ -131,7 +137,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 baed059..912ddb6 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -10,7 +10,6 @@ const utils = require('./webpack/utils'); const buildBaseConfig = require('./webpack.config.base'); const cli = require('./webpack.config.cli'); - const useBrowserMocha = () => { return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; }; diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js new file mode 100644 index 0000000..286081c --- /dev/null +++ b/webpack.config.federate.remote.js @@ -0,0 +1,115 @@ +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 shared = processShared(singletons, { singleton: true }); + console.log(shared); + + 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' + } + }, + 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: /\.js.map$/, + enforce: "pre", + use: ['source-map-loader'], + } + ] + }, + externals: processExternals(['@folio/stripes', 'stripes-config']), + 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; diff --git a/webpack/federate.js b/webpack/federate.js new file mode 100644 index 0000000..1d6702e --- /dev/null +++ b/webpack/federate.js @@ -0,0 +1,64 @@ +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 applyWebpackOverrides = require('./apply-webpack-overrides'); +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 = 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(); + + process.on('SIGINT', async () => { + await axios.delete(registryUrl, { data: metadata }); + process.exit(0); + }); +}; diff --git a/webpack/module-paths.js b/webpack/module-paths.js index 00ea92b..9989090 100644 --- a/webpack/module-paths.js +++ b/webpack/module-paths.js @@ -264,4 +264,5 @@ module.exports = { getNonTranspiledModules, getTranspiledModules, getTranspiledCssPaths, + locatePackageJsonPath, }; diff --git a/webpack/registryServer.js b/webpack/registryServer.js new file mode 100644 index 0000000..6f17a04 --- /dev/null +++ b/webpack/registryServer.js @@ -0,0 +1,40 @@ +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`); + }); + + // return entire registry + app.get('/registry', (_, res) => res.json(registry)); + + app.delete('/registry', (req, res) => { + const metadata = req.body; + const { name } = metadata; + delete registry.remotes[name]; + + res.status(200).send(`Remote ${name} removed`); + }); + + app.listen(3001, () => { + console.log('Starting registry server at http://localhost:3001'); + }); + } +}; + +module.exports = registryServer; diff --git a/webpack/serve.js b/webpack/serve.js index c0e3b51..29c0291 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -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'); @@ -32,6 +33,9 @@ module.exports = function serve(stripesConfig, options) { serviceWorkerConfig.resolve = { modules: ['node_modules', platformModulePath, coreModulePath] }; serviceWorkerConfig.resolveLoader = { modules: ['node_modules', platformModulePath, coreModulePath] }; + // stripes module registry + registryServer.start(); + let config = buildConfig(stripesConfig); config = sharedStylesConfig(config, {}); diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index 3ef8996..d9fdffa 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -44,8 +44,14 @@ module.exports = class StripesConfigPlugin { apply(compiler) { const enabledModules = this.options.modules; logger.log('enabled modules:', enabledModules); - const { config, metadata, icons, stripesDeps, warnings } = stripesModuleParser.parseAllModules(enabledModules, compiler.context, compiler.options.resolve.alias); - this.mergedConfig = Object.assign({}, this.options, { modules: config }); + //const { config, metadata, icons, stripesDeps, warnings } = stripesModuleParser.parseAllModules(enabledModules, compiler.context, compiler.options.resolve.alias); + const stripesDeps = {}; + const config = this.options; + const warnings = {}; + const metadata = {}; + const icons = {}; + this.mergedConfig = config; + // Object.assign({}, this.options); this.metadata = metadata; this.icons = icons; this.warnings = warnings; @@ -78,11 +84,13 @@ module.exports = class StripesConfigPlugin { const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)}; const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)}; const translations = ${serialize(pluginData.translations, { space: 2 })}; - const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; - const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; + // const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; + // const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; export { okapi, config, modules, branding, errorLogging, translations, metadata, icons }; `; + console.log(stripesVirtualModule); + logger.log('writing virtual module...', stripesVirtualModule); this.virtualModule.writeModule('node_modules/stripes-config.js', stripesVirtualModule); } diff --git a/webpack/stripes-module-parser.js b/webpack/stripes-module-parser.js index 93b06f8..a7e482f 100644 --- a/webpack/stripes-module-parser.js +++ b/webpack/stripes-module-parser.js @@ -109,7 +109,8 @@ class StripesModuleParser { // Validates and parses a module's stripes data parseStripesConfig(moduleName, packageJson) { const { stripes, description, version } = packageJson; - const getModule = new Function([], `return require('${moduleName}').default;`); + //const getModule = new Function([], `return require('${moduleName}').default;`); + const getModule = new Function([], ``); const stripesConfig = _.omit(Object.assign({}, stripes, this.overrideConfig, { module: moduleName, getModule, @@ -207,14 +208,17 @@ function parseAllModules(enabledModules, context, aliases) { // stripesDeps const config = parsedModule.config; + if (Array.isArray(config.stripesDeps)) { config.stripesDeps.forEach(dep => { // locate dep relative to the module that depends on it const depContext = modulePaths.locateStripesModule(context, config.module, aliases, 'package.json'); const packageJsonPath = modulePaths.locateStripesModule(depContext, dep, aliases, 'package.json'); + if (!packageJsonPath) { throw new StripesBuildError(`StripesModuleParser: Unable to locate ${dep}'s package.json (dependency of ${config.module})`); } + const packageJson = require(packageJsonPath); const resolvedPath = packageJsonPath.replace('/package.json', ''); unsortedStripesDeps[dep] = appendOrSingleton(unsortedStripesDeps[dep], { @@ -255,6 +259,7 @@ function parseAllModules(enabledModules, context, aliases) { } return depIcons; }, {}); + for (const [key, value] of Object.entries(stripesDeps)) { if (anyHasIcon(value)) { icons[key] = mergeIcons(value); diff --git a/webpack/stripes-node-api.js b/webpack/stripes-node-api.js index afffb6d..2b0d9d7 100644 --- a/webpack/stripes-node-api.js +++ b/webpack/stripes-node-api.js @@ -1,9 +1,11 @@ const build = require('./build'); const serve = require('./serve'); const transpile = require('./transpile'); +const federate = require('./federate'); module.exports = { build, serve, transpile, + federate, }; diff --git a/webpack/utils.js b/webpack/utils.js index b29bfda..b274568 100644 --- a/webpack/utils.js +++ b/webpack/utils.js @@ -14,8 +14,16 @@ const processExternals = (peerDeps) => { }, {}); }; +const processShared = (shared, options = {}) => { + return shared.reduce((acc, name) => { + acc[name] = options; + return acc; + }, {}); +}; + module.exports = { processExternals, isDevelopment, isProduction, + processShared, }; From b9431f10ba0977dddf9cedc9a57c0e9c8e05bc9c Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 09:42:49 -0400 Subject: [PATCH 02/60] Cleanup --- webpack.config.federate.remote.js | 1 - webpack/stripes-config-plugin.js | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 286081c..3fc1af2 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -13,7 +13,6 @@ const buildConfig = (metadata) => { const stripesModulePaths = getStripesModulesPaths(); const translationsPath = path.join(process.cwd(), 'translations', displayName.split('.').shift()); const shared = processShared(singletons, { singleton: true }); - console.log(shared); const config = { name, diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index d9fdffa..a9f3672 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -51,7 +51,6 @@ module.exports = class StripesConfigPlugin { const metadata = {}; const icons = {}; this.mergedConfig = config; - // Object.assign({}, this.options); this.metadata = metadata; this.icons = icons; this.warnings = warnings; @@ -84,13 +83,11 @@ module.exports = class StripesConfigPlugin { const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)}; const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)}; const translations = ${serialize(pluginData.translations, { space: 2 })}; - // const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; - // const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; + const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; + const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; export { okapi, config, modules, branding, errorLogging, translations, metadata, icons }; `; - console.log(stripesVirtualModule); - logger.log('writing virtual module...', stripesVirtualModule); this.virtualModule.writeModule('node_modules/stripes-config.js', stripesVirtualModule); } From 467f39049a9203f4a759a8652b9f281ff629b388 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 10:20:33 -0400 Subject: [PATCH 03/60] Cleanup --- consts.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/consts.js b/consts.js index 041f7a9..6a81051 100644 --- a/consts.js +++ b/consts.js @@ -1,17 +1,14 @@ -// TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129 -const singletons = { - '@folio/stripes': '^9.0.0', - '@folio/stripes-shared-context': '^1.0.0', - 'react': '^18.2', - 'react-dom': '^18.2', - 'react-intl': '^6.4.4', - 'react-query': '^3.39.3', - 'react-redux': '^8.0.5', - 'react-router': '^5.2.0', - 'react-router-dom': '^5.2.0', - 'redux-observable': '^1.2.0', - 'rxjs': '^6.6.3' -}; +const singletons = [ + '@folio/stripes', + '@folio/stripes-shared-context', + 'react', + 'react-dom', + 'react-router', + 'react-router-dom', + 'react-redux', + 'react-intl', + 'react-query', +]; module.exports = { singletons, From 7c3576afe2d21413f22ec3de0dce34cc1a11a033 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 12:38:29 -0400 Subject: [PATCH 04/60] cleanup --- webpack.config.federate.remote.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 3fc1af2..e82338a 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -93,7 +93,8 @@ const buildConfig = (metadata) => { } ] }, - externals: processExternals(['@folio/stripes', 'stripes-config']), + // TODO: remove this after stripes-config is gone. + externals: processExternals({ 'stripes-config': true }), plugins: [ new MiniCssExtractPlugin({ filename: 'style.css', ignoreOrder: false }), new container.ModuleFederationPlugin({ From 2deed9e6e5843e031a67901acc02bb5e8ab6dece Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 16:01:46 -0400 Subject: [PATCH 05/60] Use shutdown hook --- webpack/federate.js | 9 ++++++--- webpack/registryServer.js | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/webpack/federate.js b/webpack/federate.js index 1d6702e..d9637ab 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -57,8 +57,11 @@ module.exports = async function federate(options = {}) { console.log(`Starting remote server on port ${port}`); server.start(); - process.on('SIGINT', async () => { - await axios.delete(registryUrl, { data: metadata }); - process.exit(0); + compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { + try { + await axios.delete(registryUrl, { data: metadata }); + } catch (error) { + console.error('AsyncShutdownHook error:', error); + } }); }; diff --git a/webpack/registryServer.js b/webpack/registryServer.js index 6f17a04..edf2e69 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -26,6 +26,7 @@ const registryServer = { app.delete('/registry', (req, res) => { const metadata = req.body; const { name } = metadata; + delete registry.remotes[name]; res.status(200).send(`Remote ${name} removed`); From 49189bd718a3651a448b26e96b1c394ab38e624c Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 21:16:42 -0400 Subject: [PATCH 06/60] Cleanup --- webpack/stripes-config-plugin.js | 35 ++++---------------------- webpack/stripes-translations-plugin.js | 6 ----- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index a9f3672..9cdc0fa 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -10,7 +10,6 @@ const _ = require('lodash'); const VirtualModulesPlugin = require('webpack-virtual-modules'); const serialize = require('serialize-javascript'); const { SyncHook } = require('tapable'); -const stripesModuleParser = require('./stripes-module-parser'); const StripesBuildError = require('./stripes-build-error'); const stripesSerialize = require('./stripes-serialize'); const logger = require('./logger')('stripesConfigPlugin'); @@ -20,9 +19,7 @@ const stripesConfigPluginHooksMap = new WeakMap(); module.exports = class StripesConfigPlugin { constructor(options) { logger.log('initializing...'); - if (!_.isObject(options.modules)) { - throw new StripesBuildError('stripes-config-plugin was not provided a "modules" object for enabling stripes modules'); - } + this.options = _.omit(options, 'branding', 'errorLogging'); } @@ -42,36 +39,24 @@ module.exports = class StripesConfigPlugin { } apply(compiler) { - const enabledModules = this.options.modules; - logger.log('enabled modules:', enabledModules); - //const { config, metadata, icons, stripesDeps, warnings } = stripesModuleParser.parseAllModules(enabledModules, compiler.context, compiler.options.resolve.alias); - const stripesDeps = {}; const config = this.options; - const warnings = {}; - const metadata = {}; - const icons = {}; - this.mergedConfig = config; - this.metadata = metadata; - this.icons = icons; - this.warnings = warnings; + this.config = config; // Prep the virtual module now, we will write to it when ready this.virtualModule = new VirtualModulesPlugin(); this.virtualModule.apply(compiler); StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap( { name: 'StripesConfigPlugin', context: true }, - context => Object.assign(context, { config, metadata, icons, stripesDeps, warnings })); + context => Object.assign(context, { config })); // Wait until after other plugins to generate virtual stripes-config compiler.hooks.afterPlugins.tap('StripesConfigPlugin', (theCompiler) => this.afterPlugins(theCompiler)); - compiler.hooks.emit.tapAsync('StripesConfigPlugin', (compilation, callback) => this.processWarnings(compilation, callback)); } afterPlugins(compiler) { // Data provided by other stripes plugins via hooks const pluginData = { branding: {}, - errorLogging: {}, translations: {}, }; @@ -79,23 +64,13 @@ module.exports = class StripesConfigPlugin { // Create a virtual module for Webpack to include in the build const stripesVirtualModule = ` - const { okapi, config, modules } = ${serialize(this.mergedConfig, { space: 2 })}; + const { okapi, config } = ${serialize(this.config, { space: 2 })}; const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)}; - const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)}; const translations = ${serialize(pluginData.translations, { space: 2 })}; - const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; - const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; - export { okapi, config, modules, branding, errorLogging, translations, metadata, icons }; + export { okapi, config, branding, translations }; `; logger.log('writing virtual module...', stripesVirtualModule); this.virtualModule.writeModule('node_modules/stripes-config.js', stripesVirtualModule); } - - processWarnings(compilation, callback) { - if (this.warnings.length) { - compilation.warnings.push(new StripesBuildError(`stripes-config-plugin:\n ${this.warnings.join('\n ')}`)); - } - callback(); - } }; diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js index c2dcbb2..97fd973 100644 --- a/webpack/stripes-translations-plugin.js +++ b/webpack/stripes-translations-plugin.js @@ -47,12 +47,6 @@ module.exports = class StripesTranslationPlugin { // Hook into stripesConfigPlugin to supply paths to translation files // and gather additional modules from stripes.stripesDeps StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap({ name: 'StripesTranslationsPlugin', context: true }, (context, config) => { - // Add stripesDeps - for (const [key, value] of Object.entries(context.stripesDeps)) { - // TODO: merge translations from all versions of stripesDeps - this.modules[key] = value[value.length - 1]; - } - // Gather all translations available in each module const allTranslations = this.gatherAllTranslations(); From 12cc324f4357dca6173a2ba243344570e56b7959 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 22:20:48 -0400 Subject: [PATCH 07/60] Cleanup --- webpack/federate.js | 2 +- webpack/stripes-module-parser.js | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/webpack/federate.js b/webpack/federate.js index d9637ab..9aa62a2 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -61,7 +61,7 @@ module.exports = async function federate(options = {}) { try { await axios.delete(registryUrl, { data: metadata }); } catch (error) { - console.error('AsyncShutdownHook error:', error); + console.error(`registry not found. Please check ${registryUrl}`); } }); }; diff --git a/webpack/stripes-module-parser.js b/webpack/stripes-module-parser.js index a7e482f..a5fd649 100644 --- a/webpack/stripes-module-parser.js +++ b/webpack/stripes-module-parser.js @@ -109,8 +109,7 @@ class StripesModuleParser { // Validates and parses a module's stripes data parseStripesConfig(moduleName, packageJson) { const { stripes, description, version } = packageJson; - //const getModule = new Function([], `return require('${moduleName}').default;`); - const getModule = new Function([], ``); + const getModule = new Function([], `return require('${moduleName}').default;`); const stripesConfig = _.omit(Object.assign({}, stripes, this.overrideConfig, { module: moduleName, getModule, @@ -208,13 +207,11 @@ function parseAllModules(enabledModules, context, aliases) { // stripesDeps const config = parsedModule.config; - if (Array.isArray(config.stripesDeps)) { config.stripesDeps.forEach(dep => { // locate dep relative to the module that depends on it const depContext = modulePaths.locateStripesModule(context, config.module, aliases, 'package.json'); const packageJsonPath = modulePaths.locateStripesModule(depContext, dep, aliases, 'package.json'); - if (!packageJsonPath) { throw new StripesBuildError(`StripesModuleParser: Unable to locate ${dep}'s package.json (dependency of ${config.module})`); } @@ -259,7 +256,6 @@ function parseAllModules(enabledModules, context, aliases) { } return depIcons; }, {}); - for (const [key, value] of Object.entries(stripesDeps)) { if (anyHasIcon(value)) { icons[key] = mergeIcons(value); From 60b5daed25f00132527f49f12b87210f76b1da3d Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 22:22:08 -0400 Subject: [PATCH 08/60] Cleanup --- webpack/stripes-module-parser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/webpack/stripes-module-parser.js b/webpack/stripes-module-parser.js index a5fd649..93b06f8 100644 --- a/webpack/stripes-module-parser.js +++ b/webpack/stripes-module-parser.js @@ -215,7 +215,6 @@ function parseAllModules(enabledModules, context, aliases) { if (!packageJsonPath) { throw new StripesBuildError(`StripesModuleParser: Unable to locate ${dep}'s package.json (dependency of ${config.module})`); } - const packageJson = require(packageJsonPath); const resolvedPath = packageJsonPath.replace('/package.json', ''); unsortedStripesDeps[dep] = appendOrSingleton(unsortedStripesDeps[dep], { From 97ba1557e4597b3c355823315397426b31fdbb93 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Thu, 27 Apr 2023 14:43:49 -0400 Subject: [PATCH 09/60] Add required version to shared singletons --- consts.js | 24 +++++++++++++----------- webpack/utils.js | 8 ++++++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/consts.js b/consts.js index 6a81051..d4bef6a 100644 --- a/consts.js +++ b/consts.js @@ -1,14 +1,16 @@ -const singletons = [ - '@folio/stripes', - '@folio/stripes-shared-context', - 'react', - 'react-dom', - 'react-router', - 'react-router-dom', - 'react-redux', - 'react-intl', - 'react-query', -]; +// TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129 +const singletons = { + '@folio/stripes': '^8.1.0', + '@folio/stripes-shared-context': '^1.0.0', + 'react': '^17.0.2', + 'react-dom': '^17.0.2', + 'react-intl': '^5.7.0', + 'react-redux': '^8.0.5', + 'react-router': '^5.2.0', + 'react-router-dom': '^5.2.0', + 'redux-observable': '^1.2.0', + 'rxjs': '^6.6.3' +}; module.exports = { singletons, diff --git a/webpack/utils.js b/webpack/utils.js index b274568..d106d65 100644 --- a/webpack/utils.js +++ b/webpack/utils.js @@ -15,8 +15,12 @@ const processExternals = (peerDeps) => { }; const processShared = (shared, options = {}) => { - return shared.reduce((acc, name) => { - acc[name] = options; + return Object.keys(shared).reduce((acc, name) => { + acc[name] = { + requiredVersion: shared[name], + ...options + }; + return acc; }, {}); }; From 8c47f565dadb3caec59511c52010854b34789e6c Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Mon, 1 May 2023 13:39:22 -0400 Subject: [PATCH 10/60] Start remotes automatically --- webpack.config.base.js | 2 +- webpack.config.cli.dev.js | 4 ++- webpack/federate.js | 3 +-- webpack/stripes-federation-plugin.js | 38 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 webpack/stripes-federation-plugin.js diff --git a/webpack.config.base.js b/webpack.config.base.js index 3889973..14d76f3 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -7,7 +7,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts'); const { ModuleFederationPlugin } = require('webpack').container; -const { generateStripesAlias, locatePackageJsonPath } = 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'); diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 912ddb6..fe4259b 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -9,6 +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'; @@ -55,7 +56,8 @@ const buildConfig = (stripesConfig) => { if (utils.isDevelopment) { devConfig.plugins = devConfig.plugins.concat([ new webpack.HotModuleReplacementPlugin(), - new ReactRefreshWebpackPlugin() + new ReactRefreshWebpackPlugin(), + new StripesFederationPlugin(stripesConfig) ]); } diff --git a/webpack/federate.js b/webpack/federate.js index 9aa62a2..f72bef1 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -5,7 +5,6 @@ const axios = require('axios'); const { snakeCase } = require('lodash'); const portfinder = require('portfinder'); -const applyWebpackOverrides = require('./apply-webpack-overrides'); const buildConfig = require('../webpack.config.federate.remote'); const { tryResolve } = require('./module-paths'); const logger = require('./logger')(); @@ -23,7 +22,7 @@ module.exports = async function federate(options = {}) { process.exit(); } - const port = await portfinder.getPortPromise(); + const port = options.port ?? await portfinder.getPortPromise(); const host = `http://localhost`; const url = `${host}:${port}/remoteEntry.js`; diff --git a/webpack/stripes-federation-plugin.js b/webpack/stripes-federation-plugin.js new file mode 100644 index 0000000..50fcf21 --- /dev/null +++ b/webpack/stripes-federation-plugin.js @@ -0,0 +1,38 @@ +// This webpack plugin wraps all other stripes webpack plugins to simplify inclusion within the webpack config +const spawn = require('child_process').spawn; +const path = require('path'); +const portfinder = require('portfinder'); + +const { locateStripesModule } = require('./module-paths'); + +portfinder.setBasePort(3002); + +module.exports = class StripesFederationPlugin { + constructor(stripesConfig) { + this.stripesConfig = stripesConfig; + } + + async startRemotes(modules) { + const ctx = process.cwd(); + + for (const moduleName in modules) { + const packageJsonPath = locateStripesModule(ctx, moduleName, {}, 'package.json'); + const basePath = path.dirname(packageJsonPath); + + portfinder.getPort((err, port) => { + const child = spawn(`yarn stripes federate --port ${port}`, { + cwd: basePath, + shell: true, + }); + + child.stdout.pipe(process.stdout); + }); + } + } + + apply() { + const { modules } = this.stripesConfig; + + this.startRemotes(modules); + } +}; From 84c01996da5c08e501f73bccfdad44002f5f940a Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 9 May 2023 20:13:19 -0400 Subject: [PATCH 11/60] Expose icons via public endpoint --- webpack.config.federate.remote.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index e82338a..f10a799 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -12,6 +12,7 @@ const buildConfig = (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'); const shared = processShared(singletons, { singleton: true }); const config = { @@ -28,10 +29,16 @@ const buildConfig = (metadata) => { headers: { 'Access-Control-Allow-Origin': '*', }, - static: { - directory: translationsPath, - publicPath: '/translations' - } + static: [ + { + directory: translationsPath, + publicPath: '/translations' + }, + { + directory: iconsPath, + publicPath: '/icons' + }, + ] }, module: { rules: [ From 54db9c937c00bbb66c5a31c831fb2385f588be97 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 16 May 2023 19:34:36 -0400 Subject: [PATCH 12/60] react-query provides a context, so must be a singleton --- consts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/consts.js b/consts.js index d4bef6a..8566160 100644 --- a/consts.js +++ b/consts.js @@ -5,6 +5,7 @@ const singletons = { 'react': '^17.0.2', 'react-dom': '^17.0.2', 'react-intl': '^5.7.0', + 'react-query': '^3.39.3', 'react-redux': '^8.0.5', 'react-router': '^5.2.0', 'react-router-dom': '^5.2.0', From ecaa7a63c6b2ee263bf0ca748ffa02adb981679d Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 8 Jun 2023 16:50:18 -0400 Subject: [PATCH 13/60] STCOR-726 map sounds directory for remote applications * map the `sounds` directory for remote applications, analogous to how translations and icons are served * provide `/code` to make the registry human-readable * catch and display startup errors in case humans make stupid coding mistakes and need help finding them --- webpack.config.federate.remote.js | 8 ++++++++ webpack/registryServer.js | 5 ++++- webpack/serve.js | 7 ++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index f10a799..afb8932 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -13,6 +13,10 @@ const buildConfig = (metadata) => { 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 = { @@ -38,6 +42,10 @@ const buildConfig = (metadata) => { directory: iconsPath, publicPath: '/icons' }, + { + directory: soundsPath, + publicPath: '/sounds' + }, ] }, module: { diff --git a/webpack/registryServer.js b/webpack/registryServer.js index edf2e69..b4e3fb4 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -20,9 +20,12 @@ const registryServer = { res.status(200).send(`Remote ${name} metadata updated`); }); - // return entire registry + // return entire registry for machines app.get('/registry', (_, res) => res.json(registry)); + // return entire registry for humans + app.get('/code', (_, res) => res.send(`
${JSON.stringify(registry, null, 2)}
`)); + app.delete('/registry', (req, res) => { const metadata = req.body; const { name } = metadata; diff --git a/webpack/serve.js b/webpack/serve.js index 29c0291..2afddf2 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -34,7 +34,12 @@ module.exports = function serve(stripesConfig, options) { serviceWorkerConfig.resolveLoader = { modules: ['node_modules', platformModulePath, coreModulePath] }; // stripes module registry - registryServer.start(); + try { + registryServer.start(); + } + catch (e) { + console.error(e) + } let config = buildConfig(stripesConfig); From 42cac160705fc322434105e233918953f755f396 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 4 Dec 2024 14:54:20 -0500 Subject: [PATCH 14/60] current versions --- consts.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/consts.js b/consts.js index 8566160..cd76b30 100644 --- a/consts.js +++ b/consts.js @@ -1,12 +1,12 @@ // TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129 const singletons = { - '@folio/stripes': '^8.1.0', + '@folio/stripes': '^9.3.0', '@folio/stripes-shared-context': '^1.0.0', - 'react': '^17.0.2', - 'react-dom': '^17.0.2', - 'react-intl': '^5.7.0', + 'react': '~18.2', + 'react-dom': '~18.2', + 'react-intl': '^6.8.0', 'react-query': '^3.39.3', - 'react-redux': '^8.0.5', + 'react-redux': '^8.1', 'react-router': '^5.2.0', 'react-router-dom': '^5.2.0', 'redux-observable': '^1.2.0', From 12613edc917b79e2acabd69a564df1ceaf65f666 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 4 Dec 2024 14:54:26 -0500 Subject: [PATCH 15/60] separate handling of stripes-components and application icons Icons in stripes-components are imported as components whereas those in applications are just resources, so we need to load them differently. --- webpack.config.federate.remote.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index afb8932..5ccab6a 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -92,14 +92,24 @@ const buildConfig = (metadata) => { filename: './img/[name].[contenthash].[ext]', }, }, + // { + // test: /\.svg$/, + // use: [{ + // loader: 'url-loader', + // options: { + // esModule: false, + // }, + // }] + // }, { test: /\.svg$/, - use: [{ - loader: 'url-loader', - options: { - esModule: false, - }, - }] + 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$/, From 626a7c86815bc1e83845c21c01a08736038a0f13 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 5 Nov 2025 14:41:30 -0600 Subject: [PATCH 16/60] provide registry url in stripes-config --- consts.js | 9 ++++++--- webpack/serve.js | 9 +++++++++ webpack/stripes-config-plugin.js | 22 ++++++++++++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/consts.js b/consts.js index cd76b30..2b72fb7 100644 --- a/consts.js +++ b/consts.js @@ -2,9 +2,9 @@ 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': '~18.3', + 'react-dom': '~18.3', + 'react-intl': '^7.1.14', 'react-query': '^3.39.3', 'react-redux': '^8.1', 'react-router': '^5.2.0', @@ -13,6 +13,9 @@ const singletons = { 'rxjs': '^6.6.3' }; +const defaultRegistryUrl = 'http://localhost:3001/registry'; + module.exports = { + defaultRegistryUrl, singletons, }; diff --git a/webpack/serve.js b/webpack/serve.js index 87ec589..2d64af8 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -25,6 +25,15 @@ module.exports = function serve(stripesConfig, options) { logger.log('starting serve...'); const app = express(); + + // stripes module registry + try { + registryServer.start(); + } + catch (e) { + console.error(e) + } + let config = buildConfig(stripesConfig); config = sharedStylesConfig(config, {}); diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index d1ef5a0..1bc9f94 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -10,8 +10,10 @@ const _ = require('lodash'); const VirtualModulesPlugin = require('webpack-virtual-modules'); const serialize = require('serialize-javascript'); const { SyncHook } = require('tapable'); +const stripesModuleParser = require('./stripes-module-parser'); const StripesBuildError = require('./stripes-build-error'); const stripesSerialize = require('./stripes-serialize'); +const { defaultRegistryUrl } = require('../consts'); const logger = require('./logger')('stripesConfigPlugin'); const stripesConfigPluginHooksMap = new WeakMap(); @@ -20,7 +22,9 @@ module.exports = class StripesConfigPlugin { // options is actually stripes.config.js constructor(options, lazy) { logger.log('initializing...'); - + if (!_.isObject(options.modules)) { + throw new StripesBuildError('stripes-config-plugin was not provided a "modules" object for enabling stripes modules'); + } this.options = _.omit(options, 'branding', 'errorLogging'); this.lazy = lazy; } @@ -44,7 +48,14 @@ module.exports = class StripesConfigPlugin { const enabledModules = this.options.modules; logger.log('enabled modules:', enabledModules); const { config, metadata, icons, stripesDeps, warnings } = stripesModuleParser.parseAllModules(enabledModules, compiler.context, compiler.options.resolve.alias, this.lazy); - this.mergedConfig = Object.assign({}, this.options, { modules: config }); + const modulesInitialState = { + app: [], + handler: [], + plugin: [], + settings: [], + }; + this.mergedOkapi = Object.assign({ registryUrl: defaultRegistryUrl }, this.options.okapi); + this.mergedConfig = Object.assign({ modules: modulesInitialState }, this.options, { modules: config, okapi: this.mergedOkapi }); this.metadata = metadata; this.icons = icons; this.warnings = warnings; @@ -71,10 +82,13 @@ module.exports = class StripesConfigPlugin { // Create a virtual module for Webpack to include in the build const stripesVirtualModule = ` - const { okapi, config } = ${serialize(this.config, { space: 2 })}; + const { okapi, config, modules } = ${serialize(this.mergedConfig, { space: 2 })}; + const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)}; const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)}; const translations = ${serialize(pluginData.translations, { space: 2 })}; - export { okapi, config, branding, translations }; + const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; + const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; + export { branding, config, errorLogging, icons, metadata, modules, okapi, translations }; `; logger.log('writing virtual module...', stripesVirtualModule); From 08068edc1c9b922183b8544de410c4f88ee17079 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 18 Nov 2025 13:59:25 -0600 Subject: [PATCH 17/60] collect translations from 'StripesDeps' modules in built translations --- webpack.config.federate.remote.js | 9 ++- webpack/stripes-translations-plugin.js | 83 +++++++++++++++++++------- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 5ccab6a..db97085 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -1,6 +1,7 @@ const path = require('path'); const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const StripesTranslationsPlugin = require('./webpack/stripes-translations-plugin'); const { container } = webpack; const { processExternals, processShared } = require('./webpack/utils'); const { getStripesModulesPaths } = require('./webpack/module-paths'); @@ -8,8 +9,10 @@ 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 { host, port, name, displayName, main } = metadata; + + // using main from metadata since the location of main could vary between modules. + let mainEntry = path.join(process.cwd(), main || 'index.js'); const stripesModulePaths = getStripesModulesPaths(); const translationsPath = path.join(process.cwd(), 'translations', displayName.split('.').shift()); const iconsPath = path.join(process.cwd(), 'icons'); @@ -17,6 +20,7 @@ const buildConfig = (metadata) => { // 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 = { @@ -121,6 +125,7 @@ const buildConfig = (metadata) => { // TODO: remove this after stripes-config is gone. externals: processExternals({ 'stripes-config': true }), plugins: [ + new StripesTranslationsPlugin({ federate: true }), new MiniCssExtractPlugin({ filename: 'style.css', ignoreOrder: false }), new container.ModuleFederationPlugin({ library: { type: 'var', name }, diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js index 97fd973..2d04d21 100644 --- a/webpack/stripes-translations-plugin.js +++ b/webpack/stripes-translations-plugin.js @@ -16,23 +16,30 @@ function prefixKeys(obj, prefix) { module.exports = class StripesTranslationPlugin { constructor(options) { + // in module federation mode, we emit translations for the module being built and + // for any stripesDeps it has. + this.federate = options?.federate || false; + // Include stripes-core et al because they have translations - this.modules = { + // translations should come from the host application for stripes + // rather than being overwritten by consuming apps. + this.modules = this.federate ? {} : { '@folio/stripes-core': {}, '@folio/stripes-components': {}, '@folio/stripes-smart-components': {}, '@folio/stripes-form': {}, '@folio/stripes-ui': {}, }; - Object.assign(this.modules, options.modules); - this.languageFilter = options.config.languages || []; + + Object.assign(this.modules, options?.modules); + this.languageFilter = options?.config?.languages || []; logger.log('language filter', this.languageFilter); } apply(compiler) { // Used to help locate modules this.context = compiler.context; - // 'publicPath' is not present when running tests via karma-webpack + // 'publicPath' is not present when running tests via karma-webpack // so when running in test mode use absolute 'path'. this.publicPath = process.env.NODE_ENV !== 'test' ? compiler.options.output.publicPath : `./absolute${compiler.options.output.path}`; this.aliases = compiler.options.resolve.alias; @@ -44,21 +51,30 @@ module.exports = class StripesTranslationPlugin { new webpack.ContextReplacementPlugin(/moment[/\\]locale/, filterRegex).apply(compiler); } - // Hook into stripesConfigPlugin to supply paths to translation files - // and gather additional modules from stripes.stripesDeps - StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap({ name: 'StripesTranslationsPlugin', context: true }, (context, config) => { - // Gather all translations available in each module - const allTranslations = this.gatherAllTranslations(); + if (this.federate) { + const packageJsonPath = path.join(this.context, 'package.json'); + const packageJson = StripesTranslationPlugin.loadFile(packageJsonPath); + + this.modules[packageJson.name] = {}; + if (packageJson) { + const stripesDeps = packageJson?.stripes?.stripesDeps; + if (stripesDeps) { + stripesDeps.forEach((dep) => { + // TODO: merge translations from all versions of stripesDeps + this.modules[dep] = {}; + }); + } + } - const fileData = this.generateFileNames(allTranslations); - const allFiles = _.mapValues(fileData, data => data.browserPath); + compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => { + compilation.hooks.processAssets.tap({ + name: 'StripesTranslationsPlugin', + stage: compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS + }, () => { - config.translations = allFiles; - logger.log('stripesConfigPluginBeforeWrite', config.translations); + const allTranslations = this.gatherAllTranslations(); + const fileData = this.generateFileNames(allTranslations, false); - compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => { - // Emit merged translations to the output directory - compilation.hooks.processAssets.tap('StripesTranslationsPlugin', () => { Object.keys(allTranslations).forEach((language) => { logger.log(`emitting translations for ${language} --> ${fileData[language].emitPath}`); const content = JSON.stringify(allTranslations[language]); @@ -69,7 +85,34 @@ module.exports = class StripesTranslationPlugin { }); }); }); - }); + } else { + // Hook into stripesConfigPlugin to supply paths to translation files + // and gather additional modules from stripes.stripesDeps + StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap({ name: 'StripesTranslationsPlugin', context: true }, (context, config) => { + // Gather all translations available in each module + const allTranslations = this.gatherAllTranslations(); + + const fileData = this.generateFileNames(allTranslations); + const allFiles = _.mapValues(fileData, data => data.browserPath); + + config.translations = allFiles; + logger.log('stripesConfigPluginBeforeWrite', config.translations); + + compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => { + // Emit merged translations to the output directory + compilation.hooks.processAssets.tap('StripesTranslationsPlugin', () => { + Object.keys(allTranslations).forEach((language) => { + logger.log(`emitting translations for ${language} --> ${fileData[language].emitPath}`); + const content = JSON.stringify(allTranslations[language]); + compilation.assets[fileData[language].emitPath] = { + source: () => content, + size: () => content.length, + }; + }); + }); + }); + }); + } } // Locate each module's translations directory (current) or package.json data (fallback) @@ -170,14 +213,14 @@ module.exports = class StripesTranslationPlugin { } // Assign output path names for each to be accessed later by stripes-config-plugin - generateFileNames(allTranslations) { + generateFileNames(allTranslations, useSuffix = true) { const files = {}; - const timestamp = Date.now(); // To facilitate cache busting, could also generate a hash + const timestamp = useSuffix ? Date.now() : ''; // To facilitate cache busting, could also generate a hash Object.keys(allTranslations).forEach((language) => { files[language] = { // Fetching from the browser must take into account public path. The replace regex removes double slashes browserPath: `${this.publicPath}/translations/${language}-${timestamp}.json`.replace(/\/\//, '/'), - emitPath: `translations/${language}-${timestamp}.json`, + emitPath: `translations/${language}${timestamp ? `-${timestamp}` : ''}.json`, }; }); return files; From 8889e08874276557edefb7127149c246e1bc74cc Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 18 Nov 2025 14:08:02 -0600 Subject: [PATCH 18/60] get the app's main entry from package.json --- webpack/federate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack/federate.js b/webpack/federate.js index f72bef1..7380f8f 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -26,7 +26,7 @@ module.exports = async function federate(options = {}) { const host = `http://localhost`; const url = `${host}:${port}/remoteEntry.js`; - const { name: packageName, version, description, stripes } = require(packageJsonPath); + const { name: packageName, version, description, stripes, main } = require(packageJsonPath); const { permissionSets: _, ...stripesRest } = stripes; const name = snakeCase(packageName); const metadata = { @@ -37,6 +37,7 @@ module.exports = async function federate(options = {}) { port, url, name, + main, ...stripesRest, }; From a8b5fb0214d5e29ce426fca15ebaf80becf2a123 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 18 Nov 2025 14:09:19 -0600 Subject: [PATCH 19/60] ensure there are aliases before member access --- webpack/module-paths.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/module-paths.js b/webpack/module-paths.js index 9989090..de1ce03 100644 --- a/webpack/module-paths.js +++ b/webpack/module-paths.js @@ -63,7 +63,7 @@ function locateStripesModule(context, moduleName, alias, ...segments) { } // When available, try for the alias first - if (alias[moduleName]) { + if (alias && alias[moduleName]) { tryPaths.unshift({ request: path.join(alias[moduleName], ...segments), }); From fddfec2c37fd8c37081292730107d12d948fde3c Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 18 Nov 2025 14:10:08 -0600 Subject: [PATCH 20/60] expand the singletons for the platform --- consts.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/consts.js b/consts.js index 2b72fb7..1db7962 100644 --- a/consts.js +++ b/consts.js @@ -1,7 +1,15 @@ // TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129 +// Anythign that we want *the platform to provide to modules should be here. +// If an item is not in this list, modules will each load their own version of it. +// This can be problematic for React Context if mutliple copies of the same context are loaded. + const singletons = { '@folio/stripes': '^9.3.0', + '@folio/stripes-components': '^13.1.0', + '@folio/stripes-connect': '^10.0.1', + '@folio/stripes-core': '^11.1.0', '@folio/stripes-shared-context': '^1.0.0', + "moment": "^2.29.0", 'react': '~18.3', 'react-dom': '~18.3', 'react-intl': '^7.1.14', From 52d545f3f37d3155b619711f06eb6c16f8c00236 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 5 Dec 2025 10:47:38 -0600 Subject: [PATCH 21/60] make module-federation opt-in via stripes-config --- consts.js | 4 ++-- webpack.config.base.js | 11 +++-------- webpack.config.cli.dev.js | 15 +++++++++++++-- webpack.config.cli.prod.js | 12 ++++++++++++ webpack/federate.js | 12 ++++++------ webpack/registryServer.js | 5 +++-- webpack/serve.js | 17 ++++++++++++----- webpack/stripes-config-plugin.js | 5 ++--- ...in.js => stripes-local-federation-plugin.js} | 12 ++++++++---- 9 files changed, 61 insertions(+), 32 deletions(-) rename webpack/{stripes-federation-plugin.js => stripes-local-federation-plugin.js} (62%) diff --git a/consts.js b/consts.js index 1db7962..6c3ff86 100644 --- a/consts.js +++ b/consts.js @@ -21,9 +21,9 @@ const singletons = { 'rxjs': '^6.6.3' }; -const defaultRegistryUrl = 'http://localhost:3001/registry'; +const defaultentitlementUrl = 'http://localhost:3001/registry'; module.exports = { - defaultRegistryUrl, + defaultentitlementUrl, singletons, }; diff --git a/webpack.config.base.js b/webpack.config.base.js index 14d76f3..1512aa7 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -5,16 +5,12 @@ 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 @@ -70,7 +66,6 @@ const baseConfig = { }), new webpack.EnvironmentPlugin(['NODE_ENV']), new RemoveEmptyScriptsPlugin(), - new ModuleFederationPlugin({ name: 'host', shared }), ], module: { rules: [ @@ -137,7 +132,7 @@ const baseConfig = { }, }; -const buildConfig = (modulePaths) => { +const buildConfig = (modulePaths, federatePlatform = false) => { const transpiledCssPaths = getTranspiledCssPaths(modulePaths); const cssDistPathRegex = /dist[\/\\]style\.css/; @@ -167,7 +162,7 @@ const buildConfig = (modulePaths) => { test: /\.css$/, exclude: [cssDistPathRegex], use: [ - { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, + { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, { loader: 'css-loader', options: { diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 4c42e83..56dba2d 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -9,7 +9,10 @@ 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 StripesLocalFederationPlugin = require('./webpack/stripes-local-federation-plugin'); +const { singletons } = require('./consts'); +const { ModuleFederationPlugin } = require('webpack').container; +const { processShared } = require('./webpack/utils'); const useBrowserMocha = () => { return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; @@ -57,10 +60,18 @@ const buildConfig = (stripesConfig) => { devConfig.plugins = devConfig.plugins.concat([ new webpack.HotModuleReplacementPlugin(), new ReactRefreshWebpackPlugin(), - new StripesFederationPlugin(stripesConfig) ]); } + // Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules. + if (stripesConfig.okapi.entitlementUrl) { + // for development, use local federation plugin that will start multiple based on the modules list from stripes.config.js, incrementing ports 3002, 3003, etc... + devConfig.plugins.push(new StripesLocalFederationPlugin(stripesConfig)) + + const shared = processShared(singletons, { singleton: true, eager: true }); + devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared })); + } + // This alias avoids a console warning for react-dom patch devConfig.resolve.alias.process = 'process/browser.js'; devConfig.resolve.alias['mocha'] = useBrowserMocha(); diff --git a/webpack.config.cli.prod.js b/webpack.config.cli.prod.js index c558aed..bd265ef 100644 --- a/webpack.config.cli.prod.js +++ b/webpack.config.cli.prod.js @@ -9,6 +9,10 @@ const buildBaseConfig = require('./webpack.config.base'); 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 { singletons } = require('./consts'); + const buildConfig = (stripesConfig, options = {}) => { const modulePaths = getModulesPaths(stripesConfig.modules); @@ -54,6 +58,14 @@ const buildConfig = (stripesConfig, options = {}) => { }), ]); + // build platform with Module Federation if entitlementUrl is provided + if (stripesConfig.okapi.entitlementUrl) { + const shared = processShared(singletons, { singleton: true, eager: true }); + prodConfig.plugins.push( + new ModuleFederationPlugin({ name: 'host', shared }) + ); + } + prodConfig.optimization = { mangleWasmImports: false, minimizer: [ diff --git a/webpack/federate.js b/webpack/federate.js index 7380f8f..194939f 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -43,12 +43,12 @@ module.exports = async function federate(options = {}) { const config = buildConfig(metadata); - // TODO: allow for configuring registryUrl via env var or stripes config - const registryUrl = 'http://localhost:3001/registry'; + // TODO: allow for configuring entitlementUrl via env var or stripes config + const entitlementUrl = 'http://localhost:3001/registry'; // update registry - axios.post(registryUrl, metadata).catch(error => { - console.error(`Registry not found. Please check ${registryUrl}`); + axios.post(entitlementUrl, metadata).catch(error => { + console.error(`Registry not found. Please check ${entitlementUrl}`); process.exit(); }); @@ -59,9 +59,9 @@ module.exports = async function federate(options = {}) { compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { try { - await axios.delete(registryUrl, { data: metadata }); + await axios.delete(entitlementUrl, { data: metadata }); } catch (error) { - console.error(`registry not found. Please check ${registryUrl}`); + console.error(`registry not found. Please check ${entitlementUrl}`); } }); }; diff --git a/webpack/registryServer.js b/webpack/registryServer.js index b4e3fb4..8339bfd 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -5,7 +5,7 @@ const cors = require('cors'); const registry = { remotes: {} }; const registryServer = { - start: () => { + start: (url) => { const app = express(); app.use(express.json()); @@ -35,7 +35,8 @@ const registryServer = { res.status(200).send(`Remote ${name} removed`); }); - app.listen(3001, () => { + const port = new URL(url).port || 3001; + app.listen(port, () => { console.log('Starting registry server at http://localhost:3001'); }); } diff --git a/webpack/serve.js b/webpack/serve.js index 2d64af8..0d842f1 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -27,11 +27,18 @@ module.exports = function serve(stripesConfig, options) { // stripes module registry - try { - registryServer.start(); - } - catch (e) { - console.error(e) + if (stripesConfig.okapi.entitlementUrl) { + const { entitlementUrl } = stripesConfig.okapi; + + // Start the local registry server if the entitlementUrl is not an absolute URL ex localhost:3001/registry + if (entitlementUrl.includes('localhost')) { + try { + registryServer.start(entitlementUrl); + } + catch (e) { + console.error(e) + } + } } let config = buildConfig(stripesConfig); diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index d5579ed..5fb4c65 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -13,7 +13,7 @@ const { SyncHook } = require('tapable'); const stripesModuleParser = require('./stripes-module-parser'); const StripesBuildError = require('./stripes-build-error'); const stripesSerialize = require('./stripes-serialize'); -const { defaultRegistryUrl } = require('../consts'); +const { defaultentitlementUrl } = require('../consts'); const logger = require('./logger')('stripesConfigPlugin'); const stripesConfigPluginHooksMap = new WeakMap(); @@ -54,8 +54,7 @@ module.exports = class StripesConfigPlugin { plugin: [], settings: [], }; - this.mergedOkapi = Object.assign({ registryUrl: defaultRegistryUrl }, this.options.okapi); - this.mergedConfig = Object.assign({ modules: modulesInitialState }, this.options, { modules: config, okapi: this.mergedOkapi }); + this.mergedConfig = Object.assign({ modules: modulesInitialState }, this.options, { modules: config }); this.metadata = metadata; this.icons = icons; this.warnings = warnings; diff --git a/webpack/stripes-federation-plugin.js b/webpack/stripes-local-federation-plugin.js similarity index 62% rename from webpack/stripes-federation-plugin.js rename to webpack/stripes-local-federation-plugin.js index 50fcf21..f7eb319 100644 --- a/webpack/stripes-federation-plugin.js +++ b/webpack/stripes-local-federation-plugin.js @@ -1,4 +1,8 @@ -// This webpack plugin wraps all other stripes webpack plugins to simplify inclusion within the webpack config +// This plugin is used for local development/debugging related to Module Federation of ui-modules. +// it spawns child processes to start remote ui-modules defined in stripes.config.js if those modules are included +// within the same workspace. If the module is not within the same workspace, the 'stripes federate' command will have to be +// executed manually from the directory of that module. + const spawn = require('child_process').spawn; const path = require('path'); const portfinder = require('portfinder'); @@ -7,7 +11,7 @@ const { locateStripesModule } = require('./module-paths'); portfinder.setBasePort(3002); -module.exports = class StripesFederationPlugin { +module.exports = class StripesLocalFederationPlugin { constructor(stripesConfig) { this.stripesConfig = stripesConfig; } @@ -21,8 +25,8 @@ module.exports = class StripesFederationPlugin { portfinder.getPort((err, port) => { const child = spawn(`yarn stripes federate --port ${port}`, { - cwd: basePath, - shell: true, + cwd: basePath, + shell: true, }); child.stdout.pipe(process.stdout); From 2c8dbae0de5a3dc4999757295ff47022c447c4c4 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 16 Dec 2025 15:44:08 -0600 Subject: [PATCH 22/60] sync up stripes-config-plugin --- webpack/stripes-config-plugin.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index 5fb4c65..7fb3252 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -65,16 +65,18 @@ module.exports = class StripesConfigPlugin { StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap( { name: 'StripesConfigPlugin', context: true }, - context => Object.assign(context, { config })); + context => Object.assign(context, { config, metadata, icons, stripesDeps, warnings })); // Wait until after other plugins to generate virtual stripes-config compiler.hooks.afterPlugins.tap('StripesConfigPlugin', (theCompiler) => this.afterPlugins(theCompiler)); + compiler.hooks.emit.tapAsync('StripesConfigPlugin', (compilation, callback) => this.processWarnings(compilation, callback)); } afterPlugins(compiler) { // Data provided by other stripes plugins via hooks const pluginData = { branding: {}, + errorLogging: {}, translations: {}, }; @@ -84,8 +86,8 @@ module.exports = class StripesConfigPlugin { const stripesVirtualModule = ` ${Array.from(this.lazyImports).join('\n')} const { okapi, config, modules } = ${serialize(this.mergedConfig, { space: 2 })}; - const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)}; const branding = ${stripesSerialize.serializeWithRequire(pluginData.branding)}; + const errorLogging = ${stripesSerialize.serializeWithRequire(pluginData.errorLogging)}; const translations = ${serialize(pluginData.translations, { space: 2 })}; const metadata = ${stripesSerialize.serializeWithRequire(this.metadata)}; const icons = ${stripesSerialize.serializeWithRequire(this.icons)}; @@ -95,4 +97,11 @@ module.exports = class StripesConfigPlugin { logger.log('writing virtual module...', stripesVirtualModule); this.virtualModule.writeModule('node_modules/stripes-config.js', stripesVirtualModule); } + + processWarnings(compilation, callback) { + if (this.warnings.length) { + compilation.warnings.push(new StripesBuildError(`stripes-config-plugin:\n ${this.warnings.join('\n ')}`)); + } + callback(); + } }; From c9152181446518f6a7e8218b2029fac51338412a Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 16 Dec 2025 16:05:33 -0600 Subject: [PATCH 23/60] tweak local federation commands --- webpack/stripes-local-federation-plugin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webpack/stripes-local-federation-plugin.js b/webpack/stripes-local-federation-plugin.js index f7eb319..f44e16f 100644 --- a/webpack/stripes-local-federation-plugin.js +++ b/webpack/stripes-local-federation-plugin.js @@ -3,7 +3,7 @@ // within the same workspace. If the module is not within the same workspace, the 'stripes federate' command will have to be // executed manually from the directory of that module. -const spawn = require('child_process').spawn; +const spawnSync = require('child_process').spawnSync; const path = require('path'); const portfinder = require('portfinder'); @@ -24,9 +24,9 @@ module.exports = class StripesLocalFederationPlugin { const basePath = path.dirname(packageJsonPath); portfinder.getPort((err, port) => { - const child = spawn(`yarn stripes federate --port ${port}`, { + const child = spawnSync(`yarn stripes federate --port ${port}`, { cwd: basePath, - shell: true, + shell: process.platform === 'win32', // true on windows, false elsewhere... }); child.stdout.pipe(process.stdout); From 233244e96c434e916a98832c4cf5095e8fa54873 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 16 Dec 2025 16:12:12 -0600 Subject: [PATCH 24/60] switch back to spawn vs spawnsync --- webpack/stripes-local-federation-plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack/stripes-local-federation-plugin.js b/webpack/stripes-local-federation-plugin.js index f44e16f..9032e4e 100644 --- a/webpack/stripes-local-federation-plugin.js +++ b/webpack/stripes-local-federation-plugin.js @@ -3,7 +3,7 @@ // within the same workspace. If the module is not within the same workspace, the 'stripes federate' command will have to be // executed manually from the directory of that module. -const spawnSync = require('child_process').spawnSync; +const spawn = require('child_process').spawn; const path = require('path'); const portfinder = require('portfinder'); @@ -24,7 +24,7 @@ module.exports = class StripesLocalFederationPlugin { const basePath = path.dirname(packageJsonPath); portfinder.getPort((err, port) => { - const child = spawnSync(`yarn stripes federate --port ${port}`, { + const child = spawn(`yarn stripes federate --port ${port}`, { cwd: basePath, shell: process.platform === 'win32', // true on windows, false elsewhere... }); From c7739b051a916fc096a91b26337c3af25d5a0c5c Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 10:02:45 -0600 Subject: [PATCH 25/60] update to current vm expected exports --- test/webpack/stripes-config-plugin.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webpack/stripes-config-plugin.spec.js b/test/webpack/stripes-config-plugin.spec.js index cb66626..95fdfdb 100644 --- a/test/webpack/stripes-config-plugin.spec.js +++ b/test/webpack/stripes-config-plugin.spec.js @@ -136,7 +136,7 @@ describe('The stripes-config-plugin', function () { expect(writeModuleArgs[0]).to.be.a('string').that.equals('node_modules/stripes-config.js'); // TODO: More thorough analysis of the generated virtual module - expect(writeModuleArgs[1]).to.be.a('string').with.match(/export { okapi, config, modules, branding, errorLogging, translations, metadata, icons }/); + expect(writeModuleArgs[1]).to.be.a('string').with.match(/export { branding, config, errorLogging, icons, metadata, modules, okapi, translations }/); }); }); From 89c80cb5e464793ed6b072596dc3942f2cf166ff Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 10:24:15 -0600 Subject: [PATCH 26/60] put back inclusion of stripes-deps in the main stripes-translation plugin, StripesConfig logic --- webpack/stripes-translations-plugin.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js index 2d04d21..a5a91dd 100644 --- a/webpack/stripes-translations-plugin.js +++ b/webpack/stripes-translations-plugin.js @@ -89,6 +89,12 @@ module.exports = class StripesTranslationPlugin { // Hook into stripesConfigPlugin to supply paths to translation files // and gather additional modules from stripes.stripesDeps StripesConfigPlugin.getPluginHooks(compiler).beforeWrite.tap({ name: 'StripesTranslationsPlugin', context: true }, (context, config) => { + // Add stripesDeps to the list of modules to load translations from + for (const [key, value] of Object.entries(context.stripesDeps)) { + // TODO: merge translations from all versions of stripesDeps + this.modules[key] = value[value.length - 1]; + } + // Gather all translations available in each module const allTranslations = this.gatherAllTranslations(); From a3b01e5f6d3eae37dd71cccc5860ae136029c9e0 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 11:14:38 -0600 Subject: [PATCH 27/60] add tests for StripesTranslationPlugin federate mode --- .../stripes-translations-plugin.spec.js | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/test/webpack/stripes-translations-plugin.spec.js b/test/webpack/stripes-translations-plugin.spec.js index b13c230..ebaf834 100644 --- a/test/webpack/stripes-translations-plugin.spec.js +++ b/test/webpack/stripes-translations-plugin.spec.js @@ -9,8 +9,8 @@ const StripesTranslationsPlugin = require('../../webpack/stripes-translations-pl // Stub the parts of the webpack compiler that the StripesTranslationsPlugin interacts with const compilerStub = { - apply: () => {}, - plugin: () => {}, + apply: () => { }, + plugin: () => { }, options: { output: { publicPath: '/', @@ -21,22 +21,22 @@ const compilerStub = { }, hooks: { beforeWrite: { - tap: () => {}, + tap: () => { }, }, emit: { - tapAsync: () => {}, + tapAsync: () => { }, }, processAssets: { - tap: () => {}, + tap: () => { }, }, thisCompilation: { - tap: () => {}, + tap: () => { }, }, contextModuleFactory: { - tap: () => {}, + tap: () => { }, }, afterResolve: { - tap: () => {}, + tap: () => { }, } } }; @@ -52,6 +52,8 @@ describe('The stripes-translations-plugin', function () { '@folio/checkout': {}, }, }; + + this.stripesFederateConfig = { ...this.stripesConfig, federate: true }; }); describe('constructor', function () { @@ -89,12 +91,12 @@ describe('The stripes-translations-plugin', function () { this.sandbox.spy(webpack.ContextReplacementPlugin.prototype, 'apply'); this.sandbox.spy(compilerStub.hooks.emit, 'tapAsync'); this.sandbox.spy(compilerStub.hooks.thisCompilation, 'tap'); - this.sandbox.stub(StripesTranslationsPlugin, 'loadFile').returns({ key1: 'Value 1', key2: 'Value 2' }); + this.sandbox.stub(StripesTranslationsPlugin, 'loadFile').returns({ key1: 'Value 1', key2: 'Value 2', name: 'testPackage', stripes: { stripesDeps: ['stripes-federate-dependency'] } }); this.compilationStub = { assets: {}, hooks: { processAssets: { - tap: () => {} + tap: () => { } }, }, }; @@ -131,6 +133,15 @@ describe('The stripes-translations-plugin', function () { expect(this.sut.modules).to.be.an('object').with.property('stripes-dependency'); }); + it('includes certain modules and stripes-deps in "federate" mpode', function () { + // federate mode is per-module, so the plugin executes outside of StripesConfigPlugin, with its own hook. + this.sut = new StripesTranslationsPlugin(this.stripesFederateConfig); + this.sut.apply({ ...compilerStub, context: __dirname }); + + expect(this.sut.modules).to.be.an('object').with.property('testPackage'); + expect(this.sut.modules).to.be.an('object').with.property('stripes-federate-dependency'); + }); + it('generates an emit function with all translations', function () { this.sut = new StripesTranslationsPlugin(this.stripesConfig); this.sut.apply(compilerStub); @@ -156,6 +167,30 @@ describe('The stripes-translations-plugin', function () { expect(emitFiles).to.match(/translations\/fr-\d+\.json/); }); + it('generates an emit function with all translations (federate mode)', function () { + this.sut = new StripesTranslationsPlugin(this.stripesFederateConfig); + this.sut.apply({ ...compilerStub, context: __dirname }); + + // Get the callback passed to 'thisCompilation' hook + const pluginArgs = compilerStub.hooks.thisCompilation.tap.getCall(0).args; + const compilerCallback = pluginArgs[1]; + + compilerCallback(this.compilationStub); + + const compilationArgs = this.compilationStub.hooks.processAssets.tap.getCall(0).args; + const compilationCallback = compilationArgs[1]; + + // Call it and observe the modification to compilation.asset + compilationCallback(); + + const emitFiles = Object.keys(this.compilationStub.assets); + + expect(emitFiles).to.have.length(3); + expect(emitFiles).to.match(/translations\/en-\d+\.json/); + expect(emitFiles).to.match(/translations\/es-\d+\.json/); + expect(emitFiles).to.match(/translations\/fr-\d+\.json/); + }); + it('applies ContextReplacementPlugins when language filters are set', function () { this.sut = new StripesTranslationsPlugin(this.stripesConfig); this.sut.languageFilter = ['en']; From 9639baae36ed863de9a326ed621dfd3eaa1dc772 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 14:35:57 -0600 Subject: [PATCH 28/60] use base webpack config from main branch --- webpack.config.base.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webpack.config.base.js b/webpack.config.base.js index 1512aa7..0be1037 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -6,8 +6,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts'); -const { generateStripesAlias, } = require('./webpack/module-paths'); -const { processShared } = require('./webpack/utils'); +const { generateStripesAlias } = require('./webpack/module-paths'); const typescriptLoaderRule = require('./webpack/typescript-loader-rule'); const { isProduction } = require('./webpack/utils'); const { getTranspiledCssPaths } = require('./webpack/module-paths'); @@ -132,7 +131,8 @@ const baseConfig = { }, }; -const buildConfig = (modulePaths, federatePlatform = false) => { + +const buildConfig = (modulePaths) => { const transpiledCssPaths = getTranspiledCssPaths(modulePaths); const cssDistPathRegex = /dist[\/\\]style\.css/; @@ -162,7 +162,7 @@ const buildConfig = (modulePaths, federatePlatform = false) => { test: /\.css$/, exclude: [cssDistPathRegex], use: [ - { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, + { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, { loader: 'css-loader', options: { From 2018a557aae0e321906b5a58963e8f02094d4cc7 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 14:44:46 -0600 Subject: [PATCH 29/60] add comment to top of federate remote webpack config --- webpack.config.federate.remote.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index db97085..6428a52 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -1,3 +1,7 @@ +// This configuration file is used for building individual ui modules for a +// federated module platform setup. +// note the static hosted folders "icons", "translations", "sounds" in the devserver config. + const path = require('path'); const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); From 0ef94389c9e4bfe95f0df9d2cc4632a67dfedd6f Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 15:01:55 -0600 Subject: [PATCH 30/60] log changes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a12a496..c7b3b55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Unlock `esbuild-loader` from `~3.0.0`, bumping to `^4.2.2`. Refs STRWEB-95. * Prune dead code, `stripes.js` and its dep `commander`. Refs STRWEB-134. * Provide `getDynamicModule`, returning a module via `import()`. Refs STRWEB-137. +* Add `StripesLocalFederation` plugin, inject module federation plugin for federated platforms and modules. Refs STRIPES-861. ## [6.0.0](https://github.com/folio-org/stripes-webpack/tree/v6.0.0) (2025-02-24) [Full Changelog](https://github.com/folio-org/stripes-webpack/compare/v5.2.0...v6.0.0) From 17ff13474262fd71176a91b5cd101e139554dafe Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 17 Dec 2025 15:03:40 -0600 Subject: [PATCH 31/60] log more changes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b3b55..ed63fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Prune dead code, `stripes.js` and its dep `commander`. Refs STRWEB-134. * Provide `getDynamicModule`, returning a module via `import()`. Refs STRWEB-137. * Add `StripesLocalFederation` plugin, inject module federation plugin for federated platforms and modules. Refs STRIPES-861. +* Adjust `StripesTranslationsPlugin` for working at the module level and including translations from `stripesDeps`. Refs STRIPES-861. ## [6.0.0](https://github.com/folio-org/stripes-webpack/tree/v6.0.0) (2025-02-24) [Full Changelog](https://github.com/folio-org/stripes-webpack/compare/v5.2.0...v6.0.0) From eea2bb97d1c88e6a04a028a8902ae55aa1c9ebff Mon Sep 17 00:00:00 2001 From: John Coburn Date: Thu, 18 Dec 2025 16:13:28 -0600 Subject: [PATCH 32/60] add loader entry for sound file --- webpack.config.federate.remote.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 6428a52..98f4bc9 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -66,6 +66,13 @@ const buildConfig = (metadata) => { filename: './fonts/[name].[contenthash].[ext]', }, }, + { + test: /\.(mp3|m4a)$/, + type: 'asset/resource', + generator: { + filename: './sound/[name].[contenthash].[ext]', + }, + }, { test: /\.css$/, use: [ From 7cf2de00619d8a50a4fb20239273d2f885a0753d Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 22 Dec 2025 14:38:43 -0600 Subject: [PATCH 33/60] remove StripesLocalFederationPlugin --- webpack.config.cli.dev.js | 4 --- webpack/stripes-local-federation-plugin.js | 42 ---------------------- 2 files changed, 46 deletions(-) delete mode 100644 webpack/stripes-local-federation-plugin.js diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 56dba2d..1f68918 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -9,7 +9,6 @@ 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 StripesLocalFederationPlugin = require('./webpack/stripes-local-federation-plugin'); const { singletons } = require('./consts'); const { ModuleFederationPlugin } = require('webpack').container; const { processShared } = require('./webpack/utils'); @@ -65,9 +64,6 @@ const buildConfig = (stripesConfig) => { // Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules. if (stripesConfig.okapi.entitlementUrl) { - // for development, use local federation plugin that will start multiple based on the modules list from stripes.config.js, incrementing ports 3002, 3003, etc... - devConfig.plugins.push(new StripesLocalFederationPlugin(stripesConfig)) - const shared = processShared(singletons, { singleton: true, eager: true }); devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared })); } diff --git a/webpack/stripes-local-federation-plugin.js b/webpack/stripes-local-federation-plugin.js deleted file mode 100644 index 9032e4e..0000000 --- a/webpack/stripes-local-federation-plugin.js +++ /dev/null @@ -1,42 +0,0 @@ -// This plugin is used for local development/debugging related to Module Federation of ui-modules. -// it spawns child processes to start remote ui-modules defined in stripes.config.js if those modules are included -// within the same workspace. If the module is not within the same workspace, the 'stripes federate' command will have to be -// executed manually from the directory of that module. - -const spawn = require('child_process').spawn; -const path = require('path'); -const portfinder = require('portfinder'); - -const { locateStripesModule } = require('./module-paths'); - -portfinder.setBasePort(3002); - -module.exports = class StripesLocalFederationPlugin { - constructor(stripesConfig) { - this.stripesConfig = stripesConfig; - } - - async startRemotes(modules) { - const ctx = process.cwd(); - - for (const moduleName in modules) { - const packageJsonPath = locateStripesModule(ctx, moduleName, {}, 'package.json'); - const basePath = path.dirname(packageJsonPath); - - portfinder.getPort((err, port) => { - const child = spawn(`yarn stripes federate --port ${port}`, { - cwd: basePath, - shell: process.platform === 'win32', // true on windows, false elsewhere... - }); - - child.stdout.pipe(process.stdout); - }); - } - } - - apply() { - const { modules } = this.stripesConfig; - - this.startRemotes(modules); - } -}; From c9182a953da76606df08f6ea91d9d397674a1c18 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 22 Dec 2025 14:43:01 -0600 Subject: [PATCH 34/60] remove commented svg rules in federate config --- webpack.config.federate.remote.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 98f4bc9..26bd763 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -107,15 +107,6 @@ const buildConfig = (metadata) => { filename: './img/[name].[contenthash].[ext]', }, }, - // { - // test: /\.svg$/, - // use: [{ - // loader: 'url-loader', - // options: { - // esModule: false, - // }, - // }] - // }, { test: /\.svg$/, type: 'asset/inline', From 6d4e80a85201a5a6bcb7ad62e71ee3820937254b Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 22 Dec 2025 15:43:04 -0600 Subject: [PATCH 35/60] remove axios --- package.json | 1 - webpack/federate.js | 32 ++++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 945ca4b..bd4ad0c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@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", diff --git a/webpack/federate.js b/webpack/federate.js index 194939f..1fc1292 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -1,7 +1,6 @@ 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'); @@ -46,11 +45,25 @@ module.exports = async function federate(options = {}) { // TODO: allow for configuring entitlementUrl via env var or stripes config const entitlementUrl = 'http://localhost:3001/registry'; + const requestHeader = { + "Content-Type": "application/json", + }; + // update registry - axios.post(entitlementUrl, metadata).catch(error => { - console.error(`Registry not found. Please check ${entitlementUrl}`); - process.exit(); - }); + await fetch( + entitlementUrl, { + method: 'POST', + headers: requestHeader, + body: JSON.stringify(metadata), + }) + .catch(error => { + console.error(`Registry not found. Please check ${entitlementUrl}`); + process.exit(); + }); + // axios.post(entitlementUrl, metadata).catch(error => { + // console.error(`Registry not found. Please check ${entitlementUrl}`); + // process.exit(); + // }); const compiler = webpack(config); const server = new WebpackDevServer(config.devServer, compiler); @@ -59,7 +72,14 @@ module.exports = async function federate(options = {}) { compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { try { - await axios.delete(entitlementUrl, { data: metadata }); + await fetch(entitlementUrl, { + method: 'DELETE', + headers: requestHeader, + body: JSON.stringify(metadata), + }).catch(error => { + throw new Error(error); + }); + // await axios.delete(entitlementUrl, { data: metadata }); } catch (error) { console.error(`registry not found. Please check ${entitlementUrl}`); } From 55c989a37ab54b865b2110a7d60f37cee9713afd Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 22 Dec 2025 16:17:31 -0600 Subject: [PATCH 36/60] high level comments and mod-fed vs monolithic translation differences --- webpack/stripes-translations-plugin.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js index a5a91dd..d2a8e66 100644 --- a/webpack/stripes-translations-plugin.js +++ b/webpack/stripes-translations-plugin.js @@ -16,8 +16,6 @@ function prefixKeys(obj, prefix) { module.exports = class StripesTranslationPlugin { constructor(options) { - // in module federation mode, we emit translations for the module being built and - // for any stripesDeps it has. this.federate = options?.federate || false; // Include stripes-core et al because they have translations @@ -51,6 +49,13 @@ module.exports = class StripesTranslationPlugin { new webpack.ContextReplacementPlugin(/moment[/\\]locale/, filterRegex).apply(compiler); } + // In module federation mode, we emit translations for the module being built and + // for any stripesDeps it has. Since the translations for 'stripes-core', components, form, etc are + // provided by a host application, we do not include them here. + // Translations are loaded at runtime from the built static 'translations' directory when the remote itself is loaded. + // In a monolithic build, StripesTranslationsPlugin is included in StripesConfigPlugin as + // its list of generated files is passed to the `stripes-config` virtual module as its `translations` object. + // In the monolithic build, the `stripes-config` virtual module's file path information is used to load translations. if (this.federate) { const packageJsonPath = path.join(this.context, 'package.json'); const packageJson = StripesTranslationPlugin.loadFile(packageJsonPath); @@ -66,6 +71,8 @@ module.exports = class StripesTranslationPlugin { } } + // for usage at the module level, this plugin is used independently of the `StripesConfigPlugin`, so we + // register hooks under itself rather then the config plugin. compiler.hooks.thisCompilation.tap('StripesTranslationsPlugin', (compilation) => { compilation.hooks.processAssets.tap({ name: 'StripesTranslationsPlugin', From cef8363a2c655f005e62b66e5a5bcfdf6a9b1764 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 23 Dec 2025 10:52:10 -0600 Subject: [PATCH 37/60] dynamically fetch versions for platform singletons --- consts.js | 58 ++++++++++++++++++++++++++++++- package.json | 1 + webpack.config.cli.dev.js | 4 +-- webpack.config.federate.remote.js | 7 ++-- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/consts.js b/consts.js index 6c3ff86..a67ecaa 100644 --- a/consts.js +++ b/consts.js @@ -1,4 +1,5 @@ -// TODO: should these come from https://github.com/folio-org/stripes-core/blob/1d5d4f00a3756702e828856d4ef9349ceb9f1c08/package.json#L116-L129 +const { Octokit } = require('@octokit/rest'); + // Anythign that we want *the platform to provide to modules should be here. // If an item is not in this list, modules will each load their own version of it. // This can be problematic for React Context if mutliple copies of the same context are loaded. @@ -21,9 +22,64 @@ const singletons = { 'rxjs': '^6.6.3' }; +/** getPlatformSingletons +* get singletons from platform deps +* TODO - specify versions/branches of platform/stripes and additional entries... +*/ +const getPlatformSingletons = async () => { + const platformSingletons = {}; + const octokit = new Octokit(); + + try { + const platformPkg = await octokit.request('GET /repos/folio-org/platform-complete/contents/package.json', { + headers: { + accept: 'application/vnd.github.raw+json' + } + }); + + if (platformPkg.status === 200) { + const pkgObject = JSON.parse(platformPkg.data); + Object.keys(singletons).forEach(dep => { + const depVersion = pkgObject.dependencies[dep]; + if (depVersion) { + platformSingletons[dep] = depVersion; + } + }); + } else { + throw new Error('Error retrieving singletons list from platform. Falling back to static list'); + } + + // fetch dep versions from stripes... + const stripesPkg = await octokit.request('GET /repos/folio-org/stripes/contents/package.json', { + headers: { + accept: 'application/vnd.github.raw+json' + } + }); + + if (stripesPkg.status === 200) { + const pkgObject = JSON.parse(stripesPkg.data); + Object.keys(singletons).forEach(dep => { + const depVersion = pkgObject.dependencies[dep]; + if (depVersion) { + platformSingletons[dep] = depVersion; + } + }); + } else { + throw new Error('Error retrieving singletons list from stripes version. Falling back to static list'); + } + + return platformSingletons; + } catch (e) { + console.log(e); + return singletons; + } +} + + const defaultentitlementUrl = 'http://localhost:3001/registry'; module.exports = { defaultentitlementUrl, singletons, + getPlatformSingletons }; diff --git a/package.json b/package.json index bd4ad0c..acd7461 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@csstools/postcss-global-data": "^3.0.0", "@csstools/postcss-relative-color-syntax": "^3.0.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", + "@octokit/rest": "^19.0.7", "@svgr/webpack": "^8.1.0", "add-asset-html-webpack-plugin": "^6.0.0", "autoprefixer": "^10.4.13", diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 1f68918..ac81396 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -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 { singletons } = require('./consts'); +const { getPlatformSingletons } = require('./consts'); const { ModuleFederationPlugin } = require('webpack').container; const { processShared } = require('./webpack/utils'); @@ -64,7 +64,7 @@ const buildConfig = (stripesConfig) => { // Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules. if (stripesConfig.okapi.entitlementUrl) { - const shared = processShared(singletons, { singleton: true, eager: true }); + const shared = processShared(getPlatformSingletons, { singleton: true, eager: true }); devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared })); } diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 26bd763..198504d 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -10,9 +10,9 @@ 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 { getPlatformSingletons } = require('./consts'); -const buildConfig = (metadata) => { +const buildConfig = async (metadata) => { const { host, port, name, displayName, main } = metadata; // using main from metadata since the location of main could vary between modules. @@ -25,7 +25,8 @@ const buildConfig = (metadata) => { // 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 configSingletons = await getPlatformSingletons(); + const shared = processShared(configSingletons, { singleton: true }); const config = { name, From 84dd206dd349be45ce842470f46649c9a7fb3bb8 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 14 Jan 2026 14:24:33 -0600 Subject: [PATCH 38/60] configure for module-federation with --federate flag on build and serve commands --- consts.js | 75 +++++++++++++------------------ package.json | 5 ++- webpack.config.base.js | 3 +- webpack.config.cli.dev.js | 7 +-- webpack.config.federate.remote.js | 75 ++++++++++++++++++++----------- webpack/build.js | 35 ++++++++++----- webpack/federate.js | 32 +++++++------ webpack/serve.js | 21 ++++++--- webpack/utils.js | 15 +++++++ 9 files changed, 161 insertions(+), 107 deletions(-) diff --git a/consts.js b/consts.js index a67ecaa..0751438 100644 --- a/consts.js +++ b/consts.js @@ -5,11 +5,9 @@ const { Octokit } = require('@octokit/rest'); // This can be problematic for React Context if mutliple copies of the same context are loaded. const singletons = { - '@folio/stripes': '^9.3.0', '@folio/stripes-components': '^13.1.0', '@folio/stripes-connect': '^10.0.1', '@folio/stripes-core': '^11.1.0', - '@folio/stripes-shared-context': '^1.0.0', "moment": "^2.29.0", 'react': '~18.3', 'react-dom': '~18.3', @@ -22,64 +20,55 @@ const singletons = { 'rxjs': '^6.6.3' }; -/** getPlatformSingletons -* get singletons from platform deps -* TODO - specify versions/branches of platform/stripes and additional entries... +/** getHostAppSingletons +* get singletons from stripes-core package.json on Github. */ -const getPlatformSingletons = async () => { +const getHostAppSingletons = async () => { const platformSingletons = {}; - const octokit = new Octokit(); - try { - const platformPkg = await octokit.request('GET /repos/folio-org/platform-complete/contents/package.json', { - headers: { - accept: 'application/vnd.github.raw+json' + const handlePkgData = (corePkg) => { + const pkgObject = corePkg.data ? JSON.parse(corePkg.data) : corePkg; + const stripesCoreVersion = pkgObject.version; + platformSingletons['@folio/stripes-core'] = `~${stripesCoreVersion}`; + Object.keys(singletons).forEach(dep => { + const depVersion = pkgObject.peerDependencies[dep]; + if (depVersion) { + platformSingletons[dep] = depVersion; } }); + } - if (platformPkg.status === 200) { - const pkgObject = JSON.parse(platformPkg.data); - Object.keys(singletons).forEach(dep => { - const depVersion = pkgObject.dependencies[dep]; - if (depVersion) { - platformSingletons[dep] = depVersion; + let corePkg; + // try to get the locally installed stripes-core + try { + corePkg = require('@folio/stripes-core/package.json'); + } catch (e) { + console.log('Unable to locate local stripes-core package.json, fetching from Github...'); + try { + const octokit = new Octokit(); + corePkg = await octokit.request('GET /repos/folio-org/stripes-core/contents/package.json', { + headers: { + accept: 'application/vnd.github.raw+json' } }); - } else { - throw new Error('Error retrieving singletons list from platform. Falling back to static list'); - } - // fetch dep versions from stripes... - const stripesPkg = await octokit.request('GET /repos/folio-org/stripes/contents/package.json', { - headers: { - accept: 'application/vnd.github.raw+json' + if (corePkg.status !== 200) { + throw new Error('Error retrieving singletons list from platform. Falling back to static list'); } - }); - - if (stripesPkg.status === 200) { - const pkgObject = JSON.parse(stripesPkg.data); - Object.keys(singletons).forEach(dep => { - const depVersion = pkgObject.dependencies[dep]; - if (depVersion) { - platformSingletons[dep] = depVersion; - } - }); - } else { - throw new Error('Error retrieving singletons list from stripes version. Falling back to static list'); + } catch (e) { + console.log(e); + return singletons; } - - return platformSingletons; - } catch (e) { - console.log(e); - return singletons; } -} + handlePkgData(corePkg); + return platformSingletons; +} const defaultentitlementUrl = 'http://localhost:3001/registry'; module.exports = { defaultentitlementUrl, singletons, - getPlatformSingletons + getHostAppSingletons }; diff --git a/package.json b/package.json index acd7461..8782dc6 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,15 @@ "@cerner/duplicate-package-checker-webpack-plugin": "~2.1.0", "@csstools/postcss-global-data": "^3.0.0", "@csstools/postcss-relative-color-syntax": "^3.0.7", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@octokit/rest": "^19.0.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@svgr/webpack": "^8.1.0", "add-asset-html-webpack-plugin": "^6.0.0", "autoprefixer": "^10.4.13", "babel-loader": "^9.1.3", "buffer": "^6.0.3", "connect-history-api-fallback": "^1.3.0", + "copy-webpack-plugin": "^13.0.1", "core-js": "^3.6.1", "cors": "^2.8.5", "css-loader": "^6.4.0", @@ -92,4 +93,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 0be1037..ecee96e 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -65,6 +65,7 @@ const baseConfig = { }), new webpack.EnvironmentPlugin(['NODE_ENV']), new RemoveEmptyScriptsPlugin(), + new webpack.ManifestPlugin({ entrypoints: true }), ], module: { rules: [ @@ -162,7 +163,7 @@ const buildConfig = (modulePaths) => { test: /\.css$/, exclude: [cssDistPathRegex], use: [ - { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, + { loader: isProduction ? MiniCssExtractPlugin.loader : 'style-loader' }, { loader: 'css-loader', options: { diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index ac81396..3933c14 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -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 { getPlatformSingletons } = require('./consts'); +const { getHostAppSingletons } = require('./consts'); const { ModuleFederationPlugin } = require('webpack').container; const { processShared } = require('./webpack/utils'); @@ -17,7 +17,7 @@ const useBrowserMocha = () => { return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; }; -const buildConfig = (stripesConfig) => { +const buildConfig = async (stripesConfig) => { const modulePaths = getModulesPaths(stripesConfig.modules); const stripesModulePaths = getStripesModulesPaths(); const allModulePaths = [...stripesModulePaths, ...modulePaths]; @@ -64,7 +64,8 @@ const buildConfig = (stripesConfig) => { // Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules. if (stripesConfig.okapi.entitlementUrl) { - const shared = processShared(getPlatformSingletons, { singleton: true, eager: true }); + const hostAppSingletons = await getHostAppSingletons(); + const shared = processShared(hostAppSingletons, { singleton: true, eager: true }); devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared })); } diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 198504d..fa18828 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -5,14 +5,15 @@ const path = require('path'); 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 { processExternals, processShared } = require('./webpack/utils'); const { getStripesModulesPaths } = require('./webpack/module-paths'); const esbuildLoaderRule = require('./webpack/esbuild-loader-rule'); -const { getPlatformSingletons } = require('./consts'); +const { getHostAppSingletons } = require('./consts'); -const buildConfig = async (metadata) => { +const buildConfig = async (metadata, options) => { const { host, port, name, displayName, main } = metadata; // using main from metadata since the location of main could vary between modules. @@ -25,37 +26,16 @@ const buildConfig = async (metadata) => { // other paths are plural and I'm sticking with that convention. const soundsPath = path.join(process.cwd(), 'sound'); - const configSingletons = await getPlatformSingletons(); + const configSingletons = await getHostAppSingletons(); const shared = processShared(configSingletons, { singleton: true }); const config = { name, - devtool: 'inline-source-map', - mode: 'development', + mode: options.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' - }, - ] + publicPath: options.mode === 'production' ? options.publicPath ?? 'auto' : `${host}:${port}/`, + path: options.outputPath ? path.resolve(options.outputPath) : undefined }, module: { rules: [ @@ -142,6 +122,47 @@ const buildConfig = async (metadata) => { ] }; + // for a build/production mode copy sounds and icons to the output folder... + if (options.mode === 'production') { + config.plugins.push( + new CopyPlugin({ + patterns: [ + { from: 'sound', to: 'sound', noErrorOnMissing: true }, + { from: 'icons', to: 'icons', noErrorOnMissing: true } + ] + }) + ) + } else { + // in development mode, setup the devserver... + config.devtool = 'inline-source-map'; + config.devserver = { + port: port, + open: false, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + static: [ + { + directory: translationsPath, + publicPath: '/translations' + }, + { + directory: iconsPath, + publicPath: '/icons' + }, + { + directory: soundsPath, + publicPath: '/sounds' + }, + ] + } + } + + if (options.minify === false) { + config.optimization = config.optimization || {}; + config.optimization.minimize = false; + } + return config; } diff --git a/webpack/build.js b/webpack/build.js index fb058a9..33f8763 100644 --- a/webpack/build.js +++ b/webpack/build.js @@ -5,6 +5,7 @@ const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); const applyWebpackOverrides = require('./apply-webpack-overrides'); const logger = require('./logger')(); const buildConfig = require('../webpack.config.cli.prod'); +const federate = require('./federate'); const sharedStylesConfig = require('../webpack.config.cli.shared.styles'); const platformModulePath = path.join(path.resolve(), 'node_modules'); @@ -12,7 +13,27 @@ module.exports = function build(stripesConfig, options) { return new Promise((resolve, reject) => { logger.log('starting build...'); - let config = buildConfig(stripesConfig, options); + const buildCallback = (err, stats) => { + if (err) { + console.error(err.stack || err); + if (err.details) { + console.error(err.details); + } + reject(err); + } else { + resolve(stats); + } + }; + + let config; + if (options.context.isUiModule && options.federate) { + return federate( + stripesConfig, + { ...options, build: true, mode: 'production' }, + buildCallback); + } else { + config = buildConfig(stripesConfig, options) + } config = sharedStylesConfig(config, {}); @@ -73,16 +94,6 @@ module.exports = function build(stripesConfig, options) { logger.log('assign final webpack config', config); - webpack(config, (err, stats) => { - if (err) { - console.error(err.stack || err); - if (err.details) { - console.error(err.details); - } - reject(err); - } else { - resolve(stats); - } - }); + webpack(config, buildCallback); }); }; diff --git a/webpack/federate.js b/webpack/federate.js index 1fc1292..562678f 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -11,9 +11,9 @@ const logger = require('./logger')(); // Remotes will be serve starting from port 3002 portfinder.setBasePort(3002); -module.exports = async function federate(options = {}) { +module.exports = async function federate(stripesConfig, options = {}, callback = () => { }) { logger.log('starting federation...'); - + const { entitlementUrl } = stripesConfig.okapi; const packageJsonPath = tryResolve(path.join(process.cwd(), 'package.json')); if (!packageJsonPath) { @@ -21,9 +21,15 @@ module.exports = async function federate(options = {}) { process.exit(); } + // publicPath for how remoteEntry will be accessed. + let url; const port = options.port ?? await portfinder.getPortPromise(); - const host = `http://localhost`; - const url = `${host}:${port}/remoteEntry.js`; + const host = options.host ?? `http://localhost`; + if (options.publicPath) { + url = `${options.publicPath}/remoteEntry.js` + } else { + url = `${host}:${port}/remoteEntry.js`; + } const { name: packageName, version, description, stripes, main } = require(packageJsonPath); const { permissionSets: _, ...stripesRest } = stripes; @@ -40,10 +46,12 @@ module.exports = async function federate(options = {}) { ...stripesRest, }; - const config = buildConfig(metadata); + const config = await buildConfig(metadata, options); - // TODO: allow for configuring entitlementUrl via env var or stripes config - const entitlementUrl = 'http://localhost:3001/registry'; + if (options.build) { // build only + webpack(config, callback); + return; + } const requestHeader = { "Content-Type": "application/json", @@ -60,15 +68,11 @@ module.exports = async function federate(options = {}) { console.error(`Registry not found. Please check ${entitlementUrl}`); process.exit(); }); - // axios.post(entitlementUrl, metadata).catch(error => { - // console.error(`Registry not found. Please check ${entitlementUrl}`); - // 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 { @@ -79,9 +83,11 @@ module.exports = async function federate(options = {}) { }).catch(error => { throw new Error(error); }); - // await axios.delete(entitlementUrl, { data: metadata }); } catch (error) { console.error(`registry not found. Please check ${entitlementUrl}`); } }); + + // serve command expects a promise... + return server.start(); }; diff --git a/webpack/serve.js b/webpack/serve.js index 0d842f1..b03ea73 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -1,6 +1,7 @@ const webpack = require('webpack'); const path = require('path'); const express = require('express'); +const cors = require('cors'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const connectHistoryApiFallback = require('connect-history-api-fallback'); @@ -10,6 +11,7 @@ const logger = require('./logger')(); const buildConfig = require('../webpack.config.cli.dev'); const sharedStylesConfig = require('../webpack.config.cli.shared.styles'); const registryServer = require('./registryServer'); +const federate = require('./federate'); const cwd = path.resolve(); const platformModulePath = path.join(cwd, 'node_modules'); @@ -17,20 +19,27 @@ const coreModulePath = path.join(__dirname, '..', 'node_modules'); const serverRoot = path.join(__dirname, '..'); module.exports = function serve(stripesConfig, options) { + // serving a locally federated module + if (options.federate && options.context.isUiModule) { + return federate(stripesConfig, options); + } + if (typeof stripesConfig.okapi !== 'object') throw new Error('Missing Okapi config'); if (typeof stripesConfig.okapi.url !== 'string') throw new Error('Missing Okapi URL'); if (stripesConfig.okapi.url.endsWith('/')) throw new Error('Trailing slash in Okapi URL will prevent Stripes from functioning'); - return new Promise((resolve) => { + return new Promise(async (resolve) => { logger.log('starting serve...'); const app = express(); - + app.use(express.json()); + app.use(cors()); // stripes module registry - if (stripesConfig.okapi.entitlementUrl) { + if (options.federate && stripesConfig.okapi.entitlementUrl) { const { entitlementUrl } = stripesConfig.okapi; - // Start the local registry server if the entitlementUrl is not an absolute URL ex localhost:3001/registry + // If the entitlement URL points to 'localhost', start a local registry for development/debug. + // For production, entitlementUrl will point to some non-local endpoint and the UI will fetch accordingly. if (entitlementUrl.includes('localhost')) { try { registryServer.start(entitlementUrl); @@ -41,7 +50,7 @@ module.exports = function serve(stripesConfig, options) { } } - let config = buildConfig(stripesConfig); + let config = await buildConfig(stripesConfig); config = sharedStylesConfig(config, {}); @@ -72,6 +81,7 @@ module.exports = function serve(stripesConfig, options) { // To handle rewrites without the dot rule, we should include the static middleware twice // https://github.com/bripkens/connect-history-api-fallback/blob/master/examples/static-files-and-index-rewrite app.use(staticFileMiddleware); + // app.use(express.static(outputDir)); // Process index rewrite before webpack-dev-middleware // to respond with webpack's dist copy of index.html @@ -85,7 +95,6 @@ module.exports = function serve(stripesConfig, options) { })); app.use(webpackHotMiddleware(compiler)); - app.listen(port, host, (err) => { if (err) { console.log(err); diff --git a/webpack/utils.js b/webpack/utils.js index d106d65..7133f8d 100644 --- a/webpack/utils.js +++ b/webpack/utils.js @@ -1,5 +1,14 @@ const isDevelopment = process.env.NODE_ENV === 'development'; const isProduction = process.env.NODE_ENV === 'production'; + +// processExternals +// Accepts a list of peerDeps in the shape of an object +// with { [packageName]: [version] } +// this generates configuration for setting the peer dep as +// an 'external' dependency for webpack, meaning it won't be bundled, +// but will be expected to exist where the bundle is executed. +// the different module types adjust the way webpack transforms the code when +// an external module is encountered within a particular module. const processExternals = (peerDeps) => { return Object.keys(peerDeps).reduce((acc, name) => { acc[name] = { @@ -14,6 +23,12 @@ const processExternals = (peerDeps) => { }, {}); }; +// processShared +// This function takes an object of shared dependencies in the shape of +// { [packageName]: [version] } ex { '@folio/stripes': '^9.3.0' } +// 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 = {}) => { return Object.keys(shared).reduce((acc, name) => { acc[name] = { From 7d47eb9d6adb9496d7f04adae8bba62ac7dafb58 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 14 Jan 2026 14:57:32 -0600 Subject: [PATCH 39/60] clean and comment edit --- webpack.config.cli.prod.js | 5 +++-- webpack.config.federate.remote.js | 3 ++- webpack/serve.js | 1 - webpack/stripes-config-plugin.js | 1 - webpack/stripes-node-api.js | 2 -- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/webpack.config.cli.prod.js b/webpack.config.cli.prod.js index bd265ef..849a21a 100644 --- a/webpack.config.cli.prod.js +++ b/webpack.config.cli.prod.js @@ -11,10 +11,10 @@ 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 { singletons } = require('./consts'); +const { getHostAppSingletons } = require('./consts'); -const buildConfig = (stripesConfig, options = {}) => { +const buildConfig = async (stripesConfig, options = {}) => { const modulePaths = getModulesPaths(stripesConfig.modules); const stripesModulePaths = getStripesModulesPaths(); const allModulePaths = [...stripesModulePaths, ...modulePaths]; @@ -60,6 +60,7 @@ const buildConfig = (stripesConfig, options = {}) => { // build platform with Module Federation if entitlementUrl is provided if (stripesConfig.okapi.entitlementUrl) { + const singletons = await getHostAppSingletons(); const shared = processShared(singletons, { singleton: true, eager: true }); prodConfig.plugins.push( new ModuleFederationPlugin({ name: 'host', shared }) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index fa18828..3016467 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -1,6 +1,7 @@ // This configuration file is used for building individual ui modules for a // federated module platform setup. -// note the static hosted folders "icons", "translations", "sounds" in the devserver config. +// "icons", "translations", "sound" folders are statically hosted in the devServer config. +// "icons" and "sound" directories, with subfolders are copied to the output folder for a production build. const path = require('path'); const webpack = require('webpack'); diff --git a/webpack/serve.js b/webpack/serve.js index b03ea73..7dc19ce 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -81,7 +81,6 @@ module.exports = function serve(stripesConfig, options) { // To handle rewrites without the dot rule, we should include the static middleware twice // https://github.com/bripkens/connect-history-api-fallback/blob/master/examples/static-files-and-index-rewrite app.use(staticFileMiddleware); - // app.use(express.static(outputDir)); // Process index rewrite before webpack-dev-middleware // to respond with webpack's dist copy of index.html diff --git a/webpack/stripes-config-plugin.js b/webpack/stripes-config-plugin.js index a777b91..74fb359 100644 --- a/webpack/stripes-config-plugin.js +++ b/webpack/stripes-config-plugin.js @@ -13,7 +13,6 @@ const { SyncHook } = require('tapable'); const stripesModuleParser = require('./stripes-module-parser'); const StripesBuildError = require('./stripes-build-error'); const stripesSerialize = require('./stripes-serialize'); -const { defaultentitlementUrl } = require('../consts'); const logger = require('./logger')('stripesConfigPlugin'); const stripesConfigPluginHooksMap = new WeakMap(); diff --git a/webpack/stripes-node-api.js b/webpack/stripes-node-api.js index 2b0d9d7..afffb6d 100644 --- a/webpack/stripes-node-api.js +++ b/webpack/stripes-node-api.js @@ -1,11 +1,9 @@ const build = require('./build'); const serve = require('./serve'); const transpile = require('./transpile'); -const federate = require('./federate'); module.exports = { build, serve, transpile, - federate, }; From 093724da2ed6de0862bfce3985a599912ae39a62 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 16 Jan 2026 15:04:11 -0600 Subject: [PATCH 40/60] buildConfig is now async --- webpack/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack/build.js b/webpack/build.js index 33f8763..a5620b4 100644 --- a/webpack/build.js +++ b/webpack/build.js @@ -10,7 +10,7 @@ const sharedStylesConfig = require('../webpack.config.cli.shared.styles'); const platformModulePath = path.join(path.resolve(), 'node_modules'); module.exports = function build(stripesConfig, options) { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { logger.log('starting build...'); const buildCallback = (err, stats) => { @@ -32,7 +32,7 @@ module.exports = function build(stripesConfig, options) { { ...options, build: true, mode: 'production' }, buildCallback); } else { - config = buildConfig(stripesConfig, options) + config = await buildConfig(stripesConfig, options) } config = sharedStylesConfig(config, {}); From b9c334455ae9807e614a79d62455f72f6c70a2cc Mon Sep 17 00:00:00 2001 From: John Coburn Date: Thu, 22 Jan 2026 00:17:40 -0600 Subject: [PATCH 41/60] remove async fallback for platform singletons --- consts.js | 27 ++++++--------------------- webpack.config.cli.dev.js | 4 ++-- webpack.config.cli.prod.js | 4 ++-- webpack.config.federate.remote.js | 8 ++++---- webpack/build.js | 2 +- webpack/serve.js | 2 ++ 6 files changed, 17 insertions(+), 30 deletions(-) diff --git a/consts.js b/consts.js index 0751438..6510aac 100644 --- a/consts.js +++ b/consts.js @@ -1,5 +1,3 @@ -const { Octokit } = require('@octokit/rest'); - // Anythign that we want *the platform to provide to modules should be here. // If an item is not in this list, modules will each load their own version of it. // This can be problematic for React Context if mutliple copies of the same context are loaded. @@ -17,14 +15,14 @@ const singletons = { 'react-router': '^5.2.0', 'react-router-dom': '^5.2.0', 'redux-observable': '^1.2.0', - 'rxjs': '^6.6.3' + 'rxjs': '^6.6.3', }; /** getHostAppSingletons * get singletons from stripes-core package.json on Github. */ -const getHostAppSingletons = async () => { - const platformSingletons = {}; +const getHostAppSingletons = () => { + let platformSingletons = {}; const handlePkgData = (corePkg) => { const pkgObject = corePkg.data ? JSON.parse(corePkg.data) : corePkg; @@ -36,6 +34,7 @@ const getHostAppSingletons = async () => { platformSingletons[dep] = depVersion; } }); + platformSingletons = { ...platformSingletons, ...singletons }; } let corePkg; @@ -43,22 +42,8 @@ const getHostAppSingletons = async () => { try { corePkg = require('@folio/stripes-core/package.json'); } catch (e) { - console.log('Unable to locate local stripes-core package.json, fetching from Github...'); - try { - const octokit = new Octokit(); - corePkg = await octokit.request('GET /repos/folio-org/stripes-core/contents/package.json', { - headers: { - accept: 'application/vnd.github.raw+json' - } - }); - - if (corePkg.status !== 200) { - throw new Error('Error retrieving singletons list from platform. Falling back to static list'); - } - } catch (e) { - console.log(e); - return singletons; - } + corePkg = singletons; + throw new Error('Error retrieving singletons list from platform. Falling back to static list'); } handlePkgData(corePkg); diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index 3933c14..e2af356 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -17,7 +17,7 @@ const useBrowserMocha = () => { return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha'; }; -const buildConfig = async (stripesConfig) => { +const buildConfig = (stripesConfig) => { const modulePaths = getModulesPaths(stripesConfig.modules); const stripesModulePaths = getStripesModulesPaths(); const allModulePaths = [...stripesModulePaths, ...modulePaths]; @@ -64,7 +64,7 @@ const buildConfig = async (stripesConfig) => { // Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules. if (stripesConfig.okapi.entitlementUrl) { - const hostAppSingletons = await getHostAppSingletons(); + const hostAppSingletons = getHostAppSingletons(); const shared = processShared(hostAppSingletons, { singleton: true, eager: true }); devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared })); } diff --git a/webpack.config.cli.prod.js b/webpack.config.cli.prod.js index 849a21a..768033a 100644 --- a/webpack.config.cli.prod.js +++ b/webpack.config.cli.prod.js @@ -14,7 +14,7 @@ const { ModuleFederationPlugin } = require('webpack').container; const { getHostAppSingletons } = require('./consts'); -const buildConfig = async (stripesConfig, options = {}) => { +const buildConfig = (stripesConfig, options = {}) => { const modulePaths = getModulesPaths(stripesConfig.modules); const stripesModulePaths = getStripesModulesPaths(); const allModulePaths = [...stripesModulePaths, ...modulePaths]; @@ -60,7 +60,7 @@ const buildConfig = async (stripesConfig, options = {}) => { // build platform with Module Federation if entitlementUrl is provided if (stripesConfig.okapi.entitlementUrl) { - const singletons = await getHostAppSingletons(); + const singletons = getHostAppSingletons(); const shared = processShared(singletons, { singleton: true, eager: true }); prodConfig.plugins.push( new ModuleFederationPlugin({ name: 'host', shared }) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 3016467..9d5557d 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -14,7 +14,7 @@ const { getStripesModulesPaths } = require('./webpack/module-paths'); const esbuildLoaderRule = require('./webpack/esbuild-loader-rule'); const { getHostAppSingletons } = require('./consts'); -const buildConfig = async (metadata, options) => { +const buildConfig = (metadata, options) => { const { host, port, name, displayName, main } = metadata; // using main from metadata since the location of main could vary between modules. @@ -27,7 +27,7 @@ const buildConfig = async (metadata, options) => { // other paths are plural and I'm sticking with that convention. const soundsPath = path.join(process.cwd(), 'sound'); - const configSingletons = await getHostAppSingletons(); + const configSingletons = getHostAppSingletons(); const shared = processShared(configSingletons, { singleton: true }); const config = { @@ -35,7 +35,7 @@ const buildConfig = async (metadata, options) => { mode: options.mode || 'development', entry: mainEntry, output: { - publicPath: options.mode === 'production' ? options.publicPath ?? 'auto' : `${host}:${port}/`, + publicPath: 'auto', // webpack will determine publicPath of script at runtime. path: options.outputPath ? path.resolve(options.outputPath) : undefined }, module: { @@ -136,7 +136,7 @@ const buildConfig = async (metadata, options) => { } else { // in development mode, setup the devserver... config.devtool = 'inline-source-map'; - config.devserver = { + config.devServer = { port: port, open: false, headers: { diff --git a/webpack/build.js b/webpack/build.js index a5620b4..6a50305 100644 --- a/webpack/build.js +++ b/webpack/build.js @@ -32,7 +32,7 @@ module.exports = function build(stripesConfig, options) { { ...options, build: true, mode: 'production' }, buildCallback); } else { - config = await buildConfig(stripesConfig, options) + config = buildConfig(stripesConfig, options) } config = sharedStylesConfig(config, {}); diff --git a/webpack/serve.js b/webpack/serve.js index 7dc19ce..400ca1a 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -21,6 +21,8 @@ const serverRoot = path.join(__dirname, '..'); module.exports = function serve(stripesConfig, options) { // serving a locally federated module if (options.federate && options.context.isUiModule) { + // override default port 3000 option, as locally federated modules will be on >= 3002... + options.port = options.port !== 3000 ? options.port : undefined; return federate(stripesConfig, options); } From 4e83e7b1553b5b3d6d67060cd389d5dc0f7d0667 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Thu, 22 Jan 2026 00:19:22 -0600 Subject: [PATCH 42/60] shape dev registry response closer to actual entitlement response --- webpack/federate.js | 3 ++- webpack/registryServer.js | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/webpack/federate.js b/webpack/federate.js index 562678f..a445ae7 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -40,8 +40,9 @@ module.exports = async function federate(stripesConfig, options = {}, callback = description, host, port, - url, + location: url, name, + id: `${name}-${version}`, main, ...stripesRest, }; diff --git a/webpack/registryServer.js b/webpack/registryServer.js index 8339bfd..b67d589 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -2,7 +2,7 @@ const express = require('express'); const cors = require('cors'); // Registry data -const registry = { remotes: {} }; +const registry = { discovery: [] }; const registryServer = { start: (url) => { @@ -16,7 +16,10 @@ const registryServer = { const metadata = req.body; const { name } = metadata; - registry.remotes[name] = metadata; + if (registry.discovery.findIndex(r => r.name === name) === -1) { + registry.discovery.push(metadata) + } + res.status(200).send(`Remote ${name} metadata updated`); }); @@ -30,7 +33,7 @@ const registryServer = { const metadata = req.body; const { name } = metadata; - delete registry.remotes[name]; + registry.discovery = registry.discovery.filter(r => r.name !== name); res.status(200).send(`Remote ${name} removed`); }); From 4e58dc61eed0dba9d82b260f9d3e781c6d1fda64 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Thu, 22 Jan 2026 00:25:08 -0600 Subject: [PATCH 43/60] remove the await for buildConfig --- webpack/federate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/federate.js b/webpack/federate.js index a445ae7..605a5a0 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -47,7 +47,7 @@ module.exports = async function federate(stripesConfig, options = {}, callback = ...stripesRest, }; - const config = await buildConfig(metadata, options); + const config = buildConfig(metadata, options); if (options.build) { // build only webpack(config, callback); From bded4e7ecc5aa15a602494878d92a81a8d9a1513 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Thu, 22 Jan 2026 09:44:59 -0600 Subject: [PATCH 44/60] add tenant name to registry server for spoofing the metadata endpoint --- webpack.config.cli.prod.js | 4 ++-- webpack/registryServer.js | 13 +++++++++++-- webpack/serve.js | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/webpack.config.cli.prod.js b/webpack.config.cli.prod.js index 768033a..6675680 100644 --- a/webpack.config.cli.prod.js +++ b/webpack.config.cli.prod.js @@ -58,8 +58,8 @@ const buildConfig = (stripesConfig, options = {}) => { }), ]); - // build platform with Module Federation if entitlementUrl is provided - if (stripesConfig.okapi.entitlementUrl) { + // build platform with Module Federation if --federate flag is passed + if (options.federate) { const singletons = getHostAppSingletons(); const shared = processShared(singletons, { singleton: true, eager: true }); prodConfig.plugins.push( diff --git a/webpack/registryServer.js b/webpack/registryServer.js index b67d589..99e483c 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -2,10 +2,17 @@ const express = require('express'); const cors = require('cors'); // Registry data -const registry = { discovery: [] }; +const registry = { + discovery: [{ + id: 'folio_stripes-1.0', + version: '1.0', + name: 'folio_stripes', + url: 'http://localhost:3000' + }] +}; const registryServer = { - start: (url) => { + start: (url, tenant = 'diku') => { const app = express(); app.use(express.json()); @@ -26,6 +33,8 @@ const registryServer = { // return entire registry for machines app.get('/registry', (_, res) => res.json(registry)); + app.get(`/registry/entitlements/${tenant}/applications`, (_, res) => res.json(registry)); + // return entire registry for humans app.get('/code', (_, res) => res.send(`
${JSON.stringify(registry, null, 2)}
`)); diff --git a/webpack/serve.js b/webpack/serve.js index 400ca1a..bef6c4b 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -38,13 +38,13 @@ module.exports = function serve(stripesConfig, options) { // stripes module registry if (options.federate && stripesConfig.okapi.entitlementUrl) { - const { entitlementUrl } = stripesConfig.okapi; + const { entitlementUrl, tenant } = stripesConfig.okapi; // If the entitlement URL points to 'localhost', start a local registry for development/debug. // For production, entitlementUrl will point to some non-local endpoint and the UI will fetch accordingly. if (entitlementUrl.includes('localhost')) { try { - registryServer.start(entitlementUrl); + registryServer.start(entitlementUrl, tenant); } catch (e) { console.error(e) From f2099e87cfed638193fc49ab824b29c65804e500 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 23 Jan 2026 17:32:16 -0600 Subject: [PATCH 45/60] look in same folder for module-level builds --- webpack/stripes-translations-plugin.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js index d2a8e66..2a4b896 100644 --- a/webpack/stripes-translations-plugin.js +++ b/webpack/stripes-translations-plugin.js @@ -136,6 +136,11 @@ module.exports = class StripesTranslationPlugin { const locateContext = this.modules[mod].resolvedPath || this.context; const modPackageJsonPath = modulePaths.locateStripesModule(locateContext, mod, this.aliases, 'package.json'); + // if this is a module-level build of a cloned module, the package.json will be in the current folder/context. + if (!modPackageJsonPath && this.federate) { + modPackageJsonPath = path.join(this.context, 'package.json'); + } + if (modPackageJsonPath) { const moduleName = StripesTranslationPlugin.getModuleName(mod); const modTranslationDir = modPackageJsonPath.replace('package.json', `translations/${moduleName}`); From 7fc5d00f953a4b7c73b3abd28c992a8b5fde94cb Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 26 Jan 2026 21:09:58 -0600 Subject: [PATCH 46/60] const/let in stripes-translations-plugin, ts loader rules in federate webpack config --- webpack.config.federate.remote.js | 5 +++++ webpack/stripes-translations-plugin.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 9d5557d..e40d714 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -12,6 +12,7 @@ const { container } = webpack; const { processExternals, processShared } = require('./webpack/utils'); const { getStripesModulesPaths } = require('./webpack/module-paths'); const esbuildLoaderRule = require('./webpack/esbuild-loader-rule'); +const typescriptLoaderRule = require('./webpack/typescript-loader-rule') const { getHostAppSingletons } = require('./consts'); const buildConfig = (metadata, options) => { @@ -38,8 +39,12 @@ const buildConfig = (metadata, options) => { publicPath: 'auto', // webpack will determine publicPath of script at runtime. path: options.outputPath ? path.resolve(options.outputPath) : undefined }, + stats: { + errorDetails: true + }, module: { rules: [ + typescriptLoaderRule, esbuildLoaderRule(stripesModulePaths), { test: /\.(woff2?)$/, diff --git a/webpack/stripes-translations-plugin.js b/webpack/stripes-translations-plugin.js index 2a4b896..c7ef2aa 100644 --- a/webpack/stripes-translations-plugin.js +++ b/webpack/stripes-translations-plugin.js @@ -134,7 +134,7 @@ module.exports = class StripesTranslationPlugin { for (const mod of Object.keys(this.modules)) { // translations from module dependencies may need to be located relative to their dependent (eg. in yarn workspaces) const locateContext = this.modules[mod].resolvedPath || this.context; - const modPackageJsonPath = modulePaths.locateStripesModule(locateContext, mod, this.aliases, 'package.json'); + let modPackageJsonPath = modulePaths.locateStripesModule(locateContext, mod, this.aliases, 'package.json'); // if this is a module-level build of a cloned module, the package.json will be in the current folder/context. if (!modPackageJsonPath && this.federate) { From a799aa6b83daf22be5413a4ee0388b3ae955c030 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 26 Jan 2026 21:32:42 -0600 Subject: [PATCH 47/60] add extensions to resolve in federate webpack config --- webpack.config.federate.remote.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index e40d714..00cdfda 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -42,6 +42,9 @@ const buildConfig = (metadata, options) => { stats: { errorDetails: true }, + resolve: { + extensions: ['.js', '.json', '.ts', '.tsx'], + }, module: { rules: [ typescriptLoaderRule, From 43b1d097f711d82ead709ed932b23c05147f41a9 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 26 Jan 2026 23:15:16 -0600 Subject: [PATCH 48/60] add more 'include' paths for module-level builds --- webpack/tsconfig.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/webpack/tsconfig.json b/webpack/tsconfig.json index 9d3be5c..fc35d74 100644 --- a/webpack/tsconfig.json +++ b/webpack/tsconfig.json @@ -3,7 +3,10 @@ "noImplicitAny": true, "esModuleInterop": true, "jsx": "react", - "lib": ["esnext", "dom"], + "lib": [ + "esnext", + "dom" + ], "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true @@ -16,7 +19,9 @@ "../node_modules/@folio/**/*.ts", "../node_modules/@folio/**/*.tsx", "../../node_modules/@folio/**/*.ts", - "../../node_modules/@folio/**/*.tsx" + "../../node_modules/@folio/**/*.tsx", + "../../../../src/**/*.ts", + "../../../../src/**/*.tsx" ], "exclude": [ "../../**/*.test.ts", @@ -32,4 +37,4 @@ "../../node_modules/@folio/**/test/**/*.ts", "../../node_modules/@folio/**/test/**/*.tsx" ] -} +} \ No newline at end of file From 2072229de442cf32808ae4cdf41da419ce94376a Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 28 Jan 2026 08:12:23 -0600 Subject: [PATCH 49/60] clean up catches --- webpack/federate.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/webpack/federate.js b/webpack/federate.js index 605a5a0..6b87379 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -76,17 +76,13 @@ module.exports = async function federate(stripesConfig, options = {}, callback = compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { - try { - await fetch(entitlementUrl, { - method: 'DELETE', - headers: requestHeader, - body: JSON.stringify(metadata), - }).catch(error => { - throw new Error(error); - }); - } catch (error) { - console.error(`registry not found. Please check ${entitlementUrl}`); - } + await fetch(entitlementUrl, { + method: 'DELETE', + headers: requestHeader, + body: JSON.stringify(metadata), + }).catch(error => { + throw new Error(`registry not found. Please check ${entitlementUrl} : ${error}`); + }); }); // serve command expects a promise... From f0b958a4d3d77e0eb042f4316ce04d2772b7ce71 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 28 Jan 2026 15:40:05 -0600 Subject: [PATCH 50/60] add rxjs/operators to singletons list --- consts.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/consts.js b/consts.js index 6510aac..cce1078 100644 --- a/consts.js +++ b/consts.js @@ -2,6 +2,13 @@ // If an item is not in this list, modules will each load their own version of it. // This can be problematic for React Context if mutliple copies of the same context are loaded. +// This list is best thought of as 'aliases' vs package-names. Whether or not the host +// app provides the dependency depends on exactly *how the remote app imports it. +// ex: +// ✓ 'import { Route } from 'react-router' // covered by this list +// ✗ import Route from 'react-router/route' // not covered +// deep imports should be marked with their offending app for future pruning of this list. +// ui-modules should not have deep imports unless they absolutely have to. const singletons = { '@folio/stripes-components': '^13.1.0', '@folio/stripes-connect': '^10.0.1', @@ -16,6 +23,7 @@ const singletons = { 'react-router-dom': '^5.2.0', 'redux-observable': '^1.2.0', 'rxjs': '^6.6.3', + 'rxjs/operators': '^6.6.3', // for eholdings usage }; /** getHostAppSingletons From 8b233acb2913369c6be400e2e948e4e1ed131d97 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 14:39:58 -0600 Subject: [PATCH 51/60] add tests for federate function, remove portfinder dep --- CHANGELOG.md | 1 - test/webpack/federate.spec.js | 178 +++++++++++++++++++++++++++++ test/webpack/fixtures/package.json | 10 ++ webpack/federate.js | 30 ++++- 4 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 test/webpack/federate.spec.js create mode 100644 test/webpack/fixtures/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b2491..fccb290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ * Prune dead code, `stripes.js` and its dep `commander`. Refs STRWEB-134. * Provide `getDynamicModule`, returning a module via `import()`. Refs STRWEB-137. * * Add `subscribesTo` field to module metadata. Refs STRWEB-143. -* Add `StripesLocalFederation` plugin, inject module federation plugin for federated platforms and modules. Refs STRIPES-861. * Adjust `StripesTranslationsPlugin` for working at the module level and including translations from `stripesDeps`. Refs STRIPES-861. ## [6.0.0](https://github.com/folio-org/stripes-webpack/tree/v6.0.0) (2025-02-24) diff --git a/test/webpack/federate.spec.js b/test/webpack/federate.spec.js new file mode 100644 index 0000000..6df0b77 --- /dev/null +++ b/test/webpack/federate.spec.js @@ -0,0 +1,178 @@ +const expect = require('chai').expect; +const sinon = require('sinon'); +const path = require('path'); +// Ensure global test setup (sinon-chai) is applied when mocha is invoked for this single spec +require('./test-setup.spec'); + +describe('The federate function', function () { + let fetchStub; + let webpackModule; + let webpackStub; + let WebpackDevServerModule; + let WebpackDevServerStub; + let tryResolveStub; + let buildConfigStub; + let snakeCaseStub; + let consoleLogStub; + let consoleErrorStub; + let processExitStub; + let compilerShutdownFn; + let federate; + + beforeEach(function () { + // Stub global fetch (used for registry POST/DELETE) + fetchStub = sinon.stub(global, 'fetch').resolves(); + + // Stub webpack (returns a compiler stub) + webpackModule = require('webpack'); + + const compilerStub = { + hooks: { + shutdown: { + tapPromise: sinon.stub().callsFake((name, fn) => { + // capture shutdown function for later simulation + compilerShutdownFn = fn; + }) + } + } + }; + + // replace webpack module export with a stub function that returns our compiler + webpackStub = sinon.stub().callsFake((cfg) => compilerStub); + try { + require.cache[require.resolve('webpack')].exports = webpackStub; + } catch (e) { + // ignore if cannot patch + } + + // Stub WebpackDevServer constructor + WebpackDevServerModule = require('webpack-dev-server'); + + WebpackDevServerStub = sinon.stub().callsFake((devServerCfg, compiler) => ({ + start: sinon.stub().resolves() + })); + + // replace the module export on the real webpack-dev-server module (for when federate requires it) + // Some versions export a class; we attach our stub to the module export + Object.keys(WebpackDevServerModule).forEach(k => { + // noop - ensure module is loaded + }); + // patching the module's export directly (works for common test environment) + try { + // In many installs webpack-dev-server exports a function/class; overwrite it safely + require.cache[require.resolve('webpack-dev-server')].exports = WebpackDevServerStub; + } catch (e) { + // ignore if cannot patch + } + + // Stub module-paths.tryResolve + const modulePaths = require('../../webpack/module-paths'); + tryResolveStub = sinon.stub(modulePaths, 'tryResolve').returns(path.join(__dirname, 'fixtures', 'package.json')); + + // Stub the build config factory by injecting a fake module into the require cache + buildConfigStub = sinon.stub().returns({ devServer: {} }); + try { + const resolved = require.resolve('../../webpack.config.federate.remote'); + require.cache[resolved] = { id: resolved, filename: resolved, loaded: true, exports: buildConfigStub }; + } catch (e) { + // ignore if cannot inject + } + + // Stub snakeCase before requiring federate (so destructured import picks up stub) + // const lodash = require('lodash'); + // snakeCaseStub = sinon.stub(lodash, 'snakeCase').callsFake((s) => s.replace(/[^a-zA-Z0-9]+/g, '-')); + + // Stub console and process.exit + consoleLogStub = sinon.stub(console, 'log'); + consoleErrorStub = sinon.stub(console, 'error'); + processExitStub = sinon.stub(process, 'exit'); + + // Now require the federate module under test after stubs are in place + delete require.cache[require.resolve('../../webpack/federate')]; + federate = require('../../webpack/federate'); + }); + + afterEach(function () { + // restore any sinon stubs we created in this file + sinon.restore(); + + // Clean up any patched module export + try { + // restore webpack-dev-server to original export by reloading module + delete require.cache[require.resolve('webpack-dev-server')]; + require('webpack-dev-server'); + } catch (e) { + // ignore + } + }); + + it('starts federation successfully', async function () { + const stripesConfig = { okapi: { entitlementUrl: 'http://localhost:3001/registry' } }; + + await federate(stripesConfig, { port: 3003 }); + + // ensure package.json resolution was tried + expect(tryResolveStub).to.have.been.calledWith(sinon.match.string); + + // ensure fetch was called to POST to registry + expect(fetchStub).to.have.been.calledWith('http://localhost:3001/registry', sinon.match.object); + + // ensure webpack and dev server were invoked + expect(webpackStub).to.have.been.called; + expect(WebpackDevServerStub).to.have.been.called; + + // ensure console logged the server start + expect(consoleLogStub).to.have.been.calledWith('Starting remote server on port 3003'); + }); + + it('uses default port when not provided', async function () { + const stripesConfig = { okapi: { entitlementUrl: 'http://localhost:3001/registry' } }; + + await federate(stripesConfig); + + expect(consoleLogStub).to.have.been.calledWith('Starting remote server on port 3002'); + }); + + it('exits when package.json not found', async function () { + // make tryResolve return falsy + const modulePaths = require('../../webpack/module-paths'); + tryResolveStub.returns(false); + + // Make process.exit throw so the function stops executing and we can assert the behaviour + processExitStub.callsFake(() => { throw new Error('process.exit called'); }); + + let thrown; + try { + await federate({ okapi: { entitlementUrl: 'http://localhost:3001/registry' } }); + } catch (e) { + thrown = e; + } + + expect(thrown).to.be.an('error'); + expect(consoleErrorStub).to.have.been.calledWith('package.json not found'); + expect(processExitStub).to.have.been.called; + }); + + it('exits when registry post fails', async function () { + fetchStub.rejects(new Error('Network error')); + + await federate({ okapi: { entitlementUrl: 'http://localhost:3001/registry' } }); + + expect(consoleErrorStub).to.have.been.calledWith('Registry not found. Please check http://localhost:3001/registry'); + expect(processExitStub).to.have.been.called; + }); + + it('handles shutdown hook (DELETE is called)', async function () { + const stripesConfig = { okapi: { entitlementUrl: 'http://localhost:3001/registry' } }; + + await federate(stripesConfig, { port: 3004 }); + + // we captured the shutdown function when compiler.hooks.shutdown.tapPromise was called + expect(compilerShutdownFn).to.be.a('function'); + + // simulate shutdown - it should call fetch with DELETE + await compilerShutdownFn(); + + expect(fetchStub).to.have.been.calledWith('http://localhost:3001/registry', sinon.match({ method: 'DELETE' })); + }); +}); diff --git a/test/webpack/fixtures/package.json b/test/webpack/fixtures/package.json new file mode 100644 index 0000000..1a208b5 --- /dev/null +++ b/test/webpack/fixtures/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-module", + "version": "1.0.0", + "description": "test description", + "stripes": { + "permissionSets": ["perm1"], + "other": "config" + }, + "main": "index.js" +} diff --git a/webpack/federate.js b/webpack/federate.js index 6b87379..cbfb719 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -1,15 +1,35 @@ const path = require('path'); +const net = require('net'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); 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); +// Function to check if a port is free +function isPortFree(port) { + return new Promise((resolve) => { + const server = net.createServer(); + server.listen(port, () => { + server.close(); + resolve(true); + }); + server.on('error', () => { + resolve(false); + }); + }); +} + +// Function to find the next free port starting from a given port +async function findFreePort(startPort) { + let port = startPort; + while (!(await isPortFree(port))) { + port++; + } + return port; +} module.exports = async function federate(stripesConfig, options = {}, callback = () => { }) { logger.log('starting federation...'); @@ -23,8 +43,8 @@ module.exports = async function federate(stripesConfig, options = {}, callback = // publicPath for how remoteEntry will be accessed. let url; - const port = options.port ?? await portfinder.getPortPromise(); - const host = options.host ?? `http://localhost`; + const port = options.port ?? await findFreePort(3002); + const host = options.host ?? 'http://localhost'; if (options.publicPath) { url = `${options.publicPath}/remoteEntry.js` } else { From c3e29cb3c389af289a5290de3e4b88050e1bfb7a Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 15:29:58 -0600 Subject: [PATCH 52/60] bountiful comments. --- webpack.config.federate.remote.js | 24 ++++++++++++++++++++++-- webpack/federate.js | 12 ++++-------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/webpack.config.federate.remote.js b/webpack.config.federate.remote.js index 00cdfda..795fed6 100644 --- a/webpack.config.federate.remote.js +++ b/webpack.config.federate.remote.js @@ -16,7 +16,7 @@ const typescriptLoaderRule = require('./webpack/typescript-loader-rule') const { getHostAppSingletons } = require('./consts'); const buildConfig = (metadata, options) => { - const { host, port, name, displayName, main } = metadata; + const { port, name, displayName, main } = metadata; // using main from metadata since the location of main could vary between modules. let mainEntry = path.join(process.cwd(), main || 'index.js'); @@ -28,9 +28,25 @@ const buildConfig = (metadata, options) => { // other paths are plural and I'm sticking with that convention. const soundsPath = path.join(process.cwd(), 'sound'); + // Module federation resolves dependencies at runtime. + // 'shared' holds a key-value list of modules and versions that are common between + // the host app and the remote modules. + // When a remote ui-module is loaded, module federation runtime will check these + // dependencies and load the individual chunks accordingly. + // 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 }); + // general webpack config. + // Some noteworthy settings: + // publicPath: 'auto' setting will include a bit of runtime logic so that the + // loaded script can load its individual chunks. + // For more info, see https://webpack.js.org/guides/public-path/#automatic-publicpath + // entry: mainEntry - this is the 'trunk' of webpack's import tree - webpack will start here + // and work its way through the module. This file is also 'exposed' via the module-federation + // plugin as './MainEntry': mainEntry. When a remote module is loaded, the mod-fed api will + // load 'MainEntry' by name, which imports/requires the module. const config = { name, mode: options.mode || 'development', @@ -114,11 +130,15 @@ const buildConfig = (metadata, options) => { } ] }, - // TODO: remove this after stripes-config is gone. externals: processExternals({ 'stripes-config': true }), plugins: [ new StripesTranslationsPlugin({ federate: true }), new MiniCssExtractPlugin({ filename: 'style.css', ignoreOrder: false }), + // At runtime, the host app will + // 1. load the remoteEntry.js script as directed by the module's location. + // 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({ library: { type: 'var', name }, name, diff --git a/webpack/federate.js b/webpack/federate.js index cbfb719..a1e449a 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -41,15 +41,12 @@ module.exports = async function federate(stripesConfig, options = {}, callback = process.exit(); } - // publicPath for how remoteEntry will be accessed. - let url; + // These variables are for spinning up and serving a federated remote module locally. + // The values here are sent to the local discovery service to register the module + // and the port is passed through to webpack-dev-server to host the module in dev mode. const port = options.port ?? await findFreePort(3002); const host = options.host ?? 'http://localhost'; - if (options.publicPath) { - url = `${options.publicPath}/remoteEntry.js` - } else { - url = `${host}:${port}/remoteEntry.js`; - } + const url = `${host}:${port}/remoteEntry.js`; const { name: packageName, version, description, stripes, main } = require(packageJsonPath); const { permissionSets: _, ...stripesRest } = stripes; @@ -58,7 +55,6 @@ module.exports = async function federate(stripesConfig, options = {}, callback = module: packageName, version, description, - host, port, location: url, name, From ff54884c1027fd8c3df7c8e911cc88bda870a47e Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 15:39:03 -0600 Subject: [PATCH 53/60] export casing on defaultEntitlementUrl --- consts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consts.js b/consts.js index cce1078..606ace6 100644 --- a/consts.js +++ b/consts.js @@ -58,10 +58,10 @@ const getHostAppSingletons = () => { return platformSingletons; } -const defaultentitlementUrl = 'http://localhost:3001/registry'; +const defaultEntitlementUrl = 'http://localhost:3001/registry'; module.exports = { - defaultentitlementUrl, + defaultEntitlementUrl, singletons, getHostAppSingletons }; From 423d3eb2efd86714af8cfbb2fe39a8e297eea232 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 15:54:27 -0600 Subject: [PATCH 54/60] console output, test tweaks --- test/webpack/federate.spec.js | 2 +- webpack/federate.js | 37 +++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/test/webpack/federate.spec.js b/test/webpack/federate.spec.js index 6df0b77..cde1b0b 100644 --- a/test/webpack/federate.spec.js +++ b/test/webpack/federate.spec.js @@ -158,7 +158,7 @@ describe('The federate function', function () { await federate({ okapi: { entitlementUrl: 'http://localhost:3001/registry' } }); - expect(consoleErrorStub).to.have.been.calledWith('Registry not found. Please check http://localhost:3001/registry'); + expect(consoleErrorStub).to.have.been.calledWith(sinon.match(/^Local discovery not found/)); expect(processExitStub).to.have.been.called; }); diff --git a/webpack/federate.js b/webpack/federate.js index a1e449a..0ec9f43 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -75,16 +75,17 @@ module.exports = async function federate(stripesConfig, options = {}, callback = }; // update registry - await fetch( - entitlementUrl, { - method: 'POST', - headers: requestHeader, - body: JSON.stringify(metadata), - }) - .catch(error => { - console.error(`Registry not found. Please check ${entitlementUrl}`); - process.exit(); - }); + try { + await fetch( + entitlementUrl, { + method: 'POST', + headers: requestHeader, + body: JSON.stringify(metadata), + }) + } catch (err) { + console.error(`Local discovery not found for module registration. Please check ${entitlementUrl}: ${err}`); + process.exit(); + } const compiler = webpack(config); const server = new WebpackDevServer(config.devServer, compiler); @@ -92,13 +93,15 @@ module.exports = async function federate(stripesConfig, options = {}, callback = compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { - await fetch(entitlementUrl, { - method: 'DELETE', - headers: requestHeader, - body: JSON.stringify(metadata), - }).catch(error => { - throw new Error(`registry not found. Please check ${entitlementUrl} : ${error}`); - }); + try { + await fetch(entitlementUrl, { + method: 'DELETE', + headers: requestHeader, + body: JSON.stringify(metadata), + }) + } catch (err) { + throw new Error(`Local discovery not found when removing module. Please check ${entitlementUrl} : ${err}`); + }; }); // serve command expects a promise... From 1805f7814e695b563cb05d02e2774efbc94dfc4f Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 16:14:11 -0600 Subject: [PATCH 55/60] remove commented code from tests --- test/webpack/federate.spec.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/webpack/federate.spec.js b/test/webpack/federate.spec.js index cde1b0b..f557845 100644 --- a/test/webpack/federate.spec.js +++ b/test/webpack/federate.spec.js @@ -6,13 +6,11 @@ require('./test-setup.spec'); describe('The federate function', function () { let fetchStub; - let webpackModule; let webpackStub; let WebpackDevServerModule; let WebpackDevServerStub; let tryResolveStub; let buildConfigStub; - let snakeCaseStub; let consoleLogStub; let consoleErrorStub; let processExitStub; @@ -78,10 +76,6 @@ describe('The federate function', function () { // ignore if cannot inject } - // Stub snakeCase before requiring federate (so destructured import picks up stub) - // const lodash = require('lodash'); - // snakeCaseStub = sinon.stub(lodash, 'snakeCase').callsFake((s) => s.replace(/[^a-zA-Z0-9]+/g, '-')); - // Stub console and process.exit consoleLogStub = sinon.stub(console, 'log'); consoleErrorStub = sinon.stub(console, 'error'); From 485a9e36dfdccae5d1679d78d29efa9effd6b662 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 21:32:54 -0600 Subject: [PATCH 56/60] clean up security concerns with cors and local servers --- webpack/registryServer.js | 8 +++++++- webpack/serve.js | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/webpack/registryServer.js b/webpack/registryServer.js index 99e483c..ab26389 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -15,8 +15,14 @@ const registryServer = { start: (url, tenant = 'diku') => { const app = express(); + app.disable("x-powered-by"); + app.use(express.json()); - app.use(cors()); + + const corsOptions = { + origin: 'localhost' + }; + app.use(cors(corsOptions)); // add/update remote to registry app.post('/registry', (req, res) => { diff --git a/webpack/serve.js b/webpack/serve.js index bef6c4b..78b3883 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -33,8 +33,15 @@ module.exports = function serve(stripesConfig, options) { return new Promise(async (resolve) => { logger.log('starting serve...'); const app = express(); + + app.disable("x-powered-by"); + app.use(express.json()); - app.use(cors()); + + const corsOptions = { + origin: 'localhost' + }; + app.use(cors(corsOptions)); // stripes module registry if (options.federate && stripesConfig.okapi.entitlementUrl) { From ed7ff6348cc8cd992fe659b083a2adad6427a428 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Mon, 2 Feb 2026 21:37:26 -0600 Subject: [PATCH 57/60] clean up async keyword --- webpack/build.js | 2 +- webpack/serve.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webpack/build.js b/webpack/build.js index 6a50305..33f8763 100644 --- a/webpack/build.js +++ b/webpack/build.js @@ -10,7 +10,7 @@ const sharedStylesConfig = require('../webpack.config.cli.shared.styles'); const platformModulePath = path.join(path.resolve(), 'node_modules'); module.exports = function build(stripesConfig, options) { - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { logger.log('starting build...'); const buildCallback = (err, stats) => { diff --git a/webpack/serve.js b/webpack/serve.js index 78b3883..19f2d84 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -30,7 +30,7 @@ module.exports = function serve(stripesConfig, options) { if (typeof stripesConfig.okapi.url !== 'string') throw new Error('Missing Okapi URL'); if (stripesConfig.okapi.url.endsWith('/')) throw new Error('Trailing slash in Okapi URL will prevent Stripes from functioning'); - return new Promise(async (resolve) => { + return new Promise((resolve) => { logger.log('starting serve...'); const app = express(); @@ -59,7 +59,7 @@ module.exports = function serve(stripesConfig, options) { } } - let config = await buildConfig(stripesConfig); + let config = buildConfig(stripesConfig); config = sharedStylesConfig(config, {}); From b7fa6188de96677e5aeb87bd80aa574710515e3f Mon Sep 17 00:00:00 2001 From: John Coburn Date: Tue, 3 Feb 2026 09:31:02 -0600 Subject: [PATCH 58/60] leaving cors open for now --- webpack/registryServer.js | 5 +---- webpack/serve.js | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/webpack/registryServer.js b/webpack/registryServer.js index ab26389..d7dccb0 100644 --- a/webpack/registryServer.js +++ b/webpack/registryServer.js @@ -19,10 +19,7 @@ const registryServer = { app.use(express.json()); - const corsOptions = { - origin: 'localhost' - }; - app.use(cors(corsOptions)); + app.use(cors()); // add/update remote to registry app.post('/registry', (req, res) => { diff --git a/webpack/serve.js b/webpack/serve.js index 19f2d84..e800dd0 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -38,10 +38,7 @@ module.exports = function serve(stripesConfig, options) { app.use(express.json()); - const corsOptions = { - origin: 'localhost' - }; - app.use(cors(corsOptions)); + app.use(cors()); // stripes module registry if (options.federate && stripesConfig.okapi.entitlementUrl) { From dc8a575720bd9913ea67795cb2fc341d7d5bba5a Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 3 Feb 2026 15:38:23 -0500 Subject: [PATCH 59/60] s/entitlementUrl/discoveryUrl/g --- consts.js | 13 +++++++------ package.json | 1 - webpack.config.cli.dev.js | 2 +- webpack/federate.js | 10 +++++----- webpack/serve.js | 12 ++++++------ 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/consts.js b/consts.js index 606ace6..f0978a7 100644 --- a/consts.js +++ b/consts.js @@ -1,11 +1,11 @@ -// Anythign that we want *the platform to provide to modules should be here. +// Anything that we want *the platform to provide to modules should be here. // If an item is not in this list, modules will each load their own version of it. -// This can be problematic for React Context if mutliple copies of the same context are loaded. +// This can be problematic for React Context if multiple copies of the same context are loaded. // This list is best thought of as 'aliases' vs package-names. Whether or not the host // app provides the dependency depends on exactly *how the remote app imports it. // ex: -// ✓ 'import { Route } from 'react-router' // covered by this list +// ✓ import { Route } from 'react-router' // covered by this list // ✗ import Route from 'react-router/route' // not covered // deep imports should be marked with their offending app for future pruning of this list. // ui-modules should not have deep imports unless they absolutely have to. @@ -26,9 +26,10 @@ const singletons = { 'rxjs/operators': '^6.6.3', // for eholdings usage }; -/** getHostAppSingletons -* get singletons from stripes-core package.json on Github. -*/ +/** + * getHostAppSingletons + * get singletons from stripes-core package.json on Github. + */ const getHostAppSingletons = () => { let platformSingletons = {}; diff --git a/package.json b/package.json index 8782dc6..f07752b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@cerner/duplicate-package-checker-webpack-plugin": "~2.1.0", "@csstools/postcss-global-data": "^3.0.0", "@csstools/postcss-relative-color-syntax": "^3.0.7", - "@octokit/rest": "^19.0.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@svgr/webpack": "^8.1.0", "add-asset-html-webpack-plugin": "^6.0.0", diff --git a/webpack.config.cli.dev.js b/webpack.config.cli.dev.js index e2af356..8942fdc 100644 --- a/webpack.config.cli.dev.js +++ b/webpack.config.cli.dev.js @@ -63,7 +63,7 @@ const buildConfig = (stripesConfig) => { } // Enable module federation, setting up the host platform to share singletons (react, stripes-core, etc) with remote modules. - if (stripesConfig.okapi.entitlementUrl) { + if (stripesConfig.okapi.discoveryUrl) { const hostAppSingletons = getHostAppSingletons(); const shared = processShared(hostAppSingletons, { singleton: true, eager: true }); devConfig.plugins.push(new ModuleFederationPlugin({ name: 'host', shared })); diff --git a/webpack/federate.js b/webpack/federate.js index 0ec9f43..b188f32 100644 --- a/webpack/federate.js +++ b/webpack/federate.js @@ -33,7 +33,7 @@ async function findFreePort(startPort) { module.exports = async function federate(stripesConfig, options = {}, callback = () => { }) { logger.log('starting federation...'); - const { entitlementUrl } = stripesConfig.okapi; + const { discoveryUrl } = stripesConfig.okapi; const packageJsonPath = tryResolve(path.join(process.cwd(), 'package.json')); if (!packageJsonPath) { @@ -77,13 +77,13 @@ module.exports = async function federate(stripesConfig, options = {}, callback = // update registry try { await fetch( - entitlementUrl, { + discoveryUrl, { method: 'POST', headers: requestHeader, body: JSON.stringify(metadata), }) } catch (err) { - console.error(`Local discovery not found for module registration. Please check ${entitlementUrl}: ${err}`); + console.error(`Local discovery not found for module registration. Please check ${discoveryUrl}:${err}`); process.exit(); } @@ -94,13 +94,13 @@ module.exports = async function federate(stripesConfig, options = {}, callback = compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { try { - await fetch(entitlementUrl, { + await fetch(discoveryUrl, { method: 'DELETE', headers: requestHeader, body: JSON.stringify(metadata), }) } catch (err) { - throw new Error(`Local discovery not found when removing module. Please check ${entitlementUrl} : ${err}`); + throw new Error(`Local discovery not found when removing module. Please check ${discoveryUrl} : ${err}`); }; }); diff --git a/webpack/serve.js b/webpack/serve.js index e800dd0..4d19acc 100644 --- a/webpack/serve.js +++ b/webpack/serve.js @@ -41,14 +41,14 @@ module.exports = function serve(stripesConfig, options) { app.use(cors()); // stripes module registry - if (options.federate && stripesConfig.okapi.entitlementUrl) { - const { entitlementUrl, tenant } = stripesConfig.okapi; + if (options.federate && stripesConfig.okapi.discoveryUrl) { + const { discoveryUrl, tenant } = stripesConfig.okapi; - // If the entitlement URL points to 'localhost', start a local registry for development/debug. - // For production, entitlementUrl will point to some non-local endpoint and the UI will fetch accordingly. - if (entitlementUrl.includes('localhost')) { + // If the discoveryUrl points to 'localhost', start a local registry for development/debug. + // For production, discoveryUrl will point to some non-local endpoint and the UI will fetch accordingly. + if (discoveryUrl.includes('localhost')) { try { - registryServer.start(entitlementUrl, tenant); + registryServer.start(discoveryUrl, tenant); } catch (e) { console.error(e) From ac25039d29cfe4849a7047b8bda204e9d56ff1d5 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 3 Feb 2026 17:03:49 -0500 Subject: [PATCH 60/60] s/entitlementUrl/discoveryUrl/g in tests, too, duh --- test/webpack/federate.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/webpack/federate.spec.js b/test/webpack/federate.spec.js index f557845..640b4f0 100644 --- a/test/webpack/federate.spec.js +++ b/test/webpack/federate.spec.js @@ -101,7 +101,7 @@ describe('The federate function', function () { }); it('starts federation successfully', async function () { - const stripesConfig = { okapi: { entitlementUrl: 'http://localhost:3001/registry' } }; + const stripesConfig = { okapi: { discoveryUrl: 'http://localhost:3001/registry' } }; await federate(stripesConfig, { port: 3003 }); @@ -120,7 +120,7 @@ describe('The federate function', function () { }); it('uses default port when not provided', async function () { - const stripesConfig = { okapi: { entitlementUrl: 'http://localhost:3001/registry' } }; + const stripesConfig = { okapi: { discoveryUrl: 'http://localhost:3001/registry' } }; await federate(stripesConfig); @@ -137,7 +137,7 @@ describe('The federate function', function () { let thrown; try { - await federate({ okapi: { entitlementUrl: 'http://localhost:3001/registry' } }); + await federate({ okapi: { discoveryUrl: 'http://localhost:3001/registry' } }); } catch (e) { thrown = e; } @@ -150,14 +150,14 @@ describe('The federate function', function () { it('exits when registry post fails', async function () { fetchStub.rejects(new Error('Network error')); - await federate({ okapi: { entitlementUrl: 'http://localhost:3001/registry' } }); + await federate({ okapi: { discoveryUrl: 'http://localhost:3001/registry' } }); expect(consoleErrorStub).to.have.been.calledWith(sinon.match(/^Local discovery not found/)); expect(processExitStub).to.have.been.called; }); it('handles shutdown hook (DELETE is called)', async function () { - const stripesConfig = { okapi: { entitlementUrl: 'http://localhost:3001/registry' } }; + const stripesConfig = { okapi: { discoveryUrl: 'http://localhost:3001/registry' } }; await federate(stripesConfig, { port: 3004 });