Skip to content

Commit a2b7daf

Browse files
authored
Merge pull request #8 from dev-five-git/multi
Export Assets and render code
2 parents d004749 + 0da542e commit a2b7daf

20 files changed

+827
-113
lines changed

manifest.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
{
3333
"name": "Import Devup",
3434
"command": "import-devup"
35+
},
36+
{
37+
"name": "Export Assets",
38+
"command": "export-assets"
3539
}
3640
]
3741
}

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
1515
"author": "",
1616
"license": "",
1717
"devDependencies": {
18-
"eslint-plugin-devup": "^2.0.5",
18+
"@eslint/compat": "^1.2.9",
1919
"@figma/eslint-plugin-figma-plugins": "*",
2020
"@figma/plugin-typings": "*",
2121
"@rspack/cli": "^1.3.9",
2222
"@rspack/core": "^1.3.9",
2323
"@typescript-eslint/eslint-plugin": "^8.32.0",
2424
"@typescript-eslint/parser": "^8.32.0",
25-
"@eslint/compat": "^1.2.9",
25+
"@vitest/coverage-v8": "^3.1.3",
2626
"eslint": "^9.26.0",
27+
"eslint-plugin-devup": "^2.0.5",
2728
"typescript": "^5.8.3",
28-
"@vitest/coverage-v8": "^3.1.3",
2929
"vitest": "^3.1.3"
30+
},
31+
"dependencies": {
32+
"jszip": "^3.10.1"
3033
}
3134
}

pnpm-lock.yaml

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Element.ts

Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
checkSvgImageChildrenType,
3+
createInterface,
34
cssToProps,
5+
filterPropsByChildrenCountAndType,
46
fixChildrenText,
57
formatSvg,
68
organizeProps,
@@ -10,7 +12,7 @@ import {
1012
} from './utils'
1113
import { extractKeyValueFromCssVar } from './utils/extract-key-value-from-css-var'
1214
import { textSegmentToTypography } from './utils/text-segment-to-typography'
13-
import { toCamel } from './utils/to-camel'
15+
import { toPascal } from './utils/to-pascal'
1416

