@barefootjs/jsx 0.33.4 → 0.33.6
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/child-scope.d.ts +13 -0
- package/dist/adapters/child-scope.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1298 -1106
- package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/component-scope.d.ts +20 -0
- package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -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.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +49 -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/rewrite-props-object.d.ts +12 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +5 -2
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +49 -11
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
- package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
- package/src/__tests__/date-lowering.test.ts +18 -4
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
- package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
- package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
- package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
- package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
- package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
- package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
- package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
- package/src/__tests__/loop-item-root-scope.test.ts +7 -3
- package/src/__tests__/markup-prop-brand.test.ts +165 -26
- package/src/__tests__/namespace-import-primitive.test.ts +244 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
- package/src/adapters/child-scope.ts +23 -0
- package/src/analyzer.ts +130 -0
- package/src/errors.ts +14 -1
- package/src/index.ts +7 -0
- package/src/ir-to-client-js/child-components.ts +8 -1
- package/src/ir-to-client-js/collect-elements.ts +10 -38
- package/src/ir-to-client-js/component-scope.ts +60 -0
- package/src/ir-to-client-js/compute-inlinability.ts +46 -11
- package/src/ir-to-client-js/control-flow/shared.ts +12 -5
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
- package/src/ir-to-client-js/csr-substitute.ts +82 -16
- package/src/ir-to-client-js/emit-reactive.ts +29 -13
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +141 -70
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +34 -23
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
- package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
- package/src/jsx-to-ir.ts +200 -42
- package/src/prop-rewrite.ts +23 -5
- package/src/props-binding.ts +74 -18
- package/src/types.ts +6 -5
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2806 — a component reading its own rest-bag spread directly in JSX
|
|
3
|
+
* (`{rest.header}`, not spread onto an element's attrs) emitted a CSR
|
|
4
|
+
* template that referenced the bare, unbound `rest` identifier.
|
|
5
|
+
*
|
|
6
|
+
* The rest binding IS `_p` at runtime (`applyRestAttrs` excludes the
|
|
7
|
+
* consumed keys by name rather than constructing a narrower object, and
|
|
8
|
+
* the init body already rewrites `rest.x` → `_p.x` via
|
|
9
|
+
* `rewritePropsObjectRef`, #2723). The four CSR/static template builders
|
|
10
|
+
* in `html-template.ts` each call that same function for `propsObjectName`
|
|
11
|
+
* but were passing `null` for the rest name, so a direct rest-bag read
|
|
12
|
+
* never got the same treatment there.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, test, expect } from 'bun:test'
|
|
15
|
+
import { compileJSX } from '../compiler'
|
|
16
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
17
|
+
|
|
18
|
+
const adapter = new TestAdapter()
|
|
19
|
+
|
|
20
|
+
function clientJs(source: string): string {
|
|
21
|
+
const result = compileJSX(source, 'Repro.tsx', { adapter })
|
|
22
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
23
|
+
return result.files.find(f => f.type === 'clientJs')!.content
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('#2806 — rest-bag identifier resolves in the CSR template', () => {
|
|
27
|
+
test('a direct rest-bag read in a text expression emits `_p.x`, not bare `rest`', () => {
|
|
28
|
+
const content = clientJs(`
|
|
29
|
+
"use client";
|
|
30
|
+
import { createSignal } from '@barefootjs/client'
|
|
31
|
+
export function Card({ children, ...rest }: { children?: any; [key: string]: any }) {
|
|
32
|
+
const [cond, setCond] = createSignal(true)
|
|
33
|
+
return (
|
|
34
|
+
<section>
|
|
35
|
+
<header>{cond() ? rest.header : null}</header>
|
|
36
|
+
<div>{children}</div>
|
|
37
|
+
</section>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
`)
|
|
41
|
+
expect(content).toContain('_p.header')
|
|
42
|
+
expect(content).not.toMatch(/[^.\w]rest\.header/)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('a rest-bag read inside a `.map()` row still resolves', () => {
|
|
46
|
+
// NOTE: `rest.items ?? []` is classified `isStaticArray` here (no signal,
|
|
47
|
+
// no `isArrayExprDirectPropRef` match — see `jsx-to-ir.ts`'s `isStaticArray`
|
|
48
|
+
// derivation), so this row is emitted through `buildStaticLoopPlan`'s
|
|
49
|
+
// SSR-time-only `forEach`, NOT through the reactive `mapArray`/`mapArrayLazy`
|
|
50
|
+
// loop emitters. It does not exercise those two paths — the tests below do.
|
|
51
|
+
const content = clientJs(`
|
|
52
|
+
"use client";
|
|
53
|
+
export function Row({ children, ...rest }: { children?: any; items?: Array<{ id: number }>; [key: string]: any }) {
|
|
54
|
+
return (
|
|
55
|
+
<ul>
|
|
56
|
+
{(rest.items ?? []).map((item: { id: number }) => (
|
|
57
|
+
<li key={item.id}>{rest.suffix}</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
`)
|
|
63
|
+
expect(content).toContain('_p.suffix')
|
|
64
|
+
expect(content).not.toMatch(/[^.\w]rest\.suffix/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('a rest-bag read inside a lazy-eligible `.map()` row resolves (`mapArrayLazy`)', () => {
|
|
68
|
+
// Signal-backed source + a keyed, single-root, child-component-free row
|
|
69
|
+
// is lazy-eligible (`lazyRowEligibility`), so this hits the `createRow`/
|
|
70
|
+
// `applyOuter` bodies emitted by `stringify/lazy-row.ts` — a SEPARATE
|
|
71
|
+
// emitter from the whole-init-body rewrite the other tests here exercise
|
|
72
|
+
// only incidentally through the eager `mapArray` row body.
|
|
73
|
+
const content = clientJs(`
|
|
74
|
+
"use client";
|
|
75
|
+
import { createSignal } from '@barefootjs/client'
|
|
76
|
+
export function Row({ children, ...rest }: { children?: any; [key: string]: any }) {
|
|
77
|
+
const [items, setItems] = createSignal<{ id: number }[]>([])
|
|
78
|
+
return (
|
|
79
|
+
<ul>
|
|
80
|
+
{items().map((item: { id: number }) => (
|
|
81
|
+
<li key={item.id}>{item.id}{rest.suffix}</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
`)
|
|
87
|
+
expect(content).toContain('mapArrayLazy')
|
|
88
|
+
expect(content).toContain('_p.suffix')
|
|
89
|
+
expect(content).not.toMatch(/[^.\w]rest\.suffix/)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('a rest-bag read inside an eager `.map()` row with a child component resolves (`mapArray`)', () => {
|
|
93
|
+
// A child component in the row forces eager `mapArray` eligibility
|
|
94
|
+
// (`lazyRowEligibility`'s `hasChildComponent` gate) — the loop-row-body
|
|
95
|
+
// emission path #2812 actually fixed (the `hydrate(... template: ...)`
|
|
96
|
+
// line and the CSR/static template builders in `html-template.ts`).
|
|
97
|
+
const content = clientJs(`
|
|
98
|
+
"use client";
|
|
99
|
+
import { createSignal } from '@barefootjs/client'
|
|
100
|
+
function Item(props: { id: number }) {
|
|
101
|
+
return <span>{props.id}</span>
|
|
102
|
+
}
|
|
103
|
+
export function Row({ children, ...rest }: { children?: any; [key: string]: any }) {
|
|
104
|
+
const [items, setItems] = createSignal<{ id: number }[]>([])
|
|
105
|
+
return (
|
|
106
|
+
<ul>
|
|
107
|
+
{items().map((item: { id: number }) => (
|
|
108
|
+
<li key={item.id}><Item id={item.id} />{rest.suffix}</li>
|
|
109
|
+
))}
|
|
110
|
+
</ul>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
`)
|
|
114
|
+
expect(content).toContain('mapArray(')
|
|
115
|
+
expect(content).toContain('_p.suffix')
|
|
116
|
+
expect(content).not.toMatch(/[^.\w]rest\.suffix/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('a `.map()` row PARAMETER literally named `rest` is not rewritten (shadow guard)', () => {
|
|
120
|
+
const content = clientJs(`
|
|
121
|
+
"use client";
|
|
122
|
+
export function List({ children, ...outer }: { children?: any; [key: string]: any }) {
|
|
123
|
+
const rows = [{ id: 1, name: 'a' }]
|
|
124
|
+
return (
|
|
125
|
+
<ul>
|
|
126
|
+
{rows.map(rest => (
|
|
127
|
+
<li key={rest.id}>{rest.name}</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
`)
|
|
133
|
+
expect(content).not.toContain('_p.name')
|
|
134
|
+
expect(content).toContain('rest.name')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2835 — `isDirectPropBindingName` (jsx-to-ir.ts) checked `ctx.patterns.props`,
|
|
3
|
+
* a regex-matching set that, for a whole `(props: Props)` parameter, includes
|
|
4
|
+
* every one of `Props`'s type-member names regardless of whether that name is
|
|
5
|
+
* bound to anything in the current component (`extractPropsFromTypeMembers`,
|
|
6
|
+
* analyzer.ts — needed there so `props.<key>` still regex-matches). Reused as
|
|
7
|
+
* a "is this name a prop binding" check, that over-inclusion let an unrelated
|
|
8
|
+
* module/local identifier resolve as prop-derived purely by sharing a type
|
|
9
|
+
* member's name, with zero aliasing involved (reproduces identically on
|
|
10
|
+
* pre-#2724 `main`).
|
|
11
|
+
*
|
|
12
|
+
* Fix: `isDirectPropBindingName` now reads `ctx.boundPropNames`
|
|
13
|
+
* (`boundPropLocalNames`, `props-binding.ts`) — empty for a whole
|
|
14
|
+
* non-destructured props parameter, since `propsParams` there is type-member
|
|
15
|
+
* names, not local bindings.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, test, expect } from 'bun:test'
|
|
19
|
+
import { compileJSX } from '../compiler'
|
|
20
|
+
import { analyzeComponent } from '../analyzer'
|
|
21
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
22
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
23
|
+
import type { IRNode } from '../types'
|
|
24
|
+
|
|
25
|
+
const adapter = new TestAdapter()
|
|
26
|
+
|
|
27
|
+
function getClientJs(source: string, filename: string): string {
|
|
28
|
+
const result = compileJSX(source, filename, { adapter })
|
|
29
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
30
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
31
|
+
expect(clientJs).toBeDefined()
|
|
32
|
+
return clientJs!.content
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** First `IRLoop` node found in a DFS of the component IR. */
|
|
36
|
+
function findLoop(node: IRNode | null, predicate: (n: any) => boolean = () => true): any {
|
|
37
|
+
if (!node || typeof node !== 'object') return undefined
|
|
38
|
+
if ((node as any).type === 'loop' && predicate(node)) return node
|
|
39
|
+
for (const key of Object.keys(node)) {
|
|
40
|
+
const v = (node as any)[key]
|
|
41
|
+
if (Array.isArray(v)) {
|
|
42
|
+
for (const c of v) {
|
|
43
|
+
const found = findLoop(c, predicate)
|
|
44
|
+
if (found) return found
|
|
45
|
+
}
|
|
46
|
+
} else if (v && typeof v === 'object') {
|
|
47
|
+
const found = findLoop(v, predicate)
|
|
48
|
+
if (found) return found
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** First element node with a non-null `slotId`, found in a DFS of the IR. */
|
|
55
|
+
function findSlottedElement(node: IRNode | null): any {
|
|
56
|
+
if (!node || typeof node !== 'object') return undefined
|
|
57
|
+
if ((node as any).type === 'element' && (node as any).slotId) return node
|
|
58
|
+
for (const key of Object.keys(node)) {
|
|
59
|
+
const v = (node as any)[key]
|
|
60
|
+
if (Array.isArray(v)) {
|
|
61
|
+
for (const c of v) {
|
|
62
|
+
const found = findSlottedElement(c)
|
|
63
|
+
if (found) return found
|
|
64
|
+
}
|
|
65
|
+
} else if (v && typeof v === 'object') {
|
|
66
|
+
const found = findSlottedElement(v)
|
|
67
|
+
if (found) return found
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loopFlags(source: string, filename: string, predicate?: (n: any) => boolean) {
|
|
74
|
+
const ctx = analyzeComponent(source, filename, 'List')
|
|
75
|
+
const ir = jsxToIR(ctx)
|
|
76
|
+
expect(ir).not.toBeNull()
|
|
77
|
+
const loop = findLoop(ir, predicate)
|
|
78
|
+
expect(loop).toBeDefined()
|
|
79
|
+
return { isStaticArray: loop.isStaticArray as boolean, isPropDerivedArray: loop.isPropDerivedArray as boolean | undefined }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CHILD = `
|
|
83
|
+
type ItemProps = { label: string }
|
|
84
|
+
function Item(props: ItemProps) {
|
|
85
|
+
const [on, setOn] = createSignal(false)
|
|
86
|
+
return <button onClick={() => setOn(!on())}>{props.label}: {on() ? 'ON' : 'OFF'}</button>
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
|
|
90
|
+
describe('#2835 — name collision with a whole-props type member is not prop-derived', () => {
|
|
91
|
+
test('headline: a module const colliding with `Props.base` stays static (type alias)', () => {
|
|
92
|
+
const flags = loopFlags(`
|
|
93
|
+
'use client'
|
|
94
|
+
${CHILD}
|
|
95
|
+
const base = [{ label: 'z' }]
|
|
96
|
+
type Props = { base: ItemProps[] }
|
|
97
|
+
export function List(props: Props) {
|
|
98
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
99
|
+
}
|
|
100
|
+
`, 'ModuleConstCollision.tsx')
|
|
101
|
+
expect(flags.isStaticArray).toBe(true)
|
|
102
|
+
expect(flags.isPropDerivedArray).toBeUndefined()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('same collision with `interface Props`', () => {
|
|
106
|
+
const flags = loopFlags(`
|
|
107
|
+
'use client'
|
|
108
|
+
${CHILD}
|
|
109
|
+
const base = [{ label: 'z' }]
|
|
110
|
+
interface Props { base: ItemProps[] }
|
|
111
|
+
export function List(props: Props) {
|
|
112
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
113
|
+
}
|
|
114
|
+
`, 'InterfaceCollision.tsx')
|
|
115
|
+
expect(flags.isStaticArray).toBe(true)
|
|
116
|
+
expect(flags.isPropDerivedArray).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('same collision with an inline type-literal props annotation', () => {
|
|
120
|
+
const flags = loopFlags(`
|
|
121
|
+
'use client'
|
|
122
|
+
${CHILD}
|
|
123
|
+
const base = [{ label: 'z' }]
|
|
124
|
+
export function List(props: { base: ItemProps[] }) {
|
|
125
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
126
|
+
}
|
|
127
|
+
`, 'InlineTypeCollision.tsx')
|
|
128
|
+
expect(flags.isStaticArray).toBe(true)
|
|
129
|
+
expect(flags.isPropDerivedArray).toBeUndefined()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('a component-local const literal colliding with a type member also stays static (not reachable via Go BF101 either — bakedChildLoop screens it out first)', () => {
|
|
133
|
+
const flags = loopFlags(`
|
|
134
|
+
'use client'
|
|
135
|
+
${CHILD}
|
|
136
|
+
type Props = { base: ItemProps[] }
|
|
137
|
+
export function List(props: Props) {
|
|
138
|
+
const base = [{ label: 'z' }]
|
|
139
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
140
|
+
}
|
|
141
|
+
`, 'LocalConstCollision.tsx')
|
|
142
|
+
expect(flags.isStaticArray).toBe(true)
|
|
143
|
+
expect(flags.isPropDerivedArray).toBeUndefined()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('the collision survives an alias hop', () => {
|
|
147
|
+
const flags = loopFlags(`
|
|
148
|
+
'use client'
|
|
149
|
+
${CHILD}
|
|
150
|
+
const base = [{ label: 'z' }]
|
|
151
|
+
type Props = { base: ItemProps[] }
|
|
152
|
+
export function List(props: Props) {
|
|
153
|
+
const b = base
|
|
154
|
+
return <div>{b.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
155
|
+
}
|
|
156
|
+
`, 'AliasedCollision.tsx')
|
|
157
|
+
expect(flags.isStaticArray).toBe(true)
|
|
158
|
+
expect(flags.isPropDerivedArray).toBeUndefined()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('regression guard: destructured props still resolve to mapArray', () => {
|
|
162
|
+
const clientJs = getClientJs(`
|
|
163
|
+
'use client'
|
|
164
|
+
import { createSignal } from '@barefootjs/client'
|
|
165
|
+
${CHILD}
|
|
166
|
+
type Props = { base: ItemProps[] }
|
|
167
|
+
export function List({ base }: Props) {
|
|
168
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
169
|
+
}
|
|
170
|
+
`, 'DestructuredRegression.tsx')
|
|
171
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
172
|
+
expect(clientJs).not.toMatch(/qsaChildScopes\(/)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('regression guard: a renamed destructure ({ base: rows }) still resolves to mapArray', () => {
|
|
176
|
+
const clientJs = getClientJs(`
|
|
177
|
+
'use client'
|
|
178
|
+
import { createSignal } from '@barefootjs/client'
|
|
179
|
+
${CHILD}
|
|
180
|
+
type Props = { base: ItemProps[] }
|
|
181
|
+
export function List({ base: rows }: Props) {
|
|
182
|
+
return <div>{rows.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
183
|
+
}
|
|
184
|
+
`, 'RenamedDestructureRegression.tsx')
|
|
185
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
186
|
+
expect(clientJs).not.toMatch(/qsaChildScopes\(/)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('regression guard: a renamed destructure keeps a same-named module const static (no name collision through the rename)', () => {
|
|
190
|
+
const flags = loopFlags(`
|
|
191
|
+
'use client'
|
|
192
|
+
${CHILD}
|
|
193
|
+
const base = [{ label: 'z' }]
|
|
194
|
+
type Props = { base: ItemProps[] }
|
|
195
|
+
export function List({ base: rows }: Props) {
|
|
196
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
197
|
+
}
|
|
198
|
+
`, 'RenamedDestructureModuleConst.tsx')
|
|
199
|
+
expect(flags.isStaticArray).toBe(true)
|
|
200
|
+
expect(flags.isPropDerivedArray).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('regression guard: whole-props body destructure (`const { base } = props`) still resolves to mapArray via `.parsed`, not the name-only branch', () => {
|
|
204
|
+
const clientJs = getClientJs(`
|
|
205
|
+
'use client'
|
|
206
|
+
import { createSignal } from '@barefootjs/client'
|
|
207
|
+
${CHILD}
|
|
208
|
+
type Props = { base: ItemProps[] }
|
|
209
|
+
export function List(props: Props) {
|
|
210
|
+
const { base } = props
|
|
211
|
+
return <div>{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
212
|
+
}
|
|
213
|
+
`, 'WholePropsBodyDestructure.tsx')
|
|
214
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
215
|
+
expect(clientJs).not.toMatch(/qsaChildScopes\(/)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('regression guard: direct `props.base` access still resolves to mapArray', () => {
|
|
219
|
+
const clientJs = getClientJs(`
|
|
220
|
+
'use client'
|
|
221
|
+
import { createSignal } from '@barefootjs/client'
|
|
222
|
+
${CHILD}
|
|
223
|
+
type Props = { base: ItemProps[] }
|
|
224
|
+
export function List(props: Props) {
|
|
225
|
+
return <div>{props.base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
226
|
+
}
|
|
227
|
+
`, 'DirectMemberAccessRegression.tsx')
|
|
228
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
229
|
+
expect(clientJs).not.toMatch(/qsaChildScopes\(/)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('regression guard: a rest-spread sibling name is unaffected (no collision to guard against)', () => {
|
|
233
|
+
const clientJs = getClientJs(`
|
|
234
|
+
'use client'
|
|
235
|
+
import { createSignal } from '@barefootjs/client'
|
|
236
|
+
${CHILD}
|
|
237
|
+
const base = [{ label: 'z' }]
|
|
238
|
+
type Props = { other: string; base: ItemProps[] }
|
|
239
|
+
export function List({ other, ...rest }: Props) {
|
|
240
|
+
return <div>{other}{base.map((item) => <Item key={item.label} label={item.label} />)}</div>
|
|
241
|
+
}
|
|
242
|
+
`, 'RestSpreadSibling.tsx')
|
|
243
|
+
expect(clientJs).not.toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('non-impact pin: `ctx.patterns.props`\' regex detection of `props.<key>` in a whole-props component is unaffected — the element still gets a slot id', () => {
|
|
247
|
+
const ctx = analyzeComponent(`
|
|
248
|
+
'use client'
|
|
249
|
+
type Props = { base: { label: string }[] }
|
|
250
|
+
export function List(props: Props) {
|
|
251
|
+
return <div class={props.base.length ? 'a' : 'b'}>static</div>
|
|
252
|
+
}
|
|
253
|
+
`, 'NonImpactPin.tsx', 'List')
|
|
254
|
+
const ir = jsxToIR(ctx)
|
|
255
|
+
const el = findSlottedElement(ir)
|
|
256
|
+
expect(el).toBeDefined()
|
|
257
|
+
expect(el.slotId).not.toBeNull()
|
|
258
|
+
})
|
|
259
|
+
})
|
|
@@ -185,7 +185,7 @@ describe('IRComponent.loopItemRoot (#2444)', () => {
|
|
|
185
185
|
expect(clientJs!.content).toMatch(/renderChild\('Badge[^']*',\s*\{text:\s*row\.label\},\s*undefined,\s*'s\d+'\)/)
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
-
test('CSR template: a component that IS the loop row root
|
|
188
|
+
test('CSR template: a component that IS the loop row root still passes its slot id, with the loopItemRoot flag (#2833)', () => {
|
|
189
189
|
const source = `
|
|
190
190
|
'use client'
|
|
191
191
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -207,7 +207,11 @@ describe('IRComponent.loopItemRoot (#2444)', () => {
|
|
|
207
207
|
expect(result.errors).toHaveLength(0)
|
|
208
208
|
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
209
209
|
expect(clientJs).toBeDefined()
|
|
210
|
-
//
|
|
211
|
-
|
|
210
|
+
// A row root still doesn't DERIVE its scope from the slot (no `_sN`
|
|
211
|
+
// scope-id suffix elsewhere in this file's other assertions), but it
|
|
212
|
+
// must keep slot identity — a pure-CSR mount's static init selector
|
|
213
|
+
// depends on the `bf-h`/`bf-m` this renders, per `renderChild`'s
|
|
214
|
+
// `loopItemRoot` param docstring (component.ts).
|
|
215
|
+
expect(clientJs!.content).toMatch(/renderChild\('Row[^']*',\s*\{label: row\.label\},\s*row\.id,\s*'s\d+',\s*true\)/)
|
|
212
216
|
})
|
|
213
217
|
})
|
|
@@ -159,27 +159,18 @@ describe('markup-prop brand (#2651)', () => {
|
|
|
159
159
|
expect(clientJs).toMatch(/get header\(\)\s*\{\s*return __slot\(/)
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// `
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
// Graduation: once `bfMarkup()` branding is extended to this multi-part
|
|
176
|
-
// shape, this assertion starts failing — replace it with a positive
|
|
177
|
-
// `bfMarkup(...)`-wrapped assertion (mirroring test (b) above) and
|
|
178
|
-
// delete this comment block; that is the fixture's own graduation
|
|
179
|
-
// trigger too (its `expectedHtml` is already the correct SSR output, so
|
|
180
|
-
// no fixture change is needed, only this pin's removal and #2667's
|
|
181
|
-
// BF021 suggestion regaining the fragment-wrap escape).
|
|
182
|
-
test('(h) KNOWN BUG (#2702): a conditional-in-fragment jsx-children prop reaches initChild UNbranded', () => {
|
|
162
|
+
// #2702: a conditional-in-fragment jsx-children prop now brands EACH
|
|
163
|
+
// element branch with `bfMarkup()` (`jsxChildrenPropGetterExpr`,
|
|
164
|
+
// `ir-to-client-js/html-template.ts`) — the shape `isSingleElementJsx
|
|
165
|
+
// Children`'s narrower single-element gate (#2651) left unbranded,
|
|
166
|
+
// which corrupted the DOM the moment the child's own `escapeTextOrNode`
|
|
167
|
+
// reactive effect first ran (re-escaped the chosen branch's HTML as
|
|
168
|
+
// literal text; see `jsx-element-prop-fragment-conditional`'s fixture
|
|
169
|
+
// docstring for the full mechanism). Branding each LEAF rather than the
|
|
170
|
+
// whole ternary keeps a non-element branch (text/expression/`&&`'s
|
|
171
|
+
// `''`) unbranded and thus still escaped normally — see the sibling
|
|
172
|
+
// tests below.
|
|
173
|
+
test('(h) a conditional-in-fragment jsx-children prop brands each element branch with bfMarkup()', () => {
|
|
183
174
|
const source = `
|
|
184
175
|
'use client'
|
|
185
176
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -201,10 +192,158 @@ describe('markup-prop brand (#2651)', () => {
|
|
|
201
192
|
}
|
|
202
193
|
`
|
|
203
194
|
const clientJs = clientJsFor(source)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
195
|
+
expect(clientJs).toMatch(/get header\(\)\s*\{\s*return cond\(\)\s*\?\s*bfMarkup\(`<a>x<\/a>`\)\s*:\s*bfMarkup\(`<b>y<\/b>`\)\s*\}/)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('(i) a conditional-in-fragment jsx-children prop leaves a non-element branch unbranded', () => {
|
|
199
|
+
const source = `
|
|
200
|
+
'use client'
|
|
201
|
+
import { createSignal } from '@barefootjs/client'
|
|
202
|
+
export function Card(props: { header?: any }) {
|
|
203
|
+
return <section><header>{props.header}</header></section>
|
|
204
|
+
}
|
|
205
|
+
export function label() { return 'x' }
|
|
206
|
+
export function TextBranch() {
|
|
207
|
+
const [cond, setCond] = createSignal(true)
|
|
208
|
+
return <Card header={<>{cond() ? <a>x</a> : 'plain'}</>} />
|
|
209
|
+
}
|
|
210
|
+
export function ExpressionBranch() {
|
|
211
|
+
const [cond, setCond] = createSignal(true)
|
|
212
|
+
return <Card header={<>{cond() ? <a>x</a> : label()}</>} />
|
|
213
|
+
}
|
|
214
|
+
export function LogicalAnd() {
|
|
215
|
+
const [cond, setCond] = createSignal(true)
|
|
216
|
+
return <Card header={<>{cond() && <a>x</a>}</>} />
|
|
217
|
+
}
|
|
218
|
+
`
|
|
219
|
+
const clientJs = clientJsFor(source)
|
|
220
|
+
expect(clientJs).toMatch(/get header\(\)\s*\{\s*return cond\(\)\s*\?\s*bfMarkup\(`<a>x<\/a>`\)\s*:\s*'plain'\s*\}/)
|
|
221
|
+
expect(clientJs).toMatch(/get header\(\)\s*\{\s*return cond\(\)\s*\?\s*bfMarkup\(`<a>x<\/a>`\)\s*:\s*label\(\)\s*\}/)
|
|
222
|
+
expect(clientJs).toMatch(/get header\(\)\s*\{\s*return cond\(\)\s*\?\s*bfMarkup\(`<a>x<\/a>`\)\s*:\s*''\s*\}/)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('(j) nested conditionals brand every element branch', () => {
|
|
226
|
+
const source = `
|
|
227
|
+
'use client'
|
|
228
|
+
import { createSignal } from '@barefootjs/client'
|
|
229
|
+
export function Card(props: { header?: any }) {
|
|
230
|
+
return <section><header>{props.header}</header></section>
|
|
231
|
+
}
|
|
232
|
+
export function P() {
|
|
233
|
+
const [a, setA] = createSignal(true)
|
|
234
|
+
const [b, setB] = createSignal(true)
|
|
235
|
+
return <Card header={<>{a() ? (b() ? <a>x</a> : <c>z</c>) : <b>y</b>}</>} />
|
|
236
|
+
}
|
|
237
|
+
`
|
|
238
|
+
const clientJs = clientJsFor(source)
|
|
239
|
+
expect(clientJs).toMatch(
|
|
240
|
+
/get header\(\)\s*\{\s*return a\(\)\s*\?\s*b\(\)\s*\?\s*bfMarkup\(`<a>x<\/a>`\)\s*:\s*bfMarkup\(`<c>z<\/c>`\)\s*:\s*bfMarkup\(`<b>y<\/b>`\)\s*\}/,
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// A multi-child fragment (more than one flattened part) has no `bfMarkup`/
|
|
245
|
+
// `escapeTextOrNode` array contract — stays unbranded, unchanged from
|
|
246
|
+
// before #2702 (a separate, pre-existing gap, not this fix's scope).
|
|
247
|
+
test('(k) a multi-child fragment jsx-children prop stays unbranded', () => {
|
|
248
|
+
const source = `
|
|
249
|
+
'use client'
|
|
250
|
+
export function Card(props: { header?: any }) {
|
|
251
|
+
return <section><header>{props.header}</header></section>
|
|
252
|
+
}
|
|
253
|
+
export function P() {
|
|
254
|
+
return <Card header={<>text<strong>x</strong></>} />
|
|
255
|
+
}
|
|
256
|
+
`
|
|
257
|
+
const clientJs = clientJsFor(source)
|
|
258
|
+
expect(clientJs).toMatch(/get header\(\)\s*\{\s*return \["text", `<strong>x<\/strong>`\]\s*\}/)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// An explicit `children={<jsx/>}` prop stays unbranded regardless of
|
|
262
|
+
// shape — its consumer is a bare `{children}` passthrough with no
|
|
263
|
+
// unwrap call (test (b)'s comment above explains the same exclusion).
|
|
264
|
+
// pullfrog review, PR #2820: `jsxChildrenPropGetterExpr` (the shared
|
|
265
|
+
// brand-decision door test (h)-(k) pin) wasn't wired into EVERY builder
|
|
266
|
+
// that constructs the identical `get <propName>() { return <expr> }`
|
|
267
|
+
// getter — `buildComponentPropsExpr` (`control-flow/shared.ts`, shared by
|
|
268
|
+
// loop AND conditional-branch component reconciliation) and
|
|
269
|
+
// `buildStaticPropsExpr` (`build-static-array-child-init.ts`) both still
|
|
270
|
+
// called the plain, unbranded `irChildrenToJsExpr` directly. The same
|
|
271
|
+
// conditional-in-fragment shape test (h) pins for a top-level component
|
|
272
|
+
// corrupted the DOM at hydrate time when the component was instead
|
|
273
|
+
// instantiated inside a `.map()` loop.
|
|
274
|
+
test('(m) a conditional-in-fragment jsx-children prop is branded for a component instantiated inside a .map() loop', () => {
|
|
275
|
+
const source = `
|
|
276
|
+
'use client'
|
|
277
|
+
import { createSignal } from '@barefootjs/client'
|
|
278
|
+
export function Card(props: { header?: any }) {
|
|
279
|
+
return <header>{props.header}</header>
|
|
280
|
+
}
|
|
281
|
+
export function List() {
|
|
282
|
+
const [items, setItems] = createSignal([{ id: 1 }])
|
|
283
|
+
return (
|
|
284
|
+
<div>
|
|
285
|
+
{items().map(item => (
|
|
286
|
+
<Card key={item.id} header={<>{item.id > 0 ? <a>x</a> : <b>y</b>}</>} />
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
`
|
|
292
|
+
const clientJs = clientJsFor(source)
|
|
293
|
+
// Both the reactive-patch (initChild) and initial-mount (createComponent)
|
|
294
|
+
// paths through mapArray's callback must brand each element branch.
|
|
295
|
+
expect(clientJs).toMatch(
|
|
296
|
+
/initChild\('Card', __existing, \{ get header\(\) \{ return item\(\)\.id > 0 \? bfMarkup\(`<a>x<\/a>`\) : bfMarkup\(`<b>y<\/b>`\) \} \}\)/,
|
|
297
|
+
)
|
|
298
|
+
expect(clientJs).toMatch(
|
|
299
|
+
/createComponent\('Card', \{ get header\(\) \{ return item\(\)\.id > 0 \? bfMarkup\(`<a>x<\/a>`\) : bfMarkup\(`<b>y<\/b>`\) \} \}, item\(\)\.id\)/,
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// pullfrog review, PR #2820: test (m) covers the SIGNAL-driven `.map()`
|
|
304
|
+
// loop (`buildComponentPropsExpr`); `buildStaticPropsExpr`
|
|
305
|
+
// (`build-static-array-child-init.ts`) — the sibling builder for a
|
|
306
|
+
// STATIC, non-signal array `.map()` — got the identical fix but no
|
|
307
|
+
// dedicated test, which is exactly the untested-call-site pattern that
|
|
308
|
+
// let #2702's original corruption go uncaught. Pins the hydrate-time
|
|
309
|
+
// `initChild` path this builder feeds.
|
|
310
|
+
test('(n) a conditional-in-fragment jsx-children prop is branded for a component instantiated inside a static (non-signal) array .map()', () => {
|
|
311
|
+
const source = `
|
|
312
|
+
'use client'
|
|
313
|
+
import { createSignal } from '@barefootjs/client'
|
|
314
|
+
export function Card(props: { header?: any }) {
|
|
315
|
+
return <header>{props.header}</header>
|
|
316
|
+
}
|
|
317
|
+
export function List() {
|
|
318
|
+
const [cond, setCond] = createSignal(true)
|
|
319
|
+
return (
|
|
320
|
+
<div>
|
|
321
|
+
{[{ id: 1 }].map(item => (
|
|
322
|
+
<Card key={item.id} header={<>{cond() ? <a>x</a> : <b>y</b>}</>} />
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
`
|
|
328
|
+
const clientJs = clientJsFor(source)
|
|
329
|
+
expect(clientJs).toMatch(
|
|
330
|
+
/initChild\('Card', childScope, \{ get header\(\) \{ return cond\(\) \? bfMarkup\(`<a>x<\/a>`\) : bfMarkup\(`<b>y<\/b>`\) \} \}\)/,
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('(l) an explicit children prop with a conditional stays unbranded', () => {
|
|
335
|
+
const source = `
|
|
336
|
+
'use client'
|
|
337
|
+
import { createSignal } from '@barefootjs/client'
|
|
338
|
+
export function Card(props: { children?: any }) {
|
|
339
|
+
return <section>{props.children}</section>
|
|
340
|
+
}
|
|
341
|
+
export function P() {
|
|
342
|
+
const [cond, setCond] = createSignal(true)
|
|
343
|
+
return <Card children={<>{cond() ? <a>x</a> : <b>y</b>}</>} />
|
|
344
|
+
}
|
|
345
|
+
`
|
|
346
|
+
const clientJs = clientJsFor(source)
|
|
347
|
+
expect(clientJs).toMatch(/get children\(\)\s*\{\s*return cond\(\)\s*\?\s*`<a>x<\/a>`\s*:\s*`<b>y<\/b>`\s*\}/)
|
|
209
348
|
})
|
|
210
349
|
})
|