@barefootjs/jsx 0.6.0 → 0.7.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.
Files changed (38) hide show
  1. package/dist/adapters/parsed-expr-emitter.d.ts +14 -1
  2. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  3. package/dist/expression-parser.d.ts +137 -0
  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 +335 -5
  8. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts +4 -0
  10. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +46 -2
  12. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/types.d.ts +8 -1
  15. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  16. package/dist/types.d.ts +9 -0
  17. package/dist/types.d.ts.map +1 -1
  18. package/package.json +2 -2
  19. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +7 -7
  20. package/src/__tests__/child-components-in-map.test.ts +84 -0
  21. package/src/__tests__/client-js-generation.test.ts +51 -0
  22. package/src/__tests__/compiler-stress-1244.test.ts +43 -0
  23. package/src/__tests__/expression-parser.test.ts +109 -1
  24. package/src/__tests__/foreach-client-only.test.ts +80 -0
  25. package/src/__tests__/ir-async.test.ts +64 -0
  26. package/src/__tests__/ir-dynamic-tag.test.ts +104 -0
  27. package/src/__tests__/ir-reduce-op.test.ts +51 -0
  28. package/src/__tests__/reduce-op.test.ts +201 -0
  29. package/src/adapters/parsed-expr-emitter.ts +43 -1
  30. package/src/expression-parser.ts +570 -4
  31. package/src/index.ts +1 -1
  32. package/src/ir-to-client-js/collect-elements.ts +27 -4
  33. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +55 -1
  34. package/src/ir-to-client-js/plan/static-array-child-init.ts +47 -1
  35. package/src/ir-to-client-js/stringify/static-array-child-init.ts +69 -0
  36. package/src/ir-to-client-js/types.ts +8 -1
  37. package/src/jsx-to-ir.ts +69 -0
  38. package/src/types.ts +9 -0
@@ -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)
@@ -847,9 +853,26 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
847
853
  const elemRestName = ctx.restPropsName
848
854
  const elemPropsObjName = ctx.propsObjectName
849
855
  if (spreadVal && (spreadVal === elemRestName || spreadVal === elemPropsObjName)) {
850
- const excludeKeys = element.attrs
856
+ // `applyRestAttrs(_el, _p, exclude)` is handed the FULL props
857
+ // object (`PROPS_PARAM`), not a computed JS rest binding, and the
858
+ // runtime filters by SOURCE KEY (`source[key]`). So `exclude` must
859
+ // list every prop the component already consumed, keyed the way it
860
+ // arrives on `_p`. For the destructured `...rest` form that set is
861
+ // exactly the destructured param names (the JS rest-exclusion set):
862
+ // it covers every statically/reactively bound attr AND the
863
+ // separately-wired event/ref handlers. Without it, applyRestAttrs
864
+ // re-binds those events (double-fire) and re-emits consumed-but-
865
+ // unbound props under their raw key (e.g. `error` → `error="…"`,
866
+ // `describedBy` → `describedBy="…"`). The element-attr-name list
867
+ // alone was wrong on both counts — it keys on HTML attr names
868
+ // (`aria-invalid` ≠ source key `error`) and omits event handlers.
869
+ // (#1467)
870
+ const consumedKeys =
871
+ spreadVal === elemRestName ? ctx.propsParams.map(p => p.name) : []
872
+ const staticAttrKeys = element.attrs
851
873
  .filter(a => a.name !== '...')
852
874
  .map(a => a.name)
875
+ const excludeKeys = [...new Set([...consumedKeys, ...staticAttrKeys])]
853
876
  ctx.restAttrElements.push({
854
877
  slotId: element.slotId,
855
878
  source: PROPS_PARAM,
@@ -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
+ }
@@ -550,6 +550,13 @@ export interface RestAttrElement {
550
550
  slotId: string
551
551
  /** The spread source expression (e.g., 'rest', 'props') */
552
552
  source: string
553
- /** Attribute names already statically set on the element (exclude from applyRestAttrs) */
553
+ /**
554
+ * Prop SOURCE KEYS already consumed by the component (exclude from
555
+ * applyRestAttrs). For the destructured `...rest` form this is the
556
+ * destructured param names (the JS rest-exclusion set) unioned with any
557
+ * statically-set attr names; applyRestAttrs filters `source[key]` by these
558
+ * so it neither re-binds separately-wired events nor re-emits consumed
559
+ * props under their raw key. See collect-elements.ts for the rationale.
560
+ */
554
561
  excludeKeys: string[]
555
562
  }
package/src/jsx-to-ir.ts CHANGED
@@ -1050,6 +1050,7 @@ function transformComponentElement(
1050
1050
  children,
1051
1051
  template: name.toLowerCase(),
1052
1052
  slotId,
1053
+ ...(isDynamicTagLocal(name, ctx) ? { dynamicTag: true } : {}),
1053
1054
  loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
1054
1055
  }
1055
1056
  }
@@ -1079,6 +1080,7 @@ function transformSelfClosingComponent(
1079
1080
  children: [],
1080
1081
  template: name.toLowerCase(),
1081
1082
  slotId,
1083
+ ...(isDynamicTagLocal(name, ctx) ? { dynamicTag: true } : {}),
1082
1084
  loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
1083
1085
  }
1084
1086
  }