1517
export type ComponentType =
1618
| 'Fragment'
@@ -45,12 +47,15 @@ export class Element {
4547
props?: Record<string, string>
4648
css?: Record<string, string>
4749
additionalProps?: Record<string, string>
50+
parent?: Element
4851
// for svg
4952
svgVarKeyValue?: [string, string]
5053
componentType?: ComponentType
5154
skipChildren: boolean = false
52-
constructor(node: SceneNode) {
55+
assets: Record<string, () => Promise<Uint8Array>> = {}
56+
constructor(node: SceneNode, parent?: Element) {
5357
this.node = node
58+
this.parent = parent
5459
}
5560
async getCss(): Promise<Record<string, string>> {
5661
if (this.css) return this.css
@@ -96,19 +101,22 @@ export class Element {
96101
css['padding-right']
97102
)
98103
}
99-
getImageProps(): Record<string, string> {
104+
getImageProps(
105+
dir: 'icons' | 'images',
106+
extension: 'svg' | 'png',
107+
): Record<string, string> {
100108
return cssToProps(
101109
this.node.parent &&
102110
'width' in this.node.parent &&
103111
this.node.parent.width === this.node.width
104112
? {
105-
src: this.node.name,
113+
src: `/${dir}/${this.node.name}.${extension}`,
106114
width: '100%',
107115
height: '',
108116
'aspect-ratio': `${Math.floor((this.node.width / this.node.height) * 100) / 100}`,
109117
}
110118
: {
111-
src: this.node.name,
119+
src: `/${dir}/${this.node.name}.${extension}`,
112120
width: this.node.width + 'px',
113121
height: this.node.height + 'px',
114122
},
@@ -148,7 +156,14 @@ export class Element {
148156
break
149157
}
150158
this.componentType = 'Image'
151-
Object.assign(this.additionalProps, this.getImageProps())
159+
this.addAsset(this.node, 'svg')
160+
Object.assign(
161+
this.additionalProps,
162+
this.getImageProps(
163+
this.node.width !== this.node.height ? 'images' : 'icons',
164+
'svg',
165+
),
166+
)
152167
break
153168
}
154169
case 'TEXT':
@@ -160,7 +175,11 @@ export class Element {
160175
(this.node.fills as any)[0].type === 'IMAGE'
161176
) {
162177
this.componentType = 'Image'
163-
Object.assign(this.additionalProps, this.getImageProps())
178+
Object.assign(
179+
this.additionalProps,
180+
this.getImageProps('images', 'png'),
181+
)
182+
this.addAsset(this.node, 'png')
164183
}
165184
break
166185
}
@@ -180,16 +199,40 @@ export class Element {
180199
break
181200
const res = await checkSvgImageChildrenType(this.node)
182201
if (res) {
183-
if (res.type === 'SVG' && res.fill) {
184-
this.componentType = 'svg'
185-
this.skipChildren = true
186-
this.svgVarKeyValue = extractKeyValueFromCssVar(res.fill)
202+
if (res.type === 'SVG' && res.fill.size > 0) {
203+
if (res.fill.size === 1) {
204+
// mask image
205+
this.componentType = 'Box'
206+
this.skipChildren = true
207+
208+
const props = this.getImageProps('icons', 'svg')
209+
props['maskImage'] = `url(${props.src})`
210+
delete props.src
211+
delete props.aspectRatio
212+
Object.assign(this.additionalProps, {
213+
...props,
214+
bg: res.fill.values().next().value,
215+
maskSize: 'contain',
216+
maskRepeat: 'no-repeat',
217+
})
218+
this.addAsset(this.node, 'svg')
219+
} else {
220+
this.componentType = 'svg'
221+
// render string
222+
this.skipChildren = false
223+
224+
this.addAsset(this.node, 'svg')
225+
}
187226
break
188227
}
189228

190229
this.componentType = 'Image'
191230
this.skipChildren = true
192-
Object.assign(this.additionalProps, this.getImageProps())
231+
Object.assign(
232+
this.additionalProps,
233+
this.getImageProps('icons', 'svg'),
234+
)
235+
this.addAsset(this.node, 'svg')
193236
}
194237
break
195238
}
@@ -211,33 +254,48 @@ export class Element {
211254
if (this.node.type === 'TEXT')
212255
return this.node.characters ? [this.node.characters] : []
213256
if (!('children' in this.node)) return []
214-
return this.node.children.map((node) => new Element(node))
257+
return this.node.children.map((node) => new Element(node, this))
258+
}
259+
260+
async getAssets(): Promise<Record<string, () => Promise<Uint8Array>>> {
261+
await this.render()
262+
return this.assets
263+
}
264+
addAsset(node: SceneNode, type: 'svg' | 'png') {
265+
if (this.parent) this.parent.addAsset(node, type)
266+
else
267+
this.assets[node.name + '.' + type] = () =>
268+
node.exportAsync({
269+
format: type === 'svg' ? 'SVG' : 'PNG',
270+
})
215271
}
216272

217273
async render(dep: number = 0): Promise<string> {
218274
if (!this.node.visible) return ''
275+
276+
if (this.node.type === 'INSTANCE') {
277+
return space(dep) + `<${toPascal(this.node.name)} />`
278+
}
279+
if (this.node.type === 'COMPONENT_SET') {
280+
return (
281+
await Promise.all(
282+
this.node.children
283+
.map((child) => new Element(child, this))
284+
.map((child) => child.render(dep)),
285+
)
286+
).join('\n')
287+
}
288+
219289
const componentType = await this.getComponentType()
220290

221291
if (componentType === 'svg') {
222-
// prue svg
223-
let value = (
292+
// prue svg
293+
const value = (
224294
await this.node.exportAsync({
225295
format: 'SVG_STRING',
226296
})
227297
).toString()
228298

229-
if (this.svgVarKeyValue) {
230-
value = value.replaceAll(this.svgVarKeyValue[1], 'currentColor')
231-
if (this.svgVarKeyValue[0].startsWith('$'))
232-
this.svgVarKeyValue[0] =
233-
'$' + toCamel(this.svgVarKeyValue[0].slice(1))
234-
235-
value = value.replace(
236-
'<svg',
237-
`<svg className={css({ color: "${this.svgVarKeyValue[0]}" })}`,
238-
)
239-
}
240-
241299
return formatSvg(value, dep)
242300
}
243301

@@ -246,8 +304,15 @@ export class Element {
246304
if ('error' in originProps)
247305
return `<${componentType} error="${originProps.error}" />`
248306

249-
const mergedProps = { ...originProps, ...this.additionalProps }
250307
const children = this.getChildren()
308+
const mergedProps = filterPropsByChildrenCountAndType(
309+
children.length,
310+
componentType,
311+
{
312+
...originProps,
313+
...this.additionalProps,
314+
},
315+
)
251316

252317
if (this.node.type === 'TEXT') {
253318
const segs = this.node.getStyledTextSegments(SEGMENT_TYPE)
@@ -329,6 +394,22 @@ export class Element {
329394
const propsString = Object.entries(props)
330395
.map(([key, value]) => `${key}="${value}"`)
331396
.join(' ')
332-
return `${space(dep)}<${componentType}${propsString ? ' ' + propsString : ''}${hasChildren ? '' : ' /'}>${hasChildren ? `\n${space(dep + 1)}${renderChildren}\n` : ''}${hasChildren ? `${space(dep)}</${componentType}>` : ''}`
397+
const body = `${space(dep)}<${componentType}${propsString ? ' ' + propsString : ''}${hasChildren ? '' : ' /'}>${hasChildren ? `\n${space(dep + 1)}${renderChildren}\n` : ''}${hasChildren ? `${space(dep)}</${componentType}>` : ''}`
398+
if (this.node.type === 'COMPONENT') {
399+
const componentName = toPascal(this.node.name)
400+
const interfaceDecl = createInterface(
401+
componentName,
402+
this.node.variantProperties,
403+
)
404+
return `${interfaceDecl ? interfaceDecl + '\n' : ''}${space(dep)}export function ${componentName}(${interfaceDecl ? `props: ${componentName}Props` : ''}) {
405+
return (
406+
${body
407+
.split('\n')
408+
.map((line) => space(dep + 2) + line)
409+
.join('\n')}
410+
)
411+
}`
412+
}
413+
return body
333414
}
334415
}

0 commit comments

Comments
 (0)