@mohamedatia/fly-design-system 2.7.2 → 2.7.4

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.
@@ -749,9 +749,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
749
749
  * 3. Parses the HTML with DOMParser to extract the first
750
750
  * `<link rel="stylesheet">` — the hashed production stylesheet
751
751
  * (e.g. `styles-ABC123.css`). This is Strategy (b): index.html discovery.
752
- * 4. Injects `<link rel="stylesheet" data-fly-app="<appId>">` into
753
- * `document.head`. Idempotent: if an identical href is already present,
754
- * the call is a no-op. If the href differs (hot upgrade), it is replaced.
752
+ * 4. Injects an inline `<style data-fly-app="<appId>">` block containing
753
+ * `@layer remote { @import url("..."); }` into `document.head`.
754
+ * Idempotent: if a `<style data-fly-app="appId">` with the same href
755
+ * is already present, the call is a no-op. If the href differs (hot
756
+ * upgrade), the existing element is replaced.
755
757
  *
756
758
  * Why strategy (b)?
757
759
  * -----------------
@@ -761,6 +763,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
761
763
  * (c) `stylesUrl` in manifest/remoteEntry.json — cleanest long-term; requires
762
764
  * protocol change to every remote's CI pipeline (out of scope for Wave A1).
763
765
  *
766
+ * CSS Cascade Layer — `@import url("...") layer(remote)`
767
+ * --------------------------------------------------------
768
+ * Remote stylesheets are injected via `@import url("…") layer(remote)` (CSS
769
+ * Cascade Level 5). This keeps remote styles in a named cascade layer so the
770
+ * shell can reliably override them via `@layer overrides`.
771
+ *
772
+ * The block-form `@layer remote { @import url("…"); }` is INVALID CSS — the
773
+ * spec forbids `@import` inside any block at-rule. Browsers silently drop such
774
+ * rules, causing remote stylesheets to never load. The correct syntax is the
775
+ * `layer()` modifier on a top-level `@import`.
776
+ *
777
+ * Browser support: Chrome 99+, Firefox 97+, Safari 15.4+ — all current engines.
778
+ *
779
+ * Alternative approaches rejected:
780
+ * - Fetch+inline: relative `url(...)` paths inside the remote CSS would
781
+ * resolve against the shell origin instead of the remote origin, breaking
782
+ * fonts and images. Dealbreaker without a full URL-rewrite pass.
783
+ * - `<link layer="remote">`: the `layer` attribute on `<link>` is not yet
784
+ * shipped in Firefox (as of 2025-05). Non-portable.
785
+ *
786
+ * Layer order declaration
787
+ * -----------------------
788
+ * A one-time `<style data-fly-layers>` element is prepended to `<head>` the first
789
+ * time any remote style is loaded. It establishes the canonical layer order:
790
+ * reset → designsystem → shell → remote → overrides
791
+ * This guarantees that even if individual `@layer` blocks are injected in any
792
+ * order at runtime, the cascade priority is always deterministic.
793
+ *
764
794
  * Unload decision
765
795
  * ---------------
766
796
  * `unloadRemoteStyles` is exported for symmetry but should NOT be called on
@@ -774,7 +804,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
774
804
  * - CORS: the remote's dev/prod server must serve `index.html` with a
775
805
  * permissive `Access-Control-Allow-Origin` header (or be same-origin via
776
806
  * the YARP gateway). The fetch uses `credentials: 'omit'` to avoid
777
- * credential-carrying preflights.
807
+ * credential-carrying preflights. The CSS file itself must also be CORS-
808
+ * accessible since `@import` inside a `<style>` is subject to CORS checks.
778
809
  * - Race on rapid mount/unmount: if `loadRemoteStyles` is called a second time
779
810
  * for the same appId while the first fetch is still in-flight, the second
780
811
  * call joins the same in-flight Promise (idempotency guard at fetch level).
@@ -787,6 +818,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
787
818
  const _resolvedHref = new Map();
788
819
  /** In-flight fetch promises keyed by appId — prevents duplicate fetches. */
789
820
  const _inFlight = new Map();
