@@ -235,6 +235,253 @@ describe('devup commands', () => {
235235 )
236236 } )
237237
238+ test ( 'exportDevup treeshake true handles mixed-style text nodes' , async ( ) => {
239+ getColorCollectionSpy = spyOn (
240+ getColorCollectionModule ,
241+ 'getDevupColorCollection' ,
242+ ) . mockResolvedValue ( null )
243+ styleNameToTypographySpy = spyOn (
244+ styleNameToTypographyModule ,
245+ 'styleNameToTypography' ,
246+ ) . mockReturnValue ( { level : 0 , name : 'heading' } )
247+ textStyleToTypographySpy = spyOn (
248+ textStyleToTypographyModule ,
249+ 'textStyleToTypography' ,
250+ ) . mockReturnValue ( { fontFamily : 'Inter' } as unknown as DevupTypography )
251+
252+ const mixedSymbol = Symbol ( 'mixed' )
253+ const mixedTextNode = {
254+ type : 'TEXT' ,
255+ textStyleId : mixedSymbol ,
256+ getStyledTextSegments : ( ) => [
257+ { textStyleId : 'style1' } ,
258+ { textStyleId : 'style2' } ,
259+ ] ,
260+ } as unknown as TextNode
261+
262+ ; ( globalThis as { figma ?: unknown } ) . figma = {
263+ util : { rgba : ( v : unknown ) => v } ,
264+ loadAllPagesAsync : async ( ) => { } ,
265+ getLocalTextStylesAsync : async ( ) => [
266+ { id : 'style1' , name : 'heading/1' } as unknown as TextStyle ,
267+ ] ,
268+ root : { findAllWithCriteria : ( ) => [ mixedTextNode ] } ,
269+ mixed : mixedSymbol ,
270+ variables : { getVariableByIdAsync : async ( ) => null } ,
271+ } as unknown as typeof figma
272+
273+ await exportDevup ( 'json' , true )
274+
275+ expect ( downloadFileMock ) . toHaveBeenCalledWith (
276+ 'devup.json' ,
277+ expect . stringContaining ( '"typography"' ) ,
278+ )
279+ } )
280+
281+ test ( 'exportDevup treeshake true stops within current page subtree and skips later pages' , async ( ) => {
282+ getColorCollectionSpy = spyOn (
283+ getColorCollectionModule ,
284+ 'getDevupColorCollection' ,
285+ ) . mockResolvedValue ( null )
286+ styleNameToTypographySpy = spyOn (
287+ styleNameToTypographyModule ,
288+ 'styleNameToTypography' ,
289+ ) . mockImplementation ( ( name : string ) =>
290+ name . includes ( '2' )
291+ ? ( { level : 1 , name : 'heading' } as const )
292+ : ( { level : 0 , name : 'heading' } as const ) ,
293+ )
294+ textStyleToTypographySpy = spyOn (
295+ textStyleToTypographyModule ,
296+ 'textStyleToTypography' ,
297+ ) . mockReturnValue ( { fontFamily : 'Inter' } as unknown as DevupTypography )
298+
299+ const currentTextNode = {
300+ type : 'TEXT' ,
301+ textStyleId : 'style1' ,
302+ getStyledTextSegments : ( ) => [ { textStyleId : 'style1' } ] ,
303+ } as unknown as TextNode
304+ const firstSectionFindAllWithCriteria = mock ( ( ) => [ currentTextNode ] )
305+ const secondSectionFindAllWithCriteria = mock ( ( ) => [ ] )
306+ const otherPageLoadAsync = mock ( async ( ) => { } )
307+ const firstSection = {
308+ type : 'SECTION' ,
309+ findAllWithCriteria : firstSectionFindAllWithCriteria ,
310+ } as unknown as SectionNode
311+ const secondSection = {
312+ type : 'SECTION' ,
313+ findAllWithCriteria : secondSectionFindAllWithCriteria ,
314+ } as unknown as SectionNode
315+ const currentPage = {
316+ id : 'page-current' ,
317+ children : [ firstSection , secondSection ] ,
318+ } as unknown as PageNode
319+ const otherPage = {
320+ id : 'page-other' ,
321+ children : [ ] ,
322+ loadAsync : otherPageLoadAsync ,
323+ } as unknown as PageNode
324+
325+ ; ( globalThis as { figma ?: unknown } ) . figma = {
326+ util : { rgba : ( v : unknown ) => v } ,
327+ currentPage,
328+ getLocalTextStylesAsync : async ( ) => [
329+ { id : 'style1' , name : 'heading/1' } as unknown as TextStyle ,
330+ { id : 'style2' , name : 'heading/2' } as unknown as TextStyle ,
331+ ] ,
332+ root : {
333+ children : [ otherPage , currentPage ] ,
334+ } ,
335+ mixed : Symbol ( 'mixed' ) ,
336+ variables : { getVariableByIdAsync : async ( ) => null } ,
337+ } as unknown as typeof figma
338+
339+ await exportDevup ( 'json' , true )
340+
341+ expect ( firstSectionFindAllWithCriteria ) . toHaveBeenCalledTimes ( 1 )
342+ expect ( secondSectionFindAllWithCriteria ) . not . toHaveBeenCalled ( )
343+ expect ( otherPageLoadAsync ) . not . toHaveBeenCalled ( )
344+ expect ( downloadFileMock ) . toHaveBeenCalledWith (
345+ 'devup.json' ,
346+ expect . stringContaining ( '"typography"' ) ,
347+ )
348+ } )
349+
350+ test ( 'exportDevup treeshake true lazily loads later pages when needed' , async ( ) => {
351+ getColorCollectionSpy = spyOn (
352+ getColorCollectionModule ,
353+ 'getDevupColorCollection' ,
354+ ) . mockResolvedValue ( null )
355+ styleNameToTypographySpy = spyOn (
356+ styleNameToTypographyModule ,
357+ 'styleNameToTypography' ,
358+ ) . mockImplementation ( ( name : string ) =>
359+ name . includes ( '2' )
360+ ? ( { level : 1 , name : 'body' } as const )
361+ : ( { level : 0 , name : 'heading' } as const ) ,
362+ )
363+ textStyleToTypographySpy = spyOn (
364+ textStyleToTypographyModule ,
365+ 'textStyleToTypography' ,
366+ ) . mockReturnValue ( { fontFamily : 'Inter' } as unknown as DevupTypography )
367+
368+ const currentSectionFindAllWithCriteria = mock ( ( ) => [ ] )
369+ const otherTextNode = {
370+ type : 'TEXT' ,
371+ textStyleId : 'style2' ,
372+ getStyledTextSegments : ( ) => [ { textStyleId : 'style2' } ] ,
373+ } as unknown as TextNode
374+ const otherSectionFindAllWithCriteria = mock ( ( ) => [ otherTextNode ] )
375+ const otherPageLoadAsync = mock ( async ( ) => { } )
376+ const currentPage = {
377+ id : 'page-current' ,
378+ children : [
379+ {
380+ type : 'SECTION' ,
381+ findAllWithCriteria : currentSectionFindAllWithCriteria ,
382+ } as unknown as SectionNode ,
383+ ] ,
384+ } as unknown as PageNode
385+ const otherPage = {
386+ id : 'page-other' ,
387+ children : [
388+ {
389+ type : 'SECTION' ,
390+ findAllWithCriteria : otherSectionFindAllWithCriteria ,
391+ } as unknown as SectionNode ,
392+ ] ,
393+ loadAsync : otherPageLoadAsync ,
394+ } as unknown as PageNode
395+
396+ ; ( globalThis as { figma ?: unknown } ) . figma = {
397+ util : { rgba : ( v : unknown ) => v } ,
398+ currentPage,
399+ getLocalTextStylesAsync : async ( ) => [
400+ { id : 'style1' , name : 'heading/1' } as unknown as TextStyle ,
401+ { id : 'style2' , name : 'body/2' } as unknown as TextStyle ,
402+ ] ,
403+ root : {
404+ children : [ currentPage , otherPage ] ,
405+ } ,
406+ mixed : Symbol ( 'mixed' ) ,
407+ variables : { getVariableByIdAsync : async ( ) => null } ,
408+ } as unknown as typeof figma
409+
410+ await exportDevup ( 'json' , true )
411+
412+ expect ( currentSectionFindAllWithCriteria ) . toHaveBeenCalledTimes ( 1 )
413+ expect ( otherPageLoadAsync ) . toHaveBeenCalledTimes ( 1 )
414+ expect ( otherSectionFindAllWithCriteria ) . toHaveBeenCalledTimes ( 1 )
415+ expect ( downloadFileMock ) . toHaveBeenCalledWith (
416+ 'devup.json' ,
417+ expect . stringContaining ( '"typography"' ) ,
418+ )
419+ } )
420+
421+ test ( 'exportDevup treeshake true handles direct text children and recursive fallback nodes' , async ( ) => {
422+ getColorCollectionSpy = spyOn (
423+ getColorCollectionModule ,
424+ 'getDevupColorCollection' ,
425+ ) . mockResolvedValue ( null )
426+ styleNameToTypographySpy = spyOn (
427+ styleNameToTypographyModule ,
428+ 'styleNameToTypography' ,
429+ ) . mockImplementation ( ( name : string ) =>
430+ name . includes ( '2' )
431+ ? ( { level : 1 , name : 'body' } as const )
432+ : ( { level : 0 , name : 'heading' } as const ) ,
433+ )
434+ textStyleToTypographySpy = spyOn (
435+ textStyleToTypographyModule ,
436+ 'textStyleToTypography' ,
437+ ) . mockImplementation (
438+ ( style : TextStyle ) => ( { id : style . id } ) as unknown as DevupTypography ,
439+ )
440+
441+ const directTextNode = {
442+ type : 'TEXT' ,
443+ textStyleId : 'style1' ,
444+ getStyledTextSegments : ( ) => [ { textStyleId : 'style1' } ] ,
445+ } as unknown as TextNode
446+ const nestedTextNode = {
447+ type : 'TEXT' ,
448+ textStyleId : 'style2' ,
449+ getStyledTextSegments : ( ) => [ { textStyleId : 'style2' } ] ,
450+ } as unknown as TextNode
451+ const recursiveNode = {
452+ type : 'GROUP' ,
453+ children : [ nestedTextNode ] ,
454+ } as unknown as GroupNode
455+ const currentPage = {
456+ id : 'page-current' ,
457+ children : [ directTextNode , recursiveNode ] ,
458+ } as unknown as PageNode
459+
460+ ; ( globalThis as { figma ?: unknown } ) . figma = {
461+ util : { rgba : ( v : unknown ) => v } ,
462+ currentPage,
463+ getLocalTextStylesAsync : async ( ) => [
464+ { id : 'style1' , name : 'heading/1' } as unknown as TextStyle ,
465+ { id : 'style2' , name : 'body/2' } as unknown as TextStyle ,
466+ ] ,
467+ root : {
468+ children : [ currentPage ] ,
469+ } ,
470+ mixed : Symbol ( 'mixed' ) ,
471+ variables : { getVariableByIdAsync : async ( ) => null } ,
472+ } as unknown as typeof figma
473+
474+ await exportDevup ( 'json' , true )
475+
476+ const firstCall = downloadFileMock . mock . calls [ 0 ] as unknown [ ] | undefined
477+ const data = ( firstCall ?. [ 1 ] as string ) ?? '{}'
478+ const parsed = JSON . parse ( data ) as {
479+ theme ?: { typography ?: Record < string , unknown > }
480+ }
481+ expect ( parsed . theme ?. typography ?. heading ) . toBeDefined ( )
482+ expect ( parsed . theme ?. typography ?. body ) . toBeDefined ( )
483+ } )
484+
238485 test ( 'exportDevup fills missing typography levels from styles map' , async ( ) => {
239486 getColorCollectionSpy = spyOn (
240487 getColorCollectionModule ,
0 commit comments