@barefootjs/jsx 0.34.0 → 0.35.1
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/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1072 -903
- package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
- package/dist/ir-to-client-js/collect-elements.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-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +11 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.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/build-plain-row.d.ts +52 -0
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +5 -2
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +24 -44
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +6 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -6
- 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 +11 -9
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
- 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/lazy-row.d.ts +1 -3
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
- 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.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -2
- 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/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
- package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
- package/dist/ir-to-client-js/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/safe-html.d.ts +158 -0
- package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
- package/dist/ir-to-client-js/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +34 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +22 -1
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +138 -22
- package/src/__tests__/binding-scope-ratchet.test.ts +25 -2
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
- package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
- package/src/__tests__/lazy-conditional.test.ts +52 -12
- package/src/__tests__/lazy-preamble.test.ts +11 -11
- package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
- package/src/__tests__/loop-index-reactivity.test.ts +208 -0
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +41 -8
- package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
- package/src/__tests__/nested-loop-plain.test.ts +39 -25
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +16 -6
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
- package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
- package/src/__tests__/safe-html-splice-door.test.ts +61 -0
- package/src/__tests__/ssr-defaults.test.ts +59 -0
- package/src/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +126 -32
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +12 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +36 -59
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +25 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +36 -9
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +116 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +23 -8
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/shared.ts +40 -17
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +22 -15
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +92 -6
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +67 -245
- package/src/ir-to-client-js/imports.ts +3 -0
- package/src/ir-to-client-js/index.ts +4 -4
- package/src/ir-to-client-js/markup-slots.ts +25 -0
- package/src/ir-to-client-js/reactivity.ts +73 -27
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +136 -24
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/reactivity-checker.ts +60 -0
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS Compiler — nested `.map()` reactivity when the inner array
|
|
3
|
+
* (or the row's own bindings) does NOT derive from the outer loop's item.
|
|
4
|
+
*
|
|
5
|
+
* #2865: before this fix, `build-inner-loop.ts` decided "reactive vs
|
|
6
|
+
* static" for a nested loop by checking only whether `inner.array`
|
|
7
|
+
* textually referenced the OUTER loop's item — `refsParent`. Anything
|
|
8
|
+
* else the inner loop depended on fell through to a hydration-only
|
|
9
|
+
* `forEach` (`emitStatic` in `stringify/inner-loop.ts`) that never wired
|
|
10
|
+
* a single `createEffect`, regardless of whether the array itself, or a
|
|
11
|
+
* text/attr/conditional inside the row, was genuinely reactive. That
|
|
12
|
+
* fork has been removed — every nested loop now gets the full reactive
|
|
13
|
+
* `mapArray` emission unconditionally.
|
|
14
|
+
*
|
|
15
|
+
* This file pins the previously-frozen shapes directly (as opposed to
|
|
16
|
+
* `nested-loop-plain.test.ts` / `nested-loop-index-param.test.ts`, whose
|
|
17
|
+
* relevant cases were originally written to pin the OLD static shape and
|
|
18
|
+
* have been flipped in place).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect } from 'bun:test'
|
|
22
|
+
import { compileJSX } from '../compiler'
|
|
23
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
24
|
+
|
|
25
|
+
const adapter = new TestAdapter()
|
|
26
|
+
|
|
27
|
+
function clientJsFor(source: string): string {
|
|
28
|
+
const result = compileJSX(source, 'Repro.tsx', { adapter })
|
|
29
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
30
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
31
|
+
expect(clientJs).toBeDefined()
|
|
32
|
+
return clientJs!.content
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('nested .map() reactivity independent of the outer item (#2865)', () => {
|
|
36
|
+
test('inner array read from a component signal (not the outer item) wires a nested mapArray', () => {
|
|
37
|
+
const content = clientJsFor(`
|
|
38
|
+
'use client'
|
|
39
|
+
import { createSignal } from '@barefootjs/client'
|
|
40
|
+
export function Repro() {
|
|
41
|
+
const [rows] = createSignal([{ id: 1 }])
|
|
42
|
+
const [tags] = createSignal(['x', 'y'])
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
{rows().map(row => (
|
|
46
|
+
<div key={row.id}>
|
|
47
|
+
<ul>
|
|
48
|
+
{tags().map(t => <li key={t}>{t}</li>)}
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
`)
|
|
56
|
+
|
|
57
|
+
// The old bug emitted `tags().forEach(...)` with no mapArray/createEffect
|
|
58
|
+
// at all — appending a tag never reached the DOM.
|
|
59
|
+
expect(content).toMatch(/mapArray\(\(\) => tags\(\) \|\| \[\], /)
|
|
60
|
+
expect(content).not.toMatch(/tags\(\)\.forEach\(/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('inner array read from a memo (not the outer item) wires a nested mapArray', () => {
|
|
64
|
+
const content = clientJsFor(`
|
|
65
|
+
'use client'
|
|
66
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
67
|
+
export function Repro() {
|
|
68
|
+
const [rows] = createSignal([{ id: 1 }])
|
|
69
|
+
const [count] = createSignal(2)
|
|
70
|
+
const tags = createMemo(() => Array.from({ length: count() }, (_, i) => \`t\${i}\`))
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
{rows().map(row => (
|
|
74
|
+
<div key={row.id}>
|
|
75
|
+
<ul>
|
|
76
|
+
{tags().map(t => <li key={t}>{t}</li>)}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
`)
|
|
84
|
+
|
|
85
|
+
expect(content).toMatch(/mapArray\(\(\) => tags\(\) \|\| \[\], /)
|
|
86
|
+
expect(content).not.toMatch(/tags\(\)\.forEach\(/)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('a row-content signal read (text) inside a LITERAL inner array still wires createEffect', () => {
|
|
90
|
+
// Even when the ARRAY itself is a plain constant (never needs to
|
|
91
|
+
// reconcile), a text/attr/conditional inside the row that reads a
|
|
92
|
+
// component signal must still update. The old static emitter never
|
|
93
|
+
// read `reactiveTexts`/`reactiveAttrs`/`conditionals` at all, so this
|
|
94
|
+
// froze independent of whether the array was reactive.
|
|
95
|
+
const content = clientJsFor(`
|
|
96
|
+
'use client'
|
|
97
|
+
import { createSignal } from '@barefootjs/client'
|
|
98
|
+
const TAGS = ['x', 'y']
|
|
99
|
+
export function Repro() {
|
|
100
|
+
const [rows] = createSignal([{ id: 1 }])
|
|
101
|
+
const [count, setCount] = createSignal(0)
|
|
102
|
+
return (
|
|
103
|
+
<div onClick={() => setCount(c => c + 1)}>
|
|
104
|
+
{rows().map(row => (
|
|
105
|
+
<div key={row.id}>
|
|
106
|
+
<ul>
|
|
107
|
+
{TAGS.map(t => <li key={t}>{t}: {count()}</li>)}
|
|
108
|
+
</ul>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
`)
|
|
115
|
+
|
|
116
|
+
expect(content).toMatch(/mapArray\(\(\) => TAGS \|\| \[\], /)
|
|
117
|
+
expect(content).toContain('createEffect(')
|
|
118
|
+
expect(content).not.toMatch(/TAGS\.forEach\(/)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('destructured outer param used by the inner array no longer throws ReferenceError at row construction', () => {
|
|
122
|
+
// Before #2865: static mode skipped the destructure-unwrap statement,
|
|
123
|
+
// so the emitted `forEach` referenced a bare `tags` identifier that
|
|
124
|
+
// was never declared in scope.
|
|
125
|
+
const content = clientJsFor(`
|
|
126
|
+
'use client'
|
|
127
|
+
import { createSignal } from '@barefootjs/client'
|
|
128
|
+
interface Row { id: number; tags: string[] }
|
|
129
|
+
export function Repro() {
|
|
130
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tags: ['a', 'b'] }])
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
{rows().map(({ id, tags }) => (
|
|
134
|
+
<div key={id}>
|
|
135
|
+
<ul>
|
|
136
|
+
{tags.map(t => <li key={t}>{t}</li>)}
|
|
137
|
+
</ul>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
`)
|
|
144
|
+
|
|
145
|
+
// `tags` must resolve through the outer row accessor
|
|
146
|
+
// (`__bfItem().tags`) — not a bare free identifier that was never
|
|
147
|
+
// declared in scope (the old static-mode bug: the destructured
|
|
148
|
+
// pattern names `tags` for the SSR template, but the reactive
|
|
149
|
+
// renderItem's own param is the whole-item accessor, and every
|
|
150
|
+
// reference to a destructured name is rewritten to go through it).
|
|
151
|
+
expect(content).toMatch(/mapArray\(\(\) => __bfItem\(\)\.tags \|\| \[\], /)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('outer-index-derived inner array wires a nested mapArray', () => {
|
|
155
|
+
const content = clientJsFor(`
|
|
156
|
+
'use client'
|
|
157
|
+
import { createSignal } from '@barefootjs/client'
|
|
158
|
+
export function Repro() {
|
|
159
|
+
const [matrix] = createSignal([['a', 'b'], ['c']])
|
|
160
|
+
return (
|
|
161
|
+
<div>
|
|
162
|
+
{matrix().map((row, i) => (
|
|
163
|
+
<div key={i}>
|
|
164
|
+
<ul>
|
|
165
|
+
{matrix()[i].map(cell => <li key={cell}>{cell}</li>)}
|
|
166
|
+
</ul>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
`)
|
|
173
|
+
|
|
174
|
+
expect(content).toContain('mapArray(')
|
|
175
|
+
expect(content).not.toMatch(/\.forEach\(\(cell,/)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -138,18 +138,21 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
test('regression: 3-level nested loop wires mapArray at the deepest level (not forEach)', () => {
|
|
141
|
-
//
|
|
142
|
-
// by checking the IR's `inner.refsOuterParam` — a
|
|
143
|
-
// time against the *outermost* loop's param only.
|
|
144
|
-
// expression typically references the
|
|
145
|
-
// `g.items` inside `t.groups.map(g => ...)`
|
|
146
|
-
// and the loop fell through to the static
|
|
147
|
-
// additions / removals at the deepest level
|
|
148
|
-
// the DOM.
|
|
141
|
+
// Originally (the O-8 narrowing fix), `emitInnerLoopSetup` decided "is
|
|
142
|
+
// this loop reactive?" by checking the IR's `inner.refsOuterParam` — a
|
|
143
|
+
// flag set at collect time against the *outermost* loop's param only.
|
|
144
|
+
// At depth 2+, the array expression typically references the
|
|
145
|
+
// *immediate* parent (e.g. `g.items` inside `t.groups.map(g => ...)`
|
|
146
|
+
// body), so the check failed and the loop fell through to the static
|
|
147
|
+
// `forEach` branch. Result: additions / removals at the deepest level
|
|
148
|
+
// silently never reached the DOM.
|
|
149
149
|
//
|
|
150
|
-
//
|
|
151
|
-
// passed in, and narrow the parent param when recursing into
|
|
152
|
-
// levels.
|
|
150
|
+
// O-8's own fix: re-check dynamically at each level against the parent
|
|
151
|
+
// param passed in, and narrow the parent param when recursing into
|
|
152
|
+
// child levels. #2865 later removed the reactive/static fork (and
|
|
153
|
+
// `refsOuterParam` with it) entirely — every level is unconditionally
|
|
154
|
+
// reactive now — so this assertion holds trivially, but the test stays
|
|
155
|
+
// as a regression pin on "every depth gets its own `mapArray`."
|
|
153
156
|
const source = `
|
|
154
157
|
'use client'
|
|
155
158
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -293,10 +296,13 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
293
296
|
// locals — `initChild`'s prop getter would throw `ReferenceError` when
|
|
294
297
|
// the child component first mounted.
|
|
295
298
|
//
|
|
296
|
-
// Fix: thread `inner.mapPreamble` through
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
299
|
+
// Fix: thread `inner.mapPreamble` through the static-array-child-init
|
|
300
|
+
// emission (`build-static-array-child-init.ts` — the OUTER array here
|
|
301
|
+
// is a literal, so this is unrelated to #2865's nested-loop reactive/
|
|
302
|
+
// static fork, which only ever applied under a REACTIVE outer loop)
|
|
303
|
+
// and emit it (raw — `forEach`'s param is the literal item, not a
|
|
304
|
+
// signal accessor) at the top of the forEach body so the component
|
|
305
|
+
// setup can resolve the locals.
|
|
300
306
|
const source = `
|
|
301
307
|
'use client'
|
|
302
308
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -442,13 +448,17 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
442
448
|
expect(section.indexOf('const rowTag')).toBeLessThan(section.indexOf('initChild'))
|
|
443
449
|
})
|
|
444
450
|
|
|
445
|
-
test('regression #1064:
|
|
446
|
-
// The
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
451
|
+
test('regression #1064: reactive inner mapArray (signal outer + non-outer-derived inner array) emits inner preamble', () => {
|
|
452
|
+
// The reactive composite renderItem hosts an inner loop whose array is
|
|
453
|
+
// a non-outer-derived lookup (a module-level constant here — could just
|
|
454
|
+
// as easily be a signal, a memo, or a prop). Before #2865, an inner
|
|
455
|
+
// array not referencing the outer loop param routed to
|
|
456
|
+
// `control-flow/inner-loop.ts::emitStatic` — a hydration-only `forEach`
|
|
457
|
+
// that silently never re-ran the row's own preamble/reactivity on a
|
|
458
|
+
// later update. That fork is gone: every inner loop now gets the full
|
|
459
|
+
// `mapArray` emission. Use a `__innerIdx<uid>` suffix in the regex to
|
|
460
|
+
// specifically target the nested `mapArray` (vs. the unsuffixed
|
|
461
|
+
// `static-array-child-init` for a fully-static OUTER array).
|
|
452
462
|
const source = `
|
|
453
463
|
'use client'
|
|
454
464
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -480,8 +490,12 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
480
490
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
481
491
|
const js = result.files.find(f => f.type === 'clientJs')!.content
|
|
482
492
|
|
|
483
|
-
// The
|
|
484
|
-
// assert preamble appears in that body
|
|
485
|
-
|
|
493
|
+
// The inner mapArray's renderItem uses a `__innerIdx<uid>` suffix —
|
|
494
|
+
// assert the preamble appears in that body, re-derived from the
|
|
495
|
+
// ACCESSOR form of both the outer and inner loop params (`row()`,
|
|
496
|
+
// `cell()`), and precedes the child-component prop read.
|
|
497
|
+
expect(js).toMatch(
|
|
498
|
+
/mapArray\(\(\) => SHARED \|\| \[\], .*?, \(cell, __innerIdx\d+_\d+, __existing\) => \{[\s\S]*?const\s+tag\s*=\s*`\$\{row\(\)\.key\}-\$\{cell\(\)\.n\}`[\s\S]*?upsertChild/
|
|
499
|
+
)
|
|
486
500
|
})
|
|
487
501
|
})
|
|
@@ -140,8 +140,10 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
140
140
|
const content = result.files.find((f) => f.type === 'clientJs')!.content
|
|
141
141
|
|
|
142
142
|
// `data-off` uses the truthy-check shape (no `__v != null` for this
|
|
143
|
-
// attribute) so a concrete `false` removes the attribute.
|
|
144
|
-
|
|
143
|
+
// attribute) so a concrete `false` removes the attribute. The dedup
|
|
144
|
+
// guard (#2869) computes into `__x` first, so the truthy check reads
|
|
145
|
+
// `__x`, not `c().isOff` inline.
|
|
146
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?const __x = c\(\)\.isOff[\s\S]*?if \(__x\)\s*\S+\.setAttribute\('data-off',\s*''\)/)
|
|
145
147
|
// aria-* keeps the explicit "true" value per WAI-ARIA.
|
|
146
148
|
expect(content).toContain("setAttribute('aria-pressed', 'true')")
|
|
147
149
|
})
|
|
@@ -233,8 +235,11 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
233
235
|
// and the inner-loop reactive `createEffect` (must be `task().id`
|
|
234
236
|
// to subscribe to the per-item accessor). The bug we're locking
|
|
235
237
|
// down is purely in the createEffect emission, so scope to that.
|
|
238
|
+
// The object literal now sits in the dedup guard's `const __x = {...}`
|
|
239
|
+
// temp (#2869) — `styleToCss` reads it back as `styleToCss(__x)`, not
|
|
240
|
+
// the object literal inline.
|
|
236
241
|
const effectMatch = content.match(
|
|
237
|
-
/createEffect\(\(\) => \{[\s\S]*?
|
|
242
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = \{[\s\S]*?\}[\s\S]*?styleToCss\(__x\)[\s\S]*?__ta_s\d+\.setAttribute\('style'/,
|
|
238
243
|
)
|
|
239
244
|
expect(effectMatch).not.toBeNull()
|
|
240
245
|
const effectBody = effectMatch![0]
|
|
@@ -282,8 +287,11 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
282
287
|
const result = compileJSX(source, 'G.tsx', { adapter })
|
|
283
288
|
expect(result.errors).toHaveLength(0)
|
|
284
289
|
const content = result.files.find((f) => f.type === 'clientJs')!.content
|
|
290
|
+
// The object literal now sits in the dedup guard's `const __x = {...}`
|
|
291
|
+
// temp (#2869) — `styleToCss` reads it back as `styleToCss(__x)`, not
|
|
292
|
+
// the object literal inline.
|
|
285
293
|
const effectMatch = content.match(
|
|
286
|
-
/createEffect\(\(\) => \{[\s\S]*?
|
|
294
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = \{[\s\S]*?\}[\s\S]*?styleToCss\(__x\)[\s\S]*?__ta_s\d+\.setAttribute\('style'/,
|
|
287
295
|
)
|
|
288
296
|
expect(effectMatch).not.toBeNull()
|
|
289
297
|
const effectBody = effectMatch![0]
|
|
@@ -394,8 +402,10 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
394
402
|
// The helper call must appear inside a createEffect alongside the
|
|
395
403
|
// textContent write — `labelAt(` also appears in the static template
|
|
396
404
|
// clone, so asserting it independently would pass even with the
|
|
397
|
-
// effect missing (the exact regression here).
|
|
398
|
-
|
|
405
|
+
// effect missing (the exact regression here). `pi` reads the row's
|
|
406
|
+
// live index accessor (#2859), so `labelAt(pi())` also stays correct
|
|
407
|
+
// if this innermost loop's rows ever survive a same-key reorder.
|
|
408
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?__bfw_\w+\('s\d+', String\(labelAt\(pi\(\)\)\)\)/)
|
|
399
409
|
expect(content).toContain("setAttribute('class'")
|
|
400
410
|
})
|
|
401
411
|
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen-shape pins for the loop-row branch-arm preamble gap (#2596/#2447
|
|
3
|
+
* follow-up, found while discussing #2869's aftermath). Companion to the
|
|
4
|
+
* behavioral test in
|
|
5
|
+
* `packages/client/__tests__/runtime/preamble-branch-attr-staleness.test.ts`,
|
|
6
|
+
* which is the one that actually proves the fix works end to end — this
|
|
7
|
+
* file pins the exact emitted shape so a future refactor of
|
|
8
|
+
* `stringifyLoopChildArm`/`stringifyBranchReactiveAttrs`/
|
|
9
|
+
* `stringifyLoopChildConditional` can't silently drop the preamble re-run
|
|
10
|
+
* again.
|
|
11
|
+
*
|
|
12
|
+
* Two sites were fixed together: a reactive ATTRIBUTE inside a loop row's
|
|
13
|
+
* branch conditional that reads a `.map()` preamble local (case a), and a
|
|
14
|
+
* NESTED conditional's own CONDITION expression doing the same (case b,
|
|
15
|
+
* #2596 one nesting level deeper than the already-fixed outer-conditional
|
|
16
|
+
* case pinned by `preamble-conditional-reactivity.test.ts`). Case c is the
|
|
17
|
+
* negative control: no preamble local in play, so nothing should be
|
|
18
|
+
* injected.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect } from 'bun:test'
|
|
22
|
+
import { compileJSX } from '../compiler'
|
|
23
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
24
|
+
|
|
25
|
+
const adapter = new TestAdapter()
|
|
26
|
+
|
|
27
|
+
function clientJsFor(source: string, filename: string): string {
|
|
28
|
+
const result = compileJSX(source, filename, { adapter })
|
|
29
|
+
expect(result.errors).toHaveLength(0)
|
|
30
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')?.content
|
|
31
|
+
if (!clientJs) throw new Error('No client JS emitted')
|
|
32
|
+
return clientJs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('#2596/#2447 follow-up — preamble re-run inside a loop-row branch arm', () => {
|
|
36
|
+
test('(a) an attribute inside a branch arm re-runs the preamble before reading it', () => {
|
|
37
|
+
const source = `'use client'
|
|
38
|
+
import { createSignal } from '@barefootjs/client'
|
|
39
|
+
type Row = { id: number; tier: string }
|
|
40
|
+
export function List() {
|
|
41
|
+
const [visible] = createSignal(true)
|
|
42
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tier: 'gold' }])
|
|
43
|
+
return (
|
|
44
|
+
<ul>
|
|
45
|
+
{rows().map(row => {
|
|
46
|
+
const cls = 'tier-' + row.tier
|
|
47
|
+
return (
|
|
48
|
+
<li key={row.id}>
|
|
49
|
+
{visible() ? <span class={cls} id="target">{row.tier}</span> : <span>x</span>}
|
|
50
|
+
</li>
|
|
51
|
+
)
|
|
52
|
+
})}
|
|
53
|
+
</ul>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
const js = clientJsFor(source, 'List.tsx')
|
|
58
|
+
|
|
59
|
+
// Pin the eager per-row mapArray path (not lazy-row, not relevant here
|
|
60
|
+
// but keeps this fixture stable if eligibility ever widens).
|
|
61
|
+
expect(js).toContain('mapArray(')
|
|
62
|
+
|
|
63
|
+
// The outer conditional's own getter is NOT preamble-guarded (`visible`
|
|
64
|
+
// is a plain signal, no preamble read) — stays bare.
|
|
65
|
+
expect(js).toContain("insert(__el, 's0', () => visible(), {")
|
|
66
|
+
|
|
67
|
+
// The arm's attr effect re-runs the preamble before reading `cls`.
|
|
68
|
+
const disposerStart = js.indexOf('const __disposers = []')
|
|
69
|
+
const disposerEnd = js.indexOf('return () => __disposers.forEach')
|
|
70
|
+
const arm = js.slice(disposerStart, disposerEnd)
|
|
71
|
+
const preambleAt = arm.indexOf("const cls = 'tier-' + row().tier;")
|
|
72
|
+
const readAt = arm.indexOf('const __x = cls')
|
|
73
|
+
expect(preambleAt).toBeGreaterThanOrEqual(0)
|
|
74
|
+
expect(readAt).toBeGreaterThan(preambleAt)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('(b) a nested conditional inside a branch arm re-runs the preamble in its own condition getter', () => {
|
|
78
|
+
const source = `'use client'
|
|
79
|
+
import { createSignal } from '@barefootjs/client'
|
|
80
|
+
type Row = { id: number; tier: string }
|
|
81
|
+
export function List() {
|
|
82
|
+
const [visible] = createSignal(true)
|
|
83
|
+
const [selected] = createSignal(1)
|
|
84
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tier: 'gold' }])
|
|
85
|
+
return (
|
|
86
|
+
<ul>
|
|
87
|
+
{rows().map(row => {
|
|
88
|
+
const cls = 'tier-' + row.tier
|
|
89
|
+
const on = selected() === row.id
|
|
90
|
+
return (
|
|
91
|
+
<li key={row.id}>
|
|
92
|
+
{visible() ? <div><span>{cls}</span>{on ? <em id="yes">yes</em> : <em id="no">no</em>}</div> : <span>x</span>}
|
|
93
|
+
</li>
|
|
94
|
+
)
|
|
95
|
+
})}
|
|
96
|
+
</ul>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
`
|
|
100
|
+
const js = clientJsFor(source, 'List2.tsx')
|
|
101
|
+
|
|
102
|
+
expect(js).toMatch(
|
|
103
|
+
/insert\(__branchScope, 's\d+', \(\) => \{ const cls = 'tier-' \+ row\(\)\.tier; const on = selected\(\) === row\(\)\.id;; return \(on\) \}, \{/,
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('(c) negative control — no preamble local in play, nothing injected', () => {
|
|
108
|
+
const source = `'use client'
|
|
109
|
+
import { createSignal } from '@barefootjs/client'
|
|
110
|
+
type Row = { id: number; tier: string }
|
|
111
|
+
export function List() {
|
|
112
|
+
const [visible] = createSignal(true)
|
|
113
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tier: 'gold' }])
|
|
114
|
+
return (
|
|
115
|
+
<ul>
|
|
116
|
+
{rows().map(row => (
|
|
117
|
+
<li key={row.id}>
|
|
118
|
+
{visible() ? <span class={row.tier} id="target">{row.tier}</span> : <span>x</span>}
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
const js = clientJsFor(source, 'List3.tsx')
|
|
126
|
+
|
|
127
|
+
expect(js).not.toContain('const cls =')
|
|
128
|
+
// The attr effect still exists and still writes — just with no
|
|
129
|
+
// preamble re-run ahead of it, since there is nothing to re-run.
|
|
130
|
+
const disposerStart = js.indexOf('const __disposers = []')
|
|
131
|
+
const disposerEnd = js.indexOf('return () => __disposers.forEach')
|
|
132
|
+
const arm = js.slice(disposerStart, disposerEnd)
|
|
133
|
+
expect(arm).toContain("setAttribute('class'")
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -266,17 +266,20 @@ describe('reactive attributes inside .map() callbacks', () => {
|
|
|
266
266
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
267
267
|
|
|
268
268
|
const clientJs = result.files.find(f => f.type === 'clientJs')!.content
|
|
269
|
-
// The per-item style binding must be wired
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
269
|
+
// The per-item style binding must be wired reactively — re-reading the
|
|
270
|
+
// helper and writing the style attribute, so it tracks the `items()`
|
|
271
|
+
// signal and updates after hydration, rather than being baked into the
|
|
272
|
+
// item template once. This row has no ref/child component/inner loop,
|
|
273
|
+
// so it is lazy-eligible (#2859 follow-on): `widthAt(i)` reads the
|
|
274
|
+
// index (forcing `applyItem`) and closes over `items()` opaquely
|
|
275
|
+
// (forcing `applyOuter` too, via the re-subscribe seam), so BOTH apply
|
|
276
|
+
// bodies re-evaluate it, not a `createEffect`. Pin the helper call
|
|
277
|
+
// *inside* `applyItem` alongside the style write — `widthAt(` also
|
|
273
278
|
// appears in the static template clone, so asserting it independently
|
|
274
|
-
// could pass even if the
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
expect(
|
|
278
|
-
/createEffect\(\(\)\s*=>\s*\{[\s\S]*?widthAt\([\s\S]*?setAttribute\('style'/,
|
|
279
|
-
)
|
|
279
|
+
// could pass even if the reactive wiring were missing (the exact
|
|
280
|
+
// regression here).
|
|
281
|
+
const applyItem = clientJs.slice(clientJs.indexOf('applyItem:'), clientJs.indexOf('applyOuter:'))
|
|
282
|
+
expect(applyItem).toMatch(/widthAt\([\s\S]*?setAttribute\('style'/)
|
|
280
283
|
})
|
|
281
284
|
|
|
282
285
|
test('keyed loop: `key` prop is not emitted as a reactive DOM attribute', () => {
|
|
@@ -124,3 +124,40 @@ export { Comp }
|
|
|
124
124
|
expect(js).toContain("items.map((tag) => tag.a).join(',') + _p.tag")
|
|
125
125
|
})
|
|
126
126
|
})
|
|
127
|
+
|
|
128
|
+
// #2828 follow-up (pullfrog review on #2853): `collectBranchLocalPropRefsViaSubstitution`
|
|
129
|
+
// (jsx-to-ir.ts) is a second, independent AST walk that decides whether a
|
|
130
|
+
// branch-local variable's TRANSITIVE prop dependency gets unioned into a
|
|
131
|
+
// reactive expression's rewrite — same "is this a value reference" question
|
|
132
|
+
// #2828 fixed in `collectAstPropRefs`, but this walk had its own,
|
|
133
|
+
// un-fixed `isShorthand` exclusion. A branch-local whose own initializer
|
|
134
|
+
// reads a prop, referenced via object-literal shorthand inside a REACTIVE
|
|
135
|
+
// expression, had its prop dependency silently dropped: the emitted
|
|
136
|
+
// module-scope hydrate template lambda kept a bare, unprefixed reference
|
|
137
|
+
// to the prop name instead of `_p.<prop>`.
|
|
138
|
+
describe('branch-local transitive prop-dep via shorthand reference (#2828 follow-up)', () => {
|
|
139
|
+
test('a branch-local read from a prop, referenced via shorthand, still resolves to _p.<prop> in the hydrate template', () => {
|
|
140
|
+
const source = `
|
|
141
|
+
"use client"
|
|
142
|
+
import { createSignal } from '@barefootjs/client'
|
|
143
|
+
export function Comp({ tag }: { tag: string }) {
|
|
144
|
+
const [n] = createSignal(0)
|
|
145
|
+
if (n() > 0) {
|
|
146
|
+
const local = tag
|
|
147
|
+
return <div data-x={JSON.stringify({ local }) + n()}>{n()}</div>
|
|
148
|
+
}
|
|
149
|
+
return <div>none</div>
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
const js = compileClientJs(source)
|
|
153
|
+
// Pre-fix: the hydrate template's module-scope lambda kept a bare
|
|
154
|
+
// `tag` reference (no binding at that scope -- ReferenceError at pure
|
|
155
|
+
// CSR mount). Post-fix: it resolves through `_p.tag` like every other
|
|
156
|
+
// prop reference. (The surrounding `{ (_p.tag) }` shape -- the branch-
|
|
157
|
+
// local substitution dropping the shorthand's own key -- is a
|
|
158
|
+
// separate, pre-existing defect tracked independently; this test pins
|
|
159
|
+
// only the prop-dependency question #2828's follow-up fixed.)
|
|
160
|
+
expect(js).not.toContain('{ (tag) }')
|
|
161
|
+
expect(js).toContain('{ (_p.tag) }')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct unit tests for `spliceChildValue`'s arm order (#2795 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* The door's arm order changed during the #2795 migration:
|
|
5
|
+
* `node.joinArrayChild` is now checked BEFORE `cx.branchSlotsVar`. Before,
|
|
6
|
+
* `irToHtmlTemplate` wrapped an array in `__bfSlot` first and joined after —
|
|
7
|
+
* for an element-array child (`{out}`, built by an arbitrary `.map()`
|
|
8
|
+
* preamble) rendered INSIDE a conditional branch, that would have handed
|
|
9
|
+
* `__bfSlot` the whole array-join expression, which stringifies an array
|
|
10
|
+
* argument via `String(...)`-style coercion internally rather than joining
|
|
11
|
+
* compiled-leaf HTML — corrupting already-escaped leaf markup. Joining first
|
|
12
|
+
* (the current order) hands `__bfSlot` a single already-assembled HTML
|
|
13
|
+
* string instead.
|
|
14
|
+
*
|
|
15
|
+
* PR #2858 shipped this reorder without a test exercising the co-occurrence
|
|
16
|
+
* (join-array-child AND branch-slot at once) — this file is that test,
|
|
17
|
+
* added directly against the door function rather than a full JSX compile,
|
|
18
|
+
* since the arm order is entirely `spliceChildValue`'s own concern.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect } from 'bun:test'
|
|
22
|
+
import { spliceChildValue } from '../ir-to-client-js/safe-html'
|
|
23
|
+
|
|
24
|
+
describe('spliceChildValue arm order', () => {
|
|
25
|
+
test('joinArrayChild wins over branchSlotsVar — the array is joined, not handed to __bfSlot', () => {
|
|
26
|
+
const result = spliceChildValue(
|
|
27
|
+
{ expr: 'out', slotId: null, joinArrayChild: true },
|
|
28
|
+
'out',
|
|
29
|
+
{ branchSlotsVar: 'slots' },
|
|
30
|
+
)
|
|
31
|
+
expect(result).toBe(`Array.isArray(out) ? out.join('') : (out ?? '')`)
|
|
32
|
+
expect(result).not.toContain('__bfSlot')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('branchSlotsVar alone (no joinArrayChild) still routes through __bfSlot', () => {
|
|
36
|
+
const result = spliceChildValue(
|
|
37
|
+
{ expr: 'value', slotId: null, joinArrayChild: false },
|
|
38
|
+
'value',
|
|
39
|
+
{ branchSlotsVar: 'slots' },
|
|
40
|
+
)
|
|
41
|
+
expect(result).toBe('__bfSlot(value, slots)')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('neither joinArrayChild nor branchSlotsVar nor slotId nor children — plain escapeText default', () => {
|
|
45
|
+
const result = spliceChildValue(
|
|
46
|
+
{ expr: 'label', slotId: null, joinArrayChild: false },
|
|
47
|
+
'label',
|
|
48
|
+
{},
|
|
49
|
+
)
|
|
50
|
+
expect(result).toBe('escapeText(label)')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('a markup-claimed slotId still escapes-or-unwraps even with joinArrayChild absent', () => {
|
|
54
|
+
const result = spliceChildValue(
|
|
55
|
+
{ expr: 'title', slotId: 's0', joinArrayChild: false },
|
|
56
|
+
'(title)',
|
|
57
|
+
{ markupSlotIds: new Set(['s0']) },
|
|
58
|
+
)
|
|
59
|
+
expect(result).toBe('escapeTextOrMarkup((title))')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -499,6 +499,65 @@ describe('extractSsrDefaults', () => {
|
|
|
499
499
|
// it does not abort the whole computation.
|
|
500
500
|
expect(defaults?.x).toEqual({ value: [1, null] })
|
|
501
501
|
})
|
|
502
|
+
|
|
503
|
+
// #2813 (pullfrog nitpick on the PR fixing it): a bare local-const alias
|
|
504
|
+
// of a signal getter must be seeded under its OWN name too, alongside
|
|
505
|
+
// the getter's — a template-stash adapter's `.map()` loop over
|
|
506
|
+
// `items__alias()` looks up the stash entry by that name, which the
|
|
507
|
+
// getter-name-only seeding above never creates.
|
|
508
|
+
test('a bare local-const alias of a signal getter is seeded under the alias name too', () => {
|
|
509
|
+
const metadata = metadataFor(`
|
|
510
|
+
'use client'
|
|
511
|
+
import { createSignal } from '@barefootjs/client'
|
|
512
|
+
function C() {
|
|
513
|
+
const [items] = createSignal([1, 2, 3])
|
|
514
|
+
const items__alias = items
|
|
515
|
+
return <ul>{items__alias().map(n => <li key={n}>{n}</li>)}</ul>
|
|
516
|
+
}
|
|
517
|
+
`)
|
|
518
|
+
|
|
519
|
+
const defaults = extractSsrDefaults(metadata)
|
|
520
|
+
expect(defaults?.items).toEqual({ value: [1, 2, 3] })
|
|
521
|
+
expect(defaults?.items__alias).toEqual({ value: [1, 2, 3] })
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
test('a multi-hop local-const alias chain resolves through every link', () => {
|
|
525
|
+
const metadata = metadataFor(`
|
|
526
|
+
'use client'
|
|
527
|
+
import { createSignal } from '@barefootjs/client'
|
|
528
|
+
function C() {
|
|
529
|
+
const [items] = createSignal([1, 2, 3])
|
|
530
|
+
const hop1 = items
|
|
531
|
+
const hop2 = hop1
|
|
532
|
+
return <ul>{hop2().map(n => <li key={n}>{n}</li>)}</ul>
|
|
533
|
+
}
|
|
534
|
+
`)
|
|
535
|
+
|
|
536
|
+
const defaults = extractSsrDefaults(metadata)
|
|
537
|
+
expect(defaults?.items).toEqual({ value: [1, 2, 3] })
|
|
538
|
+
expect(defaults?.hop1).toEqual({ value: [1, 2, 3] })
|
|
539
|
+
expect(defaults?.hop2).toEqual({ value: [1, 2, 3] })
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('a getter alias shares the ORIGIN entry object, including a self-derivation-collision propName shape', () => {
|
|
543
|
+
// The origin signal's OWN name collides with the prop its initializer
|
|
544
|
+
// derives from (#2669), so its entry is a propName-carrying prop
|
|
545
|
+
// entry, not a plain `{ value }` — the alias must share that exact
|
|
546
|
+
// shape (same object reference), not re-derive a plain value entry.
|
|
547
|
+
const metadata = metadataFor(`
|
|
548
|
+
'use client'
|
|
549
|
+
import { createSignal } from '@barefootjs/client'
|
|
550
|
+
function C(props: { label?: string }) {
|
|
551
|
+
const [label] = createSignal(props.label ?? 'Default')
|
|
552
|
+
const labelAlias = label
|
|
553
|
+
return <p>{labelAlias()}</p>
|
|
554
|
+
}
|
|
555
|
+
`)
|
|
556
|
+
|
|
557
|
+
const defaults = extractSsrDefaults(metadata)
|
|
558
|
+
expect(defaults?.label).toEqual({ propName: 'label', value: null })
|
|
559
|
+
expect(defaults?.labelAlias).toBe(defaults?.label)
|
|
560
|
+
})
|
|
502
561
|
})
|
|
503
562
|
|
|
504
563
|
// TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
|