@squide/firefly 3.0.3 → 4.0.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # @squide/firefly
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#131](https://github.com/gsoft-inc/wl-squide/pull/131) [`7caa44b`](https://github.com/gsoft-inc/wl-squide/commit/7caa44ba81a97d0705caf2f56e6536ae285c920d) Thanks [@patricklafrance](https://github.com/patricklafrance)! - - The `AppRouter` component now requires to define a `RouterProvider` as a child. This change has been made to provide more flexibility on the consumer side about the definition of the React Router router.
8
+
9
+ Before:
10
+
11
+ ```tsx
12
+ <AppRouter
13
+ fallbackElement={...}
14
+ errorElement={...}
15
+ waitForMsw={...}
16
+ />
17
+ ```
18
+
19
+ Now:
20
+
21
+ ```tsx
22
+ <AppRouter
23
+ fallbackElement={...}
24
+ errorElement={...}
25
+ waitForMsw={...}
26
+ >
27
+ {(routes, providerProps) => (
28
+ <RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
29
+ )}
30
+ </AppRouter>
31
+ ```
32
+
33
+ - When in development and using React strict mode, the public and protected handler can be called twice. This issue highlighted that the `AppRouter` component doesn't equipe correctly the handlers to dispose of previous HTTP requests if they are called multiple times because of re-renders. Therefore, the handlers now receives an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that should be forwared to the HTTP client initiating the fetch request.
34
+ - The fix also requires the consumer to provide new properties (`isPublicDataLoaded` and `isProtectedDataLoaded`) indicating whether or not the public and/or protected data has been loaded.
35
+
36
+ ```tsx
37
+ async function fetchPublicData(setFeatureFlags: (featureFlags: FeatureFlags) => void, signal: AbortSignal) {
38
+ try {
39
+ const response = await fetch("/api/feature-flags", {
40
+ signal
41
+ });
42
+
43
+ if (response.ok) {
44
+ const data = await response.json();
45
+
46
+ setFeatureFlags(data);
47
+ }
48
+ } catch (error: unknown) {
49
+ if (!signal.aborted) {
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+
55
+ const [featureFlags, setFeatureFlags] = useState<FeatureFlags>();
56
+
57
+ const handleLoadPublicData = useCallback((signal: AbortSignal) => {
58
+ return fetchPublicData(setFeatureFlags, signal);
59
+ }, []);
60
+
61
+ <AppRouter
62
+ onLoadPublicData={handleLoadPublicData}
63
+ isPublicDataLoaded={!!featureFlags}
64
+ fallbackElement={...}
65
+ errorElement={...}
66
+ waitForMsw={...}
67
+ >
68
+ {(routes, providerProps) => (
69
+ <RouterProvider router={createBrowserRouter(routes)} {...providerProps} />
70
+ )}
71
+ </AppRouter>
72
+ ```
73
+
74
+ - Fixed an issue where the deferred registrations could be completed before the protected data has been loaded.
75
+
76
+ ### Patch Changes
77
+
78
+ - Updated dependencies [[`7caa44b`](https://github.com/gsoft-inc/wl-squide/commit/7caa44ba81a97d0705caf2f56e6536ae285c920d), [`7caa44b`](https://github.com/gsoft-inc/wl-squide/commit/7caa44ba81a97d0705caf2f56e6536ae285c920d)]:
79
+ - @squide/core@3.2.0
80
+ - @squide/react-router@4.0.0
81
+ - @squide/msw@2.0.8
82
+ - @squide/webpack-module-federation@3.0.3
83
+ - @squide/webpack-configs@1.1.2
84
+
3
85
  ## 3.0.3
4
86
 
5
87
  ### Patch Changes
@@ -1,29 +1,36 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { ReactElement } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Route } from '@squide/react-router';
3
5
  import { RouterProviderProps } from 'react-router-dom';
4
6
 
5
- type OnLoadPublicDataFunction = () => Promise<unknown>;
6
- type OnLoadProtectedDataFunction = () => Promise<unknown>;
7
+ type OnLoadPublicDataFunction = (signal: AbortSignal) => Promise<unknown>;
8
+ type OnLoadProtectedDataFunction = (signal: AbortSignal) => Promise<unknown>;
7
9
  type OnCompleteRegistrationsFunction = () => Promise<unknown>;
8
10
  interface BootstrappingRouteProps {
9
11
  fallbackElement: ReactElement;
10
12
  onLoadPublicData?: OnLoadPublicDataFunction;
11
13
  onLoadProtectedData?: OnLoadProtectedDataFunction;
14
+ isPublicDataLoaded: boolean;
15
+ isProtectedDataLoaded: boolean;
12
16
  onCompleteRegistrations?: OnCompleteRegistrationsFunction;
13
17
  waitForMsw: boolean;
14
18
  areModulesRegistered: boolean;
15
19
  areModulesReady: boolean;
16
20
  }
17
21
  declare function BootstrappingRoute(props: BootstrappingRouteProps): react_jsx_runtime.JSX.Element;
22
+ type RenderRouterProviderFunction = (routes: Route[], providerProps: Omit<RouterProviderProps, "router">) => ReactElement;
18
23
  interface AppRouterProps {
19
24
  fallbackElement: ReactElement;
20
25
  errorElement: ReactElement;
21
26
  onLoadPublicData?: OnLoadPublicDataFunction;
22
27
  onLoadProtectedData?: OnLoadProtectedDataFunction;
28
+ isPublicDataLoaded?: boolean;
29
+ isProtectedDataLoaded?: boolean;
23
30
  onCompleteRegistrations?: OnCompleteRegistrationsFunction;
24
31
  waitForMsw: boolean;
25
- routerProvidersProps?: Omit<RouterProviderProps, "router">;
32
+ children: RenderRouterProviderFunction;
26
33
  }
27
- declare function AppRouter(props: AppRouterProps): react_jsx_runtime.JSX.Element;
34
+ declare function AppRouter(props: AppRouterProps): ReactElement<any, string | react.JSXElementConstructor<any>>;
28
35
 
29
- export { AppRouter, type AppRouterProps, BootstrappingRoute, type OnCompleteRegistrationsFunction, type OnLoadProtectedDataFunction, type OnLoadPublicDataFunction };
36
+ export { AppRouter, type AppRouterProps, BootstrappingRoute, type OnCompleteRegistrationsFunction, type OnLoadProtectedDataFunction, type OnLoadPublicDataFunction, type RenderRouterProviderFunction };
package/dist/AppRouter.js CHANGED
@@ -1 +1 @@
1
- export { AppRouter, BootstrappingRoute } from './chunk-MKFAFYJH.js';
1
+ export { AppRouter, BootstrappingRoute } from './chunk-QZACR5NK.js';
@@ -0,0 +1,150 @@
1
+ import { useLogOnceLogger, isNil } from '@squide/core';
2
+ import { useIsMswStarted } from '@squide/msw';
3
+ import { useIsRouteMatchProtected, useRoutes } from '@squide/react-router';
4
+ import { useAreModulesRegistered, useAreModulesReady } from '@squide/webpack-module-federation';
5
+ import { useEffect, useCallback, cloneElement, useMemo } from 'react';
6
+ import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
7
+ import { useLocation, Outlet } from 'react-router-dom';
8
+ import { jsx } from 'react/jsx-runtime';
9
+
10
+ // src/AppRouter.tsx
11
+ function useLoadPublicData(areModulesRegistered, areModulesReady, isMswStarted, isLoaded, onLoadData) {
12
+ const logger = useLogOnceLogger();
13
+ const { showBoundary } = useErrorBoundary();
14
+ useEffect(() => {
15
+ if (onLoadData && !isLoaded) {
16
+ if ((areModulesRegistered || areModulesReady) && isMswStarted) {
17
+ logger.debugOnce("loading-public-data", "[shell] Loading public data.");
18
+ const abortController = new AbortController();
19
+ const result = onLoadData(abortController.signal);
20
+ if (!isPromise(result)) {
21
+ throw Error("[squide] An AppRouter onLoadPublicData handler must return a promise object.");
22
+ }
23
+ result.then(() => {
24
+ logger.debugOnce("public-data-loaded", "[shell] Public data has been loaded.");
25
+ }).catch((error) => {
26
+ showBoundary(error);
27
+ });
28
+ return () => {
29
+ abortController.abort();
30
+ };
31
+ }
32
+ }
33
+ }, [areModulesRegistered, areModulesReady, isMswStarted, isLoaded, showBoundary, onLoadData, logger]);
34
+ }
35
+ function useLoadProtectedData(areModulesRegistered, areModulesReady, isMswStarted, isActiveRouteProtected, isLoaded, onLoadData) {
36
+ const logger = useLogOnceLogger();
37
+ const { showBoundary } = useErrorBoundary();
38
+ useEffect(() => {
39
+ if (onLoadData && !isLoaded) {
40
+ if ((areModulesRegistered || areModulesReady) && isMswStarted) {
41
+ if (isActiveRouteProtected) {
42
+ logger.debugOnce("loading-protected-data", `[shell] Loading protected data as "${location.pathname}" is a protected route.`);
43
+ const abortController = new AbortController();
44
+ const result = onLoadData(abortController.signal);
45
+ if (!isPromise(result)) {
46
+ throw Error("[squide] An AppRouter onLoadProtectedData handler must return a promise object.");
47
+ }
48
+ result.then(() => {
49
+ logger.debugOnce("protected-data-loaded", "[shell] Protected data has been loaded.");
50
+ }).catch((error) => {
51
+ showBoundary(error);
52
+ });
53
+ return () => {
54
+ abortController.abort();
55
+ };
56
+ } else {
57
+ logger.debugOnce("is-a-public-route", `[shell] Not loading protected data as "${location.pathname}" is a public route.`);
58
+ }
59
+ }
60
+ }
61
+ }, [areModulesRegistered, areModulesReady, isMswStarted, isActiveRouteProtected, isLoaded, showBoundary, onLoadData, logger]);
62
+ }
63
+ function isPromise(value) {
64
+ return !isNil(value) && !isNil(value.then) && !isNil(value.catch);
65
+ }
66
+ function BootstrappingRoute(props) {
67
+ const {
68
+ fallbackElement,
69
+ onLoadPublicData,
70
+ onLoadProtectedData,
71
+ isPublicDataLoaded,
72
+ isProtectedDataLoaded,
73
+ onCompleteRegistrations,
74
+ waitForMsw,
75
+ areModulesRegistered,
76
+ areModulesReady
77
+ } = props;
78
+ const logger = useLogOnceLogger();
79
+ const location2 = useLocation();
80
+ const isMswStarted = useIsMswStarted(waitForMsw);
81
+ useEffect(() => {
82
+ if (waitForMsw) {
83
+ if ((areModulesRegistered || areModulesReady) && !isMswStarted) {
84
+ logger.debugOnce("waiting-for-msw", `[shell] Modules are ${areModulesReady ? "ready" : "registered"}, waiting for MSW to start...`);
85
+ } else if (!areModulesRegistered && !areModulesReady && isMswStarted) {
86
+ logger.debugOnce("waiting-for-modules", "[shell] MSW is started, waiting for the modules...");
87
+ }
88
+ }
89
+ }, [logger, areModulesRegistered, areModulesReady, isMswStarted, waitForMsw]);
90
+ useLoadPublicData(areModulesRegistered, areModulesReady, isMswStarted, isPublicDataLoaded, onLoadPublicData);
91
+ const isActiveRouteProtected = useIsRouteMatchProtected(location2, { throwWhenThereIsNoMatch: areModulesReady });
92
+ useLoadProtectedData(areModulesRegistered, areModulesReady, isMswStarted, isActiveRouteProtected, isProtectedDataLoaded, onLoadProtectedData);
93
+ useEffect(() => {
94
+ if (onCompleteRegistrations) {
95
+ if (areModulesRegistered && isMswStarted && isPublicDataLoaded && (!isActiveRouteProtected || isProtectedDataLoaded)) {
96
+ if (!areModulesReady) {
97
+ onCompleteRegistrations();
98
+ }
99
+ }
100
+ }
101
+ }, [areModulesRegistered, areModulesReady, isMswStarted, isPublicDataLoaded, isProtectedDataLoaded, isActiveRouteProtected, onCompleteRegistrations]);
102
+ if (!areModulesReady || !isMswStarted || !isPublicDataLoaded || isActiveRouteProtected && !isProtectedDataLoaded) {
103
+ return fallbackElement;
104
+ }
105
+ return /* @__PURE__ */ jsx(Outlet, {});
106
+ }
107
+ function AppRouter(props) {
108
+ const {
109
+ fallbackElement,
110
+ errorElement,
111
+ onLoadPublicData,
112
+ onLoadProtectedData,
113
+ isPublicDataLoaded = true,
114
+ isProtectedDataLoaded = true,
115
+ onCompleteRegistrations,
116
+ waitForMsw,
117
+ children: renderRouterProvider
118
+ } = props;
119
+ const areModulesRegistered = useAreModulesRegistered();
120
+ const areModulesReady = useAreModulesReady();
121
+ const routes = useRoutes();
122
+ const errorRenderer = useCallback(({ error }) => {
123
+ return cloneElement(errorElement, {
124
+ error
125
+ });
126
+ }, [errorElement]);
127
+ return useMemo(() => {
128
+ return renderRouterProvider([
129
+ {
130
+ element: /* @__PURE__ */ jsx(ErrorBoundary, { fallbackRender: errorRenderer, children: /* @__PURE__ */ jsx(
131
+ BootstrappingRoute,
132
+ {
133
+ fallbackElement,
134
+ onLoadPublicData,
135
+ onLoadProtectedData,
136
+ isPublicDataLoaded,
137
+ isProtectedDataLoaded,
138
+ onCompleteRegistrations,
139
+ waitForMsw,
140
+ areModulesRegistered,
141
+ areModulesReady
142
+ }
143
+ ) }),
144
+ children: routes
145
+ }
146
+ ], {});
147
+ }, [areModulesRegistered, areModulesReady, routes, onLoadPublicData, onLoadProtectedData, isPublicDataLoaded, isProtectedDataLoaded, onCompleteRegistrations, waitForMsw, errorRenderer, fallbackElement, renderRouterProvider]);
148
+ }
149
+
150
+ export { AppRouter, BootstrappingRoute };
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ export * from '@squide/core';
2
2
  export * from '@squide/msw';
3
3
  export * from '@squide/react-router';
4
4
  export * from '@squide/webpack-module-federation';
5
- export { AppRouter, AppRouterProps, BootstrappingRoute, OnCompleteRegistrationsFunction, OnLoadProtectedDataFunction, OnLoadPublicDataFunction } from './AppRouter.js';
5
+ export { AppRouter, AppRouterProps, BootstrappingRoute, OnCompleteRegistrationsFunction, OnLoadProtectedDataFunction, OnLoadPublicDataFunction, RenderRouterProviderFunction } from './AppRouter.js';
6
6
  export { FireflyRuntime } from './fireflyRuntime.js';
7
- import 'react/jsx-runtime';
8
7
  import 'react';
8
+ import 'react/jsx-runtime';
9
9
  import 'react-router-dom';
10
10
  import 'msw';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { AppRouter, BootstrappingRoute } from './chunk-MKFAFYJH.js';
1
+ export { AppRouter, BootstrappingRoute } from './chunk-QZACR5NK.js';
2
2
  export { FireflyRuntime } from './chunk-6R4K3V6F.js';
3
3
  export * from '@squide/core';
4
4
  export * from '@squide/msw';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@squide/firefly",
3
3
  "author": "Workleap",
4
- "version": "3.0.3",
4
+ "version": "4.0.0",
5
5
  "description": "Helpers to facilitate the creation of a shell package with Squide firefly technology stack.",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -88,13 +88,13 @@
88
88
  "tsup": "8.0.1",
89
89
  "typescript": "5.2.2",
90
90
  "webpack": "5.89.0",
91
- "@squide/webpack-configs": "1.1.1"
91
+ "@squide/webpack-configs": "1.1.2"
92
92
  },
