@pyreon/compiler 0.13.1 → 0.15.0

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.
@@ -0,0 +1,129 @@
1
+ // @vitest-environment happy-dom
2
+ /// <reference lib="dom" />
3
+ import { computed, signal } from '@pyreon/reactivity'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { flush } from '@pyreon/test-utils/browser'
6
+ import { compileAndMount } from './harness'
7
+
8
+ /**
9
+ * Compiler-runtime tests — signal patterns in JSX.
10
+ *
11
+ * The #352 signal-method auto-call bug surfaced because the compiler
12
+ * couldn't tell `signal.set(x)` (call on the signal as object) from
13
+ * `signal()` (call the signal to read). The fix added scope-aware
14
+ * detection. This file pins down the matrix: bare reference, function
15
+ * call, member call, accessor wrapper, computed — in different positions.
16
+ */
17
+
18
+ describe('compiler-runtime — signals', () => {
19
+ it('signal in text position is reactive', async () => {
20
+ const name = signal('alice')
21
+ const { container, unmount } = compileAndMount(
22
+ `<div><span id="s">{name()}</span></div>`,
23
+ { name },
24
+ )
25
+ expect(container.querySelector('#s')!.textContent).toBe('alice')
26
+ name.set('bob')
27
+ await flush()
28
+ expect(container.querySelector('#s')!.textContent).toBe('bob')
29
+ unmount()
30
+ })
31
+
32
+ it('signal.method() in event handler does not auto-call signal', () => {
33
+ const x = signal(0)
34
+ const { container, unmount } = compileAndMount(
35
+ `<div><button id="b" onClick={() => x.set(99)}>set</button></div>`,
36
+ { x },
37
+ )
38
+ container.querySelector<HTMLButtonElement>('#b')!.click()
39
+ expect(x()).toBe(99)
40
+ unmount()
41
+ })
42
+
43
+ it('signal.update() in event handler does not auto-call signal', () => {
44
+ const x = signal(10)
45
+ const { container, unmount } = compileAndMount(
46
+ `<div><button id="b" onClick={() => x.update((v) => v * 2)}>x2</button></div>`,
47
+ { x },
48
+ )
49
+ const btn = container.querySelector<HTMLButtonElement>('#b')!
50
+ btn.click()
51
+ btn.click()
52
+ expect(x()).toBe(40)
53
+ unmount()
54
+ })
55
+
56
+ it('signal.peek() in event handler does not auto-call signal', () => {
57
+ const x = signal(7)
58
+ const out = { value: 0 }
59
+ const { container, unmount } = compileAndMount(
60
+ `<div><button id="b" onClick={() => { out.value = x.peek() }}>read</button></div>`,
61
+ { x, out },
62
+ )
63
+ container.querySelector<HTMLButtonElement>('#b')!.click()
64
+ expect(out.value).toBe(7)
65
+ unmount()
66
+ })
67
+
68
+ it('computed value reflected in DOM updates when source changes', async () => {
69
+ const a = signal(2)
70
+ const b = signal(3)
71
+ const sum = computed(() => a() + b())
72
+ const { container, unmount } = compileAndMount(
73
+ `<div><span id="s">{sum()}</span></div>`,
74
+ { sum },
75
+ )
76
+ expect(container.querySelector('#s')!.textContent).toBe('5')
77
+ a.set(10)
78
+ await flush()
79
+ expect(container.querySelector('#s')!.textContent).toBe('13')
80
+ unmount()
81
+ })
82
+
83
+ it('explicit accessor wrapper preserves reactivity', async () => {
84
+ const x = signal('hi')
85
+ const { container, unmount } = compileAndMount(
86
+ `<div><span id="s">{() => x()}</span></div>`,
87
+ { x },
88
+ )
89
+ expect(container.querySelector('#s')!.textContent).toBe('hi')
90
+ x.set('hey')
91
+ await flush()
92
+ expect(container.querySelector('#s')!.textContent).toBe('hey')
93
+ unmount()
94
+ })
95
+
96
+ it('signal in attribute position is reactive', async () => {
97
+ const cls = signal('a')
98
+ const { container, unmount } = compileAndMount(
99
+ `<div><span id="s" class={cls()}>x</span></div>`,
100
+ { cls },
101
+ )
102
+ expect(container.querySelector('#s')!.className).toBe('a')
103
+ cls.set('b')
104
+ await flush()
105
+ expect(container.querySelector('#s')!.className).toBe('b')
106
+ unmount()
107
+ })
108
+
109
+ it('multiple signals on the same element track independently', async () => {
110
+ const txt = signal('hello')
111
+ const cls = signal('a')
112
+ const { container, unmount } = compileAndMount(
113
+ `<div><span id="s" class={cls()}>{txt()}</span></div>`,
114
+ { txt, cls },
115
+ )
116
+ const span = container.querySelector('#s')!
117
+ expect(span.textContent).toBe('hello')
118
+ expect(span.className).toBe('a')
119
+ txt.set('world')
120
+ await flush()
121
+ expect(span.textContent).toBe('world')
122
+ expect(span.className).toBe('a')
123
+ cls.set('b')
124
+ await flush()
125
+ expect(span.textContent).toBe('world')
126
+ expect(span.className).toBe('b')
127
+ unmount()
128
+ })
129
+ })
@@ -0,0 +1,106 @@
1
+ // @vitest-environment happy-dom
2
+ /// <reference lib="dom" />
3
+ import { signal } from '@pyreon/reactivity'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { flush } from '@pyreon/test-utils/browser'
6
+ import { compileAndMount } from './harness'
7
+
8
+ /**
9
+ * Compiler-runtime tests — JSX text/expression whitespace handling.
10
+ *
11
+ * The #352 whitespace bug stripped same-line spaces adjacent to
12
+ * expressions: `<p>doubled: {x}</p>` rendered "doubled:0" instead of
13
+ * "doubled: 0". The fix implements React/Babel's
14
+ * `cleanJSXElementLiteralChild` algorithm. This file pins down the
15
+ * matrix: same-line text±expression, multi-line text, fragments,
16
+ * leading/trailing/internal whitespace.
17
+ */
18
+
19
+ describe('compiler-runtime — JSX whitespace', () => {
20
+ it('preserves trailing space before expression on same line', async () => {
21
+ const x = signal(7)
22
+ const { container, unmount } = compileAndMount(
23
+ `<div><p id="p">doubled: {x()}</p></div>`,
24
+ { x },
25
+ )
26
+ expect(container.querySelector('#p')!.textContent).toBe('doubled: 7')
27
+ unmount()
28
+ })
29
+
30
+ // Regression: when an expression sits BETWEEN static text segments
31
+ // on the same line, the compiler used to append the dynamic text
32
+ // node to the parent's children AFTER all static text via
33
+ // `appendChild`. Whitespace was preserved correctly (#352), but
34
+ // positioning was wrong:
35
+ // `<p>{x()} remaining</p>` → template `<p> remaining</p>` +
36
+ // appended text → renders as " remaining3" instead of "3 remaining".
37
+ // Fix: extend `analyzeChildren.useMixed` to fire whenever ≥2 of
38
+ // {element, text, expression} are present (not just element+nonElement).
39
+ // Then placeholder-based positional mounting puts the dynamic text in
40
+ // the right slot via `replaceChild`.
41
+ it('preserves leading space after expression on same line', async () => {
42
+ const x = signal(3)
43
+ const { container, unmount } = compileAndMount(
44
+ `<div><p id="p">{x()} remaining</p></div>`,
45
+ { x },
46
+ )
47
+ expect(container.querySelector('#p')!.textContent).toBe('3 remaining')
48
+ unmount()
49
+ })
50
+
51
+ it('preserves spaces on BOTH sides of expression', async () => {
52
+ const x = signal('cat')
53
+ const { container, unmount } = compileAndMount(
54
+ `<div><p id="p">a {x()} b</p></div>`,
55
+ { x },
56
+ )
57
+ expect(container.querySelector('#p')!.textContent).toBe('a cat b')
58
+ unmount()
59
+ })
60
+
61
+ it('multi-line JSX with indentation collapses correctly', async () => {
62
+ const x = signal('inner')
63
+ // Multi-line JSX expression. Whitespace inside the JSX literal between
64
+ // <p> and {x()} is treated by React/Babel cleanJSX as "indentation"
65
+ // and collapses; same for between {x()} and </p>.
66
+ const { container, unmount } = compileAndMount(
67
+ `<div>
68
+ <p id="p">
69
+ {x()}
70
+ </p>
71
+ </div>`,
72
+ { x },
73
+ )
74
+ expect(container.querySelector('#p')!.textContent?.trim()).toBe('inner')
75
+ unmount()
76
+ })
77
+
78
+ it('reactive text updates without losing surrounding whitespace', async () => {
79
+ const x = signal(0)
80
+ const { container, unmount } = compileAndMount(
81
+ `<div><p id="p">count: {x()} items</p></div>`,
82
+ { x },
83
+ )
84
+ expect(container.querySelector('#p')!.textContent).toBe('count: 0 items')
85
+ x.set(42)
86
+ await flush()
87
+ expect(container.querySelector('#p')!.textContent).toBe('count: 42 items')
88
+ unmount()
89
+ })
90
+
91
+ it('reactive text at end of paragraph updates correctly', async () => {
92
+ // The expression-at-END shape works because the dynamic text node is
93
+ // appended to the parent — which happens to match the source order
94
+ // when there's no text after.
95
+ const x = signal(0)
96
+ const { container, unmount } = compileAndMount(
97
+ `<div><p id="p">count: {x()}</p></div>`,
98
+ { x },
99
+ )
100
+ expect(container.querySelector('#p')!.textContent).toBe('count: 0')
101
+ x.set(42)
102
+ await flush()
103
+ expect(container.querySelector('#p')!.textContent).toBe('count: 42')
104
+ unmount()
105
+ })
106
+ })