@barefootjs/jsx 0.33.6 → 0.35.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/adapters/parsed-expr-emitter.d.ts +17 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +17 -4
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +445 -282
- 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-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.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 +2 -0
- 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/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.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/lazy-row.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.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/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/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/lowering-registry.d.ts +12 -0
- package/dist/lowering-registry.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/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 +10 -12
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- 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-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/lowering-value-position.test.ts +232 -0
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/nested-loop-index-param.test.ts +14 -3
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -2
- 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/adapters/parsed-expr-emitter.ts +43 -0
- package/src/analyzer.ts +4 -0
- package/src/expression-parser.ts +63 -29
- package/src/index.ts +10 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -2
- 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 +10 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +8 -8
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +3 -3
- 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 +110 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- 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.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/lazy-row.ts +10 -0
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +4 -1
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +58 -244
- 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/safe-html.ts +243 -0
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +115 -20
- package/src/lowering-registry.ts +21 -0
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -51,13 +51,19 @@ const armsOf = (c: LoopChildConditional) => ({
|
|
|
51
51
|
describe('analyzeLazyConditional — accepted', () => {
|
|
52
52
|
test('two wiring-free static element arms', () => {
|
|
53
53
|
const c = cond()
|
|
54
|
-
const r = analyzeLazyConditional(c,
|
|
54
|
+
const r = analyzeLazyConditional(c, armsOf(c))
|
|
55
55
|
expect(r.lazySafe).toBe(true)
|
|
56
56
|
if (r.lazySafe) {
|
|
57
57
|
expect(r.facts.whenTrueHtml).toContain('bf-c="s0"')
|
|
58
58
|
expect(r.facts.condition).toBe('row.done')
|
|
59
59
|
}
|
|
60
60
|
})
|
|
61
|
+
|
|
62
|
+
test('a condition reading the loop index is accepted (#2859 follow-up) — the caller classifies it like any other binding', () => {
|
|
63
|
+
const c = cond({ conditionFreeIdentifiers: new Set(['row', '__idx']) })
|
|
64
|
+
const r = analyzeLazyConditional(c, armsOf(c))
|
|
65
|
+
expect(r.lazySafe).toBe(true)
|
|
66
|
+
})
|
|
61
67
|
})
|
|
62
68
|
|
|
63
69
|
describe('analyzeLazyConditional — refusals', () => {
|
|
@@ -73,7 +79,7 @@ describe('analyzeLazyConditional — refusals', () => {
|
|
|
73
79
|
for (const [name, over, reason] of wiring) {
|
|
74
80
|
test(`the true arm owns ${name}`, () => {
|
|
75
81
|
const c = cond({ whenTrue: bare(over) })
|
|
76
|
-
const r = analyzeLazyConditional(c,
|
|
82
|
+
const r = analyzeLazyConditional(c, armsOf(c))
|
|
77
83
|
expect(r.lazySafe).toBe(false)
|
|
78
84
|
if (!r.lazySafe) expect(r.reason).toMatch(reason)
|
|
79
85
|
})
|
|
@@ -81,14 +87,14 @@ describe('analyzeLazyConditional — refusals', () => {
|
|
|
81
87
|
|
|
82
88
|
test('the FALSE arm is checked too, not just the true one', () => {
|
|
83
89
|
const c = cond({ whenFalse: bare({ events: [{} as never] }) })
|
|
84
|
-
const r = analyzeLazyConditional(c,
|
|
90
|
+
const r = analyzeLazyConditional(c, armsOf(c))
|
|
85
91
|
expect(r.lazySafe).toBe(false)
|
|
86
92
|
if (!r.lazySafe) expect(r.reason).toMatch(/its false arm owns events/)
|
|
87
93
|
})
|
|
88
94
|
|
|
89
95
|
test('a fragment conditional has no single node to replace', () => {
|
|
90
96
|
const c = cond()
|
|
91
|
-
const r = analyzeLazyConditional(c,
|
|
97
|
+
const r = analyzeLazyConditional(c, {
|
|
92
98
|
whenTrueHtml: asFragment('text only'),
|
|
93
99
|
whenFalseHtml: asElement(c.whenFalseHtml),
|
|
94
100
|
})
|
|
@@ -98,24 +104,18 @@ describe('analyzeLazyConditional — refusals', () => {
|
|
|
98
104
|
|
|
99
105
|
test('an arm that interpolates a value cannot be hoisted once per loop', () => {
|
|
100
106
|
const c = cond({ whenTrueHtml: '<span>${row().label}</span>' })
|
|
101
|
-
const r = analyzeLazyConditional(c,
|
|
107
|
+
const r = analyzeLazyConditional(c, armsOf(c))
|
|
102
108
|
expect(r.lazySafe).toBe(false)
|
|
103
109
|
if (!r.lazySafe) expect(r.reason).toMatch(/interpolates a value/)
|
|
104
110
|
})
|
|
105
111
|
|
|
106
112
|
test('a condition with no analyzable identifier set refuses rather than assumes', () => {
|
|
107
113
|
const c = cond({ conditionFreeIdentifiers: undefined })
|
|
108
|
-
const r = analyzeLazyConditional(c,
|
|
114
|
+
const r = analyzeLazyConditional(c, armsOf(c))
|
|
109
115
|
expect(r.lazySafe).toBe(false)
|
|
110
116
|
if (!r.lazySafe) expect(r.reason).toMatch(/no analyzable identifier set/)
|
|
111
117
|
})
|
|
112
118
|
|
|
113
|
-
test('a condition reading the loop index refuses — the apply bodies have none', () => {
|
|
114
|
-
const c = cond({ conditionFreeIdentifiers: new Set(['row', '__idx']) })
|
|
115
|
-
const r = analyzeLazyConditional(c, '__idx', armsOf(c))
|
|
116
|
-
expect(r.lazySafe).toBe(false)
|
|
117
|
-
if (!r.lazySafe) expect(r.reason).toMatch(/reads the loop index parameter '__idx'/)
|
|
118
|
-
})
|
|
119
119
|
})
|
|
120
120
|
|
|
121
121
|
// --- emission ---------------------------------------------------------------
|
|
@@ -175,6 +175,46 @@ describe('lazy row emission — wiring-free row conditional', () => {
|
|
|
175
175
|
})
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
+
describe('lazy row emission — a conditional reading the loop index (#2859 follow-up)', () => {
|
|
179
|
+
// Mixed item+index condition — a PURE index-only reactive condition/attr is
|
|
180
|
+
// a separate, pre-existing, broader gap (the analyzer never classifies an
|
|
181
|
+
// expression that reads ONLY the loop index as reactive AT ALL, in either
|
|
182
|
+
// the eager or lazy path — filed separately, not this widening's job to
|
|
183
|
+
// fix). `row.done` is what makes the analyzer treat this condition as
|
|
184
|
+
// reactive in the first place; `i % 2 === 0` then rides along, which is
|
|
185
|
+
// exactly what this widening needs to prove.
|
|
186
|
+
const source = `
|
|
187
|
+
'use client'
|
|
188
|
+
import { createSignal } from '@barefootjs/client'
|
|
189
|
+
type Row = { id: number; label: string; done: boolean }
|
|
190
|
+
export function CondRowsIndex(props: { rows: Row[] }) {
|
|
191
|
+
const [rows, setRows] = createSignal<Row[]>(props.rows)
|
|
192
|
+
return (
|
|
193
|
+
<ul>
|
|
194
|
+
{rows().map((row, i) => (
|
|
195
|
+
<li key={row.id}>{(row.done || i % 2 === 0) ? <span class="even">even</span> : <em class="odd">odd</em>}</li>
|
|
196
|
+
))}
|
|
197
|
+
</ul>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
`
|
|
201
|
+
const js = clientJs(source, 'CondRowsIndex.tsx')
|
|
202
|
+
|
|
203
|
+
test('the loop is still lazy — the index reference no longer forces eager', () => {
|
|
204
|
+
expect(js).toContain('mapArrayLazy(')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('the condition is emitted into applyItem, reading the live entry.index', () => {
|
|
208
|
+
const applyItem = js.slice(js.indexOf('applyItem:'), js.indexOf("}, 'l0')"))
|
|
209
|
+
expect(applyItem).toMatch(/const i = __e\.index/)
|
|
210
|
+
expect(applyItem).toContain('i % 2 === 0')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('the plan is marked indexDriven so a pure reorder re-evaluates it', () => {
|
|
214
|
+
expect(js).toMatch(/indexDriven:\s*true/)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
178
218
|
describe('lazy row emission — conditionals that keep the eager path', () => {
|
|
179
219
|
const cases: Array<[string, string]> = [
|
|
180
220
|
['an arm with an event', '{row.done ? <button onClick={() => {}}>x</button> : <em>open</em>}'],
|
|
@@ -33,11 +33,11 @@ const SIGNALS: ReadonlySet<string> = new Set(['selected', 'filter'])
|
|
|
33
33
|
|
|
34
34
|
describe('analyzeLazyPreamble — accepted', () => {
|
|
35
35
|
test('no preamble at all', () => {
|
|
36
|
-
expect(analyzeLazyPreamble(undefined,
|
|
36
|
+
expect(analyzeLazyPreamble(undefined, SIGNALS).lazySafe).toBe(true)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
test('const declarations reading only the item', () => {
|
|
40
|
-
const r = analyzeLazyPreamble(preamble("const full = `${row.a} ${row.b}`;", ['full']),
|
|
40
|
+
const r = analyzeLazyPreamble(preamble("const full = `${row.a} ${row.b}`;", ['full']), SIGNALS)
|
|
41
41
|
expect(r.lazySafe).toBe(true)
|
|
42
42
|
if (r.lazySafe) expect([...r.facts.declaredNames]).toEqual(['full'])
|
|
43
43
|
})
|
|
@@ -45,17 +45,22 @@ describe('analyzeLazyPreamble — accepted', () => {
|
|
|
45
45
|
test('a zero-arg signal read is allowed — the krausest shape', () => {
|
|
46
46
|
const r = analyzeLazyPreamble(
|
|
47
47
|
preamble("const cls = selected() === row.id ? 'danger' : '';", ['cls']),
|
|
48
|
-
'__idx',
|
|
49
48
|
SIGNALS,
|
|
50
49
|
)
|
|
51
50
|
expect(r.lazySafe).toBe(true)
|
|
52
51
|
})
|
|
53
52
|
|
|
54
53
|
test('a destructuring const contributes every bound name, not the property keys', () => {
|
|
55
|
-
const r = analyzeLazyPreamble(preamble('const { a, b: [c] } = row;', ['a', 'c']),
|
|
54
|
+
const r = analyzeLazyPreamble(preamble('const { a, b: [c] } = row;', ['a', 'c']), SIGNALS)
|
|
56
55
|
expect(r.lazySafe).toBe(true)
|
|
57
56
|
if (r.lazySafe) expect([...r.facts.declaredNames].sort()).toEqual(['a', 'c'])
|
|
58
57
|
})
|
|
58
|
+
|
|
59
|
+
test('a preamble reading the loop index is accepted (#2859 follow-up) — the name survives into freeNames', () => {
|
|
60
|
+
const r = analyzeLazyPreamble(preamble('const n = __idx + 1;', ['n']), SIGNALS)
|
|
61
|
+
expect(r.lazySafe).toBe(true)
|
|
62
|
+
if (r.lazySafe) expect(r.facts.freeNames.has('__idx')).toBe(true)
|
|
63
|
+
})
|
|
59
64
|
})
|
|
60
65
|
|
|
61
66
|
describe('analyzeLazyPreamble — refusals', () => {
|
|
@@ -112,16 +117,11 @@ describe('analyzeLazyPreamble — refusals', () => {
|
|
|
112
117
|
preamble('const selected = row.sel;', ['selected']),
|
|
113
118
|
/shadows the signal\/memo getter 'selected'/,
|
|
114
119
|
],
|
|
115
|
-
[
|
|
116
|
-
'a preamble reading the loop index',
|
|
117
|
-
preamble('const n = __idx + 1;', ['n']),
|
|
118
|
-
/reads the loop index parameter '__idx'/,
|
|
119
|
-
],
|
|
120
120
|
]
|
|
121
121
|
|
|
122
122
|
for (const [name, p, reason] of cases) {
|
|
123
123
|
test(name, () => {
|
|
124
|
-
const r = analyzeLazyPreamble(p,
|
|
124
|
+
const r = analyzeLazyPreamble(p, SIGNALS)
|
|
125
125
|
expect(r.lazySafe).toBe(false)
|
|
126
126
|
if (!r.lazySafe) expect(r.reason).toMatch(reason)
|
|
127
127
|
})
|
|
@@ -134,7 +134,7 @@ describe('analyzeLazyPreamble — refusals', () => {
|
|
|
134
134
|
declaredNames: [],
|
|
135
135
|
builderNames: [],
|
|
136
136
|
}
|
|
137
|
-
const r = analyzeLazyPreamble(p,
|
|
137
|
+
const r = analyzeLazyPreamble(p, SIGNALS)
|
|
138
138
|
expect(r.lazySafe).toBe(false)
|
|
139
139
|
if (!r.lazySafe) expect(r.reason).toMatch(/JSX leaf/)
|
|
140
140
|
})
|
|
@@ -152,12 +152,17 @@ describe('lazyRowEligibility — profile mode', () => {
|
|
|
152
152
|
})
|
|
153
153
|
|
|
154
154
|
describe('lazyRowEligibility — binding refusals', () => {
|
|
155
|
-
|
|
155
|
+
// #2859 follow-up, LIFTED: a binding referencing the loop index no longer
|
|
156
|
+
// sinks the loop. `classifyLazyBinding` forces `readsItem: true` for it
|
|
157
|
+
// (the row's position is delivered on `entry.index` exactly like the item
|
|
158
|
+
// is on `entry.item`), so the row stays eligible and the binding lands in
|
|
159
|
+
// `applyItem`. Inverted from the refusal it replaces so a gate that starts
|
|
160
|
+
// refusing again fails here by name.
|
|
161
|
+
test('a binding referencing the loop index parameter is ELIGIBLE (index tracked via entry.index)', () => {
|
|
156
162
|
const decision = lazyRowEligibility(args({
|
|
157
163
|
bindings: [{ ...itemBinding(), referencesIndex: true }],
|
|
158
164
|
}))
|
|
159
|
-
expect(decision.eligible).toBe(
|
|
160
|
-
expect((decision as { reason: string }).reason).toMatch(/index parameter/)
|
|
165
|
+
expect(decision.eligible).toBe(true)
|
|
161
166
|
})
|
|
162
167
|
|
|
163
168
|
// §9.3a, LIFTED: an outer name the emitter cannot prime no longer sinks
|
|
@@ -36,6 +36,23 @@
|
|
|
36
36
|
* `collectLoopChildBindings` / `collectLoopChildConditionals` /
|
|
37
37
|
* `summarizeLoopChildBranch` / `collectLoopChildReactiveAttrs` /
|
|
38
38
|
* `collectLoopChildReactiveTexts` into `buildLoopRowScope`.
|
|
39
|
+
*
|
|
40
|
+
* #2859: `i` is now ALSO rewritten to `i()` (`wrapLoopParamAsAccessor`,
|
|
41
|
+
* `ir-to-client-js/utils.ts`) on the EAGER path — `mapArray` hands
|
|
42
|
+
* `renderItem` an index ACCESSOR (mirroring the item accessor) and pushes
|
|
43
|
+
* each row's current position through it on every same-key reconcile, so
|
|
44
|
+
* a row surviving a reorder keeps reading its live index instead of the
|
|
45
|
+
* value frozen at creation. The #2595 fix above only prevented the
|
|
46
|
+
* module-const shadow; it still left `i` reading a plain, never-updated
|
|
47
|
+
* closure variable.
|
|
48
|
+
*
|
|
49
|
+
* #2859 follow-on (lazy-row widening): this exact shape has no ref/child
|
|
50
|
+
* component/inner loop, so it is now LAZY-eligible — the index reference
|
|
51
|
+
* no longer forces eager on its own. On the lazy path there is no `i()`
|
|
52
|
+
* accessor call at all: `applyItem` binds `const i = __e.index` (a plain
|
|
53
|
+
* value read off the reconciler-tracked entry) and the attribute reads
|
|
54
|
+
* that bare `i`, which is exactly as live across a reorder as the eager
|
|
55
|
+
* accessor was.
|
|
39
56
|
*/
|
|
40
57
|
|
|
41
58
|
import { describe, test, expect } from 'bun:test'
|
|
@@ -99,9 +116,14 @@ describe('loop-child reactive attr vs a loop INDEX param shadowing a module cons
|
|
|
99
116
|
}
|
|
100
117
|
`)
|
|
101
118
|
|
|
102
|
-
// The per-item attribute effect must read the row's OWN index
|
|
103
|
-
//
|
|
104
|
-
|
|
119
|
+
// The per-item attribute effect must read the row's OWN, LIVE index
|
|
120
|
+
// (#2859 follow-on: `entry.index`, tracked by the lazy-row reconciler
|
|
121
|
+
// the same way it tracks `entry.item`) — never the outer module
|
|
122
|
+
// const's baked literal, and never a frozen closure variable that
|
|
123
|
+
// would go stale after a reorder.
|
|
124
|
+
expect(js).toContain('const i = __e.index')
|
|
125
|
+
expect(js).toContain('const __x = i')
|
|
105
126
|
expect(js).not.toContain("const __v = 'MODULE_CONST';")
|
|
127
|
+
expect(js).not.toContain("const __x = 'MODULE_CONST'")
|
|
106
128
|
})
|
|
107
129
|
})
|
|
@@ -71,9 +71,11 @@ describe('#1665 — keyless whole-item conditional emits valid JS (BF023)', () =
|
|
|
71
71
|
const { js, errorCodes } = clientJsFor('sel() === t.id && <li>{t.id}</li>', /* withKey */ false)
|
|
72
72
|
expect(errorCodes).toContain(ErrorCodes.MISSING_KEY_IN_LIST)
|
|
73
73
|
// No empty template-literal interpolation, and the anchor falls back to the
|
|
74
|
-
// iteration index so the comment value is well-formed.
|
|
74
|
+
// iteration index so the comment value is well-formed. `__idx` is bound to
|
|
75
|
+
// an index ACCESSOR at runtime (#2859), so the fallback calls it like
|
|
76
|
+
// every other reference to the loop's index parameter.
|
|
75
77
|
expect(js).not.toContain('${}')
|
|
76
|
-
expect(js).toContain('bf-loop-i:${__idx}')
|
|
78
|
+
expect(js).toContain('bf-loop-i:${__idx()}')
|
|
77
79
|
// The whole client module must parse.
|
|
78
80
|
const body = js.replace(/^import[^\n]*\n/gm, '').replace(/^export /gm, '')
|
|
79
81
|
expect(() => new Function(body)).not.toThrow()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered-lowering recognition at ANY value position (#2843), not only
|
|
3
|
+
* a call's own top-level entry point. Two independent seams share this
|
|
4
|
+
* shared-layer test:
|
|
5
|
+
*
|
|
6
|
+
* - the SUPPORT GATE (`isSupported`/`isSupportedValue`, `expression-parser.ts`),
|
|
7
|
+
* which must admit a matched call's own params — even an `object-literal`,
|
|
8
|
+
* normally refused at `rendered` position — wherever it's nested
|
|
9
|
+
* (a ternary branch, a template-literal interpolation, a binary operand),
|
|
10
|
+
* by re-checking the neutral node's `loweringNodeChildren` instead of the
|
|
11
|
+
* call's raw `args`;
|
|
12
|
+
* - the shared `emitParsedExpr` DISPATCHER (`parsed-expr-emitter.ts`), whose
|
|
13
|
+
* `call` case now tries an emitter's optional `lowering` seam before its
|
|
14
|
+
* ordinary `call()`/`callbackMethod` dispatch, so an adapter that adopts
|
|
15
|
+
* it renders the matched node correctly at any nesting depth too.
|
|
16
|
+
*
|
|
17
|
+
* Uses a standalone SAMPLE plugin (independent of `queryHref`) so these tests
|
|
18
|
+
* exercise the mechanism itself, mirroring `lowering-registry.test.ts`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect, afterEach } from 'bun:test'
|
|
22
|
+
import {
|
|
23
|
+
registerLoweringPlugin,
|
|
24
|
+
getLoweringPlugins,
|
|
25
|
+
prepareLoweringMatchers,
|
|
26
|
+
__resetLoweringPluginsForTest,
|
|
27
|
+
isSupported,
|
|
28
|
+
isSupportedValue,
|
|
29
|
+
emitParsedExpr,
|
|
30
|
+
type LoweringPlugin,
|
|
31
|
+
type LoweringEmitter,
|
|
32
|
+
type ParsedExprEmitter,
|
|
33
|
+
type IRMetadata,
|
|
34
|
+
type ParsedExpr,
|
|
35
|
+
} from '../index'
|
|
36
|
+
|
|
37
|
+
// A minimal sample plugin: active only when the component imports anything
|
|
38
|
+
// from `@sample/pkg`, lowering `demo(base, { … })` to a neutral `guard-list`
|
|
39
|
+
// on the `query` helper — the exact shape `queryHref` produces, so these
|
|
40
|
+
// tests exercise the same object-literal-admission path #2743/#2841 needed.
|
|
41
|
+
const samplePlugin: LoweringPlugin = {
|
|
42
|
+
name: 'sample-value-position-demo',
|
|
43
|
+
prepare(metadata) {
|
|
44
|
+
const active = metadata.imports.some(i => i.source === '@sample/pkg' && !i.isTypeOnly)
|
|
45
|
+
if (!active) return null
|
|
46
|
+
return (callee, args) => {
|
|
47
|
+
if (callee.kind !== 'identifier' || callee.name !== 'demo') return null
|
|
48
|
+
const [base, obj] = args
|
|
49
|
+
if (!base || obj?.kind !== 'object-literal') return null
|
|
50
|
+
return {
|
|
51
|
+
kind: 'guard-list',
|
|
52
|
+
helper: 'query',
|
|
53
|
+
base,
|
|
54
|
+
triples: obj.properties
|
|
55
|
+
.filter((p): p is Extract<typeof p, { kind: 'prop' }> => p.kind === 'prop')
|
|
56
|
+
.map(p => ({ guard: null, key: p.key, value: p.value })),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function metadataImporting(source: string): IRMetadata {
|
|
63
|
+
return { imports: [{ source, isTypeOnly: false, specifiers: [] }] } as unknown as IRMetadata
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
const remaining = getLoweringPlugins().filter(p => p.name !== 'sample-value-position-demo')
|
|
68
|
+
__resetLoweringPluginsForTest(remaining)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const base = { kind: 'literal', value: '/x', literalType: 'string' } as ParsedExpr
|
|
72
|
+
const tagValue = { kind: 'literal', value: 'a', literalType: 'string' } as ParsedExpr
|
|
73
|
+
const paramsObj = {
|
|
74
|
+
kind: 'object-literal',
|
|
75
|
+
raw: '{ tag: "a" }',
|
|
76
|
+
properties: [{ kind: 'prop', key: 'tag', shorthand: false, value: tagValue }],
|
|
77
|
+
} as ParsedExpr
|
|
78
|
+
const demoCall = { kind: 'call', callee: { kind: 'identifier', name: 'demo' }, args: [base, paramsObj] } as ParsedExpr
|
|
79
|
+
|
|
80
|
+
function matchers(): ReturnType<typeof prepareLoweringMatchers> {
|
|
81
|
+
registerLoweringPlugin(samplePlugin)
|
|
82
|
+
return prepareLoweringMatchers(metadataImporting('@sample/pkg'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('isSupported / isSupportedValue with loweringMatchers (#2843)', () => {
|
|
86
|
+
test('a matched call nested in a conditional branch is admitted at rendered position', () => {
|
|
87
|
+
const ms = matchers()
|
|
88
|
+
const cond: ParsedExpr = {
|
|
89
|
+
kind: 'conditional',
|
|
90
|
+
test: { kind: 'identifier', name: 'ok' },
|
|
91
|
+
consequent: demoCall,
|
|
92
|
+
alternate: { kind: 'literal', value: '/fallback', literalType: 'string' },
|
|
93
|
+
}
|
|
94
|
+
expect(isSupported(cond, { loweringMatchers: ms }).supported).toBe(true)
|
|
95
|
+
// Without matchers, the same tree is refused (the object-literal arg).
|
|
96
|
+
expect(isSupported(cond).supported).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('a matched call nested in a logical operand is admitted', () => {
|
|
100
|
+
const ms = matchers()
|
|
101
|
+
const logical: ParsedExpr = {
|
|
102
|
+
kind: 'logical',
|
|
103
|
+
op: '||',
|
|
104
|
+
left: demoCall,
|
|
105
|
+
right: { kind: 'literal', value: '/fallback', literalType: 'string' },
|
|
106
|
+
}
|
|
107
|
+
expect(isSupported(logical, { loweringMatchers: ms }).supported).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a matched call nested in a template-literal interpolation is admitted', () => {
|
|
111
|
+
const ms = matchers()
|
|
112
|
+
const tmpl: ParsedExpr = {
|
|
113
|
+
kind: 'template-literal',
|
|
114
|
+
parts: [
|
|
115
|
+
{ type: 'string', value: 'pre ' },
|
|
116
|
+
{ type: 'expression', expr: demoCall },
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
expect(isSupported(tmpl, { loweringMatchers: ms }).supported).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('an unmatched call inside the same shapes still refuses (no regression)', () => {
|
|
123
|
+
const ms = matchers()
|
|
124
|
+
const unmatchedCall: ParsedExpr = {
|
|
125
|
+
kind: 'call',
|
|
126
|
+
callee: { kind: 'identifier', name: 'other' },
|
|
127
|
+
args: [base, paramsObj],
|
|
128
|
+
}
|
|
129
|
+
const cond: ParsedExpr = {
|
|
130
|
+
kind: 'conditional',
|
|
131
|
+
test: { kind: 'identifier', name: 'ok' },
|
|
132
|
+
consequent: unmatchedCall,
|
|
133
|
+
alternate: { kind: 'literal', value: '/fallback', literalType: 'string' },
|
|
134
|
+
}
|
|
135
|
+
expect(isSupported(cond, { loweringMatchers: ms }).supported).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('a matched call whose own child is unsupported still refuses, loudly (sound-or-loud)', () => {
|
|
139
|
+
const ms = matchers()
|
|
140
|
+
// The value of the one triple is itself an unsupported node (a bare
|
|
141
|
+
// arrow function) — the plugin's node still carries it as a child, so
|
|
142
|
+
// it must still be checked, not silently passed through.
|
|
143
|
+
const arrow: ParsedExpr = { kind: 'arrow', params: ['x'], body: { kind: 'identifier', name: 'x' } }
|
|
144
|
+
const badParams = {
|
|
145
|
+
kind: 'object-literal',
|
|
146
|
+
raw: '{ tag: (x) => x }',
|
|
147
|
+
properties: [{ kind: 'prop', key: 'tag', shorthand: false, value: arrow }],
|
|
148
|
+
} as ParsedExpr
|
|
149
|
+
const badCall: ParsedExpr = {
|
|
150
|
+
kind: 'call',
|
|
151
|
+
callee: { kind: 'identifier', name: 'demo' },
|
|
152
|
+
args: [base, badParams],
|
|
153
|
+
}
|
|
154
|
+
expect(isSupported(badCall, { loweringMatchers: ms }).supported).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('isSupportedValue behaves the same way as isSupported for a matched nested call', () => {
|
|
158
|
+
const ms = matchers()
|
|
159
|
+
const cond: ParsedExpr = {
|
|
160
|
+
kind: 'conditional',
|
|
161
|
+
test: { kind: 'identifier', name: 'ok' },
|
|
162
|
+
consequent: demoCall,
|
|
163
|
+
alternate: { kind: 'literal', value: '/fallback', literalType: 'string' },
|
|
164
|
+
}
|
|
165
|
+
expect(isSupportedValue(cond, { loweringMatchers: ms }).supported).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('emitParsedExpr dispatch with an emitter.lowering seam (#2843)', () => {
|
|
170
|
+
// A stub emitter exercising only what the `call` case needs; every other
|
|
171
|
+
// method throws so an accidental fallthrough fails loudly in the test.
|
|
172
|
+
function stubEmitter(lowering?: LoweringEmitter): ParsedExprEmitter {
|
|
173
|
+
const unimplemented = (name: string) => () => {
|
|
174
|
+
throw new Error(`unexpected emitter.${name} call in this test`)
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
identifier: (name: string) => `ID(${name})`,
|
|
178
|
+
literal: (v) => `LIT(${JSON.stringify(v)})`,
|
|
179
|
+
call: (callee, args, emit) => `CALL(${emit(callee)}, ${args.map(emit).join(', ')})`,
|
|
180
|
+
member: unimplemented('member'),
|
|
181
|
+
indexAccess: unimplemented('indexAccess'),
|
|
182
|
+
binary: unimplemented('binary'),
|
|
183
|
+
unary: unimplemented('unary'),
|
|
184
|
+
logical: unimplemented('logical'),
|
|
185
|
+
conditional: unimplemented('conditional'),
|
|
186
|
+
templateLiteral: unimplemented('templateLiteral'),
|
|
187
|
+
callbackMethod: unimplemented('callbackMethod'),
|
|
188
|
+
arrow: unimplemented('arrow'),
|
|
189
|
+
regex: unimplemented('regex'),
|
|
190
|
+
arrayLiteral: unimplemented('arrayLiteral'),
|
|
191
|
+
objectLiteral: (_props, raw) => `OBJ(${raw})`,
|
|
192
|
+
arrayMethod: unimplemented('arrayMethod'),
|
|
193
|
+
flatMethod: unimplemented('flatMethod'),
|
|
194
|
+
unsupported: (raw) => `UNSUPPORTED(${raw})`,
|
|
195
|
+
lowering,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
test('a matched call renders via emitter.lowering.render, not the ordinary call() dispatch', () => {
|
|
200
|
+
const ms = matchers()
|
|
201
|
+
const lowering: LoweringEmitter = {
|
|
202
|
+
matchers: ms,
|
|
203
|
+
render: (node, emit) => {
|
|
204
|
+
if (node.kind !== 'guard-list') return null
|
|
205
|
+
const parts = node.triples.map(t => `${t.key}=${emit(t.value)}`).join('&')
|
|
206
|
+
return `QUERY(${emit(node.base)}, ${parts})`
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
expect(emitParsedExpr(demoCall, stubEmitter(lowering))).toBe('QUERY(LIT("/x"), tag=LIT("a"))')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('without emitter.lowering, the same call falls through to the ordinary call() dispatch', () => {
|
|
213
|
+
expect(emitParsedExpr(demoCall, stubEmitter())).toBe('CALL(ID(demo), LIT("/x"), OBJ({ tag: "a" }))')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('lowering present but the node kind/helper has no mapping falls through to call()', () => {
|
|
217
|
+
const ms = matchers()
|
|
218
|
+
const lowering: LoweringEmitter = { matchers: ms, render: () => null }
|
|
219
|
+
expect(emitParsedExpr(demoCall, stubEmitter(lowering))).toBe('CALL(ID(demo), LIT("/x"), OBJ({ tag: "a" }))')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('an unmatched call is unaffected by an emitter.lowering seam', () => {
|
|
223
|
+
const ms = matchers()
|
|
224
|
+
const lowering: LoweringEmitter = { matchers: ms, render: () => 'SHOULD NOT BE REACHED' }
|
|
225
|
+
const unmatchedCall: ParsedExpr = {
|
|
226
|
+
kind: 'call',
|
|
227
|
+
callee: { kind: 'identifier', name: 'other' },
|
|
228
|
+
args: [base],
|
|
229
|
+
}
|
|
230
|
+
expect(emitParsedExpr(unmatchedCall, stubEmitter(lowering))).toBe('CALL(ID(other), LIT("/x"))')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
@@ -60,7 +60,7 @@ describe('.map() array-builder body — JS-runtime verbatim (Stage 3 / D4)', ()
|
|
|
60
60
|
// Byte-level parity is pinned by the map-array-builder-escaping fixture.
|
|
61
61
|
const r = compile(arrayBuilder, false)
|
|
62
62
|
const cj = r.files.find(f => f.type === 'clientJs')!.content
|
|
63
|
-
expect(cj).toMatch(/out\.push\(`<td>\$\{escapeText\(
|
|
63
|
+
expect(cj).toMatch(/out\.push\(`<td>\$\{escapeText\(c\)\}<\/td>`\)/)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
test('the element-array child {out} is joined into the row', () => {
|
|
@@ -63,7 +63,11 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
63
63
|
// The renderItem body binds the alias before using it.
|
|
64
64
|
expect(content).toContain('const i = __innerIdx')
|
|
65
65
|
const aliasIdx = content.indexOf('const i = __innerIdx')
|
|
66
|
-
|
|
66
|
+
// `i` is bound to an index ACCESSOR at runtime (#2859) — `mapArray` hands
|
|
67
|
+
// every renderItem an index accessor (mirroring the item accessor) and
|
|
68
|
+
// pushes the row's current position through it on reorder, so `i()` reads
|
|
69
|
+
// the live value instead of a plain number frozen at row creation.
|
|
70
|
+
const setAttrIdx = content.indexOf(`setAttribute('data-key-1', String(i()))`)
|
|
67
71
|
expect(aliasIdx).toBeGreaterThanOrEqual(0)
|
|
68
72
|
expect(setAttrIdx).toBeGreaterThan(aliasIdx)
|
|
69
73
|
// The alias must land before the clone IIFE too — the cloned template's
|
|
@@ -156,7 +160,12 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
156
160
|
`)
|
|
157
161
|
|
|
158
162
|
expect(content).toContain('const i = __innerIdx')
|
|
159
|
-
|
|
163
|
+
// A direct (non-delegated) listener on a nested-loop row reads the row's
|
|
164
|
+
// LIVE index accessor (#2859) — unlike the top-level delegated click
|
|
165
|
+
// dispatcher (`event-delegation-index-param.test.ts`), this handler is
|
|
166
|
+
// attached once at row creation, so without this it would keep firing
|
|
167
|
+
// with the index the row was first created at after a same-key reorder.
|
|
168
|
+
expect(content).toContain('setClicked(i())')
|
|
160
169
|
})
|
|
161
170
|
|
|
162
171
|
test('byte-stability: index declared but never referenced emits no alias', () => {
|
|
@@ -278,7 +287,9 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
278
287
|
expect(content).toContain('(item, i) => String(i)')
|
|
279
288
|
expect(content).toContain('const i = __bidx')
|
|
280
289
|
const aliasIdx = content.indexOf('const i = __bidx')
|
|
281
|
-
|
|
290
|
+
// `i` reads the live index accessor (#2859) — see the sibling assertion
|
|
291
|
+
// in the 'key={i}' test above.
|
|
292
|
+
const setAttrIdx = content.indexOf(`setAttribute('data-key-1', String(i()))`)
|
|
282
293
|
expect(aliasIdx).toBeGreaterThanOrEqual(0)
|
|
283
294
|
expect(setAttrIdx).toBeGreaterThan(aliasIdx)
|
|
284
295
|
})
|
|
@@ -394,8 +394,10 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
394
394
|
// The helper call must appear inside a createEffect alongside the
|
|
395
395
|
// textContent write — `labelAt(` also appears in the static template
|
|
396
396
|
// clone, so asserting it independently would pass even with the
|
|
397
|
-
// effect missing (the exact regression here).
|
|
398
|
-
|
|
397
|
+
// effect missing (the exact regression here). `pi` reads the row's
|
|
398
|
+
// live index accessor (#2859), so `labelAt(pi())` also stays correct
|
|
399
|
+
// if this innermost loop's rows ever survive a same-key reorder.
|
|
400
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?__bfw_\w+\('s\d+', String\(labelAt\(pi\(\)\)\)\)/)
|
|
399
401
|
expect(content).toContain("setAttribute('class'")
|
|
400
402
|
})
|
|
401
403
|
})
|
|
@@ -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
|
+
})
|