From bc644ceb35d405c1f19a30fa1cdb5d8f9cca8857 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 09:41:17 -0400 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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',