@pyreon/styler 0.11.5 → 0.11.7
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/README.md +27 -23
- package/lib/index.d.ts +9 -2
- package/lib/index.js +49 -4
- package/package.json +22 -22
- package/src/ThemeProvider.ts +10 -3
- package/src/__tests__/ThemeProvider.test.ts +21 -21
- package/src/__tests__/benchmark.bench.ts +56 -45
- package/src/__tests__/composition-chain.test.ts +200 -151
- package/src/__tests__/forward.test.ts +122 -122
- package/src/__tests__/globalStyle.test.ts +18 -18
- package/src/__tests__/hash.test.ts +27 -27
- package/src/__tests__/hybrid-injection.test.ts +83 -59
- package/src/__tests__/index.ts +10 -10
- package/src/__tests__/insertion-effect.test.ts +45 -32
- package/src/__tests__/integration.test.ts +81 -51
- package/src/__tests__/keyframes.test.ts +13 -13
- package/src/__tests__/memory-growth.test.ts +21 -21
- package/src/__tests__/p3-features.test.ts +162 -104
- package/src/__tests__/shared.test.ts +51 -33
- package/src/__tests__/sheet-advanced.test.ts +227 -227
- package/src/__tests__/sheet-split-atrules.test.ts +85 -85
- package/src/__tests__/sheet.test.ts +69 -69
- package/src/__tests__/styled-ssr.test.ts +36 -28
- package/src/__tests__/styled.test.ts +214 -145
- package/src/__tests__/theme.test.ts +11 -11
- package/src/__tests__/useCSS.test.ts +89 -59
- package/src/css.ts +1 -1
- package/src/forward.ts +187 -187
- package/src/globalStyle.ts +5 -5
- package/src/index.ts +15 -15
- package/src/keyframes.ts +3 -3
- package/src/resolve.ts +14 -14
- package/src/shared.ts +2 -2
- package/src/sheet.ts +26 -26
- package/src/styled.tsx +157 -100
- package/src/useCSS.ts +4 -4
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
2
|
-
import { hash } from
|
|
3
|
-
import { createSheet, StyleSheet } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { hash } from '../hash'
|
|
3
|
+
import { createSheet, StyleSheet } from '../sheet'
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
describe(
|
|
7
|
-
it(
|
|
5
|
+
describe('StyleSheet -- advanced features', () => {
|
|
6
|
+
describe('getClassName (pure hash computation)', () => {
|
|
7
|
+
it('returns className without inserting a rule', () => {
|
|
8
8
|
const s = createSheet()
|
|
9
|
-
const cls = s.getClassName(
|
|
9
|
+
const cls = s.getClassName('display: flex;')
|
|
10
10
|
expect(cls).toMatch(/^pyr-[0-9a-z]+$/)
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
it(
|
|
13
|
+
it('returns same className as insert for same CSS', () => {
|
|
14
14
|
const s = createSheet()
|
|
15
|
-
const clsPure = s.getClassName(
|
|
16
|
-
const clsInsert = s.insert(
|
|
15
|
+
const clsPure = s.getClassName('display: flex;')
|
|
16
|
+
const clsInsert = s.insert('display: flex;')
|
|
17
17
|
expect(clsPure).toBe(clsInsert)
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it(
|
|
20
|
+
it('matches hash-based className', () => {
|
|
21
21
|
const s = createSheet()
|
|
22
|
-
const cssText =
|
|
22
|
+
const cssText = 'display: flex;'
|
|
23
23
|
expect(s.getClassName(cssText)).toBe(`pyr-${hash(cssText)}`)
|
|
24
24
|
})
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
describe(
|
|
27
|
+
describe('bounded cache (eviction) -- DOM mode', () => {
|
|
28
28
|
beforeEach(() => {
|
|
29
|
-
document.querySelectorAll(
|
|
29
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
30
30
|
el.remove()
|
|
31
31
|
})
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
it(
|
|
34
|
+
it('still deduplicates within cache bounds', () => {
|
|
35
35
|
const s = createSheet({ maxCacheSize: 100 })
|
|
36
|
-
const cls1 = s.insert(
|
|
37
|
-
const cls2 = s.insert(
|
|
36
|
+
const cls1 = s.insert('color: red;')
|
|
37
|
+
const cls2 = s.insert('color: red;')
|
|
38
38
|
expect(cls1).toBe(cls2)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it(
|
|
41
|
+
it('cacheSize property reflects current cache size', () => {
|
|
42
42
|
const s = createSheet()
|
|
43
43
|
expect(s.cacheSize).toBe(0)
|
|
44
44
|
|
|
45
|
-
s.insert(
|
|
45
|
+
s.insert('color: red;')
|
|
46
46
|
expect(s.cacheSize).toBe(1)
|
|
47
47
|
|
|
48
|
-
s.insert(
|
|
48
|
+
s.insert('color: blue;')
|
|
49
49
|
expect(s.cacheSize).toBe(2)
|
|
50
50
|
|
|
51
51
|
// Duplicate doesn't increase size
|
|
52
|
-
s.insert(
|
|
52
|
+
s.insert('color: red;')
|
|
53
53
|
expect(s.cacheSize).toBe(2)
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it(
|
|
56
|
+
it('evicts oldest entries when max cache size is exceeded', () => {
|
|
57
57
|
const s = createSheet({ maxCacheSize: 10 })
|
|
58
58
|
|
|
59
59
|
for (let i = 0; i < 15; i++) {
|
|
@@ -63,7 +63,7 @@ describe("StyleSheet -- advanced features", () => {
|
|
|
63
63
|
expect(s.cacheSize).toBeLessThanOrEqual(15)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
it(
|
|
66
|
+
it('accepts custom maxCacheSize option', () => {
|
|
67
67
|
const s = createSheet({ maxCacheSize: 5 })
|
|
68
68
|
|
|
69
69
|
for (let i = 0; i < 20; i++) {
|
|
@@ -74,267 +74,267 @@ describe("StyleSheet -- advanced features", () => {
|
|
|
74
74
|
})
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
describe(
|
|
78
|
-
it(
|
|
79
|
-
const warnSpy = vi.spyOn(console,
|
|
80
|
-
document.querySelectorAll(
|
|
77
|
+
describe('dev-mode warnings', () => {
|
|
78
|
+
it('warns on invalid CSS in dev mode', () => {
|
|
79
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
80
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
81
81
|
el.remove()
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
const s = new StyleSheet()
|
|
85
|
-
s.insert(
|
|
85
|
+
s.insert('invalid{{{css')
|
|
86
86
|
|
|
87
87
|
warnSpy.mockRestore()
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
describe(
|
|
91
|
+
describe('insertGlobal -- multi-rule splitting', () => {
|
|
92
92
|
beforeEach(() => {
|
|
93
|
-
document.querySelectorAll(
|
|
93
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
94
94
|
el.remove()
|
|
95
95
|
})
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it(
|
|
98
|
+
it('injects multiple top-level rules from a single insertGlobal call', () => {
|
|
99
99
|
const s = createSheet()
|
|
100
|
-
s.insertGlobal(
|
|
100
|
+
s.insertGlobal('html { font-size: 16px; } body { margin: 0; }')
|
|
101
101
|
// Should not throw -- both rules are injected individually
|
|
102
102
|
expect(s.cacheSize).toBe(1) // single cache entry for the whole CSS text
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
it(
|
|
105
|
+
it('injects nested @media rules correctly', () => {
|
|
106
106
|
const s = createSheet()
|
|
107
|
-
s.insertGlobal(
|
|
107
|
+
s.insertGlobal('body { margin: 0; } @media (min-width: 768px) { body { font-size: 18px; } }')
|
|
108
108
|
expect(s.cacheSize).toBe(1)
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
-
it(
|
|
111
|
+
it('handles three or more rules', () => {
|
|
112
112
|
const s = createSheet()
|
|
113
113
|
s.insertGlobal(
|
|
114
|
-
|
|
114
|
+
'html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } body { margin: 0; font-family: sans-serif; }',
|
|
115
115
|
)
|
|
116
116
|
expect(s.cacheSize).toBe(1)
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
-
it(
|
|
119
|
+
it('deduplicates identical multi-rule CSS', () => {
|
|
120
120
|
const s = createSheet()
|
|
121
|
-
const cssStr =
|
|
121
|
+
const cssStr = 'html { font-size: 16px; } body { margin: 0; }'
|
|
122
122
|
s.insertGlobal(cssStr)
|
|
123
123
|
s.insertGlobal(cssStr)
|
|
124
124
|
expect(s.cacheSize).toBe(1)
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
it(
|
|
127
|
+
it('inserts multiple rules into the CSSOM via splitRules and insertRule', () => {
|
|
128
128
|
const s = createSheet()
|
|
129
|
-
s.insertGlobal(
|
|
129
|
+
s.insertGlobal('body{margin:0}div{padding:0}')
|
|
130
130
|
|
|
131
131
|
// Verify rules were actually inserted into the CSSOM
|
|
132
|
-
const styleEl = document.querySelector(
|
|
132
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
133
133
|
const cssSheet = styleEl.sheet as CSSStyleSheet
|
|
134
134
|
const ruleTexts = Array.from(cssSheet.cssRules).map((r) => r.cssText)
|
|
135
|
-
expect(ruleTexts.some((r) => r.includes(
|
|
136
|
-
expect(ruleTexts.some((r) => r.includes(
|
|
135
|
+
expect(ruleTexts.some((r) => r.includes('margin'))).toBe(true)
|
|
136
|
+
expect(ruleTexts.some((r) => r.includes('padding'))).toBe(true)
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
it(
|
|
139
|
+
it('deduplicates insertGlobal -- second call with same CSS is a no-op', () => {
|
|
140
140
|
const s = createSheet()
|
|
141
|
-
s.insertGlobal(
|
|
141
|
+
s.insertGlobal('body{margin:0}div{padding:0}')
|
|
142
142
|
|
|
143
|
-
const styleEl = document.querySelector(
|
|
143
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
144
144
|
const cssSheet = styleEl.sheet as CSSStyleSheet
|
|
145
145
|
const countBefore = cssSheet.cssRules.length
|
|
146
146
|
|
|
147
147
|
// Second call should be deduped via cache
|
|
148
|
-
s.insertGlobal(
|
|
148
|
+
s.insertGlobal('body{margin:0}div{padding:0}')
|
|
149
149
|
expect(cssSheet.cssRules.length).toBe(countBefore)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
-
it(
|
|
152
|
+
it('inserts global CSS with @media into CSSOM', () => {
|
|
153
153
|
const s = createSheet()
|
|
154
|
-
s.insertGlobal(
|
|
154
|
+
s.insertGlobal('body{margin:0}@media (min-width:768px){body{font-size:18px}}')
|
|
155
155
|
|
|
156
|
-
const styleEl = document.querySelector(
|
|
156
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
157
157
|
const cssSheet = styleEl.sheet as CSSStyleSheet
|
|
158
158
|
const ruleTexts = Array.from(cssSheet.cssRules).map((r) => r.cssText)
|
|
159
|
-
expect(ruleTexts.some((r) => r.includes(
|
|
160
|
-
expect(ruleTexts.some((r) => r.includes(
|
|
159
|
+
expect(ruleTexts.some((r) => r.includes('margin'))).toBe(true)
|
|
160
|
+
expect(ruleTexts.some((r) => r.includes('media'))).toBe(true)
|
|
161
161
|
})
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
-
describe(
|
|
164
|
+
describe('@layer on client-side insert', () => {
|
|
165
165
|
beforeEach(() => {
|
|
166
|
-
document.querySelectorAll(
|
|
166
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
167
167
|
el.remove()
|
|
168
168
|
})
|
|
169
169
|
})
|
|
170
170
|
|
|
171
|
-
it(
|
|
172
|
-
const s = createSheet({ layer:
|
|
173
|
-
const className = s.insert(
|
|
171
|
+
it('wraps inserted rules in @layer on client side', () => {
|
|
172
|
+
const s = createSheet({ layer: 'components' })
|
|
173
|
+
const className = s.insert('color: red;')
|
|
174
174
|
expect(className).toMatch(/^pyr-/)
|
|
175
175
|
// The sheet should have injected a @layer declaration + the wrapped rule
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
it(
|
|
179
|
-
const s = createSheet({ layer:
|
|
180
|
-
s.insert(
|
|
178
|
+
it('injects @layer declaration rule on mount', () => {
|
|
179
|
+
const s = createSheet({ layer: 'myLayer' })
|
|
180
|
+
s.insert('display: flex;')
|
|
181
181
|
// No crash -- @layer declaration was injected at mount time
|
|
182
182
|
expect(s.cacheSize).toBe(1)
|
|
183
183
|
})
|
|
184
184
|
})
|
|
185
185
|
|
|
186
|
-
describe(
|
|
186
|
+
describe('hydration from existing SSR style tag', () => {
|
|
187
187
|
beforeEach(() => {
|
|
188
|
-
document.querySelectorAll(
|
|
188
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
189
189
|
el.remove()
|
|
190
190
|
})
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
-
it(
|
|
193
|
+
it('reuses existing <style data-pyreon-styler> tag and hydrates cache', () => {
|
|
194
194
|
// Create a style tag that simulates SSR output
|
|
195
|
-
const el = document.createElement(
|
|
196
|
-
el.setAttribute(
|
|
195
|
+
const el = document.createElement('style')
|
|
196
|
+
el.setAttribute('data-pyreon-styler', '')
|
|
197
197
|
document.head.appendChild(el)
|
|
198
198
|
|
|
199
199
|
// Insert a rule directly so hydrateFromTag can discover it
|
|
200
200
|
const sheetRef = el.sheet
|
|
201
|
-
if (sheetRef) sheetRef.insertRule(
|
|
201
|
+
if (sheetRef) sheetRef.insertRule('.pyr-abc { color: red; }', 0)
|
|
202
202
|
|
|
203
203
|
// Create a new sheet -- it should find and reuse the existing tag
|
|
204
204
|
const s = createSheet()
|
|
205
205
|
// The hydration should have populated the cache with 'pyr-abc'
|
|
206
|
-
expect(s.has(
|
|
206
|
+
expect(s.has('pyr-abc')).toBe(true)
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
-
it(
|
|
210
|
-
const el = document.createElement(
|
|
211
|
-
el.setAttribute(
|
|
209
|
+
it('hydrates @media-wrapped rules from SSR style tag', () => {
|
|
210
|
+
const el = document.createElement('style')
|
|
211
|
+
el.setAttribute('data-pyreon-styler', '')
|
|
212
212
|
document.head.appendChild(el)
|
|
213
213
|
|
|
214
214
|
const sheetRef = el.sheet
|
|
215
215
|
if (sheetRef) {
|
|
216
|
-
sheetRef.insertRule(
|
|
216
|
+
sheetRef.insertRule('@media (min-width: 768px) { .pyr-xyz { font-size: 2rem; } }', 0)
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
const s = createSheet()
|
|
220
|
-
expect(s.has(
|
|
220
|
+
expect(s.has('pyr-xyz')).toBe(true)
|
|
221
221
|
})
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
describe(
|
|
225
|
-
it(
|
|
226
|
-
document.querySelectorAll(
|
|
224
|
+
describe('boost on prepare()', () => {
|
|
225
|
+
it('doubles selector when boost is true', () => {
|
|
226
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
227
227
|
el.remove()
|
|
228
228
|
})
|
|
229
229
|
const s = createSheet()
|
|
230
|
-
const result = s.prepare(
|
|
230
|
+
const result = s.prepare('color: red;', true)
|
|
231
231
|
expect(result.className).toMatch(/^pyr-/)
|
|
232
232
|
// Boosted: selector is doubled
|
|
233
233
|
expect(result.rules).toContain(`.${result.className}.${result.className}`)
|
|
234
234
|
})
|
|
235
235
|
|
|
236
|
-
it(
|
|
237
|
-
document.querySelectorAll(
|
|
236
|
+
it('single selector when boost is false', () => {
|
|
237
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
238
238
|
el.remove()
|
|
239
239
|
})
|
|
240
240
|
const s = createSheet()
|
|
241
|
-
const result = s.prepare(
|
|
241
|
+
const result = s.prepare('color: red;', false)
|
|
242
242
|
// Non-boosted: single selector
|
|
243
243
|
expect(result.rules).toContain(`.${result.className}{`)
|
|
244
244
|
expect(result.rules).not.toContain(`.${result.className}.${result.className}`)
|
|
245
245
|
})
|
|
246
246
|
|
|
247
|
-
it(
|
|
248
|
-
document.querySelectorAll(
|
|
247
|
+
it('prepare with empty base -- all CSS is @rules', () => {
|
|
248
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
249
249
|
el.remove()
|
|
250
250
|
})
|
|
251
251
|
const s = createSheet()
|
|
252
252
|
// CSS that is entirely an @media rule, so base is empty after splitting
|
|
253
|
-
const result = s.prepare(
|
|
253
|
+
const result = s.prepare('@media(min-width:0){color:red}')
|
|
254
254
|
expect(result.className).toMatch(/^pyr-/)
|
|
255
255
|
// Should contain the @media rule but NOT a bare selector rule (no base)
|
|
256
|
-
expect(result.rules).toContain(
|
|
257
|
-
expect(result.rules).toContain(
|
|
256
|
+
expect(result.rules).toContain('@media')
|
|
257
|
+
expect(result.rules).toContain('color:red')
|
|
258
258
|
// The rules should NOT start with a plain selector -- no base rule emitted
|
|
259
259
|
expect(result.rules).not.toMatch(new RegExp(`^\\.${result.className}\\{`))
|
|
260
260
|
})
|
|
261
261
|
|
|
262
|
-
it(
|
|
263
|
-
document.querySelectorAll(
|
|
262
|
+
it('prepare with only @supports produces no base rule', () => {
|
|
263
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
264
264
|
el.remove()
|
|
265
265
|
})
|
|
266
266
|
const s = createSheet()
|
|
267
267
|
const result = s.prepare(
|
|
268
|
-
|
|
268
|
+
'@supports(display:grid){display:grid}@media(min-width:0){color:red}',
|
|
269
269
|
)
|
|
270
270
|
expect(result.className).toMatch(/^pyr-/)
|
|
271
|
-
expect(result.rules).toContain(
|
|
272
|
-
expect(result.rules).toContain(
|
|
271
|
+
expect(result.rules).toContain('@supports')
|
|
272
|
+
expect(result.rules).toContain('@media')
|
|
273
273
|
// No base selector rule
|
|
274
274
|
expect(result.rules).not.toMatch(new RegExp(`^\\.${result.className}\\{`))
|
|
275
275
|
})
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
-
describe(
|
|
278
|
+
describe('boost on insert()', () => {
|
|
279
279
|
beforeEach(() => {
|
|
280
|
-
document.querySelectorAll(
|
|
280
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
281
281
|
el.remove()
|
|
282
282
|
})
|
|
283
283
|
})
|
|
284
284
|
|
|
285
|
-
it(
|
|
285
|
+
it('inserts boosted rule on client side', () => {
|
|
286
286
|
const s = createSheet()
|
|
287
|
-
const cls = s.insert(
|
|
287
|
+
const cls = s.insert('color: red;', true)
|
|
288
288
|
expect(cls).toMatch(/^pyr-/)
|
|
289
289
|
})
|
|
290
290
|
|
|
291
|
-
it(
|
|
291
|
+
it('deduplicates boosted and non-boosted separately via insertCache key', () => {
|
|
292
292
|
const s = createSheet()
|
|
293
|
-
const cls1 = s.insert(
|
|
294
|
-
const cls2 = s.insert(
|
|
293
|
+
const cls1 = s.insert('color: green;', false)
|
|
294
|
+
const cls2 = s.insert('color: green;', true)
|
|
295
295
|
// Same className (same hash) but both should work without error
|
|
296
296
|
expect(cls1).toBe(cls2)
|
|
297
297
|
})
|
|
298
298
|
})
|
|
299
299
|
|
|
300
|
-
describe(
|
|
300
|
+
describe('clearAll() with rules', () => {
|
|
301
301
|
beforeEach(() => {
|
|
302
|
-
document.querySelectorAll(
|
|
302
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
303
303
|
el.remove()
|
|
304
304
|
})
|
|
305
305
|
})
|
|
306
306
|
|
|
307
|
-
it(
|
|
307
|
+
it('removes all CSS rules from the DOM sheet', () => {
|
|
308
308
|
const s = createSheet()
|
|
309
|
-
s.insert(
|
|
310
|
-
s.insert(
|
|
309
|
+
s.insert('color: red;')
|
|
310
|
+
s.insert('color: blue;')
|
|
311
311
|
expect(s.cacheSize).toBe(2)
|
|
312
312
|
|
|
313
313
|
s.clearAll()
|
|
314
314
|
expect(s.cacheSize).toBe(0)
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
it(
|
|
317
|
+
it('allows re-insertion after clearAll', () => {
|
|
318
318
|
const s = createSheet()
|
|
319
|
-
s.insert(
|
|
319
|
+
s.insert('color: red;')
|
|
320
320
|
s.clearAll()
|
|
321
321
|
// Re-insert the same CSS -- should work since cache was cleared
|
|
322
|
-
const cls = s.insert(
|
|
322
|
+
const cls = s.insert('color: red;')
|
|
323
323
|
expect(cls).toMatch(/^pyr-/)
|
|
324
324
|
expect(s.cacheSize).toBe(1)
|
|
325
325
|
})
|
|
326
326
|
})
|
|
327
327
|
|
|
328
|
-
describe(
|
|
328
|
+
describe('clearCache()', () => {
|
|
329
329
|
beforeEach(() => {
|
|
330
|
-
document.querySelectorAll(
|
|
330
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
331
331
|
el.remove()
|
|
332
332
|
})
|
|
333
333
|
})
|
|
334
334
|
|
|
335
|
-
it(
|
|
335
|
+
it('clears the dedup cache but leaves DOM rules in place', () => {
|
|
336
336
|
const s = createSheet()
|
|
337
|
-
s.insert(
|
|
337
|
+
s.insert('color: red;')
|
|
338
338
|
expect(s.cacheSize).toBe(1)
|
|
339
339
|
|
|
340
340
|
s.clearCache()
|
|
@@ -342,118 +342,118 @@ describe("StyleSheet -- advanced features", () => {
|
|
|
342
342
|
})
|
|
343
343
|
})
|
|
344
344
|
|
|
345
|
-
describe(
|
|
345
|
+
describe('insertRule failure warning', () => {
|
|
346
346
|
beforeEach(() => {
|
|
347
|
-
document.querySelectorAll(
|
|
347
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
348
348
|
el.remove()
|
|
349
349
|
})
|
|
350
350
|
})
|
|
351
351
|
|
|
352
|
-
it(
|
|
353
|
-
const warnSpy = vi.spyOn(console,
|
|
352
|
+
it('warns in dev mode when insertRule fails for insert()', () => {
|
|
353
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
354
354
|
const s = createSheet()
|
|
355
355
|
// Insert invalid CSS that will cause insertRule to throw
|
|
356
|
-
s.insert(
|
|
356
|
+
s.insert('color: red; @INVALID_RULE {{{')
|
|
357
357
|
warnSpy.mockRestore()
|
|
358
358
|
})
|
|
359
359
|
|
|
360
|
-
it(
|
|
361
|
-
const warnSpy = vi.spyOn(console,
|
|
360
|
+
it('warns in dev mode when insertGlobal insertRule fails', () => {
|
|
361
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
362
362
|
const s = createSheet()
|
|
363
363
|
// Insert intentionally malformed global CSS
|
|
364
|
-
s.insertGlobal(
|
|
364
|
+
s.insertGlobal('@INVALID {{{')
|
|
365
365
|
warnSpy.mockRestore()
|
|
366
366
|
})
|
|
367
367
|
|
|
368
|
-
it(
|
|
369
|
-
const warnSpy = vi.spyOn(console,
|
|
368
|
+
it('warns in dev mode when insertKeyframes insertRule fails', () => {
|
|
369
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
370
370
|
const s = createSheet()
|
|
371
|
-
s.insertKeyframes(
|
|
371
|
+
s.insertKeyframes('bad', '{{{invalid')
|
|
372
372
|
warnSpy.mockRestore()
|
|
373
373
|
})
|
|
374
374
|
})
|
|
375
375
|
|
|
376
|
-
describe(
|
|
376
|
+
describe('insert() insertRule failure via mock', () => {
|
|
377
377
|
beforeEach(() => {
|
|
378
|
-
document.querySelectorAll(
|
|
378
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
379
379
|
el.remove()
|
|
380
380
|
})
|
|
381
381
|
})
|
|
382
382
|
|
|
383
|
-
it(
|
|
384
|
-
const warnSpy = vi.spyOn(console,
|
|
383
|
+
it('warns when CSSStyleSheet.insertRule throws during insert()', () => {
|
|
384
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
385
385
|
const s = createSheet()
|
|
386
386
|
|
|
387
387
|
// Access the internal sheet and make insertRule throw
|
|
388
|
-
const styleEl = document.querySelector(
|
|
388
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
389
389
|
const realSheet = styleEl.sheet
|
|
390
|
-
if (!realSheet) throw new Error(
|
|
390
|
+
if (!realSheet) throw new Error('Expected sheet to exist')
|
|
391
391
|
const origInsertRule = realSheet.insertRule.bind(realSheet)
|
|
392
392
|
realSheet.insertRule = (rule: string, index?: number) => {
|
|
393
393
|
// Let @layer declaration through (if any), fail on component rules
|
|
394
|
-
if (rule.startsWith(
|
|
395
|
-
throw new Error(
|
|
394
|
+
if (rule.startsWith('.pyr-')) {
|
|
395
|
+
throw new Error('Mock insertRule failure')
|
|
396
396
|
}
|
|
397
397
|
return origInsertRule(rule, index)
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
s.insert(
|
|
400
|
+
s.insert('color: magenta;')
|
|
401
401
|
|
|
402
402
|
expect(warnSpy).toHaveBeenCalled()
|
|
403
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain(
|
|
403
|
+
expect(warnSpy.mock.calls[0]?.[0]).toContain('[styler] Failed to insert CSS rule')
|
|
404
404
|
|
|
405
405
|
realSheet.insertRule = origInsertRule
|
|
406
406
|
warnSpy.mockRestore()
|
|
407
407
|
})
|
|
408
408
|
})
|
|
409
409
|
|
|
410
|
-
describe(
|
|
410
|
+
describe('insertGlobal insertRule failure via mock', () => {
|
|
411
411
|
beforeEach(() => {
|
|
412
|
-
document.querySelectorAll(
|
|
412
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
413
413
|
el.remove()
|
|
414
414
|
})
|
|
415
415
|
})
|
|
416
416
|
|
|
417
|
-
it(
|
|
418
|
-
const warnSpy = vi.spyOn(console,
|
|
417
|
+
it('warns when CSSStyleSheet.insertRule throws during insertGlobal()', () => {
|
|
418
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
419
419
|
const s = createSheet()
|
|
420
420
|
|
|
421
|
-
const styleEl = document.querySelector(
|
|
421
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
422
422
|
const realSheet = styleEl.sheet
|
|
423
|
-
if (!realSheet) throw new Error(
|
|
423
|
+
if (!realSheet) throw new Error('Expected sheet to exist')
|
|
424
424
|
const origInsertRule = realSheet.insertRule.bind(realSheet)
|
|
425
425
|
realSheet.insertRule = (_rule: string, _index?: number) => {
|
|
426
|
-
throw new Error(
|
|
426
|
+
throw new Error('Mock insertGlobal failure')
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
s.insertGlobal(
|
|
429
|
+
s.insertGlobal('body { margin: 0; }')
|
|
430
430
|
|
|
431
431
|
expect(warnSpy).toHaveBeenCalled()
|
|
432
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain(
|
|
432
|
+
expect(warnSpy.mock.calls[0]?.[0]).toContain('[styler] Failed to insert global CSS rule')
|
|
433
433
|
|
|
434
434
|
realSheet.insertRule = origInsertRule
|
|
435
435
|
warnSpy.mockRestore()
|
|
436
436
|
})
|
|
437
437
|
})
|
|
438
438
|
|
|
439
|
-
describe(
|
|
439
|
+
describe('getClassName with insertCache populated', () => {
|
|
440
440
|
beforeEach(() => {
|
|
441
|
-
document.querySelectorAll(
|
|
441
|
+
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
442
442
|
el.remove()
|
|
443
443
|
})
|
|
444
444
|
})
|
|
445
445
|
|
|
446
|
-
it(
|
|
446
|
+
it('returns cached className from insertCache after insert()', () => {
|
|
447
447
|
const s = createSheet()
|
|
448
|
-
const cls1 = s.insert(
|
|
448
|
+
const cls1 = s.insert('color: purple;')
|
|
449
449
|
// getClassName should hit the insertCache
|
|
450
|
-
const cls2 = s.getClassName(
|
|
450
|
+
const cls2 = s.getClassName('color: purple;')
|
|
451
451
|
expect(cls1).toBe(cls2)
|
|
452
452
|
})
|
|
453
453
|
})
|
|
454
454
|
|
|
455
455
|
// SSR-specific tests: mock document as undefined to simulate server
|
|
456
|
-
describe(
|
|
456
|
+
describe('SSR mode (mocked)', () => {
|
|
457
457
|
let originalDocument: typeof document
|
|
458
458
|
|
|
459
459
|
beforeEach(() => {
|
|
@@ -467,81 +467,81 @@ describe("StyleSheet -- advanced features", () => {
|
|
|
467
467
|
globalThis.document = originalDocument
|
|
468
468
|
})
|
|
469
469
|
|
|
470
|
-
it(
|
|
470
|
+
it('creates independent StyleSheet instances in SSR', () => {
|
|
471
471
|
const sheet1 = createSheet()
|
|
472
472
|
const sheet2 = createSheet()
|
|
473
473
|
|
|
474
|
-
sheet1.insert(
|
|
475
|
-
expect(sheet1.getStyles()).toContain(
|
|
476
|
-
expect(sheet2.getStyles()).toBe(
|
|
474
|
+
sheet1.insert('color: red;')
|
|
475
|
+
expect(sheet1.getStyles()).toContain('color: red;')
|
|
476
|
+
expect(sheet2.getStyles()).toBe('')
|
|
477
477
|
})
|
|
478
478
|
|
|
479
|
-
it(
|
|
479
|
+
it('each instance has its own SSR buffer', () => {
|
|
480
480
|
const sheet1 = createSheet()
|
|
481
481
|
const sheet2 = createSheet()
|
|
482
482
|
|
|
483
|
-
sheet1.insert(
|
|
484
|
-
sheet2.insert(
|
|
483
|
+
sheet1.insert('color: red;')
|
|
484
|
+
sheet2.insert('color: blue;')
|
|
485
485
|
|
|
486
|
-
expect(sheet1.getStyles()).toContain(
|
|
487
|
-
expect(sheet1.getStyles()).not.toContain(
|
|
488
|
-
expect(sheet2.getStyles()).toContain(
|
|
489
|
-
expect(sheet2.getStyles()).not.toContain(
|
|
486
|
+
expect(sheet1.getStyles()).toContain('color: red;')
|
|
487
|
+
expect(sheet1.getStyles()).not.toContain('color: blue;')
|
|
488
|
+
expect(sheet2.getStyles()).toContain('color: blue;')
|
|
489
|
+
expect(sheet2.getStyles()).not.toContain('color: red;')
|
|
490
490
|
})
|
|
491
491
|
|
|
492
|
-
it(
|
|
492
|
+
it('reset only clears the buffer of that instance', () => {
|
|
493
493
|
const sheet1 = createSheet()
|
|
494
494
|
const sheet2 = createSheet()
|
|
495
495
|
|
|
496
|
-
sheet1.insert(
|
|
497
|
-
sheet2.insert(
|
|
496
|
+
sheet1.insert('color: red;')
|
|
497
|
+
sheet2.insert('color: blue;')
|
|
498
498
|
|
|
499
499
|
sheet1.reset()
|
|
500
|
-
expect(sheet1.getStyles()).toBe(
|
|
501
|
-
expect(sheet2.getStyles()).toContain(
|
|
500
|
+
expect(sheet1.getStyles()).toBe('')
|
|
501
|
+
expect(sheet2.getStyles()).toContain('color: blue;')
|
|
502
502
|
})
|
|
503
503
|
|
|
504
|
-
it(
|
|
504
|
+
it('each instance has its own dedup cache', () => {
|
|
505
505
|
const sheet1 = createSheet()
|
|
506
506
|
const sheet2 = createSheet()
|
|
507
507
|
|
|
508
|
-
const cls1 = sheet1.insert(
|
|
509
|
-
const cls2 = sheet2.insert(
|
|
508
|
+
const cls1 = sheet1.insert('display: flex;')
|
|
509
|
+
const cls2 = sheet2.insert('display: flex;')
|
|
510
510
|
|
|
511
511
|
// Same CSS -> same class name (deterministic hashing)
|
|
512
512
|
expect(cls1).toBe(cls2)
|
|
513
513
|
|
|
514
514
|
// Both inject independently
|
|
515
|
-
expect(sheet1.getStyles()).toContain(
|
|
516
|
-
expect(sheet2.getStyles()).toContain(
|
|
515
|
+
expect(sheet1.getStyles()).toContain('display: flex;')
|
|
516
|
+
expect(sheet2.getStyles()).toContain('display: flex;')
|
|
517
517
|
})
|
|
518
518
|
|
|
519
|
-
describe(
|
|
520
|
-
it(
|
|
519
|
+
describe('concurrent SSR simulation', () => {
|
|
520
|
+
it('handles concurrent requests without cross-contamination', () => {
|
|
521
521
|
const request1 = createSheet()
|
|
522
522
|
const request2 = createSheet()
|
|
523
523
|
|
|
524
|
-
request1.insert(
|
|
525
|
-
request1.insertGlobal(
|
|
524
|
+
request1.insert('color: red;')
|
|
525
|
+
request1.insertGlobal('body { margin: 0; }')
|
|
526
526
|
|
|
527
|
-
request2.insert(
|
|
528
|
-
request2.insertGlobal(
|
|
527
|
+
request2.insert('color: blue;')
|
|
528
|
+
request2.insertGlobal('body { padding: 0; }')
|
|
529
529
|
|
|
530
530
|
const html1 = request1.getStyleTag()
|
|
531
531
|
const html2 = request2.getStyleTag()
|
|
532
532
|
|
|
533
|
-
expect(html1).toContain(
|
|
534
|
-
expect(html1).not.toContain(
|
|
535
|
-
expect(html1).toContain(
|
|
536
|
-
expect(html1).not.toContain(
|
|
533
|
+
expect(html1).toContain('color: red;')
|
|
534
|
+
expect(html1).not.toContain('color: blue;')
|
|
535
|
+
expect(html1).toContain('body { margin: 0; }')
|
|
536
|
+
expect(html1).not.toContain('body { padding: 0; }')
|
|
537
537
|
|
|
538
|
-
expect(html2).toContain(
|
|
539
|
-
expect(html2).not.toContain(
|
|
540
|
-
expect(html2).toContain(
|
|
541
|
-
expect(html2).not.toContain(
|
|
538
|
+
expect(html2).toContain('color: blue;')
|
|
539
|
+
expect(html2).not.toContain('color: red;')
|
|
540
|
+
expect(html2).toContain('body { padding: 0; }')
|
|
541
|
+
expect(html2).not.toContain('body { margin: 0; }')
|
|
542
542
|
})
|
|
543
543
|
|
|
544
|
-
it(
|
|
544
|
+
it('handles many concurrent requests', () => {
|
|
545
545
|
const requests = Array.from({ length: 50 }, (_, i) => {
|
|
546
546
|
const s = createSheet()
|
|
547
547
|
s.insert(`color: color-${i};`)
|
|
@@ -557,108 +557,108 @@ describe("StyleSheet -- advanced features", () => {
|
|
|
557
557
|
})
|
|
558
558
|
})
|
|
559
559
|
|
|
560
|
-
describe(
|
|
561
|
-
it(
|
|
562
|
-
const s = createSheet({ layer:
|
|
560
|
+
describe('@layer support', () => {
|
|
561
|
+
it('wraps inserted rules in @layer when configured', () => {
|
|
562
|
+
const s = createSheet({ layer: 'components' })
|
|
563
563
|
|
|
564
|
-
s.insert(
|
|
564
|
+
s.insert('color: red;')
|
|
565
565
|
const styles = s.getStyles()
|
|
566
566
|
|
|
567
|
-
expect(styles).toContain(
|
|
568
|
-
expect(styles).toContain(
|
|
567
|
+
expect(styles).toContain('@layer components')
|
|
568
|
+
expect(styles).toContain('color: red;')
|
|
569
569
|
})
|
|
570
570
|
|
|
571
|
-
it(
|
|
571
|
+
it('does not wrap rules without layer option', () => {
|
|
572
572
|
const s = createSheet()
|
|
573
573
|
|
|
574
|
-
s.insert(
|
|
574
|
+
s.insert('color: red;')
|
|
575
575
|
const styles = s.getStyles()
|
|
576
576
|
|
|
577
|
-
expect(styles).not.toContain(
|
|
578
|
-
expect(styles).toContain(
|
|
577
|
+
expect(styles).not.toContain('@layer')
|
|
578
|
+
expect(styles).toContain('color: red;')
|
|
579
579
|
})
|
|
580
580
|
|
|
581
|
-
it(
|
|
582
|
-
const s = createSheet({ layer:
|
|
581
|
+
it('does not wrap @keyframes in @layer', () => {
|
|
582
|
+
const s = createSheet({ layer: 'components' })
|
|
583
583
|
|
|
584
|
-
s.insertKeyframes(
|
|
584
|
+
s.insertKeyframes('fadeIn', 'from { opacity: 0; } to { opacity: 1; }')
|
|
585
585
|
const styles = s.getStyles()
|
|
586
586
|
|
|
587
|
-
expect(styles).toContain(
|
|
587
|
+
expect(styles).toContain('@keyframes fadeIn')
|
|
588
588
|
// Keyframes are not wrapped
|
|
589
589
|
expect(styles).not.toMatch(/@layer.*@keyframes/)
|
|
590
590
|
})
|
|
591
591
|
|
|
592
|
-
it(
|
|
593
|
-
const s = createSheet({ layer:
|
|
592
|
+
it('does not wrap global rules in @layer', () => {
|
|
593
|
+
const s = createSheet({ layer: 'components' })
|
|
594
594
|
|
|
595
|
-
s.insertGlobal(
|
|
595
|
+
s.insertGlobal('body { margin: 0; }')
|
|
596
596
|
const styles = s.getStyles()
|
|
597
597
|
|
|
598
|
-
expect(styles).toContain(
|
|
598
|
+
expect(styles).toContain('body { margin: 0; }')
|
|
599
599
|
expect(styles).not.toMatch(/@layer components\{body/)
|
|
600
600
|
})
|
|
601
601
|
|
|
602
|
-
it(
|
|
603
|
-
const s = createSheet({ layer:
|
|
604
|
-
const result = s.prepare(
|
|
605
|
-
expect(result.rules).toContain(
|
|
606
|
-
expect(result.rules).toContain(
|
|
602
|
+
it('prepare() wraps rules in @layer when configured', () => {
|
|
603
|
+
const s = createSheet({ layer: 'components' })
|
|
604
|
+
const result = s.prepare('color: red;')
|
|
605
|
+
expect(result.rules).toContain('@layer components')
|
|
606
|
+
expect(result.rules).toContain('color: red;')
|
|
607
607
|
})
|
|
608
608
|
|
|
609
|
-
it(
|
|
610
|
-
const s = createSheet({ layer:
|
|
611
|
-
const result = s.prepare(
|
|
612
|
-
expect(result.rules).toContain(
|
|
609
|
+
it('prepare() with boost wraps in @layer', () => {
|
|
610
|
+
const s = createSheet({ layer: 'lib' })
|
|
611
|
+
const result = s.prepare('color: blue;', true)
|
|
612
|
+
expect(result.rules).toContain('@layer lib')
|
|
613
613
|
expect(result.rules).toContain(`.${result.className}.${result.className}`)
|
|
614
614
|
})
|
|
615
615
|
})
|
|
616
616
|
|
|
617
|
-
describe(
|
|
618
|
-
it(
|
|
617
|
+
describe('SSR output', () => {
|
|
618
|
+
it('getStyleTag returns complete <style> tag', () => {
|
|
619
619
|
const s = createSheet()
|
|
620
|
-
s.insert(
|
|
620
|
+
s.insert('color: red;')
|
|
621
621
|
const tag = s.getStyleTag()
|
|
622
622
|
|
|
623
623
|
expect(tag).toMatch(/^<style data-pyreon-styler="">.*<\/style>$/)
|
|
624
|
-
expect(tag).toContain(
|
|
624
|
+
expect(tag).toContain('color: red;')
|
|
625
625
|
})
|
|
626
626
|
|
|
627
|
-
it(
|
|
627
|
+
it('getStyles returns raw CSS', () => {
|
|
628
628
|
const s = createSheet()
|
|
629
|
-
s.insert(
|
|
630
|
-
s.insert(
|
|
629
|
+
s.insert('color: red;')
|
|
630
|
+
s.insert('color: blue;')
|
|
631
631
|
const styles = s.getStyles()
|
|
632
632
|
|
|
633
|
-
expect(styles).not.toContain(
|
|
634
|
-
expect(styles).toContain(
|
|
635
|
-
expect(styles).toContain(
|
|
633
|
+
expect(styles).not.toContain('<style')
|
|
634
|
+
expect(styles).toContain('color: red;')
|
|
635
|
+
expect(styles).toContain('color: blue;')
|
|
636
636
|
})
|
|
637
637
|
|
|
638
|
-
it(
|
|
638
|
+
it('reset clears SSR buffer and cache', () => {
|
|
639
639
|
const s = createSheet()
|
|
640
|
-
s.insert(
|
|
641
|
-
expect(s.getStyles()).toContain(
|
|
640
|
+
s.insert('color: red;')
|
|
641
|
+
expect(s.getStyles()).toContain('color: red;')
|
|
642
642
|
|
|
643
643
|
s.reset()
|
|
644
|
-
expect(s.getStyles()).toBe(
|
|
644
|
+
expect(s.getStyles()).toBe('')
|
|
645
645
|
expect(s.cacheSize).toBe(0) // cache also cleared for SSR correctness
|
|
646
646
|
})
|
|
647
647
|
|
|
648
|
-
it(
|
|
648
|
+
it('insertKeyframes deduplicates in SSR', () => {
|
|
649
649
|
const s = createSheet()
|
|
650
|
-
s.insertKeyframes(
|
|
651
|
-
s.insertKeyframes(
|
|
650
|
+
s.insertKeyframes('fadeIn', 'from { opacity: 0; } to { opacity: 1; }')
|
|
651
|
+
s.insertKeyframes('fadeIn', 'from { opacity: 0; } to { opacity: 1; }')
|
|
652
652
|
|
|
653
653
|
const styles = s.getStyles()
|
|
654
654
|
const matches = styles.match(/@keyframes fadeIn/g)
|
|
655
655
|
expect(matches).toHaveLength(1)
|
|
656
656
|
})
|
|
657
657
|
|
|
658
|
-
it(
|
|
658
|
+
it('insertGlobal deduplicates in SSR', () => {
|
|
659
659
|
const s = createSheet()
|
|
660
|
-
s.insertGlobal(
|
|
661
|
-
s.insertGlobal(
|
|
660
|
+
s.insertGlobal('body { margin: 0; }')
|
|
661
|
+
s.insertGlobal('body { margin: 0; }')
|
|
662
662
|
|
|
663
663
|
const styles = s.getStyles()
|
|
664
664
|
const matches = styles.match(/body \{ margin: 0; \}/g)
|