catalyst-core 0.2.0-beta.3 → 0.3.0-beta.2

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.
Files changed (167) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/globals.d.ts +7 -0
  6. package/dist/index.jsx +8 -0
  7. package/dist/logger.js +74 -3
  8. package/dist/native/androidProject/app/build.gradle.kts +45 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +86 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +173 -13
  12. package/dist/native/androidProject/settings.gradle.kts +10 -0
  13. package/dist/native/androidSetup.js +300 -8
  14. package/dist/native/bridge/WebBridge.js +481 -59
  15. package/dist/native/bridge/constants/NativeInterfaces.js +304 -45
  16. package/dist/native/bridge/errors.js +250 -26
  17. package/dist/native/bridge/hooks/index.js +132 -1
  18. package/dist/native/bridge/hooks/useAI.js +94 -0
  19. package/dist/native/bridge/hooks/useCamera.js +347 -9
  20. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  21. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  22. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  23. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  24. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  25. package/dist/native/bridge/hooks/useIntent.js +95 -1
  26. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  27. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  28. package/dist/native/bridge/hooks/useNotification.js +318 -2
  29. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  30. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  31. package/dist/native/bridge/hooks/utils.js +27 -1
  32. package/dist/native/bridge/hooks.js +16 -1
  33. package/dist/native/bridge/perf/collectors/ApiCollector.js +66 -3
  34. package/dist/native/bridge/perf/collectors/CacheCollector.js +186 -11
  35. package/dist/native/bridge/perf/collectors/HookCollector.js +141 -19
  36. package/dist/native/bridge/perf/collectors/InsightsCollector.js +481 -23
  37. package/dist/native/bridge/perf/collectors/InteractionCollector.js +245 -10
  38. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +107 -8
  39. package/dist/native/bridge/perf/collectors/NavigationCollector.js +273 -32
  40. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +178 -2
  41. package/dist/native/bridge/perf/collectors/RenderCollector.js +258 -14
  42. package/dist/native/bridge/perf/collectors/ScrollCollector.js +95 -9
  43. package/dist/native/bridge/perf/collectors/ThreadCollector.js +90 -6
  44. package/dist/native/bridge/perf/core/constants.js +103 -20
  45. package/dist/native/bridge/perf/core/measure.js +179 -30
  46. package/dist/native/bridge/perf/core/session.js +89 -23
  47. package/dist/native/bridge/perf/core/store.js +554 -1
  48. package/dist/native/bridge/perf/core/summary.js +174 -1
  49. package/dist/native/bridge/perf/core/traceExport.js +240 -1
  50. package/dist/native/bridge/perf/core/waterfalls.js +146 -1
  51. package/dist/native/bridge/perf/index.js +359 -38
  52. package/dist/native/bridge/safeArea.js +53 -5
  53. package/dist/native/bridge/standardizedHooks.js +595 -82
  54. package/dist/native/bridge/useBaseHook.js +274 -38
  55. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  56. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  57. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  58. package/dist/native/buildAndroid/assets.js +510 -13
  59. package/dist/native/buildAndroid/build.js +296 -19
  60. package/dist/native/buildAndroid/config.js +87 -1
  61. package/dist/native/buildAndroid/index.js +406 -1
  62. package/dist/native/buildAppAndroid.js +34 -2
  63. package/dist/native/buildAppIos.js +62 -2
  64. package/dist/native/buildIos/assets.js +609 -8
  65. package/dist/native/buildIos/build.js +639 -20
  66. package/dist/native/buildIos/config.js +294 -12
  67. package/dist/native/buildIos/index.js +343 -6
  68. package/dist/native/buildIos/plugins.js +343 -16
  69. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  70. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  71. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  72. package/dist/native/internalPluginUtils.js +232 -7
  73. package/dist/native/package.json +3 -0
  74. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  75. package/dist/native/pluginComposerAndroid.js +407 -23
  76. package/dist/native/pluginComposerIos.js +435 -11
  77. package/dist/native/renameAndroidProject.js +1094 -88
  78. package/dist/native/setupEmulatorIos.js +289 -22
  79. package/dist/native/setupServer.js +160 -19
  80. package/dist/native/terminalProgress.js +199 -11
  81. package/dist/native/utils.js +193 -15
  82. package/dist/offline/catalyst-sw.js +145 -1
  83. package/dist/offline/registerServiceWorker.js +33 -1
  84. package/dist/otel.js +375 -46
  85. package/dist/router/ClientRouter.js +14 -2
  86. package/dist/router/ServerRouter.js +9 -1
  87. package/dist/router.js +1 -1
  88. package/dist/scripts/build.js +112 -3
  89. package/dist/scripts/checkVersion.js +3 -1
  90. package/dist/scripts/cleanCache.js +30 -1
  91. package/dist/scripts/generateOfflineManifest.js +116 -1
  92. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  93. package/dist/scripts/plugins.js +309 -1
  94. package/dist/scripts/preServerInit.js +13 -0
  95. package/dist/scripts/scriptUtils.js +54 -3
  96. package/dist/scripts/serve.js +51 -4
  97. package/dist/scripts/start.js +47 -10
  98. package/dist/scripts/validator.js +149 -1
  99. package/dist/sentry.cjs +127 -0
  100. package/dist/sentry.js +125 -5
  101. package/dist/server/expressServer.js +296 -5
  102. package/dist/server/manifestCache.js +36 -0
  103. package/dist/server/renderer/ChunkExtractor.js +144 -0
  104. package/dist/server/renderer/document/Body.jsx +59 -0
  105. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  106. package/dist/server/renderer/document/Head.jsx +60 -0
  107. package/dist/server/renderer/document/index.jsx +2 -0
  108. package/dist/server/renderer/extract.js +149 -31
  109. package/dist/server/renderer/handler.jsx +450 -0
  110. package/dist/server/renderer/index.js +8 -1
  111. package/dist/server/renderer/render.js +47 -0
  112. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  113. package/dist/server/utils/cjsRequire.js +10 -0
  114. package/dist/server/utils/userAgentUtil.js +87 -9
  115. package/dist/server/utils/validator.js +164 -1
  116. package/dist/vite/FastRefresh.jsx +20 -0
  117. package/dist/vite/inject-cache-key-plugin.js +276 -0
  118. package/dist/vite/loadCustomViteConfig.js +22 -0
  119. package/dist/vite/manifest-categorization-plugin.js +455 -0
  120. package/dist/vite/node-loader.mjs +458 -0
  121. package/dist/vite/scssParams.js +21 -0
  122. package/dist/vite/vite.config.client.js +130 -0
  123. package/dist/vite/vite.config.js +498 -0
  124. package/dist/vite/vite.config.server.js +84 -0
  125. package/dist/web-router/components/MetaTag.jsx +34 -0
  126. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  127. package/dist/web-router/components/Split.jsx +200 -0
  128. package/dist/web-router/components/SplitInview.jsx +106 -0
  129. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  130. package/dist/web-router/context.jsx +12 -0
  131. package/dist/web-router/hooks.jsx +72 -0
  132. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  133. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  134. package/dist/web-router/types/context.d.ts +14 -0
  135. package/dist/web-router/types/hooks.d.ts +1 -0
  136. package/dist/web-router/types/index.d.ts +3 -0
  137. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  138. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  139. package/mcp_v2/conversion-tasks.json +35 -17
  140. package/mcp_v2/knowledge-base.json +176 -17
  141. package/mcp_v2/lib/helpers.js +10 -0
  142. package/mcp_v2/mcp.js +40 -8
  143. package/mcp_v2/package.json +1 -0
  144. package/mcp_v2/tools/build.js +161 -37
  145. package/mcp_v2/tools/conversion.js +106 -5
  146. package/mcp_v2/tools/tasks.js +18 -14
  147. package/package.json +35 -68
  148. package/babel.config.js +0 -32
  149. package/dist/index.js +0 -1
  150. package/dist/scripts/devBuild.js +0 -3
  151. package/dist/scripts/devServe.js +0 -3
  152. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  153. package/dist/scripts/registerAliases.js +0 -6
  154. package/dist/server/renderer/document/Body.js +0 -11
  155. package/dist/server/renderer/document/Head.js +0 -9
  156. package/dist/server/renderer/document/index.js +0 -1
  157. package/dist/server/renderer/handler.js +0 -47
  158. package/dist/server/startServer.js +0 -57
  159. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  160. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  161. package/dist/webpack/babel.config.client.js +0 -1
  162. package/dist/webpack/babel.config.ssr.js +0 -1
  163. package/dist/webpack/base.babel.js +0 -7
  164. package/dist/webpack/development.client.babel.js +0 -10
  165. package/dist/webpack/production.client.babel.js +0 -4
  166. package/dist/webpack/production.ssr.babel.js +0 -8
  167. 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({})