@barefootjs/jsx 0.5.3 → 0.6.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.
@@ -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
  }
@@ -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[]