@master/css-runtime 2.0.0-rc.63 → 2.0.0-rc.65

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.cjs CHANGED
@@ -40,12 +40,21 @@ class RuntimeLayer extends css.Layer {
40
40
  const insertedIndex = super.insert(rule, index);
41
41
  if (insertedIndex === undefined || !this.native) return;
42
42
  const insertRuleSafely = (text, position)=>{
43
- try {
44
- const insertedIndex = this.native.insertRule(text, position);
45
- return this.native.cssRules.item(insertedIndex);
46
- } catch (error) {
47
- console.error(error, rule);
48
- return;
43
+ // Checks if the rule is inserted in a native CSS rule with this.attach()
44
+ if (this.rules.length === 1) {
45
+ return this.native.cssRules.item(position);
46
+ } else {
47
+ try {
48
+ const insertedIndex = this.native.insertRule(text, position);
49
+ return this.native.cssRules.item(insertedIndex);
50
+ } catch (error) {
51
+ console.error(error, rule);
52
+ /**
53
+ * If the rule is invalid, remove it from the rules array.
54
+ * It's important to note that the rule may break the entire CSS runtime,
55
+ */ this.rules.splice(insertedIndex, 1);
56
+ return;
57
+ }
49
58
  }
50
59
  };
51
60
  if ('nodes' in rule) {
@@ -55,7 +64,7 @@ class RuntimeLayer extends css.Layer {
55
64
  if (node.native) currentIndex++;
56
65
  });
57
66
  } else {
58
- rule.native = insertRuleSafely(rule.text, index);
67
+ rule.native = insertRuleSafely(rule.text, insertedIndex);
59
68
  }
60
69
  return insertedIndex;
61
70
  }
