@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.
- package/README.md +13 -10
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +4 -4
- package/package.json +10 -10
- package/src/index.ts +6 -11
- package/src/jsx.ts +104 -104
- package/src/project-scanner.ts +21 -21
- package/src/react-intercept.ts +213 -213
- package/src/tests/jsx.test.ts +583 -583
- package/src/tests/project-scanner.test.ts +63 -63
- package/src/tests/react-intercept.test.ts +280 -280
|
@@ -3,88 +3,88 @@ import {
|
|
|
3
3
|
diagnoseError,
|
|
4
4
|
hasReactPatterns,
|
|
5
5
|
migrateReactCode,
|
|
6
|
-
} from
|
|
6
|
+
} from '../react-intercept'
|
|
7
7
|
|
|
8
8
|
// ─── hasReactPatterns ────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
describe(
|
|
11
|
-
test(
|
|
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(
|
|
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(
|
|
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(
|
|
24
|
-
expect(hasReactPatterns(
|
|
23
|
+
test('returns true for useState call', () => {
|
|
24
|
+
expect(hasReactPatterns('const [a, b] = useState(0)')).toBe(true)
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
test(
|
|
28
|
-
expect(hasReactPatterns(
|
|
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(
|
|
32
|
-
expect(hasReactPatterns(
|
|
31
|
+
test('returns true for useEffect call', () => {
|
|
32
|
+
expect(hasReactPatterns('useEffect(() => {}, [])')).toBe(true)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
test(
|
|
36
|
-
expect(hasReactPatterns(
|
|
35
|
+
test('returns true for useMemo call', () => {
|
|
36
|
+
expect(hasReactPatterns('useMemo(() => x * 2, [x])')).toBe(true)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
test(
|
|
40
|
-
expect(hasReactPatterns(
|
|
39
|
+
test('returns true for useCallback call', () => {
|
|
40
|
+
expect(hasReactPatterns('useCallback(() => doThing(), [])')).toBe(true)
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
test(
|
|
44
|
-
expect(hasReactPatterns(
|
|
43
|
+
test('returns true for useRef call', () => {
|
|
44
|
+
expect(hasReactPatterns('useRef(null)')).toBe(true)
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
test(
|
|
48
|
-
expect(hasReactPatterns(
|
|
47
|
+
test('returns true for useRef with type parameter', () => {
|
|
48
|
+
expect(hasReactPatterns('useRef<HTMLDivElement>(null)')).toBe(true)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
test(
|
|
52
|
-
expect(hasReactPatterns(
|
|
51
|
+
test('returns true for useReducer call', () => {
|
|
52
|
+
expect(hasReactPatterns('useReducer(reducer, init)')).toBe(true)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
test(
|
|
56
|
-
expect(hasReactPatterns(
|
|
55
|
+
test('returns true for useReducer with type parameter', () => {
|
|
56
|
+
expect(hasReactPatterns('useReducer<State>(reducer, init)')).toBe(true)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
test(
|
|
60
|
-
expect(hasReactPatterns(
|
|
59
|
+
test('returns true for React.memo', () => {
|
|
60
|
+
expect(hasReactPatterns('React.memo(MyComponent)')).toBe(true)
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
test(
|
|
64
|
-
expect(hasReactPatterns(
|
|
63
|
+
test('returns true for forwardRef call', () => {
|
|
64
|
+
expect(hasReactPatterns('forwardRef((props, ref) => {})')).toBe(true)
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
test(
|
|
68
|
-
expect(hasReactPatterns(
|
|
67
|
+
test('returns true for forwardRef with type parameter', () => {
|
|
68
|
+
expect(hasReactPatterns('forwardRef<HTMLInputElement>((props, ref) => {})')).toBe(true)
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
test(
|
|
71
|
+
test('returns true for className attribute', () => {
|
|
72
72
|
expect(hasReactPatterns('className="foo"')).toBe(true)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
test(
|
|
76
|
-
expect(hasReactPatterns(
|
|
75
|
+
test('returns true for className with space', () => {
|
|
76
|
+
expect(hasReactPatterns('className ')).toBe(true)
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
test(
|
|
79
|
+
test('returns true for htmlFor attribute', () => {
|
|
80
80
|
expect(hasReactPatterns('htmlFor="name"')).toBe(true)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
test(
|
|
84
|
-
expect(hasReactPatterns(
|
|
83
|
+
test('returns true for .value assignment', () => {
|
|
84
|
+
expect(hasReactPatterns('count.value = 5')).toBe(true)
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
-
test(
|
|
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(
|
|
98
|
-
expect(hasReactPatterns(
|
|
97
|
+
test('returns false for empty code', () => {
|
|
98
|
+
expect(hasReactPatterns('')).toBe(false)
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
-
test(
|
|
102
|
-
expect(hasReactPatterns(
|
|
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(
|
|
109
|
-
test(
|
|
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 ===
|
|
111
|
+
const importDiag = diags.find((d) => d.code === 'react-import')
|
|
112
112
|
expect(importDiag).toBeDefined()
|
|
113
|
-
expect(importDiag!.suggested).toContain(
|
|
113
|
+
expect(importDiag!.suggested).toContain('@pyreon/core')
|
|
114
114
|
expect(importDiag!.fixable).toBe(true)
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
test(
|
|
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 ===
|
|
119
|
+
const importDiag = diags.find((d) => d.code === 'react-dom-import')
|
|
120
120
|
expect(importDiag).toBeDefined()
|
|
121
|
-
expect(importDiag!.suggested).toContain(
|
|
121
|
+
expect(importDiag!.suggested).toContain('@pyreon/runtime-dom')
|
|
122
122
|
expect(importDiag!.fixable).toBe(true)
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
test(
|
|
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 ===
|
|
127
|
+
const importDiag = diags.find((d) => d.code === 'react-router-import')
|
|
128
128
|
expect(importDiag).toBeDefined()
|
|
129
|
-
expect(importDiag!.suggested).toContain(
|
|
129
|
+
expect(importDiag!.suggested).toContain('@pyreon/router')
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
test(
|
|
133
|
-
const diags = detectReactPatterns(
|
|
134
|
-
const d = diags.find((d) => d.code ===
|
|
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(
|
|
137
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
142
|
-
const diags = detectReactPatterns(
|
|
143
|
-
const d = diags.find((d) => d.code ===
|
|
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(
|
|
145
|
+
expect(d!.suggested).toContain('signal')
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
-
test(
|
|
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 ===
|
|
151
|
+
const d = diags.find((d) => d.code === 'use-effect-mount')
|
|
152
152
|
expect(d).toBeDefined()
|
|
153
|
-
expect(d!.message).toContain(
|
|
154
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
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 ===
|
|
161
|
+
const d = diags.find((d) => d.code === 'use-effect-mount')
|
|
162
162
|
expect(d).toBeDefined()
|
|
163
|
-
expect(d!.suggested).toContain(
|
|
163
|
+
expect(d!.suggested).toContain('cleanup')
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
-
test(
|
|
167
|
-
const code =
|
|
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 ===
|
|
169
|
+
const d = diags.find((d) => d.code === 'use-effect-deps')
|
|
170
170
|
expect(d).toBeDefined()
|
|
171
|
-
expect(d!.message).toContain(
|
|
171
|
+
expect(d!.message).toContain('auto-tracks')
|
|
172
172
|
expect(d!.fixable).toBe(true)
|
|
173
173
|
})
|
|
174
174
|
|
|
175
|
-
test(
|
|
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 ===
|
|
178
|
+
const d = diags.find((d) => d.code === 'use-effect-no-deps')
|
|
179
179
|
expect(d).toBeDefined()
|
|
180
|
-
expect(d!.message).toContain(
|
|
180
|
+
expect(d!.message).toContain('auto-tracks')
|
|
181
181
|
expect(d!.fixable).toBe(true)
|
|
182
182
|
})
|
|
183
183
|
|
|
184
|
-
test(
|
|
185
|
-
const code =
|
|
184
|
+
test('detects useLayoutEffect', () => {
|
|
185
|
+
const code = 'useLayoutEffect(() => { measure() }, [])'
|
|
186
186
|
const diags = detectReactPatterns(code)
|
|
187
|
-
const d = diags.find((d) => d.code ===
|
|
187
|
+
const d = diags.find((d) => d.code === 'use-effect-mount')
|
|
188
188
|
expect(d).toBeDefined()
|
|
189
|
-
expect(d!.message).toContain(
|
|
189
|
+
expect(d!.message).toContain('useLayoutEffect')
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
-
test(
|
|
193
|
-
const code =
|
|
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 ===
|
|
195
|
+
const d = diags.find((d) => d.code === 'use-memo')
|
|
196
196
|
expect(d).toBeDefined()
|
|
197
|
-
expect(d!.message).toContain(
|
|
198
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
203
|
-
const code =
|
|
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 ===
|
|
205
|
+
const d = diags.find((d) => d.code === 'use-callback')
|
|
206
206
|
expect(d).toBeDefined()
|
|
207
|
-
expect(d!.message).toContain(
|
|
208
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
213
|
-
const code =
|
|
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 ===
|
|
215
|
+
const d = diags.find((d) => d.code === 'use-ref-dom')
|
|
216
216
|
expect(d).toBeDefined()
|
|
217
|
-
expect(d!.message).toContain(
|
|
218
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
223
|
-
const code =
|
|
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 ===
|
|
225
|
+
const d = diags.find((d) => d.code === 'use-ref-box')
|
|
226
226
|
expect(d).toBeDefined()
|
|
227
|
-
expect(d!.message).toContain(
|
|
228
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
233
|
-
const code =
|
|
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 ===
|
|
235
|
+
const d = diags.find((d) => d.code === 'use-reducer')
|
|
236
236
|
expect(d).toBeDefined()
|
|
237
|
-
expect(d!.message).toContain(
|
|
237
|
+
expect(d!.message).toContain('signal')
|
|
238
238
|
expect(d!.fixable).toBe(false)
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
-
test(
|
|
242
|
-
const code =
|
|
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 ===
|
|
244
|
+
const d = diags.find((d) => d.code === 'memo-wrapper')
|
|
245
245
|
expect(d).toBeDefined()
|
|
246
|
-
expect(d!.message).toContain(
|
|
246
|
+
expect(d!.message).toContain('not needed')
|
|
247
247
|
expect(d!.fixable).toBe(true)
|
|
248
248
|
})
|
|
249
249
|
|
|
250
|
-
test(
|
|
251
|
-
const code =
|
|
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 ===
|
|
253
|
+
const d = diags.find((d) => d.code === 'memo-wrapper')
|
|
254
254
|
expect(d).toBeDefined()
|
|
255
255
|
})
|
|
256
256
|
|
|
257
|
-
test(
|
|
258
|
-
const code =
|
|
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 ===
|
|
260
|
+
const d = diags.find((d) => d.code === 'forward-ref')
|
|
261
261
|
expect(d).toBeDefined()
|
|
262
|
-
expect(d!.message).toContain(
|
|
262
|
+
expect(d!.message).toContain('not needed')
|
|
263
263
|
expect(d!.fixable).toBe(true)
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
-
test(
|
|
267
|
-
const code =
|
|
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 ===
|
|
269
|
+
const d = diags.find((d) => d.code === 'forward-ref')
|
|
270
270
|
expect(d).toBeDefined()
|
|
271
271
|
})
|
|
272
272
|
|
|
273
|
-
test(
|
|
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 ===
|
|
276
|
+
const d = diags.find((d) => d.code === 'class-name-prop')
|
|
277
277
|
expect(d).toBeDefined()
|
|
278
|
-
expect(d!.message).toContain(
|
|
279
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
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 ===
|
|
286
|
+
const d = diags.find((d) => d.code === 'html-for-prop')
|
|
287
287
|
expect(d).toBeDefined()
|
|
288
|
-
expect(d!.message).toContain(
|
|
289
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
294
|
-
const code =
|
|
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 ===
|
|
296
|
+
const d = diags.find((d) => d.code === 'on-change-input')
|
|
297
297
|
expect(d).toBeDefined()
|
|
298
|
-
expect(d!.message).toContain(
|
|
299
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
304
|
-
const code =
|
|
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 ===
|
|
306
|
+
const d = diags.find((d) => d.code === 'on-change-input')
|
|
307
307
|
expect(d).toBeDefined()
|
|
308
|
-
expect(d!.message).toContain(
|
|
308
|
+
expect(d!.message).toContain('textarea')
|
|
309
309
|
})
|
|
310
310
|
|
|
311
|
-
test(
|
|
312
|
-
const code =
|
|
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 ===
|
|
314
|
+
const d = diags.find((d) => d.code === 'on-change-input')
|
|
315
315
|
expect(d).toBeDefined()
|
|
316
|
-
expect(d!.message).toContain(
|
|
316
|
+
expect(d!.message).toContain('select')
|
|
317
317
|
})
|
|
318
318
|
|
|
319
|
-
test(
|
|
320
|
-
const code =
|
|
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 ===
|
|
322
|
+
const d = diags.find((d) => d.code === 'on-change-input')
|
|
323
323
|
expect(d).toBeUndefined()
|
|
324
324
|
})
|
|
325
325
|
|
|
326
|
-
test(
|
|
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 ===
|
|
329
|
+
const d = diags.find((d) => d.code === 'dangerously-set-inner-html')
|
|
330
330
|
expect(d).toBeDefined()
|
|
331
|
-
expect(d!.message).toContain(
|
|
332
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
337
|
-
const code =
|
|
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 ===
|
|
339
|
+
const d = diags.find((d) => d.code === 'dot-value-signal')
|
|
340
340
|
expect(d).toBeDefined()
|
|
341
|
-
expect(d!.message).toContain(
|
|
342
|
-
expect(d!.suggested).toContain(
|
|
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(
|
|
347
|
-
const code =
|
|
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 ===
|
|
349
|
+
const d = diags.find((d) => d.code === 'array-map-jsx')
|
|
350
350
|
expect(d).toBeDefined()
|
|
351
|
-
expect(d!.message).toContain(
|
|
351
|
+
expect(d!.message).toContain('<For>')
|
|
352
352
|
expect(d!.fixable).toBe(false)
|
|
353
353
|
})
|
|
354
354
|
|
|
355
|
-
test(
|
|
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(
|
|
367
|
-
const code =
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
384
|
-
expect(diags.find((d) => d.code ===
|
|
385
|
-
expect(diags.find((d) => d.code ===
|
|
386
|
-
expect(diags.find((d) => d.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()
|
|
387
387
|
expect(diags.length).toBeGreaterThanOrEqual(4)
|
|
388
388
|
})
|
|
389
389
|
})
|
|
390
390
|
|
|
391
391
|
// ─── migrateReactCode ────────────────────────────────────────────────────────
|
|
392
392
|
|
|
393
|
-
describe(
|
|
394
|
-
test(
|
|
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(
|
|
398
|
-
expect(result.code).not.toContain(
|
|
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(
|
|
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(
|
|
406
|
-
expect(result.code).not.toContain(
|
|
405
|
+
expect(result.code).toContain('effect(')
|
|
406
|
+
expect(result.code).not.toContain('useEffect')
|
|
407
407
|
})
|
|
408
408
|
|
|
409
|
-
test(
|
|
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(
|
|
413
|
-
expect(result.code).not.toContain(
|
|
412
|
+
expect(result.code).toContain('onMount(')
|
|
413
|
+
expect(result.code).not.toContain('useEffect')
|
|
414
414
|
})
|
|
415
415
|
|
|
416
|
-
test(
|
|
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(
|
|
420
|
-
expect(result.code).not.toContain(
|
|
419
|
+
expect(result.code).toContain('effect(')
|
|
420
|
+
expect(result.code).not.toContain('useEffect')
|
|
421
421
|
})
|
|
422
422
|
|
|
423
|
-
test(
|
|
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(
|
|
427
|
-
expect(result.code).not.toContain(
|
|
426
|
+
expect(result.code).toContain('computed(')
|
|
427
|
+
expect(result.code).not.toContain('useMemo')
|
|
428
428
|
})
|
|
429
429
|
|
|
430
|
-
test(
|
|
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(
|
|
434
|
-
expect(result.code).not.toContain(
|
|
433
|
+
expect(result.code).toContain('() => doThing()')
|
|
434
|
+
expect(result.code).not.toContain('useCallback')
|
|
435
435
|
})
|
|
436
436
|
|
|
437
|
-
test(
|
|
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(
|
|
441
|
-
expect(result.code).not.toContain(
|
|
440
|
+
expect(result.code).toContain('createRef()')
|
|
441
|
+
expect(result.code).not.toContain('useRef')
|
|
442
442
|
})
|
|
443
443
|
|
|
444
|
-
test(
|
|
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(
|
|
448
|
-
expect(result.code).not.toContain(
|
|
447
|
+
expect(result.code).toContain('signal(0)')
|
|
448
|
+
expect(result.code).not.toContain('useRef')
|
|
449
449
|
})
|
|
450
450
|
|
|
451
|
-
test(
|
|
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(
|
|
454
|
+
expect(result.code).toContain('createRef()')
|
|
455
455
|
})
|
|
456
456
|
|
|
457
|
-
test(
|
|
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(
|
|
461
|
-
expect(result.code).toContain(
|
|
460
|
+
expect(result.code).not.toContain('memo(')
|
|
461
|
+
expect(result.code).toContain('function MyComp()')
|
|
462
462
|
})
|
|
463
463
|
|
|
464
|
-
test(
|
|
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(
|
|
468
|
-
expect(result.code).toContain(
|
|
467
|
+
expect(result.code).not.toContain('React.memo')
|
|
468
|
+
expect(result.code).toContain('function MyComp()')
|
|
469
469
|
})
|
|
470
470
|
|
|
471
|
-
test(
|
|
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(
|
|
475
|
-
expect(result.code).toContain(
|
|
474
|
+
expect(result.code).not.toContain('forwardRef')
|
|
475
|
+
expect(result.code).toContain('(props, ref) =>')
|
|
476
476
|
})
|
|
477
477
|
|
|
478
|
-
test(
|
|
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(
|
|
482
|
-
expect(result.code).toContain(
|
|
481
|
+
expect(result.code).not.toContain('forwardRef')
|
|
482
|
+
expect(result.code).toContain('(props, ref) =>')
|
|
483
483
|
})
|
|
484
484
|
|
|
485
|
-
test(
|
|
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(
|
|
489
|
+
expect(result.code).not.toContain('className')
|
|
490
490
|
})
|
|
491
491
|
|
|
492
|
-
test(
|
|
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(
|
|
496
|
-
expect(result.code).not.toContain(
|
|
495
|
+
expect(result.code).toContain('onInput')
|
|
496
|
+
expect(result.code).not.toContain('onChange')
|
|
497
497
|
})
|
|
498
498
|
|
|
499
|
-
test(
|
|
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(
|
|
502
|
+
expect(result.code).toContain('onInput')
|
|
503
503
|
})
|
|
504
504
|
|
|
505
|
-
test(
|
|
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(
|
|
508
|
+
expect(result.code).toContain('onInput')
|
|
509
509
|
})
|
|
510
510
|
|
|
511
|
-
test(
|
|
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(
|
|
518
|
+
expect(result.code).toContain('@pyreon/reactivity')
|
|
519
519
|
})
|
|
520
520
|
|
|
521
|
-
test(
|
|
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(
|
|
525
|
-
expect(result.code).not.toContain(
|
|
524
|
+
expect(result.code).toContain('innerHTML={htmlString}')
|
|
525
|
+
expect(result.code).not.toContain('dangerouslySetInnerHTML')
|
|
526
526
|
})
|
|
527
527
|
|
|
528
|
-
test(
|
|
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(
|
|
532
|
+
expect(result.changes[0]!.description).toContain('useState')
|
|
533
533
|
})
|
|
534
534
|
|
|
535
|
-
test(
|
|
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(
|
|
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 ===
|
|
547
|
+
expect(result.diagnostics.find((d) => d.code === 'use-state')).toBeDefined()
|
|
548
548
|
})
|
|
549
549
|
|
|
550
|
-
test(
|
|
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(
|
|
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(
|
|
563
|
-
expect(result.code).toContain(
|
|
564
|
-
expect(result.code).toContain(
|
|
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(
|
|
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(
|
|
584
|
-
expect(result.code).not.toContain(
|
|
585
|
-
expect(result.code).not.toContain(
|
|
586
|
-
expect(result.code).not.toContain(
|
|
587
|
-
expect(result.code).not.toContain(
|
|
588
|
-
expect(result.code).toContain(
|
|
589
|
-
expect(result.code).toContain(
|
|
590
|
-
expect(result.code).toContain(
|
|
591
|
-
expect(result.code).toContain(
|
|
592
|
-
expect(result.code).toContain(
|
|
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(
|
|
599
|
+
describe('diagnoseError', () => {
|
|
600
600
|
test("diagnoses 'X is not a function' as signal access issue", () => {
|
|
601
|
-
const result = diagnoseError(
|
|
601
|
+
const result = diagnoseError('count is not a function')
|
|
602
602
|
expect(result).not.toBeNull()
|
|
603
|
-
expect(result!.cause).toContain(
|
|
604
|
-
expect(result!.fix).toContain(
|
|
605
|
-
expect(result!.fixCode).toContain(
|
|
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(
|
|
612
|
-
expect(result!.fix).toContain(
|
|
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(
|
|
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(
|
|
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(
|
|
630
|
+
expect(result!.cause).toContain('.subscribe()')
|
|
631
631
|
})
|
|
632
632
|
|
|
633
|
-
test(
|
|
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(
|
|
637
|
-
expect(result!.fix).toContain(
|
|
636
|
+
expect(result!.cause).toContain('@pyreon/reactivity')
|
|
637
|
+
expect(result!.fix).toContain('bun add')
|
|
638
638
|
})
|
|
639
639
|
|
|
640
|
-
test(
|
|
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(
|
|
644
|
-
expect(result!.fix).toContain(
|
|
643
|
+
expect(result!.cause).toContain('react')
|
|
644
|
+
expect(result!.fix).toContain('Pyreon')
|
|
645
645
|
})
|
|
646
646
|
|
|
647
|
-
test(
|
|
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(
|
|
651
|
-
expect(result!.fix).toContain(
|
|
652
|
-
expect(result!.fixCode).toContain(
|
|
650
|
+
expect(result!.cause).toContain('.value')
|
|
651
|
+
expect(result!.fix).toContain('callable')
|
|
652
|
+
expect(result!.fixCode).toContain('mySignal()')
|
|
653
653
|
})
|
|
654
654
|
|
|
655
|
-
test(
|
|
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(
|
|
659
|
-
expect(result!.fix).toContain(
|
|
658
|
+
expect(result!.cause).toContain('.current')
|
|
659
|
+
expect(result!.fix).toContain('.set()')
|
|
660
660
|
})
|
|
661
661
|
|
|
662
|
-
test(
|
|
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(
|
|
666
|
-
expect(result!.fix).toContain(
|
|
665
|
+
expect(result!.cause).toContain('number')
|
|
666
|
+
expect(result!.fix).toContain('JSX')
|
|
667
667
|
})
|
|
668
668
|
|
|
669
|
-
test(
|
|
670
|
-
const result = diagnoseError(
|
|
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(
|
|
673
|
-
expect(result!.fixCode).toContain(
|
|
672
|
+
expect(result!.cause).toContain('CleanupFn')
|
|
673
|
+
expect(result!.fixCode).toContain('onMount')
|
|
674
674
|
})
|
|
675
675
|
|
|
676
|
-
test(
|
|
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(
|
|
680
|
-
expect(result!.fixCode).toContain(
|
|
679
|
+
expect(result!.cause).toContain('by')
|
|
680
|
+
expect(result!.fixCode).toContain('by=')
|
|
681
681
|
})
|
|
682
682
|
|
|
683
|
-
test(
|
|
684
|
-
const result = diagnoseError(
|
|
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(
|
|
687
|
-
expect(result!.fix).toContain(
|
|
686
|
+
expect(result!.cause).toContain('outside')
|
|
687
|
+
expect(result!.fix).toContain('component')
|
|
688
688
|
})
|
|
689
689
|
|
|
690
|
-
test(
|
|
691
|
-
const result = diagnoseError(
|
|
690
|
+
test('diagnoses hydration mismatch', () => {
|
|
691
|
+
const result = diagnoseError('Hydration mismatch')
|
|
692
692
|
expect(result).not.toBeNull()
|
|
693
|
-
expect(result!.cause).toContain(
|
|
694
|
-
expect(result!.related).toContain(
|
|
693
|
+
expect(result!.cause).toContain('Server-rendered')
|
|
694
|
+
expect(result!.related).toContain('window')
|
|
695
695
|
})
|
|
696
696
|
|
|
697
|
-
test(
|
|
698
|
-
expect(diagnoseError(
|
|
699
|
-
expect(diagnoseError(
|
|
700
|
-
expect(diagnoseError(
|
|
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
|
})
|