@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.
@@ -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__;
@@ -134,12 +160,21 @@ class RuntimeLayer extends Layer {
134
160
  const insertedIndex = super.insert(rule, index);
135
161
  if (insertedIndex === undefined || !this.native) return;
136
162
  const insertRuleSafely = (text, position)=>{
137
- try {
138
- const insertedIndex = this.native.insertRule(text, position);
139
- return this.native.cssRules.item(insertedIndex);
140
- } catch (error) {
141
- console.error(error, rule);
142
- return;
163
+ // Checks if the rule is inserted in a native CSS rule with this.attach()
164
+ if (this.rules.length === 1) {
165
+ return this.native.cssRules.item(position);
166
+ } else {
167
+ try {
168
+ const insertedIndex = this.native.insertRule(text, position);
169
+ return this.native.cssRules.item(insertedIndex);
170
+ } catch (error) {
171
+ console.error(error, rule);
172
+ /**
173
+ * If the rule is invalid, remove it from the rules array.
174
+ * It's important to note that the rule may break the entire CSS runtime,
175
+ */ this.rules.splice(insertedIndex, 1);
176
+ return;
177
+ }
143
178
  }
144
179
  };
145
180
  if ('nodes' in rule) {
@@ -149,7 +184,7 @@ class RuntimeLayer extends Layer {
149
184
  if (node.native) currentIndex++;
150
185
  });
151
186
  } else {
152
- rule.native = insertRuleSafely(rule.text, index);
187
+ rule.native = insertRuleSafely(rule.text, insertedIndex);
153
188
  }
154
189
  return insertedIndex;
155
190
  }
@@ -204,20 +239,19 @@ class CSSRuntime extends MasterCSS {
204
239
  customConfig;
205
240
  static instances = new WeakMap();
206
241
  host;
207
- observing;
208
- progressive;
209
242
  container;
210
- observer;
211
243
  baseLayer;
212
244
  themeLayer;
213
245
  presetLayer;
214
246
  componentsLayer;
215
247
  generalLayer;
216
248
  classCounts;
249
+ observer;
250
+ progressive;
251
+ observing;
217
252
  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') {
253
+ 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;
254
+ if (this.root instanceof Document || this.root instanceof HTMLDocument) {
221
255
  this.root.defaultView.globalThis.cssRuntime = this;
222
256
  this.container = this.root.head;
223
257
  this.host = this.root.documentElement;
@@ -236,39 +270,46 @@ class CSSRuntime extends MasterCSS {
236
270
  * @returns this
237
271
  */ observe() {
238
272
  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;
273
+ // Detect prerendered stylesheet
274
+ if (this.root.styleSheets) {
275
+ for (const sheet of this.root.styleSheets){
276
+ const { ownerNode } = sheet;
277
+ if (ownerNode instanceof HTMLStyleElement && ownerNode.id === 'master') {
278
+ this.style = ownerNode;
279
+ if (this.style.sheet?.cssRules.length) {
280
+ this.progressive = true;
281
+ }
282
+ break;
247
283
  }
248
- break;
249
284
  }
250
285
  }
251
- // 2. Count the classes in the host and its children.
286
+ // Prepare snapshot map
287
+ const elementClasses = new WeakMap();
288
+ // Initial scan and populate counts + snapshot
252
289
  const connectedNames = new Set();
253
290
  const increaseClassCount = (className)=>{
254
291
  const count = this.classCounts.get(className) || 0;
255
- if (!count) {
256
- connectedNames.add(className);
257
- }
292
+ if (!count) connectedNames.add(className);
258
293
  this.classCounts.set(className, count + 1);
259
294
  };
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.
295
+ const rootEl = this.root instanceof Document || this.root instanceof HTMLDocument ? this.root : this.container;
296
+ const elementsWithClass = rootEl.querySelectorAll('[class]');
297
+ elementsWithClass.forEach((el)=>{
298
+ const clsList = el.classList;
299
+ if (clsList) {
300
+ el.classList.forEach(increaseClassCount);
301
+ }
302
+ elementClasses.set(el, new Set(clsList));
303
+ });
304
+ // Hydration or style creation
263
305
  if (this.progressive) {
264
306
  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.`);
307
+ for (const cls of connectedNames){
308
+ if (!hydrateResult.allSyntaxRules.find((r)=>(r.fixedClass || r.name) === cls)) {
309
+ this.add(cls);
310
+ {
311
+ console.debug(`Missing prerendered rule for class \`${cls}\``);
312
+ }
272
313
  }
273
314
  }
274
315
  } else {
@@ -277,125 +318,81 @@ class CSSRuntime extends MasterCSS {
277
318
  this.container.append(this.style);
278
319
  this.style.sheet.insertRule(this.layerStatementRule.text);
279
320
  this.layerStatementRule.native = this.style.sheet.cssRules.item(0);
280
- for (const eachConnectedName of connectedNames){
281
- this.add(eachConnectedName);
282
- }
321
+ connectedNames.forEach((cls)=>this.add(cls));
283
322
  }
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
- });
323
+ this.observer = new MutationObserver((records)=>{
324
+ const deltaCounts = new Map();
325
+ const nodeMap = new Map();
326
+ const attrRecords = new Set();
327
+ const visited = new WeakSet();
328
+ const updateDelta = (cls, delta)=>deltaCounts.set(cls, (deltaCounts.get(cls) || 0) + delta);
329
+ const diffAndSnapshot = (el)=>{
330
+ const prev = new Set(elementClasses.get(el) || []);
331
+ const next = new Set(el.classList);
332
+ for (const c of next)if (!prev.has(c)) updateDelta(c, 1);
333
+ for (const c of prev)if (!next.has(c)) updateDelta(c, -1);
334
+ elementClasses.set(el, next);
293
335
  };
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
- }
336
+ const removeSnapshot = (el)=>{
337
+ for (const c of elementClasses.get(el) || [])updateDelta(c, -1);
338
+ elementClasses.delete(el);
312
339
  };
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;
340
+ for (const record of records){
341
+ if (record.type === 'childList') {
342
+ for (const node of record.addedNodes)if (node instanceof Element && node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) + 1);
343
+ for (const node of record.removedNodes)if (node instanceof Element && !node.isConnected) nodeMap.set(node, (nodeMap.get(node) || 0) - 1);
344
+ } else if (record.type === 'attributes' && record.attributeName === 'class') {
345
+ attrRecords.add(record.target);
326
346
  }
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);
347
+ }
348
+ const traverseIncludingSelf = (el, fn, visited)=>{
349
+ if (!visited.has(el)) {
350
+ visited.add(el);
351
+ fn(el);
352
+ for (const child of el.children){
353
+ traverseIncludingSelf(child, fn, visited);
340
354
  }
341
- disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget)=>{
342
- if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
343
- updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0);
344
- }
345
- });
355
+ } else {
356
+ nodeMap.delete(el);
346
357
  }
