@barefootjs/jsx 0.26.3 → 0.27.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/dist/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +735 -140
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +12 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +27 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +13 -2
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -3
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts +8 -3
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +73 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +35 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +555 -678
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +26 -12
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/composite-branch-loop.test.ts +2 -2
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- package/src/__tests__/flatmap-segments.test.ts +182 -0
- package/src/__tests__/inline-jsx-callback.test.ts +6 -4
- package/src/__tests__/ir-jsx-props.test.ts +6 -5
- package/src/__tests__/loop-fallback-wrap.test.ts +3 -4
- package/src/__tests__/map-body-no-silent-divergence.test.ts +45 -0
- package/src/__tests__/module-level-signal.audit.test.ts +7 -2
- package/src/__tests__/nested-loop-conditional.test.ts +9 -7
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
- package/src/__tests__/preamble-region-patch.test.ts +156 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
- package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
- package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
- package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/build-references.ts +16 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +74 -14
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +12 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +11 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +36 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/shared.ts +27 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +67 -19
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +35 -16
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +80 -14
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +1 -1
- package/src/ir-to-client-js/element-refs.ts +9 -11
- package/src/ir-to-client-js/emit-reactive.ts +50 -14
- package/src/ir-to-client-js/html-template.ts +144 -15
- package/src/ir-to-client-js/imports.ts +7 -4
- package/src/ir-to-client-js/reactivity.ts +7 -0
- package/src/ir-to-client-js/types.ts +34 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +382 -15
- package/src/types.ts +72 -0
|
@@ -211,7 +211,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
211
211
|
expect(clientJs!.content).toContain('toggleRow(index)')
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
-
test('dynamic signal array: component generates
|
|
214
|
+
test('dynamic signal array: component generates mapArray with createComponent', () => {
|
|
215
215
|
const source = `
|
|
216
216
|
'use client'
|
|
217
217
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -321,7 +321,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
321
321
|
const uniqueVars = new Set(allVars)
|
|
322
322
|
expect(allVars.length).toBe(uniqueVars.size)
|
|
323
323
|
|
|
324
|
-
// Component slot ref ($c) and
|
|
324
|
+
// Component slot ref ($c) and mapArray should both be present
|
|
325
325
|
expect(content).toContain('$c(__scope')
|
|
326
326
|
expect(content).toContain('mapArray')
|
|
327
327
|
})
|
|
@@ -355,7 +355,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
355
355
|
expect(clientJs).toBeDefined()
|
|
356
356
|
const content = clientJs!.content
|
|
357
357
|
|
|
358
|
-
// Should use
|
|
358
|
+
// Should use mapArray with createComponent
|
|
359
359
|
expect(content).toContain('mapArray')
|
|
360
360
|
expect(content).toContain("createComponent('TableRow'")
|
|
361
361
|
|
|
@@ -650,7 +650,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
650
650
|
expect(clientJs).toBeDefined()
|
|
651
651
|
const content = clientJs!.content
|
|
652
652
|
|
|
653
|
-
// Dynamic array should use
|
|
653
|
+
// Dynamic array should use mapArray and event delegation
|
|
654
654
|
expect(content).toContain('mapArray')
|
|
655
655
|
expect(content).toContain(".addEventListener('click', (__bfEvt) => {")
|
|
656
656
|
expect(content).toContain('handleClick(item.id)')
|
|
@@ -151,7 +151,7 @@ describe('Client JS generation', () => {
|
|
|
151
151
|
})
|
|
152
152
|
|
|
153
153
|
describe('map with index parameter', () => {
|
|
154
|
-
test('includes index parameter in
|
|
154
|
+
test('includes index parameter in mapArray renderItem callback', () => {
|
|
155
155
|
const source = `
|
|
156
156
|
'use client'
|
|
157
157
|
import { createMemo } from '@barefootjs/client'
|
|
@@ -1753,8 +1753,17 @@ describe('Client JS generation', () => {
|
|
|
1753
1753
|
|
|
1754
1754
|
// Should contain the preamble in the map callback
|
|
1755
1755
|
expect(clientJs?.content).toContain('const label = item.name.toUpperCase()')
|
|
1756
|
-
//
|
|
1757
|
-
|
|
1756
|
+
// The sole child expression reads the preamble local `label` (#2389
|
|
1757
|
+
// patch-on-update): it is classified as a preamble-patched region —
|
|
1758
|
+
// slot-marked and escaped, like an ordinary text slot — rather than a
|
|
1759
|
+
// bare `${label}` interpolation that would freeze on a same-key
|
|
1760
|
+
// update. See `preamble-region-patch.test.ts` for the full region
|
|
1761
|
+
// contract (row-template marker, region-patch effect, SSR parity).
|
|
1762
|
+
expect(clientJs?.content).toContain('${escapeText(label)}')
|
|
1763
|
+
// Slot unification A3: preamble regions now write through a claimed
|
|
1764
|
+
// 'markup' slot instead of `patchSlotRange`.
|
|
1765
|
+
expect(clientJs?.content).toContain("kind: 'markup'")
|
|
1766
|
+
expect(clientJs?.content).toContain('lazySlots(__el')
|
|
1758
1767
|
})
|
|
1759
1768
|
|
|
1760
1769
|
test('handles block body with multiple variable declarations', () => {
|
|
@@ -2417,11 +2426,12 @@ describe('Client JS generation', () => {
|
|
|
2417
2426
|
expect(clientJs).toBeDefined()
|
|
2418
2427
|
const content = clientJs!.content
|
|
2419
2428
|
|
|
2420
|
-
// Text node updates route through
|
|
2421
|
-
//
|
|
2422
|
-
//
|
|
2423
|
-
//
|
|
2424
|
-
|
|
2429
|
+
// Text node updates route through a claimed 'markup' slot writer (slot
|
|
2430
|
+
// unification A3), whose `writeMarkup` renders nullish values as ''
|
|
2431
|
+
// (not the string "undefined") and also splices live Nodes by
|
|
2432
|
+
// identity (#1663) — the same contract `__bfText` used to provide.
|
|
2433
|
+
// The nullish guard lives in the runtime writer, not inline here.
|
|
2434
|
+
expect(content).toContain("kind: 'markup'")
|
|
2425
2435
|
expect(content).not.toMatch(/\.nodeValue = String\(__val\)(?! )/)
|
|
2426
2436
|
})
|
|
2427
2437
|
|
|
@@ -2458,8 +2468,10 @@ describe('Client JS generation', () => {
|
|
|
2458
2468
|
expect(clientJs).toBeDefined()
|
|
2459
2469
|
const content = clientJs!.content
|
|
2460
2470
|
|
|
2461
|
-
// text() inside a component inside a conditional
|
|
2462
|
-
|
|
2471
|
+
// text() inside a component inside a conditional writes through a
|
|
2472
|
+
// claimed 'markup' slot built fresh in the branch's own bindEvents
|
|
2473
|
+
// (slot unification A3) — re-claimed on every branch activation.
|
|
2474
|
+
expect(content).toContain("lazySlots(__branchScope, [{ id: '^s2', kind: 'markup', path: [] }])")
|
|
2463
2475
|
})
|
|
2464
2476
|
|
|
2465
2477
|
test('propagates insideConditional through fragment children', () => {
|
|
@@ -2489,8 +2501,10 @@ describe('Client JS generation', () => {
|
|
|
2489
2501
|
expect(clientJs).toBeDefined()
|
|
2490
2502
|
const content = clientJs!.content
|
|
2491
2503
|
|
|
2492
|
-
// text() inside a fragment inside a conditional
|
|
2493
|
-
|
|
2504
|
+
// text() inside a fragment inside a conditional writes through a
|
|
2505
|
+
// claimed 'markup' slot built fresh in the branch's own bindEvents
|
|
2506
|
+
// (slot unification A3) — re-claimed on every branch activation.
|
|
2507
|
+
expect(content).toContain("lazySlots(__branchScope, [{ id: 's2', kind: 'markup', path: [] }])")
|
|
2494
2508
|
})
|
|
2495
2509
|
})
|
|
2496
2510
|
|
|
@@ -154,11 +154,11 @@ describe('Compiler-Runtime Contract', () => {
|
|
|
154
154
|
expect(js).not.toContain('data-key-2')
|
|
155
155
|
// Outer loop items must have data-key in the SSR branch template.
|
|
156
156
|
// Post-O-7 the emission is an unguarded template-literal interpolation
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
expect(js).toMatch(/data-key="\$\{group\.id\}"/)
|
|
157
|
+
// — escapeAttr-wrapped like every dynamic attribute, so a `"` in a key
|
|
158
|
+
// can't corrupt the client-assembled HTML or diverge from SSR bytes.
|
|
159
|
+
expect(js).toMatch(/data-key="\$\{escapeAttr\(group\.id\)\}"/)
|
|
160
160
|
// Inner loop items must have data-key-1 in the SSR branch template.
|
|
161
|
-
expect(js).toMatch(/data-key-1="\$\{item\.id\}"/)
|
|
161
|
+
expect(js).toMatch(/data-key-1="\$\{escapeAttr\(item\.id\)\}"/)
|
|
162
162
|
})
|
|
163
163
|
|
|
164
164
|
test('loop inside conditional wires reactive conditionals in mapArray callback', () => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* BarefootJS Compiler - Composite loops inside conditional branches (#724)
|
|
3
3
|
*
|
|
4
4
|
* Verifies that loops with child components inside ternary conditionals
|
|
5
|
-
* generate
|
|
5
|
+
* generate mapArray with createComponent in the branch's bindEvents.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, test, expect } from 'bun:test'
|
|
@@ -52,7 +52,7 @@ describe('composite loops inside conditional branches (#724)', () => {
|
|
|
52
52
|
expect(clientJs).toBeDefined()
|
|
53
53
|
const js = clientJs!.content
|
|
54
54
|
|
|
55
|
-
// Should use
|
|
55
|
+
// Should use mapArray inside the branch's bindEvents
|
|
56
56
|
expect(js).toContain('mapArray(')
|
|
57
57
|
|
|
58
58
|
// After O-1's mode-collapse PR, child component init goes through the
|
|
@@ -104,7 +104,11 @@ export function Viewer() {
|
|
|
104
104
|
const files = Object.fromEntries(r.files.map(f => [f.path, f.content]))
|
|
105
105
|
const ssr = Object.values(files).find(c => c.includes('export function Viewer'))
|
|
106
106
|
expect(ssr).toBeDefined()
|
|
107
|
-
|
|
107
|
+
// Slot unification Step B: `{val()}` is the SPAN's only, non-adjacent
|
|
108
|
+
// child, so its marker pair is elided entirely (see the identical note
|
|
109
|
+
// in module-level-signal.audit.test.ts) — the client's claim plan
|
|
110
|
+
// creates the missing Text node from `elidedPath` alone.
|
|
111
|
+
expect(ssr).not.toContain('bfText(')
|
|
108
112
|
expect(ssr).not.toContain("const val = () =>")
|
|
109
113
|
})
|
|
110
114
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A keyed `.map()` body with an array-builder preamble (`const cells = [];
|
|
3
|
+
* cells.push(<span>...)`) plus a row-level event handler used to make EVERY
|
|
4
|
+
* delegated action throw `t is not a function` (BUG-3).
|
|
5
|
+
*
|
|
6
|
+
* Root cause: the delegated handler's item-lookup binds the plain `.find()`
|
|
7
|
+
* result under the loop param (`const t = items().find(...)`) — `t` is a
|
|
8
|
+
* plain object there, never a signal accessor. But the preamble's JSX leaf
|
|
9
|
+
* was rendered through `irToHtmlTemplate` WITH a loopParams spec, the same
|
|
10
|
+
* one used for the mapArray row-render context (where the param genuinely
|
|
11
|
+
* IS an accessor). That rewrote leaf refs to `t().name` — a call on a plain
|
|
12
|
+
* object — which throws at click time.
|
|
13
|
+
*
|
|
14
|
+
* `build-event-delegation.ts` now renders the delegation-context preamble
|
|
15
|
+
* leaf with no loopParams spec, so refs stay in their plain (`t.name`) form.
|
|
16
|
+
* It also only splices the preamble into a given event's handler when that
|
|
17
|
+
* handler's free identifiers actually reference one of the preamble's
|
|
18
|
+
* declared names (`MapCallbackPreamble.declaredNames`) — the common case is
|
|
19
|
+
* an array builder the handler never reads (`cells` here) — and, when
|
|
20
|
+
* spliced, the preamble always runs INSIDE the item-null guard (BUG-4: a
|
|
21
|
+
* `.find()` miss from a stale-DOM race must short-circuit before a preamble
|
|
22
|
+
* that dereferences the item runs).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, test, expect } from 'bun:test'
|
|
26
|
+
import { compileJSX } from '../compiler'
|
|
27
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
28
|
+
|
|
29
|
+
const adapter = new TestAdapter()
|
|
30
|
+
|
|
31
|
+
function clientJsFor(source: string): string {
|
|
32
|
+
const result = compileJSX(source, 'Repro.tsx', { adapter })
|
|
33
|
+
expect(result.errors.filter((e) => e.severity === 'error')).toHaveLength(0)
|
|
34
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')
|
|
35
|
+
expect(clientJs).toBeDefined()
|
|
36
|
+
return clientJs!.content
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The delegated-dispatcher body sits between the loop's `addEventListener`
|
|
41
|
+
* call and the `hydrate(...)` call that follows it in the emitted module —
|
|
42
|
+
* isolate it so assertions about the HANDLER shape don't accidentally match
|
|
43
|
+
* the (legitimately accessor-form) mapArray row-render callback that
|
|
44
|
+
* precedes it in the same file.
|
|
45
|
+
*/
|
|
46
|
+
function delegationBlock(js: string): string {
|
|
47
|
+
const start = js.indexOf('.addEventListener(')
|
|
48
|
+
const end = js.indexOf('\n\nhydrate(')
|
|
49
|
+
expect(start).toBeGreaterThan(-1)
|
|
50
|
+
expect(end).toBeGreaterThan(start)
|
|
51
|
+
return js.slice(start, end)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('delegated-handler preamble splicing (BUG-3 / BUG-4)', () => {
|
|
55
|
+
test('unreferenced array-builder preamble: no getter-call leaf refs, no dead splice', () => {
|
|
56
|
+
const js = clientJsFor(`
|
|
57
|
+
'use client'
|
|
58
|
+
import { createSignal } from '@barefootjs/client'
|
|
59
|
+
export function R2a() {
|
|
60
|
+
const [items, setItems] = createSignal([{ id: 1, name: 'x' }, { id: 2, name: 'y' }])
|
|
61
|
+
const del = (id: number) => setItems(items().filter(i => i.id !== id))
|
|
62
|
+
return <ul id="list">{items().map(t => {
|
|
63
|
+
const cells = []
|
|
64
|
+
cells.push(<span>{t.name}</span>)
|
|
65
|
+
return <li key={t.id}>{cells}<button className="del" onClick={() => del(t.id)}>del</button></li>
|
|
66
|
+
})}</ul>
|
|
67
|
+
}
|
|
68
|
+
`)
|
|
69
|
+
const handler = delegationBlock(js)
|
|
70
|
+
|
|
71
|
+
// BUG-3: the leaf inside the delegation-context preamble must stay in
|
|
72
|
+
// plain-object form — `t` is the `.find()` result, not a signal accessor.
|
|
73
|
+
expect(handler).not.toContain('t().')
|
|
74
|
+
|
|
75
|
+
// The preamble (`cells`) is never read by the click handler
|
|
76
|
+
// (`() => del(t.id)`) — it must not be spliced into the dispatcher at
|
|
77
|
+
// all. `cells.push` legitimately appears three times elsewhere: the
|
|
78
|
+
// mapArray row-render callback, the SSR-template literal it mirrors,
|
|
79
|
+
// and (#2389 patch-on-update) the `{cells}` region-patch effect, which
|
|
80
|
+
// re-runs the preamble so it re-reads the live per-item signal. It must
|
|
81
|
+
// not appear a FOURTH time, in the handler.
|
|
82
|
+
expect(handler).not.toContain('cells.push')
|
|
83
|
+
expect(js.split('cells.push')).toHaveLength(4) // three occurrences total
|
|
84
|
+
|
|
85
|
+
// The item guard still gates the handler call.
|
|
86
|
+
expect(handler).toContain('if (t) {')
|
|
87
|
+
expect(handler).toContain('(() => del(t.id))(__bfEvt)')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('preamble referenced by the handler: spliced in plain form, inside the item guard', () => {
|
|
91
|
+
const js = clientJsFor(`
|
|
92
|
+
'use client'
|
|
93
|
+
import { createSignal } from '@barefootjs/client'
|
|
94
|
+
export function R2b() {
|
|
95
|
+
const [items, setItems] = createSignal([{ id: 1, name: 'x' }, { id: 2, name: 'y' }])
|
|
96
|
+
const del = (label: string) => setItems(items().filter(i => i.name !== label))
|
|
97
|
+
return <ul id="list">{items().map(t => {
|
|
98
|
+
const label = t.name + '!'
|
|
99
|
+
return <li key={t.id}>{label}<button className="del" onClick={() => del(label)}>del</button></li>
|
|
100
|
+
})}</ul>
|
|
101
|
+
}
|
|
102
|
+
`)
|
|
103
|
+
const handler = delegationBlock(js)
|
|
104
|
+
|
|
105
|
+
// Spliced in plain (non-getter) form.
|
|
106
|
+
expect(handler).toContain('const label = t.name + \'!\';')
|
|
107
|
+
expect(handler).not.toContain('t().')
|
|
108
|
+
|
|
109
|
+
// Guard-order (part 3): the preamble line and the handler call both
|
|
110
|
+
// land AFTER `if (t) {`, i.e. inside the item-null guard.
|
|
111
|
+
const guardIdx = handler.indexOf('if (t) {')
|
|
112
|
+
const preambleIdx = handler.indexOf('const label = t.name')
|
|
113
|
+
const callIdx = handler.indexOf('(() => del(label))(__bfEvt)')
|
|
114
|
+
expect(guardIdx).toBeGreaterThanOrEqual(0)
|
|
115
|
+
expect(preambleIdx).toBeGreaterThan(guardIdx)
|
|
116
|
+
expect(callIdx).toBeGreaterThan(preambleIdx)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('destructured param + preamble + handler: no accessor leaks, real destructure resolves refs', () => {
|
|
120
|
+
const js = clientJsFor(`
|
|
121
|
+
'use client'
|
|
122
|
+
import { createSignal } from '@barefootjs/client'
|
|
123
|
+
export function R2c() {
|
|
124
|
+
const [items, setItems] = createSignal([{ id: 1, name: 'x' }, { id: 2, name: 'y' }])
|
|
125
|
+
const del = (label: string) => setItems(items().filter(i => i.name !== label))
|
|
126
|
+
return <ul id="list">{items().map(({ id, name }) => {
|
|
127
|
+
const label = name + '!'
|
|
128
|
+
return <li key={id}>{label}<button className="del" onClick={() => del(label)}>del</button></li>
|
|
129
|
+
})}</ul>
|
|
130
|
+
}
|
|
131
|
+
`)
|
|
132
|
+
const handler = delegationBlock(js)
|
|
133
|
+
|
|
134
|
+
// No `__bfItem()`-style accessor leaks into the delegated handler — the
|
|
135
|
+
// destructured names resolve as real local bindings off `__bfLoopItem`.
|
|
136
|
+
expect(handler).not.toContain('__bfItem(')
|
|
137
|
+
|
|
138
|
+
// Real destructure (#951 TDZ-safe shape) binds `id`/`name` for real —
|
|
139
|
+
// the preamble and handler both close over the resulting plain locals.
|
|
140
|
+
expect(handler).toContain('const __bfLoopItem = ')
|
|
141
|
+
expect(handler).toContain('const { id, name } = __bfLoopItem')
|
|
142
|
+
expect(handler).toContain('const label = name + \'!\';')
|
|
143
|
+
expect(handler).toContain('(() => del(label))(__bfEvt)')
|
|
144
|
+
|
|
145
|
+
// Guard-order holds for the bindings-branch shape too.
|
|
146
|
+
const guardIdx = handler.indexOf('if (__bfLoopItem) {')
|
|
147
|
+
const preambleIdx = handler.indexOf('const label = name')
|
|
148
|
+
const callIdx = handler.indexOf('(() => del(label))(__bfEvt)')
|
|
149
|
+
expect(guardIdx).toBeGreaterThanOrEqual(0)
|
|
150
|
+
expect(preambleIdx).toBeGreaterThan(guardIdx)
|
|
151
|
+
expect(callIdx).toBeGreaterThan(preambleIdx)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -65,12 +65,10 @@ describe('JSX expression referencing a local declared inside an early-return if-
|
|
|
65
65
|
|
|
66
66
|
const content = clientJsContent(result)
|
|
67
67
|
|
|
68
|
-
// Pre-fix
|
|
69
|
-
//
|
|
70
|
-
// inlined directly into the template, so
|
|
71
|
-
//
|
|
72
|
-
// shape.
|
|
73
|
-
expect(content).not.toContain('updateClientMarker')
|
|
68
|
+
// Pre-fix, `aLocal` leaked into a reactive-update block at outer init
|
|
69
|
+
// scope where it doesn't exist (see file docstring). Post-fix: the JSX
|
|
70
|
+
// is inlined directly into the template, so no reactive-update block is
|
|
71
|
+
// needed at all for this static shape.
|
|
74
72
|
expect(content).not.toMatch(/\baLocal\b/)
|
|
75
73
|
|
|
76
74
|
const hydrate = hydrateLine(result)
|
|
@@ -97,9 +95,10 @@ describe('JSX expression referencing a local declared inside an early-return if-
|
|
|
97
95
|
const content = clientJsContent(result)
|
|
98
96
|
// The bare `label` identifier must not appear in the emitted init
|
|
99
97
|
// function — it's resolved to its initializer value at IR-build
|
|
100
|
-
// time.
|
|
101
|
-
|
|
102
|
-
expect(content).
|
|
98
|
+
// time. `@client` writes through a claimed 'text' slot writer
|
|
99
|
+
// (`claim-slots.ts`).
|
|
100
|
+
expect(content).not.toMatch(/__bfw_\w+\([^)]*\blabel\b[^)]*\)/)
|
|
101
|
+
expect(content).toContain("__bfw_s4('s4', 'hello')")
|
|
103
102
|
})
|
|
104
103
|
|
|
105
104
|
test('ternary scope var (`readOnly ? null : <jsx/>`) routes to client-only conditional', () => {
|
|
@@ -124,8 +123,8 @@ describe('JSX expression referencing a local declared inside an early-return if-
|
|
|
124
123
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
125
124
|
|
|
126
125
|
const content = clientJsContent(result)
|
|
127
|
-
// Pre-fix
|
|
128
|
-
//
|
|
126
|
+
// Pre-fix, `compactResize` leaked into the emitted reactive-update
|
|
127
|
+
// block as an undeclared name. Post-fix: the ternary becomes an
|
|
129
128
|
// IRConditional with `clientOnly: true`, routed through `insert()`
|
|
130
129
|
// — and `props.readOnly` is rewritten to `_p.readOnly`.
|
|
131
130
|
expect(content).not.toMatch(/\bcompactResize\b/)
|
|
@@ -22,9 +22,13 @@ import { TestAdapter } from '../adapters/test-adapter'
|
|
|
22
22
|
|
|
23
23
|
describe('flatMap block bodies on structured segments', () => {
|
|
24
24
|
test('leaf text interpolations are escapeText-wrapped in the client bundle', () => {
|
|
25
|
+
// The early return keeps this a STATEMENT-carrying body (segments
|
|
26
|
+
// carrier) — a pure single-`return` projection now lowers to neutral
|
|
27
|
+
// nested-loop IR instead (see the projection tests below).
|
|
25
28
|
const src = `
|
|
26
29
|
function F({ items }: { items: { id: string; tags: string[] }[] }) {
|
|
27
30
|
return <ul>{items.flatMap((it) => {
|
|
31
|
+
if (it.tags.length > 9) return []
|
|
28
32
|
return it.tags.map((t) => <li key={t}>{t}</li>)
|
|
29
33
|
})}</ul>
|
|
30
34
|
}
|
|
@@ -59,6 +63,184 @@ export { F }
|
|
|
59
63
|
expect(tpl).not.toMatch(/[^.\w]limit\b/)
|
|
60
64
|
})
|
|
61
65
|
|
|
66
|
+
test('projection expression bodies lower to neutral IR + descriptor client', () => {
|
|
67
|
+
// `it => it.tags.map(...)` (no braces, no statements) is a pure
|
|
68
|
+
// nested-loop PROJECTION: it lowers to neutral IR (inner IRLoop child)
|
|
69
|
+
// that every SSR adapter templatizes — including DSL backends, per
|
|
70
|
+
// spec/callback-fidelity.md's fidelity table — while the client
|
|
71
|
+
// reconciles the flattened leaves through the descriptor mapArray path
|
|
72
|
+
// synthesized from the same inner loop. Pre-fix it fell through to the
|
|
73
|
+
// IRExpression scalar path and spliced raw JSX into the bundle.
|
|
74
|
+
const src = `
|
|
75
|
+
function F({ items }: { items: { id: string; tags: string[] }[] }) {
|
|
76
|
+
return <ul>{items.flatMap((it) => it.tags.map((t) => <li key={t}>{t}</li>))}</ul>
|
|
77
|
+
}
|
|
78
|
+
export { F }
|
|
79
|
+
`
|
|
80
|
+
const r = compileJSX(src, 'F.tsx', { adapter: new TestAdapter() })
|
|
81
|
+
expect(r.errors).toHaveLength(0)
|
|
82
|
+
const cj = r.files.find(f => f.type === 'clientJs')!.content
|
|
83
|
+
// No raw JSX anywhere in the bundle.
|
|
84
|
+
expect(cj).not.toMatch(/<li key=\{t\}>/)
|
|
85
|
+
expect(cj).not.toMatch(/__BF_JSX_/)
|
|
86
|
+
if (cj.includes('mapArray(')) {
|
|
87
|
+
// Descriptor accessor flattens through the inner loop with the leaf key.
|
|
88
|
+
expect(cj).toMatch(/\.flatMap\(\(it\) => it\.tags\.map\(\(t\) => \(\{ k: \(t\), h: `<li/)
|
|
89
|
+
expect(cj).toMatch(/String\(__bfD\.k \?\? __bfI\)/)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('a member-expression tag leaf is a component, not a projection leaf', () => {
|
|
94
|
+
// `<icons.Tag/>` starts lowercase but is a component per JSX semantics —
|
|
95
|
+
// the wireless-leaf gate must not admit it to the projection route
|
|
96
|
+
// (which cannot wire components). It falls to the segments carrier,
|
|
97
|
+
// where a DSL-tier adapter refuses loudly instead of templatizing a
|
|
98
|
+
// component tag as literal HTML.
|
|
99
|
+
const src = `
|
|
100
|
+
import { icons } from './icons'
|
|
101
|
+
function F({ items }: { items: { id: string; tags: string[] }[] }) {
|
|
102
|
+
return <ul>{items.flatMap((it) => it.tags.map((t) => <icons.Tag key={t} label={t} />))}</ul>
|
|
103
|
+
}
|
|
104
|
+
export { F }
|
|
105
|
+
`
|
|
106
|
+
const dsl = new TestAdapter()
|
|
107
|
+
;(dsl as { acceptsCallbackBody?: () => boolean }).acceptsCallbackBody = () => false
|
|
108
|
+
const r = compileJSX(src, 'F.tsx', { adapter: dsl })
|
|
109
|
+
expect(r.errors.filter(e => e.severity === 'error').length).toBeGreaterThan(0)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('single-return block projection lowers identically (DSL adapters accept it)', () => {
|
|
113
|
+
const src = `
|
|
114
|
+
function F({ items }: { items: { id: string; tags: string[] }[] }) {
|
|
115
|
+
return <ul>{items.flatMap((it) => {
|
|
116
|
+
return it.tags.map((t) => <li key={t}>{t}</li>)
|
|
117
|
+
})}</ul>
|
|
118
|
+
}
|
|
119
|
+
export { F }
|
|
120
|
+
`
|
|
121
|
+
const dsl = new TestAdapter()
|
|
122
|
+
;(dsl as { acceptsCallbackBody?: () => boolean }).acceptsCallbackBody = () => false
|
|
123
|
+
const r = compileJSX(src, 'F.tsx', { adapter: dsl })
|
|
124
|
+
// Neutral IR — no BF021 gate on a DSL-tier adapter.
|
|
125
|
+
expect(r.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('destructured prop refs rewrite to _p.xxx in expression-body hydrate templates', () => {
|
|
129
|
+
const src = `
|
|
130
|
+
function F({ owner, items }: { owner: string; items: { id: string; tags: string[] }[] }) {
|
|
131
|
+
return <ul>{items.flatMap((it) => it.tags.map((t) => <li key={t}>{t} ({owner})</li>))}</ul>
|
|
132
|
+
}
|
|
133
|
+
export { F }
|
|
134
|
+
`
|
|
135
|
+
const r = compileJSX(src, 'F.tsx', { adapter: new TestAdapter() })
|
|
136
|
+
expect(r.errors).toHaveLength(0)
|
|
137
|
+
const cj = r.files.find(f => f.type === 'clientJs')!.content
|
|
138
|
+
const tpl = cj.match(/template: \(_p\) => `[\s\S]*?` \}\)/)?.[0] ?? ''
|
|
139
|
+
expect(tpl).toMatch(/_p\.owner/)
|
|
140
|
+
expect(tpl).not.toMatch(/[^.\w]owner\b/)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('client loop reconciles the FLATTENED descriptors, not the source items', () => {
|
|
144
|
+
// The mapArray accessor flattens through the callback body — each leaf a
|
|
145
|
+
// `({ k, h })` descriptor keyed by the leaf's own `key` — so hydration
|
|
146
|
+
// adopts every SSR leaf (not one per source item), adds build real
|
|
147
|
+
// elements from `h` (not an empty template), and the keyFn is the leaf
|
|
148
|
+
// key (not null/index). This pins the client half of the flatMap loop;
|
|
149
|
+
// pre-fix it emitted `mapArray(() => todos(), _sN, null, …innerHTML = ``…)`
|
|
150
|
+
// — leaf loss at hydration and a cloneNode(null) crash on adds.
|
|
151
|
+
const src = `
|
|
152
|
+
'use client'
|
|
153
|
+
import { createSignal } from '@barefootjs/client'
|
|
154
|
+
export function F() {
|
|
155
|
+
const [todos, setTodos] = createSignal<{ id: number; tags: string[] }[]>([{ id: 1, tags: ['a'] }])
|
|
156
|
+
return <ul>{todos().flatMap((t) => {
|
|
157
|
+
if (t.tags.length > 5) return []
|
|
158
|
+
const prefix = t.id + ':'
|
|
159
|
+
return t.tags.map((tag) => <li key={\`\${t.id}:\${tag}\`}>{prefix}{tag}</li>)
|
|
160
|
+
})}</ul>
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
const r = compileJSX(src, 'F.tsx', { adapter: new TestAdapter() })
|
|
164
|
+
expect(r.errors).toHaveLength(0)
|
|
165
|
+
const cj = r.files.find(f => f.type === 'clientJs')!.content
|
|
166
|
+
// Flattened source with descriptor leaves…
|
|
167
|
+
expect(cj).toMatch(/mapArray\(\(\) => \(todos\(\)\)\.flatMap\(\(t\) => \{/)
|
|
168
|
+
expect(cj).toMatch(/\(\{ k: \(`\$\{t\.id\}:\$\{tag\}`\), h: `<li>/)
|
|
169
|
+
// …keyed on the leaf key with index fallback…
|
|
170
|
+
expect(cj).toMatch(/\(__bfD, __bfI\) => String\(__bfD\.k \?\? __bfI\)/)
|
|
171
|
+
// …renderItem builds from the descriptor HTML and patches on change…
|
|
172
|
+
expect(cj).toMatch(/__tpl\.innerHTML = __bfD\(\)\.h/)
|
|
173
|
+
expect(cj).toMatch(/patchLeaf\(__el, __html\)/)
|
|
174
|
+
// …and the statements-before-return are NOT duplicated as a renderItem
|
|
175
|
+
// preamble (the segments carrier is the single door).
|
|
176
|
+
expect(cj).not.toMatch(/return \[\];* if \(__existing\)/)
|
|
177
|
+
// Leaf data-key never rides the string templates — mapArray stamps it.
|
|
178
|
+
expect(cj).not.toMatch(/data-key/)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('a leaf with an event handler refuses loudly (no silent dead DOM)', () => {
|
|
182
|
+
const src = `
|
|
183
|
+
'use client'
|
|
184
|
+
import { createSignal } from '@barefootjs/client'
|
|
185
|
+
export function F() {
|
|
186
|
+
const [todos, setTodos] = createSignal<{ id: number; tags: string[] }[]>([])
|
|
187
|
+
return <ul>{todos().flatMap((t) => {
|
|
188
|
+
return t.tags.map((tag) => <li key={tag} onClick={() => console.log(tag)}>{tag}</li>)
|
|
189
|
+
})}</ul>
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
const r = compileJSX(src, 'F.tsx', { adapter: new TestAdapter() })
|
|
193
|
+
const errs = r.errors.filter(e => e.severity === 'error')
|
|
194
|
+
expect(errs.length).toBeGreaterThan(0)
|
|
195
|
+
expect(errs[0].message).toContain('cannot carry')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('a fragment leaf refuses loudly (descriptor path is single-element)', () => {
|
|
199
|
+
// The renderItem adopts `template.content.firstElementChild` and
|
|
200
|
+
// patchLeaf patches ONE element root — a fragment leaf would silently
|
|
201
|
+
// drop its siblings client-side while SSR renders them all.
|
|
202
|
+
const src = `
|
|
203
|
+
'use client'
|
|
204
|
+
import { createSignal } from '@barefootjs/client'
|
|
205
|
+
export function F() {
|
|
206
|
+
const [todos, setTodos] = createSignal<{ id: number; tags: string[] }[]>([])
|
|
207
|
+
return <ul>{todos().flatMap((t) => {
|
|
208
|
+
return t.tags.map((tag) => <><b>{tag}</b><i>{tag}</i></>)
|
|
209
|
+
})}</ul>
|
|
210
|
+
}
|
|
211
|
+
`
|
|
212
|
+
const r = compileJSX(src, 'F.tsx', { adapter: new TestAdapter() })
|
|
213
|
+
const errs = r.errors.filter(e => e.severity === 'error')
|
|
214
|
+
expect(errs.length).toBeGreaterThan(0)
|
|
215
|
+
expect(errs.some(e => e.message.includes('must be a single'))).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('the structural net stays armed when an unrelated diagnostic fired earlier', () => {
|
|
219
|
+
// The scalar-fallthrough net de-dups against refusals fired during the
|
|
220
|
+
// SAME map call (entry-count gate), never against diagnostics recorded
|
|
221
|
+
// earlier in the file — a prior error in another loop must not let raw
|
|
222
|
+
// JSX splice silently into the bundle.
|
|
223
|
+
const src = `
|
|
224
|
+
'use client'
|
|
225
|
+
import { createSignal } from '@barefootjs/client'
|
|
226
|
+
export function F() {
|
|
227
|
+
const [todos, setTodos] = createSignal<{ id: number; tags: string[] }[]>([])
|
|
228
|
+
const wrap = (n: unknown) => n
|
|
229
|
+
return <div>
|
|
230
|
+
<ul>{todos().flatMap((t) => {
|
|
231
|
+
return t.tags.map((tag) => <li key={tag} onClick={() => console.log(tag)}>{tag}</li>)
|
|
232
|
+
})}</ul>
|
|
233
|
+
<ol>{todos().map((t) => wrap(<li>{t.id}</li>))}</ol>
|
|
234
|
+
</div>
|
|
235
|
+
}
|
|
236
|
+
`
|
|
237
|
+
const r = compileJSX(src, 'F.tsx', { adapter: new TestAdapter() })
|
|
238
|
+
const errs = r.errors.filter(e => e.severity === 'error')
|
|
239
|
+
// One refusal per loop: the leaf-wiring refusal AND the structural net.
|
|
240
|
+
expect(errs.some(e => e.message.includes('cannot carry'))).toBe(true)
|
|
241
|
+
expect(errs.some(e => e.message.includes('would leak verbatim'))).toBe(true)
|
|
242
|
+
})
|
|
243
|
+
|
|
62
244
|
test('TS type annotations in the block body are stripped from the client bundle', () => {
|
|
63
245
|
const src = `
|
|
64
246
|
function F({ items }: { items: { id: string; labels: string[] }[] }) {
|
|
@@ -440,10 +440,12 @@ describe('inline JSX as object-literal arrow value (#1663)', () => {
|
|
|
440
440
|
expect(js).toMatch(/hydrate\(\s*['"]BFInlineJsxCallback1(?:__|['"])/)
|
|
441
441
|
})
|
|
442
442
|
|
|
443
|
-
test('dynamic JSX-returning call routes the slot through
|
|
443
|
+
test('dynamic JSX-returning call routes the slot through a claimed markup slot', () => {
|
|
444
444
|
// The child expression `{themeLogo(props.id)}` evaluates to a live
|
|
445
445
|
// component element on the client; the slot update must splice it in by
|
|
446
|
-
// identity via
|
|
446
|
+
// identity via a claimed 'markup' slot writer (slot unification A3)
|
|
447
|
+
// rather than stringify it into "[object …]" — the same contract
|
|
448
|
+
// `__bfText` used to provide.
|
|
447
449
|
const source = `
|
|
448
450
|
'use client'
|
|
449
451
|
import { BrandLogo } from './brand-logo'
|
|
@@ -458,7 +460,7 @@ describe('inline JSX as object-literal arrow value (#1663)', () => {
|
|
|
458
460
|
}
|
|
459
461
|
`
|
|
460
462
|
const js = clientJs(source)
|
|
461
|
-
expect(js).toContain('
|
|
462
|
-
expect(js).toMatch(/
|
|
463
|
+
expect(js).toContain("kind: 'markup'")
|
|
464
|
+
expect(js).toMatch(/lazySlots[^']*from '@barefootjs\/client\/runtime'/)
|
|
463
465
|
})
|
|
464
466
|
})
|
|
@@ -359,11 +359,12 @@ describe('JSX props (#559)', () => {
|
|
|
359
359
|
|
|
360
360
|
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
361
361
|
expect(clientJs).toBeDefined()
|
|
362
|
-
// The text update routes through
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
362
|
+
// The text update routes through a claimed 'markup' slot writer (slot
|
|
363
|
+
// unification A3), whose `writeMarkup` preserves the server-rendered
|
|
364
|
+
// DOM for `__isSlot` values (and splices live Nodes by identity)
|
|
365
|
+
// instead of the old inline `nodeValue = String(...)` assignment —
|
|
366
|
+
// the same contract `__bfText` used to provide (#1663).
|
|
367
|
+
expect(clientJs!.content).toContain("kind: 'markup'")
|
|
367
368
|
})
|
|
368
369
|
})
|
|
369
370
|
})
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* and the conditional follow-up (#941), so this loop widening closes
|
|
19
19
|
* the divergence gap between the allow-list path and the AST-flag path.
|
|
20
20
|
*
|
|
21
|
-
* Over-reconciliation of a pure-call array has a real cost (
|
|
21
|
+
* Over-reconciliation of a pure-call array has a real cost (mapArray
|
|
22
22
|
* is not a cheap primitive), so the fixture sweep in the accompanying
|
|
23
23
|
* PR measures byte delta too; but the silent-drop bug this closes is
|
|
24
24
|
* the expensive one — a frozen client list is a visible correctness
|
|
@@ -89,8 +89,8 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
89
89
|
|
|
90
90
|
test('inline literal array stays static (optimisation preserved)', () => {
|
|
91
91
|
// `[1, 2, 3]` is an ArrayLiteralExpression with no calls — the
|
|
92
|
-
// existing static-render path is the right call. No
|
|
93
|
-
//
|
|
92
|
+
// existing static-render path is the right call. No `mapArray` call
|
|
93
|
+
// should appear.
|
|
94
94
|
const source = `
|
|
95
95
|
'use client'
|
|
96
96
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -107,7 +107,6 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
107
107
|
|
|
108
108
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
109
109
|
expect(clientJs).not.toMatch(/\bmapArray\s*\(/)
|
|
110
|
-
expect(clientJs).not.toMatch(/\breconcileList\s*\(/)
|
|
111
110
|
})
|
|
112
111
|
|
|
113
112
|
test('prop array compiles to mapArray (#1586)', () => {
|