Skip to content

Commit 060c4f0

Browse files
JohnC-80mkukliszburke
authored
STRIPES-861 - integration of module federation logic with main branch. (#1679)
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](folio-org/stripes-webpack#173) and [STRIPES-CLI PR](folio-org/stripes-cli#392) 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: ````bash 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! 🎉🚀🎸🤘 --------- Co-authored-by: Michal Kuklis <michal.kuklis@gmail.com> Co-authored-by: Zak Burke <zburke@ebsco.com>
1 parent 5887b96 commit 060c4f0

17 files changed

+1105
-175
lines changed

src/App.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { Component, StrictMode } from 'react';
22
import PropTypes from 'prop-types';
33
import { okapi as okapiConfig, config } from 'stripes-config';
44
import merge from 'lodash/merge';
5-
5+
import localforage from 'localforage';
66
import AppConfigError from './components/AppConfigError';
77
import connectErrorEpic from './connectErrorEpic';
88
import configureEpics from './configureEpics';
@@ -15,9 +15,10 @@ import { modulesInitialState } from './ModulesContext';
1515
import css from './components/SessionEventContainer/style.css';
1616

1717
import Root from './components/Root';
18-
import { eventsPortal } from './constants';
18+
import { eventsPortal, stripesHubAPI } from './constants';
1919
import { getLoginTenant } from './loginServices';
2020

21+
2122
const StrictWrapper = ({ children }) => {
2223
if (config.disableStrictMode) {
2324
return children;
@@ -102,11 +103,21 @@ export default class StripesCore extends Component {
102103
if (this.state.isStorageEnabled) {
103104
try {
104105
const modules = await getModules();
106+
107+
const discoveryUrl = await localforage.getItem(stripesHubAPI.DISCOVERY_URL_KEY);
108+
const hostLocation = await localforage.getItem(stripesHubAPI.HOST_LOCATION_KEY);
109+
const remotesList = await localforage.getItem(stripesHubAPI.REMOTE_LIST_KEY);
110+
105111
const actionNames = gatherActions(modules);
106112

107113
this.setState({
108114
actionNames,
109115
modules,
116+
stripesHub: {
117+
discoveryUrl,
118+
hostLocation,
119+
remotesList,
120+
}
110121
});
111122
} catch (error) {
112123
console.error('Failed to gather actions:', error); // eslint-disable-line no-console
@@ -122,6 +133,7 @@ export default class StripesCore extends Component {
122133
const {
123134
actionNames,
124135
modules,
136+
stripesHub,
125137
} = this.state;
126138
// Stripes requires cookies (for login) and session and local storage
127139
// (for session state and all manner of things). If these are not enabled,
@@ -148,6 +160,7 @@ export default class StripesCore extends Component {
148160
actionNames={actionNames}
149161
modules={modules}
150162
disableAuth={(config?.disableAuth) || false}
163+
stripesHub={stripesHub}
151164
{...props}
152165
/>
153166
</StrictWrapper>

src/RootWithIntl.js

Lines changed: 128 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import StaleBundleWarning from './components/StaleBundleWarning';
4343
import { StripesContext } from './StripesContext';
4444
import { CalloutContext } from './CalloutContext';
4545
import AuthnLogin from './components/AuthnLogin';
46+
import EntitlementLoader from './components/EntitlementLoader';
4647

4748
const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {}, queryClient }) => {
4849
const connect = connectFor('@folio/core', stripes.epics, stripes.logger);
@@ -72,132 +73,134 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
7273
<StripesContext.Provider value={connectedStripes}>
7374
<CalloutContext.Provider value={callout}>
7475
<ModuleTranslator>
75-
<TitleManager>
76-
<HotKeys
77-
keyMap={connectedStripes.bindings}
78-
attach={document.body}
79-
noWrapper
80-
>
81-
<Provider store={connectedStripes.store}>
82-
<Router history={history}>
83-
{isAuthenticated || token || disableAuth ?
84-
<>
85-
<QueryStateUpdater stripes={connectedStripes} queryClient={queryClient} />
86-
<MainContainer>
87-
<AppCtxMenuProvider>
88-
<AppOrderProvider>
89-
<MainNav stripes={connectedStripes} queryClient={queryClient} />
90-
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
91-
<HandlerManager
92-
event={events.LOGIN}
93-
stripes={connectedStripes}
94-
/>
95-
{(typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
96-
<ModuleContainer id="content">
97-
<OverlayContainer />
98-
<SessionEventContainer history={history} queryClient={queryClient} />
99-
<Switch>
100-
<TitledRoute
101-
name="home"
102-
path="/"
103-
key="root"
104-
exact
105-
component={<Front stripes={connectedStripes} />}
106-
/>
107-
<TitledRoute
108-
name="ssoRedirect"
109-
path="/sso-landing"
110-
key="sso-landing"
111-
component={<SSORedirect stripes={connectedStripes} />}
112-
/>
113-
<TitledRoute
114-
name="oidcRedirect"
115-
path="/oidc-landing"
116-
key="oidc-landing"
117-
component={<OIDCRedirect stripes={stripes} />}
118-
/>
119-
<TitledRoute
120-
name="logoutTimeout"
121-
path="/logout-timeout"
122-
component={<Logout />}
123-
/>
124-
<TitledRoute
125-
name="settings"
126-
path="/settings"
127-
component={<Settings stripes={connectedStripes} />}
128-
/>
129-
<TitledRoute
130-
name="logout"
131-
path="/logout"
132-
component={<Logout />}
133-
/>
134-
<ModuleRoutes stripes={connectedStripes} />
135-
</Switch>
136-
</ModuleContainer>
137-
)}
138-
</AppOrderProvider>
139-
</AppCtxMenuProvider>
140-
</MainContainer>
141-
<Callout ref={setCalloutDomRef} />
142-
</> :
143-
<Switch>
144-
{/* The ? after :token makes that part of the path optional, so that token may optionally
76+
<EntitlementLoader>
77+
<TitleManager>
78+
<HotKeys
79+
keyMap={connectedStripes.bindings}
80+
attach={document.body}
81+
noWrapper
82+
>
83+
<Provider store={connectedStripes.store}>
84+
<Router history={history}>
85+
{isAuthenticated || token || disableAuth ?
86+
<>
87+
<QueryStateUpdater stripes={connectedStripes} queryClient={queryClient} />
88+
<MainContainer>
89+
<AppCtxMenuProvider>
90+
<AppOrderProvider>
91+
<MainNav stripes={connectedStripes} queryClient={queryClient} />
92+
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
93+
<HandlerManager
94+
event={events.LOGIN}
95+
stripes={connectedStripes}
96+
/>
97+
{(typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
98+
<ModuleContainer id="content">
99+
<OverlayContainer />
100+
<SessionEventContainer history={history} queryClient={queryClient} />
101+
<Switch>
102+
<TitledRoute
103+
name="home"
104+
path="/"
105+
key="root"
106+
exact
107+
component={<Front stripes={connectedStripes} />}
108+
/>
109+
<TitledRoute
110+
name="ssoRedirect"
111+
path="/sso-landing"
112+
key="sso-landing"
113+
component={<SSORedirect stripes={connectedStripes} />}
114+
/>
115+
<TitledRoute
116+
name="oidcRedirect"
117+
path="/oidc-landing"
118+
key="oidc-landing"
119+
component={<OIDCRedirect stripes={stripes} />}
120+
/>
121+
<TitledRoute
122+
name="logoutTimeout"
123+
path="/logout-timeout"
124+
component={<Logout />}
125+
/>
126+
<TitledRoute
127+
name="settings"
128+
path="/settings"
129+
component={<Settings stripes={connectedStripes} />}
130+
/>
131+
<TitledRoute
132+
name="logout"
133+
path="/logout"
134+
component={<Logout />}
135+
/>
136+
<ModuleRoutes stripes={connectedStripes} />
137+
</Switch>
138+
</ModuleContainer>
139+
)}
140+
</AppOrderProvider>
141+
</AppCtxMenuProvider>
142+
</MainContainer>
143+
<Callout ref={setCalloutDomRef} />
144+
</> :
145+
<Switch>
146+
{/* The ? after :token makes that part of the path optional, so that token may optionally
145147
be passed in via URL parameter to avoid length restrictions */}
146-
<TitledRoute
147-
name="CreateResetPassword"
148-
path="/reset-password/:token?"
149-
component={<CreateResetPassword stripes={connectedStripes} />}
150-
/>
151-
<TitledRoute
152-
name="ssoLanding"
153-
exact
154-
path="/sso-landing"
155-
component={<CookiesProvider><SSOLanding stripes={connectedStripes} /></CookiesProvider>}
156-
key="sso-landing"
157-
/>
158-
<TitledRoute
159-
name="oidcLanding"
160-
exact
161-
path="/oidc-landing"
162-
component={<CookiesProvider><OIDCLanding stripes={stripes} /></CookiesProvider>}
163-
key="oidc-landing"
164-
/>
165-
<TitledRoute
166-
name="forgotPassword"
167-
path="/forgot-password"
168-
component={<ForgotPassword stripes={connectedStripes} />}
169-
/>
170-
<TitledRoute
171-
name="forgotUsername"
172-
path="/forgot-username"
173-
component={<ForgotUserName stripes={connectedStripes} />}
174-
/>
175-
<TitledRoute
176-
name="checkEmail"
177-
path="/check-email"
178-
component={<CheckEmailStatusPage />}
179-
/>
180-
<TitledRoute
181-
name="logout"
182-
path="/logout"
183-
component={<Logout />}
184-
/>
185-
<TitledRoute
186-
name="logoutTimeout"
187-
path="/logout-timeout"
188-
component={<Logout />}
189-
/>
190-
<TitledRoute
191-
name="login"
192-
path="*"
193-
component={<AuthnLogin stripes={connectedStripes} />}
194-
/>
195-
</Switch>
196-
}
197-
</Router>
198-
</Provider>
199-
</HotKeys>
200-
</TitleManager>
148+
<TitledRoute
149+
name="CreateResetPassword"
150+
path="/reset-password/:token?"
151+
component={<CreateResetPassword stripes={connectedStripes} />}
152+
/>
153+
<TitledRoute
154+
name="ssoLanding"
155+
exact
156+
path="/sso-landing"
157+
component={<CookiesProvider><SSOLanding stripes={connectedStripes} /></CookiesProvider>}
158+
key="sso-landing"
159+
/>
160+
<TitledRoute
161+
name="oidcLanding"
162+
exact
163+
path="/oidc-landing"
164+
component={<CookiesProvider><OIDCLanding stripes={stripes} /></CookiesProvider>}
165+
key="oidc-landing"
166+
/>
167+
<TitledRoute
168+
name="forgotPassword"
169+
path="/forgot-password"
170+
component={<ForgotPassword stripes={connectedStripes} />}
171+
/>
172+
<TitledRoute
173+
name="forgotUsername"
174+
path="/forgot-username"
175+
component={<ForgotUserName stripes={connectedStripes} />}
176+
/>
177+
<TitledRoute
178+
name="checkEmail"
179+
path="/check-email"
180+
component={<CheckEmailStatusPage />}
181+
/>
182+
<TitledRoute
183+
name="logout"
184+
path="/logout"
185+
component={<Logout />}
186+
/>
187+
<TitledRoute
188+
name="logoutTimeout"
189+
path="/logout-timeout"
190+
component={<Logout />}
191+
/>
192+
<TitledRoute
193+
name="login"
194+
path="*"
195+
component={<AuthnLogin stripes={connectedStripes} />}
196+
/>
197+
</Switch>
198+
}
199+
</Router>
200+
</Provider>
201+
</HotKeys>
202+
</TitleManager>
203+
</EntitlementLoader>
201204
</ModuleTranslator>
202205
</CalloutContext.Provider>
203206
</StripesContext.Provider>

0 commit comments

Comments
 (0)