@barefootjs/jsx 0.26.4 → 0.28.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 +941 -130
- 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 +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 +2 -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-insert.d.ts +7 -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-lazy-row.d.ts +107 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -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/lazy-row-eligibility.d.ts +203 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -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 +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/branch-loop.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/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/lazy-row.d.ts +98 -0
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
- 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/control-flow.d.ts.map +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 +542 -689
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +23 -12
- package/src/__tests__/composite-branch-loop.test.ts +14 -5
- package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
- package/src/__tests__/create-selector.test.ts +21 -7
- 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__/lazy-row-eligibility.test.ts +692 -0
- package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
- package/src/__tests__/loop-fallback-wrap.test.ts +34 -16
- package/src/__tests__/loop-hoisted-template.test.ts +10 -6
- 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__/static-loop-csr-materialize.test.ts +6 -1
- 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 +56 -17
- 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 +35 -12
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
- package/src/ir-to-client-js/control-flow/shared.ts +1 -1
- 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/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/lazy-row.ts +478 -0
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +41 -17
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +71 -46
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +8 -3
- 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 +13 -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 +26 -8
- package/src/types.ts +25 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emission pin for the loop-branch-stale-text fix.
|
|
3
|
+
*
|
|
4
|
+
* `summarizeLoopChildBranch` (ir-to-client-js/collect-elements.ts) used to
|
|
5
|
+
* collect NO `reactiveTexts` for ANY loop-row conditional branch whose whole
|
|
6
|
+
* content is a bare `expression` (no wrapping element) — including a plain
|
|
7
|
+
* property read like `row.label`, which can never construct a live DOM
|
|
8
|
+
* node. That blanket skip meant a keyed row's branch text silently froze at
|
|
9
|
+
* its mount-time value whenever the item changed without the conditional's
|
|
10
|
+
* OWN condition flipping (`insert()`, runtime/insert.ts, correctly no-ops
|
|
11
|
+
* when its condition is unchanged — branch-internal updates are the effect
|
|
12
|
+
* system's job).
|
|
13
|
+
*
|
|
14
|
+
* The fix narrows the skip to `node.hasFunctionCalls` (jsx-to-ir.ts's
|
|
15
|
+
* `exprHasFunctionCalls`, a recursive AST walk — catches a call nested
|
|
16
|
+
* anywhere, not just a top-level one) — a call can return a live DOM node
|
|
17
|
+
* (a hoisted `renderNode={(n) => <Pill/>}` callback lowered to a component
|
|
18
|
+
* call, #1211/#1213, pinned by `nested-loop-conditional.test.ts`) and the
|
|
19
|
+
* compiler cannot tell from syntax whether it does, so that shape must keep
|
|
20
|
+
* the skip. Everything call-free is provably incapable of yielding a Node
|
|
21
|
+
* and is now collected.
|
|
22
|
+
*
|
|
23
|
+
* The companion half of the fix lives in `transformConditionalBranch`
|
|
24
|
+
* (jsx-to-ir.ts): a bare loop-item read like `row.label` previously got NO
|
|
25
|
+
* `slotId` at all (`needsSlot` didn't consider a `render-item` free
|
|
26
|
+
* reference), so even with the collect-elements.ts skip narrowed there was
|
|
27
|
+
* nothing for the new effect — or the `<!--bf:sN-->` marker
|
|
28
|
+
* `irToHtmlTemplate` emits for it — to attach to. Both are exercised here
|
|
29
|
+
* together because neither half alone reproduces the fix.
|
|
30
|
+
*/
|
|
31
|
+
import { describe, test, expect } from 'bun:test'
|
|
32
|
+
import { compileJSX } from '../compiler'
|
|
33
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
34
|
+
|
|
35
|
+
const adapter = new TestAdapter()
|
|
36
|
+
|
|
37
|
+
function clientJsFor(branchTrue: string): string {
|
|
38
|
+
const source = `
|
|
39
|
+
'use client'
|
|
40
|
+
import { createSignal } from '@barefootjs/client'
|
|
41
|
+
type Props = { format?: (s: string) => string }
|
|
42
|
+
type Row = { id: number; label: string; done: boolean }
|
|
43
|
+
export function C(_p: Props) {
|
|
44
|
+
const [rows] = createSignal<Row[]>([])
|
|
45
|
+
return <ul>{rows().map(row => <li key={row.id}>{row.done ? ${branchTrue} : 'pending'}</li>)}</ul>
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
const result = compileJSX(source, 'C.tsx', { adapter })
|
|
49
|
+
const errors = result.errors.filter(e => e.severity === 'error')
|
|
50
|
+
if (errors.length > 0) throw new Error(errors.map(e => e.message).join('\n'))
|
|
51
|
+
return result.files.find(f => f.type === 'clientJs')!.content
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('loop-branch-stale-text — reactiveTexts narrowing (collect-elements.ts)', () => {
|
|
55
|
+
test('a bare property-read branch (no call anywhere) gets its own text effect', () => {
|
|
56
|
+
const js = clientJsFor('row.label')
|
|
57
|
+
expect(js).toContain("escapeTextOrNode(row().label)")
|
|
58
|
+
// And a slotId was actually allocated for it — `transformConditionalBranch`'s
|
|
59
|
+
// `refsLoopParam` half of the fix, without which the effect above would
|
|
60
|
+
// have nothing to `lazySlots(...)` claim.
|
|
61
|
+
expect(js).toMatch(/lazySlots\(__branchScope, \[\{ id: 's\d+', kind: 'markup', path: \[\] \}\]\)/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('a template-literal branch built from property reads only (no call) also gets a text effect', () => {
|
|
65
|
+
const js = clientJsFor('`[${row.label}]`')
|
|
66
|
+
expect(js).toContain('escapeTextOrNode(`[${row().label}]`)')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('a branch with a call ANYWHERE (top-level) keeps the skip — no per-item text effect', () => {
|
|
70
|
+
const js = clientJsFor('_p.format(row.label)')
|
|
71
|
+
expect(js).not.toContain('escapeTextOrNode(_p.format(row().label))')
|
|
72
|
+
// bindEvents for that arm stays empty (mirrors the pre-fix shape exactly).
|
|
73
|
+
expect(js).toMatch(/bindEvents: \(__branchScope, \{ isFirstRun: __bfFirstRun = false \} = \{\}\) => \{\s*\}/m)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('a branch with a call NESTED inside a template literal also keeps the skip', () => {
|
|
77
|
+
// The call is not at the top level of the expression — `exprHasFunctionCalls`
|
|
78
|
+
// is a full recursive AST walk, so this must be caught too.
|
|
79
|
+
const js = clientJsFor('`${_p.format(row.label)}!`')
|
|
80
|
+
expect(js).not.toContain("escapeTextOrNode(`${_p.format(row().label)}!`)")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a branch with a call nested inside a sub-ternary keeps the skip', () => {
|
|
84
|
+
const js = clientJsFor("row.id > 0 ? _p.format(row.label) : row.label")
|
|
85
|
+
// The outer branch (itself a nested conditional) is unaffected by this
|
|
86
|
+
// narrowing — nested conditionals already get their own `insert()` via
|
|
87
|
+
// `collectLoopChildConditionals`, never through `reactiveTexts`. This
|
|
88
|
+
// case exists to confirm compilation succeeds and the call-bearing leaf
|
|
89
|
+
// still doesn't get a bare reactiveTexts effect wrapping the whole
|
|
90
|
+
// nested-ternary expression text.
|
|
91
|
+
expect(js).not.toContain('escapeTextOrNode(row.id > 0 ? _p.format(row().label) : row().label)')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('an inert literal branch (no loop-param ref, no call) stays exactly as before — no slotId, no effect', () => {
|
|
95
|
+
const js = clientJsFor("'yes'")
|
|
96
|
+
expect(js).not.toContain("escapeTextOrNode('yes')")
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -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
|
|
@@ -58,7 +58,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
58
58
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
59
59
|
// Dynamic loops emit a `mapArray(` call site. Tighter than
|
|
60
60
|
// `/mapArray|reconcile/` which would fire on any stray mention.
|
|
61
|
-
expect(clientJs).
|
|
61
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
62
62
|
expect(clientJs).toContain('items()')
|
|
63
63
|
})
|
|
64
64
|
|
|
@@ -83,14 +83,14 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
83
83
|
`
|
|
84
84
|
|
|
85
85
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
86
|
-
expect(clientJs).
|
|
86
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
87
87
|
expect(clientJs).toContain('getItems()')
|
|
88
88
|
})
|
|
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)', () => {
|
|
@@ -125,7 +124,11 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
125
124
|
`
|
|
126
125
|
|
|
127
126
|
const clientJs = getClientJs(source, 'List.tsx')
|
|
128
|
-
|
|
127
|
+
// `mapArrayLazy` counts: a keyed, single-root, conditional-free row over a
|
|
128
|
+
// PROP array is lazy-row-eligible (spec/slot-unification.md §9.4 — props
|
|
129
|
+
// are the canonical hydration-consistent source), so it takes the lazy
|
|
130
|
+
// reconciler. What #1586 pins is "reconciles at all", not which family.
|
|
131
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
129
132
|
})
|
|
130
133
|
|
|
131
134
|
test('unrecognised-call chain with filter now reconciles', () => {
|
|
@@ -150,7 +153,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
150
153
|
`
|
|
151
154
|
|
|
152
155
|
const clientJs = getClientJs(source, 'DoneList.tsx')
|
|
153
|
-
expect(clientJs).
|
|
156
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
154
157
|
expect(clientJs).toContain('computeItems()')
|
|
155
158
|
})
|
|
156
159
|
|
|
@@ -182,7 +185,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
182
185
|
// Outer reconciles via mapArray. Also assert the inner loop's array
|
|
183
186
|
// expression (`o.children`) appears in the emitted template — without
|
|
184
187
|
// this, the test would pass even if the inner loop were dropped.
|
|
185
|
-
expect(clientJs).
|
|
188
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
186
189
|
expect(clientJs).toContain('outer()')
|
|
187
190
|
expect(clientJs).toContain('o.children')
|
|
188
191
|
})
|
|
@@ -220,8 +223,13 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
220
223
|
// mapArray is emitted (widening now applies). renderItem uses the
|
|
221
224
|
// synthetic `__bfItem` param; references to `cfg` are rewritten
|
|
222
225
|
// inline and no unwrap statement is emitted.
|
|
223
|
-
expect(clientJs).
|
|
224
|
-
|
|
226
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
227
|
+
// The synthetic accessor param, in whichever reconciler shape applies:
|
|
228
|
+
// `mapArray`'s renderItem head `(__bfItem, __idx, __existing) =>`, or a
|
|
229
|
+
// lazy row plan's `const __bfItem = () => __e.item` binding
|
|
230
|
+
// (spec/slot-unification.md §9). Either way the destructured references
|
|
231
|
+
// below must read through the accessor, which is what this pins.
|
|
232
|
+
expect(clientJs).toMatch(/\(__bfItem, |const __bfItem = \(\) => __e\.item/)
|
|
225
233
|
expect(clientJs).not.toContain('const [, cfg] = __bfItem();')
|
|
226
234
|
expect(clientJs).toContain('__bfItem()[1].color')
|
|
227
235
|
})
|
|
@@ -248,7 +256,7 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
248
256
|
`
|
|
249
257
|
|
|
250
258
|
const clientJs = getClientJs(source, 'Shelf.tsx')
|
|
251
|
-
expect(clientJs).
|
|
259
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
252
260
|
expect(clientJs).toContain('listOf()')
|
|
253
261
|
})
|
|
254
262
|
|
|
@@ -279,8 +287,13 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
279
287
|
`
|
|
280
288
|
|
|
281
289
|
const clientJs = getClientJs(source, 'TypedLegend.tsx')
|
|
282
|
-
expect(clientJs).
|
|
283
|
-
|
|
290
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
291
|
+
// The synthetic accessor param, in whichever reconciler shape applies:
|
|
292
|
+
// `mapArray`'s renderItem head `(__bfItem, __idx, __existing) =>`, or a
|
|
293
|
+
// lazy row plan's `const __bfItem = () => __e.item` binding
|
|
294
|
+
// (spec/slot-unification.md §9). Either way the destructured references
|
|
295
|
+
// below must read through the accessor, which is what this pins.
|
|
296
|
+
expect(clientJs).toMatch(/\(__bfItem, |const __bfItem = \(\) => __e\.item/)
|
|
284
297
|
expect(clientJs).not.toContain('const [, cfg] = __bfItem();')
|
|
285
298
|
expect(clientJs).toContain('__bfItem()[1].color')
|
|
286
299
|
})
|
|
@@ -310,8 +323,13 @@ describe('Solid-style wrap-by-default fallback for loops (#943)', () => {
|
|
|
310
323
|
`
|
|
311
324
|
|
|
312
325
|
const clientJs = getClientJs(source, 'Fields.tsx')
|
|
313
|
-
expect(clientJs).
|
|
314
|
-
|
|
326
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
327
|
+
// The synthetic accessor param, in whichever reconciler shape applies:
|
|
328
|
+
// `mapArray`'s renderItem head `(__bfItem, __idx, __existing) =>`, or a
|
|
329
|
+
// lazy row plan's `const __bfItem = () => __e.item` binding
|
|
330
|
+
// (spec/slot-unification.md §9). Either way the destructured references
|
|
331
|
+
// below must read through the accessor, which is what this pins.
|
|
332
|
+
expect(clientJs).toMatch(/\(__bfItem, |const __bfItem = \(\) => __e\.item/)
|
|
315
333
|
expect(clientJs).not.toContain('const { label, value } = __bfItem();')
|
|
316
334
|
expect(clientJs).toContain('__bfItem().label')
|
|
317
335
|
expect(clientJs).toContain('__bfItem().value')
|
|
@@ -72,16 +72,20 @@ describe('hoisted loop-body skeleton template (perf)', () => {
|
|
|
72
72
|
`
|
|
73
73
|
const { clientJs, template } = compile(source, 'Bench.tsx')
|
|
74
74
|
|
|
75
|
-
// Hoisted declaration present, before the
|
|
75
|
+
// Hoisted declaration present, before the reconciler call. This row shape
|
|
76
|
+
// is also lazy-row-eligible (spec/slot-unification.md §9.4), so the
|
|
77
|
+
// reconciler is `mapArrayLazy` and the clone happens in the row plan's
|
|
78
|
+
// `createRow` rather than in a `mapArray` renderItem — the hoisting
|
|
79
|
+
// question this suite pins is orthogonal to which reconciler consumes it.
|
|
76
80
|
const declIdx = clientJs.indexOf('.innerHTML = `<tr')
|
|
77
|
-
const mapArrayIdx = clientJs.
|
|
81
|
+
const mapArrayIdx = clientJs.search(/\bmapArray(Lazy)?\(/)
|
|
78
82
|
expect(declIdx).toBeGreaterThan(-1)
|
|
79
83
|
expect(mapArrayIdx).toBeGreaterThan(-1)
|
|
80
84
|
expect(declIdx).toBeLessThan(mapArrayIdx)
|
|
81
85
|
|
|
82
|
-
//
|
|
86
|
+
// The row is built by cloning the hoisted template instead of re-parsing
|
|
83
87
|
// innerHTML per row.
|
|
84
|
-
expect(clientJs).toMatch(/const __el =
|
|
88
|
+
expect(clientJs).toMatch(/const __el = __tpl_\w+\.content\.firstElementChild\.cloneNode\(true\)/)
|
|
85
89
|
|
|
86
90
|
// The hoisted template itself carries no interpolations and no dynamic
|
|
87
91
|
// `class` attribute (that's covered by the reactive-attr createEffect).
|
|
@@ -172,7 +176,7 @@ describe('hoisted loop-body skeleton template (perf)', () => {
|
|
|
172
176
|
}
|
|
173
177
|
`
|
|
174
178
|
const { clientJs } = compile(source, 'SpreadList.tsx')
|
|
175
|
-
expect(clientJs).
|
|
179
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
176
180
|
expect(clientJs).not.toMatch(/__tpl_\w+ = document\.createElement\('template'\)/)
|
|
177
181
|
})
|
|
178
182
|
|
|
@@ -195,7 +199,7 @@ describe('hoisted loop-body skeleton template (perf)', () => {
|
|
|
195
199
|
}
|
|
196
200
|
`
|
|
197
201
|
const { clientJs } = compile(source, 'Dots.tsx')
|
|
198
|
-
expect(clientJs).
|
|
202
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\(/)
|
|
199
203
|
// A `<circle>` root with only dynamic attrs (all covered by reactive-attr
|
|
200
204
|
// createEffects) and a literal `r="4"` is exactly the shape the fast path
|
|
201
205
|
// proves safe — this DOES hoist, and it must wrap in a synthetic `<svg>`
|
|
@@ -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)
|
|
@@ -95,7 +95,12 @@ describe('#1247 — static-loop CSR self-heal', () => {
|
|
|
95
95
|
}
|
|
96
96
|
`
|
|
97
97
|
const clientJs = getClientJs(source, 'PropList.tsx')
|
|
98
|
-
|
|
98
|
+
// Either reconciler shape satisfies the point of this test — the loop must
|
|
99
|
+
// go through the keyed `mapArray` family, not the static `forEach` path.
|
|
100
|
+
// A keyed, single-root, conditional-free row over a prop array is
|
|
101
|
+
// lazy-row-eligible (spec/slot-unification.md §9.4), so it emits
|
|
102
|
+
// `mapArrayLazy`; widen (don't narrow) the assertion.
|
|
103
|
+
expect(clientJs).toMatch(/\bmapArray(Lazy)?\s*\(/)
|
|
99
104
|
expect(clientJs).not.toMatch(/\.forEach\s*\(/)
|
|
100
105
|
})
|
|
101
106
|
|
|
@@ -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'
|