@barefootjs/jsx 0.5.3 → 0.6.1

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.
Files changed (40) hide show
  1. package/dist/adapters/parsed-expr-emitter.d.ts +15 -2
  2. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  3. package/dist/expression-parser.d.ts +138 -1
  4. package/dist/expression-parser.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +450 -23
  8. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/html-template.d.ts +30 -1
  11. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts +4 -0
  14. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +46 -2
  16. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/types.d.ts +10 -0
  19. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  20. package/package.json +2 -2
  21. package/src/__tests__/child-components-in-map.test.ts +84 -0
  22. package/src/__tests__/expression-parser.test.ts +276 -14
  23. package/src/__tests__/foreach-client-only.test.ts +80 -0
  24. package/src/__tests__/ir-reduce-op.test.ts +51 -0
  25. package/src/__tests__/ir-to-client-js/reactivity.test.ts +1 -0
  26. package/src/__tests__/reduce-op.test.ts +201 -0
  27. package/src/__tests__/staged-ir/06-multi-stage-soak.test.ts +18 -3
  28. package/src/adapters/parsed-expr-emitter.ts +50 -1
  29. package/src/expression-parser.ts +770 -21
  30. package/src/index.ts +1 -1
  31. package/src/ir-to-client-js/collect-elements.ts +9 -3
  32. package/src/ir-to-client-js/emit-registration.ts +1 -1
  33. package/src/ir-to-client-js/generate-init.ts +16 -1
  34. package/src/ir-to-client-js/html-template.ts +156 -2
  35. package/src/ir-to-client-js/index.ts +1 -0
  36. package/src/ir-to-client-js/phases/provider-and-child-inits.ts +12 -1
  37. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +55 -1
  38. package/src/ir-to-client-js/plan/static-array-child-init.ts +47 -1
  39. package/src/ir-to-client-js/stringify/static-array-child-init.ts +69 -0
  40. package/src/ir-to-client-js/types.ts +10 -0
package/src/index.ts CHANGED
@@ -245,7 +245,7 @@ export { ErrorCodes, createError, formatError, generateCodeFrame } from './error
245
245
 
246
246
  // Expression Parser
247
247
  export { parseExpression, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, containsHigherOrder } from './expression-parser'
248
- export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, SupportLevel, SupportResult, TemplatePart } from './expression-parser'
248
+ export type { ParsedExpr, ParsedStatement, SortComparator, SortKey, ReduceOp, FlatDepth, FlatMapOp, FlatMapLeaf, SupportLevel, SupportResult, TemplatePart } from './expression-parser'
249
249
  export { buildLoopChainExpr } from './loop-chain'
250
250
  export type { LoopChainInputs } from './loop-chain'
251
251
 
