@pyreon/compiler 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -54
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +414 -9
- package/lib/types/index.d.ts +94 -1
- package/package.json +12 -12
- package/src/index.ts +2 -0
- package/src/jsx.ts +425 -5
- package/src/lpih.ts +270 -0
- package/src/pyreon-intercept.ts +19 -8
- package/src/ssg-audit.ts +3 -3
- package/src/tests/collapse-bail-census.test.ts +101 -16
- package/src/tests/component-child-no-wrap.test.ts +204 -0
- package/src/tests/dynamic-collapse-detector.test.ts +164 -0
- package/src/tests/dynamic-collapse-emit.test.ts +192 -0
- package/src/tests/dynamic-collapse-scan.test.ts +111 -0
- package/src/tests/lpih.test.ts +404 -0
- package/src/tests/native-equivalence.test.ts +92 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Program Inlay Hints — pure merge-function tests.
|
|
3
|
+
*
|
|
4
|
+
* Proves the end-to-end story: static findings from `analyzeReactivity()`
|
|
5
|
+
* merge with runtime fire data into enriched findings that an LSP can
|
|
6
|
+
* serve as inlay hints. The runtime side is tested separately in
|
|
7
|
+
* `@pyreon/reactivity` (`lpih-source-location.test.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
import {
|
|
11
|
+
firesToCreationSiteFindings,
|
|
12
|
+
type LPIHFireDatum,
|
|
13
|
+
mergeFireDataIntoFindings,
|
|
14
|
+
} from '../lpih'
|
|
15
|
+
import { analyzeReactivity } from '../reactivity-lens'
|
|
16
|
+
import type { ReactivityFinding } from '../reactivity-lens'
|
|
17
|
+
|
|
18
|
+
const finding = (
|
|
19
|
+
kind: ReactivityFinding['kind'],
|
|
20
|
+
line: number,
|
|
21
|
+
detail: string,
|
|
22
|
+
): ReactivityFinding => ({
|
|
23
|
+
kind,
|
|
24
|
+
line,
|
|
25
|
+
column: 0,
|
|
26
|
+
endLine: line,
|
|
27
|
+
endColumn: 10,
|
|
28
|
+
detail,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const fire = (
|
|
32
|
+
file: string,
|
|
33
|
+
line: number,
|
|
34
|
+
count: number,
|
|
35
|
+
kind?: LPIHFireDatum['kind'],
|
|
36
|
+
): LPIHFireDatum => ({ file, line, count, kind })
|
|
37
|
+
|
|
38
|
+
describe('mergeFireDataIntoFindings — basic shape', () => {
|
|
39
|
+
it('passes findings through unchanged when no fires', () => {
|
|
40
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
41
|
+
const out = mergeFireDataIntoFindings(findings, [], 'app.tsx')
|
|
42
|
+
expect(out).toEqual(findings)
|
|
43
|
+
expect(out).toBe(findings) // identity preserved on no-op
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('passes findings through unchanged when no fires match the file', () => {
|
|
47
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
48
|
+
const out = mergeFireDataIntoFindings(
|
|
49
|
+
findings,
|
|
50
|
+
[fire('other.tsx', 5, 3, 'signal')],
|
|
51
|
+
'app.tsx',
|
|
52
|
+
)
|
|
53
|
+
expect(out[0]?.detail).toBe('live') // not enriched
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('enriches a matching reactive finding with the fire count + kind', () => {
|
|
57
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
58
|
+
const out = mergeFireDataIntoFindings(
|
|
59
|
+
findings,
|
|
60
|
+
[fire('app.tsx', 5, 240, 'signal')],
|
|
61
|
+
'app.tsx',
|
|
62
|
+
)
|
|
63
|
+
expect(out[0]?.detail).toBe('live — signal fired 240×')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('does NOT mutate the input findings', () => {
|
|
67
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
68
|
+
const before = findings[0]?.detail
|
|
69
|
+
mergeFireDataIntoFindings(
|
|
70
|
+
findings,
|
|
71
|
+
[fire('app.tsx', 5, 240, 'signal')],
|
|
72
|
+
'app.tsx',
|
|
73
|
+
)
|
|
74
|
+
expect(findings[0]?.detail).toBe(before) // unchanged
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('mergeFireDataIntoFindings — span-kind filtering', () => {
|
|
79
|
+
it('skips footgun findings (not runtime-active reactive reads)', () => {
|
|
80
|
+
const findings = [finding('footgun', 5, 'props destructured')]
|
|
81
|
+
const out = mergeFireDataIntoFindings(
|
|
82
|
+
findings,
|
|
83
|
+
[fire('app.tsx', 5, 5, 'signal')],
|
|
84
|
+
'app.tsx',
|
|
85
|
+
)
|
|
86
|
+
expect(out[0]?.detail).toBe('props destructured') // unchanged
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('skips hoisted-static findings', () => {
|
|
90
|
+
const findings = [finding('hoisted-static', 5, 'hoisted')]
|
|
91
|
+
const out = mergeFireDataIntoFindings(
|
|
92
|
+
findings,
|
|
93
|
+
[fire('app.tsx', 5, 5, 'signal')],
|
|
94
|
+
'app.tsx',
|
|
95
|
+
)
|
|
96
|
+
expect(out[0]?.detail).toBe('hoisted')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('skips static-text findings', () => {
|
|
100
|
+
const findings = [finding('static-text', 5, 'baked')]
|
|
101
|
+
const out = mergeFireDataIntoFindings(
|
|
102
|
+
findings,
|
|
103
|
+
[fire('app.tsx', 5, 5, 'signal')],
|
|
104
|
+
'app.tsx',
|
|
105
|
+
)
|
|
106
|
+
expect(out[0]?.detail).toBe('baked')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('enriches reactive-prop kinds', () => {
|
|
110
|
+
const findings = [finding('reactive-prop', 7, 'reactive prop')]
|
|
111
|
+
const out = mergeFireDataIntoFindings(
|
|
112
|
+
findings,
|
|
113
|
+
[fire('app.tsx', 7, 12, 'signal')],
|
|
114
|
+
'app.tsx',
|
|
115
|
+
)
|
|
116
|
+
expect(out[0]?.detail).toBe('reactive prop — signal fired 12×')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('enriches reactive-attr kinds', () => {
|
|
120
|
+
const findings = [finding('reactive-attr', 9, 'live attr')]
|
|
121
|
+
const out = mergeFireDataIntoFindings(
|
|
122
|
+
findings,
|
|
123
|
+
[fire('app.tsx', 9, 3, 'derived')],
|
|
124
|
+
'app.tsx',
|
|
125
|
+
)
|
|
126
|
+
expect(out[0]?.detail).toBe('live attr — derived fired 3×')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('mergeFireDataIntoFindings — aggregation', () => {
|
|
131
|
+
it('sums fires at the same line', () => {
|
|
132
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
133
|
+
const out = mergeFireDataIntoFindings(
|
|
134
|
+
findings,
|
|
135
|
+
[
|
|
136
|
+
fire('app.tsx', 5, 10, 'signal'),
|
|
137
|
+
fire('app.tsx', 5, 30, 'signal'),
|
|
138
|
+
],
|
|
139
|
+
'app.tsx',
|
|
140
|
+
)
|
|
141
|
+
expect(out[0]?.detail).toBe('live — signal fired 40×')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('uses latest lastFire + corresponding kind when summing', () => {
|
|
145
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
146
|
+
const out = mergeFireDataIntoFindings(
|
|
147
|
+
findings,
|
|
148
|
+
[
|
|
149
|
+
{ file: 'app.tsx', line: 5, count: 10, lastFire: 100, kind: 'signal' },
|
|
150
|
+
{ file: 'app.tsx', line: 5, count: 5, lastFire: 999, kind: 'derived' },
|
|
151
|
+
],
|
|
152
|
+
'app.tsx',
|
|
153
|
+
)
|
|
154
|
+
// Latest fire is `derived` at ts=999, so the kind label is 'derived'.
|
|
155
|
+
expect(out[0]?.detail).toBe('live — derived fired 15×')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('keeps the earlier kind when incoming fire has no lastFire', () => {
|
|
159
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
160
|
+
const out = mergeFireDataIntoFindings(
|
|
161
|
+
findings,
|
|
162
|
+
[
|
|
163
|
+
{ file: 'app.tsx', line: 5, count: 10, lastFire: 100, kind: 'signal' },
|
|
164
|
+
{ file: 'app.tsx', line: 5, count: 5, kind: 'derived' }, // no lastFire
|
|
165
|
+
],
|
|
166
|
+
'app.tsx',
|
|
167
|
+
)
|
|
168
|
+
expect(out[0]?.detail).toBe('live — signal fired 15×')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('mergeFireDataIntoFindings — file-normalization', () => {
|
|
173
|
+
it('uses normalizeFile to compare paths', () => {
|
|
174
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
175
|
+
const norm = (p: string): string => p.replace(/^.*\//, '')
|
|
176
|
+
const out = mergeFireDataIntoFindings(
|
|
177
|
+
findings,
|
|
178
|
+
[fire('/abs/path/app.tsx', 5, 7, 'signal')],
|
|
179
|
+
'workspace://app.tsx',
|
|
180
|
+
{ normalizeFile: norm },
|
|
181
|
+
)
|
|
182
|
+
expect(out[0]?.detail).toBe('live — signal fired 7×')
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('mergeFireDataIntoFindings — custom format', () => {
|
|
187
|
+
it('uses formatDetail when provided', () => {
|
|
188
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
189
|
+
const out = mergeFireDataIntoFindings(
|
|
190
|
+
findings,
|
|
191
|
+
[fire('app.tsx', 5, 42, 'signal')],
|
|
192
|
+
'app.tsx',
|
|
193
|
+
{ formatDetail: (d, f) => `${d} [${f.count}]` },
|
|
194
|
+
)
|
|
195
|
+
expect(out[0]?.detail).toBe('live [42]')
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('firesToCreationSiteFindings — synthetic creation-site hints', () => {
|
|
200
|
+
it('returns empty when no fires', () => {
|
|
201
|
+
expect(firesToCreationSiteFindings([], 'app.tsx')).toEqual([])
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('produces one finding per unique line', () => {
|
|
205
|
+
const out = firesToCreationSiteFindings(
|
|
206
|
+
[
|
|
207
|
+
fire('app.tsx', 5, 100, 'signal'),
|
|
208
|
+
fire('app.tsx', 8, 50, 'derived'),
|
|
209
|
+
],
|
|
210
|
+
'app.tsx',
|
|
211
|
+
)
|
|
212
|
+
expect(out).toHaveLength(2)
|
|
213
|
+
expect(out[0]?.line).toBe(5)
|
|
214
|
+
expect(out[0]?.detail).toBe('signal fired 100×')
|
|
215
|
+
expect(out[1]?.line).toBe(8)
|
|
216
|
+
expect(out[1]?.detail).toBe('derived fired 50×')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('aggregates multiple fires on the same line', () => {
|
|
220
|
+
const out = firesToCreationSiteFindings(
|
|
221
|
+
[
|
|
222
|
+
fire('app.tsx', 5, 100, 'signal'),
|
|
223
|
+
fire('app.tsx', 5, 50, 'signal'),
|
|
224
|
+
],
|
|
225
|
+
'app.tsx',
|
|
226
|
+
)
|
|
227
|
+
expect(out).toHaveLength(1)
|
|
228
|
+
expect(out[0]?.detail).toBe('signal fired 150×')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('skips fires from other files', () => {
|
|
232
|
+
const out = firesToCreationSiteFindings(
|
|
233
|
+
[
|
|
234
|
+
fire('app.tsx', 5, 100, 'signal'),
|
|
235
|
+
fire('other.tsx', 5, 50, 'signal'),
|
|
236
|
+
],
|
|
237
|
+
'app.tsx',
|
|
238
|
+
)
|
|
239
|
+
expect(out).toHaveLength(1)
|
|
240
|
+
expect(out[0]?.line).toBe(5)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('uses live-fire kind for the synthetic finding', () => {
|
|
244
|
+
const out = firesToCreationSiteFindings(
|
|
245
|
+
[fire('app.tsx', 5, 100, 'signal')],
|
|
246
|
+
'app.tsx',
|
|
247
|
+
)
|
|
248
|
+
expect(out[0]?.kind).toBe('live-fire')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('sorts findings by line ascending', () => {
|
|
252
|
+
const out = firesToCreationSiteFindings(
|
|
253
|
+
[
|
|
254
|
+
fire('app.tsx', 10, 1, 'signal'),
|
|
255
|
+
fire('app.tsx', 3, 1, 'signal'),
|
|
256
|
+
fire('app.tsx', 7, 1, 'signal'),
|
|
257
|
+
],
|
|
258
|
+
'app.tsx',
|
|
259
|
+
)
|
|
260
|
+
expect(out.map((f) => f.line)).toEqual([3, 7, 10])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('honors normalizeFile for cross-realm paths', () => {
|
|
264
|
+
const out = firesToCreationSiteFindings(
|
|
265
|
+
[fire('/abs/app.tsx', 5, 100, 'signal')],
|
|
266
|
+
'workspace://app.tsx',
|
|
267
|
+
{ normalizeFile: (p) => p.replace(/^.*\//, '') },
|
|
268
|
+
)
|
|
269
|
+
expect(out).toHaveLength(1)
|
|
270
|
+
expect(out[0]?.detail).toBe('signal fired 100×')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('honors custom formatDetail', () => {
|
|
274
|
+
const out = firesToCreationSiteFindings(
|
|
275
|
+
[fire('app.tsx', 5, 42, 'signal')],
|
|
276
|
+
'app.tsx',
|
|
277
|
+
{ formatDetail: (_, f) => `🔥 ${f.count}` },
|
|
278
|
+
)
|
|
279
|
+
expect(out[0]?.detail).toBe('🔥 42')
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('end-to-end — analyzeReactivity + merge', () => {
|
|
284
|
+
it('enriches the static analysis output with runtime data', () => {
|
|
285
|
+
const code = `function App() {
|
|
286
|
+
const count = signal(0)
|
|
287
|
+
return <div>{count()}</div>
|
|
288
|
+
}`
|
|
289
|
+
const { findings } = analyzeReactivity(code, 'app.tsx')
|
|
290
|
+
// The reactive {count()} span is on line 3.
|
|
291
|
+
const reactiveFinding = findings.find(
|
|
292
|
+
(f) => f.kind === 'reactive' && f.line === 3,
|
|
293
|
+
)
|
|
294
|
+
expect(reactiveFinding).toBeDefined()
|
|
295
|
+
const enriched = mergeFireDataIntoFindings(
|
|
296
|
+
findings,
|
|
297
|
+
[fire('app.tsx', 3, 50, 'signal')],
|
|
298
|
+
'app.tsx',
|
|
299
|
+
)
|
|
300
|
+
const target = enriched.find((f) => f.kind === 'reactive' && f.line === 3)
|
|
301
|
+
expect(target?.detail).toContain('fired 50×')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('leaves footguns visible even after merge (LPIH is additive)', () => {
|
|
305
|
+
// `const { x } = props` triggers the props-destructured footgun.
|
|
306
|
+
const code = `function App(props) {
|
|
307
|
+
const { x } = props
|
|
308
|
+
return <div>{x}</div>
|
|
309
|
+
}`
|
|
310
|
+
const { findings } = analyzeReactivity(code, 'app.tsx')
|
|
311
|
+
const footgun = findings.find((f) => f.kind === 'footgun')
|
|
312
|
+
expect(footgun).toBeDefined()
|
|
313
|
+
const enriched = mergeFireDataIntoFindings(
|
|
314
|
+
findings,
|
|
315
|
+
[fire('app.tsx', footgun?.line ?? 0, 5, 'signal')],
|
|
316
|
+
'app.tsx',
|
|
317
|
+
)
|
|
318
|
+
const afterFootgun = enriched.find((f) => f.kind === 'footgun')
|
|
319
|
+
expect(afterFootgun?.detail).toBe(footgun?.detail) // unchanged
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('rate1s — label formatting', () => {
|
|
324
|
+
it('omits rate when below threshold (dormant)', () => {
|
|
325
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
326
|
+
const out = mergeFireDataIntoFindings(
|
|
327
|
+
findings,
|
|
328
|
+
[{ file: 'app.tsx', line: 5, count: 100, kind: 'signal', rate1s: 0.1 }],
|
|
329
|
+
'app.tsx',
|
|
330
|
+
)
|
|
331
|
+
expect(out[0]?.detail).toBe('live — signal fired 100×')
|
|
332
|
+
expect(out[0]?.detail).not.toContain('/s')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('omits rate when undefined (older runtime / no field)', () => {
|
|
336
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
337
|
+
const out = mergeFireDataIntoFindings(
|
|
338
|
+
findings,
|
|
339
|
+
[{ file: 'app.tsx', line: 5, count: 100, kind: 'signal' }],
|
|
340
|
+
'app.tsx',
|
|
341
|
+
)
|
|
342
|
+
expect(out[0]?.detail).toBe('live — signal fired 100×')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('includes 1-decimal rate when above threshold and < 10/s', () => {
|
|
346
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
347
|
+
const out = mergeFireDataIntoFindings(
|
|
348
|
+
findings,
|
|
349
|
+
[{ file: 'app.tsx', line: 5, count: 100, kind: 'signal', rate1s: 3.7 }],
|
|
350
|
+
'app.tsx',
|
|
351
|
+
)
|
|
352
|
+
expect(out[0]?.detail).toBe('live — signal fired 100× (3.7/s)')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('rounds rate to integer at >= 10/s', () => {
|
|
356
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
357
|
+
const out = mergeFireDataIntoFindings(
|
|
358
|
+
findings,
|
|
359
|
+
[{ file: 'app.tsx', line: 5, count: 1000, kind: 'signal', rate1s: 47.3 }],
|
|
360
|
+
'app.tsx',
|
|
361
|
+
)
|
|
362
|
+
expect(out[0]?.detail).toBe('live — signal fired 1000× (47/s)')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('firesToCreationSiteFindings respects rate1s in label', () => {
|
|
366
|
+
const out = firesToCreationSiteFindings(
|
|
367
|
+
[
|
|
368
|
+
{ file: 'app.tsx', line: 5, count: 50, kind: 'signal', rate1s: 5.2 },
|
|
369
|
+
{ file: 'app.tsx', line: 8, count: 100, kind: 'effect', rate1s: 0.0 },
|
|
370
|
+
],
|
|
371
|
+
'app.tsx',
|
|
372
|
+
)
|
|
373
|
+
expect(out).toHaveLength(2)
|
|
374
|
+
expect(out[0]?.detail).toBe('signal fired 50× (5.2/s)')
|
|
375
|
+
expect(out[1]?.detail).toBe('effect fired 100×')
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('sums rates when multiple fires share the same line', () => {
|
|
379
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
380
|
+
const out = mergeFireDataIntoFindings(
|
|
381
|
+
findings,
|
|
382
|
+
[
|
|
383
|
+
{ file: 'app.tsx', line: 5, count: 30, kind: 'signal', rate1s: 2.0 },
|
|
384
|
+
{ file: 'app.tsx', line: 5, count: 20, kind: 'signal', rate1s: 3.5 },
|
|
385
|
+
],
|
|
386
|
+
'app.tsx',
|
|
387
|
+
)
|
|
388
|
+
expect(out[0]?.detail).toBe('live — signal fired 50× (5.5/s)')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('custom formatDetail receives rate1s in the fire object', () => {
|
|
392
|
+
const findings = [finding('reactive', 5, 'live')]
|
|
393
|
+
const out = mergeFireDataIntoFindings(
|
|
394
|
+
findings,
|
|
395
|
+
[{ file: 'app.tsx', line: 5, count: 100, kind: 'signal', rate1s: 7.5 }],
|
|
396
|
+
'app.tsx',
|
|
397
|
+
{
|
|
398
|
+
formatDetail: (d, f) =>
|
|
399
|
+
`${d} [rate=${f.rate1s?.toFixed(1) ?? 'n/a'}]`,
|
|
400
|
+
},
|
|
401
|
+
)
|
|
402
|
+
expect(out[0]?.detail).toBe('live [rate=7.5]')
|
|
403
|
+
})
|
|
404
|
+
})
|
|
@@ -830,3 +830,95 @@ describeNative('Reactivity-lens — JS↔Rust span parity', () => {
|
|
|
830
830
|
test('signal auto-call shape — declared signal, bare {count} → reactive', () =>
|
|
831
831
|
compareLens('const count = signal(0); const App = () => <div>{count}</div>', 'auto.tsx'))
|
|
832
832
|
})
|
|
833
|
+
|
|
834
|
+
describeNative('cross-backend: component-child stable-reference carve-out', () => {
|
|
835
|
+
test('bare Identifier (splitProps-derived const) emitted bare in component child', () =>
|
|
836
|
+
compare(`
|
|
837
|
+
const Kinetic = (props) => {
|
|
838
|
+
const [childHolder, restHtml] = splitProps(props, ['children'])
|
|
839
|
+
const children = childHolder.children
|
|
840
|
+
return <StaggerRenderer htmlProps={restHtml}>{children}</StaggerRenderer>
|
|
841
|
+
}
|
|
842
|
+
`))
|
|
843
|
+
|
|
844
|
+
test('simple MemberExpression chain emitted bare in component child', () =>
|
|
845
|
+
compare(`
|
|
846
|
+
const Comp = (props) => {
|
|
847
|
+
const [obj] = splitProps(props, ['deep'])
|
|
848
|
+
return <Inner>{obj.deep.x}</Inner>
|
|
849
|
+
}
|
|
850
|
+
`))
|
|
851
|
+
|
|
852
|
+
test('CallExpression keeps the wrap in component child', () =>
|
|
853
|
+
compare(`
|
|
854
|
+
const count = signal(0)
|
|
855
|
+
const Comp = () => <Inner>{count()}</Inner>
|
|
856
|
+
`))
|
|
857
|
+
|
|
858
|
+
test('BinaryExpression keeps the wrap in component child', () =>
|
|
859
|
+
compare(`
|
|
860
|
+
const Comp = (props) => {
|
|
861
|
+
const [own] = splitProps(props, ['a', 'b'])
|
|
862
|
+
return <Inner>{own.a + own.b}</Inner>
|
|
863
|
+
}
|
|
864
|
+
`))
|
|
865
|
+
|
|
866
|
+
test('DOM-element parent keeps reactive binding (no carve-out)', () =>
|
|
867
|
+
compare(`
|
|
868
|
+
const Comp = (props) => {
|
|
869
|
+
const [own] = splitProps(props, ['children'])
|
|
870
|
+
const children = own.children
|
|
871
|
+
return <div>{children}</div>
|
|
872
|
+
}
|
|
873
|
+
`))
|
|
874
|
+
|
|
875
|
+
test('user-written accessor child passes through unchanged', () =>
|
|
876
|
+
compare(`
|
|
877
|
+
const x = signal('a')
|
|
878
|
+
const Comp = () => <Inner>{() => x()}</Inner>
|
|
879
|
+
`))
|
|
880
|
+
|
|
881
|
+
test('bare signal identifier in component child — KEEPS wrap (auto-call + reactivity)', () =>
|
|
882
|
+
compare(`
|
|
883
|
+
function C() {
|
|
884
|
+
const count = signal(0)
|
|
885
|
+
return <MyComp>{count}</MyComp>
|
|
886
|
+
}
|
|
887
|
+
`))
|
|
888
|
+
|
|
889
|
+
test('TS-cast wrapper (`children as VNode[]`) is transparent — both backends', () =>
|
|
890
|
+
compare(`
|
|
891
|
+
const Kinetic = (props) => {
|
|
892
|
+
const [childHolder] = splitProps(props, ['children'])
|
|
893
|
+
const children = childHolder.children
|
|
894
|
+
return <Inner>{children as VNode[]}</Inner>
|
|
895
|
+
}
|
|
896
|
+
`))
|
|
897
|
+
|
|
898
|
+
test('non-null `!` postfix is transparent — both backends', () =>
|
|
899
|
+
compare(`
|
|
900
|
+
const Comp = (props) => {
|
|
901
|
+
const [own] = splitProps(props, ['children'])
|
|
902
|
+
return <Inner>{own.children!}</Inner>
|
|
903
|
+
}
|
|
904
|
+
`))
|
|
905
|
+
|
|
906
|
+
test('fragment child of component does NOT propagate component context', () =>
|
|
907
|
+
compare(`
|
|
908
|
+
const Comp = (props) => {
|
|
909
|
+
const [own] = splitProps(props, ['children'])
|
|
910
|
+
const children = own.children
|
|
911
|
+
return <Inner><>{children}</></Inner>
|
|
912
|
+
}
|
|
913
|
+
`))
|
|
914
|
+
|
|
915
|
+
test('static-array children (rest-args form, no expression container) — unchanged', () =>
|
|
916
|
+
compare(`
|
|
917
|
+
const Comp = (props) => (
|
|
918
|
+
<Inner>
|
|
919
|
+
<A />
|
|
920
|
+
<B />
|
|
921
|
+
</Inner>
|
|
922
|
+
)
|
|
923
|
+
`))
|
|
924
|
+
})
|