@master/css-runtime 2.0.0-rc.62 → 2.0.0-rc.64

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.
@@ -57,10 +57,36 @@ function registerClassCountDebugger() {
57
57
  });
58
58
  if (errored) {
59
59
  log.debug('Records:', records);
60
+ log.debug('Serialized Records:', serializeMutations(records));
60
61
  log.debug('Counts:', classCounts);
61
62
  }
62
63
  });
63
64
  }
65
+ function serializeMutations(mutations) {
66
+ return mutations.map((m)=>{
67
+ const target = m.target;
68
+ return {
69
+ type: m.type,
70
+ attributeName: m.attributeName,
71
+ oldValue: m.oldValue,
72
+ target: {
73
+ tagName: target.tagName,
74
+ id: target.id,
75
+ classList: Array.from(target.classList)
76
+ },
77
+ added: Array.from(m.addedNodes).filter((n)=>n.nodeType === 1).map((el)=>({
78
+ tagName: el.tagName,
79
+ id: el.id,
80
+ classList: Array.from(el.classList)
81
+ })),
82
+ removed: Array.from(m.removedNodes).filter((n)=>n.nodeType === 1).map((el)=>({
83
+ tagName: el.tagName,
84
+ id: el.id,
85
+ classList: Array.from(el.classList)
86
+ }))
87
+ };
88
+ });
89
+ }
64
90
 
65
91
  function registerLifecycleDebugger() {
66
92
  const hook = globalThis.__MASTER_CSS_DEVTOOLS_HOOK__;
@@ -139,6 +165,10 @@ class RuntimeLayer extends Layer {
139
165
  return this.native.cssRules.item(insertedIndex);
140
166
  } catch (error) {
141
167
  console.error(error, rule);
168
+ /**
169
+ * If the rule is invalid, remove it from the rules array.
170
+ * It's important to note that the rule may break the entire CSS runtime,
171
+ */ this.rules.splice(insertedIndex, 1);
142
172
  return;
143
173
  }
144
174
  };
