@barefootjs/jsx 0.18.5 → 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.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +977 -273
- package/dist/ir-to-client-js/collect-elements.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/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/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 +23 -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 +21 -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/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/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__/inner-loop-svg-namespace.test.ts +245 -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__/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 +9 -0
- package/src/analyzer.ts +6 -0
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +15 -3
- 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/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/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 +53 -0
- package/src/ir-to-client-js/html-template.ts +235 -2
- 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 +21 -0
- package/src/jsx-to-ir.ts +230 -39
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
|
@@ -875,6 +875,196 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
|
|
|
875
875
|
}
|
|
876
876
|
}
|
|
877
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
|
+
|
|
878
1068
|
/**
|
|
879
1069
|
* Generate an HTML template for composite element reconciliation.
|
|
880
1070
|
* Identical to irToHtmlTemplate except component nodes become placeholder
|
|
@@ -1135,6 +1325,20 @@ export interface TemplateOptions {
|
|
|
1135
1325
|
csrEnv?: CsrEnv
|
|
1136
1326
|
insideLoop?: boolean
|
|
1137
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>
|
|
1138
1342
|
/** Emit `bf-s` placeholder on scoped elements inside a jsx-children prop (#1320). */
|
|
1139
1343
|
inHoistedChildren?: boolean
|
|
1140
1344
|
/**
|
|
@@ -1988,7 +2192,36 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
1988
2192
|
}
|
|
1989
2193
|
|
|
1990
2194
|
case 'loop': {
|
|
1991
|
-
|
|
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('')
|
|
1992
2225
|
// Whole-item conditional loops (#1665): prepend the per-item
|
|
1993
2226
|
// `<!--bf-loop-i:KEY-->` anchor so `mapArrayAnchored` can track items
|
|
1994
2227
|
// that render no element. Mirrors the `irToHtmlTemplate` loop case.
|
|
@@ -2022,7 +2255,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2022
2255
|
if (node.flatMapCallback) {
|
|
2023
2256
|
let body = node.flatMapCallback.templateBody ?? node.flatMapCallback.body
|
|
2024
2257
|
for (const frag of node.flatMapCallback.fragments) {
|
|
2025
|
-
const renderedIr =
|
|
2258
|
+
const renderedIr = recurseInLoopBody(frag.ir)
|
|
2026
2259
|
body = body.replace(frag.placeholder, `\`${renderedIr}\``)
|
|
2027
2260
|
}
|
|
2028
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
|
|
@@ -404,6 +405,18 @@ export type ConditionalBranchConditional = ConditionalElement
|
|
|
404
405
|
export interface NestedLoop extends LoopCore {
|
|
405
406
|
kind: 'nested'
|
|
406
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
|
|
407
420
|
containerSlotId: string | null // Slot ID of the parent element containing the loop (for hydration)
|
|
408
421
|
/** HTML template for a single inner loop item (for mapArray CSR rendering) */
|
|
409
422
|
template?: string
|
|
@@ -529,6 +542,14 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
529
542
|
* proven covered by an effect).
|
|
530
543
|
*/
|
|
531
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
|
|
532
553
|
childEventHandlers: string[] // Bare-identifier event handler names (for the reachability graph)
|
|
533
554
|
childComponent?: IRLoopChildComponent // For createComponent-based rendering
|
|
534
555
|
nestedComponents?: IRLoopChildComponent[] // For nested components in loop bodies
|