@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.
@@ -59,10 +59,36 @@ function registerClassCountDebugger() {
59
59
  });
60
60
  if (errored) {
61
61
  log.debug('Records:', records);
62
+ log.debug('Serialized Records:', serializeMutations(records));
62
63
  log.debug('Counts:', classCounts);
63
64
  }
64
65
  });
65
66
  }
67
+ function serializeMutations(mutations) {
68
+ return mutations.map((m)=>{
69
+ const target = m.target;
70
+ return {
71
+ type: m.type,
72
+ attributeName: m.attributeName,
73
+ oldValue: m.oldValue,
74
+ target: {
75
+ tagName: target.tagName,
76
+ id: target.id,
77
+ classList: Array.from(target.classList)
78
+ },
79
+ added: Array.from(m.addedNodes).filter((n)=>n.nodeType === 1).map((el)=>({
80
+ tagName: el.tagName,
81
+ id: el.id,
82
+ classList: Array.from(el.classList)
83
+ })),
84
+ removed: Array.from(m.removedNodes).filter((n)=>n.nodeType === 1).map((el)=>({
85
+ tagName: el.tagName,
86
+ id: el.id,
87
+ classList: Array.from(el.classList)
88
+ }))
89
+ };
90
+ });
91
+ }
66
92
 
67
93
  function registerLifecycleDebugger() {
68
94
  const hook = globalThis.__MASTER_CSS_DEVTOOLS_HOOK__;
@@ -136,12 +162,21 @@ class RuntimeLayer extends css.Layer {
136
162
  const insertedIndex = super.insert(rule, index);
137
163
  if (insertedIndex === undefined || !this.native) return;
138
164
  const insertRuleSafely = (text, position)=>{
139
- try {
140
- const insertedIndex = this.native.insertRule(text, position);
141
- return this.native.cssRules.item(insertedIndex);
142
- } catch (error) {
143
- console.error(error, rule);
144
- return;
165
+ // Checks if the rule is inserted in a native CSS rule with this.attach()
166
+ if (this.rules.length === 1) {
167
+ return this.native.cssRules.item(position);
168
+ } else {
169
+ try {
170
+ const insertedIndex = this.native.insertRule(text, position);
171
+ return this.native.cssRules.item(insertedIndex);
172
+ } catch (error) {
173
+ console.error(error, rule);
174
+ /**
175
+ * If the rule is invalid, remove it from the rules array.
176
+ * It's important to note that the rule may break the entire CSS runtime,
177
+ */ this.rules.splice(insertedIndex, 1);
178
+ return;
179
+ }
145
180
  }
146
181
  };
147
182
  if ('nodes' in rule) {
@@ -151,7 +186,7 @@ class RuntimeLayer extends css.Layer {
151
186
  if (node.native) currentIndex++;
152
187
  });
153
188
  } else {
154
- rule.native = insertRuleSafely(rule.text, index);
189
+ rule.native = insertRuleSafely(rule.text, insertedIndex);
155
190
  }
156
191
  return insertedIndex;
157
192
  }
@@ -206,20 +241,19 @@ class CSSRuntime extends css.MasterCSS {
206
241
  customConfig;
207
242
  static instances = new WeakMap();
208
243
  host;
209
- observing;
210
- progressive;
211
244
  container;
212
- observer;
213
245
  baseLayer;
214
246
  themeLayer;
215
247
  presetLayer;
216
248
  componentsLayer;
217
249
  generalLayer;
218
250
  classCounts;
251
+ observer;
252
+ progressive;
253
+ observing;
219
254
  constructor(root = document, customConfig = css.config){
220
- 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();
221
- const rootConstructorName = root?.constructor.name;
222
- if (rootConstructorName === 'HTMLDocument' || rootConstructorName === 'Document') {
255
+ 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;
256
+ if (this.root instanceof Document || this.root instanceof HTMLDocument) {
223
257
  this.root.defaultView.globalThis.cssRuntime = this;
224
258
  this.container = this.root.head;
225
259
  this.host = this.root.documentElement;
@@ -238,39 +272,46 @@ class CSSRuntime extends css.MasterCSS {
238
272
  * @returns this
239
273
  */ observe() {
240
274
  if (this.observing) return this;
241
- // 1. Check if the stylesheet is progressive.
242
- if (this.root.styleSheets) for (const sheet of this.root.styleSheets){
243
- const { ownerNode } = sheet;
244
- if (ownerNode && ownerNode.id === 'master') {
245
- this.style = ownerNode;
246
- if (this.style.sheet?.cssRules.length) {
247
- // @ts-ignore
248
- this.progressive = true;
275
+ // Detect prerendered stylesheet
276
+ if (this.root.styleSheets) {
277
+ for (const sheet of this.root.styleSheets){
278
+ const { ownerNode } = sheet;
279
+ if (ownerNode instanceof HTMLStyleElement && ownerNode.id === 'master') {
280
+ this.style = ownerNode;
281
+ if (this.style.sheet?.cssRules.length) {
282
+ this.progressive = true;
283
+ }
284
+ break;
249
285
  }
250
- break;
251
286
  }
252
287
  }
253
- // 2. Count the classes in the host and its children.
288
+ // Prepare snapshot map
289
+ const elementClasses = new WeakMap();
290
+ // Initial scan and populate counts + snapshot
254
291
  const connectedNames = new Set();
255
292
  const increaseClassCount = (className)=>{
256
293
  const count = this.classCounts.get(className) || 0;
257
- if (!count) {
258
- connectedNames.add(className);
259
- }
294
+ if (!count) connectedNames.add(className);
260
295
  this.classCounts.set(className, count + 1);
261
296
  };
262
- this.host.classList.forEach(increaseClassCount);
263
- (this.root.constructor.name === 'HTMLDocument' ? this.host : this.container).querySelectorAll('[class]').forEach((element)=>element.classList.forEach(increaseClassCount));
264
- // 3. Hydrate the existing CSS rules or create a new one.
297
+ const rootEl = this.root instanceof Document || this.root instanceof HTMLDocument ? this.root : this.container;
298
+ const elementsWithClass = rootEl.querySelectorAll('[class]');
299
+ elementsWithClass.forEach((el)=>{
300
+ const clsList = el.classList;
301
+ if (clsList) {
302
+ el.classList.forEach(increaseClassCount);
303
+ }
304
+ elementClasses.set(el, new Set(clsList));
305
+ });
306
+ // Hydration or style creation
265
307
  if (this.progressive) {
266
308
  const hydrateResult = this.hydrate(this.style.sheet.cssRules);
267
- // Add the connected class names that are not hydrated yet.
268
- for (const eachConnectedName of connectedNames){
269
- if (hydrateResult.allSyntaxRules.find((rule)=>(rule.fixedClass || rule.name) === eachConnectedName)) continue;
270
- this.add(eachConnectedName);
271
- {
272
- // Basically, the style#master should have all the prerendered CSS rules that are connected in the DOM.
273
- console.debug(`The class \`${eachConnectedName}\` was added via script before calling hydrate, or the corresponding CSS rule were not properly pre-rendered.`);
309
+ for (const cls of connectedNames){
310
+ if (!hydrateResult.allSyntaxRules.find((r)=>(r.fixedClass || r.name) === cls)) {
311
+ this.add(cls);
312
+ {
313
+ console.debug(`Missing prerendered rule for class \`${cls}\``);
314
+ }
274
315
  }
275
316
  }
276
317
  } else {
@@ -279,125 +320,81 @@ class CSSRuntime extends css.MasterCSS {
279
320
  this.container.append(this.style);
280
321
  this.style.sheet.insertRule(this.layerStatementRule.text);
281
322
  this.layerStatementRule.native = this.style.sheet.cssRules.item(0);
282
- for (const eachConnectedName of connectedNames){
283
- this.add(eachConnectedName);
284
- }
323
+ connectedNames.forEach((cls)=>this.add(cls));
285
324
  }
286
- // @ts-expect-error readonly
287
- this.observer = new MutationObserver((mutationRecords)=>{
288
- const eachClassCounts = new Map();
289
- const targetFirstAttrMutationRecord = new Map();
290
- const updateClassCount = (classes, isAdding = false)=>{
291
- const usage = isAdding ? 1 : -1;
292
- classes.forEach((className)=>{
293
- eachClassCounts.set(className, (eachClassCounts.get(className) || 0) + usage);
294
- });
325
+ this.observer = new MutationObserver((records)=>{
326
+ const deltaCounts = new Map();
327
+ const nodeMap = new Map();
328
+ const attrRecords = new Set();
329
+ const visited = new WeakSet();
330
+ const updateDelta = (cls, delta)=>deltaCounts.set(cls, (deltaCounts.get(cls) || 0) + delta);
331
+ const diffAndSnapshot = (el)=>{
332
+ const prev = new Set(elementClasses.get(el) || []);
333
+ const next = new Set(el.classList);
334
+ for (const c of next)if (!prev.has(c)) updateDelta(c, 1);
335
+ for (const c of prev)if (!next.has(c)) updateDelta(c, -1);
336
+ elementClasses.set(el, next);
295
337
  };
296
- const connectedStatusMap = new Map();
297
- const disconnectedStatusMap = new Map();
298
- const recordStatus = (target, mutationRecord, map, adding)=>{
299
- const status = map.get(target);
300
- if (status) {
301
- status.change += adding ? 1 : -1;
302
- status.mutationRecord = mutationRecord;
303
- } else {
304
- map.set(target, {
305
- change: adding ? 1 : -1,
306
- mutationRecord
307
- });
308
- }
309
- for (const child of target.children){
310
- if ('classList' in child) {
311
- recordStatus(child, mutationRecord, map, adding);
312
- }
313
- }
338
+ const removeSnapshot = (el)=>{
339
+ for (const c of elementClasses.get(el) || [])updateDelta(c, -1);
340
+ elementClasses.delete(el);
314
341
  };
315
- mutationRecords.forEach((mutationRecord)=>{
316
- const target = mutationRecord.target;
317
- switch(mutationRecord.type){
318
- case 'attributes':
319
- if (!targetFirstAttrMutationRecord.has(target)) {
320
- targetFirstAttrMutationRecord.set(target, mutationRecord);
321
- }
322
- break;
323
- case 'childList':
324
- const targetStatusMap = target.isConnected ? connectedStatusMap : disconnectedStatusMap;
325
- mutationRecord.addedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, true));
326
- mutationRecord.removedNodes.forEach((node)=>'classList' in node && recordStatus(node, mutationRecord, targetStatusMap, false));
327
- break;
342
+ for (const record of records){
343
+ if (record.type === 'childList') {
344
+ for (const node of record.addedNodes)if (node instanceof Element && node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) + 1);
345
+ for (const node of record.removedNodes)if (node instanceof Element && !node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) - 1);
346
+ } else if (record.type === 'attributes' && record.attributeName === 'class') {
347
+ attrRecords.add(record.target);
328
348
  }
329
- });
330
- const updateTarget = (target, adding)=>{
331
- const firstAttrMutationRecord = targetFirstAttrMutationRecord.get(target);
332
- if (firstAttrMutationRecord) {
333
- targetFirstAttrMutationRecord.delete(target);
334
- }
335
- if (adding) {
336
- updateClassCount(target.classList, adding);
337
- } else {
338
- if (firstAttrMutationRecord) {
339
- updateClassCount(firstAttrMutationRecord.oldValue ? firstAttrMutationRecord.oldValue.split(/\s+/) : [], adding);
340
- } else {
341
- updateClassCount(target.classList, adding);
349
+ }
350
+ const traverseIncludingSelf = (el, fn, visited)=>{
351
+ if (!visited.has(el)) {
352
+ visited.add(el);
353
+ fn(el);
354
+ for (const child of el.children){
355
+ traverseIncludingSelf(child, fn, visited);
342
356
  }
343
- disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget)=>{
344
- if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
345
- updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0);
346
- }
347
- });
357
+ } else {
358
+ nodeMap.delete(el);
348
359
  }
349
360
  };
350
- connectedStatusMap.forEach(({ change }, target)=>change !== 0 && updateTarget(target, change > 0));
351
- targetFirstAttrMutationRecord.forEach((mutation, target)=>{
352
- if (!target.isConnected) return;
353
- const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : [];
354
- const newClassList = target.classList;
355
- const addedClasses = [];
356
- newClassList.forEach((c)=>{
357
- if (!oldClassList.includes(c)) addedClasses.push(c);
358
- });
359
- const removedClasses = oldClassList.filter((c)=>!newClassList.contains(c));
360
- if (addedClasses.length) {
361
- updateClassCount(addedClasses, true);
361
+ for (const [node, count] of nodeMap){
362
+ if (count > 0) {
363
+ traverseIncludingSelf(node, diffAndSnapshot, visited);
364
+ } else if (count < 0 && !node.isConnected) {
365
+ traverseIncludingSelf(node, removeSnapshot, visited);
362
366
  }
363
- if (removedClasses.length) {
364
- updateClassCount(removedClasses, false);
365
- }
366
- });
367
- /**
368
- * Merge the class usage changes into the current class usage map.
369
- */ eachClassCounts.forEach((countChange, className)=>{
370
- let currentCount = this.classCounts.get(className) || 0;
371
- let newCount = currentCount + countChange;
372
- if (newCount > 0) {
373
- this.classCounts.set(className, newCount);
374
- if (currentCount === 0) {
375
- this.add(className);
376
- }
367
+ }
368
+ for (const el of attrRecords){
369
+ if (!visited.has(el)) diffAndSnapshot(el);
370
+ }
371
+ for (const [cls, change] of deltaCounts){
372
+ const current = this.classCounts.get(cls) || 0;
373
+ const next = current + change;
374
+ if (next > 0) {
375
+ this.classCounts.set(cls, next);
376
+ if (current === 0) this.add(cls);
377
377
  } else {
378
- this.classCounts.delete(className);
379
- this.remove(className);
378
+ this.classCounts.delete(cls);
379
+ this.remove(cls);
380
380
  }
381
- });
381
+ }
382
382
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:mutated', {
383
- records: mutationRecords,
384
- classCounts: eachClassCounts,
383
+ records,
384
+ classCounts: deltaCounts,
385
385
  cssRuntime: this
386
386
  });
387
387
  });
388
388
  this.observer.observe(this.root, {
389
+ childList: true,
389
390
  attributes: true,
390
- attributeOldValue: true,
391
391
  attributeFilter: [
392
392
  'class'
393
393
  ],
394
- childList: true,
394
+ attributeOldValue: true,
395
395
  subtree: true
396
396
  });
397
- if (!this.progressive) {
398
- this.host.removeAttribute('hidden');
399
- }
400
- // @ts-ignore
397
+ if (!this.progressive) this.host.removeAttribute('hidden');
401
398
  this.observing = true;
402
399
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:observed', {
403
400
  cssRuntime: this
@@ -543,7 +540,6 @@ class CSSRuntime extends css.MasterCSS {
543
540
  if (!this.observing) return;
544
541
  if (this.observer) {
545
542
  this.observer.disconnect();
546
- // @ts-expect-error readonly
547
543
  this.observer = undefined;
548
544
  }
549
545
  // @ts-ignore