-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
STRIPES-861: Setup module federation #105
base: master
Are you sure you want to change the base?
Changes from all commits
bc644ce
b9431f1
467f390
7c3576a
2deed9e
49189bd
12cc324
60b5dae
97ba155
8c47f56
84c0199
54db9c9
ecaa7a6
42cac16
12613ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.3.0', | ||
'@folio/stripes-shared-context': '^1.0.0', | ||
'react': '~18.2', | ||
'react-dom': '~18.2', | ||
'react-intl': '^6.8.0', | ||
'react-query': '^3.39.3', | ||
'react-redux': '^8.1', | ||
'react-router': '^5.2.0', | ||
'react-router-dom': '^5.2.0', | ||
'redux-observable': '^1.2.0', | ||
'rxjs': '^6.6.3' | ||
}; | ||
|
||
module.exports = { | ||
singletons, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
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 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 = { | ||
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' | ||
}, | ||
{ | ||
directory: iconsPath, | ||
publicPath: '/icons' | ||
}, | ||
{ | ||
directory: soundsPath, | ||
publicPath: '/sounds' | ||
}, | ||
] | ||
}, | ||
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: /\.svg$/, | ||
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$/, | ||
enforce: "pre", | ||
use: ['source-map-loader'], | ||
} | ||
] | ||
}, | ||
// TODO: remove this after stripes-config is gone. | ||
externals: processExternals({ 'stripes-config': true }), | ||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
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 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 = options.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(); | ||
|
||
compiler.hooks.shutdown.tapPromise('AsyncShutdownHook', async (stats) => { | ||
try { | ||
await axios.delete(registryUrl, { data: metadata }); | ||
} catch (error) { | ||
console.error(`registry not found. Please check ${registryUrl}`); | ||
} | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
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 for machines | ||
app.get('/registry', (_, res) => res.json(registry)); | ||
|
||
// return entire registry for humans | ||
app.get('/code', (_, res) => res.send(`<pre>${JSON.stringify(registry, null, 2)}</pre>`)); | ||
Check failure Code scanning / SonarCloud Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks High
Change this code to not reflect user-controlled data. See more on SonarQube Cloud
|
||
|
||
app.delete('/registry', (req, res) => { | ||
const metadata = req.body; | ||
const { name } = metadata; | ||
|
||
delete registry.remotes[name]; | ||
|
||
res.status(200).send(`Remote ${name} removed`); | ||
Check failure Code scanning / SonarCloud Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks High
Change this code to not reflect user-controlled data. See more on SonarQube Cloud
|
||
}); | ||
|
||
app.listen(3001, () => { | ||
console.log('Starting registry server at http://localhost:3001'); | ||
}); | ||
} | ||
}; | ||
|
||
module.exports = registryServer; |
Check failure
Code scanning / SonarCloud
Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks High