@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.
Files changed (64) hide show
  1. package/package.json +11 -13
  2. package/src/defer-inline.ts +0 -686
  3. package/src/event-names.ts +0 -65
  4. package/src/index.ts +0 -61
  5. package/src/island-audit.ts +0 -675
  6. package/src/jsx.ts +0 -2792
  7. package/src/load-native.ts +0 -156
  8. package/src/lpih.ts +0 -270
  9. package/src/manifest.ts +0 -280
  10. package/src/project-scanner.ts +0 -214
  11. package/src/pyreon-intercept.ts +0 -1029
  12. package/src/react-intercept.ts +0 -1217
  13. package/src/reactivity-lens.ts +0 -190
  14. package/src/ssg-audit.ts +0 -513
  15. package/src/test-audit.ts +0 -435
  16. package/src/tests/backend-parity-r7-r9.test.ts +0 -91
  17. package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
  18. package/src/tests/collapse-bail-census.test.ts +0 -330
  19. package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
  20. package/src/tests/component-child-no-wrap.test.ts +0 -204
  21. package/src/tests/defer-inline.test.ts +0 -387
  22. package/src/tests/depth-stress.test.ts +0 -16
  23. package/src/tests/detector-tag-consistency.test.ts +0 -101
  24. package/src/tests/dynamic-collapse-detector.test.ts +0 -164
  25. package/src/tests/dynamic-collapse-emit.test.ts +0 -192
  26. package/src/tests/dynamic-collapse-scan.test.ts +0 -111
  27. package/src/tests/element-valued-const-child.test.ts +0 -61
  28. package/src/tests/falsy-child-characterization.test.ts +0 -48
  29. package/src/tests/island-audit.test.ts +0 -524
  30. package/src/tests/jsx.test.ts +0 -2908
  31. package/src/tests/load-native.test.ts +0 -53
  32. package/src/tests/lpih.test.ts +0 -404
  33. package/src/tests/malformed-input-resilience.test.ts +0 -50
  34. package/src/tests/manifest-snapshot.test.ts +0 -55
  35. package/src/tests/native-equivalence.test.ts +0 -924
  36. package/src/tests/partial-collapse-detector.test.ts +0 -121
  37. package/src/tests/partial-collapse-emit.test.ts +0 -104
  38. package/src/tests/partial-collapse-robustness.test.ts +0 -53
  39. package/src/tests/project-scanner.test.ts +0 -269
  40. package/src/tests/prop-derived-shadow.test.ts +0 -96
  41. package/src/tests/pure-call-reactive-args.test.ts +0 -50
  42. package/src/tests/pyreon-intercept.test.ts +0 -816
  43. package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
  44. package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
  45. package/src/tests/r15-elemconst-propderived.test.ts +0 -47
  46. package/src/tests/r19-defer-inline-robust.test.ts +0 -54
  47. package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
  48. package/src/tests/react-intercept.test.ts +0 -1104
  49. package/src/tests/reactivity-lens.test.ts +0 -170
  50. package/src/tests/rocketstyle-collapse.test.ts +0 -208
  51. package/src/tests/runtime/control-flow.test.ts +0 -159
  52. package/src/tests/runtime/dom-properties.test.ts +0 -138
  53. package/src/tests/runtime/events.test.ts +0 -301
  54. package/src/tests/runtime/harness.ts +0 -94
  55. package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
  56. package/src/tests/runtime/reactive-props.test.ts +0 -81
  57. package/src/tests/runtime/signals.test.ts +0 -129
  58. package/src/tests/runtime/whitespace.test.ts +0 -106
  59. package/src/tests/signal-autocall-shadow.test.ts +0 -86
  60. package/src/tests/sourcemap-fidelity.test.ts +0 -77
  61. package/src/tests/ssg-audit.test.ts +0 -402
  62. package/src/tests/static-text-baking.test.ts +0 -64
  63. package/src/tests/test-audit.test.ts +0 -549
  64. package/src/tests/transform-state-isolation.test.ts +0 -49
