Skip to content

Commit 9007ada

Browse files
committed
Fix boolean props issue
1 parent 4d4031e commit 9007ada

5 files changed

Lines changed: 282 additions & 13 deletions

File tree

src/codegen/Codegen.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,49 @@ function getTextPropName(
115115
return undefined
116116
}
117117

118+
/**
119+
* Recursively apply BOOLEAN conditions and TEXT bindings to nested descendants.
120+
* buildTree produces the tree structure but doesn't check BOOLEAN/TEXT bindings —
121+
* those are only available via componentPropertyReferences on the original Figma nodes.
122+
* This walks the tree and Figma node tree in parallel, applying conditions at every level.
123+
*/
124+
function applyNestedConditions(
125+
tree: NodeTree,
126+
node: SceneNode,
127+
booleanSlots: Map<string, string>,
128+
textSlots: Map<string, string>,
129+
): void {
130+
// Skip component references and wrappers — their children aren't expanded in the tree
131+
if (tree.isComponent || tree.nodeType === 'WRAPPER') return
132+
if (!('children' in node) || tree.children.length === 0) return
133+
134+
const figmaChildren = (node as SceneNode & ChildrenMixin).children
135+
const len = Math.min(figmaChildren.length, tree.children.length)
136+
137+
for (let i = 0; i < len; i++) {
138+
const childTree = tree.children[i]
139+
const childNode = figmaChildren[i]
140+
141+
if (!childTree.condition) {
142+
const conditionName = getBooleanConditionName(childNode, booleanSlots)
143+
if (conditionName) {
144+
childTree.condition = conditionName
145+
}
146+
}
147+
148+
const textPropName = getTextPropName(childNode, textSlots)
149+
if (
150+
textPropName &&
151+
childTree.textChildren &&
152+
!childTree.textChildren[0]?.startsWith('{')
153+
) {
154+
childTree.textChildren = [`{${textPropName}}`]
155+
}
156+
157+
applyNestedConditions(childTree, childNode, booleanSlots, textSlots)
158+
}
159+
}
160+
118161
/**
119162
* Shallow-clone a NodeTree — creates a new object so that per-instance
120163
* property reassignment (e.g., `tree.props = { ...tree.props, ...selectorProps }`)
@@ -485,6 +528,22 @@ export class Codegen {
485528
): Promise<void> {
486529
const tAdd = perfStart()
487530

531+
// Reserve position in componentTrees so parent components appear
532+
// before their children in Map iteration order.
533+
// Map.set() with the same key later updates the value without changing position.
534+
this.componentTrees.set(nodeId, {
535+
name: getComponentName(node),
536+
node,
537+
tree: {
538+
component: '',
539+
props: {},
540+
children: [],
541+
nodeType: node.type,
542+
nodeName: node.name,
543+
},
544+
variants: {},
545+
})
546+
488547
// Fire getProps + getSelectorProps early (2 independent API calls)
489548
const propsPromise = getProps(node)
490549
const t = perfStart()
@@ -540,6 +599,8 @@ export class Codegen {
540599
if (textPropName && tree.textChildren) {
541600
tree.textChildren = [`{${textPropName}}`]
542601
}
602+
// Apply conditions to nested descendants (grandchildren and deeper)
603+
applyNestedConditions(tree, child, booleanSlots, textSlots)
543604
childrenTrees.push(tree)
544605
}
545606
}

src/codegen/__tests__/__snapshots__/codegen.test.ts.snap

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,12 +1250,6 @@ exports[`Codegen renders component with parent component set name: component: Ho
12501250
}
12511251
`;
12521252

1253-
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with INSTANCE_SWAP slot: INSTANCE_SWAP slot: Icon 1`] = `
1254-
"export function Icon() {
1255-
return <Box boxSize="100%" />
1256-
}"
1257-
`;
1258-
12591253
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with INSTANCE_SWAP slot: INSTANCE_SWAP slot: IconButton 1`] = `
12601254
"export interface IconButtonProps {
12611255
size: 'sm' | 'md' | 'lg'
@@ -1284,6 +1278,12 @@ export function IconButton({ size, leftIcon }: IconButtonProps) {
12841278
}"
12851279
`;
12861280

1281+
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with INSTANCE_SWAP slot: INSTANCE_SWAP slot: Icon 1`] = `
1282+
"export function Icon() {
1283+
return <Box boxSize="100%" />
1284+
}"
1285+
`;
1286+
12871287
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with BOOLEAN conditional: BOOLEAN conditional: AlertCard 1`] = `
12881288
"export interface AlertCardProps {
12891289
State: 'Default' | 'Hover'
@@ -1312,8 +1312,26 @@ export function AlertCard({ State, showBadge }: AlertCardProps) {
13121312
}"
13131313
`;
13141314

1315-
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with INSTANCE_SWAP + BOOLEAN combined: INSTANCE_SWAP+BOOLEAN: Icon 1`] = `
1316-
"export function Icon() {
1315+
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with nested BOOLEAN conditionals (grandchildren): nested BOOLEAN conditional: CurriculumCard 1`] = `
1316+
"export interface CurriculumCardProps {
1317+
_1st?: boolean
1318+
_2nd?: boolean
1319+
}
1320+
1321+
export function CurriculumCard({ _1st, _2nd }: CurriculumCardProps) {
1322+
return (
1323+
<Box>
1324+
<Box boxSize="100%">
1325+
{_1st && <CurriculumSubTitle />}
1326+
{_2nd && <CurriculumSubTitle />}
1327+
</Box>
1328+
</Box>
1329+
)
1330+
}"
1331+
`;
1332+
1333+
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with nested BOOLEAN conditionals (grandchildren): nested BOOLEAN conditional: CurriculumSubTitle 1`] = `
1334+
"export function CurriculumSubTitle() {
13171335
return <Box boxSize="100%" />
13181336
}"
13191337
`;
@@ -1350,6 +1368,12 @@ export function Button({ size, leftIcon, showLeftIcon, rightIcon, showRightIcon
13501368
}"
13511369
`;
13521370

1371+
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with INSTANCE_SWAP + BOOLEAN combined: INSTANCE_SWAP+BOOLEAN: Icon 1`] = `
1372+
"export function Icon() {
1373+
return <Box boxSize="100%" />
1374+
}"
1375+
`;
1376+
13531377
exports[`Codegen Tree Methods INSTANCE_SWAP / BOOLEAN snapshot renders component with INSTANCE_SWAP + BOOLEAN combined: INSTANCE_SWAP+BOOLEAN: ArrowIcon 1`] = `
13541378
"export function ArrowIcon() {
13551379
return <Box boxSize="100%" />

src/codegen/__tests__/codegen.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3759,6 +3759,96 @@ describe('Codegen Tree Methods', () => {
37593759
expect(condChild?.condition).toBe('showIcon')
37603760
})
37613761

3762+
test('detects BOOLEAN conditions on nested grandchildren (not just direct children)', async () => {
3763+
// Grandchild INSTANCE nodes with componentPropertyReferences.visible
3764+
// These are inside a FRAME wrapper, not direct children of the COMPONENT
3765+
const subTitle1 = {
3766+
type: 'INSTANCE',
3767+
name: 'CurriculumSubTitle1',
3768+
visible: true,
3769+
componentPropertyReferences: { visible: '_1st#80:1' },
3770+
getMainComponentAsync: async () =>
3771+
({
3772+
type: 'COMPONENT',
3773+
name: 'CurriculumSubTitle',
3774+
children: [],
3775+
visible: true,
3776+
}) as unknown as ComponentNode,
3777+
} as unknown as InstanceNode
3778+
3779+
const subTitle2 = {
3780+
type: 'INSTANCE',
3781+
name: 'CurriculumSubTitle2',
3782+
visible: true,
3783+
componentPropertyReferences: { visible: '_2nd#80:2' },
3784+
getMainComponentAsync: async () =>
3785+
({
3786+
type: 'COMPONENT',
3787+
name: 'CurriculumSubTitle',
3788+
children: [],
3789+
visible: true,
3790+
}) as unknown as ComponentNode,
3791+
} as unknown as InstanceNode
3792+
3793+
// Intermediate wrapper frame — this is the direct child of the COMPONENT
3794+
const contentWrapper = {
3795+
type: 'FRAME',
3796+
name: 'ContentWrapper',
3797+
children: [subTitle1, subTitle2],
3798+
visible: true,
3799+
strokes: [],
3800+
effects: [],
3801+
reactions: [],
3802+
} as unknown as FrameNode
3803+
3804+
const defaultVariant = {
3805+
type: 'COMPONENT',
3806+
name: 'State=Default',
3807+
children: [contentWrapper],
3808+
visible: true,
3809+
reactions: [],
3810+
} as unknown as ComponentNode
3811+
3812+
const node = {
3813+
type: 'COMPONENT_SET',
3814+
name: 'CurriculumCard',
3815+
children: [defaultVariant],
3816+
defaultVariant,
3817+
visible: true,
3818+
componentPropertyDefinitions: {
3819+
'_1st#80:1': {
3820+
type: 'BOOLEAN',
3821+
defaultValue: true,
3822+
},
3823+
'_2nd#80:2': {
3824+
type: 'BOOLEAN',
3825+
defaultValue: true,
3826+
},
3827+
},
3828+
} as unknown as ComponentSetNode
3829+
addParent(node)
3830+
3831+
const codegen = new Codegen(node)
3832+
await codegen.buildTree()
3833+
3834+
const componentTrees = codegen.getComponentTrees()
3835+
const compTree = [...componentTrees.values()].find(
3836+
(ct) => ct.name === 'CurriculumCard',
3837+
)
3838+
expect(compTree).toBeDefined()
3839+
3840+
// The direct child (ContentWrapper) should NOT have a condition
3841+
const wrapper = compTree?.tree.children[0]
3842+
expect(wrapper?.condition).toBeUndefined()
3843+
3844+
// The grandchildren (inside ContentWrapper) SHOULD have conditions
3845+
const grandchildren = wrapper?.children ?? []
3846+
const cond1 = grandchildren.find((c) => c.condition === '_1st')
3847+
const cond2 = grandchildren.find((c) => c.condition === '_2nd')
3848+
expect(cond1).toBeDefined()
3849+
expect(cond2).toBeDefined()
3850+
})
3851+
37623852
test('detects combined INSTANCE_SWAP + BOOLEAN slot with condition', async () => {
37633853
const iconChild = {
37643854
type: 'INSTANCE',
@@ -4196,6 +4286,80 @@ describe('Codegen Tree Methods', () => {
41964286
}
41974287
})
41984288

4289+
test('renders component with nested BOOLEAN conditionals (grandchildren)', async () => {
4290+
const subTitle1 = {
4291+
type: 'INSTANCE',
4292+
name: 'CurriculumSubTitle1',
4293+
visible: true,
4294+
componentPropertyReferences: { visible: '_1st#80:1' },
4295+
getMainComponentAsync: async () =>
4296+
({
4297+
type: 'COMPONENT',
4298+
name: 'CurriculumSubTitle',
4299+
children: [],
4300+
visible: true,
4301+
}) as unknown as ComponentNode,
4302+
} as unknown as InstanceNode
4303+
4304+
const subTitle2 = {
4305+
type: 'INSTANCE',
4306+
name: 'CurriculumSubTitle2',
4307+
visible: true,
4308+
componentPropertyReferences: { visible: '_2nd#80:2' },
4309+
getMainComponentAsync: async () =>
4310+
({
4311+
type: 'COMPONENT',
4312+
name: 'CurriculumSubTitle',
4313+
children: [],
4314+
visible: true,
4315+
}) as unknown as ComponentNode,
4316+
} as unknown as InstanceNode
4317+
4318+
const contentWrapper = {
4319+
type: 'FRAME',
4320+
name: 'ContentWrapper',
4321+
children: [subTitle1, subTitle2],
4322+
visible: true,
4323+
strokes: [],
4324+
effects: [],
4325+
reactions: [],
4326+
} as unknown as FrameNode
4327+
4328+
const defaultVariant = {
4329+
type: 'COMPONENT',
4330+
name: 'State=Default',
4331+
children: [contentWrapper],
4332+
visible: true,
4333+
reactions: [],
4334+
} as unknown as ComponentNode
4335+
4336+
const node = {
4337+
type: 'COMPONENT_SET',
4338+
name: 'CurriculumCard',
4339+
children: [defaultVariant],
4340+
defaultVariant,
4341+
visible: true,
4342+
componentPropertyDefinitions: {
4343+
'_1st#80:1': {
4344+
type: 'BOOLEAN',
4345+
defaultValue: true,
4346+
},
4347+
'_2nd#80:2': {
4348+
type: 'BOOLEAN',
4349+
defaultValue: true,
4350+
},
4351+
},
4352+
} as unknown as ComponentSetNode
4353+
addParent(node)
4354+
4355+
const codegen = new Codegen(node)
4356+
await codegen.run()
4357+
4358+
for (const [name, code] of codegen.getComponentsCodes()) {
4359+
expect(code).toMatchSnapshot(`nested BOOLEAN conditional: ${name}`)
4360+
}
4361+
})
4362+
41994363
test('renders component with INSTANCE_SWAP + BOOLEAN combined', async () => {
42004364
const leftIconChild = {
42014365
type: 'INSTANCE',

src/codegen/utils/__tests__/extract-instance-variant-props.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('extractInstanceVariantProps', () => {
2020
})
2121
})
2222

23-
test('ignores non-VARIANT type props', () => {
23+
test('extracts BOOLEAN type props when value is true', () => {
2424
const node = {
2525
componentProperties: {
2626
'status#123:456': { type: 'VARIANT', value: 'active' },
@@ -31,6 +31,23 @@ describe('extractInstanceVariantProps', () => {
3131

3232
const result = extractInstanceVariantProps(node)
3333

34+
expect(result).toEqual({
35+
status: 'active',
36+
visible: true,
37+
})
38+
})
39+
40+
test('ignores BOOLEAN type props when value is false', () => {
41+
const node = {
42+
componentProperties: {
43+
'status#123:456': { type: 'VARIANT', value: 'active' },
44+
'hidden#345:678': { type: 'BOOLEAN', value: false },
45+
'label#789:012': { type: 'TEXT', value: 'Click me' },
46+
},
47+
} as unknown as InstanceNode
48+
49+
const result = extractInstanceVariantProps(node)
50+
3451
expect(result).toEqual({
3552
status: 'active',
3653
})

src/codegen/utils/extract-instance-variant-props.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@ export function isReservedVariantKey(key: string): boolean {
2727
*/
2828
export function extractInstanceVariantProps(
2929
node: InstanceNode,
30-
): Record<string, string> {
31-
const variantProps: Record<string, string> = {}
30+
): Record<string, unknown> {
31+
const variantProps: Record<string, unknown> = {}
3232

3333
if (!node.componentProperties) {
3434
return variantProps
3535
}
3636

3737
for (const [key, prop] of Object.entries(node.componentProperties)) {
38-
if (prop.type === 'VARIANT' && !isReservedVariantKey(key)) {
39-
const sanitizedKey = sanitizePropertyName(key)
38+
if (isReservedVariantKey(key)) continue
39+
const sanitizedKey = sanitizePropertyName(key)
40+
if (prop.type === 'VARIANT') {
4041
variantProps[sanitizedKey] = String(prop.value)
42+
} else if (prop.type === 'BOOLEAN' && prop.value === true) {
43+
variantProps[sanitizedKey] = true
4144
}
4245
}
4346

0 commit comments

Comments
 (0)