Skip to content

Conversation

@JohnC-80
Copy link
Contributor

@JohnC-80 JohnC-80 commented Dec 3, 2025

This PR takes the logic from #105 and implements backward compatibility/opt-in behavior to module federation. Huge thanks to @mkuklis so long ago for his initial lift!

This works with the Stripes-webpack PR and STRIPES-CLI PR to run a federated ui platform.

The changes in this PR implement an <EntitlementLoader> component that, provided an entitlementUrl via stripes-config.okapi, will fetch its list of modules from a dynamic source containing URL's for those module bundles. The bundles are then prefetched individually, translations, icons, loaded. The component passes through any modules provided via the monolithic build stripes-config setup.

Adds addIcon and adjusts setTranslations reducers to accumulate translations from async remote modules (loaded all together up front rather than on-demand at instanciation points such as AppRoutes and Settings (so the synchronous syntax of those implementations remain the same.)

Try the script to clone applicable branches, a few modules and set up a workspace:

mkdir -p module_federation && cd module_federation

# checkout all stripes modules
stripes_modules=(
  webpack
  core
  cli
)

for m in "${stripes_modules[@]}"; do
  git clone "https://github.com/folio-org/stripes-$m.git" --branch STRIPES-861-int &
done

git clone "https://github.com/folio-org/stripes-ui.git"

git clone "https://github.com/folio-org/ui-users.git" &
git clone "https://github.com/folio-org/ui-inventory.git" &
wait

# create workspace via package.json
cat > package.json <<EOF
{
  "private": true,
  "workspaces": [
    "*"
  ]
}
EOF

# create stripes.config
cat > stripes.config.js <<EOF
module.exports = {
  okapi: {
    url: 'https://folio-snapshot-okapi.dev.folio.org',
    tenant: 'diku',
    entitlementUrl: 'http://localhost:3001/registry'
  },
  config: {
    logCategories: 'core,path,action,xhr',
    logPrefix: '--',
    showPerms: false,
    hasAllPerms: false,
    languages: ['en'],
    suppressIntlErrors: true,
  },
  modules: {
  }
};
EOF

# install dependencies
yarn

Then you can:
Build a module-federation host app (from the workspace level):

yarn stripes build --federate stripes.config.js

Serve the host app (dev mode from the workspace level)

yarn stripes serve --federate stripes.config.js

Build module bundle for static hosting (at the ui-module level)

yarn stripes build --federate

Serving the federated ui-module (dev mode at the ui-module level)

yarn stripes serve --federate

Have fun! 🎉🚀🎸🤘

mkuklis and others added 23 commits December 4, 2024 12:33
Draft: load translations when loading remote modules

Note: QueryClientProvider must be explicitly shared
See https://tanstack.com/query/v3/docs/react/reference/QueryClientProvider

Refs STCOR-718, STRIPES-861
Load remote icons, and clean up the translation loading a bit; it was
still very much in draft form, and still is, but at least it doesn't
throw lint errors everywhere now.

Refs STCOR-725, STRIPES-861
Correctly set each apps' localized `displayName` attribute. It isn't
totally clear to me why this doesn't work via `formattedMessage`. It
seems that something is happening asynchronously that we don't realize
is async, and therefore don't await, and then we end up calling
`formatMessage()` before the translations have been pushed to the store.
In any case, pulling the value straight from the translations array
works fine.

Refs STCOR-718
Correctly handle multiple icons per application.

Refs STCOR-725
Major refactoring in stripes-core between this branch's initial work and
the present lead to some discrepancies. The only change of note here, I
think, is the relocation of `<Suspense>` from ModuleRoutes down into
AppRoutes. It isn't clear to me why that was necessary or why it worked.
It was just a hunch that I tried ... and it worked.

Prior to that change, AppRoutes would get stuck in a render loop,
infinitely reloading (yes, even the memoized functions). I don't have a
good explanation for the bug or the fix.
@github-actions
Copy link

github-actions bot commented Dec 3, 2025

Jest Unit Test Results

  1 files  ± 0   84 suites  +1   1m 45s ⏱️ +3s
522 tests +25  489 ✅  - 8  0 💤 ±0  33 ❌ +33 
528 runs  +25  495 ✅  - 8  0 💤 ±0  33 ❌ +33 

For more details on these failures, see this check.

Results for commit 4282ea8. ± Comparison against base commit 5d4fda8.

This pull request removes 1 and adds 26 tests. Note that renamed tests count towards both.
isAuthenticationRequest accepts authn endpoints ‑ isAuthenticationRequest accepts authn endpoints
EntitlementLoader children rendering renders children when modules are available ‑ EntitlementLoader children rendering renders children when modules are available
EntitlementLoader loadModuleAssets converts kebab-case locale to snake_case for translations ‑ EntitlementLoader loadModuleAssets converts kebab-case locale to snake_case for translations
EntitlementLoader loadModuleAssets handles array translation values with messageFormatPattern ‑ EntitlementLoader loadModuleAssets handles array translation values with messageFormatPattern
EntitlementLoader loadModuleAssets handles translation fetch errors ‑ EntitlementLoader loadModuleAssets handles translation fetch errors
EntitlementLoader loadModuleAssets loadEntitlement calls fetch ‑ EntitlementLoader loadModuleAssets loadEntitlement calls fetch
EntitlementLoader loadModuleAssets loads translations for a module ‑ EntitlementLoader loadModuleAssets loads translations for a module
EntitlementLoader preloadModules assigns getModule function to loaded modules ‑ EntitlementLoader preloadModules assigns getModule function to loaded modules
EntitlementLoader preloadModules handles loading errors gracefully ‑ EntitlementLoader preloadModules handles loading errors gracefully
EntitlementLoader preloadModules loads remote components and builds module structure ‑ EntitlementLoader preloadModules loads remote components and builds module structure
EntitlementLoader when entitlementUrl is configured fetches the registry and loads modules dynamically ‑ EntitlementLoader when entitlementUrl is configured fetches the registry and loads modules dynamically
…

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Dec 3, 2025

Bigtest Unit Test Results

152 tests  +2   149 ✅ ±0   6s ⏱️ ±0s
  1 suites ±0     1 💤 ±0 
  1 files   ±0     2 ❌ +2 

For more details on these failures, see this check.

Results for commit 4282ea8. ± Comparison against base commit 5d4fda8.

This pull request removes 152 and adds 152 tests. Note that renamed tests count towards both.
      equal to check email label in english translation
      equal to check email precautions label in english translation
      equal to sent email precautions label in english translation
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Passing a string to the tag-prop ‑ AppIcon Passing a string to the tag-prop Should render an AppIcon with a HTML tag of "div"
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Passing a string using the children-prop ‑ AppIcon Passing a string using the children-prop Should render an AppIcon with a label
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using Stripes-context ‑ AppIcon Rendering an AppIcon using Stripes-context Should render an <img>
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using Stripes-context ‑ AppIcon Rendering an AppIcon using Stripes-context Should render an img with an alt-attribute
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using an icon-object ‑ AppIcon Rendering an AppIcon using an icon-object Should render an <img>
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using an icon-object ‑ AppIcon Rendering an AppIcon using an icon-object Should render an img with an alt-attribute
Chrome_143_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using an icon-object ‑ AppIcon Rendering an AppIcon using an icon-object Should render with a className of "My className"
…
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Passing a string to the tag-prop ‑ AppIcon Passing a string to the tag-prop Should render an AppIcon with a HTML tag of "div"
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Passing a string using the children-prop ‑ AppIcon Passing a string using the children-prop Should render an AppIcon with a label
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using Stripes-context ‑ AppIcon Rendering an AppIcon using Stripes-context Should render an <img>
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using Stripes-context ‑ AppIcon Rendering an AppIcon using Stripes-context Should render an img with an alt-attribute
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using an icon-object ‑ AppIcon Rendering an AppIcon using an icon-object Should render an <img>
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using an icon-object ‑ AppIcon Rendering an AppIcon using an icon-object Should render an img with an alt-attribute
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Rendering an AppIcon using an icon-object ‑ AppIcon Rendering an AppIcon using an icon-object Should render with a className of "My className"
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Size tests Passing a size of "large" ‑ AppIcon Size tests Passing a size of "large" Should render an icon into a large-sized container
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Size tests Passing a size of "medium" ‑ AppIcon Size tests Passing a size of "medium" Should render an icon into a medium-sized container
Chrome_144_0_0_0_(Linux_x86_64).AppIcon Size tests Passing a size of "small" ‑ AppIcon Size tests Passing a size of "small" Should render an icon into a small-sized container
…
This pull request removes 1 skipped test and adds 1 skipped test. Note that renamed tests count towards both.
Chrome_143_0_0_0_(Linux_x86_64).PasswordValidationField with an invalid password ‑ PasswordValidationField with an invalid password shows a validation error
Chrome_144_0_0_0_(Linux_x86_64).PasswordValidationField with an invalid password ‑ PasswordValidationField with an invalid password shows a validation error

♻️ This comment has been updated with latest results.

@JohnC-80 JohnC-80 changed the title STRIPES-861 - integration with main. STRIPES-861 - integration of module federation logic with main branch. Dec 5, 2025
@JohnC-80 JohnC-80 marked this pull request as ready for review December 15, 2025 21:12
@JohnC-80 JohnC-80 requested a review from a team as a code owner December 15, 2025 21:12
Copy link
Member

@zburke zburke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loosk good overall, though I can't get it working with a monolithic stripes.config.js so we need to investigate that before we merge.

Nothing jumps out at me beyond the need to clean up comments and async/await a tiny bit. Since mod-fed hinges on the presence of okapi.entitlementUrl, don't we need a check like


if (okapi.entitlementUrl === okapi.url) { ... } // production
else { ... }                                    // development

to indicate whether to use the entitlement-registry passed into stripes.config.js (dev) or just to grab stuff from the regular discovery request (production)?

@JohnC-80
Copy link
Contributor Author

@zburke

if (okapi.entitlementUrl === okapi.url) { ... } // production
else { ... } // development

Let's talk about this part of things... I don't know enough about discovery to confirm this as a assured case.

Copy link
Member

@zburke zburke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌 with this branch I can now serve a federated build, serve a monolith, and build a monolith or code-split bundle. Sweet! 👏👏👏

Copy link
Member

@zburke zburke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

monkey <<< I cannot explain that except that I typed in some junk text while testing https://github.com/orgs/community/discussions/183653 and the value persisted even though it didn't display in the UI

* @param {array} remotes
* @returns {app: [], plugin: [], settings: [], handler: []}
*/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omit this extra newline

* settings, handler) where the value of each is an array of corresponding
* applications.
*
* @param {array} remotes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document all the arguments. What is the shape of an entry in the remotes array? Someday, this'll all be TS, won't it?

