@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.
- package/dist/adapters/parsed-expr-emitter.d.ts +15 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +138 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +450 -23
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +30 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts +4 -0
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +46 -2
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +10 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/child-components-in-map.test.ts +84 -0
- package/src/__tests__/expression-parser.test.ts +276 -14
- package/src/__tests__/foreach-client-only.test.ts +80 -0
- package/src/__tests__/ir-reduce-op.test.ts +51 -0
- package/src/__tests__/ir-to-client-js/reactivity.test.ts +1 -0
- package/src/__tests__/reduce-op.test.ts +201 -0
- package/src/__tests__/staged-ir/06-multi-stage-soak.test.ts +18 -3
- package/src/adapters/parsed-expr-emitter.ts +50 -1
- package/src/expression-parser.ts +770 -21
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +9 -3
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/generate-init.ts +16 -1
- package/src/ir-to-client-js/html-template.ts +156 -2
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +12 -1
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +55 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +47 -1
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +69 -0
- 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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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()))
|
|
@@ -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('${
|
|
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
|
-
|
|
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
|
|
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[]
|