347
358
  };
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);
359
+ for (const [node, count] of nodeMap){
360
+ if (count > 0) {
361
+ traverseIncludingSelf(node, diffAndSnapshot, visited);
362
+ } else if (count < 0 && !node.isConnected) {
363
+ traverseIncludingSelf(node, removeSnapshot, visited);
360
364
  }
361
- if (removedClasses.length) {
362
- updateClassCount(removedClasses, false);
363
- }
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
- }
365
+ }
366
+ for (const el of attrRecords){
367
+ if (!visited.has(el)) diffAndSnapshot(el);
368
+ }
369
+ for (const [cls, change] of deltaCounts){
370
+ const current = this.classCounts.get(cls) || 0;
371
+ const next = current + change;
372
+ if (next > 0) {
373
+ this.classCounts.set(cls, next);
374
+ if (current === 0) this.add(cls);
375
375
  } else {
376
- this.classCounts.delete(className);
377
- this.remove(className);
376
+ this.classCounts.delete(cls);
377
+ this.remove(cls);
378
378
  }
379
- });
379
+ }
380
380
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:mutated', {
381
- records: mutationRecords,
382
- classCounts: eachClassCounts,
381
+ records,
382
+ classCounts: deltaCounts,
383
383
  cssRuntime: this
384
384
  });
385
385
  });
386
386
  this.observer.observe(this.root, {
387
+ childList: true,
387
388
  attributes: true,
388
- attributeOldValue: true,
389
389
  attributeFilter: [
390
390
  'class'
391
391
  ],
392
- childList: true,
392
+ attributeOldValue: true,
393
393
  subtree: true
394
394
  });
395
- if (!this.progressive) {
396
- this.host.removeAttribute('hidden');
397
- }
398
- // @ts-ignore
395
+ if (!this.progressive) this.host.removeAttribute('hidden');
399
396
  this.observing = true;
400
397
  globalThis.__MASTER_CSS_DEVTOOLS_HOOK__?.emit('runtime:observed', {
401
398
  cssRuntime: this
@@ -541,7 +538,6 @@ class CSSRuntime extends MasterCSS {
541
538
  if (!this.observing) return;
542
539
  if (this.observer) {
543
540
  this.observer.disconnect();
544
- // @ts-expect-error readonly
545
541
  this.observer = undefined;
546
542
  }
547
543
  // @ts-ignore