Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
bc644ce
STRIPES-861: Setup module federation
mkuklis Apr 25, 2023
b9431f1
Cleanup
mkuklis Apr 25, 2023
467f390
Cleanup
mkuklis Apr 25, 2023
7c3576a
cleanup
mkuklis Apr 25, 2023
2deed9e
Use shutdown hook
mkuklis Apr 25, 2023
49189bd
Cleanup
mkuklis Apr 26, 2023
12cc324
Cleanup
mkuklis Apr 26, 2023
60b5dae
Cleanup
mkuklis Apr 26, 2023
97ba155
Add required version to shared singletons
mkuklis Apr 27, 2023
8c47f56
Start remotes automatically
mkuklis May 1, 2023
84c0199
Expose icons via public endpoint
mkuklis May 10, 2023
54db9c9
react-query provides a context, so must be a singleton
zburke May 16, 2023
ecaa7a6
STCOR-726 map sounds directory for remote applications
zburke Jun 8, 2023
42cac16
current versions
zburke Dec 4, 2024
12613ed
separate handling of stripes-components and application icons
zburke Dec 4, 2024
0c73e79
resolve conflicts
JohnC-80 Oct 31, 2025
626a7c8
provide registry url in stripes-config
JohnC-80 Nov 5, 2025
b399047
resolve conflict
JohnC-80 Nov 5, 2025
08068ed
collect translations from 'StripesDeps' modules in built translations
JohnC-80 Nov 18, 2025
8889e08
get the app's main entry from package.json
JohnC-80 Nov 18, 2025
a8b5fb0
ensure there are aliases before member access
JohnC-80 Nov 18, 2025
fddfec2
expand the singletons for the platform
JohnC-80 Nov 18, 2025
52d545f
make module-federation opt-in via stripes-config
JohnC-80 Dec 5, 2025
2c8dbae
sync up stripes-config-plugin
JohnC-80 Dec 16, 2025
dbd8bde
Merge branch 'main' into STRIPES-861-int
JohnC-80 Dec 16, 2025
c915218
tweak local federation commands
JohnC-80 Dec 16, 2025
0b23f66
Merge branch 'STRIPES-861-int' of github.com:folio-org/stripes-webpac…
JohnC-80 Dec 16, 2025
233244e
switch back to spawn vs spawnsync
JohnC-80 Dec 16, 2025
c7739b0
update to current vm expected exports
JohnC-80 Dec 17, 2025
89c80cb
put back inclusion of stripes-deps in the main stripes-translation pl…
JohnC-80 Dec 17, 2025
a3b01e5
add tests for StripesTranslationPlugin federate mode
JohnC-80 Dec 17, 2025
9639baa
use base webpack config from main branch
JohnC-80 Dec 17, 2025
2018a55
add comment to top of federate remote webpack config
JohnC-80 Dec 17, 2025
0ef9438
log changes
JohnC-80 Dec 17, 2025
17ff134
log more changes
JohnC-80 Dec 17, 2025
eea2bb9
add loader entry for sound file
JohnC-80 Dec 18, 2025
7cf2de0
remove StripesLocalFederationPlugin
JohnC-80 Dec 22, 2025
c9182a9
remove commented svg rules in federate config
JohnC-80 Dec 22, 2025
6d4e80a
remove axios
JohnC-80 Dec 22, 2025
55c989a
high level comments and mod-fed vs monolithic translation differences
JohnC-80 Dec 22, 2025
cef8363
dynamically fetch versions for platform singletons
JohnC-80 Dec 23, 2025
84dd206
configure for module-federation with --federate flag on build and ser…
JohnC-80 Jan 14, 2026
7d47eb9
clean and comment edit
JohnC-80 Jan 14, 2026
dbbeef8
Merge branch 'main' into STRIPES-861-int
JohnC-80 Jan 14, 2026
093724d
buildConfig is now async
JohnC-80 Jan 16, 2026
08f7281
Merge branch 'STRIPES-861-int' of github.com:folio-org/stripes-webpac…
JohnC-80 Jan 16, 2026
b9c3344
remove async fallback for platform singletons
JohnC-80 Jan 22, 2026
4e83e7b
shape dev registry response closer to actual entitlement response
JohnC-80 Jan 22, 2026
4e58dc6
remove the await for buildConfig
JohnC-80 Jan 22, 2026
bded4e7
add tenant name to registry server for spoofing the metadata endpoint
JohnC-80 Jan 22, 2026
f2099e8
look in same folder for module-level builds
JohnC-80 Jan 23, 2026
7fc5d00
const/let in stripes-translations-plugin, ts loader rules in federate…
JohnC-80 Jan 27, 2026
a799aa6
add extensions to resolve in federate webpack config
JohnC-80 Jan 27, 2026
43b1d09
add more 'include' paths for module-level builds
JohnC-80 Jan 27, 2026
2072229
clean up catches
JohnC-80 Jan 28, 2026
f0b958a
add rxjs/operators to singletons list
JohnC-80 Jan 28, 2026
8b233ac
add tests for federate function, remove portfinder dep
JohnC-80 Feb 2, 2026
c3e29cb
bountiful comments.
JohnC-80 Feb 2, 2026
ff54884
export casing on defaultEntitlementUrl
JohnC-80 Feb 2, 2026
423d3eb
console output, test tweaks
JohnC-80 Feb 2, 2026
9d6912a
Merge branch 'main' into STRIPES-861-int
JohnC-80 Feb 2, 2026
1805f78
remove commented code from tests
JohnC-80 Feb 2, 2026
284f7e6
Merge branch 'STRIPES-861-int' of github.com:folio-org/stripes-webpac…
JohnC-80 Feb 2, 2026
485a9e3
clean up security concerns with cors and local servers
JohnC-80 Feb 3, 2026
ed7ff63
clean up async keyword
JohnC-80 Feb 3, 2026
b7fa618
leaving cors open for now
JohnC-80 Feb 3, 2026
dc8a575
s/entitlementUrl/discoveryUrl/g
zburke Feb 3, 2026
ac25039
s/entitlementUrl/discoveryUrl/g in tests, too, duh
zburke Feb 3, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Prune dead code, `stripes.js` and its dep `commander`. Refs STRWEB-134.
* Provide `getDynamicModule`, returning a module via `import()`. Refs STRWEB-137.
* Add `subscribesTo` field to module metadata. Refs STRWEB-143.
* Adjust `StripesTranslationsPlugin` for working at the module level and including translations from `stripesDeps`. Refs STRIPES-861.
* Implement module federation functionality for building and serving remote modules. Refs STRIPES-861.
* Generate an asset manifest for the build. Refs STRWEB-144.