@@ -428,9 +428,15 @@ function decideLoopRendering(
428
428
  ctx: ClientJsContext | undefined,
429
429
  ): { useElementReconciliation: boolean; innerLoops: NestedLoop[] | undefined } {
430
430
  const hasNestedComps = (loop.nestedComponents?.length ?? 0) > 0
431
- const innerLoops = !loop.childComponent
432
- ? collectInnerLoops(loop.children, siblingOffsets, loop.param, ctx)
433
- : undefined
431
+ // Collect inner loops even when the outer item is a child component
432
+ // (#1725): a `.map()` of components living inside the child component's
433
+ // JSX children (e.g. `<SelectGroup>{items.map(...)}</SelectGroup>`) needs
434
+ // its own `initChild` pass. `loop.children` is the single child-component
435
+ // node; `collectInnerLoops` descends into its children to find the nested
436
+ // loop. These only surface for static arrays (gated at the call site via
437
+ // `isStaticArray && innerLoops.length`) so dynamic child-component loops —
438
+ // which render through `createComponent` — are unaffected.
439
+ const innerLoops = collectInnerLoops(loop.children, siblingOffsets, loop.param, ctx)
434
440
  const hasInnerLoops = (innerLoops?.length ?? 0) > 0
435
441
  const useElementReconciliation =
436
442
  !loop.childComponent && !loop.isStaticArray && (hasNestedComps || hasInnerLoops)
@@ -168,7 +168,7 @@ export function emitRegistrationAndHydration(
168
168
  // transformation runs at this layer (#1277).
169
169
  const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
170
170
  const templateHtml = generateCsrTemplate(
171
- _ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames
171
+ _ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
172
172
  )
173
173
  if (templateHtml) {
174
174
  defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``)
@@ -14,7 +14,8 @@ import { PROPS_PARAM } from './utils'
14
14
  import { buildReferencesGraph } from './build-references'
15
15
  import { computePropUsage } from './compute-prop-usage'
16
16
  import { IMPORT_PLACEHOLDER, MODULE_CONSTANTS_PLACEHOLDER } from './imports'
17
- import { emitRegistrationAndHydration } from './emit-registration'
17
+ import { emitRegistrationAndHydration, csrInlinableConstantsFromCtx } from './emit-registration'
18
+ import { computeDeferredChildSlots } from './html-template'
18
19
  import { emitChildComponentImports } from './child-components'
19
20
  import { classifyLocalDeclarations } from './init-declarations'
20
21
  import { emitModuleLevelDeclarations, resolveFinalImports } from './emit-module-level'
@@ -55,6 +56,20 @@ export function generateInitFunction(
55
56
  // duplicate warnings (#1247).
56
57
  const inlinability = buildInlinableConstants(ctx, graph, ir.root)
57
58
 
59
+ // Decide which direct child components must defer their render to init
60
+ // because a forwarded prop resolves to an init-scope-only / non-inlinable
61
+ // local (dropped-prop fix). The child-init phase reads this set to emit
62
+ // `upsertChild` instead of `initChild`; `emitRegistrationAndHydration`
63
+ // reads it to emit a `data-bf-ph` placeholder instead of
64
+ // `renderChild(...)`. Computed here, once `unsafeLocalNames` is known.
65
+ ctx.deferredChildSlots = computeDeferredChildSlots(
66
+ ir.root,
67
+ ctx,
68
+ csrInlinableConstantsFromCtx(ctx),
69
+ inlinability.unsafeLocalNames,
70
+ ctx.propsObjectName,
71
+ )
72
+
58
73
  // --- Emission: declarative phase pipeline. Each entry in `PHASES`
59
74
  // declares its inputs (dependsOn) and emission action (run); the
60
75
  // stable topological execution preserves the legacy by-position
@@ -2,7 +2,7 @@
2
2
  * IR → HTML template string generation and validation.
3
3
  */
4
4
 
5
- import type { AttrValue, IRAttribute, IRNode } from '../types'
5
+ import type { AttrValue, IRAttribute, IRNode, IRProp } from '../types'
6
6
  import { isBooleanAttr } from '../html-constants'
7
7
  import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_PH, keyAttrName, loopStartMarker, loopEndMarker, loopItemMarker, freeIdsFromRefs, setIntersects, wrapExprWithLoopParams } from './utils'
8
8
  import type { LoopParamSpec } from './utils'
@@ -962,6 +962,20 @@ export interface TemplateOptions {
962
962
  loopDepth?: number
963
963
  /** Emit `bf-s` placeholder on scoped elements inside a jsx-children prop (#1320). */
964
964
  inHoistedChildren?: boolean
965
+ /**
966
+ * Slot ids of direct child components whose render must be DEFERRED to
967
+ * init because at least one forwarded (non-`/* @client *\/`) prop value
968
+ * references an init-scope-only / non-inlinable local — the module-scope
969
+ * template lambda can't supply it, so eagerly calling `renderChild` with
970
+ * the prop dropped would make the child template read `undefined`.
971
+ *
972
+ * For these slots the CSR `component` case emits a `data-bf-ph`
973
+ * placeholder instead of `renderChild(...)`; the parent init replaces it
974
+ * via `upsertChild` (→ `createComponent` with the complete getter props).
975
+ * Computed up front by `computeDeferredChildSlots` so the init phase and
976
+ * the template phase agree on which children defer (dropped-prop fix).
977
+ */
978
+ deferredChildSlots?: ReadonlySet<string>
965
979
  }
966
980
 
967
981
  /**
@@ -1353,6 +1367,7 @@ export function generateCsrTemplate(
1353
1367
  restSpreadNames?: Set<string>,
1354
1368
  propsObjectName?: string | null,
1355
1369
  unsafeLocalNames?: Set<string>,
1370
+ deferredChildSlots?: ReadonlySet<string>,
1356
1371
  ): string {
1357
1372
  // Build the substitution env once per component. Signals + memos come
1358
1373
  // from `buildSignalMemoEnv`; inlinable constants layer in here so
@@ -1367,7 +1382,134 @@ export function generateCsrTemplate(
1367
1382
  }
1368
1383
  }
1369
1384
  }
1370
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, loopDepth: -1 })
1385
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, deferredChildSlots, loopDepth: -1 })
1386
+ }
1387
+
1388
+ /**
1389
+ * Build the per-component CSR substitution env (signals + memos + inlinable
1390
+ * constants), matching what `generateCsrTemplate` builds. Shared so the
1391
+ * deferred-child analysis and the template emit agree on substitution
1392
+ * results.
1393
+ */
1394
+ function buildCsrEnvForCtx(
1395
+ ctx: ClientJsContext,
1396
+ inlinableConstants: Map<string, string> | undefined,
1397
+ propsObjectName?: string | null,
1398
+ ): CsrEnv {
1399
+ const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null)
1400
+ const csrEnv: CsrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName }
1401
+ if (inlinableConstants) {
1402
+ for (const [name, value] of inlinableConstants) {
1403
+ if (!csrEnv.substitutions.has(name)) {
1404
+ csrEnv.substitutions.set(name, { kind: 'identifier', replacement: value, freeIdentifiers: new Set() })
1405
+ }
1406
+ }
1407
+ }
1408
+ return csrEnv
1409
+ }
1410
+
1411
+ /**
1412
+ * Decide whether a single forwarded component prop value would be DROPPED
1413
+ * by the CSR `component` emit — i.e. after `csrSubstitute` its expression
1414
+ * still references a name in `unsafeLocalNames`. Mirrors the
1415
+ * `transformExpr` UNSAFE gate so the deferral analysis matches the actual
1416
+ * template output exactly.
1417
+ */
1418
+ function propResolvesUnsafe(
1419
+ prop: IRProp,
1420
+ env: CsrEnv,
1421
+ unsafeLocalNames: ReadonlySet<string>,
1422
+ ): boolean {
1423
+ if (unsafeLocalNames.size === 0) return false
1424
+ let source: string | undefined
1425
+ switch (prop.value.kind) {
1426
+ case 'expression':
1427
+ case 'spread':
1428
+ source = prop.value.expr
1429
+ break
1430
+ case 'template':
1431
+ source = attrValueToString(prop.value, { useTemplate: true }) ?? undefined
1432
+ break
1433
+ default:
1434
+ // literal / boolean / jsx-children carry no init-scope identifiers.
1435
+ return false
1436
+ }
1437
+ if (!source) return false
1438
+ const { freeIdentifiers } = csrSubstitute(source, env)
1439
+ return setIntersects(freeIdentifiers, unsafeLocalNames)
1440
+ }
1441
+
1442
+ /**
1443
+ * Walk the component IR and collect the slot ids of DIRECT child
1444
+ * components whose render must be deferred to init because at least one
1445
+ * forwarded (non-`/* @client *\/`, non-event) prop resolves to an
1446
+ * init-scope-only / non-inlinable local. The module-scope CSR template
1447
+ * lambda can't supply such a value, so `renderChild(...)` would drop the
1448
+ * prop and the child template would read `undefined` and throw.
1449
+ *
1450
+ * Only top-level (non-loop, non-clientOnly-conditional) children are
1451
+ * considered — those are the ones rendered via the `renderChild(...)` form
1452
+ * in the registration template and wired through `ctx.childInits`. Loop /
1453
+ * conditional-branch children already go through their own
1454
+ * placeholder + `createComponent` materialize paths.
1455
+ */
1456
+ export function computeDeferredChildSlots(
1457
+ node: IRNode,
1458
+ ctx: ClientJsContext,
1459
+ inlinableConstants: Map<string, string> | undefined,
1460
+ unsafeLocalNames: ReadonlySet<string> | undefined,
1461
+ propsObjectName?: string | null,
1462
+ ): Set<string> {
1463
+ const deferred = new Set<string>()
1464
+ if (!unsafeLocalNames || unsafeLocalNames.size === 0) return deferred
1465
+ const env = buildCsrEnvForCtx(ctx, inlinableConstants, propsObjectName)
1466
+
1467
+ const visit = (n: IRNode): void => {
1468
+ switch (n.type) {
1469
+ case 'component': {
1470
+ if (n.name === 'Portal') {
1471
+ n.children.forEach(visit)
1472
+ return
1473
+ }
1474
+ if (n.slotId) {
1475
+ const dropped = n.props.some(p => {
1476
+ // Spread props (`...`) are forwarded via the rest-spread path
1477
+ // (`restSpreadNames`), not the per-prop inline form, so they are
1478
+ // out of scope for this drop check; `key` and event handlers
1479
+ // (`onX`) likewise never carry init-scope render values. This
1480
+ // filter set MUST mirror the `propsEntries` filter in the CSR
1481
+ // `component` emit below so the deferral decision matches output.
1482
+ if (p.name === '...' || p.name.startsWith('...') || p.name === 'key') return false
1483
+ if (p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()) return false
1484
+ if (p.clientOnly) return false
1485
+ return propResolvesUnsafe(p, env, unsafeLocalNames)
1486
+ })
1487
+ if (dropped) deferred.add(n.slotId)
1488
+ }
1489
+ // Do not descend into a component's JSX-children props here: those
1490
+ // children render in the parent scope only when hoisted, and the
1491
+ // deferral concern is the direct child component's own props.
1492
+ return
1493
+ }
1494
+ case 'element':
1495
+ n.children.forEach(visit)
1496
+ return
1497
+ case 'fragment':
1498
+ n.children.forEach(visit)
1499
+ return
1500
+ case 'conditional':
1501
+ // Conditional branch children are handled by the branch
1502
+ // materialize path, not the top-level renderChild form.
1503
+ return
1504
+ case 'loop':
1505
+ // Loop children go through the loop materialize path.
1506
+ return
1507
+ default:
1508
+ return
1509
+ }
1510
+ }
1511
+ visit(node)
1512
+ return deferred
1371
1513
  }
