@pyreon/styler 0.11.0 → 0.11.2

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.
Files changed (36) hide show
  1. package/package.json +12 -10
  2. package/src/ThemeProvider.ts +37 -0
  3. package/src/__tests__/ThemeProvider.test.ts +67 -0
  4. package/src/__tests__/benchmark.bench.ts +189 -0
  5. package/src/__tests__/composition-chain.test.ts +489 -0
  6. package/src/__tests__/css.test.ts +70 -0
  7. package/src/__tests__/forward.test.ts +282 -0
  8. package/src/__tests__/globalStyle.test.ts +72 -0
  9. package/src/__tests__/hash.test.ts +70 -0
  10. package/src/__tests__/hybrid-injection.test.ts +205 -0
  11. package/src/__tests__/index.ts +14 -0
  12. package/src/__tests__/insertion-effect.test.ts +106 -0
  13. package/src/__tests__/integration.test.ts +149 -0
  14. package/src/__tests__/keyframes.test.ts +68 -0
  15. package/src/__tests__/memory-growth.test.ts +152 -0
  16. package/src/__tests__/p3-features.test.ts +258 -0
  17. package/src/__tests__/resolve.test.ts +249 -0
  18. package/src/__tests__/shared.test.ts +73 -0
  19. package/src/__tests__/sheet-advanced.test.ts +669 -0
  20. package/src/__tests__/sheet-split-atrules.test.ts +411 -0
  21. package/src/__tests__/sheet.test.ts +164 -0
  22. package/src/__tests__/styled-ssr.test.ts +67 -0
  23. package/src/__tests__/styled.test.ts +303 -0
  24. package/src/__tests__/theme.test.ts +33 -0
  25. package/src/__tests__/useCSS.test.ts +142 -0
  26. package/src/css.ts +13 -0
  27. package/src/forward.ts +276 -0
  28. package/src/globalStyle.ts +48 -0
  29. package/src/hash.ts +30 -0
  30. package/src/index.ts +15 -0
  31. package/src/keyframes.ts +36 -0
  32. package/src/resolve.ts +172 -0
  33. package/src/shared.ts +12 -0
  34. package/src/sheet.ts +387 -0
  35. package/src/styled.tsx +277 -0
  36. package/src/useCSS.ts +20 -0
