@barefootjs/jsx 0.26.4 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +941 -130
- package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +2 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +203 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
- package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +17 -20
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts +8 -3
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +24 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +9 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +1 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +542 -689
- package/src/__tests__/child-components-in-map.test.ts +4 -4
- package/src/__tests__/client-js-generation.test.ts +23 -12
- package/src/__tests__/composite-branch-loop.test.ts +14 -5
- package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
- package/src/__tests__/create-selector.test.ts +21 -7
- package/src/__tests__/cross-file-client-signal.test.ts +5 -1
- package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
- package/src/__tests__/inline-jsx-callback.test.ts +6 -4
- package/src/__tests__/ir-jsx-props.test.ts +6 -5
- package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
- package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
- package/src/__tests__/loop-fallback-wrap.test.ts +34 -16
- package/src/__tests__/loop-hoisted-template.test.ts +10 -6
- package/src/__tests__/module-level-signal.audit.test.ts +7 -2
- package/src/__tests__/nested-loop-conditional.test.ts +9 -7
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
- package/src/__tests__/preamble-region-patch.test.ts +18 -12
- package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
- package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
- package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
- package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
- package/src/__tests__/template-closure.test.ts +1 -1
- package/src/compiler.ts +11 -0
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/client-only-elision.ts +273 -0
- package/src/ir-to-client-js/collect-elements.ts +56 -17
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
- package/src/ir-to-client-js/control-flow/shared.ts +1 -1
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
- package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +41 -17
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +71 -46
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
- package/src/ir-to-client-js/control-flow.ts +8 -3
- package/src/ir-to-client-js/element-refs.ts +9 -11
- package/src/ir-to-client-js/emit-reactive.ts +50 -14
- package/src/ir-to-client-js/html-template.ts +29 -9
- package/src/ir-to-client-js/imports.ts +13 -4
- package/src/ir-to-client-js/reactivity.ts +3 -2
- package/src/ir-to-client-js/types.ts +10 -2
- package/src/ir-to-client-js/utils.ts +1 -1
- package/src/jsx-to-ir.ts +26 -8
- package/src/types.ts +25 -2
|
@@ -9,6 +9,7 @@ import type { AttrMeta, IRMetadata } from '../types.ts'
|
|
|
9
9
|
import { isBooleanAttr } from '../html-constants.ts'
|
|
10
10
|
import type { ClientJsContext } from './types.ts'
|
|
11
11
|
import { toHtmlAttrName, varSlotId, PROPS_PARAM } from './utils.ts'
|
|
12
|
+
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './control-flow/stringify/claim-plan.ts'
|
|
12
13
|
import { createTemplateAwareStringProtector } from './html-template.ts'
|
|
13
14
|
import { datePlugin, DATE_METHODS } from '../date-lowering.ts'
|
|
14
15
|
import { toLocaleDatePlugin, foldedArgToClientJs } from '../to-locale-date-lowering.ts'
|
|
@@ -303,24 +304,44 @@ export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): v
|
|
|
303
304
|
const normalElems = elems.filter(e => !e.insideConditional)
|
|
304
305
|
|
|
305
306
|
if (normalElems.length > 0 || conditionalElems.length > 0) {
|
|
306
|
-
// Persistent slot trackers for non-conditional elements. `__bfText`
|
|
307
|
-
// returns the node now occupying the slot; a JSX-valued expression
|
|
308
|
-
// (`{themeLogo(id)}`) replaces the text node with a live element, so
|
|
309
|
-
// the next reactive run must operate on that element, not the stale
|
|
310
|
-
// text node (#1663). Primitive values keep the same text node.
|
|
311
|
-
for (const elem of normalElems) {
|
|
312
|
-
const v = varSlotId(elem.slotId)
|
|
313
|
-
lines.push(` let __anchor_${v} = _${v}`)
|
|
314
|
-
}
|
|
315
307
|
const __textSlot = (normalElems[0] ?? conditionalElems[0])?.slotId
|
|
308
|
+
|
|
309
|
+
// Non-conditional elements: ONE claimed 'markup' writer covering every
|
|
310
|
+
// slot this expression feeds (slot unification A3, row-level claim at
|
|
311
|
+
// `__scope` — spec/slot-unification.md §3(a)/§5-A3). 'markup' (not
|
|
312
|
+
// 'text') because the value may be a live Node (e.g. a JSX-returning
|
|
313
|
+
// call like `{themeLogo(id)}`) — the writer splices a Node in by
|
|
314
|
+
// identity, exactly the contract `__bfText` used to provide (#1663).
|
|
315
|
+
let writer = ''
|
|
316
|
+
if (normalElems.length > 0) {
|
|
317
|
+
const slots: ClaimSlotSpec[] = normalElems.map(elem => ({ id: elem.slotId, kind: 'markup', path: [] }))
|
|
318
|
+
writer = claimWriterVarName(slots, varSlotId)
|
|
319
|
+
lines.push(` const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`)
|
|
320
|
+
}
|
|
321
|
+
|
|
316
322
|
lines.push(` createEffect(() => {`)
|
|
317
323
|
if (normalElems.length > 0) {
|
|
318
324
|
// Expression is always evaluated for non-conditional elements
|
|
319
325
|
lines.push(` const __val = ${expr}`)
|
|
326
|
+
// `escapeTextOrNode`: `writeMarkup` inserts a string via
|
|
327
|
+
// `innerHTML =`, which — unlike the plain `Text.nodeValue =`
|
|
328
|
+
// assignment the initial SSR/CSR TEMPLATE's `escapeText(${expr})`
|
|
329
|
+
// was standing in for — DOES interpret HTML, so a raw un-escaped
|
|
330
|
+
// string here is an injection/byte-parity gap; a live Node (this
|
|
331
|
+
// expression may return one) must pass through untouched instead.
|
|
320
332
|
for (const elem of normalElems) {
|
|
321
|
-
|
|
322
|
-
lines.push(` __anchor_${v} = __bfText(__anchor_${v}, __val)`)
|
|
333
|
+
lines.push(` ${writer}('${elem.slotId}', escapeTextOrNode(__val))`)
|
|
323
334
|
}
|
|
335
|
+
// Conditional elements: deferred from slot unification A3 — see
|
|
336
|
+
// `__bfText`'s docstring for why. `insert()` may swap the branch's
|
|
337
|
+
// DOM independently of this effect's own reruns, and a cached claim
|
|
338
|
+
// door's dedup state would go stale across that swap (unlike the
|
|
339
|
+
// 'text'-kind conditional cases elsewhere in the compiler, this
|
|
340
|
+
// slot's value may be a live Node, so it can't reuse the "just
|
|
341
|
+
// re-claim fresh every run" trick those use — a fresh 'markup'
|
|
342
|
+
// claim's dedup state always starts empty, so re-claiming per-run
|
|
343
|
+
// would throw away the unchanged-value skip on every single run).
|
|
344
|
+
// Keeps `$t` + `__bfText`.
|
|
324
345
|
for (const elem of conditionalElems) {
|
|
325
346
|
const v = varSlotId(elem.slotId)
|
|
326
347
|
lines.push(` const [__el_${v}] = $t(__scope, '${elem.slotId}')`)
|
|
@@ -348,13 +369,28 @@ export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): v
|
|
|
348
369
|
}
|
|
349
370
|
}
|
|
350
371
|
|
|
351
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Emit createEffect blocks for `@client` expressions. Slot unification A3:
|
|
374
|
+
* these are ordinary claimed 'text' slots now (never Node-valued — `@client`
|
|
375
|
+
* only ever evaluates a plain expression), one writer per slot at `__scope`,
|
|
376
|
+
* replacing `updateClientMarker`'s full-tree rescan + zero-width-space
|
|
377
|
+
* managed-node marker (no longer needed: a claimed Text node's identity is
|
|
378
|
+
* already known, so there's nothing to disambiguate from other content).
|
|
379
|
+
*/
|
|
352
380
|
export function emitClientOnlyExpressions(lines: string[], ctx: ClientJsContext): void {
|
|
353
381
|
for (const elem of ctx.clientOnlyElements) {
|
|
382
|
+
// Slot unification Step B: `elem.elidedPath`, when present, was proven
|
|
383
|
+
// safe by `client-only-elision.ts` before this pass ran — use the real
|
|
384
|
+
// position-based path and skip the marker-scan fallback entirely.
|
|
385
|
+
const slots: ClaimSlotSpec[] = elem.elidedPath
|
|
386
|
+
? [{ id: elem.slotId, kind: 'text', path: elem.elidedPath, markerless: true }]
|
|
387
|
+
: [{ id: elem.slotId, kind: 'text', path: [] }]
|
|
388
|
+
const writer = claimWriterVarName(slots, varSlotId)
|
|
354
389
|
lines.push(` // @client: ${elem.slotId}`)
|
|
390
|
+
lines.push(` { const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`)
|
|
355
391
|
lines.push(` createEffect(() => {`)
|
|
356
|
-
lines.push(`
|
|
357
|
-
lines.push(` }${bindingIdArg(ctx, elem.slotId)})`)
|
|
392
|
+
lines.push(` ${writer}('${elem.slotId}', ${elem.expression})`)
|
|
393
|
+
lines.push(` }${bindingIdArg(ctx, elem.slotId)}) }`)
|
|
358
394
|
lines.push('')
|
|
359
395
|
}
|
|
360
396
|
}
|
|
@@ -126,7 +126,11 @@ export function createTemplateAwareStringProtector(): {
|
|
|
126
126
|
return { protect, restore, replaceProtectedCall }
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// Exported (not just module-local) so `client-only-elision.ts` (slot
|
|
130
|
+
// unification Step B) can reuse the exact same void-element list when it
|
|
131
|
+
// walks the IR tree computing markerless-eligible child-index paths —
|
|
132
|
+
// duplicating this list would risk the two walks silently drifting apart.
|
|
133
|
+
export const VOID_ELEMENTS = new Set([
|
|
130
134
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
|
|
131
135
|
'input', 'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
132
136
|
])
|
|
@@ -794,6 +798,16 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
794
798
|
|
|
795
799
|
case 'expression': {
|
|
796
800
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
801
|
+
// Slot unification Step B (`markerless`, decided once by
|
|
802
|
+
// `client-only-elision.ts` before this CSR pass runs): drop the
|
|
803
|
+
// marker pair and fall through to the same bare `${...}` shape the
|
|
804
|
+
// no-slotId branch below already uses — `elidedPath` (not this
|
|
805
|
+
// string) is what the claim plan resolves against, so no marker
|
|
806
|
+
// anchor is needed here at all.
|
|
807
|
+
if (node.markerless) {
|
|
808
|
+
const bare = wrapInterpolation(wrapExpr(node.expr))
|
|
809
|
+
return `\${${bare}}`
|
|
810
|
+
}
|
|
797
811
|
const inner = wrapInterpolation(wrapExpr(node.expr))
|
|
798
812
|
// Stage 3 / D4 — an element-array child ({out}) built by an arbitrary
|
|
799
813
|
// .map() preamble is an array of HTML strings; join it rather than let
|
|
@@ -908,8 +922,8 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
908
922
|
// (Go's `bf_sort`, etc.) applied the sort separately and
|
|
909
923
|
// hydration only needed to match, but broken on Hono / CSR
|
|
910
924
|
// where the template is the only source of truth. The chain
|
|
911
|
-
// mirrors `buildChainedArrayExpr` so
|
|
912
|
-
// same array shape this template emits.
|
|
925
|
+
// mirrors `buildChainedArrayExpr` so mapArray/mapArrayAnchored sees
|
|
926
|
+
// the same array shape this template emits.
|
|
913
927
|
const rawChainedArray = applyLoopChain(node)
|
|
914
928
|
const { array: iterArray, callbackParam } = applyIterationShape(node, rawChainedArray, indexParam)
|
|
915
929
|
const wrappedArray = wrapExpr(iterArray)
|
|
@@ -1120,7 +1134,9 @@ export interface SkeletonSlotPaths {
|
|
|
1120
1134
|
* the loop keeps its hoisted-clone fast path, but every slot lookup falls
|
|
1121
1135
|
* back to `qsa`/`$t`.
|
|
1122
1136
|
*/
|
|
1123
|
-
|
|
1137
|
+
// Exported for reuse by `client-only-elision.ts` (Step B) — same hazard
|
|
1138
|
+
// class, same reasoning, must never drift out of sync with a second copy.
|
|
1139
|
+
export const SKELETON_PATH_HAZARD_TAGS = new Set([
|
|
1124
1140
|
'table', 'thead', 'tbody', 'tfoot', 'caption', 'colgroup', 'col',
|
|
1125
1141
|
'select', 'optgroup',
|
|
1126
1142
|
'p',
|
|
@@ -1136,7 +1152,7 @@ const SKELETON_PATH_HAZARD_TAGS = new Set([
|
|
|
1136
1152
|
* `<dt>` closes the `<dt>`). A skeleton hitting this bails on path
|
|
1137
1153
|
* computation entirely (see `SKELETON_PATH_HAZARD_TAGS` doc).
|
|
1138
1154
|
*/
|
|
1139
|
-
const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
|
|
1155
|
+
export const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
|
|
1140
1156
|
new Set(['a']),
|
|
1141
1157
|
new Set(['button']),
|
|
1142
1158
|
new Set(['form']),
|
|
@@ -1146,7 +1162,7 @@ const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
|
|
|
1146
1162
|
new Set(['li']),
|
|
1147
1163
|
]
|
|
1148
1164
|
|
|
1149
|
-
function skeletonForceCloseGroup(tag: string): number {
|
|
1165
|
+
export function skeletonForceCloseGroup(tag: string): number {
|
|
1150
1166
|
return SKELETON_PATH_FORCE_CLOSE_GROUPS.findIndex(group => group.has(tag))
|
|
1151
1167
|
}
|
|
1152
1168
|
|
|
@@ -1204,7 +1220,7 @@ function walkSkeletonPathNode(
|
|
|
1204
1220
|
}
|
|
1205
1221
|
|
|
1206
1222
|
/** True if `children` (already flattened) contains anything the HTML parser would foster-parent out of a `<tr>`. */
|
|
1207
|
-
function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
|
|
1223
|
+
export function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
|
|
1208
1224
|
for (const child of children) {
|
|
1209
1225
|
if (child.type === 'text') {
|
|
1210
1226
|
if (child.value.trim() !== '') return true
|
|
@@ -1218,7 +1234,7 @@ function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
|
|
|
1218
1234
|
}
|
|
1219
1235
|
|
|
1220
1236
|
/** Splice fragment children inline — a fragment contributes no DOM node of its own. */
|
|
1221
|
-
function flattenSkeletonChildren(children: readonly IRNode[]): IRNode[] {
|
|
1237
|
+
export function flattenSkeletonChildren(children: readonly IRNode[]): IRNode[] {
|
|
1222
1238
|
const out: IRNode[] = []
|
|
1223
1239
|
for (const child of children) {
|
|
1224
1240
|
if (child.type === 'fragment') {
|
|
@@ -2322,7 +2338,11 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2322
2338
|
case 'expression':
|
|
2323
2339
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
2324
2340
|
if (node.clientOnly && node.slotId) {
|
|
2325
|
-
|
|
2341
|
+
// Ordinary claimed 'text' slot pair (slot unification A3) — matches
|
|
2342
|
+
// the SSR adapters' `renderExpression` byte-for-byte (byte parity):
|
|
2343
|
+
// empty at CSR mount too, since the expression is evaluated only
|
|
2344
|
+
// once init's createEffect runs, same as the SSR case.
|
|
2345
|
+
return `<!--bf:${node.slotId}--><!--/-->`
|
|
2326
2346
|
}
|
|
2327
2347
|
{
|
|
2328
2348
|
const transformed = transformExpr(node.expr, node.templateExpr)
|
|
@@ -8,13 +8,22 @@ import { isClientBuiltinName } from '../builtins.ts'
|
|
|
8
8
|
// All exports from @barefootjs/client/runtime that may be used in generated code
|
|
9
9
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
10
10
|
'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
|
|
11
|
-
'hydrate', 'insert', '
|
|
12
|
-
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
|
|
11
|
+
'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect',
|
|
12
|
+
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
|
|
13
13
|
'createPortal',
|
|
14
14
|
'provideContext', 'createContext', 'useContext',
|
|
15
|
-
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText',
|
|
15
|
+
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
|
|
16
16
|
'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText',
|
|
17
|
-
|
|
17
|
+
// Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
|
|
18
|
+
// — the "one claim mechanism" that replaced `patchSlotRange` and
|
|
19
|
+
// `updateClientMarker` (both deleted) as the content-slot update door.
|
|
20
|
+
// `lazyClaimSlots` is the read-capable twin of `lazySlots` over the same
|
|
21
|
+
// claim — emitted only by lazy loops that seed an outer-involving TEXT
|
|
22
|
+
// binding by read-compare-write (§9.3(1)).
|
|
23
|
+
// `textOrNode` is the 'text' door's Node guard: a child-position value that
|
|
24
|
+
// turns out to be a live Node must reach the writer as a Node so the claim
|
|
25
|
+
// can promote to 'markup', never as `String(node)`.
|
|
26
|
+
'claimSlots', 'lazySlots', 'lazyClaimSlots', 'textOrNode',
|
|
18
27
|
// Profile mode (#1690, SR3) — turn-boundary markers around event handlers.
|
|
19
28
|
'beginTurn', 'endTurn',
|
|
20
29
|
// Catalogued `Date` lowering (#2274/#2292) — the client counterpart to
|
|
@@ -571,8 +571,9 @@ export function collectLoopChildReactiveTexts(
|
|
|
571
571
|
expression: ({ node: n, scope: insideConditional }) => {
|
|
572
572
|
if (!n.slotId) return
|
|
573
573
|
// #2389 — a preamble-patched region (see `IRLoop.preambleRegions`) has
|
|
574
|
-
// its own
|
|
575
|
-
// client-JS loop plan
|
|
574
|
+
// its own claimed 'markup'-slot effect (`preambleRegions` in the
|
|
575
|
+
// client-JS loop plan; slot unification A3, `@barefootjs/client/
|
|
576
|
+
// runtime/claim-slots.ts`); it must never ALSO become a `reactiveTexts`
|
|
576
577
|
// entry, which would patch it via `.textContent` and corrupt markup
|
|
577
578
|
// (a `joinArrayChild` region's value is raw HTML, not text).
|
|
578
579
|
if (n.preambleRegion) return
|
|
@@ -255,7 +255,7 @@ export interface LoopCore {
|
|
|
255
255
|
/**
|
|
256
256
|
* Unique id for this loop's `<!--bf-loop:<id>--> ... <!--bf-/loop:<id>-->`
|
|
257
257
|
* marker pair, threaded from `IRLoop.markerId`. Each `mapArray()` /
|
|
258
|
-
* `
|
|
258
|
+
* `mapArrayAnchored()` call passes this id so sibling loops under the
|
|
259
259
|
* same parent reconcile their own range (#1087).
|
|
260
260
|
*/
|
|
261
261
|
markerId: string
|
|
@@ -609,7 +609,7 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
609
609
|
// Per-item bindings (events / reactiveAttrs / reactiveTexts / refs / conditionals)
|
|
610
610
|
// now live on `LoopCore.bindings` — see issue #1244 §B.
|
|
611
611
|
isStaticArray: boolean // True if array is a static prop (not a signal)
|
|
612
|
-
useElementReconciliation?: boolean // True:
|
|
612
|
+
useElementReconciliation?: boolean // True: mapArray/mapArrayAnchored + composite rendering (native root with child components)
|
|
613
613
|
/** Inner loop metadata for composite element reconciliation (array, param, key, container) */
|
|
614
614
|
innerLoops?: NestedLoop[]
|
|
615
615
|
/** Offset of this loop's items past its preceding container siblings (#1693). */
|
|
@@ -654,6 +654,14 @@ export interface ReactiveAttribute extends AttrMeta {
|
|
|
654
654
|
export interface ClientOnlyElement {
|
|
655
655
|
slotId: string
|
|
656
656
|
expression: string
|
|
657
|
+
/**
|
|
658
|
+
* Slot unification Step B (`spec/slot-unification.md` §3(b), §5 Step B):
|
|
659
|
+
* copied from `IRExpression.elidedPath` (`client-only-elision.ts`) when
|
|
660
|
+
* the compiler proved this slot's marker pair can be elided. Absent
|
|
661
|
+
* (`undefined`) is always safe — the claim plan falls back to `path: []`
|
|
662
|
+
* / marker-based resolution exactly as before Step B.
|
|
663
|
+
*/
|
|
664
|
+
elidedPath?: readonly number[]
|
|
657
665
|
}
|
|
658
666
|
|
|
659
667
|
|
|
@@ -180,7 +180,7 @@ export function applyObjectIterationWrap(
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Build the chained array expression for
|
|
183
|
+
* Build the chained array expression for mapArray/mapArrayAnchored. Thin
|
|
184
184
|
* adapter over `buildLoopChainExpr` that unpacks the collected
|
|
185
185
|
* `TopLevelLoop` / `BranchLoop` shape into the primitive inputs.
|
|
186
186
|
* Branch loops carry the same `filterPredicate` / `sortComparator`
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -2707,7 +2707,24 @@ function transformConditionalBranch(
|
|
|
2707
2707
|
const callsReactive = exprCallsReactiveGetters(node, ctx)
|
|
2708
2708
|
const hasCalls = exprHasFunctionCalls(node)
|
|
2709
2709
|
const reactive = isReactiveExpression(exprText, ctx, node) || isReactiveOrigin(branchOrigin)
|
|
2710
|
-
|
|
2710
|
+
// A branch whose entire value is a bare loop-item read (`row.label`) sets
|
|
2711
|
+
// neither `reactive` nor `callsReactive`: `render-item` is deliberately
|
|
2712
|
+
// excluded from `REACTIVE_BINDING_KINDS` (types.ts) because per-item
|
|
2713
|
+
// reactivity flows through the loop's own per-item signal accessor, not
|
|
2714
|
+
// this origin-based classification, and there is no call to trip
|
|
2715
|
+
// `callsReactive`/`hasCalls` either. Without a slotId here, a keyed loop
|
|
2716
|
+
// row that changes value without its condition flipping has nothing for
|
|
2717
|
+
// `collectLoopChildReactiveTexts` (ir-to-client-js/reactivity.ts) to
|
|
2718
|
+
// attach an update effect to, and the branch is frozen at its
|
|
2719
|
+
// mount-time value forever (the loop-branch-stale-text defect). Read the
|
|
2720
|
+
// freeRefs this function already computed above — no new parse, and no
|
|
2721
|
+
// regex re-scan of `exprText` (contrast the legacy `referencesLoopParam`
|
|
2722
|
+
// used by the sibling `transformConditional`/`transformLogicalAnd`
|
|
2723
|
+
// condition-side decisions, which token-matches the loop param name
|
|
2724
|
+
// against expression TEXT and can false-match inside an unrelated string
|
|
2725
|
+
// literal branch like `"this row is empty"`).
|
|
2726
|
+
const refsLoopParam = branchOrigin.freeRefs?.some(r => r.kind === 'render-item') ?? false
|
|
2727
|
+
const needsSlot = reactive || callsReactive || refsLoopParam
|
|
2711
2728
|
const slotId = needsSlot ? generateSlotId(ctx) : null
|
|
2712
2729
|
return {
|
|
2713
2730
|
type: 'expression',
|
|
@@ -4610,8 +4627,8 @@ function transformMapCall(
|
|
|
4610
4627
|
const bodyIsMultiRoot = loopBodyIsMultiRoot(children)
|
|
4611
4628
|
|
|
4612
4629
|
// Determine if array is static (prop) or dynamic (signal/memo).
|
|
4613
|
-
// Static arrays don't need
|
|
4614
|
-
// directly. Signal / memo arrays need
|
|
4630
|
+
// Static arrays don't need mapArray/mapArrayAnchored — SSR elements are
|
|
4631
|
+
// hydrated directly. Signal / memo arrays need mapArray for dynamic DOM
|
|
4615
4632
|
// updates.
|
|
4616
4633
|
//
|
|
4617
4634
|
// Solid-style wrap-by-default fallback (#943, follow-up to
|
|
@@ -4621,7 +4638,7 @@ function transformMapCall(
|
|
|
4621
4638
|
// where `getItems` is an imported helper previously silent-dropped
|
|
4622
4639
|
// into the static-render path, freezing the SSR-time list on the
|
|
4623
4640
|
// client. Over-reconciling an array that happens to contain a pure
|
|
4624
|
-
// call costs one extra `
|
|
4641
|
+
// call costs one extra `mapArray` per loop; under-reconciling
|
|
4625
4642
|
// is the silent-drop bug this closes.
|
|
4626
4643
|
//
|
|
4627
4644
|
// Destructured map params (`([, cfg]) => ...`, `({ a, b }) => ...`,
|
|
@@ -4660,8 +4677,9 @@ function transformMapCall(
|
|
|
4660
4677
|
|
|
4661
4678
|
// Collect nested components for both static and dynamic arrays.
|
|
4662
4679
|
// Static arrays: needed for initChild hydration.
|
|
4663
|
-
// Dynamic arrays with native root + component descendants: enables
|
|
4664
|
-
// with composite rendering (placeholder +
|
|
4680
|
+
// Dynamic arrays with native root + component descendants: enables
|
|
4681
|
+
// mapArray/mapArrayAnchored with composite rendering (placeholder +
|
|
4682
|
+
// createComponent replacement).
|
|
4665
4683
|
const nestedComponents = collectNestedComponents(children).filter(c => c.name !== childComponent?.name)
|
|
4666
4684
|
|
|
4667
4685
|
return {
|
|
@@ -6564,7 +6582,7 @@ function isArrayExprDirectPropRef(arrayExpr: ts.Expression, ctx: TransformContex
|
|
|
6564
6582
|
|
|
6565
6583
|
/**
|
|
6566
6584
|
* Check if array expression is a signal or memo getter call.
|
|
6567
|
-
* Used to determine if a loop needs
|
|
6585
|
+
* Used to determine if a loop needs mapArray for dynamic DOM updates.
|
|
6568
6586
|
* Props and local constants are considered static (don't change at runtime).
|
|
6569
6587
|
*/
|
|
6570
6588
|
function isSignalOrMemoArray(array: string, ctx: TransformContext): boolean {
|
|
@@ -6767,7 +6785,7 @@ function attrValueReactivityProbe(value: AttrValue): string | null {
|
|
|
6767
6785
|
|
|
6768
6786
|
/**
|
|
6769
6787
|
* Propagate slotId to loop children that need it.
|
|
6770
|
-
* Loops need to use their parent element's slotId for
|
|
6788
|
+
* Loops need to use their parent element's slotId for mapArray.
|
|
6771
6789
|
* This handles loops directly in children or nested in fragments.
|
|
6772
6790
|
*/
|
|
6773
6791
|
function propagateSlotIdToLoops(children: IRNode[], slotId: string): void {
|
package/src/types.ts
CHANGED
|
@@ -386,7 +386,8 @@ export interface IRExpression {
|
|
|
386
386
|
* intersect the enclosing loop's `preamble.declaredNames`. Excludes the
|
|
387
387
|
* node from `collectLoopChildReactiveTexts` (which patches via
|
|
388
388
|
* `.textContent`, wrong for markup) regardless of `reactive` / `slotId` —
|
|
389
|
-
* the region-patch effect (
|
|
389
|
+
* the region-patch effect (a claimed 'markup' slot writer, slot
|
|
390
|
+
* unification A3) is its only wiring.
|
|
390
391
|
*/
|
|
391
392
|
preambleRegion?: boolean
|
|
392
393
|
/** When true, expression calls signal getters or memos (has reactive `foo()` pattern). */
|
|
@@ -400,6 +401,28 @@ export interface IRExpression {
|
|
|
400
401
|
* source of truth for free-reference / reactivity classification.
|
|
401
402
|
*/
|
|
402
403
|
origin: OriginInfo
|
|
404
|
+
/**
|
|
405
|
+
* Slot unification Step B (`spec/slot-unification.md` §3(b), §5 Step B):
|
|
406
|
+
* true when this slot's `<!--bf:sN-->…<!--/-->` marker pair can be safely
|
|
407
|
+
* omitted from BOTH SSR and CSR output, because `elidedPath` already gives
|
|
408
|
+
* every claimer a real compile-time DOM path to the slot's position.
|
|
409
|
+
* Decided EXACTLY ONCE, by `client-only-elision.ts`, before either
|
|
410
|
+
* `adapter.generate()` (SSR) or `generateClientJs()` (CSR) run — every one
|
|
411
|
+
* of the nine SSR adapters' `renderExpression` and the CSR template
|
|
412
|
+
* emitters (`html-template.ts`) read this single flag and must never
|
|
413
|
+
* re-derive their own elision decision. Unset/false is always safe
|
|
414
|
+
* (keeps markers, today's behavior); only the compiler pass above may
|
|
415
|
+
* ever set it true.
|
|
416
|
+
*/
|
|
417
|
+
markerless?: boolean
|
|
418
|
+
/**
|
|
419
|
+
* Root-relative child-index path to this slot's position (parent index
|
|
420
|
+
* chain, LAST element = the index of the slot itself within its parent's
|
|
421
|
+
* `childNodes`). Valid, and required, only when `markerless` is true —
|
|
422
|
+
* see `SlotSpec.markerless` in `@barefootjs/client/runtime/claim-slots.ts`
|
|
423
|
+
* for the claim-time resolve-or-create semantics this path feeds.
|
|
424
|
+
*/
|
|
425
|
+
elidedPath?: readonly number[]
|
|
403
426
|
}
|
|
404
427
|
|
|
405
428
|
export interface IRConditional {
|
|
@@ -723,7 +746,7 @@ export interface IRLoop {
|
|
|
723
746
|
* `slotId` is emitted as the usual `<!--bf:sN-->...<!--/-->` marker pair
|
|
724
747
|
* (same door as a reactive text slot — `irToHtmlTemplate` / Hono's
|
|
725
748
|
* `renderExpression` key off `slotId` alone), but the CLIENT wiring is a
|
|
726
|
-
* distinct
|
|
749
|
+
* distinct claimed 'markup'-slot region-patch effect (`preambleRegions`
|
|
727
750
|
* in the client-JS loop plan), NOT a `reactiveTexts` entry — patching via
|
|
728
751
|
* `.textContent` would escape markup that a `joinArrayChild` region must
|
|
729
752
|
* render raw. `collectLoopChildReactiveTexts` excludes any node collected
|