Skip to content

Commit cefff94

Browse files
committed
Fix icon codegen
1 parent 9c09dc8 commit cefff94

File tree

10 files changed

+2584
-196
lines changed

10 files changed

+2584
-196
lines changed

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

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,9 @@ exports[`registerCodegen should register codegen 1`] = `
88
"title": "Usage",
99
},
1010
{
11-
"code":
12-
"export function Test() {
13-
return <Box boxSize="100%" />
14-
}"
15-
,
11+
"code": "<Box boxSize="100%" />",
1612
"language": "TYPESCRIPT",
17-
"title": "Test - Components",
18-
},
19-
{
20-
"code":
21-
"mkdir -p src/components
22-
23-
echo 'import { Box } from \\'@devup-ui/react\\'
24-
25-
export function Test() {
26-
return <Box boxSize="100%" />
27-
}' > src/components/Test.tsx"
28-
,
29-
"language": "BASH",
30-
"title": "Test - Components CLI (Bash)",
31-
},
32-
{
33-
"code":
34-
"New-Item -ItemType Directory -Force -Path src\\components | Out-Null
35-
36-
@'
37-
import { Box } from '@devup-ui/react'
38-
39-
export function Test() {
40-
return <Box boxSize="100%" />
41-
}
42-
'@ | Out-File -FilePath src\\components\\Test.tsx -Encoding UTF8"
43-
,
44-
"language": "BASH",
45-
"title": "Test - Components CLI (PowerShell)",
13+
"title": "Test",
4614
},
4715
]
4816
`;

src/__tests__/code.test.ts

Lines changed: 225 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ describe('registerCodegen with viewport variant', () => {
409409
typeof r === 'object' &&
410410
r !== null &&
411411
'title' in r &&
412-
(r as { title: string }).title.includes('Responsive'),
412+
(r as { title: string }).title.endsWith('- Components'),
413413
)
414414
expect(responsiveResult).toBeDefined()
415415
})
@@ -494,7 +494,7 @@ describe('registerCodegen with viewport variant', () => {
494494
typeof r === 'object' &&
495495
r !== null &&
496496
'title' in r &&
497-
(r as { title: string }).title.includes('Responsive'),
497+
(r as { title: string }).title.endsWith('- Components'),
498498
)
499499
expect(responsiveResult).toBeDefined()
500500

@@ -505,6 +505,118 @@ describe('registerCodegen with viewport variant', () => {
505505
}
506506
})
507507

