Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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',
'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'
};

const defaultRegistryUrl = 'http://localhost:3001/registry';

module.exports = {
defaultRegistryUrl,
singletons,
};
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^8.1.0",
"add-asset-html-webpack-plugin": "^6.0.0",
"axios": "^1.3.6",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.3",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^1.3.0",
"core-js": "^3.6.1",
"cors": "^2.8.5",
"css-loader": "^6.4.0",
"csv-loader": "^3.0.3",
"debug": "^4.0.1",
Expand All @@ -52,6 +54,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",
Expand All @@ -70,6 +73,7 @@
"util-ex": "^0.3.15",
"validate-npm-package-name": "^6.0.2",
"webpack-dev-middleware": "^5.2.1",
"webpack-dev-server": "^4.13.1",
"webpack-hot-middleware": "^2.25.1",
"webpack-remove-empty-scripts": "^1.0.1",
"webpack-virtual-modules": "^0.4.3"
Expand All @@ -88,4 +92,4 @@
"react-dom": "^18.2.0",
"webpack": "^5.58.1"
}
}
}
9 changes: 7 additions & 2 deletions webpack.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, } = 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
Expand Down Expand Up @@ -65,6 +70,7 @@ const baseConfig = {
}),
new webpack.EnvironmentPlugin(['NODE_ENV']),
new RemoveEmptyScriptsPlugin(),
new ModuleFederationPlugin({ name: 'host', shared }),
],
module: {
rules: [
Expand Down Expand Up @@ -131,7 +137,6 @@ const baseConfig = {
},
};


const buildConfig = (modulePaths) => {
const transpiledCssPaths = getTranspiledCssPaths(modulePaths);
const cssDistPathRegex = /dist[\/\\]style\.css/;
Expand Down
5 changes: 3 additions & 2 deletions webpack.config.cli.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const esbuildLoaderRule = require('./webpack/esbuild-loader-rule');
const utils = require('./webpack/utils');
const buildBaseConfig = require('./webpack.config.base');
const cli = require('./webpack.config.cli');

const StripesFederationPlugin = require('./webpack/stripes-federation-plugin');

const useBrowserMocha = () => {
return tryResolve('mocha/mocha-es2018.js') ? 'mocha/mocha-es2018.js' : 'mocha';
Expand Down Expand Up @@ -56,7 +56,8 @@ const buildConfig = (stripesConfig) => {
if (utils.isDevelopment) {
devConfig.plugins = devConfig.plugins.concat([
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin()
new ReactRefreshWebpackPlugin(),
new StripesFederationPlugin(stripesConfig)
]);
}

Expand Down
145 changes: 145 additions & 0 deletions webpack.config.federate.remote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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');
const esbuildLoaderRule = require('./webpack/esbuild-loader-rule');
const { singletons } = require('./consts');

const buildConfig = (metadata) => {
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');

// 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 StripesTranslationsPlugin({ federate: true }),
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;
67 changes: 67 additions & 0 deletions webpack/federate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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, main } = require(packageJsonPath);
const { permissionSets: _, ...stripesRest } = stripes;
const name = snakeCase(packageName);
const metadata = {
module: packageName,
version,
description,
host,
port,
url,
name,
main,
...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}`);
}
});
};
3 changes: 2 additions & 1 deletion webpack/module-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
Expand Down Expand Up @@ -264,4 +264,5 @@ module.exports = {
getNonTranspiledModules,
getTranspiledModules,
getTranspiledCssPaths,
locatePackageJsonPath,
};
44 changes: 44 additions & 0 deletions webpack/registryServer.js
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`);

Check failure

Code scanning / SonarCloud

Endpoints should not be vulnerable to reflected cross-site scripting (XSS) attacks

<!--SONAR_ISSUE_KEY:AZOTRIftzi_v0-Uo8dk0-->Change this code to not reflect user-controlled data. <p>See more on <a href="https://sonarcloud.io/project/issues?id=org.folio%3Astripes-webpack&issues=AZOTRIftzi_v0-Uo8dk0&open=AZOTRIftzi_v0-Uo8dk0&branch=STRIPES-861">SonarQube Cloud</a></p>
});

// 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

<!--SONAR_ISSUE_KEY:AZOTRIftzi_v0-Uo8dk1-->Change this code to not reflect user-controlled data. <p>See more on <a href="https://sonarcloud.io/project/issues?id=org.folio%3Astripes-webpack&issues=AZOTRIftzi_v0-Uo8dk1&open=AZOTRIftzi_v0-Uo8dk1&branch=STRIPES-861">SonarQube Cloud</a></p>

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

<!--SONAR_ISSUE_KEY:AZOTRIftzi_v0-Uo8dkz-->Change this code to not reflect user-controlled data. <p>See more on <a href="https://sonarcloud.io/project/issues?id=org.folio%3Astripes-webpack&issues=AZOTRIftzi_v0-Uo8dkz&open=AZOTRIftzi_v0-Uo8dkz&branch=STRIPES-861">SonarQube Cloud</a></p>
});

app.listen(3001, () => {
console.log('Starting registry server at http://localhost:3001');
});
}
};

module.exports = registryServer;
Loading
Loading