Skip to content

Commit 1a2a958

Browse files
committed
fix(vite-plugin): walk nested-conditional alternates into sub-templates
Component templates whose `template()` body chained multiple `if (cond) return <JSX>` guards lowered (via `foldEarlyReturnGuards`) to nested ternaries, but every alternate beyond the outermost arm was dropped: the IR walker only recognized JSX shapes, and the JS-emit `buildBranchFn` fell through to an empty-comment placeholder for non-JSX branches. Both sites now recognize `ConditionalExpression` / `&&` `LogicalExpression` arms with JSX-or-nullish operands, wrap them in a JSXFragment, and walk into a real sub-template — recursing through `buildBranchFn` for further nesting.
1 parent e392943 commit 1a2a958

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@geajs/vite-plugin': patch
3+
---
4+
5+
### @geajs/vite-plugin (patch)
6+
7+
- **Closure-codegen nested-conditional alternates**: A chain of `if (cond) return <JSX>` guards lowered by `foldEarlyReturnGuards` into nested ternaries previously dropped every inner alternate at the IR and JS-emit layers — the IR walker only recognized JSX shapes, and `buildBranchFn` fell through to an empty-comment placeholder for any non-JSX branch. Component templates that branched on multiple states then rendered only the outermost arm.
8+
- `closure-codegen/ir.ts`: extend `jsxNodeToTemplateIr` to recognize `ConditionalExpression` / `&&` `LogicalExpression` arms with JSX-or-nullish operands, wrap them in a JSXFragment, and walk into a real sub-template instead of returning `null`.
9+
- `closure-codegen/emit/emit-conditional.ts`: extend `buildBranchFn` with the same recognition — when a branch expression is a nestable conditional, lift it into a JSXFragment and route through `compileJsxToBlock` so the inner ternary becomes its own template + clone block (and recurses into `buildBranchFn` again for further nesting).

packages/vite-plugin-gea/src/closure-codegen/emit/emit-conditional.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { substituteBindings } from './emit-substitution.ts'
88
import { buildMapBranchFn } from './emit-map-branch.ts'
99
import type { Slot } from '../generator.ts'
1010
import { eagerTrackSkippedReads } from '../utils/path-helpers.ts'
11+
import { isJsxOrNullish } from '../generator/generator-jsx-helpers.ts'
1112

1213
export function emitConditionalSlot(slot: Slot, stmts: Statement[], ctx: EmitContext): void {
1314
const anchorId = t.identifier('anchor' + slot.index)
@@ -73,6 +74,18 @@ function buildBranchFn(branchExpr: any, ctx: EmitContext): Expression {
7374
const block = compileJsxToBlock(branchExpr, ctx)
7475
return t.arrowFunctionExpression([t.identifier('d')], block)
7576
}
77+
// Nested conditional alternates (`cond1 ? <A> : cond2 ? <B> : <C>`) or
78+
// `cond && <X>` chains — produced by `foldEarlyReturnGuards` from multiple
79+
// `if (...) return <JSX>` guards. Wrap in a JSXFragment with an expression
80+
// container so `compileJsxToBlock` routes them through the normal slot path
81+
// (which re-enters here recursively for each inner ternary level).
82+
if (isNestableConditionalExpression(branchExpr)) {
83+
const fragment = t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [
84+
t.jsxExpressionContainer(branchExpr as Expression),
85+
])
86+
const block = compileJsxToBlock(fragment, ctx)
87+
return t.arrowFunctionExpression([t.identifier('d')], block)
88+
}
7689
// Compiler-emitted hoisted-const IIFE (tagged by foldEarlyReturnGuards).
7790
if (
7891
t.isCallExpression(branchExpr) &&
@@ -147,6 +160,23 @@ function buildBranchFn(branchExpr: any, ctx: EmitContext): Expression {
147160
)
148161
}
149162

163+
// Mirrors `isWalkableConditionalExpression` from ir.ts (and the JSX walker's
164+
// own recognition): a ConditionalExpression or `&&` LogicalExpression that has
165+
// at least one JSX-or-nullish arm. These are the shapes `foldEarlyReturnGuards`
166+
// emits from chained `if (cond) return <JSX>` guards; if buildBranchFn doesn't
167+
// recognise them, every nested level beyond the outermost ternary collapses
168+
// to the "Generic fallback: empty comment" path above and silently drops the
169+
// JSX subtree.
170+
function isNestableConditionalExpression(node: unknown): boolean {
171+
if (t.isConditionalExpression(node)) {
172+
return isJsxOrNullish(node.consequent) || isJsxOrNullish(node.alternate)
173+
}
174+
if (t.isLogicalExpression(node) && node.operator === '&&') {
175+
return isJsxOrNullish(node.right)
176+
}
177+
return false
178+
}
179+
150180
/**
151181
* Build a branch fn for a bare `xs.map(item => <jsx/>)` expression. Creates a
152182
* `<span style="display:contents">` wrapper, inserts a comment anchor inside,

packages/vite-plugin-gea/src/closure-codegen/ir.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ClassDeclaration, Expression } from '@babel/types'
22
import { generate, t } from '../utils/babel-interop.ts'
33
import { walkJsxToTemplate, type Slot, type TemplateSpec } from './generator.ts'
4+
import { isJsxOrNullish } from './generator/generator-jsx-helpers.ts'
45
import { substituteBindings } from './emit/emit-substitution.ts'
56

67
export interface GeaIrBundleV1 {
@@ -424,10 +425,36 @@ function jsxNodeToTemplateIr(node: unknown, bindings: Map<string, Expression>):
424425
if (t.isJSXElement(inner) || t.isJSXFragment(inner)) {
425426
return templateSpecToIr(walkJsxToTemplate(inner), bindings)
426427
}
427-
}
428+
// Conditional/logical expressions don't materialise as JSX themselves but
429+
// are recognised by the JSX walker when they appear inside a JSXExpressionContainer.
430+
// Fall through to the wrapping path below so a nested ternary alternate
431+
// (e.g. the chain `foldEarlyReturnGuards` produces from multiple `if (cond)
432+
// return <X>` guards) still walks into a real sub-template instead of being
433+
// dropped.
434+
if (isWalkableConditionalExpression(inner)) return wrapAsFragmentTemplate(inner, bindings)
435+
}
436+
if (isWalkableConditionalExpression(node)) return wrapAsFragmentTemplate(node as any, bindings)
428437
return null
429438
}
430439

440+
// Mirrors the walker's recognition (walk.ts): `cond ? <A> : <B>`, `cond && <X>`,
441+
// `cond && xs.map(...)`. Anything else has no JSX shape we can lower into a
442+
// template branch.
443+
function isWalkableConditionalExpression(node: unknown): node is Expression {
444+
if (t.isConditionalExpression(node)) {
445+
return isJsxOrNullish(node.consequent) || isJsxOrNullish(node.alternate)
446+
}
447+
if (t.isLogicalExpression(node) && node.operator === '&&') {
448+
return isJsxOrNullish(node.right)
449+
}
450+
return false
451+
}
452+
453+
function wrapAsFragmentTemplate(expression: Expression, bindings: Map<string, Expression>): GeaIrTemplate {
454+
const fragment = t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [t.jsxExpressionContainer(expression)])
455+
return templateSpecToIr(walkJsxToTemplate(fragment), bindings)
456+
}
457+
431458
function jsxChildrenToTemplateIr(children: unknown, bindings: Map<string, Expression>): GeaIrTemplate | null {
432459
if (!Array.isArray(children) || children.length === 0) return null
433460
const hasContent = children.some(

0 commit comments

Comments
 (0)