## [6.0.0](https://github.com/folio-org/stripes-webpack/tree/v6.0.0) (2025-02-24)
Expand Down
68 changes: 68 additions & 0 deletions consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Anything that we want *the platform to provide to modules should be here.
// If an item is not in this list, modules will each load their own version of it.
// This can be problematic for React Context if multiple copies of the same context are loaded.

// This list is best thought of as 'aliases' vs package-names. Whether or not the host
// app provides the dependency depends on exactly *how the remote app imports it.
// ex:
// ✓ import { Route } from 'react-router' // covered by this list
// ✗ import Route from 'react-router/route' // not covered
// deep imports should be marked with their offending app for future pruning of this list.
// ui-modules should not have deep imports unless they absolutely have to.
const singletons = {
'@folio/stripes-components': '^13.1.0',
'@folio/stripes-connect': '^10.0.1',
'@folio/stripes-core': '^11.1.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',
'rxjs/operators': '^6.6.3', // for eholdings usage
};

/**
* getHostAppSingletons
* get singletons from stripes-core package.json on Github.
*/
const getHostAppSingletons = () => {
let platformSingletons = {};

const handlePkgData = (corePkg) => {
const pkgObject = corePkg.data ? JSON.parse(corePkg.data) : corePkg;
const stripesCoreVersion = pkgObject.version;
platformSingletons['@folio/stripes-core'] = `~${stripesCoreVersion}`;
Object.keys(singletons).forEach(dep => {
const depVersion = pkgObject.peerDependencies[dep];
if (depVersion) {
platformSingletons[dep] = depVersion;
}
});
platformSingletons = { ...platformSingletons, ...singletons };
}

let corePkg;
// try to get the locally installed stripes-core
try {
corePkg = require('@folio/stripes-core/package.json');
} catch (e) {
corePkg = singletons;
throw new Error('Error retrieving singletons list from platform. Falling back to static list');
}

handlePkgData(corePkg);
return platformSingletons;
}

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

