@gears-frontx/react 0.2.0-alpha.0 → 0.2.0-alpha.3

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/FrontXProvider.tsx
2
- import { useMemo as useMemo2, useEffect as useEffect2, useState as useState2 } from "react";
2
+ import { useMemo as useMemo2, useEffect as useEffect3, useState as useState2 } from "react";
3
3
  import { Provider as ReduxProvider } from "react-redux";
4
4
  import {
5
5
  createFrontXApp
@@ -41,7 +41,7 @@ var MfeProvider = ({ value, children }) => {
41
41
  import React, {
42
42
  createContext as createContext3,
43
43
  useContext as useContext3,
44
- useEffect,
44
+ useEffect as useEffect2,
45
45
  useLayoutEffect,
46
46
  useMemo,
47
47
  useState
@@ -164,13 +164,23 @@ function createQueryCache(queryClient) {
164
164
  };
165
165
  }
166
166
 
167
+ // src/hooks/useLatest.ts
168
+ import { useEffect, useRef } from "react";
169
+ function useLatest(value) {
170
+ const ref = useRef(value);
171
+ useEffect(() => {
172
+ ref.current = value;
173
+ });
174
+ return ref;
175
+ }
176
+
167
177
  // src/queryClient.tsx
168
178
  import { jsx as jsx2 } from "react/jsx-runtime";
169
179
  var APP_QUERY_CLIENT_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client");
170
180
  var APP_QUERY_CLIENT_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client-resolver");
171
181
  var APP_QUERY_CLIENT_ACTIVATOR_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client-activator");
172
182
  var FrontXQueryClientContext = createContext3(void 0);
173
- var useCommitEffect = typeof globalThis.window === "undefined" ? useEffect : useLayoutEffect;
183
+ var useCommitEffect = typeof globalThis.window === "undefined" ? useEffect2 : useLayoutEffect;
174
184
  function resolveFrontXQueryClient(app) {
175
185
  const clientApp = app;
176
186
  return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();
@@ -409,18 +419,19 @@ function useFrontXSuspenseInfiniteQuery(options) {
409
419
  }
410
420
  function useFrontXMutation(options, callbackCtx) {
411
421
  useRequiredFrontXQueryClient();
412
- const latestRef = React.useRef({ options, callbackCtx });
413
- latestRef.current = { options, callbackCtx };
422
+ const latestRef = useLatest({ options, callbackCtx });
414
423
  const fetchAbortControllersRef = React.useRef(/* @__PURE__ */ new Set());
424
+ const abortFetchesOnUnmount = React.useEffectEvent(() => {
425
+ if (options.abortOnUnmount) {
426
+ for (const controller of fetchAbortControllersRef.current) {
427
+ controller.abort();
428
+ }
429
+ fetchAbortControllersRef.current.clear();
430
+ }
431
+ });
415
432
  React.useEffect(() => {
416
- const fetchAbortControllers = fetchAbortControllersRef.current;
417
433
  return () => {
418
- if (latestRef.current.options.abortOnUnmount) {
419
- for (const controller of fetchAbortControllers) {
420
- controller.abort();
421
- }
422
- fetchAbortControllers.clear();
423
- }
434
+ abortFetchesOnUnmount();
424
435
  };
425
436
  }, []);
426
437
  const mutationFn = React.useCallback((variables, context) => {
@@ -444,22 +455,22 @@ function useFrontXMutation(options, callbackCtx) {
444
455
  return latestRef.current.options.endpoint.fetch(variables, { signal: controller.signal }).finally(() => {
445
456
  fetchAbortControllersRef.current.delete(controller);
446
457
  });
447
- }, []);
458
+ }, [latestRef]);
448
459
  const onMutate = React.useCallback((variables, _context) => {
449
460
  const current = latestRef.current;
450
461
  if (!current.options.onMutate) {
451
462
  return void 0;
452
463
  }
453
464
  return current.options.onMutate(variables, current.callbackCtx);
454
- }, []);
465
+ }, [latestRef]);
455
466
  const onSuccess = React.useCallback((data, variables, context) => {
456
467
  const current = latestRef.current;
457
468
  return current.options.onSuccess?.(data, variables, context, current.callbackCtx);
458
- }, []);
469
+ }, [latestRef]);
459
470
  const onError = React.useCallback((error, variables, context) => {
460
471
  const current = latestRef.current;
461
472
  return current.options.onError?.(error, variables, context, current.callbackCtx);
462
- }, []);
473
+ }, [latestRef]);
463
474
  const onSettled = React.useCallback((data, error, variables, context) => {
464
475
  const current = latestRef.current;
465
476
  return current.options.onSettled?.(
@@ -469,7 +480,7 @@ function useFrontXMutation(options, callbackCtx) {
469
480
  context,
470
481
  current.callbackCtx
471
482
  );
472
- }, []);
483
+ }, [latestRef]);
473
484
  const mutation = useMutation({
474
485
  mutationFn,
475
486
  onMutate: options.onMutate ? onMutate : void 0,
@@ -520,7 +531,7 @@ var FrontXProvider = ({
520
531
  }, [providedApp, stableConfig]);
521
532
  const queryClient = useBootstrappedFrontXQueryClient(app);
522
533
  const deferQuerySubtree = hasFrontXQueryClientActivator(app) && queryClient === void 0;
523
- useEffect2(() => {
534
+ useEffect3(() => {
524
535
  if (queryClient || hasFrontXQueryClientActivator(app) || process.env.NODE_ENV === "production" || process.env.NODE_ENV === "test" || process.env.VITEST === "true") {
525
536
  return;
526
537
  }
@@ -528,7 +539,7 @@ var FrontXProvider = ({
528
539
  "[FrontXProvider] No query cache available. Add queryCache() or queryCacheShared() to your plugin composition. useApiQuery/useApiMutation will fail without it."
529
540
  );
530
541
  }, [app, queryClient]);
531
- useEffect2(() => {
542
+ useEffect3(() => {
532
543
  return () => {
533
544
  if (!providedApp) {
534
545
  app.destroy();
@@ -543,7 +554,7 @@ var FrontXProvider = ({
543
554
  };
544
555
 
545
556
  // src/hooks/useCanAccess.ts
546
- import { useEffect as useEffect3, useState as useState3 } from "react";
557
+ import { useEffect as useEffect4, useState as useState3 } from "react";
547
558
  function serializeRecordValue(value) {
548
559
  if (value === null) return "n:";
549
560
  if (typeof value === "string") return `s:${value}`;
@@ -560,24 +571,23 @@ function accessQueryKey(query) {
560
571
  function getAuthRuntime(app) {
561
572
  return app.auth ?? null;
562
573
  }
574
+ var DENIED_NO_AUTH = { allow: false, isResolving: false };
563
575
  function useCanAccess(query) {
564
576
  const app = useFrontX();
577
+ const auth2 = getAuthRuntime(app);
565
578
  const stableKey = accessQueryKey(query);
566
579
  const [stableQuery, setStableQuery] = useState3(() => query);
567
- const [storedKey, setStoredKey] = useState3(stableKey);
580
+ const [runState, setRunState] = useState3({ auth: auth2, stableKey });
568
581
  const [result, setResult] = useState3({ allow: false, isResolving: true });
569
- if (storedKey !== stableKey) {
570
- setStoredKey(stableKey);
582
+ if (runState.auth !== auth2 || runState.stableKey !== stableKey) {
583
+ setRunState({ auth: auth2, stableKey });
571
584
  setStableQuery(query);
572
585
  setResult({ allow: false, isResolving: true });
573
586
  }
574
- useEffect3(() => {
575
- const auth2 = getAuthRuntime(app);
587
+ useEffect4(() => {
576
588
  if (!auth2) {
577
- setResult({ allow: false, isResolving: false });
578
589
  return;
579
590
  }
580
- setResult({ allow: false, isResolving: true });
581
591
  let alive = true;
582
592
  const controller = new AbortController();
583
593
  void auth2.canAccess(stableQuery, { signal: controller.signal }).then((decision) => {
@@ -593,8 +603,8 @@ function useCanAccess(query) {
593
603
  alive = false;
594
604
  controller.abort();
595
605
  };
596
- }, [app, stableQuery]);
597
- return result;
606
+ }, [auth2, stableQuery]);
607
+ return auth2 ? result : DENIED_NO_AUTH;
598
608
  }
599
609
 
600
610
  // src/components/CanAccess.tsx
@@ -667,7 +677,7 @@ function useTranslation() {
667
677
  }
668
678
 
669
679
  // src/hooks/useScreenTranslations.ts
670
- import { useState as useState4, useEffect as useEffect4, useMemo as useMemo4, useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore2 } from "react";
680
+ import { useState as useState4, useEffect as useEffect5, useMemo as useMemo4, useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore2 } from "react";
671
681
  function isTranslationLoader(input) {
672
682
  return typeof input === "function";
673
683
  }
@@ -675,7 +685,6 @@ function useScreenTranslations(screensetId, screenId, translations) {
675
685
  const app = useFrontX();
676
686
  const { i18nRegistry: i18nRegistry2 } = app;
677
687
  const [loadedLanguage, setLoadedLanguage] = useState4(null);
678
- const [isLoading, setIsLoading] = useState4(false);
679
688
  const [error, setError] = useState4(null);
680
689
  const version = useSyncExternalStore2(
681
690
  useCallback2(
@@ -702,12 +711,11 @@ function useScreenTranslations(screensetId, screenId, translations) {
702
711
  return module.default;
703
712
  };
704
713
  }, [translations]);
705
- useEffect4(() => {
714
+ useEffect5(() => {
706
715
  if (!currentLanguage || currentLanguage === loadedLanguage) {
707
716
  return;
708
717
  }
709
718
  let cancelled = false;
710
- setIsLoading(true);
711
719
  const loadTranslations = async () => {
712
720
  try {
713
721
  const namespace = `screen.${screensetId}.${screenId}`;
@@ -716,13 +724,11 @@ function useScreenTranslations(screensetId, screenId, translations) {
716
724
  i18nRegistry2.register(namespace, currentLanguage, loadedTranslations);
717
725
  if (!cancelled) {
718
726
  setLoadedLanguage(currentLanguage);
719
- setIsLoading(false);
720
727
  setError(null);
721
728
  }
722
729
  } catch (err) {
723
730
  if (!cancelled) {
724
731
  setError(err instanceof Error ? err : new Error(String(err)));
725
- setIsLoading(false);
726
732
  }
727
733
  }
728
734
  };
@@ -731,7 +737,7 @@ function useScreenTranslations(screensetId, screenId, translations) {
731
737
  cancelled = true;
732
738
  };
733
739
  }, [screensetId, screenId, loader, i18nRegistry2, currentLanguage, loadedLanguage]);
734
- const isLoaded = currentLanguage !== null && currentLanguage === loadedLanguage && !isLoading;
740
+ const isLoaded = currentLanguage !== null && currentLanguage === loadedLanguage;
735
741
  return { isLoaded, error };
736
742
  }
737
743
 
@@ -842,25 +848,38 @@ function useApiMutation(options) {
842
848
  }
843
849
 
844
850
  // src/hooks/useApiStream.ts
845
- import { useEffect as useEffect5, useMemo as useMemo8, useRef, useState as useState5, useCallback as useCallback4 } from "react";
851
+ import { useEffect as useEffect6, useMemo as useMemo8, useRef as useRef2, useState as useState5, useCallback as useCallback4 } from "react";
846
852
  function useApiStream(descriptor, options) {
847
853
  const mode = options?.mode ?? "latest";
848
854
  const enabled = options?.enabled ?? true;
849
855
  const [data, setData] = useState5(void 0);
850
856
  const [events, setEvents] = useState5([]);
851
- const [status, setStatus] = useState5("idle");
857
+ const [status, setStatus] = useState5(() => enabled ? "connecting" : "idle");
852
858
  const [error, setError] = useState5(null);
853
- const connectPromiseRef = useRef(null);
854
- const connectionIdRef = useRef(null);
855
- const disconnectRequestedRef = useRef(false);
856
- const descriptorRef = useRef(descriptor);
857
- descriptorRef.current = descriptor;
859
+ const connectPromiseRef = useRef2(null);
860
+ const connectionIdRef = useRef2(null);
861
+ const disconnectRequestedRef = useRef2(false);
862
+ const descriptorRef = useLatest(descriptor);
858
863
  const descriptorKey = useMemo8(() => JSON.stringify(descriptor.key), [descriptor.key]);
859
- useEffect5(() => {
864
+ const resetKey = `${descriptorKey}:${mode}`;
865
+ const [prevResetKey, setPrevResetKey] = useState5(resetKey);
866
+ if (resetKey !== prevResetKey) {
867
+ setPrevResetKey(resetKey);
860
868
  setData(void 0);
861
869
  setEvents([]);
862
870
  setError(null);
863
- }, [descriptorKey, mode]);
871
+ }
872
+ const connectKey = `${resetKey}:${enabled}`;
873
+ const [prevConnectKey, setPrevConnectKey] = useState5(connectKey);
874
+ if (connectKey !== prevConnectKey) {
875
+ setPrevConnectKey(connectKey);
876
+ if (enabled) {
877
+ setData(void 0);
878
+ setEvents([]);
879
+ setStatus("connecting");
880
+ setError(null);
881
+ }
882
+ }
864
883
  const disconnect = useCallback4(() => {
865
884
  disconnectRequestedRef.current = true;
866
885
  if (connectionIdRef.current) {
@@ -869,19 +888,14 @@ function useApiStream(descriptor, options) {
869
888
  disconnectRequestedRef.current = false;
870
889
  }
871
890
  setStatus("disconnected");
872
- }, []);
873
- useEffect5(() => {
891
+ }, [descriptorRef]);
892
+ useEffect6(() => {
874
893
  if (!enabled) {
875
- setStatus("idle");
876
894
  return;
877
895
  }
878
896
  let cancelled = false;
879
897
  disconnectRequestedRef.current = false;
880
898
  const d = descriptorRef.current;
881
- setData(void 0);
882
- setEvents([]);
883
- setStatus("connecting");
884
- setError(null);
885
899
  const connectPromise = d.connect(
886
900
  (event) => {
887
901
  if (cancelled) return;
@@ -927,8 +941,8 @@ function useApiStream(descriptor, options) {
927
941
  connectPromiseRef.current = null;
928
942
  connectionIdRef.current = null;
929
943
  };
930
- }, [descriptorKey, enabled, mode]);
931
- return { data, events, status, error, disconnect };
944
+ }, [descriptorKey, enabled, mode, descriptorRef]);
945
+ return { data, events, status: enabled ? status : "idle", error, disconnect };
932
946
  }
933
947
 
934
948
  // src/hooks/useQueryCache.ts
@@ -974,7 +988,7 @@ function useHostAction(actionTypeId) {
974
988
  const chain = {
975
989
  action: {
976
990
  type: actionTypeId,
977
- target: bridge.domainId,
991
+ target: bridge.extDomainId,
978
992
  payload
979
993
  }
980
994
  };
@@ -988,7 +1002,7 @@ function useHostAction(actionTypeId) {
988
1002
  }
989
1003
 
990
1004
  // src/mfe/hooks/useDomainExtensions.ts
991
- import { useSyncExternalStore as useSyncExternalStore5, useCallback as useCallback7, useRef as useRef2 } from "react";
1005
+ import { useSyncExternalStore as useSyncExternalStore5, useCallback as useCallback7, useRef as useRef3 } from "react";
992
1006
  function useDomainExtensions(domainId) {
993
1007
  const app = useFrontX();
994
1008
  const registry = app.mfeRegistry;
@@ -1003,7 +1017,7 @@ function useDomainExtensions(domainId) {
1003
1017
  },
1004
1018
  [app.store]
1005
1019
  );
1006
- const cacheRef = useRef2({ ids: "", extensions: [] });
1020
+ const cacheRef = useRef3({ ids: "", extensions: [] });
1007
1021
  const getSnapshot = useCallback7(() => {
1008
1022
  const extensions = registry.getExtensionsForDomain(domainId);
1009
1023
  const ids = extensions.map((e) => e.id).join(",");
@@ -1016,7 +1030,7 @@ function useDomainExtensions(domainId) {
1016
1030
  }
1017
1031
 
1018
1032
  // src/mfe/hooks/useMountedExtensions.ts
1019
- import { useSyncExternalStore as useSyncExternalStore6, useCallback as useCallback8, useRef as useRef3 } from "react";
1033
+ import { useSyncExternalStore as useSyncExternalStore6, useCallback as useCallback8, useRef as useRef4 } from "react";
1020
1034
  function useMountedExtensions(domainId) {
1021
1035
  const app = useFrontX();
1022
1036
  const registry = app.mfeRegistry;
@@ -1031,7 +1045,7 @@ function useMountedExtensions(domainId) {
1031
1045
  },
1032
1046
  [app.store]
1033
1047
  );
1034
- const cacheRef = useRef3({ key: "", extensions: [] });
1048
+ const cacheRef = useRef4({ key: "", extensions: [] });
1035
1049
  const getSnapshot = useCallback8(() => {
1036
1050
  const ids = registry.getMountedExtensions(domainId);
1037
1051
  const resolved = ids.map((id) => registry.getExtension(id)).filter((ext) => ext !== void 0);
@@ -1045,7 +1059,7 @@ function useMountedExtensions(domainId) {
1045
1059
  }
1046
1060
 
1047
1061
  // src/mfe/hooks/useRegisteredPackages.ts
1048
- import { useSyncExternalStore as useSyncExternalStore7, useCallback as useCallback9, useRef as useRef4 } from "react";
1062
+ import { useSyncExternalStore as useSyncExternalStore7, useCallback as useCallback9, useRef as useRef5 } from "react";
1049
1063
  function useRegisteredPackages() {
1050
1064
  const app = useFrontX();
1051
1065
  const registry = app.mfeRegistry;
@@ -1060,7 +1074,7 @@ function useRegisteredPackages() {
1060
1074
  },
1061
1075
  [app.store]
1062
1076
  );
1063
- const cacheRef = useRef4({ packages: "", list: [] });
1077
+ const cacheRef = useRef5({ packages: "", list: [] });
1064
1078
  const getSnapshot = useCallback9(() => {
1065
1079
  const list = registry.getRegisteredPackages();
1066
1080
  const packages = list.join(",");
@@ -1075,6 +1089,9 @@ function useRegisteredPackages() {
1075
1089
  // src/mfe/ThemeAwareReactLifecycle.tsx
1076
1090
  import { createRoot } from "react-dom/client";
1077
1091
  import { jsx as jsx4 } from "react/jsx-runtime";
1092
+ var ADOPTED_HOST_STYLE_ATTR = "data-frontx-adopted-host-style";
1093
+ var BASE_RESETS_STYLE_ID = "__frontx-base-resets__";
1094
+ var SHELL_OWNED_SHADOW_STYLE_IDS = ["__frontx-shadow-isolation__", "__frontx-css-variables__"];
1078
1095
  function resolveProviderMountOptions(app, bridge, mountContext) {
1079
1096
  const extensionId = mountContext?.extensionId;
1080
1097
  const domainId = mountContext?.domainId;
@@ -1129,27 +1146,108 @@ var ThemeAwareReactLifecycle = class {
1129
1146
  /**
1130
1147
  * Copy all inline <style> and <link rel="stylesheet"> from the host document
1131
1148
  * into the shadow root so that Tailwind and component styles apply inside the MFE.
1149
+ *
1150
+ * The clones are inserted ahead of everything already in the shadow root, never
1151
+ * appended, because adopted host styles are context rather than authority: where
1152
+ * they and the MFE's own CSS declare the same property at equal specificity, the
1153
+ * MFE has to win. Appending inverted that. `MfeHandlerMF` injects the MFE's
1154
+ * compiled stylesheet into the shadow root before it calls mount, so an appended
1155
+ * clone of the shell's Tailwind preflight - whose
1156
+ * `button, [type='button'], [type='reset'], [type='submit']` rule sets
1157
+ * `background-color: transparent` at specificity (0,1,0) - tied with
1158
+ * `@gears-frontx/ui-kit`'s single-class button rule and won on document order,
1159
+ * leaving every kit Button transparent until it was hovered.
1160
+ *
1161
+ * Inserting at the front rather than pushing MFE styles to the back is what makes
1162
+ * the invariant hold for stylesheets that appear after mount too - a lazily
1163
+ * imported component's CSS module, or whatever initializeStyles() adds - since
1164
+ * those land behind the adopted block by construction.
1165
+ *
1166
+ * A cascade layer around the adopted CSS was the alternative and is rejected:
1167
+ * layered rules lose to unlayered ones at *any* specificity, so a host utility
1168
+ * class would stop overriding an MFE element selector. Document order keeps
1169
+ * specificity in charge and only settles the ties.
1170
+ *
1171
+ * The block replaces its predecessor instead of stacking on it, the same
1172
+ * upsert shape `injectStylesheet` and `injectCssVariables` use in
1173
+ * `@gears-frontx/mfes`' shadow utilities, widened from one id-keyed node to a
1174
+ * marked block. Replacement is required rather than tidy: `createShadowRoot`
1175
+ * hands back an existing `element.shadowRoot` instead of attaching a new one,
1176
+ * so remounting the same container runs this method again on a shadow root
1177
+ * that already holds a full adopted block, and appending would grow it once
1178
+ * per mount with no bound. It does not save re-resolving the cloned `<link>`s -
1179
+ * every mount clones them afresh and the browser resolves each clone; what
1180
+ * replacement buys is that the count stays at one block instead of one per
1181
+ * mount.
1182
+ *
1183
+ * The block is rebuilt from the host head rather than left in place, because
1184
+ * the head is not fixed: a lazily imported chunk adds stylesheets to it after
1185
+ * the first mount, and the second mount has to pick those up.
1186
+ *
1187
+ * The fresh block goes in before the previous one comes out. Removing first
1188
+ * leaves the shadow root with no host CSS at all, and everything between that
1189
+ * removal and the insertion is synchronous work in this same task - so the gap
1190
+ * never reaches a paint, but any code that reads computed styles from the tree
1191
+ * in between sees an unstyled one. Inserting first costs one moment with two
1192
+ * blocks present, which changes nothing: the old block is a clone of the same
1193
+ * host head and loses every tie to the new one on document order anyway.
1132
1194
  */
1133
1195
  adoptHostStylesIntoShadowRoot(shadowRoot) {
1134
- const styleElements = document.head.querySelectorAll("style");
1135
- styleElements.forEach((el) => {
1136
- const clone = document.createElement("style");
1137
- clone.textContent = el.textContent ?? "";
1138
- shadowRoot.appendChild(clone);
1139
- });
1140
- const linkElements = document.head.querySelectorAll('link[rel="stylesheet"]');
1141
- linkElements.forEach((el) => {
1142
- const clone = el.cloneNode(true);
1143
- shadowRoot.appendChild(clone);
1196
+ const supersededStyles = Array.from(shadowRoot.querySelectorAll(`[${ADOPTED_HOST_STYLE_ATTR}]`));
1197
+ const adoptedStyles = document.createDocumentFragment();
1198
+ const hostStyleNodes = document.head.querySelectorAll('style, link[rel="stylesheet"]');
1199
+ hostStyleNodes.forEach((el) => {
1200
+ if (el instanceof HTMLStyleElement) {
1201
+ const clone = document.createElement("style");
1202
+ clone.textContent = el.textContent ?? "";
1203
+ clone.setAttribute(ADOPTED_HOST_STYLE_ATTR, "");
1204
+ adoptedStyles.appendChild(clone);
1205
+ return;
1206
+ }
1207
+ const linkClone = el.cloneNode(true);
1208
+ linkClone.setAttribute(ADOPTED_HOST_STYLE_ATTR, "");
1209
+ adoptedStyles.appendChild(linkClone);
1144
1210
  });
1211
+ shadowRoot.insertBefore(adoptedStyles, shadowRoot.firstChild);
1212
+ supersededStyles.forEach((el) => el.remove());
1145
1213
  }
1146
1214
  /**
1147
1215
  * Box-model resets and :host defaults needed inside every shadow root.
1148
1216
  * These aren't part of Tailwind's compiled output but are required for
1149
1217
  * consistent rendering across browsers.
1218
+ *
1219
+ * Reuses the node a previous mount left behind, exactly as `injectStylesheet`
1220
+ * does in `@gears-frontx/mfes`' shadow utilities: a remount runs against a
1221
+ * shadow root that `createShadowRoot` handed back intact, and appending a
1222
+ * second copy would add one node per mount forever.
1223
+ *
1224
+ * Reuse alone is not enough, which is why the node is also positioned on every
1225
+ * mount rather than left where the previous one sat. These resets are context
1226
+ * like the adopted block is - `border-color: currentColor` on `*` is a 0-0-0
1227
+ * declaration that beats an MFE preflight's own 0-0-0 reset purely on document
1228
+ * order - so they have to stay ahead of the MFE's CSS, and where they land is
1229
+ * not stable on its own. `MfeHandlerMF.wrapLifecycleWithStylesheets` removes the
1230
+ * MFE's stylesheet on unmount and appends a fresh one before the next mount, so
1231
+ * a node merely left in place ends up ahead of that stylesheet on a remount and
1232
+ * behind it on the first mount: two different cascades for one component.
1233
+ * Anchoring it to the end of the shell-owned head gives every mount the same
1234
+ * order - adopted block, isolation, resets, MFE - which is also the order the
1235
+ * adoption method's invariant is written against.
1236
+ *
1237
+ * With no shell-owned node to anchor to, the resets go to the front. That is the
1238
+ * plain-`Element` container: no adopted block, no isolation style, and the MFE
1239
+ * stylesheet already appended by the handler, so the front is again the only
1240
+ * position that leaves the MFE winning ties.
1241
+ *
1242
+ * Looked up by attribute-selector form rather than `getElementById`, which
1243
+ * `ShadowRoot` has but `Element` does not, and this hook takes either.
1150
1244
  */
1151
1245
  injectBaseResets(container) {
1152
- const style = document.createElement("style");
1246
+ const existing = container.querySelector(
1247
+ `style[id="${BASE_RESETS_STYLE_ID}"]`
1248
+ );
1249
+ const style = existing ?? document.createElement("style");
1250
+ style.id = BASE_RESETS_STYLE_ID;
1153
1251
  style.textContent = `
1154
1252
  *, *::before, *::after {
1155
1253
  box-sizing: border-box;
@@ -1167,7 +1265,17 @@ var ThemeAwareReactLifecycle = class {
1167
1265
  background-color: hsl(var(--background));
1168
1266
  }
1169
1267
  `;
1170
- container.appendChild(style);
1268
+ const shellOwnedHead = container.querySelectorAll(
1269
+ [
1270
+ `[${ADOPTED_HOST_STYLE_ATTR}]`,
1271
+ ...SHELL_OWNED_SHADOW_STYLE_IDS.map((id) => `[id="${id}"]`)
1272
+ ].join(", ")
1273
+ );
1274
+ const lastShellOwnedNode = shellOwnedHead[shellOwnedHead.length - 1];
1275
+ container.insertBefore(
1276
+ style,
1277
+ lastShellOwnedNode ? lastShellOwnedNode.nextSibling : container.firstChild
1278
+ );
1171
1279
  }
1172
1280
  /**
1173
1281
  * Hook for subclasses to inject additional CSS not covered by the adopted host
@@ -1180,7 +1288,7 @@ var ThemeAwareReactLifecycle = class {
1180
1288
  };
1181
1289
 
1182
1290
  // src/mfe/components/ExtensionDomainSlot.tsx
1183
- import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
1291
+ import { useEffect as useEffect7, useEffectEvent, useRef as useRef6, useState as useState6 } from "react";
1184
1292
  import { jsx as jsx5 } from "react/jsx-runtime";
1185
1293
  function ExtensionDomainSlot(props) {
1186
1294
  const {
@@ -1191,13 +1299,15 @@ function ExtensionDomainSlot(props) {
1191
1299
  onDetached,
1192
1300
  loadingComponent = null
1193
1301
  } = props;
1194
- const rootRef = useRef5(null);
1195
- const onAttachedRef = useRef5(onAttached);
1196
- const onDetachedRef = useRef5(onDetached);
1197
- onAttachedRef.current = onAttached;
1198
- onDetachedRef.current = onDetached;
1302
+ const rootRef = useRef6(null);
1303
+ const emitAttached = useEffectEvent((root) => {
1304
+ onAttached?.(root);
1305
+ });
1306
+ const emitDetached = useEffectEvent(() => {
1307
+ onDetached?.();
1308
+ });
1199
1309
  const [attached, setAttached] = useState6(false);
1200
- useEffect6(() => {
1310
+ useEffect7(() => {
1201
1311
  const root = rootRef.current;
1202
1312
  if (!root) {
1203
1313
  return;
@@ -1205,11 +1315,11 @@ function ExtensionDomainSlot(props) {
1205
1315
  const mounter = registry.getMounter(domainId);
1206
1316
  mounter.attach(root);
1207
1317
  setAttached(true);
1208
- onAttachedRef.current?.(root);
1318
+ emitAttached(root);
1209
1319
  return () => {
1210
1320
  void mounter.detach();
1211
1321
  setAttached(false);
1212
- onDetachedRef.current?.();
1322
+ emitDetached();
1213
1323
  };
1214
1324
  }, [registry, domainId]);
1215
1325
  return /* @__PURE__ */ jsx5(