├── .gitignore ├── README.md ├── figma2react.js ├── lib ├── figma.js └── typeGen.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /src 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .figma2react 20 | /misc 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # figma2react 2 | 3 | Convert figma components in to react components 4 | 5 | ![](https://dl.dropboxusercontent.com/s/g271pk7p25o3x1n/ezgif.com-video-to-gif.gif?dl=0) 6 | 7 | 8 | ## installation 9 | 10 | ``` 11 | npm install -g figma2react 12 | ``` 13 | 14 | ## create CRA application 15 | ``` 16 | create-react-app test-figma2react 17 | ``` 18 | you can use any name 19 | 20 | ## create the configuration file in the root of the project 21 | ``` 22 | cd test-figma2react 23 | touch .figma2react 24 | ``` 25 | 26 | ## insert your configuration 27 | ``` 28 | //.figma2react 29 | 30 | { 31 | "projectId": , 32 | "token": , 33 | "directory": , 34 | "types": 35 | } 36 | ``` 37 | 38 | you can findout how to get your token [here](https://www.figma.com/developers/docs#auth) 39 | 40 | if you wanna test it out just use the test project, projectId: "eYk3d4ngtHUXkg82atfuJP" 41 | 42 | ## generate your components 43 | 44 | inside your CRA project 45 | 46 | ``` 47 | figma2react generate 48 | ``` 49 | 50 | ## watch for changes in the figma project and generate the components 51 | 52 | ``` 53 | figma2react watch 54 | ``` 55 | 56 | 57 | ###### Marcell Monteiro Cruz - 2018 58 | -------------------------------------------------------------------------------- /figma2react.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fetch = require('node-fetch'); 3 | const headers = new fetch.Headers(); 4 | const figma = require('./lib/figma'); 5 | const typeGen = require('./lib/typeGen'); 6 | const program = require('commander'); 7 | const fs = require('fs'); 8 | const Path = require('path'); 9 | 10 | const baseUrl = 'https://api.figma.com'; 11 | 12 | 13 | 14 | const vectorMap = {}; 15 | const vectorList = []; 16 | const vectorTypes = ['VECTOR', 'LINE', 'REGULAR_POLYGON', 'ELLIPSE', 'STAR']; 17 | 18 | function preprocessTree(node) { 19 | let vectorsOnly = node.name.charAt(0) !== '#'; 20 | let vectorVConstraint = null; 21 | let vectorHConstraint = null; 22 | 23 | function paintsRequireRender(paints) { 24 | if (!paints) return false; 25 | 26 | let numPaints = 0; 27 | for (const paint of paints) { 28 | if (paint.visible === false) continue; 29 | 30 | numPaints++; 31 | if (paint.type === 'EMOJI') return true; 32 | } 33 | 34 | return numPaints > 1; 35 | } 36 | 37 | if (paintsRequireRender(node.fills) || 38 | paintsRequireRender(node.strokes) || 39 | (node.blendMode != null && ['PASS_THROUGH', 'NORMAL'].indexOf(node.blendMode) < 0)) { 40 | node.type = 'VECTOR'; 41 | } 42 | 43 | const children = node.children && node.children.filter((child) => child.visible !== false); 44 | if (children) { 45 | for (let j=0; j 0 && vectorsOnly) { 58 | node.type = 'VECTOR'; 59 | node.constraints = { 60 | vertical: vectorVConstraint, 61 | horizontal: vectorHConstraint, 62 | }; 63 | } 64 | 65 | if (vectorTypes.indexOf(node.type) >= 0) { 66 | node.type = 'VECTOR'; 67 | vectorMap[node.id] = node; 68 | vectorList.push(node.id); 69 | node.children = []; 70 | } 71 | 72 | if (node.children) { 73 | for (const child of node.children) { 74 | preprocessTree(child); 75 | } 76 | } 77 | } 78 | 79 | async function generateComponents(data) { 80 | const doc = data.document; 81 | const canvas = doc.children[0]; 82 | const config = data.config; 83 | const ts = (config.types || []).includes('ts'); 84 | const headers = data.headers; 85 | const componentsDir = `./${config.directory || 'src/components'}`; 86 | 87 | try { 88 | fs.accessSync(componentsDir); 89 | } catch (e) { 90 | fs.mkdirSync(componentsDir, { recursive: true }); 91 | } 92 | 93 | let html = ''; 94 | 95 | for (let i=0; i { 176 | let data = await fetchProject(); 177 | generateComponents(data); 178 | }); 179 | 180 | program 181 | .command('watch') 182 | .alias('w') 183 | .description('watch for changes in the figma projects and generate the components') 184 | .action(async () => { 185 | let data = await fetchProject(), 186 | currentDate = data.lastModified; 187 | setInterval(async () => { 188 | data = await fetchProject(); 189 | if(data.lastModified > currentDate) { 190 | generateComponents(data); 191 | currentDate = data.lastModified; 192 | console.log(`project changed, making modifications!`); 193 | } 194 | }, 1000); 195 | console.log(`watching figma project ${data.config.projectId}`); 196 | console.log(`last modified ${currentDate}`); 197 | }); 198 | 199 | program.parse(process.argv); 200 | -------------------------------------------------------------------------------- /lib/figma.js: -------------------------------------------------------------------------------- 1 | const VECTOR_TYPES = ['VECTOR', 'LINE', 'REGULAR_POLYGON', 'ELLIPSE']; 2 | const GROUP_TYPES = ['GROUP', 'BOOLEAN_OPERATION']; 3 | const PROP_TYPES = { 4 | STRING: 0, 5 | NUMBER: 1 6 | }; 7 | 8 | function colorString(color) { 9 | return `rgba(${Math.round(color.r*255)}, ${Math.round(color.g*255)}, ${Math.round(color.b*255)}, ${color.a})`; 10 | } 11 | 12 | function dropShadow(effect) { 13 | return `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${colorString(effect.color)}`; 14 | } 15 | 16 | function innerShadow(effect) { 17 | return `inset ${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${colorString(effect.color)}`; 18 | } 19 | 20 | function imageURL(hash) { 21 | const squash = hash.split('-').join(''); 22 | return `url(https://s3-us-west-2.amazonaws.com/figma-alpha/img/${squash.substring(0, 4)}/${squash.substring(4, 8)}/${squash.substring(8)})`; 23 | } 24 | 25 | function backgroundSize(scaleMode) { 26 | if (scaleMode === 'FILL') { 27 | return 'cover'; 28 | } 29 | } 30 | 31 | function nodeSort(a, b) { 32 | if (a.absoluteBoundingBox.y < b.absoluteBoundingBox.y) return -1; 33 | else if (a.absoluteBoundingBox.y === b.absoluteBoundingBox.y) return 0; 34 | else return 1; 35 | } 36 | 37 | function getPaint(paintList) { 38 | if (paintList && paintList.length > 0) { 39 | return paintList[paintList.length - 1]; 40 | } 41 | 42 | return null; 43 | } 44 | 45 | function paintToLinearGradient(paint) { 46 | const handles = paint.gradientHandlePositions; 47 | const handle0 = handles[0]; 48 | const handle1 = handles[1]; 49 | 50 | const ydiff = handle1.y - handle0.y; 51 | const xdiff = handle0.x - handle1.x; 52 | 53 | const angle = Math.atan2(-xdiff, -ydiff); 54 | const stops = paint.gradientStops.map((stop) => { 55 | return `${colorString(stop.color)} ${Math.round(stop.position * 100)}%`; 56 | }).join(', '); 57 | return `linear-gradient(${angle}rad, ${stops})`; 58 | } 59 | 60 | function paintToRadialGradient(paint) { 61 | const stops = paint.gradientStops.map((stop) => { 62 | return `${colorString(stop.color)} ${Math.round(stop.position * 60)}%`; 63 | }).join(', '); 64 | 65 | return `radial-gradient(${stops})`; 66 | } 67 | 68 | function expandChildren(node, parent, minChildren, maxChildren, centerChildren, offset) { 69 | const children = node.children; 70 | let added = offset; 71 | 72 | if (children) { 73 | for (let i=0; i= 0) { 81 | added += expandChildren(child, parent, minChildren, maxChildren, centerChildren, added+i); 82 | continue; 83 | } 84 | 85 | child.order = i + added; 86 | 87 | if (child.constraints && child.constraints.vertical === 'BOTTOM') { 88 | maxChildren.push(child); 89 | } else if (child.constraints && child.constraints.vertical === 'TOP') { 90 | minChildren.push(child); 91 | } else { 92 | centerChildren.push(child); 93 | } 94 | } 95 | 96 | minChildren.sort(nodeSort); 97 | maxChildren.sort(nodeSort); 98 | 99 | return added + children.length - offset; 100 | } 101 | 102 | return added - offset; 103 | } 104 | 105 | const createComponent = (component, imgMap, componentMap) => { 106 | const name = 'C' + component.name.replace(/\W+/g, ''); 107 | const instance = name + component.id.replace(';', 'S').replace(':', 'D'); 108 | const props = []; 109 | 110 | let doc = ''; 111 | print(`class ${name} extends Component {`, ''); 112 | print(` render() {`, ''); 113 | print(` return (`, ''); 114 | 115 | function print(msg, indent) { 116 | doc += `${indent}${msg}\n`; 117 | } 118 | 119 | const visitNode = (node, parent, lastVertical, indent) => { 120 | let content = null; 121 | let img = null; 122 | const styles = {}; 123 | let minChildren = []; 124 | const maxChildren = []; 125 | const centerChildren = []; 126 | let bounds = null; 127 | let nodeBounds = null; 128 | 129 | if (parent != null) { 130 | nodeBounds = node.absoluteBoundingBox; 131 | const nx2 = nodeBounds.x + nodeBounds.width; 132 | const ny2 = nodeBounds.y + nodeBounds.height; 133 | const parentBounds = parent.absoluteBoundingBox; 134 | const px = parentBounds.x; 135 | const py = parentBounds.y; 136 | 137 | bounds = { 138 | left: nodeBounds.x - px, 139 | right: px + parentBounds.width - nx2, 140 | top: lastVertical == null ? nodeBounds.y - py : nodeBounds.y - lastVertical, 141 | bottom: py + parentBounds.height - ny2, 142 | width: nodeBounds.width, 143 | height: nodeBounds.height, 144 | } 145 | } 146 | 147 | expandChildren(node, parent, minChildren, maxChildren, centerChildren, 0); 148 | 149 | let outerClass = 'outerDiv'; 150 | let innerClass = 'innerDiv'; 151 | const cHorizontal = node.constraints && node.constraints.horizontal; 152 | const cVertical = node.constraints && node.constraints.vertical; 153 | const outerStyle = {}; 154 | 155 | if (node.order) { 156 | outerStyle.zIndex = node.order; 157 | } 158 | 159 | if (cHorizontal === 'LEFT_RIGHT') { 160 | if (bounds != null) { 161 | styles.marginLeft = bounds.left; 162 | styles.marginRight = bounds.right; 163 | styles.flexGrow = 1; 164 | } 165 | } else if (cHorizontal === 'RIGHT') { 166 | outerStyle.justifyContent = 'flex-end'; 167 | if (bounds != null) { 168 | styles.marginRight = bounds.right; 169 | styles.width = bounds.width; 170 | styles.minWidth = bounds.width; 171 | } 172 | } else if (cHorizontal === 'CENTER') { 173 | outerStyle.justifyContent = 'center'; 174 | if (bounds != null) { 175 | styles.width = bounds.width; 176 | styles.marginLeft = bounds.left && bounds.right ? bounds.left - bounds.right : null; 177 | } 178 | } else if (cHorizontal === 'SCALE') { 179 | if (bounds != null) { 180 | const parentWidth = bounds.left + bounds.width + bounds.right; 181 | styles.width = `${bounds.width*100/parentWidth}%`; 182 | styles.marginLeft = `${bounds.left*100/parentWidth}%`; 183 | } 184 | } else { 185 | if (bounds != null) { 186 | styles.marginLeft = bounds.left; 187 | styles.width = bounds.width; 188 | styles.minWidth = bounds.width; 189 | } 190 | } 191 | 192 | if (bounds && bounds.height && cVertical !== 'TOP_BOTTOM') styles.height = bounds.height; 193 | if (cVertical === 'TOP_BOTTOM') { 194 | outerClass += ' centerer'; 195 | if (bounds != null) { 196 | styles.marginTop = bounds.top; 197 | styles.marginBottom = bounds.bottom; 198 | } 199 | } else if (cVertical === 'CENTER') { 200 | outerClass += ' centerer'; 201 | outerStyle.alignItems = 'center'; 202 | if (bounds != null) { 203 | styles.marginTop = bounds.top - bounds.bottom; 204 | } 205 | } else if (cVertical === 'SCALE') { 206 | outerClass += ' centerer'; 207 | if (bounds != null) { 208 | const parentHeight = bounds.top + bounds.height + bounds.bottom; 209 | styles.height = `${bounds.height*100/parentHeight}%`; 210 | styles.top = `${bounds.top*100/parentHeight}%`; 211 | } 212 | } else { 213 | if (bounds != null) { 214 | styles.marginTop = bounds.top; 215 | styles.marginBottom = bounds.bottom; 216 | styles.minHeight = styles.height; 217 | styles.height = null; 218 | } 219 | } 220 | 221 | if (['FRAME', 'RECTANGLE', 'INSTANCE', 'COMPONENT'].indexOf(node.type) >= 0) { 222 | if (['FRAME', 'COMPONENT', 'INSTANCE'].indexOf(node.type) >= 0) { 223 | styles.backgroundColor = colorString(node.backgroundColor); 224 | if (node.clipsContent) styles.overflow = 'hidden'; 225 | } else if (node.type === 'RECTANGLE') { 226 | const lastFill = getPaint(node.fills); 227 | if (lastFill) { 228 | if (lastFill.type === 'SOLID') { 229 | styles.backgroundColor = colorString(lastFill.color); 230 | styles.opacity = lastFill.opacity; 231 | } else if (lastFill.type === 'IMAGE') { 232 | styles.backgroundImage = imageURL(lastFill.imageHash); 233 | styles.backgroundSize = backgroundSize(lastFill.scaleMode); 234 | } else if (lastFill.type === 'GRADIENT_LINEAR') { 235 | styles.background = paintToLinearGradient(lastFill); 236 | } else if (lastFill.type === 'GRADIENT_RADIAL') { 237 | styles.background = paintToRadialGradient(lastFill); 238 | } 239 | } 240 | 241 | if (node.effects) { 242 | for (let i=0; i 0) { 264 | styles.borderRadius = `${cornerRadii[0]}px ${cornerRadii[1]}px ${cornerRadii[2]}px ${cornerRadii[3]}px`; 265 | } 266 | } 267 | } else if (node.type === 'TEXT') { 268 | const lastFill = getPaint(node.fills); 269 | if (lastFill) { 270 | styles.color = colorString(lastFill.color); 271 | } 272 | 273 | const lastStroke = getPaint(node.strokes); 274 | if (lastStroke) { 275 | const weight = node.strokeWeight || 1; 276 | styles.WebkitTextStroke = `${weight}px ${colorString(lastStroke.color)}`; 277 | } 278 | 279 | const fontStyle = node.style; 280 | 281 | const applyFontStyle = (_styles, fontStyle) => { 282 | if (fontStyle) { 283 | _styles.fontSize = fontStyle.fontSize; 284 | _styles.fontWeight = fontStyle.fontWeight; 285 | _styles.fontFamily = fontStyle.fontFamily; 286 | _styles.textAlign = fontStyle.textAlignHorizontal; 287 | _styles.fontStyle = fontStyle.italic ? 'italic' : 'normal'; 288 | _styles.lineHeight = `${fontStyle.lineHeightPercent * 1.25}%`; 289 | _styles.letterSpacing = `${fontStyle.letterSpacing}px`; 290 | } 291 | } 292 | applyFontStyle(styles, fontStyle); 293 | 294 | if (node.name.substring(0, 6) === 'input:') { 295 | content = [``]; 296 | } else if (node.characterStyleOverrides) { 297 | let para = ''; 298 | const ps = []; 299 | const styleCache = {}; 300 | let currStyle = 0; 301 | 302 | const commitParagraph = (key) => { 303 | if (para !== '') { 304 | if (styleCache[currStyle] == null && currStyle !== 0) { 305 | styleCache[currStyle] = {}; 306 | applyFontStyle(styleCache[currStyle], node.styleOverrideTable[currStyle]); 307 | } 308 | 309 | const styleOverride = styleCache[currStyle] ? JSON.stringify(styleCache[currStyle]) : '{}'; 310 | 311 | ps.push(`${para}`); 312 | para = ''; 313 | } 314 | } 315 | 316 | for (const i in node.characters) { 317 | let idx = node.characterStyleOverrides[i]; 318 | 319 | if (node.characters[i] === '\n') { 320 | commitParagraph(i); 321 | ps.push(`
`); 322 | continue; 323 | } 324 | 325 | if (idx == null) idx = 0; 326 | if (idx !== currStyle) { 327 | commitParagraph(i); 328 | currStyle = idx; 329 | } 330 | 331 | para += node.characters[i]; 332 | } 333 | commitParagraph('end'); 334 | 335 | content = ps; 336 | } else { 337 | content = node.characters.split("\n").map((line, idx) => `
${line}
`); 338 | } 339 | } 340 | 341 | function printDiv(styles, outerStyle, indent) { 342 | print(`
`, indent); 343 | print(` `, indent); 348 | } 349 | if (parent != null) { 350 | printDiv(styles, outerStyle, indent); 351 | } 352 | 353 | if (node.id !== component.id && node.name.charAt(0) === '#') { 354 | print(` `, indent); 355 | createComponent(node, imgMap, componentMap); 356 | doc = `import C${node.name.replace(/\W+/g, '')} from './C${node.name.replace(/\W+/g, '')}';\n` + doc; 357 | } else if (node.type === 'VECTOR') { 358 | print(`
`, indent); 359 | } else { 360 | const newNodeBounds = node.absoluteBoundingBox; 361 | const newLastVertical = newNodeBounds && newNodeBounds.y + newNodeBounds.height; 362 | print(`
`, indent); 363 | let first = true; 364 | for (const child of minChildren) { 365 | visitNode(child, node, first ? null : newLastVertical, indent + ' '); 366 | first = false; 367 | } 368 | for (const child of centerChildren) visitNode(child, node, null, indent + ' '); 369 | if (maxChildren.length > 0) { 370 | outerClass += ' maxer'; 371 | styles.width = '100%'; 372 | styles.pointerEvents = 'none'; 373 | styles.backgroundColor = null; 374 | printDiv(styles, outerStyle, indent + ' '); 375 | first = true; 376 | for (const child of maxChildren) { 377 | visitNode(child, node, first ? null : newLastVertical, indent + ' '); 378 | first = false; 379 | } 380 | print(`
`, indent); 381 | print(`
`, indent); 382 | } 383 | if (content != null) { 384 | if (node.name.charAt(0) === '$') { 385 | const varName = node.name.substring(1); 386 | props.push({ name: varName, type: [PROP_TYPES.NUMBER, PROP_TYPES.STRING] }); 387 | print(` {this.props.${varName} && this.props.${varName}.split("\\n").map((line, idx) =>
{line}
)}`, indent); 388 | print(` {!this.props.${varName} && (
`, indent); 389 | for (const piece of content) { 390 | print(piece, indent + ' '); 391 | } 392 | print(`
)}`, indent); 393 | } else { 394 | for (const piece of content) { 395 | print(piece, indent + ' '); 396 | } 397 | } 398 | } 399 | print(`
`, indent); 400 | } 401 | 402 | if (parent != null) { 403 | print(` `, indent); 404 | print(``, indent); 405 | } 406 | } 407 | 408 | visitNode(component, null, null, ' '); 409 | print(' );', ''); 410 | print(' }', ''); 411 | print('}', ''); 412 | print(`export default ${name};`, ''); 413 | componentMap[component.id] = {instance, name, doc, props}; 414 | } 415 | 416 | module.exports = {createComponent, colorString, PROP_TYPES} 417 | -------------------------------------------------------------------------------- /lib/typeGen.js: -------------------------------------------------------------------------------- 1 | const { PROP_TYPES } = require('./figma'); 2 | 3 | const mapTsType = type => { 4 | switch (type) { 5 | case PROP_TYPES.STRING: 6 | return 'string'; 7 | case PROP_TYPES.NUMBER: 8 | return 'number'; 9 | default: 10 | return 'any'; 11 | } 12 | }; 13 | 14 | const mapTsProp = ({ name, type }) => 15 | `${name}: ${type.map(mapTsType).join(' | ')};`; 16 | 17 | const ts = ({ name, props }) => `interface Props { ${props.map(mapTsProp).join(' ')} } 18 | declare class ${name} extends Component {} 19 | export default ${name}; 20 | `; 21 | 22 | module.exports = { ts }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma2react", 3 | "version": "0.1.5", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "commander": { 8 | "version": "2.15.1", 9 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 10 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" 11 | }, 12 | "node-fetch": { 13 | "version": "2.6.1", 14 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 15 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma2react", 3 | "version": "0.1.5", 4 | "description": "converts figma designs in to react components", 5 | "preferGlobal": true, 6 | "bin": "./figma2react.js", 7 | "main": "figma2react.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/0000marcell/figma2react.git" 11 | }, 12 | "dependencies": { 13 | "commander": "^2.15.1", 14 | "node-fetch": "^2.1.2" 15 | }, 16 | "devDependencies": {}, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC" 23 | } 24 | --------------------------------------------------------------------------------