@pyreon/compiler 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.
@@ -3,88 +3,88 @@ import {
3
3
  diagnoseError,
4
4
  hasReactPatterns,
5
5
  migrateReactCode,
6
- } from "../react-intercept"
6
+ } from '../react-intercept'
7
7
 
8
8
  // ─── hasReactPatterns ────────────────────────────────────────────────────────
9
9
 
10
- describe("hasReactPatterns", () => {
11
- test("returns true for React import", () => {
10
+ describe('hasReactPatterns', () => {
11
+ test('returns true for React import', () => {
12
12
  expect(hasReactPatterns(`import { useState } from "react"`)).toBe(true)
13
13
  })
14
14
 
15
- test("returns true for react-dom import", () => {
15
+ test('returns true for react-dom import', () => {
16
16
  expect(hasReactPatterns(`import { createRoot } from "react-dom/client"`)).toBe(true)
17
17
  })
18
18
 
19
- test("returns true for react-router import", () => {
19
+ test('returns true for react-router import', () => {
20
20
  expect(hasReactPatterns(`import { Link } from "react-router-dom"`)).toBe(true)
21
21
  })
22
22
 
23
- test("returns true for useState call", () => {
24
- expect(hasReactPatterns("const [a, b] = useState(0)")).toBe(true)
23
+ test('returns true for useState call', () => {
24
+ expect(hasReactPatterns('const [a, b] = useState(0)')).toBe(true)
25
25
  })
26
26
 
27
- test("returns true for useState with type parameter", () => {
28
- expect(hasReactPatterns("const [a, b] = useState<number>(0)")).toBe(true)
27
+ test('returns true for useState with type parameter', () => {
28
+ expect(hasReactPatterns('const [a, b] = useState<number>(0)')).toBe(true)
29
29
  })
30
30
 
31
- test("returns true for useEffect call", () => {
32
- expect(hasReactPatterns("useEffect(() => {}, [])")).toBe(true)
31
+ test('returns true for useEffect call', () => {
32
+ expect(hasReactPatterns('useEffect(() => {}, [])')).toBe(true)
33
33
  })
34
34
 
35
- test("returns true for useMemo call", () => {
36
- expect(hasReactPatterns("useMemo(() => x * 2, [x])")).toBe(true)
35
+ test('returns true for useMemo call', () => {
36
+ expect(hasReactPatterns('useMemo(() => x * 2, [x])')).toBe(true)
37
37
  })
38
38
 
39
- test("returns true for useCallback call", () => {
40
- expect(hasReactPatterns("useCallback(() => doThing(), [])")).toBe(true)
39
+ test('returns true for useCallback call', () => {
40
+ expect(hasReactPatterns('useCallback(() => doThing(), [])')).toBe(true)
41
41
  })
42
42
 
43
- test("returns true for useRef call", () => {
44
- expect(hasReactPatterns("useRef(null)")).toBe(true)
43
+ test('returns true for useRef call', () => {
44
+ expect(hasReactPatterns('useRef(null)')).toBe(true)
45
45
  })
46
46
 
47
- test("returns true for useRef with type parameter", () => {
48
- expect(hasReactPatterns("useRef<HTMLDivElement>(null)")).toBe(true)
47
+ test('returns true for useRef with type parameter', () => {
48
+ expect(hasReactPatterns('useRef<HTMLDivElement>(null)')).toBe(true)
49
49
  })
50
50
 
51
- test("returns true for useReducer call", () => {
52
- expect(hasReactPatterns("useReducer(reducer, init)")).toBe(true)
51
+ test('returns true for useReducer call', () => {
52
+ expect(hasReactPatterns('useReducer(reducer, init)')).toBe(true)
53
53
  })
54
54
 
55
- test("returns true for useReducer with type parameter", () => {
56
- expect(hasReactPatterns("useReducer<State>(reducer, init)")).toBe(true)
55
+ test('returns true for useReducer with type parameter', () => {
56
+ expect(hasReactPatterns('useReducer<State>(reducer, init)')).toBe(true)
57
57
  })
58
58
 
59
- test("returns true for React.memo", () => {
60
- expect(hasReactPatterns("React.memo(MyComponent)")).toBe(true)
59
+ test('returns true for React.memo', () => {
60
+ expect(hasReactPatterns('React.memo(MyComponent)')).toBe(true)
61
61
  })
62
62
 
63
- test("returns true for forwardRef call", () => {
64
- expect(hasReactPatterns("forwardRef((props, ref) => {})")).toBe(true)
63
+ test('returns true for forwardRef call', () => {
64
+ expect(hasReactPatterns('forwardRef((props, ref) => {})')).toBe(true)
65
65
  })
66
66
 
67
- test("returns true for forwardRef with type parameter", () => {
68
- expect(hasReactPatterns("forwardRef<HTMLInputElement>((props, ref) => {})")).toBe(true)
67
+ test('returns true for forwardRef with type parameter', () => {
68
+ expect(hasReactPatterns('forwardRef<HTMLInputElement>((props, ref) => {})')).toBe(true)
69
69
  })
70
70
 
71
- test("returns true for className attribute", () => {
71
+ test('returns true for className attribute', () => {
72
72
  expect(hasReactPatterns('className="foo"')).toBe(true)
73
73
  })
74
74
 
75
- test("returns true for className with space", () => {
76
- expect(hasReactPatterns("className ")).toBe(true)
75
+ test('returns true for className with space', () => {
76
+ expect(hasReactPatterns('className ')).toBe(true)
77
77
  })
78
78
 
79
- test("returns true for htmlFor attribute", () => {
79
+ test('returns true for htmlFor attribute', () => {
80
80
  expect(hasReactPatterns('htmlFor="name"')).toBe(true)
81
81
  })
82
82
 
83
- test("returns true for .value assignment", () => {
84
- expect(hasReactPatterns("count.value = 5")).toBe(true)
83
+ test('returns true for .value assignment', () => {
84
+ expect(hasReactPatterns('count.value = 5')).toBe(true)
85
85
  })
86
86
 
87
- test("returns false for pure Pyreon code", () => {
87
+ test('returns false for pure Pyreon code', () => {
88
88
  expect(
89
89
  hasReactPatterns(`
90
90
  import { signal, effect, computed } from "@pyreon/reactivity"
@@ -94,265 +94,265 @@ describe("hasReactPatterns", () => {
94
94
  ).toBe(false)
95
95
  })
96
96
 
97
- test("returns false for empty code", () => {
98
- expect(hasReactPatterns("")).toBe(false)
97
+ test('returns false for empty code', () => {
98
+ expect(hasReactPatterns('')).toBe(false)
99
99
  })
100
100
 
101
- test("returns false for plain JavaScript", () => {
102
- expect(hasReactPatterns("const x = 42\nfunction foo() { return x }")).toBe(false)
101
+ test('returns false for plain JavaScript', () => {
102
+ expect(hasReactPatterns('const x = 42\nfunction foo() { return x }')).toBe(false)
103
103
  })
104
104
  })
105
105
 
106
106
  // ─── detectReactPatterns ─────────────────────────────────────────────────────
107
107
 
108
- describe("detectReactPatterns", () => {
109
- test("detects React import", () => {
108
+ describe('detectReactPatterns', () => {
109
+ test('detects React import', () => {
110
110
  const diags = detectReactPatterns(`import { useState } from "react"`)
111
- const importDiag = diags.find((d) => d.code === "react-import")
111
+ const importDiag = diags.find((d) => d.code === 'react-import')
112
112
  expect(importDiag).toBeDefined()
113
- expect(importDiag!.suggested).toContain("@pyreon/core")
113
+ expect(importDiag!.suggested).toContain('@pyreon/core')
114
114
  expect(importDiag!.fixable).toBe(true)
115
115
  })
116
116
 
117
- test("detects react-dom import", () => {
117
+ test('detects react-dom import', () => {
118
118
  const diags = detectReactPatterns(`import { createRoot } from "react-dom/client"`)
119
- const importDiag = diags.find((d) => d.code === "react-dom-import")
119
+ const importDiag = diags.find((d) => d.code === 'react-dom-import')
120
120
  expect(importDiag).toBeDefined()
121
- expect(importDiag!.suggested).toContain("@pyreon/runtime-dom")
121
+ expect(importDiag!.suggested).toContain('@pyreon/runtime-dom')
122
122
  expect(importDiag!.fixable).toBe(true)
123
123
  })
124
124
 
125
- test("detects react-router import", () => {
125
+ test('detects react-router import', () => {
126
126
  const diags = detectReactPatterns(`import { Link } from "react-router-dom"`)
127
- const importDiag = diags.find((d) => d.code === "react-router-import")
127
+ const importDiag = diags.find((d) => d.code === 'react-router-import')
128
128
  expect(importDiag).toBeDefined()
129
- expect(importDiag!.suggested).toContain("@pyreon/router")
129
+ expect(importDiag!.suggested).toContain('@pyreon/router')
130
130
  })
131
131
 
132
- test("detects useState with destructuring", () => {
133
- const diags = detectReactPatterns("const [count, setCount] = useState(0)")
134
- const d = diags.find((d) => d.code === "use-state")
132
+ test('detects useState with destructuring', () => {
133
+ const diags = detectReactPatterns('const [count, setCount] = useState(0)')
134
+ const d = diags.find((d) => d.code === 'use-state')
135
135
  expect(d).toBeDefined()
136
- expect(d!.message).toContain("signal")
137
- expect(d!.suggested).toContain("count = signal(0)")
136
+ expect(d!.message).toContain('signal')
137
+ expect(d!.suggested).toContain('count = signal(0)')
138
138
  expect(d!.fixable).toBe(true)
139
139
  })
140
140
 
141
- test("detects useState without array destructuring", () => {
142
- const diags = detectReactPatterns("const state = useState(0)")
143
- const d = diags.find((d) => d.code === "use-state")
141
+ test('detects useState without array destructuring', () => {
142
+ const diags = detectReactPatterns('const state = useState(0)')
143
+ const d = diags.find((d) => d.code === 'use-state')
144
144
  expect(d).toBeDefined()
145
- expect(d!.suggested).toContain("signal")
145
+ expect(d!.suggested).toContain('signal')
146
146
  })
147
147
 
148
- test("detects useEffect with empty deps (mount pattern)", () => {
148
+ test('detects useEffect with empty deps (mount pattern)', () => {
149
149
  const code = `useEffect(() => { console.log("mounted") }, [])`
150
150
  const diags = detectReactPatterns(code)
151
- const d = diags.find((d) => d.code === "use-effect-mount")
151
+ const d = diags.find((d) => d.code === 'use-effect-mount')
152
152
  expect(d).toBeDefined()
153
- expect(d!.message).toContain("onMount")
154
- expect(d!.suggested).toContain("onMount")
153
+ expect(d!.message).toContain('onMount')
154
+ expect(d!.suggested).toContain('onMount')
155
155
  expect(d!.fixable).toBe(true)
156
156
  })
157
157
 
158
- test("detects useEffect with empty deps and cleanup", () => {
158
+ test('detects useEffect with empty deps and cleanup', () => {
159
159
  const code = `useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id) }, [])`
160
160
  const diags = detectReactPatterns(code)
161
- const d = diags.find((d) => d.code === "use-effect-mount")
161
+ const d = diags.find((d) => d.code === 'use-effect-mount')
162
162
  expect(d).toBeDefined()
163
- expect(d!.suggested).toContain("cleanup")
163
+ expect(d!.suggested).toContain('cleanup')
164
164
  })
165
165
 
166
- test("detects useEffect with dependency array", () => {
167
- const code = "useEffect(() => { document.title = count }, [count])"
166
+ test('detects useEffect with dependency array', () => {
167
+ const code = 'useEffect(() => { document.title = count }, [count])'
168
168
  const diags = detectReactPatterns(code)
169
- const d = diags.find((d) => d.code === "use-effect-deps")
169
+ const d = diags.find((d) => d.code === 'use-effect-deps')
170
170
  expect(d).toBeDefined()
171
- expect(d!.message).toContain("auto-tracks")
171
+ expect(d!.message).toContain('auto-tracks')
172
172
  expect(d!.fixable).toBe(true)
173
173
  })
174
174
 
175
- test("detects useEffect with no dependency array", () => {
175
+ test('detects useEffect with no dependency array', () => {
176
176
  const code = "useEffect(() => { console.log('render') })"
177
177
  const diags = detectReactPatterns(code)
178
- const d = diags.find((d) => d.code === "use-effect-no-deps")
178
+ const d = diags.find((d) => d.code === 'use-effect-no-deps')
179
179
  expect(d).toBeDefined()
180
- expect(d!.message).toContain("auto-tracks")
180
+ expect(d!.message).toContain('auto-tracks')
181
181
  expect(d!.fixable).toBe(true)
182
182
  })
183
183
 
184
- test("detects useLayoutEffect", () => {
185
- const code = "useLayoutEffect(() => { measure() }, [])"
184
+ test('detects useLayoutEffect', () => {
185
+ const code = 'useLayoutEffect(() => { measure() }, [])'
186
186
  const diags = detectReactPatterns(code)
187
- const d = diags.find((d) => d.code === "use-effect-mount")
187
+ const d = diags.find((d) => d.code === 'use-effect-mount')
188
188
  expect(d).toBeDefined()
189
- expect(d!.message).toContain("useLayoutEffect")
189
+ expect(d!.message).toContain('useLayoutEffect')
190
190
  })
191
191
 
192
- test("detects useMemo", () => {
193
- const code = "const doubled = useMemo(() => count * 2, [count])"
192
+ test('detects useMemo', () => {
193
+ const code = 'const doubled = useMemo(() => count * 2, [count])'
194
194
  const diags = detectReactPatterns(code)
195
- const d = diags.find((d) => d.code === "use-memo")
195
+ const d = diags.find((d) => d.code === 'use-memo')
196
196
  expect(d).toBeDefined()
197
- expect(d!.message).toContain("computed")
198
- expect(d!.suggested).toContain("computed")
197
+ expect(d!.message).toContain('computed')
198
+ expect(d!.suggested).toContain('computed')
199
199
  expect(d!.fixable).toBe(true)
200
200
  })
201
201
 
202
- test("detects useCallback", () => {
203
- const code = "const handleClick = useCallback(() => doThing(), [doThing])"
202
+ test('detects useCallback', () => {
203
+ const code = 'const handleClick = useCallback(() => doThing(), [doThing])'
204
204
  const diags = detectReactPatterns(code)
205
- const d = diags.find((d) => d.code === "use-callback")
205
+ const d = diags.find((d) => d.code === 'use-callback')
206
206
  expect(d).toBeDefined()
207
- expect(d!.message).toContain("not needed")
208
- expect(d!.suggested).toContain("() => doThing()")
207
+ expect(d!.message).toContain('not needed')
208
+ expect(d!.suggested).toContain('() => doThing()')
209
209
  expect(d!.fixable).toBe(true)
210
210
  })
211
211
 
212
- test("detects useRef with null (DOM ref)", () => {
213
- const code = "const inputRef = useRef(null)"
212
+ test('detects useRef with null (DOM ref)', () => {
213
+ const code = 'const inputRef = useRef(null)'
214
214
  const diags = detectReactPatterns(code)
215
- const d = diags.find((d) => d.code === "use-ref-dom")
215
+ const d = diags.find((d) => d.code === 'use-ref-dom')
216
216
  expect(d).toBeDefined()
217
- expect(d!.message).toContain("createRef")
218
- expect(d!.suggested).toContain("createRef()")
217
+ expect(d!.message).toContain('createRef')
218
+ expect(d!.suggested).toContain('createRef()')
219
219
  expect(d!.fixable).toBe(true)
220
220
  })
221
221
 
222
- test("detects useRef with value (mutable box)", () => {
223
- const code = "const prevCount = useRef(0)"
222
+ test('detects useRef with value (mutable box)', () => {
223
+ const code = 'const prevCount = useRef(0)'
224
224
  const diags = detectReactPatterns(code)
225
- const d = diags.find((d) => d.code === "use-ref-box")
225
+ const d = diags.find((d) => d.code === 'use-ref-box')
226
226
  expect(d).toBeDefined()
227
- expect(d!.message).toContain("signal")
228
- expect(d!.suggested).toContain("signal(0)")
227
+ expect(d!.message).toContain('signal')
228
+ expect(d!.suggested).toContain('signal(0)')
229
229
  expect(d!.fixable).toBe(true)
230
230
  })
231
231
 
232
- test("detects useReducer", () => {
233
- const code = "const [state, dispatch] = useReducer(reducer, initialState)"
232
+ test('detects useReducer', () => {
233
+ const code = 'const [state, dispatch] = useReducer(reducer, initialState)'
234
234
  const diags = detectReactPatterns(code)
235
- const d = diags.find((d) => d.code === "use-reducer")
235
+ const d = diags.find((d) => d.code === 'use-reducer')
236
236
  expect(d).toBeDefined()
237
- expect(d!.message).toContain("signal")
237
+ expect(d!.message).toContain('signal')
238
238
  expect(d!.fixable).toBe(false)
239
239
  })
240
240
 
241
- test("detects memo() wrapper", () => {
242
- const code = "const MyComp = memo(function MyComp() { return <div /> })"
241
+ test('detects memo() wrapper', () => {
242
+ const code = 'const MyComp = memo(function MyComp() { return <div /> })'
243
243
  const diags = detectReactPatterns(code)
244
- const d = diags.find((d) => d.code === "memo-wrapper")
244
+ const d = diags.find((d) => d.code === 'memo-wrapper')
245
245
  expect(d).toBeDefined()
246
- expect(d!.message).toContain("not needed")
246
+ expect(d!.message).toContain('not needed')
247
247
  expect(d!.fixable).toBe(true)
248
248
  })
249
249
 
250
- test("detects React.memo() wrapper", () => {
251
- const code = "const MyComp = React.memo(function MyComp() { return <div /> })"
250
+ test('detects React.memo() wrapper', () => {
251
+ const code = 'const MyComp = React.memo(function MyComp() { return <div /> })'
252
252
  const diags = detectReactPatterns(code)
253
- const d = diags.find((d) => d.code === "memo-wrapper")
253
+ const d = diags.find((d) => d.code === 'memo-wrapper')
254
254
  expect(d).toBeDefined()
255
255
  })
256
256
 
257
- test("detects forwardRef()", () => {
258
- const code = "const Input = forwardRef((props, ref) => <input ref={ref} />)"
257
+ test('detects forwardRef()', () => {
258
+ const code = 'const Input = forwardRef((props, ref) => <input ref={ref} />)'
259
259
  const diags = detectReactPatterns(code)
260
- const d = diags.find((d) => d.code === "forward-ref")
260
+ const d = diags.find((d) => d.code === 'forward-ref')
261
261
  expect(d).toBeDefined()
262
- expect(d!.message).toContain("not needed")
262
+ expect(d!.message).toContain('not needed')
263
263
  expect(d!.fixable).toBe(true)
264
264
  })
265
265
 
266
- test("detects React.forwardRef()", () => {
267
- const code = "const Input = React.forwardRef((props, ref) => <input ref={ref} />)"
266
+ test('detects React.forwardRef()', () => {
267
+ const code = 'const Input = React.forwardRef((props, ref) => <input ref={ref} />)'
268
268
  const diags = detectReactPatterns(code)
269
- const d = diags.find((d) => d.code === "forward-ref")
269
+ const d = diags.find((d) => d.code === 'forward-ref')
270
270
  expect(d).toBeDefined()
271
271
  })
272
272
 
273
- test("detects className attribute", () => {
273
+ test('detects className attribute', () => {
274
274
  const code = 'const el = <div className="container">Hello</div>'
275
275
  const diags = detectReactPatterns(code)
276
- const d = diags.find((d) => d.code === "class-name-prop")
276
+ const d = diags.find((d) => d.code === 'class-name-prop')
277
277
  expect(d).toBeDefined()
278
- expect(d!.message).toContain("class")
279
- expect(d!.suggested).toContain("class")
278
+ expect(d!.message).toContain('class')
279
+ expect(d!.suggested).toContain('class')
280
280
  expect(d!.fixable).toBe(true)
281
281
  })
282
282
 
283
- test("detects htmlFor attribute", () => {
283
+ test('detects htmlFor attribute', () => {
284
284
  const code = 'const el = <label htmlFor="name">Name</label>'
285
285
  const diags = detectReactPatterns(code)
286
- const d = diags.find((d) => d.code === "html-for-prop")
286
+ const d = diags.find((d) => d.code === 'html-for-prop')
287
287
  expect(d).toBeDefined()
288
- expect(d!.message).toContain("for")
289
- expect(d!.suggested).toContain("for")
288
+ expect(d!.message).toContain('for')
289
+ expect(d!.suggested).toContain('for')
290
290
  expect(d!.fixable).toBe(true)
291
291
  })
292
292
 
293
- test("detects onChange on input", () => {
294
- const code = "const el = <input onChange={handleChange} />"
293
+ test('detects onChange on input', () => {
294
+ const code = 'const el = <input onChange={handleChange} />'
295
295
  const diags = detectReactPatterns(code)
296
- const d = diags.find((d) => d.code === "on-change-input")
296
+ const d = diags.find((d) => d.code === 'on-change-input')
297
297
  expect(d).toBeDefined()
298
- expect(d!.message).toContain("onInput")
299
- expect(d!.suggested).toContain("onInput")
298
+ expect(d!.message).toContain('onInput')
299
+ expect(d!.suggested).toContain('onInput')
300
300
  expect(d!.fixable).toBe(true)
301
301
  })
302
302
 
303
- test("detects onChange on textarea", () => {
304
- const code = "const el = <textarea onChange={handleChange} />"
303
+ test('detects onChange on textarea', () => {
304
+ const code = 'const el = <textarea onChange={handleChange} />'
305
305
  const diags = detectReactPatterns(code)
306
- const d = diags.find((d) => d.code === "on-change-input")
306
+ const d = diags.find((d) => d.code === 'on-change-input')
307
307
  expect(d).toBeDefined()
308
- expect(d!.message).toContain("textarea")
308
+ expect(d!.message).toContain('textarea')
309
309
  })
310
310
 
311
- test("detects onChange on select", () => {
312
- const code = "const el = <select onChange={handleChange} />"
311
+ test('detects onChange on select', () => {
312
+ const code = 'const el = <select onChange={handleChange} />'
313
313
  const diags = detectReactPatterns(code)
314
- const d = diags.find((d) => d.code === "on-change-input")
314
+ const d = diags.find((d) => d.code === 'on-change-input')
315
315
  expect(d).toBeDefined()
316
- expect(d!.message).toContain("select")
316
+ expect(d!.message).toContain('select')
317
317
  })
318
318
 
319
- test("does NOT detect onChange on non-input element", () => {
320
- const code = "const el = <div onChange={handleChange} />"
319
+ test('does NOT detect onChange on non-input element', () => {
320
+ const code = 'const el = <div onChange={handleChange} />'
321
321
  const diags = detectReactPatterns(code)
322
- const d = diags.find((d) => d.code === "on-change-input")
322
+ const d = diags.find((d) => d.code === 'on-change-input')
323
323
  expect(d).toBeUndefined()
324
324
  })
325
325
 
326
- test("detects dangerouslySetInnerHTML", () => {
326
+ test('detects dangerouslySetInnerHTML', () => {
327
327
  const code = 'const el = <div dangerouslySetInnerHTML={{ __html: "<b>hi</b>" }} />'
328
328
  const diags = detectReactPatterns(code)
329
- const d = diags.find((d) => d.code === "dangerously-set-inner-html")
329
+ const d = diags.find((d) => d.code === 'dangerously-set-inner-html')
330
330
  expect(d).toBeDefined()
331
- expect(d!.message).toContain("innerHTML")
332
- expect(d!.suggested).toContain("innerHTML")
331
+ expect(d!.message).toContain('innerHTML')
332
+ expect(d!.suggested).toContain('innerHTML')
333
333
  expect(d!.fixable).toBe(true)
334
334
  })
335
335
 
336
- test("detects .value assignment on signal-like variable", () => {
337
- const code = "count.value = 5"
336
+ test('detects .value assignment on signal-like variable', () => {
337
+ const code = 'count.value = 5'
338
338
  const diags = detectReactPatterns(code)
339
- const d = diags.find((d) => d.code === "dot-value-signal")
339
+ const d = diags.find((d) => d.code === 'dot-value-signal')
340
340
  expect(d).toBeDefined()
341
- expect(d!.message).toContain("Vue ref")
342
- expect(d!.suggested).toContain("count.set(5)")
341
+ expect(d!.message).toContain('Vue ref')
342
+ expect(d!.suggested).toContain('count.set(5)')
343
343
  expect(d!.fixable).toBe(false)
344
344
  })
345
345
 
346
- test("detects .map() in JSX expression", () => {
347
- const code = "const el = <ul>{items.map(item => <li>{item}</li>)}</ul>"
346
+ test('detects .map() in JSX expression', () => {
347
+ const code = 'const el = <ul>{items.map(item => <li>{item}</li>)}</ul>'
348
348
  const diags = detectReactPatterns(code)
349
- const d = diags.find((d) => d.code === "array-map-jsx")
349
+ const d = diags.find((d) => d.code === 'array-map-jsx')
350
350
  expect(d).toBeDefined()
351
- expect(d!.message).toContain("<For>")
351
+ expect(d!.message).toContain('<For>')
352
352
  expect(d!.fixable).toBe(false)
353
353
  })
354
354
 
355
- test("returns empty array for pure Pyreon code", () => {
355
+ test('returns empty array for pure Pyreon code', () => {
356
356
  const code = `
357
357
  import { signal, effect } from "@pyreon/reactivity"
358
358
  const count = signal(0)
@@ -363,16 +363,16 @@ const el = <div class="foo">{count()}</div>
363
363
  expect(diags).toEqual([])
364
364
  })
365
365
 
366
- test("includes correct line and column information", () => {
367
- const code = "const [count, setCount] = useState(0)"
366
+ test('includes correct line and column information', () => {
367
+ const code = 'const [count, setCount] = useState(0)'
368
368
  const diags = detectReactPatterns(code)
369
- const d = diags.find((d) => d.code === "use-state")
369
+ const d = diags.find((d) => d.code === 'use-state')
370
370
  expect(d).toBeDefined()
371
371
  expect(d!.line).toBe(1)
372
372
  expect(d!.column).toBeGreaterThanOrEqual(0)
373
373
  })
374
374
 
375
- test("detects multiple patterns in one file", () => {
375
+ test('detects multiple patterns in one file', () => {
376
376
  const code = `
377
377
  import { useState, useEffect, useMemo } from "react"
378
378
  const [count, setCount] = useState(0)
@@ -380,159 +380,159 @@ useEffect(() => {}, [])
380
380
  const doubled = useMemo(() => count * 2, [count])
381
381
  `
382
382
  const diags = detectReactPatterns(code)
383
- expect(diags.find((d) => d.code === "react-import")).toBeDefined()
384
- expect(diags.find((d) => d.code === "use-state")).toBeDefined()
385
- expect(diags.find((d) => d.code === "use-effect-mount")).toBeDefined()
386
- expect(diags.find((d) => d.code === "use-memo")).toBeDefined()
383
+ expect(diags.find((d) => d.code === 'react-import')).toBeDefined()
384
+ expect(diags.find((d) => d.code === 'use-state')).toBeDefined()
385
+ expect(diags.find((d) => d.code === 'use-effect-mount')).toBeDefined()
386
+ expect(diags.find((d) => d.code === 'use-memo')).toBeDefined()
387
387
  expect(diags.length).toBeGreaterThanOrEqual(4)
388
388
  })
389
389
  })
390
390
 
391
391
  // ─── migrateReactCode ────────────────────────────────────────────────────────
392
392
 
393
- describe("migrateReactCode", () => {
394
- test("rewrites useState to signal", () => {
393
+ describe('migrateReactCode', () => {
394
+ test('rewrites useState to signal', () => {
395
395
  const code = `const [count, setCount] = useState(0)`
396
396
  const result = migrateReactCode(code)
397
- expect(result.code).toContain("count = signal(0)")
398
- expect(result.code).not.toContain("useState")
397
+ expect(result.code).toContain('count = signal(0)')
398
+ expect(result.code).not.toContain('useState')
399
399
  expect(result.changes.length).toBeGreaterThan(0)
400
400
  })
401
401
 
402
- test("rewrites useEffect with deps to effect", () => {
402
+ test('rewrites useEffect with deps to effect', () => {
403
403
  const code = `useEffect(() => { console.log(count) }, [count])`
404
404
  const result = migrateReactCode(code)
405
- expect(result.code).toContain("effect(")
406
- expect(result.code).not.toContain("useEffect")
405
+ expect(result.code).toContain('effect(')
406
+ expect(result.code).not.toContain('useEffect')
407
407
  })
408
408
 
409
- test("rewrites useEffect with empty deps to onMount", () => {
409
+ test('rewrites useEffect with empty deps to onMount', () => {
410
410
  const code = `useEffect(() => { setup() }, [])`
411
411
  const result = migrateReactCode(code)
412
- expect(result.code).toContain("onMount(")
413
- expect(result.code).not.toContain("useEffect")
412
+ expect(result.code).toContain('onMount(')
413
+ expect(result.code).not.toContain('useEffect')
414
414
  })
415
415
 
416
- test("rewrites useEffect with no deps to effect", () => {
416
+ test('rewrites useEffect with no deps to effect', () => {
417
417
  const code = `useEffect(() => { console.log("render") })`
418
418
  const result = migrateReactCode(code)
419
- expect(result.code).toContain("effect(")
420
- expect(result.code).not.toContain("useEffect")
419
+ expect(result.code).toContain('effect(')
420
+ expect(result.code).not.toContain('useEffect')
421
421
  })
422
422
 
423
- test("rewrites useMemo to computed", () => {
423
+ test('rewrites useMemo to computed', () => {
424
424
  const code = `const doubled = useMemo(() => count * 2, [count])`
425
425
  const result = migrateReactCode(code)
426
- expect(result.code).toContain("computed(")
427
- expect(result.code).not.toContain("useMemo")
426
+ expect(result.code).toContain('computed(')
427
+ expect(result.code).not.toContain('useMemo')
428
428
  })
429
429
 
430
- test("removes useCallback wrapper", () => {
430
+ test('removes useCallback wrapper', () => {
431
431
  const code = `const handleClick = useCallback(() => doThing(), [doThing])`
432
432
  const result = migrateReactCode(code)
433
- expect(result.code).toContain("() => doThing()")
434
- expect(result.code).not.toContain("useCallback")
433
+ expect(result.code).toContain('() => doThing()')
434
+ expect(result.code).not.toContain('useCallback')
435
435
  })
436
436
 
437
- test("rewrites useRef(null) to createRef()", () => {
437
+ test('rewrites useRef(null) to createRef()', () => {
438
438
  const code = `const inputRef = useRef(null)`
439
439
  const result = migrateReactCode(code)
440
- expect(result.code).toContain("createRef()")
441
- expect(result.code).not.toContain("useRef")
440
+ expect(result.code).toContain('createRef()')
441
+ expect(result.code).not.toContain('useRef')
442
442
  })
443
443
 
444
- test("rewrites useRef(value) to signal(value)", () => {
444
+ test('rewrites useRef(value) to signal(value)', () => {
445
445
  const code = `const prevCount = useRef(0)`
446
446
  const result = migrateReactCode(code)
447
- expect(result.code).toContain("signal(0)")
448
- expect(result.code).not.toContain("useRef")
447
+ expect(result.code).toContain('signal(0)')
448
+ expect(result.code).not.toContain('useRef')
449
449
  })
450
450
 
451
- test("rewrites useRef(undefined) to createRef()", () => {
451
+ test('rewrites useRef(undefined) to createRef()', () => {
452
452
  const code = `const ref = useRef(undefined)`
453
453
  const result = migrateReactCode(code)
454
- expect(result.code).toContain("createRef()")
454
+ expect(result.code).toContain('createRef()')
455
455
  })
456
456
 
457
- test("removes memo() wrapper", () => {
457
+ test('removes memo() wrapper', () => {
458
458
  const code = `const MyComp = memo(function MyComp() { return <div /> })`
459
459
  const result = migrateReactCode(code)
460
- expect(result.code).not.toContain("memo(")
461
- expect(result.code).toContain("function MyComp()")
460
+ expect(result.code).not.toContain('memo(')
461
+ expect(result.code).toContain('function MyComp()')
462
462
  })
463
463
 
464
- test("removes React.memo() wrapper", () => {
464
+ test('removes React.memo() wrapper', () => {
465
465
  const code = `const MyComp = React.memo(function MyComp() { return <div /> })`
466
466
  const result = migrateReactCode(code)
467
- expect(result.code).not.toContain("React.memo")
468
- expect(result.code).toContain("function MyComp()")
467
+ expect(result.code).not.toContain('React.memo')
468
+ expect(result.code).toContain('function MyComp()')
469
469
  })
470
470
 
471
- test("removes forwardRef() wrapper", () => {
471
+ test('removes forwardRef() wrapper', () => {
472
472
  const code = `const Input = forwardRef((props, ref) => <input ref={ref} />)`
473
473
  const result = migrateReactCode(code)
474
- expect(result.code).not.toContain("forwardRef")
475
- expect(result.code).toContain("(props, ref) =>")
474
+ expect(result.code).not.toContain('forwardRef')
475
+ expect(result.code).toContain('(props, ref) =>')
476
476
  })
477
477
 
478
- test("removes React.forwardRef() wrapper", () => {
478
+ test('removes React.forwardRef() wrapper', () => {
479
479
  const code = `const Input = React.forwardRef((props, ref) => <input ref={ref} />)`
480
480
  const result = migrateReactCode(code)
481
- expect(result.code).not.toContain("forwardRef")
482
- expect(result.code).toContain("(props, ref) =>")
481
+ expect(result.code).not.toContain('forwardRef')
482
+ expect(result.code).toContain('(props, ref) =>')
483
483
  })
484
484
 
485
- test("rewrites className to class", () => {
485
+ test('rewrites className to class', () => {
486
486
  const code = `const el = <div className="container">Hello</div>`
487
487
  const result = migrateReactCode(code)
488
488
  expect(result.code).toContain('class="container"')
489
- expect(result.code).not.toContain("className")
489
+ expect(result.code).not.toContain('className')
490
490
  })
491
491
 
492
- test("rewrites onChange to onInput on input", () => {
492
+ test('rewrites onChange to onInput on input', () => {
493
493
  const code = `const el = <input onChange={handleChange} />`
494
494
  const result = migrateReactCode(code)
495
- expect(result.code).toContain("onInput")
496
- expect(result.code).not.toContain("onChange")
495
+ expect(result.code).toContain('onInput')
496
+ expect(result.code).not.toContain('onChange')
497
497
  })
498
498
 
499
- test("rewrites onChange to onInput on textarea", () => {
499
+ test('rewrites onChange to onInput on textarea', () => {
500
500
  const code = `const el = <textarea onChange={handleChange} />`
501
501
  const result = migrateReactCode(code)
502
- expect(result.code).toContain("onInput")
502
+ expect(result.code).toContain('onInput')
503
503
  })
504
504
 
505
- test("rewrites onChange to onInput on select", () => {
505
+ test('rewrites onChange to onInput on select', () => {
506
506
  const code = `const el = <select onChange={handleChange} />`
507
507
  const result = migrateReactCode(code)
508
- expect(result.code).toContain("onInput")
508
+ expect(result.code).toContain('onInput')
509
509
  })
510
510
 
511
- test("rewrites React imports to Pyreon imports", () => {
511
+ test('rewrites React imports to Pyreon imports', () => {
512
512
  const code = `import { useState, useEffect, useMemo } from "react"
513
513
  const [count, setCount] = useState(0)
514
514
  useEffect(() => { console.log(count) }, [count])
515
515
  const doubled = useMemo(() => count * 2, [count])`
516
516
  const result = migrateReactCode(code)
517
517
  expect(result.code).not.toContain(`from "react"`)
518
- expect(result.code).toContain("@pyreon/reactivity")
518
+ expect(result.code).toContain('@pyreon/reactivity')
519
519
  })
520
520
 
521
- test("rewrites dangerouslySetInnerHTML to innerHTML", () => {
521
+ test('rewrites dangerouslySetInnerHTML to innerHTML', () => {
522
522
  const code = `const el = <div dangerouslySetInnerHTML={{ __html: htmlString }} />`
523
523
  const result = migrateReactCode(code)
524
- expect(result.code).toContain("innerHTML={htmlString}")
525
- expect(result.code).not.toContain("dangerouslySetInnerHTML")
524
+ expect(result.code).toContain('innerHTML={htmlString}')
525
+ expect(result.code).not.toContain('dangerouslySetInnerHTML')
526
526
  })
527
527
 
528
- test("returns change descriptions", () => {
528
+ test('returns change descriptions', () => {
529
529
  const code = `const [count, setCount] = useState(0)`
530
530
  const result = migrateReactCode(code)
531
531
  expect(result.changes.length).toBeGreaterThan(0)
532
- expect(result.changes[0]!.description).toContain("useState")
532
+ expect(result.changes[0]!.description).toContain('useState')
533
533
  })
534
534
 
535
- test("handles code with no React patterns (no changes)", () => {
535
+ test('handles code with no React patterns (no changes)', () => {
536
536
  const code = `const count = signal(0)\neffect(() => console.log(count()))`
537
537
  const result = migrateReactCode(code)
538
538
  expect(result.code).toBe(code)
@@ -540,14 +540,14 @@ const doubled = useMemo(() => count * 2, [count])`
540
540
  expect(result.diagnostics).toEqual([])
541
541
  })
542
542
 
543
- test("includes diagnostics in migration result", () => {
543
+ test('includes diagnostics in migration result', () => {
544
544
  const code = `import { useState } from "react"\nconst [count, setCount] = useState(0)`
545
545
  const result = migrateReactCode(code)
546
546
  expect(result.diagnostics.length).toBeGreaterThan(0)
547
- expect(result.diagnostics.find((d) => d.code === "use-state")).toBeDefined()
547
+ expect(result.diagnostics.find((d) => d.code === 'use-state')).toBeDefined()
548
548
  })
549
549
 
550
- test("adds correct Pyreon imports", () => {
550
+ test('adds correct Pyreon imports', () => {
551
551
  const code = `import { useState, useMemo } from "react"
552
552
  const [count, setCount] = useState(0)
553
553
  const doubled = useMemo(() => count * 2, [count])`
@@ -555,16 +555,16 @@ const doubled = useMemo(() => count * 2, [count])`
555
555
  expect(result.code).toContain(`import { computed, signal } from "@pyreon/reactivity"`)
556
556
  })
557
557
 
558
- test("rewrites react-dom/client import specifiers", () => {
558
+ test('rewrites react-dom/client import specifiers', () => {
559
559
  const code = `import { createRoot } from "react-dom/client"
560
560
  createRoot(document.getElementById("root"))`
561
561
  const result = migrateReactCode(code)
562
- expect(result.code).not.toContain("react-dom")
563
- expect(result.code).toContain("@pyreon/runtime-dom")
564
- expect(result.code).toContain("mount")
562
+ expect(result.code).not.toContain('react-dom')
563
+ expect(result.code).toContain('@pyreon/runtime-dom')
564
+ expect(result.code).toContain('mount')
565
565
  })
566
566
 
567
- test("migrates full React component", () => {
567
+ test('migrates full React component', () => {
568
568
  const code = `import { useState, useEffect, useMemo, useCallback, useRef, memo } from "react"
569
569
 
570
570
  const Counter = memo(function Counter() {
@@ -580,123 +580,123 @@ const Counter = memo(function Counter() {
580
580
  return <div className="counter">{count}</div>
581
581
  })`
582
582
  const result = migrateReactCode(code)
583
- expect(result.code).not.toContain("useState")
584
- expect(result.code).not.toContain("useMemo")
585
- expect(result.code).not.toContain("useCallback")
586
- expect(result.code).not.toContain("useRef")
587
- expect(result.code).not.toContain("className")
588
- expect(result.code).toContain("signal")
589
- expect(result.code).toContain("computed")
590
- expect(result.code).toContain("effect")
591
- expect(result.code).toContain("createRef")
592
- expect(result.code).toContain("class=")
583
+ expect(result.code).not.toContain('useState')
584
+ expect(result.code).not.toContain('useMemo')
585
+ expect(result.code).not.toContain('useCallback')
586
+ expect(result.code).not.toContain('useRef')
587
+ expect(result.code).not.toContain('className')
588
+ expect(result.code).toContain('signal')
589
+ expect(result.code).toContain('computed')
590
+ expect(result.code).toContain('effect')
591
+ expect(result.code).toContain('createRef')
592
+ expect(result.code).toContain('class=')
593
593
  expect(result.changes.length).toBeGreaterThan(0)
594
594
  })
595
595
  })
596
596
 
597
597
  // ─── diagnoseError ───────────────────────────────────────────────────────────
598
598
 
599
- describe("diagnoseError", () => {
599
+ describe('diagnoseError', () => {
600
600
  test("diagnoses 'X is not a function' as signal access issue", () => {
601
- const result = diagnoseError("count is not a function")
601
+ const result = diagnoseError('count is not a function')
602
602
  expect(result).not.toBeNull()
603
- expect(result!.cause).toContain("count")
604
- expect(result!.fix).toContain("signal")
605
- expect(result!.fixCode).toContain("count()")
603
+ expect(result!.cause).toContain('count')
604
+ expect(result!.fix).toContain('signal')
605
+ expect(result!.fixCode).toContain('count()')
606
606
  })
607
607
 
608
608
  test("diagnoses Cannot read properties of undefined (reading 'set')", () => {
609
609
  const result = diagnoseError("Cannot read properties of undefined (reading 'set')")
610
610
  expect(result).not.toBeNull()
611
- expect(result!.cause).toContain(".set()")
612
- expect(result!.fix).toContain("signal")
611
+ expect(result!.cause).toContain('.set()')
612
+ expect(result!.fix).toContain('signal')
613
613
  })
614
614
 
615
615
  test("diagnoses Cannot read properties of undefined (reading 'update')", () => {
616
616
  const result = diagnoseError("Cannot read properties of undefined (reading 'update')")
617
617
  expect(result).not.toBeNull()
618
- expect(result!.cause).toContain(".update()")
618
+ expect(result!.cause).toContain('.update()')
619
619
  })
620
620
 
621
621
  test("diagnoses Cannot read properties of undefined (reading 'peek')", () => {
622
622
  const result = diagnoseError("Cannot read properties of undefined (reading 'peek')")
623
623
  expect(result).not.toBeNull()
624
- expect(result!.cause).toContain(".peek()")
624
+ expect(result!.cause).toContain('.peek()')
625
625
  })
626
626
 
627
627
  test("diagnoses Cannot read properties of undefined (reading 'subscribe')", () => {
628
628
  const result = diagnoseError("Cannot read properties of undefined (reading 'subscribe')")
629
629
  expect(result).not.toBeNull()
630
- expect(result!.cause).toContain(".subscribe()")
630
+ expect(result!.cause).toContain('.subscribe()')
631
631
  })
632
632
 
633
- test("diagnoses missing @pyreon package", () => {
633
+ test('diagnoses missing @pyreon package', () => {
634
634
  const result = diagnoseError("Cannot find module '@pyreon/reactivity'")
635
635
  expect(result).not.toBeNull()
636
- expect(result!.cause).toContain("@pyreon/reactivity")
637
- expect(result!.fix).toContain("bun add")
636
+ expect(result!.cause).toContain('@pyreon/reactivity')
637
+ expect(result!.fix).toContain('bun add')
638
638
  })
639
639
 
640
- test("diagnoses missing react module in Pyreon project", () => {
640
+ test('diagnoses missing react module in Pyreon project', () => {
641
641
  const result = diagnoseError("Cannot find module 'react'")
642
642
  expect(result).not.toBeNull()
643
- expect(result!.cause).toContain("react")
644
- expect(result!.fix).toContain("Pyreon")
643
+ expect(result!.cause).toContain('react')
644
+ expect(result!.fix).toContain('Pyreon')
645
645
  })
646
646
 
647
- test("diagnoses .value property on Signal type", () => {
647
+ test('diagnoses .value property on Signal type', () => {
648
648
  const result = diagnoseError("Property 'value' does not exist on type 'Signal<number>'")
649
649
  expect(result).not.toBeNull()
650
- expect(result!.cause).toContain(".value")
651
- expect(result!.fix).toContain("callable")
652
- expect(result!.fixCode).toContain("mySignal()")
650
+ expect(result!.cause).toContain('.value')
651
+ expect(result!.fix).toContain('callable')
652
+ expect(result!.fixCode).toContain('mySignal()')
653
653
  })
654
654
 
655
- test("diagnoses non-value property on Signal type", () => {
655
+ test('diagnoses non-value property on Signal type', () => {
656
656
  const result = diagnoseError("Property 'current' does not exist on type 'Signal<number>'")
657
657
  expect(result).not.toBeNull()
658
- expect(result!.cause).toContain(".current")
659
- expect(result!.fix).toContain(".set()")
658
+ expect(result!.cause).toContain('.current')
659
+ expect(result!.fix).toContain('.set()')
660
660
  })
661
661
 
662
- test("diagnoses type not assignable to VNode", () => {
662
+ test('diagnoses type not assignable to VNode', () => {
663
663
  const result = diagnoseError("Type 'number' is not assignable to type 'VNode'")
664
664
  expect(result).not.toBeNull()
665
- expect(result!.cause).toContain("number")
666
- expect(result!.fix).toContain("JSX")
665
+ expect(result!.cause).toContain('number')
666
+ expect(result!.fix).toContain('JSX')
667
667
  })
668
668
 
669
- test("diagnoses onMount callback return type error", () => {
670
- const result = diagnoseError("onMount callback must return")
669
+ test('diagnoses onMount callback return type error', () => {
670
+ const result = diagnoseError('onMount callback must return')
671
671
  expect(result).not.toBeNull()
672
- expect(result!.cause).toContain("CleanupFn")
673
- expect(result!.fixCode).toContain("onMount")
672
+ expect(result!.cause).toContain('CleanupFn')
673
+ expect(result!.fixCode).toContain('onMount')
674
674
  })
675
675
 
676
- test("diagnoses missing by prop on For", () => {
676
+ test('diagnoses missing by prop on For', () => {
677
677
  const result = diagnoseError("Expected 'by' prop on <For>")
678
678
  expect(result).not.toBeNull()
679
- expect(result!.cause).toContain("by")
680
- expect(result!.fixCode).toContain("by=")
679
+ expect(result!.cause).toContain('by')
680
+ expect(result!.fixCode).toContain('by=')
681
681
  })
682
682
 
683
- test("diagnoses hook called outside component", () => {
684
- const result = diagnoseError("useHook called outside component boundary")
683
+ test('diagnoses hook called outside component', () => {
684
+ const result = diagnoseError('useHook called outside component boundary')
685
685
  expect(result).not.toBeNull()
686
- expect(result!.cause).toContain("outside")
687
- expect(result!.fix).toContain("component")
686
+ expect(result!.cause).toContain('outside')
687
+ expect(result!.fix).toContain('component')
688
688
  })
689
689
 
690
- test("diagnoses hydration mismatch", () => {
691
- const result = diagnoseError("Hydration mismatch")
690
+ test('diagnoses hydration mismatch', () => {
691
+ const result = diagnoseError('Hydration mismatch')
692
692
  expect(result).not.toBeNull()
693
- expect(result!.cause).toContain("Server-rendered")
694
- expect(result!.related).toContain("window")
693
+ expect(result!.cause).toContain('Server-rendered')
694
+ expect(result!.related).toContain('window')
695
695
  })
696
696
 
697
- test("returns null for unknown errors", () => {
698
- expect(diagnoseError("Something completely unrelated happened")).toBeNull()
699
- expect(diagnoseError("")).toBeNull()
700
- expect(diagnoseError("TypeError: Cannot freeze")).toBeNull()
697
+ test('returns null for unknown errors', () => {
698
+ expect(diagnoseError('Something completely unrelated happened')).toBeNull()
699
+ expect(diagnoseError('')).toBeNull()
700
+ expect(diagnoseError('TypeError: Cannot freeze')).toBeNull()
701
701
  })
702
702
  })