@master/css-runtime 2.0.0-rc.74 → 2.0.0-rc.75

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.
@@ -643,6 +643,12 @@ const browserNativeDeclarationMatcher = ({ property, value }) => {
643
643
  //#endregion
644
644
  //#region src/core.ts
645
645
  const MASTER_CSS_RUNTIME_STYLE_SELECTOR = `style#${MASTER_CSS_RUNTIME_STYLE_ID}`;
646
+ const RETAINED_CLASS_RULE_MIN_AGE_MS = 1e3;
647
+ const RETAINED_CLASS_RULE_IDLE_TIMEOUT_MS = 5e3;
648
+ const RETAINED_CLASS_RULE_CLEANUP_BATCH_SIZE = 64;
649
+ const RETAINED_CLASS_RULE_SOFT_TARGET = 128;
650
+ const RETAINED_CLASS_RULE_HARD_LIMIT = 512;
651
+ const RETAINED_CLASS_RULE_HARD_RAW_BYTES = 256 * 1024;
646
652
  function isDocumentRoot(root) {
647
653
  const rootConstructorName = root?.constructor.name;
648
654
  return rootConstructorName === "HTMLDocument" || rootConstructorName === "Document";
@@ -650,10 +656,12 @@ function isDocumentRoot(root) {
650
656
  function findElementById(root, id) {
651
657
  return isDocumentRoot(root) ? root.getElementById(id) : "querySelector" in root ? root.querySelector(`#${id}`) : void 0;
652
658
  }
659
+ function validateHydrationManifest(hydrationManifest) {
660
+ return hydrationManifest?.version === 1 && Array.isArray(hydrationManifest?.rules) ? hydrationManifest : void 0;
661
+ }
653
662
  function parseHydrationManifest(source) {
654
663
  try {
655
- const hydrationManifest = JSON.parse(source);
656
- return hydrationManifest?.version === 1 && Array.isArray(hydrationManifest.rules) ? hydrationManifest : void 0;
664
+ return validateHydrationManifest(JSON.parse(source));
657
665
  } catch (error) {
658
666
  console.debug("Cannot parse Master CSS hydration manifest.", error);
659
667
  }
@@ -668,6 +676,13 @@ function readExternalHydrationManifestSource(root) {
668
676
  const HTMLStyleElementConstructor = globalThis.HTMLStyleElement;
669
677
  return HTMLStyleElementConstructor && styleElement instanceof HTMLStyleElementConstructor ? styleElement.getAttribute(MASTER_CSS_HYDRATION_MANIFEST_ATTR) : void 0;
670
678
  }
679
+ function resolveExternalHydrationManifestURL(root, source) {
680
+ const ownerDocument = isDocumentRoot(root) ? root : root.ownerDocument;
681
+ return new URL(source, ownerDocument.baseURI).href;
682
+ }
683
+ async function importHydrationManifest(url) {
684
+ return validateHydrationManifest((await globalThis.Function("specifier", "return import(specifier, { with: { type: 'json' } })")(url)).default);
685
+ }
671
686
  var CSSRuntime = class CSSRuntime extends MasterCSS {
672
687
  root;
673
688
  hydrationManifest;
@@ -680,10 +695,16 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
680
695
  componentsLayer = new RuntimeUtilityLayer("components", this);
681
696
  utilitiesLayer = new RuntimeUtilityLayer("utilities", this);
682
697
  classCounts = /* @__PURE__ */ new Map();
698
+ retainedClassNames = /* @__PURE__ */ new Set();
683
699
  classTracker = new RuntimeClassTracker();
700
+ pendingAddedClassNames = /* @__PURE__ */ new Set();
684
701
  pendingRemovedClassNames = /* @__PURE__ */ new Set();
702
+ retainedClassRules = /* @__PURE__ */ new Map();
703
+ pendingAdditionFrame;
685
704
  pendingRemovalFrame;
686
705
  pendingRemovalFlushFrame;
706
+ retainedCleanupIdleHandle;
707
+ retainedCleanupTimeoutHandle;
687
708
  hydrationFailureReason;
688
709
  observer;
689
710
  progressive = false;
@@ -740,9 +761,7 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
740
761
  const source = readExternalHydrationManifestSource(this.root);
741
762
  if (!source) return this;
742
763
  try {
743
- const response = await fetch(source, { credentials: "same-origin" });
744
- if (!response.ok) return this;
745
- this.setHydrationManifest(parseHydrationManifest(await response.text()));
764
+ this.setHydrationManifest(await importHydrationManifest(resolveExternalHydrationManifestURL(this.root, source)));
746
765
  } catch (error) {
747
766
  console.debug("Cannot load Master CSS hydration manifest.", error);
748
767
  }
@@ -765,7 +784,7 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
765
784
  this.progressive = false;
766
785
  this.createRuntimeStyle();
767
786
  this.insertStaticResources();
768
- connectedNames.forEach((cls) => this.add(cls));
787
+ connectedNames.forEach((cls) => this.ensureClassRules(cls));
769
788
  }
770
789
  detectRuntimeStyle() {
771
790
  if (this.root.styleSheets) for (const sheet of this.root.styleSheets) {
@@ -785,7 +804,7 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
785
804
  if (hydrateResult) {
786
805
  const hydratedClassNames = new Set(hydrateResult.allUtilities.map(({ fixedClass, name }) => fixedClass || name));
787
806
  for (const cls of connectedNames) if (!hydratedClassNames.has(cls)) {
788
- this.add(cls);
807
+ this.ensureClassRules(cls);
789
808
  console.debug(`Missing prerendered rule for class \`${cls}\``);
790
809
  }
791
810
  } else this.useRuntimeStyle(connectedNames, this.hydrationFailureReason || `Cannot read ${MASTER_CSS_RUNTIME_STYLE_SELECTOR} CSS rules.`);
@@ -793,7 +812,7 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
793
812
  renderRuntimeStyle(connectedNames) {
794
813
  this.createRuntimeStyle();
795
814
  this.insertStaticResources();
796
- connectedNames.forEach((cls) => this.add(cls));
815
+ connectedNames.forEach((cls) => this.ensureClassRules(cls));
797
816
  }
798
817
  getAnimationFrameWindow() {
799
818
  return (isDocumentRoot(this.root) ? this.root : this.root.ownerDocument)?.defaultView || globalThis;
@@ -809,15 +828,56 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
809
828
  this.pendingRemovalFlushFrame = void 0;
810
829
  }
811
830
  }
831
+ cancelPendingAdditionFrame() {
832
+ if (this.pendingAdditionFrame === void 0) return;
833
+ this.getAnimationFrameWindow().cancelAnimationFrame(this.pendingAdditionFrame);
834
+ this.pendingAdditionFrame = void 0;
835
+ }
836
+ clearPendingAddedClassNames() {
837
+ this.pendingAddedClassNames.clear();
838
+ this.cancelPendingAdditionFrame();
839
+ }
812
840
  clearPendingRemovedClassNames() {
813
841
  this.pendingRemovedClassNames.clear();
814
842
  this.cancelPendingRemovalFrames();
815
843
  }
844
+ getCleanupWindow() {
845
+ return this.getAnimationFrameWindow();
846
+ }
847
+ cancelRetainedClassRuleCleanup() {
848
+ const view = this.getCleanupWindow();
849
+ if (this.retainedCleanupIdleHandle !== void 0) {
850
+ view.cancelIdleCallback?.(this.retainedCleanupIdleHandle);
851
+ this.retainedCleanupIdleHandle = void 0;
852
+ }
853
+ if (this.retainedCleanupTimeoutHandle !== void 0) {
854
+ view.clearTimeout(this.retainedCleanupTimeoutHandle);
855
+ this.retainedCleanupTimeoutHandle = void 0;
856
+ }
857
+ }
858
+ clearRetainedClassRules() {
859
+ this.retainedClassNames.clear();
860
+ this.retainedClassRules.clear();
861
+ this.cancelRetainedClassRuleCleanup();
862
+ }
816
863
  cancelPendingRemovedClassNames(classNames) {
817
864
  if (!this.pendingRemovedClassNames.size) return;
818
865
  for (const className of classNames) this.pendingRemovedClassNames.delete(className);
819
866
  if (!this.pendingRemovedClassNames.size) this.cancelPendingRemovalFrames();
820
867
  }
868
+ cancelPendingAddedClassNames(classNames) {
869
+ if (!this.pendingAddedClassNames.size) return;
870
+ for (const className of classNames) this.pendingAddedClassNames.delete(className);
871
+ if (!this.pendingAddedClassNames.size) this.cancelPendingAdditionFrame();
872
+ }
873
+ cancelRetainedClassNames(classNames) {
874
+ if (!this.retainedClassNames.size) return;
875
+ for (const className of classNames) {
876
+ this.retainedClassNames.delete(className);
877
+ this.retainedClassRules.delete(className);
878
+ }
879
+ if (!this.retainedClassNames.size) this.cancelRetainedClassRuleCleanup();
880
+ }
821
881
  schedulePendingRemovalFlush() {
822
882
  if (!this.pendingRemovedClassNames.size) return;
823
883
  if (this.pendingRemovalFrame !== void 0 || this.pendingRemovalFlushFrame !== void 0) return;
@@ -834,39 +894,167 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
834
894
  for (const className of classNames) if (!this.classCounts.has(className)) this.pendingRemovedClassNames.add(className);
835
895
  this.schedulePendingRemovalFlush();
836
896
  }
897
+ schedulePendingAdditionFlush() {
898
+ if (!this.pendingAddedClassNames.size) return;
899
+ if (this.pendingAdditionFrame !== void 0) return;
900
+ const view = this.getAnimationFrameWindow();
901
+ this.pendingAdditionFrame = view.requestAnimationFrame(() => {
902
+ this.pendingAdditionFrame = void 0;
903
+ this.flushPendingAddedClassNames();
904
+ });
905
+ }
906
+ queueAddedClassNames(classNames) {
907
+ for (const className of classNames) if (this.classCounts.has(className)) this.pendingAddedClassNames.add(className);
908
+ this.schedulePendingAdditionFlush();
909
+ }
910
+ estimateRetainedClassRule(className) {
911
+ const rules = this.classUtilities.get(className);
912
+ if (!rules?.length) return;
913
+ let rawBytes = 0;
914
+ let ruleCount = 0;
915
+ for (const rule of rules) {
916
+ const nodes = rule.nodes;
917
+ if (nodes?.length) for (const node of nodes) {
918
+ rawBytes += node.text.length;
919
+ ruleCount++;
920
+ }
921
+ else {
922
+ rawBytes += rule.text.length;
923
+ ruleCount++;
924
+ }
925
+ }
926
+ return {
927
+ retainedAt: Date.now(),
928
+ rawBytes,
929
+ ruleCount
930
+ };
931
+ }
932
+ getRetainedClassRuleRawBytes() {
933
+ let rawBytes = 0;
934
+ for (const retainedRule of this.retainedClassRules.values()) rawBytes += retainedRule.rawBytes;
935
+ return rawBytes;
936
+ }
937
+ exceedsRetainedClassRuleHardLimits() {
938
+ return this.retainedClassNames.size > RETAINED_CLASS_RULE_HARD_LIMIT || this.getRetainedClassRuleRawBytes() > RETAINED_CLASS_RULE_HARD_RAW_BYTES;
939
+ }
940
+ scheduleRetainedClassRuleCleanup() {
941
+ if (!this.retainedClassNames.size) return;
942
+ if (this.retainedCleanupIdleHandle !== void 0 || this.retainedCleanupTimeoutHandle !== void 0) return;
943
+ const view = this.getCleanupWindow();
944
+ const hardLimitExceeded = this.exceedsRetainedClassRuleHardLimits();
945
+ const cleanup = () => {
946
+ this.retainedCleanupIdleHandle = void 0;
947
+ this.retainedCleanupTimeoutHandle = void 0;
948
+ this.cleanupRetainedClassRules();
949
+ };
950
+ const timeout = hardLimitExceeded ? 0 : RETAINED_CLASS_RULE_IDLE_TIMEOUT_MS;
951
+ if (view.requestIdleCallback) this.retainedCleanupIdleHandle = view.requestIdleCallback(cleanup, { timeout });
952
+ else this.retainedCleanupTimeoutHandle = view.setTimeout(cleanup, timeout);
953
+ }
954
+ retainRemovedClassRules(classNames) {
955
+ for (const className of classNames) {
956
+ if (this.classCounts.has(className)) continue;
957
+ const retainedRule = this.retainedClassRules.get(className) || this.estimateRetainedClassRule(className);
958
+ if (!retainedRule) continue;
959
+ this.retainedClassNames.add(className);
960
+ this.retainedClassRules.set(className, retainedRule);
961
+ }
962
+ this.scheduleRetainedClassRuleCleanup();
963
+ }
964
+ getRetainedClassRuleCleanupCandidates(force = false) {
965
+ const now = Date.now();
966
+ const hardLimitExceeded = this.exceedsRetainedClassRuleHardLimits();
967
+ const entries = [];
968
+ for (const className of this.retainedClassNames) {
969
+ if (this.classCounts.has(className)) {
970
+ this.retainedClassNames.delete(className);
971
+ this.retainedClassRules.delete(className);
972
+ continue;
973
+ }
974
+ const retainedRule = this.retainedClassRules.get(className);
975
+ if (!retainedRule) {
976
+ this.retainedClassNames.delete(className);
977
+ continue;
978
+ }
979
+ const oldEnough = now - retainedRule.retainedAt >= RETAINED_CLASS_RULE_MIN_AGE_MS;
980
+ if (force || hardLimitExceeded || oldEnough && this.retainedClassNames.size > RETAINED_CLASS_RULE_SOFT_TARGET) entries.push([className, retainedRule]);
981
+ }
982
+ entries.sort(([, a], [, b]) => a.retainedAt - b.retainedAt);
983
+ const cleanupCount = hardLimitExceeded ? Math.max(RETAINED_CLASS_RULE_CLEANUP_BATCH_SIZE, this.retainedClassNames.size - RETAINED_CLASS_RULE_SOFT_TARGET) : RETAINED_CLASS_RULE_CLEANUP_BATCH_SIZE;
984
+ return entries.slice(0, force ? entries.length : cleanupCount).map(([className]) => className);
985
+ }
986
+ removeRetainedClassRules(classNames) {
987
+ const removedClassNames = [];
988
+ for (const className of classNames) {
989
+ if (!this.retainedClassNames.has(className)) continue;
990
+ this.retainedClassNames.delete(className);
991
+ this.retainedClassRules.delete(className);
992
+ if (!this.classCounts.has(className)) removedClassNames.push(className);
993
+ }
994
+ if (removedClassNames.length) super.deleteClassRules(...removedClassNames);
995
+ return removedClassNames.length;
996
+ }
997
+ cleanupRetainedClassRules(force = false) {
998
+ const removedCount = this.removeRetainedClassRules(this.getRetainedClassRuleCleanupCandidates(force));
999
+ if (this.retainedClassNames.size && (force || this.retainedClassNames.size > RETAINED_CLASS_RULE_SOFT_TARGET || this.exceedsRetainedClassRuleHardLimits())) this.scheduleRetainedClassRuleCleanup();
1000
+ return removedCount;
1001
+ }
1002
+ flushRetainedClassRules() {
1003
+ this.cancelRetainedClassRuleCleanup();
1004
+ const removedCount = this.cleanupRetainedClassRules(true);
1005
+ if (this.retainedClassNames.size) this.scheduleRetainedClassRuleCleanup();
1006
+ return removedCount;
1007
+ }
837
1008
  flushPendingRemovedClassNames() {
838
1009
  if (!this.pendingRemovedClassNames.size) return;
839
1010
  const classNames = [];
840
1011
  for (const className of this.pendingRemovedClassNames) if (!this.classCounts.has(className)) classNames.push(className);
841
1012
  this.pendingRemovedClassNames.clear();
842
- if (classNames.length) super.remove(...classNames);
1013
+ if (classNames.length) this.retainRemovedClassRules(classNames);
1014
+ }
1015
+ flushPendingAddedClassNames() {
1016
+ if (!this.pendingAddedClassNames.size) return;
1017
+ const classNames = [];
1018
+ for (const className of this.pendingAddedClassNames) if (this.classCounts.has(className)) classNames.push(className);
1019
+ this.pendingAddedClassNames.clear();
1020
+ if (classNames.length) this.ensureClassRules(...classNames);
843
1021
  }
844
1022
  handleMutationRecords(records) {
845
1023
  const deltaCounts = this.classTracker.collectMutations(records);
846
- const addedClassNames = [];
1024
+ const warmClassNames = [];
1025
+ const queuedClassNames = [];
847
1026
  const removedClassNames = [];
848
1027
  for (const [cls, change] of deltaCounts) {
849
1028
  const current = this.classCounts.get(cls) || 0;
850
1029
  const next = current + change;
851
1030
  if (next > 0) {
852
1031
  this.classCounts.set(cls, next);
853
- if (current === 0) addedClassNames.push(cls);
1032
+ if (current === 0) if (this.classUtilities.has(cls) || this.retainedClassNames.has(cls)) warmClassNames.push(cls);
1033
+ else queuedClassNames.push(cls);
854
1034
  } else {
855
1035
  this.classCounts.delete(cls);
856
1036
  removedClassNames.push(cls);
857
1037
  }
858
1038
  }
859
- if (addedClassNames.length) this.add(...addedClassNames);
860
- if (removedClassNames.length) this.queueRemovedClassNames(removedClassNames);
1039
+ if (warmClassNames.length) this.ensureClassRules(...warmClassNames);
1040
+ if (queuedClassNames.length) this.queueAddedClassNames(queuedClassNames);
1041
+ if (removedClassNames.length) {
1042
+ this.cancelPendingAddedClassNames(removedClassNames);
1043
+ this.queueRemovedClassNames(removedClassNames);
1044
+ }
861
1045
  debugRuntimeMutation(records, deltaCounts, this);
862
1046
  }
863
- add(...classNames) {
1047
+ ensureClassRules(...classNames) {
1048
+ this.cancelPendingAddedClassNames(classNames);
864
1049
  this.cancelPendingRemovedClassNames(classNames);
865
- return super.add(...classNames);
1050
+ this.cancelRetainedClassNames(classNames);
1051
+ return super.ensureClassRules(...classNames);
866
1052
  }
867
- remove(...classNames) {
1053
+ deleteClassRules(...classNames) {
1054
+ this.cancelPendingAddedClassNames(classNames);
868
1055
  this.cancelPendingRemovedClassNames(classNames);
869
- super.remove(...classNames);
1056
+ this.cancelRetainedClassNames(classNames);
1057
+ super.deleteClassRules(...classNames);
870
1058
  }
871
1059
  startMutationObserver() {
872
1060
  this.observer = new MutationObserver((records) => this.handleMutationRecords(records));
@@ -1119,7 +1307,9 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
1119
1307
  return result;
1120
1308
  }
1121
1309
  disconnect() {
1310
+ this.clearPendingAddedClassNames();
1122
1311
  this.clearPendingRemovedClassNames();
1312
+ this.clearRetainedClassRules();
1123
1313
  if (!this.observing) return;
1124
1314
  if (this.observer) {
1125
1315
  this.observer.disconnect();
@@ -1138,7 +1328,9 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
1138
1328
  return this;
1139
1329
  }
1140
1330
  refresh(manifest = this.manifest) {
1331
+ this.clearPendingAddedClassNames();
1141
1332
  this.clearPendingRemovedClassNames();
1333
+ this.clearRetainedClassRules();
1142
1334
  if (!this.observing || !this.style.sheet) return this;
1143
1335
  const cssRules = this.style.sheet.cssRules;
1144
1336
  for (let i = cssRules.length - 1; i >= 0; i--) this.style.sheet.deleteRule(i);
@@ -1148,7 +1340,7 @@ var CSSRuntime = class CSSRuntime extends MasterCSS {
1148
1340
  * 所以 refresh 過後 rules 可能會變多也可能會變少
1149
1341
  */
1150
1342
  this.classCounts.forEach((_, className) => {
1151
- this.add(className);
1343
+ this.ensureClassRules(className);
1152
1344
  });
1153
1345
  debugRuntimeRefreshed(this, manifest);
1154
1346
  return this;