508+
it('should generate responsive component with multiple non-viewport variants (size + varient)', async () => {
509+
let capturedHandler: CodegenHandler | null = null
510+
511+
const figmaMock = {
512+
editorType: 'dev',
513+
mode: 'codegen',
514+
command: 'noop',
515+
codegen: {
516+
on: (_event: string, handler: CodegenHandler) => {
517+
capturedHandler = handler
518+
},
519+
},
520+
closePlugin: mock(() => {}),
521+
} as unknown as typeof figma
522+
523+
codeModule.registerCodegen(figmaMock)
524+
525+
expect(capturedHandler).not.toBeNull()
526+
if (capturedHandler === null) throw new Error('Handler not captured')
527+
528+
// COMPONENT_SET with two non-viewport variants: size and varient
529+
const componentSetNode = {
530+
type: 'COMPONENT_SET',
531+
name: 'MyButton',
532+
visible: true,
533+
componentPropertyDefinitions: {
534+
size: {
535+
type: 'VARIANT',
536+
defaultValue: 'md',
537+
variantOptions: ['sm', 'md'],
538+
},
539+
varient: {
540+
type: 'VARIANT',
541+
defaultValue: 'primary',
542+
variantOptions: ['primary', 'white'],
543+
},
544+
},
545+
children: [
546+
{
547+
type: 'COMPONENT',
548+
name: 'size=sm, varient=primary',
549+
visible: true,
550+
variantProperties: { size: 'sm', varient: 'primary' },
551+
children: [],
552+
layoutMode: 'HORIZONTAL',
553+
width: 100,
554+
height: 40,
555+
},
556+
{
557+
type: 'COMPONENT',
558+
name: 'size=md, varient=primary',
559+
visible: true,
560+
variantProperties: { size: 'md', varient: 'primary' },
561+
children: [],
562+
layoutMode: 'HORIZONTAL',
563+
width: 200,
564+
height: 50,
565+
},
566+
{
567+
type: 'COMPONENT',
568+
name: 'size=sm, varient=white',
569+
visible: true,
570+
variantProperties: { size: 'sm', varient: 'white' },
571+
children: [],
572+
layoutMode: 'HORIZONTAL',
573+
width: 100,
574+
height: 40,
575+
},
576+
{
577+
type: 'COMPONENT',
578+
name: 'size=md, varient=white',
579+
visible: true,
580+
variantProperties: { size: 'md', varient: 'white' },
581+
children: [],
582+
layoutMode: 'HORIZONTAL',
583+
width: 200,
584+
height: 50,
585+
},
586+
],
587+
defaultVariant: {
588+
type: 'COMPONENT',
589+
name: 'size=md, varient=primary',
590+
visible: true,
591+
variantProperties: { size: 'md', varient: 'primary' },
592+
children: [],
593+
},
594+
} as unknown as SceneNode
595+
596+
const handler = capturedHandler as CodegenHandler
597+
const result = await handler({
598+
node: componentSetNode,
599+
language: 'devup-ui',
600+
})
601+
602+
// Should include responsive components result
603+
const responsiveResult = result.find(
604+
(r: unknown) =>
605+
typeof r === 'object' &&
606+
r !== null &&
607+
'title' in r &&
608+
(r as { title: string }).title.endsWith('- Components'),
609+
)
610+
expect(responsiveResult).toBeDefined()
611+
612+
// The generated code should include BOTH variant keys in the interface
613+
const resultWithCode = responsiveResult as { code: string } | undefined
614+
if (resultWithCode?.code) {
615+
expect(resultWithCode.code).toContain('size')
616+
expect(resultWithCode.code).toContain('varient')
617+
}
618+
})
619+
508620
it('should generate responsive code for node with parent SECTION', async () => {
509621
let capturedHandler: CodegenHandler | null = null
510622

@@ -576,7 +688,7 @@ describe('registerCodegen with viewport variant', () => {
576688
typeof r === 'object' &&
577689
r !== null &&
578690
'title' in r &&
579-
(r as { title: string }).title.includes('Responsive'),
691+
(r as { title: string }).title.endsWith('- Responsive'),
580692
)
581693
expect(responsiveResult).toBeDefined()
582694
})
@@ -601,6 +713,33 @@ describe('registerCodegen with viewport variant', () => {
601713
expect(capturedHandler).not.toBeNull()
602714
if (capturedHandler === null) throw new Error('Handler not captured')
603715

716+
// Create a nested custom component (NestedIcon) that CustomButton references.
717+
// When CustomButton's code renders <NestedIcon />, generateImportStatements
718+
// will extract it as a custom import — covering the customImports loop.
719+
const nestedIconComponent = {
720+
type: 'COMPONENT',
721+
name: 'NestedIcon',
722+
visible: true,
723+
children: [],
724+
width: 16,
725+
height: 16,
726+
layoutMode: 'NONE',
727+
componentPropertyDefinitions: {},
728+
variantProperties: {} as Record<string, string>,
729+
reactions: [],
730+
parent: null,
731+
}
732+
733+
// INSTANCE of NestedIcon, placed inside CustomButton variants
734+
const nestedIconInstance = {
735+
type: 'INSTANCE',
736+
name: 'NestedIcon',
737+
visible: true,
738+
width: 16,
739+
height: 16,
740+
getMainComponentAsync: async () => nestedIconComponent,
741+
}
742+
604743
// Create a custom component that will be referenced
605744
const customComponent = {
606745
type: 'COMPONENT',
@@ -611,6 +750,7 @@ describe('registerCodegen with viewport variant', () => {
611750
height: 40,
612751
layoutMode: 'NONE',
613752
componentPropertyDefinitions: {},
753+
variantProperties: {} as Record<string, string>,
614754
parent: null,
615755
}
616756

@@ -624,36 +764,93 @@ describe('registerCodegen with viewport variant', () => {
624764
getMainComponentAsync: async () => customComponent,
625765
}
626766

627-
// Create a COMPONENT that contains the INSTANCE
628-
const componentNode = {
767+
// Create COMPONENT variants that the instance references.
768+
// Each variant contains a NestedIcon INSTANCE child — this causes
769+
// the generated component code to include <NestedIcon />.
770+
const componentVariant1 = {
629771
type: 'COMPONENT',
630-
name: 'MyComponent',
772+
name: 'CustomButton',
631773
visible: true,
632-
children: [instanceNode],
633-
width: 200,
634-
height: 100,
635-
layoutMode: 'VERTICAL',
774+
children: [
775+
{
776+
...nestedIconInstance,
777+
name: 'NestedIcon',
778+
parent: null as unknown,
779+
},
780+
],
781+
width: 100,
782+
height: 40,
783+
layoutMode: 'HORIZONTAL',
636784
componentPropertyDefinitions: {},
637785
reactions: [],
786+
variantProperties: { size: 'md' },
638787
parent: null,
639-
} as unknown as SceneNode
788+
}
640789

641-
// Create COMPONENT_SET parent with proper children array
790+
const componentVariant2 = {
791+
type: 'COMPONENT',
792+
name: 'CustomButton',
793+
visible: true,
794+
children: [
795+
{
796+
...nestedIconInstance,
797+
name: 'NestedIcon',
798+
parent: null as unknown,
799+
},
800+
],
801+
width: 100,
802+
height: 40,
803+
layoutMode: 'HORIZONTAL',
804+
componentPropertyDefinitions: {},
805+
reactions: [],
806+
variantProperties: { size: 'lg' },
807+
parent: null,
808+
}
809+
810+
// Create COMPONENT_SET parent with a variant key so Components tab is generated
642811
const componentSetNode = {
643812
type: 'COMPONENT_SET',
644-
name: 'MyComponentSet',
645-
componentPropertyDefinitions: {},
646-
children: [componentNode],
647-
defaultVariant: componentNode,
813+
name: 'CustomButton',
814+
componentPropertyDefinitions: {
815+
size: {
816+
type: 'VARIANT',
817+
variantOptions: ['md', 'lg'],
818+
},
819+
},
820+
children: [componentVariant1, componentVariant2],
821+
defaultVariant: componentVariant1,
648822
reactions: [],
649823
}
650824

651-
// Set parent reference
652-
;(componentNode as { parent: unknown }).parent = componentSetNode
825+
// Set parent references
826+
;(componentVariant1 as { parent: unknown }).parent = componentSetNode
827+
;(componentVariant2 as { parent: unknown }).parent = componentSetNode
828+
for (const variant of [componentVariant1, componentVariant2]) {
829+
for (const child of variant.children) {
830+
;(child as { parent: unknown }).parent = variant
831+
}
832+
}
833+
;(customComponent as { parent: unknown }).parent = componentSetNode
834+
;(customComponent as { variantProperties: unknown }).variantProperties = {
835+
size: 'md',
836+
}
837+
838+
// Create a FRAME that contains the INSTANCE (not a COMPONENT)
839+
const frameNode = {
840+
type: 'FRAME',
841+
name: 'MyFrame',
842+
visible: true,
843+
children: [instanceNode],
844+
width: 200,
845+
height: 100,
846+
layoutMode: 'VERTICAL',
847+
reactions: [],
848+
parent: null,
849+
} as unknown as SceneNode
653850

654851
const handler = capturedHandler as CodegenHandler
655852
const result = await handler({
656-
node: componentNode,
853+
node: frameNode,
657854
language: 'devup-ui',
658855
})
659856

@@ -676,19 +873,17 @@ describe('registerCodegen with viewport variant', () => {
676873
expect(bashCLI).toBeDefined()
677874
expect(powershellCLI).toBeDefined()
678875

679-
// Check that custom component import is included (bash escapes quotes)
876+
// Check that custom component file is included in CLI output
680877
const bashCode = (bashCLI as { code: string } | undefined)?.code
681878
const powershellCode = (powershellCLI as { code: string } | undefined)?.code
682879

683880
if (bashCode) {
684-
expect(bashCode).toContain(
685-
"import { CustomButton } from \\'@/components/CustomButton\\'",
686-
)
881+
expect(bashCode).toContain('CustomButton')
882+
expect(bashCode).toContain('src/components/CustomButton.tsx')
687883
}
688884
if (powershellCode) {
689-
expect(powershellCode).toContain(
690-
"import { CustomButton } from '@/components/CustomButton'",
691-
)
885+
expect(powershellCode).toContain('CustomButton')
886+
expect(powershellCode).toContain('src\\components\\CustomButton.tsx')
692887
}
693888
})
694889

@@ -793,18 +988,17 @@ describe('registerCodegen with viewport variant', () => {
793988
typeof r === 'object' &&
794989
r !== null &&
795990
'title' in r &&
796-
(r as { title: string }).title === 'MyFrame - Components Responsive',
991+
(r as { title: string }).title === 'MyFrame - Components',
797992
)
798993
expect(responsiveResult).toBeDefined()
799994

800-
// Should also include CLI results for Components Responsive
995+
// Should also include CLI results for Components
801996
const bashCLI = result.find(
802997
(r: unknown) =>
803998
typeof r === 'object' &&
804999
r !== null &&
8051000
'title' in r &&
806-
(r as { title: string }).title ===
807-
'MyFrame - Components Responsive CLI (Bash)',
1001+
(r as { title: string }).title === 'MyFrame - Components CLI (Bash)',
8081002
)
8091003
expect(bashCLI).toBeDefined()
8101004

@@ -814,7 +1008,7 @@ describe('registerCodegen with viewport variant', () => {
8141008
r !== null &&
8151009
'title' in r &&
8161010
(r as { title: string }).title ===
817-
'MyFrame - Components Responsive CLI (PowerShell)',
1011+
'MyFrame - Components CLI (PowerShell)',
8181012
)
8191013
expect(powershellCLI).toBeDefined()
8201014
})

0 commit comments

Comments
 (0)