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