@barefootjs/jsx 0.26.3 → 0.27.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/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +735 -140
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +12 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +27 -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 +13 -2
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.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 +19 -3
- 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 +63 -4
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts +8 -3
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +73 -1
- 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/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +35 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +555 -678
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +26 -12
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/composite-branch-loop.test.ts +2 -2
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- package/src/__tests__/flatmap-segments.test.ts +182 -0
- package/src/__tests__/inline-jsx-callback.test.ts +6 -4
- package/src/__tests__/ir-jsx-props.test.ts +6 -5
- package/src/__tests__/loop-fallback-wrap.test.ts +3 -4
- package/src/__tests__/map-body-no-silent-divergence.test.ts +45 -0
- package/src/__tests__/module-level-signal.audit.test.ts +7 -2
- package/src/__tests__/nested-loop-conditional.test.ts +9 -7
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
- package/src/__tests__/preamble-region-patch.test.ts +156 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
- package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
- package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
- package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/build-references.ts +16 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +74 -14
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +12 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +11 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +36 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/shared.ts +27 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +67 -19
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +35 -16
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +80 -14
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +1 -1
- package/src/ir-to-client-js/element-refs.ts +9 -11
- package/src/ir-to-client-js/emit-reactive.ts +50 -14
- package/src/ir-to-client-js/html-template.ts +144 -15
- package/src/ir-to-client-js/imports.ts +7 -4
- package/src/ir-to-client-js/reactivity.ts +7 -0
- package/src/ir-to-client-js/types.ts +34 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +382 -15
- package/src/types.ts +72 -0
|
@@ -258,6 +258,51 @@ function T({ groups }: { groups: { id: string; items: { id: string; tags: string
|
|
|
258
258
|
})}</ul>
|
|
259
259
|
))}</div>
|
|
260
260
|
}
|
|
261
|
+
export { T }`,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'flatmap-expression-body',
|
|
265
|
+
// The unbraced twin of flatmap-block-body: `t => t.tags.map(...)` with no
|
|
266
|
+
// block. Pre-fix this fell through every dispatch arm to the IRExpression
|
|
267
|
+
// scalar path and spliced the raw callback — JSX included — verbatim into
|
|
268
|
+
// the client bundle (a silent SyntaxError caught only by parseErrors).
|
|
269
|
+
dslTier: true,
|
|
270
|
+
source: `
|
|
271
|
+
function T({ items }: { items: { id: string; tags: string[] }[] }) {
|
|
272
|
+
return <ul>{items.flatMap((it) => it.tags.map((t) => <li key={t}>{t}</li>))}</ul>
|
|
273
|
+
}
|
|
274
|
+
export { T }`,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'flatmap-expression-body-parenthesized',
|
|
278
|
+
source: `
|
|
279
|
+
function T({ items }: { items: { id: string; tags: string[] }[] }) {
|
|
280
|
+
return <ul>{items.flatMap((it) => (it.tags.map((t) => <li key={t}>{t}</li>)))}</ul>
|
|
281
|
+
}
|
|
282
|
+
export { T }`,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'flatmap-expression-body-signal-array',
|
|
286
|
+
source: `
|
|
287
|
+
'use client'
|
|
288
|
+
import { createSignal } from '@barefootjs/client'
|
|
289
|
+
function T() {
|
|
290
|
+
const [items] = createSignal([{ id: '1', tags: ['a'] }])
|
|
291
|
+
return <ul>{items().flatMap((it) => it.tags.map((t) => <li key={t}>{t}</li>))}</ul>
|
|
292
|
+
}
|
|
293
|
+
export { T }`,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: 'map-jsx-inside-unrecognized-call',
|
|
297
|
+
// An inline JSX literal in a body shape no dispatch arm recognizes (a call
|
|
298
|
+
// wrapping JSX). The structural net refuses loudly instead of letting the
|
|
299
|
+
// scalar fallback splice the raw JSX into the bundle.
|
|
300
|
+
dslTier: true,
|
|
301
|
+
source: `
|
|
302
|
+
declare function wrap(x: unknown): unknown
|
|
303
|
+
function T({ items }: { items: { id: string }[] }) {
|
|
304
|
+
return <ul>{items.map((it) => wrap(<li key={it.id}>{it.id}</li>))}</ul>
|
|
305
|
+
}
|
|
261
306
|
export { T }`,
|
|
262
307
|
},
|
|
263
308
|
{
|
|
@@ -234,8 +234,13 @@ export function Counter() {
|
|
|
234
234
|
const ssr = files['Counter.tsx']
|
|
235
235
|
// SSR template should NOT declare count as a getter
|
|
236
236
|
expect(ssr).not.toContain('const count = () =>')
|
|
237
|
-
//
|
|
238
|
-
|
|
237
|
+
// Slot unification Step B: `{count()}` is the SPAN's only, non-adjacent
|
|
238
|
+
// child, so `client-only-elision.ts` elides its marker pair entirely —
|
|
239
|
+
// no `bfText`/`bfTextEnd` call reaches the SSR output at all. The
|
|
240
|
+
// client's claim plan (`Counter.client.js`) creates the missing Text
|
|
241
|
+
// node from `elidedPath` alone; see `claim-slots.test.ts`'s
|
|
242
|
+
// "markerless text kind" describe block for the runtime contract.
|
|
243
|
+
expect(ssr).not.toContain('bfText(')
|
|
239
244
|
})
|
|
240
245
|
|
|
241
246
|
test('full compile: exported signal uses `export const` in client JS', () => {
|
|
@@ -134,13 +134,14 @@ describe('nested loops/conditionals inside mapArray (#830, #839)', () => {
|
|
|
134
134
|
expect(mapArrayCount).toBeGreaterThanOrEqual(3)
|
|
135
135
|
})
|
|
136
136
|
|
|
137
|
-
test('reactive text inside conditional inside inner loop uses re-
|
|
137
|
+
test('reactive text inside conditional inside inner loop uses re-claim pattern (#840)', () => {
|
|
138
138
|
// When a reactive text is inside a conditional branch inside a nested (inner) loop,
|
|
139
139
|
// insert() may replace the SSR element after the text node is captured.
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
140
|
+
// Slot unification A3: the generated code must re-claim on EVERY run via
|
|
141
|
+
// a fresh `claimSlots(...).write(...)` call inside the effect body — not
|
|
142
|
+
// a `lazySlots` writer built once outside it, which would go stale once
|
|
143
|
+
// insert() swaps the branch's DOM (see `stringifyInnerLoops`' docstring
|
|
144
|
+
// in `ir-to-client-js/control-flow/stringify/inner-loop.ts`).
|
|
144
145
|
const source = `
|
|
145
146
|
'use client'
|
|
146
147
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -175,8 +176,9 @@ describe('nested loops/conditionals inside mapArray (#830, #839)', () => {
|
|
|
175
176
|
expect(clientJs).toBeDefined()
|
|
176
177
|
const content = clientJs!.content
|
|
177
178
|
|
|
178
|
-
// Re-
|
|
179
|
-
|
|
179
|
+
// Re-claim pattern: a fresh `claimSlots(...)` call inside createEffect so
|
|
180
|
+
// it always resolves against the live node, never a cached stale ref.
|
|
181
|
+
expect(content).toMatch(/createEffect\(\(\) => \{ claimSlots\(__innerEl\w*, \[\{ id: 's\d+', kind: 'text', path: \[\] \}\]\)\.write\('s\d+', String\(/)
|
|
180
182
|
})
|
|
181
183
|
|
|
182
184
|
test('event handler inside conditional branch of loop item appears in bindEvents (#839)', () => {
|
|
@@ -339,7 +339,7 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
339
339
|
// The innermost loop's per-item renderItem must emit BOTH a
|
|
340
340
|
// reactive-text effect for `panel.text` and a reactive-attr effect
|
|
341
341
|
// for `panel.cls` — the bug dropped only the former.
|
|
342
|
-
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]
|
|
342
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?__bfw_\w+\('s\d+', String\(panel\(\)\.text\)\)/)
|
|
343
343
|
expect(content).toContain("setAttribute('class'")
|
|
344
344
|
})
|
|
345
345
|
|
|
@@ -395,7 +395,7 @@ describe('reactive attributes inside a nested .map() body (#135)', () => {
|
|
|
395
395
|
// textContent write — `labelAt(` also appears in the static template
|
|
396
396
|
// clone, so asserting it independently would pass even with the
|
|
397
397
|
// effect missing (the exact regression here).
|
|
398
|
-
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]
|
|
398
|
+
expect(content).toMatch(/createEffect\(\(\) => \{[\s\S]*?__bfw_\w+\('s\d+', String\(labelAt\(pi\)\)\)/)
|
|
399
399
|
expect(content).toContain("setAttribute('class'")
|
|
400
400
|
})
|
|
401
401
|
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2389 patch-on-update: a keyed `.map()` row body whose preamble builds
|
|
3
|
+
* content from item state (`const stateLabel = t.done ? ... ; const cells
|
|
4
|
+
* = []; cells.push(<td>{stateLabel}</td>)`) goes STALE on a same-key item
|
|
5
|
+
* update — `mapArray` reuses the row via per-item `setItem`, re-running
|
|
6
|
+
* only the row's wired text/attr slots. `{cells}` had no slot wiring at
|
|
7
|
+
* all, so it froze at its mount-time content forever while the sibling
|
|
8
|
+
* `{t.name}` text slot updated normally.
|
|
9
|
+
*
|
|
10
|
+
* The fix: a loop-body expression child whose free identifiers intersect
|
|
11
|
+
* the preamble's `declaredNames` is classified as a preamble-patched
|
|
12
|
+
* region — slot-marked like an ordinary reactive text (so SSR/CSR row
|
|
13
|
+
* templates render `<!--bf:sN-->...<!--/-->` / `{bfText("sN")}` the same
|
|
14
|
+
* door a reactive text uses), but wired on the client via a claimed
|
|
15
|
+
* 'markup' slot writer (`@barefootjs/client/runtime/claim-slots.ts`) rather
|
|
16
|
+
* than a `reactiveTexts` `.textContent` assignment (which would corrupt the
|
|
17
|
+
* array-joined markup). Dedup — skip the DOM write when the new value
|
|
18
|
+
* matches the writer's own held `last` value — lives inside that writer;
|
|
19
|
+
* every write, including the first, applies unless deduped (see
|
|
20
|
+
* `claim-slots.ts`'s module docstring).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, test, expect } from 'bun:test'
|
|
24
|
+
import { compileJSX } from '../compiler'
|
|
25
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
26
|
+
import { HonoAdapter } from '../../../adapter-hono/src/index.ts'
|
|
27
|
+
|
|
28
|
+
const ROW_SOURCE = `
|
|
29
|
+
'use client'
|
|
30
|
+
import { createSignal } from '@barefootjs/client'
|
|
31
|
+
export function Todos() {
|
|
32
|
+
const [todos, setTodos] = createSignal([
|
|
33
|
+
{ id: 1, name: 'a', done: false },
|
|
34
|
+
{ id: 2, name: 'b', done: false },
|
|
35
|
+
])
|
|
36
|
+
const toggle = (id: number) =>
|
|
37
|
+
setTodos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t))
|
|
38
|
+
return (
|
|
39
|
+
<table><tbody>
|
|
40
|
+
{todos().map((t) => {
|
|
41
|
+
const stateLabel = t.done ? 'done & dusted' : 'open'
|
|
42
|
+
const cells = []
|
|
43
|
+
cells.push(<td className="state">{stateLabel}</td>)
|
|
44
|
+
return (
|
|
45
|
+
<tr key={t.id}>
|
|
46
|
+
{cells}
|
|
47
|
+
<td>{t.name}</td>
|
|
48
|
+
<td><button onClick={() => toggle(t.id)}>toggle</button></td>
|
|
49
|
+
</tr>
|
|
50
|
+
)
|
|
51
|
+
})}
|
|
52
|
+
</tbody></table>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
function compileWith(adapter: TestAdapter | HonoAdapter, source: string = ROW_SOURCE) {
|
|
58
|
+
const result = compileJSX(source, 'Todos.tsx', { adapter })
|
|
59
|
+
expect(result.errors.filter((e) => e.severity === 'error')).toHaveLength(0)
|
|
60
|
+
const clientJs = result.files.find((f) => f.type === 'clientJs')
|
|
61
|
+
const marked = result.files.find((f) => f.type === 'markedTemplate' || f.type === 'ssr')
|
|
62
|
+
expect(clientJs).toBeDefined()
|
|
63
|
+
return { clientJs: clientJs!.content, marked: marked?.content, result }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('preamble-region-patch (#2389)', () => {
|
|
67
|
+
test('(a) row template + SSR marked template both carry the region slot marker', () => {
|
|
68
|
+
const { clientJs } = compileWith(new TestAdapter())
|
|
69
|
+
// Row template (the `__tpl.innerHTML = ...` string literal inside
|
|
70
|
+
// renderItem): the region renders as a paired comment marker around the
|
|
71
|
+
// array-joined value, exactly like a reactive text slot.
|
|
72
|
+
expect(clientJs).toMatch(/<!--bf:s\d+-->\$\{Array\.isArray\(cells\) \? cells\.join\(''\) : \(cells \?\? ''\)\}<!--\/-->/)
|
|
73
|
+
|
|
74
|
+
const hono = compileWith(new HonoAdapter())
|
|
75
|
+
expect(hono.clientJs).toContain("kind: 'markup'")
|
|
76
|
+
expect(hono.clientJs).toContain('lazySlots(__el')
|
|
77
|
+
// Hono SSR renders the SAME slotId through `renderExpression`'s generic
|
|
78
|
+
// `{bfText("id")}...{bfTextEnd()}` door — no bespoke region handling.
|
|
79
|
+
const slotMatch = /<!--bf:(s\d+)-->\$\{Array\.isArray\(cells\)/.exec(hono.clientJs)
|
|
80
|
+
expect(slotMatch).not.toBeNull()
|
|
81
|
+
const slotId = slotMatch![1]
|
|
82
|
+
expect(hono.marked).toContain(`{bfText("${slotId}")}`)
|
|
83
|
+
expect(hono.marked).toContain('{cells}')
|
|
84
|
+
expect(hono.marked).toContain('{bfTextEnd()}')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('(b) renderItem emits the region-patch effect, re-running the preamble in accessor form', () => {
|
|
88
|
+
const { clientJs } = compileWith(new TestAdapter())
|
|
89
|
+
expect(clientJs).toContain("kind: 'markup'")
|
|
90
|
+
// The preamble re-runs inside the effect with the loop-param accessor
|
|
91
|
+
// wrap (`t().done`), not the plain (`t.done`) form used at the
|
|
92
|
+
// top-level construction line.
|
|
93
|
+
expect(clientJs).toMatch(/createEffect\(\(\) => \{\s*const stateLabel = t\(\)\.done/)
|
|
94
|
+
// The write goes through the claimed 'markup' writer — dedup (skip an
|
|
95
|
+
// unchanged string, on every write including the first) now lives
|
|
96
|
+
// inside `writeMarkup` itself (`claim-slots.ts`), not a per-region
|
|
97
|
+
// `__last` local next to the write call.
|
|
98
|
+
expect(clientJs).toMatch(/__bfw_\w+\('s\d+', Array\.isArray\(cells\)/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('(c) a loop without a preamble gets no region', () => {
|
|
102
|
+
const source = `
|
|
103
|
+
'use client'
|
|
104
|
+
import { createSignal } from '@barefootjs/client'
|
|
105
|
+
export function Plain() {
|
|
106
|
+
const [items, setItems] = createSignal([{ id: 1, name: 'a' }])
|
|
107
|
+
return <ul>{items().map(t => <li key={t.id}>{t.name}</li>)}</ul>
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
const { clientJs } = compileWith(new TestAdapter(), source)
|
|
111
|
+
expect(clientJs).not.toContain("kind: 'markup'")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('(c) a static-array loop with a preamble gets no region', () => {
|
|
115
|
+
const source = `
|
|
116
|
+
import { createSignal } from '@barefootjs/client'
|
|
117
|
+
const items = [{ id: 1, done: false }, { id: 2, done: true }]
|
|
118
|
+
export function StaticRows() {
|
|
119
|
+
return (
|
|
120
|
+
<table><tbody>
|
|
121
|
+
{items.map((t) => {
|
|
122
|
+
const stateLabel = t.done ? 'done' : 'open'
|
|
123
|
+
const cells = []
|
|
124
|
+
cells.push(<td>{stateLabel}</td>)
|
|
125
|
+
return <tr key={t.id}>{cells}<td>{t.id}</td></tr>
|
|
126
|
+
})}
|
|
127
|
+
</tbody></table>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
`
|
|
131
|
+
const { clientJs } = compileWith(new TestAdapter(), source)
|
|
132
|
+
expect(clientJs).not.toContain("kind: 'markup'")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('(d) a preamble local never referenced by an expression child gets no region', () => {
|
|
136
|
+
const source = `
|
|
137
|
+
'use client'
|
|
138
|
+
import { createSignal } from '@barefootjs/client'
|
|
139
|
+
export function Unread() {
|
|
140
|
+
const [todos, setTodos] = createSignal([{ id: 1, name: 'a', done: false }])
|
|
141
|
+
return (
|
|
142
|
+
<ul>
|
|
143
|
+
{todos().map((t) => {
|
|
144
|
+
// Declared but never read as a bare expression child anywhere
|
|
145
|
+
// in the returned JSX — nothing qualifies for a region.
|
|
146
|
+
const stateLabel = t.done ? 'done' : 'open'
|
|
147
|
+
return <li key={t.id}>{t.name}</li>
|
|
148
|
+
})}
|
|
149
|
+
</ul>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
`
|
|
153
|
+
const { clientJs } = compileWith(new TestAdapter(), source)
|
|
154
|
+
expect(clientJs).not.toContain("kind: 'markup'")
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -60,8 +60,9 @@ describe('conditional binding-effect ids (#1795 Phase 1)', () => {
|
|
|
60
60
|
const on = clientJs(true)
|
|
61
61
|
// Branch reactive attribute effect (createDisposableEffect with the class write).
|
|
62
62
|
expect(on).toMatch(/setAttribute\('class'[\s\S]*?}, "Disclosure#binding:s\d+"\)\)/)
|
|
63
|
-
// Branch reactive text effect (
|
|
64
|
-
|
|
63
|
+
// Branch reactive text effect (claimed 'markup' slot writer, slot
|
|
64
|
+
// unification A3).
|
|
65
|
+
expect(on).toMatch(/__bfw_\w+\([\s\S]*?}, "Disclosure#binding:s\d+"\)\)/)
|
|
65
66
|
})
|
|
66
67
|
|
|
67
68
|
test('buildIdIndex resolves conditional / attribute / text binding ids to source loc', () => {
|
|
@@ -50,9 +50,10 @@ describe('loop-child binding-effect ids (#1795 Phase 2)', () => {
|
|
|
50
50
|
|
|
51
51
|
test('profile on: loop-child text effects carry #binding ids', () => {
|
|
52
52
|
const on = clientJs(true)
|
|
53
|
-
// Both `{it.t}` and `{it.n}` text effects
|
|
54
|
-
|
|
55
|
-
expect(on).toMatch(/
|
|
53
|
+
// Both `{it.t}` and `{it.n}` text effects write through the row's
|
|
54
|
+
// claimed 'text' slot writer (slot unification A3).
|
|
55
|
+
expect(on).toMatch(/__bfw_\w+\('s\d+', String\(it\(\)\.t\)\) }, "List#binding:s\d+"\)/)
|
|
56
|
+
expect(on).toMatch(/__bfw_\w+\('s\d+', String\(it\(\)\.n\)\) }, "List#binding:s\d+"\)/)
|
|
56
57
|
})
|
|
57
58
|
|
|
58
59
|
test('profile on: the loop-child attribute effect carries a #binding id', () => {
|
|
@@ -112,12 +112,17 @@ describe('nested loop binding ids (#1795 Phase 3)', () => {
|
|
|
112
112
|
test('loop-child conditional insert() and its branch text carry ids', () => {
|
|
113
113
|
const on = clientJs(nestedSource, 'Nested', true)
|
|
114
114
|
// The branch-arm text (`{r.label}` inside the conditional's true arm) —
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// stringifying
|
|
118
|
-
|
|
115
|
+
// a claimed 'markup' slot writer (slot unification A3), built fresh in
|
|
116
|
+
// the branch's own bindEvents, so a Child-position expression whose
|
|
117
|
+
// value is a live Node splices by identity instead of stringifying
|
|
118
|
+
// (#2347) — the same contract `__bfText` used to provide. Wrapped in
|
|
119
|
+
// `escapeTextOrNode` (A3 follow-up): a string value must be escaped
|
|
120
|
+
// before this 'markup' writer's `innerHTML =` insertion, matching what
|
|
121
|
+
// the initial SSR/CSR template already does for the same expression;
|
|
122
|
+
// a live Node passes through untouched.
|
|
123
|
+
expect(on).toMatch(/__bfw_\w+\('s\d+', escapeTextOrNode\(r\(\)\.label\)\) }, "Nested#binding:s\d+"\)/)
|
|
119
124
|
// The inner loop's child text (`{t}`).
|
|
120
|
-
expect(on).toMatch(/String\(t\(\)\) }, "Nested#binding:s\d+"\)/)
|
|
125
|
+
expect(on).toMatch(/String\(t\(\)\)\) }, "Nested#binding:s\d+"\)/)
|
|
121
126
|
})
|
|
122
127
|
|
|
123
128
|
test('branch-scoped loop (loop inside a conditional) attributes its mapArray', () => {
|
|
@@ -132,7 +137,7 @@ describe('nested loop binding ids (#1795 Phase 3)', () => {
|
|
|
132
137
|
const emitted = assertNoCoverageGap(staticLoopSource, 'StaticLoop')
|
|
133
138
|
expect(emitted.length).toBeGreaterThanOrEqual(1)
|
|
134
139
|
const on = clientJs(staticLoopSource, 'StaticLoop', true)
|
|
135
|
-
expect(on).toMatch(/
|
|
140
|
+
expect(on).toMatch(/__bfw_\w+\('s\d+', String\(n\(\)\)\) }, "StaticLoop#binding:s\d+"\)/)
|
|
136
141
|
})
|
|
137
142
|
|
|
138
143
|
test('component loop attributes its mapArray', () => {
|
|
@@ -68,7 +68,7 @@ describe('reactive attributes inside .map() callbacks', () => {
|
|
|
68
68
|
expect(clientJs!.content).toContain('disabled')
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
test('dynamic array: reactive className handled by
|
|
71
|
+
test('dynamic array: reactive className handled by mapArray re-render (no extra effect needed)', () => {
|
|
72
72
|
const source = `
|
|
73
73
|
'use client'
|
|
74
74
|
|
|
@@ -91,7 +91,7 @@ describe('reactive attributes inside .map() callbacks', () => {
|
|
|
91
91
|
|
|
92
92
|
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
93
93
|
expect(clientJs).toBeDefined()
|
|
94
|
-
// Dynamic arrays use
|
|
94
|
+
// Dynamic arrays use mapArray which re-creates items on signal change
|
|
95
95
|
expect(clientJs!.content).toContain('mapArray')
|
|
96
96
|
})
|
|
97
97
|
|
|
@@ -228,9 +228,11 @@ describe('reactive attributes inside .map() callbacks', () => {
|
|
|
228
228
|
const result = compileJSX(source, 'L.tsx', { adapter })
|
|
229
229
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
230
230
|
const js = result.files.find(f => f.type === 'clientJs')!.content
|
|
231
|
-
// Both effects must still be present.
|
|
231
|
+
// Both effects must still be present. The text effect writes through a
|
|
232
|
+
// claimed 'text' slot writer now (slot unification A3), not a bare
|
|
233
|
+
// `.textContent =` assignment.
|
|
232
234
|
expect(js).toContain("setAttribute('class'")
|
|
233
|
-
expect(js).toMatch(/
|
|
235
|
+
expect(js).toMatch(/__bfw_\w+\('s\d+', String\(item\.name\)\)/)
|
|
234
236
|
// But there must be exactly one `items.forEach` call (not two).
|
|
235
237
|
const forEachMatches = js.match(/items\.forEach\(/g) ?? []
|
|
236
238
|
expect(forEachMatches.length).toBe(1)
|
|
@@ -124,8 +124,9 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
124
124
|
const m = clientJs.match(/if \(!__iterEl\) \{[\s\S]*?\n\s+\}\n\s+if \(__iterEl\)/)
|
|
125
125
|
expect(m).toBeTruthy()
|
|
126
126
|
const block = m![0]
|
|
127
|
-
// The cloned template must reference `emoji` directly
|
|
128
|
-
|
|
127
|
+
// The cloned template must reference `emoji` directly (possibly through
|
|
128
|
+
// the escapeAttr/escapeText interpolation wrappers).
|
|
129
|
+
expect(block).toMatch(/\$\{(?:escape(?:Attr|Text)\()?emoji\)?\}/)
|
|
129
130
|
// It must NOT reference `__bfItem()` — that accessor only exists
|
|
130
131
|
// inside `mapArray` renderItems, not inside a plain forEach.
|
|
131
132
|
expect(block).not.toMatch(/__bfItem\(\)/)
|
|
@@ -203,7 +203,7 @@ describe('#1128 — template body never reaches init-scope identifiers', () => {
|
|
|
203
203
|
|
|
204
204
|
expect(tpl).not.toMatch(/\bitems\b/)
|
|
205
205
|
// Bare `undefined.map(...)` would throw; substitute `[]` so the
|
|
206
|
-
// template renders an empty list and
|
|
206
|
+
// template renders an empty list and mapArray populates it.
|
|
207
207
|
expect(tpl).toMatch(/\$\{\[\]\.map\(/)
|
|
208
208
|
})
|
|
209
209
|
|
package/src/compiler.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { analyzeComponent, listComponentFunctions, createProgramForFile, needsTy
|
|
|
16
16
|
import { jsxToIR } from './jsx-to-ir.ts'
|
|
17
17
|
import { stripClientBuiltinImports } from './builtins.ts'
|
|
18
18
|
import { generateClientJs, generateClientJsWithSourceMap, analyzeClientNeeds } from './ir-to-client-js/index.ts'
|
|
19
|
+
import { decideClientOnlyElision } from './ir-to-client-js/client-only-elision.ts'
|
|
19
20
|
import { emitModuleLevelDeclarations } from './ir-to-client-js/emit-module-level.ts'
|
|
20
21
|
import { RUNTIME_MODULE, detectUsedImports as detectUsedImportsFromCode } from './ir-to-client-js/imports.ts'
|
|
21
22
|
import { setActiveComponentScope, computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
@@ -140,6 +141,10 @@ function compileMultipleComponents(
|
|
|
140
141
|
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR)
|
|
141
142
|
checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors)
|
|
142
143
|
|
|
144
|
+
// Slot unification Step B — see the single-component path's identical
|
|
145
|
+
// call for why this must run before adapter.generate/generateClientJs.
|
|
146
|
+
decideClientOnlyElision(componentIR.root)
|
|
147
|
+
|
|
143
148
|
if (options.cssLayerPrefix) {
|
|
144
149
|
applyCssLayerPrefix(componentIR, options.cssLayerPrefix)
|
|
145
150
|
}
|
|
@@ -619,6 +624,12 @@ export function compileJSX(
|
|
|
619
624
|
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR)
|
|
620
625
|
checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors)
|
|
621
626
|
|
|
627
|
+
// Slot unification Step B (`spec/slot-unification.md` §5 Step B): decide
|
|
628
|
+
// marker elision ONCE, mutating `componentIR.root` in place, before either
|
|
629
|
+
// `adapter.generate` or `generateClientJs` run below — both must see the
|
|
630
|
+
// SAME `markerless`/`elidedPath` flags on the SAME IR nodes.
|
|
631
|
+
decideClientOnlyElision(componentIR.root)
|
|
632
|
+
|
|
622
633
|
// Cross-file @client signal sources: identify which import sources
|
|
623
634
|
// need `.client.js` path rewriting in the client bundle.
|
|
624
635
|
if (ctx.importedClientSignalNames.size > 0) {
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export { createProgramForCorpus, type SharedProgramOptions } from './shared-prog
|
|
|
65
65
|
|
|
66
66
|
// JSX to IR transformer
|
|
67
67
|
export { jsxToIR } from './jsx-to-ir.ts'
|
|
68
|
+
export { decideClientOnlyElision } from './ir-to-client-js/client-only-elision.ts'
|
|
68
69
|
|
|
69
70
|
// Module exports generation (compiler layer)
|
|
70
71
|
export { generateModuleExports, extractFunctionParams, formatParamWithType, findReachableNames } from './module-exports.ts'
|
|
@@ -153,6 +153,13 @@ export function buildReferencesGraph(ctx: ClientJsContext, irRoot: IRNode): Refe
|
|
|
153
153
|
if (elem.filterPredicate) addExprEdges(ROOT_SOURCE, elem.filterPredicate.raw, 'template-closure')
|
|
154
154
|
if (elem.sortComparator) addExprEdges(ROOT_SOURCE, elem.sortComparator.raw, 'template-closure')
|
|
155
155
|
if (elem.preamble) addExprEdges(ROOT_SOURCE, preambleAnalysisText(elem.preamble), 'template-closure')
|
|
156
|
+
// flatMap descriptor bodies (`elem.flatMapClient`) are NOT traced here:
|
|
157
|
+
// the rendered body embeds leaf HTML template literals, and the regex
|
|
158
|
+
// identifier extractor would turn tag/attr names into false-positive
|
|
159
|
+
// edges. The Phase 3 loop visitor traces the same content structurally
|
|
160
|
+
// (js segments via `preambleAnalysisText`, leaf attrs/keys/interpolations
|
|
161
|
+
// via `walkIR` on `flatMapCallback` segments) — that's what keeps
|
|
162
|
+
// `const maxTags = _p.maxTags` in the extracted props.
|
|
156
163
|
for (const attr of elem.bindings.reactiveAttrs) {
|
|
157
164
|
addExprEdges(ROOT_SOURCE, attr.expression, 'template-closure')
|
|
158
165
|
}
|
|
@@ -318,6 +325,15 @@ export function buildReferencesGraph(ctx: ClientJsContext, irRoot: IRNode): Refe
|
|
|
318
325
|
if (l.filterPredicate) addExprEdges(ROOT_SOURCE, l.filterPredicate.raw, 'template-closure')
|
|
319
326
|
if (l.sortComparator) addExprEdges(ROOT_SOURCE, l.sortComparator.raw, 'template-closure')
|
|
320
327
|
if (l.preamble) addExprEdges(ROOT_SOURCE, preambleAnalysisText(l.preamble), 'template-closure')
|
|
328
|
+
// flatMap bodies: js segments reference init-scope names; segment
|
|
329
|
+
// leaves are IR nodes off `children`, so `descend()` never reaches
|
|
330
|
+
// them — walk them explicitly (mirrors attachParsedExpressions).
|
|
331
|
+
if (l.flatMapCallback) {
|
|
332
|
+
addExprEdges(ROOT_SOURCE, preambleAnalysisText(l.flatMapCallback), 'template-closure')
|
|
333
|
+
for (const seg of l.flatMapCallback.segments) {
|
|
334
|
+
if (seg.kind === 'jsx') walkIR(seg.ir, null, visitor)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
321
337
|
descend()
|
|
322
338
|
if (l.childComponent) walkChildComponent(l.childComponent)
|
|
323
339
|
if (l.nestedComponents) {
|