@barefootjs/jsx 0.35.0 → 0.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/index.js +731 -691
- package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -2
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +24 -44
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +128 -10
- package/src/__tests__/binding-scope-ratchet.test.ts +26 -3
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
- package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
- package/src/__tests__/loop-index-reactivity.test.ts +208 -0
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +27 -5
- package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
- package/src/__tests__/nested-loop-plain.test.ts +39 -25
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +12 -4
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- package/src/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/ir-to-client-js/collect-elements.ts +110 -32
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +28 -51
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +7 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +33 -6
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +7 -1
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +18 -6
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +12 -15
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
- package/src/ir-to-client-js/emit-reactive.ts +88 -5
- package/src/ir-to-client-js/html-template.ts +9 -1
- package/src/ir-to-client-js/reactivity.ts +73 -27
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/jsx-to-ir.ts +21 -4
- package/src/reactivity-checker.ts +60 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2867 / #2830: `shouldAutoDeferReactiveBrand` (jsx-to-ir.ts) used to call
|
|
3
|
+
* `getJS()` on the whole candidate expression to regex-test it for a native
|
|
4
|
+
* signal/memo call. For a `.map()` loop or a ternary/`&&` with JSX arms, that
|
|
5
|
+
* expression IS JSX-bearing — and whenever the TypeChecker was active and
|
|
6
|
+
* resolved a `Reactive<T>` brand anywhere in the expression, `getJS()`'s
|
|
7
|
+
* trust-boundary assertion (armed by `map-body-no-silent-divergence.test.ts`)
|
|
8
|
+
* threw, uncaught, on `signal-array-builder` / `flatmap-expression-body-signal-array`.
|
|
9
|
+
*
|
|
10
|
+
* That harness's own `assess()` compiles via a bare relative path (`T.tsx`)
|
|
11
|
+
* with no injected `ts.Program`, so whether the checker actually activates —
|
|
12
|
+
* and thus whether this reproduces — silently depended on the process's cwd
|
|
13
|
+
* and whether `@barefootjs/client`'s `dist` happened to be built, which is
|
|
14
|
+
* exactly why the failure looked like an environment-dependent flake (#2867)
|
|
15
|
+
* rather than the deterministic compiler bug it is. These tests pin it with
|
|
16
|
+
* an explicitly injected `ts.Program` (mirroring `auto-defer-brand.test.ts`)
|
|
17
|
+
* so the repro — and the fix — hold regardless of cwd or build state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
|
21
|
+
import ts from 'typescript'
|
|
22
|
+
import path from 'path'
|
|
23
|
+
import { compileJSX } from '../compiler'
|
|
24
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
25
|
+
|
|
26
|
+
let prevAssertEnv: string | undefined
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
prevAssertEnv = process.env.BF_ASSERT_NO_JSX_IN_GETJS
|
|
29
|
+
process.env.BF_ASSERT_NO_JSX_IN_GETJS = '1'
|
|
30
|
+
})
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
if (prevAssertEnv === undefined) delete process.env.BF_ASSERT_NO_JSX_IN_GETJS
|
|
33
|
+
else process.env.BF_ASSERT_NO_JSX_IN_GETJS = prevAssertEnv
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
interface Compiled {
|
|
37
|
+
errors: string[]
|
|
38
|
+
hasError: boolean
|
|
39
|
+
clientJs: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compile `source` with a real TypeChecker that resolves `@barefootjs/client`
|
|
44
|
+
* (found deterministically via `packages/jsx/node_modules`, since the virtual
|
|
45
|
+
* file lives under this file's own directory — no dependency on `process.cwd()`).
|
|
46
|
+
*/
|
|
47
|
+
function compileWithChecker(source: string): Compiled {
|
|
48
|
+
const componentPath = path.join(path.resolve(__dirname), '_map-body-brand-component.tsx')
|
|
49
|
+
const virtualFiles = new Map<string, string>([[componentPath, source]])
|
|
50
|
+
|
|
51
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
52
|
+
target: ts.ScriptTarget.Latest,
|
|
53
|
+
module: ts.ModuleKind.ESNext,
|
|
54
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
55
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
56
|
+
strict: true,
|
|
57
|
+
skipLibCheck: true,
|
|
58
|
+
noEmit: true,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultHost = ts.createCompilerHost(compilerOptions)
|
|
62
|
+
const host: ts.CompilerHost = {
|
|
63
|
+
...defaultHost,
|
|
64
|
+
getSourceFile(fileName, languageVersion) {
|
|
65
|
+
const resolved = path.resolve(fileName)
|
|
66
|
+
const content = virtualFiles.get(resolved)
|
|
67
|
+
if (content !== undefined) {
|
|
68
|
+
return ts.createSourceFile(fileName, content, languageVersion, true, ts.ScriptKind.TSX)
|
|
69
|
+
}
|
|
70
|
+
return defaultHost.getSourceFile(fileName, languageVersion)
|
|
71
|
+
},
|
|
72
|
+
fileExists(fileName) {
|
|
73
|
+
return virtualFiles.has(path.resolve(fileName)) || defaultHost.fileExists(fileName)
|
|
74
|
+
},
|
|
75
|
+
readFile(fileName) {
|
|
76
|
+
return virtualFiles.get(path.resolve(fileName)) ?? defaultHost.readFile(fileName)
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const program = ts.createProgram([componentPath], compilerOptions, host)
|
|
81
|
+
const adapter = new TestAdapter()
|
|
82
|
+
const r = compileJSX(source, componentPath, { adapter, program })
|
|
83
|
+
const clientJs = r.files.find((f) => f.type === 'clientJs')?.content ?? ''
|
|
84
|
+
return {
|
|
85
|
+
errors: r.errors.map((e) => `[${e.code}] ${e.message}`),
|
|
86
|
+
hasError: r.errors.some((e) => e.severity === 'error'),
|
|
87
|
+
clientJs,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertSound(clientJs: string): void {
|
|
92
|
+
const sf = ts.createSourceFile('bundle.js', clientJs, ts.ScriptTarget.ES2022, false, ts.ScriptKind.JS)
|
|
93
|
+
const parseErrors = (sf as unknown as { parseDiagnostics: unknown[] }).parseDiagnostics.length
|
|
94
|
+
expect(parseErrors).toBe(0)
|
|
95
|
+
expect(clientJs).not.toMatch(/__BF_JSX_\d+__/)
|
|
96
|
+
expect(clientJs).not.toMatch(/\bpush\(</)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('shouldAutoDeferReactiveBrand does not stringify JSX-bearing expressions (#2867/#2830)', () => {
|
|
100
|
+
test('signal-sourced .map() array builder compiles clean under an active checker', () => {
|
|
101
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
102
|
+
'use client'
|
|
103
|
+
import { createSignal } from '@barefootjs/client'
|
|
104
|
+
function T() {
|
|
105
|
+
const [rows] = createSignal([{ id: '1', cells: ['a'] }])
|
|
106
|
+
return <table><tbody>{rows().map((r) => {
|
|
107
|
+
const out = []
|
|
108
|
+
for (const c of r.cells) out.push(<td>{c}</td>)
|
|
109
|
+
return <tr key={r.id}>{out}</tr>
|
|
110
|
+
})}</tbody></table>
|
|
111
|
+
}
|
|
112
|
+
export { T }`)
|
|
113
|
+
expect(hasError).toBe(false)
|
|
114
|
+
assertSound(clientJs)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('signal-sourced .flatMap() with an expression-body inner .map() compiles clean under an active checker', () => {
|
|
118
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
119
|
+
'use client'
|
|
120
|
+
import { createSignal } from '@barefootjs/client'
|
|
121
|
+
function T() {
|
|
122
|
+
const [items] = createSignal([{ id: '1', tags: ['a'] }])
|
|
123
|
+
return <ul>{items().flatMap((it) => it.tags.map((t) => <li key={t}>{t}</li>))}</ul>
|
|
124
|
+
}
|
|
125
|
+
export { T }`)
|
|
126
|
+
expect(hasError).toBe(false)
|
|
127
|
+
assertSound(clientJs)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('memo-sourced .map() array builder also compiles clean (not just signals)', () => {
|
|
131
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
132
|
+
'use client'
|
|
133
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
134
|
+
function T() {
|
|
135
|
+
const [rows] = createSignal([{ id: '1', cells: ['a'] }])
|
|
136
|
+
const upper = createMemo(() => rows())
|
|
137
|
+
return <table><tbody>{upper().map((r) => {
|
|
138
|
+
const out = []
|
|
139
|
+
for (const c of r.cells) out.push(<td>{c}</td>)
|
|
140
|
+
return <tr key={r.id}>{out}</tr>
|
|
141
|
+
})}</tbody></table>
|
|
142
|
+
}
|
|
143
|
+
export { T }`)
|
|
144
|
+
expect(hasError).toBe(false)
|
|
145
|
+
assertSound(clientJs)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('signal-conditioned ternary with JSX on both arms, no props destructuring (BF043) to mask the assertion', () => {
|
|
149
|
+
// No destructured props here (which would emit a BF043 warning and, pre-
|
|
150
|
+
// fix, incidentally have silently disarmed the trust-boundary assertion
|
|
151
|
+
// for this shape). This pins the conditional-arm path directly.
|
|
152
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
153
|
+
'use client'
|
|
154
|
+
import { createSignal } from '@barefootjs/client'
|
|
155
|
+
function T() {
|
|
156
|
+
const [show] = createSignal(true)
|
|
157
|
+
return <div>{show() ? <b>yes</b> : <em>no</em>}</div>
|
|
158
|
+
}
|
|
159
|
+
export { T }`)
|
|
160
|
+
expect(hasError).toBe(false)
|
|
161
|
+
assertSound(clientJs)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('a component also importing @barefootjs/client but with no .map() and no reactive JSX branch is unaffected', () => {
|
|
165
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
166
|
+
'use client'
|
|
167
|
+
import { createSignal } from '@barefootjs/client'
|
|
168
|
+
function T() {
|
|
169
|
+
const [count, setCount] = createSignal(0)
|
|
170
|
+
return <button onClick={() => setCount((c) => c + 1)}>{count()}</button>
|
|
171
|
+
}
|
|
172
|
+
export { T }`)
|
|
173
|
+
expect(hasError).toBe(false)
|
|
174
|
+
assertSound(clientJs)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -108,6 +108,14 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
108
108
|
// *after* the alias, inside the inner renderItem's own clone template.
|
|
109
109
|
const templateIdx = content.indexOf('escapeText(i)', aliasIdx)
|
|
110
110
|
expect(templateIdx).toBeGreaterThan(aliasIdx)
|
|
111
|
+
// #2861: a bare `{i}` — no signal, no function call anywhere in the
|
|
112
|
+
// expression — used to be classified fully static by Phase 2 and never
|
|
113
|
+
// get a `createEffect` at all, so it stayed frozen at whatever value it
|
|
114
|
+
// had when the row was first created. `classifyReactivity`'s new
|
|
115
|
+
// `loop-index` source fixes this: the index is wired through the same
|
|
116
|
+
// accessor `mapArray` already hands every row (#2859/#2860), and the
|
|
117
|
+
// slot now gets a real update effect.
|
|
118
|
+
expect(content).toContain(`createEffect(() => { __bfw_s0('s0', String(i())) })`)
|
|
111
119
|
})
|
|
112
120
|
|
|
113
121
|
test('index referenced in a reactive attribute expression is bound', () => {
|
|
@@ -134,6 +142,17 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
134
142
|
|
|
135
143
|
expect(content).toContain('const i = __innerIdx')
|
|
136
144
|
expect(content).toContain(`i % 2 === 0 ? 'even' : 'odd'`)
|
|
145
|
+
// #2861: `class={i % 2 === 0 ? …}` has no signal read and no function
|
|
146
|
+
// call — before `classifyReactivity`'s `loop-index` source existed this
|
|
147
|
+
// was classified fully static and never got a `createEffect`, so the
|
|
148
|
+
// class stayed frozen at whichever value the row was created with.
|
|
149
|
+
// The expression is read into `__x` first (#2869's dedup-guard shape,
|
|
150
|
+
// `emitDedupedAttrUpdate`) before the actual attribute write reads it
|
|
151
|
+
// back as `__v` — assert both halves rather than the old direct
|
|
152
|
+
// `const __v = <expr>` shape #2869 replaced.
|
|
153
|
+
expect(content).toMatch(
|
|
154
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = `\$\{i\(\) % 2 === 0 \? 'even' : 'odd'\}`[\s\S]*?const __v = __x;/
|
|
155
|
+
)
|
|
137
156
|
})
|
|
138
157
|
|
|
139
158
|
test('index referenced only in an event handler inside the inner loop is bound', () => {
|
|
@@ -223,10 +242,13 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
223
242
|
expect(content).not.toContain('const id = __innerIdx')
|
|
224
243
|
})
|
|
225
244
|
|
|
226
|
-
test('
|
|
227
|
-
// Sibling of the reactive-path test above,
|
|
228
|
-
//
|
|
229
|
-
//
|
|
245
|
+
test('inner array with no outer-item dependency still binds the index alias (#2865: no more static forEach)', () => {
|
|
246
|
+
// Sibling of the reactive-path test above. Before #2865, an inner loop
|
|
247
|
+
// whose array didn't reference the outer loop param was routed to
|
|
248
|
+
// `emitStatic` in `stringify/inner-loop.ts` — a hydration-only
|
|
249
|
+
// `forEach` that never wired reactivity for anything inside the row.
|
|
250
|
+
// That fork is gone: every inner loop now gets the full `mapArray`
|
|
251
|
+
// emission regardless of what its array depends on.
|
|
230
252
|
const content = clientJsFor(`
|
|
231
253
|
'use client'
|
|
232
254
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -251,7 +273,7 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
251
273
|
}
|
|
252
274
|
`)
|
|
253
275
|
|
|
254
|
-
expect(content).toMatch(/
|
|
276
|
+
expect(content).toMatch(/mapArray\(\(\) => SHARED \|\| \[\], .*?, \(cell, __innerIdx[^)]+, __existing\) => \{[\s\S]*?const\s+i\s*=\s*__innerIdx/)
|
|
255
277
|
})
|
|
256
278
|
|
|
257
279
|
test('conditional-branch-arm inner loop (loop -> conditional -> inner loop) binds the index alias', () => {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS Compiler — nested `.map()` reactivity when the inner array
|
|
3
|
+
* (or the row's own bindings) does NOT derive from the outer loop's item.
|
|
4
|
+
*
|
|
5
|
+
* #2865: before this fix, `build-inner-loop.ts` decided "reactive vs
|
|
6
|
+
* static" for a nested loop by checking only whether `inner.array`
|
|
7
|
+
* textually referenced the OUTER loop's item — `refsParent`. Anything
|
|
8
|
+
* else the inner loop depended on fell through to a hydration-only
|
|
9
|
+
* `forEach` (`emitStatic` in `stringify/inner-loop.ts`) that never wired
|
|
10
|
+
* a single `createEffect`, regardless of whether the array itself, or a
|
|
11
|
+
* text/attr/conditional inside the row, was genuinely reactive. That
|
|
12
|
+
* fork has been removed — every nested loop now gets the full reactive
|
|
13
|
+
* `mapArray` emission unconditionally.
|
|
14
|
+
*
|
|
15
|
+
* This file pins the previously-frozen shapes directly (as opposed to
|
|
16
|
+
* `nested-loop-plain.test.ts` / `nested-loop-index-param.test.ts`, whose
|
|
17
|
+
* relevant cases were originally written to pin the OLD static shape and
|
|
18
|
+
* have been flipped in place).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, test, expect } from 'bun:test'
|
|
22
|
+
import { compileJSX } from '../compiler'
|
|
23
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
24
|
+
|
|
25
|
+
const adapter = new TestAdapter()
|
|
26
|
+
|
|
27
|
+
function clientJsFor(source: string): string {
|
|
28
|
+
const result = compileJSX(source, 'Repro.tsx', { adapter })
|
|
29
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
30
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
31
|
+
expect(clientJs).toBeDefined()
|
|
32
|
+
return clientJs!.content
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('nested .map() reactivity independent of the outer item (#2865)', () => {
|
|
36
|
+
test('inner array read from a component signal (not the outer item) wires a nested mapArray', () => {
|
|
37
|
+
const content = clientJsFor(`
|
|
38
|
+
'use client'
|
|
39
|
+
import { createSignal } from '@barefootjs/client'
|
|
40
|
+
export function Repro() {
|
|
41
|
+
const [rows] = createSignal([{ id: 1 }])
|
|
42
|
+
const [tags] = createSignal(['x', 'y'])
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
{rows().map(row => (
|
|
46
|
+
<div key={row.id}>
|
|
47
|
+
<ul>
|
|
48
|
+
{tags().map(t => <li key={t}>{t}</li>)}
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
`)
|
|
56
|
+
|
|
57
|
+
// The old bug emitted `tags().forEach(...)` with no mapArray/createEffect
|
|
58
|
+
// at all — appending a tag never reached the DOM.
|
|
59
|
+
expect(content).toMatch(/mapArray\(\(\) => tags\(\) \|\| \[\], /)
|
|
60
|
+
expect(content).not.toMatch(/tags\(\)\.forEach\(/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('inner array read from a memo (not the outer item) wires a nested mapArray', () => {
|
|
64
|
+
const content = clientJsFor(`
|
|
65
|
+
'use client'
|
|
66
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
67
|
+
export function Repro() {
|
|
68
|
+
const [rows] = createSignal([{ id: 1 }])
|
|
69
|
+
const [count] = createSignal(2)
|
|
70
|
+
const tags = createMemo(() => Array.from({ length: count() }, (_, i) => \`t\${i}\`))
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
{rows().map(row => (
|
|
74
|
+
<div key={row.id}>
|
|
75
|
+
<ul>
|
|
76
|
+
{tags().map(t => <li key={t}>{t}</li>)}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
`)
|
|
84
|
+
|
|
85
|
+
expect(content).toMatch(/mapArray\(\(\) => tags\(\) \|\| \[\], /)
|
|
86
|
+
expect(content).not.toMatch(/tags\(\)\.forEach\(/)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('a row-content signal read (text) inside a LITERAL inner array still wires createEffect', () => {
|
|
90
|
+
// Even when the ARRAY itself is a plain constant (never needs to
|
|
91
|
+
// reconcile), a text/attr/conditional inside the row that reads a
|
|
92
|
+
// component signal must still update. The old static emitter never
|
|
93
|
+
// read `reactiveTexts`/`reactiveAttrs`/`conditionals` at all, so this
|
|
94
|
+
// froze independent of whether the array was reactive.
|
|
95
|
+
const content = clientJsFor(`
|
|
96
|
+
'use client'
|
|
97
|
+
import { createSignal } from '@barefootjs/client'
|
|
98
|
+
const TAGS = ['x', 'y']
|
|
99
|
+
export function Repro() {
|
|
100
|
+
const [rows] = createSignal([{ id: 1 }])
|
|
101
|
+
const [count, setCount] = createSignal(0)
|
|
102
|
+
return (
|
|
103
|
+
<div onClick={() => setCount(c => c + 1)}>
|
|
104
|
+
{rows().map(row => (
|
|
105
|
+
<div key={row.id}>
|
|
106
|
+
<ul>
|
|
107
|
+
{TAGS.map(t => <li key={t}>{t}: {count()}</li>)}
|
|
108
|
+
</ul>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
`)
|
|
115
|
+
|
|
116
|
+
expect(content).toMatch(/mapArray\(\(\) => TAGS \|\| \[\], /)
|
|
117
|
+
expect(content).toContain('createEffect(')
|
|
118
|
+
expect(content).not.toMatch(/TAGS\.forEach\(/)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('destructured outer param used by the inner array no longer throws ReferenceError at row construction', () => {
|
|
122
|
+
// Before #2865: static mode skipped the destructure-unwrap statement,
|
|
123
|
+
// so the emitted `forEach` referenced a bare `tags` identifier that
|
|
124
|
+
// was never declared in scope.
|
|
125
|
+
const content = clientJsFor(`
|
|
126
|
+
'use client'
|
|
127
|
+
import { createSignal } from '@barefootjs/client'
|
|
128
|
+
interface Row { id: number; tags: string[] }
|
|
129
|
+
export function Repro() {
|
|
130
|
+
const [rows] = createSignal<Row[]>([{ id: 1, tags: ['a', 'b'] }])
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
{rows().map(({ id, tags }) => (
|
|
134
|
+
<div key={id}>
|
|
135
|
+
<ul>
|
|
136
|
+
{tags.map(t => <li key={t}>{t}</li>)}
|
|
137
|
+
</ul>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
`)
|
|
144
|
+
|
|
145
|
+
// `tags` must resolve through the outer row accessor
|
|
146
|
+
// (`__bfItem().tags`) — not a bare free identifier that was never
|
|
147
|
+
// declared in scope (the old static-mode bug: the destructured
|
|
148
|
+
// pattern names `tags` for the SSR template, but the reactive
|
|
149
|
+
// renderItem's own param is the whole-item accessor, and every
|
|
150
|
+
// reference to a destructured name is rewritten to go through it).
|
|
151
|
+
expect(content).toMatch(/mapArray\(\(\) => __bfItem\(\)\.tags \|\| \[\], /)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('outer-index-derived inner array wires a nested mapArray', () => {
|
|
155
|
+
const content = clientJsFor(`
|
|
156
|
+
'use client'
|
|
157
|
+
import { createSignal } from '@barefootjs/client'
|
|
158
|
+
export function Repro() {
|
|
159
|
+
const [matrix] = createSignal([['a', 'b'], ['c']])
|
|
160
|
+
return (
|
|
161
|
+
<div>
|
|
162
|
+
{matrix().map((row, i) => (
|
|
163
|
+
<div key={i}>
|
|
164
|
+
<ul>
|
|
165
|
+
{matrix()[i].map(cell => <li key={cell}>{cell}</li>)}
|
|
166
|
+
</ul>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
`)
|
|
173
|
+
|
|
174
|
+
expect(content).toContain('mapArray(')
|
|
175
|
+
expect(content).not.toMatch(/\.forEach\(\(cell,/)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -138,18 +138,21 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
test('regression: 3-level nested loop wires mapArray at the deepest level (not forEach)', () => {
|
|
141
|
-
//
|
|
142
|
-
// by checking the IR's `inner.refsOuterParam` — a
|
|
143
|
-
// time against the *outermost* loop's param only.
|
|
144
|
-
// expression typically references the
|
|
145
|
-
// `g.items` inside `t.groups.map(g => ...)`
|
|
146
|
-
// and the loop fell through to the static
|
|
147
|
-
// additions / removals at the deepest level
|
|
148
|
-
// the DOM.
|
|
141
|
+
// Originally (the O-8 narrowing fix), `emitInnerLoopSetup` decided "is
|
|
142
|
+
// this loop reactive?" by checking the IR's `inner.refsOuterParam` — a
|
|
143
|
+
// flag set at collect time against the *outermost* loop's param only.
|
|
144
|
+
// At depth 2+, the array expression typically references the
|
|
145
|
+
// *immediate* parent (e.g. `g.items` inside `t.groups.map(g => ...)`
|
|
146
|
+
// body), so the check failed and the loop fell through to the static
|
|
147
|
+
// `forEach` branch. Result: additions / removals at the deepest level
|
|
148
|
+
// silently never reached the DOM.
|
|
149
149
|
//
|
|
150
|
-
//
|
|
151
|
-
// passed in, and narrow the parent param when recursing into
|
|
152
|
-
// levels.
|
|
150
|
+
// O-8's own fix: re-check dynamically at each level against the parent
|
|
151
|
+
// param passed in, and narrow the parent param when recursing into
|
|
152
|
+
// child levels. #2865 later removed the reactive/static fork (and
|
|
153
|
+
// `refsOuterParam` with it) entirely — every level is unconditionally
|
|
154
|
+
// reactive now — so this assertion holds trivially, but the test stays
|
|
155
|
+
// as a regression pin on "every depth gets its own `mapArray`."
|
|
153
156
|
const source = `
|
|
154
157
|
'use client'
|
|
155
158
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -293,10 +296,13 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
293
296
|
// locals — `initChild`'s prop getter would throw `ReferenceError` when
|
|
294
297
|
// the child component first mounted.
|
|
295
298
|
//
|
|
296
|
-
// Fix: thread `inner.mapPreamble` through
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
299
|
+
// Fix: thread `inner.mapPreamble` through the static-array-child-init
|
|
300
|
+
// emission (`build-static-array-child-init.ts` — the OUTER array here
|
|
301
|
+
// is a literal, so this is unrelated to #2865's nested-loop reactive/
|
|
302
|
+
// static fork, which only ever applied under a REACTIVE outer loop)
|
|
303
|
+
// and emit it (raw — `forEach`'s param is the literal item, not a
|
|
304
|
+
// signal accessor) at the top of the forEach body so the component
|
|
305
|
+
// setup can resolve the locals.
|
|
300
306
|
const source = `
|
|
301
307
|
'use client'
|
|
302
308
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -442,13 +448,17 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
442
448
|
expect(section.indexOf('const rowTag')).toBeLessThan(section.indexOf('initChild'))
|
|
443
449
|
})
|
|
444
450
|
|
|
445
|
-
test('regression #1064:
|
|
446
|
-
// The
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
451
|
+
test('regression #1064: reactive inner mapArray (signal outer + non-outer-derived inner array) emits inner preamble', () => {
|
|
452
|
+
// The reactive composite renderItem hosts an inner loop whose array is
|
|
453
|
+
// a non-outer-derived lookup (a module-level constant here — could just
|
|
454
|
+
// as easily be a signal, a memo, or a prop). Before #2865, an inner
|
|
455
|
+
// array not referencing the outer loop param routed to
|
|
456
|
+
// `control-flow/inner-loop.ts::emitStatic` — a hydration-only `forEach`
|
|
457
|
+
// that silently never re-ran the row's own preamble/reactivity on a
|
|
458
|
+
// later update. That fork is gone: every inner loop now gets the full
|
|
459
|
+
// `mapArray` emission. Use a `__innerIdx<uid>` suffix in the regex to
|
|
460
|
+
// specifically target the nested `mapArray` (vs. the unsuffixed
|
|
461
|
+
// `static-array-child-init` for a fully-static OUTER array).
|
|
452
462
|
const source = `
|
|
453
463
|
'use client'
|
|
454
464
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -480,8 +490,12 @@ describe('plain nested loops without conditional wrapper', () => {
|
|
|
480
490
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
481
491
|
const js = result.files.find(f => f.type === 'clientJs')!.content
|
|
482
492
|
|
|
483
|
-
// The
|
|
484
|
-
// assert preamble appears in that body
|
|
485
|
-
|
|
493
|
+
// The inner mapArray's renderItem uses a `__innerIdx<uid>` suffix —
|
|
494
|
+
// assert the preamble appears in that body, re-derived from the
|
|
495
|
+
// ACCESSOR form of both the outer and inner loop params (`row()`,
|
|
496
|
+
// `cell()`), and precedes the child-component prop read.
|
|
497
|
+
expect(js).toMatch(
|
|
498
|
+
/mapArray\(\(\) => SHARED \|\| \[\], .*?, \(cell, __innerIdx\d+_\d+, __existing\) => \{[\s\S]*?const\s+tag\s*=\s*`\$\{row\(\)\.key\}-\$\{cell\(\)\.n\}`[\s\S]*?upsertChild/
|
|
499
|
+
)
|
|
486
500
|
})
|
|
487
501
|
})
|
|
@@ -140,8 +140,10 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
140
140
|
const content = result.files.find((f) => f.type === 'clientJs')!.content
|
|
141
141
|
|
|
142
142
|
// `data-off` uses the truthy-check shape (no `__v != null` for this
|
|
143
|
-
// attribute) so a concrete `false` removes the attribute.
|
|
144
|
-
|
|
143
|
+
// attribute) so a concrete `false` removes the attribute. The dedup
|
|
144
|
+
// guard (#2869) computes into `__x` first, so the truthy check reads
|
|
145
|
+
// `__x`, not `c().isOff` inline.
|
|
146
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?const __x = c\(\)\.isOff[\s\S]*?if \(__x\)\s*\S+\.setAttribute\('data-off',\s*''\)/)
|
|
145
147
|
// aria-* keeps the explicit "true" value per WAI-ARIA.
|
|
146
148
|
expect(content).toContain("setAttribute('aria-pressed', 'true')")
|
|
147
149
|
})
|
|
@@ -233,8 +235,11 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
233
235
|
// and the inner-loop reactive `createEffect` (must be `task().id`
|
|
234
236
|
// to subscribe to the per-item accessor). The bug we're locking
|
|
235
237
|
// down is purely in the createEffect emission, so scope to that.
|
|
238
|
+
// The object literal now sits in the dedup guard's `const __x = {...}`
|
|
239
|
+
// temp (#2869) — `styleToCss` reads it back as `styleToCss(__x)`, not
|
|
240
|
+
// the object literal inline.
|
|
236
241
|
const effectMatch = content.match(
|
|
237
|
-
/createEffect\(\(\) => \{[\s\S]*?
|
|
242
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = \{[\s\S]*?\}[\s\S]*?styleToCss\(__x\)[\s\S]*?__ta_s\d+\.setAttribute\('style'/,
|
|
238
243
|
)
|
|
239
244
|
expect(effectMatch).not.toBeNull()
|
|
240
245
|
const effectBody = effectMatch![0]
|
|
@@ -282,8 +287,11 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
282
287
|
const result = compileJSX(source, 'G.tsx', { adapter })
|
|
283
288
|
expect(result.errors).toHaveLength(0)
|
|
284
289
|
const content = result.files.find((f) => f.type === 'clientJs')!.content
|
|
290
|
+
// The object literal now sits in the dedup guard's `const __x = {...}`
|
|
291
|
+
// temp (#2869) — `styleToCss` reads it back as `styleToCss(__x)`, not
|
|
292
|
+
// the object literal inline.
|
|
285
293
|
const effectMatch = content.match(
|
|
286
|
-
/createEffect\(\(\) => \{[\s\S]*?
|
|
294
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = \{[\s\S]*?\}[\s\S]*?styleToCss\(__x\)[\s\S]*?__ta_s\d+\.setAttribute\('style'/,
|
|
287
295
|
)
|
|
288
296
|
expect(effectMatch).not.toBeNull()
|
|
289
297
|
const effectBody = effectMatch![0]
|