@@ -0,0 +1,106 @@
1
+ import type { VNode } from "@pyreon/core"
2
+ import { describe, expect, it } from "vitest"
3
+ import { styled } from "../styled"
4
+
5
+ describe("style injection (className generation)", () => {
6
+ describe("dynamic styled components", () => {
7
+ it("generates a proper className for dynamic CSS", () => {
8
+ const Comp = styled("div")`
9
+ color: ${(props: any) => props.$color};
10
+ `
11
+
12
+ const vnode = Comp({ $color: "red" }) as VNode
13
+
14
+ // Class should be present and properly generated
15
+ expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
16
+ })
17
+
18
+ it("works with multiple dynamic components", () => {
19
+ const Comp1 = styled("div")`color: ${(p: any) => p.$c};`
20
+ const Comp2 = styled("span")`font-size: ${(p: any) => p.$s};`
21
+
22
+ const vnode1 = Comp1({ $c: "red" }) as VNode
23
+ const vnode2 = Comp2({ $s: "16px" }) as VNode
24
+
25
+ expect(vnode1.props.class).toMatch(/^pyr-/)
26
+ expect(vnode2.props.class).toMatch(/^pyr-/)
27
+ expect(vnode1.props.class).not.toBe(vnode2.props.class)
28
+ })
29
+
30
+ it("handles different prop values producing different classNames", () => {
31
+ const Comp = styled("div")`color: ${(p: any) => p.$color};`
32
+
33
+ const colors = ["blue", "green", "yellow", "purple", "orange"]
34
+ const classNames = new Set<string>()
35
+
36
+ for (const color of colors) {
37
+ const vnode = Comp({ $color: color }) as VNode
38
+ expect(vnode.props.class).toMatch(/^pyr-/)
39
+ classNames.add(vnode.props.class as string)
40
+ }
41
+
42
+ // Different colors should produce different classNames
43
+ expect(classNames.size).toBe(colors.length)
44
+ })
45
+
46
+ it("same dynamic CSS produces same className", () => {
47
+ const Comp = styled("div")`color: ${(p: any) => p.$color};`
48
+
49
+ const vnode1 = Comp({ $color: "red" }) as VNode
50
+ const cls1 = vnode1.props.class as string
51
+
52
+ const _vnode2 = Comp({ $color: "blue" }) as VNode
53
+
54
+ const vnode3 = Comp({ $color: "red" }) as VNode // back to red
55
+ const cls3 = vnode3.props.class as string
56
+
57
+ // Same resolved CSS -> same className
58
+ expect(cls1).toBe(cls3)
59
+ })
60
+ })
61
+
62
+ describe("static styled components", () => {
63
+ it("static components compute class at creation time", () => {
64
+ // Static components compute class at creation time
65
+ const Comp = styled("div")`display: flex; color: red;`
66
+
67
+ const vnode = Comp({}) as VNode
68
+
69
+ expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
70
+ })
71
+
72
+ it("static className is stable across calls", () => {
73
+ const Comp = styled("div")`display: flex;`
74
+
75
+ const vnode1 = Comp({}) as VNode
76
+ const cls1 = vnode1.props.class as string
77
+
78
+ const _vnode2 = Comp({}) as VNode
79
+ const vnode3 = Comp({}) as VNode
80
+ const cls3 = vnode3.props.class as string
81
+
82
+ expect(cls1).toBe(cls3)
83
+ })
84
+ })
85
+
86
+ describe("theme-dependent components", () => {
87
+ it("produces different className for different resolved CSS", () => {
88
+ // In Pyreon, theme is accessed via useTheme() inside the component.
89
+ // Without ThemeProvider context, theme is {} (default).
90
+ // We test via direct prop-based dynamic interpolation instead.
91
+ const Comp = styled("div")`
92
+ background: ${(p: any) => p.$bg};
93
+ `
94
+
95
+ const vnode1 = Comp({ $bg: "white" }) as VNode
96
+ const cls1 = vnode1.props.class as string
97
+
98
+ const vnode2 = Comp({ $bg: "black" }) as VNode
99
+ const cls2 = vnode2.props.class as string
100
+
101
+ expect(cls1).not.toBe(cls2)
102
+ expect(cls1).toMatch(/^pyr-/)
103
+ expect(cls2).toMatch(/^pyr-/)
104
+ })
105
+ })
106
+ })
@@ -0,0 +1,149 @@
1
+ import type { VNode } from "@pyreon/core"
2
+ import { describe, expect, it } from "vitest"
3
+ import { css } from "../css"
4
+ import { styled } from "../styled"
5
+
6
+ describe("integration", () => {
7
+ describe("nested css results", () => {
8
+ it("resolves nested css tagged templates", () => {
9
+ const flexCSS = css`display: flex;`
10
+ const Comp = styled("div")`
11
+ ${flexCSS}
12
+ color: red;
13
+ `
14
+ const vnode = Comp({}) as VNode
15
+ expect(vnode.props.class).toMatch(/^pyr-/)
16
+ })
17
+
18
+ it("resolves conditional css (logical AND pattern)", () => {
19
+ const isWeb = true
20
+ const Comp = styled("div")`
21
+ ${isWeb && css`box-sizing: border-box;`};
22
+ display: flex;
23
+ `
24
+ const vnode = Comp({}) as VNode
25
+ expect(vnode.props.class).toMatch(/^pyr-/)
26
+ })
27
+
28
+ it("handles false conditional css", () => {
29
+ const isWeb = false
30
+ const Comp = styled("div")`
31
+ ${isWeb && css`box-sizing: border-box;`};
32
+ display: flex;
33
+ `
34
+ const vnode = Comp({}) as VNode
35
+ expect(vnode.props.class).toMatch(/^pyr-/)
36
+ })
37
+ })
38
+
39
+ describe("array interpolations (makeItResponsive pattern)", () => {
40
+ it("resolves array of css results (simulating breakpoints)", () => {
41
+ // Simulates what makeItResponsive returns: array of css results per breakpoint
42
+ const breakpointStyles = [
43
+ css`color: red;`,
44
+ css`@media (min-width: 48em) { color: blue; }`,
45
+ css`@media (min-width: 62em) { color: green; }`,
46
+ ]
47
+
48
+ const Comp = styled("div")`
49
+ display: flex;
50
+ ${breakpointStyles};
51
+ `
52
+ const vnode = Comp({}) as VNode
53
+ expect(vnode.props.class).toMatch(/^pyr-/)
54
+ })
55
+
56
+ it("resolves function returning array (makeItResponsive full pattern)", () => {
57
+ // makeItResponsive returns a function (props) => CSSResult[]
58
+ const responsiveFn = (props: any) => {
59
+ const theme = props.$element || {}
60
+ return [
61
+ css`color: ${theme.color || "black"};`,
62
+ theme.breakpoint ? css`@media (min-width: 48em) { color: ${theme.breakpoint}; }` : "",
63
+ ]
64
+ }
65
+
66
+ const Comp = styled("div")`
67
+ display: flex;
68
+ ${responsiveFn};
69
+ `
70
+ const vnode = Comp({ $element: { color: "red", breakpoint: "blue" } }) as VNode
71
+ expect(vnode.props.class).toMatch(/^pyr-/)
72
+ })
73
+ })
74
+
75
+ describe("createMediaQueries pattern", () => {
76
+ it("css called as function (css(...args)) wrapping in @media", () => {
77
+ // Simulates createMediaQueries: builds functions that call css(...args)
78
+ const createMedia = (breakpoint: number, rootSize: number) => {
79
+ const emSize = breakpoint / rootSize
80
+ return (...args: any[]) =>
81
+ css`@media only screen and (min-width: ${emSize}em) {
82
+ ${css(...(args as [TemplateStringsArray, ...any[]]))};
83
+ }`
84
+ }
85
+
86
+ const md = createMedia(768, 16)
87
+ const result = md`color: blue;`
88
+
89
+ // Wrap in a styled component
90
+ const Comp = styled("div")`
91
+ color: red;
92
+ ${result};
93
+ `
94
+ const vnode = Comp({}) as VNode
95
+ expect(vnode.props.class).toMatch(/^pyr-/)
96
+ })
97
+
98
+ it("zero-breakpoint passthrough (css(...args) without @media)", () => {
99
+ // Breakpoint 0 means no @media wrapper
100
+ const passthrough = (...args: any[]) => css(...(args as [TemplateStringsArray, ...any[]]))
101
+
102
+ const result = passthrough`color: red;`
103
+
104
+ const Comp = styled("div")`
105
+ ${result};
106
+ `
107
+ const vnode = Comp({}) as VNode
108
+ expect(vnode.props.class).toMatch(/^pyr-/)
109
+ })
110
+ })
111
+
112
+ describe("complex styled patterns", () => {
113
+ it("function interpolation with prop-based conditional", () => {
114
+ const Comp = styled("div")`
115
+ display: flex;
116
+ ${({ $contentType }: any) => $contentType === "content" && "flex: 1;"};
117
+ `
118
+ const vnode = Comp({ $contentType: "content" }) as VNode
119
+ expect(vnode.props.class).toMatch(/^pyr-/)
120
+ })
121
+
122
+ it("platform-specific CSS (compile-time constant)", () => {
123
+ const __WEB__ = true
124
+ const platformCSS = __WEB__ ? "box-sizing: border-box;" : ""
125
+
126
+ const Comp = styled("div")`
127
+ ${platformCSS};
128
+ display: flex;
129
+ `
130
+ const vnode = Comp({}) as VNode
131
+ expect(vnode.props.class).toMatch(/^pyr-/)
132
+ })
133
+
134
+ it("multiple function interpolations", () => {
135
+ const Comp = styled("div")`
136
+ color: ${(p: any) => p.$color || "black"};
137
+ font-size: ${(p: any) => p.$size || "16px"};
138
+ `
139
+ const vnode = Comp({ $color: "red", $size: "20px" }) as VNode
140
+ expect(vnode.props.class).toMatch(/^pyr-/)
141
+ })
142
+
143
+ it("empty template produces no className", () => {
144
+ const Comp = styled("div")``
145
+ const vnode = Comp({}) as VNode
146
+ expect(vnode.props.class).toBeFalsy()
147
+ })
148
+ })
149
+ })
@@ -0,0 +1,68 @@
1
+ import { afterEach, describe, expect, it } from "vitest"
2
+ import { keyframes } from "../keyframes"
3
+ import { sheet } from "../sheet"
4
+
5
+ describe("keyframes", () => {
6
+ afterEach(() => {
7
+ sheet.reset()
8
+ })
9
+
10
+ it("returns a KeyframesResult with a name property", () => {
11
+ const fadeIn = keyframes`
12
+ from { opacity: 0; }
13
+ to { opacity: 1; }
14
+ `
15
+ expect(fadeIn.name).toMatch(/^pyr-kf-/)
16
+ })
17
+
18
+ it("returns pyr-kf- prefix", () => {
19
+ const fadeIn = keyframes`
20
+ from { opacity: 0; }
21
+ to { opacity: 1; }
22
+ `
23
+ expect(fadeIn.name).toMatch(/^pyr-kf-[0-9a-z]+$/)
24
+ })
25
+
26
+ it("is deterministic — same input produces same name", () => {
27
+ const a = keyframes`from { opacity: 0; } to { opacity: 1; }`
28
+ const b = keyframes`from { opacity: 0; } to { opacity: 1; }`
29
+ expect(a.name).toBe(b.name)
30
+ })
31
+
32
+ it("different input produces different names", () => {
33
+ const fadeIn = keyframes`from { opacity: 0; } to { opacity: 1; }`
34
+ const slideIn = keyframes`from { transform: translateX(-100%); } to { transform: translateX(0); }`
35
+ expect(fadeIn.name).not.toBe(slideIn.name)
36
+ })
37
+
38
+ it("supports interpolation values", () => {
39
+ const from = 0
40
+ const to = 1
41
+ const anim = keyframes`
42
+ from { opacity: ${from}; }
43
+ to { opacity: ${to}; }
44
+ `
45
+ expect(anim.name).toMatch(/^pyr-kf-/)
46
+ })
47
+
48
+ it("toString returns the animation name", () => {
49
+ const fadeIn = keyframes`from { opacity: 0; } to { opacity: 1; }`
50
+ expect(fadeIn.toString()).toBe(fadeIn.name)
51
+ })
52
+
53
+ it("can be used in template literals for animation property", () => {
54
+ const fadeIn = keyframes`from { opacity: 0; } to { opacity: 1; }`
55
+ const animationValue = `${fadeIn} 0.3s ease-in`
56
+ expect(animationValue).toContain(fadeIn.name)
57
+ expect(animationValue).toContain("0.3s ease-in")
58
+ })
59
+
60
+ it("handles complex keyframe definitions", () => {
61
+ const pulse = keyframes`
62
+ 0% { transform: scale(1); }
63
+ 50% { transform: scale(1.1); }
64
+ 100% { transform: scale(1); }
65
+ `
66
+ expect(pulse.name).toMatch(/^pyr-kf-/)
67
+ })
68
+ })
@@ -0,0 +1,152 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
2
+ import { createSheet } from "../sheet"
3
+
4
+ describe("memory growth", () => {
5
+ describe("bounded cache prevents unbounded growth (DOM mode)", () => {
6
+ it("cache stays bounded with maxCacheSize", () => {
7
+ const maxSize = 50
8
+ const s = createSheet({ maxCacheSize: maxSize })
9
+
10
+ for (let i = 0; i < maxSize * 3; i++) {
11
+ s.insert(`property-${i}: value-${i};`)
12
+ }
13
+
14
+ expect(s.cacheSize).toBeLessThanOrEqual(maxSize * 1.5)
15
+ })
16
+
17
+ it("cache eviction preserves recent entries", () => {
18
+ const maxSize = 20
19
+ const s = createSheet({ maxCacheSize: maxSize })
20
+
21
+ for (let i = 0; i < maxSize; i++) {
22
+ s.insert(`old-prop-${i}: old-val-${i};`)
23
+ }
24
+
25
+ const recentClasses: string[] = []
26
+ for (let i = 0; i < 5; i++) {
27
+ recentClasses.push(s.insert(`new-prop-${i}: new-val-${i};`))
28
+ }
29
+
30
+ for (let i = 0; i < 5; i++) {
31
+ const cls = s.insert(`new-prop-${i}: new-val-${i};`)
32
+ expect(cls).toBe(recentClasses[i])
33
+ }
34
+ })
35
+
36
+ it("handles rapid insertions without memory issues", () => {
37
+ const s = createSheet({ maxCacheSize: 100 })
38
+ const iterations = 1000
39
+
40
+ for (let i = 0; i < iterations; i++) {
41
+ s.insert(`rapid-${i}: value;`)
42
+ }
43
+
44
+ expect(s.cacheSize).toBeLessThan(iterations)
45
+ expect(s.cacheSize).toBeGreaterThan(0)
46
+ })
47
+ })
48
+
49
+ describe("default cache (large limit, DOM mode)", () => {
50
+ beforeEach(() => {
51
+ document.querySelectorAll("style[data-pyreon-styler]").forEach((el) => {
52
+ el.remove()
53
+ })
54
+ })
55
+
56
+ it("default cache handles many unique rules", () => {
57
+ const s = createSheet()
58
+
59
+ for (let i = 0; i < 500; i++) {
60
+ s.insert(`default-prop-${i}: value-${i};`)
61
+ }
62
+
63
+ expect(s.cacheSize).toBe(500)
64
+ })
65
+
66
+ it("deduplication prevents growth from repeated rules", () => {
67
+ const s = createSheet()
68
+
69
+ for (let cycle = 0; cycle < 100; cycle++) {
70
+ for (let i = 0; i < 10; i++) {
71
+ s.insert(`repeated-${i}: value;`)
72
+ }
73
+ }
74
+
75
+ expect(s.cacheSize).toBe(10)
76
+ })
77
+ })
78
+
79
+ describe("SSR mode memory", () => {
80
+ let originalDocument: typeof document
81
+
82
+ beforeEach(() => {
83
+ originalDocument = globalThis.document
84
+ // @ts-expect-error - intentionally deleting for SSR simulation
85
+ delete globalThis.document
86
+ })
87
+
88
+ afterEach(() => {
89
+ globalThis.document = originalDocument
90
+ })
91
+
92
+ it("reset prevents SSR buffer accumulation across requests", () => {
93
+ const s = createSheet()
94
+
95
+ for (let i = 0; i < 100; i++) {
96
+ s.insert(`req1-prop-${i}: value;`)
97
+ }
98
+ expect(s.getStyles().length).toBeGreaterThan(0)
99
+
100
+ s.reset()
101
+ expect(s.getStyles()).toBe("")
102
+
103
+ s.insert("req2-single: value;")
104
+ expect(s.getStyles()).not.toContain("req1-prop")
105
+ })
106
+
107
+ it("keyframes cache does not grow unboundedly", () => {
108
+ const s = createSheet({ maxCacheSize: 20 })
109
+
110
+ for (let i = 0; i < 50; i++) {
111
+ s.insertKeyframes(`anim-${i}`, `from { opacity: ${i}; } to { opacity: 1; }`)
112
+ }
113
+
114
+ expect(s.cacheSize).toBeLessThanOrEqual(50)
115
+ })
116
+
117
+ it("global rules cache does not grow unboundedly", () => {
118
+ const s = createSheet({ maxCacheSize: 20 })
119
+
120
+ for (let i = 0; i < 50; i++) {
121
+ s.insertGlobal(`body { prop${i}: val${i}; }`)
122
+ }
123
+
124
+ expect(s.cacheSize).toBeLessThanOrEqual(50)
125
+ })
126
+
127
+ it("SSR buffer grows with unique rules (expected behavior)", () => {
128
+ const s = createSheet()
129
+ const ruleCount = 100
130
+
131
+ for (let i = 0; i < ruleCount; i++) {
132
+ s.insert(`ssr-prop-${i}: value;`)
133
+ }
134
+
135
+ const styles = s.getStyles()
136
+ for (let i = 0; i < ruleCount; i++) {
137
+ expect(styles).toContain(`ssr-prop-${i}`)
138
+ }
139
+ })
140
+
141
+ it("SSR buffer does not duplicate identical rules", () => {
142
+ const s = createSheet()
143
+
144
+ for (let cycle = 0; cycle < 10; cycle++) {
145
+ s.insert("color: red;")
146
+ }
147
+
148
+ const matches = s.getStyles().match(/color: red;/g)
149
+ expect(matches).toHaveLength(1)
150
+ })
151
+ })
152
+ })