@invisibleloop/pulse 0.18.0 → 0.18.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "description": "AI-first frontend framework. The spec is the source of truth.",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- 0.18.0
1
+ 0.18.1
@@ -35,6 +35,21 @@ export function extractUsedClasses(htmlContents, jsContents, safelist = []) {
35
35
  }
36
36
  }
37
37
 
38
+ // classList method arguments are class names BY DEFINITION — collect them even
39
+ // without a hyphen. The generic string scan above requires a hyphen to avoid
40
+ // swallowing every English word, which silently missed runtime state classes
41
+ // (classList.add('open'), .toggle('active')) and purged their compound rules
42
+ // (`.docs-sidebar.open`) from production CSS while dev looked fine.
43
+ for (const js of jsContents) {
44
+ const re = /classList\s*\.\s*(?:add|remove|toggle|replace)\s*\(\s*((?:['"][^'"]+['"]\s*,?\s*)+)/g
45
+ let m
46
+ while ((m = re.exec(js)) !== null) {
47
+ for (const [, cls] of m[1].matchAll(/['"]([^'"]+)['"]/g)) {
48
+ if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(cls)) used.add(cls)
49
+ }
50
+ }
51
+ }
52
+
38
53
  // Apply safelist from pulse.config.js — css.safelist accepts strings or RegExp.
39
54
  // String entries are added directly; RegExp entries are matched against all
40
55
  // class names defined in the CSS (resolved later in purgeCss via selectorUsed
@@ -92,3 +92,40 @@ test('minifyCss: calc() expressions keep their operator spacing', () => {
92
92
  const out = minifyCss(`.x { width: calc(100% - 2rem); }`)
93
93
  assert.ok(out.includes('calc(100% - 2rem)'), `calc broken: ${out}`)
94
94
  })
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Runtime state classes — classList arguments
98
+ // ---------------------------------------------------------------------------
99
+ // Regression: the JS string scan only keeps hyphenated names (to avoid keeping
100
+ // every English word), so runtime state classes like classList.add('open') were
101
+ // never collected — and selectorUsed checks the rightmost class token, so
102
+ // `.docs-sidebar.open { … }` was purged from production CSS. The mobile docs
103
+ // hamburger toggled the class, but the rule that slides the sidebar in was gone.
104
+
105
+ test('classList method arguments are collected regardless of hyphens', () => {
106
+ const used = extractUsedClasses([], [
107
+ `sidebar.classList.add('open')`,
108
+ `overlay.classList.remove("visible")`,
109
+ `el.classList.toggle('active', isOn)`,
110
+ `el.classList.replace('open', 'closed')`,
111
+ ])
112
+ for (const cls of ['open', 'visible', 'active', 'closed']) {
113
+ assert.ok(used.has(cls), `Expected classList arg "${cls}" to be collected`)
114
+ }
115
+ })
116
+
117
+ test('multiple classList arguments in one call are all collected', () => {
118
+ const used = extractUsedClasses([], [`el.classList.add('one', 'two-x', 'three')`])
119
+ for (const cls of ['one', 'two-x', 'three']) assert.ok(used.has(cls), cls)
120
+ })
121
+
122
+ test('runtime-toggled compound rule survives the purge', () => {
123
+ const css = `.docs-sidebar { transform: translateX(-100%); }\n.docs-sidebar.open { transform: translateX(0); }\n.unused { color: red; }`
124
+ const used = extractUsedClasses(
125
+ [`<aside class="docs-sidebar">`],
126
+ [`document.querySelector('.docs-sidebar').classList.add('open')`]
127
+ )
128
+ const out = purgeCss(css, used)
129
+ assert.ok(out.includes('.docs-sidebar.open'), `Compound state rule was purged: ${out}`)
130
+ assert.ok(!out.includes('.unused'), 'Unused rule should still be purged')
131
+ })