1372
1514
 
1373
1515
  function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
@@ -1542,6 +1684,18 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
1542
1684
  return node.children.map(recurse).join('')
1543
1685
  }
1544
1686
 
1687
+ // Deferred child (dropped-prop fix): at least one forwarded prop
1688
+ // resolves to an init-scope-only local the module-scope template
1689
+ // lambda can't supply. Emitting `renderChild('Child', { /* prop
1690
+ // dropped */ })` would make the child template read `undefined` and
1691
+ // throw. Emit a `data-bf-ph` placeholder instead — the parent init
1692
+ // resolves it via `upsertChild` → `createComponent` with the full
1693
+ // getter props (mirrors the `irToPlaceholderTemplate` deferral and
1694
+ // the clientOnly-conditional empty-marker precedent).
1695
+ if (node.slotId && opts.deferredChildSlots?.has(node.slotId)) {
1696
+ return `<div ${DATA_BF_PH}="${node.slotId}"></div>`
1697
+ }
1698
+
1545
1699
  const propsEntries = node.props
1546
1700
  .filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
1547
1701
  .filter(p => !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
@@ -176,6 +176,7 @@ function createContext(
176
176
  loopElements: [],
177
177
  refElements: [],
178
178
  childInits: [],
179
+ deferredChildSlots: new Set(),
179
180
  reactiveProps: [],
180
181
  reactiveChildProps: [],
181
182
  reactiveAttrs: [],
@@ -28,8 +28,19 @@ export function emitProviderAndChildInits(lines: string[], ctx: ClientJsContext)
28
28
  lines.push('')
29
29
  lines.push(` // Initialize child components with props`)
30
30
  for (const child of ctx.childInits) {
31
+ const registryName = nameForRegistryRef(child.name)
32
+ // Deferred child (dropped-prop fix): the registration template emits
33
+ // a `data-bf-ph` placeholder for this slot rather than rendering it.
34
+ // `upsertChild` resolves both shapes — an existing SSR scope (→
35
+ // initChild) or the placeholder (→ createComponent with the full
36
+ // getter props). Use it so the child is created/initialised with
37
+ // complete props instead of running against a missing prop.
38
+ if (child.slotId && ctx.deferredChildSlots.has(child.slotId)) {
39
+ lines.push(` upsertChild(__scope, '${registryName}', '${child.slotId}', ${child.propsExpr})`)
40
+ continue
41
+ }
31
42
  const scopeRef = child.slotId ? `_${varSlotId(child.slotId)}` : '__scope'
32
- lines.push(` initChild('${nameForRegistryRef(child.name)}', ${scopeRef}, ${child.propsExpr})`)
43
+ lines.push(` initChild('${registryName}', ${scopeRef}, ${child.propsExpr})`)
33
44
  }
34
45
  }
35
46
  }