actsAs.forEach(type => modules[type].push({ ...remote }));
});
} catch (e) {
stripes.logger.log('core', `Error preloading modules from entitlement response: ${e}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need stripes for anything other than logging and if so, is it worth it? There is only a single catch-clause here; it's not like we're handling errors inside the loop and carrying on with 99 applications if there was only an error in 1. If there's an error, everything stops. Is logging this through stripes a good-enough way to handle this kind of error, or should we throw it up a level, log it via console.error etc?

*/
const loadIcons = (stripes, module) => {
if (module.icons?.length) {
stripes.logger.log('core', `loading icons for ${module.module}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was probably my code ... but now I'm wondering if we want to log verbose stuff like "Hola! I found an icon!" in a category like core that has been enabled by default since the dawn of FOLIO. Change the category? Skip logging altogether?

setRemoteModules(cachedModules);
};

fetchRegistry();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await fetchRegistry()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect() handler itself is not async, but the fetchRegistry() function is, so all of the async/await is within that defined function.

// eslint-disable-next-line no-undef
await container.init(__webpack_share_scopes__.default);

const factory = await container.get('./MainEntry');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's cross-reference this with the corresponding entry in stripes-webpack/webpack.config.federate.remote. Even just a comment that says "the black magic here has corresponding black magic there" would be helpful. Otherwise, magic-strings like MainEntry remain, uhhhhhhm, magical.

Comment on lines 309 to 313
// if stripes-core is served from a different origin (module-federation) then
// we need to fetch translations from that origin as well rather than a relative path.
// const stripsesCoreOrigin = 'http://localhost:3000';
// const translationUrl = new URL(translationName, stripsesCoreOrigin);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment confuses me. Here, we are loading translations from the host, right? Let's just say that. Can we remove the commented out lines?

Comment on lines 177 to 178
// if platform is configured for module federation, read the list of registered apps from <fill in source of truth>
// localstorage, okapi, direct call to registry endpoint?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you think this "figure out the source of truth" logic belongs -- in loadEntitlement() itself?

  • in production, it'll be localstorage (having been populated during session init)
  • in development, it'll be a direct API call to the registry

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both.. if not localstorage, then the local registry or whatever 'entitlementUrl' is provided... There has to be an 'entitlementUrl' in the config at build-time to even reach this logic... but this is worth a short discussion/question - will host-app builds necessarily *always involve an entitlementUrl since stripes-hub is the thing that actually uses it/cares about it the most? In this current state, stripes-core cares about the entitlementUrl, and it can work for setups that may *not use stripes-hub.

// read the list of registered apps
let remotes;
try {
remotes = await loadEntitlement(okapi.entitlementUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than pulling okapi from the stripes-config import, should we instead be pulling it directly off stripes, which is available to this component via useStripes()?

Comment on lines +117 to +118
// register sounds
// TODO loadSounds(stripes, module);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've lost track of where this work stands. There is handling for sounds in your folio-org/stripes-webpack#173 PR, so should we have corresponding code here, too?

Copy link
Contributor Author

@JohnC-80 JohnC-80 Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limited case of sound is - with code in the current state - leaning on the federated app (check-out) falling back to its own optionalDependencies - and loading sound from there if the logic gets to that particular path. check-out uses a dynamic require for pulling sounds from its optionalDependency- @folio/circulation - we * could do something with what a ui-module exposes like globbing together some 'assets' and mapping out entries to the ModuleFederationPlugin's exposes config... but the case is so obscure at the moment and nothing is broken in a federated build with this. There's other cases for this kind of provision - like dashboard importing components from other ui-modules. It's a spike-able aspect to reach beyond 'sounds' and just provide a means for a ui-module to expose bits of code, components, assets, whatever to other ui-modules.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
64.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants