@barefootjs/jsx 0.26.4 → 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 +321 -104
- 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/shared.d.ts +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/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 +17 -20
- 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 +24 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +9 -1
- 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/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +398 -671
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +19 -11
- package/src/__tests__/composite-branch-loop.test.ts +2 -2
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- 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__/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 +18 -12
- 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__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +8 -4
- package/src/ir-to-client-js/control-flow/shared.ts +1 -1
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +12 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- 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 +48 -46
- 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 +29 -9
- package/src/ir-to-client-js/imports.ts +7 -4
- package/src/ir-to-client-js/reactivity.ts +3 -2
- package/src/ir-to-client-js/types.ts +10 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +8 -7
- package/src/types.ts +25 -2
|
@@ -211,7 +211,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
211
211
|
expect(clientJs!.content).toContain('toggleRow(index)')
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
-
test('dynamic signal array: component generates
|
|
214
|
+
test('dynamic signal array: component generates mapArray with createComponent', () => {
|
|
215
215
|
const source = `
|
|
216
216
|
'use client'
|
|
217
217
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -321,7 +321,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
321
321
|
const uniqueVars = new Set(allVars)
|
|
322
322
|
expect(allVars.length).toBe(uniqueVars.size)
|
|
323
323
|
|
|
324
|
-
// Component slot ref ($c) and
|
|
324
|
+
// Component slot ref ($c) and mapArray should both be present
|
|
325
325
|
expect(content).toContain('$c(__scope')
|
|
326
326
|
expect(content).toContain('mapArray')
|
|
327
327
|
})
|
|
@@ -355,7 +355,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
355
355
|
expect(clientJs).toBeDefined()
|
|
356
356
|
const content = clientJs!.content
|
|
357
357
|
|
|
358
|
-
// Should use
|
|
358
|
+
// Should use mapArray with createComponent
|
|
359
359
|
expect(content).toContain('mapArray')
|
|
360
360
|
expect(content).toContain("createComponent('TableRow'")
|
|
361
361
|
|
|
@@ -650,7 +650,7 @@ describe('child components inside .map() (#344)', () => {
|
|
|
650
650
|
expect(clientJs).toBeDefined()
|
|
651
651
|
const content = clientJs!.content
|
|
652
652
|
|
|
653
|
-
// Dynamic array should use
|
|
653
|
+
// Dynamic array should use mapArray and event delegation
|
|
654
654
|
expect(content).toContain('mapArray')
|
|
655
655
|
expect(content).toContain(".addEventListener('click', (__bfEvt) => {")
|
|
656
656
|
expect(content).toContain('handleClick(item.id)')
|
|
@@ -151,7 +151,7 @@ describe('Client JS generation', () => {
|
|
|
151
151
|
})
|
|
152
152
|
|
|
153
153
|
describe('map with index parameter', () => {
|
|
154
|
-
test('includes index parameter in
|
|
154
|
+
test('includes index parameter in mapArray renderItem callback', () => {
|
|
155
155
|
const source = `
|
|
156
156
|
'use client'
|
|
157
157
|
import { createMemo } from '@barefootjs/client'
|
|
@@ -1760,7 +1760,10 @@ describe('Client JS generation', () => {
|
|
|
1760
1760
|
// update. See `preamble-region-patch.test.ts` for the full region
|
|
1761
1761
|
// contract (row-template marker, region-patch effect, SSR parity).
|
|
1762
1762
|
expect(clientJs?.content).toContain('${escapeText(label)}')
|
|
1763
|
-
|
|
1763
|
+
// Slot unification A3: preamble regions now write through a claimed
|
|
1764
|
+
// 'markup' slot instead of `patchSlotRange`.
|
|
1765
|
+
expect(clientJs?.content).toContain("kind: 'markup'")
|
|
1766
|
+
expect(clientJs?.content).toContain('lazySlots(__el')
|
|
1764
1767
|
})
|
|
1765
1768
|
|
|
1766
1769
|
test('handles block body with multiple variable declarations', () => {
|
|
@@ -2423,11 +2426,12 @@ describe('Client JS generation', () => {
|
|
|
2423
2426
|
expect(clientJs).toBeDefined()
|
|
2424
2427
|
const content = clientJs!.content
|
|
2425
2428
|
|
|
2426
|
-
// Text node updates route through
|
|
2427
|
-
//
|
|
2428
|
-
//
|
|
2429
|
-
//
|
|
2430
|
-
|
|
2429
|
+
// Text node updates route through a claimed 'markup' slot writer (slot
|
|
2430
|
+
// unification A3), whose `writeMarkup` renders nullish values as ''
|
|
2431
|
+
// (not the string "undefined") and also splices live Nodes by
|
|
2432
|
+
// identity (#1663) — the same contract `__bfText` used to provide.
|
|
2433
|
+
// The nullish guard lives in the runtime writer, not inline here.
|
|
2434
|
+
expect(content).toContain("kind: 'markup'")
|
|
2431
2435
|
expect(content).not.toMatch(/\.nodeValue = String\(__val\)(?! )/)
|
|
2432
2436
|
})
|
|
2433
2437
|
|
|
@@ -2464,8 +2468,10 @@ describe('Client JS generation', () => {
|
|
|
2464
2468
|
expect(clientJs).toBeDefined()
|
|
2465
2469
|
const content = clientJs!.content
|
|
2466
2470
|
|
|
2467
|
-
// text() inside a component inside a conditional
|
|
2468
|
-
|
|
2471
|
+
// text() inside a component inside a conditional writes through a
|
|
2472
|
+
// claimed 'markup' slot built fresh in the branch's own bindEvents
|
|
2473
|
+
// (slot unification A3) — re-claimed on every branch activation.
|
|
2474
|
+
expect(content).toContain("lazySlots(__branchScope, [{ id: '^s2', kind: 'markup', path: [] }])")
|
|
2469
2475
|
})
|
|
2470
2476
|
|
|
2471
2477
|
test('propagates insideConditional through fragment children', () => {
|
|
@@ -2495,8 +2501,10 @@ describe('Client JS generation', () => {
|
|
|
2495
2501
|
expect(clientJs).toBeDefined()
|
|
2496
2502
|
const content = clientJs!.content
|
|
2497
2503
|
|
|
2498
|
-
// text() inside a fragment inside a conditional
|
|
2499
|
-
|
|
2504
|
+
// text() inside a fragment inside a conditional writes through a
|
|
2505
|
+
// claimed 'markup' slot built fresh in the branch's own bindEvents
|
|
2506
|
+
// (slot unification A3) — re-claimed on every branch activation.
|
|
2507
|
+
expect(content).toContain("lazySlots(__branchScope, [{ id: 's2', kind: 'markup', path: [] }])")
|
|
2500
2508
|
})
|
|
2501
2509
|
})
|
|
2502
2510
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* BarefootJS Compiler - Composite loops inside conditional branches (#724)
|
|
3
3
|
*
|
|
4
4
|
* Verifies that loops with child components inside ternary conditionals
|
|
5
|
-
* generate
|
|
5
|
+
* generate mapArray with createComponent in the branch's bindEvents.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, test, expect } from 'bun:test'
|
|
@@ -52,7 +52,7 @@ describe('composite loops inside conditional branches (#724)', () => {
|
|
|
52
52
|
expect(clientJs).toBeDefined()
|
|
53
53
|
const js = clientJs!.content
|
|
54
54
|
|
|
55
|
-
// Should use
|
|
55
|
+
// Should use mapArray inside the branch's bindEvents
|
|
56
56
|
expect(js).toContain('mapArray(')
|
|
57
57
|
|
|
58
58
|
// After O-1's mode-collapse PR, child component init goes through the
|
|
@@ -104,7 +104,11 @@ export function Viewer() {
|
|
|
104
104
|
const files = Object.fromEntries(r.files.map(f => [f.path, f.content]))
|
|
105
105
|
const ssr = Object.values(files).find(c => c.includes('export function Viewer'))
|
|
106
106
|
expect(ssr).toBeDefined()
|
|
107
|
-
|
|
107
|
+
// Slot unification Step B: `{val()}` is the SPAN's only, non-adjacent
|
|
108
|
+
// child, so its marker pair is elided entirely (see the identical note
|
|
109
|
+
// in module-level-signal.audit.test.ts) — the client's claim plan
|
|
110
|
+
// creates the missing Text node from `elidedPath` alone.
|
|
111
|
+
expect(ssr).not.toContain('bfText(')
|
|
108
112
|
expect(ssr).not.toContain("const val = () =>")
|
|
109
113
|
})
|
|
110
114
|
|
|
@@ -65,12 +65,10 @@ describe('JSX expression referencing a local declared inside an early-return if-
|
|
|
65
65
|
|
|
66
66
|
const content = clientJsContent(result)
|
|
67
67
|
|
|
68
|
-
// Pre-fix
|
|
69
|
-
//
|
|
70
|
-
// inlined directly into the template, so
|
|
71
|
-
//
|
|
72
|
-
// shape.
|
|
73
|
-
expect(content).not.toContain('updateClientMarker')
|
|
68
|
+
// Pre-fix, `aLocal` leaked into a reactive-update block at outer init
|
|
69
|
+
// scope where it doesn't exist (see file docstring). Post-fix: the JSX
|
|
70
|
+
// is inlined directly into the template, so no reactive-update block is
|
|
71
|
+
// needed at all for this static shape.
|
|
74
72
|
expect(content).not.toMatch(/\baLocal\b/)
|
|
75
73
|
|
|
76
74
|
const hydrate = hydrateLine(result)
|
|
@@ -97,9 +95,10 @@ describe('JSX expression referencing a local declared inside an early-return if-
|
|
|
97
95
|
const content = clientJsContent(result)
|
|
98
96
|
// The bare `label` identifier must not appear in the emitted init
|
|
99
97
|
// function — it's resolved to its initializer value at IR-build
|
|
100
|
-
// time.
|
|
101
|
-
|
|
102
|
-
expect(content).
|
|
98
|
+
// time. `@client` writes through a claimed 'text' slot writer
|
|
99
|
+
// (`claim-slots.ts`).
|
|
100
|
+
expect(content).not.toMatch(/__bfw_\w+\([^)]*\blabel\b[^)]*\)/)
|
|
101
|
+
expect(content).toContain("__bfw_s4('s4', 'hello')")
|
|
103
102
|
})
|
|
104
103
|
|
|
105
104
|
test('ternary scope var (`readOnly ? null : <jsx/>`) routes to client-only conditional', () => {
|
|
@@ -124,8 +123,8 @@ describe('JSX expression referencing a local declared inside an early-return if-
|
|
|
124
123
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
125
124
|
|
|
126
125
|
const content = clientJsContent(result)
|
|
127
|
-
// Pre-fix
|
|
128
|
-
//
|
|
126
|
+
// Pre-fix, `compactResize` leaked into the emitted reactive-update
|
|
127
|
+
// block as an undeclared name. Post-fix: the ternary becomes an
|
|
129
128
|
// IRConditional with `clientOnly: true`, routed through `insert()`
|
|
130
129
|
// — and `props.readOnly` is rewritten to `_p.readOnly`.
|
|
131
130
|
expect(content).not.toMatch(/\bcompactResize\b/)
|
|
@@ -440,10 +440,12 @@ describe('inline JSX as object-literal arrow value (#1663)', () => {
|
|
|
440
440
|
expect(js).toMatch(/hydrate\(\s*['"]BFInlineJsxCallback1(?:__|['"])/)
|
|
441
441
|
})
|
|
442
442
|
|
|
443
|
-
test('dynamic JSX-returning call routes the slot through
|
|
443
|
+
test('dynamic JSX-returning call routes the slot through a claimed markup slot', () => {
|
|
444
444
|
// The child expression `{themeLogo(props.id)}` evaluates to a live
|
|
445
445
|
// component element on the client; the slot update must splice it in by
|
|
446
|
-
// identity via
|
|
446
|
+
// identity via a claimed 'markup' slot writer (slot unification A3)
|
|
447
|
+
// rather than stringify it into "[object …]" — the same contract
|
|
448
|
+
// `__bfText` used to provide.
|
|
447
449
|
const source = `
|
|
448
450
|
'use client'
|
|
449
451
|
import { BrandLogo } from './brand-logo'
|
|
@@ -458,7 +460,7 @@ describe('inline JSX as object-literal arrow value (#1663)', () => {
|
|
|
458
460
|
}
|
|
459
461
|
`
|
|
460
462
|
const js = clientJs(source)
|
|
461
|
-
expect(js).toContain('
|
|
462
|
-
expect(js).toMatch(/
|
|
463
|
+
expect(js).toContain("kind: 'markup'")
|
|
464
|
+
expect(js).toMatch(/lazySlots[^']*from '@barefootjs\/client\/runtime'/)
|
|
463
465
|
})
|
|
464
466
|
})
|
|
@@ -359,11 +359,12 @@ describe('JSX props (#559)', () => {
|
|
|
359
359
|
|
|
360
360
|
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
361
361
|
expect(clientJs).toBeDefined()
|
|
362
|
-
// The text update routes through
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
362
|
+
// The text update routes through a claimed 'markup' slot writer (slot
|
|
363
|
+
// unification A3), whose `writeMarkup` preserves the server-rendered
|
|
364
|
+
// DOM for `__isSlot` values (and splices live Nodes by identity)
|
|
365
|
+
// instead of the old inline `nodeValue = String(...)` assignment —
|
|
366
|
+
// the same contract `__bfText` used to provide (#1663).
|
|
367
|
+
expect(clientJs!.content).toContain("kind: 'markup'")
|
|
367
368
|
})
|
|
368
369
|
})
|
|
369
370
|
})
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* and the conditional follow-up (#941), so this loop widening closes
|
|
19
19
|
* the divergence gap between the allow-list path and the AST-flag path.
|
|
20
20
|
*
|
|
21
|
-
* Over-reconciliation of a pure-call array has a real cost (
|
|
21
|
+
* Over-reconciliation of a pure-call array has a real cost (mapArray
|
|
22
22
|
* is not a cheap primitive), so the fixture sweep in the accompanying
|
|
23
23
|
* PR measures byte delta too; but the silent-drop bug this closes is
|
|
24
24
|
* the expensive one — a frozen client list is a visible correctness
|
|
@@ -89,8 +89,8 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
89
89
|
|
|
90
90
|
test('inline literal array stays static (optimisation preserved)', () => {
|
|
91
91
|
// `[1, 2, 3]` is an ArrayLiteralExpression with no calls — the
|
|
92
|
-
// existing static-render path is the right call. No
|
|
93
|
-
//
|
|
92
|
+
// existing static-render path is the right call. No `mapArray` call
|
|
93
|
+
// should appear.
|
|
94
94
|
const source = `
|
|
95
95
|
'use client'
|
|
96
96
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -107,7 +107,6 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
107
107
|
|
|
108
108
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
109
109
|
expect(clientJs).not.toMatch(/\bmapArray\s*\(/)
|
|
110
|
-
expect(clientJs).not.toMatch(/\breconcileList\s*\(/)
|
|
111
110
|
})
|
|
112
111
|
|
|
113
112
|
test('prop array compiles to mapArray (#1586)', () => {
|
|
@@ -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
|
})
|
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
* the preamble's `declaredNames` is classified as a preamble-patched
|
|
12
12
|
* region — slot-marked like an ordinary reactive text (so SSR/CSR row
|
|
13
13
|
* templates render `<!--bf:sN-->...<!--/-->` / `{bfText("sN")}` the same
|
|
14
|
-
* door a reactive text uses), but wired on the client via a
|
|
15
|
-
*
|
|
16
|
-
* `.textContent` assignment (which would corrupt the
|
|
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).
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { describe, test, expect } from 'bun:test'
|
|
@@ -68,7 +72,8 @@ describe('preamble-region-patch (#2389)', () => {
|
|
|
68
72
|
expect(clientJs).toMatch(/<!--bf:s\d+-->\$\{Array\.isArray\(cells\) \? cells\.join\(''\) : \(cells \?\? ''\)\}<!--\/-->/)
|
|
69
73
|
|
|
70
74
|
const hono = compileWith(new HonoAdapter())
|
|
71
|
-
expect(hono.clientJs).toContain('
|
|
75
|
+
expect(hono.clientJs).toContain("kind: 'markup'")
|
|
76
|
+
expect(hono.clientJs).toContain('lazySlots(__el')
|
|
72
77
|
// Hono SSR renders the SAME slotId through `renderExpression`'s generic
|
|
73
78
|
// `{bfText("id")}...{bfTextEnd()}` door — no bespoke region handling.
|
|
74
79
|
const slotMatch = /<!--bf:(s\d+)-->\$\{Array\.isArray\(cells\)/.exec(hono.clientJs)
|
|
@@ -81,15 +86,16 @@ describe('preamble-region-patch (#2389)', () => {
|
|
|
81
86
|
|
|
82
87
|
test('(b) renderItem emits the region-patch effect, re-running the preamble in accessor form', () => {
|
|
83
88
|
const { clientJs } = compileWith(new TestAdapter())
|
|
84
|
-
expect(clientJs).toContain('
|
|
89
|
+
expect(clientJs).toContain("kind: 'markup'")
|
|
85
90
|
// The preamble re-runs inside the effect with the loop-param accessor
|
|
86
91
|
// wrap (`t().done`), not the plain (`t.done`) form used at the
|
|
87
92
|
// top-level construction line.
|
|
88
93
|
expect(clientJs).toMatch(/createEffect\(\(\) => \{\s*const stateLabel = t\(\)\.done/)
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
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\)/)
|
|
93
99
|
})
|
|
94
100
|
|
|
95
101
|
test('(c) a loop without a preamble gets no region', () => {
|
|
@@ -102,7 +108,7 @@ describe('preamble-region-patch (#2389)', () => {
|
|
|
102
108
|
}
|
|
103
109
|
`
|
|
104
110
|
const { clientJs } = compileWith(new TestAdapter(), source)
|
|
105
|
-
expect(clientJs).not.toContain('
|
|
111
|
+
expect(clientJs).not.toContain("kind: 'markup'")
|
|
106
112
|
})
|
|
107
113
|
|
|
108
114
|
test('(c) a static-array loop with a preamble gets no region', () => {
|
|
@@ -123,7 +129,7 @@ describe('preamble-region-patch (#2389)', () => {
|
|
|
123
129
|
}
|
|
124
130
|
`
|
|
125
131
|
const { clientJs } = compileWith(new TestAdapter(), source)
|
|
126
|
-
expect(clientJs).not.toContain('
|
|
132
|
+
expect(clientJs).not.toContain("kind: 'markup'")
|
|
127
133
|
})
|
|
128
134
|
|
|
129
135
|
test('(d) a preamble local never referenced by an expression child gets no region', () => {
|
|
@@ -145,6 +151,6 @@ describe('preamble-region-patch (#2389)', () => {
|
|
|
145
151
|
}
|
|
146
152
|
`
|
|
147
153
|
const { clientJs } = compileWith(new TestAdapter(), source)
|
|
148
|
-
expect(clientJs).not.toContain('
|
|
154
|
+
expect(clientJs).not.toContain("kind: 'markup'")
|
|
149
155
|
})
|
|
150
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)
|
|
@@ -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'
|