@barefootjs/jsx 0.10.1 → 0.12.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/debug-profile.d.ts +115 -0
- package/dist/debug-profile.d.ts.map +1 -0
- package/dist/debug.d.ts +4 -3
- package/dist/debug.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +31 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1881 -207
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- 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 +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-component-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts +2 -2
- 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-event-delegation.d.ts +3 -3
- 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-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +4 -2
- 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/build-reactive-effects.d.ts +3 -1
- 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/event-delegation.d.ts +7 -0
- 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/inner-loop.d.ts +7 -0
- 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/insert.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +28 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- 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/event-listener.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +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 +2 -2
- 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.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts +2 -2
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/effects-and-on-mounts.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +5 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +29 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +26 -0
- package/dist/loop-destructure.d.ts.map +1 -0
- package/dist/profiler.d.ts +502 -0
- package/dist/profiler.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/debug-profile.test.ts +405 -0
- package/src/__tests__/expression-parser.test.ts +44 -1
- package/src/__tests__/profile-bfid-emission.test.ts +63 -0
- package/src/__tests__/profile-binding-ids.test.ts +123 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +80 -0
- package/src/__tests__/profile-loop-binding-ids.test.ts +106 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +153 -0
- package/src/__tests__/profile-turn-markers-branch.test.ts +83 -0
- package/src/__tests__/profile-turn-markers-delegation.test.ts +63 -0
- package/src/__tests__/profile-turn-markers.test.ts +54 -0
- package/src/__tests__/profiler-batch-advisor.test.ts +198 -0
- package/src/__tests__/profiler-coverage-conformance.test.ts +360 -0
- package/src/__tests__/profiler-e2e.test.ts +104 -0
- package/src/__tests__/profiler-hot-subscribers.test.ts +263 -0
- package/src/__tests__/profiler-wasted-re-runs.test.ts +147 -0
- package/src/__tests__/profiler.test.ts +466 -0
- package/src/compiler.ts +3 -0
- package/src/debug-profile.ts +543 -0
- package/src/debug.ts +192 -28
- package/src/expression-parser.ts +53 -0
- package/src/index.ts +72 -1
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +3 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +8 -2
- 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-inner-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +9 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +12 -8
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +10 -4
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/insert.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +6 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +14 -2
- package/src/ir-to-client-js/control-flow/stringify/event-listener.ts +5 -2
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +13 -11
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +19 -7
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +18 -13
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +18 -14
- package/src/ir-to-client-js/control-flow.ts +12 -6
- package/src/ir-to-client-js/emit-reactive.ts +18 -5
- package/src/ir-to-client-js/imports.ts +2 -0
- package/src/ir-to-client-js/index.ts +6 -1
- package/src/ir-to-client-js/phases/effects-and-on-mounts.ts +10 -4
- package/src/ir-to-client-js/phases/event-handlers.ts +6 -2
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +7 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +12 -6
- package/src/ir-to-client-js/types.ts +5 -0
- package/src/ir-to-client-js/utils.ts +37 -0
- package/src/jsx-to-ir.ts +2 -2
- package/src/loop-destructure.ts +170 -0
- package/src/profiler.ts +1520 -0
- package/src/types.ts +8 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode ids on conditional-branch DOM-binding effects (#1690, SR3/SR4,
|
|
3
|
+
* issue #1795 Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* A conditional's `insert()` effect, and the attribute / text binding effects
|
|
6
|
+
* emitted inside its branch `bindEvents`, carry a `<Component>#binding:<slotId>`
|
|
7
|
+
* id in profile mode so the profiler attributes their re-runs to source.
|
|
8
|
+
* `buildIdIndex` resolves those ids from the graph's `domBindings`
|
|
9
|
+
* (conditional / attribute / text slot + loc). Off by default the emitted
|
|
10
|
+
* effects are byte-identical (SR8).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect } from 'bun:test'
|
|
14
|
+
import { compileJSX } from '../compiler'
|
|
15
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
16
|
+
import { buildIdIndex } from '../profiler'
|
|
17
|
+
import { buildComponentAnalysis } from '../debug'
|
|
18
|
+
|
|
19
|
+
const adapter = new TestAdapter()
|
|
20
|
+
|
|
21
|
+
// `open() &&` produces a conditional (s1); inside its branch the `class={...}`
|
|
22
|
+
// is a reactive attribute (s3) and `{n()}` is a reactive text effect (s2).
|
|
23
|
+
const source = `
|
|
24
|
+
'use client'
|
|
25
|
+
import { createSignal } from '@barefootjs/client'
|
|
26
|
+
export function Disclosure() {
|
|
27
|
+
const [open, setOpen] = createSignal(false)
|
|
28
|
+
const [n] = createSignal(0)
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<button onClick={() => setOpen(!open())}>toggle</button>
|
|
32
|
+
{open() && <p class={open() ? 'a' : 'b'}>count: {n()}</p>}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
function clientJs(profile: boolean): string {
|
|
39
|
+
return compileJSX(source, 'Disclosure.tsx', { adapter, profile })
|
|
40
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('conditional binding-effect ids (#1795 Phase 1)', () => {
|
|
44
|
+
test('profile off: no #binding ids anywhere (SR8)', () => {
|
|
45
|
+
expect(clientJs(false)).not.toContain('#binding:')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('profile on: the conditional insert() effect carries a #binding id', () => {
|
|
49
|
+
const on = clientJs(true)
|
|
50
|
+
// The id is the last argument to the `insert(...)` call, which closes on
|
|
51
|
+
// its own line as `}, "<id>")` (single paren — distinct from the branch
|
|
52
|
+
// effects' `}, "<id>"))`). Anchor the line so this can't match a nested
|
|
53
|
+
// effect's close instead.
|
|
54
|
+
expect(on).toMatch(/^\s*}, "Disclosure#binding:s\d+"\)$/m)
|
|
55
|
+
// And it really is an insert() call that opened the block.
|
|
56
|
+
expect(on).toContain("insert(__scope, 's1'")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('profile on: branch attribute and text effects carry #binding ids', () => {
|
|
60
|
+
const on = clientJs(true)
|
|
61
|
+
// Branch reactive attribute effect (createDisposableEffect with the class write).
|
|
62
|
+
expect(on).toMatch(/setAttribute\('class'[\s\S]*?}, "Disclosure#binding:s\d+"\)\)/)
|
|
63
|
+
// Branch reactive text effect (__bfText path).
|
|
64
|
+
expect(on).toMatch(/__bfText\([\s\S]*?}, "Disclosure#binding:s\d+"\)\)/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('buildIdIndex resolves conditional / attribute / text binding ids to source loc', () => {
|
|
68
|
+
const { graph } = buildComponentAnalysis(source, 'Disclosure.tsx')
|
|
69
|
+
const index = buildIdIndex(graph)
|
|
70
|
+
|
|
71
|
+
for (const type of ['conditional', 'attribute', 'text'] as const) {
|
|
72
|
+
const binding = graph.domBindings.find(b => b.type === type)
|
|
73
|
+
expect(binding, `expected a ${type} domBinding`).toBeTruthy()
|
|
74
|
+
const node = index.get(`Disclosure#binding:${binding!.slotId}`)
|
|
75
|
+
expect(node?.kind, `${type} binding should resolve to an effect`).toBe('effect')
|
|
76
|
+
expect(node?.loc.file).toBe('Disclosure.tsx')
|
|
77
|
+
expect(node?.loc.line).toBeGreaterThan(0)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode ids on loop-child DOM-binding effects (#1690, SR3/SR4,
|
|
3
|
+
* issue #1795 Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* Inside a `map(it => …)` body the emitter wraps every loop-param read in a
|
|
6
|
+
* `createEffect`. In profile mode each of those per-item attribute / text
|
|
7
|
+
* effects now carries a `<Component>#binding:<slotId>` id, and `buildIdIndex`
|
|
8
|
+
* resolves it from the graph's loop-child `domBindings` (slot + loc) — the
|
|
9
|
+
* analyzer now threads loop-param context so `{it.t}` / `class={it.n…}` reads
|
|
10
|
+
* register as reactive bindings. Off by default the effects are byte-identical
|
|
11
|
+
* (SR8).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect } from 'bun:test'
|
|
15
|
+
import { compileJSX } from '../compiler'
|
|
16
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
17
|
+
import { buildIdIndex } from '../profiler'
|
|
18
|
+
import { buildComponentAnalysis } from '../debug'
|
|
19
|
+
|
|
20
|
+
const adapter = new TestAdapter()
|
|
21
|
+
|
|
22
|
+
// `{it.t}` and `: {it.n}` are loop-child text effects (s0, s1); `class={…}` is
|
|
23
|
+
// a loop-child attribute effect (s2); the loop itself is s3.
|
|
24
|
+
const source = `
|
|
25
|
+
'use client'
|
|
26
|
+
import { createSignal } from '@barefootjs/client'
|
|
27
|
+
export function List() {
|
|
28
|
+
const [items] = createSignal([{ id: 1, t: 'a', n: 0 }])
|
|
29
|
+
return (
|
|
30
|
+
<ul>
|
|
31
|
+
{items().map(it => (
|
|
32
|
+
<li key={it.id} class={it.n > 0 ? 'hot' : 'cold'}>
|
|
33
|
+
<span>{it.t}</span>: {it.n}
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
function clientJs(profile: boolean): string {
|
|
42
|
+
return compileJSX(source, 'List.tsx', { adapter, profile })
|
|
43
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('loop-child binding-effect ids (#1795 Phase 2)', () => {
|
|
47
|
+
test('profile off: no #binding ids anywhere (SR8)', () => {
|
|
48
|
+
expect(clientJs(false)).not.toContain('#binding:')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('profile on: loop-child text effects carry #binding ids', () => {
|
|
52
|
+
const on = clientJs(true)
|
|
53
|
+
// Both `{it.t}` and `{it.n}` text effects.
|
|
54
|
+
expect(on).toMatch(/__rt_s\d+\.textContent = String\(it\(\)\.t\) }, "List#binding:s\d+"\)/)
|
|
55
|
+
expect(on).toMatch(/__rt_s\d+\.textContent = String\(it\(\)\.n\) }, "List#binding:s\d+"\)/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('profile on: the loop-child attribute effect carries a #binding id', () => {
|
|
59
|
+
const on = clientJs(true)
|
|
60
|
+
expect(on).toMatch(/setAttribute\('class'[\s\S]*?}, "List#binding:s\d+"\)/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('profile on: the `key` attribute is NOT emitted as a binding effect', () => {
|
|
64
|
+
const on = clientJs(true)
|
|
65
|
+
// `key` becomes the loop keyFn / data-key, never a createEffect.
|
|
66
|
+
expect(on).not.toContain('it().id) }, "List#binding')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('analyzer registers loop-child text/attribute bindings (not `key`)', () => {
|
|
70
|
+
const { graph } = buildComponentAnalysis(source, 'List.tsx')
|
|
71
|
+
const loopChild = graph.domBindings.filter(b => b.type !== 'loop')
|
|
72
|
+
// class (attribute) + two texts = 3; `key` is excluded.
|
|
73
|
+
expect(loopChild.map(b => b.label).sort()).toEqual(['class', 'text "s0"', 'text "s1"'])
|
|
74
|
+
expect(loopChild.every(b => b.classification === 'reactive')).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('a loop-param name inside a string literal is NOT mistaken for a read', () => {
|
|
78
|
+
// Index param `i`; `data-lit={'i'}` references no identifier — only the
|
|
79
|
+
// string literal `'i'`. Lexer-aware detection (freeIdentifiers / freeRefs)
|
|
80
|
+
// must not register it as a reactive binding, where a raw-string regex would.
|
|
81
|
+
const litSource = `
|
|
82
|
+
'use client'
|
|
83
|
+
import { createSignal } from '@barefootjs/client'
|
|
84
|
+
export function L() {
|
|
85
|
+
const [items] = createSignal([{ id: 1 }])
|
|
86
|
+
return <ul>{items().map((it, i) => <li key={it.id} data-lit={'i'} data-idx={i === 0 ? 'a' : 'b'}>x</li>)}</ul>
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
const { graph } = buildComponentAnalysis(litSource, 'L.tsx')
|
|
90
|
+
const labels = graph.domBindings.filter(b => b.type === 'attribute').map(b => b.label)
|
|
91
|
+
// `data-idx` reads the index identifier → reactive; `data-lit` is literal-only → not.
|
|
92
|
+
expect(labels).toContain('data-idx')
|
|
93
|
+
expect(labels).not.toContain('data-lit')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('buildIdIndex resolves every loop-child binding to source loc', () => {
|
|
97
|
+
const { graph } = buildComponentAnalysis(source, 'List.tsx')
|
|
98
|
+
const index = buildIdIndex(graph)
|
|
99
|
+
for (const b of graph.domBindings) {
|
|
100
|
+
const node = index.get(`List#binding:${b.slotId}`)
|
|
101
|
+
expect(node?.kind, `binding ${b.slotId} (${b.type})`).toBe('effect')
|
|
102
|
+
expect(node?.loc.file).toBe('List.tsx')
|
|
103
|
+
expect(node?.loc.line).toBeGreaterThan(0)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode ids on deeply-nested loop binding effects (#1690, SR3/SR4,
|
|
3
|
+
* issue #1795 Phase 3).
|
|
4
|
+
*
|
|
5
|
+
* Phases 1–2 covered top-level, conditional-branch, and direct loop-child
|
|
6
|
+
* bindings. Phase 3 closes the remaining nested emit paths so every binding
|
|
7
|
+
* effect a loop produces carries a `<Component>#binding:<slotId>` id and every
|
|
8
|
+
* emitted id resolves via `buildIdIndex`:
|
|
9
|
+
*
|
|
10
|
+
* - loop-child conditional `insert()` + its branch text (`emitArmText`)
|
|
11
|
+
* - inner / nested loop `mapArray` + inner-loop child text/attr effects
|
|
12
|
+
* - branch-scoped loop `mapArray` (loop inside a conditional)
|
|
13
|
+
* - static-array loop child text/attr effects
|
|
14
|
+
* - component loop `mapArray`
|
|
15
|
+
*
|
|
16
|
+
* Off by default the emitted effects are byte-for-byte unchanged (SR8).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, expect } from 'bun:test'
|
|
20
|
+
import { compileJSX } from '../compiler'
|
|
21
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
22
|
+
import { buildIdIndex } from '../profiler'
|
|
23
|
+
import { buildComponentAnalysis } from '../debug'
|
|
24
|
+
import { profileBindingId } from '../ir-to-client-js/utils'
|
|
25
|
+
|
|
26
|
+
const adapter = new TestAdapter()
|
|
27
|
+
|
|
28
|
+
function clientJs(source: string, name: string, profile: boolean): string {
|
|
29
|
+
return compileJSX(source, `${name}.tsx`, { adapter, profile })
|
|
30
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every emitted `<Comp>#binding:sN` id must resolve via buildIdIndex (no gap). */
|
|
34
|
+
function assertNoCoverageGap(source: string, name: string): string[] {
|
|
35
|
+
const on = clientJs(source, name, true)
|
|
36
|
+
const { graph } = buildComponentAnalysis(source, `${name}.tsx`)
|
|
37
|
+
const index = buildIdIndex(graph)
|
|
38
|
+
const emitted = [...new Set([...on.matchAll(new RegExp(`"(${name}#binding:s\\d+)"`, 'g'))].map(m => m[1]))]
|
|
39
|
+
const unresolved = emitted.filter(id => !index.has(id))
|
|
40
|
+
expect(unresolved).toEqual([])
|
|
41
|
+
return emitted
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A loop item with BOTH a conditional (with a branch text) AND an inner loop —
|
|
45
|
+
// exercises emitOuterConditional, emitArmText, and the inner-loop mapArray.
|
|
46
|
+
const nestedSource = `
|
|
47
|
+
'use client'
|
|
48
|
+
import { createSignal } from '@barefootjs/client'
|
|
49
|
+
export function Nested() {
|
|
50
|
+
const [rows] = createSignal([{ id: 1, on: true, label: 'a', tags: ['x'] }])
|
|
51
|
+
return (
|
|
52
|
+
<ul>
|
|
53
|
+
{rows().map(r => (
|
|
54
|
+
<li key={r.id}>
|
|
55
|
+
{r.on ? <span>{r.label}</span> : <em>off</em>}
|
|
56
|
+
<ul>{r.tags.map(t => <li key={t}>{t}</li>)}</ul>
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
const branchLoopSource = `
|
|
65
|
+
'use client'
|
|
66
|
+
import { createSignal } from '@barefootjs/client'
|
|
67
|
+
export function BranchLoop() {
|
|
68
|
+
const [open] = createSignal(true)
|
|
69
|
+
const [items] = createSignal([{ id: 1, t: 'a' }])
|
|
70
|
+
return <div>{open() && <ul>{items().map(it => <li key={it.id}>{it.t}</li>)}</ul>}</div>
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
const staticLoopSource = `
|
|
75
|
+
'use client'
|
|
76
|
+
import { createSignal } from '@barefootjs/client'
|
|
77
|
+
export function StaticLoop() {
|
|
78
|
+
const [n] = createSignal(0)
|
|
79
|
+
const tabs = [{ id: 1 }, { id: 2 }]
|
|
80
|
+
return <ul>{tabs.map(tab => <li key={tab.id}>{n()}</li>)}</ul>
|
|
81
|
+
}
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
const componentLoopSource = `
|
|
85
|
+
'use client'
|
|
86
|
+
import { createSignal } from '@barefootjs/client'
|
|
87
|
+
import { Row } from './Row'
|
|
88
|
+
export function CompLoop() {
|
|
89
|
+
const [items] = createSignal([{ id: 1, label: 'a' }])
|
|
90
|
+
return <div>{items().map(it => <Row key={it.id} label={it.label} />)}</div>
|
|
91
|
+
}
|
|
92
|
+
`
|
|
93
|
+
|
|
94
|
+
describe('nested loop binding ids (#1795 Phase 3)', () => {
|
|
95
|
+
test('profile off: no #binding ids in any nested shape (SR8)', () => {
|
|
96
|
+
for (const [src, name] of [
|
|
97
|
+
[nestedSource, 'Nested'],
|
|
98
|
+
[branchLoopSource, 'BranchLoop'],
|
|
99
|
+
[staticLoopSource, 'StaticLoop'],
|
|
100
|
+
[componentLoopSource, 'CompLoop'],
|
|
101
|
+
] as const) {
|
|
102
|
+
expect(clientJs(src, name, false)).not.toContain('#binding:')
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('loop-child conditional + inner loop: every binding resolves', () => {
|
|
107
|
+
const emitted = assertNoCoverageGap(nestedSource, 'Nested')
|
|
108
|
+
// conditional + arm text + inner-loop text + inner loop + outer loop = 5.
|
|
109
|
+
expect(emitted.length).toBe(5)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('loop-child conditional insert() and its branch text carry ids', () => {
|
|
113
|
+
const on = clientJs(nestedSource, 'Nested', true)
|
|
114
|
+
// The branch-arm text (`{r.label}` inside the conditional's true arm).
|
|
115
|
+
expect(on).toMatch(/__rt_s\d+\.textContent = String\(r\(\)\.label\) }, "Nested#binding:s\d+"\)/)
|
|
116
|
+
// The inner loop's child text (`{t}`).
|
|
117
|
+
expect(on).toMatch(/String\(t\(\)\) }, "Nested#binding:s\d+"\)/)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('branch-scoped loop (loop inside a conditional) attributes its mapArray', () => {
|
|
121
|
+
const emitted = assertNoCoverageGap(branchLoopSource, 'BranchLoop')
|
|
122
|
+
// conditional + branch loop + loop-child text = 3.
|
|
123
|
+
expect(emitted.length).toBe(3)
|
|
124
|
+
const on = clientJs(branchLoopSource, 'BranchLoop', true)
|
|
125
|
+
expect(on).toMatch(/mapArray\([\s\S]*?"BranchLoop#binding:s\d+"\)/)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('static-array loop child effects carry ids', () => {
|
|
129
|
+
const emitted = assertNoCoverageGap(staticLoopSource, 'StaticLoop')
|
|
130
|
+
expect(emitted.length).toBeGreaterThanOrEqual(1)
|
|
131
|
+
const on = clientJs(staticLoopSource, 'StaticLoop', true)
|
|
132
|
+
expect(on).toMatch(/textContent = String\(n\(\)\) }, "StaticLoop#binding:s\d+"\)/)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('component loop attributes its mapArray', () => {
|
|
136
|
+
const emitted = assertNoCoverageGap(componentLoopSource, 'CompLoop')
|
|
137
|
+
expect(emitted).toContain('CompLoop#binding:s1')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('a slot-less inner loop ("?") emits no id — never an unresolvable #binding:?', () => {
|
|
141
|
+
// An inner loop whose container element has no `bf` slot yields slotId '?'.
|
|
142
|
+
// buildIdIndex keys on real domBinding slots, so `#binding:?` could never
|
|
143
|
+
// resolve; the helper must suppress it (the analyzer emits no binding for a
|
|
144
|
+
// slot-less loop either, so both sides stay silent). Guards the single
|
|
145
|
+
// chokepoint every loop/binding emit site routes through.
|
|
146
|
+
expect(profileBindingId('Comp', '?')).toBe('')
|
|
147
|
+
expect(profileBindingId('Comp', 's3')).toBe(', "Comp#binding:s3"')
|
|
148
|
+
expect(profileBindingId(undefined, 's3')).toBe('')
|
|
149
|
+
// The nested case is the only matrix shape with an inner loop; confirm it
|
|
150
|
+
// never emits a `:?` id end-to-end.
|
|
151
|
+
expect(clientJs(nestedSource, 'Nested', true)).not.toContain('#binding:?')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode turn markers on conditional-branch handler paths (#1690, SR3,
|
|
3
|
+
* issue #1786): branch-arm direct listeners and branch-scoped loop delegation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from 'bun:test'
|
|
7
|
+
import { compileJSX } from '../compiler'
|
|
8
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
9
|
+
|
|
10
|
+
const adapter = new TestAdapter()
|
|
11
|
+
|
|
12
|
+
const source = `
|
|
13
|
+
'use client'
|
|
14
|
+
import { createSignal } from '@barefootjs/client'
|
|
15
|
+
export function Panel() {
|
|
16
|
+
const [open, setOpen] = createSignal(false)
|
|
17
|
+
const [items] = createSignal([{ id: 1 }])
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<button onClick={() => setOpen(!open())}>toggle</button>
|
|
21
|
+
{open() && (
|
|
22
|
+
<div>
|
|
23
|
+
<button onClick={() => setOpen(false)}>close</button>
|
|
24
|
+
<ul>{items().map(i => <li key={i.id} onClick={() => setOpen(false)}>x</li>)}</ul>
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
function clientJs(profile: boolean): string {
|
|
33
|
+
return compileJSX(source, 'Panel.tsx', { adapter, profile })
|
|
34
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('branch handler turn markers', () => {
|
|
38
|
+
test('profile off: no turn markers anywhere (SR8)', () => {
|
|
39
|
+
expect(clientJs(false)).not.toContain('beginTurn')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('profile on: branch-arm listener and branch-loop delegation are wrapped', () => {
|
|
43
|
+
const on = clientJs(true)
|
|
44
|
+
// One per handler site: top-level toggle, branch-arm close, branch-loop li.
|
|
45
|
+
expect((on.match(/beginTurn\("Panel#handler:/g) ?? []).length).toBe(3)
|
|
46
|
+
// The branch-arm direct listener (addEventListener inside an arm).
|
|
47
|
+
expect(on).toMatch(/addEventListener\('click', \(\.\.\.__bfa\) => \{ beginTurn\("Panel#handler:s\d+:click"\)/)
|
|
48
|
+
// The branch-scoped loop's delegated handler (inline try/finally form).
|
|
49
|
+
expect(on).toMatch(/beginTurn\("Panel#handler:s\d+:click"\); try \{ \(\(\) => setOpen\(false\)\)\(__bfEvt\)/)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const loopCondSource = `
|
|
54
|
+
'use client'
|
|
55
|
+
import { createSignal } from '@barefootjs/client'
|
|
56
|
+
export function List() {
|
|
57
|
+
const [items, setItems] = createSignal([{ id: 1, on: true }])
|
|
58
|
+
return (
|
|
59
|
+
<ul>
|
|
60
|
+
{items().map(it => (
|
|
61
|
+
<li key={it.id}>
|
|
62
|
+
{it.on ? <button onClick={() => setItems(items().filter(x => x.id !== it.id))}>x</button> : <span>off</span>}
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
`
|
|
69
|
+
|
|
70
|
+
describe('loop-cond arm handler turn markers (#1786)', () => {
|
|
71
|
+
const compile = (profile: boolean) =>
|
|
72
|
+
compileJSX(loopCondSource, 'List.tsx', { adapter, profile }).files.find(f => f.type === 'clientJs')!.content
|
|
73
|
+
|
|
74
|
+
test('profile off: no turn markers (SR8)', () => {
|
|
75
|
+
expect(compile(false)).not.toContain('beginTurn')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('profile on: a handler inside a loop-item conditional arm is turn-wrapped', () => {
|
|
79
|
+
const on = compile(true)
|
|
80
|
+
// The arm listener emitted inside bindEvents (addEventListener) is wrapped.
|
|
81
|
+
expect(on).toMatch(/addEventListener\('click', \(\.\.\.__bfa\) => \{ beginTurn\("List#handler:s\d+:click"\)/)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode turn markers on the loop event-delegation path (#1690, SR3).
|
|
3
|
+
*
|
|
4
|
+
* A dynamic list delegates its child events to a single container listener.
|
|
5
|
+
* In profile mode each delegated handler call is bracketed with
|
|
6
|
+
* `beginTurn`/`endTurn` so the reactive work it triggers is attributed to one
|
|
7
|
+
* turn, with the same id namespace as direct handlers. Off by default the
|
|
8
|
+
* dispatcher is unchanged (SR8).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect } from 'bun:test'
|
|
12
|
+
import { compileJSX } from '../compiler'
|
|
13
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
14
|
+
|
|
15
|
+
const adapter = new TestAdapter()
|
|
16
|
+
|
|
17
|
+
const source = `
|
|
18
|
+
'use client'
|
|
19
|
+
import { createSignal } from '@barefootjs/client'
|
|
20
|
+
|
|
21
|
+
export function TodoList() {
|
|
22
|
+
const [items, setItems] = createSignal([{ id: 1, label: 'a' }])
|
|
23
|
+
return (
|
|
24
|
+
<ul>
|
|
25
|
+
{items().map(item => (
|
|
26
|
+
<li key={item.id} onClick={() => setItems(xs => xs.filter(x => x.id !== item.id))}>
|
|
27
|
+
{item.label}
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
function clientJs(profile: boolean): string {
|
|
36
|
+
return compileJSX(source, 'TodoList.tsx', { adapter, profile })
|
|
37
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('loop delegation — profile off (SR8)', () => {
|
|
41
|
+
test('dispatcher carries no turn markers', () => {
|
|
42
|
+
const off = clientJs(false)
|
|
43
|
+
expect(off).toContain('addEventListener') // sanity: delegation present
|
|
44
|
+
expect(off).not.toContain('beginTurn')
|
|
45
|
+
expect(off).not.toContain('endTurn')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('loop delegation — profile on (SR3)', () => {
|
|
50
|
+
test('brackets the delegated handler call with a try/finally turn', () => {
|
|
51
|
+
const on = clientJs(true)
|
|
52
|
+
expect(on).toContain('beginTurn("TodoList#handler:')
|
|
53
|
+
expect(on).toContain('try {')
|
|
54
|
+
expect(on).toContain('finally { endTurn() }')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('auto-wires the beginTurn/endTurn runtime import', () => {
|
|
58
|
+
const on = clientJs(true)
|
|
59
|
+
const importLine = on.split('\n').find(l => l.includes('@barefootjs/client/runtime'))
|
|
60
|
+
expect(importLine).toContain('beginTurn')
|
|
61
|
+
expect(importLine).toContain('endTurn')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode turn-boundary markers (#1690, SR3).
|
|
3
|
+
*
|
|
4
|
+
* In profile mode each event handler is bracketed with `beginTurn`/`endTurn`
|
|
5
|
+
* so a profiling run attributes the reactive work the handler triggers to one
|
|
6
|
+
* turn. Off by default the emitted code is unchanged (SR8).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { compileJSX } from '../compiler'
|
|
11
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
12
|
+
|
|
13
|
+
const adapter = new TestAdapter()
|
|
14
|
+
|
|
15
|
+
const source = `
|
|
16
|
+
'use client'
|
|
17
|
+
import { createSignal } from '@barefootjs/client'
|
|
18
|
+
|
|
19
|
+
export function Counter() {
|
|
20
|
+
const [count, setCount] = createSignal(0)
|
|
21
|
+
return <button onClick={() => setCount(n => n + 1)}>{count()}</button>
|
|
22
|
+
}
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
function clientJs(profile: boolean): string {
|
|
26
|
+
return compileJSX(source, 'Counter.tsx', { adapter, profile })
|
|
27
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('profile mode off (default, SR8)', () => {
|
|
31
|
+
test('emits no turn markers and no turn imports', () => {
|
|
32
|
+
const off = clientJs(false)
|
|
33
|
+
expect(off).not.toContain('beginTurn')
|
|
34
|
+
expect(off).not.toContain('endTurn')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('profile mode on (SR3)', () => {
|
|
39
|
+
test('brackets the handler with beginTurn/endTurn carrying an IR-aligned id', () => {
|
|
40
|
+
const on = clientJs(true)
|
|
41
|
+
expect(on).toContain('beginTurn("Counter#handler:')
|
|
42
|
+
expect(on).toContain('endTurn()')
|
|
43
|
+
// The original handler is still invoked verbatim with forwarded args.
|
|
44
|
+
expect(on).toContain('(() => setCount(n => n + 1))(...__bfa)')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('auto-wires the beginTurn/endTurn runtime import', () => {
|
|
48
|
+
const on = clientJs(true)
|
|
49
|
+
const importLine = on.split('\n').find(l => l.includes('@barefootjs/client/runtime'))
|
|
50
|
+
expect(importLine).toBeDefined()
|
|
51
|
+
expect(importLine).toContain('beginTurn')
|
|
52
|
+
expect(importLine).toContain('endTurn')
|
|
53
|
+
})
|
|
54
|
+
})
|