@barefootjs/jsx 0.26.3 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +735 -140
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +12 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +27 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +13 -2
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -3
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts +8 -3
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +73 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +35 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +555 -678
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +26 -12
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/composite-branch-loop.test.ts +2 -2
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- package/src/__tests__/flatmap-segments.test.ts +182 -0
- package/src/__tests__/inline-jsx-callback.test.ts +6 -4
- package/src/__tests__/ir-jsx-props.test.ts +6 -5
- package/src/__tests__/loop-fallback-wrap.test.ts +3 -4
- package/src/__tests__/map-body-no-silent-divergence.test.ts +45 -0
- package/src/__tests__/module-level-signal.audit.test.ts +7 -2
- package/src/__tests__/nested-loop-conditional.test.ts +9 -7
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
- package/src/__tests__/preamble-region-patch.test.ts +156 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
- package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
- package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
- package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/build-references.ts +16 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +74 -14
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +12 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +11 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +36 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/shared.ts +27 -2
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +67 -19
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +35 -16
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +80 -14
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +1 -1
- package/src/ir-to-client-js/element-refs.ts +9 -11
- package/src/ir-to-client-js/emit-reactive.ts +50 -14
- package/src/ir-to-client-js/html-template.ts +144 -15
- package/src/ir-to-client-js/imports.ts +7 -4
- package/src/ir-to-client-js/reactivity.ts +7 -0
- package/src/ir-to-client-js/types.ts +34 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +382 -15
- package/src/types.ts +72 -0
|
@@ -66,8 +66,19 @@ export interface KeyedItemLookup {
|
|
|
66
66
|
* decision.
|
|
67
67
|
*/
|
|
68
68
|
keyWithItem: string
|
|
69
|
-
/** Optional preamble line — emitted before the handler call. */
|
|
69
|
+
/** Optional preamble line — emitted before the handler call, inside the item guard. */
|
|
70
70
|
mapPreamble: string | null
|
|
71
|
+
/**
|
|
72
|
+
* `const`/`let`/function names `mapPreamble` declares (from
|
|
73
|
+
* `MapCallbackPreamble.declaredNames`). The stringifier only splices
|
|
74
|
+
* `mapPreamble` for an event whose handler text actually references one of
|
|
75
|
+
* these — a preamble that only feeds unused local state (e.g. a `cells`
|
|
76
|
+
* array builder never read by the click handler) is dead weight the
|
|
77
|
+
* delegated handler shouldn't pay for, and emitting it unconditionally is
|
|
78
|
+
* what let BUG-3's stale-context leaf render go unnoticed. Empty when
|
|
79
|
+
* `mapPreamble` is `null`.
|
|
80
|
+
*/
|
|
81
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
71
82
|
/** True when `paramBindings` is non-empty — drives TDZ-safe lookup shape. */
|
|
72
83
|
hasBindings: boolean
|
|
73
84
|
}
|
|
@@ -77,6 +88,8 @@ export interface DynamicIndexItemLookup {
|
|
|
77
88
|
arrayExpr: string
|
|
78
89
|
param: string
|
|
79
90
|
mapPreamble: string | null
|
|
91
|
+
/** See `KeyedItemLookup.mapPreambleDeclaredNames`. */
|
|
92
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
80
93
|
hasBindings: boolean
|
|
81
94
|
/** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
|
|
82
95
|
indexParam: string | null
|
|
@@ -87,6 +100,8 @@ export interface StaticIndexItemLookup {
|
|
|
87
100
|
arrayExpr: string
|
|
88
101
|
param: string
|
|
89
102
|
mapPreamble: string | null
|
|
103
|
+
/** See `KeyedItemLookup.mapPreambleDeclaredNames`. */
|
|
104
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
90
105
|
/** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
|
|
91
106
|
indexParam: string | null
|
|
92
107
|
/**
|
|
@@ -78,6 +78,19 @@ interface DynamicLoopCommon extends LoopPlanCommon {
|
|
|
78
78
|
paramUnwrap: string
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* A resolved preamble-patched region (#2389), ready to emit: `valueExpr` is
|
|
83
|
+
* already wrapped with the loop-param accessor and, for a `joinArrayChild`
|
|
84
|
+
* source, already carries the array-join ternary — the exact value
|
|
85
|
+
* `irToHtmlTemplate` computes for the same node in the row template, so a
|
|
86
|
+
* same-key update's re-patch stays byte-identical to what a fresh mount
|
|
87
|
+
* would render.
|
|
88
|
+
*/
|
|
89
|
+
export interface PreambleRegionPlan {
|
|
90
|
+
slotId: string
|
|
91
|
+
valueExpr: string
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
/** Per-item ref callback resolved against a child slot for emission (#1244). */
|
|
82
95
|
export interface LoopChildRefBinding {
|
|
83
96
|
/** bf slot ID of the target element (root or descendant of the body). */
|
|
@@ -146,6 +159,21 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
146
159
|
* Empty when the loop has no key (only meaningful when `anchored`).
|
|
147
160
|
*/
|
|
148
161
|
anchorKeyExpr: string
|
|
162
|
+
/**
|
|
163
|
+
* flatMap descriptor mode (`LoopCore.flatMapClient`): `arrayExpr` already
|
|
164
|
+
* includes the `.flatMap(params => descriptorBody)` projection — the
|
|
165
|
+
* runtime reconciles the FLATTENED `({ k, h })` leaves — and `keyFn` keys
|
|
166
|
+
* on `d.k` (index fallback). The stringifier emits the build-or-patch
|
|
167
|
+
* renderItem (`patchLeaf`) instead of the template-clone shape; `template`
|
|
168
|
+
* is empty and `mapPreambleWrapped` unused in this mode.
|
|
169
|
+
*/
|
|
170
|
+
flatMapLeafItem?: boolean
|
|
171
|
+
/**
|
|
172
|
+
* Preamble-patched regions (#2389) — non-empty forces the multi-line
|
|
173
|
+
* renderItem layout (mirrors `childRefs`) so the region-patch effects have
|
|
174
|
+
* `__el` to query against.
|
|
175
|
+
*/
|
|
176
|
+
preambleRegions: readonly PreambleRegionPlan[]
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/**
|
|
@@ -20,7 +20,8 @@ import { irChildrenToJsExpr } from '../html-template.ts'
|
|
|
20
20
|
import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
21
21
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
22
22
|
import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
23
|
-
import type { LoopChildRefBinding } from './plan/loop.ts'
|
|
23
|
+
import type { LoopChildRefBinding, PreambleRegionPlan } from './plan/loop.ts'
|
|
24
|
+
import type { PreambleRegionSource } from '../../types.ts'
|
|
24
25
|
import {
|
|
25
26
|
extractFreeIdentifiersFromText,
|
|
26
27
|
extractFreeIdentifiersFromStatementText,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
} from '../csr-substitute.ts'
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* Build the `keyFn` argument for mapArray /
|
|
32
|
+
* Build the `keyFn` argument for mapArray / mapArrayAnchored. `null` when
|
|
32
33
|
* the loop has no key expression. Every `CollectedLoop` variant (top-level /
|
|
33
34
|
* branch / nested) carries an `index: string | null` field (#2218 threaded
|
|
34
35
|
* it onto `NestedLoop` too), so the index param — when present — is always
|
|
@@ -237,6 +238,30 @@ export function destructureLoopParam(
|
|
|
237
238
|
return { head: param, unwrap: '' }
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Resolve `IRLoop.preambleRegions` (#2389) into ready-to-emit
|
|
243
|
+
* `PreambleRegionPlan`s: wrap each source expression with the loop-param
|
|
244
|
+
* accessor and, for a `joinArrayChild` source, apply the same array-join
|
|
245
|
+
* ternary `irToHtmlTemplate` uses for the identical node in the row
|
|
246
|
+
* template — so the region-patch effect's re-render matches what a fresh
|
|
247
|
+
* mount would have produced. Shared by the top-level (`build-loop.ts`) and
|
|
248
|
+
* branch (`build-branch-loop.ts`) plain-loop-plan builders.
|
|
249
|
+
*/
|
|
250
|
+
export function buildPreambleRegionPlans(
|
|
251
|
+
regions: readonly PreambleRegionSource[] | undefined,
|
|
252
|
+
loopParam: string,
|
|
253
|
+
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
254
|
+
): readonly PreambleRegionPlan[] {
|
|
255
|
+
if (!regions || regions.length === 0) return []
|
|
256
|
+
return regions.map((r) => {
|
|
257
|
+
const wrapped = wrapLoopParamAsAccessor(r.expr, loopParam, loopParamBindings)
|
|
258
|
+
const valueExpr = r.joinArrayChild
|
|
259
|
+
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
260
|
+
: `escapeText(${wrapped})`
|
|
261
|
+
return { slotId: r.slotId, valueExpr }
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
240
265
|
/**
|
|
241
266
|
* Build a props object expression string from component prop definitions.
|
|
242
267
|
* Shared by emitComponentLoopReconciliation and emitCompositeElementReconciliation.
|
|
@@ -58,6 +58,7 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
58
58
|
childRefs,
|
|
59
59
|
bodyIsMultiRoot,
|
|
60
60
|
profileLoopId,
|
|
61
|
+
preambleRegions,
|
|
61
62
|
} = plan
|
|
62
63
|
|
|
63
64
|
const loopBfId = profileLoopId ? `, ${JSON.stringify(profileLoopId)}` : ''
|
|
@@ -69,9 +70,29 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
69
70
|
// both the effect and its dependency subscriptions (observation O-2).
|
|
70
71
|
lines.push(` __disposers.push(createDisposableEffect(() => {`)
|
|
71
72
|
|
|
73
|
+
// flatMap descriptor mode — build-or-patch renderItem, mirrors the
|
|
74
|
+
// top-level shape in stringify/loop.ts (see comment there).
|
|
75
|
+
if (plan.flatMapLeafItem) {
|
|
76
|
+
lines.push(` if (${containerVar}) mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (__bfD, ${indexParam}, __existing) => {`)
|
|
77
|
+
lines.push(` let __el = __existing`)
|
|
78
|
+
lines.push(` if (!__el) { const __tpl = document.createElement('template'); __tpl.innerHTML = __bfD().h; __el = __tpl.content.firstElementChild }`)
|
|
79
|
+
lines.push(` let __last = __existing ? undefined : __bfD().h`)
|
|
80
|
+
lines.push(` createEffect(() => {`)
|
|
81
|
+
lines.push(` const __html = __bfD().h`)
|
|
82
|
+
lines.push(` if (__last === undefined) { __last = __html; return }`)
|
|
83
|
+
lines.push(` if (__html !== __last) { __last = __html; patchLeaf(__el, __html) }`)
|
|
84
|
+
lines.push(` })`)
|
|
85
|
+
lines.push(` return __el`)
|
|
86
|
+
lines.push(` }, '${markerId}'${loopBfId})`)
|
|
87
|
+
lines.push(` }))`)
|
|
88
|
+
stringifyEventDelegation(lines, eventDelegation)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
// Non-empty `childRefs` need `__el` as a handle inside the factory body,
|
|
73
|
-
// so force the multi-line layout (#1244)
|
|
74
|
-
|
|
93
|
+
// so force the multi-line layout (#1244) — preamble-patched regions
|
|
94
|
+
// (#2389) need the same handle for their own effect.
|
|
95
|
+
if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0 && preambleRegions.length === 0) {
|
|
75
96
|
// Simple case: single-line renderItem (single root, no reactive effects).
|
|
76
97
|
const cloneExpr = emitTemplateCloneInline(template)
|
|
77
98
|
if (mapPreambleWrapped) {
|
|
@@ -94,8 +115,17 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
94
115
|
indent: ' ',
|
|
95
116
|
singleRootLayout: 'inline',
|
|
96
117
|
})
|
|
97
|
-
if (reactiveEffects !== null) {
|
|
98
|
-
|
|
118
|
+
if (reactiveEffects !== null || preambleRegions.length > 0) {
|
|
119
|
+
// Row-granularity effects (§3(c)): mirrors `stringifyPlainLoop`'s
|
|
120
|
+
// top-level call — attrs, outer texts, and preamble regions merge
|
|
121
|
+
// into ONE row effect (see `stringifyReactiveEffects`'s docstring).
|
|
122
|
+
stringifyReactiveEffects(lines, reactiveEffects, {
|
|
123
|
+
indent: ' ',
|
|
124
|
+
elVar: '__el',
|
|
125
|
+
bodyIsMultiRoot,
|
|
126
|
+
preambleRegions,
|
|
127
|
+
mapPreambleWrapped,
|
|
128
|
+
})
|
|
99
129
|
}
|
|
100
130
|
emitLoopChildRefs(lines, childRefs, { indent: ' ', elVar: '__el', bodyIsMultiRoot })
|
|
101
131
|
lines.push(` return __el`)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim-plan literal rendering — the compiler-side half of slot unification
|
|
3
|
+
* A3 (`spec/slot-unification.md` §4/§5-A3). The runtime's `SlotSpec`/
|
|
4
|
+
* `ClaimPlan` (`@barefootjs/client/runtime/claim-slots.ts`, A2) are pure
|
|
5
|
+
* data: `{ id, kind, path }` triples. This module is the single place that
|
|
6
|
+
* turns compile-time slot descriptors into that data's source-text form —
|
|
7
|
+
* every content-slot emission site (loop rows, preamble regions, dynamic
|
|
8
|
+
* text/JSX slots, `@client` expressions) renders its plan through
|
|
9
|
+
* `claimPlanLiteral`, never by hand-splicing strings, so the literal shape
|
|
10
|
+
* can only ever match `SlotSpec`.
|
|
11
|
+
*
|
|
12
|
+
* Paths are real root-relative child-index arrays wherever the caller
|
|
13
|
+
* already has them (the hoisted single-root loop skeleton, #2143's
|
|
14
|
+
* `SkeletonSlotPaths`); everywhere else a slot gets `path: []` and A2's
|
|
15
|
+
* `lazySlots`/`claimSlots` marker-scan fallback resolves it — sound, just a
|
|
16
|
+
* per-claim scan instead of an O(depth) walk. This is the explicit
|
|
17
|
+
* "cannot be statically pathed" escape valve `spec/slot-unification.md`
|
|
18
|
+
* §5-A3 sanctions, not a shortcut invented here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ClaimSlotSpec {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly kind: 'text' | 'markup'
|
|
24
|
+
/** Root-relative child-index path, used when the path is unconditionally
|
|
25
|
+
* valid at claim time. Ignored when `pathExpr` is set. */
|
|
26
|
+
readonly path: readonly number[]
|
|
27
|
+
/**
|
|
28
|
+
* Raw JS expression (already rendered source text) evaluating to a
|
|
29
|
+
* `number[]` at runtime, for the one case a plain literal can't express:
|
|
30
|
+
* a hoisted-skeleton path that's valid ONLY on the fresh-CSR-clone branch,
|
|
31
|
+
* never on the hydration-adopts-SSR branch (#2143 precedent — `__p` is
|
|
32
|
+
* nulled when `__existing` is truthy because the skeleton's simplified
|
|
33
|
+
* markup doesn't describe the real SSR-rendered tree). Typically
|
|
34
|
+
* `__existing ? [] : [1, 0]`. Overrides `path` when present.
|
|
35
|
+
*/
|
|
36
|
+
readonly pathExpr?: string
|
|
37
|
+
/**
|
|
38
|
+
* Slot unification Step B (`spec/slot-unification.md` §3(b), §5 Step B):
|
|
39
|
+
* true when NO marker was emitted for this slot — `path`'s last index is
|
|
40
|
+
* the slot's own position, not an anchor comment. Only ever set by
|
|
41
|
+
* `client-only-elision.ts`-derived callers; every other emission site
|
|
42
|
+
* omits it (falsy = today's marker-based behavior, unchanged).
|
|
43
|
+
*/
|
|
44
|
+
readonly markerless?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render one `SlotSpec` as a source-text object literal. */
|
|
48
|
+
function slotSpecLiteral(slot: ClaimSlotSpec): string {
|
|
49
|
+
const pathSrc = slot.pathExpr ?? `[${slot.path.join(', ')}]`
|
|
50
|
+
const markerlessSrc = slot.markerless ? ', markerless: true' : ''
|
|
51
|
+
return `{ id: '${slot.id}', kind: '${slot.kind}', path: ${pathSrc}${markerlessSrc} }`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Render a `ClaimPlan` (an array of `SlotSpec`) as a source-text array literal. */
|
|
55
|
+
export function claimPlanLiteral(slots: readonly ClaimSlotSpec[]): string {
|
|
56
|
+
return `[${slots.map(slotSpecLiteral).join(', ')}]`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A stable JS identifier for the claimed-slot writer covering `slots`,
|
|
61
|
+
* derived from the first slot's id. Slot ids are unique per component (the
|
|
62
|
+
* `bf="sN"` marker namespace), so this can never collide across two
|
|
63
|
+
* distinct claim-plan call sites within the same emitted component.
|
|
64
|
+
*/
|
|
65
|
+
export function claimWriterVarName(slots: readonly ClaimSlotSpec[], sanitize: (id: string) => string): string {
|
|
66
|
+
const first = slots[0]?.id ?? '0'
|
|
67
|
+
return `__bfw_${sanitize(first)}`
|
|
68
|
+
}
|
|
@@ -74,6 +74,26 @@ function indexBindingLine(handler: string, indexParam: string | null, indexExpr:
|
|
|
74
74
|
return `const ${indexParam} = ${indexExpr}`
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Splice-only-when-referenced (#3, BUG-3 fix part 2): a preamble is dead
|
|
79
|
+
* weight for an event whose handler never reads one of the names it
|
|
80
|
+
* declares — the common case (an unused array builder like `cells` in the
|
|
81
|
+
* BUG-3 repro). Only emit `mapPreamble` when `handler`'s free identifiers
|
|
82
|
+
* intersect `declaredNames`; otherwise the delegated handler pays nothing
|
|
83
|
+
* for a preamble it never uses. `declaredNames` is empty whenever
|
|
84
|
+
* `mapPreamble` is `null`, so the `!mapPreamble` short-circuit is mostly
|
|
85
|
+
* redundant but keeps this correct-by-construction if that ever changes.
|
|
86
|
+
*/
|
|
87
|
+
function preambleLineForHandler(
|
|
88
|
+
mapPreamble: string | null,
|
|
89
|
+
declaredNames: readonly string[],
|
|
90
|
+
handler: string,
|
|
91
|
+
): string | null {
|
|
92
|
+
if (!mapPreamble || declaredNames.length === 0) return null
|
|
93
|
+
const free = extractFreeIdentifiersFromText(handler)
|
|
94
|
+
return declaredNames.some((name) => free.has(name)) ? mapPreamble : null
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
export function stringifyEventDelegation(lines: string[], plan: EventDelegationPlan): void {
|
|
78
98
|
const { containerVar, events, itemLookup, profileComponentName } = plan
|
|
79
99
|
const eventsByName = new Map<string, LoopChildEvent[]>()
|
|
@@ -133,7 +153,8 @@ function emitKeyedLookup(
|
|
|
133
153
|
handlerCall: string,
|
|
134
154
|
lookup: KeyedItemLookup,
|
|
135
155
|
): void {
|
|
136
|
-
const { arrayExpr, param, keyWithItem, mapPreamble, hasBindings, indexParam } = lookup
|
|
156
|
+
const { arrayExpr, param, keyWithItem, mapPreamble, mapPreambleDeclaredNames, hasBindings, indexParam } = lookup
|
|
157
|
+
const preambleLine = preambleLineForHandler(mapPreamble, mapPreambleDeclaredNames, ev.handler)
|
|
137
158
|
|
|
138
159
|
if (ev.nestedLoops.length === 0) {
|
|
139
160
|
// Single-level keyed lookup.
|
|
@@ -146,15 +167,28 @@ function emitKeyedLookup(
|
|
|
146
167
|
ls.push(` const __bfLoopItem = ${arrayExpr}.find(item => String(${keyWithItem}) === key)`)
|
|
147
168
|
ls.push(` if (__bfLoopItem) {`)
|
|
148
169
|
ls.push(` const ${param} = __bfLoopItem`)
|
|
149
|
-
if (
|
|
170
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
150
171
|
if (idxLine) ls.push(` ${idxLine}`)
|
|
151
|
-
|
|
172
|
+
// Leading `;` (not just relying on the preceding line's own
|
|
173
|
+
// semicolon): `handlerCall` always starts with `(` — an ASI hazard
|
|
174
|
+
// pre-existing in this branch even with neither optional line above
|
|
175
|
+
// (`const ${param} = __bfLoopItem` has no trailing `;`), which glues
|
|
176
|
+
// the call onto it as `__bfLoopItem(...)` and throws
|
|
177
|
+
// `TypeError: __bfLoopItem is not a function`. Defend at the one
|
|
178
|
+
// emission point rather than chasing every preceding-line shape.
|
|
179
|
+
ls.push(` ;${handlerCall}`)
|
|
152
180
|
ls.push(` }`)
|
|
153
181
|
} else {
|
|
182
|
+
// The preamble (when referenced) and the handler call both run INSIDE
|
|
183
|
+
// the item null guard — a `.find()` miss (stale-DOM race, e.g. the
|
|
184
|
+
// clicked row's key no longer in the current array) must short-circuit
|
|
185
|
+
// before a preamble that dereferences the item ever runs (BUG-4).
|
|
154
186
|
ls.push(` const ${param} = ${arrayExpr}.find(item => String(${keyWithItem}) === key)`)
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
187
|
+
ls.push(` if (${param}) {`)
|
|
188
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
189
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
190
|
+
ls.push(` ;${handlerCall}`)
|
|
191
|
+
ls.push(` }`)
|
|
158
192
|
}
|
|
159
193
|
ls.push(` }`)
|
|
160
194
|
return
|
|
@@ -187,10 +221,14 @@ function emitKeyedLookup(
|
|
|
187
221
|
}
|
|
188
222
|
const outerGuard = hasBindings ? '__bfLoopItem' : param
|
|
189
223
|
const allParams = [outerGuard, ...ev.nestedLoops.map(n => n.param)]
|
|
190
|
-
|
|
224
|
+
// Preamble and idx binding run INSIDE the combined item guard (BUG-4) —
|
|
225
|
+
// a nested `.find()` miss must short-circuit before the preamble runs.
|
|
191
226
|
const idxLine = indexBindingLine(ev.handler, indexParam, `${arrayExpr}.findIndex(item => String(${keyWithItem}) === outerKey)`)
|
|
192
|
-
|
|
193
|
-
|
|
227
|
+
ls.push(` if (${allParams.join(' && ')}) {`)
|
|
228
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
229
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
230
|
+
ls.push(` ;${handlerCall}`)
|
|
231
|
+
ls.push(` }`)
|
|
194
232
|
}
|
|
195
233
|
|
|
196
234
|
function emitDynamicIndexLookup(
|
|
@@ -199,7 +237,8 @@ function emitDynamicIndexLookup(
|
|
|
199
237
|
handlerCall: string,
|
|
200
238
|
lookup: DynamicIndexItemLookup,
|
|
201
239
|
): void {
|
|
202
|
-
const { arrayExpr, param, mapPreamble, hasBindings, indexParam } = lookup
|
|
240
|
+
const { arrayExpr, param, mapPreamble, mapPreambleDeclaredNames, hasBindings, indexParam } = lookup
|
|
241
|
+
const preambleLine = preambleLineForHandler(mapPreamble, mapPreambleDeclaredNames, ev.handler)
|
|
203
242
|
const idxLine = indexBindingLine(ev.handler, indexParam, 'idx')
|
|
204
243
|
ls.push(` const li = ${varSlotId(ev.childSlotId)}El.closest('li, [bf-i]')`)
|
|
205
244
|
ls.push(` if (li && li.parentElement) {`)
|
|
@@ -208,15 +247,19 @@ function emitDynamicIndexLookup(
|
|
|
208
247
|
ls.push(` const __bfLoopItem = ${arrayExpr}[idx]`)
|
|
209
248
|
ls.push(` if (__bfLoopItem) {`)
|
|
210
249
|
ls.push(` const ${param} = __bfLoopItem`)
|
|
211
|
-
if (
|
|
250
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
212
251
|
if (idxLine) ls.push(` ${idxLine}`)
|
|
213
|
-
ls.push(`
|
|
252
|
+
ls.push(` ;${handlerCall}`)
|
|
214
253
|
ls.push(` }`)
|
|
215
254
|
} else {
|
|
255
|
+
// Preamble and idx binding run INSIDE the item null guard (BUG-4) — an
|
|
256
|
+
// out-of-range index (stale-DOM race) must short-circuit first.
|
|
216
257
|
ls.push(` const ${param} = ${arrayExpr}[idx]`)
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
258
|
+
ls.push(` if (${param}) {`)
|
|
259
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
260
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
261
|
+
ls.push(` ;${handlerCall}`)
|
|
262
|
+
ls.push(` }`)
|
|
220
263
|
}
|
|
221
264
|
ls.push(` }`)
|
|
222
265
|
}
|
|
@@ -228,7 +271,8 @@ function emitStaticIndexLookup(
|
|
|
228
271
|
lookup: StaticIndexItemLookup,
|
|
229
272
|
containerVar: string,
|
|
230
273
|
): void {
|
|
231
|
-
const { arrayExpr, param, mapPreamble, offset, indexParam } = lookup
|
|
274
|
+
const { arrayExpr, param, mapPreamble, mapPreambleDeclaredNames, offset, indexParam } = lookup
|
|
275
|
+
const preambleLine = preambleLineForHandler(mapPreamble, mapPreambleDeclaredNames, ev.handler)
|
|
232
276
|
const idxLine = indexBindingLine(ev.handler, indexParam, '__idx')
|
|
233
277
|
ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`)
|
|
234
278
|
ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`)
|
|
@@ -236,8 +280,12 @@ function emitStaticIndexLookup(
|
|
|
236
280
|
const idxOffset = buildLoopChildIndexSubtraction(offset ?? undefined)
|
|
237
281
|
ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`)
|
|
238
282
|
ls.push(` const ${param} = ${arrayExpr}[__idx]`)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
283
|
+
// Preamble and idx binding run INSIDE the item null guard (BUG-4) — an
|
|
284
|
+
// out-of-range index (stale-DOM race) must short-circuit first.
|
|
285
|
+
ls.push(` if (${param}) {`)
|
|
286
|
+
if (preambleLine) ls.push(` ${preambleLine}`)
|
|
287
|
+
if (idxLine) ls.push(` ${idxLine}`)
|
|
288
|
+
ls.push(` ;${handlerCall}`)
|
|
289
|
+
ls.push(` }`)
|
|
242
290
|
ls.push(` }`)
|
|
243
291
|
}
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
* <indent>}) }
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { keyAttrName, profileBindingId } from '../../utils.ts'
|
|
33
|
+
import { keyAttrName, profileBindingId, varSlotId } from '../../utils.ts'
|
|
34
34
|
import { emitComponentAndEventSetup } from '../shared.ts'
|
|
35
35
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
36
36
|
import { emitMultiRootTemplateCloneLines, templateRootIsSvg } from './template-parse.ts'
|
|
37
37
|
import { emitLoopChildRefs } from './loop.ts'
|
|
38
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
38
39
|
import type {
|
|
39
40
|
InnerLoopPlan,
|
|
40
41
|
InnerLoopsPlan,
|
|
@@ -110,15 +111,23 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
|
|
|
110
111
|
if (inner.childLevels.length > 0) {
|
|
111
112
|
stringifyInnerLoops(lines, inner.childLevels, `${indent} `, pc)
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
const conditionalTexts = emit.reactiveTexts.filter(t => t.insideConditional)
|
|
115
|
+
const plainTexts = emit.reactiveTexts.filter(t => !t.insideConditional)
|
|
116
|
+
for (const text of conditionalTexts) {
|
|
114
117
|
const bf = profileBindingId(pc, text.slotId)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// A fresh `claimSlots` claim on every run (not the cached `lazySlots`
|
|
119
|
+
// door): insert() may swap the branch's DOM between runs, and 'text'
|
|
120
|
+
// writes are a plain, idempotent `nodeValue` assignment with no
|
|
121
|
+
// dedup/trust-first-run state to go stale — so re-claiming here is exactly
|
|
122
|
+
// as safe as, and replaces, the old re-query-$t-on-every-run discipline.
|
|
123
|
+
lines.push(`${indent} createEffect(() => { claimSlots(__innerEl${uid}, [{ id: '${text.slotId}', kind: 'text', path: [] }]).write('${text.slotId}', String(${text.wrappedExpression})) }${bf})`)
|
|
124
|
+
}
|
|
125
|
+
if (plainTexts.length > 0) {
|
|
126
|
+
const slots: ClaimSlotSpec[] = plainTexts.map(t => ({ id: t.slotId, kind: 'text', path: [] }))
|
|
127
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
128
|
+
lines.push(`${indent} const ${writer} = lazySlots(__innerEl${uid}, ${claimPlanLiteral(slots)})`)
|
|
129
|
+
for (const text of plainTexts) {
|
|
130
|
+
lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`)
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
for (const attr of emit.reactiveAttrs) {
|
|
@@ -34,6 +34,7 @@ import type { InsertPlan, InsertArm, ArmBody, ScopeRef } from '../plan/types.ts'
|
|
|
34
34
|
import { stringifyBranchLoops } from './branch-loop.ts'
|
|
35
35
|
import { emitListenerLine } from './event-listener.ts'
|
|
36
36
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
37
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
37
38
|
|
|
38
39
|
export interface StringifyInsertOptions {
|
|
39
40
|
/** Indent on the `insert(` line itself. */
|
|
@@ -173,18 +174,23 @@ function emitArmBody(
|
|
|
173
174
|
lines.push(`${indent}} }`)
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Route through `__bfText` so a JSX-valued expression (`{cond && logo(id)}`)
|
|
177
|
+
if (body.textEffects.length > 0) {
|
|
178
|
+
// 'markup' kind so a JSX-valued expression (`{cond && logo(id)}`)
|
|
179
179
|
// re-splices the live element by identity instead of stringifying it to
|
|
180
|
-
// "[object HTMLElement]" — the
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lines.push(`${indent}}${
|
|
180
|
+
// "[object HTMLElement]" — the same contract `__bfText` used to provide
|
|
181
|
+
// (#1663). Built fresh here, inside this arm's `bindEvents` body, so
|
|
182
|
+
// every branch activation re-claims against that swap's own
|
|
183
|
+
// `__branchScope` DOM (spec/slot-unification.md §5-A3's "branch-internal
|
|
184
|
+
// slots re-claim per branch activation").
|
|
185
|
+
const slots: ClaimSlotSpec[] = body.textEffects.map(te => ({ id: te.slotId, kind: 'markup', path: [] }))
|
|
186
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
187
|
+
lines.push(`${indent}const ${writer} = lazySlots(__branchScope, ${claimPlanLiteral(slots)})`)
|
|
188
|
+
for (const te of body.textEffects) {
|
|
189
|
+
// `escapeTextOrNode`: see emit-reactive.ts's identical wrap for why a
|
|
190
|
+
// string value must be escaped before a 'markup' writer's
|
|
191
|
+
// `innerHTML =` insertion, while a live Node passes through untouched.
|
|
192
|
+
lines.push(`${indent}__disposers.push(createDisposableEffect(() => { ${writer}('${te.slotId}', escapeTextOrNode(${te.expression})) }${bindingBfId(te.slotId)}))`)
|
|
193
|
+
}
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
// Branch loops, now fully Plan-built. The stringifier writes its own
|
|
@@ -17,6 +17,7 @@ import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
|
17
17
|
import { templateRootIsSvg } from './template-parse.ts'
|
|
18
18
|
import { emitListenerLine } from './event-listener.ts'
|
|
19
19
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
20
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
20
21
|
import type {
|
|
21
22
|
BranchChildComponentInitsPlan,
|
|
22
23
|
BranchEventBindingsPlan,
|
|
@@ -153,15 +154,22 @@ export function stringifyBranchInnerLoops(
|
|
|
153
154
|
inner.outerLoopParamBindings,
|
|
154
155
|
)
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
const conditionalTexts = inner.reactiveTexts.filter(t => t.insideConditional)
|
|
158
|
+
const plainTexts = inner.reactiveTexts.filter(t => !t.insideConditional)
|
|
159
|
+
for (const text of conditionalTexts) {
|
|
157
160
|
const bf = profileBindingId(pc, text.slotId)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
// Fresh `claimSlots` claim every run (not the cached `lazySlots` door):
|
|
162
|
+
// insert() may swap the branch's DOM between runs, and a 'text' write
|
|
163
|
+
// is a plain, idempotent `nodeValue` assignment with no dedup state to
|
|
164
|
+
// go stale — replaces the old re-query-$t-on-every-run discipline.
|
|
165
|
+
lines.push(`${indent} createEffect(() => { claimSlots(__bel${uid}, [{ id: '${text.slotId}', kind: 'text', path: [] }]).write('${text.slotId}', String(${text.wrappedExpression})) }${bf})`)
|
|
166
|
+
}
|
|
167
|
+
if (plainTexts.length > 0) {
|
|
168
|
+
const slots: ClaimSlotSpec[] = plainTexts.map(t => ({ id: t.slotId, kind: 'text', path: [] }))
|
|
169
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
170
|
+
lines.push(`${indent} const ${writer} = lazySlots(__bel${uid}, ${claimPlanLiteral(slots)})`)
|
|
171
|
+
for (const text of plainTexts) {
|
|
172
|
+
lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`)
|
|
165
173
|
}
|
|
166
174
|
}
|
|
167
175
|
if (inner.nestedConditionals.length > 0) {
|
|
@@ -254,14 +262,25 @@ export function stringifyLoopChildArm(
|
|
|
254
262
|
lines.push(`${armIndent}}))`)
|
|
255
263
|
}
|
|
256
264
|
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// instead of
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
if (arm.texts.length > 0) {
|
|
266
|
+
// 'markup' kind (not 'text') because a Child-position expression's value
|
|
267
|
+
// may be a live Node (e.g. a hoisted `renderNode={(n) => <PillNode/>}`
|
|
268
|
+
// callback, #1213) — claim-slots' markup writer splices a Node in by
|
|
269
|
+
// identity instead of stringifying it to "[object HTMLElement]" (#2347),
|
|
270
|
+
// the same contract `__bfText` used to provide. The writer is built
|
|
271
|
+
// fresh HERE, inside this arm's `bindEvents` body, so every branch
|
|
272
|
+
// activation re-claims against that swap's own `__branchScope` DOM
|
|
273
|
+
// (spec/slot-unification.md §5-A3's "branch-internal slots re-claim per
|
|
274
|
+
// branch activation").
|
|
275
|
+
const slots: ClaimSlotSpec[] = arm.texts.map(t => ({ id: t.slotId, kind: 'markup', path: [] }))
|
|
276
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
277
|
+
lines.push(`${armIndent}const ${writer} = lazySlots(__branchScope, ${claimPlanLiteral(slots)})`)
|
|
278
|
+
for (const text of arm.texts) {
|
|
279
|
+
// `escapeTextOrNode`: see emit-reactive.ts's identical wrap for why a
|
|
280
|
+
// string value must be escaped before a 'markup' writer's
|
|
281
|
+
// `innerHTML =` insertion, while a live Node passes through untouched.
|
|
282
|
+
lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => { ${writer}('${text.slotId}', escapeTextOrNode(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)}))`)
|
|
283
|
+
}
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
lines.push(`${armIndent}return () => __disposers.forEach(d => d())`)
|