├── .circleci └── config.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.d.ts ├── package.json ├── src ├── __tests__ │ ├── allocatingId.test.ts │ ├── component.test.ts │ ├── create.test.ts │ ├── find.test.ts │ ├── getNodeById.test.ts │ ├── getPluginData.test.ts │ ├── globals.test.ts │ ├── group.test.ts │ ├── mixin.test.ts │ ├── page.test.ts │ └── postmessage.test.ts ├── applyMixins.ts ├── componentStubs.ts ├── config.ts ├── fonts.ts ├── index.ts ├── mixins.ts ├── stubs.ts └── styleStubs.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | 7 | defaults: &defaults 8 | working_directory: ~/repo 9 | docker: 10 | - image: circleci/node:12.6 11 | 12 | jobs: 13 | test: 14 | <<: *defaults 15 | 16 | steps: 17 | - checkout 18 | 19 | # Download and cache dependencies 20 | - restore_cache: 21 | keys: 22 | - v1-dependencies-{{ checksum "package.json" }} 23 | # fallback to using the latest cache if no exact match is found 24 | - v1-dependencies- 25 | 26 | - run: yarn --frozen-lockfile 27 | 28 | - run: 29 | name: Run TypeScript 30 | command: yarn tsc 31 | 32 | - run: 33 | name: Run tests 34 | command: yarn test --maxWorkers=2 --ci 35 | 36 | - save_cache: 37 | paths: 38 | - node_modules 39 | key: v1-dependencies-{{ checksum "package.json" }} 40 | 41 | - persist_to_workspace: 42 | root: ~/repo 43 | paths: . 44 | 45 | deploy: 46 | <<: *defaults 47 | steps: 48 | - attach_workspace: 49 | at: ~/repo 50 | - run: 51 | name: Authenticate with registry 52 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 53 | - run: 54 | name: Publish package 55 | command: npm publish 56 | 57 | workflows: 58 | version: 2 59 | test-deploy: 60 | jobs: 61 | - test: 62 | filters: 63 | tags: 64 | only: /^v.*/ 65 | - deploy: 66 | requires: 67 | - test 68 | filters: 69 | tags: 70 | only: /^v.*/ 71 | branches: 72 | ignore: /.*/ 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | /build 4 | .idea 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | 4 | # Coverage directory used by tools like istanbul 5 | coverage 6 | .nyc_output 7 | 8 | # Dependency directories 9 | node_modules 10 | 11 | # npm package lock 12 | package-lock.json 13 | yarn.lock 14 | 15 | # project files 16 | src 17 | test 18 | examples 19 | .travis.yml 20 | .babelrc 21 | .gitignore 22 | .flowconfig 23 | .prettierignore 24 | .prettierrc.json 25 | .idea 26 | .storybook 27 | demo.gif 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ilya Lesik 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma API Stub 2 | 3 | [![npm version](https://img.shields.io/npm/v/figma-api-stub.svg)](https://www.npmjs.com/package/figma-api-stub) 4 | [![CircleCI](https://circleci.com/gh/react-figma/figma-api-stub.svg?style=shield)](https://circleci.com/gh/react-figma/figma-api-stub) 5 | 6 | A stub implementation of [Figma Plugins API](https://www.figma.com/plugin-docs/intro/). 7 | 8 | ```javascript 9 | import {createFigma} from "figma-api-stub"; 10 | 11 | const figma = createFigma(); 12 | const rect = figma.createRectangle(); 13 | rect.resize(100, 200); 14 | ``` 15 | 16 | --- 17 | ⚠️ Warning! It's not an official implementation, and it hasn't the purpose to fully reproduce Figma behavior and API. 18 | 19 | --- 20 | 21 | ## Installation 22 | 23 | Install it with yarn: 24 | 25 | ``` 26 | yarn add figma-api-stub 27 | ``` 28 | 29 | Or with npm: 30 | 31 | ``` 32 | npm i figma-api-stub --save 33 | ``` 34 | 35 | ## API 36 | 37 | #### Stub api creation 38 | 39 | ```javascript 40 | createFigma(): PluginAPI 41 | ``` 42 | 43 | ## Used by 44 | 45 | - [react-figma](https://github.com/react-figma/react-figma) 46 | - [FigmaToCode](https://github.com/bernaferrari/FigmaToCode) 47 | - [figx](https://github.com/n0ruSh/figx) 48 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // import "@figma-plugin/types" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-api-stub", 3 | "version": "0.0.57", 4 | "description": "Figma API stub", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "tsc": "tsc -p tsconfig.build.json", 8 | "prettify": "prettier \"src/**/*.{js,jsx,ts,tsx}\" --ignore-path ./.prettierignore --write && git add . && git status", 9 | "build": "npm run build:clean && npm run build:lib", 10 | "build:clean": "rimraf dist", 11 | "build:lib": "cross-env BABEL_ENV=production tsc -p tsconfig.build.json", 12 | "prepublishOnly": "npm run build", 13 | "test": "jest" 14 | }, 15 | "jest": { 16 | "preset": "ts-jest/presets/js-with-babel" 17 | }, 18 | "pre-commit": [ 19 | "prettify", 20 | "tsc", 21 | "test" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+ssh://git@github.com/react-figma/figma-api-stub.git" 26 | }, 27 | "keywords": [ 28 | "figma", 29 | "figma-plugins" 30 | ], 31 | "author": "Ilya Lesik ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/react-figma/figma-api-stub/issues" 35 | }, 36 | "homepage": "https://github.com/react-figma/figma-api-stub#readme", 37 | "devDependencies": { 38 | "@figma/plugin-typings": "^1.42.1", 39 | "@types/jest": "^24.0.23", 40 | "cross-env": "^6.0.3", 41 | "jest": "^24.9.0", 42 | "pre-commit": "^1.2.2", 43 | "prettier": "^1.18.2", 44 | "rimraf": "^3.0.0", 45 | "ts-jest": "^24.2.0", 46 | "typescript": "3.5.3" 47 | }, 48 | "dependencies": { 49 | "nanoid": "^3.1.20", 50 | "rxjs": "^6.5.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/__tests__/allocatingId.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("allocating id", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({}); 7 | }); 8 | 9 | it("basic support", () => { 10 | const rect1 = figma.createRectangle(); 11 | const rect2 = figma.createRectangle(); 12 | expect(figma.root.id).toEqual("0:0"); 13 | expect(figma.currentPage.id).toEqual("0:1"); 14 | expect(rect1.id).toEqual("1:2"); 15 | expect(rect2.id).toEqual("1:3"); 16 | }); 17 | 18 | it("page ids increment", () => { 19 | const page1 = figma.currentPage; 20 | const page2 = figma.createPage(); 21 | const page3 = figma.createPage(); 22 | const page4 = figma.createPage(); 23 | expect(page1.id).toEqual("0:1"); 24 | expect(page2.id).toEqual("1:1"); 25 | expect(page3.id).toEqual("2:1"); 26 | expect(page4.id).toEqual("3:1"); 27 | }); 28 | 29 | it("node ids after page increment", () => { 30 | const page1 = figma.currentPage; 31 | const rect1 = figma.createRectangle(); 32 | const rect2 = figma.createRectangle(); 33 | const page2 = figma.createPage(); 34 | const rect3 = figma.createRectangle(); 35 | const rect4 = figma.createRectangle(); 36 | expect(page1.id).toEqual("0:1"); 37 | expect(rect1.id).toEqual("1:2"); 38 | expect(rect2.id).toEqual("1:3"); 39 | expect(page2.id).toEqual("1:1"); 40 | expect(rect3.id).toEqual("2:5"); 41 | expect(rect4.id).toEqual("2:6"); 42 | }); 43 | 44 | it("style id", () => { 45 | const style = figma.createPaintStyle(); 46 | expect(style.id.length).toEqual(43); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/__tests__/component.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("components ", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({}); 7 | }); 8 | 9 | it("generates an instance that's connected to the main component", () => { 10 | const component = figma.createComponent(); 11 | const instance = component.createInstance(); 12 | expect(instance.mainComponent).toBe(component); 13 | }); 14 | 15 | it("create ShapeWithTextNode", () => { 16 | const shapeWithTextNode = figma.createShapeWithText(); 17 | 18 | expect(shapeWithTextNode.type).toBe("SHAPE_WITH_TEXT"); 19 | expect(shapeWithTextNode.text.characters).toStrictEqual(""); 20 | }); 21 | 22 | it("group two nodes ", () => { 23 | const tN1 = figma.createText(); 24 | tN1.name = "sampleTextNode"; 25 | tN1.locked = false; 26 | tN1.visible = true; 27 | tN1.characters = "This is sample text"; 28 | const tN2 = figma.createText(); 29 | 30 | tN2.name = "anotherSampleTextNode"; 31 | tN2.locked = false; 32 | tN2.visible = true; 33 | tN2.characters = "This is another sample text."; 34 | 35 | figma.group([tN1, tN2], figma.currentPage, 0); 36 | expect(figma.currentPage.children.length).toBe(1); 37 | expect(figma.currentPage.children[0].type).toBe("GROUP"); 38 | const innerGroup = figma.currentPage.children[0] as GroupNode; 39 | expect(innerGroup.children.length).toBe(2); 40 | expect(innerGroup.children[0].id).toBe(tN1.id); 41 | expect(innerGroup.children[1].id).toBe(tN2.id); 42 | }); 43 | 44 | it("union two nodes ", () => { 45 | const tN1 = figma.createText(); 46 | tN1.name = "sampleTextNode"; 47 | tN1.locked = false; 48 | tN1.visible = true; 49 | tN1.characters = "This is sample text"; 50 | const tN2 = figma.createText(); 51 | 52 | tN2.name = "anotherSampleTextNode"; 53 | tN2.locked = false; 54 | tN2.visible = true; 55 | tN2.characters = "This is another sample text."; 56 | 57 | figma.union([tN1, tN2], figma.currentPage, 0); 58 | expect(figma.currentPage.children.length).toBe(1); 59 | expect(figma.currentPage.children[0].type).toBe("BOOLEAN_OPERATION"); 60 | expect( 61 | (figma.currentPage.children[0] as BooleanOperationNode).booleanOperation 62 | ).toBe("UNION"); 63 | 64 | const innerUnion = figma.currentPage.children[0] as BooleanOperationNode; 65 | expect(innerUnion.children.length).toBe(2); 66 | expect(innerUnion.children[0].id).toBe(tN1.id); 67 | expect(innerUnion.children[1].id).toBe(tN2.id); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/__tests__/create.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("create", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({}); 7 | }); 8 | 9 | it("create image", async () => { 10 | const bytes = new Uint8Array([1, 2, 3]); 11 | const image = figma.createImage(bytes); 12 | expect(image.hash).toBeDefined(); 13 | const _bytes = await image.getBytesAsync(); 14 | expect(_bytes).toEqual(new Uint8Array([1, 2, 3])); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/find.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("find functions", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({}); 7 | }); 8 | 9 | describe("findAll", () => { 10 | it("returns all children and subchildren that adheres to the given callback", () => { 11 | const component1 = figma.createComponent(); 12 | const rect1 = figma.createRectangle(); 13 | const rect2 = figma.createRectangle(); 14 | const rect3 = figma.createRectangle(); 15 | figma.group([rect1, rect2, rect3], component1); 16 | 17 | // component1.appendChild(rectGroup); 18 | 19 | const foundChildren = component1.findAll(() => true); 20 | 21 | expect(foundChildren.length).toBe(4); 22 | }); 23 | }); 24 | 25 | describe("findOne", () => { 26 | it("returns the first child or subchild that adheres to the given callback", () => { 27 | const component1 = figma.createComponent(); 28 | const rect1 = figma.createRectangle(); 29 | rect1.name = "FIND_ME"; 30 | const rect2 = figma.createRectangle(); 31 | const rect3 = figma.createRectangle(); 32 | 33 | figma.group([rect1, rect2, rect3], component1); 34 | 35 | const foundChild = component1.findOne(node => node.name === "FIND_ME"); 36 | 37 | expect(foundChild).not.toBeNull(); 38 | expect(foundChild).toBe(rect1); 39 | }); 40 | }); 41 | 42 | describe("findChildren", () => { 43 | it("returns all immediate children that adheres to the given callback", () => { 44 | const component1 = figma.createComponent(); 45 | const rect1 = figma.createRectangle(); 46 | rect1.name = "FIND_ME"; 47 | const rect2 = figma.createRectangle(); 48 | rect2.name = "FIND_ME"; 49 | const rect3 = figma.createRectangle(); 50 | rect3.name = "FIND_ME"; 51 | 52 | figma.group([rect1], component1); 53 | component1.appendChild(rect2); 54 | component1.appendChild(rect3); 55 | 56 | // component1.appendChild(rectGroup); 57 | 58 | const foundChildren = component1.findChildren( 59 | node => node.name === "FIND_ME" 60 | ); 61 | 62 | expect(foundChildren.length).toBe(2); 63 | expect(foundChildren).toContain(rect2); 64 | expect(foundChildren).toContain(rect3); 65 | }); 66 | }); 67 | 68 | describe("findChild", () => { 69 | it("returns the first immediate child that adheres to the given callback", () => { 70 | const component1 = figma.createComponent(); 71 | const rect1 = figma.createRectangle(); 72 | rect1.name = "FIND_ME"; 73 | const rect2 = figma.createRectangle(); 74 | rect2.name = "FIND_ME"; 75 | const rect3 = figma.createRectangle(); 76 | rect3.name = "FIND_ME"; 77 | 78 | figma.group([rect1], component1); 79 | component1.appendChild(rect2); 80 | component1.appendChild(rect3); 81 | 82 | // component1.appendChild(rectGroup); 83 | 84 | const foundChild = component1.findChild(node => node.name === "FIND_ME"); 85 | 86 | expect(foundChild).not.toBeNull(); 87 | expect(foundChild).toBe(rect2); 88 | }); 89 | }); 90 | 91 | describe("findAllWithCriteria", () => { 92 | it("returns all children that match the specified types", () => { 93 | const component1 = figma.createComponent(); 94 | const component2 = figma.createComponent(); 95 | const rect1 = figma.createRectangle(); 96 | const rect2 = figma.createRectangle(); 97 | const rect3 = figma.createRectangle(); 98 | const instance = component2.createInstance(); 99 | 100 | component1.appendChild(rect1); 101 | component1.appendChild(rect2); 102 | component1.appendChild(rect3); 103 | component1.appendChild(instance); 104 | 105 | const foundChildren = component1.findAllWithCriteria({ 106 | types: ["INSTANCE"] 107 | }); 108 | 109 | expect(foundChildren.length).toBe(1); 110 | expect(foundChildren[0]).toBe(instance); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/__tests__/getNodeById.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("getNodeById", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({}); 7 | // @ts-ignore 8 | figma.root.id = "0:0"; 9 | }); 10 | 11 | it("finding figma root", () => { 12 | // @ts-ignore 13 | figma.root.id = "0:0"; 14 | const root = figma.getNodeById("0:0"); 15 | expect(figma.root).toEqual(root); 16 | }); 17 | 18 | it("finding nested node ", () => { 19 | const page = figma.createPage(); 20 | figma.root.appendChild(page); 21 | 22 | const frame = figma.createFrame(); 23 | page.appendChild(frame); 24 | 25 | const rect1 = figma.createRectangle(); 26 | // @ts-ignore 27 | rect1.id = "2:1"; 28 | const rect2 = figma.createRectangle(); 29 | // @ts-ignore 30 | rect2.id = "2:2"; 31 | const rect3 = figma.createRectangle(); 32 | // @ts-ignore 33 | rect3.id = "2:2"; 34 | frame.appendChild(rect1); 35 | frame.appendChild(rect2); 36 | frame.appendChild(rect3); 37 | 38 | const rect2Found = figma.getNodeById("2:2"); 39 | expect(rect2Found.id).toEqual("2:2"); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__tests__/getPluginData.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("getPluginData", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({ 7 | simulateErrors: true 8 | }); 9 | }); 10 | 11 | it("basic support", () => { 12 | const rect = figma.createRectangle(); 13 | rect.setPluginData("key", "value"); 14 | expect(rect.getPluginData("key")).toEqual("value"); 15 | }); 16 | 17 | it("can retreive keys", () => { 18 | const rect = figma.createRectangle(); 19 | rect.setPluginData("key1", "value"); 20 | rect.setPluginData("key2", "value"); 21 | expect(rect.getPluginDataKeys()).toEqual(["key1", "key2"]); 22 | }); 23 | 24 | it("can retreive inherited pluginData keys from the main component", () => { 25 | const component = figma.createComponent(); 26 | const componentRect = figma.createRectangle(); 27 | componentRect.setPluginData("key1", "value1"); 28 | component.appendChild(componentRect); 29 | 30 | const instance = component.createInstance(); 31 | const instanceRect = instance.findOne(node => node.type === "RECTANGLE"); 32 | instanceRect.setPluginData("key2", "value2"); 33 | 34 | expect(componentRect.getPluginDataKeys()).toEqual( 35 | expect.arrayContaining(["key1"]) 36 | ); 37 | expect(componentRect.getPluginDataKeys()).toHaveLength(1); 38 | 39 | expect(instanceRect.getPluginDataKeys()).toEqual( 40 | expect.arrayContaining(["key1", "key2"]) 41 | ); 42 | expect(instanceRect.getPluginDataKeys()).toHaveLength(2); 43 | }); 44 | 45 | it("can retreive inherited sharedPluginData keys from the main component", () => { 46 | const component = figma.createComponent(); 47 | const componentRect = figma.createRectangle(); 48 | componentRect.setSharedPluginData("shared", "key1", "value1"); 49 | component.appendChild(componentRect); 50 | 51 | const instance = component.createInstance(); 52 | const instanceRect = instance.findOne(node => node.type === "RECTANGLE"); 53 | instanceRect.setSharedPluginData("shared", "key2", "value2"); 54 | 55 | expect(componentRect.getSharedPluginDataKeys("shared")).toEqual( 56 | expect.arrayContaining(["key1"]) 57 | ); 58 | expect(componentRect.getSharedPluginDataKeys("shared")).toHaveLength(1); 59 | 60 | expect(instanceRect.getSharedPluginDataKeys("shared")).toEqual( 61 | expect.arrayContaining(["key1", "key2"]) 62 | ); 63 | expect(instanceRect.getSharedPluginDataKeys("shared")).toHaveLength(2); 64 | }); 65 | 66 | it("removed node throws error", () => { 67 | const rect = figma.createRectangle(); 68 | rect.setPluginData("key", "value"); 69 | rect.remove(); 70 | expect(() => { 71 | rect.getPluginData("key"); 72 | }).toThrow("The node with id 1:2 does not exist"); 73 | }); 74 | }); 75 | 76 | describe("components and instances", () => { 77 | beforeEach(() => { 78 | // @ts-ignore 79 | global.figma = createFigma({ 80 | simulateErrors: true 81 | }); 82 | }); 83 | 84 | describe("pluginData", () => { 85 | it("instances inherit plugin data from main component", () => { 86 | const component = figma.createComponent(); 87 | component.setPluginData("foo", "bar"); 88 | 89 | const instance = component.createInstance(); 90 | 91 | expect(instance.getPluginData("foo")).toBe("bar"); 92 | }); 93 | 94 | it("the children of instances inherit data from the main component", () => { 95 | const component = figma.createComponent(); 96 | const componentRect = figma.createRectangle(); 97 | componentRect.setPluginData("foo", "bar"); 98 | component.appendChild(componentRect); 99 | 100 | const instance = component.createInstance(); 101 | const instanceRect = instance.findOne(node => node.type === "RECTANGLE"); 102 | 103 | expect(instanceRect).not.toBeNull(); 104 | expect(instanceRect.getPluginData("foo")).toBe("bar"); 105 | }); 106 | 107 | it("modifying the plugin data of the main component modifies the instance", () => { 108 | const component = figma.createComponent(); 109 | component.setPluginData("foo", "bar"); 110 | 111 | const instance = component.createInstance(); 112 | 113 | component.setPluginData("foo", "baz"); 114 | 115 | expect(instance.getPluginData("foo")).toBe("baz"); 116 | }); 117 | 118 | it("modifying the plugin data of the main component child modifies the instance child", () => { 119 | const component = figma.createComponent(); 120 | const componentRect = figma.createRectangle(); 121 | componentRect.setPluginData("foo", "bar"); 122 | component.appendChild(componentRect); 123 | 124 | const instance = component.createInstance(); 125 | componentRect.setPluginData("foo", "baz"); 126 | const instanceRect = instance.findOne(node => node.type === "RECTANGLE"); 127 | 128 | expect(instanceRect).not.toBeNull(); 129 | expect(instanceRect.getPluginData("foo")).toBe("baz"); 130 | }); 131 | 132 | it("instances can override the main component plugin data", () => { 133 | const component = figma.createComponent(); 134 | component.setPluginData("foo", "bar"); 135 | 136 | const instance = component.createInstance(); 137 | instance.setPluginData("foo", "baz"); 138 | 139 | expect(instance.getPluginData("foo")).toBe("baz"); 140 | }); 141 | 142 | it('setting plugin data to "" deletes that key and reverts to the main component\'s pluginData for that key', () => { 143 | const component = figma.createComponent(); 144 | component.setPluginData("foo", "bar"); 145 | 146 | const instance = component.createInstance(); 147 | instance.setPluginData("foo", "baz"); 148 | 149 | expect(instance.getPluginData("foo")).toBe("baz"); 150 | 151 | instance.setPluginData("foo", ""); 152 | expect(instance.getPluginData("foo")).toBe("bar"); 153 | }); 154 | 155 | it("the children of instances can override the children of main component plugin data", () => { 156 | const component = figma.createComponent(); 157 | const componentRect = figma.createRectangle(); 158 | component.appendChild(componentRect); 159 | componentRect.setPluginData("foo", "bar"); 160 | 161 | const instance = component.createInstance(); 162 | const instanceRect = instance.findOne( 163 | child => child.type === "RECTANGLE" 164 | ); 165 | instanceRect.setPluginData("foo", "baz"); 166 | 167 | expect(instanceRect.getPluginData("foo")).toBe("baz"); 168 | expect(componentRect.getPluginData("foo")).toBe("bar"); 169 | }); 170 | 171 | it("setting plugin data to \"\" in an instance child deletes that key and reverts to the main component's corresponding child's pluginData for that key", () => { 172 | const component = figma.createComponent(); 173 | const componentRect = figma.createRectangle(); 174 | component.appendChild(componentRect); 175 | componentRect.setPluginData("foo", "bar"); 176 | 177 | const instance = component.createInstance(); 178 | const instanceRect = instance.findOne( 179 | child => child.type === "RECTANGLE" 180 | ); 181 | instanceRect.setPluginData("foo", "baz"); 182 | 183 | expect(instanceRect.getPluginData("foo")).toBe("baz"); 184 | 185 | instanceRect.setPluginData("foo", ""); 186 | 187 | expect(componentRect.getPluginData("foo")).toBe("bar"); 188 | expect(instanceRect.getPluginData("foo")).toBe("bar"); 189 | }); 190 | }); 191 | 192 | describe("sharedPluginData", () => { 193 | it("instances inherit plugin data from main component", () => { 194 | const component = figma.createComponent(); 195 | component.setSharedPluginData("shared", "foo", "bar"); 196 | 197 | const instance = component.createInstance(); 198 | 199 | expect(instance.getSharedPluginData("shared", "foo")).toBe("bar"); 200 | }); 201 | 202 | it("the children of instances inherit data from the main component", () => { 203 | const component = figma.createComponent(); 204 | const componentRect = figma.createRectangle(); 205 | componentRect.setSharedPluginData("shared", "foo", "bar"); 206 | component.appendChild(componentRect); 207 | 208 | const instance = component.createInstance(); 209 | const instanceRect = instance.findOne(node => node.type === "RECTANGLE"); 210 | 211 | expect(instanceRect).not.toBeNull(); 212 | expect(instanceRect.getSharedPluginData("shared", "foo")).toBe("bar"); 213 | }); 214 | 215 | it("modifying the plugin data of the main component modifies the instance", () => { 216 | const component = figma.createComponent(); 217 | component.setSharedPluginData("shared", "foo", "bar"); 218 | 219 | const instance = component.createInstance(); 220 | 221 | component.setSharedPluginData("shared", "foo", "baz"); 222 | 223 | expect(instance.getSharedPluginData("shared", "foo")).toBe("baz"); 224 | }); 225 | 226 | it("modifying the plugin data of the main component child modifies the instance child", () => { 227 | const component = figma.createComponent(); 228 | const componentRect = figma.createRectangle(); 229 | componentRect.setSharedPluginData("shared", "foo", "bar"); 230 | component.appendChild(componentRect); 231 | 232 | const instance = component.createInstance(); 233 | componentRect.setSharedPluginData("shared", "foo", "baz"); 234 | const instanceRect = instance.findOne(node => node.type === "RECTANGLE"); 235 | 236 | expect(instanceRect).not.toBeNull(); 237 | expect(instanceRect.getSharedPluginData("shared", "foo")).toBe("baz"); 238 | }); 239 | 240 | it("instances can override the main component plugin data", () => { 241 | const component = figma.createComponent(); 242 | component.setSharedPluginData("shared", "foo", "bar"); 243 | 244 | const instance = component.createInstance(); 245 | instance.setSharedPluginData("shared", "foo", "baz"); 246 | 247 | expect(instance.getSharedPluginData("shared", "foo")).toBe("baz"); 248 | }); 249 | 250 | it('setting plugin data to "" deletes that key and reverts to the main component\'s pluginData for that key', () => { 251 | const component = figma.createComponent(); 252 | component.setSharedPluginData("shared", "foo", "bar"); 253 | 254 | const instance = component.createInstance(); 255 | instance.setSharedPluginData("shared", "foo", "baz"); 256 | 257 | expect(instance.getSharedPluginData("shared", "foo")).toBe("baz"); 258 | 259 | instance.setSharedPluginData("shared", "foo", ""); 260 | expect(instance.getSharedPluginData("shared", "foo")).toBe("bar"); 261 | }); 262 | 263 | it("the children of instances can override the children of main component plugin data", () => { 264 | const component = figma.createComponent(); 265 | const componentRect = figma.createRectangle(); 266 | component.appendChild(componentRect); 267 | componentRect.setSharedPluginData("shared", "foo", "bar"); 268 | 269 | const instance = component.createInstance(); 270 | const instanceRect = instance.findOne( 271 | child => child.type === "RECTANGLE" 272 | ); 273 | instanceRect.setSharedPluginData("shared", "foo", "baz"); 274 | 275 | expect(instanceRect.getSharedPluginData("shared", "foo")).toBe("baz"); 276 | expect(componentRect.getSharedPluginData("shared", "foo")).toBe("bar"); 277 | }); 278 | 279 | it("setting plugin data to \"\" in an instance child deletes that key and reverts to the main component's corresponding child's pluginData for that key", () => { 280 | const component = figma.createComponent(); 281 | const componentRect = figma.createRectangle(); 282 | component.appendChild(componentRect); 283 | componentRect.setSharedPluginData("shared", "foo", "bar"); 284 | 285 | const instance = component.createInstance(); 286 | const instanceRect = instance.findOne( 287 | child => child.type === "RECTANGLE" 288 | ); 289 | instanceRect.setSharedPluginData("shared", "foo", "baz"); 290 | 291 | expect(instanceRect.getSharedPluginData("shared", "foo")).toBe("baz"); 292 | 293 | instanceRect.setSharedPluginData("shared", "foo", ""); 294 | 295 | expect(componentRect.getSharedPluginData("shared", "foo")).toBe("bar"); 296 | expect(instanceRect.getSharedPluginData("shared", "foo")).toBe("bar"); 297 | }); 298 | 299 | it("if an instance attempts to get plugin data before its set on the main component, it can still access main component data after it's been defined", () => { 300 | const component = figma.createComponent(); 301 | const componentRect = figma.createRectangle(); 302 | component.appendChild(componentRect); 303 | 304 | const instance = component.createInstance(); 305 | const instanceRect = instance.findOne( 306 | child => child.type === "RECTANGLE" 307 | ); 308 | instanceRect.setSharedPluginData("shared", "foo", "bar"); 309 | componentRect.setSharedPluginData("shared", "baz", "bing"); 310 | 311 | expect(instanceRect.getSharedPluginData("shared", "foo")).toBe("bar"); 312 | expect(instanceRect.getSharedPluginData("shared", "baz")).toBe("bing"); 313 | expect( 314 | componentRect.getSharedPluginData("shared", "foo") 315 | ).toBeUndefined(); 316 | expect(componentRect.getSharedPluginData("shared", "baz")).toBe("bing"); 317 | }); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /src/__tests__/globals.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("globals", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({}); 7 | }); 8 | 9 | it("can use showUI with global variables", () => { 10 | expect(() => { 11 | figma.showUI(__html__); 12 | }).not.toThrow(); 13 | expect(() => { 14 | figma.showUI(__uiFiles__.secondary); 15 | }).not.toThrow(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/group.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("group", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({ 7 | simulateErrors: true 8 | }); 9 | }); 10 | 11 | it("Applying constraints raises error", () => { 12 | const rect = figma.createRectangle(); 13 | const group = figma.group([rect], figma.currentPage); 14 | expect(() => { 15 | // @ts-ignore 16 | group.constraints = { 17 | horizontal: "MIN", 18 | vertical: "MIN" 19 | }; 20 | }).toThrowError( 21 | "Error: Cannot add property constraints, object is not extensible" 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/mixin.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("Mixin", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({ 7 | simulateErrors: true 8 | }); 9 | }); 10 | 11 | it("GeometryMixin", () => { 12 | const page = figma.createPage(); 13 | const text = figma.createText(); 14 | const frame = figma.createFrame(); 15 | const instance = figma.createComponent().createInstance(); 16 | 17 | expect("fills" in figma.root).toBeFalsy(); 18 | expect("fills" in page).toBeFalsy(); 19 | expect("fills" in instance).toBeFalsy(); 20 | expect("fills" in frame).toBeTruthy(); 21 | expect("fills" in text).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/page.test.ts: -------------------------------------------------------------------------------- 1 | import { createFigma } from "../stubs"; 2 | 3 | describe("page ", () => { 4 | beforeEach(() => { 5 | // @ts-ignore 6 | global.figma = createFigma({ 7 | simulateErrors: true 8 | }); 9 | }); 10 | 11 | it("assign empty backgrounds", () => { 12 | const page = figma.createPage(); 13 | 14 | expect(() => { 15 | page.backgrounds = []; 16 | }).toThrow( 17 | "Error: in set_backgrounds: Page backgrounds must be a single solid paint" 18 | ); 19 | }); 20 | 21 | it("assign non solid background", () => { 22 | const page = figma.createPage(); 23 | 24 | expect(() => { 25 | page.backgrounds = [ 26 | { 27 | type: "GRADIENT_LINEAR", 28 | gradientTransform: [[0, 0, 0], [0, 0, 0]], 29 | gradientStops: [] 30 | } 31 | ]; 32 | }).toThrow( 33 | "Error: in set_backgrounds: Page backgrounds must be a single solid paint" 34 | ); 35 | }); 36 | 37 | it("assign more than one backgrounds", () => { 38 | const page = figma.createPage(); 39 | 40 | expect(() => { 41 | page.backgrounds = [ 42 | { 43 | type: "SOLID", 44 | visible: true, 45 | opacity: 1, 46 | blendMode: "NORMAL", 47 | color: { 48 | r: 0.9607843160629272, 49 | g: 0.9607843160629272, 50 | b: 0.9607843160629272 51 | } 52 | }, 53 | { 54 | type: "SOLID", 55 | visible: true, 56 | opacity: 1, 57 | blendMode: "NORMAL", 58 | color: { 59 | r: 0.9607843160629272, 60 | g: 0.9607843160629272, 61 | b: 0.9607843160629272 62 | } 63 | } 64 | ]; 65 | }).toThrow( 66 | "Error: in set_backgrounds: Page backgrounds must be a single solid paint" 67 | ); 68 | }); 69 | 70 | it("assign correctly", () => { 71 | const page = figma.createPage(); 72 | 73 | page.backgrounds = [ 74 | { 75 | type: "SOLID", 76 | visible: true, 77 | opacity: 1, 78 | blendMode: "NORMAL", 79 | color: { 80 | r: 1, 81 | g: 0, 82 | b: 0 83 | } 84 | } 85 | ]; 86 | 87 | expect((page.backgrounds[0] as any).color).toStrictEqual({ 88 | r: 1, 89 | g: 0, 90 | b: 0 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/__tests__/postmessage.test.ts: -------------------------------------------------------------------------------- 1 | import { createParentPostMessage, createFigma } from "../stubs"; 2 | import { Subject } from "rxjs"; 3 | import { take } from "rxjs/operators"; 4 | 5 | describe("postMessage", () => { 6 | beforeEach(() => { 7 | // @ts-ignore 8 | global.figma = createFigma({}); 9 | // @ts-ignore 10 | global.parent.postMessage = createParentPostMessage(global.figma); 11 | }); 12 | 13 | it("UI sends message and plugin receives it", async () => { 14 | const waiting = new Subject(); 15 | // @ts-ignore 16 | figma.ui.onmessage = jest.fn().mockImplementation(() => waiting.next()); 17 | parent.postMessage({ pluginMessage: "abc" }, "*"); 18 | 19 | return new Promise(resolve => { 20 | waiting.pipe(take(1)).subscribe(() => { 21 | // @ts-ignore 22 | expect(figma.ui.onmessage).toHaveBeenCalledTimes(1); 23 | // @ts-ignore 24 | expect(figma.ui.onmessage).toHaveBeenCalledWith( 25 | "abc", 26 | expect.any(Object) 27 | ); 28 | resolve(); 29 | }); 30 | }); 31 | }); 32 | 33 | it("Plugin sends message and UI receives it", () => { 34 | const waiting = new Subject(); 35 | 36 | //@ts-ignore 37 | global.onmessage = jest.fn().mockImplementation(() => waiting.next()); 38 | // @ts-ignore 39 | figma.ui.postMessage("abc"); 40 | 41 | return new Promise(resolve => { 42 | waiting.pipe(take(1)).subscribe(() => { 43 | //@ts-ignore 44 | expect(global.onmessage).toHaveBeenCalledTimes(1); 45 | resolve(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('on("message")', () => { 52 | beforeAll(() => { 53 | jest.useFakeTimers(); 54 | }); 55 | 56 | beforeEach(() => { 57 | // @ts-ignore 58 | global.figma = createFigma({}); 59 | // @ts-ignore 60 | global.parent.postMessage = createParentPostMessage(global.figma); 61 | }); 62 | 63 | it("can add a listener for a message", () => { 64 | const cb = jest.fn(); 65 | figma.ui.on("message", cb); 66 | 67 | parent.postMessage({ pluginMessage: "abc" }, "*"); 68 | parent.postMessage({ pluginMessage: "def" }, "*"); 69 | 70 | jest.runAllTimers(); 71 | 72 | expect(cb).toHaveBeenCalledTimes(2); 73 | expect(cb).toHaveBeenCalledWith("abc", expect.any(Object)); 74 | expect(cb).toHaveBeenCalledWith("def", expect.any(Object)); 75 | }); 76 | 77 | it("can remove a listener for a message", () => { 78 | const cb = jest.fn(); 79 | figma.ui.on("message", cb); 80 | 81 | parent.postMessage({ pluginMessage: "abc" }, "*"); 82 | 83 | jest.runAllTimers(); 84 | 85 | expect(cb).toHaveBeenCalledTimes(1); 86 | expect(cb).toHaveBeenCalledWith("abc", expect.any(Object)); 87 | 88 | cb.mockClear(); 89 | figma.ui.off("message", cb); 90 | 91 | parent.postMessage({ pluginMessage: "def" }, "*"); 92 | 93 | expect(cb).not.toHaveBeenCalled(); 94 | }); 95 | 96 | it("can call a listener once", () => { 97 | const cb = jest.fn(); 98 | figma.ui.once("message", cb); 99 | 100 | parent.postMessage({ pluginMessage: "abc" }, "*"); 101 | parent.postMessage({ pluginMessage: "def" }, "*"); 102 | 103 | jest.runAllTimers(); 104 | 105 | expect(cb).toHaveBeenCalledTimes(1); 106 | expect(cb).toHaveBeenCalledWith("abc", expect.any(Object)); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/applyMixins.ts: -------------------------------------------------------------------------------- 1 | export function applyMixins(derivedCtor: any, baseCtors: any[]) { 2 | baseCtors.forEach(baseCtor => { 3 | Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { 4 | Object.defineProperty( 5 | derivedCtor.prototype, 6 | name, 7 | Object.getOwnPropertyDescriptor(baseCtor.prototype, name) 8 | ); 9 | }); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/componentStubs.ts: -------------------------------------------------------------------------------- 1 | import { TConfig } from "./config"; 2 | import { Fonts } from "./fonts"; 3 | import { nanoid } from "nanoid"; 4 | import { Subject } from "rxjs"; 5 | 6 | export const selectionChangeSubject = new Subject(); 7 | 8 | export class RectangleNodeStub { 9 | constructor(private config: TConfig) {} 10 | 11 | type = "RECTANGLE"; 12 | } 13 | 14 | const defaultFont = { family: "Inter", style: "Regular" }; 15 | const defaultRangeListOptions = { type: "NONE" } as const; 16 | 17 | export class TextNodeStub { 18 | constructor(private config: TConfig) {} 19 | 20 | type = "TEXT"; 21 | private _fontName: FontName; 22 | private _characters: string; 23 | private _textAutoResize: string; 24 | private _rangeListOptions: TextListOptions | PluginAPI["mixed"]; 25 | get fontName() { 26 | return this._fontName || defaultFont; 27 | } 28 | set fontName(fontName) { 29 | if (this.config.simulateErrors && !fontName) { 30 | throw new Error(`Error: fontName is undefined`); 31 | } 32 | this._fontName = fontName; 33 | } 34 | get characters() { 35 | return this._characters || ""; 36 | } 37 | set characters(characters) { 38 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this.fontName)) { 39 | throw new Error( 40 | `Error: font is not loaded ${this.fontName.family} ${this.fontName.style}` 41 | ); 42 | } 43 | this._characters = characters; 44 | } 45 | get textAutoResize() { 46 | return this._textAutoResize; 47 | } 48 | set textAutoResize(value) { 49 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this.fontName)) { 50 | throw new Error( 51 | `Error: font is not loaded ${this.fontName.family} ${this.fontName.style}` 52 | ); 53 | } 54 | this._textAutoResize = value; 55 | } 56 | getRangeFontName(start: number, end: number): FontName | PluginAPI["mixed"] { 57 | if (this.config.simulateErrors && start < 0) { 58 | throw new Error(`Error: Expected "start" to have value >=0`); 59 | } 60 | if (this.config.simulateErrors && end < 0) { 61 | throw new Error(`Error: Expected "end" to have value >=0`); 62 | } 63 | if (this.config.simulateErrors && end > this._characters.length) { 64 | throw new Error( 65 | `Error: Range outside of available characters. 'start' must be less than node.characters.length and 'end' must be less than or equal to node.characters.length` 66 | ); 67 | } 68 | if (this.config.simulateErrors && end === start) { 69 | throw new Error( 70 | `Error: Empty range selected. 'end' must be greater than 'start'` 71 | ); 72 | } 73 | return this._fontName || defaultFont; 74 | } 75 | deleteCharacters(start: number, end: number): void { 76 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this.fontName)) { 77 | throw new Error( 78 | `Error: font is not loaded ${this.fontName.family} ${this.fontName.style}` 79 | ); 80 | } 81 | if (this.config.simulateErrors && start < 0) { 82 | throw new Error(`Error: Expected "start" to have value >=0`); 83 | } 84 | if (this.config.simulateErrors && end < 0) { 85 | throw new Error(`Error: Expected "end" to have value >=0`); 86 | } 87 | if (this.config.simulateErrors && end > this._characters.length) { 88 | throw new Error( 89 | `Error: Cannot delete characters at index greater than the length of the text` 90 | ); 91 | } 92 | this._characters = 93 | this._characters.slice(start, end) + 94 | (end === this._characters.length ? "" : this._characters.slice(end + 1)); 95 | } 96 | insertCharacters( 97 | start: number, 98 | characters: string, 99 | _useStyle: "BEFORE" | "AFTER" = "BEFORE" 100 | ): void { 101 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this.fontName)) { 102 | throw new Error( 103 | `Error: font is not loaded ${this.fontName.family} ${this.fontName.style}` 104 | ); 105 | } 106 | if (this.config.simulateErrors && start < 0) { 107 | throw new Error(`Error: Expected "start" to have value >=0`); 108 | } 109 | if (this.config.simulateErrors && start > this._characters.length) { 110 | throw new Error( 111 | `Error: Cannot insert characters at index greater than the length of the text` 112 | ); 113 | } 114 | this._characters = [ 115 | this._characters.slice(0, start), 116 | characters, 117 | this._characters.slice(start) 118 | ].join(""); 119 | } 120 | getRangeListOptions( 121 | start: number, 122 | end: number 123 | ): TextListOptions | PluginAPI["mixed"] { 124 | if (this.config.simulateErrors && start < 0) { 125 | throw new Error(`Error: Expected "start" to have value >=0`); 126 | } 127 | if (this.config.simulateErrors && end < 0) { 128 | throw new Error(`Error: Expected "end" to have value >=0`); 129 | } 130 | if (this.config.simulateErrors && end > this._characters.length) { 131 | throw new Error( 132 | `Error: Range outside of available characters. 'start' must be less than node.characters.length and 'end' must be less than or equal to node.characters.length` 133 | ); 134 | } 135 | if (this.config.simulateErrors && end === start) { 136 | throw new Error( 137 | `Error: Empty range selected. 'end' must be greater than 'start'` 138 | ); 139 | } 140 | return this._rangeListOptions || defaultRangeListOptions; 141 | } 142 | setRangeListOptions(start: number, end: number, value: TextListOptions) { 143 | if (this.config.simulateErrors && start < 0) { 144 | throw new Error(`Error: Expected "start" to have value >=0`); 145 | } 146 | if (this.config.simulateErrors && end < 0) { 147 | throw new Error(`Error: Expected "end" to have value >=0`); 148 | } 149 | if (this.config.simulateErrors && end > this._characters.length) { 150 | throw new Error( 151 | `Error: Range outside of available characters. 'start' must be less than node.characters.length and 'end' must be less than or equal to node.characters.length` 152 | ); 153 | } 154 | if (this.config.simulateErrors && end === start) { 155 | throw new Error( 156 | `Error: Empty range selected. 'end' must be greater than 'start'` 157 | ); 158 | } 159 | this._rangeListOptions = value; 160 | } 161 | } 162 | 163 | export class TextSublayerNode { 164 | readonly hasMissingFont; 165 | paragraphIndent: number; 166 | paragraphSpacing: number; 167 | fontSize: number | PluginAPI["mixed"]; 168 | textCase: TextCase | PluginAPI["mixed"]; 169 | textDecoration: TextDecoration | PluginAPI["mixed"]; 170 | letterSpacing: LetterSpacing | PluginAPI["mixed"]; 171 | hyperlink: HyperlinkTarget | null | PluginAPI["mixed"]; 172 | 173 | private _fontName: FontName; 174 | private _characters: string; 175 | private _rangeListOptions: TextListOptions | PluginAPI["mixed"]; 176 | 177 | get fontName() { 178 | return this._fontName || defaultFont; 179 | } 180 | set fontName(fontName) { 181 | if (this.config.simulateErrors && !fontName) { 182 | throw new Error(`Error: fontName is undefined`); 183 | } 184 | this._fontName = fontName; 185 | } 186 | get characters() { 187 | return this._characters || ""; 188 | } 189 | set characters(characters) { 190 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this.fontName)) { 191 | throw new Error( 192 | `Error: font is not loaded ${this.fontName.family} ${this.fontName.style}` 193 | ); 194 | } 195 | this._characters = characters; 196 | } 197 | 198 | constructor(private config: TConfig) {} 199 | 200 | insertCharacters( 201 | start: number, 202 | characters: string, 203 | _useStyle: "BEFORE" | "AFTER" = "BEFORE" 204 | ): void { 205 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this._fontName)) { 206 | throw new Error( 207 | `Error: font is not loaded ${(this._fontName as FontName).family} ${ 208 | (this._fontName as FontName).style 209 | }` 210 | ); 211 | } 212 | if (this.config.simulateErrors && start < 0) { 213 | throw new Error(`Error: Expected "start" to have value >=0`); 214 | } 215 | if (this.config.simulateErrors && start > this._characters.length) { 216 | throw new Error( 217 | `Error: Cannot insert characters at index greater than the length of the text` 218 | ); 219 | } 220 | this._characters = [ 221 | this._characters.slice(0, start), 222 | characters, 223 | this._characters.slice(start) 224 | ].join(""); 225 | } 226 | 227 | deleteCharacters(start: number, end: number): void { 228 | if (this.config.simulateErrors && !Fonts.isFontLoaded(this._fontName)) { 229 | throw new Error( 230 | `Error: font is not loaded ${(this._fontName as FontName).family} ${ 231 | (this._fontName as FontName).style 232 | }` 233 | ); 234 | } 235 | if (this.config.simulateErrors && start < 0) { 236 | throw new Error(`Error: Expected "start" to have value >=0`); 237 | } 238 | if (this.config.simulateErrors && end < 0) { 239 | throw new Error(`Error: Expected "end" to have value >=0`); 240 | } 241 | if (this.config.simulateErrors && end > this._characters.length) { 242 | throw new Error( 243 | `Error: Cannot delete characters at index greater than the length of the text` 244 | ); 245 | } 246 | this._characters = 247 | this._characters.slice(start, end) + 248 | (end === this._characters.length ? "" : this._characters.slice(end + 1)); 249 | } 250 | 251 | getRangeFontName(start: number, end: number): FontName | PluginAPI["mixed"] { 252 | if (this.config.simulateErrors && start < 0) { 253 | throw new Error(`Error: Expected "start" to have value >=0`); 254 | } 255 | if (this.config.simulateErrors && end < 0) { 256 | throw new Error(`Error: Expected "end" to have value >=0`); 257 | } 258 | if (this.config.simulateErrors && end > this._characters.length) { 259 | throw new Error( 260 | `Error: Range outside of available characters. 'start' must be less than node.characters.length and 'end' must be less than or equal to node.characters.length` 261 | ); 262 | } 263 | if (this.config.simulateErrors && end === start) { 264 | throw new Error( 265 | `Error: Empty range selected. 'end' must be greater than 'start'` 266 | ); 267 | } 268 | return this._fontName || defaultFont; 269 | } 270 | 271 | getRangeListOptions( 272 | start: number, 273 | end: number 274 | ): TextListOptions | PluginAPI["mixed"] { 275 | if (this.config.simulateErrors && start < 0) { 276 | throw new Error(`Error: Expected "start" to have value >=0`); 277 | } 278 | if (this.config.simulateErrors && end < 0) { 279 | throw new Error(`Error: Expected "end" to have value >=0`); 280 | } 281 | if (this.config.simulateErrors && end > this._characters.length) { 282 | throw new Error( 283 | `Error: Range outside of available characters. 'start' must be less than node.characters.length and 'end' must be less than or equal to node.characters.length` 284 | ); 285 | } 286 | if (this.config.simulateErrors && end === start) { 287 | throw new Error( 288 | `Error: Empty range selected. 'end' must be greater than 'start'` 289 | ); 290 | } 291 | return this._rangeListOptions || defaultRangeListOptions; 292 | } 293 | 294 | setRangeListOptions(start: number, end: number, value: TextListOptions) { 295 | if (this.config.simulateErrors && start < 0) { 296 | throw new Error(`Error: Expected "start" to have value >=0`); 297 | } 298 | if (this.config.simulateErrors && end < 0) { 299 | throw new Error(`Error: Expected "end" to have value >=0`); 300 | } 301 | if (this.config.simulateErrors && end > this._characters.length) { 302 | throw new Error( 303 | `Error: Range outside of available characters. 'start' must be less than node.characters.length and 'end' must be less than or equal to node.characters.length` 304 | ); 305 | } 306 | if (this.config.simulateErrors && end === start) { 307 | throw new Error( 308 | `Error: Empty range selected. 'end' must be greater than 'start'` 309 | ); 310 | } 311 | this._rangeListOptions = value; 312 | } 313 | } 314 | 315 | export class ShapeWithTextNodeStub { 316 | type = "SHAPE_WITH_TEXT"; 317 | private _text: TextSublayerNode; 318 | private _cornerRadius = 50; 319 | shapeType: 320 | | "SQUARE" 321 | | "ELLIPSE" 322 | | "ROUNDED_RECTANGLE" 323 | | "DIAMOND" 324 | | "TRIANGLE_UP" 325 | | "TRIANGLE_DOWN" 326 | | "PARALLELOGRAM_RIGHT" 327 | | "PARALLELOGRAM_LEFT" 328 | | "ENG_DATABASE" 329 | | "ENG_QUEUE" 330 | | "ENG_FILE" 331 | | "ENG_FOLDER" = "ELLIPSE"; 332 | rotation = 0; 333 | 334 | constructor(private config: TConfig) { 335 | this._text = new TextSublayerNode(this.config); 336 | } 337 | 338 | get text() { 339 | return this._text; 340 | } 341 | 342 | get cornerRadius() { 343 | return this._cornerRadius; 344 | } 345 | } 346 | 347 | export class StickyNodeStub { 348 | type = "STICKY"; 349 | private _text: TextSublayerNode; 350 | authorVisible = true; 351 | authorName = ""; 352 | 353 | constructor(private config: TConfig) { 354 | this._text = new TextSublayerNode(this.config); 355 | } 356 | 357 | get text() { 358 | return this._text; 359 | } 360 | } 361 | 362 | export class ConnectorNodeStub { 363 | type = "CONNECTOR"; 364 | private _text: TextSublayerNode; 365 | private _textBackground; 366 | private _cornerRadius; 367 | 368 | connectorLineType: "ELBOWED" | "STRAIGHT"; 369 | 370 | connectorStart; 371 | connectorEnd; 372 | connectorStartStrokeCap; 373 | connectorEndStrokeCap; 374 | 375 | constructor(private config: TConfig) { 376 | this._text = new TextSublayerNode(config); 377 | } 378 | 379 | get cornerRadius() { 380 | return this._cornerRadius; 381 | } 382 | 383 | get textBackground() { 384 | return this._textBackground; 385 | } 386 | 387 | get text() { 388 | return this._text; 389 | } 390 | } 391 | 392 | export class DocumentNodeStub { 393 | type = "DOCUMENT"; 394 | children = []; 395 | 396 | constructor(private config: TConfig) {} 397 | } 398 | 399 | export class PageNodeStub { 400 | type = "PAGE"; 401 | children = []; 402 | _selection: Array; 403 | _backgrounds: Array; 404 | 405 | constructor(private config: TConfig) {} 406 | 407 | get selection() { 408 | return this._selection || []; 409 | } 410 | 411 | set selection(value) { 412 | this._selection = value; 413 | selectionChangeSubject.next(); 414 | } 415 | 416 | get backgrounds() { 417 | return ( 418 | this._backgrounds || [ 419 | { 420 | type: "SOLID", 421 | visible: true, 422 | opacity: 1, 423 | blendMode: "NORMAL", 424 | color: { 425 | r: 0.9607843160629272, 426 | g: 0.9607843160629272, 427 | b: 0.9607843160629272 428 | } 429 | } 430 | ] 431 | ); 432 | } 433 | 434 | set backgrounds(value) { 435 | if ( 436 | this.config.simulateErrors && 437 | (value.length !== 1 || value[0].type !== "SOLID") 438 | ) { 439 | throw new Error( 440 | `Error: in set_backgrounds: Page backgrounds must be a single solid paint` 441 | ); 442 | } 443 | this._backgrounds = value; 444 | } 445 | } 446 | 447 | export class FrameNodeStub { 448 | type = "FRAME"; 449 | children = []; 450 | 451 | constructor(private config: TConfig) {} 452 | } 453 | 454 | export class GroupNodeStub { 455 | constructor(private config: TConfig) {} 456 | 457 | type = "GROUP"; 458 | 459 | set constraints(value) { 460 | if (this.config.simulateErrors) { 461 | throw new Error( 462 | `Error: Cannot add property constraints, object is not extensible` 463 | ); 464 | } 465 | } 466 | } 467 | 468 | export class BooleanOperationNodeStub { 469 | constructor(private config: TConfig) {} 470 | 471 | type = "BOOLEAN_OPERATION"; 472 | 473 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE"; 474 | expand = false; 475 | } 476 | 477 | function cloneChildren(node) { 478 | const clone = new node.constructor(); 479 | for (let key in node) { 480 | if (typeof node[key] === "function") { 481 | clone[key] = node[key].bind(clone); 482 | } else { 483 | clone[key] = node[key]; 484 | } 485 | } 486 | clone._orig = node; 487 | clone.pluginData = {}; 488 | clone.sharedPluginData = {}; 489 | if ("children" in node) { 490 | clone.children = node.children.map(child => cloneChildren(child)); 491 | clone.children.forEach(child => { 492 | child.parent = clone; 493 | }); 494 | } 495 | return clone; 496 | } 497 | export class ComponentNodeStub { 498 | constructor(private config: TConfig) {} 499 | 500 | type = "COMPONENT"; 501 | key = nanoid(40); 502 | children = []; 503 | createInstance() { 504 | const instance = new InstanceNodeStub(this.config); 505 | instance.children = this.children.map(child => cloneChildren(child)); 506 | instance.children.forEach(child => { 507 | child.parent = this; 508 | }); 509 | // instance.pluginData = {}; 510 | instance._orig = this; 511 | instance.mainComponent = this; 512 | return instance; 513 | } 514 | } 515 | 516 | export class InstanceNodeStub { 517 | constructor(private config: TConfig) {} 518 | 519 | type = "INSTANCE"; 520 | children: any; 521 | mainComponent: null | ComponentNodeStub; 522 | 523 | _orig: ComponentNodeStub | null; 524 | 525 | detachInstance(): void { 526 | this.type = "FRAME"; 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export type TConfig = { 2 | simulateErrors?: boolean; 3 | isWithoutTimeout?: boolean; 4 | }; 5 | 6 | export const defaultConfig: TConfig = { 7 | simulateErrors: false, 8 | isWithoutTimeout: false 9 | }; 10 | -------------------------------------------------------------------------------- /src/fonts.ts: -------------------------------------------------------------------------------- 1 | export const Inter: Font[] = [ 2 | { 3 | fontName: { 4 | family: "Inter", 5 | style: "Thin" 6 | } 7 | }, 8 | { 9 | fontName: { 10 | family: "Inter", 11 | style: "Extra Light" 12 | } 13 | }, 14 | { 15 | fontName: { 16 | family: "Inter", 17 | style: "Light" 18 | } 19 | }, 20 | { 21 | fontName: { 22 | family: "Inter", 23 | style: "Regular" 24 | } 25 | }, 26 | { 27 | fontName: { 28 | family: "Inter", 29 | style: "Medium" 30 | } 31 | }, 32 | { 33 | fontName: { 34 | family: "Inter", 35 | style: "Semi Bold" 36 | } 37 | }, 38 | { 39 | fontName: { 40 | family: "Inter", 41 | style: "Bold" 42 | } 43 | }, 44 | { 45 | fontName: { 46 | family: "Inter", 47 | style: "Extra Bold" 48 | } 49 | }, 50 | { 51 | fontName: { 52 | family: "Inter", 53 | style: "Black" 54 | } 55 | }, 56 | { 57 | fontName: { 58 | family: "Inter", 59 | style: "Thin Italic" 60 | } 61 | }, 62 | { 63 | fontName: { 64 | family: "Inter", 65 | style: "Extra Light Italic" 66 | } 67 | }, 68 | { 69 | fontName: { 70 | family: "Inter", 71 | style: "Light Italic" 72 | } 73 | }, 74 | { 75 | fontName: { 76 | family: "Inter", 77 | style: "Regular Italic" 78 | } 79 | }, 80 | { 81 | fontName: { 82 | family: "Inter", 83 | style: "Medium Italic" 84 | } 85 | }, 86 | { 87 | fontName: { 88 | family: "Inter", 89 | style: "Semi Bold Italic" 90 | } 91 | }, 92 | { 93 | fontName: { 94 | family: "Inter", 95 | style: "Bold Italic" 96 | } 97 | }, 98 | { 99 | fontName: { 100 | family: "Inter", 101 | style: "Extra Bold Italic" 102 | } 103 | }, 104 | { 105 | fontName: { 106 | family: "Inter", 107 | style: "Black Italic" 108 | } 109 | } 110 | ]; 111 | 112 | export const Roboto: Font[] = [ 113 | { 114 | fontName: { 115 | family: "Roboto", 116 | style: "Thin" 117 | } 118 | }, 119 | { 120 | fontName: { 121 | family: "Roboto", 122 | style: "Light" 123 | } 124 | }, 125 | { 126 | fontName: { 127 | family: "Roboto", 128 | style: "Regular" 129 | } 130 | }, 131 | { 132 | fontName: { 133 | family: "Roboto", 134 | style: "Medium" 135 | } 136 | }, 137 | { 138 | fontName: { 139 | family: "Roboto", 140 | style: "Bold" 141 | } 142 | }, 143 | { 144 | fontName: { 145 | family: "Roboto", 146 | style: "Black" 147 | } 148 | }, 149 | { 150 | fontName: { 151 | family: "Roboto", 152 | style: "Thin Italic" 153 | } 154 | }, 155 | { 156 | fontName: { 157 | family: "Roboto", 158 | style: "Light Italic" 159 | } 160 | }, 161 | { 162 | fontName: { 163 | family: "Roboto", 164 | style: "Regular Italic" 165 | } 166 | }, 167 | { 168 | fontName: { 169 | family: "Roboto", 170 | style: "Medium Italic" 171 | } 172 | }, 173 | { 174 | fontName: { 175 | family: "Roboto", 176 | style: "Bold Italic" 177 | } 178 | }, 179 | { 180 | fontName: { 181 | family: "Roboto", 182 | style: "Black Italic" 183 | } 184 | } 185 | ]; 186 | 187 | export const Helvetica: Font[] = [ 188 | { 189 | fontName: { 190 | family: "Helvetica", 191 | style: "Light" 192 | } 193 | }, 194 | { 195 | fontName: { 196 | family: "Helvetica", 197 | style: "Regular" 198 | } 199 | }, 200 | { 201 | fontName: { 202 | family: "Helvetica", 203 | style: "Bold" 204 | } 205 | }, 206 | { 207 | fontName: { 208 | family: "Helvetica", 209 | style: "Light Oblique" 210 | } 211 | }, 212 | { 213 | fontName: { 214 | family: "Helvetica", 215 | style: "Oblique" 216 | } 217 | }, 218 | { 219 | fontName: { 220 | family: "Helvetica", 221 | style: "Oblique" 222 | } 223 | } 224 | ]; 225 | 226 | export class Fonts { 227 | static loadedFonts: Array = []; 228 | 229 | static isFontLoaded(fontName) { 230 | return this.loadedFonts.find( 231 | font => font.family === fontName.family && font.style === fontName.style 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createFigma, createParentPostMessage } from "./stubs"; 2 | -------------------------------------------------------------------------------- /src/mixins.ts: -------------------------------------------------------------------------------- 1 | import { TConfig } from "./config"; 2 | 3 | export const isInsideInstance = node => { 4 | if (!node.parent) { 5 | return; 6 | } 7 | return node.parent.type === "INSTANCE" || isInsideInstance(node.parent); 8 | }; 9 | 10 | export const getChildrenMixinStub = (config: TConfig) => 11 | class ChildrenMixinStub implements ChildrenMixin { 12 | children: Array; 13 | 14 | appendChild(item) { 15 | if (!this.children) { 16 | this.children = []; 17 | } 18 | if (item.parent) { 19 | item.parent.children = item.parent.children.filter( 20 | child => child !== item 21 | ); 22 | } 23 | 24 | if (config.simulateErrors && !item) { 25 | throw new Error("Error: empty child"); 26 | } 27 | 28 | if ( 29 | config.simulateErrors && 30 | // @ts-ignore 31 | this.type === "DOCUMENT" && 32 | item.type !== "PAGE" 33 | ) { 34 | throw new Error( 35 | "Error: The root node cannot have children of type other than PAGE" 36 | ); 37 | } 38 | item.parent = this; 39 | this.children.push(item); 40 | } 41 | 42 | insertChild(index: number, child: any) { 43 | if (!this.children) { 44 | this.children = []; 45 | } 46 | 47 | if (config.simulateErrors && !child) { 48 | throw new Error("Error: empty child"); 49 | } 50 | 51 | // @ts-ignore 52 | if (config.simulateErrors && child.parent === this) { 53 | throw new Error("Error: Node already inside parent"); 54 | } 55 | 56 | if ( 57 | config.simulateErrors && 58 | // @ts-ignore 59 | this.type === "DOCUMENT" && 60 | child.type !== "PAGE" 61 | ) { 62 | throw new Error( 63 | "Error: The root node cannot have children of type other than PAGE" 64 | ); 65 | } 66 | if (child.parent) { 67 | child.parent.children = child.parent.children.filter( 68 | _child => child !== _child 69 | ); 70 | } 71 | // @ts-ignore 72 | child.parent = this; 73 | this.children.splice(index, 0, child); 74 | } 75 | 76 | findAllWithCriteria(criteria: { types: T }) { 77 | const typeLookup = new Set(criteria.types); 78 | return this.findAll(() => true).filter(child => 79 | typeLookup.has(child.type) 80 | ); 81 | } 82 | 83 | findAll(callback) { 84 | if (!this.children) { 85 | return []; 86 | } 87 | const matchingChildren = []; 88 | this.children.forEach(child => { 89 | if (callback(child)) { 90 | matchingChildren.push(child); 91 | } 92 | if ("findAll" in child) { 93 | matchingChildren.push(...child.findAll(callback)); 94 | } 95 | }); 96 | return matchingChildren; 97 | } 98 | 99 | findOne(callback) { 100 | const matches = this.findAll(callback); 101 | if (matches.length > 0) { 102 | return matches[0]; 103 | } 104 | return null; 105 | } 106 | 107 | findChild(callback) { 108 | if (!this.children) { 109 | return null; 110 | } 111 | return this.children.find(callback); 112 | } 113 | 114 | findChildren(callback) { 115 | if (!this.children) { 116 | return null; 117 | } 118 | return this.children.filter(callback); 119 | } 120 | }; 121 | 122 | export const getBaseNodeMixinStub = (config: TConfig) => 123 | class BaseNodeMixinStub implements BaseNodeMixin { 124 | id: string; 125 | parent: (BaseNode & ChildrenMixin) | null; 126 | name: string; 127 | removed: boolean; 128 | relaunchData: { [command: string]: string }; 129 | pluginData: { [key: string]: string }; 130 | sharedPluginData: { [namespace: string]: { [key: string]: string } }; 131 | 132 | // instance nodes that are cloned from components will have `_orig` set to 133 | // the value of the original node. This is used internally for inheriting 134 | // things like plugin data and relaunch data 135 | _orig: BaseNodeMixin | null = null; 136 | 137 | setPluginData(key: string, value: string) { 138 | if (!this.pluginData) { 139 | this.pluginData = {}; 140 | } 141 | if (value === "") { 142 | delete this.pluginData[key]; 143 | } else { 144 | this.pluginData[key] = value; 145 | } 146 | } 147 | 148 | getPluginData(key: string) { 149 | if (config.simulateErrors && this.removed) { 150 | throw new Error(`The node with id ${this.id} does not exist`); 151 | } 152 | 153 | // first, try to retrieve the key from local plugin data 154 | if (this.pluginData && this.pluginData[key]) { 155 | return this.pluginData[key]; 156 | } 157 | // if we don't find the key in local plugin data, try and retrieve it from 158 | // the original node it was cloned from, if it exists if 159 | if (this._orig) { 160 | return this._orig.getPluginData(key); 161 | } 162 | // otherwise, return nothing 163 | return; 164 | } 165 | 166 | getPluginDataKeys(): string[] { 167 | if (config.simulateErrors && this.removed) { 168 | throw new Error(`The node with id ${this.id} does not exist`); 169 | } 170 | // get all local and inherited keys 171 | const localKeys = this.pluginData ? Object.keys(this.pluginData) : []; 172 | const inheritedKeys = this._orig ? this._orig.getPluginDataKeys() : []; 173 | 174 | // combine them into one list and de-dupe any copies 175 | const combinedKeys = Array.from( 176 | new Set([...localKeys, ...inheritedKeys]) 177 | ); 178 | return combinedKeys; 179 | } 180 | 181 | setSharedPluginData(namespace: string, key: string, value: string) { 182 | if (!this.sharedPluginData) { 183 | this.sharedPluginData = {}; 184 | } 185 | if (!this.sharedPluginData[namespace]) { 186 | this.sharedPluginData[namespace] = {}; 187 | } 188 | if (value === "") { 189 | delete this.sharedPluginData[namespace][key]; 190 | // if (Object.keys(this.sharedPluginData[namespace]).length === 0) { 191 | // delete this.sharedPluginData[namespace]; 192 | // } 193 | } else { 194 | this.sharedPluginData[namespace][key] = value; 195 | } 196 | } 197 | 198 | getSharedPluginData(namespace: string, key: string) { 199 | // first, try to retrieve the key from local plugin data 200 | if ( 201 | this.sharedPluginData && 202 | this.sharedPluginData[namespace] && 203 | this.sharedPluginData[namespace][key] 204 | ) { 205 | return this.sharedPluginData[namespace][key]; 206 | } 207 | // if we don't find the key in local plugin data, try and retrieve it from 208 | // the original node it was cloned from, if it exists if 209 | if (this._orig) { 210 | return this._orig.getSharedPluginData(namespace, key); 211 | } 212 | // otherwise, return nothing 213 | return; 214 | } 215 | 216 | getSharedPluginDataKeys(namespace: string): string[] { 217 | // get all local and inherited keys 218 | const localKeys = 219 | this.sharedPluginData && this.sharedPluginData[namespace] 220 | ? Object.keys(this.sharedPluginData[namespace]) 221 | : []; 222 | const inheritedKeys = this._orig 223 | ? this._orig.getSharedPluginDataKeys(namespace) 224 | : []; 225 | 226 | // combine them into one list and de-dupe any copies 227 | const combinedKeys = Array.from( 228 | new Set([...localKeys, ...inheritedKeys]) 229 | ); 230 | return combinedKeys; 231 | } 232 | 233 | setRelaunchData(data: { [command: string]: string }) { 234 | this.relaunchData = data; 235 | } 236 | 237 | getRelaunchData(): { [command: string]: string } { 238 | return this.relaunchData || {}; 239 | } 240 | 241 | remove() { 242 | this.removed = true; 243 | if (config.simulateErrors && isInsideInstance(this)) { 244 | throw new Error("Error: can't remove item"); 245 | } 246 | if (this.parent) { 247 | // @ts-ignore 248 | this.parent.children = this.parent.children.filter( 249 | (child: any) => child !== this 250 | ); 251 | } 252 | } 253 | }; 254 | 255 | export const getLayoutMixinStub = (config: TConfig) => 256 | class LayoutMixinStub implements LayoutMixin { 257 | layoutGrow: number; 258 | rescale(scale: number): void { 259 | if (config.simulateErrors && scale < 0.01) { 260 | throw new Error( 261 | 'Error: in rescale: Expected "scale" to have value >= 0.01' 262 | ); 263 | } 264 | this.width = this.width * scale; 265 | this.height = this.height * scale; 266 | } 267 | absoluteTransform: Transform; 268 | relativeTransform: Transform; 269 | x: number; 270 | y: number; 271 | rotation: number; 272 | 273 | width: number; 274 | height: number; 275 | 276 | constrainProportions: boolean; 277 | layoutAlign: LayoutMixin["layoutAlign"]; 278 | 279 | absoluteRenderBounds: Rect | null; 280 | 281 | resize(width, height) { 282 | if (config.simulateErrors && isInsideInstance(this)) { 283 | throw new Error("Error: can't change layout inside item"); 284 | } 285 | if (config.simulateErrors && width < 0.01) { 286 | throw new Error( 287 | 'Error: in resize: Expected "width" to have value >= 0.01' 288 | ); 289 | } 290 | if (config.simulateErrors && height < 0.01) { 291 | throw new Error( 292 | 'Error: in resize: Expected "height" to have value >= 0.01' 293 | ); 294 | } 295 | this.width = width; 296 | this.height = height; 297 | } 298 | 299 | resizeWithoutConstraints(width, height) { 300 | this.width = width; 301 | this.height = height; 302 | } 303 | }; 304 | 305 | export class ExportMixinStub implements ExportMixin { 306 | exportSettings: ReadonlyArray; 307 | 308 | exportAsync(settings?: ExportSettings) { 309 | // "exportAsync" is not implemented in stubs 310 | return Promise.resolve(new Uint8Array()); 311 | } 312 | } 313 | 314 | export class GeometryMixinStub implements GeometryMixin { 315 | private _fills: ReadonlyArray | PluginAPI["mixed"]; 316 | get fills() { 317 | return this._fills || []; 318 | } 319 | set fills(theFills) { 320 | this._fills = theFills; 321 | } 322 | strokes: ReadonlyArray; 323 | strokeWeight: number; 324 | strokeMiterLimit: number; 325 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE"; 326 | strokeCap: StrokeCap | PluginAPI["mixed"]; 327 | strokeJoin: StrokeJoin | PluginAPI["mixed"]; 328 | dashPattern: ReadonlyArray; 329 | fillStyleId: string | PluginAPI["mixed"]; 330 | strokeStyleId: string; 331 | strokeGeometry: VectorPaths; 332 | fillGeometry: VectorPaths; 333 | outlineStroke() { 334 | return null; 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/stubs.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | import { Subject, Subscription } from "rxjs"; 3 | import { take } from "rxjs/operators"; 4 | import { 5 | getEffectStyleStub, 6 | getGridStyleStub, 7 | getPaintStyleStub, 8 | getTextStyleStub 9 | } from "./styleStubs"; 10 | import { applyMixins } from "./applyMixins"; 11 | import { 12 | BooleanOperationNodeStub, 13 | ComponentNodeStub, 14 | ConnectorNodeStub, 15 | DocumentNodeStub, 16 | FrameNodeStub, 17 | GroupNodeStub, 18 | InstanceNodeStub, 19 | PageNodeStub, 20 | RectangleNodeStub, 21 | selectionChangeSubject, 22 | ShapeWithTextNodeStub, 23 | StickyNodeStub, 24 | TextNodeStub 25 | } from "./componentStubs"; 26 | import { defaultConfig, TConfig } from "./config"; 27 | import { Fonts, Helvetica, Inter, Roboto } from "./fonts"; 28 | import { 29 | ExportMixinStub, 30 | GeometryMixinStub, 31 | getBaseNodeMixinStub, 32 | getChildrenMixinStub, 33 | getLayoutMixinStub 34 | } from "./mixins"; 35 | 36 | export const createFigma = (paramConfig: TConfig): PluginAPI => { 37 | const config = { ...defaultConfig, ...paramConfig }; 38 | const BaseNodeMixinStub = getBaseNodeMixinStub(config); 39 | const LayoutMixinStub = getLayoutMixinStub(config); 40 | const ChildrenMixinStub = getChildrenMixinStub(config); 41 | 42 | // @ts-ignore 43 | global.__html__ = "main.html"; 44 | 45 | // @ts-ignore 46 | global.__uiFiles__ = {}; 47 | 48 | applyMixins(RectangleNodeStub, [ 49 | BaseNodeMixinStub, 50 | LayoutMixinStub, 51 | ExportMixinStub, 52 | GeometryMixinStub 53 | ]); 54 | 55 | applyMixins(TextNodeStub, [ 56 | BaseNodeMixinStub, 57 | LayoutMixinStub, 58 | ExportMixinStub, 59 | GeometryMixinStub 60 | ]); 61 | 62 | applyMixins(ShapeWithTextNodeStub, [ 63 | BaseNodeMixinStub, 64 | LayoutMixinStub, 65 | ExportMixinStub, 66 | GeometryMixinStub 67 | ]); 68 | 69 | applyMixins(StickyNodeStub, [ 70 | BaseNodeMixinStub, 71 | LayoutMixinStub, 72 | ExportMixinStub, 73 | GeometryMixinStub 74 | ]); 75 | 76 | applyMixins(ConnectorNodeStub, [ 77 | BaseNodeMixinStub, 78 | LayoutMixinStub, 79 | ExportMixinStub, 80 | GeometryMixinStub 81 | ]); 82 | 83 | applyMixins(DocumentNodeStub, [BaseNodeMixinStub, ChildrenMixinStub]); 84 | 85 | applyMixins(PageNodeStub, [ 86 | BaseNodeMixinStub, 87 | ChildrenMixinStub, 88 | ExportMixinStub 89 | ]); 90 | 91 | applyMixins(FrameNodeStub, [ 92 | BaseNodeMixinStub, 93 | ChildrenMixinStub, 94 | LayoutMixinStub, 95 | ExportMixinStub, 96 | GeometryMixinStub 97 | ]); 98 | 99 | applyMixins(GroupNodeStub, [ 100 | BaseNodeMixinStub, 101 | ChildrenMixinStub, 102 | ExportMixinStub, 103 | LayoutMixinStub 104 | ]); 105 | 106 | applyMixins(BooleanOperationNodeStub, [ 107 | BaseNodeMixinStub, 108 | ChildrenMixinStub, 109 | ExportMixinStub, 110 | LayoutMixinStub 111 | ]); 112 | 113 | applyMixins(ComponentNodeStub, [ 114 | BaseNodeMixinStub, 115 | ChildrenMixinStub, 116 | ExportMixinStub, 117 | LayoutMixinStub, 118 | GeometryMixinStub 119 | ]); 120 | 121 | applyMixins(InstanceNodeStub, [ 122 | BaseNodeMixinStub, 123 | ExportMixinStub, 124 | LayoutMixinStub, 125 | ChildrenMixinStub 126 | ]); 127 | 128 | const selectionChangeSubscribes = new Map(); 129 | 130 | const currentPageChangeSubject = new Subject(); 131 | const currentPageChangeSubscribes = new Map(); 132 | 133 | let majorId = 1; 134 | let minorId = 1; 135 | const allocateNodeId = (node, shouldIncreaseMajor?: boolean) => { 136 | minorId += 1; 137 | if (!shouldIncreaseMajor) { 138 | node.id = `${majorId}:${minorId}`; 139 | } else { 140 | node.id = `${majorId}:${1}`; 141 | majorId += 1; 142 | } 143 | }; 144 | 145 | const allocateStyleId = style => { 146 | style.id = `S:${nanoid(40)},`; 147 | }; 148 | 149 | const getImageHash = () => { 150 | return nanoid(40); 151 | }; 152 | 153 | class UIAPIStub { 154 | _listeners = new Set(); 155 | 156 | onmessage: MessageEventHandler | undefined; 157 | 158 | on: (type: "message", cb: MessageEventHandler | undefined) => void = ( 159 | type, 160 | cb 161 | ) => { 162 | if (type === "message" && cb) { 163 | this._listeners.add(cb); 164 | } 165 | }; 166 | 167 | off: (type: "message", cb: MessageEventHandler | undefined) => void = ( 168 | type, 169 | cb 170 | ) => { 171 | if (type === "message" && cb) { 172 | this._listeners.delete(cb); 173 | } 174 | }; 175 | 176 | once: (type: "message", cb: MessageEventHandler | undefined) => void = ( 177 | type, 178 | cb 179 | ) => { 180 | if (type === "message" && cb) { 181 | const wrappedCb = (pluginMessage, props) => { 182 | cb(pluginMessage, props); 183 | this.off("message", wrappedCb); 184 | }; 185 | this.on("message", wrappedCb); 186 | } 187 | }; 188 | 189 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void { 190 | const message = { 191 | data: { pluginMessage, pluginId: "000000000000000000" }, 192 | type: "message" 193 | }; 194 | 195 | // @ts-ignore 196 | if (global && global.onmessage) { 197 | if (config.isWithoutTimeout) { 198 | // @ts-ignore 199 | global.onmessage(message); 200 | } else { 201 | setTimeout(() => { 202 | // @ts-ignore 203 | global.onmessage(message); 204 | }, 0); 205 | } 206 | } 207 | } 208 | } 209 | 210 | // --- styles 211 | 212 | const PaintStyleStub = getPaintStyleStub(config); 213 | const EffectStyleStub = getEffectStyleStub(config); 214 | const TextStyleStub = getTextStyleStub(config); 215 | const GridStyleStub = getGridStyleStub(config); 216 | 217 | const styleBasics: { 218 | styles: Map; 219 | paintStyles: any[]; 220 | effectStyles: any[]; 221 | textStyles: any[]; 222 | gridStyles: any[]; 223 | } = { 224 | styles: new Map(), 225 | paintStyles: [], 226 | effectStyles: [], 227 | textStyles: [], 228 | gridStyles: [] 229 | }; 230 | 231 | // @ts-ignore 232 | class PluginApiStub implements PluginAPI { 233 | root: DocumentNode; 234 | _currentPage: PageNode; 235 | readonly ui: UIAPI; 236 | 237 | constructor() { 238 | // @ts-ignore 239 | this.root = new DocumentNodeStub(); 240 | // @ts-ignore 241 | this.root.id = "0:0"; 242 | // @ts-ignore 243 | this._currentPage = new PageNodeStub(); 244 | // @ts-ignore 245 | this._currentPage.id = "0:1"; 246 | this.root.appendChild(this._currentPage); 247 | // @ts-ignore 248 | this.ui = new UIAPIStub(); 249 | } 250 | 251 | get currentPage() { 252 | return this._currentPage; 253 | } 254 | 255 | set currentPage(value) { 256 | this._currentPage = value; 257 | currentPageChangeSubject.next(); 258 | } 259 | 260 | skipInvisibleInstanceChildren: boolean = false; 261 | 262 | // @ts-ignore 263 | createPage() { 264 | const result: any = new PageNodeStub(config); 265 | allocateNodeId(result, true); 266 | this.root.appendChild(result); 267 | return result; 268 | } 269 | 270 | // @ts-ignore 271 | createFrame() { 272 | const result: any = new FrameNodeStub(config); 273 | allocateNodeId(result); 274 | this.currentPage.appendChild(result); 275 | return result; 276 | } 277 | 278 | // @ts-ignore 279 | createShapeWithText() { 280 | const result: any = new ShapeWithTextNodeStub(config); 281 | allocateNodeId(result); 282 | this.root.appendChild(result); 283 | return result; 284 | } 285 | 286 | // @ts-ignore 287 | createSticky() { 288 | const result: any = new StickyNodeStub(config); 289 | allocateNodeId(result); 290 | this.root.appendChild(result); 291 | return result; 292 | } 293 | 294 | // @ts-ignore 295 | createComponent() { 296 | const result: any = new ComponentNodeStub(config); 297 | allocateNodeId(result); 298 | this.currentPage.appendChild(result); 299 | return result; 300 | } 301 | 302 | // @ts-ignore 303 | createRectangle() { 304 | const result: any = new RectangleNodeStub(config); 305 | allocateNodeId(result); 306 | this.currentPage.appendChild(result); 307 | return result; 308 | } 309 | 310 | // @ts-ignore 311 | createText() { 312 | const result: any = new TextNodeStub(config); 313 | allocateNodeId(result); 314 | this.currentPage.appendChild(result); 315 | return result; 316 | } 317 | 318 | createConnector() { 319 | const result: any = new ConnectorNodeStub(config); 320 | allocateNodeId(result); 321 | this.currentPage.appendChild(result); 322 | return result; 323 | } 324 | 325 | getStyleById(id) { 326 | if (styleBasics.styles.has(id)) { 327 | return styleBasics.styles.get(id); 328 | } 329 | 330 | return null; 331 | } 332 | 333 | getLocalPaintStyles() { 334 | return styleBasics.paintStyles; 335 | } 336 | 337 | getLocalEffectStyles() { 338 | return styleBasics.effectStyles; 339 | } 340 | 341 | getLocalTextStyles() { 342 | return styleBasics.textStyles; 343 | } 344 | 345 | getLocalGridStyles() { 346 | return styleBasics.gridStyles; 347 | } 348 | 349 | // @ts-ignore 350 | createPaintStyle() { 351 | const style = new PaintStyleStub(styleBasics); 352 | allocateStyleId(style); 353 | styleBasics.styles.set(style.id, style); 354 | styleBasics.paintStyles.push(style); 355 | return style; 356 | } 357 | 358 | // @ts-ignore 359 | createEffectStyle() { 360 | const style = new EffectStyleStub(styleBasics); 361 | allocateStyleId(style); 362 | styleBasics.styles.set(style.id, style); 363 | styleBasics.effectStyles.push(style); 364 | return style; 365 | } 366 | 367 | // @ts-ignore 368 | createTextStyle() { 369 | const style = new TextStyleStub(styleBasics); 370 | allocateStyleId(style); 371 | styleBasics.styles.set(style.id, style); 372 | styleBasics.textStyles.push(style); 373 | return style; 374 | } 375 | 376 | // @ts-ignore 377 | createGridStyle() { 378 | const style = new GridStyleStub(styleBasics); 379 | allocateStyleId(style); 380 | styleBasics.styles.set(style.id, style); 381 | styleBasics.gridStyles.push(style); 382 | return style; 383 | } 384 | 385 | createImage(bytes: Uint8Array) { 386 | const hash = getImageHash(); 387 | return { 388 | hash, 389 | getBytesAsync: () => Promise.resolve(bytes) 390 | }; 391 | } 392 | 393 | union( 394 | nodes: readonly BaseNode[], 395 | parent: BaseNode & ChildrenMixin, 396 | index?: number 397 | ): BooleanOperationNode { 398 | const booleanOperation = this.booleanOperate(nodes, parent, index); 399 | booleanOperation.booleanOperation = "UNION"; 400 | return booleanOperation as any; 401 | } 402 | 403 | intersect( 404 | nodes: readonly BaseNode[], 405 | parent: BaseNode & ChildrenMixin, 406 | index?: number 407 | ): BooleanOperationNode { 408 | const booleanOperation = this.booleanOperate(nodes, parent, index); 409 | booleanOperation.booleanOperation = "INTERSECT"; 410 | return booleanOperation as any; 411 | } 412 | 413 | subtract( 414 | nodes: readonly BaseNode[], 415 | parent: BaseNode & ChildrenMixin, 416 | index?: number 417 | ): BooleanOperationNode { 418 | const booleanOperation = this.booleanOperate(nodes, parent, index); 419 | booleanOperation.booleanOperation = "SUBTRACT"; 420 | return booleanOperation as any; 421 | } 422 | 423 | exlude( 424 | nodes: readonly BaseNode[], 425 | parent: BaseNode & ChildrenMixin, 426 | index?: number 427 | ): BooleanOperationNode { 428 | const booleanOperation = this.booleanOperate(nodes, parent, index); 429 | booleanOperation.booleanOperation = "EXCLUDE"; 430 | return booleanOperation as any; 431 | } 432 | 433 | private booleanOperate( 434 | nodes: readonly BaseNode[], 435 | parent: BaseNode & ChildrenMixin, 436 | index?: number 437 | ): BooleanOperationNodeStub { 438 | if (config.simulateErrors && (!nodes || nodes.length === 0)) { 439 | throw new Error( 440 | "Error: First argument must be an array of at least one node" 441 | ); 442 | } 443 | 444 | const booleanOperation: any = new BooleanOperationNodeStub(config); 445 | allocateNodeId(booleanOperation); 446 | nodes.forEach(node => booleanOperation.appendChild(node)); 447 | if (index) { 448 | parent.insertChild(index, booleanOperation); 449 | } else { 450 | parent.appendChild(booleanOperation); 451 | } 452 | booleanOperation.parent = parent; 453 | 454 | return booleanOperation; 455 | } 456 | 457 | // @ts-ignore 458 | group(nodes: any, parent: any, index) { 459 | if (config.simulateErrors && (!nodes || nodes.length === 0)) { 460 | throw new Error( 461 | "Error: First argument must be an array of at least one node" 462 | ); 463 | } 464 | 465 | const group: any = new GroupNodeStub(config); 466 | allocateNodeId(group); 467 | nodes.forEach(node => group.appendChild(node)); 468 | if (index) { 469 | parent.insertChild(index, group); 470 | } else { 471 | parent.appendChild(group); 472 | } 473 | group.parent = parent; 474 | return group; 475 | } 476 | // @ts-ignore 477 | loadFontAsync(fontName) { 478 | if (Fonts.isFontLoaded(fontName)) { 479 | return; 480 | } 481 | return new Promise(resolve => { 482 | Fonts.loadedFonts.push(fontName); 483 | resolve(); 484 | }); 485 | } 486 | 487 | listAvailableFontsAsync(): Promise { 488 | return Promise.resolve([...Inter, ...Roboto, ...Helvetica]); 489 | } 490 | 491 | on(type: ArgFreeEventType, callback: () => void); 492 | on(type: "run", callback: (event: RunEvent) => void); 493 | on(type: "drop", callback: (event: DropEvent) => boolean): void; 494 | on(type: any, callback: any) { 495 | if (type === "selectionchange") { 496 | selectionChangeSubscribes.set( 497 | callback, 498 | selectionChangeSubject.subscribe(callback) 499 | ); 500 | } 501 | if (type === "currentpagechange") { 502 | currentPageChangeSubscribes.set( 503 | callback, 504 | currentPageChangeSubject.subscribe(callback) 505 | ); 506 | } 507 | } 508 | 509 | once(type: ArgFreeEventType, callback: () => void); 510 | once(type: "run", callback: (event: RunEvent) => void); 511 | once(type: "drop", callback: (event: DropEvent) => boolean): void; 512 | once(type: any, callback: any) { 513 | if (type === "selectionchange") { 514 | selectionChangeSubscribes.set( 515 | callback, 516 | selectionChangeSubject.pipe(take(1)).subscribe(callback) 517 | ); 518 | } 519 | if (type === "currentpagechange") { 520 | currentPageChangeSubscribes.set( 521 | callback, 522 | currentPageChangeSubject.pipe(take(1)).subscribe(callback) 523 | ); 524 | } 525 | } 526 | 527 | off(type: ArgFreeEventType, callback: () => void); 528 | off(type: "run", callback: (event: RunEvent) => void); 529 | off(type: "drop", callback: (event: DropEvent) => boolean): void; 530 | off(type: any, callback: any) { 531 | if (type === "selectionchange") { 532 | selectionChangeSubscribes.get(callback).unsubscribe(); 533 | } 534 | if (type === "currentpagechange") { 535 | currentPageChangeSubscribes.get(callback).unsubscribe(); 536 | } 537 | } 538 | 539 | getNodeById(id) { 540 | const _genNodeById = (nodes, id) => { 541 | for (const node of nodes) { 542 | if (node.id === id) { 543 | return node; 544 | } 545 | const childMatch = node.children && _genNodeById(node.children, id); 546 | if (childMatch) { 547 | return childMatch; 548 | } 549 | } 550 | }; 551 | return _genNodeById([figma.root], id) || null; 552 | } 553 | 554 | notify() { 555 | return { cancel: () => {} }; 556 | } 557 | 558 | showUI() {} 559 | } 560 | 561 | // @ts-ignore 562 | return new PluginApiStub(); 563 | }; 564 | 565 | export const createParentPostMessage = ( 566 | figma: PluginAPI, 567 | isWithoutTimeout?: boolean 568 | ) => (message: { pluginMessage: any }, target: string) => { 569 | if (figma.ui.onmessage) { 570 | const call = () => { 571 | // @ts-ignore 572 | figma.ui.onmessage(message.pluginMessage, { origin: null }); 573 | }; 574 | if (isWithoutTimeout) { 575 | call(); 576 | } else { 577 | setTimeout(call, 0); 578 | } 579 | } else { 580 | const call = () => { 581 | // @ts-ignore 582 | figma.ui._listeners.forEach((cb: MessageEventHandler) => { 583 | cb(message.pluginMessage, { origin: null }); 584 | }); 585 | }; 586 | if (isWithoutTimeout) { 587 | call(); 588 | } else { 589 | setTimeout(call, 0); 590 | } 591 | } 592 | }; 593 | -------------------------------------------------------------------------------- /src/styleStubs.ts: -------------------------------------------------------------------------------- 1 | import { TConfig } from "./config"; 2 | 3 | type StyleBasics = { 4 | styles: Map; 5 | paintStyles: any[]; 6 | effectStyles: any[]; 7 | textStyles: any[]; 8 | gridStyles: any[]; 9 | }; 10 | 11 | export const getBaseStyleStub = (config: TConfig) => 12 | class BaseStyleStub implements BaseStyle { 13 | constructor(public styleBasics: StyleBasics) {} 14 | 15 | id: string; 16 | type: StyleType; 17 | name: string; 18 | description: string; 19 | remote: boolean = false; 20 | key: string; 21 | documentationLinks: readonly DocumentationLink[]; 22 | removed: boolean; 23 | 24 | relaunchData: { [command: string]: string }; 25 | pluginData: { [key: string]: string }; 26 | sharedPluginData: { [namespace: string]: { [key: string]: string } }; 27 | 28 | setPluginData(key: string, value: string) { 29 | if (!this.pluginData) { 30 | this.pluginData = {}; 31 | } 32 | this.pluginData[key] = value; 33 | } 34 | 35 | getPluginData(key: string) { 36 | if (config.simulateErrors && this.removed) { 37 | throw new Error(`The style with id ${this.id} does not exist`); 38 | } 39 | if (!this.pluginData) { 40 | return; 41 | } 42 | return this.pluginData[key]; 43 | } 44 | 45 | getPluginDataKeys(): string[] { 46 | if (config.simulateErrors && this.removed) { 47 | throw new Error(`The style with id ${this.id} does not exist`); 48 | } 49 | if (!this.pluginData) { 50 | return []; 51 | } 52 | return Object.keys(this.pluginData); 53 | } 54 | 55 | setSharedPluginData(namespace: string, key: string, value: string) { 56 | if (!this.sharedPluginData) { 57 | this.sharedPluginData = {}; 58 | } 59 | if (!this.sharedPluginData[namespace]) { 60 | this.sharedPluginData[namespace] = {}; 61 | } 62 | this.sharedPluginData[namespace][key] = value; 63 | } 64 | 65 | getSharedPluginData(namespace: string, key: string) { 66 | if (!this.sharedPluginData || !this.sharedPluginData[namespace]) { 67 | return; 68 | } 69 | return this.sharedPluginData[namespace][key]; 70 | } 71 | 72 | getSharedPluginDataKeys(namespace: string): string[] { 73 | if (!this.sharedPluginData || !this.sharedPluginData[namespace]) { 74 | return; 75 | } 76 | return Object.keys(this.sharedPluginData[namespace]); 77 | } 78 | 79 | remove(): void { 80 | this.removed = true; 81 | this.styleBasics.styles.delete(this.id); 82 | } 83 | 84 | async getPublishStatusAsync(): Promise { 85 | return await "UNPUBLISHED"; 86 | } 87 | }; 88 | 89 | export const getPaintStyleStub = (config: TConfig) => { 90 | const BaseStyleStub = getBaseStyleStub(config); 91 | 92 | return class PaintStyleStub extends BaseStyleStub implements PaintStyle { 93 | constructor(styleBasics: StyleBasics) { 94 | super(styleBasics); 95 | } 96 | 97 | // @ts-ignore 98 | type = "PAINT" as StyleType; 99 | paints: readonly Paint[]; 100 | 101 | remove() { 102 | super.remove(); 103 | this.styleBasics.paintStyles.splice( 104 | this.styleBasics.paintStyles.indexOf(this), 105 | 1 106 | ); 107 | } 108 | }; 109 | }; 110 | 111 | export const getEffectStyleStub = (config: TConfig) => { 112 | const BaseStyleStub = getBaseStyleStub(config); 113 | 114 | return class EffectStyleStub extends BaseStyleStub implements EffectStyle { 115 | constructor(styleBasics: StyleBasics) { 116 | super(styleBasics); 117 | } 118 | // @ts-ignore 119 | type = "EFFECT" as StyleType; 120 | effects: readonly Effect[]; 121 | 122 | remove() { 123 | super.remove(); 124 | this.styleBasics.effectStyles.splice( 125 | this.styleBasics.effectStyles.indexOf(this), 126 | 1 127 | ); 128 | } 129 | }; 130 | }; 131 | export const getTextStyleStub = (config: TConfig) => { 132 | const BaseStyleStub = getBaseStyleStub(config); 133 | 134 | return class TextStyleStub extends BaseStyleStub implements TextStyle { 135 | constructor(styleBasics: StyleBasics) { 136 | super(styleBasics); 137 | } 138 | // @ts-ignore 139 | type = "TEXT" as StyleType; 140 | fontName: FontName; 141 | fontSize: number; 142 | letterSpacing: LetterSpacing; 143 | lineHeight: LineHeight; 144 | paragraphIndent: number; 145 | paragraphSpacing: number; 146 | textCase: TextCase; 147 | textDecoration: TextDecoration; 148 | 149 | remove() { 150 | super.remove(); 151 | this.styleBasics.textStyles.splice( 152 | this.styleBasics.textStyles.indexOf(this), 153 | 1 154 | ); 155 | } 156 | }; 157 | }; 158 | 159 | export const getGridStyleStub = (config: TConfig) => { 160 | const BaseStyleStub = getBaseStyleStub(config); 161 | 162 | return class GridStyleStub extends BaseStyleStub implements GridStyle { 163 | constructor(styleBasics: StyleBasics) { 164 | super(styleBasics); 165 | } 166 | // @ts-ignore 167 | type = "GRID" as StyleType; 168 | layoutGrids: readonly LayoutGrid[]; 169 | 170 | remove() { 171 | super.remove(); 172 | this.styleBasics.gridStyles.splice( 173 | this.styleBasics.gridStyles.indexOf(this), 174 | 1 175 | ); 176 | } 177 | }; 178 | }; 179 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "**/__tests__/**" 5 | ] 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": [ 7 | "es5", 8 | "es6", 9 | "es7", 10 | "es2017", 11 | "dom" 12 | ], 13 | "sourceMap": true, 14 | "allowJs": false, 15 | "jsx": "react", 16 | "moduleResolution": "node", 17 | "rootDirs": [ 18 | "src" 19 | ], 20 | "baseUrl": "src", 21 | "skipLibCheck": true, 22 | "declaration": true, 23 | "typeRoots": [ 24 | "./node_modules/@types", 25 | "./node_modules/@figma" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*", 30 | "index.d.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules", 34 | "dist" 35 | ] 36 | } --------------------------------------------------------------------------------