@@ -8,6 +8,10 @@
8
8
  * 2. `outer-nested` for each depth-0 entry in `elem.nestedComponents`.
9
9
  * 3. `inner-loop-nested` for each `elem.innerLoops` entry that has
10
10
  * matching depth-N components.
11
+ * 4. `component-rooted-inner-loop` instead of (3) when the outer item is
12
+ * itself a child component (#1725) — the inner `.map()` lives inside
13
+ * the component's JSX children, so it's addressed by a document-order
14
+ * zip rather than element offsets.
11
15
  *
12
16
  * Selector / propsExpr / offset decisions all resolve here. The
13
17
  * stringifier never inspects raw IR.
@@ -23,6 +27,7 @@ import { buildCompSelector } from '../control-flow/shared'
23
27
  /** The inline prop shape carried on `IRLoopChildComponent.props`. */
24
28
  type LoopChildCompProp = IRLoopChildComponent['props'][number]
25
29
  import type {
30
+ ComponentRootedInnerLoopInitPlan,
26
31
  InnerLoopComp,
27
32
  InnerLoopNestedInitPlan,
28
33
  OuterNestedInitPlan,
@@ -56,7 +61,16 @@ export function buildStaticArrayChildInitsPlan(
56
61
  (c.loopDepth ?? 0) === innerLoop.depth && c.innerLoopArray === innerLoop.array,
57
62
  )
58
63
  if (innerComps.length === 0) continue
59
- plans.push(buildInnerLoopNestedPlan(elem, innerLoop, innerComps))
64
+ // Component-rooted outer item (#1725): the inner `.map()` lives
65
+ // inside the child component's JSX children. The element-offset
66
+ // addressing of `inner-loop-nested` can't reach a fragment-rooted
67
+ // passthrough's flattened items, so use the document-order zip
68
+ // shape instead.
69
+ plans.push(
70
+ elem.childComponent
71
+ ? buildComponentRootedInnerLoopPlan(elem, innerLoop, innerComps)
72
+ : buildInnerLoopNestedPlan(elem, innerLoop, innerComps),
73
+ )
60
74
  }
61
75
  }
