@barefootjs/jsx 0.18.4 → 0.18.7
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/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- 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 +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.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-inner-loop.d.ts.map +1 -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/event-delegation.d.ts +10 -0
- 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-child-arm.d.ts +9 -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 +9 -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 +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- 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/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- 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 +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- 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/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { analyzeComponent } from '../analyzer'
|
|
3
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
4
|
+
import { asCallbackMethodCall, isSupported } from '../expression-parser'
|
|
5
|
+
import type { IRExpression } from '../types'
|
|
6
|
+
|
|
7
|
+
// #2206: a bare-identifier callback passed to a value-position higher-order
|
|
8
|
+
// array method (`tags.map(format)`) resolves one hop to its declaration
|
|
9
|
+
// (`resolveCallbackMethodFunctionReferences` in jsx-to-ir.ts), reusing the
|
|
10
|
+
// SAME `findLocalConst` / `findLocalFunction` scope machinery #2090
|
|
11
|
+
// established for `.sort(cb)` — but as a post-parse pass over the whole
|
|
12
|
+
// `ParsedExpr` tree, since the value-returning `.map(cb).join(...)` form is
|
|
13
|
+
// recognized generically downstream of parsing (`asCallbackMethodCall`), not
|
|
14
|
+
// during it.
|
|
15
|
+
describe('function-reference .map(cb) callback (#2206)', () => {
|
|
16
|
+
function textExpr(ir: ReturnType<typeof jsxToIR>): IRExpression {
|
|
17
|
+
if (!ir || ir.type !== 'element') throw new Error('expected a root element')
|
|
18
|
+
const expr = ir.children.find(c => c.type === 'expression')
|
|
19
|
+
if (!expr || expr.type !== 'expression') throw new Error('expected an expression child')
|
|
20
|
+
return expr
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('const arrow reference resolves like an inline arrow', () => {
|
|
24
|
+
const source = `
|
|
25
|
+
const format = (t: string) => '#' + t
|
|
26
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
27
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
28
|
+
}
|
|
29
|
+
export { TagLine }
|
|
30
|
+
`
|
|
31
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
32
|
+
const ir = jsxToIR(ctx)
|
|
33
|
+
const expr = textExpr(ir)
|
|
34
|
+
|
|
35
|
+
expect(expr.parsed).toBeDefined()
|
|
36
|
+
// .join(' ') is the outer call; its object is the resolved .map(...) call.
|
|
37
|
+
const joinCall = expr.parsed!
|
|
38
|
+
expect(joinCall.kind).toBe('array-method')
|
|
39
|
+
if (joinCall.kind !== 'array-method') return
|
|
40
|
+
const mapCall = asCallbackMethodCall(joinCall.object)
|
|
41
|
+
expect(mapCall).not.toBeNull()
|
|
42
|
+
expect(mapCall!.method).toBe('map')
|
|
43
|
+
expect(mapCall!.arrow.kind).toBe('arrow')
|
|
44
|
+
expect(mapCall!.arrow.params).toEqual(['t'])
|
|
45
|
+
// Resolved body is identical to what an inline `t => '#' + t` would parse to.
|
|
46
|
+
expect(isSupported(joinCall).supported).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('function-declaration reference resolves too', () => {
|
|
50
|
+
const source = `
|
|
51
|
+
function format(t: string) { return '#' + t }
|
|
52
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
53
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
54
|
+
}
|
|
55
|
+
export { TagLine }
|
|
56
|
+
`
|
|
57
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
58
|
+
const ir = jsxToIR(ctx)
|
|
59
|
+
const expr = textExpr(ir)
|
|
60
|
+
|
|
61
|
+
expect(expr.parsed).toBeDefined()
|
|
62
|
+
const joinCall = expr.parsed!
|
|
63
|
+
expect(joinCall.kind).toBe('array-method')
|
|
64
|
+
if (joinCall.kind !== 'array-method') return
|
|
65
|
+
const mapCall = asCallbackMethodCall(joinCall.object)
|
|
66
|
+
expect(mapCall).not.toBeNull()
|
|
67
|
+
expect(mapCall!.arrow.params).toEqual(['t'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('component-scope const shadows a module-scope const of the same name', () => {
|
|
71
|
+
const source = `
|
|
72
|
+
const format = (t: string) => 'module:' + t
|
|
73
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
74
|
+
const format = (t: string) => 'local:' + t
|
|
75
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
76
|
+
}
|
|
77
|
+
export { TagLine }
|
|
78
|
+
`
|
|
79
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
80
|
+
const ir = jsxToIR(ctx)
|
|
81
|
+
const expr = textExpr(ir)
|
|
82
|
+
const joinCall = expr.parsed!
|
|
83
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
84
|
+
const mapCall = asCallbackMethodCall(joinCall.object)!
|
|
85
|
+
// The resolved body must be the component-scope binding ('local:'), not
|
|
86
|
+
// the module-scope one ('module:').
|
|
87
|
+
expect(JSON.stringify(mapCall.arrow.body)).toContain('local:')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('unresolvable reference (imported) stays refused with BF101', () => {
|
|
91
|
+
const source = `
|
|
92
|
+
import { format } from './format'
|
|
93
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
94
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
95
|
+
}
|
|
96
|
+
export { TagLine }
|
|
97
|
+
`
|
|
98
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
99
|
+
const ir = jsxToIR(ctx)
|
|
100
|
+
const expr = textExpr(ir)
|
|
101
|
+
const joinCall = expr.parsed!
|
|
102
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
103
|
+
// The identifier arg passes through untouched — asCallbackMethodCall
|
|
104
|
+
// still refuses it, so the adapter's UNSUPPORTED_METHODS gate fires.
|
|
105
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
106
|
+
expect(isSupported(joinCall).supported).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('non-function const (unresolvable) stays refused with BF101', () => {
|
|
110
|
+
const source = `
|
|
111
|
+
const format = 5
|
|
112
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
113
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
114
|
+
}
|
|
115
|
+
export { TagLine }
|
|
116
|
+
`
|
|
117
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
118
|
+
const ir = jsxToIR(ctx)
|
|
119
|
+
const expr = textExpr(ir)
|
|
120
|
+
const joinCall = expr.parsed!
|
|
121
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
122
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
123
|
+
expect(isSupported(joinCall).supported).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('/* @client */ still suppresses regardless of resolvability', () => {
|
|
127
|
+
const source = `
|
|
128
|
+
import { format } from './format'
|
|
129
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
130
|
+
return <p>{/* @client */ tags.map(format).join(' ')}</p>
|
|
131
|
+
}
|
|
132
|
+
export { TagLine }
|
|
133
|
+
`
|
|
134
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
135
|
+
const ir = jsxToIR(ctx)
|
|
136
|
+
const expr = textExpr(ir)
|
|
137
|
+
expect(expr.clientOnly).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('inline arrow callback is unaffected (pre-existing #2073 shape)', () => {
|
|
141
|
+
const source = `
|
|
142
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
143
|
+
return <p>{tags.map(t => '#' + t).join(' ')}</p>
|
|
144
|
+
}
|
|
145
|
+
export { TagLine }
|
|
146
|
+
`
|
|
147
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
148
|
+
const ir = jsxToIR(ctx)
|
|
149
|
+
const expr = textExpr(ir)
|
|
150
|
+
const joinCall = expr.parsed!
|
|
151
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
152
|
+
expect(isSupported(joinCall).supported).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('resolution generalizes to other CALLBACK_METHODS (e.g. value-position .sort)', () => {
|
|
156
|
+
const source = `
|
|
157
|
+
const byLen = (a: string, b: string) => a.length - b.length
|
|
158
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
159
|
+
return <p>{tags.sort(byLen).join(' ')}</p>
|
|
160
|
+
}
|
|
161
|
+
export { TagLine }
|
|
162
|
+
`
|
|
163
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
164
|
+
const ir = jsxToIR(ctx)
|
|
165
|
+
const expr = textExpr(ir)
|
|
166
|
+
const joinCall = expr.parsed!
|
|
167
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
168
|
+
const sortCall = asCallbackMethodCall(joinCall.object)
|
|
169
|
+
expect(sortCall).not.toBeNull()
|
|
170
|
+
expect(sortCall!.arrow.params).toEqual(['a', 'b'])
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('cross-kind ambiguity (const AND function of the same name) stays refused', () => {
|
|
174
|
+
const source = `
|
|
175
|
+
const format = (t: string) => 'const:' + t
|
|
176
|
+
function format2() {}
|
|
177
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
178
|
+
function format(t: string) { return 'fn:' + t }
|
|
179
|
+
return <p>{tags.map(format).join(' ')}</p>
|
|
180
|
+
}
|
|
181
|
+
export { TagLine }
|
|
182
|
+
`
|
|
183
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
184
|
+
const ir = jsxToIR(ctx)
|
|
185
|
+
const expr = textExpr(ir)
|
|
186
|
+
const joinCall = expr.parsed!
|
|
187
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
188
|
+
// Component scope has BOTH a const `format` (module scope) and a
|
|
189
|
+
// `function format` (component scope) in play once findLocalConst /
|
|
190
|
+
// findLocalFunction search across scopes — same cross-kind ambiguity
|
|
191
|
+
// `resolveSortComparatorIdentifier` refuses for `.sort(fnref)`.
|
|
192
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
193
|
+
expect(isSupported(joinCall).supported).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('.reduce(fnref, init) resolves the callback and preserves the init arg', () => {
|
|
197
|
+
const source = `
|
|
198
|
+
const sum = (acc: number, t: string) => acc + t.length
|
|
199
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
200
|
+
return <p>{tags.reduce(sum, 0)}</p>
|
|
201
|
+
}
|
|
202
|
+
export { TagLine }
|
|
203
|
+
`
|
|
204
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
205
|
+
const ir = jsxToIR(ctx)
|
|
206
|
+
const expr = textExpr(ir)
|
|
207
|
+
const parsed = expr.parsed!
|
|
208
|
+
const reduceCall = asCallbackMethodCall(parsed)
|
|
209
|
+
expect(reduceCall).not.toBeNull()
|
|
210
|
+
expect(reduceCall!.method).toBe('reduce')
|
|
211
|
+
expect(reduceCall!.arrow.params).toEqual(['acc', 't'])
|
|
212
|
+
// The trailing `0` init arg survives resolution untouched.
|
|
213
|
+
expect(reduceCall!.args).toEqual([{ kind: 'literal', value: 0, literalType: 'number', raw: '0' }])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Fable review (#2214): a spliced-in resolved arrow's body is NOT
|
|
217
|
+
// re-walked, so a bare-identifier callback nested inside another
|
|
218
|
+
// resolved declaration stays refused — a deliberate one-hop limit (see
|
|
219
|
+
// resolveCallbackMethodFunctionReferences's docstring), pinned here so a
|
|
220
|
+
// future change to that behavior is a conscious decision, not a silent
|
|
221
|
+
// regression.
|
|
222
|
+
test('a bare-identifier callback nested inside a resolved declaration stays refused (one-hop limit)', () => {
|
|
223
|
+
const source = `
|
|
224
|
+
const inner = (t: string) => '#' + t
|
|
225
|
+
const outer = (xs: string[]) => xs.map(inner).join(',')
|
|
226
|
+
function TagLine({ rows }: { rows: string[][] }) {
|
|
227
|
+
return <p>{rows.map(outer).join(' ')}</p>
|
|
228
|
+
}
|
|
229
|
+
export { TagLine }
|
|
230
|
+
`
|
|
231
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
232
|
+
const ir = jsxToIR(ctx)
|
|
233
|
+
const expr = textExpr(ir)
|
|
234
|
+
const outerJoin = expr.parsed!
|
|
235
|
+
if (outerJoin.kind !== 'array-method') throw new Error('expected array-method')
|
|
236
|
+
const outerMap = asCallbackMethodCall(outerJoin.object)!
|
|
237
|
+
expect(outerMap).not.toBeNull()
|
|
238
|
+
// `outer`'s body resolves one hop (`xs.map(inner).join(',')`), but the
|
|
239
|
+
// nested `.map(inner)` inside that spliced body is never re-walked.
|
|
240
|
+
const innerJoin = outerMap.arrow.body
|
|
241
|
+
if (innerJoin.kind !== 'array-method') throw new Error('expected inner array-method')
|
|
242
|
+
if (innerJoin.object.kind !== 'call') throw new Error('expected inner call')
|
|
243
|
+
expect(innerJoin.object.args[0]).toEqual({ kind: 'identifier', name: 'inner' })
|
|
244
|
+
expect(asCallbackMethodCall(innerJoin.object)).toBeNull()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Fable review (#2214): the resolver must respect lexical scoping — a
|
|
248
|
+
// bare identifier in callback position that's actually bound by an
|
|
249
|
+
// ENCLOSING arrow's own parameter (not a same-file const/function) must
|
|
250
|
+
// never be resolved against the const/function tables, or SSR silently
|
|
251
|
+
// renders the wrong function while CSR (which evaluates the raw,
|
|
252
|
+
// correctly-scoped expression) renders the right one.
|
|
253
|
+
test('an enclosing callback arrow param shadows a same-named module const', () => {
|
|
254
|
+
const source = `
|
|
255
|
+
const fn = (t: string) => 'WRONG:' + t
|
|
256
|
+
function TagLine({ rows }: { rows: { tags: string[] }[] }) {
|
|
257
|
+
return <p>{rows.map(fn => fn.tags.map(fn).join(',')).join(' ')}</p>
|
|
258
|
+
}
|
|
259
|
+
export { TagLine }
|
|
260
|
+
`
|
|
261
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
262
|
+
const ir = jsxToIR(ctx)
|
|
263
|
+
const expr = textExpr(ir)
|
|
264
|
+
const outerJoin = expr.parsed!
|
|
265
|
+
if (outerJoin.kind !== 'array-method') throw new Error('expected array-method')
|
|
266
|
+
const outerMap = asCallbackMethodCall(outerJoin.object)!
|
|
267
|
+
// The outer arrow's body is `fn.tags.map(fn).join(',')` — the inner
|
|
268
|
+
// `.map(fn)` refers to the outer arrow's OWN param `fn`, not the
|
|
269
|
+
// module-scope const, so it must stay unresolved (an identifier arg).
|
|
270
|
+
const innerJoin = outerMap.arrow.body
|
|
271
|
+
if (innerJoin.kind !== 'array-method') throw new Error('expected inner array-method')
|
|
272
|
+
if (innerJoin.object.kind !== 'call') throw new Error('expected inner call')
|
|
273
|
+
expect(innerJoin.object.args[0]).toEqual({ kind: 'identifier', name: 'fn' })
|
|
274
|
+
expect(asCallbackMethodCall(innerJoin.object)).toBeNull()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('a loop item param shadows a same-named module const inside the loop body', () => {
|
|
278
|
+
const source = `
|
|
279
|
+
const format = (t: string) => 'WRONG:' + t
|
|
280
|
+
function TagLine({ fns, tags }: { fns: string[]; tags: string[] }) {
|
|
281
|
+
return <ul>{fns.map(format => <li>{tags.map(format).join(' ')}</li>)}</ul>
|
|
282
|
+
}
|
|
283
|
+
export { TagLine }
|
|
284
|
+
`
|
|
285
|
+
const ctx = analyzeComponent(source, 'TagLine.tsx')
|
|
286
|
+
const ir = jsxToIR(ctx)
|
|
287
|
+
if (!ir || ir.type !== 'element') throw new Error('expected a root element')
|
|
288
|
+
const loop = ir.children.find(c => c.type === 'loop')
|
|
289
|
+
if (!loop || loop.type !== 'loop') throw new Error('expected a loop child')
|
|
290
|
+
expect(loop.param).toBe('format')
|
|
291
|
+
const li = loop.children.find(c => c.type === 'element')
|
|
292
|
+
if (!li || li.type !== 'element') throw new Error('expected the <li> element')
|
|
293
|
+
const innerExpr = li.children.find(c => c.type === 'expression')
|
|
294
|
+
if (!innerExpr || innerExpr.type !== 'expression') throw new Error('expected an expression child')
|
|
295
|
+
const joinCall = innerExpr.parsed!
|
|
296
|
+
if (joinCall.kind !== 'array-method') throw new Error('expected array-method')
|
|
297
|
+
// `tags.map(format)` inside the loop body refers to the loop's own item
|
|
298
|
+
// variable `format`, not the module-scope const — must stay unresolved.
|
|
299
|
+
if (joinCall.object.kind !== 'call') throw new Error('expected inner call')
|
|
300
|
+
expect(joinCall.object.args[0]).toEqual({ kind: 'identifier', name: 'format' })
|
|
301
|
+
expect(asCallbackMethodCall(joinCall.object)).toBeNull()
|
|
302
|
+
})
|
|
303
|
+
})
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A NESTED (inner) `.map()` callback closing over its own `.map((item, i) =>
|
|
3
|
+
* ...)` index param (#2218). Sibling of `event-delegation-index-param.test.ts`
|
|
4
|
+
* (#2189), which fixed the same class of bug for delegated event handlers —
|
|
5
|
+
* this covers the emit path #2189 did NOT touch: the nested-loop `key`,
|
|
6
|
+
* reactive text/attr, and template-interpolation emit path in
|
|
7
|
+
* `control-flow/plan/build-inner-loop.ts` / `stringify/inner-loop.ts` (and
|
|
8
|
+
* the conditional-branch-arm sibling in `build-loop-child-arm.ts` /
|
|
9
|
+
* `stringify/loop-child-arm.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Root cause: `NestedLoop` never carried an `index` field, so `loopKeyFn`
|
|
12
|
+
* dropped the index param from the keyFn's own parameter list, and the
|
|
13
|
+
* renderItem body only ever received the index as a synthetic
|
|
14
|
+
* `__innerIdx<uid>` positional param — the user's index name (e.g. `i`) was
|
|
15
|
+
* never bound to it, so any reference threw `ReferenceError: i is not
|
|
16
|
+
* defined` and the inner loop rendered nothing.
|
|
17
|
+
*
|
|
18
|
+
* Gating (byte-for-byte stability, same philosophy as #2189): the alias
|
|
19
|
+
* `const i = __innerIdx<uid>` is only emitted when something in the inner
|
|
20
|
+
* loop body actually references the index — see `nestedLoopReferencesIndex`
|
|
21
|
+
* in `control-flow/shared.ts`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, test, expect } from 'bun:test'
|
|
25
|
+
import { compileJSX } from '../compiler'
|
|
26
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
27
|
+
|
|
28
|
+
const adapter = new TestAdapter()
|
|
29
|
+
|
|
30
|
+
function clientJsFor(source: string): string {
|
|
31
|
+
const result = compileJSX(source, 'Repro.tsx', { adapter })
|
|
32
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
33
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
34
|
+
expect(clientJs).toBeDefined()
|
|
35
|
+
return clientJs!.content
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('nested .map() index param referenced in key/text/attr (#2218)', () => {
|
|
39
|
+
test('key={i}: keyFn includes the index param and renderItem binds the alias', () => {
|
|
40
|
+
const content = clientJsFor(`
|
|
41
|
+
'use client'
|
|
42
|
+
import { createSignal } from '@barefootjs/client'
|
|
43
|
+
interface Item { id: number; text: string }
|
|
44
|
+
interface Group { id: number; items: Item[] }
|
|
45
|
+
export function Repro() {
|
|
46
|
+
const [outer] = createSignal<Group[]>([])
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
{outer().map((o) => (
|
|
50
|
+
<div key={o.id}>
|
|
51
|
+
{o.items.map((item, i) => (
|
|
52
|
+
<span key={i}>{item.text}</span>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
|
|
61
|
+
// keyFn threads the index param, mirroring TopLevelLoop/BranchLoop.
|
|
62
|
+
expect(content).toContain('(item, i) => String(i)')
|
|
63
|
+
// The renderItem body binds the alias before using it.
|
|
64
|
+
expect(content).toContain('const i = __innerIdx')
|
|
65
|
+
const aliasIdx = content.indexOf('const i = __innerIdx')
|
|
66
|
+
const setAttrIdx = content.indexOf(`setAttribute('data-key-1', String(i))`)
|
|
67
|
+
expect(aliasIdx).toBeGreaterThanOrEqual(0)
|
|
68
|
+
expect(setAttrIdx).toBeGreaterThan(aliasIdx)
|
|
69
|
+
// The alias must land before the clone IIFE too — the cloned template's
|
|
70
|
+
// `data-key-1="${i}"` interpolation reads it.
|
|
71
|
+
const cloneIdx = content.indexOf('__t.innerHTML')
|
|
72
|
+
expect(cloneIdx).toBeGreaterThan(aliasIdx)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('index referenced in a template text interpolation is bound and rendered', () => {
|
|
76
|
+
const content = clientJsFor(`
|
|
77
|
+
'use client'
|
|
78
|
+
import { createSignal } from '@barefootjs/client'
|
|
79
|
+
interface Item { id: number; text: string }
|
|
80
|
+
interface Group { id: number; items: Item[] }
|
|
81
|
+
export function Repro() {
|
|
82
|
+
const [outer] = createSignal<Group[]>([])
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
{outer().map((o) => (
|
|
86
|
+
<div key={o.id}>
|
|
87
|
+
{o.items.map((item, i) => (
|
|
88
|
+
<span key={item.id}>{i}: {item.text}</span>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
`)
|
|
96
|
+
|
|
97
|
+
expect(content).toContain('const i = __innerIdx')
|
|
98
|
+
// The template's `${i}` interpolation is emitted (escaped for text).
|
|
99
|
+
expect(content).toContain('escapeText(i)')
|
|
100
|
+
const aliasIdx = content.indexOf('const i = __innerIdx')
|
|
101
|
+
// The outer element's own clone IIFE also bakes the initial nested HTML
|
|
102
|
+
// via a plain (unrelated, arrow-scoped) `.map((item, i) => ...)` — so
|
|
103
|
+
// `escapeText(i)` appears once there too. Look for the occurrence
|
|
104
|
+
// *after* the alias, inside the inner renderItem's own clone template.
|
|
105
|
+
const templateIdx = content.indexOf('escapeText(i)', aliasIdx)
|
|
106
|
+
expect(templateIdx).toBeGreaterThan(aliasIdx)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('index referenced in a reactive attribute expression is bound', () => {
|
|
110
|
+
const content = clientJsFor(`
|
|
111
|
+
'use client'
|
|
112
|
+
import { createSignal } from '@barefootjs/client'
|
|
113
|
+
interface Item { id: number; text: string; active: boolean }
|
|
114
|
+
interface Group { id: number; items: Item[] }
|
|
115
|
+
export function Repro() {
|
|
116
|
+
const [outer] = createSignal<Group[]>([])
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
{outer().map((o) => (
|
|
120
|
+
<div key={o.id}>
|
|
121
|
+
{o.items.map((item, i) => (
|
|
122
|
+
<span key={item.id} className={i % 2 === 0 ? 'even' : 'odd'}>{item.text}</span>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
`)
|
|
130
|
+
|
|
131
|
+
expect(content).toContain('const i = __innerIdx')
|
|
132
|
+
expect(content).toContain(`i % 2 === 0 ? 'even' : 'odd'`)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('index referenced only in an event handler inside the inner loop is bound', () => {
|
|
136
|
+
const content = clientJsFor(`
|
|
137
|
+
'use client'
|
|
138
|
+
import { createSignal } from '@barefootjs/client'
|
|
139
|
+
interface Item { id: number; text: string }
|
|
140
|
+
interface Group { id: number; items: Item[] }
|
|
141
|
+
export function Repro() {
|
|
142
|
+
const [outer] = createSignal<Group[]>([])
|
|
143
|
+
const [clicked, setClicked] = createSignal(-1)
|
|
144
|
+
return (
|
|
145
|
+
<div>
|
|
146
|
+
{outer().map((o) => (
|
|
147
|
+
<div key={o.id}>
|
|
148
|
+
{o.items.map((item, i) => (
|
|
149
|
+
<span key={item.id} onClick={() => setClicked(i)}>{item.text}</span>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
`)
|
|
157
|
+
|
|
158
|
+
expect(content).toContain('const i = __innerIdx')
|
|
159
|
+
expect(content).toContain('setClicked(i)')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('byte-stability: index declared but never referenced emits no alias', () => {
|
|
163
|
+
const content = clientJsFor(`
|
|
164
|
+
'use client'
|
|
165
|
+
import { createSignal } from '@barefootjs/client'
|
|
166
|
+
interface Item { id: number; text: string }
|
|
167
|
+
interface Group { id: number; items: Item[] }
|
|
168
|
+
export function Repro() {
|
|
169
|
+
const [outer] = createSignal<Group[]>([])
|
|
170
|
+
return (
|
|
171
|
+
<div>
|
|
172
|
+
{outer().map((o) => (
|
|
173
|
+
<div key={o.id}>
|
|
174
|
+
{o.items.map((item, i) => (
|
|
175
|
+
<span key={item.id}>{item.text}</span>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
`)
|
|
183
|
+
|
|
184
|
+
expect(content).not.toContain('const i = __innerIdx')
|
|
185
|
+
// The keyFn also has no reason to thread `i` — the key doesn't use it.
|
|
186
|
+
expect(content).toContain('(item, i) => String(item.id)')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('index named like a property is not bound when only the property is used', () => {
|
|
190
|
+
// Mirrors the #2189 test of the same name: gating is AST-based (free
|
|
191
|
+
// identifiers), so `item.id` (a property access) never false-matches
|
|
192
|
+
// an index param literally named `id`.
|
|
193
|
+
const content = clientJsFor(`
|
|
194
|
+
'use client'
|
|
195
|
+
import { createSignal } from '@barefootjs/client'
|
|
196
|
+
interface Item { id: number; text: string }
|
|
197
|
+
interface Group { id: number; items: Item[] }
|
|
198
|
+
export function Repro() {
|
|
199
|
+
const [outer] = createSignal<Group[]>([])
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
{outer().map((o) => (
|
|
203
|
+
<div key={o.id}>
|
|
204
|
+
{o.items.map((item, id) => (
|
|
205
|
+
<span key={item.id}>{item.text}</span>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
`)
|
|
213
|
+
|
|
214
|
+
expect(content).not.toContain('const id = __innerIdx')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('static inner array (no signal dependency) also binds the index alias', () => {
|
|
218
|
+
// Sibling of the reactive-path test above, exercising `emitStatic` in
|
|
219
|
+
// `stringify/inner-loop.ts` (the `forEach` renderItem path used when the
|
|
220
|
+
// inner array does not reference the outer loop param reactively).
|
|
221
|
+
const content = clientJsFor(`
|
|
222
|
+
'use client'
|
|
223
|
+
import { createSignal } from '@barefootjs/client'
|
|
224
|
+
interface Item { id: number; n: number }
|
|
225
|
+
const SHARED: Item[] = [{ id: 1, n: 7 }]
|
|
226
|
+
function MyChild(props: { tag: string }) {
|
|
227
|
+
return <span>{props.tag}</span>
|
|
228
|
+
}
|
|
229
|
+
export function Repro() {
|
|
230
|
+
const [rows, setRows] = createSignal([{ id: 1, key: 'a' }])
|
|
231
|
+
return (
|
|
232
|
+
<div onClick={() => setRows(prev => [...prev])}>
|
|
233
|
+
{rows().map((row) => (
|
|
234
|
+
<div key={row.id}>
|
|
235
|
+
{SHARED.map((cell, i) => (
|
|
236
|
+
<MyChild key={cell.id} tag={\`\${i}-\${cell.n}\`} />
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
`)
|
|
244
|
+
|
|
245
|
+
expect(content).toMatch(/SHARED\.forEach\(\(cell, __innerIdx[^)]+\) => \{[\s\S]*?const\s+i\s*=\s*__innerIdx/)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('conditional-branch-arm inner loop (loop -> conditional -> inner loop) binds the index alias', () => {
|
|
249
|
+
// Exercises `build-loop-child-arm.ts::buildBranchInnerLoopsPlan` /
|
|
250
|
+
// `stringify/loop-child-arm.ts::stringifyBranchInnerLoops` — the sibling
|
|
251
|
+
// emit path for an inner loop that lives inside a reactive conditional
|
|
252
|
+
// branch which is itself inside an outer loop's item body.
|
|
253
|
+
const content = clientJsFor(`
|
|
254
|
+
'use client'
|
|
255
|
+
import { createSignal } from '@barefootjs/client'
|
|
256
|
+
interface Item { id: number; text: string }
|
|
257
|
+
interface Group { id: number; flag: boolean; items: Item[] }
|
|
258
|
+
export function Repro() {
|
|
259
|
+
const [outer] = createSignal<Group[]>([])
|
|
260
|
+
return (
|
|
261
|
+
<div>
|
|
262
|
+
{outer().map((o) => (
|
|
263
|
+
<div key={o.id}>
|
|
264
|
+
{o.flag ? (
|
|
265
|
+
<div>
|
|
266
|
+
{o.items.map((item, i) => (
|
|
267
|
+
<span key={i}>{i}: {item.text}</span>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
) : null}
|
|
271
|
+
</div>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
`)
|
|
277
|
+
|
|
278
|
+
expect(content).toContain('(item, i) => String(i)')
|
|
279
|
+
expect(content).toContain('const i = __bidx')
|
|
280
|
+
const aliasIdx = content.indexOf('const i = __bidx')
|
|
281
|
+
const setAttrIdx = content.indexOf(`setAttribute('data-key-1', String(i))`)
|
|
282
|
+
expect(aliasIdx).toBeGreaterThanOrEqual(0)
|
|
283
|
+
expect(setAttrIdx).toBeGreaterThan(aliasIdx)
|
|
284
|
+
})
|
|
285
|
+
})
|