@openmrs/esm-react-utils 9.0.3-pre.4834 → 9.0.3-pre.4841
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/.turbo/turbo-build.log +1 -1
- package/dist/openmrsComponentDecorator.d.ts +1 -1
- package/dist/openmrsComponentDecorator.d.ts.map +1 -1
- package/dist/openmrsComponentDecorator.js +8 -0
- package/package.json +12 -12
- package/src/openmrsComponentDecorator.swr-cache.test.tsx +98 -0
- package/src/openmrsComponentDecorator.tsx +10 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "9.0.3-pre.4841",
|
|
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.
|
|
83
|
-
"@openmrs/esm-config": "9.0.3-pre.
|
|
84
|
-
"@openmrs/esm-context": "9.0.3-pre.
|
|
85
|
-
"@openmrs/esm-emr-api": "9.0.3-pre.
|
|
86
|
-
"@openmrs/esm-error-handling": "9.0.3-pre.
|
|
87
|
-
"@openmrs/esm-extensions": "9.0.3-pre.
|
|
88
|
-
"@openmrs/esm-feature-flags": "9.0.3-pre.
|
|
89
|
-
"@openmrs/esm-globals": "9.0.3-pre.
|
|
90
|
-
"@openmrs/esm-navigation": "9.0.3-pre.
|
|
91
|
-
"@openmrs/esm-state": "9.0.3-pre.
|
|
92
|
-
"@openmrs/esm-utils": "9.0.3-pre.
|
|
82
|
+
"@openmrs/esm-api": "9.0.3-pre.4841",
|
|
83
|
+
"@openmrs/esm-config": "9.0.3-pre.4841",
|
|
84
|
+
"@openmrs/esm-context": "9.0.3-pre.4841",
|
|
85
|
+
"@openmrs/esm-emr-api": "9.0.3-pre.4841",
|
|
86
|
+
"@openmrs/esm-error-handling": "9.0.3-pre.4841",
|
|
87
|
+
"@openmrs/esm-extensions": "9.0.3-pre.4841",
|
|
88
|
+
"@openmrs/esm-feature-flags": "9.0.3-pre.4841",
|
|
89
|
+
"@openmrs/esm-globals": "9.0.3-pre.4841",
|
|
90
|
+
"@openmrs/esm-navigation": "9.0.3-pre.4841",
|
|
91
|
+
"@openmrs/esm-state": "9.0.3-pre.4841",
|
|
92
|
+
"@openmrs/esm-utils": "9.0.3-pre.4841",
|
|
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
|
-
|
|
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 {
|