-
Notifications
You must be signed in to change notification settings - Fork 214
Expand file tree
/
Copy pathindex.js
More file actions
438 lines (384 loc) · 16.2 KB
/
index.js
File metadata and controls
438 lines (384 loc) · 16.2 KB
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
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import PropTypes from 'prop-types'
import React from 'react'
import {withRouter} from 'react-router-dom'
import hoistNonReactStatic from 'hoist-non-react-statics'
import {AppErrorContext} from '../../components/app-error-boundary'
import Throw404 from '../../components/throw-404'
import {getAppConfig} from '../../compatibility'
import routes from '../../routes'
import {pages as pageEvents} from '../../events'
import {withLegacyGetProps} from '../../components/with-legacy-get-props'
import Refresh from '../refresh'
const noop = () => undefined
const isServerSide = typeof window === 'undefined'
const isHydrating = () => !isServerSide && window.__HYDRATING__
const hasPerformanceAPI = !isServerSide && window.performance && window.performance.timing
/* istanbul ignore next */
const now = () => {
return hasPerformanceAPI
? window.performance.timing.navigationStart + window.performance.now()
: Date.now()
}
/**
* @private
*/
const withErrorHandling = (Wrapped) => {
/* istanbul ignore next */
const wrappedComponentName = Wrapped.displayName || Wrapped.name
const WithErrorHandling = (props) => (
<AppErrorContext.Consumer>
{(ctx) => <Wrapped {...props} {...ctx} />}
</AppErrorContext.Consumer>
)
// Expose statics from the wrapped component on the HOC
hoistNonReactStatic(WithErrorHandling, Wrapped)
WithErrorHandling.displayName = `WithErrorHandling(${wrappedComponentName})`
return WithErrorHandling
}
/**
* The `routeComponent` HOC is automatically used on every component in a project's
* route-config. It provides an interface, via static methods on React components,
* that can be used to fetch data on the server and on the client, seamlessly.
*/
export const routeComponent = (Wrapped, isPage, locals) => {
const AppConfig = getAppConfig()
const hocs = AppConfig.getHOCsInUse()
const getPropsEnabled = hocs.indexOf(withLegacyGetProps) >= 0
const extraArgs = getPropsEnabled ? AppConfig.extraGetPropsArgs(locals) : {}
/* istanbul ignore next */
const wrappedComponentName = Wrapped.displayName || Wrapped.name
class RouteComponent extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
childProps: {
// When serverside or hydrating, forward props from the frozen app state
// to the wrapped component.
...(isServerSide || isHydrating() ? this.props.preloadedProps : undefined),
isLoading: false
}
}
this._suppressUpdate = false
}
/**
* Route-components implement `shouldGetProps()` to control when the
* component should fetch data from the server by calling `getProps()`.
* Typically, this is done by looking at the request URL.
*
* If not implemented, route-components will call `getProps()` again whenever
* `location.pathname` changes.
*
* The `shouldGetProps` function is called once on the server and every time
* a component updates on the client.
*
* @param {Object} args
*
* @param {Location} args.previousLocation - the previous value of
* window.location, or a server-side equivalent.
*
* @param {Location} args.location - the current value of window.location,
* or a server-side equivalent.
*
* @param {Object} args.previousParams - the previous parameters that were
* parsed from the URL by react-router.
*
* @param {Object} args.params - the current parameters that were parsed
* from the URL by react-router.
*
* @return {Promise<Boolean>}
*/
static async shouldGetProps(args) {
if (!getPropsEnabled) {
return false
}
const defaultImpl = () => {
const {previousLocation, location} = args
return !previousLocation || previousLocation.pathname !== location.pathname
}
const component = await RouteComponent.getComponent()
return component.shouldGetProps ? component.shouldGetProps(args) : defaultImpl()
}
/**
* Route-components implement `getProps()` to fetch the data they need to
* display. The `getProps` function must return an Object which is later
* passed to the component as props for rendering. The returned Object is
* serialzied and embedded into the rendered HTML as the initial app
* state when running server-side.
*
* Throwing or rejecting inside `getProps` will cause the server to return
* an Error, with an appropriate status code.
*
* Note that `req` and `res` are only defined on the server – the only place
* the code actually has access to Express requests or responses.
*
* If not implemented `getProps()` does nothing and the component will not
* fetch any data.
*
* Before the promise is returned, a reference is stored for later
* comparision with a call to isLatestPropsPromise. This is used to
* resolve race conditions when there are multiple getProps calls
* active.
*
* @param {Object} args
*
* @param {Request} args.req - an Express HTTP Request object on the server,
* undefined on the client.
*
* @param {Response} args.res - an Express HTTP Response object on the server,
* undefined on the client.
*
* @param {Object} args.params - the parameters that were parsed from the URL
* by react-router.
*
* @param {Location} args.location - the current value of window.location,
* or a server-side equivalent.
*
* @param {Boolean} args.isLoading - the current execution state of `getProps`,
* `true` while `getProp` is executing, and `false` when it's not.
*
* @return {Promise<Object>}
*/
static getProps(args) {
if (!getPropsEnabled) {
return Promise.resolve({})
}
RouteComponent._latestPropsPromise = RouteComponent.getComponent().then((component) =>
component.getProps ? component.getProps({...args, ...extraArgs}) : Promise.resolve()
)
return RouteComponent._latestPropsPromise
}
/**
* Get the underlying component this HoC wraps. This handles loading of
* `@loadable/component` components.
*
* @return {Promise<React.Component>}
*/
static async getComponent() {
return Wrapped.load
? Wrapped.load().then((module) => module.default)
: Promise.resolve(Wrapped)
}
/**
* Route-components implement `getTemplateName()` to return a readable
* name for the component that is used internally for analytics-tracking –
* eg. performance/page-view events.
*
* If not implemented defaults to the `displayName` of the React component.
*
* @return {Promise<String>}
*/
static async getTemplateName() {
return RouteComponent.getComponent().then((c) =>
c.getTemplateName ? c.getTemplateName() : Promise.resolve(wrappedComponentName)
)
}
/**
* Check if a promise is still the latest call to getProps. This is used
* to check if the results are outdated before using them.
*
* @param {Promise} propsPromise - The promise from the call to getProps to check
* @returns true or false
*/
static isLatestPropsPromise(propsPromise) {
return propsPromise === RouteComponent._latestPropsPromise
}
componentDidMount() {
this.componentDidUpdate({})
}
async componentDidUpdate(previousProps) {
// Because we are setting the component state from within this function we need a
// guard prevent various events (update, error, complete, and load) from being
// called multiple times.
if (this._suppressUpdate) {
this._suppressUpdate = false
return
}
const {location: previousLocation, match: previousMatch} = previousProps
const {location, match, onGetPropsComplete, onGetPropsError, onUpdateComplete} =
this.props
const {params} = match || {}
const {params: previousParams} = previousMatch || {}
// The wasHydratingOnUpdate flag MUST only be used to decide whether
// or not to call static lifecycle methods. Do not use it in
// component rendering - you will not be able to trigger updates,
// because this is intentionally outside of a component's
// state/props.
const wasHydratingOnUpdate = isHydrating()
/* istanbul ignore next */
// Don't getProps() when hydrating - the server has already done
// getProps() frozen the state in the page.
const shouldGetPropsNow = async () => {
return (
!wasHydratingOnUpdate &&
(await RouteComponent.shouldGetProps({
previousLocation,
location,
previousParams,
params
}))
)
}
const setStateAsync = (newState) => {
return new Promise((resolve) => {
this.setState(newState, resolve)
})
}
// Note: We've built a reasonable notion of a "page load time" here:
//
// 1. For first loads the load time is the time elapsed between the
// user pressing enter in the URL bar and the first pageLoad event
// fired by this component.
//
// 2. For subsequent loads the load time is the time elapsed while
// running the getProps() function.
//
// Since the time is overwhelmingly spent fetching data on soft-navs,
// we think this is a good approximation in both cases.
const templateName = await RouteComponent.getTemplateName()
const start = now()
const emitPageLoadEvent = (templateName, end) =>
isPage && pageEvents.pageLoad(templateName, start, end)
const emitPageErrorEvent = (name, content) => isPage && pageEvents.error(name, content)
// If hydrating, we know that the server just fetched and
// rendered for us, embedding the app-state in the page HTML.
// For that reason, we don't ever do getProps while Hydrating.
// However, we still want to report a page load time for this
// initial render. Rather than fetching again, trigger the event
// right away and do nothing.
if (wasHydratingOnUpdate) {
emitPageLoadEvent(templateName, now())
}
const willGetProps = await shouldGetPropsNow()
if (!willGetProps) {
onUpdateComplete()
return
}
try {
this._suppressUpdate = true
await setStateAsync({
childProps: {
...this.state.childProps,
isLoading: true
}
})
/**
* When a user triggers two getProps for the same component,
* we'd like to always use the one for the later user action
* instead of the one that resolves last. getProps
* stores a reference to the promise that we check before we use
* the results from it.
*/
const req = undefined
const res = undefined
const propsPromise = RouteComponent.getProps({
req,
res,
params,
location
})
const childProps = await propsPromise
this._suppressUpdate = false
if (RouteComponent.isLatestPropsPromise(propsPromise)) {
await setStateAsync({
childProps: {
...childProps,
isLoading: false
}
})
}
onGetPropsComplete()
emitPageLoadEvent(templateName, now())
} catch (err) {
onGetPropsError(err)
emitPageErrorEvent(templateName, err)
}
onUpdateComplete()
}
/**
* Return the props that are intended for the wrapped component, excluding
* private or test-only props for this HOC.
*/
getChildProps() {
const excludes = [
'onGetPropsComplete',
'onGetPropsError',
'onUpdateComplete',
'preloadedProps'
]
return Object.assign(
{},
...Object.entries(this.props)
.filter((entry) => excludes.indexOf(entry[0]) < 0)
.map(([k, v]) => ({[k]: v}))
)
}
render() {
return <Wrapped {...this.getChildProps()} {...this.state.childProps} />
}
}
RouteComponent.displayName = `routeComponent(${wrappedComponentName})`
RouteComponent.defaultProps = {
onGetPropsComplete: noop,
onGetPropsError: noop,
onUpdateComplete: noop
}
RouteComponent.propTypes = {
location: PropTypes.object,
match: PropTypes.object,
onGetPropsComplete: PropTypes.func,
onGetPropsError: PropTypes.func,
onUpdateComplete: PropTypes.func,
preloadedProps: PropTypes.object
}
const excludes = {
shouldGetProps: true,
getProps: true,
getTemplateName: true
}
hoistNonReactStatic(RouteComponent, Wrapped, excludes)
return withErrorHandling(withRouter(RouteComponent))
}
/**
* Wrap all the components found in the application's route config with the
* route-component HOC so that they all support `getProps` methods server-side
* and client-side in the same way.
*
* @private
*/
export const getRoutes = async (locals = {}, req = {}) => {
let _routes = routes
const {applicationExtensions = []} = locals
if (typeof routes === 'function') {
_routes = await routes(locals)
}
// Call the `extendRoutes` function for all the Application Extensions.
for (const applicationExtension of applicationExtensions) {
const routes = await applicationExtension.extendRoutes(_routes, req)
const extensionName = applicationExtension.constructor.name
// Prefix each component displayName with the extension name so it can later be deserialized
routes.forEach((route) => {
// Skip if component is already prefixed with an application extension name
if (route.component.displayName.includes(".") && route.component.displayName.match(/^[^.]+/)[0]) return
route.component.displayName = `${extensionName}.${route.component.displayName}`
})
_routes = routes
}
const allRoutes = [
// NOTE: this route needs to be above _routes, in case _routes has a fallback route of `path: '*'`
{path: '/__pwa-kit/refresh', component: Refresh},
..._routes,
{path: '*', component: Throw404}
]
return allRoutes.map(({component, ...rest}) => {
return {
component: component ? routeComponent(component, true, locals) : component,
...rest
}
})
}