-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathinitialize.js
374 lines (348 loc) · 12.7 KB
/
initialize.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
/**
* #### Import members from **@openedx/frontend-base**
*
* The initialization module provides a function for managing an application's initialization
* lifecycle. It also provides constants and default handler implementations.
*
* ```
* import {
* initialize,
* APP_INIT_ERROR,
* APP_READY,
* subscribe,
* AppProvider,
* ErrorPage,
* PageWrap
* } from '@openedx/frontend-base';
* import React from 'react';
* import ReactDOM from 'react-dom';
* import { Routes, Route } from 'react-router-dom';
*
* subscribe(APP_READY, () => {
* ReactDOM.render(
* <AppProvider>
* <Header />
* <main>
* <Routes>
* <Route path="/" element={<PageWrap><PaymentPage /></PageWrap>} />
* </Routes>
* </main>
* <Footer />
* </AppProvider>,
* document.getElementById('root'),
* );
* });
*
* subscribe(APP_INIT_ERROR, (error) => {
* ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
* });
*
* initialize({
* messages: [appMessages],
* requireAuthenticatedUser: true,
* hydrateAuthenticatedUser: true,
* });
```
* @module Initialization
*/
import {
createBrowserHistory,
createMemoryHistory
} from 'history';
/*
This 'site.config' package is a special 'magic' alias in our webpack configuration in the `config`
folder. It points at an `site.config.tsx` file in the root of an project's repository.
*/
import siteConfig from 'site.config';
import { getPath } from './utils';
// eslint-disable-next-line import/no-cycle
import {
configure as configureAnalytics,
identifyAnonymousUser,
identifyAuthenticatedUser,
SegmentAnalyticsService,
} from './analytics';
import {
AxiosJwtAuthService,
configure as configureAuth,
ensureAuthenticatedUser,
fetchAuthenticatedUser,
getAuthenticatedHttpClient,
getAuthenticatedUser,
hydrateAuthenticatedUser,
} from './auth';
import configureCache from './auth/LocalForageCache';
import {
getConfig, mergeConfig
} from './config';
import {
APP_ANALYTICS_INITIALIZED,
APP_AUTH_INITIALIZED,
APP_CONFIG_INITIALIZED,
APP_I18N_INITIALIZED,
APP_INIT_ERROR,
APP_LOGGING_INITIALIZED,
APP_PUBSUB_INITIALIZED,
APP_READY,
} from './constants';
import { configure as configureI18n } from './i18n';
import {
configure as configureLogging,
getLoggingService,
logError,
NewRelicLoggingService,
} from './logging';
import { GoogleAnalyticsLoader } from './scripts';
/**
* A browser history or memory history object created by the [history](https://github.com/ReactTraining/history)
* package. Applications are encouraged to use this history object, rather than creating their own,
* as behavior may be undefined when managing history via multiple mechanisms/instances. Note that
* in environments where browser history may be inaccessible due to `window` being undefined, this
* falls back to memory history.
*/
export const getHistory = () => ((typeof window !== 'undefined')
? createBrowserHistory({ basename: getPath(getConfig().PUBLIC_PATH) })
: createMemoryHistory());
/**
* The string basename that is the root directory of this MFE.
*
* In devstack, this should always just return "/", because each MFE is in its own server/domain.
*
* In Tutor, all MFEs are deployed to a common server, each under a different top-level directory.
* The basename is the root path for a given MFE, e.g. "/library-authoring". It is set by tutor-mfe
* as an ENV variable in the Docker file, and we read it here from that configuration so that it
* can be passed into a Router later.
*/
export const getBasename = () => getPath(getConfig().PUBLIC_PATH);
/**
* The default handler for the initialization lifecycle's `initError` phase. Logs the error to the
* LoggingService using `logError`
*
* @see {@link module:frontend-base~logError}
* @param {*} error
*/
export async function initError(error) {
logError(error);
}
/**
* The default handler for the initialization lifecycle's `auth` phase.
*
* The handler has several responsibilities:
* - Determining the user's authentication state (authenticated or anonymous)
* - Optionally redirecting to login if the application requires an authenticated user.
* - Optionally loading additional user information via the application's user account data
* endpoint.
*
* @param {boolean} requireUser Whether or not we should redirect to login if a user is not
* authenticated.
* @param {boolean} hydrateUser Whether or not we should fetch additional user account data.
*/
export async function auth(requireUser, hydrateUser) {
if (requireUser) {
await ensureAuthenticatedUser(globalThis.location.href);
} else {
await fetchAuthenticatedUser();
}
if (hydrateUser && getAuthenticatedUser() !== null) {
// We intentionally do not await the promise returned by hydrateAuthenticatedUser. All the
// critical data is returned as part of fetch/ensureAuthenticatedUser above, and anything else
// is a nice-to-have for application code.
hydrateAuthenticatedUser();
}
}
/**
* Set or overrides configuration via an site.config.tsx file in the consuming application.
* This site.config.tsx is loaded at runtime and must export one of two things:
*
* - An object which will be merged into the application config via `mergeConfig`.
* - A function which returns an object which will be merged into the application config via
* `mergeConfig`. This function can return a promise.
*/
async function fileConfig() {
let config = {};
if (typeof siteConfig === 'function') {
config = await siteConfig();
} else {
config = siteConfig;
}
mergeConfig(config);
}
/*
* Set or overrides configuration through an API.
* This method allows runtime configuration.
* Set a basic configuration when an error happen and allow initError and display the ErrorPage.
*/
async function runtimeConfig() {
try {
const { MFE_CONFIG_API_URL, APP_ID } = getConfig();
if (MFE_CONFIG_API_URL) {
const apiConfig = { headers: { accept: 'application/json' } };
const apiService = await configureCache();
const params = new URLSearchParams();
params.append('mfe', APP_ID);
const url = `${MFE_CONFIG_API_URL}?${params.toString()}`;
const { data } = await apiService.get(url, apiConfig);
mergeConfig(data);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error with config API', error.message);
}
}
export function loadExternalScripts(externalScripts, data) {
externalScripts.forEach(ExternalScript => {
const script = new ExternalScript(data);
script.loadScript();
});
}
/**
* The default handler for the initialization lifecycle's `analytics` phase.
*
* The handler is responsible for identifying authenticated and anonymous users with the analytics
* service. This is a pre-requisite for sending analytics events, thus, we do it during the
* initialization sequence so that analytics is ready once the application's UI code starts to load.
*
*/
export async function analytics() {
const authenticatedUser = getAuthenticatedUser();
if (authenticatedUser && authenticatedUser.userId) {
identifyAuthenticatedUser(authenticatedUser.userId);
} else {
await identifyAnonymousUser();
}
}
function applyOverrideHandlers(overrides) {
const noOp = async () => { };
return {
pubSub: noOp,
config: noOp,
logging: noOp,
auth,
analytics,
i18n: noOp,
ready: noOp,
initError,
...overrides, // This will override any same-keyed handlers from above.
};
}
/**
* Invokes the application initialization sequence.
*
* The sequence proceeds through a number of lifecycle phases, during which pertinent services are
* configured.
*
* Using the `handlers` option, lifecycle phase handlers can be overridden to perform custom
* functionality. Note that while these override handlers _do_ replace the default handler
* functionality for analytics, auth, and initError (the other phases have no default
* functionality), they do _not_ override the configuration of the actual services that those
* handlers leverage.
*
* Some services can be overridden via the loggingService and analyticsService options. The other
* services (auth and i18n) cannot currently be overridden.
*
* The following lifecycle phases exist:
*
* - pubSub: A no-op by default.
* - config: A no-op by default.
* - logging: A no-op by default.
* - auth: Uses the 'auth' handler defined above.
* - analytics: Uses the 'analytics' handler defined above.
* - i18n: A no-op by default.
* - ready: A no-op by default.
* - initError: Uses the 'initError' handler defined above.
*
* @param {Object} [options]
* @param {*} [options.loggingService=NewRelicLoggingService] The `LoggingService` implementation
* to use.
* @param {*} [options.analyticsService=SegmentAnalyticsService] The `AnalyticsService`
* implementation to use.
* @param {*} [options.authMiddleware=[]] An array of middleware to apply to http clients in the auth service.
* @param {*} [options.externalScripts=[GoogleAnalyticsLoader]] An array of externalScripts.
* By default added GoogleAnalyticsLoader.
* @param {*} [options.requireAuthenticatedUser=false] If true, turns on automatic login
* redirection for unauthenticated users. Defaults to false, meaning that by default the
* application will allow anonymous/unauthenticated sessions.
* @param {*} [options.hydrateAuthenticatedUser=false] If true, makes an API call to the user
* account endpoint (`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}`) to fetch
* detailed account information for the authenticated user. This data is merged into the return
* value of `getAuthenticatedUser`, overriding any duplicate keys that already exist. Defaults to
* false, meaning that no additional account information will be loaded.
* @param {*} [options.messages] A i18n-compatible messages object, or an array of such objects. If
* an array is provided, duplicate keys are resolved with the last-one-in winning.
* @param {*} [options.handlers={}] An optional object of handlers which can be used to replace the
* default behavior of any part of the startup sequence. It can also be used to add additional
* initialization behavior before or after the rest of the sequence.
*/
export async function initialize({
loggingService = NewRelicLoggingService,
analyticsService = SegmentAnalyticsService,
authService = AxiosJwtAuthService,
authMiddleware = [],
externalScripts = [GoogleAnalyticsLoader],
requireAuthenticatedUser: requireUser = false,
hydrateAuthenticatedUser: hydrateUser = false,
messages,
handlers: overrideHandlers = {},
}) {
const handlers = applyOverrideHandlers(overrideHandlers);
try {
// Pub/Sub
await handlers.pubSub();
global.PubSub.publish(APP_PUBSUB_INITIALIZED);
// Configuration
await fileConfig();
await handlers.config();
await runtimeConfig();
global.PubSub.publish(APP_CONFIG_INITIALIZED);
loadExternalScripts(externalScripts, {
config: getConfig(),
});
// This allows us to replace the implementations of the logging, analytics, and auth services
// based on keys in the ConfigDocument. The JavaScript File Configuration method is the only
// one capable of supplying an alternate implementation since it can import other modules.
// If a service wasn't supplied we fall back to the default parameters on the initialize
// function signature.
const loggingServiceImpl = getConfig().loggingService || loggingService;
const analyticsServiceImpl = getConfig().analyticsService || analyticsService;
const authServiceImpl = getConfig().authService || authService;
// Logging
configureLogging(loggingServiceImpl, {
config: getConfig(),
});
await handlers.logging();
global.PubSub.publish(APP_LOGGING_INITIALIZED);
// Internationalization
configureI18n({
messages,
config: getConfig(),
loggingService: getLoggingService(),
});
await handlers.i18n();
global.PubSub.publish(APP_I18N_INITIALIZED);
// Authentication
configureAuth(authServiceImpl, {
loggingService: getLoggingService(),
config: getConfig(),
middleware: authMiddleware,
});
await handlers.auth(requireUser, hydrateUser);
global.PubSub.publish(APP_AUTH_INITIALIZED);
// Analytics
configureAnalytics(analyticsServiceImpl, {
config: getConfig(),
loggingService: getLoggingService(),
httpClient: getAuthenticatedHttpClient(),
});
await handlers.analytics();
global.PubSub.publish(APP_ANALYTICS_INITIALIZED);
// Application Ready
await handlers.ready();
global.PubSub.publish(APP_READY);
} catch (error) {
if (!error.isRedirecting) {
// Initialization Error
await handlers.initError(error);
global.PubSub.publish(APP_INIT_ERROR, error);
}
}
}