@openmrs/esm-react-utils 9.0.3-pre.4834 → 9.0.3-pre.4843

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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 53 files with swc (119.35ms)
1
+ [0] Successfully compiled: 53 files with swc (178.96ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -6,7 +6,7 @@ export interface ComponentDecoratorOptions {
6
6
  featureName: string;
7
7
  disableTranslations?: boolean;
8
8
  strictMode?: boolean;
9
- swrConfig?: Partial<Omit<SWRConfiguration, 'fetcher'>>;
9
+ swrConfig?: Partial<Omit<SWRConfiguration, 'fetcher' | 'provider'>>;
10
10
  }
11
11
  export interface OpenmrsReactComponentProps {
12
12
  _extensionContext?: ExtensionData;
@@ -1 +1 @@
1
- {"version":3,"file":"openmrsComponentDecorator.d.ts","sourceRoot":"","sources":["../src/openmrsComponentDecorator.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAE5E,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAGnE,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AA8CnF,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,CAAC,EAAE,aAAa,CAAC;CACnC;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,GAAG,CAAC;IACjB,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,QAAQ,EAAE,yBAAyB,IAY5C,MAAM,aAAa,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,CAAC,CA4E5E"}
1
+ {"version":3,"file":"openmrsComponentDecorator.d.ts","sourceRoot":"","sources":["../src/openmrsComponentDecorator.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAE5E,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAInE,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAqDnF,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,CAAC,EAAE,aAAa,CAAC;CACnC;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,GAAG,CAAC;IACjB,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,QAAQ,EAAE,yBAAyB,IAY5C,MAAM,aAAa,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,CAAC,CA4E5E"}
@@ -14,6 +14,7 @@ function _define_property(obj, key, value) {
14
14
  import React, { Suspense } from "react";
15
15
  import { I18nextProvider } from "react-i18next";
16
16
  import { SWRConfig } from "swr";
17
+ import { initCache } from "swr/_internal";
17
18
  import { openmrsFetch, OpenmrsFetchError } from "@openmrs/esm-api";
18
19
  import { ComponentContext } from "./ComponentContext.js";
19
20
  const defaultOpts = {
@@ -21,7 +22,14 @@ const defaultOpts = {
21
22
  throwErrorsToConsole: true,
22
23
  disableTranslations: false
23
24
  };
25
+ // One global SWR cache shared by every decorated component, the same regardless
26
+ // of module-federation load order (see #1397). It is pre-initialized here so its
27
+ // SWRGlobalState is owned by this (singleton) module, not by the first
28
+ // `<SWRConfig>` that mounts. Otherwise that boundary's unmount would run
29
+ // `SWRGlobalState.delete(swrCache)` and the other still-mounted decorated
30
+ // components would crash on their next render ("undefined is not iterable").
24
31
  const swrCache = new Map();
32
+ initCache(swrCache);
25
33
  // Read more about the available config options here: https://swr.vercel.app/docs/api#configuration
26
34
  const defaultSwrConfig = {
27
35
  // max number of retries after requests have failed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-react-utils",
3
- "version": "9.0.3-pre.4834",
3
+ "version": "9.0.3-pre.4843",
4
4
  "license": "MPL-2.0",
5
5
  "description": "React utilities for OpenMRS.",
6
6
  "type": "module",
@@ -79,17 +79,17 @@
79
79
  "swr": "2.x"
80
80
  },
81
81
  "devDependencies": {
82
- "@openmrs/esm-api": "9.0.3-pre.4834",
83
- "@openmrs/esm-config": "9.0.3-pre.4834",
84
- "@openmrs/esm-context": "9.0.3-pre.4834",
85
- "@openmrs/esm-emr-api": "9.0.3-pre.4834",
86
- "@openmrs/esm-error-handling": "9.0.3-pre.4834",
87
- "@openmrs/esm-extensions": "9.0.3-pre.4834",
88
- "@openmrs/esm-feature-flags": "9.0.3-pre.4834",
89
- "@openmrs/esm-globals": "9.0.3-pre.4834",
90
- "@openmrs/esm-navigation": "9.0.3-pre.4834",
91
- "@openmrs/esm-state": "9.0.3-pre.4834",
92
- "@openmrs/esm-utils": "9.0.3-pre.4834",
82
+ "@openmrs/esm-api": "9.0.3-pre.4843",
83
+ "@openmrs/esm-config": "9.0.3-pre.4843",
84
+ "@openmrs/esm-context": "9.0.3-pre.4843",
85
+ "@openmrs/esm-emr-api": "9.0.3-pre.4843",
86
+ "@openmrs/esm-error-handling": "9.0.3-pre.4843",
87
+ "@openmrs/esm-extensions": "9.0.3-pre.4843",
88
+ "@openmrs/esm-feature-flags": "9.0.3-pre.4843",
89
+ "@openmrs/esm-globals": "9.0.3-pre.4843",
90
+ "@openmrs/esm-navigation": "9.0.3-pre.4843",
91
+ "@openmrs/esm-state": "9.0.3-pre.4843",
92
+ "@openmrs/esm-utils": "9.0.3-pre.4843",
93
93
  "@swc/cli": "0.8.1",
94
94
  "@swc/core": "1.15.21",
95
95
  "@vitest/coverage-v8": "^4.1.2",
@@ -0,0 +1,98 @@
1
+ import React, { useReducer, useState } from 'react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import useSWR from 'swr';
6
+ import { openmrsComponentDecorator } from './openmrsComponentDecorator';
7
+
8
+ // Regression test for the shared-SWR-cache lifecycle crash: every decorated
9
+ // component has its own `<SWRConfig>`, and SWR deletes a provider cache's state
10
+ // when the first boundary to init it unmounts. Before the fix, that crashed any
11
+ // still-mounted decorated component ("undefined is not iterable").
12
+
13
+ function decorate(featureName: string, Inner: React.ComponentType) {
14
+ return openmrsComponentDecorator({
15
+ moduleName: featureName.toLowerCase(),
16
+ featureName,
17
+ strictMode: false,
18
+ throwErrorsToConsole: false,
19
+ disableTranslations: true,
20
+ })(Inner);
21
+ }
22
+
23
+ function Widget({ id }: { id: string }) {
24
+ // Touch SWR so this component reads the shared SWRGlobalState on every render.
25
+ useSWR(`widget-${id}`, () => Promise.resolve(id));
26
+ const [count, bump] = useReducer((n) => n + 1, 0);
27
+ return (
28
+ <button type="button" onClick={bump}>
29
+ {id}-{count}
30
+ </button>
31
+ );
32
+ }
33
+
34
+ describe('openmrsComponentDecorator shared SWR cache lifecycle', () => {
35
+ it('keeps a still-mounted decorated component working after a sibling decorated component unmounts', async () => {
36
+ const user = userEvent.setup();
37
+
38
+ // `A` renders first, so it is the first `<SWRConfig>` to initialize the
39
+ // shared cache (the boundary that owned SWR's deleter before the fix).
40
+ const A = decorate('A', () => <Widget id="A" />);
41
+ const B = decorate('B', () => <Widget id="B" />);
42
+
43
+ function Harness() {
44
+ const [showA, setShowA] = useState(true);
45
+ return (
46
+ <div>
47
+ {showA && <A />}
48
+ <B />
49
+ <button type="button" onClick={() => setShowA(false)}>
50
+ hide-a
51
+ </button>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ render(<Harness />);
57
+
58
+ expect(await screen.findByRole('button', { name: 'A-0' })).toBeInTheDocument();
59
+ expect(await screen.findByRole('button', { name: 'B-0' })).toBeInTheDocument();
60
+
61
+ // Unmount the cache-owning component.
62
+ await user.click(screen.getByRole('button', { name: 'hide-a' }));
63
+ expect(screen.queryByRole('button', { name: 'A-0' })).not.toBeInTheDocument();
64
+
65
+ // Force the still-mounted component to re-render. Before the fix this threw
66
+ // and the decorator's error boundary replaced B with an error message.
67
+ await user.click(screen.getByRole('button', { name: 'B-0' }));
68
+
69
+ expect(await screen.findByRole('button', { name: 'B-1' })).toBeInTheDocument();
70
+ expect(screen.queryByText(/an error has occurred/i)).not.toBeInTheDocument();
71
+ });
72
+
73
+ it('shares cached data across separately decorated components (single global cache, #1397)', async () => {
74
+ // Fixed key, unique within this file (the shared cache persists across renders).
75
+ const key = 'swr-cache-sharing-test';
76
+
77
+ function Producer() {
78
+ const { data } = useSWR(key, () => Promise.resolve('shared-value'));
79
+ return <span>producer:{data ?? 'loading'}</span>;
80
+ }
81
+
82
+ // The consumer's own fetcher never resolves; if it renders the value, it can
83
+ // only have come from a cache shared with the producer.
84
+ function Consumer() {
85
+ const { data } = useSWR(key, () => new Promise<string>(() => {}));
86
+ return <span>consumer:{data ?? 'loading'}</span>;
87
+ }
88
+
89
+ const DecoratedProducer = decorate('Producer', Producer);
90
+ const DecoratedConsumer = decorate('Consumer', Consumer);
91
+
92
+ render(<DecoratedProducer />);
93
+ expect(await screen.findByText('producer:shared-value')).toBeInTheDocument();
94
+
95
+ render(<DecoratedConsumer />);
96
+ expect(await screen.findByText('consumer:shared-value')).toBeInTheDocument();
97
+ });
98
+ });
@@ -1,6 +1,7 @@
1
1
  import React, { type ComponentType, type ErrorInfo, Suspense } from 'react';
2
2
  import { I18nextProvider } from 'react-i18next';
3
3
  import { type Cache, SWRConfig, type SWRConfiguration } from 'swr';
4
+ import { initCache } from 'swr/_internal';
4
5
  import type {} from '@openmrs/esm-globals';
5
6
  import { openmrsFetch, OpenmrsFetchError } from '@openmrs/esm-api';
6
7
  import { type ComponentConfig, type ExtensionData } from '@openmrs/esm-extensions';
@@ -12,7 +13,14 @@ const defaultOpts = {
12
13
  disableTranslations: false,
13
14
  };
14
15
 
16
+ // One global SWR cache shared by every decorated component, the same regardless
17
+ // of module-federation load order (see #1397). It is pre-initialized here so its
18
+ // SWRGlobalState is owned by this (singleton) module, not by the first
19
+ // `<SWRConfig>` that mounts. Otherwise that boundary's unmount would run
20
+ // `SWRGlobalState.delete(swrCache)` and the other still-mounted decorated
21
+ // components would crash on their next render ("undefined is not iterable").
15
22
  const swrCache: Cache = new Map();
23
+ initCache(swrCache);
16
24
 
17
25
  // Read more about the available config options here: https://swr.vercel.app/docs/api#configuration
18
26
  const defaultSwrConfig: SWRConfiguration = {
@@ -54,7 +62,8 @@ export interface ComponentDecoratorOptions {
54
62
  featureName: string;
55
63
  disableTranslations?: boolean;
56
64
  strictMode?: boolean;
57
- swrConfig?: Partial<Omit<SWRConfiguration, 'fetcher'>>;
65
+ // `provider` omitted deliberately (see defaultSwrConfig); `fetcher` is fixed.
66
+ swrConfig?: Partial<Omit<SWRConfiguration, 'fetcher' | 'provider'>>;
58
67
  }
59
68
 
60
69
  export interface OpenmrsReactComponentProps {