93
93
  "dependencies": {
94
- "@squide/core": "3.1.1",
95
- "@squide/msw": "2.0.7",
96
- "@squide/react-router": "3.0.2",
97
- "@squide/webpack-module-federation": "3.0.2"
94
+ "@squide/core": "3.2.0",
95
+ "@squide/msw": "2.0.8",
96
+ "@squide/react-router": "4.0.0",
97
+ "@squide/webpack-module-federation": "3.0.3"
98
98
  },
99
99
  "sideEffects": false,
100
100
  "engines": {
@@ -1,136 +0,0 @@
1
- import { useLogger, isNil } from '@squide/core';
2
- import { useIsMswStarted } from '@squide/msw';
3
- import { useIsRouteMatchProtected, useRoutes } from '@squide/react-router';
4
- import { useAreModulesRegistered, useAreModulesReady } from '@squide/webpack-module-federation';
5
- import { useState, useEffect, useCallback, cloneElement, useMemo } from 'react';
6
- import { useErrorBoundary, ErrorBoundary } from 'react-error-boundary';
7
- import { useLocation, Outlet, createBrowserRouter, RouterProvider } from 'react-router-dom';
8
- import { jsx } from 'react/jsx-runtime';
9
-
10
- // src/AppRouter.tsx
11
- function isPromise(value) {
12
- return !isNil(value) && !isNil(value.then) && !isNil(value.catch);
13
- }
14
- function BootstrappingRoute(props) {
15
- const {
16
- fallbackElement,
17
- onLoadPublicData,
18
- onLoadProtectedData,
19
- onCompleteRegistrations,
20
- waitForMsw,
21
- areModulesRegistered,
22
- areModulesReady
23
- } = props;
24
- const [isPublicDataLoaded, setIsPublicDataLoaded] = useState(!onLoadPublicData);
25
- const [isProtectedDataLoaded, setIsProtectedDataLoaded] = useState(!onLoadProtectedData);
26
- const { showBoundary } = useErrorBoundary();
27
- const logger = useLogger();
28
- const location = useLocation();
29
- const isMswStarted = useIsMswStarted(waitForMsw);
30
- useEffect(() => {
31
- if (waitForMsw) {
32
- if ((areModulesRegistered || areModulesReady) && !isMswStarted) {
33
- logger.debug(`[shell] Modules are ${areModulesReady ? "ready" : "registered"}, waiting for MSW to start...`);
34
- } else if (!areModulesRegistered && !areModulesReady && isMswStarted) {
35
- logger.debug("[shell] MSW is started, waiting for the modules...");
36
- }
37
- }
38
- }, [logger, areModulesRegistered, areModulesReady, isMswStarted, waitForMsw]);
39
- useEffect(() => {
40
- if (onLoadPublicData) {
41
- if ((areModulesRegistered || areModulesReady) && isMswStarted) {
42
- if (!isPublicDataLoaded) {
43
- logger.debug("[shell] Loading public data.");
44
- const result = onLoadPublicData();
45
- if (!isPromise(result)) {
46
- throw Error("[squide] An AppRouter onLoadPublicData handler must return a promise object.");
47
- }
48
- result.then(() => {
49
- setIsPublicDataLoaded(true);
50
- logger.debug("[shell] Public data has been loaded.");
51
- }).catch((error) => {
52
- showBoundary(error);
53
- });
54
- }
55
- }
56
- }
57
- }, [logger, areModulesRegistered, areModulesReady, isMswStarted, isPublicDataLoaded, showBoundary, onLoadPublicData]);
58
- const isActiveRouteProtected = useIsRouteMatchProtected(location, { throwWhenThereIsNoMatch: areModulesReady });
59
- useEffect(() => {
60
- if (onLoadProtectedData) {
61
- if ((areModulesRegistered || areModulesReady) && isMswStarted) {
62
- if (isActiveRouteProtected) {
63
- if (!isProtectedDataLoaded) {
64
- logger.debug(`[shell] Loading protected data as "${location.pathname}" is a protected route.`);
65
- const result = onLoadProtectedData();
66
- if (!isPromise(result)) {
67
- throw Error("[squide] An AppRouter onLoadProtectedData handler must return a promise object.");
68
- }
69
- result.then(() => {
70
- setIsProtectedDataLoaded(true);
71
- logger.debug("[shell] Protected data has been loaded.");
72
- }).catch((error) => {
73
- showBoundary(error);
74
- });
75
- }
76
- } else {
77
- logger.debug(`[shell] Not loading protected data as "${location.pathname}" is a public route.`);
78
- }
79
- }
80
- }
81
- }, [logger, location, areModulesRegistered, areModulesReady, isMswStarted, isActiveRouteProtected, isProtectedDataLoaded, showBoundary, onLoadProtectedData]);
82
- useEffect(() => {
83
- if (onCompleteRegistrations) {
84
- if (areModulesRegistered && isMswStarted && isPublicDataLoaded) {
85
- if (!areModulesReady) {
86
- onCompleteRegistrations();
87
- }
88
- }
89
- }
90
- }, [areModulesRegistered, areModulesReady, isMswStarted, isPublicDataLoaded, onCompleteRegistrations]);
91
- if (!areModulesReady || !isMswStarted || !isPublicDataLoaded || isActiveRouteProtected && !isProtectedDataLoaded) {
92
- return fallbackElement;
93
- }
94
- return /* @__PURE__ */ jsx(Outlet, {});
95
- }
96
- function AppRouter(props) {
97
- const {
98
- fallbackElement,
99
- errorElement,
100
- onLoadPublicData,
101
- onLoadProtectedData,
102
- onCompleteRegistrations,
103
- waitForMsw,
104
- routerProvidersProps = {}
105
- } = props;
106
- const areModulesRegistered = useAreModulesRegistered();
107
- const areModulesReady = useAreModulesReady();
108
- const routes = useRoutes();
109
- const errorRenderer = useCallback(({ error }) => {
110
- return cloneElement(errorElement, {
111
- error
112
- });
113
- }, [errorElement]);
114
- const router = useMemo(() => {
115
- return createBrowserRouter([
116
- {
117
- element: /* @__PURE__ */ jsx(ErrorBoundary, { fallbackRender: errorRenderer, children: /* @__PURE__ */ jsx(
118
- BootstrappingRoute,
119
- {
120
- fallbackElement,
121
- onLoadPublicData,
122
- onLoadProtectedData,
123
- onCompleteRegistrations,
124
- waitForMsw,
125
- areModulesRegistered,
126
- areModulesReady
127
- }
128
- ) }),
129
- children: routes
130
- }
131
- ]);
132
- }, [areModulesRegistered, areModulesReady, routes, onLoadPublicData, onLoadProtectedData, onCompleteRegistrations, waitForMsw, errorRenderer, fallbackElement]);
133
- return /* @__PURE__ */ jsx(RouterProvider, { ...routerProvidersProps, router });
134
- }
135
-
136
- export { AppRouter, BootstrappingRoute };