catalyst-core 0.2.0-beta.3 → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/babel.native.config.cjs +8 -0
- package/bin/catalyst.js +49 -105
- package/changelog.md +8 -0
- package/dist/caching.js +1 -1
- package/dist/index.jsx +8 -0
- package/dist/logger.js +74 -3
- package/dist/native/androidSetup.js +300 -8
- package/dist/native/bridge/WebBridge.js +481 -59
- package/dist/native/bridge/constants/NativeInterfaces.js +290 -45
- package/dist/native/bridge/errors.js +250 -26
- package/dist/native/bridge/hooks/index.js +125 -1
- package/dist/native/bridge/hooks/useCamera.js +347 -9
- package/dist/native/bridge/hooks/useDataProtection.js +200 -2
- package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
- package/dist/native/bridge/hooks/useFilePicker.js +429 -2
- package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
- package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
- package/dist/native/bridge/hooks/useIntent.js +95 -1
- package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
- package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
- package/dist/native/bridge/hooks/useNotification.js +318 -2
- package/dist/native/bridge/hooks/useSafeArea.js +84 -2
- package/dist/native/bridge/hooks/useVideoStream.js +434 -6
- package/dist/native/bridge/hooks/utils.js +27 -1
- package/dist/native/bridge/hooks.js +16 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +66 -3
- package/dist/native/bridge/perf/collectors/CacheCollector.js +186 -11
- package/dist/native/bridge/perf/collectors/HookCollector.js +141 -19
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +481 -23
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +245 -10
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +107 -8
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +273 -32
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +178 -2
- package/dist/native/bridge/perf/collectors/RenderCollector.js +258 -14
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +95 -9
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +90 -6
- package/dist/native/bridge/perf/core/constants.js +103 -20
- package/dist/native/bridge/perf/core/measure.js +179 -30
- package/dist/native/bridge/perf/core/session.js +89 -23
- package/dist/native/bridge/perf/core/store.js +554 -1
- package/dist/native/bridge/perf/core/summary.js +174 -1
- package/dist/native/bridge/perf/core/traceExport.js +240 -1
- package/dist/native/bridge/perf/core/waterfalls.js +146 -1
- package/dist/native/bridge/perf/index.js +359 -38
- package/dist/native/bridge/safeArea.js +53 -5
- package/dist/native/bridge/standardizedHooks.js +595 -82
- package/dist/native/bridge/useBaseHook.js +274 -38
- package/dist/native/bridge/utils/CameraUtils.js +201 -56
- package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
- package/dist/native/bridge/utils/NativeBridge.js +425 -141
- package/dist/native/buildAndroid/assets.js +510 -13
- package/dist/native/buildAndroid/build.js +296 -19
- package/dist/native/buildAndroid/config.js +87 -1
- package/dist/native/buildAndroid/index.js +374 -1
- package/dist/native/buildAppAndroid.js +34 -2
- package/dist/native/buildAppIos.js +62 -2
- package/dist/native/buildIos/assets.js +609 -8
- package/dist/native/buildIos/build.js +639 -20
- package/dist/native/buildIos/config.js +294 -12
- package/dist/native/buildIos/index.js +343 -6
- package/dist/native/buildIos/plugins.js +343 -16
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
- package/dist/native/internalPluginUtils.js +232 -7
- package/dist/native/package.json +3 -0
- package/dist/native/plugin-bridge/PluginBridge.js +227 -1
- package/dist/native/pluginComposerAndroid.js +407 -23
- package/dist/native/pluginComposerIos.js +435 -11
- package/dist/native/renameAndroidProject.js +1094 -88
- package/dist/native/setupEmulatorIos.js +289 -22
- package/dist/native/setupServer.js +160 -19
- package/dist/native/terminalProgress.js +199 -11
- package/dist/native/utils.js +193 -15
- package/dist/offline/catalyst-sw.js +145 -1
- package/dist/offline/registerServiceWorker.js +33 -1
- package/dist/otel.js +375 -46
- package/dist/router/ClientRouter.js +14 -2
- package/dist/router/ServerRouter.js +9 -1
- package/dist/router.js +1 -1
- package/dist/scripts/build.js +112 -3
- package/dist/scripts/checkVersion.js +3 -1
- package/dist/scripts/cleanCache.js +30 -1
- package/dist/scripts/generateOfflineManifest.js +116 -1
- package/dist/scripts/loadEnvironmentVariables.js +32 -6
- package/dist/scripts/plugins.js +309 -1
- package/dist/scripts/preServerInit.js +13 -0
- package/dist/scripts/scriptUtils.js +54 -3
- package/dist/scripts/serve.js +51 -4
- package/dist/scripts/start.js +47 -10
- package/dist/scripts/validator.js +149 -1
- package/dist/sentry.cjs +127 -0
- package/dist/sentry.js +125 -5
- package/dist/server/expressServer.js +250 -5
- package/dist/server/manifestCache.js +36 -0
- package/dist/server/renderer/ChunkExtractor.js +144 -0
- package/dist/server/renderer/document/Body.jsx +59 -0
- package/dist/server/renderer/document/FastRefresh.jsx +20 -0
- package/dist/server/renderer/document/Head.jsx +60 -0
- package/dist/server/renderer/document/index.jsx +2 -0
- package/dist/server/renderer/extract.js +149 -31
- package/dist/server/renderer/handler.jsx +433 -0
- package/dist/server/renderer/index.js +8 -1
- package/dist/server/renderer/render.js +47 -0
- package/dist/server/utils/botDetectionMiddleware.js +29 -0
- package/dist/server/utils/cjsRequire.js +10 -0
- package/dist/server/utils/userAgentUtil.js +87 -9
- package/dist/server/utils/validator.js +164 -1
- package/dist/vite/FastRefresh.jsx +20 -0
- package/dist/vite/inject-cache-key-plugin.js +276 -0
- package/dist/vite/loadCustomViteConfig.js +22 -0
- package/dist/vite/manifest-categorization-plugin.js +455 -0
- package/dist/vite/node-loader.mjs +458 -0
- package/dist/vite/scssParams.js +21 -0
- package/dist/vite/vite.config.client.js +130 -0
- package/dist/vite/vite.config.js +422 -0
- package/dist/vite/vite.config.server.js +84 -0
- package/dist/web-router/components/MetaTag.jsx +34 -0
- package/dist/web-router/components/RouterDataProvider.jsx +332 -0
- package/dist/web-router/components/Split.jsx +200 -0
- package/dist/web-router/components/SplitInview.jsx +106 -0
- package/dist/web-router/components/SsrRequestContext.jsx +14 -0
- package/dist/web-router/context.jsx +12 -0
- package/dist/web-router/hooks.jsx +72 -0
- package/dist/web-router/types/components/MetaTag.d.ts +2 -0
- package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
- package/dist/web-router/types/context.d.ts +14 -0
- package/dist/web-router/types/hooks.d.ts +1 -0
- package/dist/web-router/types/index.d.ts +3 -0
- package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
- package/dist/web-router/utils/metaDataUtils.jsx +119 -0
- package/mcp_v2/conversion-tasks.json +35 -17
- package/mcp_v2/knowledge-base.json +124 -13
- package/mcp_v2/lib/helpers.js +10 -0
- package/mcp_v2/mcp.js +4 -4
- package/mcp_v2/package.json +1 -0
- package/mcp_v2/tools/build.js +116 -20
- package/mcp_v2/tools/conversion.js +106 -5
- package/mcp_v2/tools/tasks.js +18 -14
- package/package.json +35 -68
- package/babel.config.js +0 -32
- package/dist/index.js +0 -1
- package/dist/scripts/devBuild.js +0 -3
- package/dist/scripts/devServe.js +0 -3
- package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
- package/dist/scripts/registerAliases.js +0 -6
- package/dist/server/renderer/document/Body.js +0 -11
- package/dist/server/renderer/document/Head.js +0 -9
- package/dist/server/renderer/document/index.js +0 -1
- package/dist/server/renderer/handler.js +0 -47
- package/dist/server/startServer.js +0 -57
- package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
- package/dist/webpack/babel.config.client.js +0 -1
- package/dist/webpack/babel.config.ssr.js +0 -1
- package/dist/webpack/base.babel.js +0 -7
- package/dist/webpack/development.client.babel.js +0 -10
- package/dist/webpack/production.client.babel.js +0 -4
- package/dist/webpack/production.ssr.babel.js +0 -8
- package/dist/webpack/scssParams.js +0 -9
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import React, { useEffect, useState, createContext, useContext, useMemo, useRef } from "react"
|
|
2
|
+
import {
|
|
3
|
+
useLocation,
|
|
4
|
+
useMatch,
|
|
5
|
+
useNavigate,
|
|
6
|
+
useParams,
|
|
7
|
+
useSearchParams,
|
|
8
|
+
UNSAFE_RouteContext,
|
|
9
|
+
matchRoutes,
|
|
10
|
+
} from "react-router-dom"
|
|
11
|
+
import { OneMgRouterContext } from "../context.jsx"
|
|
12
|
+
// import sanitizeHtml from "sanitize-html"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @description Router Data
|
|
16
|
+
* @typedef {{data: any, error: any, isFetching: boolean, isFetched:boolean, refetch?:(args:any)=>Promise<void>}} RouteData
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @type {import("react").Context<Object.<string, RouteData>>}
|
|
21
|
+
*/
|
|
22
|
+
export const RouterContext = createContext({})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef RouterDataProviderConfig
|
|
26
|
+
* @property {boolean} [disableCaching=false] disableCaching disable caching of fetched data - default is false
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initial State of context
|
|
31
|
+
*/
|
|
32
|
+
const INITIAL_DATA_STATE = {
|
|
33
|
+
data: null,
|
|
34
|
+
error: null,
|
|
35
|
+
isFetching: false,
|
|
36
|
+
isFetched: false,
|
|
37
|
+
refetch: () => {},
|
|
38
|
+
clear: () => {},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef RouterFetcherProps
|
|
43
|
+
* @property {any} route route object
|
|
44
|
+
* @property {import("react-router-dom").Location} location the current location object, which represents the current URL in web browsers.
|
|
45
|
+
* @property {import("react-router-dom").Params} params object of key/value pairs of the dynamic params from the current URL that were matched by the route path.
|
|
46
|
+
* @property {URLSearchParams} searchParams search parameters via URLSearchParams interface.
|
|
47
|
+
* @property {import("react-router-dom").NavigateFunction} navigate function to navigate to other pages based on response.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef ServerFetchDataProps
|
|
52
|
+
* @property {import("react-router-dom").RouteObject[]} routes routes Array
|
|
53
|
+
* @property {string} url current url
|
|
54
|
+
* @property {import("express").Request} req Express request object
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @description call this function to fetch data using fetchers defined in routes
|
|
59
|
+
* @param {ServerFetchDataProps} serverFetchDataProps
|
|
60
|
+
* @param {Object.<string, any>} fetcherArgs anything passed in fetcherArgs prop of RouterProvider
|
|
61
|
+
* @returns RoutesData
|
|
62
|
+
*/
|
|
63
|
+
export const serverDataFetcher = async (serverFetchDataProps, fetcherArgs) => {
|
|
64
|
+
/**
|
|
65
|
+
* @type {Object.<string, RouteData>}
|
|
66
|
+
*/
|
|
67
|
+
const routesData = {}
|
|
68
|
+
const { routes, url, req, res: responseInstance } = serverFetchDataProps
|
|
69
|
+
const matchedRoutes = matchRoutes(routes, url)
|
|
70
|
+
if (matchedRoutes) {
|
|
71
|
+
const searchParams = req.query
|
|
72
|
+
const params = matchedRoutes[matchedRoutes.length - 1].params
|
|
73
|
+
let searchParamsString = ""
|
|
74
|
+
if (searchParams) {
|
|
75
|
+
for (const key in searchParams) {
|
|
76
|
+
searchParamsString += `${key}=${searchParams[key]}&`
|
|
77
|
+
}
|
|
78
|
+
searchParamsString = searchParamsString.slice(0, -1)
|
|
79
|
+
searchParamsString = searchParamsString ? `?${searchParamsString}` : searchParamsString
|
|
80
|
+
}
|
|
81
|
+
await Promise.allSettled(
|
|
82
|
+
matchedRoutes.map(async (match) => {
|
|
83
|
+
const res = await fetchRouteData(
|
|
84
|
+
{ req, res: responseInstance, route: match.route, params, searchParams },
|
|
85
|
+
fetcherArgs
|
|
86
|
+
)
|
|
87
|
+
routesData[generateRouteKey(match, searchParamsString)] = res
|
|
88
|
+
})
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return routesData
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @description call this function to fetch data using fetchers defined in [page].fetcher.js
|
|
96
|
+
* @param {RouterFetcherProps} routerProps
|
|
97
|
+
* @param {Object.<string, any>} fetcherArgs anything passed in fetcherArgs prop of RouterProvider
|
|
98
|
+
* @param {Object.<string, any>=} refetchArgs anything passed in argument of refetch function
|
|
99
|
+
* @returns {Promise<RouteData>}
|
|
100
|
+
*/
|
|
101
|
+
const fetchRouteData = async (routerProps, fetcherArgs, refetchArgs) => {
|
|
102
|
+
const routeData = { ...INITIAL_DATA_STATE }
|
|
103
|
+
const { route } = routerProps
|
|
104
|
+
const routeComponent = route.component || route.Component || route.element
|
|
105
|
+
let component = null
|
|
106
|
+
// If component is imported through loadable
|
|
107
|
+
if (typeof routeComponent?.load === "function") {
|
|
108
|
+
try {
|
|
109
|
+
component = await routeComponent.load()
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("Error loading component", err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let fetcher = component?.default?.clientFetcher
|
|
116
|
+
|
|
117
|
+
if (typeof window === "undefined") {
|
|
118
|
+
fetcher = component?.default?.serverFetcher
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (fetcher && typeof fetcher === "function") {
|
|
122
|
+
routeData.fetcherNotAvailable = false
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetcher(routerProps, fetcherArgs, refetchArgs)
|
|
125
|
+
routeData.data = res
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// @ts-ignore
|
|
128
|
+
routeData.error = error
|
|
129
|
+
} finally {
|
|
130
|
+
routeData.isFetching = false
|
|
131
|
+
routeData.isFetched = true
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
routeData.fetcherNotAvailable = true
|
|
135
|
+
}
|
|
136
|
+
return routeData
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
*
|
|
141
|
+
* @param {RouteContextObject} routeContext route context object
|
|
142
|
+
* @returns {import('react-router-dom').RouteMatch[]} Array of matched routes
|
|
143
|
+
*/
|
|
144
|
+
const getMatchedRoutes = ({ matches, outlet }) => {
|
|
145
|
+
if (outlet) {
|
|
146
|
+
return getMatchedRoutes(outlet.props.routeContext)
|
|
147
|
+
}
|
|
148
|
+
return matches
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generates route key for given route using pathname and query params
|
|
153
|
+
* @param {import('react-router-dom').RouteMatch} match Router Match Object
|
|
154
|
+
* @param {string} searchParamsString Query params string
|
|
155
|
+
* @returns {string} routerKey
|
|
156
|
+
*/
|
|
157
|
+
const generateRouteKey = (match, searchParamsString = "") => {
|
|
158
|
+
const { pathname, route } = match
|
|
159
|
+
const sanitizedPathname = pathname
|
|
160
|
+
const sanitizedParams = searchParamsString
|
|
161
|
+
if (route.children) {
|
|
162
|
+
return `index${sanitizedPathname}${sanitizedParams}`
|
|
163
|
+
}
|
|
164
|
+
return `${sanitizedPathname}${sanitizedParams}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @typedef RouterDataProviderProps
|
|
169
|
+
* @property {any} initialState Initial State of Data Provider - Mostly used to hydrate client with data from server
|
|
170
|
+
* @property {any} children
|
|
171
|
+
* @property {Object.<string, any>} fetcherArgs anything passed in fetcherArgs is passed to all the fetcher functions
|
|
172
|
+
* @property {RouterDataProviderConfig} config Global router data provider config
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @description Render the child components with router context and execute data fetchers on path change
|
|
177
|
+
* @param {RouterDataProviderProps} props
|
|
178
|
+
* @returns React.JSX.Element
|
|
179
|
+
*/
|
|
180
|
+
export const RouterDataProvider = ({ children, initialState, fetcherArgs = {}, config }) => {
|
|
181
|
+
const match = useMatch("*")
|
|
182
|
+
const location = useLocation()
|
|
183
|
+
const params = useParams()
|
|
184
|
+
const navigate = useNavigate()
|
|
185
|
+
const [searchParams] = useSearchParams()
|
|
186
|
+
|
|
187
|
+
const isHydrated = useRef(false)
|
|
188
|
+
const routeContext = useContext(UNSAFE_RouteContext)
|
|
189
|
+
const matchedRoutes = useMemo(() => getMatchedRoutes(routeContext), [routeContext])
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @description HOF which returns a function to refetch the route data
|
|
193
|
+
* @param {import("react-router-dom").RouteObject} route
|
|
194
|
+
* @param {string} routeKey
|
|
195
|
+
* @returns
|
|
196
|
+
*/
|
|
197
|
+
const refetchData = (route, routeKey) => {
|
|
198
|
+
return async (/** @type {{ [x: string]: any; } | undefined} */ args) => {
|
|
199
|
+
setRouteData((prevData) => ({
|
|
200
|
+
...prevData,
|
|
201
|
+
[routeKey]: { ...INITIAL_DATA_STATE, isFetching: true },
|
|
202
|
+
}))
|
|
203
|
+
const routerDataRes = await fetchRouteData(
|
|
204
|
+
{ route, location, params, searchParams, navigate },
|
|
205
|
+
fetcherArgs,
|
|
206
|
+
args
|
|
207
|
+
)
|
|
208
|
+
setRouteData((prevData) => ({
|
|
209
|
+
...prevData,
|
|
210
|
+
[routeKey]: { ...routerDataRes, fetcherNotAvailable: false },
|
|
211
|
+
}))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @description HOF which returns a function to clear the route data immediately or after given time in ms
|
|
217
|
+
* @param {import("react-router-dom").RouteObject} route
|
|
218
|
+
* @param {string} routeKey
|
|
219
|
+
* @returns
|
|
220
|
+
*/
|
|
221
|
+
const clear =
|
|
222
|
+
(routeKey) =>
|
|
223
|
+
(wait = 0) => {
|
|
224
|
+
// TODO :: Need to think this use case
|
|
225
|
+
// eslint-disable-next-line no-unused-vars
|
|
226
|
+
const timeout = setTimeout(() => {
|
|
227
|
+
setRouteData((prevData) => ({ ...prevData, [routeKey]: { ...INITIAL_DATA_STATE } }))
|
|
228
|
+
}, [wait])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @type {[Object.<string, RouteData>,React.Dispatch<React.SetStateAction<Object.<string, RouteData>>>]}
|
|
233
|
+
*/
|
|
234
|
+
const [routeData, setRouteData] = useState(initialState)
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @description Check the config for refetching the data
|
|
238
|
+
* @param {import('react-router-dom').RouteObject} route
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
const shouldFetch = (route) => {
|
|
242
|
+
// do not refetch on first render if we get something in initialState
|
|
243
|
+
if (!isHydrated.current && initialState) return false
|
|
244
|
+
|
|
245
|
+
// config at route level over rides config at global level
|
|
246
|
+
if (typeof route.disableCaching === "boolean") {
|
|
247
|
+
return route.disableCaching
|
|
248
|
+
} else if (typeof config.disableCaching === "boolean") {
|
|
249
|
+
// refetch if caching is disabled at global level
|
|
250
|
+
return config.disableCaching
|
|
251
|
+
}
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
matchedRoutes.forEach(async (match) => {
|
|
257
|
+
const route = match.route
|
|
258
|
+
const routeKey = generateRouteKey(match, location.search)
|
|
259
|
+
if (routeData[routeKey]?.isFetched && !shouldFetch(route)) return
|
|
260
|
+
setRouteData((prevData) => ({
|
|
261
|
+
...(prevData || {}),
|
|
262
|
+
[routeKey]: { ...INITIAL_DATA_STATE, isFetching: true },
|
|
263
|
+
}))
|
|
264
|
+
const routerDataRes = await fetchRouteData(
|
|
265
|
+
{ route, location, params, searchParams, navigate },
|
|
266
|
+
fetcherArgs
|
|
267
|
+
)
|
|
268
|
+
setRouteData((prevData) => ({ ...prevData, [routeKey]: { ...routerDataRes } }))
|
|
269
|
+
})
|
|
270
|
+
}, [match.pathname, match.params])
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<OneMgRouterContext.Provider value={{ matchedRoutes, refetchData, clear }}>
|
|
274
|
+
<RouterContext.Provider value={{ ...routeData, refetch: refetchData }}>
|
|
275
|
+
{children}
|
|
276
|
+
</RouterContext.Provider>
|
|
277
|
+
</OneMgRouterContext.Provider>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @description returns current router context object with three values: data, error, isFetching, isFetched
|
|
283
|
+
* @returns {RouteData}
|
|
284
|
+
* @throws If used outside RouterDataProvider Context
|
|
285
|
+
*/
|
|
286
|
+
export const useCurrentRouteData = () => {
|
|
287
|
+
const routeContext = useContext(UNSAFE_RouteContext)
|
|
288
|
+
const currentPageMatch = routeContext.matches[routeContext.matches.length - 1]
|
|
289
|
+
const context = useContext(RouterContext)
|
|
290
|
+
const { refetchData, clear } = useContext(OneMgRouterContext)
|
|
291
|
+
const location = useLocation()
|
|
292
|
+
// Throw error if the hook is not used within a RouterProvider
|
|
293
|
+
if (context === undefined) {
|
|
294
|
+
throw new Error("useCurrentRouteData must be used within a RouterDataProvider")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const routeKey = generateRouteKey(currentPageMatch, location.search)
|
|
298
|
+
let currentPageData = context[routeKey]
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (currentPageData?.fetcherNotAvailable) {
|
|
302
|
+
refetchData(currentPageMatch.route, routeKey)()
|
|
303
|
+
}
|
|
304
|
+
}, [])
|
|
305
|
+
|
|
306
|
+
if (!currentPageData) return { ...INITIAL_DATA_STATE }
|
|
307
|
+
|
|
308
|
+
if (currentPageData) {
|
|
309
|
+
delete currentPageData.fetcherNotAvailable
|
|
310
|
+
currentPageData = {
|
|
311
|
+
...currentPageData,
|
|
312
|
+
refetch: refetchData(currentPageMatch.route, routeKey),
|
|
313
|
+
clear: clear(routeKey),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return currentPageData
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @description returns a router context object with data of all the fetchers in current route tree
|
|
321
|
+
* @returns {Object.<string, RouteData>}
|
|
322
|
+
* @throws If used outside RouterDataProvider Context
|
|
323
|
+
*/
|
|
324
|
+
export const useRouterData = () => {
|
|
325
|
+
const context = useContext(RouterContext)
|
|
326
|
+
// Throw error if the hook is not used within a RouterProvider
|
|
327
|
+
if (context === undefined) {
|
|
328
|
+
throw new Error("useRouterData must be used within a RouterDataProvider")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return context
|
|
332
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { Suspense, lazy, useContext, useEffect, useReducer } from "react"
|
|
2
|
+
import { SsrRequestContext } from "./SsrRequestContext.jsx"
|
|
3
|
+
import SplitInview from "./SplitInview.jsx"
|
|
4
|
+
|
|
5
|
+
// Synchronous module cache: importFn → resolved module.
|
|
6
|
+
// Populated by the eager importFn().then() calls at split() invocation time.
|
|
7
|
+
// By the time window.load fires (when hydrateRoot runs), all chunk <script>
|
|
8
|
+
// tags in the HTML have already executed, so every .then() has already
|
|
9
|
+
// resolved and the module is available here synchronously.
|
|
10
|
+
const moduleCache = new Map()
|
|
11
|
+
|
|
12
|
+
// Collects one promise per SSR-rendered split() call on the client.
|
|
13
|
+
// loadableReady() waits for all of them before hydration begins.
|
|
14
|
+
const prefetchPromises = []
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a promise that resolves once every SSR-rendered split component
|
|
18
|
+
* has been prefetched and stored in moduleCache. Call this before
|
|
19
|
+
* hydrateRoot so the first render has all modules available synchronously
|
|
20
|
+
* and no Suspense fallback is shown.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* hydrationReady().then(() => {
|
|
24
|
+
* hydrateRoot(document.getElementById("root"), <App />)
|
|
25
|
+
* })
|
|
26
|
+
*/
|
|
27
|
+
export const hydrationReady = () => Promise.all(prefetchPromises)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Split component that wraps React's lazy and Suspense for SSR compatibility
|
|
31
|
+
* @param {Object} props
|
|
32
|
+
* @param {boolean} props.ssr - Whether to render the component on the server
|
|
33
|
+
* @param {React.ComponentType|React.ReactElement} props.fallback - Fallback component for loading state
|
|
34
|
+
* @param {Function} props.children - Function that returns the lazy component import
|
|
35
|
+
* @param {string} props.cacheKey - Resolved path for better asset tracking
|
|
36
|
+
*/
|
|
37
|
+
const Split = ({
|
|
38
|
+
ssr = true,
|
|
39
|
+
fallback = null,
|
|
40
|
+
cacheKey,
|
|
41
|
+
rootOptions,
|
|
42
|
+
onVisible,
|
|
43
|
+
skipVisibility,
|
|
44
|
+
children,
|
|
45
|
+
}) => {
|
|
46
|
+
// Check if we're on the server
|
|
47
|
+
const isServer = typeof window === "undefined"
|
|
48
|
+
if (isServer) {
|
|
49
|
+
if (ssr) {
|
|
50
|
+
// On server with SSR enabled: actually load and render the component
|
|
51
|
+
try {
|
|
52
|
+
// Track this component for asset extraction
|
|
53
|
+
if (global.__CHUNK_EXTRACTOR__) {
|
|
54
|
+
global.__CHUNK_EXTRACTOR__.addComponent(cacheKey)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return <Suspense fallback={fallback}>{children}</Suspense>
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn("Error loading component for SSR:", error)
|
|
60
|
+
return fallback
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Match SplitInview's client-side wrapper so hydration doesn't mismatch.
|
|
64
|
+
// SplitInview renders <div ref>{fallback}</div> until visible; without this
|
|
65
|
+
// wrap, server outputs `fallback` and client outputs `<div>{fallback}</div>`.
|
|
66
|
+
return <div>{fallback}</div>
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
if (skipVisibility) {
|
|
70
|
+
return <Suspense fallback={fallback}>{children}</Suspense>
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
<SplitInview fallback={fallback} rootOptions={rootOptions} onVisible={onVisible}>
|
|
74
|
+
<Suspense fallback={fallback}>{children}</Suspense>
|
|
75
|
+
</SplitInview>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Like {@link split}, but forces SSR when the request is a known Google crawler (same UA rules as Head).
|
|
82
|
+
* Use for widgets that are `ssr: false` for humans but must be fully rendered for bots.
|
|
83
|
+
*
|
|
84
|
+
* Prefetch follows `window.__SSR_RENDERED_COMPONENTS__` only (not the `ssr` option) so bot-forced SSR
|
|
85
|
+
* still hydrates without a Suspense flash.
|
|
86
|
+
*/
|
|
87
|
+
export const split = (importFn, options = {}, thirdArg, fourthArg) => {
|
|
88
|
+
const { ssr = true, fallback = null } = options || {}
|
|
89
|
+
const hasThirdArg = typeof thirdArg !== "undefined"
|
|
90
|
+
const hasFourthArg = typeof fourthArg !== "undefined"
|
|
91
|
+
const cacheKey =
|
|
92
|
+
typeof fourthArg === "string" ? fourthArg : typeof thirdArg === "string" ? thirdArg : undefined
|
|
93
|
+
const rootOptions = hasFourthArg
|
|
94
|
+
? thirdArg
|
|
95
|
+
: hasThirdArg && typeof thirdArg !== "string"
|
|
96
|
+
? thirdArg
|
|
97
|
+
: undefined
|
|
98
|
+
|
|
99
|
+
const LazyComponent = lazy(importFn)
|
|
100
|
+
let loadInFlight = null
|
|
101
|
+
|
|
102
|
+
// Per-split instance subscribers. Pending wrapper instances register their
|
|
103
|
+
// forceUpdate here; notifyAll() wakes them when load() resolves or any
|
|
104
|
+
// sibling becomes visible, so they can re-render against the now-hot cache
|
|
105
|
+
// or skip their own observer setup.
|
|
106
|
+
const subscribers = new Set()
|
|
107
|
+
let anyVisible = false
|
|
108
|
+
const notifyAll = () => {
|
|
109
|
+
anyVisible = true
|
|
110
|
+
subscribers.forEach((fn) => fn())
|
|
111
|
+
}
|
|
112
|
+
const copyRouteStatics = (mod) => {
|
|
113
|
+
const Component = mod?.default || mod
|
|
114
|
+
for (const key of ["clientFetcher", "serverFetcher", "setMetaData"]) {
|
|
115
|
+
if (Component?.[key]) wrapper[key] = Component[key]
|
|
116
|
+
}
|
|
117
|
+
return mod
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof window !== "undefined" && window.__SSR_RENDERED_COMPONENTS__?.has(cacheKey)) {
|
|
121
|
+
const prefetch = importFn().then((mod) => {
|
|
122
|
+
moduleCache.set(importFn, mod)
|
|
123
|
+
copyRouteStatics(mod)
|
|
124
|
+
})
|
|
125
|
+
prefetchPromises.push(prefetch)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const wrapper = ({ fallback: fallbackProp, ...props }) => {
|
|
129
|
+
const { isBot: isBotFromContext } = useContext(SsrRequestContext)
|
|
130
|
+
const isBotFromWindow = typeof window !== "undefined" && window.__CATALYST_IS_BOT__ === true
|
|
131
|
+
const isBot = Boolean(isBotFromContext || isBotFromWindow)
|
|
132
|
+
const effectiveSsr = ssr || isBot
|
|
133
|
+
const effectiveFallback = fallbackProp !== undefined ? fallbackProp : fallback
|
|
134
|
+
|
|
135
|
+
const [, forceUpdate] = useReducer((x) => x + 1, 0)
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
subscribers.add(forceUpdate)
|
|
138
|
+
return () => {
|
|
139
|
+
subscribers.delete(forceUpdate)
|
|
140
|
+
}
|
|
141
|
+
}, [])
|
|
142
|
+
|
|
143
|
+
const mod = moduleCache.get(importFn)
|
|
144
|
+
if (mod) {
|
|
145
|
+
const Component = mod.default || mod
|
|
146
|
+
return (
|
|
147
|
+
<Suspense fallback={effectiveFallback}>
|
|
148
|
+
<Component {...props} />
|
|
149
|
+
</Suspense>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Split
|
|
155
|
+
ssr={effectiveSsr}
|
|
156
|
+
fallback={effectiveFallback}
|
|
157
|
+
cacheKey={cacheKey}
|
|
158
|
+
rootOptions={rootOptions}
|
|
159
|
+
onVisible={() => {
|
|
160
|
+
notifyAll()
|
|
161
|
+
props.onVisible?.()
|
|
162
|
+
}}
|
|
163
|
+
skipVisibility={effectiveSsr || anyVisible}
|
|
164
|
+
{...props}
|
|
165
|
+
isBot={isBot}
|
|
166
|
+
>
|
|
167
|
+
<LazyComponent {...props} />
|
|
168
|
+
</Split>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
wrapper.__cacheKey = cacheKey
|
|
173
|
+
|
|
174
|
+
/** Same contract as loadable components: RouterDataProvider awaits this before reading serverFetcher/clientFetcher. */
|
|
175
|
+
wrapper.load = () => {
|
|
176
|
+
const cached = moduleCache.get(importFn)
|
|
177
|
+
if (cached) return Promise.resolve(cached)
|
|
178
|
+
if (!loadInFlight) {
|
|
179
|
+
loadInFlight = importFn()
|
|
180
|
+
.then((mod) => {
|
|
181
|
+
copyRouteStatics(mod)
|
|
182
|
+
if (typeof window !== "undefined") {
|
|
183
|
+
moduleCache.set(importFn, mod)
|
|
184
|
+
}
|
|
185
|
+
loadInFlight = null
|
|
186
|
+
notifyAll()
|
|
187
|
+
return mod
|
|
188
|
+
})
|
|
189
|
+
.catch((err) => {
|
|
190
|
+
loadInFlight = null
|
|
191
|
+
throw err
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
return loadInFlight
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return wrapper
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default Split
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react"
|
|
2
|
+
|
|
3
|
+
// Load components 75% of viewport below the fold so they're ready by the time
|
|
4
|
+
// the user scrolls to them. Matches react-loadable-visibility's default.
|
|
5
|
+
const DEFAULT_ROOT_MARGIN = "0px 0px 75% 0px"
|
|
6
|
+
|
|
7
|
+
// Shared IntersectionObserver for the default (no rootOptions) path.
|
|
8
|
+
// Each observed element maps to a callback that fires once and unobserves.
|
|
9
|
+
const callbacks = new Map()
|
|
10
|
+
let sharedObserver = null
|
|
11
|
+
|
|
12
|
+
function getSharedObserver() {
|
|
13
|
+
if (sharedObserver) return sharedObserver
|
|
14
|
+
sharedObserver = new IntersectionObserver(
|
|
15
|
+
(entries) => {
|
|
16
|
+
entries.forEach((entry) => {
|
|
17
|
+
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
|
18
|
+
const cb = callbacks.get(entry.target)
|
|
19
|
+
if (cb) {
|
|
20
|
+
cb()
|
|
21
|
+
callbacks.delete(entry.target)
|
|
22
|
+
sharedObserver.unobserve(entry.target)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
{ rootMargin: DEFAULT_ROOT_MARGIN }
|
|
28
|
+
)
|
|
29
|
+
return sharedObserver
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasCustomOptions = (o) =>
|
|
33
|
+
o && typeof o === "object" && (o.root != null || o.rootMargin != null || o.threshold != null)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Defers rendering of children until the placeholder enters the viewport.
|
|
37
|
+
* Once visible, it stays visible.
|
|
38
|
+
*
|
|
39
|
+
* On the server or when IntersectionObserver is unavailable, children render immediately.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} props
|
|
42
|
+
* @param {React.ReactNode} props.fallback - Shown while the component is outside the viewport
|
|
43
|
+
* @param {React.ReactNode} props.children - Rendered once the placeholder scrolls into view
|
|
44
|
+
* @param {Object=} props.rootOptions - Optional IntersectionObserver options for parity with react-loadable-visibility
|
|
45
|
+
* @param {Function=} props.onVisible - Called once when the placeholder enters the viewport
|
|
46
|
+
*/
|
|
47
|
+
const SplitInview = ({ fallback = null, children, rootOptions, onVisible }) => {
|
|
48
|
+
const isServer = typeof window === "undefined"
|
|
49
|
+
const [isVisible, setIsVisible] = useState(() => {
|
|
50
|
+
if (isServer) return true
|
|
51
|
+
if (!window.IntersectionObserver) return true
|
|
52
|
+
return false
|
|
53
|
+
})
|
|
54
|
+
const ref = useRef(null)
|
|
55
|
+
// Freeze rootOptions at mount. IO options are not expected to change mid-lifecycle,
|
|
56
|
+
// so we avoid array/object identity churn (e.g. inline `threshold: [0, 0.5, 1]`).
|
|
57
|
+
const rootOptionsRef = useRef(rootOptions)
|
|
58
|
+
const onVisibleRef = useRef(onVisible)
|
|
59
|
+
onVisibleRef.current = onVisible
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (isVisible) return
|
|
63
|
+
const node = ref.current
|
|
64
|
+
if (!node) return
|
|
65
|
+
|
|
66
|
+
const opts = rootOptionsRef.current
|
|
67
|
+
const fire = () => {
|
|
68
|
+
setIsVisible(true)
|
|
69
|
+
onVisibleRef.current?.()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!hasCustomOptions(opts)) {
|
|
73
|
+
callbacks.set(node, fire)
|
|
74
|
+
getSharedObserver().observe(node)
|
|
75
|
+
return () => {
|
|
76
|
+
callbacks.delete(node)
|
|
77
|
+
if (sharedObserver) sharedObserver.unobserve(node)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const observer = new IntersectionObserver(
|
|
82
|
+
(entries) => {
|
|
83
|
+
entries.forEach((entry) => {
|
|
84
|
+
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
|
85
|
+
fire()
|
|
86
|
+
observer.unobserve(entry.target)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
root: opts.root ?? null,
|
|
92
|
+
rootMargin: opts.rootMargin ?? DEFAULT_ROOT_MARGIN,
|
|
93
|
+
threshold: opts.threshold,
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
observer.observe(node)
|
|
98
|
+
return () => observer.disconnect()
|
|
99
|
+
}, [isVisible])
|
|
100
|
+
|
|
101
|
+
if (isVisible) return children
|
|
102
|
+
|
|
103
|
+
return <div ref={ref}>{fallback}</div>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default SplitInview
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { createContext } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SSR request flags (e.g. crawler UA). Set by the document handler via SsrRequestProvider.
|
|
5
|
+
* Kept separate from Split.jsx so the server handler does not import lazy/Suspense code.
|
|
6
|
+
* `window.__CATALYST_IS_BOT__` is inlined for bots that execute JS so hydration matches the server.
|
|
7
|
+
*/
|
|
8
|
+
export const SsrRequestContext = createContext({ isBot: false })
|
|
9
|
+
|
|
10
|
+
export function SsrRequestProvider({ value, children }) {
|
|
11
|
+
return (
|
|
12
|
+
<SsrRequestContext.Provider value={value ?? { isBot: false }}>{children}</SsrRequestContext.Provider>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef oneMgRouterContext
|
|
5
|
+
* @property {import("react-router-dom").RouteMatch<string, import("react-router-dom").RouteObject>[]=} matchedRoutes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @description This is internal context made for sharing data among router components
|
|
10
|
+
* @type {import("react").Context<oneMgRouterContext>}
|
|
11
|
+
*/
|
|
12
|
+
export const OneMgRouterContext = createContext({})
|