@@ -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