@furystack/shades-mfe 1.0.28 → 2.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 +33 -0
- package/esm/create-shades-micro-frontend.spec.js +65 -60
- package/esm/create-shades-micro-frontend.spec.js.map +1 -1
- package/esm/micro-frontend.d.ts.map +1 -1
- package/esm/micro-frontend.js +39 -30
- package/esm/micro-frontend.js.map +1 -1
- package/esm/micro-frontend.spec.js +86 -78
- package/esm/micro-frontend.spec.js.map +1 -1
- package/package.json +3 -3
- package/src/create-shades-micro-frontend.spec.tsx +69 -64
- package/src/micro-frontend.spec.tsx +136 -128
- package/src/micro-frontend.tsx +53 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0] - 2026-02-09
|
|
4
|
+
|
|
5
|
+
### ⬆️ Dependencies
|
|
6
|
+
|
|
7
|
+
- Updated peer dependency `@furystack/shades` to latest minor version
|
|
8
|
+
- Peer dependency on `@furystack/shades` bumped to new major version
|
|
9
|
+
- Updated `@furystack/shades` dependency
|
|
10
|
+
|
|
11
|
+
### 💥 Breaking Changes
|
|
12
|
+
|
|
13
|
+
### Requires `@furystack/shades` v3
|
|
14
|
+
|
|
15
|
+
This package now depends on the new major version of `@furystack/shades` which removed the `constructed` callback.
|
|
16
|
+
|
|
17
|
+
### Migrated from `element` to `useRef` for Container Management
|
|
18
|
+
|
|
19
|
+
The `MicroFrontend` component no longer uses the `element` render option to access the host element. Instead, it uses `useRef` to create a container `<div>` inside the component, and the micro-frontend is mounted into that container.
|
|
20
|
+
|
|
21
|
+
**Impact:** The internal mounting behavior has changed — micro-frontends are now mounted inside a child `<div>` rather than directly into the host custom element. This should be transparent for most consumers, but any code that relies on the MFE being a direct child of the `shade-micro-frontend` element may need adjustment.
|
|
22
|
+
|
|
23
|
+
### ♻️ Refactoring
|
|
24
|
+
|
|
25
|
+
- Migrated `MicroFrontend` component from async `constructed` callback to `useDisposable()` in `render`, using `[Symbol.asyncDispose]` for cleanup
|
|
26
|
+
- Replaced `element` parameter with `useRef('mfeContainer')` and a container `<div>` for MFE mounting
|
|
27
|
+
- MFE loader now defers initialization via `queueMicrotask` to ensure the ref is available after the first render
|
|
28
|
+
- Added prop propagation to inner MFE elements for Shade-based micro-frontends
|
|
29
|
+
|
|
30
|
+
### 🧪 Tests
|
|
31
|
+
|
|
32
|
+
- Updated micro-frontend tests to work with microtask-based rendering
|
|
33
|
+
- Refactored `createShadesMicroFrontend` and `MicroFrontend` tests to use `usingAsync` for proper `Injector` disposal
|
|
34
|
+
- Updated tests to accommodate new rendering flow
|
|
35
|
+
|
|
3
36
|
## [1.0.28] - 2026-02-01
|
|
4
37
|
|
|
5
38
|
### ⬆️ Dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject';
|
|
2
2
|
import { createComponent, Shade } from '@furystack/shades';
|
|
3
|
-
import { sleepAsync } from '@furystack/utils';
|
|
3
|
+
import { sleepAsync, usingAsync } from '@furystack/utils';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { CreateMicroFrontendService } from './create-microfrontend-service.js';
|
|
6
6
|
import { createShadesMicroFrontend } from './create-shades-micro-frontend.js';
|
|
@@ -21,22 +21,23 @@ describe('createShadesMicroFrontend', () => {
|
|
|
21
21
|
expect(result.create).toBeTypeOf('function');
|
|
22
22
|
expect(result.destroy).toBeUndefined();
|
|
23
23
|
});
|
|
24
|
-
it('should create a child injector when create is called', () => {
|
|
24
|
+
it('should create a child injector when create is called', async () => {
|
|
25
25
|
const TestComponent = Shade({
|
|
26
26
|
shadowDomName: 'test-mfe-child-injector',
|
|
27
27
|
render: ({ props }) => createComponent("div", null, props.value),
|
|
28
28
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
await usingAsync(new Injector(), async (parentInjector) => {
|
|
30
|
+
const createChildSpy = vi.spyOn(parentInjector, 'createChild');
|
|
31
|
+
const mfeService = createShadesMicroFrontend(TestComponent);
|
|
32
|
+
const rootElement = document.getElementById('root');
|
|
33
|
+
mfeService.create({
|
|
34
|
+
api: { value: 'test' },
|
|
35
|
+
rootElement,
|
|
36
|
+
injector: parentInjector,
|
|
37
|
+
});
|
|
38
|
+
expect(createChildSpy).toHaveBeenCalledWith({
|
|
39
|
+
owner: createShadesMicroFrontend,
|
|
40
|
+
});
|
|
40
41
|
});
|
|
41
42
|
});
|
|
42
43
|
it('should render the component with the provided API props', async () => {
|
|
@@ -47,33 +48,35 @@ describe('createShadesMicroFrontend', () => {
|
|
|
47
48
|
"Value: ",
|
|
48
49
|
props.value),
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
52
|
+
const rootElement = document.getElementById('root');
|
|
53
|
+
const mfeService = createShadesMicroFrontend(TestComponent);
|
|
54
|
+
mfeService.create({
|
|
55
|
+
api: { value: testValue },
|
|
56
|
+
rootElement,
|
|
57
|
+
injector,
|
|
58
|
+
});
|
|
59
|
+
await sleepAsync(10);
|
|
60
|
+
expect(rootElement.innerHTML).toContain(`Value: ${testValue}`);
|
|
57
61
|
});
|
|
58
|
-
await sleepAsync(10);
|
|
59
|
-
expect(rootElement.innerHTML).toContain(`Value: ${testValue}`);
|
|
60
62
|
});
|
|
61
63
|
it('should render the component into the provided root element', async () => {
|
|
62
64
|
const TestComponent = Shade({
|
|
63
65
|
shadowDomName: 'test-mfe-root-element',
|
|
64
66
|
render: () => createComponent("span", null, "MFE Content"),
|
|
65
67
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
69
|
+
const rootElement = document.getElementById('root');
|
|
70
|
+
const mfeService = createShadesMicroFrontend(TestComponent);
|
|
71
|
+
mfeService.create({
|
|
72
|
+
api: {},
|
|
73
|
+
rootElement,
|
|
74
|
+
injector,
|
|
75
|
+
});
|
|
76
|
+
await sleepAsync(10);
|
|
77
|
+
expect(rootElement.querySelector('test-mfe-root-element')).toBeTruthy();
|
|
78
|
+
expect(rootElement.innerHTML).toContain('MFE Content');
|
|
73
79
|
});
|
|
74
|
-
await sleepAsync(10);
|
|
75
|
-
expect(rootElement.querySelector('test-mfe-root-element')).toBeTruthy();
|
|
76
|
-
expect(rootElement.innerHTML).toContain('MFE Content');
|
|
77
80
|
});
|
|
78
81
|
it('should work with complex API types', async () => {
|
|
79
82
|
const clickHandler = vi.fn();
|
|
@@ -84,24 +87,25 @@ describe('createShadesMicroFrontend', () => {
|
|
|
84
87
|
createComponent("span", { "data-testid": "item-count" }, props.items.length),
|
|
85
88
|
createComponent("button", { onclick: props.onClick }, "Click"))),
|
|
86
89
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
91
|
+
const rootElement = document.getElementById('root');
|
|
92
|
+
const mfeService = createShadesMicroFrontend(TestComponent);
|
|
93
|
+
mfeService.create({
|
|
94
|
+
api: {
|
|
95
|
+
user: { id: '123', name: 'John Doe' },
|
|
96
|
+
onClick: clickHandler,
|
|
97
|
+
items: ['a', 'b', 'c'],
|
|
98
|
+
},
|
|
99
|
+
rootElement,
|
|
100
|
+
injector,
|
|
101
|
+
});
|
|
102
|
+
await sleepAsync(10);
|
|
103
|
+
expect(rootElement.innerHTML).toContain('John Doe');
|
|
104
|
+
expect(rootElement.innerHTML).toContain('3');
|
|
105
|
+
const button = rootElement.querySelector('button');
|
|
106
|
+
button?.click();
|
|
107
|
+
expect(clickHandler).toHaveBeenCalled();
|
|
98
108
|
});
|
|
99
|
-
await sleepAsync(10);
|
|
100
|
-
expect(rootElement.innerHTML).toContain('John Doe');
|
|
101
|
-
expect(rootElement.innerHTML).toContain('3');
|
|
102
|
-
const button = rootElement.querySelector('button');
|
|
103
|
-
button?.click();
|
|
104
|
-
expect(clickHandler).toHaveBeenCalled();
|
|
105
109
|
});
|
|
106
110
|
it('should use the child injector for the shade root', async () => {
|
|
107
111
|
let capturedInjector;
|
|
@@ -112,18 +116,19 @@ describe('createShadesMicroFrontend', () => {
|
|
|
112
116
|
return createComponent("div", null, "Test");
|
|
113
117
|
},
|
|
114
118
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
await usingAsync(new Injector(), async (parentInjector) => {
|
|
120
|
+
const rootElement = document.getElementById('root');
|
|
121
|
+
const mfeService = createShadesMicroFrontend(TestComponent);
|
|
122
|
+
mfeService.create({
|
|
123
|
+
api: {},
|
|
124
|
+
rootElement,
|
|
125
|
+
injector: parentInjector,
|
|
126
|
+
});
|
|
127
|
+
await sleepAsync(10);
|
|
128
|
+
expect(capturedInjector).toBeDefined();
|
|
129
|
+
// The component should receive a child injector, not the parent
|
|
130
|
+
expect(capturedInjector).not.toBe(parentInjector);
|
|
122
131
|
});
|
|
123
|
-
await sleepAsync(10);
|
|
124
|
-
expect(capturedInjector).toBeDefined();
|
|
125
|
-
// The component should receive a child injector, not the parent
|
|
126
|
-
expect(capturedInjector).not.toBe(parentInjector);
|
|
127
132
|
});
|
|
128
133
|
});
|
|
129
134
|
//# sourceMappingURL=create-shades-micro-frontend.spec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-shades-micro-frontend.spec.js","sourceRoot":"","sources":["../src/create-shades-micro-frontend.spec.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"create-shades-micro-frontend.spec.js","sourceRoot":"","sources":["../src/create-shades-micro-frontend.spec.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"micro-frontend.d.ts","sourceRoot":"","sources":["../src/micro-frontend.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAA;AAEnF,KAAK,kBAAkB,CAAC,IAAI,IAAI;IAC9B,GAAG,EAAE,IAAI,CAAA;IACT,cAAc,EAAE,MAAM,OAAO,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/D,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;CACpE,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,kBAAkB,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"micro-frontend.d.ts","sourceRoot":"","sources":["../src/micro-frontend.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAA;AAEnF,KAAK,kBAAkB,CAAC,IAAI,IAAI;IAC9B,GAAG,EAAE,IAAI,CAAA;IACT,cAAc,EAAE,MAAM,OAAO,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/D,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;CACpE,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,kBAAkB,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,OA6DzE,CAAA"}
|
package/esm/micro-frontend.js
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
|
-
import { Shade } from '@furystack/shades';
|
|
1
|
+
import { Shade, createComponent } from '@furystack/shades';
|
|
2
2
|
export const MicroFrontend = Shade({
|
|
3
3
|
shadowDomName: 'shade-micro-frontend',
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
render: ({ props, injector, useDisposable, useRef }) => {
|
|
5
|
+
const containerRef = useRef('mfeContainer');
|
|
6
|
+
useDisposable('mfe-loader', () => {
|
|
7
|
+
let destroyFn;
|
|
8
|
+
const tryCreateComponent = async () => {
|
|
9
|
+
const container = containerRef.current;
|
|
10
|
+
if (!container)
|
|
11
|
+
return;
|
|
12
|
+
const creatorService = await props.loaderCallback();
|
|
13
|
+
const childInjector = injector.createChild({
|
|
14
|
+
owner: creatorService,
|
|
15
|
+
});
|
|
16
|
+
container.innerHTML = '';
|
|
17
|
+
creatorService.create({
|
|
18
|
+
api: props.api,
|
|
19
|
+
rootElement: container,
|
|
20
|
+
injector: childInjector,
|
|
21
|
+
});
|
|
22
|
+
destroyFn = () => creatorService.destroy?.({ api: props.api, injector: childInjector });
|
|
23
|
+
};
|
|
24
|
+
queueMicrotask(() => {
|
|
25
|
+
tryCreateComponent().catch((error) => {
|
|
26
|
+
if (props.error && containerRef.current) {
|
|
27
|
+
containerRef.current.appendChild(props.error(error, async () => {
|
|
28
|
+
await tryCreateComponent();
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
13
32
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return await tryCreateComponent();
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
if (props.error) {
|
|
27
|
-
element.appendChild(props.error(error, async () => {
|
|
28
|
-
await tryCreateComponent();
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
33
|
+
return {
|
|
34
|
+
[Symbol.asyncDispose]: async () => destroyFn?.(),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
// Propagate prop updates to the inner MFE element (for Shade-based MFEs)
|
|
38
|
+
const mfeElement = containerRef.current?.firstElementChild;
|
|
39
|
+
if (mfeElement?.updateComponent) {
|
|
40
|
+
mfeElement.props = props.api;
|
|
41
|
+
mfeElement.updateComponent();
|
|
31
42
|
}
|
|
32
|
-
|
|
33
|
-
render: ({ props }) => {
|
|
34
|
-
return props.loader || null;
|
|
43
|
+
return (createComponent("div", { ref: containerRef, style: { display: 'contents' } }, props.loader || null));
|
|
35
44
|
},
|
|
36
45
|
});
|
|
37
46
|
//# sourceMappingURL=micro-frontend.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"micro-frontend.js","sourceRoot":"","sources":["../src/micro-frontend.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"micro-frontend.js","sourceRoot":"","sources":["../src/micro-frontend.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAU1D,MAAM,CAAC,MAAM,aAAa,GAA2D,KAAK,CAAC;IACzF,aAAa,EAAE,sBAAsB;IACrC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE;QACrD,MAAM,YAAY,GAAG,MAAM,CAAiB,cAAc,CAAC,CAAA;QAE3D,aAAa,CAAC,YAAY,EAAE,GAAG,EAAE;YAC/B,IAAI,SAAmC,CAAA;YAEvC,MAAM,kBAAkB,GAAG,KAAK,IAAI,EAAE;gBACpC,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAA;gBACtC,IAAI,CAAC,SAAS;oBAAE,OAAM;gBAEtB,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE,CAAA;gBAEnD,MAAM,aAAa,GAAG,QAAQ,CAAC,WAAW,CAAC;oBACzC,KAAK,EAAE,cAAc;iBACtB,CAAC,CAAA;gBAEF,SAAS,CAAC,SAAS,GAAG,EAAE,CAAA;gBAExB,cAAc,CAAC,MAAM,CAAC;oBACpB,GAAG,EAAE,KAAK,CAAC,GAAG;oBACd,WAAW,EAAE,SAAS;oBACtB,QAAQ,EAAE,aAAa;iBACxB,CAAC,CAAA;gBAEF,SAAS,GAAG,GAAG,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAA;YACzF,CAAC,CAAA;YAED,cAAc,CAAC,GAAG,EAAE;gBAClB,kBAAkB,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;oBAC5C,IAAI,KAAK,CAAC,KAAK,IAAI,YAAY,CAAC,OAAO,EAAE,CAAC;wBACxC,YAAY,CAAC,OAAO,CAAC,WAAW,CAC9B,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;4BAC5B,MAAM,kBAAkB,EAAE,CAAA;wBAC5B,CAAC,CAAC,CACH,CAAA;oBACH,CAAC;gBACH,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;YAEF,OAAO;gBACL,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE;aACjD,CAAA;QACH,CAAC,CAAC,CAAA;QAEF,yEAAyE;QACzE,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,EAAE,iBAEjC,CAAA;QACR,IAAI,UAAU,EAAE,eAAe,EAAE,CAAC;YAChC,UAAU,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAA;YAC5B,UAAU,CAAC,eAAe,EAAE,CAAA;QAC9B,CAAC;QAED,OAAO,CACL,yBAAK,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,IACnD,KAAK,CAAC,MAAM,IAAI,IAAI,CACjB,CACP,CAAA;IACH,CAAC;CACF,CAAC,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject';
|
|
2
|
-
import { sleepAsync } from '@furystack/utils';
|
|
3
|
-
import { createComponent, initializeShadeRoot, Shade } from '@furystack/shades';
|
|
2
|
+
import { sleepAsync, usingAsync } from '@furystack/utils';
|
|
3
|
+
import { createComponent, flushUpdates, initializeShadeRoot, Shade } from '@furystack/shades';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { createShadesMicroFrontend } from './create-shades-micro-frontend.js';
|
|
6
6
|
import { MicroFrontend } from './micro-frontend.js';
|
|
@@ -12,92 +12,100 @@ describe('<MicroFrontend /> component', () => {
|
|
|
12
12
|
document.body.innerHTML = '';
|
|
13
13
|
});
|
|
14
14
|
it('Should render the MFE app', async () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
16
|
+
const rootElement = document.getElementById('root');
|
|
17
|
+
const MfeComponent = Shade({
|
|
18
|
+
shadowDomName: 'mfe-test-example',
|
|
19
|
+
render: ({ props }) => createComponent("div", null,
|
|
20
|
+
"Loaded: ",
|
|
21
|
+
props.value),
|
|
22
|
+
});
|
|
23
|
+
const value = crypto.randomUUID();
|
|
24
|
+
initializeShadeRoot({
|
|
25
|
+
injector,
|
|
26
|
+
rootElement,
|
|
27
|
+
jsxElement: (createComponent(MicroFrontend, { api: { value }, loaderCallback: async () => {
|
|
28
|
+
return createShadesMicroFrontend(MfeComponent);
|
|
29
|
+
} })),
|
|
30
|
+
});
|
|
31
|
+
await flushUpdates();
|
|
32
|
+
await sleepAsync(1);
|
|
33
|
+
expect(document.body.innerHTML).toBe(`<div id="root"><shade-micro-frontend><div style="display: contents;"><mfe-test-example><div>Loaded: ${value}</div></mfe-test-example></div></shade-micro-frontend></div>`);
|
|
22
34
|
});
|
|
23
|
-
const value = crypto.randomUUID();
|
|
24
|
-
initializeShadeRoot({
|
|
25
|
-
injector,
|
|
26
|
-
rootElement,
|
|
27
|
-
jsxElement: (createComponent(MicroFrontend, { api: { value }, loaderCallback: async () => {
|
|
28
|
-
return createShadesMicroFrontend(MfeComponent);
|
|
29
|
-
} })),
|
|
30
|
-
});
|
|
31
|
-
await sleepAsync(1);
|
|
32
|
-
expect(document.body.innerHTML).toBe(`<div id="root"><shade-micro-frontend><mfe-test-example><div>Loaded: ${value}</div></mfe-test-example></shade-micro-frontend></div>`);
|
|
33
35
|
});
|
|
34
36
|
it('Should render the MFE app with a loader', async () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
38
|
+
const rootElement = document.getElementById('root');
|
|
39
|
+
const MfeComponent = Shade({
|
|
40
|
+
shadowDomName: 'mfe-test-example-w-loader',
|
|
41
|
+
render: ({ props }) => createComponent("div", null,
|
|
42
|
+
"Loaded: ",
|
|
43
|
+
props.value),
|
|
44
|
+
});
|
|
45
|
+
const value = crypto.randomUUID();
|
|
46
|
+
initializeShadeRoot({
|
|
47
|
+
injector,
|
|
48
|
+
rootElement,
|
|
49
|
+
jsxElement: (createComponent(MicroFrontend, { loader: createComponent("div", null, "Loading..."), api: { value }, loaderCallback: async () => {
|
|
50
|
+
await sleepAsync(10);
|
|
51
|
+
return createShadesMicroFrontend(MfeComponent);
|
|
52
|
+
} })),
|
|
53
|
+
});
|
|
54
|
+
await flushUpdates();
|
|
55
|
+
expect(document.body.innerHTML).toBe('<div id="root"><shade-micro-frontend><div style="display: contents;"><div>Loading...</div></div></shade-micro-frontend></div>');
|
|
56
|
+
await sleepAsync(20);
|
|
57
|
+
expect(document.body.innerHTML).toBe(`<div id="root"><shade-micro-frontend><div style="display: contents;"><mfe-test-example-w-loader><div>Loaded: ${value}</div></mfe-test-example-w-loader></div></shade-micro-frontend></div>`);
|
|
51
58
|
});
|
|
52
|
-
expect(document.body.innerHTML).toBe('<div id="root"><shade-micro-frontend><div>Loading...</div></shade-micro-frontend></div>');
|
|
53
|
-
await sleepAsync(20);
|
|
54
|
-
expect(document.body.innerHTML).toBe(`<div id="root"><shade-micro-frontend><mfe-test-example-w-loader><div>Loaded: ${value}</div></mfe-test-example-w-loader></shade-micro-frontend></div>`);
|
|
55
59
|
});
|
|
56
60
|
it('Should render the MFE app with an error', async () => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
62
|
+
const rootElement = document.getElementById('root');
|
|
63
|
+
initializeShadeRoot({
|
|
64
|
+
injector,
|
|
65
|
+
rootElement,
|
|
66
|
+
jsxElement: (createComponent(MicroFrontend, { error: () => createComponent("div", null, "Error..."), api: {}, loaderCallback: async () => {
|
|
67
|
+
throw Error(':(');
|
|
68
|
+
} })),
|
|
69
|
+
});
|
|
70
|
+
await flushUpdates();
|
|
71
|
+
await sleepAsync(10);
|
|
72
|
+
expect(document.body.innerHTML).toBe(`<div id="root"><shade-micro-frontend><div style="display: contents;"><div>Error...</div></div></shade-micro-frontend></div>`);
|
|
65
73
|
});
|
|
66
|
-
await sleepAsync(10);
|
|
67
|
-
expect(document.body.innerHTML).toBe(`<div id="root"><shade-micro-frontend><div>Error...</div></shade-micro-frontend></div>`);
|
|
68
74
|
});
|
|
69
75
|
it('Should execute the destroy callback', async () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
76
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
77
|
+
const rootElement = document.getElementById('root');
|
|
78
|
+
const value = crypto.randomUUID();
|
|
79
|
+
const destroy = vi.fn();
|
|
80
|
+
const Host = Shade({
|
|
81
|
+
shadowDomName: 'mfe-test-example-w-destroy-host',
|
|
82
|
+
render: ({ useState }) => {
|
|
83
|
+
const [hasMfe, setHasMfe] = useState('hasMfe', true);
|
|
84
|
+
return (createComponent(createComponent, null,
|
|
85
|
+
createComponent("button", { onclick: () => {
|
|
86
|
+
setHasMfe(false);
|
|
87
|
+
} }, "Remove MFE"),
|
|
88
|
+
hasMfe && (createComponent(MicroFrontend, { loader: createComponent("div", null, "Loading..."), api: { value }, loaderCallback: async () => {
|
|
89
|
+
return {
|
|
90
|
+
create: () => { },
|
|
91
|
+
destroy,
|
|
92
|
+
};
|
|
93
|
+
} }))));
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
initializeShadeRoot({
|
|
97
|
+
injector,
|
|
98
|
+
rootElement,
|
|
99
|
+
jsxElement: createComponent(Host, null),
|
|
100
|
+
});
|
|
101
|
+
await flushUpdates();
|
|
102
|
+
await sleepAsync(10);
|
|
103
|
+
expect(document.body.innerHTML).toBe('<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button><shade-micro-frontend><div style="display: contents;"></div></shade-micro-frontend></mfe-test-example-w-destroy-host></div>');
|
|
104
|
+
document.querySelector('button').click();
|
|
105
|
+
await sleepAsync(10);
|
|
106
|
+
expect(destroy).toHaveBeenCalledWith({ api: { value }, injector: expect.any(Injector) });
|
|
107
|
+
expect(document.body.innerHTML).toBe(`<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button></mfe-test-example-w-destroy-host></div>`);
|
|
94
108
|
});
|
|
95
|
-
await sleepAsync(10);
|
|
96
|
-
expect(document.body.innerHTML).toBe('<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button><shade-micro-frontend></shade-micro-frontend></mfe-test-example-w-destroy-host></div>');
|
|
97
|
-
document.querySelector('button').click();
|
|
98
|
-
await sleepAsync(10);
|
|
99
|
-
expect(destroy).toHaveBeenCalledWith({ api: { value }, injector: expect.any(Injector) });
|
|
100
|
-
expect(document.body.innerHTML).toBe(`<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button></mfe-test-example-w-destroy-host></div>`);
|
|
101
109
|
});
|
|
102
110
|
});
|
|
103
111
|
//# sourceMappingURL=micro-frontend.spec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"micro-frontend.spec.js","sourceRoot":"","sources":["../src/micro-frontend.spec.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"micro-frontend.spec.js","sourceRoot":"","sources":["../src/micro-frontend.spec.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/shades-mfe",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Micro-Frontend support for FuryStack Shades",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/furystack/furystack",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@furystack/inject": "^12.0.
|
|
41
|
-
"@furystack/shades": "^
|
|
40
|
+
"@furystack/inject": "^12.0.29",
|
|
41
|
+
"@furystack/shades": "^12.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^25.0.10",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject'
|
|
2
2
|
import { createComponent, Shade } from '@furystack/shades'
|
|
3
|
-
import { sleepAsync } from '@furystack/utils'
|
|
3
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
5
|
import { CreateMicroFrontendService } from './create-microfrontend-service.js'
|
|
6
6
|
import { createShadesMicroFrontend } from './create-shades-micro-frontend.js'
|
|
@@ -27,26 +27,27 @@ describe('createShadesMicroFrontend', () => {
|
|
|
27
27
|
expect(result.destroy).toBeUndefined()
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it('should create a child injector when create is called', () => {
|
|
30
|
+
it('should create a child injector when create is called', async () => {
|
|
31
31
|
const TestComponent = Shade<{ value: string }>({
|
|
32
32
|
shadowDomName: 'test-mfe-child-injector',
|
|
33
33
|
render: ({ props }) => <div>{props.value}</div>,
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
await usingAsync(new Injector(), async (parentInjector) => {
|
|
37
|
+
const createChildSpy = vi.spyOn(parentInjector, 'createChild')
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const mfeService = createShadesMicroFrontend(TestComponent)
|
|
40
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
mfeService.create({
|
|
43
|
+
api: { value: 'test' },
|
|
44
|
+
rootElement,
|
|
45
|
+
injector: parentInjector,
|
|
46
|
+
})
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
expect(createChildSpy).toHaveBeenCalledWith({
|
|
49
|
+
owner: createShadesMicroFrontend,
|
|
50
|
+
})
|
|
50
51
|
})
|
|
51
52
|
})
|
|
52
53
|
|
|
@@ -58,19 +59,20 @@ describe('createShadesMicroFrontend', () => {
|
|
|
58
59
|
render: ({ props }) => <div data-testid="content">Value: {props.value}</div>,
|
|
59
60
|
})
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
63
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
const mfeService = createShadesMicroFrontend(TestComponent)
|
|
66
|
+
mfeService.create({
|
|
67
|
+
api: { value: testValue },
|
|
68
|
+
rootElement,
|
|
69
|
+
injector,
|
|
70
|
+
})
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
await sleepAsync(10)
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
expect(rootElement.innerHTML).toContain(`Value: ${testValue}`)
|
|
75
|
+
})
|
|
74
76
|
})
|
|
75
77
|
|
|
76
78
|
it('should render the component into the provided root element', async () => {
|
|
@@ -79,20 +81,21 @@ describe('createShadesMicroFrontend', () => {
|
|
|
79
81
|
render: () => <span>MFE Content</span>,
|
|
80
82
|
})
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
85
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
const mfeService = createShadesMicroFrontend(TestComponent)
|
|
88
|
+
mfeService.create({
|
|
89
|
+
api: {},
|
|
90
|
+
rootElement,
|
|
91
|
+
injector,
|
|
92
|
+
})
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
await sleepAsync(10)
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
expect(rootElement.querySelector('test-mfe-root-element')).toBeTruthy()
|
|
97
|
+
expect(rootElement.innerHTML).toContain('MFE Content')
|
|
98
|
+
})
|
|
96
99
|
})
|
|
97
100
|
|
|
98
101
|
it('should work with complex API types', async () => {
|
|
@@ -115,29 +118,30 @@ describe('createShadesMicroFrontend', () => {
|
|
|
115
118
|
),
|
|
116
119
|
})
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
122
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
const mfeService = createShadesMicroFrontend(TestComponent)
|
|
125
|
+
mfeService.create({
|
|
126
|
+
api: {
|
|
127
|
+
user: { id: '123', name: 'John Doe' },
|
|
128
|
+
onClick: clickHandler,
|
|
129
|
+
items: ['a', 'b', 'c'],
|
|
130
|
+
},
|
|
131
|
+
rootElement,
|
|
132
|
+
injector,
|
|
133
|
+
})
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
await sleepAsync(10)
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
expect(rootElement.innerHTML).toContain('John Doe')
|
|
138
|
+
expect(rootElement.innerHTML).toContain('3')
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
const button = rootElement.querySelector('button')
|
|
141
|
+
button?.click()
|
|
139
142
|
|
|
140
|
-
|
|
143
|
+
expect(clickHandler).toHaveBeenCalled()
|
|
144
|
+
})
|
|
141
145
|
})
|
|
142
146
|
|
|
143
147
|
it('should use the child injector for the shade root', async () => {
|
|
@@ -151,20 +155,21 @@ describe('createShadesMicroFrontend', () => {
|
|
|
151
155
|
},
|
|
152
156
|
})
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
await usingAsync(new Injector(), async (parentInjector) => {
|
|
159
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
156
160
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
const mfeService = createShadesMicroFrontend(TestComponent)
|
|
162
|
+
mfeService.create({
|
|
163
|
+
api: {},
|
|
164
|
+
rootElement,
|
|
165
|
+
injector: parentInjector,
|
|
166
|
+
})
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
await sleepAsync(10)
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
expect(capturedInjector).toBeDefined()
|
|
171
|
+
// The component should receive a child injector, not the parent
|
|
172
|
+
expect(capturedInjector).not.toBe(parentInjector)
|
|
173
|
+
})
|
|
169
174
|
})
|
|
170
175
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject'
|
|
2
|
-
import { sleepAsync } from '@furystack/utils'
|
|
2
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
3
3
|
|
|
4
|
-
import { createComponent, initializeShadeRoot, Shade } from '@furystack/shades'
|
|
4
|
+
import { createComponent, flushUpdates, initializeShadeRoot, Shade } from '@furystack/shades'
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
6
|
import { createShadesMicroFrontend } from './create-shades-micro-frontend.js'
|
|
7
7
|
import { MicroFrontend } from './micro-frontend.js'
|
|
@@ -15,145 +15,153 @@ describe('<MicroFrontend /> component', () => {
|
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
it('Should render the MFE app', async () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
19
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
20
|
+
|
|
21
|
+
const MfeComponent = Shade<{ value: string }>({
|
|
22
|
+
shadowDomName: 'mfe-test-example',
|
|
23
|
+
render: ({ props }) => <div>Loaded: {props.value}</div>,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const value = crypto.randomUUID()
|
|
27
|
+
|
|
28
|
+
initializeShadeRoot({
|
|
29
|
+
injector,
|
|
30
|
+
rootElement,
|
|
31
|
+
jsxElement: (
|
|
32
|
+
<MicroFrontend
|
|
33
|
+
api={{ value }}
|
|
34
|
+
loaderCallback={async () => {
|
|
35
|
+
return createShadesMicroFrontend(MfeComponent)
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await flushUpdates()
|
|
42
|
+
await sleepAsync(1)
|
|
43
|
+
|
|
44
|
+
expect(document.body.innerHTML).toBe(
|
|
45
|
+
`<div id="root"><shade-micro-frontend><div style="display: contents;"><mfe-test-example><div>Loaded: ${value}</div></mfe-test-example></div></shade-micro-frontend></div>`,
|
|
46
|
+
)
|
|
39
47
|
})
|
|
40
|
-
|
|
41
|
-
await sleepAsync(1)
|
|
42
|
-
|
|
43
|
-
expect(document.body.innerHTML).toBe(
|
|
44
|
-
`<div id="root"><shade-micro-frontend><mfe-test-example><div>Loaded: ${value}</div></mfe-test-example></shade-micro-frontend></div>`,
|
|
45
|
-
)
|
|
46
48
|
})
|
|
47
49
|
|
|
48
50
|
it('Should render the MFE app with a loader', async () => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
51
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
52
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
53
|
+
|
|
54
|
+
const MfeComponent = Shade<{ value: string }>({
|
|
55
|
+
shadowDomName: 'mfe-test-example-w-loader',
|
|
56
|
+
render: ({ props }) => <div>Loaded: {props.value}</div>,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const value = crypto.randomUUID()
|
|
60
|
+
|
|
61
|
+
initializeShadeRoot({
|
|
62
|
+
injector,
|
|
63
|
+
rootElement,
|
|
64
|
+
jsxElement: (
|
|
65
|
+
<MicroFrontend
|
|
66
|
+
loader={<div>Loading...</div>}
|
|
67
|
+
api={{ value }}
|
|
68
|
+
loaderCallback={async () => {
|
|
69
|
+
await sleepAsync(10)
|
|
70
|
+
return createShadesMicroFrontend(MfeComponent)
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
),
|
|
74
|
+
})
|
|
75
|
+
await flushUpdates()
|
|
76
|
+
expect(document.body.innerHTML).toBe(
|
|
77
|
+
'<div id="root"><shade-micro-frontend><div style="display: contents;"><div>Loading...</div></div></shade-micro-frontend></div>',
|
|
78
|
+
)
|
|
79
|
+
await sleepAsync(20)
|
|
80
|
+
expect(document.body.innerHTML).toBe(
|
|
81
|
+
`<div id="root"><shade-micro-frontend><div style="display: contents;"><mfe-test-example-w-loader><div>Loaded: ${value}</div></mfe-test-example-w-loader></div></shade-micro-frontend></div>`,
|
|
82
|
+
)
|
|
72
83
|
})
|
|
73
|
-
expect(document.body.innerHTML).toBe(
|
|
74
|
-
'<div id="root"><shade-micro-frontend><div>Loading...</div></shade-micro-frontend></div>',
|
|
75
|
-
)
|
|
76
|
-
await sleepAsync(20)
|
|
77
|
-
expect(document.body.innerHTML).toBe(
|
|
78
|
-
`<div id="root"><shade-micro-frontend><mfe-test-example-w-loader><div>Loaded: ${value}</div></mfe-test-example-w-loader></shade-micro-frontend></div>`,
|
|
79
|
-
)
|
|
80
84
|
})
|
|
81
85
|
|
|
82
86
|
it('Should render the MFE app with an error', async () => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
88
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
89
|
+
|
|
90
|
+
initializeShadeRoot({
|
|
91
|
+
injector,
|
|
92
|
+
rootElement,
|
|
93
|
+
jsxElement: (
|
|
94
|
+
<MicroFrontend
|
|
95
|
+
error={() => <div>Error...</div>}
|
|
96
|
+
api={{}}
|
|
97
|
+
loaderCallback={async () => {
|
|
98
|
+
throw Error(':(')
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
await flushUpdates()
|
|
104
|
+
await sleepAsync(10)
|
|
105
|
+
expect(document.body.innerHTML).toBe(
|
|
106
|
+
`<div id="root"><shade-micro-frontend><div style="display: contents;"><div>Error...</div></div></shade-micro-frontend></div>`,
|
|
107
|
+
)
|
|
98
108
|
})
|
|
99
|
-
await sleepAsync(10)
|
|
100
|
-
expect(document.body.innerHTML).toBe(
|
|
101
|
-
`<div id="root"><shade-micro-frontend><div>Error...</div></shade-micro-frontend></div>`,
|
|
102
|
-
)
|
|
103
109
|
})
|
|
104
110
|
|
|
105
111
|
it('Should execute the destroy callback', async () => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
Remove MFE
|
|
125
|
-
</button>
|
|
126
|
-
{hasMfe && (
|
|
127
|
-
<MicroFrontend
|
|
128
|
-
loader={<div>Loading...</div>}
|
|
129
|
-
api={{ value }}
|
|
130
|
-
loaderCallback={async () => {
|
|
131
|
-
return {
|
|
132
|
-
create: () => {},
|
|
133
|
-
destroy,
|
|
134
|
-
}
|
|
112
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
113
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
114
|
+
|
|
115
|
+
const value = crypto.randomUUID()
|
|
116
|
+
|
|
117
|
+
const destroy = vi.fn()
|
|
118
|
+
|
|
119
|
+
const Host = Shade({
|
|
120
|
+
shadowDomName: 'mfe-test-example-w-destroy-host',
|
|
121
|
+
render: ({ useState }) => {
|
|
122
|
+
const [hasMfe, setHasMfe] = useState('hasMfe', true)
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<button
|
|
126
|
+
onclick={() => {
|
|
127
|
+
setHasMfe(false)
|
|
135
128
|
}}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
129
|
+
>
|
|
130
|
+
Remove MFE
|
|
131
|
+
</button>
|
|
132
|
+
{hasMfe && (
|
|
133
|
+
<MicroFrontend
|
|
134
|
+
loader={<div>Loading...</div>}
|
|
135
|
+
api={{ value }}
|
|
136
|
+
loaderCallback={async () => {
|
|
137
|
+
return {
|
|
138
|
+
create: () => {},
|
|
139
|
+
destroy,
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
</>
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
initializeShadeRoot({
|
|
150
|
+
injector,
|
|
151
|
+
rootElement,
|
|
152
|
+
jsxElement: <Host />,
|
|
153
|
+
})
|
|
154
|
+
await flushUpdates()
|
|
155
|
+
await sleepAsync(10)
|
|
156
|
+
expect(document.body.innerHTML).toBe(
|
|
157
|
+
'<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button><shade-micro-frontend><div style="display: contents;"></div></shade-micro-frontend></mfe-test-example-w-destroy-host></div>',
|
|
158
|
+
)
|
|
159
|
+
document.querySelector('button')!.click()
|
|
160
|
+
await sleepAsync(10)
|
|
161
|
+
expect(destroy).toHaveBeenCalledWith({ api: { value }, injector: expect.any(Injector) as Injector })
|
|
162
|
+
expect(document.body.innerHTML).toBe(
|
|
163
|
+
`<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button></mfe-test-example-w-destroy-host></div>`,
|
|
164
|
+
)
|
|
147
165
|
})
|
|
148
|
-
await sleepAsync(10)
|
|
149
|
-
expect(document.body.innerHTML).toBe(
|
|
150
|
-
'<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button><shade-micro-frontend></shade-micro-frontend></mfe-test-example-w-destroy-host></div>',
|
|
151
|
-
)
|
|
152
|
-
document.querySelector('button')!.click()
|
|
153
|
-
await sleepAsync(10)
|
|
154
|
-
expect(destroy).toHaveBeenCalledWith({ api: { value }, injector: expect.any(Injector) as Injector })
|
|
155
|
-
expect(document.body.innerHTML).toBe(
|
|
156
|
-
`<div id="root"><mfe-test-example-w-destroy-host><button>Remove MFE</button></mfe-test-example-w-destroy-host></div>`,
|
|
157
|
-
)
|
|
158
166
|
})
|
|
159
167
|
})
|
package/src/micro-frontend.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Shade } from '@furystack/shades'
|
|
1
|
+
import { Shade, createComponent } from '@furystack/shades'
|
|
2
2
|
import type { CreateMicroFrontendService } from './create-microfrontend-service.js'
|
|
3
3
|
|
|
4
4
|
type MicroFrontendProps<TApi> = {
|
|
@@ -10,42 +10,63 @@ type MicroFrontendProps<TApi> = {
|
|
|
10
10
|
|
|
11
11
|
export const MicroFrontend: <TApi>(props: MicroFrontendProps<TApi>) => JSX.Element = Shade({
|
|
12
12
|
shadowDomName: 'shade-micro-frontend',
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const creatorService = await props.loaderCallback()
|
|
16
|
-
// TODO: Think about type checking
|
|
17
|
-
// if (!(creatorService instanceof CreateMicroFrontendService)) {
|
|
18
|
-
// throw new Error('Invalid creator service')
|
|
19
|
-
// }
|
|
20
|
-
|
|
21
|
-
const childInjector = injector.createChild({
|
|
22
|
-
owner: creatorService,
|
|
23
|
-
})
|
|
13
|
+
render: ({ props, injector, useDisposable, useRef }) => {
|
|
14
|
+
const containerRef = useRef<HTMLDivElement>('mfeContainer')
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
useDisposable('mfe-loader', () => {
|
|
17
|
+
let destroyFn: (() => void) | undefined
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
injector: childInjector,
|
|
31
|
-
})
|
|
19
|
+
const tryCreateComponent = async () => {
|
|
20
|
+
const container = containerRef.current
|
|
21
|
+
if (!container) return
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
const creatorService = await props.loaderCallback()
|
|
24
|
+
|
|
25
|
+
const childInjector = injector.createChild({
|
|
26
|
+
owner: creatorService,
|
|
27
|
+
})
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
29
|
+
container.innerHTML = ''
|
|
30
|
+
|
|
31
|
+
creatorService.create({
|
|
32
|
+
api: props.api,
|
|
33
|
+
rootElement: container,
|
|
34
|
+
injector: childInjector,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
destroyFn = () => creatorService.destroy?.({ api: props.api, injector: childInjector })
|
|
45
38
|
}
|
|
39
|
+
|
|
40
|
+
queueMicrotask(() => {
|
|
41
|
+
tryCreateComponent().catch((error: unknown) => {
|
|
42
|
+
if (props.error && containerRef.current) {
|
|
43
|
+
containerRef.current.appendChild(
|
|
44
|
+
props.error(error, async () => {
|
|
45
|
+
await tryCreateComponent()
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
[Symbol.asyncDispose]: async () => destroyFn?.(),
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Propagate prop updates to the inner MFE element (for Shade-based MFEs)
|
|
58
|
+
const mfeElement = containerRef.current?.firstElementChild as
|
|
59
|
+
| (HTMLElement & { props?: unknown; updateComponent?: () => void })
|
|
60
|
+
| null
|
|
61
|
+
if (mfeElement?.updateComponent) {
|
|
62
|
+
mfeElement.props = props.api
|
|
63
|
+
mfeElement.updateComponent()
|
|
46
64
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div ref={containerRef} style={{ display: 'contents' }}>
|
|
68
|
+
{props.loader || null}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
50
71
|
},
|
|
51
72
|
})
|