@barefootjs/jsx 0.18.5 → 0.19.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/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/expression-parser.d.ts +8 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1002 -284
- 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/phases/props-extraction.d.ts +0 -1
- package/dist/ir-to-client-js/phases/props-extraction.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__/client-js-generation.test.ts +40 -0
- 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__/props-destructuring.test.ts +61 -8
- 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 +36 -22
- package/src/expression-parser.ts +33 -0
- package/src/index.ts +20 -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/phases/props-extraction.ts +5 -9
- 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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn compile-time `SkeletonSlotPaths` (html-template.ts) into the `__p`
|
|
3
|
+
* array-literal contents plus slot -> index maps consumed by
|
|
4
|
+
* `stringifyPlainLoop` / `stringifyReactiveEffects` / `emitLoopChildRefs`
|
|
5
|
+
* (perf: direct child-index paths for hoisted single-root loops, #2143).
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth for `__p` ordering: `loop.ts` (which emits the
|
|
8
|
+
* array literal) and `reactive-effects.ts` (which emits the per-slot index
|
|
9
|
+
* reads) both derive their indices from the SAME `SkeletonPathPlan`, so the
|
|
10
|
+
* two can never drift out of sync.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { SkeletonSlotPaths } from '../../html-template.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert a root-relative child-index path into a `.firstChild.nextSibling…`
|
|
17
|
+
* property chain off `base`. An empty path resolves to `base` itself (the
|
|
18
|
+
* slot lives on the clone root, e.g. an attr on the loop body's own tag).
|
|
19
|
+
*/
|
|
20
|
+
export function pathExpr(base: string, path: readonly number[]): string {
|
|
21
|
+
let expr = base
|
|
22
|
+
for (const idx of path) {
|
|
23
|
+
expr += '.firstChild'
|
|
24
|
+
if (idx > 0) expr += '.nextSibling'.repeat(idx)
|
|
25
|
+
}
|
|
26
|
+
return expr
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SkeletonPathPlan {
|
|
30
|
+
/** Expressions for the `const __p = [...]` array literal, in stable order. */
|
|
31
|
+
arrayElems: string[]
|
|
32
|
+
/** slotId -> index into `__p`, for element-anchored slots (reactive attrs, refs). */
|
|
33
|
+
elementIndexBySlot: Map<string, number>
|
|
34
|
+
/** slotId -> index into `__p`, for text-marker-anchored slots (reactive texts). */
|
|
35
|
+
textIndexBySlot: Map<string, number>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the `__p` plan for a hoisted loop's reactive attr / text / ref
|
|
40
|
+
* slots. A slotId absent from `skeletonPaths` (shouldn't happen for a
|
|
41
|
+
* successfully-hoisted skeleton, but handled defensively) is simply left
|
|
42
|
+
* out of both maps — its stringifier call site falls back to `qsa`/`$t`
|
|
43
|
+
* for that one slot without affecting any other slot's resolution.
|
|
44
|
+
*/
|
|
45
|
+
export function buildSkeletonPathPlan(
|
|
46
|
+
skeletonPaths: SkeletonSlotPaths,
|
|
47
|
+
elVar: string,
|
|
48
|
+
opts: { elementSlotIds: readonly string[]; textSlotIds: readonly string[] },
|
|
49
|
+
): SkeletonPathPlan {
|
|
50
|
+
const arrayElems: string[] = []
|
|
51
|
+
const elementIndexBySlot = new Map<string, number>()
|
|
52
|
+
const textIndexBySlot = new Map<string, number>()
|
|
53
|
+
|
|
54
|
+
for (const slotId of opts.elementSlotIds) {
|
|
55
|
+
if (elementIndexBySlot.has(slotId)) continue
|
|
56
|
+
const path = skeletonPaths.elementPaths.get(slotId)
|
|
57
|
+
if (!path) continue
|
|
58
|
+
elementIndexBySlot.set(slotId, arrayElems.length)
|
|
59
|
+
arrayElems.push(pathExpr(elVar, path))
|
|
60
|
+
}
|
|
61
|
+
for (const slotId of opts.textSlotIds) {
|
|
62
|
+
if (textIndexBySlot.has(slotId)) continue
|
|
63
|
+
const path = skeletonPaths.textMarkerPaths.get(slotId)
|
|
64
|
+
if (!path) continue
|
|
65
|
+
textIndexBySlot.set(slotId, arrayElems.length)
|
|
66
|
+
arrayElems.push(pathExpr(elVar, path))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { arrayElems, elementIndexBySlot, textIndexBySlot }
|
|
70
|
+
}
|
|
@@ -82,6 +82,30 @@ export function templateRootIsSvg(template: string): boolean {
|
|
|
82
82
|
return branches.every(templateRootIsSvg)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Wrap decision for MULTI-ROOT (fragment) templates, where the synthetic
|
|
87
|
+
* `<svg>` wrap swallows every sibling root at once (#2233 Copilot review).
|
|
88
|
+
*
|
|
89
|
+
* `templateRootIsSvg` inspects only the FIRST root tag. For single-root
|
|
90
|
+
* templates that's exact, but a fragment whose first root is an `<svg>`
|
|
91
|
+
* CONTAINER (`<><svg/><span/></>`) doesn't need the wrap at all — the
|
|
92
|
+
* HTML parser enters foreign content at `<svg>` on its own — and wrapping
|
|
93
|
+
* would drag the HTML siblings into the SVG namespace (`<span>` becomes an
|
|
94
|
+
* SVGUnknownElement, silently undrawn). So `<svg>`-first fragments skip
|
|
95
|
+
* the wrap; only leaf-rooted fragments (`<line>`, `<circle>`, ...) get it.
|
|
96
|
+
*
|
|
97
|
+
* Known edge (degenerate, pre-existing): an `<svg>`-first fragment with
|
|
98
|
+
* SVG-LEAF siblings (`<><svg/><line/></>`) leaves the bare leaf siblings
|
|
99
|
+
* in the HTML namespace — exactly the pre-#2219 behavior. Deciding that
|
|
100
|
+
* shape correctly needs a scan of every top-level root tag; not worth the
|
|
101
|
+
* parser until a real component hits it.
|
|
102
|
+
*/
|
|
103
|
+
export function multiRootTemplateNeedsSvgWrap(template: string): boolean {
|
|
104
|
+
const m = stripLeadingNonContent(template).match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/)
|
|
105
|
+
if (m && m[1].toLowerCase() === 'svg') return false
|
|
106
|
+
return templateRootIsSvg(template)
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
/**
|
|
86
110
|
* Strip leading whitespace and HTML comment markers (`<!-- ... -->`) so
|
|
87
111
|
* that a branch like `<!--bf-cond-start:s0-->${...}<!--bf-cond-end:s0-->`
|
|
@@ -269,7 +293,7 @@ export function emitMultiRootTemplateCloneLines(
|
|
|
269
293
|
varEl: string,
|
|
270
294
|
varExtras: string,
|
|
271
295
|
): string[] {
|
|
272
|
-
const isSvg =
|
|
296
|
+
const isSvg = multiRootTemplateNeedsSvgWrap(template)
|
|
273
297
|
// Wrap in `<svg>` so the parser walks into SVG foreign content; we then
|
|
274
298
|
// descend one level to pick up the per-item roots.
|
|
275
299
|
const innerHtmlExpr = isSvg ? `\`<svg>${template}</svg>\`` : `\`${template}\``
|
|
@@ -338,6 +338,59 @@ export function extractFreeIdentifiersFromText(text: string): Set<string> {
|
|
|
338
338
|
return extractFreeIdentifiersFromNode(expr)
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Free identifiers referenced by a block of JS *statements* — e.g. an inner
|
|
343
|
+
* `.map()` callback's block-body preamble (`mapPreamble`, #1052). Unlike
|
|
344
|
+
* `extractFreeIdentifiersFromText`, which wraps its input in `(...)` to
|
|
345
|
+
* force single-expression parsing, statement text (`const x = ...;`) is
|
|
346
|
+
* not a valid expression and would fail to parse inside parens — parse it
|
|
347
|
+
* as top-level source instead. Used by nested-loop index-param reference
|
|
348
|
+
* gating (#2218).
|
|
349
|
+
*/
|
|
350
|
+
export function extractFreeIdentifiersFromStatementText(text: string): Set<string> {
|
|
351
|
+
if (!text || text.trim().length === 0) return new Set()
|
|
352
|
+
const sf = ts.createSourceFile(
|
|
353
|
+
'__free_ids_stmt__.ts',
|
|
354
|
+
text,
|
|
355
|
+
ts.ScriptTarget.Latest,
|
|
356
|
+
/* setParentNodes */ true,
|
|
357
|
+
ts.ScriptKind.TS,
|
|
358
|
+
)
|
|
359
|
+
return extractFreeIdentifiersFromNode(sf)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Free identifiers referenced inside a template-literal-style string's
|
|
364
|
+
* `${...}` interpolations — e.g. a nested loop's per-item HTML `template`
|
|
365
|
+
* (#2218). Wraps the text in backticks and parses it as a real
|
|
366
|
+
* `TemplateExpression` so brace balancing, string literals, and nested
|
|
367
|
+
* object/array literals inside `${...}` are handled correctly by the TS
|
|
368
|
+
* parser (AST-based — never the char-class regex `extractTemplateExpressions`
|
|
369
|
+
* in `identifiers.ts` uses for its looser references-graph pass). Safe by
|
|
370
|
+
* construction: the same `template` string is embedded verbatim inside a
|
|
371
|
+
* real backtick literal at emit time (`__t.innerHTML = \`${template}\``),
|
|
372
|
+
* so wrapping it here to parse mirrors exactly how it's already used.
|
|
373
|
+
*/
|
|
374
|
+
export function extractFreeIdentifiersFromTemplateText(template: string): Set<string> {
|
|
375
|
+
if (!template || template.length === 0) return new Set()
|
|
376
|
+
const sf = ts.createSourceFile(
|
|
377
|
+
'__free_ids_template__.ts',
|
|
378
|
+
`(\`${template}\`);`,
|
|
379
|
+
ts.ScriptTarget.Latest,
|
|
380
|
+
/* setParentNodes */ true,
|
|
381
|
+
ts.ScriptKind.TS,
|
|
382
|
+
)
|
|
383
|
+
const stmt = sf.statements[0]
|
|
384
|
+
if (!stmt || !ts.isExpressionStatement(stmt)) return new Set()
|
|
385
|
+
const expr = ts.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression
|
|
386
|
+
if (!ts.isTemplateExpression(expr)) return new Set()
|
|
387
|
+
const ids = new Set<string>()
|
|
388
|
+
for (const span of expr.templateSpans) {
|
|
389
|
+
for (const id of extractFreeIdentifiersFromNode(span.expression)) ids.add(id)
|
|
390
|
+
}
|
|
391
|
+
return ids
|
|
392
|
+
}
|
|
393
|
+
|
|
341
394
|
/**
|
|
342
395
|
* Reduce a memo's `() => expr` source to the expression that should be
|
|
343
396
|
* substituted in for `memoName()`. Matches the extraction done by the
|
|
@@ -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
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - prop has property/index access → `?? {}` (skipped when the
|
|
10
10
|
* prop is a conditional guard
|
|
11
11
|
* so falsy values stay falsy)
|
|
12
|
-
* - prop is optional with type info → `?? <inferred default>`
|
|
13
12
|
* - otherwise → no default
|
|
14
13
|
*
|
|
15
14
|
* Skipped entirely when the component uses an opaque props object name
|
|
@@ -20,7 +19,7 @@
|
|
|
20
19
|
import type { PropUsage } from '../../types.ts'
|
|
21
20
|
import { propHasPropertyAccess } from '../compute-prop-usage.ts'
|
|
22
21
|
import type { ClientJsContext } from '../types.ts'
|
|
23
|
-
import {
|
|
22
|
+
import { PROPS_PARAM } from '../utils.ts'
|
|
24
23
|
|
|
25
24
|
export function emitPropsExtraction(
|
|
26
25
|
lines: string[],
|
|
@@ -53,14 +52,11 @@ export function emitPropsExtraction(
|
|
|
53
52
|
lines.push(` const ${propName} = ${PROPS_PARAM}.${propName} ?? []`)
|
|
54
53
|
} else if (propHasPropertyAccess(usage) && !propsUsedAsConditions.has(propName)) {
|
|
55
54
|
lines.push(` const ${propName} = ${PROPS_PARAM}.${propName} ?? {}`)
|
|
56
|
-
} else if (prop?.optional && prop?.type) {
|
|
57
|
-
const inferredDefault = inferDefaultValue(prop.type)
|
|
58
|
-
if (inferredDefault !== 'undefined') {
|
|
59
|
-
lines.push(` const ${propName} = ${PROPS_PARAM}.${propName} ?? ${inferredDefault}`)
|
|
60
|
-
} else {
|
|
61
|
-
lines.push(` const ${propName} = ${PROPS_PARAM}.${propName}`)
|
|
62
|
-
}
|
|
63
55
|
} else {
|
|
56
|
+
// No synthesized default for a defaultless optional (`{ size }:
|
|
57
|
+
// { size?: number }`): the JS binding is `undefined` when absent, and
|
|
58
|
+
// a zero default would diverge from SSR (`size ?? 1` seeds 1
|
|
59
|
+
// server-side; a `_p.size ?? 0` extraction would hydrate to 0).
|
|
64
60
|
lines.push(` const ${propName} = ${PROPS_PARAM}.${propName}`)
|
|
65
61
|
}
|
|
66
62
|
}
|
|
@@ -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
|
}
|