wu-framework 1.1.12 → 1.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wu-framework",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "Universal Microfrontends Framework - 8 frameworks, zero config, Shadow DOM isolation",
5
5
  "main": "dist/wu-framework.cjs.js",
6
6
  "module": "src/index.js",
@@ -174,7 +174,7 @@ async function register(appName, Component, options = {}) {
174
174
  return;
175
175
  }
176
176
 
177
- // Si ya está montado en el MISMO container, ignorar (StrictMode double-mount)
177
+ // Si ya está montado en el MISMO container, ignorar
178
178
  const existing = adapterState.roots.get(appName);
179
179
  if (existing) {
180
180
  if (existing.container === container) {
@@ -185,15 +185,9 @@ async function register(appName, Component, options = {}) {
185
185
  }
186
186
 
187
187
  try {
188
- // Limpiar el DOM del container antes de crear un nuevo root.
189
- // En React 18+, root.unmount() es async — si se llama createRoot()
190
- // en el mismo container antes de que termine, falla silenciosamente.
191
- // Limpiar innerHTML fuerza un DOM limpio para el nuevo root.
192
188
  container.innerHTML = '';
193
-
194
189
  const root = createRoot(container);
195
190
 
196
- // Crear elemento con o sin StrictMode
197
191
  let element = React.createElement(Component, props);
198
192
  if (strictMode && React.StrictMode) {
199
193
  element = React.createElement(React.StrictMode, null, element);
@@ -211,28 +205,21 @@ async function register(appName, Component, options = {}) {
211
205
  }
212
206
  };
213
207
 
214
- // Función de unmount interna
208
+ // Unmount inmediato — la protección contra StrictMode (deferred unmount)
209
+ // se maneja en wu-core.js, no aquí en el adapter.
215
210
  const unmountApp = (container) => {
216
211
  const instance = adapterState.roots.get(appName);
217
-
218
212
  if (instance) {
219
213
  try {
220
- if (onUnmount) {
221
- onUnmount(instance.container);
222
- }
223
-
214
+ if (onUnmount) onUnmount(instance.container);
224
215
  instance.root.unmount();
225
216
  adapterState.roots.delete(appName);
226
217
  } catch (error) {
227
218
  console.error(`[WuReact] Unmount error for ${appName}:`, error);
228
219
  }
229
220
  }
230
-
231
- // Limpiar container
232
221
  const target = container || instance?.container;
233
- if (target) {
234
- target.innerHTML = '';
235
- }
222
+ if (target) target.innerHTML = '';
236
223
  };
237
224
 
238
225
  // Intentar registrar con Wu Framework
@@ -30,6 +30,8 @@ export class WuCore {
30
30
  this.manifests = new Map(); // Manifiestos cargados
31
31
  this.mounted = new Map(); // Apps montadas
32
32
  this.hidden = new Map(); // Keep-alive hidden apps
33
+ this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
34
+ this._mountingPromises = new Map(); // In-flight mount dedup
33
35
 
34
36
  // Componentes core
35
37
  this.loader = new WuLoader();
@@ -178,6 +180,31 @@ export class WuCore {
178
180
  * @param {string} containerSelector - Selector del contenedor
179
181
  */
180
182
  async mount(appName, containerSelector) {
183
+ // ── StrictMode guard: cancel pending deferred unmount ──
184
+ // React StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
185
+ // The cleanup fires between two mounts. By deferring the actual unmount,
186
+ // the second mount cancels it and the app stays alive — zero flicker.
187
+ if (this._pendingUnmounts.has(appName)) {
188
+ clearTimeout(this._pendingUnmounts.get(appName));
189
+ this._pendingUnmounts.delete(appName);
190
+ logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
191
+ }
192
+
193
+ // Already mounted in same container → no-op
194
+ if (this.mounted.has(appName)) {
195
+ const existing = this.mounted.get(appName);
196
+ if (existing.containerSelector === containerSelector) {
197
+ logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
198
+ return;
199
+ }
200
+ }
201
+
202
+ // Deduplicate concurrent mounts (StrictMode fires effect twice)
203
+ if (this._mountingPromises.has(appName)) {
204
+ logger.wuDebug(`${appName} mount already in progress, deduplicating`);
205
+ return await this._mountingPromises.get(appName);
206
+ }
207
+
181
208
  // Check if app is in keep-alive (hidden) state
182
209
  const hiddenEntry = this.hidden.get(appName);
183
210
  if (hiddenEntry) {
@@ -189,7 +216,15 @@ export class WuCore {
189
216
  await this._destroyHidden(appName);
190
217
  }
191
218
 
192
- return await this.mountWithRecovery(appName, containerSelector, 0);
219
+ // Track mount promise for deduplication
220
+ const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
221
+ this._mountingPromises.set(appName, mountPromise);
222
+
223
+ try {
224
+ return await mountPromise;
225
+ } finally {
226
+ this._mountingPromises.delete(appName);
227
+ }
193
228
  }
194
229
 
195
230
  /**
@@ -840,28 +875,60 @@ export class WuCore {
840
875
  * @param {boolean} [options.force] - Force destroy even if keepAlive
841
876
  */
842
877
  async unmount(appName, options = {}) {
843
- try {
844
- logger.wuDebug(`Unmounting ${appName}`);
878
+ logger.wuDebug(`Unmounting ${appName}`);
845
879
 
846
- const mounted = this.mounted.get(appName);
847
- if (!mounted) {
848
- // Check if it's hidden (keep-alive) — force destroy if requested
849
- if (options.force && this.hidden.has(appName)) {
850
- return await this._destroyHidden(appName);
851
- }
852
- logger.wuWarn(`App ${appName} not mounted`);
853
- return;
880
+ const mounted = this.mounted.get(appName);
881
+ if (!mounted) {
882
+ // Check if it's hidden (keep-alive) — force destroy if requested
883
+ if (options.force && this.hidden.has(appName)) {
884
+ return await this._destroyHidden(appName);
854
885
  }
886
+ logger.wuWarn(`App ${appName} not mounted`);
887
+ return;
888
+ }
855
889
 
856
- // Resolve keepAlive: per-call > per-app config > default false
857
- const keepAlive = options.force
858
- ? false
859
- : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
890
+ // Resolve keepAlive: per-call > per-app config > default false
891
+ const keepAlive = options.force
892
+ ? false
893
+ : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
860
894
 
861
- if (keepAlive) {
862
- return await this.hide(appName);
895
+ if (keepAlive) {
896
+ return await this.hide(appName);
897
+ }
898
+
899
+ // Force → immediate unmount (no deferral)
900
+ if (options.force) {
901
+ return await this._executeUnmount(appName, mounted);
902
+ }
903
+
904
+ // ── Deferred unmount: 60ms window for React StrictMode ──
905
+ // StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
906
+ // The cleanup fires between two mounts. By deferring the actual unmount,
907
+ // the second mount() call cancels the timer and the app stays alive.
908
+ if (this._pendingUnmounts.has(appName)) {
909
+ clearTimeout(this._pendingUnmounts.get(appName));
910
+ }
911
+
912
+ this._pendingUnmounts.set(appName, setTimeout(async () => {
913
+ this._pendingUnmounts.delete(appName);
914
+ // Re-verify: only unmount if the same mount entry is still current
915
+ if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
916
+ try {
917
+ await this._executeUnmount(appName, mounted);
918
+ } catch (error) {
919
+ logger.wuError(`Deferred unmount failed for ${appName}:`, error);
920
+ }
863
921
  }
922
+ }, 60));
923
+ }
864
924
 
925
+ /**
926
+ * Execute the actual unmount immediately (no deferral).
927
+ * Called by the deferred timer, force unmount, or destroy.
928
+ * @private
929
+ */
930
+ async _executeUnmount(appName, mounted) {
931
+ try {
865
932
  // Execute beforeUnmount hooks
866
933
  const beforeUnmountResult = await this.hooks.execute('beforeUnmount', { appName, mounted });
867
934
  if (beforeUnmountResult.cancelled) {
@@ -1277,6 +1344,13 @@ export class WuCore {
1277
1344
  // Call plugin onDestroy hooks
1278
1345
  await this.pluginSystem.callHook('onDestroy', {});
1279
1346
 
1347
+ // Cancel all pending deferred unmounts
1348
+ for (const timer of this._pendingUnmounts.values()) {
1349
+ clearTimeout(timer);
1350
+ }
1351
+ this._pendingUnmounts.clear();
1352
+ this._mountingPromises.clear();
1353
+
1280
1354
  // Force-destroy all hidden (keep-alive) apps first
1281
1355
  for (const appName of [...this.hidden.keys()]) {
1282
1356
  await this._destroyHidden(appName);