821
+ /**
822
+ * Whether the canonical layer-order declaration has already been injected.
823
+ * Module-level so it survives across multiple `loadRemoteStyles` calls within
824
+ * the same shell session but is reset on a full page reload.
825
+ */
826
+ let _layersDeclared = false;
827
+ /**
828
+ * Injects a single `<style data-fly-layers>` element at the top of `<head>`
829
+ * that establishes the canonical cascade-layer priority order for the shell:
830
+ *
831
+ * reset → designsystem → shell → remote → overrides
832
+ *
833
+ * Any subsequent `@layer X { … }` block lands in the already-established slot
834
+ * regardless of injection order, so late-arriving remote stylesheets never
835
+ * "win" over shell or override rules. Idempotent — runs at most once per page.
836
+ */
837
+ function _ensureLayerOrder() {
838
+ if (_layersDeclared)
839
+ return;
840
+ _layersDeclared = true;
841
+ // Guard against duplicate elements in the DOM (e.g. hot-module-reload edge cases).
842
+ if (document.head.querySelector('style[data-fly-layers]'))
843
+ return;
844
+ const style = document.createElement('style');
845
+ style.setAttribute('data-fly-layers', '');
846
+ style.textContent = '@layer reset, designsystem, shell, remote, overrides;';
847
+ // Prepend so this always precedes any other layer-bearing <style> blocks.
848
+ document.head.insertBefore(style, document.head.firstChild);
849
+ }
790
850
  /**
791
851
  * Discovers the hashed stylesheet href from the remote's `index.html`.
792
852
  * Returns `null` if no stylesheet `<link>` is found or the fetch fails.
@@ -830,13 +890,24 @@ async function _discoverStylesheetHref(remoteBaseUrl) {
830
890
  * or `http://localhost:7202`. Must NOT include `/remoteEntry.json`.
831
891
  *
832
892
  * The call is idempotent:
833
- * - If a `<link data-fly-app="appId">` with the same href already exists → no-op.
834
- * - If the href differs (hot upgrade) existing link is replaced.
893
+ * - If a `<style data-fly-app="appId">` whose content references the same href
894
+ * already existsno-op.
895
+ * - If the href differs (hot upgrade) → existing element is replaced.
835
896
  * - If no stylesheet is found in `index.html` → no-op (logged as a warning).
897
+ *
898
+ * Injection shape:
899
+ * <style data-fly-app="<appId>" data-fly-href="<href>">
900
+ * @import url("<href>") layer(remote);
901
+ * </style>
902
+ *
903
+ * The `data-fly-href` attribute stores the discovered href separately from the
904
+ * style content so idempotency checks can compare the URL without parsing CSS.
836
905
  */
837
906
  async function loadRemoteStyles(appId, remoteBaseUrl) {
838
907
  if (typeof document === 'undefined')
839
908
  return; // SSR guard
909
+ // Ensure the canonical layer order is declared before any remote layer is injected.
910
+ _ensureLayerOrder();
840
911
  // Kick off or join an in-flight fetch for this appId.
841
912
  let fetchPromise = _inFlight.get(appId);
842
913
  if (!fetchPromise) {
@@ -856,19 +927,23 @@ async function loadRemoteStyles(appId, remoteBaseUrl) {
856
927
  console.warn(`[FlyOS] loadRemoteStyles: no stylesheet found in ${remoteBaseUrl}/index.html for appId="${appId}"`);
857
928
  return;
858
929
  }
859
- const existing = document.head.querySelector(`link[data-fly-app="${CSS.escape(appId)}"]`);
930
+ const selector = `style[data-fly-app="${CSS.escape(appId)}"]`;
931
+ const existing = document.head.querySelector(selector);
860
932
  if (existing) {
861
- if (existing.getAttribute('href') === href)
933
+ if (existing.getAttribute('data-fly-href') === href)
862
934
  return; // identical — no-op
863
- // Hot upgrade: replace href.
864
- existing.setAttribute('href', href);
865
- return;
866
- }
867
- const link = document.createElement('link');
868
- link.rel = 'stylesheet';
869
- link.setAttribute('data-fly-app', appId);
870
- link.href = href;
871
- document.head.appendChild(link);
935
+ // Hot upgrade: replace the entire element so the @import URL updates atomically.
936
+ existing.remove();
937
+ }
938
+ // CSS Cascade Level 5: `@import url("…") layer(remote)` is the only valid
939
+ // way to place an @import inside a named cascade layer. Block-form
940
+ // `@layer remote { @import … }` is invalid and silently dropped by browsers.
941
+ // See module JSDoc for full rationale.
942
+ const style = document.createElement('style');
943
+ style.setAttribute('data-fly-app', appId);
944
+ style.setAttribute('data-fly-href', href);
945
+ style.textContent = `@import url("${href}") layer(remote);`;
946
+ document.head.appendChild(style);
872
947
  }
873
948
  /**
874
949
  * Removes the injected stylesheet for `appId` from `document.head`.
@@ -880,8 +955,8 @@ async function loadRemoteStyles(appId, remoteBaseUrl) {
880
955
  function unloadRemoteStyles(appId) {
881
956
  if (typeof document === 'undefined')
882
957
  return; // SSR guard
883
- const link = document.head.querySelector(`link[data-fly-app="${CSS.escape(appId)}"]`);
884
- link?.parentNode?.removeChild(link);
958
+ const style = document.head.querySelector(`style[data-fly-app="${CSS.escape(appId)}"]`);
959
+ style?.parentNode?.removeChild(style);
885
960
  _resolvedHref.delete(appId);
886
961
  }
887
962