62
76
  }
@@ -134,6 +148,46 @@ function buildInnerLoopNestedPlan(
134
148
  }
135
149
  }
136
150
 
151
+ /**
152
+ * Build the document-order-zip plan for an inner `.map()` of components living
153
+ * inside a component-rooted loop item (#1725).
154
+ *
155
+ * Known limitation (shared with `inner-loop-nested`): the emitted `forEach`
156
+ * iterates `innerLoop.array` — the *base* inner array. `NestedLoop` doesn't
157
+ * carry `filterPredicate` / `sortComparator`, so a `.filter()` / `.sort()` on
158
+ * the inner `.map()` makes the iteration order diverge from the SSR render
159
+ * order. `inner-loop-nested` masks this per-group (each group re-indexes
160
+ * `__ic.children` from 0, so a trailing filtered-out item just reads
161
+ * `undefined`); the zip's single document-order cursor instead misaligns every
162
+ * later group. Both are wrong for non-trailing filtered items — filter/sort on
163
+ * a nested static-array loop is unsupported across this family, not a
164
+ * regression introduced here.
165
+ */
166
+ function buildComponentRootedInnerLoopPlan(
167
+ elem: TopLevelLoop,
168
+ innerLoop: NestedLoop,
169
+ innerComps: readonly IRLoopChildComponent[],
170
+ ): ComponentRootedInnerLoopInitPlan {
171
+ const comps: InnerLoopComp[] = innerComps.map(comp => ({
172
+ componentName: comp.name,
173
+ selector: buildCompSelector(comp),
174
+ propsExpr: buildStaticPropsExpr(comp.props),
175
+ }))
176
+
177
+ return {
178
+ kind: 'component-rooted-inner-loop',
179
+ containerVar: `_${varSlotId(elem.slotId)}`,
180
+ outerArrayExpr: elem.array,
181
+ outerParam: elem.param,
182
+ outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
183
+ innerArrayExpr: innerLoop.array,
184
+ innerParam: innerLoop.param,
185
+ innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
186
+ depth: innerLoop.depth,
187
+ comps,
188
+ }
189
+ }
190
+
137
191
  /**
138
192
  * Build the props object expression used by static-array child inits.
139
193
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Plan types for `emitStaticArrayChildInits` — the three shapes that
2
+ * Plan types for `emitStaticArrayChildInits` — the shapes that
3
3
  * `static array` loops emit for child component initialisation:
4
4
  *
5
5
  * - `single-comp` — `loop.childComponent` ケース。一つの child component
@@ -8,6 +8,13 @@
8
8
  * `__iterEl.querySelector(...)` 経由で initChild。
9
9
  * - `inner-loop-nested` — depth > 0 の `nestedComponents`。outer + inner
10
10
  * forEach の二重ループで initChild。
11
+ * - `component-rooted-inner-loop`
12
+ * — outer の loop item root が **child component**
13
+ * (`loop.childComponent`) で、その JSX children に
14
+ * component の nested `.map()` を持つケース (#1725)。
15
+ * element offset では fragment-root passthrough の
16
+ * flatten 済み items に届かないため、document order
17
+ * の zip (`qsaChildScopes` + cursor) で initChild。
11
18
  *
12
19
  * All decisions (selector, propsExpr, offset expressions) are resolved at
13
20
  * build time so the stringifier becomes a deterministic walk.
@@ -129,9 +136,48 @@ export interface InnerLoopNestedInitPlan {
129
136
  comps: readonly InnerLoopComp[]
130
137
  }
131
138
 
139
+ /**
140
+ * Plan for an inner `.map()` of components living inside a **component-rooted**
141
+ * outer loop item (#1725). The outer loop body is a single child component
142
+ * (`loop.childComponent`, e.g. a `SelectGroup` passthrough) whose JSX
143
+ * `children` contain a nested `.map()` of components (e.g. `SelectItem`).
144
+ *
145
+ * `inner-loop-nested` can't be reused here: it addresses inner components via
146
+ * `containerVar.children[outerOffset]`, which assumes the outer loop item is a
147
+ * DOM **element**. A fragment-rooted passthrough component (`<>{children}</>`)
148
+ * emits no wrapper element, so its rendered items are flattened directly under
149
+ * the parent container with no per-group element to index.
150
+ *
151
+ * Instead this shape zips the inner component scopes — found in document order
152
+ * via `qsaChildScopes(container, <selector>)` — against the flattened
153
+ * `outer.forEach(o => inner.forEach(i => ...))` iteration. Document order is
154
+ * the SSR render order, so position `__ci++` pairs each scope with its data
155
+ * item regardless of whether the outer component root is an element or a
156
+ * fragment.
157
+ */
158
+ export interface ComponentRootedInnerLoopInitPlan {
159
+ kind: 'component-rooted-inner-loop'
160
+ containerVar: string
161
+ /** Outer loop's array expression. */
162
+ outerArrayExpr: string
163
+ outerParam: string
164
+ /** Outer `.map()` callback preamble locals (#1064). */
165
+ outerPreludeStatements: PreludeStatements
166
+ /** Inner loop's array expression (references the outer param). */
167
+ innerArrayExpr: string
168
+ innerParam: string
169
+ /** Inner `.map()` callback preamble locals (#1064). */
170
+ innerPreludeStatements: PreludeStatements
171
+ /** Depth used in the leading comment line. */
172
+ depth: number
173
+ /** Per-component initialisers emitted inside the inner forEach body. */
174
+ comps: readonly InnerLoopComp[]
175
+ }
176
+
132
177
  export type StaticArrayChildInitPlan =