@@ -3959,6 +3961,73 @@ function findLocalConst(name: string, ctx: TransformContext) {
3959
3961
  return pool[pool.length - 1]
3960
3962
  }
3961
3963
 
3964
+ /**
3965
+ * Detect a PascalCase JSX tag that is really a *dynamic tag* local
3966
+ * (`const Tag = children.tag`) rather than a component reference.
3967
+ *
3968
+ * Such a "component" has no registrable template — the tag is chosen at
3969
+ * runtime. The Go template adapter consumes the resulting `dynamicTag`
3970
+ * flag to lower the node to a children passthrough so its dead branch
3971
+ * registers cleanly (Hono/CSR/Mojo ignore the flag).
3972
+ *
3973
+ * A name qualifies only when (a) a `const <name> = <expr>.tag` binding
3974
+ * exists somewhere in the source — at any nesting depth, since the
3975
+ * canonical pattern lives inside an `if (isValidElement(children)) {…}`
3976
+ * block and never reaches the analyzer's `localConstants` (which only
3977
+ * collects component-body-level bindings) — AND (b) the name is NOT a
3978
+ * JSX-producing local (those are tracked in the analyzer's jsx* /
3979
+ * inlineable sets). The `.tag` initializer check already excludes real
3980
+ * imported components (their binding is an import, not a `.tag` const)
3981
+ * and local component factories like `const Foo = () => <div/>` (an
3982
+ * arrow initializer, not a `.tag` access); the jsx* guards are a
3983
+ * belt-and-suspenders second line. Each set is guarded defensively in
3984
+ * case it is absent on a given analyzer context.
3985
+ */
3986
+ function isDynamicTagLocal(name: string, ctx: TransformContext): boolean {
3987
+ if (!hasDynamicTagBinding(name, ctx.sourceFile)) return false
3988
+ const a = ctx.analyzer
3989
+ if (a.jsxConstants?.has(name)) return false
3990
+ if (a.jsxFunctions?.has(name)) return false
3991
+ if (a.jsxMultiReturnFunctions?.has(name)) return false
3992
+ if (a.inlineableJsxConsts?.has(name)) return false
3993
+ return true
3994
+ }
3995
+
3996
+ /**
3997
+ * True when the source contains a `const <name> = <expr>.tag` (the
3998
+ * dynamic-tag pattern), at any nesting depth. The initializer may be
3999
+ * wrapped in an `as`/`satisfies`/parenthesized cast (`children.tag as any`).
4000
+ */
4001
+ function hasDynamicTagBinding(name: string, sourceFile: ts.SourceFile): boolean {
4002
+ let found = false
4003
+ const visit = (node: ts.Node): void => {
4004
+ if (found) return
4005
+ if (
4006
+ ts.isVariableDeclaration(node) &&
4007
+ ts.isIdentifier(node.name) &&
4008
+ node.name.text === name &&
4009
+ node.initializer
4010
+ ) {
4011
+ let init: ts.Expression = node.initializer
4012
+ while (
4013
+ ts.isAsExpression(init) ||
4014
+ ts.isSatisfiesExpression(init) ||
4015
+ ts.isParenthesizedExpression(init) ||
4016
+ ts.isNonNullExpression(init)
4017
+ ) {
4018
+ init = init.expression
4019
+ }
4020
+ if (ts.isPropertyAccessExpression(init) && init.name.text === 'tag') {
4021
+ found = true
4022
+ return
4023
+ }
4024
+ }
4025
+ ts.forEachChild(node, visit)
4026
+ }
4027
+ visit(sourceFile)
4028
+ return found
4029
+ }
4030
+
3962
4031
  /**
3963
4032
  * Resolve a `className={ident}` reference where `ident` is a local
3964
4033
  * const bound to a template literal — typically the cva-style pattern
package/src/types.ts CHANGED
@@ -674,6 +674,15 @@ export interface IRComponent {
674
674
  children: IRNode[]
675
675
  template: string // Reference to partial
676
676
  slotId: string | null // For components with event handlers
677
+ /**
678
+ * True when `name` is a dynamic-tag local (`const Tag = children.tag`)
679
+ * rather than a real component reference. Such a "component" has no
680
+ * registrable template — it is a runtime-chosen tag. Consumed ONLY by
681
+ * the Go template adapter, which lowers it to a children passthrough so
682
+ * the dead branch registers cleanly (Hono/CSR/Mojo ignore this flag).
683
+ * Omitted (undefined) for ordinary components to keep IR diffs minimal.
684
+ */
685
+ dynamicTag?: boolean
677
686
  loc: SourceLocation
678
687
  }
679
688