mfe-runtime-z 1.0.0 → 1.1.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/README.md CHANGED
@@ -24,11 +24,11 @@ This library focuses on **application-level integration**, not component sharing
24
24
  #### What it solves
25
25
 
26
26
  - Load and mount remote applications dynamically
27
- - Isolate application lifecycles (mount / unmount / reload)
27
+ - Isolate application lifecycles (mount/unmount/reload) ✅ via Shadow DOM
28
28
  - Share state safely between apps
29
29
  - Synchronize routing without sharing router instances
30
30
  - Communicate via a central event bus
31
- - Work with **any framework** (React, Vue, Svelte, Vanilla JS)
31
+ - Work with **any framework** (React, Vue, Svelte, Vanilla JS...)
32
32
 
33
33
  ---
34
34
 
@@ -55,13 +55,11 @@ yarn add mfe-runtime-z
55
55
  Micro-frontends expose a simple runtime contract:
56
56
 
57
57
  ```ts
58
- window.myRemoteApp = {
59
- mount(el, ctx),
60
- unmount(el)
61
- }
58
+ window.myRemoteApp = { mount(el, ctx), unmount(el) }
62
59
  ```
63
60
 
64
- No framework or bundler assumptions.
61
+ - No framework or bundler assumptions.
62
+ - Compatible with React, Vue, Vanilla JS
65
63
 
66
64
  ---
67
65
 
@@ -72,112 +70,93 @@ No framework or bundler assumptions.
72
70
  ```ts
73
71
  import { MFEHost, createSharedStore } from "mfe-runtime-z"
74
72
 
75
- const authStore = createSharedStore({
76
- user: null
77
- })
73
+ // shared store example
74
+ const authStore = createSharedStore({ user: null })
78
75
 
76
+ // create host with isolate: true for Shadow DOM + error isolation
79
77
  const host = new MFEHost({
80
78
  stores: { auth: authStore },
81
- navigate: (path) => history.pushState({}, "", path)
79
+ navigate: (path) => history.pushState({}, "", path),
80
+ isolate: false, // => shadow
81
+ onRemoteError: (err) => console.error("Remote error:", err)
82
82
  })
83
83
 
84
- const remote = await host.load(
85
- "http://localhost:3001/remote.js",
86
- "productApp"
87
- )
84
+ // load remote apps
85
+ const productRemote = await host.load("http://localhost:3001/remote.js", "productApp")
86
+ const cartRemote = await host.load("http://localhost:3002/remote.js", "cartApp")
88
87
 
89
- host.mount(remote, document.getElementById("app"), "productApp")
88
+ // mount remotes into host container divs
89
+ host.mount(productRemote, document.getElementById("product-root")!, "productApp", { isolate: true }) // shadow
90
+ host.mount(cartRemote, document.getElementById("cart-root")!, "cartApp")
90
91
 
91
- // host.unmount(remote, document.getElementById("app"), "productApp")
92
- ```
92
+ // // Unmount Shadow DOM remote
93
+ // host.unmount(productRemote, document.getElementById("product-root")!, "productApp")
94
+ // // Unmount normal remote
95
+ // host.unmount(cartRemote, document.getElementById("cart-root")!, "cartApp")
93
96
 
94
- ---
97
+ ```
95
98
 
96
99
  ##### Remote application (framework-agnostic)
97
100
 
98
101
  ```ts
