Skip to content

Commit 6c1755c

Browse files
committed
Update Exporting
1 parent 468590e commit 6c1755c

File tree

11 files changed

+413
-62
lines changed

11 files changed

+413
-62
lines changed

manifest.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
{
3737
"name": "Export Assets",
3838
"command": "export-assets"
39+
},
40+
{
41+
"name": "Export Components",
42+
"command": "export-components"
3943
}
4044
]
4145
}

src/Element.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class Element {
5656
componentType?: ComponentType
5757
skipChildren: boolean = false
5858
assets: Record<string, () => Promise<Uint8Array>> = {}
59+
components: Record<string, () => Promise<string>> = {}
5960
constructor(node: SceneNode, parent?: Element) {
6061
this.node = node
6162
this.parent = parent
@@ -174,14 +175,25 @@ export class Element {
174175
break
175176
case 'RECTANGLE': {
176177
if (
177-
(this.node.fills as any).length === 1 &&
178-
(this.node.fills as any)[0].type === 'IMAGE'
178+
(this.node.fills as any).find((fill: any) => fill.type === 'IMAGE')
179179
) {
180-
this.componentType = 'Image'
180+
const css = await this.getCss()
181+
this.componentType =
182+
(this.node.fills as any).length === 1 ? 'Image' : 'Box'
181183
Object.assign(
182184
this.additionalProps,
183185
this.getImageProps('images', 'png'),
184186
)
187+
if (this.componentType !== 'Image') {
188+
this.additionalProps.bg = css.background.replace(
189+
'<path-to-image>',
190+
this.additionalProps.src,
191+
)
192+
193+
delete this.additionalProps.src
194+
} else {
195+
this.additionalProps.bg = ''
196+
}
185197
this.addAsset(this.node, 'png')
186198
}
187199
break
@@ -266,23 +278,46 @@ export class Element {
266278
return this.assets
267279
}
268280
addAsset(node: SceneNode, type: 'svg' | 'png') {
281+
if (
282+
type === 'svg' &&
283+
this.getChildren().length &&
284+
this.getChildren().every((c) => typeof c !== 'string' && !c.node.visible)
285+
) {
286+
return
287+
}
269288
if (this.parent) this.parent.addAsset(node, type)
270289
else
271290
this.assets[node.name + '.' + type] = async () => {
272291
const isSvg = type === 'svg'
273-
const data = await node.exportAsync({
292+
const options: ExportSettings = {
274293
format: isSvg ? 'SVG' : 'PNG',
275-
constraint: isSvg
276-
? undefined
277-
: {
278-
type: 'SCALE',
279-
value: 1.5,
280-
},
281-
})
294+
}
295+
if (options.format !== 'SVG') {
296+
;(options as any).constraint = {
297+
type: 'SCALE',
298+
value: 1.5,
299+
}
300+
} else {
301+
;(options as any).useAbsoluteBounds = true
302+
}
303+
const data = await node.exportAsync(options)
282304
return data
283305
}
284306
}
285307

308+
async getComponents(): Promise<Record<string, () => Promise<string>>> {
309+
await this.render()
310+
return this.components
311+
}
312+
313+
addComponent(node: SceneNode) {
314+
if (this.parent) this.parent.addComponent(node)
315+
else
316+
this.components[toPascal(node.name) + '.tsx'] = async () => {
317+
return (await new Element(node).render()).trim()
318+
}
319+
}
320+
286321
async render(dep: number = 0): Promise<string> {
287322
if (!this.node.visible) return ''
288323

@@ -303,6 +338,7 @@ export class Element {
303338
const value = (
304339
await this.node.exportAsync({
305340
format: 'SVG_STRING',
341+
useAbsoluteBounds: true,
306342
})
307343
).toString()
308344

@@ -492,6 +528,7 @@ export class Element {
492528
}
493529

494530
if (this.node.type === 'COMPONENT') {
531+
this.addComponent(this.node)
495532
const componentName = toPascal(this.node.name)
496533
const interfaceDecl = createInterface(
497534
componentName,

src/__tests__/Element.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function createNode(
1717
variantProperties,
1818
componentProperties,
1919
getMainComponentAsync,
20+
visible = true,
2021
...props
2122
}: {
2223
[_: string]: any
@@ -40,7 +41,7 @@ function createNode(
4041
textStyleId,
4142
parent,
4243
characters,
43-
visible: true,
44+
visible,
4445
layoutPositioning,
4546
width: props.width ? parseInt(props.width) : undefined,
4647
height: props.height ? parseInt(props.height) : undefined,
@@ -649,6 +650,7 @@ describe('Element', () => {
649650
width: '6px',
650651
height: '6px',
651652
name: 'image',
653+
background: 'url(/images/image.png)',
652654
fills: [
653655
{
654656
type: 'IMAGE',
@@ -694,6 +696,31 @@ describe('Element', () => {
694696
</Flex>`)
695697
})
696698
})
699+
700+
it('should render Rectangle with multiple fills', async () => {
701+
const element = createElement('RECTANGLE', {
702+
width: '6px',
703+
height: '6px',
704+
background: 'url(/images/image.png)',
705+
fills: [
706+
{
707+
type: 'SOLID',
708+
color: {
709+
r: 1,
710+
g: 0,
711+
b: 0,
712+
a: 1,
713+
},
714+
},
715+
{
716+
type: 'IMAGE',
717+
},
718+
],
719+
})
720+
expect(await element.render()).toEqual(
721+
`<Box bg="url(/images/image.png)" boxSize="6px" />`,
722+
)
723+
})
697724
})
698725

699726
describe('Svg', () => {

src/__tests__/code.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { exportDevup, importDevup } from '../commands/devup'
22
import { exportAssets } from '../commands/exportAssets'
3+
import { exportComponents } from '../commands/exportComponents'
34

45
vi.mock('../commands/devup')
56
vi.mock('../commands/exportAssets')
7+
vi.mock('../commands/exportComponents')
68

79
beforeEach(() => {
810
vi.resetModules()
@@ -47,7 +49,18 @@ it('should export assets', async () => {
4749
expect(exportAssets).toBeCalledTimes(1)
4850
expect(closePlugin).toBeCalledTimes(1)
4951
})
50-
52+
it('should export components', async () => {
53+
const closePlugin = vi.fn()
54+
;(globalThis as any).figma = {
55+
editorType: 'figma',
56+
command: 'export-components',
57+
closePlugin,
58+
}
59+
vi.mocked(exportComponents).mockResolvedValueOnce()
60+
await import('../code')
61+
expect(exportComponents).toBeCalledTimes(1)
62+
expect(closePlugin).toBeCalledTimes(1)
63+
})
5164
describe('codegen', () => {
5265
it('should generate code', async () => {
5366
const closePlugin = vi.fn()

src/code.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { exportDevup, importDevup } from './commands/devup'
22
import { exportAssets } from './commands/exportAssets'
3+
import { exportComponents } from './commands/exportComponents'
34
import { Element } from './Element'
45

56
if (figma.editorType === 'dev' && figma.mode === 'codegen') {
@@ -23,4 +24,7 @@ switch (figma.command) {
2324
case 'export-assets':
2425
exportAssets().finally(() => figma.closePlugin())
2526
break
27+
case 'export-components':
28+
exportComponents().finally(() => figma.closePlugin())
29+
break
2630
}

src/commands/__tests__/exportAssets.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function createNode(
3333
layoutSizingHorizontal,
3434
styledTextSegments = [],
3535
variantProperties,
36+
visible = true,
3637
...props
3738
}: {
3839
[_: string]: any
@@ -54,7 +55,7 @@ function createNode(
5455
textStyleId,
5556
parent,
5657
characters,
57-
visible: true,
58+
visible,
5859
layoutPositioning,
5960
width: props.width ? parseInt(props.width) : undefined,
6061
height: props.height ? parseInt(props.height) : undefined,
@@ -97,10 +98,22 @@ describe('exportAssets', () => {
9798
})
9899
;(globalThis as any).figma.currentPage.selection = [node]
99100
await exportAssets()
100-
expect(notifyMock).toHaveBeenCalledWith(
101-
'No assets found',
102-
expect.any(Object),
103-
)
101+
expect(notifyMock).toHaveBeenCalledWith('No assets found')
102+
})
103+
104+
it('should not export assets if all children are invisible', async () => {
105+
const node = createNode('GROUP', {
106+
fills: [],
107+
children: [
108+
createNode('VECTOR', {
109+
fills: [],
110+
visible: false,
111+
}),
112+
],
113+
})
114+
;(globalThis as any).figma.currentPage.selection = [node]
115+
await exportAssets()
116+
expect(downloadFile).not.toHaveBeenCalled()
104117
})
105118

106119
it('should export assets and call downloadFile', async () => {
@@ -122,6 +135,7 @@ describe('exportAssets', () => {
122135
scaleMode: 'FILL',
123136
},
124137
],
138+
background: 'red',
125139
width: '100px',
126140
height: '100px',
127141
name: 'image.png',
@@ -139,4 +153,21 @@ describe('exportAssets', () => {
139153
expect.any(Object),
140154
)
141155
})
156+
157+
it('should raise error if exportAsync fails', async () => {
158+
const node = createNode('RECTANGLE', {
159+
fills: [
160+
{
161+
type: 'IMAGE',
162+
},
163+
],
164+
})
165+
;(globalThis as any).figma.currentPage.selection = [node]
166+
;(node.exportAsync as any) = vi.fn().mockRejectedValue('test')
167+
await exportAssets()
168+
expect(notifyMock).toHaveBeenCalledWith('Error exporting assets', {
169+
timeout: 3000,
170+
error: true,
171+
})
172+
})
142173
})

0 commit comments

Comments
 (0)