module.exports = {
defaultEntitlementUrl,
singletons,
getHostAppSingletons
};
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"babel-loader": "^9.1.3",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^13.0.1",
"core-js": "^3.6.1",
"cors": "^2.8.5",
"css-loader": "^6.4.0",
"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 Down
172 changes: 172 additions & 0 deletions test/webpack/federate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
const expect = require('chai').expect;
const sinon = require('sinon');
const path = require('path');
// Ensure global test setup (sinon-chai) is applied when mocha is invoked for this single spec
require('./test-setup.spec');

describe('The federate function', function () {
let fetchStub;
let webpackStub;
let WebpackDevServerModule;
let WebpackDevServerStub;
let tryResolveStub;
let buildConfigStub;
let consoleLogStub;
let consoleErrorStub;
let processExitStub;
let compilerShutdownFn;
let federate;

beforeEach(function () {
// Stub global fetch (used for registry POST/DELETE)
fetchStub = sinon.stub(global, 'fetch').resolves();

// Stub webpack (returns a compiler stub)
webpackModule = require('webpack');

const compilerStub = {
hooks: {
shutdown: {
tapPromise: sinon.stub().callsFake((name, fn) => {
// capture shutdown function for later simulation
compilerShutdownFn = fn;
})
}
}
};

// replace webpack module export with a stub function that returns our compiler
webpackStub = sinon.stub().callsFake((cfg) => compilerStub);
try {
require.cache[require.resolve('webpack')].exports = webpackStub;
} catch (e) {
// ignore if cannot patch
}

// Stub WebpackDevServer constructor
WebpackDevServerModule = require('webpack-dev-server');

WebpackDevServerStub = sinon.stub().callsFake((devServerCfg, compiler) => ({
start: sinon.stub().resolves()
}));

// replace the module export on the real webpack-dev-server module (for when federate requires it)
// Some versions export a class; we attach our stub to the module export
Object.keys(WebpackDevServerModule).forEach(k => {
// noop - ensure module is loaded
});
// patching the module's export directly (works for common test environment)
try {
// In many installs webpack-dev-server exports a function/class; overwrite it safely
require.cache[require.resolve('webpack-dev-server')].exports = WebpackDevServerStub;
} catch (e) {
// ignore if cannot patch
}

// Stub module-paths.tryResolve
const modulePaths = require('../../webpack/module-paths');
tryResolveStub = sinon.stub(modulePaths, 'tryResolve').returns(path.join(__dirname, 'fixtures', 'package.json'));

// Stub the build config factory by injecting a fake module into the require cache
buildConfigStub = sinon.stub().returns({ devServer: {} });
try {
const resolved = require.resolve('../../webpack.config.federate.remote');
require.cache[resolved] = { id: resolved, filename: resolved, loaded: true, exports: buildConfigStub };
} catch (e) {
// ignore if cannot inject
}

// Stub console and process.exit
consoleLogStub = sinon.stub(console, 'log');
consoleErrorStub = sinon.stub(console, 'error');
processExitStub = sinon.stub(process, 'exit');

// Now require the federate module under test after stubs are in place
delete require.cache[require.resolve('../../webpack/federate')];
federate = require('../../webpack/federate');
});

afterEach(function () {
// restore any sinon stubs we created in this file
sinon.restore();

// Clean up any patched module export
try {
// restore webpack-dev-server to original export by reloading module
delete require.cache[require.resolve('webpack-dev-server')];
require('webpack-dev-server');
} catch (e) {
// ignore
}
});

it('starts federation successfully', async function () {
const stripesConfig = { okapi: { discoveryUrl: 'http://localhost:3001/registry' } };

await federate(stripesConfig, { port: 3003 });

// ensure package.json resolution was tried
expect(tryResolveStub).to.have.been.calledWith(sinon.match.string);

// ensure fetch was called to POST to registry
expect(fetchStub).to.have.been.calledWith('http://localhost:3001/registry', sinon.match.object);

// ensure webpack and dev server were invoked
expect(webpackStub).to.have.been.called;
expect(WebpackDevServerStub).to.have.been.called;

// ensure console logged the server start
expect(consoleLogStub).to.have.been.calledWith('Starting remote server on port 3003');
});

it('uses default port when not provided', async function () {
const stripesConfig = { okapi: { discoveryUrl: 'http://localhost:3001/registry' } };

await federate(stripesConfig);

expect(consoleLogStub).to.have.been.calledWith('Starting remote server on port 3002');
});

it('exits when package.json not found', async function () {
// make tryResolve return falsy
const modulePaths = require('../../webpack/module-paths');
tryResolveStub.returns(false);

// Make process.exit throw so the function stops executing and we can assert the behaviour
processExitStub.callsFake(() => { throw new Error('process.exit called'); });

let thrown;
try {
await federate({ okapi: { discoveryUrl: 'http://localhost:3001/registry' } });
} catch (e) {
thrown = e;
}

expect(thrown).to.be.an('error');
expect(consoleErrorStub).to.have.been.calledWith('package.json not found');
expect(processExitStub).to.have.been.called;
});

it('exits when registry post fails', async function () {
fetchStub.rejects(new Error('Network error'));

await federate({ okapi: { discoveryUrl: 'http://localhost:3001/registry' } });

expect(consoleErrorStub).to.have.been.calledWith(sinon.match(/^Local discovery not found/));
expect(processExitStub).to.have.been.called;
});

it('handles shutdown hook (DELETE is called)', async function () {
const stripesConfig = { okapi: { discoveryUrl: 'http://localhost:3001/registry' } };

await federate(stripesConfig, { port: 3004 });

// we captured the shutdown function when compiler.hooks.shutdown.tapPromise was called
expect(compilerShutdownFn).to.be.a('function');

// simulate shutdown - it should call fetch with DELETE
await compilerShutdownFn();

expect(fetchStub).to.have.been.calledWith('http://localhost:3001/registry', sinon.match({ method: 'DELETE' }));
});
});
10 changes: 10 additions & 0 deletions test/webpack/fixtures/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "test-module",
"version": "1.0.0",
"description": "test description",
"stripes": {
"permissionSets": ["perm1"],
"other": "config"
},
"main": "index.js"
}
2 changes: 1 addition & 1 deletion test/webpack/stripes-config-plugin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('The stripes-config-plugin', function () {
expect(writeModuleArgs[0]).to.be.a('string').that.equals('node_modules/stripes-config.js');

// TODO: More thorough analysis of the generated virtual module
expect(writeModuleArgs[1]).to.be.a('string').with.match(/export { okapi, config, modules, branding, errorLogging, translations, metadata, icons }/);
expect(writeModuleArgs[1]).to.be.a('string').with.match(/export { branding, config, errorLogging, icons, metadata, modules, okapi, translations }/);
});
});

Expand Down
Loading
Loading