@barefootjs/jsx 0.18.4 → 0.18.7
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/dangerous-inner-html.d.ts +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -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 +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- 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 +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -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/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
|
@@ -10,7 +10,7 @@ import { nameForRegistryRef } from './component-scope.ts'
|
|
|
10
10
|
import { assertNever } from './walker.ts'
|
|
11
11
|
import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } from './csr-substitute.ts'
|
|
12
12
|
import type { ClientJsContext } from './types.ts'
|
|
13
|
-
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE } from '@barefootjs/shared'
|
|
13
|
+
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -145,6 +145,42 @@ function applyIterationShape(
|
|
|
145
145
|
callbackParam: `(${node.param})`,
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
// `objectIteration` (#2168 object-entries-map): reconstruct the STATIC
|
|
149
|
+
// `Object.entries/keys/values(x)` call the compiler stripped at IR-build
|
|
150
|
+
// time (`isObjectIteratorCall`, `jsx-to-ir.ts`) — `arrayExpr` here is just
|
|
151
|
+
// `x` (the plain object), so the client, unlike a template adapter,
|
|
152
|
+
// re-wraps it in real JS to get the actual entries/keys/values array
|
|
153
|
+
// (this runs in a real JS engine, so no per-language lowering is needed).
|
|
154
|
+
//
|
|
155
|
+
// Unlike the array `iterationShape` case, the 'entries' ARRAY wrap does
|
|
156
|
+
// NOT require `node.index` — that field is only populated for a CLEAN
|
|
157
|
+
// 2-identifier destructure (`([word, n]) => …`); an elided/nested
|
|
158
|
+
// pattern (`([, cfg]) => …`) falls through to the generic `paramBindings`
|
|
159
|
+
// machinery instead (`node.param` stays the raw destructure TEXT, e.g.
|
|
160
|
+
// `"[, cfg]"`, which is already a syntactically valid callback param that
|
|
161
|
+
// correctly destructures a `[key, value]` pair — see the trailing
|
|
162
|
+
// fallback below). Only the ARRAY needs wrapping in that case; the
|
|
163
|
+
// callback param is unaffected either way.
|
|
164
|
+
if (node.objectIteration === 'entries') {
|
|
165
|
+
return {
|
|
166
|
+
array: `Object.entries(${arrayExpr})`,
|
|
167
|
+
callbackParam: node.index
|
|
168
|
+
? `([${node.index}, ${node.param}])`
|
|
169
|
+
: `(${node.param}${indexParam})`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (node.objectIteration === 'keys') {
|
|
173
|
+
return {
|
|
174
|
+
array: `Object.keys(${arrayExpr})`,
|
|
175
|
+
callbackParam: `(${node.param})`,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (node.objectIteration === 'values') {
|
|
179
|
+
return {
|
|
180
|
+
array: `Object.values(${arrayExpr})`,
|
|
181
|
+
callbackParam: `(${node.param})`,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
148
184
|
return { array: arrayExpr, callbackParam: `(${node.param}${indexParam})` }
|
|
149
185
|
}
|
|
150
186
|
|
|
@@ -313,7 +349,7 @@ function renderTemplateAttrPart(
|
|
|
313
349
|
case 'boolean-attr':
|
|
314
350
|
return attrName
|
|
315
351
|
case 'literal':
|
|
316
|
-
return `${attrName}="${v.value}"`
|
|
352
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
317
353
|
case 'expression': {
|
|
318
354
|
const valExpr = wrap(v.expr)
|
|
319
355
|
return templateAttrExpr(attrName, valExpr, v.presenceOrUndefined)
|
|
@@ -550,7 +586,9 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
550
586
|
}
|
|
551
587
|
|
|
552
588
|
case 'text':
|
|
553
|
-
|
|
589
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
590
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
591
|
+
return escapeHtml(node.value)
|
|
554
592
|
|
|
555
593
|
case 'expression':
|
|
556
594
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -757,7 +795,7 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
757
795
|
const v = a.value
|
|
758
796
|
switch (v.kind) {
|
|
759
797
|
case 'literal':
|
|
760
|
-
attrParts.push(`${toHtmlAttrName(a.name)}="${v.value}"`)
|
|
798
|
+
attrParts.push(`${toHtmlAttrName(a.name)}="${escapeHtml(v.value)}"`)
|
|
761
799
|
break
|
|
762
800
|
case 'boolean-attr':
|
|
763
801
|
attrParts.push(toHtmlAttrName(a.name))
|
|
@@ -796,7 +834,9 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
796
834
|
}
|
|
797
835
|
|
|
798
836
|
case 'text':
|
|
799
|
-
|
|
837
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
838
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
839
|
+
return escapeHtml(node.value)
|
|
800
840
|
|
|
801
841
|
case 'expression':
|
|
802
842
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -835,6 +875,196 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
835
875
|
}
|
|
836
876
|
}
|
|
837
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Child-node index paths for a hoisted loop skeleton (perf, #2143): computed
|
|
880
|
+
* alongside `buildLoopSkeletonTemplate` from the SAME IR tree, so the
|
|
881
|
+
* compiler can emit direct `.firstChild`/`.nextSibling` property chains
|
|
882
|
+
* (Solid-style) instead of a per-row `qsa`/`$t` runtime lookup for every
|
|
883
|
+
* dynamic slot in the hoisted single-root loop fast path.
|
|
884
|
+
*
|
|
885
|
+
* Only ever consumed for a FRESH clone of the hoisted skeleton — hydration
|
|
886
|
+
* (`__existing`, real SSR-rendered DOM) keeps using `qsa`/`$t`, since the
|
|
887
|
+
* skeleton's empty text markers and omitted dynamic attrs don't describe the
|
|
888
|
+
* SSR-rendered tree's actual shape (see `computeSkeletonSlotPaths`).
|
|
889
|
+
*/
|
|
890
|
+
export interface SkeletonSlotPaths {
|
|
891
|
+
/** slotId -> childNodes-index path from the clone root to the element carrying `bf="slotId"`. Empty array = the root itself. */
|
|
892
|
+
elementPaths: ReadonlyMap<string, readonly number[]>
|
|
893
|
+
/** slotId -> path to the first Comment of the `<!--bf:slotId--><!--/-->` marker pair. */
|
|
894
|
+
textMarkerPaths: ReadonlyMap<string, readonly number[]>
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Tags whose HTML-parser behavior can silently restructure or drop children
|
|
899
|
+
* relative to the naive IR-tree model (implied table sections, `<select>`/
|
|
900
|
+
* `<optgroup>` child-dropping, `<p>` auto-close, leading-newline drop in
|
|
901
|
+
* `<pre>`/`<textarea>`, `<template>` content relocating into `.content`). A
|
|
902
|
+
* skeleton containing any of these bails on path computation entirely —
|
|
903
|
+
* the loop keeps its hoisted-clone fast path, but every slot lookup falls
|
|
904
|
+
* back to `qsa`/`$t`.
|
|
905
|
+
*/
|
|
906
|
+
const SKELETON_PATH_HAZARD_TAGS = new Set([
|
|
907
|
+
'table', 'thead', 'tbody', 'tfoot', 'caption', 'colgroup', 'col',
|
|
908
|
+
'select', 'optgroup',
|
|
909
|
+
'p',
|
|
910
|
+
'pre', 'textarea', 'listing',
|
|
911
|
+
'template',
|
|
912
|
+
'math', // MathML foreign-content: breakout tags pop content back out, same class of hazard as SVG.
|
|
913
|
+
])
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Tag groups the HTML parser force-closes when a tag from the group is
|
|
917
|
+
* nested inside ANY other open tag from the SAME group — not just an
|
|
918
|
+
* identical tag (e.g. `<h2>` inside `<h1>` closes the `<h1>`, `<dd>` inside
|
|
919
|
+
* `<dt>` closes the `<dt>`). A skeleton hitting this bails on path
|
|
920
|
+
* computation entirely (see `SKELETON_PATH_HAZARD_TAGS` doc).
|
|
921
|
+
*/
|
|
922
|
+
const SKELETON_PATH_FORCE_CLOSE_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
|
|
923
|
+
new Set(['a']),
|
|
924
|
+
new Set(['button']),
|
|
925
|
+
new Set(['form']),
|
|
926
|
+
new Set(['option']),
|
|
927
|
+
new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
928
|
+
new Set(['dd', 'dt']),
|
|
929
|
+
new Set(['li']),
|
|
930
|
+
]
|
|
931
|
+
|
|
932
|
+
function skeletonForceCloseGroup(tag: string): number {
|
|
933
|
+
return SKELETON_PATH_FORCE_CLOSE_GROUPS.findIndex(group => group.has(tag))
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
interface SkeletonPathState {
|
|
937
|
+
elementPaths: Map<string, readonly number[]>
|
|
938
|
+
textMarkerPaths: Map<string, readonly number[]>
|
|
939
|
+
bailed: boolean
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Compute per-slot child-index paths for a loop skeleton already proven
|
|
944
|
+
* safe by `buildLoopSkeletonTemplate` — call this ONLY after that returned
|
|
945
|
+
* non-null for the same `(node, safe)` pair; it assumes the same shape
|
|
946
|
+
* guarantees (no spreads/conditionals/components/nested loops) and does not
|
|
947
|
+
* re-derive them. Returns `null` when the tree contains a parser-hazard tag
|
|
948
|
+
* (see `SKELETON_PATH_HAZARD_TAGS`) that could make the index-path model
|
|
949
|
+
* diverge from the browser's actual parsed structure.
|
|
950
|
+
*/
|
|
951
|
+
export function computeSkeletonSlotPaths(node: IRNode, safe: LoopSkeletonSafeSlots): SkeletonSlotPaths | null {
|
|
952
|
+
const state: SkeletonPathState = { elementPaths: new Map(), textMarkerPaths: new Map(), bailed: false }
|
|
953
|
+
walkSkeletonPathNode(node, [], safe, state, new Set())
|
|
954
|
+
if (state.bailed) return null
|
|
955
|
+
return { elementPaths: state.elementPaths, textMarkerPaths: state.textMarkerPaths }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function walkSkeletonPathNode(
|
|
959
|
+
node: IRNode,
|
|
960
|
+
path: readonly number[],
|
|
961
|
+
safe: LoopSkeletonSafeSlots,
|
|
962
|
+
state: SkeletonPathState,
|
|
963
|
+
forceCloseAncestors: ReadonlySet<number>,
|
|
964
|
+
): void {
|
|
965
|
+
if (state.bailed || node.type !== 'element') return
|
|
966
|
+
if (SKELETON_PATH_HAZARD_TAGS.has(node.tag)) { state.bailed = true; return }
|
|
967
|
+
const groupIdx = skeletonForceCloseGroup(node.tag)
|
|
968
|
+
if (groupIdx >= 0 && forceCloseAncestors.has(groupIdx)) { state.bailed = true; return }
|
|
969
|
+
// Void elements never legally carry children; if the IR claims otherwise
|
|
970
|
+
// the browser reparses `</tag>` as stray content instead of a close tag,
|
|
971
|
+
// diverging from the naive one-node-per-element model.
|
|
972
|
+
const flatChildren = flattenSkeletonChildren(node.children)
|
|
973
|
+
if (VOID_ELEMENTS.has(node.tag) && flatChildren.length > 0) { state.bailed = true; return }
|
|
974
|
+
// Foster parenting (#2143 review): a bare `<tr>` root is intentionally NOT
|
|
975
|
+
// in SKELETON_PATH_HAZARD_TAGS (it's the common loop-row shape), but
|
|
976
|
+
// non-`td`/`th` element children — and any non-whitespace text run —
|
|
977
|
+
// directly inside it get foster-parented OUT of the row by the HTML
|
|
978
|
+
// parser, shifting every sibling index after them. Comments (the `bf:sN`
|
|
979
|
+
// marker pairs) are unaffected — comments are inserted in place even in
|
|
980
|
+
// table-related insertion modes.
|
|
981
|
+
if (node.tag === 'tr' && hasForeignTableRowContent(flatChildren)) { state.bailed = true; return }
|
|
982
|
+
if (node.slotId) state.elementPaths.set(node.slotId, path)
|
|
983
|
+
const nextAncestors = groupIdx >= 0
|
|
984
|
+
? new Set([...forceCloseAncestors, groupIdx])
|
|
985
|
+
: forceCloseAncestors
|
|
986
|
+
walkSkeletonPathChildren(flatChildren, path, safe, state, nextAncestors)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** True if `children` (already flattened) contains anything the HTML parser would foster-parent out of a `<tr>`. */
|
|
990
|
+
function hasForeignTableRowContent(children: readonly IRNode[]): boolean {
|
|
991
|
+
for (const child of children) {
|
|
992
|
+
if (child.type === 'text') {
|
|
993
|
+
if (child.value.trim() !== '') return true
|
|
994
|
+
continue
|
|
995
|
+
}
|
|
996
|
+
if (child.type === 'element' && child.tag !== 'td' && child.tag !== 'th') {
|
|
997
|
+
return true
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return false
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Splice fragment children inline — a fragment contributes no DOM node of its own. */
|
|
1004
|
+
function flattenSkeletonChildren(children: readonly IRNode[]): IRNode[] {
|
|
1005
|
+
const out: IRNode[] = []
|
|
1006
|
+
for (const child of children) {
|
|
1007
|
+
if (child.type === 'fragment') {
|
|
1008
|
+
out.push(...flattenSkeletonChildren(child.children))
|
|
1009
|
+
} else {
|
|
1010
|
+
out.push(child)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return out
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Walk one level of (already flattened) children, tracking the DOM child
|
|
1018
|
+
* index as it will exist on a FRESH clone of the skeleton (empty text
|
|
1019
|
+
* markers, no dynamic attrs) — not the per-row interpolated template.
|
|
1020
|
+
* Mirrors the browser's text-node-merging behavior: adjacent text content
|
|
1021
|
+
* (including a dropped null/undefined expression) collapses into a single
|
|
1022
|
+
* Text node; an empty literal string contributes no node at all.
|
|
1023
|
+
*/
|
|
1024
|
+
function walkSkeletonPathChildren(
|
|
1025
|
+
children: readonly IRNode[],
|
|
1026
|
+
parentPath: readonly number[],
|
|
1027
|
+
safe: LoopSkeletonSafeSlots,
|
|
1028
|
+
state: SkeletonPathState,
|
|
1029
|
+
forceCloseAncestors: ReadonlySet<number>,
|
|
1030
|
+
): void {
|
|
1031
|
+
let idx = 0
|
|
1032
|
+
let pendingText = false
|
|
1033
|
+
for (const child of children) {
|
|
1034
|
+
if (state.bailed) return
|
|
1035
|
+
switch (child.type) {
|
|
1036
|
+
case 'text': {
|
|
1037
|
+
if (child.value === '') continue
|
|
1038
|
+
if (!pendingText) idx += 1
|
|
1039
|
+
pendingText = true
|
|
1040
|
+
continue
|
|
1041
|
+
}
|
|
1042
|
+
case 'expression': {
|
|
1043
|
+
if (child.expr === 'null' || child.expr === 'undefined') continue
|
|
1044
|
+
if (!child.slotId || !safe.reactiveTextSlotIds.has(child.slotId)) { state.bailed = true; return }
|
|
1045
|
+
state.textMarkerPaths.set(child.slotId, [...parentPath, idx])
|
|
1046
|
+
idx += 2 // the marker pair: <!--bf:sN--> then <!--/-->
|
|
1047
|
+
pendingText = false
|
|
1048
|
+
continue
|
|
1049
|
+
}
|
|
1050
|
+
case 'element': {
|
|
1051
|
+
walkSkeletonPathNode(child, [...parentPath, idx], safe, state, forceCloseAncestors)
|
|
1052
|
+
idx += 1
|
|
1053
|
+
pendingText = false
|
|
1054
|
+
continue
|
|
1055
|
+
}
|
|
1056
|
+
case 'fragment':
|
|
1057
|
+
continue // already flattened
|
|
1058
|
+
default:
|
|
1059
|
+
// conditional/component/loop/if-statement/provider/async/slot: none
|
|
1060
|
+
// of these should reach here — buildLoopSkeletonTemplate would have
|
|
1061
|
+
// already returned null for the same tree. Bail defensively.
|
|
1062
|
+
state.bailed = true
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
838
1068
|
/**
|
|
839
1069
|
* Generate an HTML template for composite element reconciliation.
|
|
840
1070
|
* Identical to irToHtmlTemplate except component nodes become placeholder
|
|
@@ -870,7 +1100,9 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
870
1100
|
}
|
|
871
1101
|
|
|
872
1102
|
case 'text':
|
|
873
|
-
|
|
1103
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
1104
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1105
|
+
return escapeHtml(node.value)
|
|
874
1106
|
|
|
875
1107
|
case 'expression':
|
|
876
1108
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1093,6 +1325,20 @@ export interface TemplateOptions {
|
|
|
1093
1325
|
csrEnv?: CsrEnv
|
|
1094
1326
|
insideLoop?: boolean
|
|
1095
1327
|
loopDepth?: number
|
|
1328
|
+
/**
|
|
1329
|
+
* Names bound by the ENCLOSING loops' callback params (item / index /
|
|
1330
|
+
* destructured binding names), accumulated by the `loop` case as the
|
|
1331
|
+
* recursion descends (#2222). Inside a loop body, `csrSubstitute`
|
|
1332
|
+
* receives each expression in isolation — no enclosing arrow text — so
|
|
1333
|
+
* its own bound-name tracking can't see the loop binding; the `loop`
|
|
1334
|
+
* case instead filters these names out of the child recursion's
|
|
1335
|
+
* `csrEnv`, so an inlinable const / signal / memo whose name is
|
|
1336
|
+
* shadowed by a loop param never substitutes at the shadowed
|
|
1337
|
+
* occurrence. Scope-accurate per nesting level (the CURRENT level's
|
|
1338
|
+
* own `transformExpr` — e.g. the loop's array-source expression —
|
|
1339
|
+
* keeps the unfiltered env).
|
|
1340
|
+
*/
|
|
1341
|
+
loopBoundNames?: ReadonlySet<string>
|
|
1096
1342
|
/** Emit `bf-s` placeholder on scoped elements inside a jsx-children prop (#1320). */
|
|
1097
1343
|
inHoistedChildren?: boolean
|
|
1098
1344
|
/**
|
|
@@ -1228,7 +1474,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1228
1474
|
return templateAttrExpr(keyName, transformExpr(tmplStr))
|
|
1229
1475
|
}
|
|
1230
1476
|
case 'literal':
|
|
1231
|
-
return `${keyName}="${v.value}"`
|
|
1477
|
+
return `${keyName}="${escapeHtml(v.value)}"`
|
|
1232
1478
|
default:
|
|
1233
1479
|
return ''
|
|
1234
1480
|
}
|
|
@@ -1238,7 +1484,7 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1238
1484
|
case 'boolean-attr':
|
|
1239
1485
|
return attrName
|
|
1240
1486
|
case 'literal':
|
|
1241
|
-
return `${attrName}="${v.value}"`
|
|
1487
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
1242
1488
|
case 'expression':
|
|
1243
1489
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
|
|
1244
1490
|
case 'template': {
|
|
@@ -1269,7 +1515,9 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1269
1515
|
}
|
|
1270
1516
|
|
|
1271
1517
|
case 'text':
|
|
1272
|
-
|
|
1518
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
1519
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1520
|
+
return escapeHtml(node.value)
|
|
1273
1521
|
|
|
1274
1522
|
case 'expression':
|
|
1275
1523
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1806,7 +2054,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1806
2054
|
case 'boolean-attr':
|
|
1807
2055
|
return attrName
|
|
1808
2056
|
case 'literal':
|
|
1809
|
-
return `${attrName}="${v.value}"`
|
|
2057
|
+
return `${attrName}="${escapeHtml(v.value)}"`
|
|
1810
2058
|
case 'expression':
|
|
1811
2059
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
|
|
1812
2060
|
case 'template': {
|
|
@@ -1837,7 +2085,9 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1837
2085
|
}
|
|
1838
2086
|
|
|
1839
2087
|
case 'text':
|
|
1840
|
-
|
|
2088
|
+
// IRText carries the entity-DECODED value; this string is parsed
|
|
2089
|
+
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
2090
|
+
return escapeHtml(node.value)
|
|
1841
2091
|
|
|
1842
2092
|
case 'expression':
|
|
1843
2093
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
@@ -1942,7 +2192,36 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1942
2192
|
}
|
|
1943
2193
|
|
|
1944
2194
|
case 'loop': {
|
|
1945
|
-
|
|
2195
|
+
// Accumulate this loop's callback-bound names and filter them out
|
|
2196
|
+
// of the child recursion's substitution env (#2222): an inlinable
|
|
2197
|
+
// const / signal / memo whose name the callback shadows must never
|
|
2198
|
+
// substitute inside the body. Destructured callbacks contribute
|
|
2199
|
+
// their individual binding names; a raw pattern-text `param` (the
|
|
2200
|
+
// BF025 fallback shapes, detected by the same leading-`[`/`{` check
|
|
2201
|
+
// `destructureLoopParam` uses — NOT an ASCII-identifier regex, which
|
|
2202
|
+
// would drop Unicode param names like `π` and re-enable the shadow
|
|
2203
|
+
// bug for that body) contributes nothing — conservatively
|
|
2204
|
+
// unfiltered rather than mis-filtered.
|
|
2205
|
+
const boundHere = new Set(opts.loopBoundNames ?? [])
|
|
2206
|
+
if (node.paramBindings && node.paramBindings.length > 0) {
|
|
2207
|
+
for (const b of node.paramBindings) boundHere.add(b.name)
|
|
2208
|
+
} else if (!node.param.startsWith('[') && !node.param.startsWith('{')) {
|
|
2209
|
+
boundHere.add(node.param)
|
|
2210
|
+
}
|
|
2211
|
+
if (node.index) boundHere.add(node.index)
|
|
2212
|
+
const childEnv: CsrEnv = {
|
|
2213
|
+
...env,
|
|
2214
|
+
substitutions: new Map([...env.substitutions].filter(([name]) => !boundHere.has(name))),
|
|
2215
|
+
}
|
|
2216
|
+
const recurseInLoopBody = (n: IRNode): string => generateCsrTemplateWithOpts(n, {
|
|
2217
|
+
...opts,
|
|
2218
|
+
insideLoop: true,
|
|
2219
|
+
loopDepth: loopDepth + 1,
|
|
2220
|
+
inHoistedChildren: false,
|
|
2221
|
+
loopBoundNames: boundHere,
|
|
2222
|
+
csrEnv: childEnv,
|
|
2223
|
+
})
|
|
2224
|
+
let childTemplate = node.children.map(recurseInLoopBody).join('')
|
|
1946
2225
|
// Whole-item conditional loops (#1665): prepend the per-item
|
|
1947
2226
|
// `<!--bf-loop-i:KEY-->` anchor so `mapArrayAnchored` can track items
|
|
1948
2227
|
// that render no element. Mirrors the `irToHtmlTemplate` loop case.
|
|
@@ -1976,7 +2255,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1976
2255
|
if (node.flatMapCallback) {
|
|
1977
2256
|
let body = node.flatMapCallback.templateBody ?? node.flatMapCallback.body
|
|
1978
2257
|
for (const frag of node.flatMapCallback.fragments) {
|
|
1979
|
-
const renderedIr =
|
|
2258
|
+
const renderedIr = recurseInLoopBody(frag.ir)
|
|
1980
2259
|
body = body.replace(frag.placeholder, `\`${renderedIr}\``)
|
|
1981
2260
|
}
|
|
1982
2261
|
body = applyPropsRewrite(body, propsObjectName ?? null)
|
|
@@ -14,6 +14,7 @@ export const RUNTIME_IMPORT_CANDIDATES = [
|
|
|
14
14
|
'provideContext', 'createContext', 'useContext',
|
|
15
15
|
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText',
|
|
16
16
|
'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText',
|
|
17
|
+
'tAfter',
|
|
17
18
|
// Profile mode (#1690, SR3) — turn-boundary markers around event handlers.
|
|
18
19
|
'beginTurn', 'endTurn',
|
|
19
20
|
] as const
|
|
@@ -124,6 +124,11 @@ function buildInnerLoopNestedPlan(
|
|
|
124
124
|
innerComps: readonly IRLoopChildComponent[],
|
|
125
125
|
): InnerLoopNestedInitPlan {
|
|
126
126
|
const outerIndexParam = elem.index || '__idx'
|
|
127
|
+
// The user's declared inner index name, falling back to the synthetic
|
|
128
|
+
// `__innerIdx` — same idiom as the outer `elem.index || '__idx'` above.
|
|
129
|
+
// Hardcoding the synthetic name left a referenced user index unbound and
|
|
130
|
+
// `initChild`'s prop getters threw `ReferenceError` at hydration (#2231).
|
|
131
|
+
const innerIndexParam = innerLoop.index || '__innerIdx'
|
|
127
132
|
const comps: InnerLoopComp[] = innerComps.map(comp => ({
|
|
128
133
|
componentName: comp.name,
|
|
129
134
|
selector: buildCompSelector(comp),
|
|
@@ -141,7 +146,8 @@ function buildInnerLoopNestedPlan(
|
|
|
141
146
|
innerContainerSlotId: innerLoop.containerSlotId ?? null,
|
|
142
147
|
innerArrayExpr: innerLoop.array,
|
|
143
148
|
innerParam: innerLoop.param,
|
|
144
|
-
|
|
149
|
+
innerIndexParam,
|
|
150
|
+
innerOffsetExpr: buildLoopChildIndexExpr(innerIndexParam, innerLoop.offset),
|
|
145
151
|
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
146
152
|
depth: innerLoop.depth,
|
|
147
153
|
comps,
|
|
@@ -179,9 +185,14 @@ function buildComponentRootedInnerLoopPlan(
|
|
|
179
185
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
180
186
|
outerArrayExpr: elem.array,
|
|
181
187
|
outerParam: elem.param,
|
|
188
|
+
// Declared index names only (#2231) — the zip shape never indexes by
|
|
189
|
+
// position, so there's no synthetic fallback and index-less loops keep
|
|
190
|
+
// byte-identical output.
|
|
191
|
+
outerIndexParam: elem.index,
|
|
182
192
|
outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
|
|
183
193
|
innerArrayExpr: innerLoop.array,
|
|
184
194
|
innerParam: innerLoop.param,
|
|
195
|
+
innerIndexParam: innerLoop.index,
|
|
185
196
|
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
186
197
|
depth: innerLoop.depth,
|
|
187
198
|
comps,
|
|
@@ -122,7 +122,16 @@ export interface InnerLoopNestedInitPlan {
|
|
|
122
122
|
innerContainerSlotId: string | null
|
|
123
123
|
innerArrayExpr: string
|
|
124
124
|
innerParam: string
|
|
125
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Inner index parameter identifier — the user's declared `.map((item, i)
|
|
127
|
+
* => ...)` index name, or the synthetic `__innerIdx` when the callback
|
|
128
|
+
* declares none (#2231). Emitted as the inner `forEach`'s second param so
|
|
129
|
+
* prop getters that close over the index resolve it (previously the
|
|
130
|
+
* synthetic name was hardcoded and a referenced user index threw
|
|
131
|
+
* `ReferenceError` from `initChild`'s first prop read).
|
|
132
|
+
*/
|
|
133
|
+
innerIndexParam: string
|
|
134
|
+
/** Inner offset — `innerIndexParam` plus any sibling-offset terms. */
|
|
126
135
|
innerOffsetExpr: string
|
|
127
136
|
/**
|
|
128
137
|
* Inner `.map()` callback preamble locals, emitted after the
|
|
@@ -161,11 +170,21 @@ export interface ComponentRootedInnerLoopInitPlan {
|
|
|
161
170
|
/** Outer loop's array expression. */
|
|
162
171
|
outerArrayExpr: string
|
|
163
172
|
outerParam: string
|
|
173
|
+
/**
|
|
174
|
+
* Outer `.map()` index param name, or `null` when the callback declares
|
|
175
|
+
* none (#2231). Unlike `inner-loop-nested`, this shape needs no synthetic
|
|
176
|
+
* fallback — the document-order zip never indexes by position — so the
|
|
177
|
+
* param is appended to the `forEach` head only when declared, keeping
|
|
178
|
+
* index-less loops byte-identical.
|
|
179
|
+
*/
|
|
180
|
+
outerIndexParam: string | null
|
|
164
181
|
/** Outer `.map()` callback preamble locals (#1064). */
|
|
165
182
|
outerPreludeStatements: PreludeStatements
|
|
166
183
|
/** Inner loop's array expression (references the outer param). */
|
|
167
184
|
innerArrayExpr: string
|
|
168
185
|
innerParam: string
|
|
186
|
+
/** Inner `.map()` index param name, or `null` — see `outerIndexParam` (#2231). */
|
|
187
|
+
innerIndexParam: string | null
|
|
169
188
|
/** Inner `.map()` callback preamble locals (#1064). */
|
|
170
189
|
innerPreludeStatements: PreludeStatements
|
|
171
190
|
/** Depth used in the leading comment line. */
|
|
@@ -434,6 +434,12 @@ export function collectLoopChildEventsWithNesting(
|
|
|
434
434
|
depth: scope.nestingStack.length + 1,
|
|
435
435
|
array: l.array,
|
|
436
436
|
param: l.param,
|
|
437
|
+
// Event-delegation metadata only (see the field comment on
|
|
438
|
+
// `NestedLoop.index`) — threaded for type-completeness (#2218)
|
|
439
|
+
// so this record stays a structurally-valid `NestedLoop`, even
|
|
440
|
+
// though delegated-handler index binding is handled separately
|
|
441
|
+
// by `indexBindingLine` in `stringify/event-delegation.ts` (#2189).
|
|
442
|
+
index: l.index,
|
|
437
443
|
key: l.key,
|
|
438
444
|
markerId: l.markerId,
|
|
439
445
|
containerSlotId: scope.lastElementSlotId,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* <i> if (!__outerEl) return
|
|
37
37
|
* <i> <outerPreludeStatements*> // raw outer preamble (#1064)
|
|
38
38
|
* <i> const __ic = <innerContainer-or-outerEl>
|
|
39
|
-
* <i> <innerArr>.forEach((<innerParam>,
|
|
39
|
+
* <i> <innerArr>.forEach((<innerParam>, <innerIdx>) => {
|
|
40
40
|
* <i> const __innerEl = __ic.children[<innerOffset>]
|
|
41
41
|
* <i> if (!__innerEl) return
|
|
42
42
|
* <i> <innerPreludeStatements*> // raw inner preamble (#1064)
|
|
@@ -137,6 +137,7 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
137
137
|
innerContainerSlotId,
|
|
138
138
|
innerArrayExpr,
|
|
139
139
|
innerParam,
|
|
140
|
+
innerIndexParam,
|
|
140
141
|
innerOffsetExpr,
|
|
141
142
|
innerPreludeStatements,
|
|
142
143
|
depth,
|
|
@@ -157,7 +158,7 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
157
158
|
} else {
|
|
158
159
|
lines.push(` const __ic = __outerEl`)
|
|
159
160
|
}
|
|
160
|
-
lines.push(` ${innerArrayExpr}.forEach((${innerParam},
|
|
161
|
+
lines.push(` ${innerArrayExpr}.forEach((${innerParam}, ${innerIndexParam}) => {`)
|
|
161
162
|
lines.push(` const __innerEl = __ic.children[${innerOffsetExpr}]`)
|
|
162
163
|
lines.push(` if (!__innerEl) return`)
|
|
163
164
|
// Inner `.map()` callback preamble — must precede the per-component
|
|
@@ -186,9 +187,9 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
|
|
|
186
187
|
* <i>if (<container>) {
|
|
187
188
|
* <i> const <scopes_c> = qsaChildScopes(<container>, <selector_c>) // per comp
|
|
188
189
|
* <i> let <cursor_c> = 0
|
|
189
|
-
* <i> <outerArr>.forEach((<outerParam>) => {
|
|
190
|
+
* <i> <outerArr>.forEach((<outerParam>[, <outerIdx>]) => {
|
|
190
191
|
* <i> <outerPreludeStatements*>
|
|
191
|
-
* <i> <innerArr>.forEach((<innerParam>) => {
|
|
192
|
+
* <i> <innerArr>.forEach((<innerParam>[, <innerIdx>]) => {
|
|
192
193
|
* <i> <innerPreludeStatements*>
|
|
193
194
|
* <i> const <compEl_c> = <scopes_c>[<cursor_c>++] // per comp
|
|
194
195
|
* <i> if (<compEl_c>) initChild('<name>', <compEl_c>, <props>)
|
|
@@ -206,9 +207,11 @@ function emitComponentRootedInnerLoop(lines: string[], plan: ComponentRootedInne
|
|
|
206
207
|
containerVar,
|
|
207
208
|
outerArrayExpr,
|
|
208
209
|
outerParam,
|
|
210
|
+
outerIndexParam,
|
|
209
211
|
outerPreludeStatements,
|
|
210
212
|
innerArrayExpr,
|
|
211
213
|
innerParam,
|
|
214
|
+
innerIndexParam,
|
|
212
215
|
innerPreludeStatements,
|
|
213
216
|
depth,
|
|
214
217
|
comps,
|
|
@@ -226,11 +229,13 @@ function emitComponentRootedInnerLoop(lines: string[], plan: ComponentRootedInne
|
|
|
226
229
|
lines.push(` const ${scopesVar(i)} = qsaChildScopes(${containerVar}, ${comp.selector})`)
|
|
227
230
|
lines.push(` let ${cursorVar(i)} = 0`)
|
|
228
231
|
})
|
|
229
|
-
|
|
232
|
+
// Declared index names are appended to the forEach heads (#2231);
|
|
233
|
+
// index-less loops keep the bare single-param head byte-identical.
|
|
234
|
+
lines.push(` ${outerArrayExpr}.forEach((${outerParam}${outerIndexParam ? `, ${outerIndexParam}` : ''}) => {`)
|
|
230
235
|
for (const stmt of outerPreludeStatements) {
|
|
231
236
|
lines.push(` ${stmt}`)
|
|
232
237
|
}
|
|
233
|
-
lines.push(` ${innerArrayExpr}.forEach((${innerParam}) => {`)
|
|
238
|
+
lines.push(` ${innerArrayExpr}.forEach((${innerParam}${innerIndexParam ? `, ${innerIndexParam}` : ''}) => {`)
|
|
234
239
|
for (const stmt of innerPreludeStatements) {
|
|
235
240
|
lines.push(` ${stmt}`)
|
|
236
241
|
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ImportInfo,
|
|
23
23
|
} from '../types.ts'
|
|
24
24
|
import type { CsrInlinabilityMap } from './csr-substitute.ts'
|
|
25
|
+
import type { SkeletonSlotPaths } from './html-template.ts'
|
|
25
26
|
|
|
26
27
|
export interface ClientJsContext {
|
|
27
28
|
componentName: string
|
|
@@ -283,6 +284,16 @@ export interface LoopCore {
|
|
|
283
284
|
* (#1448 Tier B). Threaded from `IRLoop.iterationShape`.
|
|
284
285
|
*/
|
|
285
286
|
iterationShape?: 'entries' | 'keys'
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Object iteration shape from the STATIC `Object.entries(x)` /
|
|
290
|
+
* `.keys(x)` / `.values(x)` call form (#2168 object-entries-map) —
|
|
291
|
+
* distinct from {@link iterationShape} for the same reason `IRLoop`'s
|
|
292
|
+
* own field is (see that field's docstring, `packages/jsx/src/types.ts`):
|
|
293
|
+
* `x` is a plain object, not an array. Threaded from
|
|
294
|
+
* `IRLoop.objectIteration`.
|
|
295
|
+
*/
|
|
296
|
+
objectIteration?: 'entries' | 'keys' | 'values'
|
|
286
297
|
}
|
|
287
298
|
|
|
288
299
|
/**
|
|
@@ -394,6 +405,18 @@ export type ConditionalBranchConditional = ConditionalElement
|
|
|
394
405
|
export interface NestedLoop extends LoopCore {
|
|
395
406
|
kind: 'nested'
|
|
396
407
|
depth: number // 1 for first nesting level, 2 for second, etc.
|
|
408
|
+
/**
|
|
409
|
+
* Index parameter of the inner `.map()` callback (e.g. `i` from
|
|
410
|
+
* `.map((item, i) => ...)`), or `null` when the callback doesn't declare
|
|
411
|
+
* one. Mirrors `TopLevelLoop.index` / `BranchLoop.index` — added by #2218
|
|
412
|
+
* so a nested loop's `key`/reactive-text/reactive-attr/template/event/ref
|
|
413
|
+
* expressions that reference the index can be bound instead of throwing
|
|
414
|
+
* `ReferenceError` at runtime. The renderItem body only receives the
|
|
415
|
+
* index as a synthetic `__innerIdx<uid>` positional param (to avoid
|
|
416
|
+
* cross-depth name collisions), so emitters must alias it under this
|
|
417
|
+
* name when referenced — see `nestedLoopReferencesIndex` in `shared.ts`.
|
|
418
|
+
*/
|
|
419
|
+
index: string | null
|
|
397
420
|
containerSlotId: string | null // Slot ID of the parent element containing the loop (for hydration)
|
|
398
421
|
/** HTML template for a single inner loop item (for mapArray CSR rendering) */
|
|
399
422
|
template?: string
|
|
@@ -519,6 +542,14 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
519
542
|
* proven covered by an effect).
|
|
520
543
|
*/
|
|
521
544
|
skeletonTemplate?: string
|
|
545
|
+
/**
|
|
546
|
+
* Compile-time child-index paths for `skeletonTemplate`'s dynamic slots
|
|
547
|
+
* (perf, #2143), computed by `computeSkeletonSlotPaths` from the same IR
|
|
548
|
+
* tree. `undefined` means "resolve every slot via `qsa`/`$t` at runtime"
|
|
549
|
+
* (skeletonTemplate is still hoisted/cloned — only the runtime lookups on
|
|
550
|
+
* top of the fresh clone become the fallback rather than the path chain).
|
|
551
|
+
*/
|
|
552
|
+
skeletonPaths?: SkeletonSlotPaths
|
|
522
553
|
childEventHandlers: string[] // Bare-identifier event handler names (for the reachability graph)
|
|
523
554
|
childComponent?: IRLoopChildComponent // For createComponent-based rendering
|
|
524
555
|
nestedComponents?: IRLoopChildComponent[] // For nested components in loop bodies
|