@furystack/shades-mfe 1.0.27 → 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 CHANGED
@@ -1,5 +1,44 @@
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
+
36
+ ## [1.0.28] - 2026-02-01
37
+
38
+ ### ⬆️ Dependencies
39
+
40
+ - Updated peer dependency `@furystack/shades` to include new CSS styling features
41
+
3
42
  ## [1.0.27] - 2026-01-26
4
43
 
5
44
  ### 🔧 Chores
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-shades-micro-frontend.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-shades-micro-frontend.spec.d.ts","sourceRoot":"","sources":["../src/create-shades-micro-frontend.spec.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,134 @@
1
+ import { Injector } from '@furystack/inject';
2
+ import { createComponent, Shade } from '@furystack/shades';
3
+ import { sleepAsync, usingAsync } from '@furystack/utils';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { CreateMicroFrontendService } from './create-microfrontend-service.js';
6
+ import { createShadesMicroFrontend } from './create-shades-micro-frontend.js';
7
+ describe('createShadesMicroFrontend', () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = '<div id="root"></div>';
10
+ });
11
+ afterEach(() => {
12
+ document.body.innerHTML = '';
13
+ });
14
+ it('should return a CreateMicroFrontendService instance', () => {
15
+ const TestComponent = Shade({
16
+ shadowDomName: 'test-mfe-return-type',
17
+ render: ({ props }) => createComponent("div", null, props.value),
18
+ });
19
+ const result = createShadesMicroFrontend(TestComponent);
20
+ expect(result).toBeInstanceOf(CreateMicroFrontendService);
21
+ expect(result.create).toBeTypeOf('function');
22
+ expect(result.destroy).toBeUndefined();
23
+ });
24
+ it('should create a child injector when create is called', async () => {
25
+ const TestComponent = Shade({
26
+ shadowDomName: 'test-mfe-child-injector',
27
+ render: ({ props }) => createComponent("div", null, props.value),
28
+ });
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
+ });
41
+ });
42
+ });
43
+ it('should render the component with the provided API props', async () => {
44
+ const testValue = crypto.randomUUID();
45
+ const TestComponent = Shade({
46
+ shadowDomName: 'test-mfe-render-props',
47
+ render: ({ props }) => createComponent("div", { "data-testid": "content" },
48
+ "Value: ",
49
+ props.value),
50
+ });
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}`);
61
+ });
62
+ });
63
+ it('should render the component into the provided root element', async () => {
64
+ const TestComponent = Shade({
65
+ shadowDomName: 'test-mfe-root-element',
66
+ render: () => createComponent("span", null, "MFE Content"),
67
+ });
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');
79
+ });
80
+ });
81
+ it('should work with complex API types', async () => {
82
+ const clickHandler = vi.fn();
83
+ const TestComponent = Shade({
84
+ shadowDomName: 'test-mfe-complex-api',
85
+ render: ({ props }) => (createComponent("div", null,
86
+ createComponent("span", { "data-testid": "user-name" }, props.user.name),
87
+ createComponent("span", { "data-testid": "item-count" }, props.items.length),
88
+ createComponent("button", { onclick: props.onClick }, "Click"))),
89
+ });
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();
108
+ });
109
+ });
110
+ it('should use the child injector for the shade root', async () => {
111
+ let capturedInjector;
112
+ const TestComponent = Shade({
113
+ shadowDomName: 'test-mfe-injector-capture',
114
+ render: ({ injector }) => {
115
+ capturedInjector = injector;
116
+ return createComponent("div", null, "Test");
117
+ },
118
+ });
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);
131
+ });
132
+ });
133
+ });
134
+ //# sourceMappingURL=create-shades-micro-frontend.spec.js.map
@@ -0,0 +1 @@
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,OAwCzE,CAAA"}
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"}
@@ -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
- constructed: async ({ props, element, injector }) => {
5
- const tryCreateComponent = async () => {
6
- const creatorService = await props.loaderCallback();
7
- // TODO: Think about type checking
8
- // if (!(creatorService instanceof CreateMicroFrontendService)) {
9
- // throw new Error('Invalid creator service')
10
- // }
11
- const childInjector = injector.createChild({
12
- owner: creatorService,
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
- element.innerHTML = '';
15
- creatorService.create({
16
- api: props.api,
17
- rootElement: element,
18
- injector: childInjector,
19
- });
20
- return () => creatorService.destroy?.({ api: props.api, injector: childInjector });
21
- };
22
- try {
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;AAUzC,MAAM,CAAC,MAAM,aAAa,GAA2D,KAAK,CAAC;IACzF,aAAa,EAAE,sBAAsB;IACrC,WAAW,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;QAClD,MAAM,kBAAkB,GAAG,KAAK,IAAI,EAAE;YACpC,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE,CAAA;YACnD,kCAAkC;YAClC,iEAAiE;YACjE,+CAA+C;YAC/C,IAAI;YAEJ,MAAM,aAAa,GAAG,QAAQ,CAAC,WAAW,CAAC;gBACzC,KAAK,EAAE,cAAc;aACtB,CAAC,CAAA;YAEF,OAAO,CAAC,SAAS,GAAG,EAAE,CAAA;YAEtB,cAAc,CAAC,MAAM,CAAC;gBACpB,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,WAAW,EAAE,OAAO;gBACpB,QAAQ,EAAE,aAAa;aACxB,CAAC,CAAA;YAEF,OAAO,GAAG,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAA;QACpF,CAAC,CAAA;QAED,IAAI,CAAC;YACH,OAAO,MAAM,kBAAkB,EAAE,CAAA;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChB,OAAO,CAAC,WAAW,CACjB,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;oBAC5B,MAAM,kBAAkB,EAAE,CAAA;gBAC5B,CAAC,CAAC,CACH,CAAA;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;QACpB,OAAO,KAAK,CAAC,MAAM,IAAI,IAAI,CAAA;IAC7B,CAAC;CACF,CAAC,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
- const injector = new 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),
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
- const injector = new Injector();
36
- const rootElement = document.getElementById('root');
37
- const MfeComponent = Shade({
38
- shadowDomName: 'mfe-test-example-w-loader',
39
- render: ({ props }) => createComponent("div", null,
40
- "Loaded: ",
41
- props.value),
42
- });
43
- const value = crypto.randomUUID();
44
- initializeShadeRoot({
45
- injector,
46
- rootElement,
47
- jsxElement: (createComponent(MicroFrontend, { loader: createComponent("div", null, "Loading..."), api: { value }, loaderCallback: async () => {
48
- await sleepAsync(10);
49
- return createShadesMicroFrontend(MfeComponent);
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
- const injector = new Injector();
58
- const rootElement = document.getElementById('root');
59
- initializeShadeRoot({
60
- injector,
61
- rootElement,
62
- jsxElement: (createComponent(MicroFrontend, { error: () => createComponent("div", null, "Error..."), api: {}, loaderCallback: async () => {
63
- throw Error(':(');
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
- const injector = new Injector();
71
- const rootElement = document.getElementById('root');
72
- const value = crypto.randomUUID();
73
- const destroy = vi.fn();
74
- const Host = Shade({
75
- shadowDomName: 'mfe-test-example-w-destroy-host',
76
- render: ({ useState }) => {
77
- const [hasMfe, setHasMfe] = useState('hasMfe', true);
78
- return (createComponent(createComponent, null,
79
- createComponent("button", { onclick: () => {
80
- setHasMfe(false);
81
- } }, "Remove MFE"),
82
- hasMfe && (createComponent(MicroFrontend, { loader: createComponent("div", null, "Loading..."), api: { value }, loaderCallback: async () => {
83
- return {
84
- create: () => { },
85
- destroy,
86
- };
87
- } }))));
88
- },
89
- });
90
- initializeShadeRoot({
91
- injector,
92
- rootElement,
93
- jsxElement: createComponent(Host, null),
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;AAE7C,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAC/E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,UAAU,CAAC,GAAG,EAAE;QACd,QAAQ,CAAC,IAAI,CAAC,SAAS,GAAG,uBAAuB,CAAA;IACnD,CAAC,CAAC,CAAA;IACF,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;IAC9B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;QACzC,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA;QAC/B,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAmB,CAAA;QAErE,MAAM,YAAY,GAAG,KAAK,CAAoB;YAC5C,aAAa,EAAE,kBAAkB;YACjC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;;gBAAc,KAAK,CAAC,KAAK,CAAO;SACxD,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAA;QAEjC,mBAAmB,CAAC;YAClB,QAAQ;YACR,WAAW;YACX,UAAU,EAAE,CACV,gBAAC,aAAa,IACZ,GAAG,EAAE,EAAE,KAAK,EAAE,EACd,cAAc,EAAE,KAAK,IAAI,EAAE;oBACzB,OAAO,yBAAyB,CAAC,YAAY,CAAC,CAAA;gBAChD,CAAC,GACD,CACH;SACF,CAAC,CAAA;QAEF,MAAM,UAAU,CAAC,CAAC,CAAC,CAAA;QAEnB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAClC,uEAAuE,KAAK,wDAAwD,CACrI,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA;QAC/B,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAmB,CAAA;QAErE,MAAM,YAAY,GAAG,KAAK,CAAoB;YAC5C,aAAa,EAAE,2BAA2B;YAC1C,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;;gBAAc,KAAK,CAAC,KAAK,CAAO;SACxD,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAA;QAEjC,mBAAmB,CAAC;YAClB,QAAQ;YACR,WAAW;YACX,UAAU,EAAE,CACV,gBAAC,aAAa,IACZ,MAAM,EAAE,0CAAqB,EAC7B,GAAG,EAAE,EAAE,KAAK,EAAE,EACd,cAAc,EAAE,KAAK,IAAI,EAAE;oBACzB,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;oBACpB,OAAO,yBAAyB,CAAC,YAAY,CAAC,CAAA;gBAChD,CAAC,GACD,CACH;SACF,CAAC,CAAA;QACF,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAClC,yFAAyF,CAC1F,CAAA;QACD,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;QACpB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAClC,gFAAgF,KAAK,iEAAiE,CACvJ,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA;QAC/B,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAmB,CAAA;QAErE,mBAAmB,CAAC;YAClB,QAAQ;YACR,WAAW;YACX,UAAU,EAAE,CACV,gBAAC,aAAa,IACZ,KAAK,EAAE,GAAG,EAAE,CAAC,wCAAmB,EAChC,GAAG,EAAE,EAAE,EACP,cAAc,EAAE,KAAK,IAAI,EAAE;oBACzB,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA;gBACnB,CAAC,GACD,CACH;SACF,CAAC,CAAA;QACF,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;QACpB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAClC,uFAAuF,CACxF,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA;QAC/B,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAmB,CAAA;QAErE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAA;QAEjC,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QAEvB,MAAM,IAAI,GAAG,KAAK,CAAC;YACjB,aAAa,EAAE,iCAAiC;YAChD,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;gBACvB,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;gBACpD,OAAO,CACL;oBACE,4BACE,OAAO,EAAE,GAAG,EAAE;4BACZ,SAAS,CAAC,KAAK,CAAC,CAAA;wBAClB,CAAC,iBAGM;oBACR,MAAM,IAAI,CACT,gBAAC,aAAa,IACZ,MAAM,EAAE,0CAAqB,EAC7B,GAAG,EAAE,EAAE,KAAK,EAAE,EACd,cAAc,EAAE,KAAK,IAAI,EAAE;4BACzB,OAAO;gCACL,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;gCAChB,OAAO;6BACR,CAAA;wBACH,CAAC,GACD,CACH,CACA,CACJ,CAAA;YACH,CAAC;SACF,CAAC,CAAA;QAEF,mBAAmB,CAAC;YAClB,QAAQ;YACR,WAAW;YACX,UAAU,EAAE,gBAAC,IAAI,OAAG;SACrB,CAAC,CAAA;QACF,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;QACpB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAClC,kKAAkK,CACnK,CAAA;QACD,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAE,CAAC,KAAK,EAAE,CAAA;QACzC,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;QACpB,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAa,EAAE,CAAC,CAAA;QACpG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAClC,qHAAqH,CACtH,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,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": "1.0.27",
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.28",
41
- "@furystack/shades": "^11.0.35"
40
+ "@furystack/inject": "^12.0.29",
41
+ "@furystack/shades": "^12.0.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^25.0.10",
@@ -0,0 +1,175 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, Shade } from '@furystack/shades'
3
+ import { sleepAsync, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { CreateMicroFrontendService } from './create-microfrontend-service.js'
6
+ import { createShadesMicroFrontend } from './create-shades-micro-frontend.js'
7
+
8
+ describe('createShadesMicroFrontend', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '<div id="root"></div>'
11
+ })
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = ''
15
+ })
16
+
17
+ it('should return a CreateMicroFrontendService instance', () => {
18
+ const TestComponent = Shade<{ value: string }>({
19
+ shadowDomName: 'test-mfe-return-type',
20
+ render: ({ props }) => <div>{props.value}</div>,
21
+ })
22
+
23
+ const result = createShadesMicroFrontend(TestComponent)
24
+
25
+ expect(result).toBeInstanceOf(CreateMicroFrontendService)
26
+ expect(result.create).toBeTypeOf('function')
27
+ expect(result.destroy).toBeUndefined()
28
+ })
29
+
30
+ it('should create a child injector when create is called', async () => {
31
+ const TestComponent = Shade<{ value: string }>({
32
+ shadowDomName: 'test-mfe-child-injector',
33
+ render: ({ props }) => <div>{props.value}</div>,
34
+ })
35
+
36
+ await usingAsync(new Injector(), async (parentInjector) => {
37
+ const createChildSpy = vi.spyOn(parentInjector, 'createChild')
38
+
39
+ const mfeService = createShadesMicroFrontend(TestComponent)
40
+ const rootElement = document.getElementById('root') as HTMLDivElement
41
+
42
+ mfeService.create({
43
+ api: { value: 'test' },
44
+ rootElement,
45
+ injector: parentInjector,
46
+ })
47
+
48
+ expect(createChildSpy).toHaveBeenCalledWith({
49
+ owner: createShadesMicroFrontend,
50
+ })
51
+ })
52
+ })
53
+
54
+ it('should render the component with the provided API props', async () => {
55
+ const testValue = crypto.randomUUID()
56
+
57
+ const TestComponent = Shade<{ value: string }>({
58
+ shadowDomName: 'test-mfe-render-props',
59
+ render: ({ props }) => <div data-testid="content">Value: {props.value}</div>,
60
+ })
61
+
62
+ await usingAsync(new Injector(), async (injector) => {
63
+ const rootElement = document.getElementById('root') as HTMLDivElement
64
+
65
+ const mfeService = createShadesMicroFrontend(TestComponent)
66
+ mfeService.create({
67
+ api: { value: testValue },
68
+ rootElement,
69
+ injector,
70
+ })
71
+
72
+ await sleepAsync(10)
73
+
74
+ expect(rootElement.innerHTML).toContain(`Value: ${testValue}`)
75
+ })
76
+ })
77
+
78
+ it('should render the component into the provided root element', async () => {
79
+ const TestComponent = Shade<object>({
80
+ shadowDomName: 'test-mfe-root-element',
81
+ render: () => <span>MFE Content</span>,
82
+ })
83
+
84
+ await usingAsync(new Injector(), async (injector) => {
85
+ const rootElement = document.getElementById('root') as HTMLDivElement
86
+
87
+ const mfeService = createShadesMicroFrontend(TestComponent)
88
+ mfeService.create({
89
+ api: {},
90
+ rootElement,
91
+ injector,
92
+ })
93
+
94
+ await sleepAsync(10)
95
+
96
+ expect(rootElement.querySelector('test-mfe-root-element')).toBeTruthy()
97
+ expect(rootElement.innerHTML).toContain('MFE Content')
98
+ })
99
+ })
100
+
101
+ it('should work with complex API types', async () => {
102
+ type ComplexApi = {
103
+ user: { id: string; name: string }
104
+ onClick: () => void
105
+ items: string[]
106
+ }
107
+
108
+ const clickHandler = vi.fn()
109
+
110
+ const TestComponent = Shade<ComplexApi>({
111
+ shadowDomName: 'test-mfe-complex-api',
112
+ render: ({ props }) => (
113
+ <div>
114
+ <span data-testid="user-name">{props.user.name}</span>
115
+ <span data-testid="item-count">{props.items.length}</span>
116
+ <button onclick={props.onClick}>Click</button>
117
+ </div>
118
+ ),
119
+ })
120
+
121
+ await usingAsync(new Injector(), async (injector) => {
122
+ const rootElement = document.getElementById('root') as HTMLDivElement
123
+
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
+ })
134
+
135
+ await sleepAsync(10)
136
+
137
+ expect(rootElement.innerHTML).toContain('John Doe')
138
+ expect(rootElement.innerHTML).toContain('3')
139
+
140
+ const button = rootElement.querySelector('button')
141
+ button?.click()
142
+
143
+ expect(clickHandler).toHaveBeenCalled()
144
+ })
145
+ })
146
+
147
+ it('should use the child injector for the shade root', async () => {
148
+ let capturedInjector: Injector | undefined
149
+
150
+ const TestComponent = Shade<object>({
151
+ shadowDomName: 'test-mfe-injector-capture',
152
+ render: ({ injector }) => {
153
+ capturedInjector = injector
154
+ return <div>Test</div>
155
+ },
156
+ })
157
+
158
+ await usingAsync(new Injector(), async (parentInjector) => {
159
+ const rootElement = document.getElementById('root') as HTMLDivElement
160
+
161
+ const mfeService = createShadesMicroFrontend(TestComponent)
162
+ mfeService.create({
163
+ api: {},
164
+ rootElement,
165
+ injector: parentInjector,
166
+ })
167
+
168
+ await sleepAsync(10)
169
+
170
+ expect(capturedInjector).toBeDefined()
171
+ // The component should receive a child injector, not the parent
172
+ expect(capturedInjector).not.toBe(parentInjector)
173
+ })
174
+ })
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
- const injector = new 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
- ),
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
- const injector = new Injector()
50
- const rootElement = document.getElementById('root') as HTMLDivElement
51
-
52
- const MfeComponent = Shade<{ value: string }>({
53
- shadowDomName: 'mfe-test-example-w-loader',
54
- render: ({ props }) => <div>Loaded: {props.value}</div>,
55
- })
56
-
57
- const value = crypto.randomUUID()
58
-
59
- initializeShadeRoot({
60
- injector,
61
- rootElement,
62
- jsxElement: (
63
- <MicroFrontend
64
- loader={<div>Loading...</div>}
65
- api={{ value }}
66
- loaderCallback={async () => {
67
- await sleepAsync(10)
68
- return createShadesMicroFrontend(MfeComponent)
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
- const injector = new Injector()
84
- const rootElement = document.getElementById('root') as HTMLDivElement
85
-
86
- initializeShadeRoot({
87
- injector,
88
- rootElement,
89
- jsxElement: (
90
- <MicroFrontend
91
- error={() => <div>Error...</div>}
92
- api={{}}
93
- loaderCallback={async () => {
94
- throw Error(':(')
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
- const injector = new Injector()
107
- const rootElement = document.getElementById('root') as HTMLDivElement
108
-
109
- const value = crypto.randomUUID()
110
-
111
- const destroy = vi.fn()
112
-
113
- const Host = Shade({
114
- shadowDomName: 'mfe-test-example-w-destroy-host',
115
- render: ({ useState }) => {
116
- const [hasMfe, setHasMfe] = useState('hasMfe', true)
117
- return (
118
- <>
119
- <button
120
- onclick={() => {
121
- setHasMfe(false)
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
- initializeShadeRoot({
144
- injector,
145
- rootElement,
146
- jsxElement: <Host />,
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
  })
@@ -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
- constructed: async ({ props, element, injector }) => {
14
- const tryCreateComponent = async () => {
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
- element.innerHTML = ''
16
+ useDisposable('mfe-loader', () => {
17
+ let destroyFn: (() => void) | undefined
26
18
 
27
- creatorService.create({
28
- api: props.api,
29
- rootElement: element,
30
- injector: childInjector,
31
- })
19
+ const tryCreateComponent = async () => {
20
+ const container = containerRef.current
21
+ if (!container) return
32
22
 
33
- return () => creatorService.destroy?.({ api: props.api, injector: childInjector })
34
- }
23
+ const creatorService = await props.loaderCallback()
24
+
25
+ const childInjector = injector.createChild({
26
+ owner: creatorService,
27
+ })
35
28
 
36
- try {
37
- return await tryCreateComponent()
38
- } catch (error) {
39
- if (props.error) {
40
- element.appendChild(
41
- props.error(error, async () => {
42
- await tryCreateComponent()
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
- render: ({ props }) => {
49
- return props.loader || null
65
+
66
+ return (
67
+ <div ref={containerRef} style={{ display: 'contents' }}>
68
+ {props.loader || null}
69
+ </div>
70
+ )
50
71
  },
51
72
  })