@@ -1,924 +0,0 @@
1
- /**
2
- * Cross-backend equivalence tests.
3
- * Runs every test input through BOTH the JS and Rust implementations
4
- * and asserts identical output. This catches any behavioral divergence
5
- * between the two backends.
6
- */
7
- import { transformJSX_JS } from '../jsx'
8
- import type { ReactivitySpan } from '../jsx'
9
-
10
- // Load native if available
11
- let nativeTransform:
12
- | ((
13
- code: string,
14
- filename: string,
15
- ssr: boolean,
16
- knownSignals: string[] | null,
17
- reactivityLens?: boolean,
18
- ) => {
19
- code: string
20
- usesTemplates?: boolean | null
21
- warnings: Array<{ message: string; line: number; column: number; code: string }>
22
- reactivityLens?: ReactivitySpan[] | null
23
- })
24
- | null = null
25
-
26
- try {
27
- const path = require('node:path')
28
- const native = require(path.join(__dirname, '..', '..', 'native', 'pyreon-compiler.node'))
29
- nativeTransform = native.transformJsx
30
- } catch {
31
- // Native not available — skip tests
32
- }
33
-
34
- const describeNative = nativeTransform ? describe : describe.skip
35
-
36
- function compare(input: string, filename = 'test.tsx') {
37
- const js = transformJSX_JS(input, filename)
38
- const rs = nativeTransform!(input, filename, false, null)
39
- expect(rs.code).toBe(js.code)
40
- }
41
-
42
- function compareWithSignals(input: string, knownSignals: string[]) {
43
- const js = transformJSX_JS(input, 'test.tsx', { knownSignals })
44
- const rs = nativeTransform!(input, 'test.tsx', false, knownSignals)
45
- expect(rs.code).toBe(js.code)
46
- }
47
-
48
- function compareSsr(input: string) {
49
- const js = transformJSX_JS(input, 'test.tsx', { ssr: true })
50
- const rs = nativeTransform!(input, 'test.tsx', true, null)
51
- expect(rs.code).toBe(js.code)
52
- }
53
-
54
- // Reactivity-lens cross-backend gate (Phase 3). Asserts the Rust binary
55
- // emits the SAME sidecar the JS oracle does. Two contracts:
56
- // 1. ADDITIVE — `code` is byte-identical with the lens collected vs
57
- // not, on BOTH backends (the option never affects codegen).
58
- // 2. PARITY — the SET of recorded spans is identical JS↔Rust.
59
- // Spans are compared order-independently: the LSP consumer sorts before
60
- // rendering, so traversal order is NOT part of the contract — the SET of
61
- // codegen decisions is. Sorting by (start,end,kind,detail) makes a
62
- // missing/extra/wrong span fail loudly while ignoring walk order.
63
- function canon(spans: ReactivitySpan[] | null | undefined): string[] {
64
- return (spans ?? [])
65
- .map(
66
- (s) =>
67
- `${s.start}|${s.end}|${s.line}|${s.column}|${s.endLine}|${s.endColumn}|${s.kind}|${s.detail}`,
68
- )
69
- .sort()
70
- }
71
-
72
- function compareLens(input: string, filename = 'test.tsx') {
73
- const jsOff = transformJSX_JS(input, filename)
74
- const jsOn = transformJSX_JS(input, filename, { reactivityLens: true })
75
- const rsOff = nativeTransform!(input, filename, false, null, false)
76
- const rsOn = nativeTransform!(input, filename, false, null, true)
77
-
78
- // (1) additive — collecting the lens never changes emitted code, on
79
- // either backend, and both backends still agree on that code.
80
- expect(jsOn.code).toBe(jsOff.code)
81
- expect(rsOn.code).toBe(rsOff.code)
82
- expect(rsOn.code).toBe(jsOn.code)
83
-
84
- // The opt-out path must NOT carry the sidecar (parity with JS, which
85
- // omits the field entirely when not collecting).
86
- expect(rsOff.reactivityLens == null).toBe(true)
87
- expect(jsOff.reactivityLens == null).toBe(true)
88
-
89
- // (2) parity — identical SET of spans.
90
- const j = canon(jsOn.reactivityLens)
91
- const r = canon(rsOn.reactivityLens)
92
- expect(r).toEqual(j)
93
- // Guard against the degenerate "both empty → trivially equal" pass:
94
- // every fixture below is chosen to produce ≥1 span.
95
- expect(j.length).toBeGreaterThan(0)
96
- }
97
-
98
- // ─── Cross-backend equivalence ──────────────────────────────────────────────
99
-
100
- describeNative('Native vs JS equivalence — basic', () => {
101
- test('simple signal child', () => compare('<div>{count()}</div>'))
102
- test('static string child', () => compare('<div>{"static"}</div>'))
103
- test('numeric child', () => compare('<div>{42}</div>'))
104
- test('null child', () => compare('<div>{null}</div>'))
105
- test('boolean true child', () => compare('<div>{true}</div>'))
106
- test('boolean false child', () => compare('<div>{false}</div>'))
107
- test('undefined child', () => compare('<div>{undefined}</div>'))
108
- test('arrow function child', () => compare('<div>{() => count()}</div>'))
109
- test('ternary with call', () => compare('<div>{a() ? b : c}</div>'))
110
- test('ternary without call', () => compare('<div>{a ? b : c}</div>'))
111
- test('logical AND with JSX', () => compare('<div>{show() && <span />}</div>'))
112
- test('template literal with call', () => compare('<div>{`hello ${name()}`}</div>'))
113
- test('template literal static', () => compare('<div>{`hello`}</div>'))
114
- test('tagged template', () => compare('<div>{css`color: red`}</div>'))
115
- test('binary with call', () => compare('<div>{count() + 1}</div>'))
116
- test('binary without call', () => compare('<div>{a + b}</div>'))
117
- test('member call', () => compare('<div>{obj.getValue()}</div>'))
118
- test('chained method', () => compare('<div>{items().map(x => x)}</div>'))
119
- test('empty expression', () => compare('<div>{/* comment */}</div>'))
120
- test('plain text', () => compare('<div>hello world</div>'))
121
- test('no JSX', () => compare('const x = 1 + 2'))
122
- test('empty element', () => compare('<div></div>'))
123
- test('self-closing', () => compare('<br />'))
124
- })
125
-
126
- describeNative('Native vs JS equivalence — props', () => {
127
- test('dynamic class', () => compare('<div class={activeClass()} />'))
128
- test('dynamic style', () => compare('<div style={styles()} />'))
129
- test('string class', () => compare('<div class="foo" />'))
130
- test('expression string class', () => compare('<div class={"foo"} />'))
131
- test('onClick handler', () => compare('<button onClick={handleClick} />'))
132
- test('onInput handler', () => compare('<input onInput={handler} />'))
133
- test('key prop', () => compare('<div key={id} />'))
134
- test('ref prop', () => compare('<div ref={myRef} />'))
135
- test('already wrapped', () => compare('<div class={() => cls()} />'))
136
- test('object style', () => compare('<div style={{ color: "red" }} />'))
137
- test('object style with call', () => compare('<div style={{ color: theme() }} />'))
138
- test('boolean shorthand', () => compare('<input disabled />'))
139
- test('data attribute', () => compare('<div data-id={getId()} />'))
140
- test('conditional prop', () => compare("<div title={isActive() ? 'yes' : 'no'} />"))
141
- })
142
-
143
- describeNative('Native vs JS equivalence — components', () => {
144
- test('reactive prop with _rp', () => compare('<MyComponent value={count()} />'))
145
- test('_rp import', () => compare('<Button label={getText()} />'))
146
- test('static prop', () => compare('<Button size={12} />'))
147
- test('event handler on component', () => compare('<Button onClick={handleClick} />'))
148
- test('arrow prop', () => compare('<Button render={() => "hello"} />'))
149
- test('children expression', () => compare('<MyComponent>{count()}</MyComponent>'))
150
- test('multiple reactive props', () => compare('<Comp a={x()} b={y()} c={12} />'))
151
- test('children prop', () => compare('<Comp children={items()} />'))
152
- test('spread on component', () => compare('<Comp {...getProps()} label="hi" />'))
153
- test('ternary in component prop', () => compare("<Comp x={a() ? 'yes' : 'no'} />"))
154
- test('template literal in component prop', () => compare('<Comp label={`${count()} items`} />'))
155
- })
156
-
157
- describeNative('Native vs JS equivalence — template emission', () => {
158
- test('nested elements', () => compare('<div><span>hello</span></div>'))
159
- test('single element with text', () => compare('<div>hello</div>'))
160
- test('component child bails', () => compare('<div><MyComponent /></div>'))
161
- test('root spread with _applyProps', () => compare('<div {...props}><span /></div>'))
162
- test('inner spread bails', () => compare('<div><span {...innerProps} /></div>'))
163
- test('keyed element bails', () => compare('<div key={id}><span /></div>'))
164
- test('static class in HTML', () => compare('<div class="box"><span /></div>'))
165
- test('boolean attr in HTML', () => compare('<div><input disabled /></div>'))
166
- test('_bindDirect for signal class', () => compare('<div class={cls()}><span /></div>'))
167
- test('_bindText for signal child', () => compare('<div><span>{name()}</span></div>'))
168
- test('static expression text', () => compare('<div><span>{label}</span></div>'))
169
- test('delegated event', () => compare('<div><button onClick={handler}>click</button></div>'))
170
- test('element children indexing', () => compare('<div><span>{a()}</span><em>{b()}</em></div>'))
171
- test('deep nesting', () => compare('<table><tbody><tr><td>{text()}</td></tr></tbody></table>'))
172
- test('className to class mapping', () => compare('<div className="box"><span /></div>'))
173
- test('htmlFor mapping', () => compare('<div><label htmlFor="name">Name</label></div>'))
174
- test('fragment inlining', () => compare('<div><><span>text</span></></div>'))
175
- test('expression with JSX bails', () => compare('<div><span />{show() && <em />}</div>'))
176
- test('mixed text + element + expr', () => compare('<div>hello<span />{name()}</div>'))
177
- test('multiple expressions', () => compare('<div><span>{a()}{b()}</span></div>'))
178
- test('void element', () => compare('<div><br /><span>text</span></div>'))
179
- test('ref in template (object)', () => compare('<div ref={myRef}><span /></div>'))
180
- test('ref in template (arrow)', () => compare('<div ref={(el) => { myEl = el }}><span /></div>'))
181
-
182
- // Regression: a child element with a block-arrow ref AND adjacent
183
- // reactive props used to emit `const __e0 = __root.children[N]`
184
- // followed by `((el) => { ... })(__e0)` with NO `;` between, so JS's
185
- // ASI merged them into one expression `const __e0 = X((el) => ...)(__e0)`
186
- // (calling X as fn, self-referencing __e0). Both backends now append
187
- // `;` to every bind line. This test asserts both emit the SAME `;`-
188
- // terminated output and the chained-call shape never appears.
189
- test('block-arrow ref on child element with adjacent reactive prop', () => {
190
- const input = '<div><span ref={(el) => { x = el }} data-state={cls()} /></div>'
191
- compare(input)
192
- // Tighter assertion: neither backend may emit the silent-merge shape.
193
- const js = transformJSX_JS(input, 'test.tsx')
194
- expect(js.code).not.toMatch(/children\[0\]\(\(/)
195
- expect(js.code).toMatch(/const __e0 = __root\.children\[0\];/)
196
- const rs = nativeTransform!(input, 'test.tsx', false, null)
197
- expect(rs.code).not.toMatch(/children\[0\]\(\(/)
198
- expect(rs.code).toMatch(/const __e0 = __root\.children\[0\];/)
199
- })
200
- test('non-delegated event', () => compare('<div onMouseEnter={handler}><span /></div>'))
201
- test('style object in template', () => compare('<div style={{ overflow: "hidden" }}>text</div>'))
202
- test('style string in template', () => compare('<div style="color: red">text</div>'))
203
- test('reactive style in template', () => compare('<div style={() => getStyle()}>text</div>'))
204
- test('tabindex numeric attr', () => compare('<div tabindex={0}><span /></div>'))
205
- test('hidden=true', () => compare('<div hidden={true}><span /></div>'))
206
- test('hidden=false', () => compare('<div hidden={false}><span /></div>'))
207
- test('hidden=null', () => compare('<div hidden={null}><span /></div>'))
208
- test('hidden=undefined', () => compare('<div hidden={undefined}><span /></div>'))
209
- test('one-time set for variable', () => compare('<div title={someVar}><span /></div>'))
210
- test('benchmark-like row', () => compare('<tr class={cls()}><td class="id">{String(row.id)}</td><td>{row.label()}</td></tr>'))
211
- })
212
-
213
- describeNative('Native vs JS equivalence — hoisting', () => {
214
- test('static JSX child', () => compare('<div>{<span>Hello</span>}</div>'))
215
- test('static self-closing', () => compare('<div>{<br />}</div>'))
216
- test('dynamic JSX not hoisted', () => compare('<div>{<span class={cls()}>text</span>}</div>'))
217
- test('static with string prop', () => compare('<div>{<span class="foo">text</span>}</div>'))
218
- test('multiple hoists', () => compare('<div>{<span>A</span>}{<span>B</span>}</div>'))
219
- test('static fragment', () => compare('<div>{<>text</>}</div>'))
220
- test('dynamic fragment not hoisted', () => compare('<div>{<>{count()}</>}</div>'))
221
- test('spread prevents hoisting', () => compare('<div>{<span {...props}>text</span>}</div>'))
222
- test('static boolean attr', () => compare('<div>{<input disabled />}</div>'))
223
- test('static true expression', () => compare('<div>{<input disabled={true} />}</div>'))
224
- test('static false expression', () => compare('<div>{<input disabled={false} />}</div>'))
225
- test('static null expression', () => compare('<div>{<input disabled={null} />}</div>'))
226
- test('static numeric expression', () => compare('<div>{<input tabindex={0} />}</div>'))
227
- test('empty expression attr', () => compare('<div>{<input disabled={/* comment */} />}</div>'))
228
- test('nested static element', () => compare('<div>{<div><span>text</span></div>}</div>'))
229
- test('nested static self-closing', () => compare('<div>{<div><br /></div>}</div>'))
230
- test('nested static fragment', () => compare('<div>{<div><>text</></div>}</div>'))
231
- })
232
-
233
- describeNative('Native vs JS equivalence — pure calls', () => {
234
- test('Math.max static', () => compare('<div>{Math.max(5, 10)}</div>'))
235
- test('JSON.stringify static', () => compare('<div>{JSON.stringify("hello")}</div>'))
236
- test('JSON.stringify non-static', () => compare('<div>{JSON.stringify({a: 1})}</div>'))
237
- test('Math.max with signal', () => compare('<div>{Math.max(count(), 10)}</div>'))
238
- test('unknown function', () => compare('<div>{unknownFn(5)}</div>'))
239
- test('Math.floor static', () => compare('<div>{Math.floor(3.14)}</div>'))
240
- test('Number.parseInt', () => compare('<div>{Number.parseInt("42", 10)}</div>'))
241
- })
242
-
243
- describeNative('Native vs JS equivalence — props detection', () => {
244
- test('props.x in child', () => compare('function Comp(props) { return <div>{props.name}</div> }'))
245
- test('props.x in attr', () => compare('function Comp(props) { return <div class={props.cls}></div> }'))
246
- test('prop-derived inline', () => compare('function Comp(props) { const x = props.name ?? "anon"; return <div>{x}</div> }'))
247
- test('prop-derived in attr', () => compare('function Comp(props) { const align = props.alignX ?? "left"; return <div class={align}></div> }'))
248
- test('splitProps tracking', () => compare('function Comp(props) { const [own, rest] = splitProps(props, ["x"]); const v = own.x ?? 5; return <div>{v}</div> }'))
249
- test('non-component not tracked', () => compare('function helper(props) { const x = props.y; return x }'))
250
- test('signal alongside props', () => compare('function Comp(props) { return <div>{count()}</div> }'))
251
- test('arrow component', () => compare('const Comp = (props) => <div>{props.x}</div>'))
252
- test('prop used multiple times', () => compare('function Comp(props) { const x = props.a ?? "def"; return <div class={x}>{x}</div> }'))
253
- })
254
-
255
- describeNative('Native vs JS equivalence — transitive derivation', () => {
256
- test('simple chain', () => compare('function Comp(props) { const a = props.x; const b = a + 1; return <div>{b}</div> }'))
257
- test('non-prop const', () => compare('function Comp(props) { const x = 42; return <div>{x}</div> }'))
258
- test('let not tracked', () => compare('function Comp(props) { let x = props.y; x = "override"; return <div>{x}</div> }'))
259
- test('deep chain', () => compare('function Comp(props) { const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }'))
260
- test('mixed props and signals', () => compare('function Comp(props) { return <div class={`${props.base} ${count()}`}></div> }'))
261
- })
262
-
263
- // ─── Edge cases that previously broke ───────────────────────────────────────
264
-
265
- describeNative('Native vs JS equivalence — TypeScript syntax', () => {
266
- test('as expression in prop', () => compare('function C(props) { return <div>{(props.x as string)}</div> }'))
267
- test('as in variable init', () => compare(`
268
- function C(props) {
269
- const items = props.data as any[]
270
- return <div>{items}</div>
271
- }
272
- `))
273
- test('non-null assertion', () => compare('function C(props) { return <div>{props.name!}</div> }'))
274
- test('satisfies', () => compare('function C(props) { return <div>{(props.x satisfies string)}</div> }'))
275
- test('type annotation on arrow', () => compare('const C = (props: { x: string }) => <div>{props.x}</div>'))
276
- test('generic component', () => compare('function List<T>(props: { items: T[] }) { return <div>{props.items}</div> }'))
277
- })
278
-
279
- describeNative('Native vs JS equivalence — export forms', () => {
280
- test('export default function', () => compare('export default function App(props) { return <div>{props.name}</div> }'))
281
- test('export const arrow', () => compare('export const App = (props) => <div>{props.name}</div>'))
282
- test('export named function', () => compare('export function App(props) { return <div>{props.name}</div> }'))
283
- test('export const with signal', () => compare('export const view = <div>{count()}</div>'))
284
- })
285
-
286
- describeNative('Native vs JS equivalence — control flow', () => {
287
- test('if statement before JSX', () => compare(`
288
- function C(props) {
289
- if (!props.show) return null
290
- return <div>{props.name}</div>
291
- }
292
- `))
293
- test('for loop before JSX', () => compare(`
294
- function C(props) {
295
- for (let i = 0; i < 10; i++) {}
296
- return <div>{props.name}</div>
297
- }
298
- `))
299
- test('try/catch wrapping JSX', () => compare(`
300
- function C(props) {
301
- try { return <div>{props.name}</div> }
302
- catch(e) { return <div>error</div> }
303
- }
304
- `))
305
- test('switch statement', () => compare(`
306
- function C(props) {
307
- switch (props.mode) {
308
- case 'a': return <div>A</div>
309
- default: return <div>B</div>
310
- }
311
- }
312
- `))
313
- test('while loop', () => compare(`
314
- function C() {
315
- let items = []
316
- while (items.length < 10) { items.push(1) }
317
- return <div>{items.length}</div>
318
- }
319
- `))
320
- test('ternary return', () => compare(`
321
- function C(props) {
322
- return props.show ? <div>yes</div> : <div>no</div>
323
- }
324
- `))
325
- test('logical AND return', () => compare(`
326
- function C(props) {
327
- return props.show && <div>yes</div>
328
- }
329
- `))
330
- test('arrow with block body', () => compare(`
331
- const C = (props) => {
332
- const x = props.name
333
- return <div>{x}</div>
334
- }
335
- `))
336
- })
337
-
338
- describeNative('Native vs JS equivalence — callback depth', () => {
339
- test('.map callback not tracked', () => compare(`
340
- function App(props) {
341
- return <div>{tabs.map((tab) => {
342
- const C = tab.component
343
- return <div><C /></div>
344
- })}</div>
345
- }
346
- `))
347
- test('.filter callback not tracked', () => compare(`
348
- function App(props) {
349
- return <div>{items.filter(i => i.visible).map(i => <span>{i.name()}</span>)}</div>
350
- }
351
- `))
352
- test('nested callback', () => compare(`
353
- function App(props) {
354
- return <ul>{items.map(item => (
355
- <li class={item.done() ? 'done' : ''}>
356
- <span>{item.text()}</span>
357
- </li>
358
- ))}</ul>
359
- }
360
- `))
361
- })
362
-
363
- describeNative('Native vs JS equivalence — children slot', () => {
364
- test('props.children uses _mountSlot', () => compare('function C(props) { return <div>{props.children}</div> }'))
365
- test('own.children uses _mountSlot', () => compare('function C(props) { const own = props; return <label><input/>{own.children}</label> }'))
366
- test('non-children prop uses text bind', () => compare('function C(props) { return <div>{props.name}</div> }'))
367
- })
368
-
369
- describeNative('Native vs JS equivalence — SSR mode', () => {
370
- test('SSR skips _tpl', () => {
371
- const code = 'function Btn() { return <button onClick={() => null}>Click {() => x()}</button> }'
372
- compareSsr(code)
373
- })
374
- test('SSR simple element', () => compareSsr('<div>hello</div>'))
375
- })
376
-
377
- describeNative('Native vs JS equivalence — warnings', () => {
378
- test('<For> without by produces warning', () => {
379
- const js = transformJSX_JS('<For each={items}>{(item) => <li>{item}</li>}</For>')
380
- const rs = nativeTransform!('<For each={items}>{(item) => <li>{item}</li>}</For>', 'test.tsx', false, null)
381
- expect(rs.warnings.length).toBe(js.warnings.length)
382
- if (js.warnings.length > 0) {
383
- expect(rs.warnings[0]!.code).toBe(js.warnings[0]!.code)
384
- }
385
- })
386
- test('<For> with by has no warning', () => {
387
- const js = transformJSX_JS('<For each={items} by={(i) => i.id}>{(item) => <li>{item}</li>}</For>')
388
- const rs = nativeTransform!('<For each={items} by={(i) => i.id}>{(item) => <li>{item}</li>}</For>', 'test.tsx', false, null)
389
- expect(rs.warnings.length).toBe(js.warnings.length)
390
- })
391
- })
392
-
393
- describeNative('Native vs JS equivalence — HTML escaping', () => {
394
- test('HTML entities preserved', () => compare('function C() { return <button>&lt; prev</button> }'))
395
- test('mixed entities and ampersands', () => compare('function C() { return <span>A &amp; B &lt; C</span> }'))
396
- test('quotes in attributes', () => compare('<div title="say &quot;hi&quot;"><span /></div>'))
397
- })
398
-
399
- describeNative('Native vs JS equivalence — signal() not inlined', () => {
400
- test('signal() call not tracked as prop-derived', () => compare(`
401
- function C(props) {
402
- const open = signal(props.defaultOpen ?? false)
403
- return <div>{() => open() ? 'yes' : 'no'}</div>
404
- }
405
- `))
406
- })
407
-
408
- describeNative('Native vs JS equivalence — circular references', () => {
409
- test('two-variable cycle does not crash', () => compare(`
410
- function Comp(props) {
411
- const a = b + props.x;
412
- const b = a + 1;
413
- return <div>{a}</div>
414
- }
415
- `))
416
- test('self-referencing variable', () => compare(`
417
- function Comp(props) {
418
- const a = a + props.x;
419
- return <div>{a}</div>
420
- }
421
- `))
422
- test('non-cyclic deep chain', () => compare(`
423
- function Comp(props) {
424
- const a = props.x;
425
- const b = a + 1;
426
- const c = b * 2;
427
- return <div>{c}</div>
428
- }
429
- `))
430
- })
431
-
432
- describeNative('Native vs JS equivalence — complex real-world patterns', () => {
433
- test('todo app component', () => compare(`
434
- const TodoApp = (props) => {
435
- const [items, rest] = splitProps(props, ['items'])
436
- const count = items.items?.length ?? 0
437
- return (
438
- <div class="app">
439
- <h1>Todos</h1>
440
- <footer>{count} items left</footer>
441
- </div>
442
- )
443
- }
444
- `))
445
-
446
- test('form with multiple props', () => compare(`
447
- function FormField(props) {
448
- const label = props.label ?? 'Field'
449
- const required = props.required
450
- return (
451
- <div class={required ? 'required' : ''}>
452
- <label>{label}</label>
453
- <input value={props.value} onInput={props.onInput} />
454
- </div>
455
- )
456
- }
457
- `))
458
-
459
- test('list with For and template rows', () => compare(`
460
- function UserList(props) {
461
- return (
462
- <table>
463
- <thead><tr><th>Name</th><th>Email</th></tr></thead>
464
- <tbody>
465
- <For each={props.users} by={(u) => u.id}>
466
- {(user) => (
467
- <tr class={user.active() ? 'active' : ''}>
468
- <td>{user.name()}</td>
469
- <td>{user.email()}</td>
470
- </tr>
471
- )}
472
- </For>
473
- </tbody>
474
- </table>
475
- )
476
- }
477
- `))
478
-
479
- test('conditional rendering with Show', () => compare(`
480
- function Modal(props) {
481
- return (
482
- <Show when={props.open}>
483
- <div class="overlay" onClick={props.onClose}>
484
- <div class="modal">
485
- <h2>{props.title}</h2>
486
- {props.children}
487
- </div>
488
- </div>
489
- </Show>
490
- )
491
- }
492
- `))
493
- })
494
-
495
- // ─── Unicode and multi-byte character safety ────────────────────────────────
496
-
497
- describeNative('Native vs JS equivalence — Unicode', () => {
498
- test('emoji before JSX expression', () => compare(`
499
- function C() { return <div>🔥{count()}</div> }
500
- `))
501
- test('CJK characters before JSX', () => compare(`
502
- function C() { return <div>日本語{name()}</div> }
503
- `))
504
- test('accented chars in identifier', () => compare(`
505
- function C() { const café = "test"; return <div>{café}</div> }
506
- `))
507
- test('emoji in prop value', () => compare(`
508
- <div title="🎉 Party">text</div>
509
- `))
510
- test('multi-byte chars in template literal', () => compare(`
511
- <div>{\`Hello 世界 \${name()}\`}</div>
512
- `))
513
- test('unicode in component name', () => compare(`
514
- <Ñoño value={x()} />
515
- `))
516
- test('mixed unicode and ASCII', () => compare(`
517
- function Comp(props) {
518
- const naïve = props.naïve ?? "default"
519
- return <div class={naïve}>{props.résumé}</div>
520
- }
521
- `))
522
- })
523
-
524
- // ─── String literal collision resistance ────────────────────────────────────
525
-
526
- describeNative('Native vs JS equivalence — string literal collision', () => {
527
- test('prop name matches string in ternary', () => compare(`
528
- function C(props) {
529
- const required = props.required
530
- return <div class={required ? 'required' : ''}>{required}</div>
531
- }
532
- `))
533
- test('prop name in template literal string part', () => compare(`
534
- function C(props) {
535
- const mode = props.mode
536
- return <div>{\`mode is \${mode}\`}</div>
537
- }
538
- `))
539
- test('prop name in object key position', () => compare(`
540
- function C(props) {
541
- const x = props.x
542
- return <div style={{ x: x }}></div>
543
- }
544
- `))
545
- test('prop name in nested string', () => compare(`
546
- function C(props) {
547
- const label = props.label
548
- return <div title={label || "label"}>text</div>
549
- }
550
- `))
551
- })
552
-
553
- // ─── Additional robustness tests ────────────────────────────────────────────
554
-
555
- describeNative('Native vs JS equivalence — additional edge cases', () => {
556
- test('deeply nested template', () => compare(`
557
- <div><section><article><header><h1>{title()}</h1></header><p>{body()}</p></article></section></div>
558
- `))
559
- test('multiple components in one file', () => compare(`
560
- function A(props) { return <div>{props.a}</div> }
561
- function B(props) { return <span>{props.b}</span> }
562
- `))
563
- test('component returning fragment', () => compare(`
564
- function C(props) { return <>{props.children}</> }
565
- `))
566
- test('empty component', () => compare(`
567
- function C() { return <div></div> }
568
- `))
569
- test('template inside fragment wraps in braces', () => compare(`
570
- function C() { return <><button type="button">text</button><span /></> }
571
- `))
572
- test('template inside nested fragment', () => compare(`
573
- function C() { return <><><div>inner</div></></> }
574
- `))
575
- test('template in JSX attribute value (not brace-wrapped)', () => compare(`
576
- <Show fallback={<div><p>Not logged in</p></div>}><span /></Show>
577
- `))
578
- test('full Showcase pattern with Show + fallback', () => compare(`
579
- function Demo(props) {
580
- const open = signal(false)
581
- return (
582
- <Show
583
- when={() => open()}
584
- fallback={<div class="demo"><p>Fallback</p><button type="button">Action</button></div>}
585
- >
586
- <div><p>Content</p></div>
587
- </Show>
588
- )
589
- }
590
- `))
591
- test('array destructuring from signal', () => compare(`
592
- function C(props) {
593
- const [a, b] = props.items
594
- return <div>{a}</div>
595
- }
596
- `))
597
- test('nested function not confused with component', () => compare(`
598
- function App(props) {
599
- function helper() { return props.x + 1 }
600
- return <div>{helper()}</div>
601
- }
602
- `))
603
- test('class with JSX method', () => compare(`
604
- class C { render(props) { return <div>{props.name}</div> } }
605
- `))
606
- test('immediately invoked arrow', () => compare(`
607
- const el = (() => <div>{count()}</div>)()
608
- `))
609
- test('JSX in variable init', () => compare(`
610
- const header = <header><h1>Title</h1></header>
611
- `))
612
- test('multiple JSX returns', () => compare(`
613
- function C(props) {
614
- if (props.loading) return <div>Loading...</div>
615
- if (props.error) return <div>Error</div>
616
- return <div>{props.data}</div>
617
- }
618
- `))
619
- })
620
-
621
- // ─── Signal auto-call cross-backend equivalence ─────────────────────────────
622
-
623
- describeNative('Native vs JS equivalence — signal auto-call', () => {
624
- test('bare signal in text child', () => compare(
625
- 'function C() { const name = signal("Vít"); return <div>{name}</div> }',
626
- ))
627
- test('signal in attribute', () => compare(
628
- 'function C() { const show = signal(false); return <div class={show ? "active" : ""}></div> }',
629
- ))
630
- test('already called NOT double-called', () => compare(
631
- 'function C() { const count = signal(0); return <div>{count()}</div> }',
632
- ))
633
- test('computed auto-called', () => compare(
634
- 'function C() { const d = computed(() => 2); return <div>{d}</div> }',
635
- ))
636
- test('signal in ternary', () => compare(
637
- 'function C() { const show = signal(false); return <div>{show ? "yes" : "no"}</div> }',
638
- ))
639
- test('signal in template literal', () => compare(
640
- 'function C() { const name = signal("world"); return <div>{`hello ${name}`}</div> }',
641
- ))
642
- test('signal in component prop with _rp', () => compare(
643
- 'function C() { const val = signal(42); return <MyComp value={val} /> }',
644
- ))
645
- test('multiple signals', () => compare(
646
- 'function C() { const a = signal(1); const b = signal(2); return <div>{a + b}</div> }',
647
- ))
648
- test('signal + computed together', () => compare(
649
- 'function C() { const count = signal(0); const doubled = computed(() => count() * 2); return <div>{count} + {doubled}</div> }',
650
- ))
651
- test('non-signal const NOT auto-called', () => compare(
652
- 'function C() { const x = 42; return <div>{x}</div> }',
653
- ))
654
- test('shorthand property NOT auto-called', () => compare(
655
- 'function C() { const name = signal("x"); return <div>{t("hi", { name })}</div> }',
656
- ))
657
- test('non-shorthand property value auto-called', () => compare(
658
- 'function C() { const name = signal("x"); return <div>{t("hi", { label: name })}</div> }',
659
- ))
660
- test('signal in object property value', () => compare(
661
- 'function C() { const x = signal(0); return <div>{({val: x})}</div> }',
662
- ))
663
- test('signal as member expression object', () => compare(
664
- 'function C() { const x = signal(0); return <div>{x.toString()}</div> }',
665
- ))
666
- test('signal in computed property access', () => compare(
667
- 'function C() { const idx = signal(0); return <div>{arr[idx]}</div> }',
668
- ))
669
- test('shadowed by inner const', () => compare(`
670
- const show = signal(false)
671
- function Inner() {
672
- const show = 'not a signal'
673
- return <div>{show}</div>
674
- }
675
- `))
676
- test('shadowed by function parameter', () => compare(`
677
- const count = signal(0)
678
- function Display(count) {
679
- return <div>{count}</div>
680
- }
681
- `))
682
- test('shadowed by destructured parameter', () => compare(`
683
- const name = signal('Vít')
684
- function Greet({ name }) {
685
- return <div>{name}</div>
686
- }
687
- `))
688
- test('export default function with shadow', () => compare(`
689
- const show = signal(false)
690
- export default function App(show) {
691
- return <div>{show}</div>
692
- }
693
- `))
694
- test('export named function with shadow', () => compare(`
695
- const show = signal(false)
696
- export function App(show) {
697
- return <div>{show}</div>
698
- }
699
- `))
700
- test('module-scope signal auto-called', () => compare(
701
- 'const globalSig = signal(0); function C() { return <div>{globalSig}</div> }',
702
- ))
703
- test('props + signal in same expression', () => compare(
704
- 'function C(props) { const show = signal(false); const label = props.label; return <div class={show ? label : "default"}></div> }',
705
- ))
706
- })
707
-
708
- describeNative('Native vs JS equivalence — knownSignals cross-module', () => {
709
- test('imported signal auto-called', () => compareWithSignals(
710
- 'import { count } from "./store"; function App() { return <div>{count}</div> }',
711
- ['count'],
712
- ))
713
- test('imported signal with alias', () => compareWithSignals(
714
- 'import { count as c } from "./store"; function App() { return <div>{c}</div> }',
715
- ['c'],
716
- ))
717
- test('imported signal not double-called', () => compareWithSignals(
718
- 'import { count } from "./store"; function App() { return <div>{count()}</div> }',
719
- ['count'],
720
- ))
721
- test('imported signal respects shadow', () => compareWithSignals(
722
- 'import { count } from "./store"; function App() { const count = "shadow"; return <div>{count}</div> }',
723
- ['count'],
724
- ))
725
- test('knownSignals combined with local signals', () => compareWithSignals(
726
- 'import { theme } from "./store"; function App() { const count = signal(0); return <div class={theme}>{count}</div> }',
727
- ['theme'],
728
- ))
729
- })
730
-
731
- // PR #352 added a `DOM_PROPS` set so `<input value={x()} />` inside a
732
- // template-emitting context compiles to `el.value = x()` (property
733
- // assignment) instead of `el.setAttribute("value", x())` (content
734
- // attribute). The two diverge for IDL properties whose live state
735
- // differs from the content attribute (`value`, `checked`, etc.). The
736
- // Rust native backend reimplements this list separately. A typo in
737
- // either side's list would silently produce wrong output for one
738
- // DOM_PROP without breaking any other test. This block enumerates
739
- // every DOM_PROP under template context (the only context where
740
- // DOM_PROPS actually fires — root-level standalone JSX uses the
741
- // `h()` path, not `_tpl() + _bind()`) and asserts JS↔Rust agreement,
742
- // so a drift between the two lists fails one specific test.
743
- //
744
- // Reference: packages/core/compiler/src/jsx.ts:1389 — DOM_PROPS Set.
745
- describeNative('Native vs JS equivalence — DOM properties', () => {
746
- const DOM_PROPS = [
747
- 'value',
748
- 'checked',
749
- 'selected',
750
- 'disabled',
751
- 'multiple',
752
- 'readOnly',
753
- 'indeterminate',
754
- ] as const
755
-
756
- for (const prop of DOM_PROPS) {
757
- test(`DOM_PROP in template: <div><input ${prop}={x()} /></div> (reactive)`, () => {
758
- compare(`<div><input ${prop}={x()} /></div>`)
759
- })
760
-
761
- test(`DOM_PROP in template: <div><input ${prop}={() => x()} /></div> (accessor)`, () => {
762
- compare(`<div><input ${prop}={() => x()} /></div>`)
763
- })
764
-
765
- test(`DOM_PROP in template: <div><input ${prop}={true} /></div> (literal)`, () => {
766
- compare(`<div><input ${prop}={true} /></div>`)
767
- })
768
- }
769
-
770
- test('regression: all DOM_PROPS together in one template', () => {
771
- // Sentinel — if a future PR adds a new DOM property to either
772
- // backend without adding it to the other, the loop above won't
773
- // notice unless that prop is in the test list. This single test
774
- // compiles JSX with ALL known DOM_PROPS together and verifies
775
- // both backends agree on the combined output.
776
- const allProps = DOM_PROPS.map((p) => `${p}={x()}`).join(' ')
777
- compare(`<div><input ${allProps} /></div>`)
778
- })
779
-
780
- test('non-DOM-prop control: title in template uses setAttribute, not assignment', () => {
781
- // Negative control — `title` is NOT a DOM_PROP, so it should
782
- // compile through setAttribute. If this test starts failing,
783
- // someone added `title` to DOM_PROPS — verify intent before
784
- // updating.
785
- compare('<div><input title={x()} /></div>')
786
- })
787
- })
788
-
789
- // ─── Reactivity-lens parity (Phase 3) ───────────────────────────────────────
790
- // The Rust binary must emit the SAME sidecar as the JS oracle so the
791
- // ~80% of users on the native path get the Lens too. Each fixture is
792
- // chosen to exercise one of the five structural kinds; `compareLens`
793
- // also asserts the additive guarantee (codegen byte-identical with the
794
- // option on vs off, both backends) so this block doubles as the native
795
- // regression guard for "the lens option must never affect output".
796
- //
797
- // Bisect-verified: removing ANY of the 6 `ctx.lens(...)` calls in
798
- // native/src/lib.rs fails the matching fixture below with an
799
- // array-length / element mismatch in `compareLens`'s parity assertion
800
- // (e.g. dropping the `reactive-prop` call → `<Comp value={x()} />` fails
801
- // `expect(r).toEqual(j)` because the Rust set is missing that span);
802
- // restored → 9/9 pass.
803
- describeNative('Reactivity-lens — JS↔Rust span parity', () => {
804
- test('reactive text child (_bindText)', () =>
805
- compareLens('<div>{count()}</div>'))
806
-
807
- test('reactive accessor text child (() => …)', () =>
808
- compareLens('<div>{() => count()}</div>'))
809
-
810
- test('static-text child (baked once — the high-precision negative)', () =>
811
- compareLens('<div>{someConst}</div>'))
812
-
813
- test('reactive-prop on a component (_rp(() => …))', () =>
814
- compareLens('<Comp value={count()} />'))
815
-
816
- test('reactive-attr on a DOM element (live binding)', () =>
817
- compareLens('<div><span title={count()}>hi</span></div>'))
818
-
819
- test('hoisted-static (module-scope hoist)', () =>
820
- compareLens('<Comp>{<b class="x">hi</b>}</Comp>'))
821
-
822
- test('mixed: reactive + static + prop in one tree', () =>
823
- compareLens(
824
- '<section><Comp value={count()} /><p>{count()}</p><p>{label}</p></section>',
825
- ))
826
-
827
- test('multi-line source — line/column parity across newlines', () =>
828
- compareLens('<div>\n {count()}\n <span title={other()}>\n z</span>\n</div>'))
829
-
830
- test('signal auto-call shape — declared signal, bare {count} → reactive', () =>
831
- compareLens('const count = signal(0); const App = () => <div>{count}</div>', 'auto.tsx'))
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
- })