@pyreon/compiler 0.24.5 → 0.24.6
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/package.json +11 -13
- package/src/defer-inline.ts +0 -686
- package/src/event-names.ts +0 -65
- package/src/index.ts +0 -61
- package/src/island-audit.ts +0 -675
- package/src/jsx.ts +0 -2792
- package/src/load-native.ts +0 -156
- package/src/lpih.ts +0 -270
- package/src/manifest.ts +0 -280
- package/src/project-scanner.ts +0 -214
- package/src/pyreon-intercept.ts +0 -1029
- package/src/react-intercept.ts +0 -1217
- package/src/reactivity-lens.ts +0 -190
- package/src/ssg-audit.ts +0 -513
- package/src/test-audit.ts +0 -435
- package/src/tests/backend-parity-r7-r9.test.ts +0 -91
- package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
- package/src/tests/collapse-bail-census.test.ts +0 -330
- package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
- package/src/tests/component-child-no-wrap.test.ts +0 -204
- package/src/tests/defer-inline.test.ts +0 -387
- package/src/tests/depth-stress.test.ts +0 -16
- package/src/tests/detector-tag-consistency.test.ts +0 -101
- package/src/tests/dynamic-collapse-detector.test.ts +0 -164
- package/src/tests/dynamic-collapse-emit.test.ts +0 -192
- package/src/tests/dynamic-collapse-scan.test.ts +0 -111
- package/src/tests/element-valued-const-child.test.ts +0 -61
- package/src/tests/falsy-child-characterization.test.ts +0 -48
- package/src/tests/island-audit.test.ts +0 -524
- package/src/tests/jsx.test.ts +0 -2908
- package/src/tests/load-native.test.ts +0 -53
- package/src/tests/lpih.test.ts +0 -404
- package/src/tests/malformed-input-resilience.test.ts +0 -50
- package/src/tests/manifest-snapshot.test.ts +0 -55
- package/src/tests/native-equivalence.test.ts +0 -924
- package/src/tests/partial-collapse-detector.test.ts +0 -121
- package/src/tests/partial-collapse-emit.test.ts +0 -104
- package/src/tests/partial-collapse-robustness.test.ts +0 -53
- package/src/tests/project-scanner.test.ts +0 -269
- package/src/tests/prop-derived-shadow.test.ts +0 -96
- package/src/tests/pure-call-reactive-args.test.ts +0 -50
- package/src/tests/pyreon-intercept.test.ts +0 -816
- package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
- package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
- package/src/tests/r15-elemconst-propderived.test.ts +0 -47
- package/src/tests/r19-defer-inline-robust.test.ts +0 -54
- package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
- package/src/tests/react-intercept.test.ts +0 -1104
- package/src/tests/reactivity-lens.test.ts +0 -170
- package/src/tests/rocketstyle-collapse.test.ts +0 -208
- package/src/tests/runtime/control-flow.test.ts +0 -159
- package/src/tests/runtime/dom-properties.test.ts +0 -138
- package/src/tests/runtime/events.test.ts +0 -301
- package/src/tests/runtime/harness.ts +0 -94
- package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
- package/src/tests/runtime/reactive-props.test.ts +0 -81
- package/src/tests/runtime/signals.test.ts +0 -129
- package/src/tests/runtime/whitespace.test.ts +0 -106
- package/src/tests/signal-autocall-shadow.test.ts +0 -86
- package/src/tests/sourcemap-fidelity.test.ts +0 -77
- package/src/tests/ssg-audit.test.ts +0 -402
- package/src/tests/static-text-baking.test.ts +0 -64
- package/src/tests/test-audit.test.ts +0 -549
- package/src/tests/transform-state-isolation.test.ts +0 -49
|
@@ -1,1104 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
detectReactPatterns,
|
|
3
|
-
diagnoseError,
|
|
4
|
-
hasReactPatterns,
|
|
5
|
-
migrateReactCode,
|
|
6
|
-
} from '../react-intercept'
|
|
7
|
-
|
|
8
|
-
// ─── hasReactPatterns ────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
describe('hasReactPatterns', () => {
|
|
11
|
-
test('returns true for React import', () => {
|
|
12
|
-
expect(hasReactPatterns(`import { useState } from "react"`)).toBe(true)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('returns true for react-dom import', () => {
|
|
16
|
-
expect(hasReactPatterns(`import { createRoot } from "react-dom/client"`)).toBe(true)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('returns true for react-router import', () => {
|
|
20
|
-
expect(hasReactPatterns(`import { Link } from "react-router-dom"`)).toBe(true)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('returns true for useState call', () => {
|
|
24
|
-
expect(hasReactPatterns('const [a, b] = useState(0)')).toBe(true)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('returns true for useState with type parameter', () => {
|
|
28
|
-
expect(hasReactPatterns('const [a, b] = useState<number>(0)')).toBe(true)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('returns true for useEffect call', () => {
|
|
32
|
-
expect(hasReactPatterns('useEffect(() => {}, [])')).toBe(true)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('returns true for useMemo call', () => {
|
|
36
|
-
expect(hasReactPatterns('useMemo(() => x * 2, [x])')).toBe(true)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('returns true for useCallback call', () => {
|
|
40
|
-
expect(hasReactPatterns('useCallback(() => doThing(), [])')).toBe(true)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('returns true for useRef call', () => {
|
|
44
|
-
expect(hasReactPatterns('useRef(null)')).toBe(true)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('returns true for useRef with type parameter', () => {
|
|
48
|
-
expect(hasReactPatterns('useRef<HTMLDivElement>(null)')).toBe(true)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('returns true for useReducer call', () => {
|
|
52
|
-
expect(hasReactPatterns('useReducer(reducer, init)')).toBe(true)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('returns true for useReducer with type parameter', () => {
|
|
56
|
-
expect(hasReactPatterns('useReducer<State>(reducer, init)')).toBe(true)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('returns true for React.memo', () => {
|
|
60
|
-
expect(hasReactPatterns('React.memo(MyComponent)')).toBe(true)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('returns true for forwardRef call', () => {
|
|
64
|
-
expect(hasReactPatterns('forwardRef((props, ref) => {})')).toBe(true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('returns true for forwardRef with type parameter', () => {
|
|
68
|
-
expect(hasReactPatterns('forwardRef<HTMLInputElement>((props, ref) => {})')).toBe(true)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('returns true for className attribute', () => {
|
|
72
|
-
expect(hasReactPatterns('className="foo"')).toBe(true)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('returns true for className with space', () => {
|
|
76
|
-
expect(hasReactPatterns('className ')).toBe(true)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('returns true for htmlFor attribute', () => {
|
|
80
|
-
expect(hasReactPatterns('htmlFor="name"')).toBe(true)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test('returns true for .value assignment', () => {
|
|
84
|
-
expect(hasReactPatterns('count.value = 5')).toBe(true)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('returns false for pure Pyreon code', () => {
|
|
88
|
-
expect(
|
|
89
|
-
hasReactPatterns(`
|
|
90
|
-
import { signal, effect, computed } from "@pyreon/reactivity"
|
|
91
|
-
const count = signal(0)
|
|
92
|
-
effect(() => console.log(count()))
|
|
93
|
-
`),
|
|
94
|
-
).toBe(false)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test('returns false for empty code', () => {
|
|
98
|
-
expect(hasReactPatterns('')).toBe(false)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('returns false for plain JavaScript', () => {
|
|
102
|
-
expect(hasReactPatterns('const x = 42\nfunction foo() { return x }')).toBe(false)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
// ─── detectReactPatterns ─────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
describe('detectReactPatterns', () => {
|
|
109
|
-
test('detects React import', () => {
|
|
110
|
-
const diags = detectReactPatterns(`import { useState } from "react"`)
|
|
111
|
-
const importDiag = diags.find((d) => d.code === 'react-import')
|
|
112
|
-
expect(importDiag).toBeDefined()
|
|
113
|
-
expect(importDiag!.suggested).toContain('@pyreon/core')
|
|
114
|
-
expect(importDiag!.fixable).toBe(true)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('detects react-dom import', () => {
|
|
118
|
-
const diags = detectReactPatterns(`import { createRoot } from "react-dom/client"`)
|
|
119
|
-
const importDiag = diags.find((d) => d.code === 'react-dom-import')
|
|
120
|
-
expect(importDiag).toBeDefined()
|
|
121
|
-
expect(importDiag!.suggested).toContain('@pyreon/runtime-dom')
|
|
122
|
-
expect(importDiag!.fixable).toBe(true)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('detects react-router import', () => {
|
|
126
|
-
const diags = detectReactPatterns(`import { Link } from "react-router-dom"`)
|
|
127
|
-
const importDiag = diags.find((d) => d.code === 'react-router-import')
|
|
128
|
-
expect(importDiag).toBeDefined()
|
|
129
|
-
expect(importDiag!.suggested).toContain('@pyreon/router')
|
|
130
|
-
})
|
|
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')
|
|
135
|
-
expect(d).toBeDefined()
|
|
136
|
-
expect(d!.message).toContain('signal')
|
|
137
|
-
expect(d!.suggested).toContain('count = signal(0)')
|
|
138
|
-
expect(d!.fixable).toBe(true)
|
|
139
|
-
})
|
|
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')
|
|
144
|
-
expect(d).toBeDefined()
|
|
145
|
-
expect(d!.suggested).toContain('signal')
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test('detects useEffect with empty deps (mount pattern)', () => {
|
|
149
|
-
const code = `useEffect(() => { console.log("mounted") }, [])`
|
|
150
|
-
const diags = detectReactPatterns(code)
|
|
151
|
-
const d = diags.find((d) => d.code === 'use-effect-mount')
|
|
152
|
-
expect(d).toBeDefined()
|
|
153
|
-
expect(d!.message).toContain('onMount')
|
|
154
|
-
expect(d!.suggested).toContain('onMount')
|
|
155
|
-
expect(d!.fixable).toBe(true)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
test('detects useEffect with empty deps and cleanup', () => {
|
|
159
|
-
const code = `useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id) }, [])`
|
|
160
|
-
const diags = detectReactPatterns(code)
|
|
161
|
-
const d = diags.find((d) => d.code === 'use-effect-mount')
|
|
162
|
-
expect(d).toBeDefined()
|
|
163
|
-
expect(d!.suggested).toContain('cleanup')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('detects useEffect with dependency array', () => {
|
|
167
|
-
const code = 'useEffect(() => { document.title = count }, [count])'
|
|
168
|
-
const diags = detectReactPatterns(code)
|
|
169
|
-
const d = diags.find((d) => d.code === 'use-effect-deps')
|
|
170
|
-
expect(d).toBeDefined()
|
|
171
|
-
expect(d!.message).toContain('auto-tracks')
|
|
172
|
-
expect(d!.fixable).toBe(true)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test('detects useEffect with no dependency array', () => {
|
|
176
|
-
const code = "useEffect(() => { console.log('render') })"
|
|
177
|
-
const diags = detectReactPatterns(code)
|
|
178
|
-
const d = diags.find((d) => d.code === 'use-effect-no-deps')
|
|
179
|
-
expect(d).toBeDefined()
|
|
180
|
-
expect(d!.message).toContain('auto-tracks')
|
|
181
|
-
expect(d!.fixable).toBe(true)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('detects useLayoutEffect', () => {
|
|
185
|
-
const code = 'useLayoutEffect(() => { measure() }, [])'
|
|
186
|
-
const diags = detectReactPatterns(code)
|
|
187
|
-
const d = diags.find((d) => d.code === 'use-effect-mount')
|
|
188
|
-
expect(d).toBeDefined()
|
|
189
|
-
expect(d!.message).toContain('useLayoutEffect')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
test('detects useMemo', () => {
|
|
193
|
-
const code = 'const doubled = useMemo(() => count * 2, [count])'
|
|
194
|
-
const diags = detectReactPatterns(code)
|
|
195
|
-
const d = diags.find((d) => d.code === 'use-memo')
|
|
196
|
-
expect(d).toBeDefined()
|
|
197
|
-
expect(d!.message).toContain('computed')
|
|
198
|
-
expect(d!.suggested).toContain('computed')
|
|
199
|
-
expect(d!.fixable).toBe(true)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
test('detects useCallback', () => {
|
|
203
|
-
const code = 'const handleClick = useCallback(() => doThing(), [doThing])'
|
|
204
|
-
const diags = detectReactPatterns(code)
|
|
205
|
-
const d = diags.find((d) => d.code === 'use-callback')
|
|
206
|
-
expect(d).toBeDefined()
|
|
207
|
-
expect(d!.message).toContain('not needed')
|
|
208
|
-
expect(d!.suggested).toContain('() => doThing()')
|
|
209
|
-
expect(d!.fixable).toBe(true)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
test('detects useRef with null (DOM ref)', () => {
|
|
213
|
-
const code = 'const inputRef = useRef(null)'
|
|
214
|
-
const diags = detectReactPatterns(code)
|
|
215
|
-
const d = diags.find((d) => d.code === 'use-ref-dom')
|
|
216
|
-
expect(d).toBeDefined()
|
|
217
|
-
expect(d!.message).toContain('createRef')
|
|
218
|
-
expect(d!.suggested).toContain('createRef()')
|
|
219
|
-
expect(d!.fixable).toBe(true)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test('detects useRef with value (mutable box)', () => {
|
|
223
|
-
const code = 'const prevCount = useRef(0)'
|
|
224
|
-
const diags = detectReactPatterns(code)
|
|
225
|
-
const d = diags.find((d) => d.code === 'use-ref-box')
|
|
226
|
-
expect(d).toBeDefined()
|
|
227
|
-
expect(d!.message).toContain('signal')
|
|
228
|
-
expect(d!.suggested).toContain('signal(0)')
|
|
229
|
-
expect(d!.fixable).toBe(true)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('detects useReducer', () => {
|
|
233
|
-
const code = 'const [state, dispatch] = useReducer(reducer, initialState)'
|
|
234
|
-
const diags = detectReactPatterns(code)
|
|
235
|
-
const d = diags.find((d) => d.code === 'use-reducer')
|
|
236
|
-
expect(d).toBeDefined()
|
|
237
|
-
expect(d!.message).toContain('signal')
|
|
238
|
-
expect(d!.fixable).toBe(false)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
test('detects memo() wrapper', () => {
|
|
242
|
-
const code = 'const MyComp = memo(function MyComp() { return <div /> })'
|
|
243
|
-
const diags = detectReactPatterns(code)
|
|
244
|
-
const d = diags.find((d) => d.code === 'memo-wrapper')
|
|
245
|
-
expect(d).toBeDefined()
|
|
246
|
-
expect(d!.message).toContain('not needed')
|
|
247
|
-
expect(d!.fixable).toBe(true)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
test('detects React.memo() wrapper', () => {
|
|
251
|
-
const code = 'const MyComp = React.memo(function MyComp() { return <div /> })'
|
|
252
|
-
const diags = detectReactPatterns(code)
|
|
253
|
-
const d = diags.find((d) => d.code === 'memo-wrapper')
|
|
254
|
-
expect(d).toBeDefined()
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
test('detects forwardRef()', () => {
|
|
258
|
-
const code = 'const Input = forwardRef((props, ref) => <input ref={ref} />)'
|
|
259
|
-
const diags = detectReactPatterns(code)
|
|
260
|
-
const d = diags.find((d) => d.code === 'forward-ref')
|
|
261
|
-
expect(d).toBeDefined()
|
|
262
|
-
expect(d!.message).toContain('not needed')
|
|
263
|
-
expect(d!.fixable).toBe(true)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
test('detects React.forwardRef()', () => {
|
|
267
|
-
const code = 'const Input = React.forwardRef((props, ref) => <input ref={ref} />)'
|
|
268
|
-
const diags = detectReactPatterns(code)
|
|
269
|
-
const d = diags.find((d) => d.code === 'forward-ref')
|
|
270
|
-
expect(d).toBeDefined()
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
test('detects className attribute', () => {
|
|
274
|
-
const code = 'const el = <div className="container">Hello</div>'
|
|
275
|
-
const diags = detectReactPatterns(code)
|
|
276
|
-
const d = diags.find((d) => d.code === 'class-name-prop')
|
|
277
|
-
expect(d).toBeDefined()
|
|
278
|
-
expect(d!.message).toContain('class')
|
|
279
|
-
expect(d!.suggested).toContain('class')
|
|
280
|
-
expect(d!.fixable).toBe(true)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
test('detects htmlFor attribute', () => {
|
|
284
|
-
const code = 'const el = <label htmlFor="name">Name</label>'
|
|
285
|
-
const diags = detectReactPatterns(code)
|
|
286
|
-
const d = diags.find((d) => d.code === 'html-for-prop')
|
|
287
|
-
expect(d).toBeDefined()
|
|
288
|
-
expect(d!.message).toContain('for')
|
|
289
|
-
expect(d!.suggested).toContain('for')
|
|
290
|
-
expect(d!.fixable).toBe(true)
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
test('detects onChange on input', () => {
|
|
294
|
-
const code = 'const el = <input onChange={handleChange} />'
|
|
295
|
-
const diags = detectReactPatterns(code)
|
|
296
|
-
const d = diags.find((d) => d.code === 'on-change-input')
|
|
297
|
-
expect(d).toBeDefined()
|
|
298
|
-
expect(d!.message).toContain('onInput')
|
|
299
|
-
expect(d!.suggested).toContain('onInput')
|
|
300
|
-
expect(d!.fixable).toBe(true)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
test('detects onChange on textarea', () => {
|
|
304
|
-
const code = 'const el = <textarea onChange={handleChange} />'
|
|
305
|
-
const diags = detectReactPatterns(code)
|
|
306
|
-
const d = diags.find((d) => d.code === 'on-change-input')
|
|
307
|
-
expect(d).toBeDefined()
|
|
308
|
-
expect(d!.message).toContain('textarea')
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
test('detects onChange on select', () => {
|
|
312
|
-
const code = 'const el = <select onChange={handleChange} />'
|
|
313
|
-
const diags = detectReactPatterns(code)
|
|
314
|
-
const d = diags.find((d) => d.code === 'on-change-input')
|
|
315
|
-
expect(d).toBeDefined()
|
|
316
|
-
expect(d!.message).toContain('select')
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
test('does NOT detect onChange on non-input element', () => {
|
|
320
|
-
const code = 'const el = <div onChange={handleChange} />'
|
|
321
|
-
const diags = detectReactPatterns(code)
|
|
322
|
-
const d = diags.find((d) => d.code === 'on-change-input')
|
|
323
|
-
expect(d).toBeUndefined()
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
test('detects dangerouslySetInnerHTML', () => {
|
|
327
|
-
const code = 'const el = <div dangerouslySetInnerHTML={{ __html: "<b>hi</b>" }} />'
|
|
328
|
-
const diags = detectReactPatterns(code)
|
|
329
|
-
const d = diags.find((d) => d.code === 'dangerously-set-inner-html')
|
|
330
|
-
expect(d).toBeDefined()
|
|
331
|
-
expect(d!.message).toContain('innerHTML')
|
|
332
|
-
expect(d!.suggested).toContain('innerHTML')
|
|
333
|
-
expect(d!.fixable).toBe(true)
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
test('detects .value assignment on a declared signal', () => {
|
|
337
|
-
const code = 'const count = signal(0)\ncount.value = 5'
|
|
338
|
-
const diags = detectReactPatterns(code)
|
|
339
|
-
const d = diags.find((d) => d.code === 'dot-value-signal')
|
|
340
|
-
expect(d).toBeDefined()
|
|
341
|
-
expect(d!.message).toContain('Vue ref')
|
|
342
|
-
expect(d!.suggested).toContain('count.set(5)')
|
|
343
|
-
expect(d!.fixable).toBe(false)
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
// ─── dot-value-signal precision (FP-free): only flag tracked signals ──────
|
|
347
|
-
|
|
348
|
-
test('flags .value write on signal(), computed(), useSignal(), createSignal()', () => {
|
|
349
|
-
for (const factory of ['signal', 'computed', 'useSignal', 'createSignal']) {
|
|
350
|
-
const code = `const s = ${factory}(0)\ns.value = 1`
|
|
351
|
-
const diags = detectReactPatterns(code)
|
|
352
|
-
const d = diags.find((d) => d.code === 'dot-value-signal')
|
|
353
|
-
expect(d, `expected dot-value-signal for ${factory}`).toBeDefined()
|
|
354
|
-
expect(d!.suggested).toContain('s.set(1)')
|
|
355
|
-
}
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test('does NOT flag input.value = "" (DOM element, not a signal)', () => {
|
|
359
|
-
const code = `const input = document.querySelector("input")\ninput.value = ""`
|
|
360
|
-
const diags = detectReactPatterns(code)
|
|
361
|
-
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
test('does NOT flag cell.value = x (data object, not a signal)', () => {
|
|
365
|
-
const code = `const cell = sheet.getCell(1, 1)\ncell.value = "hello"`
|
|
366
|
-
const diags = detectReactPatterns(code)
|
|
367
|
-
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
test('does NOT flag o.value = y (loop option object, not a signal)', () => {
|
|
371
|
-
const code = `for (const o of options) { o.value = y }`
|
|
372
|
-
const diags = detectReactPatterns(code)
|
|
373
|
-
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
test('does NOT flag ref.current.value = z (ref pattern, not a signal)', () => {
|
|
377
|
-
const code = `const ref = useRef(null)\nref.current.value = 42`
|
|
378
|
-
const diags = detectReactPatterns(code)
|
|
379
|
-
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
test('does NOT flag bare X.value = n with no signal declaration', () => {
|
|
383
|
-
const code = 'count.value = 5'
|
|
384
|
-
const diags = detectReactPatterns(code)
|
|
385
|
-
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
test('does NOT flag .value write on a let/var (mutable, unreliable)', () => {
|
|
389
|
-
const code = `let maybe = signal(0)\nmaybe = 5\nmaybe.value = 1`
|
|
390
|
-
const diags = detectReactPatterns(code)
|
|
391
|
-
expect(diags.find((d) => d.code === 'dot-value-signal')).toBeUndefined()
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
test('detects .map() in JSX expression', () => {
|
|
395
|
-
const code = 'const el = <ul>{items.map(item => <li>{item}</li>)}</ul>'
|
|
396
|
-
const diags = detectReactPatterns(code)
|
|
397
|
-
const d = diags.find((d) => d.code === 'array-map-jsx')
|
|
398
|
-
expect(d).toBeDefined()
|
|
399
|
-
expect(d!.message).toContain('<For>')
|
|
400
|
-
expect(d!.fixable).toBe(false)
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
test('returns empty array for pure Pyreon code', () => {
|
|
404
|
-
const code = `
|
|
405
|
-
import { signal, effect } from "@pyreon/reactivity"
|
|
406
|
-
const count = signal(0)
|
|
407
|
-
effect(() => console.log(count()))
|
|
408
|
-
const el = <div class="foo">{count()}</div>
|
|
409
|
-
`
|
|
410
|
-
const diags = detectReactPatterns(code)
|
|
411
|
-
expect(diags).toEqual([])
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
test('includes correct line and column information', () => {
|
|
415
|
-
const code = 'const [count, setCount] = useState(0)'
|
|
416
|
-
const diags = detectReactPatterns(code)
|
|
417
|
-
const d = diags.find((d) => d.code === 'use-state')
|
|
418
|
-
expect(d).toBeDefined()
|
|
419
|
-
expect(d!.line).toBe(1)
|
|
420
|
-
expect(d!.column).toBeGreaterThanOrEqual(0)
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
test('detects multiple patterns in one file', () => {
|
|
424
|
-
const code = `
|
|
425
|
-
import { useState, useEffect, useMemo } from "react"
|
|
426
|
-
const [count, setCount] = useState(0)
|
|
427
|
-
useEffect(() => {}, [])
|
|
428
|
-
const doubled = useMemo(() => count * 2, [count])
|
|
429
|
-
`
|
|
430
|
-
const diags = detectReactPatterns(code)
|
|
431
|
-
expect(diags.find((d) => d.code === 'react-import')).toBeDefined()
|
|
432
|
-
expect(diags.find((d) => d.code === 'use-state')).toBeDefined()
|
|
433
|
-
expect(diags.find((d) => d.code === 'use-effect-mount')).toBeDefined()
|
|
434
|
-
expect(diags.find((d) => d.code === 'use-memo')).toBeDefined()
|
|
435
|
-
expect(diags.length).toBeGreaterThanOrEqual(4)
|
|
436
|
-
})
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
// ─── migrateReactCode ────────────────────────────────────────────────────────
|
|
440
|
-
|
|
441
|
-
describe('migrateReactCode', () => {
|
|
442
|
-
test('rewrites useState to signal', () => {
|
|
443
|
-
const code = `const [count, setCount] = useState(0)`
|
|
444
|
-
const result = migrateReactCode(code)
|
|
445
|
-
expect(result.code).toContain('count = signal(0)')
|
|
446
|
-
expect(result.code).not.toContain('useState')
|
|
447
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
test('rewrites useEffect with deps to effect', () => {
|
|
451
|
-
const code = `useEffect(() => { console.log(count) }, [count])`
|
|
452
|
-
const result = migrateReactCode(code)
|
|
453
|
-
expect(result.code).toContain('effect(')
|
|
454
|
-
expect(result.code).not.toContain('useEffect')
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
test('rewrites useEffect with empty deps to onMount', () => {
|
|
458
|
-
const code = `useEffect(() => { setup() }, [])`
|
|
459
|
-
const result = migrateReactCode(code)
|
|
460
|
-
expect(result.code).toContain('onMount(')
|
|
461
|
-
expect(result.code).not.toContain('useEffect')
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
test('rewrites useEffect with no deps to effect', () => {
|
|
465
|
-
const code = `useEffect(() => { console.log("render") })`
|
|
466
|
-
const result = migrateReactCode(code)
|
|
467
|
-
expect(result.code).toContain('effect(')
|
|
468
|
-
expect(result.code).not.toContain('useEffect')
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
test('rewrites useMemo to computed', () => {
|
|
472
|
-
const code = `const doubled = useMemo(() => count * 2, [count])`
|
|
473
|
-
const result = migrateReactCode(code)
|
|
474
|
-
expect(result.code).toContain('computed(')
|
|
475
|
-
expect(result.code).not.toContain('useMemo')
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
test('removes useCallback wrapper', () => {
|
|
479
|
-
const code = `const handleClick = useCallback(() => doThing(), [doThing])`
|
|
480
|
-
const result = migrateReactCode(code)
|
|
481
|
-
expect(result.code).toContain('() => doThing()')
|
|
482
|
-
expect(result.code).not.toContain('useCallback')
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
test('rewrites useRef(null) to createRef()', () => {
|
|
486
|
-
const code = `const inputRef = useRef(null)`
|
|
487
|
-
const result = migrateReactCode(code)
|
|
488
|
-
expect(result.code).toContain('createRef()')
|
|
489
|
-
expect(result.code).not.toContain('useRef')
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
test('rewrites useRef(value) to signal(value)', () => {
|
|
493
|
-
const code = `const prevCount = useRef(0)`
|
|
494
|
-
const result = migrateReactCode(code)
|
|
495
|
-
expect(result.code).toContain('signal(0)')
|
|
496
|
-
expect(result.code).not.toContain('useRef')
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
test('rewrites useRef(undefined) to createRef()', () => {
|
|
500
|
-
const code = `const ref = useRef(undefined)`
|
|
501
|
-
const result = migrateReactCode(code)
|
|
502
|
-
expect(result.code).toContain('createRef()')
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
test('removes memo() wrapper', () => {
|
|
506
|
-
const code = `const MyComp = memo(function MyComp() { return <div /> })`
|
|
507
|
-
const result = migrateReactCode(code)
|
|
508
|
-
expect(result.code).not.toContain('memo(')
|
|
509
|
-
expect(result.code).toContain('function MyComp()')
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
test('removes React.memo() wrapper', () => {
|
|
513
|
-
const code = `const MyComp = React.memo(function MyComp() { return <div /> })`
|
|
514
|
-
const result = migrateReactCode(code)
|
|
515
|
-
expect(result.code).not.toContain('React.memo')
|
|
516
|
-
expect(result.code).toContain('function MyComp()')
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
test('removes forwardRef() wrapper', () => {
|
|
520
|
-
const code = `const Input = forwardRef((props, ref) => <input ref={ref} />)`
|
|
521
|
-
const result = migrateReactCode(code)
|
|
522
|
-
expect(result.code).not.toContain('forwardRef')
|
|
523
|
-
expect(result.code).toContain('(props, ref) =>')
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
test('removes React.forwardRef() wrapper', () => {
|
|
527
|
-
const code = `const Input = React.forwardRef((props, ref) => <input ref={ref} />)`
|
|
528
|
-
const result = migrateReactCode(code)
|
|
529
|
-
expect(result.code).not.toContain('forwardRef')
|
|
530
|
-
expect(result.code).toContain('(props, ref) =>')
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
test('rewrites className to class', () => {
|
|
534
|
-
const code = `const el = <div className="container">Hello</div>`
|
|
535
|
-
const result = migrateReactCode(code)
|
|
536
|
-
expect(result.code).toContain('class="container"')
|
|
537
|
-
expect(result.code).not.toContain('className')
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
test('rewrites onChange to onInput on input', () => {
|
|
541
|
-
const code = `const el = <input onChange={handleChange} />`
|
|
542
|
-
const result = migrateReactCode(code)
|
|
543
|
-
expect(result.code).toContain('onInput')
|
|
544
|
-
expect(result.code).not.toContain('onChange')
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
test('rewrites onChange to onInput on textarea', () => {
|
|
548
|
-
const code = `const el = <textarea onChange={handleChange} />`
|
|
549
|
-
const result = migrateReactCode(code)
|
|
550
|
-
expect(result.code).toContain('onInput')
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
test('rewrites onChange to onInput on select', () => {
|
|
554
|
-
const code = `const el = <select onChange={handleChange} />`
|
|
555
|
-
const result = migrateReactCode(code)
|
|
556
|
-
expect(result.code).toContain('onInput')
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
test('rewrites React imports to Pyreon imports', () => {
|
|
560
|
-
const code = `import { useState, useEffect, useMemo } from "react"
|
|
561
|
-
const [count, setCount] = useState(0)
|
|
562
|
-
useEffect(() => { console.log(count) }, [count])
|
|
563
|
-
const doubled = useMemo(() => count * 2, [count])`
|
|
564
|
-
const result = migrateReactCode(code)
|
|
565
|
-
expect(result.code).not.toContain(`from "react"`)
|
|
566
|
-
expect(result.code).toContain('@pyreon/reactivity')
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
test('rewrites dangerouslySetInnerHTML to innerHTML', () => {
|
|
570
|
-
const code = `const el = <div dangerouslySetInnerHTML={{ __html: htmlString }} />`
|
|
571
|
-
const result = migrateReactCode(code)
|
|
572
|
-
expect(result.code).toContain('innerHTML={htmlString}')
|
|
573
|
-
expect(result.code).not.toContain('dangerouslySetInnerHTML')
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
test('returns change descriptions', () => {
|
|
577
|
-
const code = `const [count, setCount] = useState(0)`
|
|
578
|
-
const result = migrateReactCode(code)
|
|
579
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
580
|
-
expect(result.changes[0]!.description).toContain('useState')
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
test('handles code with no React patterns (no changes)', () => {
|
|
584
|
-
const code = `const count = signal(0)\neffect(() => console.log(count()))`
|
|
585
|
-
const result = migrateReactCode(code)
|
|
586
|
-
expect(result.code).toBe(code)
|
|
587
|
-
expect(result.changes).toEqual([])
|
|
588
|
-
expect(result.diagnostics).toEqual([])
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
test('includes diagnostics in migration result', () => {
|
|
592
|
-
const code = `import { useState } from "react"\nconst [count, setCount] = useState(0)`
|
|
593
|
-
const result = migrateReactCode(code)
|
|
594
|
-
expect(result.diagnostics.length).toBeGreaterThan(0)
|
|
595
|
-
expect(result.diagnostics.find((d) => d.code === 'use-state')).toBeDefined()
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
test('adds correct Pyreon imports', () => {
|
|
599
|
-
const code = `import { useState, useMemo } from "react"
|
|
600
|
-
const [count, setCount] = useState(0)
|
|
601
|
-
const doubled = useMemo(() => count * 2, [count])`
|
|
602
|
-
const result = migrateReactCode(code)
|
|
603
|
-
expect(result.code).toContain(`import { computed, signal } from "@pyreon/reactivity"`)
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
test('rewrites react-dom/client import specifiers', () => {
|
|
607
|
-
const code = `import { createRoot } from "react-dom/client"
|
|
608
|
-
createRoot(document.getElementById("root"))`
|
|
609
|
-
const result = migrateReactCode(code)
|
|
610
|
-
expect(result.code).not.toContain('react-dom')
|
|
611
|
-
expect(result.code).toContain('@pyreon/runtime-dom')
|
|
612
|
-
expect(result.code).toContain('mount')
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
test('migrates full React component', () => {
|
|
616
|
-
const code = `import { useState, useEffect, useMemo, useCallback, useRef, memo } from "react"
|
|
617
|
-
|
|
618
|
-
const Counter = memo(function Counter() {
|
|
619
|
-
const [count, setCount] = useState(0)
|
|
620
|
-
const inputRef = useRef(null)
|
|
621
|
-
const doubled = useMemo(() => count * 2, [count])
|
|
622
|
-
const handleClick = useCallback(() => setCount(c => c + 1), [])
|
|
623
|
-
|
|
624
|
-
useEffect(() => {
|
|
625
|
-
document.title = \`Count: \${count}\`
|
|
626
|
-
}, [count])
|
|
627
|
-
|
|
628
|
-
return <div className="counter">{count}</div>
|
|
629
|
-
})`
|
|
630
|
-
const result = migrateReactCode(code)
|
|
631
|
-
expect(result.code).not.toContain('useState')
|
|
632
|
-
expect(result.code).not.toContain('useMemo')
|
|
633
|
-
expect(result.code).not.toContain('useCallback')
|
|
634
|
-
expect(result.code).not.toContain('useRef')
|
|
635
|
-
expect(result.code).not.toContain('className')
|
|
636
|
-
expect(result.code).toContain('signal')
|
|
637
|
-
expect(result.code).toContain('computed')
|
|
638
|
-
expect(result.code).toContain('effect')
|
|
639
|
-
expect(result.code).toContain('createRef')
|
|
640
|
-
expect(result.code).toContain('class=')
|
|
641
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
642
|
-
})
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
// ─── diagnoseError ───────────────────────────────────────────────────────────
|
|
646
|
-
|
|
647
|
-
describe('diagnoseError', () => {
|
|
648
|
-
test("diagnoses 'X is not a function' as signal access issue", () => {
|
|
649
|
-
const result = diagnoseError('count is not a function')
|
|
650
|
-
expect(result).not.toBeNull()
|
|
651
|
-
expect(result!.cause).toContain('count')
|
|
652
|
-
expect(result!.fix).toContain('signal')
|
|
653
|
-
expect(result!.fixCode).toContain('count()')
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
test("diagnoses Cannot read properties of undefined (reading 'set')", () => {
|
|
657
|
-
const result = diagnoseError("Cannot read properties of undefined (reading 'set')")
|
|
658
|
-
expect(result).not.toBeNull()
|
|
659
|
-
expect(result!.cause).toContain('.set()')
|
|
660
|
-
expect(result!.fix).toContain('signal')
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
test("diagnoses Cannot read properties of undefined (reading 'update')", () => {
|
|
664
|
-
const result = diagnoseError("Cannot read properties of undefined (reading 'update')")
|
|
665
|
-
expect(result).not.toBeNull()
|
|
666
|
-
expect(result!.cause).toContain('.update()')
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
test("diagnoses Cannot read properties of undefined (reading 'peek')", () => {
|
|
670
|
-
const result = diagnoseError("Cannot read properties of undefined (reading 'peek')")
|
|
671
|
-
expect(result).not.toBeNull()
|
|
672
|
-
expect(result!.cause).toContain('.peek()')
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
test("diagnoses Cannot read properties of undefined (reading 'subscribe')", () => {
|
|
676
|
-
const result = diagnoseError("Cannot read properties of undefined (reading 'subscribe')")
|
|
677
|
-
expect(result).not.toBeNull()
|
|
678
|
-
expect(result!.cause).toContain('.subscribe()')
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
test('diagnoses missing @pyreon package', () => {
|
|
682
|
-
const result = diagnoseError("Cannot find module '@pyreon/reactivity'")
|
|
683
|
-
expect(result).not.toBeNull()
|
|
684
|
-
expect(result!.cause).toContain('@pyreon/reactivity')
|
|
685
|
-
expect(result!.fix).toContain('bun add')
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
test('diagnoses missing react module in Pyreon project', () => {
|
|
689
|
-
const result = diagnoseError("Cannot find module 'react'")
|
|
690
|
-
expect(result).not.toBeNull()
|
|
691
|
-
expect(result!.cause).toContain('react')
|
|
692
|
-
expect(result!.fix).toContain('Pyreon')
|
|
693
|
-
})
|
|
694
|
-
|
|
695
|
-
test('diagnoses .value property on Signal type', () => {
|
|
696
|
-
const result = diagnoseError("Property 'value' does not exist on type 'Signal<number>'")
|
|
697
|
-
expect(result).not.toBeNull()
|
|
698
|
-
expect(result!.cause).toContain('.value')
|
|
699
|
-
expect(result!.fix).toContain('callable')
|
|
700
|
-
expect(result!.fixCode).toContain('mySignal()')
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
test('diagnoses non-value property on Signal type', () => {
|
|
704
|
-
const result = diagnoseError("Property 'current' does not exist on type 'Signal<number>'")
|
|
705
|
-
expect(result).not.toBeNull()
|
|
706
|
-
expect(result!.cause).toContain('.current')
|
|
707
|
-
expect(result!.fix).toContain('.set()')
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
test('diagnoses type not assignable to VNode', () => {
|
|
711
|
-
const result = diagnoseError("Type 'number' is not assignable to type 'VNode'")
|
|
712
|
-
expect(result).not.toBeNull()
|
|
713
|
-
expect(result!.cause).toContain('number')
|
|
714
|
-
expect(result!.fix).toContain('JSX')
|
|
715
|
-
})
|
|
716
|
-
|
|
717
|
-
test('diagnoses onMount callback return type error', () => {
|
|
718
|
-
const result = diagnoseError('onMount callback must return')
|
|
719
|
-
expect(result).not.toBeNull()
|
|
720
|
-
expect(result!.cause).toContain('CleanupFn')
|
|
721
|
-
expect(result!.fixCode).toContain('onMount')
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
test('diagnoses missing by prop on For', () => {
|
|
725
|
-
const result = diagnoseError("Expected 'by' prop on <For>")
|
|
726
|
-
expect(result).not.toBeNull()
|
|
727
|
-
expect(result!.cause).toContain('by')
|
|
728
|
-
expect(result!.fixCode).toContain('by=')
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
test('diagnoses hook called outside component', () => {
|
|
732
|
-
const result = diagnoseError('useHook called outside component boundary')
|
|
733
|
-
expect(result).not.toBeNull()
|
|
734
|
-
expect(result!.cause).toContain('outside')
|
|
735
|
-
expect(result!.fix).toContain('component')
|
|
736
|
-
})
|
|
737
|
-
|
|
738
|
-
test('diagnoses hydration mismatch', () => {
|
|
739
|
-
const result = diagnoseError('Hydration mismatch')
|
|
740
|
-
expect(result).not.toBeNull()
|
|
741
|
-
expect(result!.cause).toContain('Server-rendered')
|
|
742
|
-
expect(result!.related).toContain('window')
|
|
743
|
-
})
|
|
744
|
-
|
|
745
|
-
test('returns null for unknown errors', () => {
|
|
746
|
-
expect(diagnoseError('Something completely unrelated happened')).toBeNull()
|
|
747
|
-
expect(diagnoseError('')).toBeNull()
|
|
748
|
-
expect(diagnoseError('TypeError: Cannot freeze')).toBeNull()
|
|
749
|
-
})
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
// ─── Additional branch coverage for react-intercept ─────────────────────────
|
|
753
|
-
|
|
754
|
-
describe('detectReactPatterns — edge cases for branch coverage', () => {
|
|
755
|
-
test('useState with no arguments', () => {
|
|
756
|
-
const result = detectReactPatterns(`
|
|
757
|
-
import { useState } from 'react'
|
|
758
|
-
function App() {
|
|
759
|
-
const [value, setValue] = useState()
|
|
760
|
-
return <div>{value}</div>
|
|
761
|
-
}
|
|
762
|
-
`)
|
|
763
|
-
expect(result.length).toBeGreaterThan(0)
|
|
764
|
-
const useStateDiag = result.find((d) => d.code === 'use-state')
|
|
765
|
-
expect(useStateDiag).toBeDefined()
|
|
766
|
-
// No argument → init defaults to 'undefined'
|
|
767
|
-
expect(useStateDiag!.suggested).toContain('signal(undefined)')
|
|
768
|
-
})
|
|
769
|
-
|
|
770
|
-
test('useState not destructured (bare call)', () => {
|
|
771
|
-
const result = detectReactPatterns(`
|
|
772
|
-
import { useState } from 'react'
|
|
773
|
-
function App() {
|
|
774
|
-
const state = useState(0)
|
|
775
|
-
return <div>{state[0]}</div>
|
|
776
|
-
}
|
|
777
|
-
`)
|
|
778
|
-
expect(result.length).toBeGreaterThan(0)
|
|
779
|
-
const useStateDiag = result.find((d) => d.code === 'use-state')
|
|
780
|
-
expect(useStateDiag).toBeDefined()
|
|
781
|
-
// Non-destructured useState → generic suggestion
|
|
782
|
-
expect(useStateDiag!.suggested).toContain('signal(initialValue)')
|
|
783
|
-
})
|
|
784
|
-
|
|
785
|
-
test('useEffect with empty deps but no callback', () => {
|
|
786
|
-
const result = detectReactPatterns(`
|
|
787
|
-
import { useEffect } from 'react'
|
|
788
|
-
function App() {
|
|
789
|
-
useEffect(undefined, [])
|
|
790
|
-
return <div />
|
|
791
|
-
}
|
|
792
|
-
`)
|
|
793
|
-
// Should not crash, may or may not detect
|
|
794
|
-
expect(result).toBeDefined()
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
test('non-react import not flagged', () => {
|
|
798
|
-
const result = detectReactPatterns(`
|
|
799
|
-
import { signal } from '@pyreon/reactivity'
|
|
800
|
-
function App() {
|
|
801
|
-
const x = signal(0)
|
|
802
|
-
return <div>{x()}</div>
|
|
803
|
-
}
|
|
804
|
-
`)
|
|
805
|
-
expect(result).toHaveLength(0)
|
|
806
|
-
})
|
|
807
|
-
|
|
808
|
-
test('useEffect with arrow expression body (no block)', () => {
|
|
809
|
-
const result = detectReactPatterns(`
|
|
810
|
-
import { useEffect } from 'react'
|
|
811
|
-
function App() {
|
|
812
|
-
useEffect(() => console.log('hi'), [])
|
|
813
|
-
return <div />
|
|
814
|
-
}
|
|
815
|
-
`)
|
|
816
|
-
const effectDiag = result.find((d) => d.code === 'use-effect-mount')
|
|
817
|
-
expect(effectDiag).toBeDefined()
|
|
818
|
-
})
|
|
819
|
-
|
|
820
|
-
test('useEffect with non-function callback', () => {
|
|
821
|
-
const result = detectReactPatterns(`
|
|
822
|
-
import { useEffect } from 'react'
|
|
823
|
-
function App() {
|
|
824
|
-
useEffect(handler, [])
|
|
825
|
-
return <div />
|
|
826
|
-
}
|
|
827
|
-
`)
|
|
828
|
-
const effectDiag = result.find((d) => d.code === 'use-effect-mount')
|
|
829
|
-
expect(effectDiag).toBeDefined()
|
|
830
|
-
})
|
|
831
|
-
|
|
832
|
-
test('useEffect with deps array (non-empty)', () => {
|
|
833
|
-
const result = detectReactPatterns(`
|
|
834
|
-
import { useEffect } from 'react'
|
|
835
|
-
function App() {
|
|
836
|
-
useEffect(() => { console.log(x) }, [x])
|
|
837
|
-
return <div />
|
|
838
|
-
}
|
|
839
|
-
`)
|
|
840
|
-
const effectDiag = result.find((d) => d.code === 'use-effect-deps')
|
|
841
|
-
expect(effectDiag).toBeDefined()
|
|
842
|
-
})
|
|
843
|
-
|
|
844
|
-
test('useEffect with no deps array', () => {
|
|
845
|
-
const result = detectReactPatterns(`
|
|
846
|
-
import { useEffect } from 'react'
|
|
847
|
-
function App() {
|
|
848
|
-
useEffect(() => { console.log('every render') })
|
|
849
|
-
return <div />
|
|
850
|
-
}
|
|
851
|
-
`)
|
|
852
|
-
const effectDiag = result.find((d) => d.code === 'use-effect-no-deps')
|
|
853
|
-
expect(effectDiag).toBeDefined()
|
|
854
|
-
})
|
|
855
|
-
|
|
856
|
-
test('useMemo with no compute function arg', () => {
|
|
857
|
-
const result = detectReactPatterns(`
|
|
858
|
-
import { useMemo } from 'react'
|
|
859
|
-
function App() {
|
|
860
|
-
const x = useMemo()
|
|
861
|
-
return <div>{x}</div>
|
|
862
|
-
}
|
|
863
|
-
`)
|
|
864
|
-
const memoDiag = result.find((d) => d.code === 'use-memo')
|
|
865
|
-
expect(memoDiag).toBeDefined()
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
test('useCallback with no callback function arg', () => {
|
|
869
|
-
const result = detectReactPatterns(`
|
|
870
|
-
import { useCallback } from 'react'
|
|
871
|
-
function App() {
|
|
872
|
-
const fn = useCallback()
|
|
873
|
-
return <div onClick={fn}>click</div>
|
|
874
|
-
}
|
|
875
|
-
`)
|
|
876
|
-
const cbDiag = result.find((d) => d.code === 'use-callback')
|
|
877
|
-
expect(cbDiag).toBeDefined()
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
test('useRef with no argument (null ref)', () => {
|
|
881
|
-
const result = detectReactPatterns(`
|
|
882
|
-
import { useRef } from 'react'
|
|
883
|
-
function App() {
|
|
884
|
-
const ref = useRef()
|
|
885
|
-
return <div ref={ref}>text</div>
|
|
886
|
-
}
|
|
887
|
-
`)
|
|
888
|
-
const refDiag = result.find((d) => d.code === 'use-ref-dom' || d.code === 'use-ref-box')
|
|
889
|
-
expect(refDiag).toBeDefined()
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
test('memo() with no argument', () => {
|
|
893
|
-
const result = detectReactPatterns(`
|
|
894
|
-
import { memo } from 'react'
|
|
895
|
-
const App = memo()
|
|
896
|
-
`)
|
|
897
|
-
const memoDiag = result.find((d) => d.code === 'memo-wrapper')
|
|
898
|
-
expect(memoDiag).toBeDefined()
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
test('array.map in JSX detected', () => {
|
|
902
|
-
const result = detectReactPatterns(`
|
|
903
|
-
function App() {
|
|
904
|
-
return <ul>{items.map(item => <li>{item}</li>)}</ul>
|
|
905
|
-
}
|
|
906
|
-
`)
|
|
907
|
-
const mapDiag = result.find((d) => d.code === 'array-map-jsx')
|
|
908
|
-
expect(mapDiag).toBeDefined()
|
|
909
|
-
})
|
|
910
|
-
|
|
911
|
-
test('array.map in JSX with no callback argument', () => {
|
|
912
|
-
const result = detectReactPatterns(`
|
|
913
|
-
function App() {
|
|
914
|
-
return <ul>{items.map()}</ul>
|
|
915
|
-
}
|
|
916
|
-
`)
|
|
917
|
-
const mapDiag = result.find((d) => d.code === 'array-map-jsx')
|
|
918
|
-
expect(mapDiag).toBeDefined()
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
test('react-router-dom import detected', () => {
|
|
922
|
-
const result = detectReactPatterns(`
|
|
923
|
-
import { useNavigate, useParams } from 'react-router-dom'
|
|
924
|
-
`)
|
|
925
|
-
expect(result.some((d) => d.code === 'react-router-import')).toBe(true)
|
|
926
|
-
})
|
|
927
|
-
|
|
928
|
-
test('react-dom/client import detected', () => {
|
|
929
|
-
const result = detectReactPatterns(`
|
|
930
|
-
import { createRoot } from 'react-dom/client'
|
|
931
|
-
`)
|
|
932
|
-
expect(result.some((d) => d.code === 'react-dom-import')).toBe(true)
|
|
933
|
-
})
|
|
934
|
-
})
|
|
935
|
-
|
|
936
|
-
describe('migrateReactCode — edge cases for branch coverage', () => {
|
|
937
|
-
test('migrates useState without arguments', () => {
|
|
938
|
-
const result = migrateReactCode(`
|
|
939
|
-
import { useState } from 'react'
|
|
940
|
-
function App() {
|
|
941
|
-
const [count, setCount] = useState()
|
|
942
|
-
return <div>{count}</div>
|
|
943
|
-
}
|
|
944
|
-
`)
|
|
945
|
-
expect(result.code).toContain('signal(undefined)')
|
|
946
|
-
})
|
|
947
|
-
|
|
948
|
-
test('migrates code with no existing imports (inserts at top)', () => {
|
|
949
|
-
const result = migrateReactCode(`const [x, setX] = useState(0)`)
|
|
950
|
-
expect(result.code).toBeDefined()
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
test('dangerouslySetInnerHTML without expression', () => {
|
|
954
|
-
const result = migrateReactCode(`
|
|
955
|
-
import React from 'react'
|
|
956
|
-
function App() {
|
|
957
|
-
return <div dangerouslySetInnerHTML />
|
|
958
|
-
}
|
|
959
|
-
`)
|
|
960
|
-
// Should not crash, attr without value
|
|
961
|
-
expect(result.code).toBeDefined()
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
test('dangerouslySetInnerHTML with non-object expression', () => {
|
|
965
|
-
const result = migrateReactCode(`
|
|
966
|
-
import React from 'react'
|
|
967
|
-
function App() {
|
|
968
|
-
return <div dangerouslySetInnerHTML={getHtml()} />
|
|
969
|
-
}
|
|
970
|
-
`)
|
|
971
|
-
// Non-object expression → not migrated
|
|
972
|
-
expect(result.code).toBeDefined()
|
|
973
|
-
})
|
|
974
|
-
|
|
975
|
-
test('className attribute migrated to class', () => {
|
|
976
|
-
const result = migrateReactCode(`
|
|
977
|
-
import React from 'react'
|
|
978
|
-
function App() {
|
|
979
|
-
return <div className="foo">text</div>
|
|
980
|
-
}
|
|
981
|
-
`)
|
|
982
|
-
expect(result.code).toContain('class=')
|
|
983
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
test('source file with import from non-react module not rewritten', () => {
|
|
987
|
-
const result = migrateReactCode(`
|
|
988
|
-
import { signal } from '@pyreon/reactivity'
|
|
989
|
-
const x = signal(0)
|
|
990
|
-
`)
|
|
991
|
-
expect(result.changes).toHaveLength(0)
|
|
992
|
-
})
|
|
993
|
-
|
|
994
|
-
test('migrates useRef with non-null initial value (mutable box)', () => {
|
|
995
|
-
const result = migrateReactCode(`
|
|
996
|
-
import { useRef } from 'react'
|
|
997
|
-
function App() {
|
|
998
|
-
const ref = useRef(42)
|
|
999
|
-
return <div>{ref.current}</div>
|
|
1000
|
-
}
|
|
1001
|
-
`)
|
|
1002
|
-
expect(result.code).toContain('signal(42)')
|
|
1003
|
-
})
|
|
1004
|
-
|
|
1005
|
-
test('migrates useMemo', () => {
|
|
1006
|
-
const result = migrateReactCode(`
|
|
1007
|
-
import { useMemo } from 'react'
|
|
1008
|
-
function App() {
|
|
1009
|
-
const doubled = useMemo(() => count * 2, [count])
|
|
1010
|
-
return <div>{doubled}</div>
|
|
1011
|
-
}
|
|
1012
|
-
`)
|
|
1013
|
-
expect(result.code).toContain('computed(')
|
|
1014
|
-
})
|
|
1015
|
-
|
|
1016
|
-
test('migrates useCallback', () => {
|
|
1017
|
-
const result = migrateReactCode(`
|
|
1018
|
-
import { useCallback } from 'react'
|
|
1019
|
-
function App() {
|
|
1020
|
-
const handleClick = useCallback(() => console.log('click'), [])
|
|
1021
|
-
return <button onClick={handleClick}>click</button>
|
|
1022
|
-
}
|
|
1023
|
-
`)
|
|
1024
|
-
// useCallback → plain function (not needed in Pyreon)
|
|
1025
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
1026
|
-
})
|
|
1027
|
-
|
|
1028
|
-
test('migrates React.memo wrapper', () => {
|
|
1029
|
-
const result = migrateReactCode(`
|
|
1030
|
-
import React from 'react'
|
|
1031
|
-
const App = React.memo(function App() {
|
|
1032
|
-
return <div>hello</div>
|
|
1033
|
-
})
|
|
1034
|
-
`)
|
|
1035
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
1036
|
-
})
|
|
1037
|
-
|
|
1038
|
-
test('migrates standalone memo() wrapper', () => {
|
|
1039
|
-
const result = migrateReactCode(`
|
|
1040
|
-
import { memo } from 'react'
|
|
1041
|
-
const App = memo(function App() {
|
|
1042
|
-
return <div>hello</div>
|
|
1043
|
-
})
|
|
1044
|
-
`)
|
|
1045
|
-
expect(result.changes.length).toBeGreaterThan(0)
|
|
1046
|
-
})
|
|
1047
|
-
|
|
1048
|
-
test('migrates code with no existing imports (inserts at beginning)', () => {
|
|
1049
|
-
// No import statement in the source — lastImportEnd === 0 → line 926 false branch
|
|
1050
|
-
const result = migrateReactCode(`
|
|
1051
|
-
const [count, setCount] = useState(0)
|
|
1052
|
-
const x = useMemo(() => count * 2)
|
|
1053
|
-
`)
|
|
1054
|
-
// Should insert pyreon imports at the top
|
|
1055
|
-
expect(result.code).toContain('import {')
|
|
1056
|
-
expect(result.code).toContain('@pyreon/')
|
|
1057
|
-
})
|
|
1058
|
-
|
|
1059
|
-
test('migrates dangerouslySetInnerHTML with __html property', () => {
|
|
1060
|
-
const result = migrateReactCode(`
|
|
1061
|
-
import React from 'react'
|
|
1062
|
-
function App() {
|
|
1063
|
-
return <div dangerouslySetInnerHTML={{ __html: '<b>bold</b>' }} />
|
|
1064
|
-
}
|
|
1065
|
-
`)
|
|
1066
|
-
expect(result.code).toContain('innerHTML')
|
|
1067
|
-
})
|
|
1068
|
-
|
|
1069
|
-
test('migrates className on JSX elements', () => {
|
|
1070
|
-
const result = migrateReactCode(`
|
|
1071
|
-
import React from 'react'
|
|
1072
|
-
function App() {
|
|
1073
|
-
return <div className="foo"><span className="bar">text</span></div>
|
|
1074
|
-
}
|
|
1075
|
-
`)
|
|
1076
|
-
expect(result.code).toContain('class=')
|
|
1077
|
-
expect(result.changes.length).toBeGreaterThanOrEqual(2)
|
|
1078
|
-
})
|
|
1079
|
-
|
|
1080
|
-
test('migrates htmlFor on label elements', () => {
|
|
1081
|
-
const result = migrateReactCode(`
|
|
1082
|
-
import React from 'react'
|
|
1083
|
-
function App() {
|
|
1084
|
-
return <label htmlFor="name">Name</label>
|
|
1085
|
-
}
|
|
1086
|
-
`)
|
|
1087
|
-
expect(result.code).toContain('for=')
|
|
1088
|
-
})
|
|
1089
|
-
|
|
1090
|
-
test('migrates useEffect with cleanup function', () => {
|
|
1091
|
-
const result = migrateReactCode(`
|
|
1092
|
-
import { useEffect } from 'react'
|
|
1093
|
-
function App() {
|
|
1094
|
-
useEffect(() => {
|
|
1095
|
-
const handler = () => {}
|
|
1096
|
-
window.addEventListener('resize', handler)
|
|
1097
|
-
return () => window.removeEventListener('resize', handler)
|
|
1098
|
-
}, [])
|
|
1099
|
-
return <div />
|
|
1100
|
-
}
|
|
1101
|
-
`)
|
|
1102
|
-
expect(result.code).toContain('onMount')
|
|
1103
|
-
})
|
|
1104
|
-
})
|