@barefootjs/jsx 0.34.0 → 0.35.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/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -215
- package/dist/ir-to-client-js/collect-elements.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-component-loop.d.ts.map +1 -1
- 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-lazy-row.d.ts +11 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.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/build-plain-row.d.ts +52 -0
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +2 -0
- 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/lazy-conditional.d.ts +6 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -6
- 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 +11 -9
- package/dist/ir-to-client-js/control-flow/shared.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/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -2
- 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/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
- package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
- package/dist/ir-to-client-js/safe-html.d.ts +158 -0
- package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
- package/dist/ir-to-client-js/utils.d.ts +34 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +22 -1
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +10 -12
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/lazy-conditional.test.ts +52 -12
- package/src/__tests__/lazy-preamble.test.ts +11 -11
- package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/nested-loop-index-param.test.ts +14 -3
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +4 -2
- package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
- package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
- package/src/__tests__/safe-html-splice-door.test.ts +61 -0
- package/src/__tests__/ssr-defaults.test.ts +59 -0
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +8 -8
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +18 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +110 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/shared.ts +40 -17
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +10 -0
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +4 -1
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +58 -244
- package/src/ir-to-client-js/imports.ts +3 -0
- package/src/ir-to-client-js/index.ts +4 -4
- package/src/ir-to-client-js/markup-slots.ts +25 -0
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +115 -20
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -68,9 +68,10 @@
|
|
|
68
68
|
* markup, which is `preambleRegions` territory (refused separately).
|
|
69
69
|
* - No `builderNames`. Same shape as above, pinned explicitly so the reason
|
|
70
70
|
* names it rather than surfacing as a confusing "contains a call".
|
|
71
|
-
* - The preamble
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
71
|
+
* - (#2859 follow-up) The preamble MAY read the loop's index parameter — it
|
|
72
|
+
* survives into `freeNames` below like any other read, so a binding that
|
|
73
|
+
* names this preamble local substitutes through to it and is classified
|
|
74
|
+
* the same way a direct index reference is (forced into `applyItem`).
|
|
74
75
|
*
|
|
75
76
|
* Member access is deliberately NOT banned even though a getter could in
|
|
76
77
|
* principle run code: a binding expression like `class={row.cls}` is already
|
|
@@ -130,9 +131,6 @@ const NO = (reason: string): LazyPreambleAnalysis => ({ lazySafe: false, reason
|
|
|
130
131
|
* Decide whether `preamble` may be re-executed inside a lazy row's apply
|
|
131
132
|
* bodies, and if so what it declares and reads.
|
|
132
133
|
*
|
|
133
|
-
* `indexParam` is the loop's index parameter name as the emitter uses it
|
|
134
|
-
* (`elem.index || '__idx'`); a preamble reading it is refused.
|
|
135
|
-
*
|
|
136
134
|
* `primableNames` are the component's signal getters and memos — the only
|
|
137
135
|
* callees a preamble initializer may invoke (see the module docstring). Pass
|
|
138
136
|
* the same names `LazyRowScopeInfo` carries, so "primable here" and "primable
|
|
@@ -140,7 +138,6 @@ const NO = (reason: string): LazyPreambleAnalysis => ({ lazySafe: false, reason
|
|
|
140
138
|
*/
|
|
141
139
|
export function analyzeLazyPreamble(
|
|
142
140
|
preamble: MapCallbackPreamble | undefined,
|
|
143
|
-
indexParam: string,
|
|
144
141
|
primableNames: ReadonlySet<string>,
|
|
145
142
|
): LazyPreambleAnalysis {
|
|
146
143
|
if (!preamble) return NO_PREAMBLE
|
|
@@ -196,13 +193,14 @@ export function analyzeLazyPreamble(
|
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
196
|
+
// A preamble reading the loop index (#2859 follow-up, widening from a hard
|
|
197
|
+
// refusal) is no longer refused here: `indexParam` survives into
|
|
198
|
+
// `freeNames` below, and any binding that names this preamble local
|
|
199
|
+
// substitutes through to it (`classifyLazyBinding`'s preamble handling),
|
|
200
|
+
// which classifies a free `indexParam` the same way a direct reference
|
|
201
|
+
// would — forcing the binding into `applyItem` so a reorder re-evaluates
|
|
202
|
+
// it via `entry.index`.
|
|
202
203
|
const readNames = extractFreeIdentifiersFromStatementText(text)
|
|
203
|
-
if (readNames.has(indexParam) && !declaredNames.has(indexParam)) {
|
|
204
|
-
return NO(`map-callback preamble reads the loop index parameter '${indexParam}'`)
|
|
205
|
-
}
|
|
206
204
|
|
|
207
205
|
const freeNames = new Set(readNames)
|
|
208
206
|
for (const declared of declaredNames) freeNames.delete(declared)
|
|
@@ -284,9 +284,15 @@ export function lazyRowEligibility(args: LazyRowEligibilityArgs): LazyRowEligibi
|
|
|
284
284
|
|
|
285
285
|
// Per-binding gates.
|
|
286
286
|
for (const b of bindings) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
// A binding that references the loop index (#2859 follow-up, widening
|
|
288
|
+
// from a hard refusal): `applyItem`/`applyOuter` have no index PARAMETER,
|
|
289
|
+
// but the reconciler tracks each entry's current position on `entry.index`
|
|
290
|
+
// (`LazyRowEntry.index`, `map-array-lazy.ts`) exactly like it tracks
|
|
291
|
+
// `entry.item` — a reorder is a position change the reconciler already
|
|
292
|
+
// detects, the same way an item change is. `classifyLazyBinding` forces
|
|
293
|
+
// `readsItem: true` for any binding with `referencesIndex`, so it is
|
|
294
|
+
// ALWAYS emitted into `applyItem`, and the runtime calls `applyItem` on a
|
|
295
|
+
// pure reorder too when `plan.indexDriven` (`LazyRowPlanData.readsIndex`).
|
|
290
296
|
// An identifier set we could not compute at all is different in kind
|
|
291
297
|
// from a name we CAN see but cannot prime. With `free === null` the
|
|
292
298
|
// classifier knows nothing — not the outer reads, and not whether the
|
|
@@ -445,7 +451,11 @@ export function classifyLazyBinding(args: {
|
|
|
445
451
|
return
|
|
446
452
|
}
|
|
447
453
|
if (name === indexParam) {
|
|
454
|
+
// The row's current position (#2859 follow-up) — the reconciler
|
|
455
|
+
// delivers it on `entry.index` exactly like it delivers the item on
|
|
456
|
+
// `entry.item`, so a binding that reads it is item-driven too.
|
|
448
457
|
referencesIndex = true
|
|
458
|
+
readsItem = true
|
|
449
459
|
return
|
|
450
460
|
}
|
|
451
461
|
if (INERT_BINDING_GLOBALS.has(name)) return
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
TopLevelLoop,
|
|
24
24
|
} from '../../types.ts'
|
|
25
25
|
import type { IRLoopChildComponent } from '../../../types.ts'
|
|
26
|
+
import type { SafeHtml } from '../../safe-html.ts'
|
|
26
27
|
import type { SkeletonSlotPaths } from '../../html-template.ts'
|
|
27
28
|
import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
28
29
|
import type { InnerLoopsPlan } from './inner-loop.ts'
|
|
@@ -83,15 +84,16 @@ interface DynamicLoopCommon extends LoopPlanCommon {
|
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* A resolved preamble-patched region (#2389), ready to emit: `valueExpr` is
|
|
86
|
-
* already wrapped with the loop-param accessor and
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* already wrapped with the loop-param accessor and already went through the
|
|
88
|
+
* child-splice door (`safe-html.ts`) — the exact value `irToHtmlTemplate`
|
|
89
|
+
* computes for the same node in the row template, so a same-key update's
|
|
90
|
+
* re-patch stays byte-identical to what a fresh mount would render. Typed
|
|
91
|
+
* `SafeHtml` because the region writer is an `innerHTML` write: only a
|
|
92
|
+
* value the door (or a named producer) vouched for may reach it.
|
|
91
93
|
*/
|
|
92
94
|
export interface PreambleRegionPlan {
|
|
93
95
|
slotId: string
|
|
94
|
-
valueExpr:
|
|
96
|
+
valueExpr: SafeHtml
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/** Per-item ref callback resolved against a child slot for emission (#1244). */
|
|
@@ -237,6 +239,8 @@ interface CompositeLoopVariant extends DynamicLoopCommon {
|
|
|
237
239
|
loopParam: string
|
|
238
240
|
/** Destructured-binding metadata for the loop param. */
|
|
239
241
|
loopParamBindings: TopLevelLoop['paramBindings']
|
|
242
|
+
/** This loop's index param name, when it declares one (#2859). */
|
|
243
|
+
loopIndex: TopLevelLoop['index']
|
|
240
244
|
/** Reactive effects rendered after the SSR/CSR split. */
|
|
241
245
|
reactiveEffects: ReactiveEffectsPlan | null
|
|
242
246
|
/**
|
|
@@ -17,6 +17,7 @@ import type { IRLoopChildComponent, LoopParamBinding } from '../../types.ts'
|
|
|
17
17
|
import { preambleAnalysisText } from '../../types.ts'
|
|
18
18
|
import { quotePropName, wrapLoopParamAsAccessor, irChildrenFreeIds, attrValueToString } from '../utils.ts'
|
|
19
19
|
import { irChildrenToJsExpr, jsxChildrenPropGetterExpr } from '../html-template.ts'
|
|
20
|
+
import { spliceChildValue } from '../safe-html.ts'
|
|
20
21
|
import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
21
22
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
22
23
|
import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
@@ -147,11 +148,12 @@ export function buildChildRefBindings(
|
|
|
147
148
|
refs: readonly LoopChildRef[],
|
|
148
149
|
loopParam: string,
|
|
149
150
|
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
151
|
+
loopIndex?: string | null,
|
|
150
152
|
): readonly LoopChildRefBinding[] {
|
|
151
153
|
if (refs.length === 0) return []
|
|
152
154
|
return refs.map(r => ({
|
|
153
155
|
childSlotId: r.childSlotId,
|
|
154
|
-
callback: wrapLoopParamAsAccessor(r.callback, loopParam, loopParamBindings),
|
|
156
|
+
callback: wrapLoopParamAsAccessor(r.callback, loopParam, loopParamBindings, loopIndex),
|
|
155
157
|
}))
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -241,23 +243,27 @@ export function destructureLoopParam(
|
|
|
241
243
|
/**
|
|
242
244
|
* Resolve `IRLoop.preambleRegions` (#2389) into ready-to-emit
|
|
243
245
|
* `PreambleRegionPlan`s: wrap each source expression with the loop-param
|
|
244
|
-
* accessor and
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
246
|
+
* accessor and hand it to the same door (`spliceChildValue`) the row
|
|
247
|
+
* template's `irToHtmlTemplate` uses for the identical node — so the
|
|
248
|
+
* region-patch effect's re-render (a `'markup'` claim writer, i.e. an
|
|
249
|
+
* `innerHTML` write) matches what a fresh mount would have produced, and
|
|
250
|
+
* the escape decision is made in exactly one place. Shared by the
|
|
251
|
+
* top-level (`build-loop.ts`) and branch (`build-branch-loop.ts`)
|
|
252
|
+
* plain-loop-plan builders.
|
|
249
253
|
*/
|
|
250
254
|
export function buildPreambleRegionPlans(
|
|
251
255
|
regions: readonly PreambleRegionSource[] | undefined,
|
|
252
256
|
loopParam: string,
|
|
253
257
|
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
258
|
+
loopIndex?: string | null,
|
|
254
259
|
): readonly PreambleRegionPlan[] {
|
|
255
260
|
if (!regions || regions.length === 0) return []
|
|
256
261
|
return regions.map((r) => {
|
|
257
|
-
const wrapped = wrapLoopParamAsAccessor(r.expr, loopParam, loopParamBindings)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
const wrapped = wrapLoopParamAsAccessor(r.expr, loopParam, loopParamBindings, loopIndex)
|
|
263
|
+
// The region's row-template twin carries the same `slotId`, so the door
|
|
264
|
+
// takes the slotted arm; no `markupSlotIds` here — a loop-row slot's
|
|
265
|
+
// initial render is plain `escapeText`, matching `irToHtmlTemplate`.
|
|
266
|
+
const valueExpr = spliceChildValue({ expr: r.expr, slotId: r.slotId, joinArrayChild: r.joinArrayChild }, wrapped, {})
|
|
261
267
|
return { slotId: r.slotId, valueExpr }
|
|
262
268
|
})
|
|
263
269
|
}
|
|
@@ -270,8 +276,9 @@ export function buildComponentPropsExpr(
|
|
|
270
276
|
comp: { props: Array<{ name: string; value: import('../../types.ts').AttrValue; isEventHandler: boolean }>, children?: import('../../types.ts').IRNode[] },
|
|
271
277
|
loopParam?: string,
|
|
272
278
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
279
|
+
loopIndex?: string | null,
|
|
273
280
|
): string {
|
|
274
|
-
const wrap = loopParam ? (expr: string) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings) : (expr: string) => expr
|
|
281
|
+
const wrap = loopParam ? (expr: string) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings, loopIndex) : (expr: string) => expr
|
|
275
282
|
const entries = comp.props.map((p) => {
|
|
276
283
|
if (p.isEventHandler) {
|
|
277
284
|
const handlerExpr = attrValueToString(p.value) ?? 'undefined'
|
|
@@ -347,7 +354,20 @@ export function buildDepthLevels(
|
|
|
347
354
|
}))
|
|
348
355
|
}
|
|
349
356
|
|
|
350
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* Emit a single addEventListener call for a child event on a given element.
|
|
359
|
+
*
|
|
360
|
+
* This is a DIRECT, one-time listener attach (composite/inner/conditional-arm
|
|
361
|
+
* loop shapes) — unlike the top-level delegated click dispatcher
|
|
362
|
+
* (`build-event-delegation.ts`), which re-derives the current index at click
|
|
363
|
+
* time via `arr.findIndex(...)` and is unaffected by this. A handler wired
|
|
364
|
+
* here closes over whatever `renderItem` bound its raw index parameter to at
|
|
365
|
+
* ROW CREATION time, so a reference to it needs the same accessor-call
|
|
366
|
+
* rewrite as every other loop-index read in this row (#2859) — otherwise a
|
|
367
|
+
* row that survives a same-key reorder keeps firing the handler with the
|
|
368
|
+
* stale, as-created index forever, the click-handler analogue of the
|
|
369
|
+
* render-output bug #2859 itself fixes.
|
|
370
|
+
*/
|
|
351
371
|
function emitEventSetup(
|
|
352
372
|
ls: string[],
|
|
353
373
|
indent: string,
|
|
@@ -356,8 +376,9 @@ function emitEventSetup(
|
|
|
356
376
|
loopParam?: string,
|
|
357
377
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
358
378
|
bodyIsMultiRoot: boolean = false,
|
|
379
|
+
loopIndex?: string | null,
|
|
359
380
|
): void {
|
|
360
|
-
const handler = loopParam ? wrapLoopParamAsAccessor(ev.handler, loopParam, loopParamBindings) : ev.handler
|
|
381
|
+
const handler = loopParam ? wrapLoopParamAsAccessor(ev.handler, loopParam, loopParamBindings, loopIndex) : ev.handler
|
|
361
382
|
emitListenerBlock(ls, indent, elVar, ev.childSlotId, '__e', ev.eventName, handler, 'dom', bodyIsMultiRoot)
|
|
362
383
|
}
|
|
363
384
|
|
|
@@ -412,14 +433,15 @@ export function emitComponentAndEventSetup(
|
|
|
412
433
|
loopParam?: string,
|
|
413
434
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
414
435
|
bodyIsMultiRoot: boolean = false,
|
|
436
|
+
loopIndex?: string | null,
|
|
415
437
|
): void {
|
|
416
|
-
const wrap = loopParam ? (expr: string) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings) : (expr: string) => expr
|
|
438
|
+
const wrap = loopParam ? (expr: string) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings, loopIndex) : (expr: string) => expr
|
|
417
439
|
// Multi-root loop bodies (#1212) must search the item's sibling roots
|
|
418
440
|
// and the pre-insertion `__bfExtras` stash; `upsertChildItem` wraps the
|
|
419
441
|
// single-root `upsertChild` semantics in that walk.
|
|
420
442
|
const upsertFn = bodyIsMultiRoot ? 'upsertChildItem' : 'upsertChild'
|
|
421
443
|
for (const comp of comps) {
|
|
422
|
-
const propsExpr = buildComponentPropsExpr(comp, loopParam, loopParamBindings)
|
|
444
|
+
const propsExpr = buildComponentPropsExpr(comp, loopParam, loopParamBindings, loopIndex)
|
|
423
445
|
// Check if children are text-equivalent and reference the loop param — if so,
|
|
424
446
|
// emit a createEffect to reactively update textContent when the signal changes.
|
|
425
447
|
// Text-equivalent: expression, text, or conditional with text-only branches.
|
|
@@ -429,7 +451,8 @@ export function emitComponentAndEventSetup(
|
|
|
429
451
|
const rawChildrenExpr = isTextOnly ? irChildrenToJsExpr(comp.children!) : null
|
|
430
452
|
const childrenFreeIds = isTextOnly && comp.children ? irChildrenFreeIds(comp.children) : undefined
|
|
431
453
|
const childrenRefsLoop = loopParam != null && rawChildrenExpr != null && childrenFreeIds != null
|
|
432
|
-
&& exprRefsLoopBinding(childrenFreeIds, { param: loopParam, paramBindings: loopParamBindings })
|
|
454
|
+
&& (exprRefsLoopBinding(childrenFreeIds, { param: loopParam, paramBindings: loopParamBindings })
|
|
455
|
+
|| (!!loopIndex && childrenFreeIds.has(loopIndex)))
|
|
433
456
|
|
|
434
457
|
const slotIdLit = comp.slotId ? `'${comp.slotId}'` : 'null'
|
|
435
458
|
const keyProp = comp.props.find(p => p.name === 'key')
|
|
@@ -449,7 +472,7 @@ export function emitComponentAndEventSetup(
|
|
|
449
472
|
}
|
|
450
473
|
}
|
|
451
474
|
for (const ev of events) {
|
|
452
|
-
emitEventSetup(ls, indent, elVar, ev, loopParam, loopParamBindings, bodyIsMultiRoot)
|
|
475
|
+
emitEventSetup(ls, indent, elVar, ev, loopParam, loopParamBindings, bodyIsMultiRoot, loopIndex)
|
|
453
476
|
}
|
|
454
477
|
}
|
|
455
478
|
|
|
@@ -53,6 +53,7 @@ export function stringifyCompositeLoop(lines: string[], plan: CompositeLoopPlan)
|
|
|
53
53
|
innerLoops,
|
|
54
54
|
loopParam,
|
|
55
55
|
loopParamBindings,
|
|
56
|
+
loopIndex,
|
|
56
57
|
reactiveEffects,
|
|
57
58
|
childRefs,
|
|
58
59
|
branchClearChildren,
|
|
@@ -124,7 +125,7 @@ export function stringifyCompositeLoop(lines: string[], plan: CompositeLoopPlan)
|
|
|
124
125
|
// variant whose tail needs the row already connected.
|
|
125
126
|
mountRow: true,
|
|
126
127
|
})
|
|
127
|
-
emitComponentAndEventSetup(lines, bodyIndent, '__el', compsArr, eventsArr, loopParam, loopParamBindings, bodyIsMultiRoot)
|
|
128
|
+
emitComponentAndEventSetup(lines, bodyIndent, '__el', compsArr, eventsArr, loopParam, loopParamBindings, bodyIsMultiRoot, loopIndex)
|
|
128
129
|
if (innerLoops.length > 0) {
|
|
129
130
|
stringifyInnerLoops(lines, innerLoops, bodyIndent, pc)
|
|
130
131
|
}
|
|
@@ -191,6 +191,11 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
191
191
|
// --- createRow ---------------------------------------------------------
|
|
192
192
|
const b1 = `${indent} `
|
|
193
193
|
const b2 = `${indent} `
|
|
194
|
+
// Tells the runtime to also call `applyItem` on a pure reorder (#2859
|
|
195
|
+
// follow-up) — see `LazyRowPlan.indexDriven`'s docstring. Omitted (not just
|
|
196
|
+
// `false`) for every loop that never reads the index, byte-stable with
|
|
197
|
+
// this widening's predecessor.
|
|
198
|
+
if (lazyRow.readsIndex) lines.push(`${b1}indexDriven: true,`)
|
|
194
199
|
lines.push(`${b1}createRow: (__e, ${o.indexParam}) => {`)
|
|
195
200
|
// Always bound, even with no reactive bindings: the non-hoisted clone
|
|
196
201
|
// interpolates the per-row template, which reads the param for at least
|
|
@@ -225,6 +230,9 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
225
230
|
} else {
|
|
226
231
|
lines.push(`${b1}applyItem: (__e) => {`)
|
|
227
232
|
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
233
|
+
// The row's current position (#2859 follow-up) — only when some binding
|
|
234
|
+
// here actually reads it, so every other loop's applyItem is unchanged.
|
|
235
|
+
if (lazyRow.readsIndex) lines.push(`${b2}const ${o.indexParam} = __e.index`)
|
|
228
236
|
lines.push(`${b2}const __r = __e.refs ?? (__e.refs = [])`)
|
|
229
237
|
lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`)
|
|
230
238
|
// Re-run the preamble only when a binding in THIS body reads a local it
|
|
@@ -255,6 +263,8 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
255
263
|
for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`)
|
|
256
264
|
lines.push(`${b2}for (const __e of __es) {`)
|
|
257
265
|
lines.push(`${b3}const ${paramHead} = () => __e.item`)
|
|
266
|
+
// Same as `applyItem` — the row's current position (#2859 follow-up).
|
|
267
|
+
if (lazyRow.readsIndex) lines.push(`${b3}const ${o.indexParam} = __e.index`)
|
|
258
268
|
lines.push(`${b3}const __r = __e.refs ?? (__e.refs = [])`)
|
|
259
269
|
lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`)
|
|
260
270
|
// Per ENTRY, not once for the batch: the preamble reads the item, so its
|
|
@@ -486,6 +486,35 @@ export function resolveGetterAliases(
|
|
|
486
486
|
return aliases
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Which names in `signals`/`memos` are eligible ALIAS-HOP TARGETS for
|
|
491
|
+
* `resolveGetterAliases`'s `isGetter` predicate — a non-module,
|
|
492
|
+
* non-env-reader signal getter, or a non-module memo name. Env-signal
|
|
493
|
+
* getters are excluded because they resolve through their own
|
|
494
|
+
* request-scoped binding (an adapter's `searchParamsFieldRef`-style
|
|
495
|
+
* lookup or equivalent), never a seeded/`out`-keyed value an alias could
|
|
496
|
+
* usefully point at.
|
|
497
|
+
*
|
|
498
|
+
* Shared by every `resolveGetterAliases` call site (#2813's `ssr-defaults.ts`
|
|
499
|
+
* seeding and the Go adapter's `rootFieldRef` field-routing) so the
|
|
500
|
+
* eligibility rule can't silently drift between them — the "one decision,
|
|
501
|
+
* two implementations" defect class CLAUDE.md calls out (pullfrog review
|
|
502
|
+
* on #2854).
|
|
503
|
+
*/
|
|
504
|
+
export function collectAliasableGetterNames(
|
|
505
|
+
signals: readonly SignalInfo[],
|
|
506
|
+
memos: readonly MemoInfo[],
|
|
507
|
+
): Set<string> {
|
|
508
|
+
const getterNames = new Set<string>()
|
|
509
|
+
for (const sig of signals) {
|
|
510
|
+
if (sig.getter && !sig.isModule && !sig.envReader) getterNames.add(sig.getter)
|
|
511
|
+
}
|
|
512
|
+
for (const memo of memos) {
|
|
513
|
+
if (!memo.isModule) getterNames.add(memo.name)
|
|
514
|
+
}
|
|
515
|
+
return getterNames
|
|
516
|
+
}
|
|
517
|
+
|
|
489
518
|
/**
|
|
490
519
|
* Build the CSR substitution env from the live `ClientJsContext`.
|
|
491
520
|
*
|
|
@@ -11,6 +11,7 @@ import type { ClientJsContext } from './types.ts'
|
|
|
11
11
|
import { toHtmlAttrName, varSlotId, PROPS_PARAM } from './utils.ts'
|
|
12
12
|
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './control-flow/stringify/claim-plan.ts'
|
|
13
13
|
import { createTemplateAwareStringProtector } from './html-template.ts'
|
|
14
|
+
import { DYNAMIC_ELEMENT_WRITER_KIND } from './markup-slots.ts'
|
|
14
15
|
import { datePlugin, DATE_METHODS } from '../date-lowering.ts'
|
|
15
16
|
import { toLocaleDatePlugin, foldedArgToClientJs } from '../to-locale-date-lowering.ts'
|
|
16
17
|
import { tsNodeToParsedExpr } from '../expression-parser.ts'
|
|
@@ -393,9 +394,11 @@ export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): v
|
|
|
393
394
|
// 'text') because the value may be a live Node (e.g. a JSX-returning
|
|
394
395
|
// call like `{themeLogo(id)}`) — the writer splices a Node in by
|
|
395
396
|
// identity, exactly the contract `__bfText` used to provide (#1663).
|
|
397
|
+
// The kind is a named constant so the initial-render side
|
|
398
|
+
// (`markup-slots.ts` → `escapeTextOrMarkup`) reads the same decision.
|
|
396
399
|
let writer = ''
|
|
397
400
|
if (normalElems.length > 0) {
|
|
398
|
-
const slots: ClaimSlotSpec[] = normalElems.map(elem => ({ id: elem.slotId, kind:
|
|
401
|
+
const slots: ClaimSlotSpec[] = normalElems.map(elem => ({ id: elem.slotId, kind: DYNAMIC_ELEMENT_WRITER_KIND, path: [] }))
|
|
399
402
|
writer = claimWriterVarName(slots, varSlotId)
|
|
400
403
|
lines.push(` const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`)
|
|
401
404
|
}
|
|
@@ -9,6 +9,7 @@ import type { ClientJsContext } from './types.ts'
|
|
|
9
9
|
import { PROPS_PARAM } from './utils.ts'
|
|
10
10
|
import { computeInlinability, toLegacyInlinability } from './compute-inlinability.ts'
|
|
11
11
|
import { canGenerateStaticTemplate, irToComponentTemplate, generateCsrTemplate, createStringProtector } from './html-template.ts'
|
|
12
|
+
import { markupSlotIdsOf } from './markup-slots.ts'
|
|
12
13
|
import { nameForRegistryRef } from './component-scope.ts'
|
|
13
14
|
import { resolveRestSpreadNames } from './prop-handling.ts'
|
|
14
15
|
|
|
@@ -219,10 +220,7 @@ export function emitRegistrationAndHydration(
|
|
|
219
220
|
// Build ComponentDef object for hydrate()
|
|
220
221
|
const defParts: string[] = [`init: init${name}`]
|
|
221
222
|
if (canGenerateStaticTemplate(_ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
|
|
222
|
-
|
|
223
|
-
// (#2651) — see `generateCsrTemplate`'s identical derivation below
|
|
224
|
-
// for why this is reused as-is rather than re-derived.
|
|
225
|
-
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
223
|
+
const markupSlotIds = markupSlotIdsOf(ctx)
|
|
226
224
|
const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds, ctx.restPropsName)
|
|
227
225
|
if (templateHtml) {
|
|
228
226
|
defParts.push(buildTemplateDefPart(ctx, templateHtml))
|