Skip to content

Commit 912a2bf

Browse files
committed
Optimize export devup
1 parent b5376f1 commit 912a2bf

File tree

2 files changed

+117
-21
lines changed

2 files changed

+117
-21
lines changed

src/commands/devup/__tests__/index.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,68 @@ describe('devup commands', () => {
278278
)
279279
})
280280

281+
test('exportDevup treeshake true stops early once a typography key is found', 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 currentPageFindAllWithCriteria = mock(() => [currentTextNode])
305+
const otherPageFindAllWithCriteria = mock(() => [])
306+
const rootFindAllWithCriteria = mock(() => [])
307+
const currentPage = {
308+
id: 'page-current',
309+
findAllWithCriteria: currentPageFindAllWithCriteria,
310+
} as unknown as PageNode
311+
const otherPage = {
312+
id: 'page-other',
313+
findAllWithCriteria: otherPageFindAllWithCriteria,
314+
} as unknown as PageNode
315+
316+
;(globalThis as { figma?: unknown }).figma = {
317+
util: { rgba: (v: unknown) => v },
318+
currentPage,
319+
loadAllPagesAsync: async () => {},
320+
getLocalTextStylesAsync: async () => [
321+
{ id: 'style1', name: 'heading/1' } as unknown as TextStyle,
322+
{ id: 'style2', name: 'heading/2' } as unknown as TextStyle,
323+
],
324+
root: {
325+
children: [otherPage, currentPage],
326+
findAllWithCriteria: rootFindAllWithCriteria,
327+
},
328+
mixed: Symbol('mixed'),
329+
variables: { getVariableByIdAsync: async () => null },
330+
} as unknown as typeof figma
331+
332+
await exportDevup('json', true)
333+
334+
expect(currentPageFindAllWithCriteria).toHaveBeenCalledTimes(1)
335+
expect(otherPageFindAllWithCriteria).not.toHaveBeenCalled()
336+
expect(rootFindAllWithCriteria).not.toHaveBeenCalled()
337+
expect(downloadFileMock).toHaveBeenCalledWith(
338+
'devup.json',
339+
expect.stringContaining('"typography"'),
340+
)
341+
})
342+
281343
test('exportDevup fills missing typography levels from styles map', async () => {
282344
getColorCollectionSpy = spyOn(
283345
getColorCollectionModule,

src/commands/devup/export-devup.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,13 @@ export async function exportDevup(
8282
// Build both ID-keyed and name-keyed maps from a single fetch
8383
const stylesById = new Map<string, TextStyle>()
8484
const styles: Record<string, TextStyle> = {}
85+
const styleMetaById = new Map<string, { level: number; name: string }>()
86+
const allTypographyKeys = new Set<string>()
8587
for (const style of textStyles) {
88+
const meta = styleNameToTypography(style.name)
8689
stylesById.set(style.id, style)
90+
styleMetaById.set(style.id, meta)
91+
allTypographyKeys.add(meta.name)
8792
styles[style.name] = style
8893
}
8994

@@ -94,47 +99,76 @@ export async function exportDevup(
9499
const prevSkip = figma.skipInvisibleInstanceChildren
95100
figma.skipInvisibleInstanceChildren = true
96101

97-
const tFind = perfStart()
98-
const texts = figma.root.findAllWithCriteria({ types: ['TEXT'] })
99-
perfEnd('exportDevup.typography.find', tFind)
100-
101-
const tScan = perfStart()
102-
const foundStyleIds = new Set<string>()
103-
for (const text of texts) {
104-
// Early exit: all local styles discovered
105-
if (foundStyleIds.size >= stylesById.size) break
102+
const usedTypographyKeys = new Set<string>()
103+
const processText = (text: TextNode) => {
104+
if (usedTypographyKeys.size >= allTypographyKeys.size) return
106105
if (
107106
!(typeof text.textStyleId === 'string' && text.textStyleId) &&
108107
text.textStyleId !== figma.mixed
109108
)
110-
continue
109+
return
111110

112111
if (typeof text.textStyleId === 'string') {
113-
// Single-style node — skip getStyledTextSegments entirely
114112
const style = stylesById.get(text.textStyleId)
115-
if (!style || foundStyleIds.has(text.textStyleId)) continue
116-
foundStyleIds.add(text.textStyleId)
117-
const { level, name } = styleNameToTypography(style.name)
113+
const meta = styleMetaById.get(text.textStyleId)
114+
if (!style || !meta || usedTypographyKeys.has(meta.name)) return
115+
usedTypographyKeys.add(meta.name)
116+
const { level, name } = meta
118117
if (!typography[name]?.[level]) {
119118
typography[name] ??= [null, null, null, null, null, null]
120119
typography[name][level] = textStyleToTypography(style)
121120
}
122-
continue
121+
return
123122
}
124123

125-
// Mixed-style node — only request textStyleId (1 field vs 8)
126124
for (const seg of text.getStyledTextSegments(['textStyleId'])) {
127-
if (!seg?.textStyleId || foundStyleIds.has(seg.textStyleId)) continue
125+
if (usedTypographyKeys.size >= allTypographyKeys.size) return
126+
if (!seg?.textStyleId) continue
128127
const style = stylesById.get(seg.textStyleId)
129-
if (!style) continue
130-
foundStyleIds.add(seg.textStyleId)
131-
const { level, name } = styleNameToTypography(style.name)
128+
const meta = styleMetaById.get(seg.textStyleId)
129+
if (!style || !meta || usedTypographyKeys.has(meta.name)) continue
130+
usedTypographyKeys.add(meta.name)
131+
const { level, name } = meta
132132
if (typography[name]?.[level]) continue
133133
typography[name] ??= [null, null, null, null, null, null]
134134
typography[name][level] = textStyleToTypography(style)
135135
}
136136
}
137-
perfEnd('exportDevup.typography.scan', tScan)
137+
138+
const rootPages = Array.isArray(figma.root.children)
139+
? figma.root.children
140+
: []
141+
if (rootPages.length > 1) {
142+
const currentPageId = figma.currentPage.id
143+
const orderedPages = [
144+
figma.currentPage,
145+
...rootPages.filter((page) => page.id !== currentPageId),
146+
]
147+
for (const page of orderedPages) {
148+
if (usedTypographyKeys.size >= allTypographyKeys.size) break
149+
const tFind = perfStart()
150+
const texts = page.findAllWithCriteria({ types: ['TEXT'] })
151+
perfEnd('exportDevup.typography.find', tFind)
152+
153+
const tScan = perfStart()
154+
for (const text of texts) {
155+
processText(text)
156+
if (usedTypographyKeys.size >= allTypographyKeys.size) break
157+
}
158+
perfEnd('exportDevup.typography.scan', tScan)
159+
}
160+
} else {
161+
const tFind = perfStart()
162+
const texts = figma.root.findAllWithCriteria({ types: ['TEXT'] })
163+
perfEnd('exportDevup.typography.find', tFind)
164+
165+
const tScan = perfStart()
166+
for (const text of texts) {
167+
processText(text)
168+
if (usedTypographyKeys.size >= allTypographyKeys.size) break
169+
}
170+
perfEnd('exportDevup.typography.scan', tScan)
171+
}
138172

139173
figma.skipInvisibleInstanceChildren = prevSkip
140174
} else {

0 commit comments

Comments
 (0)