@barefootjs/jsx 0.18.5 → 0.19.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/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.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +8 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1002 -284
- package/dist/ir-to-client-js/collect-elements.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/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/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 +23 -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/phases/props-extraction.d.ts +0 -1
- package/dist/ir-to-client-js/phases/props-extraction.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 +21 -0
- package/dist/ir-to-client-js/types.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/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/client-js-generation.test.ts +40 -0
- 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__/inner-loop-svg-namespace.test.ts +245 -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__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/props-destructuring.test.ts +61 -8
- 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 +9 -0
- package/src/analyzer.ts +36 -22
- package/src/expression-parser.ts +33 -0
- package/src/index.ts +20 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -3
- 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/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/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 +53 -0
- package/src/ir-to-client-js/html-template.ts +235 -2
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/phases/props-extraction.ts +5 -9
- 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 +21 -0
- package/src/jsx-to-ir.ts +230 -39
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
|
@@ -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
|
+
})
|
|
@@ -356,21 +356,74 @@ describe('Destructured props keep their declared types (BF043 fixture #2150)', (
|
|
|
356
356
|
expect(typeOf(ctx, 'value')).toEqual({ kind: 'unknown', raw: 'unknown' })
|
|
357
357
|
})
|
|
358
358
|
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// interface{}
|
|
362
|
-
|
|
359
|
+
// #2259: optional primitives resolve type AND optionality, same as the
|
|
360
|
+
// props-object path — pre-#2259 they were skipped wholesale so typed
|
|
361
|
+
// adapters kept a nillable interface{} field; #2252's nullish-flip
|
|
362
|
+
// machinery now supplies the absent representation where it matters.
|
|
363
|
+
test('resolves OPTIONAL primitive members with optional: true (#2259)', () => {
|
|
363
364
|
const source = `
|
|
364
|
-
type Props = {
|
|
365
|
+
type Props = { label: string; size?: number; on?: boolean }
|
|
365
366
|
|
|
366
|
-
export function Component({
|
|
367
|
-
return <div>{
|
|
367
|
+
export function Component({ label, size, on }: Props) {
|
|
368
|
+
return <div>{label}{size ?? 0}{on ? 'y' : 'n'}</div>
|
|
368
369
|
}
|
|
369
370
|
`
|
|
370
371
|
|
|
371
372
|
const ctx = analyzeComponent(source, 'Component.tsx')
|
|
372
373
|
|
|
373
|
-
|
|
374
|
+
const param = (name: string) => ctx.propsParams.find((p) => p.name === name)
|
|
375
|
+
expect(param('label')).toMatchObject({
|
|
376
|
+
type: { kind: 'primitive', primitive: 'string' },
|
|
377
|
+
optional: false,
|
|
378
|
+
})
|
|
379
|
+
expect(param('size')).toMatchObject({
|
|
380
|
+
type: { kind: 'primitive', primitive: 'number' },
|
|
381
|
+
optional: true,
|
|
382
|
+
})
|
|
383
|
+
expect(param('on')).toMatchObject({
|
|
384
|
+
type: { kind: 'primitive', primitive: 'boolean' },
|
|
385
|
+
optional: true,
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// A non-primitive optional keeps `unknown` (interface{}-based lowering)
|
|
390
|
+
// but still reports the type's `?` — the adversarial catalogue derives
|
|
391
|
+
// absent points from `optional` alone.
|
|
392
|
+
test('marks OPTIONAL non-primitive members optional while keeping unknown type (#2259)', () => {
|
|
393
|
+
const source = `
|
|
394
|
+
type Todo = { id: number }
|
|
395
|
+
type Props = { items?: Todo[] }
|
|
396
|
+
|
|
397
|
+
export function Component({ items }: Props) {
|
|
398
|
+
return <div>{(items ?? []).length}</div>
|
|
399
|
+
}
|
|
400
|
+
`
|
|
401
|
+
|
|
402
|
+
const ctx = analyzeComponent(source, 'Component.tsx')
|
|
403
|
+
|
|
404
|
+
expect(ctx.propsParams.find((p) => p.name === 'items')).toMatchObject({
|
|
405
|
+
type: { kind: 'unknown', raw: 'unknown' },
|
|
406
|
+
optional: true,
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// A destructure default and the type's `?` both mean "caller may omit";
|
|
411
|
+
// the default keeps `defaultValue` so adapters bake it (and the Go
|
|
412
|
+
// nullish flip keeps excluding defaulted props).
|
|
413
|
+
test('keeps defaultValue alongside optional for `{ size = 5 }` (#2259)', () => {
|
|
414
|
+
const source = `
|
|
415
|
+
export function Component({ size = 5 }: { size?: number }) {
|
|
416
|
+
return <div>{size}</div>
|
|
417
|
+
}
|
|
418
|
+
`
|
|
419
|
+
|
|
420
|
+
const ctx = analyzeComponent(source, 'Component.tsx')
|
|
421
|
+
|
|
422
|
+
expect(ctx.propsParams.find((p) => p.name === 'size')).toMatchObject({
|
|
423
|
+
type: { kind: 'primitive', primitive: 'number' },
|
|
424
|
+
optional: true,
|
|
425
|
+
defaultValue: '5',
|
|
426
|
+
})
|
|
374
427
|
})
|
|
375
428
|
|
|
376
429
|
test('leaves NON-primitive members (arrays/objects) as unknown', () => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { evaluateSignalInit, tryEvaluateSignalInit } from '../signal-init-eval'
|
|
3
|
+
|
|
4
|
+
// #2209: replaces 7 near-duplicate regex-based `evaluateSignalInit`
|
|
5
|
+
// evaluators (one per template-string adapter's test-render.ts harness)
|
|
6
|
+
// with a single sandboxed real-JS evaluator. Absorbs and supersedes
|
|
7
|
+
// `packages/adapter-mojolicious/src/__tests__/evaluate-signal-init.test.ts`
|
|
8
|
+
// (#1672's pins) plus the actual #2209 repro shape.
|
|
9
|
+
describe('evaluateSignalInit (#2209)', () => {
|
|
10
|
+
test('the #2209 repro: (props.x ?? []).map(t => ({ ...t, editing: false }))', () => {
|
|
11
|
+
const expr = `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`
|
|
12
|
+
expect(evaluateSignalInit(expr, { initialTodos: [{ id: 1, done: false }] })).toEqual([
|
|
13
|
+
{ id: 1, done: false, editing: false },
|
|
14
|
+
])
|
|
15
|
+
expect(evaluateSignalInit(expr, undefined)).toEqual([])
|
|
16
|
+
expect(evaluateSignalInit(expr, {})).toEqual([])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('an object-spread override applies after the spread (matches JS semantics)', () => {
|
|
20
|
+
const expr = `(props.items ?? []).map(t => ({ ...t, done: true }))`
|
|
21
|
+
expect(evaluateSignalInit(expr, { items: [{ id: 1, done: false }] })).toEqual([
|
|
22
|
+
{ id: 1, done: true },
|
|
23
|
+
])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// #1672 pins, absorbed from the deleted mojolicious-local test file.
|
|
27
|
+
test('parses an inline object-array initial value', () => {
|
|
28
|
+
expect(evaluateSignalInit(`[{ id: 'a' }, { id: 'b' }, { id: 'c' }]`)).toEqual([
|
|
29
|
+
{ id: 'a' },
|
|
30
|
+
{ id: 'b' },
|
|
31
|
+
{ id: 'c' },
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('parses scalar and mixed arrays, including nested objects', () => {
|
|
36
|
+
expect(evaluateSignalInit(`['x', 'y']`)).toEqual(['x', 'y'])
|
|
37
|
+
expect(evaluateSignalInit(`[1, 2, 3]`)).toEqual([1, 2, 3])
|
|
38
|
+
expect(evaluateSignalInit(`[{ id: 'a', n: 1, ok: true }]`)).toEqual([
|
|
39
|
+
{ id: 'a', n: 1, ok: true },
|
|
40
|
+
])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('still parses scalars, empty array, and props passthrough', () => {
|
|
44
|
+
expect(evaluateSignalInit(`'b'`)).toBe('b')
|
|
45
|
+
expect(evaluateSignalInit(`5`)).toBe(5)
|
|
46
|
+
expect(evaluateSignalInit(`[]`)).toEqual([])
|
|
47
|
+
expect(evaluateSignalInit(`props.value`, { value: 42 })).toBe(42)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('bails to null for arrays with non-literal, unbound elements', () => {
|
|
51
|
+
expect(evaluateSignalInit(`[foo(), bar]`)).toBeNull()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Additional shapes the old regex evaluators supported.
|
|
55
|
+
test('props.x ?? default with x present, absent, or explicitly null', () => {
|
|
56
|
+
expect(evaluateSignalInit(`props.x ?? 7`, { x: 3 })).toBe(3)
|
|
57
|
+
expect(evaluateSignalInit(`props.x ?? 7`, {})).toBe(7)
|
|
58
|
+
expect(evaluateSignalInit(`props.x ?? 7`, undefined)).toBe(7)
|
|
59
|
+
// Fix over the old regex evaluators: JS `??` also falls through on an
|
|
60
|
+
// explicit `null`/`undefined` prop value — the old regex only checked
|
|
61
|
+
// `propName in props` and returned the raw (null) value, which was
|
|
62
|
+
// actually a bug relative to real JS `??` semantics.
|
|
63
|
+
expect(evaluateSignalInit(`props.x ?? 7`, { x: null })).toBe(7)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('negative and decimal number literals', () => {
|
|
67
|
+
expect(evaluateSignalInit(`-7.6`)).toBe(-7.6)
|
|
68
|
+
expect(evaluateSignalInit(`42`)).toBe(42)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('booleans', () => {
|
|
72
|
+
expect(evaluateSignalInit(`true`)).toBe(true)
|
|
73
|
+
expect(evaluateSignalInit(`false`)).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('an explicit null initializer maps to null (the "skip" outcome)', () => {
|
|
77
|
+
expect(evaluateSignalInit(`null`)).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// New coverage for #2209's sandboxing contract.
|
|
81
|
+
test('blocked globals fail deterministically and fall back to null', () => {
|
|
82
|
+
expect(evaluateSignalInit(`Date.now()`)).toBeNull()
|
|
83
|
+
expect(evaluateSignalInit(`Math.random()`)).toBeNull()
|
|
84
|
+
expect(evaluateSignalInit(`typeof window`)).not.toBe('object')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('a TS-only remnant that fails to parse as an expression falls back to null', () => {
|
|
88
|
+
expect(evaluateSignalInit(`foo as Bar`)).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('a non-transportable value (class instance) falls back to null', () => {
|
|
92
|
+
expect(evaluateSignalInit(`new Set([1, 2])`)).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a bare destructured-prop identifier (not props.x) is unbound and falls back to null', () => {
|
|
96
|
+
expect(evaluateSignalInit(`count`, { count: 5 })).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('tryEvaluateSignalInit distinguishes an explicit undefined from a real ok:false', () => {
|
|
100
|
+
expect(tryEvaluateSignalInit(`undefined`)).toEqual({ ok: true, value: undefined })
|
|
101
|
+
expect(tryEvaluateSignalInit(`foo()`)).toEqual({ ok: false })
|
|
102
|
+
// evaluateSignalInit's wrapper collapses both to null:
|
|
103
|
+
expect(evaluateSignalInit(`undefined`)).toBeNull()
|
|
104
|
+
expect(evaluateSignalInit(`foo()`)).toBeNull()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Copilot review (#2229): `Function` is now shadowed too — closes the
|
|
108
|
+
// most direct bypass of the blocked-globals list.
|
|
109
|
+
test('the Function constructor is shadowed', () => {
|
|
110
|
+
expect(evaluateSignalInit(`Function('return 1')()`)).toBeNull()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Copilot review (#2229): `isTransportable` must reject a sparse array
|
|
114
|
+
// (a hole is not the same as an element that's explicitly `undefined` —
|
|
115
|
+
// `Array.prototype.every` silently skips holes, which previously let
|
|
116
|
+
// one through undetected).
|
|
117
|
+
test('a sparse array (a hole, not an explicit undefined) falls back to null', () => {
|
|
118
|
+
// biome-ignore lint: intentional sparse-array literal for the hole test
|
|
119
|
+
expect(evaluateSignalInit(`[1, , 3]`)).toBeNull()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Copilot review (#2229): the same object appearing twice in a
|
|
123
|
+
// non-cyclic shape (shared by reference, not by cycle) is transportable
|
|
124
|
+
// — JSON-equivalent behavior would just duplicate it — and must NOT be
|
|
125
|
+
// rejected by an overly-global "seen" set.
|
|
126
|
+
test('a shared (non-cyclic) reference is transportable', () => {
|
|
127
|
+
expect(evaluateSignalInit(`(() => { const shared = { id: 1 }; return [shared, shared] })()`)).toEqual([
|
|
128
|
+
{ id: 1 },
|
|
129
|
+
{ id: 1 },
|
|
130
|
+
])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('a genuine cycle still falls back to null', () => {
|
|
134
|
+
expect(
|
|
135
|
+
evaluateSignalInit(`(() => { const o = { id: 1 }; o.self = o; return o })()`),
|
|
136
|
+
).toBeNull()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A FULLY-STATIC nested loop's inner `.map((item, i) => ...)` index param
|
|
3
|
+
* referenced in a child-component prop (#2231). Follow-up to #2218, which
|
|
4
|
+
* fixed the reactive (`mapArray`) and branch-arm paths — this covers the
|
|
5
|
+
* static-array child-init family (`plan/build-static-array-child-init.ts` /
|
|
6
|
+
* `stringify/static-array-child-init.ts`), which those fixes didn't reach
|
|
7
|
+
* because it never goes through `loopKeyFn` or the renderItem alias.
|
|
8
|
+
*
|
|
9
|
+
* Root cause: the `inner-loop-nested` shape hardcoded the synthetic
|
|
10
|
+
* `__innerIdx` as the inner `forEach`'s index param, and the
|
|
11
|
+
* `component-rooted-inner-loop` shape (#1725) declared no index params at
|
|
12
|
+
* all — so a prop getter like `get label() { return i }` threw
|
|
13
|
+
* `ReferenceError: i is not defined` on `initChild`'s first prop read at
|
|
14
|
+
* hydration.
|
|
15
|
+
*
|
|
16
|
+
* Fix: emit the user's declared index name directly as the `forEach` index
|
|
17
|
+
* param — the same idiom the outer loop has always used (`elem.index ||
|
|
18
|
+
* '__idx'`). No alias binding is needed because `forEach` supplies the real
|
|
19
|
+
* array index positionally. Loops that declare no index keep byte-identical
|
|
20
|
+
* output (`__innerIdx` fallback for `inner-loop-nested`, bare single-param
|
|
21
|
+
* heads for `component-rooted-inner-loop`).
|
|
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('static inner-loop index param in child-component props (#2231)', () => {
|
|
39
|
+
test('inner-loop-nested: the inner forEach binds the user index name', () => {
|
|
40
|
+
const content = clientJsFor(`
|
|
41
|
+
'use client'
|
|
42
|
+
import { Cell } from './cell'
|
|
43
|
+
const OUTER = [
|
|
44
|
+
{ id: 1, subs: [{ id: 11 }, { id: 12 }] },
|
|
45
|
+
{ id: 2, subs: [{ id: 21 }] },
|
|
46
|
+
]
|
|
47
|
+
export function Repro() {
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
{OUTER.map((o, oi) => (
|
|
51
|
+
<div key={o.id}>
|
|
52
|
+
{o.subs.map((sub, i) => (
|
|
53
|
+
<Cell key={sub.id} label={i} outerPos={oi} />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
`)
|
|
61
|
+
|
|
62
|
+
// The inner forEach's index param is the user's name, so the prop
|
|
63
|
+
// getters `return i` / `return oi` resolve.
|
|
64
|
+
expect(content).toContain('o.subs.forEach((sub, i) => {')
|
|
65
|
+
// The inner element offset indexes with the same (renamed) param.
|
|
66
|
+
expect(content).toContain('__ic.children[i]')
|
|
67
|
+
expect(content).toContain('get label() { return i }')
|
|
68
|
+
expect(content).toContain('get outerPos() { return oi }')
|
|
69
|
+
expect(content).not.toContain('__innerIdx')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('inner-loop-nested: no declared index keeps the synthetic __innerIdx (byte stability)', () => {
|
|
73
|
+
const content = clientJsFor(`
|
|
74
|
+
'use client'
|
|
75
|
+
import { Cell } from './cell'
|
|
76
|
+
const OUTER = [
|
|
77
|
+
{ id: 1, subs: [{ id: 11 }, { id: 12 }] },
|
|
78
|
+
]
|
|
79
|
+
export function Repro() {
|
|
80
|
+
return (
|
|
81
|
+
<div>
|
|
82
|
+
{OUTER.map((o) => (
|
|
83
|
+
<div key={o.id}>
|
|
84
|
+
{o.subs.map((sub) => (
|
|
85
|
+
<Cell key={sub.id} label={sub.id} />
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
`)
|
|
93
|
+
|
|
94
|
+
expect(content).toContain('o.subs.forEach((sub, __innerIdx) => {')
|
|
95
|
+
expect(content).toContain('__ic.children[__innerIdx]')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('component-rooted-inner-loop: outer AND inner index params are bound', () => {
|
|
99
|
+
const content = clientJsFor(`
|
|
100
|
+
'use client'
|
|
101
|
+
import { Card, Cell } from './cell'
|
|
102
|
+
const OUTER = [
|
|
103
|
+
{ id: 1, subs: [{ id: 11 }, { id: 12 }] },
|
|
104
|
+
]
|
|
105
|
+
export function Repro2() {
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
{OUTER.map((o, oi) => (
|
|
109
|
+
<Card key={o.id}>
|
|
110
|
+
{o.subs.map((sub, i) => (
|
|
111
|
+
<Cell key={sub.id} label={i} outerPos={oi} />
|
|
112
|
+
))}
|
|
113
|
+
</Card>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
`)
|
|
119
|
+
|
|
120
|
+
// Document-order zip: both forEach heads carry the declared index names.
|
|
121
|
+
expect(content).toContain('OUTER.forEach((o, oi) => {')
|
|
122
|
+
expect(content).toContain('o.subs.forEach((sub, i) => {')
|
|
123
|
+
expect(content).toContain('get label() { return i }')
|
|
124
|
+
expect(content).toContain('get outerPos() { return oi }')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('component-rooted-inner-loop: no declared index keeps bare single-param heads (byte stability)', () => {
|
|
128
|
+
const content = clientJsFor(`
|
|
129
|
+
'use client'
|
|
130
|
+
import { Card, Cell } from './cell'
|
|
131
|
+
const OUTER = [
|
|
132
|
+
{ id: 1, subs: [{ id: 11 }] },
|
|
133
|
+
]
|
|
134
|
+
export function Repro2() {
|
|
135
|
+
return (
|
|
136
|
+
<div>
|
|
137
|
+
{OUTER.map((o) => (
|
|
138
|
+
<Card key={o.id}>
|
|
139
|
+
{o.subs.map((sub) => (
|
|
140
|
+
<Cell key={sub.id} label={sub.id} />
|
|
141
|
+
))}
|
|
142
|
+
</Card>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
`)
|
|
148
|
+
|
|
149
|
+
expect(content).toContain('OUTER.forEach((o) => {')
|
|
150
|
+
expect(content).toContain('o.subs.forEach((sub) => {')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('index named like a property access is not confused (item.i stays untouched)', () => {
|
|
154
|
+
const content = clientJsFor(`
|
|
155
|
+
'use client'
|
|
156
|
+
import { Cell } from './cell'
|
|
157
|
+
const OUTER = [
|
|
158
|
+
{ id: 1, subs: [{ id: 11, i: 'a' }] },
|
|
159
|
+
]
|
|
160
|
+
export function Repro() {
|
|
161
|
+
return (
|
|
162
|
+
<div>
|
|
163
|
+
{OUTER.map((o) => (
|
|
164
|
+
<div key={o.id}>
|
|
165
|
+
{o.subs.map((sub) => (
|
|
166
|
+
<Cell key={sub.id} label={sub.i} />
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
`)
|
|
174
|
+
|
|
175
|
+
// No index declared → synthetic param; the `sub.i` property access is
|
|
176
|
+
// untouched (this fix renames the forEach param, never rewrites bodies).
|
|
177
|
+
expect(content).toContain('o.subs.forEach((sub, __innerIdx) => {')
|
|
178
|
+
expect(content).toContain('get label() { return sub.i }')
|
|
179
|
+
})
|
|
180
|
+
})
|