@@ -110,20 +119,19 @@ class CSSRuntime extends css.MasterCSS {
110
119
  customConfig;
111
120
  static instances = new WeakMap();
112
121
  host;
113
- observing;
114
- progressive;
115
122
  container;
116
- observer;
117
123
  baseLayer;
118
124
  themeLayer;
119
125
  presetLayer;
120
126
  componentsLayer;
121
127
  generalLayer;
122
128
  classCounts;
129
+ observer;
130
+ progressive;
131
+ observing;
123
132
  constructor(root = document, customConfig = css.config){
124
- super(customConfig), this.root = root, this.customConfig = customConfig, this.observing = false, this.progressive = false, this.baseLayer = new RuntimeSyntaxLayer('base', this), this.themeLayer = new RuntimeLayer('theme', this), this.presetLayer = new RuntimeSyntaxLayer('preset', this), this.componentsLayer = new RuntimeSyntaxLayer('components', this), this.generalLayer = new RuntimeSyntaxLayer('general', this), this.classCounts = new Map();
125
- const rootConstructorName = root?.constructor.name;
126
- if (rootConstructorName === 'HTMLDocument' || rootConstructorName === 'Document') {
133
+ super(customConfig), this.root = root, this.customConfig = customConfig, this.baseLayer = new RuntimeSyntaxLayer('base', this), this.themeLayer = new RuntimeLayer('theme', this), this.presetLayer = new RuntimeSyntaxLayer('preset', this), this.componentsLayer = new RuntimeSyntaxLayer('components', this), this.generalLayer = new RuntimeSyntaxLayer('general', this), this.classCounts = new Map(), this.progressive = false, this.observing = false;
134
+ if (this.root instanceof Document || this.root instanceof HTMLDocument) {
127
135
  this.root.defaultView.globalThis.cssRuntime = this;
128
136
  this.container = this.root.head;
129
137
  this.host = this.root.documentElement;
@@ -142,36 +150,44 @@ class CSSRuntime extends css.MasterCSS {
142
150
  * @returns this
143
151
  */ observe() {
144
152
  if (this.observing) return this;
145
- // 1. Check if the stylesheet is progressive.
146
- if (this.root.styleSheets) for (const sheet of this.root.styleSheets){
147
- const { ownerNode } = sheet;
148
- if (ownerNode && ownerNode.id === 'master') {
149
- this.style = ownerNode;
150
- if (this.style.sheet?.cssRules.length) {
151
- // @ts-ignore
152
- this.progressive = true;
153
+ // Detect prerendered stylesheet
154
+ if (this.root.styleSheets) {
155
+ for (const sheet of this.root.styleSheets){
156
+ const { ownerNode } = sheet;
157
+ if (ownerNode instanceof HTMLStyleElement && ownerNode.id === 'master') {
158
+ this.style = ownerNode;
159
+ if (this.style.sheet?.cssRules.length) {
160
+ this.progressive = true;
161
+ }
162
+ break;
153
163
  }
154
- break;
155
164
  }
156
165
  }
157
- // 2. Count the classes in the host and its children.
166
+ // Prepare snapshot map
167
+ const elementClasses = new WeakMap();
168
+ // Initial scan and populate counts + snapshot
158
169
  const connectedNames = new Set();
159
170
  const increaseClassCount = (className)=>{
160
171
  const count = this.classCounts.get(className) || 0;
161
- if (!count) {
162
- connectedNames.add(className);
163
- }
172
+ if (!count) connectedNames.add(className);
164
173
  this.classCounts.set(className, count + 1);
165
174
  };
166
- this.host.classList.forEach(increaseClassCount);
167
- (this.root.constructor.name === 'HTMLDocument' ? this.host : this.container).querySelectorAll('[class]').forEach((element)=>element.classList.forEach(increaseClassCount));
168
- // 3. Hydrate the existing CSS rules or create a new one.
175
+ const rootEl = this.root instanceof Document || this.root instanceof HTMLDocument ? this.root : this.container;
176
+ const elementsWithClass = rootEl.querySelectorAll('[class]');
177
+ elementsWithClass.forEach((el)=>{
178
+ const clsList = el.classList;
179
+ if (clsList) {
180
+ el.classList.forEach(increaseClassCount);
181
+ }
182
+ elementClasses.set(el, new Set(clsList));
183
+ });
184
+ // Hydration or style creation
169
185
  if (this.progressive) {
170
186
  const hydrateResult = this.hydrate(this.style.sheet.cssRules);
171
- // Add the connected class names that are not hydrated yet.
172
- for (const eachConnectedName of connectedNames){
173
- if (hydrateResult.allSyntaxRules.find((rule)=>(rule.fixedClass || rule.name) === eachConnectedName)) continue;
174
- this.add(eachConnectedName);
187
+ for (const cls of connectedNames){
188
+ if (!hydrateResult.allSyntaxRules.find((r)=>(r.fixedClass || r.name) === cls)) {
189
+ this.add(cls);
190
+ }
175
191
  }
176
192
  } else {
177
193
  this.style = document.createElement('style');
@@ -179,125 +195,81 @@ class CSSRuntime extends css.MasterCSS {
179
195
  this.container.append(this.style);
180
196
  this.style.sheet.insertRule(this.layerStatementRule.text);
181
197
  this.layerStatementRule.native = this.style.sheet.cssRules.item(0);
182
- for (const eachConnectedName of connectedNames){
183
- this.add(eachConnectedName);
184
- }
198
+ connectedNames.forEach((cls)=>this.add(cls));
185
199
  }
186
- // @ts-expect-error readonly
187
- this.observer = new MutationObserver((mutationRecords)=>{
188
- const eachClassCounts = new Map();
189
- const targetFirstAttrMutationRecord = new Map();
190
- const updateClassCount = (classes, isAdding = false)=>{
191
- const usage = isAdding ? 1 : -1;
192
- classes.forEach((className)=>{
193
- eachClassCounts.set(className, (eachClassCounts.get(className) || 0) + usage);
194
- });
200
+ this.observer = new MutationObserver((records)=>{
201
+ const deltaCounts = new Map();
202
+ const nodeMap = new Map();
203
+ const attrRecords = new Set();
204
+ const visited = new WeakSet();
205
+ const updateDelta = (cls, delta)=>deltaCounts.set(cls, (deltaCounts.get(cls) || 0) + delta);
206
+ const diffAndSnapshot = (el)=>{
207
+ const prev = new Set(elementClasses.get(el) || []);
208
+ const next = new Set(el.classList);
209
+ for (const c of next)if (!prev.has(c)) updateDelta(c, 1);
210
+ for (const c of prev)if (!next.has(c)) updateDelta(c, -1);
211
+ elementClasses.set(el, next);
195
212
  };
196
- const connectedStatusMap = new Map();
197
- const disconnectedStatusMap = new Map();
198
- const recordStatus = (target, mutationRecord, map, adding)=>{
199
- const status = map.get(target);
200
- if (status) {
201
- status.change += adding ? 1 : -1;
202
- status.mutationRecord = mutationRecord;
203
- } else {
204
- map.set(target, {
205
- change: adding ? 1 : -1,
206
- mutationRecord
207
- });
208
- }
209
- for (const child of target.children){
210
- if ('classList' in child) {
211
- recordStatus(child, mutationRecord, map, adding);
212
- }
213
- }
213
+ const removeSnapshot = (el)=>{
214
+ for (const c of elementClasses.get(el) || [])updateDelta(c, -1);
215
+ elementClasses.delete(el);
214
216
  };
215
- mutationRecords.forEach((mutationRecord)=>{
216
- const target = mutationRecord.target;
217
- switch(mutationRecord.type){
218
- case 'attributes':
219
- if (!targetFirstAttrMutationRecord.has(target)) {
220
- targetFirstAttrMutationRecord.set(target, mutationRecord);
221
- }
222
- break;
223
- case 'childList':
224
- const targetStatusMap = target.isConnected ? connectedStatusMap : disconnectedStatusMap;
225
- mutationRecord.addedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, true));
226
- mutationRecord.removedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, false));
227
- break;
228
- }
229
- });
230
- const updateTarget = (target, adding)=>{
231
- const firstAttrMutationRecord = targetFirstAttrMutationRecord.get(target);
232
- if (firstAttrMutationRecord) {
233
- targetFirstAttrMutationRecord.delete(target);
217
+ for (const record of records){
218
+ if (record.type === 'childList') {
219
+ for (const node of record.addedNodes)if (node instanceof Element && node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) + 1);
220
+ for (const node of record.removedNodes)if (node instanceof Element && !node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) - 1);
221
+ } else if (record.type === 'attributes' && record.attributeName === 'class') {
222
+ attrRecords.add(record.target);
234
223
  }
235
- if (adding) {
236
- updateClassCount(target.classList, adding);
237
- } else {
238
- if (firstAttrMutationRecord) {
239
- updateClassCount(firstAttrMutationRecord.oldValue ? firstAttrMutationRecord.oldValue.split(/\s+/) : [], adding);
240
- } else {
241
- updateClassCount(target.classList, adding);
224
+ }
225
+ const traverseIncludingSelf = (el, fn, visited)=>{
226
+ if (!visited.has(el)) {
227
+ visited.add(el);
228
+ fn(el);
229
+ for (const child of el.children){
230
+ traverseIncludingSelf(child, fn, visited);
242
231
  }
243
- disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget)=>{
244
- if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
245
- updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0);
246
- }
247
- });
232
+ } else {
233
+ nodeMap.delete(el);
248
234
  }
249
235
  };
250
- connectedStatusMap.forEach(({ change }, target)=>change !== 0 && updateTarget(target, change > 0));
251
- targetFirstAttrMutationRecord.forEach((mutation, target)=>{
252
- if (!target.isConnected) return;
253
- const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : [];
254
- const newClassList = target.classList;
255
- const addedClasses = [];
256
- newClassList.forEach((c)=>{
257
- if (!oldClassList.includes(c)) addedClasses.push(c);
258
- });
259
- const removedClasses = oldClassList.filter((c)=>!newClassList.contains(c));
260
- if (addedClasses.length) {
261
- updateClassCount(addedClasses, true);
262
- }
263
- if (removedClasses.length) {
264
- updateClassCount(removedClasses, false);
236
+ for (const [node, count] of nodeMap){
237
+ if (count > 0) {
238
+ traverseIncludingSelf(node, diffAndSnapshot, visited);
239
+ } else if (count < 0 && !node.isConnected) {
240
+ traverseIncludingSelf(node, removeSnapshot, visited);
265
241
  }
266
- });
267
- /**
268
- * Merge the class usage changes into the current class usage map.
269
- */ eachClassCounts.forEach((countChange, className)=>{
270
- let currentCount = this.classCounts.get(className) || 0;
271
- let newCount = currentCount + countChange;
272
- if (newCount > 0) {
273
- this.classCounts.set(className, newCount);
274
- if (currentCount === 0) {
275
- this.add(className);
276
- }
242
+ }
243
+ for (const el of attrRecords){
244
+ if (!visited.has(el)) diffAndSnapshot(el);
245
+ }
246
+ for (const [cls, change] of deltaCounts){
247
+ const current = this.classCounts.get(cls) || 0;
248
+ const next = current + change;
249
+ if (next > 0) {
250
+ this.classCounts.set(cls, next);
251
+ if (current === 0) this.add(cls);
277
252
  } else {
278
- this.classCounts.delete(className);
279
- this.remove(className);
253
+ this.classCounts.delete(cls);
254
+ this.remove(cls);
280
255
  }
281
- });
256
+ }
282
257
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:mutated', {
283
- records: mutationRecords,
284
- classCounts: eachClassCounts,
258
+ records,
259
+ classCounts: deltaCounts,
285
260
  cssRuntime: this
286
261
  });
287
262
  });
288
263
  this.observer.observe(this.root, {
264
+ childList: true,
289
265
  attributes: true,
290
- attributeOldValue: true,
291
266
  attributeFilter: [
292
267
  'class'
293
268
  ],
294
- childList: true,
269
+ attributeOldValue: true,
295
270
  subtree: true
296
271
  });
297
- if (!this.progressive) {
298
- this.host.removeAttribute('hidden');
299
- }
300
- // @ts-ignore
272
+ if (!this.progressive) this.host.removeAttribute('hidden');
301
273
  this.observing = true;
302
274
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:observed', {
303
275
  cssRuntime: this
@@ -440,7 +412,6 @@ class CSSRuntime extends css.MasterCSS {
440
412
  if (!this.observing) return;
441
413
  if (this.observer) {
442
414
  this.observer.disconnect();
443
- // @ts-expect-error readonly
444
415
  this.observer = undefined;
445
416
  }
446
417
  // @ts-ignore