@@ -204,20 +234,19 @@ class CSSRuntime extends MasterCSS {
204
234
  customConfig;
205
235
  static instances = new WeakMap();
206
236
  host;
207
- observing;
208
- progressive;
209
237
  container;
210
- observer;
211
238
  baseLayer;
212
239
  themeLayer;
213
240
  presetLayer;
214
241
  componentsLayer;
215
242
  generalLayer;
216
243
  classCounts;
244
+ observer;
245
+ progressive;
246
+ observing;
217
247
  constructor(root = document, customConfig = config){
218
- 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();
219
- const rootConstructorName = root?.constructor.name;
220
- if (rootConstructorName === 'HTMLDocument' || rootConstructorName === 'Document') {
248
+ 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;
249
+ if (this.root instanceof Document || this.root instanceof HTMLDocument) {
221
250
  this.root.defaultView.globalThis.cssRuntime = this;
222
251
  this.container = this.root.head;
223
252
  this.host = this.root.documentElement;
@@ -236,39 +265,46 @@ class CSSRuntime extends MasterCSS {
236
265
  * @returns this
237
266
  */ observe() {
238
267
  if (this.observing) return this;
239
- // 1. Check if the stylesheet is progressive.
240
- if (this.root.styleSheets) for (const sheet of this.root.styleSheets){
241
- const { ownerNode } = sheet;
242
- if (ownerNode && ownerNode.id === 'master') {
243
- this.style = ownerNode;
244
- if (this.style.sheet?.cssRules.length) {
245
- // @ts-ignore
246
- this.progressive = true;
268
+ // Detect prerendered stylesheet
269
+ if (this.root.styleSheets) {
270
+ for (const sheet of this.root.styleSheets){
271
+ const { ownerNode } = sheet;
272
+ if (ownerNode instanceof HTMLStyleElement && ownerNode.id === 'master') {
273
+ this.style = ownerNode;
274
+ if (this.style.sheet?.cssRules.length) {
275
+ this.progressive = true;
276
+ }
277
+ break;
247
278
  }
248
- break;
249
279
  }
250
280
  }
251
- // 2. Count the classes in the host and its children.
281
+ // Prepare snapshot map
282
+ const elementClasses = new WeakMap();
283
+ // Initial scan and populate counts + snapshot
252
284
  const connectedNames = new Set();
253
285
  const increaseClassCount = (className)=>{
254
286
  const count = this.classCounts.get(className) || 0;
255
- if (!count) {
256
- connectedNames.add(className);
257
- }
287
+ if (!count) connectedNames.add(className);
258
288
  this.classCounts.set(className, count + 1);
259
289
  };
260
- this.host.classList.forEach(increaseClassCount);
261
- (this.root.constructor.name === 'HTMLDocument' ? this.host : this.container).querySelectorAll('[class]').forEach((element)=>element.classList.forEach(increaseClassCount));
262
- // 3. Hydrate the existing CSS rules or create a new one.
290
+ const rootEl = this.root instanceof Document || this.root instanceof HTMLDocument ? this.root : this.container;
291
+ const elementsWithClass = rootEl.querySelectorAll('[class]');
292
+ elementsWithClass.forEach((el)=>{
293
+ const clsList = el.classList;
294
+ if (clsList) {
295
+ el.classList.forEach(increaseClassCount);
296
+ }
297
+ elementClasses.set(el, new Set(clsList));
298
+ });
299
+ // Hydration or style creation
263
300
  if (this.progressive) {
264
301
  const hydrateResult = this.hydrate(this.style.sheet.cssRules);
265
- // Add the connected class names that are not hydrated yet.
266
- for (const eachConnectedName of connectedNames){
267
- if (hydrateResult.allSyntaxRules.find((rule)=>(rule.fixedClass || rule.name) === eachConnectedName)) continue;
268
- this.add(eachConnectedName);
269
- {
270
- // Basically, the style#master should have all the prerendered CSS rules that are connected in the DOM.
271
- console.debug(`The class \`${eachConnectedName}\` was added via script before calling hydrate, or the corresponding CSS rule were not properly pre-rendered.`);
302
+ for (const cls of connectedNames){
303
+ if (!hydrateResult.allSyntaxRules.find((r)=>(r.fixedClass || r.name) === cls)) {
304
+ this.add(cls);
305
+ {
306
+ console.debug(`Missing prerendered rule for class \`${cls}\``);
307
+ }
272
308
  }
273
309
  }
274
310
  } else {
@@ -277,125 +313,81 @@ class CSSRuntime extends MasterCSS {
277
313
  this.container.append(this.style);
278
314
  this.style.sheet.insertRule(this.layerStatementRule.text);
279
315
  this.layerStatementRule.native = this.style.sheet.cssRules.item(0);
280
- for (const eachConnectedName of connectedNames){
281
- this.add(eachConnectedName);
282
- }
316
+ connectedNames.forEach((cls)=>this.add(cls));
283
317
  }
284
- // @ts-expect-error readonly
285
- this.observer = new MutationObserver((mutationRecords)=>{
286
- const eachClassCounts = new Map();
287
- const targetFirstAttrMutationRecord = new Map();
288
- const updateClassCount = (classes, isAdding = false)=>{
289
- const usage = isAdding ? 1 : -1;
290
- classes.forEach((className)=>{
291
- eachClassCounts.set(className, (eachClassCounts.get(className) || 0) + usage);
292
- });
318
+ this.observer = new MutationObserver((records)=>{
319
+ const deltaCounts = new Map();
320
+ const nodeMap = new Map();
321
+ const attrRecords = new Set();
322
+ const visited = new WeakSet();
323
+ const updateDelta = (cls, delta)=>deltaCounts.set(cls, (deltaCounts.get(cls) || 0) + delta);
324
+ const diffAndSnapshot = (el)=>{
325
+ const prev = new Set(elementClasses.get(el) || []);
326
+ const next = new Set(el.classList);
327
+ for (const c of next)if (!prev.has(c)) updateDelta(c, 1);
328
+ for (const c of prev)if (!next.has(c)) updateDelta(c, -1);
329
+ elementClasses.set(el, next);
293
330
  };
294
- const connectedStatusMap = new Map();
295
- const disconnectedStatusMap = new Map();
296
- const recordStatus = (target, mutationRecord, map, adding)=>{
297
- const status = map.get(target);
298
- if (status) {
299
- status.change += adding ? 1 : -1;
300
- status.mutationRecord = mutationRecord;
301
- } else {
302
- map.set(target, {
303
- change: adding ? 1 : -1,
304
- mutationRecord
305
- });
306
- }
307
- for (const child of target.children){
308
- if ('classList' in child) {
309
- recordStatus(child, mutationRecord, map, adding);
310
- }
311
- }
331
+ const removeSnapshot = (el)=>{
332
+ for (const c of elementClasses.get(el) || [])updateDelta(c, -1);
333
+ elementClasses.delete(el);
312
334
  };
313
- mutationRecords.forEach((mutationRecord)=>{
314
- const target = mutationRecord.target;
315
- switch(mutationRecord.type){
316
- case 'attributes':
317
- if (!targetFirstAttrMutationRecord.has(target)) {
318
- targetFirstAttrMutationRecord.set(target, mutationRecord);
319
- }
320
- break;
321
- case 'childList':
322
- const targetStatusMap = target.isConnected ? connectedStatusMap : disconnectedStatusMap;
323
- mutationRecord.addedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, true));
324
- mutationRecord.removedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, false));
325
- break;
335
+ for (const record of records){
336
+ if (record.type === 'childList') {
337
+ for (const node of record.addedNodes)if (node instanceof Element && node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) + 1);
338
+ for (const node of record.removedNodes)if (node instanceof Element && !node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) - 1);
339
+ } else if (record.type === 'attributes' && record.attributeName === 'class') {
340
+ attrRecords.add(record.target);
326
341
  }
327
- });
328
- const updateTarget = (target, adding)=>{
329
- const firstAttrMutationRecord = targetFirstAttrMutationRecord.get(target);
330
- if (firstAttrMutationRecord) {
331
- targetFirstAttrMutationRecord.delete(target);
332
- }
333
- if (adding) {
334
- updateClassCount(target.classList, adding);
335
- } else {
336
- if (firstAttrMutationRecord) {
337
- updateClassCount(firstAttrMutationRecord.oldValue ? firstAttrMutationRecord.oldValue.split(/\s+/) : [], adding);
338
- } else {
339
- updateClassCount(target.classList, adding);
342
+ }
343
+ const traverseIncludingSelf = (el, fn, visited)=>{
344
+ if (!visited.has(el)) {
345
+ visited.add(el);
346
+ fn(el);
347
+ for (const child of el.children){
348
+ traverseIncludingSelf(child, fn, visited);
340
349
  }
341
- disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget)=>{
342
- if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
343
- updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0);
344
- }
345
- });
350
+ } else {
351
+ nodeMap.delete(el);
346
352
  }
347
353
  };
348
- connectedStatusMap.forEach(({ change }, target)=>change !== 0 && updateTarget(target, change > 0));
349
- targetFirstAttrMutationRecord.forEach((mutation, target)=>{
350
- if (!target.isConnected) return;
351
- const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : [];
352
- const newClassList = target.classList;
353
- const addedClasses = [];
354
- newClassList.forEach((c)=>{
355
- if (!oldClassList.includes(c)) addedClasses.push(c);
356
- });
357
- const removedClasses = oldClassList.filter((c)=>!newClassList.contains(c));
358
- if (addedClasses.length) {
359
- updateClassCount(addedClasses, true);
360
- }
361
- if (removedClasses.length) {
362
- updateClassCount(removedClasses, false);
354
+ for (const [node, count] of nodeMap){
355
+ if (count > 0) {
356
+ traverseIncludingSelf(node, diffAndSnapshot, visited);
357
+ } else if (count < 0 && !node.isConnected) {
358
+ traverseIncludingSelf(node, removeSnapshot, visited);
363
359
  }
364
- });
365
- /**
366
- * Merge the class usage changes into the current class usage map.
367
- */ eachClassCounts.forEach((countChange, className)=>{
368
- let currentCount = this.classCounts.get(className) || 0;
369
- let newCount = currentCount + countChange;
370
- if (newCount > 0) {
371
- this.classCounts.set(className, newCount);
372
- if (currentCount === 0) {
373
- this.add(className);
374
- }
360
+ }
361
+ for (const el of attrRecords){
362
+ if (!visited.has(el)) diffAndSnapshot(el);
363
+ }
364
+ for (const [cls, change] of deltaCounts){
365
+ const current = this.classCounts.get(cls) || 0;
366
+ const next = current + change;
367
+ if (next > 0) {
368
+ this.classCounts.set(cls, next);
369
+ if (current === 0) this.add(cls);
375
370
  } else {
376
- this.classCounts.delete(className);
377
- this.remove(className);
371
+ this.classCounts.delete(cls);
372
+ this.remove(cls);
378
373
  }
379
- });
374
+ }
380
375
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:mutated', {
381
- records: mutationRecords,
382
- classCounts: eachClassCounts,
376
+ records,
377
+ classCounts: deltaCounts,
383
378
  cssRuntime: this
384
379
  });
385
380
  });
386
381
  this.observer.observe(this.root, {
382
+ childList: true,
387
383
  attributes: true,
388
- attributeOldValue: true,
389
384
  attributeFilter: [
390
385
  'class'
391
386
  ],
392
- childList: true,
387
+ attributeOldValue: true,
393
388
  subtree: true
394
389
  });
395
- if (!this.progressive) {
396
- this.host.removeAttribute('hidden');
397
- }
398
- // @ts-ignore
390
+ if (!this.progressive) this.host.removeAttribute('hidden');
399
391
  this.observing = true;
400
392
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:observed', {
401
393
  cssRuntime: this
@@ -541,7 +533,6 @@ class CSSRuntime extends MasterCSS {
541
533
  if (!this.observing) return;
542
534
  if (this.observer) {
543
535
  this.observer.disconnect();
544
- // @ts-expect-error readonly
545
536
  this.observer = undefined;
546
537
  }
547
538
  // @ts-ignore
package/dist/index.mjs CHANGED
@@ -41,6 +41,10 @@ class RuntimeLayer extends Layer {
41
41
  return this.native.cssRules.item(insertedIndex);
42
42
  } catch (error) {
43
43
  console.error(error, rule);
44
+ /**
45
+ * If the rule is invalid, remove it from the rules array.
46
+ * It's important to note that the rule may break the entire CSS runtime,
47
+ */ this.rules.splice(insertedIndex, 1);
44
48
  return;
45
49
  }
46
50
  };
@@ -106,20 +110,19 @@ class CSSRuntime extends MasterCSS {
106
110
  customConfig;
107
111
  static instances = new WeakMap();
108
112
  host;
109
- observing;
110
- progressive;
111
113
  container;
112
- observer;
113
114
  baseLayer;
114
115
  themeLayer;
115
116
  presetLayer;
116
117
  componentsLayer;
117
118
  generalLayer;
118
119
  classCounts;
120
+ observer;
121
+ progressive;
122
+ observing;
119
123
  constructor(root = document, customConfig = config){
120
- 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();
121
- const rootConstructorName = root?.constructor.name;
122
- if (rootConstructorName === 'HTMLDocument' || rootConstructorName === 'Document') {
124
+ 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;
125
+ if (this.root instanceof Document || this.root instanceof HTMLDocument) {
123
126
  this.root.defaultView.globalThis.cssRuntime = this;
124
127
  this.container = this.root.head;
125
128
  this.host = this.root.documentElement;
@@ -138,36 +141,44 @@ class CSSRuntime extends MasterCSS {
138
141
  * @returns this
139
142
  */ observe() {
140
143
  if (this.observing) return this;
141
- // 1. Check if the stylesheet is progressive.
142
- if (this.root.styleSheets) for (const sheet of this.root.styleSheets){
143
- const { ownerNode } = sheet;
144
- if (ownerNode && ownerNode.id === 'master') {
145
- this.style = ownerNode;
146
- if (this.style.sheet?.cssRules.length) {
147
- // @ts-ignore
148
- this.progressive = true;
144
+ // Detect prerendered stylesheet
145
+ if (this.root.styleSheets) {
146
+ for (const sheet of this.root.styleSheets){
147
+ const { ownerNode } = sheet;
148
+ if (ownerNode instanceof HTMLStyleElement && ownerNode.id === 'master') {
149
+ this.style = ownerNode;
150
+ if (this.style.sheet?.cssRules.length) {
151
+ this.progressive = true;
152
+ }
153
+ break;
149
154
  }
150
- break;
151
155
  }
152
156
  }
153
- // 2. Count the classes in the host and its children.
157
+ // Prepare snapshot map
158
+ const elementClasses = new WeakMap();
159
+ // Initial scan and populate counts + snapshot
154
160
  const connectedNames = new Set();
155
161
  const increaseClassCount = (className)=>{
156
162
  const count = this.classCounts.get(className) || 0;
157
- if (!count) {
158
- connectedNames.add(className);
159
- }
163
+ if (!count) connectedNames.add(className);
160
164
  this.classCounts.set(className, count + 1);
161
165
  };
162
- this.host.classList.forEach(increaseClassCount);
163
- (this.root.constructor.name === 'HTMLDocument' ? this.host : this.container).querySelectorAll('[class]').forEach((element)=>element.classList.forEach(increaseClassCount));
164
- // 3. Hydrate the existing CSS rules or create a new one.
166
+ const rootEl = this.root instanceof Document || this.root instanceof HTMLDocument ? this.root : this.container;
167
+ const elementsWithClass = rootEl.querySelectorAll('[class]');
168
+ elementsWithClass.forEach((el)=>{
169
+ const clsList = el.classList;
170
+ if (clsList) {
171
+ el.classList.forEach(increaseClassCount);
172
+ }
173
+ elementClasses.set(el, new Set(clsList));
174
+ });
175
+ // Hydration or style creation
165
176
  if (this.progressive) {
166
177
  const hydrateResult = this.hydrate(this.style.sheet.cssRules);
167
- // Add the connected class names that are not hydrated yet.
168
- for (const eachConnectedName of connectedNames){
169
- if (hydrateResult.allSyntaxRules.find((rule)=>(rule.fixedClass || rule.name) === eachConnectedName)) continue;
170
- this.add(eachConnectedName);
178
+ for (const cls of connectedNames){
179
+ if (!hydrateResult.allSyntaxRules.find((r)=>(r.fixedClass || r.name) === cls)) {
180
+ this.add(cls);
181
+ }
171
182
  }
172
183
  } else {
173
184
  this.style = document.createElement('style');
@@ -175,125 +186,81 @@ class CSSRuntime extends MasterCSS {
175
186
  this.container.append(this.style);
176
187
  this.style.sheet.insertRule(this.layerStatementRule.text);
177
188
  this.layerStatementRule.native = this.style.sheet.cssRules.item(0);
178
- for (const eachConnectedName of connectedNames){
179
- this.add(eachConnectedName);
180
- }
189
+ connectedNames.forEach((cls)=>this.add(cls));
181
190
  }
182
- // @ts-expect-error readonly
183
- this.observer = new MutationObserver((mutationRecords)=>{
184
- const eachClassCounts = new Map();
185
- const targetFirstAttrMutationRecord = new Map();
186
- const updateClassCount = (classes, isAdding = false)=>{
187
- const usage = isAdding ? 1 : -1;
188
- classes.forEach((className)=>{
189
- eachClassCounts.set(className, (eachClassCounts.get(className) || 0) + usage);
190
- });
191
+ this.observer = new MutationObserver((records)=>{
192
+ const deltaCounts = new Map();
193
+ const nodeMap = new Map();
194
+ const attrRecords = new Set();
195
+ const visited = new WeakSet();
196
+ const updateDelta = (cls, delta)=>deltaCounts.set(cls, (deltaCounts.get(cls) || 0) + delta);
197
+ const diffAndSnapshot = (el)=>{
198
+ const prev = new Set(elementClasses.get(el) || []);
199
+ const next = new Set(el.classList);
200
+ for (const c of next)if (!prev.has(c)) updateDelta(c, 1);
201
+ for (const c of prev)if (!next.has(c)) updateDelta(c, -1);
202
+ elementClasses.set(el, next);
191
203
  };
192
- const connectedStatusMap = new Map();
193
- const disconnectedStatusMap = new Map();
194
- const recordStatus = (target, mutationRecord, map, adding)=>{
195
- const status = map.get(target);
196
- if (status) {
197
- status.change += adding ? 1 : -1;
198
- status.mutationRecord = mutationRecord;
199
- } else {
200
- map.set(target, {
201
- change: adding ? 1 : -1,
202
- mutationRecord
203
- });
204
- }
205
- for (const child of target.children){
206
- if ('classList' in child) {
207
- recordStatus(child, mutationRecord, map, adding);
208
- }
209
- }
204
+ const removeSnapshot = (el)=>{
205
+ for (const c of elementClasses.get(el) || [])updateDelta(c, -1);
206
+ elementClasses.delete(el);
210
207
  };
211
- mutationRecords.forEach((mutationRecord)=>{
212
- const target = mutationRecord.target;
213
- switch(mutationRecord.type){
214
- case 'attributes':
215
- if (!targetFirstAttrMutationRecord.has(target)) {
216
- targetFirstAttrMutationRecord.set(target, mutationRecord);
217
- }
218
- break;
219
- case 'childList':
220
- const targetStatusMap = target.isConnected ? connectedStatusMap : disconnectedStatusMap;
221
- mutationRecord.addedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, true));
222
- mutationRecord.removedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, false));
223
- break;
224
- }
225
- });
226
- const updateTarget = (target, adding)=>{
227
- const firstAttrMutationRecord = targetFirstAttrMutationRecord.get(target);
228
- if (firstAttrMutationRecord) {
229
- targetFirstAttrMutationRecord.delete(target);
208
+ for (const record of records){
209
+ if (record.type === 'childList') {
210
+ for (const node of record.addedNodes)if (node instanceof Element && node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) + 1);
211
+ for (const node of record.removedNodes)if (node instanceof Element && !node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) - 1);
212
+ } else if (record.type === 'attributes' && record.attributeName === 'class') {
213
+ attrRecords.add(record.target);
230
214
  }
231
- if (adding) {
232
- updateClassCount(target.classList, adding);
233
- } else {
234
- if (firstAttrMutationRecord) {
235
- updateClassCount(firstAttrMutationRecord.oldValue ? firstAttrMutationRecord.oldValue.split(/\s+/) : [], adding);
236
- } else {
237
- updateClassCount(target.classList, adding);
215
+ }
216
+ const traverseIncludingSelf = (el, fn, visited)=>{
217
+ if (!visited.has(el)) {
218
+ visited.add(el);
219
+ fn(el);
220
+ for (const child of el.children){
221
+ traverseIncludingSelf(child, fn, visited);
238
222
  }
239
- disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget)=>{
240
- if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
241
- updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0);
242
- }
243
- });
223
+ } else {
224
+ nodeMap.delete(el);
244
225
  }
245
226
  };
246
- connectedStatusMap.forEach(({ change }, target)=>change !== 0 && updateTarget(target, change > 0));
247
- targetFirstAttrMutationRecord.forEach((mutation, target)=>{
248
- if (!target.isConnected) return;
249
- const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : [];
250
- const newClassList = target.classList;
251
- const addedClasses = [];
252
- newClassList.forEach((c)=>{
253
- if (!oldClassList.includes(c)) addedClasses.push(c);
254
- });
255
- const removedClasses = oldClassList.filter((c)=>!newClassList.contains(c));
256
- if (addedClasses.length) {
257
- updateClassCount(addedClasses, true);
227
+ for (const [node, count] of nodeMap){
228
+ if (count > 0) {
229
+ traverseIncludingSelf(node, diffAndSnapshot, visited);
230
+ } else if (count < 0 && !node.isConnected) {
231
+ traverseIncludingSelf(node, removeSnapshot, visited);
258
232
  }
259
- if (removedClasses.length) {
260
- updateClassCount(removedClasses, false);
261
- }
262
- });
263
- /**
264
- * Merge the class usage changes into the current class usage map.
265
- */ eachClassCounts.forEach((countChange, className)=>{
266
- let currentCount = this.classCounts.get(className) || 0;
267
- let newCount = currentCount + countChange;
268
- if (newCount > 0) {
269
- this.classCounts.set(className, newCount);
270
- if (currentCount === 0) {
271
- this.add(className);
272
- }
233
+ }
234
+ for (const el of attrRecords){
235
+ if (!visited.has(el)) diffAndSnapshot(el);
236
+ }
237
+ for (const [cls, change] of deltaCounts){
238
+ const current = this.classCounts.get(cls) || 0;
239
+ const next = current + change;
240
+ if (next > 0) {
241
+ this.classCounts.set(cls, next);
242
+ if (current === 0) this.add(cls);
273
243
  } else {
274
- this.classCounts.delete(className);
275
- this.remove(className);
244
+ this.classCounts.delete(cls);
245
+ this.remove(cls);
276
246
  }
277
- });
247
+ }
278
248
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:mutated', {
279
- records: mutationRecords,
280
- classCounts: eachClassCounts,
249
+ records,
250
+ classCounts: deltaCounts,
281
251
  cssRuntime: this
282
252
  });
283
253
  });
284
254
  this.observer.observe(this.root, {
255
+ childList: true,
285
256
  attributes: true,
286
- attributeOldValue: true,
287
257
  attributeFilter: [
288
258
  'class'
289
259
  ],
290
- childList: true,
260
+ attributeOldValue: true,
291
261
  subtree: true
292
262
  });
293
- if (!this.progressive) {
294
- this.host.removeAttribute('hidden');
295
- }
296
- // @ts-ignore
263
+ if (!this.progressive) this.host.removeAttribute('hidden');
297
264
  this.observing = true;
298
265
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:observed', {
299
266
  cssRuntime: this
@@ -436,7 +403,6 @@ class CSSRuntime extends MasterCSS {
436
403
  if (!this.observing) return;
437
404
  if (this.observer) {
438
405
  this.observer.disconnect();
439
- // @ts-expect-error readonly
440
406
  this.observer = undefined;
441
407
  }
442
408
  // @ts-ignore
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@master/css-runtime","type":"module","scripts":{"build":"techor build","dev":"pnpm build --watch","e2e":"playwright test","e2e:watch":"nodemon -e js,mjs,ts,html,css --watch dist --watch e2e --exec playwright test","type-check":"tsc --noEmit","lint":"eslint"},"license":"MIT","description":"Run Master CSS right in the browser","author":"Aoyue Design LLC.","funding":"https://rc.css.master.co/sponsor","homepage":"https://css.master.co","bugs":{"url":"https://github.com/master-co/css/issues"},"repository":{"type":"git","url":"https://github.com/master-co/css.git","directory":"packages/runtime"},"keywords":["utility","css","lightweight","mastercss","master","runtime","cdn"],"sideEffects":false,"main":"./dist/index.cjs","jsnext:main":"./dist/index.mjs","esnext":"./dist/index.mjs","module":"./dist/index.mjs","browser":"./dist/global.min.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":{"development":"./dist/index.development.cjs","default":"./dist/index.cjs"},"import":{"development":"./dist/index.development.mjs","default":"./dist/index.mjs"}}},"files":["dist"],"publishConfig":{"access":"public","provenance":true},"dependencies":{"@master/css":"^2.0.0-rc.62","@master/css-devtools-hook":"^2.0.0-rc.62"},"devDependencies":{"@playwright/test":"^1.49.1","shared":"^2.0.0-rc.62"},"version":"2.0.0-rc.62"}
1
+ {"name":"@master/css-runtime","type":"module","scripts":{"build":"techor build","dev":"pnpm build --watch","e2e":"playwright test","e2e:watch":"nodemon -e js,mjs,ts,html,css --watch dist --watch e2e --exec playwright test","type-check":"tsc --noEmit","lint":"eslint"},"license":"MIT","description":"Run Master CSS right in the browser","author":"Aoyue Design LLC.","funding":"https://rc.css.master.co/sponsor","homepage":"https://css.master.co","bugs":{"url":"https://github.com/master-co/css/issues"},"repository":{"type":"git","url":"https://github.com/master-co/css.git","directory":"packages/runtime"},"keywords":["utility","css","lightweight","mastercss","master","runtime","cdn"],"sideEffects":false,"main":"./dist/index.cjs","jsnext:main":"./dist/index.mjs","esnext":"./dist/index.mjs","module":"./dist/index.mjs","browser":"./dist/global.min.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":{"development":"./dist/index.development.cjs","default":"./dist/index.cjs"},"import":{"development":"./dist/index.development.mjs","default":"./dist/index.mjs"}}},"files":["dist"],"publishConfig":{"access":"public","provenance":true},"dependencies":{"@master/css":"^2.0.0-rc.64","@master/css-devtools-hook":"^2.0.0-rc.64"},"devDependencies":{"@playwright/test":"^1.49.1","shared":"^2.0.0-rc.64"},"version":"2.0.0-rc.64"}