99
- export function mount(el, ctx) {
100
- const authStore = ctx.stores.auth
101
-
102
- authStore.subscribe((state) => {
103
- el.innerHTML = state.user
104
- ? `Hello ${state.user.name}`
105
- : `<button id="login">Login</button>`
106
-
107
- el.querySelector("#login")?.addEventListener("click", () => {
108
- authStore.setState({
109
- user: { id: "1", name: "Alice" }
110
- })
111
- })
112
- })
102
+ export function mount(el: ShadowRoot) {
103
+ let count = 0
104
+ const div = document.createElement("div")
105
+ const btn = document.createElement("button")
106
+ const p = document.createElement("p")
107
+ btn.textContent = "Add Item"
108
+ p.textContent = count.toString()
109
+ btn.onclick = () => (p.textContent = (++count).toString())
110
+ div.appendChild(btn)
111
+ div.appendChild(p)
112
+ el.appendChild(div)
113
113
  }
114
114
 
115
- export function unmount(el) {
115
+ export function unmount(el: ShadowRoot) {
116
116
  el.innerHTML = ""
117
117
  }
118
118
 
119
- window.productApp = { mount, unmount }
119
+ ;(window as any).cartApp = { mount, unmount }
120
120
  ```
121
121
 
122
122
  ##### Remote app (framework-react-library)
123
123
 
124
124
  ```ts
125
- // src/app.tsx
126
125
  import React from "react"
127
126
  import ReactDOM from "react-dom/client"
128
127
 
129
128
  type AuthStore = {
130
129
  user: { id: string; name: string } | null
131
- subscribe: (callback: (state: any) => void) => void
132
- setState: (newState: any) => void
130
+ subscribe: (cb: (state:any)=>void)=>void
131
+ setState: (s:any)=>void
133
132
  }
134
133
 
135
- // React component
136
134
  function App({ store }: { store: AuthStore }) {
137
135
  const [user, setUser] = React.useState(store.user)
138
-
139
136
  React.useEffect(() => {
140
- const unsubscribe = store.subscribe((state: any) => {
141
- setUser(state.user)
142
- })
143
- return () => unsubscribe?.()
137
+ const unsub = store.subscribe((state:any) => setUser(state.user))
138
+ return () => unsub?.()
144
139
  }, [store])
145
-
146
- const handleLogin = () => {
147
- store.setState({ user: { id: "1", name: "Alice" } })
148
- }
149
-
150
140
  return (
151
141
  <div>
152
- {user ? (
153
- <span>Hello {user.name}</span>
154
- ) : (
155
- <button id="login" onClick={handleLogin}>
156
- Login
157
- </button>
158
- )}
142
+ {user ? <span>Hello {user.name}</span> : <button onClick={()=>store.setState({user:{id:"1",name:"Alice"}})}>Login</button>}
159
143
  </div>
160
144
  )
161
145
  }
162
146
 
163
- // index.js
164
- // mount/unmount functions
165
- export function mount(el: HTMLElement, ctx: { stores: { auth: AuthStore } }) {
166
- const root = ReactDOM.createRoot(el)
147
+ // mount/unmount compatible with Shadow DOM
148
+ export function mount(el: HTMLElement | ShadowRoot, ctx: { stores: { auth: AuthStore } }) {
149
+ const root = ReactDOM.createRoot(el as unknown as HTMLElement)
167
150
  ;(el as any)._reactRoot = root
168
151
  root.render(<App store={ctx.stores.auth} />)
169
152
  }
170
153
 
171
- export function unmount(el: HTMLElement) {
154
+ export function unmount(el: HTMLElement | ShadowRoot) {
172
155
  const root = (el as any)._reactRoot
173
- if (root) {
174
- root.unmount()
175
- }
156
+ if (root) root.unmount()
176
157
  el.innerHTML = ""
177
158
  }
178
159
 
179
-
180
- // expose mount/unmount to window
181
160
  ;(window as any).productApp = { mount, unmount }
182
161
  ```
183
162
 
@@ -187,17 +166,11 @@ export function unmount(el: HTMLElement) {
187
166
 
188
167
  ```ts
189
168
  const store = createSharedStore({ count: 0 })
190
-
191
- store.subscribe((state) => {
192
- console.log(state.count)
193
- })
194
-
169
+ store.subscribe((state) => console.log(state.count))
195
170
  store.setState({ count: 1 })
196
171
  ```
197
172
 
198
- - Push-based
199
- - No Redux
200
- - No shared framework state
173
+ - Push-based, no Redux, no shared framework state
201
174
 
202
175
  ---
203
176
 
@@ -207,12 +180,9 @@ store.setState({ count: 1 })
207
180
  import { createSharedRouter } from "mfe-runtime-z"
208
181
 
209
182
  const router = createSharedRouter(ctx)
210
-
211
183
  router.go("/cart")
184
+ router.onChange((path) => console.log("navigated to", path))
212
185
 
213
- router.onChange((path) => {
214
- console.log("navigated to", path)
215
- })
216
186
  ```
217
187
 
218
188
  - Intent-based navigation
@@ -251,7 +221,7 @@ import { mount, unmount } from "./app"
251
221
  ;(window as any).productApp = { mount, unmount }
252
222
  ```
253
223
 
254
- 👉 That’s it. `This file can be served from any CDN or server and loaded at runtime.`
224
+ 👉 Copy-paste ✅: minimal remote build, browser-loadable, no Module Federation.
255
225
 
256
226
  ---
257
227
 
@@ -315,6 +285,7 @@ if (import.meta.env.DEV) {
315
285
  - Shared state with subscriptions
316
286
  - Router synchronization
317
287
  - Hot reload support for remote apps
288
+ - Shadow DOM + error isolation (optional via isolate: true)
318
289
  - No build-time federation required
319
290
 
320
291
  ---
@@ -327,6 +298,22 @@ if (import.meta.env.DEV) {
327
298
 
328
299
  ---
329
300
 
301
+ ### Micro-Frontend Runtime Comparison
302
+
303
+ | Feature / Lib | **mfe-runtime-z** | Module Federation (Webpack 5) | single-spa | qiankun |
304
+ | ----------------------------------- | -------------------| ------------------------------- | ---------------- | ---------------- |
305
+ | **Framework-agnostic** | ✅ Yes | ❌ Mostly Webpack/JS | ✅ Yes | ✅ Yes |
306
+ | **Runtime loading** | ✅ Yes | ❌ Build-time federation only | ✅ Yes | ✅ Yes |
307
+ | **Shadow DOM support** | ✅ Optional | ❌ Not built-in | ❌ Not built-in | ✅ Partial |
308
+ | **Shared state / EventBus** | ✅ Yes | ❌ Requires external | ✅ Yes | ✅ Yes |
309
+ | **Dynamic mount/unmount** | ✅ Yes | ❌ Build-time | ✅ Yes | ✅ Yes |
310
+ | **Dev HMR support** | ✅ Yes | ❌ Limited | ⚠️ Needs plugin | ⚠️ Needs plugin |
311
+ | **Bundle size** | 🟢 Lightweight | ⚠️ Depends on Webpack setup | 🟡 Medium | 🟡 Medium |
312
+ | **Learning curve** | 🟢 Simple | ⚠️ Medium | ⚠️ Medium | ⚠️ Medium |
313
+ | **Build-time federation required?** | ❌ No | ✅ Yes | ❌ No | ❌ No |
314
+
315
+ ---
316
+
330
317
  ### License
331
318
 
332
319
  MIT
@@ -167,16 +167,19 @@ function enableRouterSync(eventBus, navigate) {
167
167
  var MFEHost = /** @class */ (function () {
168
168
  function MFEHost(options) {
169
169
  if (options === void 0) { options = {}; }
170
- var _a;
170
+ var _a, _b;
171
171
  this.eventBus = new EventBus();
172
+ this.isolate = false;
172
173
  this.stores = (_a = options.stores) !== null && _a !== void 0 ? _a : {};
173
174
  this.navigate = options.navigate;
174
175
  this.onRemoteError = options.onRemoteError;
176
+ this.fallback = options.fallback;
177
+ this.isolate = (_b = options.isolate) !== null && _b !== void 0 ? _b : false;
175
178
  if (this.navigate) {
176
179
  enableRouterSync(this.eventBus, this.navigate);
177
180
  }
178
181
  }
179
- /** load script only */
182
+ /** Load remote JS only */
180
183
  MFEHost.prototype.load = function (url, global) {
181
184
  var _a, _b;
182
185
  return __awaiter(this, void 0, void 0, function () {
@@ -205,58 +208,85 @@ var MFEHost = /** @class */ (function () {
205
208
  });
206
209
  });
207
210
  };
208
- /** mount with identity */
209
- MFEHost.prototype.mount = function (remote, el, name) {
210
- var _a;
211
+ /** Mount remote with optional Shadow DOM isolation */
212
+ MFEHost.prototype.mount = function (remote, el, name, options) {
213
+ var _a, _b;
214
+ var isolate = (_a = options === null || options === void 0 ? void 0 : options.isolate) !== null && _a !== void 0 ? _a : this.isolate;
215
+ var mountEl = el;
216
+ // Shadow DOM isolation
217
+ if (isolate) {
218
+ if (el._shadowRoot)
219
+ el._shadowRoot.innerHTML = "";
220
+ var mode = (_b = options === null || options === void 0 ? void 0 : options.shadowMode) !== null && _b !== void 0 ? _b : "open";
221
+ // @ts-ignore
222
+ var shadow = el.attachShadow({ mode: mode })(el)._shadowRoot = shadow;
223
+ // For React/Vue remotes, create container div
224
+ var container = document.createElement("div");
225
+ shadow.appendChild(container);
226
+ // @ts-ignore
227
+ mountEl = container(el)._mountEl = container;
228
+ }
211
229
  var ctx = {
212
230
  name: name,
213
- eventBus: this.eventBus,
214
231
  stores: this.stores,
215
- host: {
216
- navigate: this.navigate,
217
- },
232
+ eventBus: this.eventBus,
233
+ host: { navigate: this.navigate },
218
234
  };
219
235
  try {
220
- remote.mount(el, ctx);
221
- console.log("[MFE] mounted ".concat(name));
236
+ remote.mount(mountEl, ctx);
237
+ console.log("[MFE] mounted ".concat(name, " ").concat(isolate ? "(shadow)" : ""));
222
238
  }
223
239
  catch (err) {
224
- (_a = this.onRemoteError) === null || _a === void 0 ? void 0 : _a.call(this, err);
225
- throw err;
240
+ var error = err;
241
+ console.error("[MFE] remote ".concat(name, " mount failed:"), error);
242
+ if (this.onRemoteError)
243
+ this.onRemoteError(error);
244
+ if (this.fallback) {
245
+ this.fallback(mountEl, name, error, ctx);
246
+ }
247
+ else {
248
+ mountEl.innerHTML = "<div style=\"color:red\">Failed to load ".concat(name, "</div>");
249
+ }
226
250
  }
227
251
  };
252
+ /** Unmount remote */
228
253
  MFEHost.prototype.unmount = function (remote, el, name) {
229
- var _a, _b;
254
+ var _a, _b, _c, _d;
230
255
  try {
231
- (_a = remote.unmount) === null || _a === void 0 ? void 0 : _a.call(remote, el);
232
- name && console.log("[MFE] unmounted ".concat(name));
256
+ var mountEl = (_a = el._mountEl) !== null && _a !== void 0 ? _a : (this.isolate ? (_b = el.shadowRoot) !== null && _b !== void 0 ? _b : el : el);
257
+ (_c = remote.unmount) === null || _c === void 0 ? void 0 : _c.call(remote, mountEl);
258
+ delete el._mountEl;
259
+ if (name)
260
+ console.log("[MFE] unmounted ".concat(name));
233
261
  }
234
262
  catch (err) {
235
- (_b = this.onRemoteError) === null || _b === void 0 ? void 0 : _b.call(this, err);
263
+ (_d = this.onRemoteError) === null || _d === void 0 ? void 0 : _d.call(this, err);
236
264
  }
237
265
  };
266
+ /** Get central event bus */
238
267
  MFEHost.prototype.getEventBus = function () {
239
268
  return this.eventBus;
240
269
  };
241
- /** 🔥 reload = unmount + reload script + mount (KEEP name) */
270
+ /** Reload remote: unmount + reload script + mount again */
242
271
  MFEHost.prototype.reloadRemote = function (options) {
243
- var _a;
272
+ var _a, _b, _c;
244
273
  return __awaiter(this, void 0, void 0, function () {
245
- var name, url, global, el, oldRemote, newRemote;
246
- return __generator(this, function (_b) {
247
- switch (_b.label) {
274
+ var name, url, global, el, isolate, oldRemote, mountEl, newRemote;
275
+ return __generator(this, function (_d) {
276
+ switch (_d.label) {
248
277
  case 0:
249
- name = options.name, url = options.url, global = options.global, el = options.el;
250
- oldRemote = window[global];
278
+ name = options.name, url = options.url, global = options.global, el = options.el, isolate = options.isolate;
251
279
  try {
252
- (_a = oldRemote === null || oldRemote === void 0 ? void 0 : oldRemote.unmount) === null || _a === void 0 ? void 0 : _a.call(oldRemote, el);
280
+ oldRemote = window[global];
281
+ mountEl = (_a = el._mountEl) !== null && _a !== void 0 ? _a : ((isolate !== null && isolate !== void 0 ? isolate : this.isolate) ? (_b = el.shadowRoot) !== null && _b !== void 0 ? _b : el : el);
282
+ (_c = oldRemote === null || oldRemote === void 0 ? void 0 : oldRemote.unmount) === null || _c === void 0 ? void 0 : _c.call(oldRemote, mountEl);
253
283
  }
254
- catch (_c) { }
284
+ catch (_e) { }
255
285
  delete window[global];
256
286
  return [4 /*yield*/, this.load("".concat(url, "?t=").concat(Date.now()), global)];
257
287
  case 1:
258
- newRemote = _b.sent();
259
- this.mount(newRemote, el, name);
288
+ newRemote = _d.sent();
289
+ this.mount(newRemote, el, name, { isolate: isolate });
260
290
  return [2 /*return*/];
261
291
  }
262
292
  });
@@ -272,12 +302,17 @@ function createSharedStore(initial) {
272
302
  getState: function () {
273
303
  return state;
274
304
  },
305
+ /** Update state (shallow merge) */
275
306
  setState: function (partial) {
276
307
  state = __assign(__assign({}, state), partial);
277
- listeners.forEach(function (l) { return l(state); });
308
+ listeners.forEach(function (listener) { return listener(state); });
278
309
  },
279
- subscribe: function (fn) {
310
+ /** Subscribe to state changes */
311
+ subscribe: function (fn, callImmediately) {
312
+ if (callImmediately === void 0) { callImmediately = true; }
280
313
  listeners.add(fn);
314
+ if (callImmediately)
315
+ fn(state); // Immediately call listener with current state
281
316
  return function () { return listeners.delete(fn); };
282
317
  },
283
318
  };
package/build/index.js CHANGED
@@ -173,16 +173,19 @@
173
173
  var MFEHost = /** @class */ (function () {
174
174
  function MFEHost(options) {
175
175
  if (options === void 0) { options = {}; }
176
- var _a;
176
+ var _a, _b;
177
177
  this.eventBus = new EventBus();
178
+ this.isolate = false;
178
179
  this.stores = (_a = options.stores) !== null && _a !== void 0 ? _a : {};
179
180
  this.navigate = options.navigate;
180
181
  this.onRemoteError = options.onRemoteError;
182
+ this.fallback = options.fallback;
183
+ this.isolate = (_b = options.isolate) !== null && _b !== void 0 ? _b : false;
181
184
  if (this.navigate) {
182
185
  enableRouterSync(this.eventBus, this.navigate);
183
186
  }
184
187
  }
185
- /** load script only */
188
+ /** Load remote JS only */
186
189
  MFEHost.prototype.load = function (url, global) {
187
190
  var _a, _b;
188
191
  return __awaiter(this, void 0, void 0, function () {
@@ -211,58 +214,85 @@
211
214
  });
212
215
  });
213
216
  };
214
- /** mount with identity */
215
- MFEHost.prototype.mount = function (remote, el, name) {
216
- var _a;
217
+ /** Mount remote with optional Shadow DOM isolation */
218
+ MFEHost.prototype.mount = function (remote, el, name, options) {
219
+ var _a, _b;
220
+ var isolate = (_a = options === null || options === void 0 ? void 0 : options.isolate) !== null && _a !== void 0 ? _a : this.isolate;
221
+ var mountEl = el;
222
+ // Shadow DOM isolation
223
+ if (isolate) {
224
+ if (el._shadowRoot)
225
+ el._shadowRoot.innerHTML = "";
226
+ var mode = (_b = options === null || options === void 0 ? void 0 : options.shadowMode) !== null && _b !== void 0 ? _b : "open";
227
+ // @ts-ignore
228
+ var shadow = el.attachShadow({ mode: mode })(el)._shadowRoot = shadow;
229
+ // For React/Vue remotes, create container div
230
+ var container = document.createElement("div");
231
+ shadow.appendChild(container);
232
+ // @ts-ignore
233
+ mountEl = container(el)._mountEl = container;
234
+ }
217
235
  var ctx = {
218
236
  name: name,
219
- eventBus: this.eventBus,
220
237
  stores: this.stores,
221
- host: {
222
- navigate: this.navigate,
223
- },
238
+ eventBus: this.eventBus,
239
+ host: { navigate: this.navigate },
224
240
  };
225
241
  try {
226
- remote.mount(el, ctx);
227
- console.log("[MFE] mounted ".concat(name));
242
+ remote.mount(mountEl, ctx);
243
+ console.log("[MFE] mounted ".concat(name, " ").concat(isolate ? "(shadow)" : ""));
228
244
  }
229
245
  catch (err) {
230
- (_a = this.onRemoteError) === null || _a === void 0 ? void 0 : _a.call(this, err);
231
- throw err;
246
+ var error = err;
247
+ console.error("[MFE] remote ".concat(name, " mount failed:"), error);
248
+ if (this.onRemoteError)
249
+ this.onRemoteError(error);
250
+ if (this.fallback) {
251
+ this.fallback(mountEl, name, error, ctx);
252
+ }
253
+ else {
254
+ mountEl.innerHTML = "<div style=\"color:red\">Failed to load ".concat(name, "</div>");
255
+ }
232
256
  }
233
257
  };
258
+ /** Unmount remote */
234
259
  MFEHost.prototype.unmount = function (remote, el, name) {
235
- var _a, _b;
260
+ var _a, _b, _c, _d;
236
261
  try {
237
- (_a = remote.unmount) === null || _a === void 0 ? void 0 : _a.call(remote, el);
238
- name && console.log("[MFE] unmounted ".concat(name));
262
+ var mountEl = (_a = el._mountEl) !== null && _a !== void 0 ? _a : (this.isolate ? (_b = el.shadowRoot) !== null && _b !== void 0 ? _b : el : el);
263
+ (_c = remote.unmount) === null || _c === void 0 ? void 0 : _c.call(remote, mountEl);
264
+ delete el._mountEl;
265
+ if (name)
266
+ console.log("[MFE] unmounted ".concat(name));
239
267
  }
240
268
  catch (err) {
241
- (_b = this.onRemoteError) === null || _b === void 0 ? void 0 : _b.call(this, err);
269
+ (_d = this.onRemoteError) === null || _d === void 0 ? void 0 : _d.call(this, err);
242
270
  }
243
271
  };
272
+ /** Get central event bus */
244
273
  MFEHost.prototype.getEventBus = function () {
245
274
  return this.eventBus;
246
275
  };
247
- /** 🔥 reload = unmount + reload script + mount (KEEP name) */
276
+ /** Reload remote: unmount + reload script + mount again */
248
277
  MFEHost.prototype.reloadRemote = function (options) {
249
- var _a;
278
+ var _a, _b, _c;
250
279
  return __awaiter(this, void 0, void 0, function () {
251
- var name, url, global, el, oldRemote, newRemote;
252
- return __generator(this, function (_b) {
253
- switch (_b.label) {
280
+ var name, url, global, el, isolate, oldRemote, mountEl, newRemote;
281
+ return __generator(this, function (_d) {
282
+ switch (_d.label) {
254
283
  case 0:
255
- name = options.name, url = options.url, global = options.global, el = options.el;
256
- oldRemote = window[global];
284
+ name = options.name, url = options.url, global = options.global, el = options.el, isolate = options.isolate;
257
285
  try {
258
- (_a = oldRemote === null || oldRemote === void 0 ? void 0 : oldRemote.unmount) === null || _a === void 0 ? void 0 : _a.call(oldRemote, el);
286
+ oldRemote = window[global];
287
+ mountEl = (_a = el._mountEl) !== null && _a !== void 0 ? _a : ((isolate !== null && isolate !== void 0 ? isolate : this.isolate) ? (_b = el.shadowRoot) !== null && _b !== void 0 ? _b : el : el);
288
+ (_c = oldRemote === null || oldRemote === void 0 ? void 0 : oldRemote.unmount) === null || _c === void 0 ? void 0 : _c.call(oldRemote, mountEl);
259
289
  }
260
- catch (_c) { }
290
+ catch (_e) { }
261
291
  delete window[global];
262
292
  return [4 /*yield*/, this.load("".concat(url, "?t=").concat(Date.now()), global)];
263
293
  case 1:
264
- newRemote = _b.sent();
265
- this.mount(newRemote, el, name);
294
+ newRemote = _d.sent();
295
+ this.mount(newRemote, el, name, { isolate: isolate });
266
296
  return [2 /*return*/];
267
297
  }
268
298
  });
@@ -278,12 +308,17 @@
278
308
  getState: function () {
279
309
  return state;
280
310
  },
311
+ /** Update state (shallow merge) */
281
312
  setState: function (partial) {
282
313
  state = __assign(__assign({}, state), partial);
283
- listeners.forEach(function (l) { return l(state); });
314
+ listeners.forEach(function (listener) { return listener(state); });
284
315
  },
285
- subscribe: function (fn) {
316
+ /** Subscribe to state changes */
317
+ subscribe: function (fn, callImmediately) {
318
+ if (callImmediately === void 0) { callImmediately = true; }
286
319
  listeners.add(fn);
320
+ if (callImmediately)
321
+ fn(state); // Immediately call listener with current state
287
322
  return function () { return listeners.delete(fn); };
288
323
  },
289
324
  };
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).MFERuntimeZ={})}(this,function(t){"use strict";var e=function(){return e=Object.assign||function(t){for(var e,n=1,o=arguments.length;n<o;n++)for(var r in e=arguments[n])Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t},e.apply(this,arguments)};function n(t,e,n,o){return new(n||(n=Promise))(function(r,i){function u(t){try{c(o.next(t))}catch(t){i(t)}}function a(t){try{c(o.throw(t))}catch(t){i(t)}}function c(t){var e;t.done?r(t.value):(e=t.value,e instanceof n?e:new n(function(t){t(e)})).then(u,a)}c((o=o.apply(t,e||[])).next())})}function o(t,e){var n,o,r,i={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]},u=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return u.next=a(0),u.throw=a(1),u.return=a(2),"function"==typeof Symbol&&(u[Symbol.iterator]=function(){return this}),u;function a(a){return function(c){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;u&&(u=0,a[0]&&(i=0)),i;)try{if(n=1,o&&(r=2&a[0]?o.return:a[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,a[1])).done)return r;switch(o=0,r&&(a=[2&a[0],r.value]),a[0]){case 0:case 1:r=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,o=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(r=i.trys,(r=r.length>0&&r[r.length-1])||6!==a[0]&&2!==a[0])){i=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]<r[3])){i.label=a[1];break}if(6===a[0]&&i.label<r[1]){i.label=r[1],r=a;break}if(r&&i.label<r[2]){i.label=r[2],i.ops.push(a);break}r[2]&&i.ops.pop(),i.trys.pop();continue}a=e.call(t,i)}catch(t){a=[6,t],o=0}finally{n=r=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,c])}}}"function"==typeof SuppressedError&&SuppressedError;var r=new Set;function i(t,e){return void 0===e&&(e={}),n(this,void 0,void 0,function(){var n,i,a,c,s,l;return o(this,function(o){switch(o.label){case 0:if(n=e.timeout,i=void 0===n?15e3:n,a=e.retries,c=void 0===a?1:a,r.has(t))return[2];s=0,o.label=1;case 1:if(!(s<=c))return[3,6];o.label=2;case 2:return o.trys.push([2,4,,5]),[4,u(t,i)];case 3:return o.sent(),r.add(t),[2];case 4:if(l=o.sent(),s===c)throw l;return[3,5];case 5:return s++,[3,1];case 6:return[2]}})})}function u(t,e){return new Promise(function(n,o){var r=document.createElement("script");r.src=t,r.async=!0;var i=setTimeout(function(){u(),o(new Error("Timeout loading ".concat(t)))},e);function u(){clearTimeout(i),r.remove()}r.onload=function(){u(),n()},r.onerror=function(){u(),o(new Error("Failed to load ".concat(t)))},document.body.appendChild(r)})}var a=function(){function t(){this.map=new Map}return t.prototype.on=function(t,e){this.map.has(t)||this.map.set(t,new Set),this.map.get(t).add(e)},t.prototype.off=function(t,e){var n;null===(n=this.map.get(t))||void 0===n||n.delete(e)},t.prototype.emit=function(t,e){var n;null===(n=this.map.get(t))||void 0===n||n.forEach(function(t){return t(e)})},t}(),c="ROUTER:REQUEST_NAVIGATE",s="ROUTER:NAVIGATE";function l(t,e){t.on(c,function(n){var o=n.path;e(o),t.emit(s,{path:o})})}var f=function(){function t(t){var e;void 0===t&&(t={}),this.eventBus=new a,this.stores=null!==(e=t.stores)&&void 0!==e?e:{},this.navigate=t.navigate,this.onRemoteError=t.onRemoteError,this.navigate&&l(this.eventBus,this.navigate)}return t.prototype.load=function(t,e){var r,u;return n(this,void 0,void 0,function(){var n,a,c;return o(this,function(o){switch(o.label){case 0:return o.trys.push([0,2,,3]),[4,i(t,{retries:1})];case 1:return o.sent(),[3,3];case 2:throw n=o.sent(),null===(r=this.onRemoteError)||void 0===r||r.call(this,n),n;case 3:if(!(null==(a=window[e])?void 0:a.mount))throw c=new Error('Remote "'.concat(e,'" not found')),null===(u=this.onRemoteError)||void 0===u||u.call(this,c),c;return[2,a]}})})},t.prototype.mount=function(t,e,n){var o,r={name:n,eventBus:this.eventBus,stores:this.stores,host:{navigate:this.navigate}};try{t.mount(e,r),console.log("[MFE] mounted ".concat(n))}catch(t){throw null===(o=this.onRemoteError)||void 0===o||o.call(this,t),t}},t.prototype.unmount=function(t,e,n){var o,r;try{null===(o=t.unmount)||void 0===o||o.call(t,e),n&&console.log("[MFE] unmounted ".concat(n))}catch(t){null===(r=this.onRemoteError)||void 0===r||r.call(this,t)}},t.prototype.getEventBus=function(){return this.eventBus},t.prototype.reloadRemote=function(t){var e;return n(this,void 0,void 0,function(){var n,r,i,u,a,c;return o(this,function(o){switch(o.label){case 0:n=t.name,r=t.url,i=t.global,u=t.el,a=window[i];try{null===(e=null==a?void 0:a.unmount)||void 0===e||e.call(a,u)}catch(t){}return delete window[i],[4,this.load("".concat(r,"?t=").concat(Date.now()),i)];case 1:return c=o.sent(),this.mount(c,u,n),[2]}})})},t}();t.EventBus=a,t.MFEHost=f,t.createSharedRouter=function(t){var e=t.eventBus,n=t.name;return{go:function(t){e.emit(c,{from:n,path:t})},onChange:function(t){return e.on(s,function(e){t(e.path)})}}},t.createSharedStore=function(t){var n=t,o=new Set;return{getState:function(){return n},setState:function(t){n=e(e({},n),t),o.forEach(function(t){return t(n)})},subscribe:function(t){return o.add(t),function(){return o.delete(t)}}}},t.enableRouterSync=l,t.loadScript=i,Object.defineProperty(t,"__esModule",{value:!0})});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).MFERuntimeZ={})}(this,function(t){"use strict";var e=function(){return e=Object.assign||function(t){for(var e,n=1,o=arguments.length;n<o;n++)for(var r in e=arguments[n])Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t},e.apply(this,arguments)};function n(t,e,n,o){return new(n||(n=Promise))(function(r,i){function a(t){try{l(o.next(t))}catch(t){i(t)}}function u(t){try{l(o.throw(t))}catch(t){i(t)}}function l(t){var e;t.done?r(t.value):(e=t.value,e instanceof n?e:new n(function(t){t(e)})).then(a,u)}l((o=o.apply(t,e||[])).next())})}function o(t,e){var n,o,r,i={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]},a=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return a.next=u(0),a.throw=u(1),a.return=u(2),"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(u){return function(l){return function(u){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,u[0]&&(i=0)),i;)try{if(n=1,o&&(r=2&u[0]?o.return:u[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,u[1])).done)return r;switch(o=0,r&&(u=[2&u[0],r.value]),u[0]){case 0:case 1:r=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,o=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(r=i.trys,(r=r.length>0&&r[r.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]<r[3])){i.label=u[1];break}if(6===u[0]&&i.label<r[1]){i.label=r[1],r=u;break}if(r&&i.label<r[2]){i.label=r[2],i.ops.push(u);break}r[2]&&i.ops.pop(),i.trys.pop();continue}u=e.call(t,i)}catch(t){u=[6,t],o=0}finally{n=r=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}([u,l])}}}"function"==typeof SuppressedError&&SuppressedError;var r=new Set;function i(t,e){return void 0===e&&(e={}),n(this,void 0,void 0,function(){var n,i,u,l,c,s;return o(this,function(o){switch(o.label){case 0:if(n=e.timeout,i=void 0===n?15e3:n,u=e.retries,l=void 0===u?1:u,r.has(t))return[2];c=0,o.label=1;case 1:if(!(c<=l))return[3,6];o.label=2;case 2:return o.trys.push([2,4,,5]),[4,a(t,i)];case 3:return o.sent(),r.add(t),[2];case 4:if(s=o.sent(),c===l)throw s;return[3,5];case 5:return c++,[3,1];case 6:return[2]}})})}function a(t,e){return new Promise(function(n,o){var r=document.createElement("script");r.src=t,r.async=!0;var i=setTimeout(function(){a(),o(new Error("Timeout loading ".concat(t)))},e);function a(){clearTimeout(i),r.remove()}r.onload=function(){a(),n()},r.onerror=function(){a(),o(new Error("Failed to load ".concat(t)))},document.body.appendChild(r)})}var u=function(){function t(){this.map=new Map}return t.prototype.on=function(t,e){this.map.has(t)||this.map.set(t,new Set),this.map.get(t).add(e)},t.prototype.off=function(t,e){var n;null===(n=this.map.get(t))||void 0===n||n.delete(e)},t.prototype.emit=function(t,e){var n;null===(n=this.map.get(t))||void 0===n||n.forEach(function(t){return t(e)})},t}(),l="ROUTER:REQUEST_NAVIGATE",c="ROUTER:NAVIGATE";function s(t,e){t.on(l,function(n){var o=n.path;e(o),t.emit(c,{path:o})})}var f=function(){function t(t){var e,n;void 0===t&&(t={}),this.eventBus=new u,this.isolate=!1,this.stores=null!==(e=t.stores)&&void 0!==e?e:{},this.navigate=t.navigate,this.onRemoteError=t.onRemoteError,this.fallback=t.fallback,this.isolate=null!==(n=t.isolate)&&void 0!==n&&n,this.navigate&&s(this.eventBus,this.navigate)}return t.prototype.load=function(t,e){var r,a;return n(this,void 0,void 0,function(){var n,u,l;return o(this,function(o){switch(o.label){case 0:return o.trys.push([0,2,,3]),[4,i(t,{retries:1})];case 1:return o.sent(),[3,3];case 2:throw n=o.sent(),null===(r=this.onRemoteError)||void 0===r||r.call(this,n),n;case 3:if(!(null==(u=window[e])?void 0:u.mount))throw l=new Error('Remote "'.concat(e,'" not found')),null===(a=this.onRemoteError)||void 0===a||a.call(this,l),l;return[2,u]}})})},t.prototype.mount=function(t,e,n,o){var r,i,a=null!==(r=null==o?void 0:o.isolate)&&void 0!==r?r:this.isolate,u=e;if(a){e._shadowRoot&&(e._shadowRoot.innerHTML="");var l=null!==(i=null==o?void 0:o.shadowMode)&&void 0!==i?i:"open",c=e.attachShadow({mode:l})(e)._shadowRoot=c,s=document.createElement("div");c.appendChild(s),u=s(e)._mountEl=s}var f={name:n,stores:this.stores,eventBus:this.eventBus,host:{navigate:this.navigate}};try{t.mount(u,f),console.log("[MFE] mounted ".concat(n," ").concat(a?"(shadow)":""))}catch(t){var d=t;console.error("[MFE] remote ".concat(n," mount failed:"),d),this.onRemoteError&&this.onRemoteError(d),this.fallback?this.fallback(u,n,d,f):u.innerHTML='<div style="color:red">Failed to load '.concat(n,"</div>")}},t.prototype.unmount=function(t,e,n){var o,r,i,a;try{var u=null!==(o=e._mountEl)&&void 0!==o?o:this.isolate&&null!==(r=e.shadowRoot)&&void 0!==r?r:e;null===(i=t.unmount)||void 0===i||i.call(t,u),delete e._mountEl,n&&console.log("[MFE] unmounted ".concat(n))}catch(t){null===(a=this.onRemoteError)||void 0===a||a.call(this,t)}},t.prototype.getEventBus=function(){return this.eventBus},t.prototype.reloadRemote=function(t){var e,r,i;return n(this,void 0,void 0,function(){var n,a,u,l,c,s,f,d;return o(this,function(o){switch(o.label){case 0:n=t.name,a=t.url,u=t.global,l=t.el,c=t.isolate;try{s=window[u],f=null!==(e=l._mountEl)&&void 0!==e?e:(null!=c?c:this.isolate)&&null!==(r=l.shadowRoot)&&void 0!==r?r:l,null===(i=null==s?void 0:s.unmount)||void 0===i||i.call(s,f)}catch(t){}return delete window[u],[4,this.load("".concat(a,"?t=").concat(Date.now()),u)];case 1:return d=o.sent(),this.mount(d,l,n,{isolate:c}),[2]}})})},t}();t.EventBus=u,t.MFEHost=f,t.createSharedRouter=function(t){var e=t.eventBus,n=t.name;return{go:function(t){e.emit(l,{from:n,path:t})},onChange:function(t){return e.on(c,function(e){t(e.path)})}}},t.createSharedStore=function(t){var n=t,o=new Set;return{getState:function(){return n},setState:function(t){n=e(e({},n),t),o.forEach(function(t){return t(n)})},subscribe:function(t,e){return void 0===e&&(e=!0),o.add(t),e&&t(n),function(){return o.delete(t)}}}},t.enableRouterSync=s,t.loadScript=i,Object.defineProperty(t,"__esModule",{value:!0})});
@@ -1,28 +1,40 @@
1
1
  import { EventBus } from "../event-bus/EventBus";
2
2
  import type { RemoteApp } from "../types/RemoteApp";
3
- type HostOptions = {
4
- stores?: Record<string, any>;
3
+ import type { MFEContext } from "../types/MFEContext";
4
+ type HostOptions<S extends Record<string, any> = any> = {
5
+ stores?: S;
5
6
  navigate?: (path: string) => void;
6
7
  onRemoteError?: (error: Error) => void;
8
+ isolate?: boolean;
9
+ fallback?: (el: HTMLElement, name: string, error: Error, ctx: MFEContext<S>) => void;
7
10
  };
8
- export declare class MFEHost {
11
+ interface MountOptions {
12
+ isolate?: boolean;
13
+ shadowMode?: ShadowRootMode;
14
+ }
15
+ export declare class MFEHost<S extends Record<string, any> = any> {
9
16
  private eventBus;
10
17
  private stores;
11
18
  private navigate?;
12
19
  private onRemoteError?;
13
- constructor(options?: HostOptions);
14
- /** load script only */
20
+ private fallback?;
21
+ private isolate;
22
+ constructor(options?: HostOptions<S>);
23
+ /** Load remote JS only */
15
24
  load(url: string, global: string): Promise<RemoteApp>;
16
- /** mount with identity */
17
- mount(remote: RemoteApp, el: HTMLElement, name: string): void;
25
+ /** Mount remote with optional Shadow DOM isolation */
26
+ mount(remote: RemoteApp, el: HTMLElement, name: string, options?: MountOptions): void;
27
+ /** Unmount remote */
18
28
  unmount(remote: RemoteApp, el: HTMLElement, name?: string): void;
29
+ /** Get central event bus */
19
30
  getEventBus(): EventBus<any>;
20
- /** 🔥 reload = unmount + reload script + mount (KEEP name) */
31
+ /** Reload remote: unmount + reload script + mount again */
21
32
  reloadRemote(options: {
22
33
  name: string;
23
34
  url: string;
24
35
  global: string;
25
36
  el: HTMLElement;
37
+ isolate?: boolean;
26
38
  }): Promise<void>;
27
39
  }
28
40
  export {};
@@ -1,6 +1,8 @@
1
1
  export type Unsubscribe = () => void;
2
2
  export declare function createSharedStore<T extends object>(initial: T): {
3
3
  getState(): T;
4
+ /** Update state (shallow merge) */
4
5
  setState(partial: Partial<T>): void;
5
- subscribe(fn: (s: T) => void): Unsubscribe;
6
+ /** Subscribe to state changes */
7
+ subscribe(fn: (s: T) => void, callImmediately?: boolean): Unsubscribe;
6
8
  };
@@ -1,10 +1,9 @@
1
1
  import type { EventBus } from "../event-bus/EventBus";
2
- export type MFEContext = {
3
- /** identity của microFE hiện tại */
2
+ export interface MFEContext<S = any> {
4
3
  name: string;
4
+ stores: S;
5
5
  eventBus: EventBus<any>;
6
- stores: Record<string, any>;
7
- host?: {
6
+ host: {
8
7
  navigate?: (path: string) => void;
9
8
  };
10
- };
9
+ }
@@ -1,6 +1,5 @@
1
- import { MFEContext } from "./MFEContext";
2
- export type RemoteApp = {
1
+ export interface RemoteApp<C = any> {
3
2
  name?: string;
4
- mount: (el: HTMLElement, ctx: MFEContext) => void;
5
- unmount?: (el: HTMLElement) => void;
6
- };
3
+ mount: (el: HTMLElement | ShadowRoot, ctx: C) => void;
4
+ unmount?: (el: HTMLElement | ShadowRoot) => void;
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mfe-runtime-z",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Framework-agnostic micro-frontend runtime (host, event bus, shared state, router sync)",
5
5
  "main": "build/index.js",
6
6
  "module": "build/index.esm.js",