@@ -152,6 +152,115 @@ describe('export-devup length and shadow coverage', () => {
152152 ] )
153153 } )
154154
155+ test ( 'treeshaking scans bound variables and includes library FLOAT vars' , async ( ) => {
156+ const variablesById : Record < string , Variable | null > = {
157+ c1 : {
158+ id : 'c1' ,
159+ name : 'Primary' ,
160+ resolvedType : 'COLOR' ,
161+ valuesByMode : { mLight : { r : 1 , g : 0 , b : 0 , a : 1 } } ,
162+ } as unknown as Variable ,
163+ localFloat : {
164+ id : 'localFloat' ,
165+ name : 'spacing/sm' ,
166+ resolvedType : 'FLOAT' ,
167+ valuesByMode : { mMob : 8 , mDesk : 16 } ,
168+ variableCollectionId : 'dimColl' ,
169+ } as unknown as Variable ,
170+ libFloat : {
171+ id : 'libFloat' ,
172+ name : 'spacing/spacing08' ,
173+ resolvedType : 'FLOAT' ,
174+ valuesByMode : { lMob : 4 , lDesk : 12 } ,
175+ variableCollectionId : 'libColl' ,
176+ } as unknown as Variable ,
177+ colorVar : {
178+ id : 'colorVar' ,
179+ name : 'accent' ,
180+ resolvedType : 'COLOR' ,
181+ valuesByMode : { mLight : { r : 0 , g : 0 , b : 1 , a : 1 } } ,
182+ variableCollectionId : 'colorColl' ,
183+ } as unknown as Variable ,
184+ }
185+
186+ const collectionsById : Record < string , VariableCollection | null > = {
187+ dimColl : {
188+ id : 'dimColl' ,
189+ name : 'Dimensions' ,
190+ modes : [
191+ { modeId : 'mMob' , name : 'mobile' } ,
192+ { modeId : 'mDesk' , name : 'desktop' } ,
193+ ] ,
194+ } as unknown as VariableCollection ,
195+ libColl : {
196+ id : 'libColl' ,
197+ name : 'Library Dimensions' ,
198+ modes : [
199+ { modeId : 'lMob' , name : 'mobile' } ,
200+ { modeId : 'lDesk' , name : 'desktop' } ,
201+ ] ,
202+ } as unknown as VariableCollection ,
203+ }
204+
205+ const frameNode = {
206+ type : 'FRAME' ,
207+ boundVariables : {
208+ paddingLeft : { id : 'localFloat' } ,
209+ paddingRight : { id : 'libFloat' } ,
210+ fills : [ { id : 'colorVar' } ] ,
211+ } ,
212+ findAllWithCriteria : ( ) => [ ] ,
213+ children : [ ] ,
214+ }
215+
216+ ; ( globalThis as { figma ?: unknown } ) . figma = {
217+ util : { rgba : ( v : unknown ) => v } ,
218+ skipInvisibleInstanceChildren : false ,
219+ currentPage : {
220+ id : 'page1' ,
221+ children : [ frameNode ] ,
222+ } ,
223+ variables : {
224+ getVariableByIdAsync : async ( id : string ) => variablesById [ id ] ?? null ,
225+ getVariableCollectionByIdAsync : async ( id : string ) =>
226+ collectionsById [ id ] ?? null ,
227+ getLocalVariableCollectionsAsync : async ( ) => [
228+ {
229+ variableIds : [ 'c1' ] ,
230+ modes : [ { modeId : 'mLight' , name : 'Light' } ] ,
231+ } ,
232+ ] ,
233+ } ,
234+ getLocalTextStylesAsync : async ( ) => [ ] ,
235+ getLocalEffectStylesAsync : async ( ) => [ ] ,
236+ root : {
237+ children : [ { id : 'page1' , children : [ frameNode ] } ] ,
238+ } ,
239+ mixed : Symbol ( 'mixed' ) ,
240+ } as unknown as typeof figma
241+
242+ const devup = await buildDevupConfig ( true )
243+
244+ // Local FLOAT from bound variable
245+ expect ( devup . theme ?. length ?. light ?. spacingSm ) . toEqual ( [
246+ '8px' ,
247+ null ,
248+ null ,
249+ null ,
250+ '16px' ,
251+ ] )
252+ // Library FLOAT from bound variable — previously not exported
253+ expect ( devup . theme ?. length ?. light ?. spacingSpacing08 ) . toEqual ( [
254+ '4px' ,
255+ null ,
256+ null ,
257+ null ,
258+ '12px' ,
259+ ] )
260+ // COLOR bound variables should NOT be in length
261+ expect ( devup . theme ?. length ?. light ?. accent ) . toBeUndefined ( )
262+ } )
263+
155264 test ( 'exports FLOAT variables to default theme when colors are missing' , async ( ) => {
156265 ; ( globalThis as { figma ?: unknown } ) . figma = {
157266 util : { rgba : ( v : unknown ) => v } ,
@@ -370,6 +479,79 @@ describe('export-devup length and shadow coverage', () => {
370479 expect ( devup . theme ?. length ) . toBeUndefined ( )
371480 } )
372481
482+ test ( 'replicates length and shadow values for all color themes' , async ( ) => {
483+ const variablesById : Record < string , Variable | null > = {
484+ c1 : {
485+ id : 'c1' ,
486+ name : 'Primary' ,
487+ resolvedType : 'COLOR' ,
488+ valuesByMode : {
489+ mLight : { r : 1 , g : 0 , b : 0 , a : 1 } ,
490+ mDark : { r : 0 , g : 0 , b : 1 , a : 1 } ,
491+ } ,
492+ } as unknown as Variable ,
493+ f1 : {
494+ id : 'f1' ,
495+ name : 'spacingSm' ,
496+ resolvedType : 'FLOAT' ,
497+ valuesByMode : { mMobile : 8 } ,
498+ } as unknown as Variable ,
499+ }
500+
501+ ; ( globalThis as { figma ?: unknown } ) . figma = {
502+ util : { rgba : ( v : unknown ) => v } ,
503+ variables : {
504+ getVariableByIdAsync : async ( id : string ) => variablesById [ id ] ?? null ,
505+ getLocalVariableCollectionsAsync : async ( ) => [
506+ {
507+ variableIds : [ 'c1' ] ,
508+ modes : [
509+ { modeId : 'mLight' , name : 'light' } ,
510+ { modeId : 'mDark' , name : 'dark' } ,
511+ ] ,
512+ } ,
513+ {
514+ variableIds : [ 'f1' ] ,
515+ modes : [ { modeId : 'mMobile' , name : 'mobile' } ] ,
516+ } ,
517+ ] ,
518+ } ,
519+ getLocalTextStylesAsync : async ( ) => [ ] ,
520+ getLocalEffectStylesAsync : async ( ) =>
521+ [
522+ {
523+ id : 's1' ,
524+ name : 'mobile/focus' ,
525+ effects : [
526+ {
527+ type : 'DROP_SHADOW' ,
528+ visible : true ,
529+ radius : 6 ,
530+ spread : 0 ,
531+ color : { r : 0 , g : 0 , b : 0 , a : 0.2 } ,
532+ offset : { x : 0 , y : 2 } ,
533+ blendMode : 'NORMAL' ,
534+ showShadowBehindNode : false ,
535+ } ,
536+ ] ,
537+ } ,
538+ ] as unknown as EffectStyle [ ] ,
539+ root : { findAllWithCriteria : ( ) => [ ] , children : [ ] } ,
540+ } as unknown as typeof figma
541+
542+ const devup = await buildDevupConfig ( false )
543+
544+ // Length should exist for both light and dark themes
545+ expect ( devup . theme ?. length ?. light ?. spacingSm ) . toBe ( '8px' )
546+ expect ( devup . theme ?. length ?. dark ?. spacingSm ) . toBe ( '8px' )
547+
548+ // Shadow should exist for both light and dark themes
549+ expect ( devup . theme ?. shadows ?. light ?. focus ) . toBeDefined ( )
550+ expect ( devup . theme ?. shadows ?. dark ?. focus ) . toBe (
551+ devup . theme ?. shadows ?. light ?. focus ,
552+ )
553+ } )
554+
373555 test ( 'exportDevup sends excel output to xlsx downloader' , async ( ) => {
374556 const downloadXlsx = spyOn (
375557 downloadXlsxModule ,
0 commit comments