@barefootjs/jsx 0.29.0 → 0.30.2
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/adapters/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +990 -367
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- 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-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- 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/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +24 -7
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +167 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +409 -24
- package/src/prop-rewrite.ts +149 -19
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
|
@@ -70,7 +70,12 @@ import {
|
|
|
70
70
|
emitTemplateCloneInline,
|
|
71
71
|
hoistedCloneExpr,
|
|
72
72
|
} from './template-parse.ts'
|
|
73
|
-
import type {
|
|
73
|
+
import type {
|
|
74
|
+
LazyRowAttrBinding,
|
|
75
|
+
LazyRowConditionalBinding,
|
|
76
|
+
LazyRowPlanData,
|
|
77
|
+
LazyRowTextBinding,
|
|
78
|
+
} from '../plan/build-lazy-row.ts'
|
|
74
79
|
|
|
75
80
|
export interface StringifyLazyRowOptions {
|
|
76
81
|
/** Indent of the `mapArrayLazy(` line itself. */
|
|
@@ -94,9 +99,11 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
94
99
|
const { indent, lazyRow, paramHead } = o
|
|
95
100
|
const mid = o.markerId.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
96
101
|
const tplVar = `__tpl_${mid}`
|
|
97
|
-
const claimVar = `__lzc_${mid}`
|
|
98
102
|
const hasRefs = lazyRow.attrSlotIds.length > 0 || lazyRow.texts.length > 0
|
|
99
|
-
|
|
103
|
+
// Conditionals count: `createRow` seeds their dedup boolean into `__l`, so a
|
|
104
|
+
// row whose ONLY reactive thing is a conditional still needs `__l` declared.
|
|
105
|
+
const hasBindings =
|
|
106
|
+
lazyRow.attrs.length > 0 || lazyRow.texts.length > 0 || lazyRow.conditionals.length > 0
|
|
100
107
|
// ONE per-loop door decision (see `refParts`): the read-capable claim, or
|
|
101
108
|
// today's write-only writer. Never decided per binding — the door is
|
|
102
109
|
// allocated once per ROW.
|
|
@@ -165,28 +172,17 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// is nothing to defer (see the `deferDoor` note on `refParts`).
|
|
180
|
-
if (hasRefs) {
|
|
181
|
-
// With the door deferred, the ELEMENT refs are the only parts that read the
|
|
182
|
-
// row root — so a text-only row (no reactive-attr slot) claims to a bare
|
|
183
|
-
// `[null]` and the `__el` binding would be dead. `parts` decides, rather
|
|
184
|
-
// than a second predicate that could drift from `refParts`.
|
|
185
|
-
const parts = refParts(lazyRow, '__el', null, adoptedPlanVar, true)
|
|
186
|
-
lines.push(`${indent}const ${claimVar} = (__e) => {`)
|
|
187
|
-
if (parts.some(p => p.includes('__el'))) lines.push(`${indent} const __el = __e.primaryEl`)
|
|
188
|
-
lines.push(`${indent} return [${parts.join(', ')}]`)
|
|
189
|
-
lines.push(`${indent}}`)
|
|
175
|
+
// Hoisted arm templates, one pair per conditional. Both arms are static by
|
|
176
|
+
// construction (`analyzeLazyConditional`), so each is parsed ONCE per loop and
|
|
177
|
+
// cloned on a flip — a per-row `innerHTML` parse would undo the point of
|
|
178
|
+
// driving the swap from the loop level. The comparison the seed run makes is
|
|
179
|
+
// against `content.firstElementChild.outerHTML`, i.e. the browser's OWN
|
|
180
|
+
// serialization of the same markup, so it cannot false-mismatch on attribute
|
|
181
|
+
// order or quoting the way comparing against the authored string would.
|
|
182
|
+
for (const c of lazyRow.conditionals) {
|
|
183
|
+
const v = condVars(mid, c.slotId)
|
|
184
|
+
emitHoistedTemplateDecl(lines, indent, v.trueTpl, c.whenTrueHtml)
|
|
185
|
+
emitHoistedTemplateDecl(lines, indent, v.falseTpl, c.whenFalseHtml)
|
|
190
186
|
}
|
|
191
187
|
|
|
192
188
|
const call = `mapArrayLazy(() => ${o.arrayExpr}, ${o.containerVar}, ${o.keyFn}, {`
|
|
@@ -200,6 +196,9 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
200
196
|
// interpolates the per-row template, which reads the param for at least
|
|
201
197
|
// the `data-key` attribute.
|
|
202
198
|
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
199
|
+
// The row's `.map()` callback preamble, before the clone: a non-hoisted
|
|
200
|
+
// per-row template interpolates values the preamble declares.
|
|
201
|
+
if (lazyRow.preambleStatements) lines.push(`${b2}${lazyRow.preambleStatements}`)
|
|
203
202
|
const cloneExpr = useHoisted
|
|
204
203
|
? hoistedCloneExpr(tplVar, o.skeletonTemplate!)
|
|
205
204
|
: `(() => { ${emitTemplateCloneInline(o.template)} })()`
|
|
@@ -213,31 +212,42 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
213
212
|
const createDoor = `__r[${lazyRow.writerIndex}]`
|
|
214
213
|
for (const t of lazyRow.texts) emitTextBinding(lines, b2, t, createDoor, 'create', rwDoor)
|
|
215
214
|
}
|
|
215
|
+
for (const c of lazyRow.conditionals) emitConditional(lines, b2, mid, c, 'create')
|
|
216
216
|
lines.push(`${b2}return __el`)
|
|
217
217
|
lines.push(`${b1}},`)
|
|
218
218
|
|
|
219
219
|
// --- applyItem ---------------------------------------------------------
|
|
220
220
|
const itemAttrs = lazyRow.attrs.filter(a => a.readsItem)
|
|
221
221
|
const itemTexts = lazyRow.texts.filter(t => t.readsItem)
|
|
222
|
-
|
|
222
|
+
const itemConds = lazyRow.conditionals.filter(c => c.readsItem)
|
|
223
|
+
if (itemAttrs.length === 0 && itemTexts.length === 0 && itemConds.length === 0) {
|
|
223
224
|
lines.push(`${b1}applyItem: () => {},`)
|
|
224
225
|
} else {
|
|
225
226
|
lines.push(`${b1}applyItem: (__e) => {`)
|
|
226
227
|
lines.push(`${b2}const ${paramHead} = () => __e.item`)
|
|
227
|
-
lines.push(`${b2}const __r = __e.refs ?? (__e.refs =
|
|
228
|
+
lines.push(`${b2}const __r = __e.refs ?? (__e.refs = [])`)
|
|
228
229
|
lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`)
|
|
230
|
+
// Re-run the preamble only when a binding in THIS body reads a local it
|
|
231
|
+
// declares (#2447 follow-up). `mapArrayLazy` wraps `applyItem` in
|
|
232
|
+
// `untrack()`, so a signal read inside the preamble subscribes nothing
|
|
233
|
+
// here — the subscription is `applyOuter`'s job.
|
|
234
|
+
if (lazyRow.itemNeedsPreamble && lazyRow.preambleStatements) {
|
|
235
|
+
lines.push(`${b2}${lazyRow.preambleStatements}`)
|
|
236
|
+
}
|
|
229
237
|
for (const a of itemAttrs) emitAttrBinding(lines, b2, a, 'item')
|
|
230
238
|
if (itemTexts.length > 0) {
|
|
231
239
|
lines.push(`${b2}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
|
|
232
240
|
for (const t of itemTexts) emitTextBinding(lines, b2, t, '__d', 'item', rwDoor)
|
|
233
241
|
}
|
|
242
|
+
for (const c of itemConds) emitConditional(lines, b2, mid, c, 'item')
|
|
234
243
|
lines.push(`${b1}},`)
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
// --- applyOuter (only when some binding is outer-involving) -------------
|
|
238
247
|
const outerAttrs = lazyRow.attrs.filter(a => a.readsOuter)
|
|
239
248
|
const outerTexts = lazyRow.texts.filter(t => t.readsOuter)
|
|
240
|
-
|
|
249
|
+
const outerConds = lazyRow.conditionals.filter(c => c.readsOuter)
|
|
250
|
+
if (outerAttrs.length > 0 || outerTexts.length > 0 || outerConds.length > 0) {
|
|
241
251
|
const b3 = `${indent} `
|
|
242
252
|
lines.push(`${b1}applyOuter: (__es, __seed) => {`)
|
|
243
253
|
// Prime the outer reads so this ONE loop-level effect subscribes even
|
|
@@ -245,13 +255,20 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
245
255
|
for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`)
|
|
246
256
|
lines.push(`${b2}for (const __e of __es) {`)
|
|
247
257
|
lines.push(`${b3}const ${paramHead} = () => __e.item`)
|
|
248
|
-
lines.push(`${b3}const __r = __e.refs ?? (__e.refs =
|
|
258
|
+
lines.push(`${b3}const __r = __e.refs ?? (__e.refs = [])`)
|
|
249
259
|
lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`)
|
|
260
|
+
// Per ENTRY, not once for the batch: the preamble reads the item, so its
|
|
261
|
+
// locals differ per row. The outer signals it reads are already primed
|
|
262
|
+
// above, so this effect subscribes even when `__es` is empty.
|
|
263
|
+
if (lazyRow.outerNeedsPreamble && lazyRow.preambleStatements) {
|
|
264
|
+
lines.push(`${b3}${lazyRow.preambleStatements}`)
|
|
265
|
+
}
|
|
250
266
|
for (const a of outerAttrs) emitAttrBinding(lines, b3, a, 'outer')
|
|
251
267
|
if (outerTexts.length > 0) {
|
|
252
268
|
lines.push(`${b3}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
|
|
253
269
|
for (const t of outerTexts) emitTextBinding(lines, b3, t, '__d', 'outer', rwDoor)
|
|
254
270
|
}
|
|
271
|
+
for (const c of outerConds) emitConditional(lines, b3, mid, c, 'outer')
|
|
255
272
|
lines.push(`${b2}}`)
|
|
256
273
|
lines.push(`${b1}},`)
|
|
257
274
|
}
|
|
@@ -260,9 +277,10 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
|
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
/**
|
|
263
|
-
* The `entry.refs` array
|
|
264
|
-
* (in `attrSlotIds` order), then — when the row has text slots — the
|
|
265
|
-
* claimed-slot door at `writerIndex`.
|
|
280
|
+
* The `entry.refs` array `createRow` seeds: one element ref per reactive-attr
|
|
281
|
+
* slot (in `attrSlotIds` order), then — when the row has text slots — the
|
|
282
|
+
* claimed-slot door at `writerIndex`. Fresh-clone context only; an ADOPTED row
|
|
283
|
+
* starts empty and fills each slot on demand (`elementAccess` / `doorAccess`).
|
|
266
284
|
*
|
|
267
285
|
* **Which door (per LOOP, never per binding).** `lazySlots` returns a bare
|
|
268
286
|
* write function; `lazyClaimSlots` returns the `{ write, read }` pair over
|
|
@@ -312,7 +330,6 @@ function refParts(
|
|
|
312
330
|
elVar: string,
|
|
313
331
|
skeletonPaths: SkeletonSlotPaths | null,
|
|
314
332
|
planVar: string | null,
|
|
315
|
-
deferDoor = false,
|
|
316
333
|
): string[] {
|
|
317
334
|
const parts: string[] = []
|
|
318
335
|
for (const slotId of lazyRow.attrSlotIds) {
|
|
@@ -320,11 +337,31 @@ function refParts(
|
|
|
320
337
|
parts.push(path ? pathExpr(elVar, path) : `qsa(${elVar}, '[bf="${slotId}"]')`)
|
|
321
338
|
}
|
|
322
339
|
if (lazyRow.texts.length > 0) {
|
|
323
|
-
parts.push(
|
|
340
|
+
parts.push(`${doorCtor(lazyRow)}(${elVar}, ${planVar})`)
|
|
324
341
|
}
|
|
325
342
|
return parts
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
/**
|
|
346
|
+
* How an ADOPTED row's binding reaches its element ref: claim THIS slot, and
|
|
347
|
+
* only this slot, on first use.
|
|
348
|
+
*
|
|
349
|
+
* The previous shape claimed the whole row in one closure (`__lzc_<mid>`),
|
|
350
|
+
* which meant an `applyOuter` driving ONE attribute still ran a `qsa` scan for
|
|
351
|
+
* every OTHER reactive-attr slot in the row — on every row, at hydration. The
|
|
352
|
+
* `in` test (not `??`) is what makes the cache honest: `qsa` answers `null` for
|
|
353
|
+
* a slot it cannot find, and `null` is a real cached answer — the `in` test
|
|
354
|
+
* treats it as one and never scans again, where a `??`-guarded cache would
|
|
355
|
+
* re-scan that slot on every tick forever.
|
|
356
|
+
*
|
|
357
|
+
* This is the element-ref twin of `doorAccess`, which already deferred the
|
|
358
|
+
* content door for exactly the same reason.
|
|
359
|
+
*/
|
|
360
|
+
function elementAccess(a: LazyRowAttrBinding): string {
|
|
361
|
+
const slot = `__r[${a.refIndex}]`
|
|
362
|
+
return `${a.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf="${a.slotId}"]'))`
|
|
363
|
+
}
|
|
364
|
+
|
|
328
365
|
/** The door constructor for this loop — see `refParts`'s "which door" note. */
|
|
329
366
|
function doorCtor(lazyRow: LazyRowPlanData): string {
|
|
330
367
|
return lazyRow.textNeedsRead ? 'lazyClaimSlots' : 'lazySlots'
|
|
@@ -354,6 +391,63 @@ function doorAccess(
|
|
|
354
391
|
return `${slot} ?? (${slot} = ${doorCtor(lazyRow)}(__e.primaryEl, ${adoptedPlanVar}))`
|
|
355
392
|
}
|
|
356
393
|
|
|
394
|
+
/** Hoisted arm-template variable names for one conditional of one loop. */
|
|
395
|
+
function condVars(mid: string, slotId: string): { trueTpl: string; falseTpl: string } {
|
|
396
|
+
const key = `${mid}_${slotId.replace(/[^A-Za-z0-9_$]/g, '_')}`
|
|
397
|
+
return { trueTpl: `__cbt_${key}`, falseTpl: `__cbf_${key}` }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* A row conditional, in the same three modes as {@link emitAttrBinding}.
|
|
402
|
+
*
|
|
403
|
+
* The whole swap is `replaceWith` on the `[bf-c]` element, because
|
|
404
|
+
* `analyzeLazyConditional` has already established that the arms own nothing —
|
|
405
|
+
* no events, no children to init, no live nodes to splice. What `insert()` does
|
|
406
|
+
* per row with an effect of its own, this does with a boolean and a dedup slot.
|
|
407
|
+
*
|
|
408
|
+
* **`'create'` writes no DOM.** The row `createRow` just cloned came from the
|
|
409
|
+
* per-row template, which interpolated `cond ? trueHtml : falseHtml` itself — so
|
|
410
|
+
* the correct arm is already in place and the only thing missing is the dedup
|
|
411
|
+
* record. Swapping here would replace an element with an identical one.
|
|
412
|
+
*
|
|
413
|
+
* **`'outer'` seeds by read-compare-write (§9.3(1))** against
|
|
414
|
+
* `content.firstElementChild.outerHTML` — the browser's serialization of the arm
|
|
415
|
+
* we would install, compared with the serialization of what is there. Both sides
|
|
416
|
+
* go through the DOM, so a match means the server already rendered this arm and
|
|
417
|
+
* no write happens.
|
|
418
|
+
*
|
|
419
|
+
* The ref is REASSIGNED after a swap (`__r[i] = __n`). Without that, the next
|
|
420
|
+
* apply would hold the detached node and write into nothing.
|
|
421
|
+
*/
|
|
422
|
+
function emitConditional(
|
|
423
|
+
lines: string[],
|
|
424
|
+
ind: string,
|
|
425
|
+
mid: string,
|
|
426
|
+
c: LazyRowConditionalBinding,
|
|
427
|
+
mode: 'create' | 'item' | 'outer',
|
|
428
|
+
): void {
|
|
429
|
+
const v = condVars(mid, c.slotId)
|
|
430
|
+
if (mode === 'create') {
|
|
431
|
+
lines.push(`${ind}__l[${c.ordinal}] = !!(${c.wrappedCondition})`)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
const slot = `__r[${c.refIndex}]`
|
|
435
|
+
lines.push(`${ind}{ const __c = ${c.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf-c="${c.slotId}"]'))`)
|
|
436
|
+
lines.push(`${ind}if (__c) {`)
|
|
437
|
+
lines.push(`${ind} const __x = !!(${c.wrappedCondition})`)
|
|
438
|
+
lines.push(`${ind} const __w = (__x ? ${v.trueTpl} : ${v.falseTpl}).content.firstElementChild`)
|
|
439
|
+
const guard = mode === 'item'
|
|
440
|
+
? dedupGuard(c.ordinal)
|
|
441
|
+
: `__seed ? (__c.outerHTML !== __w.outerHTML) : (${dedupGuard(c.ordinal)})`
|
|
442
|
+
lines.push(`${ind} if (${guard}) {`)
|
|
443
|
+
lines.push(`${ind} const __n = __w.cloneNode(true)`)
|
|
444
|
+
lines.push(`${ind} __c.replaceWith(__n)`)
|
|
445
|
+
lines.push(`${ind} ${slot} = __n`)
|
|
446
|
+
lines.push(`${ind} }`)
|
|
447
|
+
lines.push(`${ind} __l[${c.ordinal}] = __x`)
|
|
448
|
+
lines.push(`${ind}} }`)
|
|
449
|
+
}
|
|
450
|
+
|
|
357
451
|
/** `entry.last`-backed dedup test. `in` (not a truthiness check) so a
|
|
358
452
|
* legitimately `undefined` value still records and still writes once. */
|
|
359
453
|
function dedupGuard(ordinal: number): string {
|
|
@@ -366,7 +460,11 @@ function emitAttrBinding(
|
|
|
366
460
|
a: LazyRowAttrBinding,
|
|
367
461
|
mode: 'create' | 'item' | 'outer',
|
|
368
462
|
): void {
|
|
369
|
-
|
|
463
|
+
// `createRow` seeded every ref from a known clone path, so there it is a
|
|
464
|
+
// plain read. An ADOPTED row starts with an EMPTY `refs` array and each
|
|
465
|
+
// binding claims its OWN slot on first use — see `elementAccess`.
|
|
466
|
+
const target = mode === 'create' ? `__r[${a.refIndex}]` : elementAccess(a)
|
|
467
|
+
lines.push(`${ind}{ const __t = ${target}`)
|
|
370
468
|
lines.push(`${ind}if (__t) {`)
|
|
371
469
|
lines.push(`${ind} const __x = ${a.wrappedExpression}`)
|
|
372
470
|
const guard = mode === 'create'
|
|
@@ -124,7 +124,7 @@ export function stringifyReactiveEffects(
|
|
|
124
124
|
if (pc) {
|
|
125
125
|
// Profile mode: preserve the granular per-slot/per-attr effect shape so
|
|
126
126
|
// every binding keeps its own bfId (see module docstring).
|
|
127
|
-
emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId)
|
|
127
|
+
emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId, mapPreambleWrapped)
|
|
128
128
|
emitOuterTexts(lines, indent, elVar, outerTexts, bindingBfId, textClaimPathExprs)
|
|
129
129
|
emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPreambleWrapped)
|
|
130
130
|
} else {
|
|
@@ -154,6 +154,7 @@ function emitAttrSlotsGranular(
|
|
|
154
154
|
attrSlots: readonly ReactiveAttrSlot[],
|
|
155
155
|
elementIndexBySlot: ReadonlyMap<string, number> | undefined,
|
|
156
156
|
bindingBfId: (slotId: string) => string,
|
|
157
|
+
mapPreambleWrapped: string | undefined,
|
|
157
158
|
): void {
|
|
158
159
|
for (const slot of attrSlots) {
|
|
159
160
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
@@ -162,6 +163,14 @@ function emitAttrSlotsGranular(
|
|
|
162
163
|
lines.push(`${indent}if (${varName}) {`)
|
|
163
164
|
for (const attr of slot.attrs) {
|
|
164
165
|
lines.push(`${indent} createEffect(() => {`)
|
|
166
|
+
// Profile mode keeps one effect per attr, so an attr reading a preamble
|
|
167
|
+
// local needs the declarations inside ITS OWN effect — there is no
|
|
168
|
+
// shared row-effect scope to hoist them into here (#2447 follow-up).
|
|
169
|
+
// Re-running per attr is the price of the per-binding bfId this mode
|
|
170
|
+
// exists to produce; `bf debug profile` is off the build hot path.
|
|
171
|
+
if (attr.readsPreamble && mapPreambleWrapped) {
|
|
172
|
+
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
173
|
+
}
|
|
165
174
|
for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
|
|
166
175
|
lines.push(`${indent} ${stmt}`)
|
|
167
176
|
}
|
|
@@ -193,6 +202,11 @@ function emitPreambleRegionsEffect(
|
|
|
193
202
|
lines.push(`${indent}})`)
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
/** Does any attr in these slots read a `.map()` preamble local (#2447)? */
|
|
206
|
+
function attrsReadPreamble(attrSlots: readonly ReactiveAttrSlot[]): boolean {
|
|
207
|
+
return attrSlots.some(slot => slot.attrs.some(a => a.readsPreamble))
|
|
208
|
+
}
|
|
209
|
+
|
|
196
210
|
function attrLookupExpr(
|
|
197
211
|
slotId: string,
|
|
198
212
|
varName: string,
|
|
@@ -270,6 +284,15 @@ function emitConsolidatedRowEffect(
|
|
|
270
284
|
return
|
|
271
285
|
}
|
|
272
286
|
lines.push(`${indent}createEffect(() => {`)
|
|
287
|
+
// The preamble re-run leads the body, not trails it (#2447 follow-up).
|
|
288
|
+
// It used to sit between the attr writes and the region writes, because
|
|
289
|
+
// regions were its only readers. An attribute can read a preamble local
|
|
290
|
+
// now (`class={cls}`), and it reads it from THIS scope — so the
|
|
291
|
+
// declarations have to exist before the first write, whichever kind that
|
|
292
|
+
// is. Regions are unaffected by the move: they were already downstream.
|
|
293
|
+
if (mapPreambleWrapped && (preambleRegions.length > 0 || attrsReadPreamble(attrSlots))) {
|
|
294
|
+
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
295
|
+
}
|
|
273
296
|
for (const slot of attrSlots) {
|
|
274
297
|
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
275
298
|
lines.push(`${indent} if (${varName}) {`)
|
|
@@ -282,9 +305,6 @@ function emitConsolidatedRowEffect(
|
|
|
282
305
|
}
|
|
283
306
|
lines.push(`${indent} }`)
|
|
284
307
|
}
|
|
285
|
-
if (preambleRegions.length > 0 && mapPreambleWrapped) {
|
|
286
|
-
lines.push(`${indent} ${mapPreambleWrapped}`)
|
|
287
|
-
}
|
|
288
308
|
for (const text of outerTexts) {
|
|
289
309
|
lines.push(`${indent} ${writer}('${text.slotId}', String(${text.wrappedExpression}))`)
|
|
290
310
|
}
|
|
@@ -447,7 +447,12 @@ export function buildSignalMemoEnv(
|
|
|
447
447
|
for (const m of memos) {
|
|
448
448
|
substitutions.set(m.name, {
|
|
449
449
|
kind: 'call',
|
|
450
|
-
|
|
450
|
+
// Destructured mode (#2468): `templateComputation` (when present) has
|
|
451
|
+
// bare destructured prop refs already rewritten to `_p.X` — the
|
|
452
|
+
// spliced body lands in the module-scope template arrow, which is not
|
|
453
|
+
// a closure over init's `const value = _p.value` extraction. Mirrors
|
|
454
|
+
// the signal `templateInitialValue` handling above (#2265).
|
|
455
|
+
replacement: extractMemoBodyExpr(m.templateComputation ?? m.computation),
|
|
451
456
|
freeIdentifiers: m.computationFreeIdentifiers ?? new Set(),
|
|
452
457
|
})
|
|
453
458
|
}
|
|
@@ -170,7 +170,7 @@ export function emitRegistrationAndHydration(
|
|
|
170
170
|
// transformation runs at this layer (#1277).
|
|
171
171
|
const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
|
|
172
172
|
const templateHtml = generateCsrTemplate(
|
|
173
|
-
_ir.root, csrInlinableConstants, ctx,
|
|
173
|
+
_ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
|
|
174
174
|
)
|
|
175
175
|
if (templateHtml) {
|
|
176
176
|
defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``)
|
|
@@ -12,6 +12,7 @@ import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } fro
|
|
|
12
12
|
import type { ClientJsContext } from './types.ts'
|
|
13
13
|
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
|
+
import { derivesScopeFromSlot } from '../adapters/child-scope.ts'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Protect string literals from regex-based replacements.
|
|
@@ -547,12 +548,11 @@ function buildSpreadAttrsMergeCall(args: {
|
|
|
547
548
|
* are wrapped with `__bfSlot(EXPR, <branchSlotsVar>)` so the runtime can
|
|
548
549
|
* splice live `Node` returns into the parsed fragment instead of
|
|
549
550
|
* stringifying them via the template literal (#1213).
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
* the per-iteration `staticItemTemplate` for static loops.
|
|
551
|
+
* Component nodes omit their `slotId` from the `renderChild` call exactly
|
|
552
|
+
* when `derivesScopeFromSlot()` says so — a loop-item-root component (its
|
|
553
|
+
* own scope, identified by `data-key`) never derives from the parent slot;
|
|
554
|
+
* a component nested below the row root does (#2444). See
|
|
555
|
+
* `IRComponent.loopItemRoot` / `./adapters/child-scope.ts`.
|
|
556
556
|
*/
|
|
557
557
|
/**
|
|
558
558
|
* Build the per-item `<!--bf-loop-i:KEY-->` anchor comment for a whole-item
|
|
@@ -660,7 +660,7 @@ export function renderFlatMapClientBody(
|
|
|
660
660
|
rawLeaf: true,
|
|
661
661
|
renderLeaf: (ir) => {
|
|
662
662
|
const key = flatMapLeafKeyExpr(ir)
|
|
663
|
-
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined
|
|
663
|
+
const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined)
|
|
664
664
|
return `({ k: ${key ?? 'undefined'}, h: \`${html}\` })`
|
|
665
665
|
},
|
|
666
666
|
})
|
|
@@ -691,7 +691,7 @@ export function renderFlatMapProjectionClientBody(
|
|
|
691
691
|
const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`
|
|
692
692
|
const key = inner.key ? `(${inner.key})` : 'undefined'
|
|
693
693
|
const html = inner.children
|
|
694
|
-
.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined
|
|
694
|
+
.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined))
|
|
695
695
|
.join('')
|
|
696
696
|
return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`
|
|
697
697
|
}
|
|
@@ -728,8 +728,8 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
|
728
728
|
}
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
-
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string,
|
|
732
|
-
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
731
|
+
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
|
|
732
|
+
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
|
|
733
733
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
734
734
|
const wrapInterpolation = (expr: string): string => branchSlotsVar
|
|
735
735
|
? `__bfSlot(${expr}, ${branchSlotsVar})`
|
|
@@ -781,7 +781,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
const attrs = attrParts.join(' ')
|
|
784
|
-
const childrenRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
784
|
+
const childrenRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, false)
|
|
785
785
|
const children = dangerouslyHtmlChildren(node.attrs, v => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join('')
|
|
786
786
|
|
|
787
787
|
// Non-void elements must use open+close tags (HTML parsers ignore self-closing on div, span, etc.)
|
|
@@ -853,13 +853,17 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
853
853
|
// instead of emitting empty placeholders (#435).
|
|
854
854
|
const propsEntries = node.props
|
|
855
855
|
.filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
|
|
856
|
-
|
|
856
|
+
// `ref` joins the `on*` handlers here: a function prop cannot run
|
|
857
|
+
// during module-scope string rendering, and a ref closing over an
|
|
858
|
+
// init-scope setter would leak it into the template (#2468). The
|
|
859
|
+
// init path re-applies refs once real DOM exists.
|
|
860
|
+
.filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
|
|
857
861
|
.map(p => {
|
|
858
862
|
// `/* @client */` defers the prop to hydrate via initChild.
|
|
859
863
|
if (p.clientOnly) return null
|
|
860
864
|
switch (p.value.kind) {
|
|
861
865
|
case 'jsx-children': {
|
|
862
|
-
const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar,
|
|
866
|
+
const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, true)
|
|
863
867
|
const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
|
|
864
868
|
return `${quotePropName(p.name)}: \`${childHtml}\``
|
|
865
869
|
}
|
|
@@ -872,7 +876,15 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
872
876
|
case 'expression':
|
|
873
877
|
case 'template':
|
|
874
878
|
case 'spread': {
|
|
875
|
-
|
|
879
|
+
// Template-variant form (`useTemplate`): this props object
|
|
880
|
+
// lands inside the module-scope registration template, which
|
|
881
|
+
// is not a closure over init's destructured-prop extractions
|
|
882
|
+
// — the raw form leaked bare refs like `${className}` into
|
|
883
|
+
// `renderChild(...)` (#2468). Mirrors the sibling component
|
|
884
|
+
// emit sites (`irToComponentTemplateWithOpts`,
|
|
885
|
+
// `generateCsrTemplateWithOpts`), which already pick the
|
|
886
|
+
// prop-rewritten variant.
|
|
887
|
+
const expr = attrValueToString(p.value, { useTemplate: true }) ?? 'undefined'
|
|
876
888
|
return `${quotePropName(p.name)}: ${expr}`
|
|
877
889
|
}
|
|
878
890
|
}
|
|
@@ -884,11 +896,12 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
884
896
|
const keyProp = node.props.find(p => p.name === 'key')
|
|
885
897
|
const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? 'undefined'}` : ''
|
|
886
898
|
// Pass slotId as suffix so $c() can find the child component by slot
|
|
887
|
-
// after branch swap.
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
|
|
899
|
+
// after branch swap. A loop-item-root component owns a separate scope
|
|
900
|
+
// per iteration (identified by `data-key`), so the parent-slot suffix
|
|
901
|
+
// is dropped to avoid anchoring it to a wrong component-wrapper scope
|
|
902
|
+
// (#1268) — a component nested BELOW the row root still derives from
|
|
903
|
+
// its slot, matching the Hono reference (#2444).
|
|
904
|
+
const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
|
|
892
905
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
|
|
893
906
|
}
|
|
894
907
|
|
|
@@ -897,13 +910,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
897
910
|
// Increment loopDepth so inner key attrs become data-key-N
|
|
898
911
|
// Forward loopParams so expressions referencing outer/inner loop params
|
|
899
912
|
// get wrapped as signal accessors (e.g., task.title → task().title).
|
|
900
|
-
|
|
901
|
-
// inner-loop templates depend on the legacy slot-suffix-keeping shape
|
|
902
|
-
// to find scopes via `findSsrScopeBySlotIn`. The opt-in at the entry
|
|
903
|
-
// call site is the only place that wants the suffix dropped (#1268
|
|
904
|
-
// Case 1 — childComponent body materialize); propagating it through
|
|
905
|
-
// every nested loop regressed form-builder's inner-loop Select wiring.
|
|
906
|
-
const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop)
|
|
913
|
+
const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar)
|
|
907
914
|
let childTemplate = node.children.map(innerRecurse).join('')
|
|
908
915
|
// Whole-item conditional loops (#1665): prepend an always-present
|
|
909
916
|
// `<!--bf-loop-i:KEY-->` anchor before each item's (possibly empty)
|
|
@@ -938,13 +945,13 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
938
945
|
// identity is stamped by mapArray via setAttribute — emitting it
|
|
939
946
|
// here was the CSR/SSR data-key asymmetry (unescaped, client-only).
|
|
940
947
|
const body = renderPreamble(node.flatMapCallback, {
|
|
941
|
-
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
948
|
+
renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar),
|
|
942
949
|
})
|
|
943
950
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
944
951
|
} else if (node.preamble) {
|
|
945
952
|
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder
|
|
946
953
|
// preamble (the hydrate-template context uses the bare loop param).
|
|
947
|
-
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar
|
|
954
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar) })
|
|
948
955
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
949
956
|
} else {
|
|
950
957
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
@@ -1563,7 +1570,6 @@ export interface TemplateOptions {
|
|
|
1563
1570
|
* `compute-inlinability` (#1277).
|
|
1564
1571
|
*/
|
|
1565
1572
|
csrEnv?: CsrEnv
|
|
1566
|
-
insideLoop?: boolean
|
|
1567
1573
|
loopDepth?: number
|
|
1568
1574
|
/**
|
|
1569
1575
|
* Names bound by the ENCLOSING loops' callback params (item / index /
|
|
@@ -1799,7 +1805,11 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1799
1805
|
// Use renderChild() to render child component's template at runtime (#435)
|
|
1800
1806
|
const propsEntries = node.props
|
|
1801
1807
|
.filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
|
|
1802
|
-
|
|
1808
|
+
// `ref` joins the `on*` handlers here: a function prop cannot run
|
|
1809
|
+
// during module-scope string rendering, and a ref closing over an
|
|
1810
|
+
// init-scope setter would leak it into the template (#2468). The
|
|
1811
|
+
// init path re-applies refs once real DOM exists.
|
|
1812
|
+
.filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
|
|
1803
1813
|
.map(p => {
|
|
1804
1814
|
// `/* @client */` defers the prop to hydrate via initChild.
|
|
1805
1815
|
if (p.clientOnly) return null
|
|
@@ -1990,7 +2000,6 @@ export function generateCsrTemplate(
|
|
|
1990
2000
|
node: IRNode,
|
|
1991
2001
|
inlinableConstants: Map<string, string> | undefined,
|
|
1992
2002
|
ctx: ClientJsContext,
|
|
1993
|
-
insideLoop?: boolean,
|
|
1994
2003
|
restSpreadNames?: Set<string>,
|
|
1995
2004
|
propsObjectName?: string | null,
|
|
1996
2005
|
unsafeLocalNames?: Set<string>,
|
|
@@ -2010,7 +2019,7 @@ export function generateCsrTemplate(
|
|
|
2010
2019
|
}
|
|
2011
2020
|
}
|
|
2012
2021
|
const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
|
|
2013
|
-
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv,
|
|
2022
|
+
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 })
|
|
2014
2023
|
}
|
|
2015
2024
|
|
|
2016
2025
|
/**
|
|
@@ -2171,6 +2180,7 @@ export function computeDeferredChildSlots(
|
|
|
2171
2180
|
// filter set MUST mirror the `propsEntries` filter in the CSR
|
|
2172
2181
|
// `component` emit below so the deferral decision matches output.
|
|
2173
2182
|
if (p.name === '...' || p.name.startsWith('...') || p.name === 'key') return false
|
|
2183
|
+
if (p.name === 'ref') return false
|
|
2174
2184
|
if (p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()) return false
|
|
2175
2185
|
if (p.clientOnly) return false
|
|
2176
2186
|
return propResolvesUnsafe(p, env, unsafeLocalNames)
|
|
@@ -2204,7 +2214,7 @@ export function computeDeferredChildSlots(
|
|
|
2204
2214
|
}
|
|
2205
2215
|
|
|
2206
2216
|
function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
|
|
2207
|
-
const { restSpreadNames, propsObjectName, csrEnv,
|
|
2217
|
+
const { restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
|
|
2208
2218
|
const env: CsrEnv = csrEnv ?? { substitutions: new Map(), propsObjectName: propsObjectName ?? null }
|
|
2209
2219
|
const transformExpr = (expr: string, templateExpr?: string): string => {
|
|
2210
2220
|
// Single AST substitution pass: replaces signal getter calls
|
|
@@ -2241,12 +2251,10 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2241
2251
|
}
|
|
2242
2252
|
|
|
2243
2253
|
// `recurse` preserves `inHoistedChildren` for structural pass-through
|
|
2244
|
-
// (fragment / conditional); `childrenRecurse`
|
|
2245
|
-
//
|
|
2246
|
-
// scope. (#1320)
|
|
2254
|
+
// (fragment / conditional); `childrenRecurse` clears it — a new element
|
|
2255
|
+
// root starts a fresh scope. (#1320)
|
|
2247
2256
|
const recurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, opts)
|
|
2248
2257
|
const childrenRecurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, inHoistedChildren: false })
|
|
2249
|
-
const recurseInLoop = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, insideLoop: true, loopDepth: loopDepth + 1, inHoistedChildren: false })
|
|
2250
2258
|
|
|
2251
2259
|
switch (node.type) {
|
|
2252
2260
|
case 'element': {
|
|
@@ -2399,7 +2407,11 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2399
2407
|
|
|
2400
2408
|
const propsEntries = node.props
|
|
2401
2409
|
.filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
|
|
2402
|
-
|
|
2410
|
+
// `ref` joins the `on*` handlers here: a function prop cannot run
|
|
2411
|
+
// during module-scope string rendering, and a ref closing over an
|
|
2412
|
+
// init-scope setter would leak it into the template (#2468). The
|
|
2413
|
+
// init path re-applies refs once real DOM exists.
|
|
2414
|
+
.filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
|
|
2403
2415
|
.map(p => {
|
|
2404
2416
|
// `/* @client */` defers the prop to hydrate. Drop from the
|
|
2405
2417
|
// SSR `renderChild` props — `initChild`'s `propsExpr` getter
|
|
@@ -2441,7 +2453,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2441
2453
|
const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(', ')}}` : '{}'
|
|
2442
2454
|
const keyProp = node.props.find(p => p.name === 'key')
|
|
2443
2455
|
const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : ''
|
|
2444
|
-
const slotArg = (
|
|
2456
|
+
const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
|
|
2445
2457
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
|
|
2446
2458
|
}
|
|
2447
2459
|
|
|
@@ -2469,7 +2481,6 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2469
2481
|
}
|
|
2470
2482
|
const recurseInLoopBody = (n: IRNode): string => generateCsrTemplateWithOpts(n, {
|
|
2471
2483
|
...opts,
|
|
2472
|
-
insideLoop: true,
|
|
2473
2484
|
loopDepth: loopDepth + 1,
|
|
2474
2485
|
inHoistedChildren: false,
|
|
2475
2486
|
loopBoundNames: boundHere,
|
|
@@ -267,7 +267,7 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
|
|
|
267
267
|
if (!templateHtml) {
|
|
268
268
|
const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
|
|
269
269
|
templateHtml = generateCsrTemplate(
|
|
270
|
-
ir.root, csrInlinableConstants, ctx,
|
|
270
|
+
ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames
|
|
271
271
|
)
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -95,6 +95,7 @@ function buildSignalPlan(
|
|
|
95
95
|
kind: 'signal',
|
|
96
96
|
getter: signal.getter,
|
|
97
97
|
setter: signal.setter,
|
|
98
|
+
getterElided: signal.getterElided,
|
|
98
99
|
initialValueExpr: '',
|
|
99
100
|
controlledEffect: null,
|
|
100
101
|
initializerOverride: `${factory}()`,
|
|
@@ -109,6 +110,7 @@ function buildSignalPlan(
|
|
|
109
110
|
kind: 'signal',
|
|
110
111
|
getter: signal.getter,
|
|
111
112
|
setter: signal.setter,
|
|
113
|
+
getterElided: signal.getterElided,
|
|
112
114
|
initialValueExpr: resolveSignalInitialValue(signal, ctx, lookups),
|
|
113
115
|
controlledEffect,
|
|
114
116
|
branchCondition: signal.branchCondition,
|