133
178
  | SingleCompInitPlan
134
179
  | OuterNestedInitPlan
135
180
  | InnerLoopNestedInitPlan
181
+ | ComponentRootedInnerLoopInitPlan
136
182
 
137
183
  export type StaticArrayChildInitsPlan = readonly StaticArrayChildInitPlan[]
@@ -50,6 +50,7 @@
50
50
  */
51
51
 
52
52
  import type {
53
+ ComponentRootedInnerLoopInitPlan,
53
54
  InnerLoopNestedInitPlan,
54
55
  OuterNestedInitPlan,
55
56
  SingleCompInitPlan,
@@ -78,6 +79,9 @@ function stringifyOne(lines: string[], plan: StaticArrayChildInitPlan): void {
78
79
  case 'inner-loop-nested':
79
80
  emitInnerLoopNested(lines, plan)
80
81
  break
82
+ case 'component-rooted-inner-loop':
83
+ emitComponentRootedInnerLoop(lines, plan)
84
+ break
81
85
  }
82
86
  }
83
87
 
@@ -174,3 +178,68 @@ function emitInnerLoopNested(lines: string[], plan: InnerLoopNestedInitPlan): vo
174
178
  lines.push(` }`)
175
179
  lines.push('')
176
180
  }
181
+
182
+ /**
183
+ * Emit shape for `component-rooted-inner-loop` (#1725):
184
+ *
185
+ * <i>// Initialize component-rooted inner-loop components (depth N)
186
+ * <i>if (<container>) {
187
+ * <i> const <scopes_c> = qsaChildScopes(<container>, <selector_c>) // per comp
188
+ * <i> let <cursor_c> = 0
189
+ * <i> <outerArr>.forEach((<outerParam>) => {
190
+ * <i> <outerPreludeStatements*>
191
+ * <i> <innerArr>.forEach((<innerParam>) => {
192
+ * <i> <innerPreludeStatements*>
193
+ * <i> const <compEl_c> = <scopes_c>[<cursor_c>++] // per comp
194
+ * <i> if (<compEl_c>) initChild('<name>', <compEl_c>, <props>)
195
+ * <i> })
196
+ * <i> })
197
+ * <i>}
198
+ *
199
+ * The scopes are queried once over the whole container and consumed in
200
+ * document order by a per-component cursor, so the SSR render order
201
+ * (outer-then-inner, depth-first) pairs each scope with its data item
202
+ * whether the outer component root is an element or a fragment.
203
+ */
204
+ function emitComponentRootedInnerLoop(lines: string[], plan: ComponentRootedInnerLoopInitPlan): void {
205
+ const {
206
+ containerVar,
207
+ outerArrayExpr,
208
+ outerParam,
209
+ outerPreludeStatements,
210
+ innerArrayExpr,
211
+ innerParam,
212
+ innerPreludeStatements,
213
+ depth,
214
+ comps,
215
+ } = plan
216
+ // A single comp uses the bare `__compScopes` / `__ci` names; multiple
217
+ // comps (e.g. `{items.map(it => <><A/><B/></>)}`) get index suffixes so
218
+ // each keeps its own document-order cursor.
219
+ const scopesVar = (i: number) => (comps.length > 1 ? `__compScopes${i}` : '__compScopes')
220
+ const cursorVar = (i: number) => (comps.length > 1 ? `__ci${i}` : '__ci')
221
+ const compElVar = (i: number) => (comps.length > 1 ? `__compEl${i}` : '__compEl')
222
+
223
+ lines.push(` // Initialize component-rooted inner-loop components (depth ${depth})`)
224
+ lines.push(` if (${containerVar}) {`)
225
+ comps.forEach((comp, i) => {
226
+ lines.push(` const ${scopesVar(i)} = qsaChildScopes(${containerVar}, ${comp.selector})`)
227
+ lines.push(` let ${cursorVar(i)} = 0`)
228
+ })
229
+ lines.push(` ${outerArrayExpr}.forEach((${outerParam}) => {`)
230
+ for (const stmt of outerPreludeStatements) {
231
+ lines.push(` ${stmt}`)
232
+ }
233
+ lines.push(` ${innerArrayExpr}.forEach((${innerParam}) => {`)
234
+ for (const stmt of innerPreludeStatements) {
235
+ lines.push(` ${stmt}`)
236
+ }
237
+ comps.forEach((comp, i) => {
238
+ lines.push(` const ${compElVar(i)} = ${scopesVar(i)}[${cursorVar(i)}++]`)
239
+ lines.push(` if (${compElVar(i)}) initChild('${nameForRegistryRef(comp.componentName)}', ${compElVar(i)}, ${comp.propsExpr})`)
240
+ })
241
+ lines.push(` })`)
242
+ lines.push(` })`)
243
+ lines.push(` }`)
244
+ lines.push('')
245
+ }
@@ -59,6 +59,16 @@ export interface ClientJsContext {
59
59
  loopElements: TopLevelLoop[]
60
60
  refElements: RefElement[]
61
61
  childInits: ChildInit[]
62
+ /**
63
+ * Slot ids of direct child components whose render is DEFERRED to init
64
+ * (dropped-prop fix). For these, the CSR registration template emits a
65
+ * `data-bf-ph` placeholder instead of `renderChild(...)`, and the init
66
+ * body uses `upsertChild` (→ `createComponent` with full getter props)
67
+ * instead of `initChild`. Computed in `generateInitFunction` once
68
+ * `unsafeLocalNames` is known, then read by the child-init phase and the
69
+ * registration-template emit so both agree on which children defer.
70
+ */
71
+ deferredChildSlots: Set<string>
62
72
  reactiveProps: ReactiveComponentProp[]
63
73
  reactiveChildProps: ReactiveChildProp[]
64
74
  reactiveAttrs: ReactiveAttribute[]