├── .github
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── figma-transformer.svg
├── jest.config.js
├── package.json
├── src
├── __tests__
│ ├── index.test.ts
│ ├── testFile.json
│ └── utils.test.ts
├── index.ts
├── types.ts
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm"
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 |
7 | steps:
8 | - name: Begin CI...
9 | uses: actions/checkout@v2
10 |
11 | - name: Use Node 12
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: 12.x
15 |
16 | - name: Use cached node_modules
17 | uses: actions/cache@v1
18 | with:
19 | path: node_modules
20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }}
21 | restore-keys: |
22 | nodeModules-
23 |
24 | - name: Install dependencies
25 | run: yarn install --frozen-lockfile
26 | env:
27 | CI: true
28 |
29 | - name: Lint
30 | run: yarn lint
31 | env:
32 | CI: true
33 |
34 | - name: Test
35 | run: yarn test --ci --coverage --maxWorkers=2
36 | env:
37 | CI: true
38 |
39 | - name: Upload coverage to Codecov
40 | uses: codecov/codecov-action@v1.0.12
41 |
42 | - name: Build
43 | run: yarn build
44 | env:
45 | CI: true
46 |
47 | - name: Publish to npm
48 | uses: pascalgn/npm-publish-action@1.3.3
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
52 |
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .rts2_cache_cjs
5 | .rts2_cache_esm
6 | .rts2_cache_umd
7 | .rts2_cache_system
8 | dist
9 | coverage
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 4,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": false,
7 | "trailingComma": "es5",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "fluid": false
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Bernardo Raposo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | figma-transformer
8 |
9 |
10 |
11 |
12 | A tiny utility library that makes the Figma API more human friendly.
13 |
14 | [![npm version][version-badge]][npm]
15 | [![npm downloads][downloads-badge]][npm]
16 | [![gzip size][size-badge]][size]
17 | ![modules][modules-badge]
18 | [![MIT License][license-badge]][license]
19 | [![PRs Welcome][prs-badge]][prs]
20 |
21 |
22 |
23 | ## How to use `figma-transformer`?
24 |
25 | ```js
26 | import { processFile } from "figma-transformer";
27 |
28 | // Fetch the file you want using your favourite method
29 | const originalFile = fetchFigmaFile();
30 |
31 | const file = processFile(originalFile);
32 |
33 | // ✨ You can now use `file` for whatever you need! ✨
34 |
35 | // Let's get the styles for a component named "Test"
36 | const testStyles = file.shortcuts.components.find(
37 | component => component.name === "Test"
38 | ).shortcuts.styles;
39 | ```
40 |
41 | ## Why use `figma-transformer`?
42 |
43 | The Figma API is great but sometimes it feels like it's built for machines, not humans. The more you use it, the more you'll end up wasting a lot of time to get to the information that you want.
44 |
45 | These are the most common problems:
46 |
47 | - Code needs to change if file structure changes
48 | - Incomplete information about styles and components
49 | - No type safety
50 |
51 | With `figma-transformer` you get the file structure that you wished the Figma API had.
52 |
53 | ## How does `figma-transformer` solve these problems?
54 |
55 | ### Break free from the file structure
56 |
57 | The Figma API response is very strict in terms of the file structure. To get to a specific node you have to navigate through the entire tree of nodes and it's really easy for your code to break if there's a change in the design file that changes the initial hierarchy.
58 |
59 | We break from that rigid structure by creating shortcuts that are grouped by node type, making it a lot easier to access the nodes that we want irrespective of their placement in the file.
60 |
61 | ```js
62 | {
63 | "children": [{...}, {...}],
64 | "shortcuts": {
65 | "CANVAS": [...],
66 | "INSTANCE": [...],
67 | "RECTANGLE": [...],
68 | "STYLE": [...],
69 | "TEXT": [...],
70 | "FRAME": [...],
71 | "COMPONENT": [...],
72 | "GROUP": [...]
73 | }
74 | }
75 | ```
76 |
77 | We can see that even though this node just has two direct children, it actually contains a lot more elements down the tree, which are surfaced in the shortcuts.
78 |
79 | Each node of the document tree contains the shortcuts to all their respective child nodes, which reduces the amount of work needed to get to the information we need.
80 |
81 | ### Missing information from nodes
82 |
83 | From the API we can get the information about the styles and components that are present in the file, which is great, but it doesn't contain all the information so we need to parse the entire file to get the additional information that we usuallly need.
84 |
85 | Let's look at how the Figma API describes the styles in a document:
86 |
87 | ```js
88 | styles: {
89 | "1:12": {
90 | key: "ea017aed6616af00f3c4d59e3d945c8c3e47adca",
91 | name: "Green",
92 | styleType: "FILL",
93 | description: "",
94 | },
95 | "1:11": {
96 | key: "e234400b962ffafce654af9b3220ce88857523ec",
97 | name: "Red",
98 | styleType: "FILL",
99 | description: "",
100 | },
101 | "97:6": {
102 | key: "cc806814e1b9b7d20ce0b6bed8adf52099899c01",
103 | name: "Body",
104 | styleType: "TEXT",
105 | description: "",
106 | },
107 | },
108 | ```
109 |
110 | and this is how it's represented after being processed (note the populated styles from the associated nodes)
111 |
112 | ```js
113 | [
114 | {
115 | id: "1:12",
116 | key: "ea017aed6616af00f3c4d59e3d945c8c3e47adca",
117 | name: "Green",
118 | styleType: "FILL",
119 | description: "",
120 | styles: [
121 | {
122 | blendMode: "NORMAL",
123 | type: "SOLID",
124 | color: {
125 | r: 0.047774821519851685,
126 | g: 0.9563318490982056,
127 | b: 0.02923285961151123,
128 | a: 1,
129 | },
130 | },
131 | ],
132 | type: "STYLE",
133 | },
134 | {
135 | id: "1:11",
136 | key: "e234400b962ffafce654af9b3220ce88857523ec",
137 | name: "Red",
138 | styleType: "FILL",
139 | description: "",
140 | styles: [
141 | {
142 | blendMode: "NORMAL",
143 | type: "SOLID",
144 | color: {
145 | r: 0.8515284061431885,
146 | g: 0.11155396699905396,
147 | b: 0.11155396699905396,
148 | a: 1,
149 | },
150 | },
151 | ],
152 | textStyles: {
153 | fontFamily: "Roboto",
154 | fontPostScriptName: null,
155 | fontWeight: 400,
156 | fontSize: 12,
157 | textAlignHorizontal: "LEFT",
158 | textAlignVertical: "TOP",
159 | letterSpacing: 0,
160 | lineHeightPx: 14.0625,
161 | lineHeightPercent: 100,
162 | lineHeightUnit: "INTRINSIC_%",
163 | },
164 | type: "STYLE",
165 | },
166 | {
167 | id: "97:6",
168 | key: "cc806814e1b9b7d20ce0b6bed8adf52099899c01",
169 | name: "Body",
170 | styleType: "TEXT",
171 | description: "",
172 | textStyles: {
173 | fontFamily: "Roboto",
174 | fontPostScriptName: null,
175 | fontWeight: 400,
176 | fontSize: 12,
177 | textAlignHorizontal: "LEFT",
178 | textAlignVertical: "TOP",
179 | letterSpacing: 0,
180 | lineHeightPx: 14.0625,
181 | lineHeightPercent: 100,
182 | lineHeightUnit: "INTRINSIC_%",
183 | },
184 | type: "STYLE",
185 | },
186 | ];
187 | ```
188 |
189 | The same happens with the components, this is what we get from the API:
190 |
191 | ```js
192 | components: {
193 | "1:5": { key: "", name: "Rectangle", description: "" },
194 | },
195 | ```
196 |
197 | and this is the processed data:
198 |
199 | ```js
200 | {
201 | "id": "1:5",
202 | "parentId": "7:0",
203 | "fileId": "cLp23bR627jcuNSoBGkhL04E",
204 | "name": "Rectangle",
205 | "type": "COMPONENT",
206 | "blendMode": "PASS_THROUGH",
207 | "absoluteBoundingBox": {
208 | "x": -232,
209 | "y": -208,
210 | "width": 201,
211 | "height": 109
212 | },
213 | "constraints": {
214 | "vertical": "TOP",
215 | "horizontal": "LEFT"
216 | },
217 | "clipsContent": false,
218 | "background": [
219 | {
220 | "blendMode": "NORMAL",
221 | "visible": false,
222 | "type": "SOLID",
223 | "color": {
224 | "r": 1,
225 | "g": 1,
226 | "b": 1,
227 | "a": 1
228 | }
229 | }
230 | ],
231 | "backgroundColor": {
232 | "r": 0,
233 | "g": 0,
234 | "b": 0,
235 | "a": 0
236 | },
237 | "effects": [],
238 | "children": [...],
239 | "shortcuts": {...}
240 | }
241 | ```
242 |
243 | Not only we have the complete node definition but we also have its child nodes and shortcuts so we can easily navigate through the component tree if needed.
244 |
245 | ### Improved type safety
246 |
247 | The Figma API doesn't have official type definitions, but fortunately we can provide a better developer experience by extending the TypeScript type definitions provided by the awesome [figma-js](https://github.com/jongold/figma-js) library.
248 |
249 | This means that you can continue to use your preferred way of fetching the data from the Figma API and `figma-transformer` will provide the types for you.
250 |
251 | ## Examples
252 |
253 | Let's see more specific examples where the benefits of the library really stand out.
254 |
255 | **Getting all text used in a document**
256 |
257 | ```js
258 | const text = file.shortcuts.texts.map(node => node.characters);
259 | ```
260 |
261 | **Finding the styles applied to a specific component**
262 |
263 | ```js
264 | const styles = file.shortcuts.components
265 | .filter(component => component.name === "Test")
266 | .map(component => component.shortcuts.styles);
267 | ```
268 |
269 | **Getting the fill colours for all the rectangles in the first page**
270 |
271 | ```js
272 | const fills = file.shortcuts.pages
273 | .filter(page => page.name === "Page 1")
274 | .map(page => page.shortcuts.rectangles.fills);
275 | ```
276 |
277 | ## Projects using `figma-transformer`
278 |
279 | - [figma-graphql](https://github.com/braposo/figma-graphql)
280 | - [theme.figma](https://github.com/ds-tools/theme.figma)
281 |
282 | ---
283 |
284 | ## Local Development
285 |
286 | Below is a list of commands you will probably find useful.
287 |
288 | #### `npm start` or `yarn start`
289 |
290 | Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab.
291 |
292 | Your library will be rebuilt if you make edits.
293 |
294 | #### `npm run build` or `yarn build`
295 |
296 | Bundles the package to the `dist` folder.
297 | The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
298 |
299 | #### `npm test` or `yarn test`
300 |
301 | Runs the test watcher (Jest) in an interactive mode.
302 | By default, runs tests related to files changed since the last commit.
303 |
304 | _This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx)._
305 |
306 | [npm]: https://www.npmjs.com/package/figma-transformer
307 | [license]: https://github.com/braposo/figma-transformer/blob/master/LICENSE
308 | [prs]: http://makeapullrequest.com
309 | [size]: https://unpkg.com/figma-transformer/dist/figma-transformer.cjs.production.min.js
310 | [version-badge]: https://img.shields.io/npm/v/figma-transformer.svg?style=flat-square
311 | [downloads-badge]: https://img.shields.io/npm/dm/figma-transformer.svg?style=flat-square
312 | [license-badge]: https://img.shields.io/npm/l/figma-transformer.svg?style=flat-square
313 | [size-badge]: http://img.badgesize.io/https://unpkg.com/figma-transformer/dist/figma-transformer.cjs.production.min.js?compression=gzip&style=flat-square
314 | [modules-badge]: https://img.shields.io/badge/module%20formats-cjs%2C%20esm-green.svg?style=flat-square
315 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
316 |
--------------------------------------------------------------------------------
/figma-transformer.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleNameMapper: {
3 | src: "/src/index",
4 | "src/(.*)": "/src/$1",
5 | },
6 | modulePathIgnorePatterns: ["/dist/"],
7 | coverageThreshold: {
8 | global: {
9 | branches: 100,
10 | functions: 100,
11 | lines: 100,
12 | statements: 100,
13 | },
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "figma-transformer",
3 | "version": "2.1.0",
4 | "license": "MIT",
5 | "keywords": [
6 | "figma",
7 | "design",
8 | "process",
9 | "transformer",
10 | "api"
11 | ],
12 | "author": "Bernardo Raposo (https://github.com/braposo)",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/braposo/figma-transformer.git"
16 | },
17 | "homepage": "https://github.com/braposo/figma-transformer#readme",
18 | "main": "dist/index.js",
19 | "module": "dist/figma-transformer.esm.js",
20 | "typings": "dist/index.d.ts",
21 | "files": [
22 | "dist"
23 | ],
24 | "scripts": {
25 | "start": "tsdx watch",
26 | "build": "tsdx build",
27 | "test": "tsdx test",
28 | "lint": "tsdx lint src",
29 | "prepublishOnly": "yarn validate",
30 | "validate": "yarn test --coverage && yarn lint --fix && yarn build && bundlesize"
31 | },
32 | "sideEffects": false,
33 | "peerDependencies": {},
34 | "husky": {
35 | "hooks": {
36 | "pre-commit": "yarn lint && yarn test"
37 | }
38 | },
39 | "devDependencies": {
40 | "@types/jest": "^26.0.14",
41 | "bundlesize": "^0.18.0",
42 | "figma-js": "^1.13.0",
43 | "husky": "^4.3.0",
44 | "tsdx": "^0.14.0",
45 | "typescript": "3.9.7"
46 | },
47 | "dependencies": {},
48 | "bundlesize": [
49 | {
50 | "path": "./dist/figma-transformer.cjs.production.min.js",
51 | "maxSize": "1.1 kB"
52 | }
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | import { processFile } from "..";
2 | import testFile from "./testFile.json";
3 | import {
4 | FileResponse,
5 | ComponentMetadata,
6 | Style,
7 | Text,
8 | VectorBase,
9 | Component,
10 | } from "figma-js";
11 |
12 | describe("processFile", () => {
13 | const fileResponse = processFile(testFile as FileResponse);
14 |
15 | it("should have correct structure", () => {
16 | expect(Object.keys(fileResponse)).toEqual([
17 | "fileId",
18 | "name",
19 | "lastModified",
20 | "thumbnailUrl",
21 | "version",
22 | "children",
23 | "shortcuts",
24 | ]);
25 | });
26 |
27 | it("should have the right file information", () => {
28 | const { fileId, name, version } = fileResponse;
29 |
30 | expect(fileId).toBeUndefined();
31 | expect(name).toEqual("figma-graphql test file");
32 | expect(version).toBeDefined();
33 | });
34 |
35 | it("should have the fileId when defined", () => {
36 | const { fileId, name, version } = processFile(
37 | testFile as FileResponse,
38 | "cLp23bR627jcuNSoBGkhL04E"
39 | );
40 |
41 | expect(fileId).toEqual("cLp23bR627jcuNSoBGkhL04E");
42 | expect(name).toEqual("figma-graphql test file");
43 | expect(version).toBeDefined();
44 | });
45 |
46 | it("should keep the same info from styles declaration", () => {
47 | const { shortcuts } = fileResponse;
48 | const nodeId = "1:12";
49 |
50 | const originalComponent: Style = testFile.styles[nodeId] as Style;
51 | const parsedComponent = shortcuts.styles.find(n => n.id === nodeId);
52 | const keys = Object.keys(originalComponent) as [keyof Style];
53 |
54 | keys.forEach(key => {
55 | expect(originalComponent[key]).toEqual(
56 | parsedComponent && parsedComponent[key]
57 | );
58 | });
59 | });
60 |
61 | it("should keep the same info from components declaration", () => {
62 | const { shortcuts } = fileResponse;
63 | const nodeId = "1:5";
64 |
65 | const originalComponent: ComponentMetadata =
66 | testFile.components[nodeId];
67 | const parsedComponent = shortcuts.components.find(n => n.id === nodeId);
68 |
69 | const keys = Object.keys(originalComponent) as [
70 | keyof ComponentMetadata
71 | ];
72 |
73 | expect(parsedComponent).toBeDefined();
74 |
75 | keys.forEach(key => {
76 | expect(originalComponent[key]).toEqual(
77 | parsedComponent && parsedComponent[key]
78 | );
79 | });
80 | });
81 |
82 | it("should create the correct shortcuts", () => {
83 | const { shortcuts } = fileResponse;
84 | expect(shortcuts.pages).toHaveLength(2);
85 | expect(shortcuts.instances).toHaveLength(2);
86 | expect(shortcuts.rectangles).toHaveLength(4);
87 | expect(shortcuts.styles).toHaveLength(
88 | Object.keys(testFile.styles).length
89 | );
90 | expect(shortcuts.texts).toHaveLength(4);
91 | expect(shortcuts.frames).toHaveLength(3);
92 | expect(shortcuts.components).toHaveLength(
93 | Object.keys(testFile.components).length
94 | );
95 | expect(shortcuts.groups).toHaveLength(1);
96 | });
97 |
98 | it("should add fill styles to STYLE shortcut", () => {
99 | const { shortcuts } = fileResponse;
100 | const { styles } = shortcuts;
101 |
102 | const style = styles.find(style => style.styleType === "FILL");
103 |
104 | expect(style).toBeDefined();
105 |
106 | const originalStyles = getObjects(
107 | testFile,
108 | style?.styleType.toLowerCase(),
109 | style?.id
110 | );
111 |
112 | const originalStyle = originalStyles[0] as VectorBase;
113 |
114 | expect(style?.styles).toBe(originalStyle.fills);
115 | });
116 |
117 | it("should add text styles to STYLE shortcut", () => {
118 | const { shortcuts } = fileResponse;
119 | const { styles } = shortcuts;
120 | const style = styles.find(style => style.styleType === "TEXT");
121 |
122 | expect(style).toBeDefined();
123 |
124 | const originalStyles = getObjects(
125 | testFile,
126 | style?.styleType.toLowerCase(),
127 | style?.id
128 | );
129 |
130 | const originalStyle = originalStyles[0] as Text;
131 |
132 | expect(style?.textStyles).toBe(originalStyle.style);
133 | });
134 |
135 | it("should add effect styles to STYLE shortcut", () => {
136 | const { shortcuts } = fileResponse;
137 | const { styles } = shortcuts;
138 | const style = styles.find(style => style.styleType === "EFFECT");
139 |
140 | expect(style).toBeDefined();
141 |
142 | const originalStyles = getObjects(
143 | testFile,
144 | style?.styleType.toLowerCase(),
145 | style?.id
146 | );
147 |
148 | const originalStyle = originalStyles[0] as VectorBase;
149 |
150 | expect(style?.styles).toBe(originalStyle.effects);
151 | });
152 |
153 | it("should add component info to COMPONENT shortcut", () => {
154 | const { shortcuts } = fileResponse;
155 | const { components } = shortcuts;
156 | const component = components.find(
157 | component => component.name === "Rectangle"
158 | );
159 |
160 | expect(component).toBeDefined();
161 |
162 | const originalComponents = getObjects(testFile, "id", component?.id);
163 | const originalComponent = originalComponents[0] as Component;
164 | const {
165 | children,
166 | ...originalComponentWithoutChildren
167 | } = originalComponent;
168 |
169 | Object.entries(originalComponentWithoutChildren).forEach(
170 | ([key, val]) => {
171 | expect(component && component[key as keyof Component]).toEqual(
172 | val
173 | );
174 | }
175 | );
176 |
177 | expect(component?.children).toHaveLength(children.length);
178 | });
179 | });
180 |
181 | function getObjects(
182 | obj: Record,
183 | key?: string,
184 | val?: any,
185 | parent?: any
186 | ): Record[] {
187 | var objects: Record[] = [];
188 | for (var i in obj) {
189 | if (!obj.hasOwnProperty(i)) continue;
190 | if (typeof obj[i] == "object") {
191 | objects = objects.concat(getObjects(obj[i], key, val, obj));
192 | }
193 | //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not)
194 | else if ((i === key && obj[i] === val) || (i === key && val === "")) {
195 | //
196 | objects.push(parent);
197 | } else if (obj[i] === val && key === "") {
198 | //only add if the object is not already in the array
199 | if (objects.lastIndexOf(obj) === -1) {
200 | objects.push(parent);
201 | }
202 | }
203 | }
204 | return objects.flat();
205 | }
206 |
--------------------------------------------------------------------------------
/src/__tests__/testFile.json:
--------------------------------------------------------------------------------
1 | {
2 | "document": {
3 | "id": "0:0",
4 | "name": "Document",
5 | "type": "DOCUMENT",
6 | "children": [
7 | {
8 | "id": "0:1",
9 | "name": "Page 1",
10 | "type": "CANVAS",
11 | "children": [
12 | {
13 | "id": "1:6",
14 | "name": "Rectangle",
15 | "type": "INSTANCE",
16 | "blendMode": "PASS_THROUGH",
17 | "children": [
18 | {
19 | "id": "I1:6;1:2",
20 | "name": "Rectangle",
21 | "type": "RECTANGLE",
22 | "blendMode": "PASS_THROUGH",
23 | "absoluteBoundingBox": {
24 | "x": 24,
25 | "y": -208,
26 | "width": 201,
27 | "height": 109
28 | },
29 | "constraints": {
30 | "vertical": "SCALE",
31 | "horizontal": "SCALE"
32 | },
33 | "fills": [
34 | {
35 | "blendMode": "NORMAL",
36 | "type": "SOLID",
37 | "color": {
38 | "r": 0.047774821519851685,
39 | "g": 0.9563318490982056,
40 | "b": 0.02923285961151123,
41 | "a": 1
42 | }
43 | }
44 | ],
45 | "strokes": [],
46 | "strokeWeight": 3,
47 | "strokeAlign": "CENTER",
48 | "styles": {
49 | "fill": "1:12",
50 | "effect": "388:0"
51 | },
52 | "effects": [
53 | {
54 | "type": "DROP_SHADOW",
55 | "visible": true,
56 | "color": {
57 | "r": 0,
58 | "g": 0,
59 | "b": 0,
60 | "a": 0.25
61 | },
62 | "blendMode": "NORMAL",
63 | "offset": {
64 | "x": 0,
65 | "y": 4
66 | },
67 | "radius": 4
68 | }
69 | ]
70 | },
71 | {
72 | "id": "I1:6;18:0",
73 | "name": "Text",
74 | "type": "TEXT",
75 | "blendMode": "PASS_THROUGH",
76 | "absoluteBoundingBox": {
77 | "x": 57,
78 | "y": -163,
79 | "width": 63,
80 | "height": 20
81 | },
82 | "constraints": {
83 | "vertical": "SCALE",
84 | "horizontal": "SCALE"
85 | },
86 | "fills": [
87 | {
88 | "blendMode": "NORMAL",
89 | "type": "SOLID",
90 | "color": {
91 | "r": 0.8515284061431885,
92 | "g": 0.11155396699905396,
93 | "b": 0.11155396699905396,
94 | "a": 1
95 | }
96 | }
97 | ],
98 | "strokes": [],
99 | "strokeWeight": 1,
100 | "strokeAlign": "OUTSIDE",
101 | "styles": {
102 | "fill": "1:11"
103 | },
104 | "effects": [],
105 | "characters": "Text",
106 | "style": {
107 | "fontFamily": "Roboto",
108 | "fontPostScriptName": null,
109 | "fontWeight": 400,
110 | "fontSize": 12,
111 | "textAlignHorizontal": "LEFT",
112 | "textAlignVertical": "TOP",
113 | "letterSpacing": 0,
114 | "lineHeightPx": 14.0625,
115 | "lineHeightPercent": 100,
116 | "lineHeightUnit": "INTRINSIC_%"
117 | },
118 | "layoutVersion": 0,
119 | "characterStyleOverrides": [],
120 | "styleOverrideTable": {}
121 | }
122 | ],
123 | "absoluteBoundingBox": {
124 | "x": 24,
125 | "y": -208,
126 | "width": 201,
127 | "height": 109
128 | },
129 | "constraints": {
130 | "vertical": "TOP",
131 | "horizontal": "LEFT"
132 | },
133 | "clipsContent": false,
134 | "background": [
135 | {
136 | "blendMode": "NORMAL",
137 | "visible": false,
138 | "type": "SOLID",
139 | "color": {
140 | "r": 1,
141 | "g": 1,
142 | "b": 1,
143 | "a": 1
144 | }
145 | }
146 | ],
147 | "fills": [
148 | {
149 | "blendMode": "NORMAL",
150 | "visible": false,
151 | "type": "SOLID",
152 | "color": {
153 | "r": 1,
154 | "g": 1,
155 | "b": 1,
156 | "a": 1
157 | }
158 | }
159 | ],
160 | "strokes": [],
161 | "strokeWeight": 1,
162 | "strokeAlign": "OUTSIDE",
163 | "backgroundColor": {
164 | "r": 0,
165 | "g": 0,
166 | "b": 0,
167 | "a": 0
168 | },
169 | "effects": [],
170 | "componentId": "1:5"
171 | },
172 | {
173 | "id": "7:0",
174 | "name": "Frame 1",
175 | "type": "FRAME",
176 | "blendMode": "PASS_THROUGH",
177 | "children": [
178 | {
179 | "id": "1:5",
180 | "name": "Rectangle",
181 | "type": "COMPONENT",
182 | "blendMode": "PASS_THROUGH",
183 | "children": [
184 | {
185 | "id": "1:2",
186 | "name": "Rectangle",
187 | "type": "RECTANGLE",
188 | "blendMode": "PASS_THROUGH",
189 | "absoluteBoundingBox": {
190 | "x": -232,
191 | "y": -208,
192 | "width": 201,
193 | "height": 109
194 | },
195 | "constraints": {
196 | "vertical": "SCALE",
197 | "horizontal": "SCALE"
198 | },
199 | "fills": [
200 | {
201 | "blendMode": "NORMAL",
202 | "type": "SOLID",
203 | "color": {
204 | "r": 0.8515284061431885,
205 | "g": 0.11155396699905396,
206 | "b": 0.11155396699905396,
207 | "a": 1
208 | }
209 | }
210 | ],
211 | "strokes": [
212 | {
213 | "blendMode": "NORMAL",
214 | "type": "SOLID",
215 | "color": {
216 | "r": 0.058090269565582275,
217 | "g": 0.939568817615509,
218 | "b": 0.9958333373069763,
219 | "a": 1
220 | }
221 | }
222 | ],
223 | "strokeWeight": 3,
224 | "strokeAlign": "CENTER",
225 | "styles": {
226 | "fill": "1:11",
227 | "stroke": "387:0",
228 | "effect": "388:0"
229 | },
230 | "effects": [
231 | {
232 | "type": "DROP_SHADOW",
233 | "visible": true,
234 | "color": {
235 | "r": 0,
236 | "g": 0,
237 | "b": 0,
238 | "a": 0.25
239 | },
240 | "blendMode": "NORMAL",
241 | "offset": {
242 | "x": 0,
243 | "y": 4
244 | },
245 | "radius": 4
246 | }
247 | ]
248 | },
249 | {
250 | "id": "18:0",
251 | "name": "Text",
252 | "type": "TEXT",
253 | "blendMode": "PASS_THROUGH",
254 | "absoluteBoundingBox": {
255 | "x": -199,
256 | "y": -163,
257 | "width": 63,
258 | "height": 20
259 | },
260 | "constraints": {
261 | "vertical": "SCALE",
262 | "horizontal": "SCALE"
263 | },
264 | "fills": [
265 | {
266 | "blendMode": "NORMAL",
267 | "type": "SOLID",
268 | "color": {
269 | "r": 0.047774821519851685,
270 | "g": 0.9563318490982056,
271 | "b": 0.02923285961151123,
272 | "a": 1
273 | }
274 | }
275 | ],
276 | "strokes": [],
277 | "strokeWeight": 1,
278 | "strokeAlign": "OUTSIDE",
279 | "styles": {
280 | "fill": "1:12"
281 | },
282 | "effects": [],
283 | "characters": "Text",
284 | "style": {
285 | "fontFamily": "Roboto",
286 | "fontPostScriptName": null,
287 | "fontWeight": 400,
288 | "fontSize": 12,
289 | "textAlignHorizontal": "LEFT",
290 | "textAlignVertical": "TOP",
291 | "letterSpacing": 0,
292 | "lineHeightPx": 14.0625,
293 | "lineHeightPercent": 100,
294 | "lineHeightUnit": "INTRINSIC_%"
295 | },
296 | "layoutVersion": 0,
297 | "characterStyleOverrides": [],
298 | "styleOverrideTable": {}
299 | }
300 | ],
301 | "absoluteBoundingBox": {
302 | "x": -232,
303 | "y": -208,
304 | "width": 201,
305 | "height": 109
306 | },
307 | "constraints": {
308 | "vertical": "TOP",
309 | "horizontal": "LEFT"
310 | },
311 | "clipsContent": false,
312 | "background": [
313 | {
314 | "blendMode": "NORMAL",
315 | "visible": false,
316 | "type": "SOLID",
317 | "color": {
318 | "r": 1,
319 | "g": 1,
320 | "b": 1,
321 | "a": 1
322 | }
323 | }
324 | ],
325 | "fills": [
326 | {
327 | "blendMode": "NORMAL",
328 | "visible": false,
329 | "type": "SOLID",
330 | "color": {
331 | "r": 1,
332 | "g": 1,
333 | "b": 1,
334 | "a": 1
335 | }
336 | }
337 | ],
338 | "strokes": [],
339 | "strokeWeight": 1,
340 | "strokeAlign": "INSIDE",
341 | "backgroundColor": {
342 | "r": 0,
343 | "g": 0,
344 | "b": 0,
345 | "a": 0
346 | },
347 | "effects": []
348 | }
349 | ],
350 | "absoluteBoundingBox": {
351 | "x": -255,
352 | "y": -230,
353 | "width": 252,
354 | "height": 151
355 | },
356 | "constraints": {
357 | "vertical": "TOP",
358 | "horizontal": "LEFT"
359 | },
360 | "clipsContent": true,
361 | "background": [
362 | {
363 | "blendMode": "NORMAL",
364 | "type": "SOLID",
365 | "color": {
366 | "r": 1,
367 | "g": 1,
368 | "b": 1,
369 | "a": 1
370 | }
371 | }
372 | ],
373 | "fills": [
374 | {
375 | "blendMode": "NORMAL",
376 | "type": "SOLID",
377 | "color": {
378 | "r": 1,
379 | "g": 1,
380 | "b": 1,
381 | "a": 1
382 | }
383 | }
384 | ],
385 | "strokes": [],
386 | "strokeWeight": 1,
387 | "strokeAlign": "INSIDE",
388 | "backgroundColor": {
389 | "r": 1,
390 | "g": 1,
391 | "b": 1,
392 | "a": 1
393 | },
394 | "effects": []
395 | },
396 | {
397 | "id": "46:0",
398 | "name": "Frame 1.1",
399 | "visible": false,
400 | "type": "FRAME",
401 | "blendMode": "PASS_THROUGH",
402 | "children": [
403 | {
404 | "id": "46:1",
405 | "name": "Rectangle",
406 | "type": "INSTANCE",
407 | "blendMode": "PASS_THROUGH",
408 | "children": [
409 | {
410 | "id": "I46:1;1:2",
411 | "name": "Rectangle",
412 | "type": "RECTANGLE",
413 | "blendMode": "PASS_THROUGH",
414 | "absoluteBoundingBox": {
415 | "x": -232,
416 | "y": -16,
417 | "width": 201,
418 | "height": 109
419 | },
420 | "constraints": {
421 | "vertical": "SCALE",
422 | "horizontal": "SCALE"
423 | },
424 | "fills": [
425 | {
426 | "blendMode": "NORMAL",
427 | "type": "SOLID",
428 | "color": {
429 | "r": 0.8515284061431885,
430 | "g": 0.11155396699905396,
431 | "b": 0.11155396699905396,
432 | "a": 1
433 | }
434 | }
435 | ],
436 | "strokes": [
437 | {
438 | "blendMode": "NORMAL",
439 | "type": "SOLID",
440 | "color": {
441 | "r": 0.058090269565582275,
442 | "g": 0.939568817615509,
443 | "b": 0.9958333373069763,
444 | "a": 1
445 | }
446 | }
447 | ],
448 | "strokeWeight": 3,
449 | "strokeAlign": "CENTER",
450 | "styles": {
451 | "fill": "1:11",
452 | "stroke": "387:0",
453 | "effect": "388:0"
454 | },
455 | "effects": [
456 | {
457 | "type": "DROP_SHADOW",
458 | "visible": true,
459 | "color": {
460 | "r": 0,
461 | "g": 0,
462 | "b": 0,
463 | "a": 0.25
464 | },
465 | "blendMode": "NORMAL",
466 | "offset": {
467 | "x": 0,
468 | "y": 4
469 | },
470 | "radius": 4
471 | }
472 | ]
473 | },
474 | {
475 | "id": "I46:1;18:0",
476 | "name": "Text",
477 | "type": "TEXT",
478 | "blendMode": "PASS_THROUGH",
479 | "absoluteBoundingBox": {
480 | "x": -199,
481 | "y": 29,
482 | "width": 63,
483 | "height": 20
484 | },
485 | "constraints": {
486 | "vertical": "SCALE",
487 | "horizontal": "SCALE"
488 | },
489 | "fills": [
490 | {
491 | "blendMode": "NORMAL",
492 | "type": "SOLID",
493 | "color": {
494 | "r": 0.047774821519851685,
495 | "g": 0.9563318490982056,
496 | "b": 0.02923285961151123,
497 | "a": 1
498 | }
499 | }
500 | ],
501 | "strokes": [],
502 | "strokeWeight": 1,
503 | "strokeAlign": "OUTSIDE",
504 | "styles": {
505 | "fill": "1:12"
506 | },
507 | "effects": [],
508 | "characters": "Text",
509 | "style": {
510 | "fontFamily": "Roboto",
511 | "fontPostScriptName": null,
512 | "fontWeight": 400,
513 | "fontSize": 12,
514 | "textAlignHorizontal": "LEFT",
515 | "textAlignVertical": "TOP",
516 | "letterSpacing": 0,
517 | "lineHeightPx": 14.0625,
518 | "lineHeightPercent": 100,
519 | "lineHeightUnit": "INTRINSIC_%"
520 | },
521 | "layoutVersion": 0,
522 | "characterStyleOverrides": [],
523 | "styleOverrideTable": {}
524 | }
525 | ],
526 | "absoluteBoundingBox": {
527 | "x": -232,
528 | "y": -16,
529 | "width": 201,
530 | "height": 109
531 | },
532 | "constraints": {
533 | "vertical": "TOP",
534 | "horizontal": "LEFT"
535 | },
536 | "clipsContent": false,
537 | "background": [
538 | {
539 | "blendMode": "NORMAL",
540 | "visible": false,
541 | "type": "SOLID",
542 | "color": {
543 | "r": 1,
544 | "g": 1,
545 | "b": 1,
546 | "a": 1
547 | }
548 | }
549 | ],
550 | "fills": [
551 | {
552 | "blendMode": "NORMAL",
553 | "visible": false,
554 | "type": "SOLID",
555 | "color": {
556 | "r": 1,
557 | "g": 1,
558 | "b": 1,
559 | "a": 1
560 | }
561 | }
562 | ],
563 | "strokes": [],
564 | "strokeWeight": 1,
565 | "strokeAlign": "INSIDE",
566 | "backgroundColor": {
567 | "r": 0,
568 | "g": 0,
569 | "b": 0,
570 | "a": 0
571 | },
572 | "effects": [],
573 | "componentId": "1:5"
574 | }
575 | ],
576 | "absoluteBoundingBox": {
577 | "x": -255,
578 | "y": -38,
579 | "width": 252,
580 | "height": 151
581 | },
582 | "constraints": {
583 | "vertical": "TOP",
584 | "horizontal": "LEFT"
585 | },
586 | "clipsContent": true,
587 | "background": [
588 | {
589 | "blendMode": "NORMAL",
590 | "type": "SOLID",
591 | "color": {
592 | "r": 1,
593 | "g": 1,
594 | "b": 1,
595 | "a": 1
596 | }
597 | }
598 | ],
599 | "fills": [
600 | {
601 | "blendMode": "NORMAL",
602 | "type": "SOLID",
603 | "color": {
604 | "r": 1,
605 | "g": 1,
606 | "b": 1,
607 | "a": 1
608 | }
609 | }
610 | ],
611 | "strokes": [],
612 | "strokeWeight": 1,
613 | "strokeAlign": "INSIDE",
614 | "backgroundColor": {
615 | "r": 1,
616 | "g": 1,
617 | "b": 1,
618 | "a": 1
619 | },
620 | "effects": []
621 | },
622 | {
623 | "id": "45:0",
624 | "name": "Hello",
625 | "type": "TEXT",
626 | "blendMode": "PASS_THROUGH",
627 | "absoluteBoundingBox": {
628 | "x": -255,
629 | "y": -335,
630 | "width": 126,
631 | "height": 45
632 | },
633 | "constraints": {
634 | "vertical": "TOP",
635 | "horizontal": "LEFT"
636 | },
637 | "fills": [
638 | {
639 | "blendMode": "NORMAL",
640 | "type": "SOLID",
641 | "color": {
642 | "r": 0,
643 | "g": 0,
644 | "b": 0,
645 | "a": 1
646 | }
647 | }
648 | ],
649 | "strokes": [],
650 | "strokeWeight": 1,
651 | "strokeAlign": "OUTSIDE",
652 | "effects": [],
653 | "characters": "Hello",
654 | "style": {
655 | "fontFamily": "Roboto",
656 | "fontPostScriptName": null,
657 | "fontWeight": 400,
658 | "fontSize": 12,
659 | "textAlignHorizontal": "LEFT",
660 | "textAlignVertical": "TOP",
661 | "letterSpacing": 0,
662 | "lineHeightPx": 14.0625,
663 | "lineHeightPercent": 100,
664 | "lineHeightUnit": "INTRINSIC_%"
665 | },
666 | "characterStyleOverrides": [],
667 | "styleOverrideTable": {},
668 | "styles": {
669 | "text": "97:6"
670 | }
671 | }
672 | ],
673 | "backgroundColor": {
674 | "r": 0.8980392217636108,
675 | "g": 0.8980392217636108,
676 | "b": 0.8980392217636108,
677 | "a": 1
678 | },
679 | "prototypeStartNodeID": null,
680 | "prototypeDevice": {
681 | "type": "NONE",
682 | "rotation": "NONE"
683 | }
684 | },
685 | {
686 | "id": "28:4",
687 | "name": "Page 2",
688 | "type": "CANVAS",
689 | "children": [
690 | {
691 | "id": "28:5",
692 | "name": "Frame",
693 | "type": "FRAME",
694 | "blendMode": "PASS_THROUGH",
695 | "children": [
696 | {
697 | "id": "56:6",
698 | "name": "Group",
699 | "type": "GROUP",
700 | "blendMode": "PASS_THROUGH",
701 | "children": [
702 | {
703 | "id": "28:6",
704 | "name": "Rectangle",
705 | "type": "RECTANGLE",
706 | "blendMode": "PASS_THROUGH",
707 | "absoluteBoundingBox": {
708 | "x": -212,
709 | "y": -16,
710 | "width": 144,
711 | "height": 62
712 | },
713 | "constraints": {
714 | "vertical": "TOP",
715 | "horizontal": "LEFT"
716 | },
717 | "fills": [
718 | {
719 | "blendMode": "NORMAL",
720 | "type": "SOLID",
721 | "color": {
722 | "r": 0.7686274647712708,
723 | "g": 0.7686274647712708,
724 | "b": 0.7686274647712708,
725 | "a": 1
726 | }
727 | },
728 | {
729 | "blendMode": "NORMAL",
730 | "type": "GRADIENT_LINEAR",
731 | "gradientHandlePositions": [
732 | {
733 | "x": 0.5,
734 | "y": -3.0616171314629196e-17
735 | },
736 | {
737 | "x": 0.5,
738 | "y": 0.9999999999999999
739 | },
740 | {
741 | "x": 0,
742 | "y": 0
743 | }
744 | ],
745 | "gradientStops": [
746 | {
747 | "color": {
748 | "r": 1,
749 | "g": 1,
750 | "b": 1,
751 | "a": 1
752 | },
753 | "position": 0
754 | },
755 | {
756 | "color": {
757 | "r": 1,
758 | "g": 1,
759 | "b": 1,
760 | "a": 0
761 | },
762 | "position": 1
763 | }
764 | ]
765 | }
766 | ],
767 | "strokes": [],
768 | "strokeWeight": 1,
769 | "strokeAlign": "INSIDE",
770 | "effects": []
771 | }
772 | ],
773 | "absoluteBoundingBox": {
774 | "x": -212,
775 | "y": -16,
776 | "width": 144,
777 | "height": 62
778 | },
779 | "constraints": {
780 | "vertical": "TOP",
781 | "horizontal": "LEFT"
782 | },
783 | "clipsContent": false,
784 | "background": [],
785 | "fills": [],
786 | "strokes": [],
787 | "strokeWeight": 1,
788 | "strokeAlign": "INSIDE",
789 | "backgroundColor": {
790 | "r": 0,
791 | "g": 0,
792 | "b": 0,
793 | "a": 0
794 | },
795 | "effects": []
796 | }
797 | ],
798 | "absoluteBoundingBox": {
799 | "x": -294,
800 | "y": -65,
801 | "width": 305,
802 | "height": 136
803 | },
804 | "constraints": {
805 | "vertical": "TOP",
806 | "horizontal": "LEFT"
807 | },
808 | "clipsContent": true,
809 | "background": [
810 | {
811 | "blendMode": "NORMAL",
812 | "type": "SOLID",
813 | "color": {
814 | "r": 1,
815 | "g": 1,
816 | "b": 1,
817 | "a": 1
818 | }
819 | }
820 | ],
821 | "fills": [
822 | {
823 | "blendMode": "NORMAL",
824 | "type": "SOLID",
825 | "color": {
826 | "r": 1,
827 | "g": 1,
828 | "b": 1,
829 | "a": 1
830 | }
831 | }
832 | ],
833 | "strokes": [],
834 | "strokeWeight": 1,
835 | "strokeAlign": "INSIDE",
836 | "backgroundColor": {
837 | "r": 1,
838 | "g": 1,
839 | "b": 1,
840 | "a": 1
841 | },
842 | "effects": []
843 | }
844 | ],
845 | "backgroundColor": {
846 | "r": 0.8980392217636108,
847 | "g": 0.8980392217636108,
848 | "b": 0.8980392217636108,
849 | "a": 1
850 | },
851 | "prototypeStartNodeID": null,
852 | "prototypeDevice": {
853 | "type": "NONE",
854 | "rotation": "NONE"
855 | }
856 | }
857 | ]
858 | },
859 | "components": {
860 | "1:5": {
861 | "key": "",
862 | "name": "Rectangle",
863 | "description": ""
864 | }
865 | },
866 | "schemaVersion": 0,
867 | "styles": {
868 | "1:12": {
869 | "key": "ea017aed6616af00f3c4d59e3d945c8c3e47adca",
870 | "name": "Green",
871 | "styleType": "FILL",
872 | "description": ""
873 | },
874 | "388:0": {
875 | "key": "3b6c1841f2c88dd0d5deff6766852127675a6369",
876 | "name": "drop shadow",
877 | "styleType": "EFFECT",
878 | "description": ""
879 | },
880 | "1:11": {
881 | "key": "e234400b962ffafce654af9b3220ce88857523ec",
882 | "name": "Red",
883 | "styleType": "FILL",
884 | "description": ""
885 | },
886 | "387:0": {
887 | "key": "c95a769d3d480636cf66d18f475b3a6fb6eb03fe",
888 | "name": "Blue",
889 | "styleType": "FILL",
890 | "description": ""
891 | },
892 | "97:6": {
893 | "key": "cc806814e1b9b7d20ce0b6bed8adf52099899c01",
894 | "name": "Body",
895 | "styleType": "TEXT",
896 | "description": ""
897 | }
898 | },
899 | "name": "figma-graphql test file",
900 | "lastModified": "2020-06-28T13:21:09.007369Z",
901 | "thumbnailUrl": "https://s3-alpha-sig.figma.com/thumbnails/bfca1972-b085-422d-a3bb-98804917f8ab?Expires=1593993600&Signature=Hil~XV7irTJk1TbOTofE8tNKxs6nXM3s~YaQG0dOzLDicLA64tGbWzxiKuZUDIVz4ECom2xuh2WP-ngXor2R6TUqcWy0xjis8eiCJLSZ9qtEBtu1MZ4hhzyjyLDZ5AtNvPhmDlDYqE955ip5LGfUuAsbqWQithqjorHHy2tYzMdcYTIiT0YwlPgF2qDkVl2OFR4R93Ji-DbbeQj5-m2LUoV8PNCw~zC7C0pwPcXp1kNEQ3l6GXBvADf3CI28b3HcEPQwLVAzrYyvqse6lKOnZY3i7FNVqQqy5~gOkszH3YNHZfhMq9aaEf9WmpjZwo6s6YNxkSo4gwnHzAgFhcV24Q__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
902 | "version": "371462163",
903 | "role": "owner"
904 | }
905 |
--------------------------------------------------------------------------------
/src/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { Node } from "figma-js";
2 | import { uniqBy, groupBy, groupNodes } from "../utils";
3 |
4 | describe("uniqBy", () => {
5 | it("should return unique values by key", () => {
6 | const nodes = [
7 | { name: "node 1", id: "1", type: "VECTOR" },
8 | { name: "node 2", id: "2", type: "VECTOR" },
9 | { name: "node 2", id: "3", type: "VECTOR" },
10 | ];
11 |
12 | expect(uniqBy(nodes, "id")).toHaveLength(3);
13 | expect(uniqBy(nodes, "name")).toHaveLength(2);
14 | expect(uniqBy(nodes, "type")).toHaveLength(1);
15 | });
16 |
17 | it("should handle undefined array", () => {
18 | const nodes: any[] = [];
19 |
20 | expect(uniqBy(nodes, "id")).toHaveLength(0);
21 | });
22 | });
23 |
24 | describe("groupBy", () => {
25 | it("should group values by key", () => {
26 | const nodes = [
27 | { name: "node 1", id: "1", type: "VECTOR" },
28 | { name: "node 2", id: "2", type: "VECTOR" },
29 | { name: "node 2", id: "3", type: "VECTOR" },
30 | ];
31 |
32 | const groupById = groupBy(nodes, "id");
33 | expect(Object.keys(groupById)).toHaveLength(3);
34 | Object.values(groupById).forEach(val => {
35 | expect(val).toHaveLength(1);
36 | });
37 |
38 | const groupByName = groupBy(nodes, "name");
39 | expect(Object.keys(groupByName)).toHaveLength(2);
40 | expect(groupByName["node 1"]).toHaveLength(1);
41 | expect(groupByName["node 2"]).toHaveLength(2);
42 |
43 | const groupByType = groupBy(nodes, "type");
44 | expect(Object.keys(groupByType)).toHaveLength(1);
45 | expect(groupByType.VECTOR).toHaveLength(3);
46 | });
47 | });
48 |
49 | describe("groupNodes", () => {
50 | it("should group unique id nodes by type", () => {
51 | const nodes = [
52 | { name: "node 1", id: "1", type: "VECTOR" },
53 | { name: "node 2", id: "2", type: "VECTOR" },
54 | { name: "node 2", id: "3", type: "VECTOR" },
55 | { name: "node 4", id: "2", type: "VECTOR" },
56 | { name: "node 5", id: "5", type: "RECTANGLE" },
57 | ] as Node[];
58 |
59 | const groupedNodes = groupNodes(nodes);
60 |
61 | expect(groupedNodes.vectors).toHaveLength(3);
62 | expect(groupedNodes.rectangles).toHaveLength(1);
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Document,
3 | FileResponse,
4 | Style,
5 | ComponentMetadata,
6 | Node,
7 | } from "figma-js";
8 | import { groupNodes } from "./utils";
9 | import { ProcessedFile } from "./types";
10 |
11 | export function processFile(data: FileResponse, id?: string): ProcessedFile {
12 | const {
13 | name,
14 | lastModified,
15 | thumbnailUrl,
16 | version,
17 | document,
18 | styles,
19 | components,
20 | } = data;
21 |
22 | const [processedNodes, processedShortcuts] = processNodes(
23 | document,
24 | styles,
25 | components,
26 | id
27 | );
28 |
29 | return {
30 | fileId: id,
31 | name,
32 | lastModified,
33 | thumbnailUrl,
34 | version,
35 | children: processedNodes[0].children,
36 | shortcuts: groupNodes(processedShortcuts),
37 | };
38 | }
39 |
40 | export function processNodes(
41 | nodes: Document,
42 | documentStyles: { [key: string]: Style },
43 | components: { [key: string]: ComponentMetadata },
44 | fileId?: string
45 | ) {
46 | const parsedStyles = new Map(Object.entries(documentStyles));
47 | const parsedComponents = new Map(Object.entries(components));
48 |
49 | const traverseChildren = (node: any, parentId: string) => {
50 | const { id, styles, children, ...rest } = node;
51 | let nodeStyles: any[] = [];
52 |
53 | // If node has styles definitions populate that with the actual styles
54 | if (styles != null) {
55 | nodeStyles = Object.entries(styles).map(([key, styleId]) => {
56 | const documentStyle = parsedStyles.get(styleId as string);
57 |
58 | return {
59 | id: styleId,
60 | ...documentStyle,
61 | styles: node[`${key}s`],
62 | textStyles: node.style,
63 | type: "STYLE",
64 | };
65 | });
66 | }
67 |
68 | // Reached a leaf so returning the simplified node
69 | if (children == null || children.length === 0) {
70 | return [[{ id, parentId, fileId, ...rest }], nodeStyles];
71 | }
72 |
73 | // If it gets here then it means it has children
74 | // so we're going to recursively go through them
75 | // and combine everything
76 | const [parsedChildren, shortcuts] = children.reduce(
77 | (acc: [Node[], Node[]], child: Node) => {
78 | const [accChildren, accShortcuts] = acc;
79 | const [tChildren, tShortcuts] = traverseChildren(child, id);
80 | return [
81 | [...accChildren, ...tChildren],
82 | [...accShortcuts, ...tChildren, ...tShortcuts],
83 | ];
84 | },
85 | [[], []]
86 | );
87 |
88 | const componentInfo = parsedComponents.get(id);
89 |
90 | // Finally we return the parsed node with the
91 | // parsed children grouped by type
92 | const parsedNode = {
93 | id,
94 | parentId,
95 | fileId,
96 | ...rest,
97 | ...(componentInfo && componentInfo),
98 | children: parsedChildren,
99 | shortcuts: groupNodes(shortcuts),
100 | };
101 |
102 | return [[parsedNode], shortcuts];
103 | };
104 |
105 | return traverseChildren(nodes, "0:0");
106 | }
107 |
108 | export * from "./types";
109 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Node,
3 | NodeType,
4 | Component,
5 | Canvas,
6 | Instance,
7 | Line,
8 | Frame,
9 | Group,
10 | Vector,
11 | Star,
12 | Ellipse,
13 | BooleanGroup,
14 | RegularPolygon,
15 | Rectangle,
16 | Text,
17 | Slice,
18 | Style,
19 | ComponentMetadata,
20 | Paint,
21 | TypeStyle,
22 | } from "figma-js";
23 |
24 | export type ShortcutType = NodeType | "STYLE";
25 |
26 | export type NodeWithShortcuts = T & { shortcuts?: Shortcuts };
27 | export type StyleNode = Style & Node & { styles: Paint; textStyles: TypeStyle };
28 | export type Shortcuts = Record<
29 | "components",
30 | NodeWithShortcuts[]
31 | > &
32 | Record<"pages", NodeWithShortcuts