├── .all-contributorsrc ├── .dependency-cruiser.cjs ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ ├── feature.yml │ └── main.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .vscode ├── launch.json └── settings.json ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── __tests__ ├── controllers │ └── FigmagicController.test.ts ├── entities │ ├── Config │ │ ├── Config.test.ts │ │ └── logic │ │ │ ├── createConfiguration.test.ts │ │ │ ├── parseCliArgs.test.ts │ │ │ └── validateConfig.test.ts │ ├── FigmagicElement │ │ ├── FigmagicElement.test.ts │ │ └── logic │ │ │ ├── getFileContents.test.ts │ │ │ ├── getTokenMatch.test.ts │ │ │ ├── parseCssFromElement.test.ts │ │ │ ├── parseTypographyStylingFromElement.test.ts │ │ │ ├── parsers.test.ts │ │ │ ├── processNestedCss.test.ts │ │ │ └── sliceOutObjectFromFile.test.ts │ └── Token │ │ ├── Token.test.ts │ │ └── logic │ │ ├── makeBorderWidthTokens.test.ts │ │ ├── makeColorTokens.test.ts │ │ ├── makeDelayTokens.test.ts │ │ ├── makeDurationTokens.test.ts │ │ ├── makeEasingTokens.test.ts │ │ ├── makeFontSizeTokens.test.ts │ │ ├── makeFontTokens.test.ts │ │ ├── makeFontWeightTokens.test.ts │ │ ├── makeLetterSpacingTokens.test.ts │ │ ├── makeLineHeightTokens.test.ts │ │ ├── makeMediaQueryTokens.test.ts │ │ ├── makeOpacityTokens.test.ts │ │ ├── makeRadiusTokens.test.ts │ │ ├── makeShadowTokens.test.ts │ │ ├── makeSpacingTokens.test.ts │ │ └── makeZindexTokens.test.ts ├── frameworks │ ├── errors │ │ └── errors.test.ts │ ├── filesystem │ │ ├── checkIfExists.test.ts │ │ ├── createFolder.test.ts │ │ ├── createMissingFoldersFromPath.test.ts │ │ ├── getDataHelpers.test.ts │ │ ├── getFileContentAndPath.test.ts │ │ ├── getSvgFileData.test.ts │ │ ├── loadFile.test.ts │ │ ├── prepFile.test.ts │ │ ├── prepareWrite.test.ts │ │ ├── refresh.test.ts │ │ ├── write.test.ts │ │ ├── writeBaseJson.test.ts │ │ └── writeFile.test.ts │ ├── messages │ │ └── messages.test.ts │ ├── network │ │ ├── downloadFile.test.ts │ │ ├── getData.test.ts │ │ ├── getDataLocal.test.ts │ │ ├── getDataRemote.test.ts │ │ └── getFromApi.test.ts │ ├── string │ │ ├── calculateDegree2Point.test.ts │ │ ├── checkIfStringOnlyContainsReturnsOrSpaces.test.ts │ │ ├── cleanSvgData.test.ts │ │ ├── convertHexToRgba.test.ts │ │ ├── convertRgbaToHex.test.ts │ │ ├── createEnumStringOutOfObject.test.ts │ │ ├── createImportStringFromList.test.ts │ │ ├── createLinearGradientString.test.ts │ │ ├── createRadialGradientString.test.ts │ │ ├── createSolidColorString.test.ts │ │ ├── getAlphaInPercent.test.ts │ │ ├── getFigmaDocumentId.test.ts │ │ ├── getId.test.ts │ │ ├── normalizeUnits.test.ts │ │ ├── removeAllIds.test.ts │ │ ├── replaceMediaQuery.test.ts │ │ ├── roundColorValue.test.ts │ │ ├── roundNumber.test.ts │ │ ├── sanitizeString.test.ts │ │ └── toPascalCase.test.ts │ └── system │ │ └── checkIfVoidElement.test.ts ├── mocks │ ├── files │ │ ├── Google__G__Logo.svg │ │ └── googlelogo_color_272x92dp.png │ ├── handlers.ts │ ├── index.ts │ └── responses │ │ ├── mockFigmaFiles.json │ │ ├── mockFigmaFilesVersions.json │ │ └── mockFigmaImages.json └── usecases │ ├── createElements.test.ts │ ├── createGraphics.test.ts │ ├── createTokens.test.ts │ └── interactors │ ├── common │ └── createPage.test.ts │ ├── elements │ ├── processElements.test.ts │ ├── processGraphicElementsMap.test.ts │ ├── writeElements.test.ts │ └── writeGraphicElementsMap.test.ts │ ├── graphics │ ├── getFileList.test.ts │ ├── getIdString.test.ts │ ├── getIds.test.ts │ ├── processGraphics.test.ts │ └── writeGraphics.test.ts │ └── tokens │ ├── processTokens.test.ts │ └── writeTokens.test.ts ├── bin ├── contracts │ ├── ApiResponse.ts │ ├── Config.ts │ ├── Css.ts │ ├── Element.ts │ ├── Figma.ts │ ├── FigmaData.ts │ ├── FigmaElement.ts │ ├── FigmagicElement.ts │ ├── Files.ts │ ├── Graphic.ts │ ├── Imports.ts │ ├── Metadata.ts │ ├── ParsedElementMetadataInterface.ts │ ├── Parsing.ts │ ├── PrepFile.ts │ ├── ProcessedToken.ts │ ├── Templates.ts │ ├── TextElement.ts │ ├── TokenMatch.ts │ ├── Tokens.ts │ ├── TypographyElement.ts │ └── Write.ts ├── controllers │ └── FigmagicController.ts ├── entities │ ├── Config │ │ ├── baseConfig.ts │ │ ├── index.ts │ │ └── logic │ │ │ ├── createConfiguration.ts │ │ │ ├── parseCliArgs.ts │ │ │ └── validateConfig.ts │ ├── FigmagicElement │ │ ├── index.ts │ │ └── logic │ │ │ ├── classRepresentsTextOnlyElement.ts │ │ │ ├── cleanArrays.ts │ │ │ ├── createCssString.ts │ │ │ ├── getFileContents.ts │ │ │ ├── getIntersectingValues.ts │ │ │ ├── getTokenMatch.ts │ │ │ ├── getUniqueValues.ts │ │ │ ├── parseCssFromElement.ts │ │ │ ├── parseTypographyStylingFromElement.ts │ │ │ ├── parsers │ │ │ ├── getBackgroundColor.ts │ │ │ ├── getBorderColor.ts │ │ │ ├── getPaddingX.ts │ │ │ ├── getPaddingY.ts │ │ │ ├── getShadow.ts │ │ │ ├── parseBackgroundColor.ts │ │ │ ├── parseBorderColor.ts │ │ │ ├── parseBorderRadius.ts │ │ │ ├── parseBorderWidth.ts │ │ │ ├── parseHeight.ts │ │ │ ├── parsePadding.ts │ │ │ ├── parseShadow.ts │ │ │ └── updateParsing.ts │ │ │ ├── processNestedCss.ts │ │ │ └── sliceOutObjectFromFile.ts │ └── Token │ │ ├── index.ts │ │ └── logic │ │ ├── makeBorderWidthTokens.ts │ │ ├── makeColorTokens.ts │ │ ├── makeDelayTokens.ts │ │ ├── makeDurationTokens.ts │ │ ├── makeEasingTokens.ts │ │ ├── makeFontSizeTokens.ts │ │ ├── makeFontTokens.ts │ │ ├── makeFontWeightTokens.ts │ │ ├── makeLetterSpacingTokens.ts │ │ ├── makeLineHeightTokens.ts │ │ ├── makeMediaQueryTokens.ts │ │ ├── makeOpacityTokens.ts │ │ ├── makeRadiusTokens.ts │ │ ├── makeShadowTokens.ts │ │ ├── makeSpacingTokens.ts │ │ └── makeZindexTokens.ts ├── frameworks │ ├── errors │ │ └── errors.ts │ ├── filesystem │ │ ├── checkIfExists.ts │ │ ├── createFolder.ts │ │ ├── createMissingFoldersFromPath.ts │ │ ├── getDataHelpers.ts │ │ ├── getFileContentAndPath.ts │ │ ├── getSvgFileData.ts │ │ ├── isJsonString.ts │ │ ├── loadFile.ts │ │ ├── prepFile.ts │ │ ├── prepareWrite.ts │ │ ├── refresh.ts │ │ ├── write.ts │ │ ├── writeBaseJson.ts │ │ └── writeFile.ts │ ├── messages │ │ └── messages.ts │ ├── network │ │ ├── downloadFile.ts │ │ ├── getData.ts │ │ ├── getDataLocal.ts │ │ ├── getDataRemote.ts │ │ ├── getFromApi.ts │ │ └── request.ts │ ├── string │ │ ├── calculateDegree2Point.ts │ │ ├── checkIfStringOnlyContainsReturnsOrSpaces.ts │ │ ├── cleanSvgData.ts │ │ ├── convertHexToRgba.ts │ │ ├── convertRgbaToHex.ts │ │ ├── createEnumStringOutOfObject.ts │ │ ├── createImportStringFromList.ts │ │ ├── createLinearGradientString.ts │ │ ├── createRadialGradientString.ts │ │ ├── createSolidColorString.ts │ │ ├── getAlphaInPercent.ts │ │ ├── getFigmaDocumentId.ts │ │ ├── getId.ts │ │ ├── normalizeUnits.ts │ │ ├── removeAllIds.ts │ │ ├── replaceMediaQuery.ts │ │ ├── roundColorValue.ts │ │ ├── roundNumber.ts │ │ ├── sanitizeString.ts │ │ └── toPascalCase.ts │ └── system │ │ ├── acceptedFileTypes.ts │ │ ├── acceptedTokenTypes.ts │ │ ├── checkIfVoidElement.ts │ │ ├── colors.ts │ │ ├── configToInit.ts │ │ ├── ignoreElementsKeywords.ts │ │ ├── loadEnv.ts │ │ └── validatorLists.ts └── usecases │ ├── createElements.ts │ ├── createGraphics.ts │ ├── createTokens.ts │ └── interactors │ ├── common │ └── createPage.ts │ ├── elements │ ├── processElements.ts │ ├── processGraphicElementsMap.ts │ ├── writeElements.ts │ └── writeGraphicElementsMap.ts │ ├── graphics │ ├── getFileList.ts │ ├── getIdString.ts │ ├── getIds.ts │ ├── processGraphics.ts │ └── writeGraphics.ts │ └── tokens │ ├── processTokens.ts │ └── writeTokens.ts ├── biome.jsonc ├── codecov.yml ├── generate-keys.sh ├── images ├── add-underscore-to-block.png ├── color-themes-demo.png ├── component-desc-field.png ├── composing-font-from-multiple-tokens.png ├── cover.png ├── demo.png ├── dependency-graph.svg ├── design-tokens.png ├── flat-element.png ├── literal-font-families-demo.png ├── nesting-code.png ├── nesting-error.png ├── nesting-normal.png ├── nesting-warning.png ├── project-structure-elements.png ├── project-structure-tokens.png ├── single-layer-support-only.png └── sometimes-you-have-to-fake.png ├── index.ts ├── jest.config.cjs ├── jest.env.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── readme ├── ANNOUNCEMENTS.md ├── elements.md └── vision.md ├── templates ├── graphic.js ├── graphic.jsx ├── graphic.mjs ├── graphic.tsx ├── react.js ├── react.jsx ├── react.mjs ├── react.tsx ├── story.js ├── story.mdx ├── story.ts ├── styled.js ├── styled.jsx ├── styled.mjs └── styled.tsx ├── testdata ├── components.ts ├── css │ ├── buttonCss.ts │ ├── nestedButton.ts │ └── nestedButtonTextOnly.ts ├── designTokensPage.ts ├── elements │ ├── buttonElement.ts │ ├── components.ts │ ├── cssLayoutElement.ts │ ├── cssLayoutElementGradient.ts │ ├── cssLayoutElementShadow.ts │ ├── cssTypographyElement.ts │ ├── elementsPage.ts │ ├── filteredElements.ts │ ├── flatH1Element.ts │ ├── flatSliderElement.ts │ ├── graphicElement.ts │ ├── groupElements.ts │ ├── nestedSelectElement.ts │ └── writeElements.ts ├── elementsPage.ts ├── enumData.ts ├── exampleData.json ├── figma-complete-cleaned.ts ├── figma-elements.ts ├── figma-minimal.json ├── figma.json ├── figmagicrc ├── fileList.ts ├── frames │ ├── borderWidthsFrame.ts │ ├── colorFrame.ts │ ├── delaysFrame.ts │ ├── durationsFrame.ts │ ├── easingFrame.ts │ ├── fontFrame.ts │ ├── fontSizeFrame.ts │ ├── fontWeightFrame.ts │ ├── graphicsFrame.ts │ ├── invalidFrame.ts │ ├── letterSpacingsFrame.ts │ ├── lineHeightFrame.ts │ ├── mediaQueriesFrame.ts │ ├── opacitiesFrame.ts │ ├── radiiFrame.ts │ ├── shadowsFrame.ts │ ├── spacingFrame.ts │ └── zIndicesFrame.ts ├── getFileContentAndPathOperation.ts ├── getFileDataOperation.ts ├── gradient.ts ├── graphics │ └── getGraphics.ts ├── graphicsPage.ts ├── svg │ ├── check.svg │ ├── close.svg │ ├── more.svg │ └── svg.ts ├── testConfig.ts └── tokens │ ├── borderWidths.js │ ├── borderWidths.mjs │ ├── borderWidths.ts │ ├── colors.js │ ├── colors.mjs │ ├── colors.ts │ ├── durations.js │ ├── durations.mjs │ ├── durations.ts │ ├── fontFamilies.js │ ├── fontFamilies.mjs │ ├── fontFamilies.ts │ ├── fontFamiliesPostscript.js │ ├── fontFamiliesPostscript.mjs │ ├── fontFamiliesPostscript.ts │ ├── fontSizes.js │ ├── fontSizes.mjs │ ├── fontSizes.ts │ ├── fontWeights.js │ ├── fontWeights.mjs │ ├── fontWeights.ts │ ├── letterSpacings.js │ ├── letterSpacings.mjs │ ├── letterSpacings.ts │ ├── lineHeights.js │ ├── lineHeights.mjs │ ├── lineHeights.ts │ ├── mediaQueries.js │ ├── mediaQueries.mjs │ ├── mediaQueries.ts │ ├── opacities.js │ ├── opacities.mjs │ ├── opacities.ts │ ├── radii.js │ ├── radii.mjs │ ├── radii.ts │ ├── shadows.js │ ├── shadows.mjs │ ├── shadows.ts │ ├── spacing.js │ ├── spacing.mjs │ ├── spacing.ts │ ├── zIndices.js │ ├── zIndices.mjs │ └── zIndices.ts ├── translation └── README.pt-br.md ├── tsconfig.json ├── typedoc.json └── typedoc.theme.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | 9 | # Declare files that will always have CRLF line endings on checkout. 10 | *.sln text eol=crlf 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.png binary 14 | *.jpg binary -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mikaelvesavuori -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main, ] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '0 13 * * 6' 10 | 11 | jobs: 12 | analyse: 13 | name: Analyse 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: read 18 | security-events: write 19 | pull-requests: read 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@main 27 | 28 | - name: Autobuild 29 | uses: github/codeql-action/autobuild@main 30 | 31 | - name: Perform CodeQL Analysis 32 | uses: github/codeql-action/analyze@main 33 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run test:types 2 | npm run test:unit 3 | npm run clean 4 | npm run test:dependencies 5 | npm run test:licenses 6 | npm run lint 7 | npm run build 8 | git add . 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Figmagic", 8 | "program": "${workspaceFolder}/index.ts", 9 | "skipFiles": ["/**"], 10 | "preLaunchTask": "tsc: build - tsconfig.json", 11 | "outFiles": ["${workspaceFolder}/build/**/*.js"] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mikaelvesavuori 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018-current Mikael Vesavuori 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | These versions of Figmagic are currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 4.x.x | :white_check_mark: | 10 | | 3.0.x | :x: | 11 | | 2.x.x | :x: | 12 | | 1.x.x | :x: | 13 | | 0.x.x | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | It's unclear to me if any user can add a security advisory report. If you can, do it! Else reach out if with a regular issue. Try to limit the amount of detail since the communication is public. I will then reach out so we can have a private conversation. Please prepare proof of the security vulnerability, and ideally a mitigation strategy. 18 | -------------------------------------------------------------------------------- /__tests__/controllers/FigmagicController.test.ts: -------------------------------------------------------------------------------- 1 | import trash from 'trash'; 2 | 3 | import { FigmagicController } from '../../bin/controllers/FigmagicController'; 4 | 5 | import figmaTestResponse from '../../testdata/figma.json'; 6 | import { testConfig } from '../../testdata/testConfig'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if calling without any arguments', async () => { 10 | // @ts-ignore 11 | await expect(() => FigmagicController()).rejects.toThrowError(); 12 | }); 13 | }); 14 | 15 | describe('Success cases', () => { 16 | test('It should successfully run a full set of operations with local data recompilation', async () => { 17 | const TEST_FOLDER = `__testing-figmagiccontroller`; 18 | const PATH = `./${TEST_FOLDER}`; 19 | 20 | testConfig.outputFolderTokens = TEST_FOLDER; 21 | const RESPONSE = await FigmagicController( 22 | testConfig as any, 23 | figmaTestResponse as any, 24 | ); 25 | expect(RESPONSE).toContain('Figmagic completed operations successfully!'); 26 | 27 | await trash([PATH]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/entities/FigmagicElement/logic/getFileContents.test.ts: -------------------------------------------------------------------------------- 1 | import { getFileContents } from '../../../../bin/entities/FigmagicElement/logic/getFileContents'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if calling without any arguments', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | getFileContents(); 8 | }).toThrow(); 9 | }); 10 | 11 | test('It should throw an error if attempting to get file contents for non-existing file', () => { 12 | const FILE_PATH = '__test-getFileContents__'; 13 | const FILE_NAME = 'testfile001'; 14 | const FORMAT = 'mjs'; 15 | expect(() => getFileContents(FILE_PATH, FILE_NAME, FORMAT)).toThrow(); 16 | }); 17 | }); 18 | 19 | describe('Success cases', () => { 20 | test('It should get file contents from MJS file, and return it as an object', () => { 21 | const FILE_PATH = 'testdata/tokens'; 22 | const FILE_NAME = 'borderWidths'; 23 | const FORMAT = 'mjs'; 24 | expect(getFileContents(FILE_PATH, FILE_NAME, FORMAT)).toEqual( 25 | expect.objectContaining({ 26 | chunky: '8px', 27 | fat: '4px', 28 | hairline: '1px', 29 | regular: '2px', 30 | }), 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/entities/FigmagicElement/logic/processNestedCss.test.ts: -------------------------------------------------------------------------------- 1 | import { processNestedCss } from '../../../../bin/entities/FigmagicElement/logic/processNestedCss'; 2 | 3 | import { 4 | buttonCss, 5 | expectedButtonCss, 6 | } from '../../../../testdata/css/buttonCss'; 7 | import { 8 | nestedButton, 9 | nestedButtonResult, 10 | } from '../../../../testdata/css/nestedButton'; 11 | import { 12 | nestedButtonTextOnly, 13 | nestedButtonTextOnlyResult, 14 | } from '../../../../testdata/css/nestedButtonTextOnly'; 15 | 16 | describe('Failure cases', () => { 17 | test('It should throw an error if no argument is provided', () => { 18 | expect(() => { 19 | // @ts-ignore 20 | processNestedCss(); 21 | }).toThrow(); 22 | }); 23 | }); 24 | 25 | describe('Success cases', () => { 26 | test('It should correctly process CSS that has valid input', () => { 27 | expect(processNestedCss(buttonCss)).toBe(expectedButtonCss); 28 | }); 29 | 30 | test('It should correctly process CSS (nested, text-only element) that has valid input', () => { 31 | expect(processNestedCss(nestedButtonTextOnly)).toBe( 32 | nestedButtonTextOnlyResult, 33 | ); 34 | }); 35 | 36 | test('It should correctly process CSS (nested button element) that has valid input', () => { 37 | expect(processNestedCss(nestedButton)).toBe(nestedButtonResult); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/entities/FigmagicElement/logic/sliceOutObjectFromFile.test.ts: -------------------------------------------------------------------------------- 1 | import { sliceOutObjectFromFile } from '../../../../bin/entities/FigmagicElement/logic/sliceOutObjectFromFile'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if calling without any arguments', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | sliceOutObjectFromFile(); 8 | }).toThrowError(); 9 | }); 10 | 11 | test('It should throw an error if attempting to get file contents for non-existing file', () => { 12 | const FILE_PATH = '__test-getFileContents__/testfile001.mjs'; 13 | expect(() => sliceOutObjectFromFile(FILE_PATH)).toThrowError(); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should get file contents from MJS file, and return it as an object', () => { 19 | const FILE_PATH = 'testdata/tokens/borderWidths.mjs'; 20 | expect(sliceOutObjectFromFile(FILE_PATH)).toEqual( 21 | expect.objectContaining({ 22 | chunky: '8px', 23 | fat: '4px', 24 | hairline: '1px', 25 | regular: '2px', 26 | }), 27 | ); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeDelayTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeDelayTokens } from '../../../../bin/entities/Token/logic/makeDelayTokens'; 2 | 3 | import { 4 | delayFrame, 5 | delayFrameInvalid, 6 | } from '../../../../testdata/frames/delaysFrame'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if frame is missing "children" array', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | makeDelayTokens({}); 13 | }).toThrow(); 14 | }); 15 | 16 | test('It should throw an error if frame does not have "characters" property', () => { 17 | expect(() => { 18 | // @ts-ignore 19 | makeDelayTokens(delayFrameInvalid); 20 | }).toThrow(); 21 | }); 22 | 23 | test('It should throw an error if no argument is provided', () => { 24 | expect(() => { 25 | // @ts-ignore 26 | makeDelayTokens(); 27 | }).toThrow(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | test('It should return a complete object when passing in valid input', () => { 33 | expect(makeDelayTokens(delayFrame)).toEqual( 34 | expect.objectContaining({ 35 | decimal: 0.5, 36 | fast: 200, 37 | medium: 400, 38 | slow: 750, 39 | }), 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeDurationTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeDurationTokens } from '../../../../bin/entities/Token/logic/makeDurationTokens'; 2 | 3 | import { 4 | durationsFrame, 5 | durationsFrameInvalid, 6 | } from '../../../../testdata/frames/durationsFrame'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if frame is missing "children" array', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | makeDurationTokens({}); 13 | }).toThrow(); 14 | }); 15 | 16 | test('It should throw an error if frame does not have "characters" property', () => { 17 | expect(() => { 18 | // @ts-ignore 19 | makeDurationTokens(durationsFrameInvalid); 20 | }).toThrow(); 21 | }); 22 | 23 | test('It should throw an error if no argument is provided', () => { 24 | expect(() => { 25 | // @ts-ignore 26 | makeDurationTokens(); 27 | }).toThrow(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | test('It should return a complete object when passing in valid input, using "s" conversion', () => { 33 | expect(makeDurationTokens(durationsFrame, 's')).toEqual( 34 | expect.objectContaining({ 35 | long: '0.6s', 36 | medium: '0.25s', 37 | short: '0.15s', 38 | veryLong: '1s', 39 | }), 40 | ); 41 | }); 42 | 43 | test('It should return a complete object when passing in valid input, using "ms" conversion', () => { 44 | expect(makeDurationTokens(durationsFrame, 'ms')).toEqual( 45 | expect.objectContaining({ 46 | long: '0.6ms', 47 | medium: '0.25ms', 48 | short: '0.15ms', 49 | veryLong: '1ms', 50 | }), 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeEasingTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeEasingTokens } from '../../../../bin/entities/Token/logic/makeEasingTokens'; 2 | 3 | import { 4 | easingFrame, 5 | easingFrameInvalid, 6 | } from '../../../../testdata/frames/easingFrame'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if frame is missing "children" array', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | makeEasingTokens({}); 13 | }).toThrow(); 14 | }); 15 | 16 | test('It should throw an error if frame does not have "characters" property', () => { 17 | expect(() => { 18 | // @ts-ignore 19 | makeEasingTokens(easingFrameInvalid); 20 | }).toThrow(); 21 | }); 22 | 23 | test('It should throw an error if no argument is provided', () => { 24 | expect(() => { 25 | // @ts-ignore 26 | makeEasingTokens(); 27 | }).toThrow(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | test('It should return a complete object when passing in valid input', () => { 33 | expect(makeEasingTokens(easingFrame)).toEqual( 34 | expect.objectContaining({ 35 | easeIn: 'cubic-bezier(0.50, 0, 1, 1)', 36 | easeOut: 'cubic-bezier(0, 0, 0.40, 1)', 37 | easeInout: 'cubic-bezier(0.45, 0, 0.40, 1)', 38 | }), 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeFontWeightTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeFontWeightTokens } from '../../../../bin/entities/Token/logic/makeFontWeightTokens'; 2 | 3 | import { 4 | fontWeightFrame, 5 | fontWeightFrameFontWeightMismatch, 6 | fontWeightFrameInvalid, 7 | } from '../../../../testdata/frames/fontWeightFrame'; 8 | 9 | describe('Failure cases', () => { 10 | test('It should throw an error if no argument is provided', () => { 11 | expect(() => { 12 | // @ts-ignore 13 | makeFontWeightTokens(); 14 | }).toThrow(); 15 | }); 16 | 17 | test('It should throw an error if children are missing', () => { 18 | expect(() => { 19 | // @ts-ignore 20 | makeFontWeightTokens({}); 21 | }).toThrow(); 22 | }); 23 | 24 | test('It should throw an error if children are missing "name" and "style" properties', () => { 25 | expect(() => { 26 | // @ts-ignore 27 | makeFontWeightTokens(fontWeightFrameInvalid); 28 | }).toThrow(); 29 | }); 30 | 31 | test('It should throw an error if children has "style" property but not "fontWeight"', () => { 32 | expect(() => { 33 | // @ts-ignore 34 | makeFontWeightTokens(fontWeightFrameFontWeightMismatch); 35 | }).toThrow(); 36 | }); 37 | }); 38 | 39 | describe('Success cases', () => { 40 | test('It should return a complete object when passing in valid input', () => { 41 | expect(makeFontWeightTokens(fontWeightFrame)).toEqual( 42 | expect.objectContaining({ light: 300, medium: 500, regular: 400 }), 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeMediaQueryTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeMediaQueryTokens } from '../../../../bin/entities/Token/logic/makeMediaQueryTokens'; 2 | 3 | import { 4 | mediaQueriesFrame, 5 | mediaQueriesFrameNoAbsoluteBoundingBox, 6 | } from '../../../../testdata/frames/mediaQueriesFrame'; 7 | 8 | describe('Success cases', () => { 9 | test('It should throw an error if frame is missing "children" array', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | makeMediaQueryTokens({}); 13 | }).toThrow(); 14 | }); 15 | 16 | test('It should throw an error if frame does not have "absoluteBoundingBox" property', () => { 17 | expect(() => { 18 | // @ts-ignore 19 | makeMediaQueryTokens(mediaQueriesFrameNoAbsoluteBoundingBox); 20 | }).toThrow(); 21 | }); 22 | 23 | test('It should throw an error if no argument is provided', () => { 24 | expect(() => { 25 | // @ts-ignore 26 | makeMediaQueryTokens(); 27 | }).toThrow(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | test('It should return a complete object when passing in valid input', () => { 33 | expect(makeMediaQueryTokens(mediaQueriesFrame)).toEqual( 34 | expect.objectContaining({ 35 | desktopLg: '1440px', 36 | desktopMd: '1180px', 37 | mobileLg: '580px', 38 | mobileMax: '767px', 39 | mobileMd: '480px', 40 | mobileSm: '320px', 41 | tabletMax: '1024px', 42 | tabletMin: '768px', 43 | wide: '1920px', 44 | }), 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeOpacityTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeOpacityTokens } from '../../../../bin/entities/Token/logic/makeOpacityTokens'; 2 | 3 | import { 4 | opacitiesFrame, 5 | opacitiesFrameNoName, 6 | } from '../../../../testdata/frames/opacitiesFrame'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | makeOpacityTokens(); 13 | }).toThrow(); 14 | }); 15 | 16 | test('It should throw an error if children are missing', () => { 17 | expect(() => { 18 | // @ts-ignore 19 | makeOpacityTokens({}); 20 | }).toThrow(); 21 | }); 22 | 23 | test('It should throw an error if children are missing "name" property', () => { 24 | expect(() => { 25 | // @ts-ignore 26 | makeOpacityTokens(opacitiesFrameNoName); 27 | }).toThrow(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | test('It should return values using float numbers', () => { 33 | expect(makeOpacityTokens(opacitiesFrame, 'float')).toEqual( 34 | expect.objectContaining({ 35 | opaque: 1, 36 | disabled: 0.65, 37 | semiOpaque: 0.5, 38 | transparent: 0, 39 | }), 40 | ); 41 | }); 42 | 43 | test('It should return values using percentages', () => { 44 | expect(makeOpacityTokens(opacitiesFrame, 'percent')).toEqual( 45 | expect.objectContaining({ 46 | opaque: '100%', 47 | disabled: '65%', 48 | semiOpaque: '50%', 49 | transparent: '0%', 50 | }), 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/entities/Token/logic/makeZindexTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { makeZindexTokens } from '../../../../bin/entities/Token/logic/makeZindexTokens'; 2 | 3 | import { 4 | zIndicesFrame, 5 | zIndicesFrameNoCharacters, 6 | } from '../../../../testdata/frames/zIndicesFrame'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if frame is missing "children" array', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | makeZindexTokens({}); 13 | }).toThrow(); 14 | }); 15 | 16 | test('It should throw an error if frame does not have "characters" property', () => { 17 | expect(() => { 18 | // @ts-ignore 19 | makeZindexTokens(zIndicesFrameNoCharacters); 20 | }).toThrow(); 21 | }); 22 | 23 | test('It should throw an error if no argument is provided', () => { 24 | expect(() => { 25 | // @ts-ignore 26 | makeZindexTokens(); 27 | }).toThrow(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | test('It should return a complete object when passing in valid input', () => { 33 | expect(makeZindexTokens(zIndicesFrame)).toEqual( 34 | expect.objectContaining({ 35 | focus: 10, 36 | high: 1, 37 | higher: 2, 38 | regular: 0, 39 | top: 100, 40 | }), 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/frameworks/errors/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { ErrorLoadFile } from '../../../bin/frameworks/errors/errors'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if not given a path', () => { 5 | // @ts-ignore 6 | expect(() => ErrorLoadFile()).toThrowError(); 7 | }); 8 | }); 9 | 10 | describe('Success cases', () => { 11 | test('It should show new and old values with separating marks', () => { 12 | expect(ErrorLoadFile('something')).toMatch( 13 | 'Could not find file: something!', 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/checkIfExists.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { checkIfExists } from '../../../bin/frameworks/filesystem/checkIfExists'; 4 | 5 | describe('Success cases', () => { 6 | test('It should return true if it finds a file', () => { 7 | const PATH = path.join('testdata', 'figma.json'); 8 | const FILE_EXISTS = checkIfExists(PATH); 9 | expect(FILE_EXISTS).toBe(true); 10 | }); 11 | 12 | test('It should return false if it does not find a file', () => { 13 | const PATH = path.join('testdata', 'ajhk3jhfkj3'); 14 | const FILE_EXISTS = checkIfExists(PATH); 15 | expect(FILE_EXISTS).toBe(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/createFolder.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { createFolder } from '../../../bin/frameworks/filesystem/createFolder'; 5 | 6 | import { ErrorCreateFolder } from '../../../bin/frameworks/errors/errors'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', () => { 10 | // @ts-ignore 11 | expect(() => createFolder()).toThrowError(ErrorCreateFolder); 12 | }); 13 | }); 14 | 15 | describe('Success cases', () => { 16 | test('It should be able to successfully create a new folder if it does not exist', async () => { 17 | const TEST_FOLDER = '___xxx'; 18 | const PATH = `./${TEST_FOLDER}`; 19 | 20 | createFolder(PATH); 21 | const FILE_EXISTS = fs.existsSync(PATH); 22 | expect(FILE_EXISTS).toBe(true); 23 | await trash(PATH); 24 | }); 25 | 26 | test('It should be able to successfully work if folder exists', async () => { 27 | const TEST_FOLDER = '___xxx'; 28 | createFolder(TEST_FOLDER); 29 | const PATH = `./${TEST_FOLDER}`; 30 | 31 | createFolder(PATH); 32 | const FILE_EXISTS = fs.existsSync(PATH); 33 | expect(FILE_EXISTS).toBe(true); 34 | await trash(PATH); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/createMissingFoldersFromPath.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { createMissingFoldersFromPath } from '../../../bin/frameworks/filesystem/createMissingFoldersFromPath'; 5 | 6 | import { ErrorCreateMissingFoldersFromPath } from '../../../bin/frameworks/errors/errors'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', () => { 10 | // @ts-ignore 11 | expect(() => createMissingFoldersFromPath()).toThrowError( 12 | ErrorCreateMissingFoldersFromPath, 13 | ); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should be able to successfully create a set of nested folders if they do not exist', async () => { 19 | const PATH = `./___createMissingFoldersFromPath1___/asdf/qwerty/`; 20 | 21 | createMissingFoldersFromPath(PATH); 22 | const FILE_EXISTS = fs.existsSync(PATH); 23 | expect(FILE_EXISTS).toBe(true); 24 | await trash('___createMissingFoldersFromPath1___'); 25 | }); 26 | 27 | test('It should not do anything if the path does not contain subfolders', async () => { 28 | const PATH = `./___createMissingFoldersFromPath2___`; 29 | 30 | createMissingFoldersFromPath(PATH); 31 | const FILE_EXISTS = fs.existsSync(PATH); 32 | expect(FILE_EXISTS).toBe(true); 33 | await trash('___createMissingFoldersFromPath2___'); 34 | }); 35 | 36 | test('It should not do anything if the path does not contain slashes', async () => { 37 | const PATH = `___createMissingFoldersFromPath3___`; 38 | 39 | createMissingFoldersFromPath(PATH); 40 | const FILE_EXISTS = fs.existsSync(PATH); 41 | expect(FILE_EXISTS).toBe(false); 42 | await trash('___createMissingFoldersFromPath3___'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/getSvgFileData.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { getSvgFileData } from '../../../bin/frameworks/filesystem/getSvgFileData'; 4 | 5 | describe('Failure cases', () => { 6 | test('It should throw an error if no argument is provided', () => { 7 | expect(() => { 8 | // @ts-ignore 9 | getSvgFileData(); 10 | }).toThrow(); 11 | }); 12 | }); 13 | 14 | describe('Success cases', () => { 15 | test('It should return an empty string if unable to find the specified file', () => { 16 | expect(getSvgFileData(path.join('akjslajsljka'))).toBe(''); 17 | }); 18 | 19 | // TODO: Test fails inexplicably on Windows? 20 | test('It should return SVG file data', () => { 21 | const SVG = `\n\n`; 22 | expect(getSvgFileData(path.join('testdata', 'svg', 'check.svg'))).toBe(SVG); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/loadFile.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { loadFile } from '../../../bin/frameworks/filesystem/loadFile'; 4 | 5 | describe('Failure cases', () => { 6 | test('It should throw an error if no argument is provided', () => { 7 | // @ts-ignore 8 | expect(() => loadFile()).toThrowError(); 9 | }); 10 | 11 | test('It should throw an error if invalid path is provided', () => { 12 | const BAD_PATH = `./AKLJR#LJKASlaks`; 13 | expect(() => loadFile(BAD_PATH)).toThrowError(); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should return data from local file', () => { 19 | const FILE = loadFile( 20 | path.join(`${process.cwd()}`, `testdata`, `figmagicrc`), 21 | ); 22 | expect(FILE).toEqual( 23 | expect.objectContaining({ 24 | debugMode: false, 25 | fontUnit: 'rem', 26 | figmaData: 'figma.json', 27 | figmagicFolder: '.figmagic', 28 | outputFolderGraphics: 'graphics', 29 | outputFolderTokens: 'tokens', 30 | outputFormatGraphics: 'svg', 31 | outputScaleGraphics: 1, 32 | outputFormatTokens: 'ts', 33 | spacingUnit: 'rem', 34 | usePostscriptFontNames: false, 35 | useLiteralFontFamilies: false, 36 | }), 37 | ); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/refresh.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { createFolder } from '../../../bin/frameworks/filesystem/createFolder'; 5 | import { refresh } from '../../../bin/frameworks/filesystem/refresh'; 6 | 7 | describe('Failure cases', () => { 8 | test('It should throw an error if no argument is provided', async () => { 9 | // @ts-ignore 10 | expect(() => refresh()).toThrow(); 11 | }); 12 | }); 13 | 14 | describe('Success cases', () => { 15 | test('It should successfully create a folder that does not already exist', async () => { 16 | const testPath = `__test-refresh__`; 17 | const refreshedPath = refresh(testPath); 18 | expect(fs.existsSync(refreshedPath)).toBeTruthy(); 19 | }); 20 | 21 | test('It should successfully refresh an existing nested folder', async () => { 22 | const testPath = `__test-refresh__/something/folder`; 23 | createFolder(testPath); 24 | const refreshedPath = refresh(testPath); 25 | // @ts-ignore 26 | expect(fs.existsSync(refreshedPath)).toBeTruthy(); 27 | }); 28 | }); 29 | 30 | afterAll(async () => { 31 | await trash(['__test-refresh__']); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/write.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { write } from '../../../bin/frameworks/filesystem/write'; 5 | 6 | describe('Failure cases', () => { 7 | test('It should throw an error if no argument is provided', () => { 8 | // @ts-ignore 9 | expect(() => write()).toThrowError(); 10 | }); 11 | }); 12 | 13 | describe('Success cases', () => { 14 | test('It should successfully create a file on disk', async () => { 15 | const FILE_PATH = `./__write-success-test__.txt`; 16 | const FILE_CONTENT = 'Something here'; 17 | 18 | write(FILE_PATH, FILE_CONTENT); 19 | const DISK_CONTENTS = fs.readFileSync(FILE_PATH, { encoding: 'utf-8' }); 20 | expect(DISK_CONTENTS).toBe(FILE_CONTENT); 21 | 22 | await trash(FILE_PATH); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/frameworks/filesystem/writeBaseJson.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { writeBaseJson } from '../../../bin/frameworks/filesystem/writeBaseJson'; 5 | 6 | const TEST_FOLDER = '__test-writeBaseJson'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', async () => { 10 | // @ts-ignore 11 | await expect(writeBaseJson()).rejects.toThrow(); 12 | }); 13 | 14 | test('It should throw an error if missing outputFolder"BaseFile', async () => { 15 | // @ts-ignore 16 | await expect(writeBaseJson(null, 'asdf', {})).rejects.toThrow(); 17 | }); 18 | 19 | test('It should throw an error if missing "figmaData"', async () => { 20 | // @ts-ignore 21 | await expect(writeBaseJson('asdf', null, {})).rejects.toThrow(); 22 | }); 23 | 24 | test('It should throw an error if missing "data"', async () => { 25 | // @ts-ignore 26 | await expect(writeBaseJson('asdf', 'asdf', null)).rejects.toThrow(); 27 | }); 28 | }); 29 | 30 | describe('Success cases', () => { 31 | test('It should write the base Figma file', async () => { 32 | const testFile = '__test-writeBaseJson.txt'; 33 | const path = `${TEST_FOLDER}/${testFile}`; 34 | await writeBaseJson(TEST_FOLDER, testFile, { 35 | data: 'something', 36 | }); 37 | const fileContent = fs.readFileSync(path, { encoding: 'utf-8' }); 38 | expect(fileContent).toBe(`{"data":"something"}`); 39 | }); 40 | }); 41 | 42 | afterAll(async () => { 43 | await trash(TEST_FOLDER); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/frameworks/messages/messages.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MsgDownloadFileWritingFile, 3 | MsgProcessElementsCreatingElement, 4 | } from '../../../bin/frameworks/messages/messages'; 5 | 6 | describe('Failure cases', () => {}); 7 | 8 | describe('Success cases', () => { 9 | test('It should show new and old values with separating marks', () => { 10 | expect(MsgProcessElementsCreatingElement('something', 'something')).toBe( 11 | `Processing Figma element \"something\" as ---> something`, 12 | ); 13 | }); 14 | 15 | test('It should write a helpful line describing what file it has written', () => { 16 | expect(MsgDownloadFileWritingFile('asdf')).toMatch( 17 | '\nWriting Figma graphics to disk: asdf', 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/frameworks/network/downloadFile.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { downloadFile } from '../../../bin/frameworks/network/downloadFile'; 5 | 6 | const FILENAME = `__downloadFile-success-image__.png`; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', async () => { 10 | // @ts-ignore 11 | await expect(downloadFile()).rejects.toThrow(); 12 | }); 13 | 14 | test('It should fail if returning a non-200 status', async () => { 15 | await expect( 16 | downloadFile( 17 | 'https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/0882/9cc7/c4731c8d0df07592cd6f7dc0519bb3bb-asdf', 18 | '__downloadFile-fail-image__.svg', 19 | ), 20 | ).rejects.toBeNull(); 21 | }); 22 | }); 23 | 24 | describe('Success cases', () => { 25 | test('It should download a file if given valid arguments', async () => { 26 | await downloadFile( 27 | 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png', 28 | FILENAME, 29 | ).then(() => { 30 | const FILE_EXISTS = fs.existsSync(FILENAME); 31 | expect(FILE_EXISTS).toBeTruthy(); 32 | }); 33 | }); 34 | }); 35 | 36 | afterAll(async () => { 37 | await trash(FILENAME); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/frameworks/network/getDataLocal.test.ts: -------------------------------------------------------------------------------- 1 | import { getDataLocal } from '../../../bin/frameworks/network/getDataLocal'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', async () => { 5 | // @ts-ignore 6 | expect(() => getDataLocal()).toThrowError(); 7 | }); 8 | 9 | test('It should throw an error if file does not exist', () => { 10 | const OUTPUT_FOLDER_BASE_FILE = 'testdata'; 11 | const OUTPUT_FILENAME = 'some-file-that-does-not-exist.mp4'; 12 | expect(() => 13 | getDataLocal(OUTPUT_FOLDER_BASE_FILE, OUTPUT_FILENAME), 14 | ).toThrowError(); 15 | }); 16 | }); 17 | 18 | describe('Success cases', () => { 19 | test('It should successfully load a local file if it exists', () => { 20 | const OUTPUT_FOLDER_BASE_FILE = 'testdata'; 21 | const OUTPUT_FILENAME = 'figma-minimal.json'; 22 | const DATA = { 23 | document: { 24 | id: '0:0', 25 | name: 'Document', 26 | type: 'DOCUMENT', 27 | children: [ 28 | { 29 | id: '2605:12', 30 | name: 'Design Tokens', 31 | }, 32 | ], 33 | }, 34 | }; 35 | // @ts-ignore); 36 | expect( 37 | getDataLocal(OUTPUT_FOLDER_BASE_FILE, OUTPUT_FILENAME), 38 | ).toMatchObject(DATA); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/frameworks/network/getDataRemote.test.ts: -------------------------------------------------------------------------------- 1 | import { getDataRemote } from '../../../bin/frameworks/network/getDataRemote'; 2 | import { loadEnv } from '../../../bin/frameworks/system/loadEnv'; 3 | 4 | loadEnv(); 5 | 6 | const FIGMA_TOKEN = process.env.IS_MOCK_ENABLED 7 | ? 'mocked' 8 | : process.env.FIGMA_TOKEN; 9 | const FIGMA_URL = process.env.IS_MOCK_ENABLED 10 | ? 'mocked' 11 | : process.env.FIGMA_URL; 12 | 13 | describe('Failure cases', () => { 14 | test('It should throw an error if no argument is provided', async () => { 15 | // @ts-ignore 16 | await expect(getDataRemote()).rejects.toThrow(); 17 | }); 18 | 19 | test('It should fail given invalid URL and token', async () => { 20 | await expect(getDataRemote('token', 'invalid-url')).rejects.toThrow(); 21 | }); 22 | }); 23 | 24 | describe('Success cases', () => { 25 | test('It should get API data given valid URL and token', async () => { 26 | await expect( 27 | // @ts-ignore 28 | getDataRemote(FIGMA_TOKEN, FIGMA_URL), 29 | ).resolves.toBeTruthy(); 30 | }); 31 | 32 | test('It should get API data given valid URL and token and a named version', async () => { 33 | await expect( 34 | // @ts-ignore 35 | getDataRemote(FIGMA_TOKEN, FIGMA_URL, 'Version 4.1.0'), 36 | ).resolves.toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/frameworks/network/getFromApi.test.ts: -------------------------------------------------------------------------------- 1 | import { getFromApi } from '../../../bin/frameworks/network/getFromApi'; 2 | import { loadEnv } from '../../../bin/frameworks/system/loadEnv'; 3 | 4 | loadEnv(); 5 | 6 | const FIGMA_TOKEN = process.env.IS_MOCK_ENABLED 7 | ? 'mocked' 8 | : process.env.FIGMA_TOKEN; 9 | const FIGMA_URL = process.env.IS_MOCK_ENABLED 10 | ? 'mocked' 11 | : process.env.FIGMA_URL; 12 | 13 | describe('Failure cases', () => { 14 | test('It should throw an error when receiving invalid token and/or URL', async () => { 15 | await expect(getFromApi('asdf', 'invalid-url')).rejects.toThrowError(); 16 | }); 17 | 18 | test('It should throw an error if no argument is provided', async () => { 19 | // @ts-ignore 20 | await expect(getFromApi()).rejects.toThrowError(); 21 | }); 22 | 23 | test('It should not find data, given a token but invalid URL', async () => { 24 | expect( 25 | getFromApi( 26 | 'some-token-here', 27 | 'https://lkhjtkl34kljf-fg3kj3443.hjt3hjk.net/.kj34jkl34', 28 | ), 29 | ).rejects.toThrowError(); 30 | }); 31 | 32 | test('It should find valid data (assuming the base document ID to be "0:0") when passed valid token and URL', async () => { 33 | // @ts-ignore 34 | const DATA = await getFromApi(FIGMA_TOKEN, FIGMA_URL); 35 | expect(DATA.document.id).toEqual('0:0'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/calculateDegree2Point.test.ts: -------------------------------------------------------------------------------- 1 | import { calculateDegree2Point } from '../../../bin/frameworks/string/calculateDegree2Point'; 2 | 3 | import { 4 | points1, 5 | points2, 6 | points3, 7 | points4, 8 | points5, 9 | } from '../../../testdata/gradient'; 10 | 11 | describe('Failure cases', () => { 12 | test('It should throw an error if no argument is provided', () => { 13 | expect(() => { 14 | // @ts-ignore 15 | calculateDegree2Point(); 16 | }).toThrow(); 17 | }); 18 | }); 19 | 20 | describe('Success cases', () => { 21 | test('It should return 180', () => { 22 | expect(calculateDegree2Point(points1[0], points1[1])).toBe(180); 23 | }); 24 | 25 | test('It should return 90', () => { 26 | expect(calculateDegree2Point(points2[0], points2[1])).toBe(90); 27 | }); 28 | 29 | // The below cases will not be equal to Figma's own calculated values 30 | test('It should return 140.38', () => { 31 | expect(calculateDegree2Point(points3[0], points3[1])).toBe(140.38); 32 | }); 33 | 34 | test('It should return 135', () => { 35 | expect(calculateDegree2Point(points4[0], points4[1])).toBe(135); 36 | }); 37 | 38 | test('It should return 118.26', () => { 39 | expect(calculateDegree2Point(points5[0], points5[1])).toBe(118.26); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/checkIfStringOnlyContainsReturnsOrSpaces.test.ts: -------------------------------------------------------------------------------- 1 | import { checkIfStringOnlyContainsReturnsOrSpaces } from '../../../bin/frameworks/string/checkIfStringOnlyContainsReturnsOrSpaces'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | checkIfStringOnlyContainsReturnsOrSpaces(); 8 | }).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | const TEST_1 = '\n'; 14 | const TEST_2 = '\n\n'; 15 | const TEST_3 = '\n\n\n'; 16 | const TEST_4 = '\n\n \n'; 17 | const TEST_5 = '\n\n \n .classname {'; 18 | const TEST_6 = ' '; 19 | 20 | test('It should return true for single return', () => { 21 | expect(checkIfStringOnlyContainsReturnsOrSpaces(TEST_1)).toBe(true); 22 | }); 23 | 24 | test('It should return true for double returns', () => { 25 | expect(checkIfStringOnlyContainsReturnsOrSpaces(TEST_2)).toBe(true); 26 | }); 27 | 28 | test('It should return true for triple returns', () => { 29 | expect(checkIfStringOnlyContainsReturnsOrSpaces(TEST_3)).toBe(true); 30 | }); 31 | 32 | test('It should return false for returns with spaces', () => { 33 | expect(checkIfStringOnlyContainsReturnsOrSpaces(TEST_4)).toBe(false); 34 | }); 35 | 36 | test('It should return true for returns with class names', () => { 37 | expect(checkIfStringOnlyContainsReturnsOrSpaces(TEST_5)).toBe(false); 38 | }); 39 | 40 | test('It should return true for single space', () => { 41 | expect(checkIfStringOnlyContainsReturnsOrSpaces(TEST_6)).toBe(true); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/cleanSvgData.test.ts: -------------------------------------------------------------------------------- 1 | import { svgData } from '../../../testdata/svg/svg'; 2 | 3 | import { cleanSvgData } from '../../../bin/frameworks/string/cleanSvgData'; 4 | 5 | describe('Failure cases', () => { 6 | test('It should throw an error if no argument is provided', () => { 7 | expect(() => { 8 | // @ts-ignore 9 | cleanSvgData(); 10 | }).toThrow(); 11 | }); 12 | }); 13 | 14 | describe('Success cases', () => { 15 | test('It should remove width and height properties from SVG data string', () => { 16 | expect(cleanSvgData(svgData)).toBe( 17 | `\n\n`, 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/convertHexToRgba.test.ts: -------------------------------------------------------------------------------- 1 | import { convertHexToRgba } from '../../../bin/frameworks/string/convertHexToRgba'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | convertHexToRgba(); 8 | }).toThrow(); 9 | }); 10 | 11 | test('It should throw an error if missing a single parameter', () => { 12 | expect(() => { 13 | // @ts-ignore 14 | convertHexToRgba(1, 1, 1); 15 | }).toThrow(); 16 | }); 17 | }); 18 | 19 | describe('Success cases', () => { 20 | test('It should correctly return a CSS standard RGBA string', () => { 21 | expect(convertHexToRgba('#33ff00')).toBe(`rgba(51, 255, 0, 1)`); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/convertRgbaToHex.test.ts: -------------------------------------------------------------------------------- 1 | import { convertRgbaToHex } from '../../../bin/frameworks/string/convertRgbaToHex'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | convertRgbaToHex(); 8 | }).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | test('It should correctly return a hexadecimal color value from an RGBA color value', () => { 14 | expect(convertRgbaToHex('rgba(7,75,114,1)')).toBe('#074b72ff'); 15 | }); 16 | 17 | test('It should correctly return a hexadecimal color value from an RGB color value, defaulting to alpha 1', () => { 18 | expect(convertRgbaToHex('rgba(7,75,114)')).toBe('#074b72ff'); 19 | }); 20 | 21 | test('It should correctly return a hexadecimal color value from an RGB color value with transparency', () => { 22 | expect(convertRgbaToHex('rgba(7,75,114,0.33)')).toBe('#074b7254'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/createEnumStringOutOfObject.test.ts: -------------------------------------------------------------------------------- 1 | import { createEnumStringOutOfObject } from '../../../bin/frameworks/string/createEnumStringOutOfObject'; 2 | 3 | import { 4 | enumDataExpectedResponse, 5 | enumDataTestObject, 6 | } from '../../../testdata/enumData'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | createEnumStringOutOfObject(); 13 | }).toThrow(); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should return a correct enum-format response', () => { 19 | expect(createEnumStringOutOfObject(enumDataTestObject)).toBe( 20 | enumDataExpectedResponse, 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/createLinearGradientString.test.ts: -------------------------------------------------------------------------------- 1 | import { createLinearGradientString } from '../../../bin/frameworks/string/createLinearGradientString'; 2 | 3 | import { 4 | gradientHandlePositions, 5 | gradientStops, 6 | } from '../../../testdata/gradient'; 7 | 8 | describe('Failure cases', () => { 9 | test('It should throw an error if no argument is provided', () => { 10 | expect(() => { 11 | // @ts-ignore 12 | createLinearGradientString(); 13 | }).toThrow(); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should successfully process a three-stop linear gradient', () => { 19 | expect( 20 | createLinearGradientString({ 21 | gradientStops, 22 | gradientHandlePositions, 23 | }), 24 | ).toBe( 25 | 'linear-gradient(180deg, rgba(255, 255, 255, 1) 20%, rgba(43, 83, 144, 0.99) 45%, rgba(1, 10, 23, 0.76) 73%)', 26 | ); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/createRadialGradientString.test.ts: -------------------------------------------------------------------------------- 1 | import { createRadialGradientString } from '../../../bin/frameworks/string/createRadialGradientString'; 2 | 3 | import { 4 | gradientHandlePositionsRadialFourStop, 5 | gradientHandlePositionsRadialThreeStop, 6 | gradientStopsRadialFourStop, 7 | gradientStopsRadialThreeStop, 8 | } from '../../../testdata/gradient'; 9 | 10 | describe('Failure cases', () => { 11 | test('It should throw an error if no argument is provided', () => { 12 | expect(() => { 13 | // @ts-ignore 14 | createRadialGradientString(); 15 | }).toThrow(); 16 | }); 17 | }); 18 | 19 | describe('Success cases', () => { 20 | test('It should successfully process a three-stop radial gradient', () => { 21 | expect( 22 | createRadialGradientString({ 23 | gradientStops: gradientStopsRadialThreeStop, 24 | gradientHandlePositions: gradientHandlePositionsRadialThreeStop, 25 | }), 26 | ).toBe( 27 | 'radial-gradient(50.0% 50.0% at 50.0% 50.0%, rgba(20, 173, 63, 1) 0%, rgba(67, 2, 252, 0.52) 48%, rgba(20, 173, 63, 0) 100%)', 28 | ); 29 | }); 30 | 31 | test('It should successfully process a bigger four-stop radial gradient', () => { 32 | expect( 33 | createRadialGradientString({ 34 | gradientStops: gradientStopsRadialFourStop, 35 | gradientHandlePositions: gradientHandlePositionsRadialFourStop, 36 | }), 37 | ).toBe( 38 | 'radial-gradient(171.6% 182.0% at 11.5% -26.0%, rgba(163, 191, 217, 1) 0%, rgba(89, 71, 143, 0.51) 28%, rgba(207, 45, 45, 0.52) 69%, rgba(33, 44, 36, 0) 100%)', 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/createSolidColorString.test.ts: -------------------------------------------------------------------------------- 1 | import { createSolidColorString } from '../../../bin/frameworks/string/createSolidColorString'; 2 | 3 | import { flatH1Element } from '../../../testdata/elements/flatH1Element'; 4 | 5 | describe('Failure cases', () => { 6 | test('It should throw an error if no argument is provided', () => { 7 | expect(() => { 8 | // @ts-ignore 9 | createSolidColorString(); 10 | }).toThrow(); 11 | }); 12 | }); 13 | 14 | describe('Success cases', () => { 15 | test('It should create a RGBA format (solid color) string from element', () => { 16 | // @ts-ignore 17 | expect(createSolidColorString(flatH1Element.children[0].fills[0])).toBe( 18 | 'rgba(51, 51, 51, 1)', 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/getAlphaInPercent.test.ts: -------------------------------------------------------------------------------- 1 | import { getAlphaInPercent } from '../../../bin/frameworks/string/getAlphaInPercent'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | getAlphaInPercent(); 8 | }).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | test('It should get alpha in percent', () => { 14 | expect(getAlphaInPercent('rgba(123,52,21,0.51)')).toBe('51%'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/getFigmaDocumentId.test.ts: -------------------------------------------------------------------------------- 1 | import { getFigmaDocumentId } from '../../../bin/frameworks/string/getFigmaDocumentId'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | getFigmaDocumentId(); 8 | }).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | test('It should return the document ID substring if a full URL is provided', () => { 14 | expect( 15 | getFigmaDocumentId( 16 | 'https://www.figma.com/file/lkf32lkncowicdschjlkskhclsjskdh/SomeFileName-€#JH', 17 | ), 18 | ).toBe('lkf32lkncowicdschjlkskhclsjskdh'); 19 | }); 20 | 21 | test('It should return the string as-is if it does not begin with "https://www.figma.com/file/"', () => { 22 | expect(getFigmaDocumentId('lkf32lkncowicdschjlkskhclsjskdh')).toBe( 23 | 'lkf32lkncowicdschjlkskhclsjskdh', 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/getId.test.ts: -------------------------------------------------------------------------------- 1 | import { getId } from '../../../bin/frameworks/string/getId'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | getId(); 8 | }).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | const TEST_1 = '.:disabled__#5199 {'; 14 | const TEST_2 = '.Normal__#5199 {'; 15 | const TEST_3 = '.Warning__#63 {'; 16 | const TEST_4 = '.Warning {'; 17 | const TEST_5 = '.Warning__#d{'; 18 | const TEST_6 = '.Warning__#6325928352mndbm238 {'; 19 | 20 | test('It should return 5199 from pseudo-class name', () => { 21 | expect(getId(TEST_1)).toBe('5199 '); 22 | }); 23 | 24 | test('It should return 5199 from class name', () => { 25 | expect(getId(TEST_2)).toBe('5199 '); 26 | }); 27 | 28 | test('It should return 63 from class name', () => { 29 | expect(getId(TEST_3)).toBe('63 '); 30 | }); 31 | 32 | test('It should return null if provided string lacking ID', () => { 33 | expect(getId(TEST_4)).toBe(null); 34 | }); 35 | 36 | test('It should return null if provided ID but no space between it and the opening brace', () => { 37 | expect(getId(TEST_5)).toBe(null); 38 | }); 39 | 40 | test('It should return value even if given arbitrarily long names', () => { 41 | expect(getId(TEST_6)).toBe('6325928352mndbm238 '); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/normalizeUnits.test.ts: -------------------------------------------------------------------------------- 1 | import { normalizeUnits } from '../../../bin/frameworks/string/normalizeUnits'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | normalizeUnits(); 8 | }).toThrow(); 9 | }); 10 | 11 | test('It should throw an error if trying to convert to rem but having no remSize provided', () => { 12 | expect(() => { 13 | normalizeUnits(400, 'px', 'rem'); 14 | }).toThrow(); 15 | }); 16 | 17 | test('It should throw an error if being passed an invalid newUnit, since it cannot set rootSize', () => { 18 | expect(() => { 19 | normalizeUnits(400, 'px', 'asdf'); 20 | }).toThrow(); 21 | }); 22 | }); 23 | 24 | describe('Success cases', () => { 25 | test('It should normalize a percent unit to unitless, when given a width value, current unit string, and a conversion type as float', () => { 26 | expect(normalizeUnits(146.484375, 'percent', 'unitless')).toBe( 27 | '1.46484375', 28 | ); 29 | }); 30 | 31 | test('It should normalize a letter-spacing unit to a converted pixel value, when given a width value, current unit string, and a conversion type as string', () => { 32 | expect(normalizeUnits(100, 'px', 'px', 16)).toBe('100px'); 33 | }); 34 | 35 | test('It should normalize a px value to base rem size, when given a width value, current unit string, and a conversion type as string', () => { 36 | expect(normalizeUnits(48, 'px', 'rem', 16)).toBe('3rem'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/replaceMediaQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { replaceMediaQuery } from '../../../bin/frameworks/string/replaceMediaQuery'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | // @ts-ignore 6 | expect(() => replaceMediaQuery()).toThrow(); 7 | }); 8 | }); 9 | 10 | describe('Success cases', () => { 11 | test('It should return the string if function could not find a match', () => { 12 | expect(replaceMediaQuery(' ', '@upto')).toBe(' '); 13 | }); 14 | 15 | test('It should return a valid media query', () => { 16 | expect(replaceMediaQuery('@upto 768', '@upto')).toBe( 17 | '@media query and (max-width:px) { 768', 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/roundColorValue.test.ts: -------------------------------------------------------------------------------- 1 | import { roundColorValue } from '../../../bin/frameworks/string/roundColorValue'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error when scale is below 0', () => { 5 | expect(() => { 6 | roundColorValue(1.0, -1); 7 | }).toThrow(); 8 | }); 9 | 10 | test('It should throw an error when scale is above 255', () => { 11 | expect(() => { 12 | roundColorValue(1.0, 256); 13 | }).toThrow(); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should return a value based on the two defaults for "quantity" and "scale"', () => { 19 | expect(roundColorValue()).toBe(0); 20 | }); 21 | 22 | test('It should set a negative "quantity" to 0 and return a value based on the two defaults for "quantity" and "scale"', () => { 23 | expect(roundColorValue(-4.2)).toBe(0); 24 | }); 25 | 26 | test('It should round a color value into a single decimal', () => { 27 | expect(roundColorValue(0.5176470875740051, 255)).toBe(132); 28 | }); 29 | 30 | test('It should set full "quantity" value (1.0) to be 255', () => { 31 | expect(roundColorValue(1.0, 255)).toBe(255); 32 | }); 33 | 34 | test('It should set scale to 255 if user does not provide value', () => { 35 | expect(roundColorValue(1.0)).toBe(255); 36 | }); 37 | 38 | test('It should max out "quantity" at 1.0, returning at most a full value of 255', () => { 39 | expect(roundColorValue(12412.1)).toBe(255); 40 | }); 41 | 42 | test('It should set an alpha/opacity value into a cleaned value, fixed to 2 decimals', () => { 43 | expect(roundColorValue(0.33000001311302185, 1)).toBe(0.33); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/roundNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { roundNumber } from '../../../bin/frameworks/string/roundNumber'; 2 | 3 | describe('Success cases', () => { 4 | test('It should round a long floating point number to a shorter one, capped at 6 decimals if no decimal count is provided', () => { 5 | expect(roundNumber(0.9283649821712)).toBe(0.928365); 6 | }); 7 | 8 | test('It should round a long floating point number to a shorter one, set to 2 decimals if 2 is provided as a value', () => { 9 | expect(roundNumber(0.23623423, 2)).toBe(0.24); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /__tests__/frameworks/string/toPascalCase.test.ts: -------------------------------------------------------------------------------- 1 | import { toPascalCase } from '../../../bin/frameworks/string/toPascalCase'; 2 | 3 | describe('Failure cases', () => { 4 | test('It should throw an error if no argument is provided', () => { 5 | expect(() => { 6 | // @ts-ignore 7 | toPascalCase(); 8 | }).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | test('It should properly pascal-case a string', () => { 14 | expect(toPascalCase('Asdf Asdf asdF')).toBe('AsdfAsdfAsdf'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/frameworks/system/checkIfVoidElement.test.ts: -------------------------------------------------------------------------------- 1 | import { checkIfVoidElement } from '../../../bin/frameworks/system/checkIfVoidElement'; 2 | 3 | describe('Success cases', () => { 4 | test('It should return true if it receives nothing', () => { 5 | expect(checkIfVoidElement('')).toBe(false); 6 | }); 7 | 8 | test('It should return false if it receives a non-"void element"', () => { 9 | expect(checkIfVoidElement('div')).toBe(false); 10 | }); 11 | 12 | test('It should return true if it receives a "void element"', () => { 13 | expect(checkIfVoidElement('input')).toBe(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/mocks/files/Google__G__Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /__tests__/mocks/files/googlelogo_color_272x92dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/__tests__/mocks/files/googlelogo_color_272x92dp.png -------------------------------------------------------------------------------- /__tests__/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | export const mockServer = setupServer(...handlers); 4 | -------------------------------------------------------------------------------- /__tests__/mocks/responses/mockFigmaFilesVersions.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": [ 3 | { 4 | "id": "1192882896", 5 | "created_at": "2021-10-08T17:35:16Z", 6 | "label": "Version 4.1.0", 7 | "description": "Version 4.1.0", 8 | "user": { 9 | "handle": "Test User", 10 | "img_url": "https://s3-alpha.figma.com/profile/47fd65c3-ac14-4593-8657-b2f155921ba8", 11 | "id": "823649067272768698" 12 | }, 13 | "thumbnail_url": null 14 | }, 15 | { 16 | "id": "1192878012", 17 | "created_at": "2021-10-08T17:32:46Z", 18 | "label": null, 19 | "description": null, 20 | "user": { 21 | "handle": "Test User", 22 | "img_url": "https://s3-alpha.figma.com/profile/47fd65c3-ac14-4593-8657-b2f155921ba8", 23 | "id": "823649067272768698" 24 | }, 25 | "thumbnail_url": "https://s3-alpha-sig.figma.com/thumbnails/f4b525ba-6856-47f8-bfbf-bbe47f659770?Expires=1635724800&Signature=gN~FMOe2i6ycrb~oqjLsVbyxRF~J17iHKZ0KEONqWPAqm2d9Irc4SteX6QPiObEuu6NxjJY0kpYRXcWR1EVoc926~zykxfusQ4a5YHRwT1u~jaCLbPJxpEevLQiuGHILvwbKE7HNxoXNxPJOKW3lbY9QNbVFX8d6elRIk3WWfVjMHLZ7fxrFsGYC2Lf35Ms10lfIJ5dj-g4eKKlUmyNTDUdhZJhP13HuRUiGIaL5~-E79XlLSd0FRNxR0059jl-ihFxVcfNl55JyCqqlm0TM3Kw7SHmpRpoFI0CokN4J-nUaHYhze9OIPMzhJjaDA0GKn~OjOH8qoXgIZ8DLJwclEA__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA" 26 | } 27 | ], 28 | "pagination": { 29 | "prev_page": "https://api.figma.com/v1/files/r341pYYMAQpbitcdRtrddo/versions?page_size=30&before=1192882896" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /__tests__/mocks/responses/mockFigmaImages.json: -------------------------------------------------------------------------------- 1 | { 2 | "err": null, 3 | "images": { 4 | "2710:7": "https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/8ba5/2322/bac5a41a1a59a9a76a12bbe21e0cd781", 5 | "2710:5": "https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/3e15/3c93/1e96386d904b25bf987abc4b87c62ee1", 6 | "3009:118": "https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/2f0d/1106/be444c1f7c5a79484c415e50e9010847" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /__tests__/usecases/createTokens.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { createTokens } from '../../bin/usecases/createTokens'; 5 | 6 | import figmaTestResponse from '../../testdata/figma.json'; 7 | import { testConfig } from '../../testdata/testConfig'; 8 | 9 | // Re-ordered these (success & failure) because they somehow seem to keep variable names in memory, causing test failures 10 | 11 | describe('Success cases', () => { 12 | test('It should write tokens given a valid configuration, valid data and an output folder', async () => { 13 | const CONFIG = testConfig; 14 | const DATA = figmaTestResponse; 15 | CONFIG.outputFolderTokens = '__test-tokens-success__'; 16 | // @ts-ignore 17 | await createTokens(CONFIG, DATA); 18 | const FILE_EXISTS = fs.existsSync(CONFIG.outputFolderTokens); 19 | expect(FILE_EXISTS).toBe(true); 20 | trash(CONFIG.outputFolderTokens); 21 | }); 22 | }); 23 | 24 | describe('Failure cases', () => { 25 | test('It should throw an error if no argument is provided', async () => { 26 | // @ts-ignore 27 | await expect(createTokens()).rejects.toThrowError(); 28 | }); 29 | 30 | test('It should throw an error if misconfigured (no children in data)', async () => { 31 | const CONFIG = testConfig; 32 | const DATA = { ...figmaTestResponse }; 33 | DATA.document.children = []; 34 | CONFIG.outputFolderTokens = '__test-tokens-fail__'; 35 | await expect( 36 | // @ts-ignore 37 | createTokens(CONFIG, DATA), 38 | ).rejects.toThrowError(); 39 | await trash(CONFIG.outputFolderTokens); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/usecases/interactors/common/createPage.test.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../../bin/contracts/Figma'; 2 | 3 | import { createPage } from '../../../../bin/usecases/interactors/common/createPage'; 4 | 5 | import { 6 | designTokensChildren, 7 | designTokensPage, 8 | } from '../../../../testdata/designTokensPage'; 9 | 10 | const matchingPageName = 'Design tokens'; 11 | 12 | describe('Failure cases', () => { 13 | test('It should throw an error if array is empty', () => { 14 | const FIGMA_PAGES: Frame[] = []; 15 | expect(() => { 16 | createPage(FIGMA_PAGES, matchingPageName); 17 | }).toThrow(); 18 | }); 19 | }); 20 | 21 | describe('Success cases', () => { 22 | test('It should return an empty array if array has non-matching values', () => { 23 | const FIGMA_PAGES: Frame[] = [ 24 | { id: '0:0', type: 'DOCUMENT', name: 'asdf', children: [] }, 25 | ]; 26 | expect(createPage(FIGMA_PAGES, matchingPageName)).toEqual( 27 | expect.arrayContaining([]), 28 | ); 29 | }); 30 | 31 | test('It should return an empty array if array has non-matching values, even with a "name" property', () => { 32 | const FIGMA_PAGES = [{ name: 'demovalue', something: 123 }]; 33 | // @ts-ignore 34 | expect(createPage(FIGMA_PAGES, matchingPageName)).toEqual( 35 | expect.arrayContaining([]), 36 | ); 37 | }); 38 | 39 | test('It should receive the children of its matching page ("design tokens")', () => { 40 | // @ts-ignore 41 | expect(createPage(designTokensPage, matchingPageName)).toEqual( 42 | expect.arrayContaining(designTokensChildren), 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/usecases/interactors/elements/processElements.test.ts: -------------------------------------------------------------------------------- 1 | import { baseConfig } from '../../../../bin/entities/Config/baseConfig'; 2 | 3 | import { FRAME as Frame } from '../../../../bin/contracts/Figma'; 4 | 5 | import { processElements } from '../../../../bin/usecases/interactors/elements/processElements'; 6 | 7 | import { components } from '../../../../testdata/components'; 8 | import { elementsPage } from '../../../../testdata/elementsPage'; 9 | 10 | describe('Failure cases', () => { 11 | test('It should throw an error if no argument is provided', () => { 12 | // @ts-ignore 13 | expect(() => processElements()).toThrowError(); 14 | }); 15 | }); 16 | 17 | describe('Success cases', () => { 18 | test('It should successfully return a valid element', () => { 19 | expect( 20 | processElements(elementsPage as Frame[], baseConfig, components), 21 | ).toMatchObject([ 22 | { 23 | css: ` 24 | color: \${colors['black']}; 25 | font-size: \${fontSizes['sub']}; 26 | font-family: \${fontFamilies['regular']}; 27 | font-weight: \${fontWeights['regular']}; 28 | line-height: \${lineHeights['xs']}; 29 | text-align: left; 30 | `, 31 | description: '\n\n# Sub\n\nTiny text snippets.', 32 | element: 'sub', 33 | extraProps: '', 34 | html: '', 35 | id: '2875:22', 36 | imports: [ 37 | 'colors', 38 | 'fontSizes', 39 | 'fontFamilies', 40 | 'fontWeights', 41 | 'lineHeights', 42 | ], 43 | name: 'Microcopy', 44 | text: 'Microcopy', 45 | }, 46 | ]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/usecases/interactors/elements/processGraphicElementsMap.test.ts: -------------------------------------------------------------------------------- 1 | import { processGraphicElementsMap } from '../../../../bin/usecases/interactors/elements/processGraphicElementsMap'; 2 | 3 | import { graphicElement } from '../../../../testdata/elements/graphicElement'; 4 | 5 | describe('Failure cases', () => { 6 | test('It should throw an error if no argument is provided', () => { 7 | expect(() => { 8 | // @ts-ignore 9 | processGraphicElementsMap(); 10 | }).toThrow(); 11 | }); 12 | 13 | test('It should throw an error if zero-length array is passed', () => { 14 | expect(() => { 15 | // @ts-ignore 16 | processGraphicElementsMap([]); 17 | }).toThrow(); 18 | }); 19 | }); 20 | 21 | describe('Success cases', () => { 22 | test('It should output a string specifying an import and export block', () => { 23 | const EXPECTED = `import More from './elements/More'; 24 | 25 | export const Graphics = { 26 | More, 27 | }; 28 | `; 29 | // @ts-ignore 30 | expect(processGraphicElementsMap(graphicElement)).toBe(EXPECTED); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/usecases/interactors/elements/writeGraphicElementsMap.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { writeGraphicElementsMap } from '../../../../bin/usecases/interactors/elements/writeGraphicElementsMap'; 5 | 6 | describe('Failure cases', () => { 7 | test('It should throw an error if no argument is provided', () => { 8 | expect(() => { 9 | // @ts-ignore 10 | writeGraphicElementsMap(); 11 | }).toThrow(); 12 | }); 13 | }); 14 | 15 | describe('Success cases', () => { 16 | test('It should write graphics elements map', async () => { 17 | const FOLDER = '__test-write-graphic-elements-map__'; 18 | const FILE_PATH = `${FOLDER}/index.tsx`; 19 | const FILE_CONTENTS = `import More from './elements/More'; 20 | 21 | export const Graphics = { 22 | More, 23 | }; 24 | `; 25 | 26 | writeGraphicElementsMap(FOLDER, FILE_PATH, FILE_CONTENTS); 27 | const FILE_EXISTS = fs.existsSync(FILE_PATH); 28 | expect(FILE_EXISTS).toBe(true); 29 | 30 | await trash(FOLDER); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/usecases/interactors/graphics/getIdString.test.ts: -------------------------------------------------------------------------------- 1 | import { getIdString } from '../../../../bin/usecases/interactors/graphics/getIdString'; 2 | 3 | import { graphicsIds } from '../../../../testdata/graphics/getGraphics'; 4 | 5 | describe('Failure cases', () => { 6 | test('It should throw an error if no argument is provided', () => { 7 | // @ts-ignore 8 | expect(() => getIdString()).toThrow(); 9 | }); 10 | }); 11 | 12 | describe('Success cases', () => { 13 | test('It should correctly product a string of IDs from an array of IDs', () => { 14 | expect(getIdString(graphicsIds)).toBe('2710:7,2710:5'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/usecases/interactors/graphics/writeGraphics.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import trash from 'trash'; 3 | 4 | import { baseConfig } from '../../../../bin/entities/Config/baseConfig'; 5 | 6 | import { writeGraphics } from '../../../../bin/usecases/interactors/graphics/writeGraphics'; 7 | 8 | import { fileList } from '../../../../testdata/fileList'; 9 | 10 | // Set temp folder 11 | const TEMP_FOLDER = `__graphics__`; 12 | baseConfig.outputFolderGraphics = TEMP_FOLDER; 13 | 14 | describe('Failure cases', () => { 15 | test('It should throw an error if no argument is provided', async () => { 16 | // @ts-ignore 17 | await expect(writeGraphics()).rejects.toThrow(); 18 | }); 19 | }); 20 | 21 | describe('Success cases', () => { 22 | test('It should successfully write graphics (PNG) if provided valid file list and config', async () => { 23 | baseConfig.outputFormatGraphics = 'png'; 24 | 25 | await writeGraphics(fileList, baseConfig); 26 | const PATH = `${TEMP_FOLDER}/${fileList[0].file}`; 27 | const FILE_EXISTS = fs.existsSync(PATH); 28 | expect(FILE_EXISTS).toBeTruthy(); 29 | }); 30 | }); 31 | 32 | describe('Success cases', () => { 33 | test('It should successfully write graphics (SVG) if provided valid file list and config', async () => { 34 | baseConfig.outputFormatGraphics = 'svg'; 35 | 36 | await writeGraphics(fileList, baseConfig); 37 | const PATH = `${TEMP_FOLDER}/${fileList[1].file}`; 38 | const FILE_EXISTS = fs.existsSync(PATH); 39 | expect(FILE_EXISTS).toBeTruthy(); 40 | }); 41 | }); 42 | 43 | afterAll(async () => { 44 | await trash(TEMP_FOLDER); 45 | }); 46 | -------------------------------------------------------------------------------- /bin/contracts/ApiResponse.ts: -------------------------------------------------------------------------------- 1 | export interface ApiResponse { 2 | err: string | null; 3 | images: Image; 4 | status?: number; 5 | document?: any; 6 | } 7 | 8 | type Image = { 9 | [key: string]: string; 10 | }; 11 | -------------------------------------------------------------------------------- /bin/contracts/Css.ts: -------------------------------------------------------------------------------- 1 | export type Css = { 2 | css: string; 3 | imports: string[]; 4 | }; 5 | 6 | export type ProcessedSelfnamedCss = { 7 | updatedCss: string; 8 | updatedImports: string[]; 9 | }; 10 | 11 | export type IntersectingCssValues = string[]; 12 | 13 | export type UniqueCssValues = { 14 | css: string[]; 15 | className: string; 16 | }; 17 | -------------------------------------------------------------------------------- /bin/contracts/Element.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './Config'; 2 | import { Components, FRAME as Frame } from './Figma'; 3 | import { Graphic } from './Graphic'; 4 | 5 | export type Element = { 6 | children: Frame[]; 7 | pageName: string; 8 | config: Config; 9 | components: Components; 10 | isGeneratingGraphics?: boolean; 11 | }; 12 | 13 | export type GraphicElementsMap = { 14 | config: Config; 15 | graphics: Graphic[]; 16 | }; 17 | -------------------------------------------------------------------------------- /bin/contracts/FigmaData.ts: -------------------------------------------------------------------------------- 1 | import { Components, FRAME as Frame } from './Figma'; 2 | 3 | export interface FigmaData { 4 | document: Document; 5 | components: Components; 6 | } 7 | 8 | export type FigmaResponse = { 9 | document: Document; 10 | components: Components; 11 | componentSets: Record; 12 | schemaVersion: number; 13 | styles: Styles; 14 | name: string; 15 | lastModified: string; 16 | thumbnailUrl: string; 17 | version: string; 18 | role: string; // owner 19 | editorType: string; // figma 20 | linkAccess: string; // view 21 | }; 22 | 23 | type Styles = { 24 | [id: string]: Style; 25 | }; 26 | 27 | type Style = { 28 | [id: string]: Record; 29 | }; 30 | 31 | type Document = { 32 | id: string; 33 | name: string; 34 | type: string; 35 | children: Frame[]; 36 | }; 37 | -------------------------------------------------------------------------------- /bin/contracts/FigmaElement.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from './Figma'; 2 | 3 | /** 4 | * The FigmaElement interface is for elements that have an added description, but are otherwise unaltered from how they appear from Figma's API. 5 | */ 6 | export interface FigmaElement { 7 | id: string; 8 | name: string; 9 | type: string; 10 | description?: string; 11 | children?: Frame[]; 12 | } 13 | -------------------------------------------------------------------------------- /bin/contracts/FigmagicElement.ts: -------------------------------------------------------------------------------- 1 | import { FigmaElement } from './FigmaElement'; 2 | 3 | /** 4 | * The FigmagicElement interface is used after parsing, and before writing elements. 5 | * It adds the necessary metadata to output needed files onto the standard Figma element data. 6 | */ 7 | export interface FigmagicElement extends FigmaElement { 8 | element?: string; 9 | imports?: string[]; 10 | css?: string; 11 | html?: string; 12 | text?: string; 13 | extraProps?: string; 14 | /* 15 | id: string; 16 | name: string; 17 | description?: string; 18 | element?: string; 19 | imports?: string[]; 20 | css?: string; 21 | html?: string; 22 | text?: string; 23 | extraProps?: string; 24 | */ 25 | } 26 | -------------------------------------------------------------------------------- /bin/contracts/Files.ts: -------------------------------------------------------------------------------- 1 | export type JsonFileData = Record; 2 | 3 | export type Id = { 4 | id: string; 5 | name: string; 6 | }; 7 | 8 | export type FileList = { 9 | url: string; 10 | file: string; 11 | }; 12 | 13 | export type FileContents = { 14 | borderWidths: JsonFileData; 15 | colors: JsonFileData; 16 | radii: JsonFileData; 17 | shadows: JsonFileData; 18 | spacing: JsonFileData; 19 | }; 20 | -------------------------------------------------------------------------------- /bin/contracts/Graphic.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './Config'; 2 | import { FigmagicElement } from './FigmagicElement'; 3 | 4 | export interface Graphic extends FigmagicElement { 5 | config: Config; 6 | } 7 | -------------------------------------------------------------------------------- /bin/contracts/Imports.ts: -------------------------------------------------------------------------------- 1 | export type Imports = string[]; 2 | 3 | export type UpdatedCssAndImports = { 4 | updatedCss: string; 5 | updatedImports: Imports[]; 6 | }; 7 | -------------------------------------------------------------------------------- /bin/contracts/Metadata.ts: -------------------------------------------------------------------------------- 1 | import { Imports } from './Imports'; 2 | 3 | export interface Metadata { 4 | dataType: null | 'enum'; 5 | html: string; 6 | element: string; 7 | extraProps: string; 8 | text: string; 9 | imports: Imports; 10 | } 11 | -------------------------------------------------------------------------------- /bin/contracts/ParsedElementMetadataInterface.ts: -------------------------------------------------------------------------------- 1 | import { Css } from './Css'; 2 | import { Imports } from './Imports'; 3 | 4 | export interface ParsedElementMetadataInterface { 5 | css: Css | string; 6 | imports: Imports | any; 7 | } 8 | -------------------------------------------------------------------------------- /bin/contracts/Parsing.ts: -------------------------------------------------------------------------------- 1 | import { JsonFileData } from './Files'; 2 | 3 | export type Color = { 4 | [key: string]: string; 5 | }; 6 | 7 | export type FileOutput = { 8 | colors: JsonFileData; 9 | fontFamilies: JsonFileData; 10 | fontSizes: JsonFileData; 11 | fontWeights: JsonFileData; 12 | letterSpacings: JsonFileData; 13 | lineHeights: JsonFileData; 14 | }; 15 | 16 | export type PaddingOptions = { 17 | padding: Padding; 18 | spacing: Spacing; 19 | remSize: number; 20 | }; 21 | 22 | type Padding = { 23 | top: number; 24 | bottom: number; 25 | left: number; 26 | right: number; 27 | }; 28 | 29 | type Spacing = { 30 | [key: string]: string; 31 | }; 32 | -------------------------------------------------------------------------------- /bin/contracts/PrepFile.ts: -------------------------------------------------------------------------------- 1 | import { Templates } from './Templates'; 2 | 3 | export interface PrepComponent { 4 | name: string; 5 | filePath: string; 6 | format: string; 7 | templates: Templates; 8 | text: string; 9 | extraProps: string; 10 | element: string; 11 | } 12 | 13 | export interface PrepStyledComponents { 14 | name: string; 15 | filePath: string; 16 | format: string; 17 | templates: Templates; 18 | element: string; 19 | } 20 | 21 | export interface PrepCss { 22 | name: string; 23 | filePath: string; 24 | format: string; 25 | imports: string; 26 | file: string; 27 | } 28 | 29 | export interface PrepStorybook { 30 | name: string; 31 | filePath: string; 32 | format: string; 33 | templates: Templates; 34 | text: string; 35 | } 36 | 37 | export interface PrepDescription { 38 | filePath: string; 39 | file: string; 40 | format: string; 41 | } 42 | 43 | export interface PrepGraphicComponent { 44 | name: string; 45 | filePath: string; 46 | format: string; 47 | templates: Templates; 48 | file: string; 49 | } 50 | -------------------------------------------------------------------------------- /bin/contracts/ProcessedToken.ts: -------------------------------------------------------------------------------- 1 | export type ProcessedToken = Record; 2 | -------------------------------------------------------------------------------- /bin/contracts/Templates.ts: -------------------------------------------------------------------------------- 1 | export interface Templates { 2 | templatePathReact: string; 3 | templatePathStyled: string; 4 | templatePathStorybook: string; 5 | templatePathGraphic: string; 6 | } 7 | -------------------------------------------------------------------------------- /bin/contracts/TextElement.ts: -------------------------------------------------------------------------------- 1 | import { RECTANGLE as Rectangle } from './Figma'; 2 | 3 | export interface TextElement { 4 | absoluteBoundingBox: Rectangle; 5 | } 6 | -------------------------------------------------------------------------------- /bin/contracts/TokenMatch.ts: -------------------------------------------------------------------------------- 1 | import { Imports } from './Imports'; 2 | 3 | /** 4 | * Initial mapping of CSS and imports. 5 | */ 6 | export type TokenMatchRaw = { 7 | css: string; 8 | imports: Imports[]; 9 | }; 10 | 11 | /** 12 | * The final output of a match. 13 | */ 14 | export type TokenMatch = { 15 | updatedCss: string; 16 | updatedImports: Imports[]; 17 | }; 18 | -------------------------------------------------------------------------------- /bin/contracts/Tokens.ts: -------------------------------------------------------------------------------- 1 | export type Tokens = Record; 2 | 3 | export type BorderWidthTokens = Tokens; 4 | export type ColorTokens = Record; 5 | export type DelayTokens = Tokens; 6 | export type DurationTokens = Tokens; 7 | export type EasingTokens = Tokens; 8 | export type FontSizeTokens = Tokens; 9 | export type FontTokens = Tokens; 10 | export type FontWeightTokens = Tokens; 11 | export type LetterSpacingTokens = Tokens; 12 | export type LineHeightTokens = Tokens; 13 | export type MediaQueryTokens = Tokens; 14 | export type OpacityTokens = Tokens; 15 | export type RadiusTokens = Tokens; 16 | export type ShadowTokens = Tokens; 17 | export type SpacingTokens = Tokens; 18 | export type ZindexTokens = Tokens; 19 | 20 | export type TokenOperations = { 21 | borderwidths: () => Tokens; 22 | color: () => Tokens; 23 | colors: () => Tokens; 24 | delays: () => Tokens; 25 | durations: () => Tokens; 26 | easings: () => Tokens; 27 | fontfamilies: () => Tokens; 28 | fontsizes: () => Tokens; 29 | fontweights: () => Tokens; 30 | letterspacings: () => Tokens; 31 | lineheights: () => Tokens; 32 | mediaqueries: () => Tokens; 33 | opacities: () => Tokens; 34 | radii: () => Tokens; 35 | shadows: () => Tokens; 36 | spacing: () => Tokens; 37 | spacings: () => Tokens; 38 | zindices: () => Tokens; 39 | }; 40 | -------------------------------------------------------------------------------- /bin/contracts/TypographyElement.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from './Figma'; 2 | 3 | import { 4 | LetterSpacingUnit, 5 | OutputFormatColors, 6 | OutputFormatTokens, 7 | } from './Config'; 8 | 9 | export type TypographyElement = { 10 | letterSpacingUnit: LetterSpacingUnit; 11 | outputFolderTokens: string; 12 | outputFormatTokens: OutputFormatTokens; 13 | outputFormatColors: OutputFormatColors; 14 | remSize: number; 15 | textElement: Frame; 16 | usePostscriptFontNames: boolean; 17 | }; 18 | -------------------------------------------------------------------------------- /bin/controllers/FigmagicController.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../contracts/Config'; 2 | import { FigmaData } from '../contracts/FigmaData'; 3 | 4 | import { createElements } from '../usecases/createElements'; 5 | import { createGraphics } from '../usecases/createGraphics'; 6 | import { createTokens } from '../usecases/createTokens'; 7 | 8 | import { ErrorFigmagicController } from '../frameworks/errors/errors'; 9 | import { MsgJobComplete } from '../frameworks/messages/messages'; 10 | 11 | /** 12 | * @description The main orchestration/controller point for Figmagic 13 | */ 14 | export async function FigmagicController( 15 | config: Config, 16 | data: FigmaData, 17 | ): Promise { 18 | if (!config || !data) throw Error(ErrorFigmagicController); 19 | 20 | if (config.syncGraphics) await createGraphics(config, data); 21 | if (config.syncTokens) await createTokens(config, data); 22 | if (config.syncElements) await createElements(config, data); 23 | 24 | console.log(MsgJobComplete); 25 | 26 | return MsgJobComplete; 27 | } 28 | -------------------------------------------------------------------------------- /bin/entities/Config/index.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../contracts/Config'; 2 | 3 | import { baseConfig } from './baseConfig'; 4 | import { createConfiguration } from './logic/createConfiguration'; 5 | import { validateConfig } from './logic/validateConfig'; 6 | 7 | export const makeConfiguration = async ( 8 | userConfigPath: string, 9 | ...cliArgs: string[] 10 | ): Promise => { 11 | const config = new Configuration(userConfigPath, cliArgs); 12 | await config.createConfig(); 13 | return config.getConfig(); 14 | }; 15 | 16 | class Configuration { 17 | baseConfiguration: Config; 18 | userConfigPath: string; 19 | cliArgs: string[]; 20 | config: Config = baseConfig; 21 | 22 | constructor(userConfigPath: string, cliArgs: string[]) { 23 | this.baseConfiguration = baseConfig; 24 | this.userConfigPath = userConfigPath; 25 | this.cliArgs = cliArgs; 26 | } 27 | 28 | async createConfig(): Promise { 29 | let config = null; 30 | 31 | config = await createConfiguration( 32 | this.baseConfiguration, 33 | this.userConfigPath, 34 | this.cliArgs, 35 | ); 36 | validateConfig(config); 37 | 38 | this.config = config; 39 | return this.getConfig(); 40 | } 41 | 42 | getConfig(): Config { 43 | return this.config; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/classRepresentsTextOnlyElement.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Checks if a class represents a text-only element. 3 | */ 4 | export const classRepresentsTextOnlyElement = ( 5 | className: string, 6 | textOnlySubchildren: string[] = [], 7 | ): boolean => { 8 | const cleanedName = className.replace(' {', ''); 9 | const rootClassName = cleanedName.slice(1, cleanedName.length - 11); 10 | 11 | let result = false; 12 | 13 | textOnlySubchildren.forEach((name) => { 14 | if (name.startsWith(`${rootClassName}`)) result = true; 15 | }); 16 | 17 | return result; 18 | }; 19 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/getFileContents.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { sliceOutObjectFromFile } from './sliceOutObjectFromFile'; 4 | 5 | import { JsonFileData } from '../../../contracts/Files'; 6 | 7 | import { ErrorGetFileContents } from '../../../frameworks/errors/errors'; 8 | 9 | /** 10 | * @description Get contents of local file as a JSON object 11 | */ 12 | export function getFileContents( 13 | filepath: string, 14 | filename: string, 15 | format: string, 16 | ): JsonFileData { 17 | if (!filepath || !filename || !format) throw Error(ErrorGetFileContents); 18 | 19 | const file = path.join(`${process.cwd()}`, filepath, `${filename}.${format}`); 20 | return sliceOutObjectFromFile(file); 21 | } 22 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/getIntersectingValues.ts: -------------------------------------------------------------------------------- 1 | import { IntersectingCssValues, UniqueCssValues } from '../../../contracts/Css'; 2 | 3 | import { ErrorGetIntersectingValues } from '../../../frameworks/errors/errors'; 4 | 5 | /** 6 | * @description Get all shared/common/intersecting values across all "classes". 7 | * These will then need to float to the top of the CSS document. 8 | */ 9 | export function getIntersectingValues( 10 | arrays: UniqueCssValues[], 11 | ): IntersectingCssValues { 12 | if (!arrays) throw Error(ErrorGetIntersectingValues); 13 | 14 | const cssArrays = arrays.map((a) => a.css); 15 | const reducedValues = cssArrays.reduce((prev: string[], curr: string[]) => 16 | prev.filter((val: string) => curr.includes(val)), 17 | ); 18 | const intersectingValues = [...new Set(reducedValues)] as string[]; 19 | 20 | return intersectingValues; 21 | } 22 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/getUniqueValues.ts: -------------------------------------------------------------------------------- 1 | import { UniqueCssValues } from '../../../contracts/Css'; 2 | 3 | import { ErrorGetUniqueValues } from '../../../frameworks/errors/errors'; 4 | 5 | /** 6 | * @description Get any unique values and collect them in arrays per class 7 | */ 8 | export function getUniqueValues( 9 | arrays: UniqueCssValues[], 10 | intersections: string[], 11 | ): UniqueCssValues[] { 12 | if (!arrays || !intersections) throw Error(ErrorGetUniqueValues); 13 | 14 | // Get unique values 15 | const cssArrays: string[][] = arrays.map((arr) => arr.css); 16 | const nonIntersectingValues: string[][] = cssArrays.map((arr: string[]) => 17 | arr.filter((val: string) => !intersections.includes(val)), 18 | ); 19 | 20 | const fixedUniqueValues: UniqueCssValues[] = []; 21 | 22 | const values = nonIntersectingValues.map((arr: string[]) => [ 23 | ...new Set(arr), 24 | ]); 25 | values.forEach((item: string[], index: number) => { 26 | const usedProperties: string[] = []; 27 | // @ts-ignore 28 | const deduplicatedCssRows = item.filter((cssRow: string) => { 29 | /** 30 | * Enrich unique values (arrays) with their respective class names as a property 31 | */ 32 | const property = cssRow.split(':')[0]; 33 | if (!usedProperties.includes(property)) { 34 | usedProperties.push(property); 35 | return cssRow; 36 | } 37 | }); 38 | 39 | if (deduplicatedCssRows.length > 0) 40 | fixedUniqueValues.push({ 41 | css: deduplicatedCssRows, 42 | className: arrays[index].className, 43 | }); 44 | }); 45 | 46 | return fixedUniqueValues; 47 | } 48 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/getBackgroundColor.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { FRAME as Frame, Paint } from '../../../../contracts/Figma'; 3 | 4 | import { createLinearGradientString } from '../../../../frameworks/string/createLinearGradientString'; 5 | import { createSolidColorString } from '../../../../frameworks/string/createSolidColorString'; 6 | 7 | import { ErrorGetBackgroundColor } from '../../../../frameworks/errors/errors'; 8 | 9 | export function getBackgroundColor( 10 | element: Frame, 11 | outputFormatColors: OutputFormatColors, 12 | ): string | null { 13 | if (!element) throw Error(ErrorGetBackgroundColor); 14 | // TODO: Does not support background-color for text 15 | if ( 16 | !element.fills || 17 | !element.fills[0] || 18 | !element.fills[0].type || 19 | element.type === 'TEXT' 20 | ) 21 | return null; 22 | 23 | const fills: Paint = element.fills[0]; 24 | 25 | if (fills.type === 'SOLID') 26 | return createSolidColorString(fills, outputFormatColors); 27 | if (fills.type === 'GRADIENT_LINEAR') 28 | return createLinearGradientString(fills); 29 | 30 | return null; 31 | } 32 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/getBorderColor.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../../contracts/Figma'; 2 | 3 | import { roundColorValue } from '../../../../frameworks/string/roundColorValue'; 4 | 5 | import { ErrorGetBorderColor } from '../../../../frameworks/errors/errors'; 6 | 7 | export function getBorderColor(element: Frame): string | null { 8 | if (!element) throw Error(ErrorGetBorderColor); 9 | if ( 10 | !( 11 | element.strokes && 12 | element.strokes.length > 0 && 13 | element.strokes[0].type === 'SOLID' 14 | ) 15 | ) 16 | return null; 17 | 18 | if (!element.strokes[0].color) throw Error(ErrorGetBorderColor); 19 | 20 | const R = roundColorValue(element.strokes[0].color.r); 21 | const G = roundColorValue(element.strokes[0].color.g); 22 | const B = roundColorValue(element.strokes[0].color.b); 23 | const A = roundColorValue(element.strokes[0].color.a, 1); 24 | 25 | return `rgba(${R}, ${G}, ${B}, ${A})`; 26 | } 27 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/getPaddingX.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../../contracts/Figma'; 2 | 3 | import { ErrorGetPaddingX } from '../../../../frameworks/errors/errors'; 4 | 5 | export type PaddingHorizontal = { 6 | left: number; 7 | right: number; 8 | }; 9 | 10 | export function getPaddingX( 11 | textElement: Frame, 12 | element: Frame, 13 | ): PaddingHorizontal | null { 14 | if (!textElement || !element) return null; 15 | 16 | if (!textElement.absoluteBoundingBox || !element.absoluteBoundingBox) 17 | throw Error(ErrorGetPaddingX); 18 | 19 | const parentWidth = element.absoluteBoundingBox.width; 20 | const textWidth = textElement.absoluteBoundingBox.width; 21 | const paddingLeft = 22 | // @ts-ignore 23 | textElement.absoluteBoundingBox.x - element.absoluteBoundingBox.x; 24 | // @ts-ignore 25 | const paddingRight = parentWidth - (paddingLeft + textWidth); 26 | 27 | return { 28 | left: Math.round(paddingLeft), 29 | right: Math.round(paddingRight), 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/getPaddingY.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../../contracts/Figma'; 2 | 3 | import { ErrorGetPaddingY } from '../../../../frameworks/errors/errors'; 4 | 5 | export type PaddingVertical = { 6 | top: number; 7 | bottom: number; 8 | }; 9 | 10 | export function getPaddingY( 11 | textElement: Frame, 12 | element: Frame, 13 | ): PaddingVertical | null { 14 | if (!textElement) return null; 15 | if ( 16 | !element.absoluteBoundingBox || 17 | !element.absoluteBoundingBox.height || 18 | !textElement.absoluteBoundingBox || 19 | !textElement.absoluteBoundingBox.height 20 | ) 21 | throw Error(ErrorGetPaddingY); 22 | 23 | const parentHeight = element.absoluteBoundingBox.height; 24 | const textHeight = textElement.absoluteBoundingBox.height; 25 | const paddingTop = 26 | // @ts-ignore 27 | textElement.absoluteBoundingBox.y - element.absoluteBoundingBox.y; 28 | const paddingBottom = parentHeight - (paddingTop + textHeight); 29 | 30 | return { 31 | top: Math.round(paddingTop), 32 | bottom: Math.round(paddingBottom), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/getShadow.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../../contracts/Figma'; 2 | 3 | import { roundColorValue } from '../../../../frameworks/string/roundColorValue'; 4 | 5 | import { ErrorGetShadow } from '../../../../frameworks/errors/errors'; 6 | 7 | export function getShadow(element: Frame): string | null { 8 | if (!element) throw Error(ErrorGetShadow); 9 | if ( 10 | !( 11 | element.effects && 12 | element.effects[0] && 13 | element.effects[0].type === 'DROP_SHADOW' 14 | ) 15 | ) 16 | return null; 17 | 18 | const dropShadow = element.effects[0]; 19 | 20 | const X = dropShadow.offset.x; 21 | const Y = dropShadow.offset.y; 22 | const radius = dropShadow.radius; 23 | const R = roundColorValue(dropShadow.color.r); 24 | const G = roundColorValue(dropShadow.color.g); 25 | const B = roundColorValue(dropShadow.color.b); 26 | const A = roundColorValue(dropShadow.color.a, 1); 27 | 28 | return `${X}px ${Y}px ${radius}px rgba(${R}, ${G}, ${B}, ${A})`; 29 | } 30 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parseBackgroundColor.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { Imports } from '../../../../contracts/Imports'; 3 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 4 | import { Color } from '../../../../contracts/Parsing'; 5 | 6 | import { getTokenMatch } from '../getTokenMatch'; 7 | import { updateParsing } from './updateParsing'; 8 | 9 | import { ErrorParseBackgroundColor } from '../../../../frameworks/errors/errors'; 10 | 11 | type BackgroundColorParams = { 12 | colors: Color; 13 | backgroundColor: string; 14 | remSize: number; 15 | outputFormatColors: OutputFormatColors; 16 | }; 17 | 18 | export function parseBackgroundColor( 19 | css: string, 20 | imports: Imports[], 21 | params: BackgroundColorParams, 22 | ): ParsedElementMetadataInterface { 23 | if (!css || !imports || !params) throw Error(ErrorParseBackgroundColor); 24 | 25 | const { colors, backgroundColor, remSize, outputFormatColors } = params; 26 | 27 | const property = backgroundColor.includes('gradient') 28 | ? 'background' 29 | : 'background-color'; 30 | 31 | const { updatedCss, updatedImports } = getTokenMatch( 32 | colors, 33 | 'colors', 34 | property, 35 | backgroundColor, 36 | remSize, 37 | outputFormatColors, 38 | ); 39 | 40 | return updateParsing(css, updatedCss, imports, updatedImports); 41 | } 42 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parseBorderColor.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { Imports } from '../../../../contracts/Imports'; 3 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 4 | import { Color } from '../../../../contracts/Parsing'; 5 | 6 | import { getTokenMatch } from '../getTokenMatch'; 7 | import { updateParsing } from './updateParsing'; 8 | 9 | import { ErrorParseBorderColor } from '../../../../frameworks/errors/errors'; 10 | 11 | type BorderColorParams = { 12 | colors: Color; 13 | borderColor: string; 14 | remSize: number; 15 | outputFormatColors: OutputFormatColors; 16 | }; 17 | 18 | export function parseBorderColor( 19 | css: string, 20 | imports: Imports[], 21 | params: BorderColorParams, 22 | ): ParsedElementMetadataInterface { 23 | if (!css || !imports || !params) throw Error(ErrorParseBorderColor); 24 | 25 | const { colors, borderColor, remSize, outputFormatColors } = params; 26 | 27 | const { updatedCss, updatedImports } = getTokenMatch( 28 | colors, 29 | 'colors', 30 | 'border-color', 31 | borderColor, 32 | remSize, 33 | outputFormatColors, 34 | ); 35 | 36 | return updateParsing(css, updatedCss, imports, updatedImports); 37 | } 38 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parseBorderRadius.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { Imports } from '../../../../contracts/Imports'; 3 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 4 | 5 | import { getTokenMatch } from '../getTokenMatch'; 6 | import { updateParsing } from './updateParsing'; 7 | 8 | import { ErrorParseBorderRadius } from '../../../../frameworks/errors/errors'; 9 | 10 | type BorderRadiusParams = { 11 | radii: Record; 12 | borderRadius: string; 13 | remSize: number; 14 | outputFormatColors: OutputFormatColors; 15 | }; 16 | 17 | export function parseBorderRadius( 18 | css: string, 19 | imports: Imports[], 20 | params: BorderRadiusParams, 21 | ): ParsedElementMetadataInterface { 22 | if (!css || !imports || !params) throw Error(ErrorParseBorderRadius); 23 | 24 | const { radii, borderRadius, remSize, outputFormatColors } = params; 25 | 26 | const { updatedCss, updatedImports } = getTokenMatch( 27 | radii, 28 | 'radii', 29 | 'border-radius', 30 | borderRadius, 31 | remSize, 32 | outputFormatColors, 33 | ); 34 | 35 | return updateParsing(css, updatedCss, imports, updatedImports); 36 | } 37 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parseBorderWidth.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { Imports } from '../../../../contracts/Imports'; 3 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 4 | 5 | import { getTokenMatch } from '../getTokenMatch'; 6 | import { updateParsing } from './updateParsing'; 7 | 8 | import { ErrorParseBorderWidth } from '../../../../frameworks/errors/errors'; 9 | 10 | type BorderWidthParams = { 11 | borderWidths: Record; 12 | borderWidth: string; 13 | remSize: number; 14 | outputFormatColors: OutputFormatColors; 15 | }; 16 | 17 | export function parseBorderWidth( 18 | css: string, 19 | imports: Imports[], 20 | params: BorderWidthParams, 21 | ): ParsedElementMetadataInterface { 22 | if (!css || !imports || !params) throw Error(ErrorParseBorderWidth); 23 | const { borderWidths, borderWidth, remSize, outputFormatColors } = params; 24 | 25 | const { updatedCss, updatedImports } = getTokenMatch( 26 | borderWidths, 27 | 'borderWidths', 28 | 'border-width', 29 | borderWidth, 30 | remSize, 31 | outputFormatColors, 32 | ); 33 | 34 | return updateParsing(css, updatedCss, imports, updatedImports); 35 | } 36 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parseHeight.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { Imports } from '../../../../contracts/Imports'; 3 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 4 | import { Tokens } from '../../../../contracts/Tokens'; 5 | 6 | import { getTokenMatch } from '../getTokenMatch'; 7 | import { updateParsing } from './updateParsing'; 8 | 9 | import { ErrorParseHeight } from '../../../../frameworks/errors/errors'; 10 | 11 | type HeightParams = { 12 | spacing: Record; 13 | height: number; 14 | remSize: number; 15 | outputFormatColors: OutputFormatColors; 16 | }; 17 | 18 | export function parseHeight( 19 | css: string, 20 | imports: Imports[], 21 | params: HeightParams, 22 | ): ParsedElementMetadataInterface { 23 | if (!css || !imports || !params) throw Error(ErrorParseHeight); 24 | const { spacing, height, remSize, outputFormatColors } = params; 25 | 26 | const { updatedCss, updatedImports } = getTokenMatch( 27 | spacing as unknown as Tokens, 28 | 'spacing', 29 | 'height', 30 | height, 31 | remSize, 32 | outputFormatColors, 33 | ); 34 | 35 | return updateParsing(css, updatedCss, imports, updatedImports); 36 | } 37 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parsePadding.ts: -------------------------------------------------------------------------------- 1 | import { Imports } from '../../../../contracts/Imports'; 2 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 3 | import { PaddingOptions } from '../../../../contracts/Parsing'; 4 | import { Tokens } from '../../../../contracts/Tokens'; 5 | 6 | import { getTokenMatch } from '../getTokenMatch'; 7 | import { updateParsing } from './updateParsing'; 8 | 9 | import { ErrorParsePadding } from '../../../../frameworks/errors/errors'; 10 | 11 | export function parsePadding( 12 | css: string, 13 | imports: Imports[], 14 | params: PaddingOptions, 15 | ): ParsedElementMetadataInterface { 16 | if (!css || !imports || !params) throw Error(ErrorParsePadding); 17 | const { padding, spacing, remSize } = params; 18 | 19 | if (!(padding && Object.keys(padding).length > 0)) return { css, imports }; 20 | 21 | const paddings = Object.values(padding).map((p) => p); 22 | if (paddings.every((item) => item === 0)) 23 | return updateParsing(css, null, imports, null); 24 | 25 | const { updatedCss, updatedImports } = getTokenMatch( 26 | spacing as unknown as Tokens, 27 | 'spacing', 28 | 'padding', 29 | padding, 30 | remSize, 31 | ); 32 | 33 | return updateParsing(css, updatedCss, imports, updatedImports); 34 | } 35 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/parseShadow.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../../../contracts/Config'; 2 | import { Imports } from '../../../../contracts/Imports'; 3 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 4 | 5 | import { getTokenMatch } from '../getTokenMatch'; 6 | import { updateParsing } from './updateParsing'; 7 | 8 | import { ErrorParseShadow } from '../../../../frameworks/errors/errors'; 9 | 10 | type ShadowParams = { 11 | shadows: Record; 12 | shadow: string; 13 | remSize: number; 14 | outputFormatColors: OutputFormatColors; 15 | }; 16 | 17 | export function parseShadow( 18 | css: string, 19 | imports: Imports[], 20 | params: ShadowParams, 21 | ): ParsedElementMetadataInterface { 22 | if (!css || !imports || !params) throw Error(ErrorParseShadow); 23 | 24 | const { shadows, shadow, remSize, outputFormatColors } = params; 25 | 26 | const { updatedCss, updatedImports } = getTokenMatch( 27 | shadows, 28 | 'shadows', 29 | 'box-shadow', 30 | shadow, 31 | remSize, 32 | outputFormatColors, 33 | ); 34 | 35 | return updateParsing(css, updatedCss, imports, updatedImports); 36 | } 37 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/parsers/updateParsing.ts: -------------------------------------------------------------------------------- 1 | import { Imports } from '../../../../contracts/Imports'; 2 | import { ParsedElementMetadataInterface } from '../../../../contracts/ParsedElementMetadataInterface'; 3 | 4 | import { ErrorUpdateParsing } from '../../../../frameworks/errors/errors'; 5 | 6 | export function updateParsing( 7 | css: string, 8 | updatedCss: string | null, 9 | imports: Imports[], 10 | updatedImports: Imports[] | null, 11 | ): ParsedElementMetadataInterface { 12 | if (!css || !imports) throw Error(ErrorUpdateParsing); 13 | 14 | return { 15 | css: updatedCss ? (css += updatedCss) : css, 16 | imports: updatedImports 17 | ? updatedImports.forEach((i) => imports.push(i)) 18 | : imports, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/processNestedCss.ts: -------------------------------------------------------------------------------- 1 | import { cleanArrays } from './cleanArrays'; 2 | import { createCssString } from './createCssString'; 3 | import { getIntersectingValues } from './getIntersectingValues'; 4 | import { getUniqueValues } from './getUniqueValues'; 5 | 6 | import { checkIfStringOnlyContainsReturnsOrSpaces } from '../../../frameworks/string/checkIfStringOnlyContainsReturnsOrSpaces'; 7 | 8 | import { ErrorProcessNestedCss } from '../../../frameworks/errors/errors'; 9 | 10 | /** 11 | * @description Process nested CSS into a format that puts shared/common intersecting CSS properties 12 | * at the top, while unique values get sorted under their respective CSS classes. 13 | */ 14 | export function processNestedCss( 15 | css: string, 16 | textOnlySubchildren: string[] = [], 17 | ): string { 18 | if (!css) throw Error(ErrorProcessNestedCss); 19 | 20 | // Match or split by CSS class name, like ".ButtonWarning {" 21 | const classNames: RegExpMatchArray | null = css.match(/\..* {/gi); 22 | const classContent = css.split(/\..* {/gi); 23 | // Remove any empty/garbage first elements 24 | if (checkIfStringOnlyContainsReturnsOrSpaces(classContent[0])) 25 | classContent.shift(); 26 | 27 | const arrays = cleanArrays(classNames, classContent, textOnlySubchildren); 28 | const intersectingValues = getIntersectingValues(arrays); 29 | const uniqueValues = getUniqueValues(arrays, intersectingValues); 30 | return createCssString(intersectingValues, uniqueValues); 31 | } 32 | -------------------------------------------------------------------------------- /bin/entities/FigmagicElement/logic/sliceOutObjectFromFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { JsonFileData } from '../../../contracts/Files'; 4 | 5 | import { ErrorSliceOutObjectFromFile } from '../../../frameworks/errors/errors'; 6 | 7 | export const sliceOutObjectFromFile = (path: string): JsonFileData => { 8 | if (!path) throw Error(ErrorSliceOutObjectFromFile); 9 | 10 | const data = fs.readFileSync(path, 'utf8'); 11 | if (!data) throw Error(ErrorSliceOutObjectFromFile); 12 | 13 | const slicedData = data.slice(data.indexOf('{'), data.indexOf('}') + 1); 14 | if (isJson(slicedData)) return JSON.parse(slicedData); // This is added because CSS generation breaks if using elements and CSS tokens 15 | return slicedData as any; 16 | }; 17 | 18 | const isJson = (input: any) => { 19 | if (typeof input !== 'string') return false; 20 | try { 21 | JSON.parse(input); 22 | return true; 23 | } catch (_e) { 24 | return false; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeDelayTokens.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../contracts/Figma'; 2 | import { DelayTokens } from '../../../contracts/Tokens'; 3 | 4 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 5 | 6 | import { 7 | ErrorMakeDelayTokensMissingProps, 8 | ErrorMakeDelayTokensNoChildren, 9 | ErrorMakeDelayTokensNoFrame, 10 | } from '../../../frameworks/errors/errors'; 11 | 12 | /** 13 | * @description Places all Figma delays into a clean object 14 | */ 15 | export function makeDelayTokens( 16 | delayFrame: Frame, 17 | camelizeTokenNames?: boolean, 18 | ): DelayTokens { 19 | if (!delayFrame) throw Error(ErrorMakeDelayTokensNoFrame); 20 | if (!delayFrame.children) throw Error(ErrorMakeDelayTokensNoChildren); 21 | 22 | const delays: Record = {}; 23 | const tokens = delayFrame.children.reverse(); 24 | tokens.forEach((item: Frame) => 25 | makeDelayToken(item, delays, camelizeTokenNames), 26 | ); 27 | 28 | return delays as DelayTokens; 29 | } 30 | 31 | function makeDelayToken( 32 | item: Frame, 33 | delays: Record, 34 | camelizeTokenNames?: boolean, 35 | ) { 36 | if (!item.name || !item.characters) 37 | throw Error(ErrorMakeDelayTokensMissingProps); 38 | 39 | const name = sanitizeString(item.name, camelizeTokenNames); 40 | delays[name] = parseFloat(item.characters); 41 | } 42 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeDurationTokens.ts: -------------------------------------------------------------------------------- 1 | import { DurationUnit } from '../../../contracts/Config'; 2 | import { FRAME as Frame } from '../../../contracts/Figma'; 3 | import { DurationTokens } from '../../../contracts/Tokens'; 4 | 5 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 6 | 7 | import { 8 | ErrorMakeDurationTokensMissingProps, 9 | ErrorMakeDurationTokensNoChildren, 10 | ErrorMakeDurationTokensNoFrame, 11 | } from '../../../frameworks/errors/errors'; 12 | 13 | /** 14 | * @description Places all Figma durations into a clean object 15 | */ 16 | export function makeDurationTokens( 17 | durationFrame: Frame, 18 | durationUnit: DurationUnit, 19 | camelizeTokenNames?: boolean, 20 | ): DurationTokens { 21 | if (!durationFrame) throw Error(ErrorMakeDurationTokensNoFrame); 22 | if (!durationFrame.children) throw Error(ErrorMakeDurationTokensNoChildren); 23 | 24 | const durations: Record = {}; 25 | const tokens = durationFrame.children.reverse(); 26 | tokens.forEach((item: Frame) => 27 | makeDurationToken(item, durations, durationUnit, camelizeTokenNames), 28 | ); 29 | 30 | return durations as DurationTokens; 31 | } 32 | 33 | function makeDurationToken( 34 | item: Frame, 35 | durations: Record, 36 | durationUnit: DurationUnit, 37 | camelizeTokenNames?: boolean, 38 | ) { 39 | if (!item.name || !item.characters) 40 | throw Error(ErrorMakeDurationTokensMissingProps); 41 | 42 | const name = sanitizeString(item.name, camelizeTokenNames); 43 | durations[name] = parseFloat(item.characters) + durationUnit; 44 | } 45 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeEasingTokens.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../contracts/Figma'; 2 | import { EasingTokens } from '../../../contracts/Tokens'; 3 | 4 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 5 | 6 | import { 7 | ErrorMakeEasingTokensMissingProps, 8 | ErrorMakeEasingTokensNoChildren, 9 | ErrorMakeEasingTokensNoFrame, 10 | } from '../../../frameworks/errors/errors'; 11 | 12 | /** 13 | * @description Places all Figma easings into a clean object 14 | */ 15 | export function makeEasingTokens( 16 | easingFrame: Frame, 17 | camelizeTokenNames?: boolean, 18 | ): EasingTokens { 19 | if (!easingFrame) throw Error(ErrorMakeEasingTokensNoFrame); 20 | if (!easingFrame.children) throw Error(ErrorMakeEasingTokensNoChildren); 21 | 22 | const easings: Record = {}; 23 | const tokens = easingFrame.children.reverse(); 24 | tokens.forEach((item: Frame) => 25 | makeEasingToken(item, easings, camelizeTokenNames), 26 | ); 27 | 28 | return easings as EasingTokens; 29 | } 30 | 31 | function makeEasingToken( 32 | item: Frame, 33 | easings: Record, 34 | camelizeTokenNames?: boolean, 35 | ) { 36 | if (!item.name || !item.characters) 37 | throw Error(ErrorMakeEasingTokensMissingProps); 38 | 39 | const name = sanitizeString(item.name, camelizeTokenNames); 40 | easings[name] = item.characters.trim(); 41 | } 42 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeFontWeightTokens.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../contracts/Figma'; 2 | import { FontWeightTokens } from '../../../contracts/Tokens'; 3 | 4 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 5 | 6 | import { 7 | ErrorMakeFontWeightTokensMissingProps, 8 | ErrorMakeFontWeightTokensMissingWeight, 9 | ErrorMakeFontWeightTokensNoChildren, 10 | ErrorMakeFontWeightTokensNoFrame, 11 | } from '../../../frameworks/errors/errors'; 12 | 13 | /** 14 | * @description Places all Figma font weights into a clean object 15 | */ 16 | export function makeFontWeightTokens( 17 | fontWeightFrame: Frame, 18 | camelizeTokenNames?: boolean, 19 | ): FontWeightTokens { 20 | if (!fontWeightFrame) throw Error(ErrorMakeFontWeightTokensNoFrame); 21 | if (!fontWeightFrame.children) 22 | throw Error(ErrorMakeFontWeightTokensNoChildren); 23 | 24 | const fontWeights: Record = {}; 25 | const tokens = fontWeightFrame.children.reverse(); 26 | tokens.forEach((item: Frame) => 27 | makeFontWeightToken(item, fontWeights, camelizeTokenNames), 28 | ); 29 | 30 | return fontWeights as FontWeightTokens; 31 | } 32 | 33 | function makeFontWeightToken( 34 | item: Frame, 35 | fontWeights: Record, 36 | camelizeTokenNames?: boolean, 37 | ) { 38 | if (!item.name || !item.style) 39 | throw Error(ErrorMakeFontWeightTokensMissingProps); 40 | if (!item.style.fontWeight) 41 | throw Error(ErrorMakeFontWeightTokensMissingWeight); 42 | 43 | const name = sanitizeString(item.name, camelizeTokenNames); 44 | fontWeights[name] = item.style.fontWeight; 45 | } 46 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeMediaQueryTokens.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../contracts/Figma'; 2 | import { MediaQueryTokens } from '../../../contracts/Tokens'; 3 | 4 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 5 | 6 | import { 7 | ErrorSetupMediaQueryTokensMissingProps, 8 | ErrorSetupMediaQueryTokensNoChildren, 9 | ErrorSetupMediaQueryTokensNoFrame, 10 | } from '../../../frameworks/errors/errors'; 11 | 12 | /** 13 | * @description Places all Figma media queries into a clean object 14 | */ 15 | export function makeMediaQueryTokens( 16 | mediaQueryFrame: Frame, 17 | camelizeTokenNames?: boolean, 18 | ): MediaQueryTokens { 19 | if (!mediaQueryFrame) throw Error(ErrorSetupMediaQueryTokensNoFrame); 20 | if (!mediaQueryFrame.children) 21 | throw Error(ErrorSetupMediaQueryTokensNoChildren); 22 | 23 | const mediaQueries: Record = {}; 24 | const tokens = mediaQueryFrame.children.reverse(); 25 | tokens.forEach((item: Frame) => 26 | makeMediaQueryToken(item, mediaQueries, camelizeTokenNames), 27 | ); 28 | 29 | return mediaQueries as MediaQueryTokens; 30 | } 31 | 32 | function makeMediaQueryToken( 33 | item: Frame, 34 | mediaQueries: Record, 35 | camelizeTokenNames?: boolean, 36 | ) { 37 | if (!item.name || !item.absoluteBoundingBox) 38 | throw Error(ErrorSetupMediaQueryTokensMissingProps); 39 | 40 | const name = sanitizeString(item.name, camelizeTokenNames); 41 | mediaQueries[name] = `${item.absoluteBoundingBox.width}px`; 42 | } 43 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeRadiusTokens.ts: -------------------------------------------------------------------------------- 1 | import { RadiusUnit } from '../../../contracts/Config'; 2 | import { FRAME as Frame } from '../../../contracts/Figma'; 3 | import { RadiusTokens } from '../../../contracts/Tokens'; 4 | 5 | import { normalizeUnits } from '../../../frameworks/string/normalizeUnits'; 6 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 7 | 8 | import { 9 | ErrorMakeRadiusTokensMissingProps, 10 | ErrorMakeRadiusTokensNoChildren, 11 | ErrorMakeRadiusTokensNoFrame, 12 | } from '../../../frameworks/errors/errors'; 13 | 14 | /** 15 | * @description Places all Figma radii into a clean object 16 | */ 17 | export function makeRadiusTokens( 18 | radiusFrame: Frame, 19 | radiusUnit: RadiusUnit, 20 | remSize: number, 21 | camelizeTokenNames?: boolean, 22 | ): RadiusTokens { 23 | if (!radiusFrame) throw Error(ErrorMakeRadiusTokensNoFrame); 24 | if (!radiusFrame.children) throw Error(ErrorMakeRadiusTokensNoChildren); 25 | 26 | const cornerRadii: Record = {}; 27 | const tokens = radiusFrame.children.reverse(); 28 | tokens.forEach((item: Frame) => 29 | makeRadiusToken(item, cornerRadii, radiusUnit, remSize, camelizeTokenNames), 30 | ); 31 | 32 | return cornerRadii as RadiusTokens; 33 | } 34 | 35 | function makeRadiusToken( 36 | item: Frame, 37 | cornerRadii: Record, 38 | radiusUnit: RadiusUnit, 39 | remSize: number, 40 | camelizeTokenNames?: boolean, 41 | ) { 42 | if (!item.name) throw Error(ErrorMakeRadiusTokensMissingProps); 43 | 44 | const name: string = sanitizeString(item.name, camelizeTokenNames); 45 | const cornerRadius: string = item.cornerRadius 46 | ? normalizeUnits(item.cornerRadius, 'px', radiusUnit as string, remSize) 47 | : `0${radiusUnit}`; 48 | cornerRadii[name] = cornerRadius; 49 | } 50 | -------------------------------------------------------------------------------- /bin/entities/Token/logic/makeZindexTokens.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../contracts/Figma'; 2 | import { ZindexTokens } from '../../../contracts/Tokens'; 3 | 4 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 5 | 6 | import { 7 | ErrorMakeZindexTokensMissingProps, 8 | ErrorMakeZindexTokensNoChildren, 9 | ErrorMakeZindexTokensNoFrame, 10 | } from '../../../frameworks/errors/errors'; 11 | 12 | /** 13 | * @description Places all Figma Z indices into a clean object 14 | */ 15 | export function makeZindexTokens( 16 | zIndexFrame: Frame, 17 | camelizeTokenNames?: boolean, 18 | ): ZindexTokens { 19 | if (!zIndexFrame) throw Error(ErrorMakeZindexTokensNoFrame); 20 | if (!zIndexFrame.children) throw Error(ErrorMakeZindexTokensNoChildren); 21 | 22 | const zIndex: Record = {}; 23 | const tokens = zIndexFrame.children.reverse(); 24 | tokens.forEach((item: Frame) => 25 | makeZindexToken(item, zIndex, camelizeTokenNames), 26 | ); 27 | 28 | return zIndex as ZindexTokens; 29 | } 30 | 31 | function makeZindexToken( 32 | item: Frame, 33 | zIndex: Record, 34 | camelizeTokenNames?: boolean, 35 | ) { 36 | if (!item.name || !item.characters) 37 | throw Error(ErrorMakeZindexTokensMissingProps); 38 | 39 | const name = sanitizeString(item.name, camelizeTokenNames); 40 | zIndex[name] = parseInt(item.characters); 41 | } 42 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/checkIfExists.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | /** 4 | * @description Check if a file exists 5 | */ 6 | export const checkIfExists = (path: string): boolean => fs.existsSync(path); 7 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/createFolder.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { ErrorCreateFolder } from '../errors/errors'; 4 | 5 | /** 6 | * @description Create folder, checking also if it already exists 7 | */ 8 | export function createFolder(dir: string): void { 9 | if (!dir) throw Error(ErrorCreateFolder); 10 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); 11 | } 12 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/createMissingFoldersFromPath.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { ErrorCreateMissingFoldersFromPath } from '../errors/errors'; 4 | 5 | /** 6 | * @description Create a recursive set of folders from a slash-separated path. 7 | */ 8 | export function createMissingFoldersFromPath(filePath: string): void { 9 | if (!filePath) throw Error(ErrorCreateMissingFoldersFromPath); 10 | 11 | const requiresFolder = filePath.includes('/'); 12 | if (!requiresFolder) return; 13 | 14 | const directoryPath = (() => { 15 | let folder = ''; 16 | filePath.split('/').forEach((pathSection: string) => { 17 | // Unless this looks like a file path, add one more directory subpath 18 | if (!pathSection.includes('.')) folder += `${pathSection}/`; 19 | }); 20 | return folder; 21 | })(); 22 | 23 | if (!fs.existsSync(directoryPath)) 24 | fs.mkdirSync(directoryPath, { recursive: true }); 25 | } 26 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/getDataHelpers.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from '../../contracts/Metadata'; 2 | import { createImportStringFromList } from '../string/createImportStringFromList'; 3 | 4 | /** 5 | * Helper block to decouple logic from prepareWrite function 6 | */ 7 | export const getElement = (metadata?: Metadata): string => { 8 | if (metadata && metadata.element) return metadata.element; 9 | return 'div'; 10 | }; 11 | 12 | export const getText = (metadata?: Metadata): string => { 13 | if (metadata && metadata.text) return metadata.text; 14 | return ''; 15 | }; 16 | 17 | export const getExtraProps = (metadata?: Metadata): string => { 18 | if (metadata && metadata.extraProps) return metadata.extraProps; 19 | return ''; 20 | }; 21 | 22 | export const getImports = ( 23 | metadata?: Metadata, 24 | outputFolderTokens?: string, 25 | tokensRelativeImportPrefix?: string, 26 | ): string => { 27 | if (metadata && metadata.imports && metadata.imports.length > 0) 28 | return createImportStringFromList( 29 | metadata.imports, 30 | outputFolderTokens, 31 | tokensRelativeImportPrefix, 32 | ); 33 | 34 | return ''; 35 | }; 36 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/getSvgFileData.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { ErrorGetSvgFileData } from '../../frameworks/errors/errors'; 4 | 5 | export function getSvgFileData(filePath: string): string { 6 | if (!filePath) throw Error(ErrorGetSvgFileData); 7 | if (!fs.existsSync(`${process.cwd()}/${filePath}`)) return ''; 8 | return fs.readFileSync(`${process.cwd()}/${filePath}`, 'utf8'); 9 | } 10 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/isJsonString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Check if JSON is really a string 3 | * @see https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string-in-javascript-without-using-try 4 | */ 5 | export const isJsonString = ( 6 | str: string, 7 | ): Record | boolean => { 8 | try { 9 | JSON.parse(str); 10 | } catch (_e) { 11 | return false; 12 | } 13 | return true; 14 | }; 15 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/loadFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { ErrorLoadFile } from '../errors/errors'; 4 | import { isJsonString } from '../filesystem/isJsonString'; 5 | 6 | /** 7 | * @description Load file from local path 8 | */ 9 | export function loadFile(path: string): string | Record { 10 | if (!path) throw Error(ErrorLoadFile(path)); 11 | if (!fs.existsSync(path)) throw Error(ErrorLoadFile(path)); 12 | 13 | const data = fs.readFileSync(path, 'utf8'); 14 | return isJsonString(data) ? JSON.parse(data) : data; 15 | } 16 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/prepareWrite.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileContentAndPath, 3 | GetFileDataOperation, 4 | WriteOperation, 5 | } from '../../contracts/Write'; 6 | 7 | import { getFileContentAndPath } from '../filesystem/getFileContentAndPath'; 8 | import { 9 | getElement, 10 | getExtraProps, 11 | getImports, 12 | getText, 13 | } from './getDataHelpers'; 14 | 15 | import { ErrorPrepareWrite, ErrorWriteFile } from '../errors/errors'; 16 | 17 | /** 18 | * @description Controller that starts the prepping/formatting of the file(s) 19 | */ 20 | export function prepareWrite( 21 | writeOperation: WriteOperation, 22 | ): FileContentAndPath { 23 | if (!writeOperation) throw Error(ErrorWriteFile); 24 | 25 | const { 26 | type, 27 | file, 28 | path, 29 | name, 30 | format, 31 | outputFolderTokens, 32 | tokensRelativeImportPrefix, 33 | metadata, 34 | templates, 35 | } = writeOperation; 36 | 37 | if ( 38 | (type === 'css' || type === 'story' || type === 'component') && 39 | !templates 40 | ) 41 | throw Error(ErrorPrepareWrite); 42 | 43 | const getFileDataOperation: GetFileDataOperation = { 44 | type, 45 | file, 46 | path, 47 | name: name.replace(/\s/g, ''), 48 | format, 49 | text: getText(metadata), 50 | element: getElement(metadata), 51 | imports: getImports( 52 | metadata, 53 | outputFolderTokens, 54 | tokensRelativeImportPrefix, 55 | ), 56 | extraProps: getExtraProps(metadata), 57 | metadata, 58 | templates, 59 | }; 60 | 61 | return getFileContentAndPath(getFileDataOperation); 62 | } 63 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/refresh.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { createFolder } from './createFolder'; 5 | 6 | import { ErrorRefresh } from '../errors/errors'; 7 | 8 | /** 9 | * @description Refreshing a folder can happen in two ways: 10 | * 1. If it exists: Delete and then re-create it. 11 | * 2. If it does not exist: Create it. 12 | * 13 | * This supports both the newer post-Node 14.14.0 method, `fs.rmSync()`, and the older method, `fs.rmdirSync()`. 14 | */ 15 | //@ts-ignore 16 | export function refresh( 17 | folderPath: string, 18 | trashExistingFolder = true, 19 | ): string { 20 | if (!folderPath) throw Error(ErrorRefresh); 21 | const resolvedPath = path.resolve(process.cwd(), folderPath); 22 | 23 | if (trashExistingFolder && fs.existsSync(resolvedPath)) { 24 | if (process.versions.node >= '14.14.0') 25 | fs.rmSync(resolvedPath, { recursive: true }); 26 | else fs.rmdirSync(resolvedPath, { recursive: true }); 27 | } 28 | 29 | createFolder(resolvedPath); 30 | return resolvedPath; 31 | } 32 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/write.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { ErrorWrite } from '../errors/errors'; 4 | 5 | /** 6 | * @description Helper that writes files. Prefer using the writeFile function since that is hooked for up for any pre-processing. 7 | */ 8 | export function write(filePath: string, fileContent: string): void { 9 | if (!filePath || !fileContent) throw Error(ErrorWrite); 10 | fs.writeFileSync(filePath, fileContent, 'utf-8'); 11 | } 12 | -------------------------------------------------------------------------------- /bin/frameworks/filesystem/writeBaseJson.ts: -------------------------------------------------------------------------------- 1 | import { FigmaData } from '../../contracts/FigmaData'; 2 | 3 | import { refresh } from './refresh'; 4 | import { write } from './write'; 5 | 6 | import { ErrorWriteBaseJson } from '../errors/errors'; 7 | import { MsgWriteBaseFile } from '../messages/messages'; 8 | 9 | /** 10 | * @description Write base Figma JSON document to disk 11 | */ 12 | export async function writeBaseJson( 13 | figmagicFolder: string, 14 | figmaData: string, 15 | data: FigmaData | Record, 16 | ): Promise { 17 | if (!figmagicFolder || !figmaData || !data) throw Error(ErrorWriteBaseJson); 18 | 19 | console.log(MsgWriteBaseFile); 20 | 21 | refresh(figmagicFolder); 22 | write(`${figmagicFolder}/${figmaData}`, JSON.stringify(data)); 23 | } 24 | -------------------------------------------------------------------------------- /bin/frameworks/network/downloadFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import https from 'https'; 3 | 4 | import { createMissingFoldersFromPath } from '../filesystem/createMissingFoldersFromPath'; 5 | 6 | import { ErrorDownloadFile } from '../errors/errors'; 7 | 8 | /** 9 | * @description Get data from API 10 | */ 11 | export async function downloadFile( 12 | url: string, 13 | filePath: string, 14 | ): Promise { 15 | if (!url || !filePath) throw Error(ErrorDownloadFile); 16 | 17 | return new Promise((resolve, reject) => { 18 | const req = https.get(url, (resp) => { 19 | // @ts-ignore 20 | if (resp.statusCode >= 200 && resp.statusCode < 300) { 21 | createMissingFoldersFromPath(filePath); 22 | const write = resp.pipe(fs.createWriteStream(filePath)); 23 | write.on('finish', resolve); 24 | } else reject(null); 25 | }); 26 | req.on('end', () => resolve()).on('error', (error) => reject(error)); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /bin/frameworks/network/getData.ts: -------------------------------------------------------------------------------- 1 | import { FigmaResponse } from '../../contracts/FigmaData'; 2 | 3 | import { getDataLocal } from './getDataLocal'; 4 | import { getDataRemote } from './getDataRemote'; 5 | 6 | import { 7 | ErrorGetData, 8 | ErrorGetDataFailedLocalAndRemote, 9 | ErrorGetDataNoData, 10 | ErrorGetDataNoTokenOrUrl, 11 | } from '../errors/errors'; 12 | 13 | /** 14 | * @description Helper/orchestrator to get data locally or from Figma (remote) 15 | */ 16 | export async function getData( 17 | recompileLocal: boolean, 18 | figmagicFolder: string, 19 | figmaData: string, 20 | token: string | null, 21 | url: string | null, 22 | versionName?: string | null, 23 | ): Promise { 24 | if (!recompileLocal && (!token || !url)) throw Error(ErrorGetData); 25 | if (recompileLocal && (!figmagicFolder || !figmaData)) 26 | throw Error(ErrorGetDataNoTokenOrUrl); 27 | 28 | const _data = (async () => { 29 | if (recompileLocal) return getDataLocal(figmagicFolder, figmaData); 30 | else if (token && url) return getDataRemote(token, url, versionName); 31 | throw Error(ErrorGetDataFailedLocalAndRemote); 32 | })(); 33 | 34 | const data = await _data; 35 | 36 | // @ts-ignore 37 | if (!recompileLocal && !data.document) throw Error(ErrorGetDataNoData); 38 | if (recompileLocal && !data) throw Error(ErrorGetDataNoData); 39 | 40 | return data; 41 | } 42 | -------------------------------------------------------------------------------- /bin/frameworks/network/getDataLocal.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { FigmaResponse } from '../../contracts/FigmaData'; 4 | 5 | import { loadFile } from '../filesystem/loadFile'; 6 | 7 | import { ErrorGetDataLocal } from '../errors/errors'; 8 | import { MsgSetDataFromLocal } from '../messages/messages'; 9 | 10 | /** 11 | * @description Helper to get Figma data from local source 12 | */ 13 | export function getDataLocal( 14 | figmagicFolder: string, 15 | figmaData: string, 16 | ): FigmaResponse { 17 | if (!figmagicFolder || !figmaData) throw Error(ErrorGetDataLocal); 18 | console.log(MsgSetDataFromLocal); 19 | return loadFile( 20 | path.join(`${figmagicFolder}`, `${figmaData}`), 21 | ) as FigmaResponse; 22 | } 23 | -------------------------------------------------------------------------------- /bin/frameworks/network/getDataRemote.ts: -------------------------------------------------------------------------------- 1 | import { getFromApi } from './getFromApi'; 2 | 3 | import { FigmaResponse } from '../../contracts/FigmaData'; 4 | 5 | import { ErrorGetData, ErrorGetDataNoTokenOrUrl } from '../errors/errors'; 6 | import { MsgSetDataFromApi } from '../messages/messages'; 7 | 8 | /** 9 | * @description Helper to get Figma data from their API 10 | */ 11 | export async function getDataRemote( 12 | token: string, 13 | url: string, 14 | versionName?: string | null, 15 | ): Promise { 16 | if (!token || !url) throw Error(ErrorGetDataNoTokenOrUrl); 17 | console.log(MsgSetDataFromApi); 18 | 19 | let data = null; 20 | data = await getFromApi(token, url, versionName); 21 | 22 | if (!data || data.status === 403) throw Error(ErrorGetData); 23 | return data as any; 24 | } 25 | -------------------------------------------------------------------------------- /bin/frameworks/network/getFromApi.ts: -------------------------------------------------------------------------------- 1 | import { request } from './request'; 2 | 3 | import { ApiResponse } from '../../contracts/ApiResponse'; 4 | 5 | import { 6 | ErrorGetFromApi, 7 | ErrorGetFromApiMissingValues, 8 | } from '../errors/errors'; 9 | import { isJsonString } from '../filesystem/isJsonString'; 10 | 11 | /** 12 | * @description Get data from the Figma API 13 | */ 14 | export async function getFromApi( 15 | figmaToken: string, 16 | figmaUrl: string, 17 | versionName?: string | null, 18 | type: 'files' | 'images' = 'files', 19 | ): Promise { 20 | if (!figmaToken || !figmaUrl) throw Error(ErrorGetFromApiMissingValues); 21 | let endpoint = `/v1/${type}/${figmaUrl}`; 22 | 23 | if (versionName) { 24 | const versions = await request( 25 | `/v1/${type}/${figmaUrl}/versions`, 26 | figmaToken, 27 | ) 28 | .then((res) => { 29 | if (isJsonString(res)) return JSON.parse(res); 30 | return res; 31 | }) 32 | .catch((error: any) => { 33 | throw Error(ErrorGetFromApi(error)); 34 | }); 35 | 36 | if (versions.versions) { 37 | const requestedVersion = versions.versions.filter( 38 | (_version: Record) => _version.label === versionName, 39 | ); 40 | const requestedVersionId = (() => { 41 | if (requestedVersion && requestedVersion.length > 0) { 42 | if (requestedVersion[0].id) { 43 | return requestedVersion[0].id; 44 | } 45 | } 46 | })(); 47 | 48 | endpoint = `/v1/${type}/${figmaUrl}?version=${requestedVersionId}`; 49 | } 50 | } 51 | 52 | return request(endpoint, figmaToken) 53 | .then((res) => res) 54 | .catch((error: any) => { 55 | throw Error(ErrorGetFromApi(error)); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /bin/frameworks/string/calculateDegree2Point.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from '../../contracts/Figma'; 2 | 3 | import { roundNumber } from '../../frameworks/string/roundNumber'; 4 | 5 | import { ErrorCalculateDegree2Point } from '../../frameworks/errors/errors'; 6 | 7 | /** 8 | * Calculate an angle based on 2 coordinates (x, y) 9 | * 10 | * NOTE! This implementation is not strictly equal to what Figma outputs in their CSS inspection panel 11 | * @see https://9elements.com/blog/gradient-angles-in-css/ 12 | * @see https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient#Composition_of_a_linear_gradient 13 | * 14 | * Overall implementation based on this work: 15 | * @see https://gist.github.com/conorbuck/2606166 16 | */ 17 | export function calculateDegree2Point(point1: Vector, point2: Vector): number { 18 | if (!point1 || !point2) throw Error(ErrorCalculateDegree2Point); 19 | 20 | const deltaY = point2.x - point1.x; 21 | const deltaX = point2.y - point1.y; 22 | const angleInRadians = Math.atan2(deltaY, deltaX); 23 | let angleInDegrees = 180 - (angleInRadians * 180) / Math.PI; 24 | if (angleInDegrees < 0) angleInDegrees += 360; // Adjust negative angles 25 | 26 | return roundNumber(angleInDegrees, 2); 27 | } 28 | -------------------------------------------------------------------------------- /bin/frameworks/string/checkIfStringOnlyContainsReturnsOrSpaces.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCheckIfStringOnlyContainsReturnsOrSpaces } from '../../../bin/frameworks/errors/errors'; 2 | 3 | /** 4 | * @description Helper to see if a string has no actual content, i.e. only returns or spaces. 5 | */ 6 | export function checkIfStringOnlyContainsReturnsOrSpaces(str: string): boolean { 7 | if (!str) throw Error(ErrorCheckIfStringOnlyContainsReturnsOrSpaces); 8 | 9 | const hasReturns = str.match(/\n/gi); 10 | const hasSpaces = str.match(/ /gi); 11 | if (hasReturns && hasSpaces) return false; 12 | 13 | str = str.replace(/\n/gi, '').replace(/ /gi, ''); 14 | if (str.length > 0) return false; 15 | return true; 16 | } 17 | -------------------------------------------------------------------------------- /bin/frameworks/string/cleanSvgData.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCleanSvgData } from '../../frameworks/errors/errors'; 2 | 3 | export function cleanSvgData(svgData: string): string { 4 | if (!svgData) throw Error(ErrorCleanSvgData); 5 | return svgData.replace(/width="\w." /gi, '').replace(/height="\w." /gi, ''); 6 | } 7 | -------------------------------------------------------------------------------- /bin/frameworks/string/convertHexToRgba.ts: -------------------------------------------------------------------------------- 1 | import { ErrorConvertHexToRgba } from '../errors/errors'; 2 | 3 | /** 4 | * @description Convert hex color to RGBA (full alpha). Expects a string like "#33ff00". 5 | * @see https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 6 | */ 7 | export function convertHexToRgba(color: string): string { 8 | if (!color) throw Error(ErrorConvertHexToRgba); 9 | 10 | const R = parseInt(color.slice(1, 3), 16); 11 | const G = parseInt(color.slice(3, 5), 16); 12 | const B = parseInt(color.slice(5, 7), 16); 13 | const A = 1; 14 | 15 | return `rgba(${R}, ${G}, ${B}, ${A})`; 16 | } 17 | -------------------------------------------------------------------------------- /bin/frameworks/string/convertRgbaToHex.ts: -------------------------------------------------------------------------------- 1 | import { ErrorConvertRgbaToHex } from '../../frameworks/errors/errors'; 2 | 3 | /** 4 | * @description Convert RGBA color to 8-digit hex color. 5 | */ 6 | export function convertRgbaToHex(color: string): string { 7 | if (!color) throw Error(ErrorConvertRgbaToHex); 8 | 9 | const values: string[] = color 10 | .replace(/rgba?\(/, '') 11 | .replace(/\)/, '') 12 | .replace(/[\s+]/g, '') 13 | .split(','); 14 | 15 | const [r, g, b, a] = values; 16 | 17 | return rgbaToHex(parseInt(r), parseInt(g), parseInt(b), parseFloat(a)); 18 | } 19 | 20 | /** 21 | * @description Output a 8-digit hex string. 22 | * @see https://caniuse.com/css-rrggbbaa 23 | * @see https://hashnode.com/post/understanding-rrggbbaa-color-notation-cisvdr52x088fwt53h1drf6m2 24 | */ 25 | function rgbaToHex(r: number, g: number, b: number, a: number) { 26 | const toHex = (number: number) => { 27 | const hex = Math.round(number).toString(16); 28 | return hex.length === 1 ? '0' + hex : hex; 29 | }; 30 | 31 | const alpha = a >= 0 && a <= 1 ? toHex(a * 255) : 'ff'; 32 | return '#' + toHex(r) + toHex(g) + toHex(b) + alpha; 33 | } 34 | -------------------------------------------------------------------------------- /bin/frameworks/string/createEnumStringOutOfObject.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCreateEnumStringOutOfObject } from '../errors/errors'; 2 | 3 | /** 4 | * @description Create enum string from object function 5 | */ 6 | export function createEnumStringOutOfObject( 7 | obj: Record | string, 8 | ): string { 9 | if (!obj) throw Error(ErrorCreateEnumStringOutOfObject); 10 | 11 | return Object.entries(obj).reduce((acc, [key, value]) => { 12 | return `${acc}\n '${key}' = '${value}',`; 13 | }, ''); 14 | } 15 | -------------------------------------------------------------------------------- /bin/frameworks/string/createImportStringFromList.ts: -------------------------------------------------------------------------------- 1 | import { Imports } from '../../contracts/Imports'; 2 | 3 | import { 4 | ErrorCreateImportStringFromList, 5 | ErrorCreateImportStringFromListZeroLength, 6 | } from '../errors/errors'; 7 | 8 | /** 9 | * @description Convert list of imports to string literal for CSS file production 10 | */ 11 | export function createImportStringFromList( 12 | importArray: Imports, 13 | outputFolderTokens = 'tokens', 14 | tokensRelativeImportPrefix = '', 15 | ): string { 16 | if (!importArray) throw Error(ErrorCreateImportStringFromList); 17 | if (importArray.length === 0) 18 | throw Error(ErrorCreateImportStringFromListZeroLength); 19 | 20 | let importString = ``; 21 | 22 | importArray.forEach((importItem: string) => { 23 | importString += `import ${importItem} from '${tokensRelativeImportPrefix}${outputFolderTokens}/${importItem}';\n`; 24 | }); 25 | 26 | return importString; 27 | } 28 | -------------------------------------------------------------------------------- /bin/frameworks/string/createSolidColorString.ts: -------------------------------------------------------------------------------- 1 | import { OutputFormatColors } from '../../contracts/Config'; 2 | import { Paint } from '../../contracts/Figma'; 3 | 4 | import { roundColorValue } from '../../frameworks/string/roundColorValue'; 5 | 6 | import { ErrorCreateSolidColorString } from '../../frameworks/errors/errors'; 7 | import { convertRgbaToHex } from './convertRgbaToHex'; 8 | 9 | /** 10 | * @description Create an RGBA- or hex-based CSS color string 11 | */ 12 | export function createSolidColorString( 13 | fills: Paint, 14 | outputFormatColors: OutputFormatColors, 15 | ): string { 16 | if (!fills) throw Error(ErrorCreateSolidColorString); 17 | 18 | const useHex = outputFormatColors === 'hex'; 19 | 20 | const R = roundColorValue(fills.color?.r, 255); 21 | const G = roundColorValue(fills.color?.g, 255); 22 | const B = roundColorValue(fills.color?.b, 255); 23 | const A = roundColorValue(fills.opacity ? fills.opacity : fills.color?.a, 1); 24 | 25 | const rgbaString = `rgba(${R}, ${G}, ${B}, ${A})`; 26 | 27 | return useHex ? convertRgbaToHex(rgbaString) : rgbaString; 28 | } 29 | -------------------------------------------------------------------------------- /bin/frameworks/string/getAlphaInPercent.ts: -------------------------------------------------------------------------------- 1 | import { ErrorGetAlphaInPercent } from '../../frameworks/errors/errors'; 2 | 3 | /** 4 | * @description Get RGBA alpha value as a percentage string 5 | */ 6 | export const getAlphaInPercent = (color: string): string => { 7 | if (!color) throw Error(ErrorGetAlphaInPercent); 8 | const sectioned = color.split(','); 9 | return ( 10 | // @ts-ignore 11 | sectioned[sectioned.length - 1].replace(/ /gi, '').replace(')', '') * 100 + 12 | '%' 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /bin/frameworks/string/getFigmaDocumentId.ts: -------------------------------------------------------------------------------- 1 | import { ErrorGetFigmaDocumentId } from '../../frameworks/errors/errors'; 2 | 3 | /** 4 | * @description Get Figma document ID, by either getting the ID substring from a full URL or passing through what seems like an ID 5 | */ 6 | export function getFigmaDocumentId(url: string): string { 7 | if (!url) throw Error(ErrorGetFigmaDocumentId); 8 | if (!url.startsWith('https://www.figma.com/file/')) return url; 9 | return url.split('https://www.figma.com/file/')[1].split('/')[0]; 10 | } 11 | -------------------------------------------------------------------------------- /bin/frameworks/string/getId.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Get temporary ID part from CSS class name (class name used during nested CSS processing) 3 | */ 4 | export function getId(str: string): string | null { 5 | const match = str.match(/__#(.*?) /gi); 6 | if (match) return match[0].replace('__#', ''); 7 | return null; 8 | } 9 | -------------------------------------------------------------------------------- /bin/frameworks/string/normalizeUnits.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorNormalizeUnits, 3 | ErrorNormalizeUnitsNoRemSize, 4 | ErrorNormalizeUnitsUndefined, 5 | } from '../errors/errors'; 6 | 7 | /** 8 | * @description Normalize and convert units 9 | */ 10 | export function normalizeUnits( 11 | value: number, 12 | currentUnit: string, 13 | newUnit: string, 14 | remSize?: number, 15 | ): string { 16 | if (!value || !currentUnit || !newUnit) throw Error(ErrorNormalizeUnits); 17 | 18 | const rootSize = setRootSize(currentUnit); 19 | const unitSize = setUnitSize(value, newUnit, remSize); 20 | 21 | if (rootSize === undefined || unitSize === undefined) 22 | throw Error(ErrorNormalizeUnitsUndefined); 23 | 24 | return getAdjustedValues(value, rootSize, unitSize, newUnit); 25 | } 26 | 27 | function setRootSize(currentUnit: string): number | undefined { 28 | if (currentUnit === 'px' || currentUnit === 'percent') return 1; 29 | return undefined; 30 | } 31 | 32 | function setUnitSize( 33 | value: number, 34 | newUnit: string, 35 | remSize?: number, 36 | ): number | undefined { 37 | if (newUnit === 'unitless') return value / 100; 38 | else if (newUnit === 'rem' || newUnit === 'em' || newUnit === 'px') { 39 | if (!remSize) throw Error(ErrorNormalizeUnitsNoRemSize); 40 | return remSize; 41 | } else return undefined; 42 | } 43 | 44 | function getAdjustedValues( 45 | value: number, 46 | rootSize: number, 47 | unitSize: number, 48 | newUnit: string, 49 | ) { 50 | if (newUnit === 'unitless') return `${unitSize}`; 51 | else if (newUnit === 'px') return `${value}${newUnit}`; 52 | else { 53 | const adjustedValue = 54 | rootSize && unitSize ? value * (rootSize / unitSize) : value; 55 | return `${adjustedValue}${newUnit}`; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /bin/frameworks/string/removeAllIds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Replace all IDs in the shape "__#{something}" from a given string, exchanging them with spaces. 3 | */ 4 | export function removeAllIds(str: string): string { 5 | return str.replace(/__#(.*?) /gi, ' '); 6 | } 7 | -------------------------------------------------------------------------------- /bin/frameworks/string/replaceMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { ErrorReplaceMediaQuery } from '../errors/errors'; 2 | 3 | /** 4 | * @description Replace media query sugar syntax from Figma description block 5 | */ 6 | export function replaceMediaQuery(str: string, match: string): string { 7 | if (!str || !match) throw Error(ErrorReplaceMediaQuery); 8 | 9 | const index = str.indexOf(match); 10 | if (index === -1) return str; 11 | 12 | // Set media query, assume only "upto" or "min" 13 | const queryType = match === '@upto' ? 'max' : 'min'; 14 | 15 | // Get the right parts 16 | const sliceStart = match.length + 1; 17 | const sliceLength = sliceStart + 6; 18 | const query = str.slice(index, index + sliceLength); 19 | let size = query.slice(sliceStart, sliceLength); 20 | const remainder = query.replace(match, ''); 21 | 22 | // If match was too greedy 23 | size = size.replace(/!\d/gi, '').trim(); 24 | 25 | return str 26 | .replace(match, `@media query and (${queryType}-width: ${size}px) {`) 27 | .replace(remainder, ''); 28 | } 29 | -------------------------------------------------------------------------------- /bin/frameworks/string/roundColorValue.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRoundColorValue } from '../errors/errors'; 2 | 3 | /** 4 | * @description Round color values so they are whole integers 5 | */ 6 | export function roundColorValue(quantity = 0.0, scale = 255): number { 7 | if (scale < 0 || scale > 255) throw Error(ErrorRoundColorValue); 8 | 9 | // Set bounds 10 | const minValue = 0.0; 11 | const maxValue = 1.0; 12 | if (quantity < minValue) quantity = minValue; 13 | if (quantity > maxValue) quantity = maxValue; 14 | 15 | // We will assume this means the alpha channel or something similar 16 | if (scale <= 1.0) return parseFloat(quantity.toFixed(2)); 17 | 18 | return parseFloat((quantity * scale).toFixed(0)); 19 | } 20 | -------------------------------------------------------------------------------- /bin/frameworks/string/roundNumber.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Round number to less crazy floating point values 3 | */ 4 | export function roundNumber(num: number, decimals = 6): number { 5 | const number = num.toFixed(decimals); 6 | return parseFloat(number); 7 | } 8 | -------------------------------------------------------------------------------- /bin/frameworks/string/toPascalCase.ts: -------------------------------------------------------------------------------- 1 | import { ErrorToPascalCase } from '../errors/errors'; 2 | 3 | /** 4 | * @description Pascal-case transform a string 5 | */ 6 | export function toPascalCase(str: string): string { 7 | if (!str) throw Error(ErrorToPascalCase); 8 | 9 | return str 10 | .replace(/\w+/g, (w) => w[0].toUpperCase() + w.slice(1).toLowerCase()) 11 | .replace(/\s+/g, ''); 12 | } 13 | -------------------------------------------------------------------------------- /bin/frameworks/system/acceptedFileTypes.ts: -------------------------------------------------------------------------------- 1 | export const acceptedFileTypes: string[] = [ 2 | 'raw', 3 | 'token', 4 | 'component', 5 | 'styled', 6 | 'css', 7 | 'story', 8 | 'description', 9 | 'graphic', 10 | ]; 11 | -------------------------------------------------------------------------------- /bin/frameworks/system/acceptedTokenTypes.ts: -------------------------------------------------------------------------------- 1 | export const acceptedTokenTypes: string[] = [ 2 | 'borderwidths', 3 | 'color', 4 | 'colors', 5 | 'delays', 6 | 'durations', 7 | 'easings', 8 | 'fontfamilies', 9 | 'fontsizes', 10 | 'fontweights', 11 | 'letterspacings', 12 | 'lineheights', 13 | 'mediaqueries', 14 | 'opacities', 15 | 'radii', 16 | 'shadows', 17 | 'spacing', 18 | 'spacings', 19 | 'zindices', 20 | ]; 21 | -------------------------------------------------------------------------------- /bin/frameworks/system/checkIfVoidElement.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Check if given element is a void element ("self-closing"). 3 | */ 4 | export function checkIfVoidElement(elementName: string): boolean { 5 | if (!elementName) return false; 6 | 7 | const elements = [ 8 | 'area', 9 | 'base', 10 | 'basefont', 11 | 'bgsound', 12 | 'br', 13 | 'col', 14 | 'command', 15 | 'embed', 16 | 'frame', 17 | 'hr', 18 | 'image', 19 | 'img', 20 | 'input', 21 | 'isindex', 22 | 'keygen', 23 | 'link', 24 | 'menuitem', 25 | 'meta', 26 | 'nextid', 27 | 'param', 28 | 'source', 29 | 'track', 30 | 'wbr', 31 | ]; 32 | 33 | return elements.includes(elementName); 34 | } 35 | -------------------------------------------------------------------------------- /bin/frameworks/system/colors.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | BgBlack: '\x1b[40m', 3 | BgBlue: '\x1b[44m', 4 | BgCyan: '\x1b[46m', 5 | BgGreen: '\x1b[42m', 6 | BgMagenta: '\x1b[45m', 7 | BgRed: '\x1b[41m', 8 | BgWhite: '\x1b[47m', 9 | BgYellow: '\x1b[43m', 10 | 11 | FgBlack: '\x1b[30m', 12 | FgBlue: '\x1b[34m', 13 | FgCyan: '\x1b[36m', 14 | FgGreen: '\x1b[32m', 15 | FgMagenta: '\x1b[35m', 16 | FgRed: '\x1b[31m', 17 | FgWhite: '\x1b[37m', 18 | FgYellow: '\x1b[33m', 19 | 20 | Blink: '\x1b[5m', 21 | Bright: '\x1b[1m', 22 | Dim: '\x1b[2m', 23 | Hidden: '\x1b[8m', 24 | Reset: '\x1b[0m', 25 | Reverse: '\x1b[7m', 26 | Underscore: '\x1b[4m', 27 | }; 28 | 29 | // Thanks to: https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color 30 | // Also refer to: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 31 | -------------------------------------------------------------------------------- /bin/frameworks/system/configToInit.ts: -------------------------------------------------------------------------------- 1 | export const configToInit = { 2 | templates: { 3 | templatePathGraphic: './node_modules/figmagic/templates/graphic', 4 | templatePathReact: './node_modules/figmagic/templates/react', 5 | templatePathStorybook: './node_modules/figmagic/templates/story', 6 | templatePathStyled: './node_modules/figmagic/templates/styled', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /bin/frameworks/system/ignoreElementsKeywords.ts: -------------------------------------------------------------------------------- 1 | export const ignoreElementsKeywords: string[] = ['ignore']; 2 | -------------------------------------------------------------------------------- /bin/frameworks/system/loadEnv.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | /** 4 | * @description Loads Figmagic environment variables if an `.env` file exists. 5 | */ 6 | export function loadEnv(): void { 7 | const filePath = `${process.cwd()}/.env`; 8 | if (fs.existsSync(filePath)) { 9 | const file = fs.readFileSync(filePath, 'utf-8'); 10 | 11 | const variables = file 12 | .toString() 13 | .split('\n') 14 | .filter( 15 | (i: string) => i.startsWith('FIGMA_TOKEN') || i.startsWith('FIGMA_URL'), 16 | ); 17 | 18 | variables.forEach((variable: string) => { 19 | const [key, value] = variable.split('='); 20 | process.env[key] = value; 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bin/frameworks/system/validatorLists.ts: -------------------------------------------------------------------------------- 1 | export const validBorderWidthUnitList = ['rem', 'em', 'px']; 2 | export const validFontUnitList = ['rem', 'em', 'px']; 3 | export const validLetterSpacingUnitList = ['em', 'px']; 4 | export const validLineHeightUnitList = ['unitless', 'em', 'px', 'rem']; 5 | export const validOpacitiesUnitList = ['float', 'percent']; 6 | export const validOutputDataTypeTokenList = ['enum', 'null', null, undefined]; 7 | export const validOutputFormatColors = ['hex', 'rgba']; 8 | export const validOutputFormatCssList = ['ts', 'mjs', 'js']; 9 | export const validOutputFormatDescList = ['md', 'txt']; 10 | export const validOutputFormatElementsList = ['tsx', 'jsx', 'mjs', 'js']; 11 | export const validOutputFormatGraphicsList = ['svg', 'png']; 12 | export const validOutputFormatStorybookList = ['ts', 'js', 'mdx']; 13 | export const validOutputFormatTokensList = [ 14 | 'ts', 15 | 'mjs', 16 | 'js', 17 | 'json', 18 | 'css', 19 | 'scss', 20 | ]; 21 | export const validRadiusUnitList = ['rem', 'em', 'px']; 22 | export const validShadowUnitList = ['rem', 'em', 'px']; 23 | export const validDurationUnitList = ['s', 'ms']; 24 | export const validSpacingUnitList = ['rem', 'em', 'px']; 25 | -------------------------------------------------------------------------------- /bin/usecases/createGraphics.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../contracts/Config'; 2 | import { FigmaData } from '../contracts/FigmaData'; 3 | 4 | import { createPage } from './interactors/common/createPage'; 5 | import { processGraphics } from './interactors/graphics/processGraphics'; 6 | import { writeGraphics } from './interactors/graphics/writeGraphics'; 7 | 8 | import { refresh } from '../frameworks/filesystem/refresh'; 9 | 10 | import { ErrorCreateGraphics } from '../frameworks/errors/errors'; 11 | import { MsgSyncGraphics } from '../frameworks/messages/messages'; 12 | 13 | /** 14 | * @description Use case for syncing (creating) graphics from Figma file 15 | */ 16 | export async function createGraphics( 17 | config: Config, 18 | data: FigmaData, 19 | ): Promise { 20 | if (!config || !data) throw Error(ErrorCreateGraphics); 21 | console.log(MsgSyncGraphics); 22 | 23 | const { outputFolderGraphics } = config; 24 | refresh(outputFolderGraphics); 25 | const graphicsPage = createPage(data.document.children, 'Graphics'); 26 | const fileList = await processGraphics(graphicsPage, config); 27 | 28 | await writeGraphics(fileList, config); 29 | } 30 | -------------------------------------------------------------------------------- /bin/usecases/createTokens.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../contracts/Config'; 2 | import { FRAME as Frame } from '../contracts/Figma'; 3 | import { FigmaData } from '../contracts/FigmaData'; 4 | 5 | import { createPage } from './interactors/common/createPage'; 6 | import { processTokens } from './interactors/tokens/processTokens'; 7 | import { writeTokens } from './interactors/tokens/writeTokens'; 8 | 9 | import { refresh } from '../frameworks/filesystem/refresh'; 10 | 11 | import { ErrorCreateTokens } from '../frameworks/errors/errors'; 12 | import { 13 | MsgNoTokensFound, 14 | MsgWriteTokens, 15 | } from '../frameworks/messages/messages'; 16 | 17 | /** 18 | * @description Use case for creating token files from Figma 19 | */ 20 | export async function createTokens( 21 | config: Config, 22 | data: FigmaData, 23 | ): Promise { 24 | if (!config || !data) throw Error(ErrorCreateTokens); 25 | console.log(MsgWriteTokens); 26 | 27 | const { outputFolderTokens } = config; 28 | refresh(outputFolderTokens); 29 | const tokensPage: Frame[] = createPage( 30 | data.document.children, 31 | 'Design Tokens', 32 | ); 33 | const processedTokens = processTokens(tokensPage, config); 34 | 35 | if (processedTokens && processedTokens.length > 0) 36 | writeTokens(processedTokens); 37 | else console.warn(MsgNoTokensFound); 38 | } 39 | -------------------------------------------------------------------------------- /bin/usecases/interactors/common/createPage.ts: -------------------------------------------------------------------------------- 1 | import { FRAME as Frame } from '../../../contracts/Figma'; 2 | 3 | import { ErrorCreatePage } from '../../../frameworks/errors/errors'; 4 | 5 | /** 6 | * @description Creates cleaned pages from the raw Figma data, for further processing 7 | */ 8 | export function createPage( 9 | figmaPages: Frame[], 10 | matchingPageName: string, 11 | ): Frame[] { 12 | if (!figmaPages || figmaPages.length === 0) throw Error(ErrorCreatePage); 13 | 14 | const page = figmaPages.filter( 15 | (page) => 16 | page.name.toLowerCase().replace(/ /g, '') === 17 | matchingPageName.toLowerCase().replace(/ /g, ''), 18 | ); 19 | 20 | if (page.length > 0 && page[0].children) return page[0].children; 21 | return []; 22 | } 23 | -------------------------------------------------------------------------------- /bin/usecases/interactors/elements/processElements.ts: -------------------------------------------------------------------------------- 1 | import { makeFigmagicElement } from '../../../entities/FigmagicElement'; 2 | 3 | import { Config } from '../../../contracts/Config'; 4 | import { Components, FRAME as Frame } from '../../../contracts/Figma'; 5 | import { FigmaElement } from '../../../contracts/FigmaElement'; 6 | import { FigmagicElement } from '../../../contracts/FigmagicElement'; 7 | 8 | import { ErrorProcessElements } from '../../../frameworks/errors/errors'; 9 | 10 | /** 11 | * @description Process all elements from a given Figma page 12 | * 1. Filter out components 13 | * 2. Parse elements (typography and styling) 14 | * 3. Return list of cleaned items 15 | */ 16 | export function processElements( 17 | elementsPage: Frame[], 18 | config: Config, 19 | components: Components, 20 | isGraphicElement = false, 21 | ): FigmagicElement[] { 22 | if (!elementsPage || !components || !config) 23 | throw Error(ErrorProcessElements); 24 | 25 | const filteredElements = elementsPage.filter( 26 | (element) => element.type === 'COMPONENT' && element.name[0] !== '_', 27 | ); 28 | 29 | const parsedElements = filteredElements.map((element: FigmaElement) => 30 | makeFigmagicElement( 31 | element, 32 | config, 33 | components[element.id].description, 34 | isGraphicElement, 35 | ), 36 | ); 37 | 38 | return parsedElements; 39 | } 40 | -------------------------------------------------------------------------------- /bin/usecases/interactors/elements/processGraphicElementsMap.ts: -------------------------------------------------------------------------------- 1 | import { Graphic } from '../../../contracts/Graphic'; 2 | 3 | import { ErrorProcessGraphicElementsMap } from '../../../frameworks/errors/errors'; 4 | /** 5 | * @description Create raw file content for the graphic elements index/map file 6 | */ 7 | export function processGraphicElementsMap(graphics: Graphic[]): string { 8 | if (!graphics) throw Error(ErrorProcessGraphicElementsMap); 9 | if (graphics.length === 0) throw Error(ErrorProcessGraphicElementsMap); 10 | 11 | let imports = ''; 12 | graphics.forEach((graphic: Graphic) => { 13 | const graphicName = getFixedGraphicName(graphic.name); 14 | imports += `import ${graphicName} from './${ 15 | graphic.config.outputFolderElements 16 | }/${graphic.name.replace(/\s/g, '')}';\n`; 17 | }); 18 | imports += '\n'; 19 | 20 | let exports = ''; 21 | graphics.forEach((graphic: Graphic) => { 22 | const graphicName = getFixedGraphicName(graphic.name); 23 | exports += ` ${graphicName},\n`; 24 | }); 25 | 26 | return imports + `export const Graphics = {\n${exports}};\n`; 27 | } 28 | 29 | // Get and use last part of name 30 | const getFixedGraphicName = (name: string) => 31 | name.split('/')[name.split('/').length - 1].trim().replace(/\s/g, ''); 32 | -------------------------------------------------------------------------------- /bin/usecases/interactors/elements/writeGraphicElementsMap.ts: -------------------------------------------------------------------------------- 1 | import { createFolder } from '../../../frameworks/filesystem/createFolder'; 2 | import { write } from '../../../frameworks/filesystem/write'; 3 | 4 | import { ErrorWriteGraphicElementsMap } from '../../../frameworks/errors/errors'; 5 | 6 | /** 7 | * @description Write a map ("index") object to export all React components that derive from the Graphics frame 8 | */ 9 | export function writeGraphicElementsMap( 10 | folder: string, 11 | filePath: string, 12 | fileContent: string, 13 | ): void { 14 | if (!folder || !filePath || !fileContent) 15 | throw Error(ErrorWriteGraphicElementsMap); 16 | 17 | createFolder(folder); 18 | write(filePath, fileContent); 19 | } 20 | -------------------------------------------------------------------------------- /bin/usecases/interactors/graphics/getFileList.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse } from '../../../contracts/ApiResponse'; 2 | import { FileList, Id } from '../../../contracts/Files'; 3 | 4 | import { ErrorGetFileList } from '../../../frameworks/errors/errors'; 5 | 6 | /** 7 | * @description Get cleaned list of files 8 | */ 9 | export const getFileList = ( 10 | response: ApiResponse, 11 | ids: Id[], 12 | outputFormatGraphics: string, 13 | ): FileList[] => { 14 | if (!response || !ids || !outputFormatGraphics) throw Error(ErrorGetFileList); 15 | 16 | return Object.entries(response.images).map((image) => { 17 | const match = ids.filter((id) => id.id === image[0]); 18 | const filePath = 19 | match[0].name.trim().replace(/ /g, '') + `.${outputFormatGraphics}`; 20 | return { 21 | url: image[1], 22 | file: filePath, 23 | }; 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /bin/usecases/interactors/graphics/getIdString.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '../../../contracts/Files'; 2 | 3 | import { ErrorGetIdstring } from '../../../frameworks/errors/errors'; 4 | 5 | /** 6 | * @description Collate valid string of IDs 7 | */ 8 | export const getIdString = (ids: Id[]): string => { 9 | if (!ids) throw Error(ErrorGetIdstring); 10 | 11 | let idString = ''; 12 | ids.forEach((item) => (idString += `${item.id},`)); 13 | return idString.slice(0, idString.length - 1); 14 | }; 15 | -------------------------------------------------------------------------------- /bin/usecases/interactors/graphics/processGraphics.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse } from '../../../contracts/ApiResponse'; 2 | import { Config } from '../../../contracts/Config'; 3 | import { FRAME as Frame } from '../../../contracts/Figma'; 4 | import { FileList } from '../../../contracts/Files'; 5 | 6 | import { getFileList } from './getFileList'; 7 | import { getIdString } from './getIdString'; 8 | import { getIds } from './getIds'; 9 | 10 | import { getFromApi } from '../../../frameworks/network/getFromApi'; 11 | 12 | import { 13 | ErrorProcessGraphics, 14 | ErrorProcessGraphicsImageError, 15 | ErrorProcessGraphicsNoImages, 16 | } from '../../../frameworks/errors/errors'; 17 | 18 | /** 19 | * @description Download all image assets from Figma page 20 | */ 21 | export async function processGraphics( 22 | graphicsPage: Frame[], 23 | config: Config, 24 | ): Promise { 25 | if (!graphicsPage) throw Error(ErrorProcessGraphics); 26 | 27 | const { token, url, outputFormatGraphics, outputScaleGraphics, versionName } = 28 | config; 29 | if (!token) throw Error(ErrorProcessGraphics); 30 | if (graphicsPage.length === 0 || !graphicsPage[0].children) 31 | throw Error(ErrorProcessGraphics); 32 | 33 | const ids = getIds(graphicsPage); 34 | const settings = `&scale=${outputScaleGraphics}&format=${outputFormatGraphics}`; 35 | const fullUrl = `${url}?ids=${getIdString(ids)}${settings}`; 36 | 37 | const imageResponse: ApiResponse = await getFromApi( 38 | token, 39 | fullUrl, 40 | versionName, 41 | 'images', 42 | ); 43 | if (imageResponse.err) throw Error(ErrorProcessGraphicsImageError); 44 | if (!imageResponse.images) throw Error(ErrorProcessGraphicsNoImages); 45 | 46 | return getFileList(imageResponse, ids, outputFormatGraphics); 47 | } 48 | -------------------------------------------------------------------------------- /bin/usecases/interactors/graphics/writeGraphics.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../../contracts/Config'; 2 | import { FileList } from '../../../contracts/Files'; 3 | 4 | import { createFolder } from '../../../frameworks/filesystem/createFolder'; 5 | import { downloadFile } from '../../../frameworks/network/downloadFile'; 6 | 7 | import { ErrorWriteGraphics } from '../../../frameworks/errors/errors'; 8 | 9 | /** 10 | * @description Write image assets from Figma page to disk 11 | */ 12 | export async function writeGraphics( 13 | fileList: FileList[], 14 | config: Config, 15 | ): Promise { 16 | if (!fileList || !config) throw Error(ErrorWriteGraphics); 17 | 18 | const { outputFolderGraphics } = config; 19 | createFolder(outputFolderGraphics); 20 | 21 | await Promise.all( 22 | fileList.map(async (file: FileList) => { 23 | return new Promise(async (resolve) => { 24 | await downloadFile(file.url, `${outputFolderGraphics}/${file.file}`); 25 | resolve(true); 26 | }); 27 | }), 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /bin/usecases/interactors/tokens/processTokens.ts: -------------------------------------------------------------------------------- 1 | import { WriteOperation } from '../../../contracts/Write'; 2 | import { makeToken } from '../../../entities/Token/index'; 3 | 4 | import { Config } from '../../../contracts/Config'; 5 | import { FRAME as Frame } from '../../../contracts/Figma'; 6 | 7 | import { sanitizeString } from '../../../frameworks/string/sanitizeString'; 8 | import { acceptedTokenTypes } from '../../../frameworks/system/acceptedTokenTypes'; 9 | 10 | import { ErrorWriteTokensNoSettings } from '../../../frameworks/errors/errors'; 11 | 12 | /** 13 | * @description Process tokens (before writing them to file; handled in another function) 14 | */ 15 | export function processTokens( 16 | tokens: Frame[], 17 | config: Config, 18 | ): WriteOperation[] { 19 | if (!config) throw Error(ErrorWriteTokensNoSettings); 20 | if (!tokens) return []; 21 | 22 | const processedTokens: WriteOperation[] = []; 23 | 24 | tokens.forEach((tokenFrame) => { 25 | const tokenName = sanitizeString(tokenFrame.name); 26 | 27 | // Skip any design token frames that begin with an underscore 28 | if (tokenFrame.type.toUpperCase() === 'FRAME' && tokenName[0] === '_') 29 | return; 30 | 31 | if ( 32 | acceptedTokenTypes.includes(tokenName.toLowerCase()) && 33 | tokenName[0] !== '_' 34 | ) { 35 | const token = makeToken(tokenFrame, tokenName, config); 36 | const writeOperation = token.getWriteOperation(); 37 | if (writeOperation) processedTokens.push(writeOperation); 38 | } 39 | }); 40 | 41 | return processedTokens; 42 | } 43 | -------------------------------------------------------------------------------- /bin/usecases/interactors/tokens/writeTokens.ts: -------------------------------------------------------------------------------- 1 | import { WriteOperation } from '../../../contracts/Write'; 2 | 3 | import { writeFile } from '../../../frameworks/filesystem/writeFile'; 4 | 5 | import { ErrorWriteTokens } from '../../../frameworks/errors/errors'; 6 | 7 | /** 8 | * @description Write processed tokens to file 9 | */ 10 | export function writeTokens(processedTokens: WriteOperation[]): void { 11 | if (!processedTokens) throw Error(ErrorWriteTokens); 12 | 13 | processedTokens.forEach((token) => writeFile(token)); 14 | } 15 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: '90...100' 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: 'reach,diff,flags,tree' 19 | behavior: default 20 | require_changes: no 21 | -------------------------------------------------------------------------------- /generate-keys.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Reference: https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/ 4 | 5 | openssl genrsa -out key.pem 6 | openssl req -new -key key.pem -out csr.pem 7 | openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem 8 | rm csr.pem -------------------------------------------------------------------------------- /images/add-underscore-to-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/add-underscore-to-block.png -------------------------------------------------------------------------------- /images/color-themes-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/color-themes-demo.png -------------------------------------------------------------------------------- /images/component-desc-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/component-desc-field.png -------------------------------------------------------------------------------- /images/composing-font-from-multiple-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/composing-font-from-multiple-tokens.png -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/cover.png -------------------------------------------------------------------------------- /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/demo.png -------------------------------------------------------------------------------- /images/design-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/design-tokens.png -------------------------------------------------------------------------------- /images/flat-element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/flat-element.png -------------------------------------------------------------------------------- /images/literal-font-families-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/literal-font-families-demo.png -------------------------------------------------------------------------------- /images/nesting-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/nesting-code.png -------------------------------------------------------------------------------- /images/nesting-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/nesting-error.png -------------------------------------------------------------------------------- /images/nesting-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/nesting-normal.png -------------------------------------------------------------------------------- /images/nesting-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/nesting-warning.png -------------------------------------------------------------------------------- /images/project-structure-elements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/project-structure-elements.png -------------------------------------------------------------------------------- /images/project-structure-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/project-structure-tokens.png -------------------------------------------------------------------------------- /images/single-layer-support-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/single-layer-support-only.png -------------------------------------------------------------------------------- /images/sometimes-you-have-to-fake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelvesavuori/figmagic/164e4337b33b66b11c377375b67652b342c37c6f/images/sometimes-you-have-to-fake.png -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | coverageDirectory: 'jest-coverage', 4 | testEnvironment: 'node', 5 | transform: { 6 | '^.+\\.ts$': 'ts-jest' 7 | }, 8 | setupFiles: ['/jest.env.js'], 9 | setupFilesAfterEnv: ['/jest.setup.ts'], 10 | moduleFileExtensions: ['js', 'ts'], 11 | testMatch: ['/__tests__/**/*.test.ts'], 12 | testPathIgnorePatterns: ['/node_modules/'], 13 | testTimeout: 60000 14 | }; 15 | -------------------------------------------------------------------------------- /jest.env.js: -------------------------------------------------------------------------------- 1 | process.env.IS_TEST = true; 2 | process.env.IS_MOCK_ENABLED = true; 3 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import { mockServer } from './__tests__/mocks'; 2 | 3 | // Allow mocking to be enabled / disabled 4 | if (process.env.IS_MOCK_ENABLED === 'true') { 5 | // Establish API mocking before all tests. 6 | beforeAll(() => mockServer.listen()); 7 | 8 | // Reset any request handlers that we may add during the tests, 9 | // so they don't affect other tests. 10 | afterEach(() => mockServer.resetHandlers()); 11 | 12 | // Clean up after the tests are finished. 13 | afterAll(() => mockServer.close()); 14 | } 15 | -------------------------------------------------------------------------------- /templates/graphic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const {{NAME}} = (props) => {{SVG}} 4 | 5 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/graphic.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const {{NAME}} = (props) => {{SVG}} 4 | 5 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/graphic.mjs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const {{NAME}} = (props) => {{SVG}} 4 | 5 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/graphic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const {{NAME}} = (): any => {{SVG}} 4 | 5 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {{NAME_STYLED}} from './{{NAME_STYLED}}'; 5 | 6 | const {{NAME}} = (props) => <{{NAME_STYLED}}{{EXTRA_PROPS}} {...props}>{props.children ? props.children : "{{TEXT}}"}; 7 | 8 | {{NAME}}.propTypes = {}; 9 | 10 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/react.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {{NAME_STYLED}} from './{{NAME_STYLED}}'; 5 | 6 | const {{NAME}} = (props) => <{{NAME_STYLED}}{{EXTRA_PROPS}} {...props}>{props.children ? props.children : "{{TEXT}}"}; 7 | 8 | {{NAME}}.propTypes = {}; 9 | 10 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/react.mjs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {{NAME_STYLED}} from './{{NAME_STYLED}}'; 5 | 6 | const {{NAME}} = (props) => <{{NAME_STYLED}}{{EXTRA_PROPS}} {...props}>{props.children ? props.children : "{{TEXT}}"}; 7 | 8 | {{NAME}}.propTypes = {}; 9 | 10 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/react.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {{NAME_STYLED}} from './{{NAME_STYLED}}'; 4 | 5 | interface {{NAME}}Props { 6 | children: any; 7 | [propName: string]: any; 8 | } 9 | 10 | const {{NAME}}: React.FC<{{NAME}}Props> = (props: any) => ( 11 | <{{NAME_STYLED}}{{EXTRA_PROPS}} {...props}>{props.children ? props.children : "{{TEXT}}"} 12 | ); 13 | 14 | export default {{NAME}}; -------------------------------------------------------------------------------- /templates/story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {{NAME}} from './{{NAME}}'; 3 | 4 | import notes from './{{NAME}}.description.md'; 5 | 6 | export default { title: '{{NAME}}', parameters: { notes } }; 7 | 8 | export const {{NAME}}Regular = () => <{{NAME}}>{{TEXT}}; -------------------------------------------------------------------------------- /templates/story.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Canvas, Story, ArgsTable } from '@storybook/addon-docs/blocks'; 2 | 3 | import {{NAME}} from './{{NAME}}'; 4 | 5 | import {{NAME}}Description from './{{NAME}}.description.md'; 6 | 7 | 12 | 13 | <{{NAME}}Description /> 14 | 15 | 16 | 17 | {(args) => <{{NAME}} {...args} />} 18 | 19 | 20 | 21 | ## ArgsTable 22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/story.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {{NAME}} from './{{NAME}}'; 3 | 4 | import notes from './{{NAME}}.description.md'; 5 | 6 | export default { title: '{{NAME}}', parameters: { notes } }; 7 | 8 | export const {{NAME}}Regular = () => <{{NAME}}>{{TEXT}}; -------------------------------------------------------------------------------- /templates/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import {{NAME_CSS}} from './{{NAME_CSS}}'; 4 | 5 | // Extend the below as needed 6 | const {{NAME_STYLED}} = styled.{{ELEMENT}}` 7 | ${{{NAME_CSS}}}; 8 | `; 9 | 10 | export default {{NAME_STYLED}}; -------------------------------------------------------------------------------- /templates/styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import {{NAME_CSS}} from './{{NAME_CSS}}'; 4 | 5 | // Extend the below as needed 6 | const {{NAME_STYLED}} = styled.{{ELEMENT}}` 7 | ${{{NAME_CSS}}}; 8 | `; 9 | 10 | export default {{NAME_STYLED}}; -------------------------------------------------------------------------------- /templates/styled.mjs: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import {{NAME_CSS}} from './{{NAME_CSS}}'; 4 | 5 | // Extend the below as needed 6 | const {{NAME_STYLED}} = styled.{{ELEMENT}}` 7 | ${{{NAME_CSS}}}; 8 | `; 9 | 10 | export default {{NAME_STYLED}}; -------------------------------------------------------------------------------- /templates/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import {{NAME_CSS}} from './{{NAME_CSS}}'; 4 | 5 | interface {{NAME}}Props { 6 | children: any; 7 | [propName: string]: {}; 8 | } 9 | 10 | // Extend the below as needed 11 | const {{NAME_STYLED}} = styled.{{ELEMENT}}<{{NAME}}Props>` 12 | ${{{NAME_CSS}}}; 13 | `; 14 | 15 | export default {{NAME_STYLED}}; -------------------------------------------------------------------------------- /testdata/components.ts: -------------------------------------------------------------------------------- 1 | export const components = { 2 | '2875:22': { 3 | key: '', 4 | name: 'Microcopy', 5 | description: 'element=sub\ndescription=\n# Sub\n\nTiny text snippets.' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/css/nestedButtonTextOnly.ts: -------------------------------------------------------------------------------- 1 | export const nestedButtonTextOnly = ` 2 | 3 | .Strong__#23c21900 { 4 | color: \${colors['white']}; 5 | font-size: \${fontSizes['tinier']}; 6 | font-family: \${fontFamilies['strong']}; 7 | font-weight: \${fontWeights['strong']}; 8 | line-height: \${lineHeights['normal']}; 9 | text-align: left; 10 | } 11 | 12 | .Standard__#64895a9e { 13 | color: \${colors['white']}; 14 | font-size: \${fontSizes['tinier']}; 15 | font-family: \${fontFamilies['normal']}; 16 | font-weight: \${fontWeights['normal']}; 17 | line-height: \${lineHeights['normal']}; 18 | text-align: left; 19 | } 20 | 21 | .Light__#02562f6f { 22 | color: \${colors['white']}; 23 | font-size: \${fontSizes['tinier']}; 24 | font-family: \${fontFamilies['light']}; 25 | font-weight: \${fontWeights['light']}; 26 | line-height: \${lineHeights['normal']}; 27 | text-align: left; 28 | }`; 29 | 30 | export const nestedButtonTextOnlyResult = ` 31 | color: \${colors['white']}; 32 | font-size: \${fontSizes['tinier']}; 33 | line-height: \${lineHeights['normal']}; 34 | text-align: left; 35 | 36 | &.Light { 37 | font-family: \${fontFamilies['light']}; 38 | font-weight: \${fontWeights['light']}; 39 | } 40 | 41 | &.Standard { 42 | font-family: \${fontFamilies['normal']}; 43 | font-weight: \${fontWeights['normal']}; 44 | } 45 | 46 | &.Strong { 47 | font-family: \${fontFamilies['strong']}; 48 | font-weight: \${fontWeights['strong']}; 49 | } 50 | 51 | `; 52 | -------------------------------------------------------------------------------- /testdata/elements/cssLayoutElement.ts: -------------------------------------------------------------------------------- 1 | export const cssLayoutElement = { 2 | id: '2768:1', 3 | name: 'Button Regular', 4 | type: 'RECTANGLE', 5 | blendMode: 'PASS_THROUGH', 6 | absoluteBoundingBox: { x: 400, y: 0, width: 320, height: 48 }, 7 | constraints: { vertical: 'SCALE', horizontal: 'SCALE' }, 8 | fills: [{ blendMode: 'NORMAL', type: 'SOLID', color: {} }], 9 | strokes: [{ blendMode: 'NORMAL', type: 'SOLID', color: {} }], 10 | strokeWeight: 1, 11 | strokeAlign: 'INSIDE', 12 | effects: [], 13 | cornerRadius: 8, 14 | rectangleCornerRadii: [8, 8, 8, 8] 15 | }; 16 | -------------------------------------------------------------------------------- /testdata/elements/cssLayoutElementShadow.ts: -------------------------------------------------------------------------------- 1 | export const cssLayoutElementShadow = { 2 | id: '2874:36', 3 | name: 'Input Regular', 4 | type: 'RECTANGLE', 5 | blendMode: 'PASS_THROUGH', 6 | absoluteBoundingBox: { 7 | x: 0, 8 | y: 150, 9 | width: 320, 10 | height: 48 11 | }, 12 | constraints: { 13 | vertical: 'CENTER', 14 | horizontal: 'LEFT_RIGHT' 15 | }, 16 | fills: [ 17 | { 18 | blendMode: 'NORMAL', 19 | type: 'SOLID', 20 | color: { 21 | r: 0.9490196108818054, 22 | g: 0.9490196108818054, 23 | b: 0.9490196108818054, 24 | a: 1 25 | } 26 | } 27 | ], 28 | strokes: [ 29 | { 30 | blendMode: 'NORMAL', 31 | type: 'SOLID', 32 | color: { 33 | r: 0.20000000298023224, 34 | g: 0.20000000298023224, 35 | b: 0.20000000298023224, 36 | a: 1 37 | } 38 | } 39 | ], 40 | strokeWeight: 2, 41 | strokeAlign: 'INSIDE', 42 | effects: [ 43 | { 44 | type: 'DROP_SHADOW', 45 | visible: true, 46 | color: { 47 | r: 0.7686274647712708, 48 | g: 0.7686274647712708, 49 | b: 0.7686274647712708, 50 | a: 0.75 51 | }, 52 | blendMode: 'NORMAL', 53 | offset: { 54 | x: 3, 55 | y: 3 56 | }, 57 | radius: 3 58 | } 59 | ], 60 | styles: { 61 | effect: '2657:135' 62 | }, 63 | cornerRadius: 4, 64 | rectangleCornerRadii: [4, 4, 4, 4] 65 | }; 66 | -------------------------------------------------------------------------------- /testdata/elements/cssTypographyElement.ts: -------------------------------------------------------------------------------- 1 | export const cssTypographyElement = { 2 | id: '2859:3', 3 | name: 'Text', 4 | type: 'TEXT', 5 | blendMode: 'PASS_THROUGH', 6 | absoluteBoundingBox: { x: 400, y: 541, width: 320, height: 48 }, 7 | constraints: { vertical: 'SCALE', horizontal: 'SCALE' }, 8 | fills: [{ blendMode: 'NORMAL', type: 'SOLID', color: {} }], 9 | strokes: [], 10 | strokeWeight: 1, 11 | strokeAlign: 'OUTSIDE', 12 | effects: [], 13 | characters: 'Button Text', 14 | style: { 15 | fontFamily: 'Helvetica Neue', 16 | fontPostScriptName: 'HelveticaNeue-Bold', 17 | fontWeight: 700, 18 | paragraphSpacing: 1, 19 | textCase: 'UPPER', 20 | fontSize: 16, 21 | textAlignHorizontal: 'CENTER', 22 | textAlignVertical: 'CENTER', 23 | letterSpacing: 0.8, 24 | lineHeightPx: 18.75, 25 | lineHeightPercent: 100, 26 | lineHeightUnit: 'INTRINSIC_%' 27 | }, 28 | characterStyleOverrides: [], 29 | styleOverrideTable: {} 30 | }; 31 | -------------------------------------------------------------------------------- /testdata/elements/graphicElement.ts: -------------------------------------------------------------------------------- 1 | export const graphicElement = [ 2 | { 3 | id: '2710:7', 4 | name: 'More', 5 | children: [[{}], [{}]], 6 | type: 'COMPONENT', 7 | config: { 8 | debugMode: false, 9 | fontUnit: 'rem', 10 | letterSpacingUnit: 'em', 11 | opacitiesUnit: 'float', 12 | figmaData: 'figma.json', 13 | figmagicFolder: '.figmagic', 14 | outputFolderElements: 'elements', 15 | outputFolderGraphics: 'graphics', 16 | outputFolderTokens: 'tokens', 17 | outputFormatCss: 'ts', 18 | outputFormatDescription: 'md', 19 | outputFormatElements: 'tsx', 20 | outputFormatGraphics: 'svg', 21 | outputFormatStorybook: 'js', 22 | outputFormatTokens: 'ts', 23 | outputGraphicElements: true, 24 | outputGraphicElementsMap: true, 25 | outputScaleGraphics: 1, 26 | outputDataTypeToken: null, 27 | recompileLocal: true, 28 | remSize: 16, 29 | skipFileGeneration: [{}], 30 | spacingUnit: 'rem', 31 | syncElements: true, 32 | syncGraphics: true, 33 | syncTokens: true, 34 | templates: [{}], 35 | token: '35475-91f7e693-3585-464b-87d5-6d0592e0fdad', 36 | url: 'K39TRbltDVcWFlpzw9r7Zh', 37 | usePostscriptFontNames: false 38 | }, 39 | description: 'Always more. So much more.', 40 | element: 'div', 41 | css: '\n', 42 | html: '
', 43 | extraProps: '', 44 | text: '', 45 | imports: [] 46 | } 47 | ]; 48 | -------------------------------------------------------------------------------- /testdata/elements/writeElements.ts: -------------------------------------------------------------------------------- 1 | export const elements = [ 2 | { 3 | id: '2875:22', 4 | name: 'Microcopy', 5 | element: 'sub', 6 | description: '\n# Sub\n\nTiny text snippets.', 7 | css: 8 | ' color: ${colors.black};\n' + 9 | 'font-size: ${fontSizes.sub};\n' + 10 | 'font-family: ${fontFamilies.regular};\n' + 11 | 'font-weight: ${fontWeights.regular};\n' + 12 | 'line-height: ${lineHeights.xs};\n' + 13 | 'text-align: left;\n', 14 | html: 'Microcopy', 15 | extraProps: '', 16 | text: 'Microcopy', 17 | imports: ['colors', 'fontSizes', 'fontFamilies', 'fontWeights', 'lineHeights'] 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /testdata/enumData.ts: -------------------------------------------------------------------------------- 1 | export const enumDataTestObject = { 2 | green3: 'rgba(111, 207, 151, 1)', 3 | green2: 'rgba(39, 174, 96, 1)', 4 | green1: 'rgba(33, 150, 83, 1)', 5 | blue3: 'rgba(86, 204, 242, 1)', 6 | blue2: 'rgba(45, 156, 219, 1)', 7 | blue1: 'rgba(47, 128, 237, 1)', 8 | yellow: 'rgba(242, 201, 76, 1)', 9 | orange: 'rgba(242, 153, 74, 1)', 10 | red: 'rgba(235, 87, 87, 1)', 11 | neon: 'rgba(228, 255, 193, 1)', 12 | gray5: 'rgba(242, 242, 242, 1)', 13 | gray4: 'rgba(224, 224, 224, 1)', 14 | gray3: 'rgba(189, 189, 189, 1)', 15 | gray2: 'rgba(130, 130, 130, 1)', 16 | gray1: 'rgba(79, 79, 79, 1)', 17 | white: 'rgba(255, 255, 255, 1)', 18 | black: 'rgba(51, 51, 51, 1)' 19 | }; 20 | 21 | export const enumDataExpectedResponse = ` 22 | 'green3' = 'rgba(111, 207, 151, 1)', 23 | 'green2' = 'rgba(39, 174, 96, 1)', 24 | 'green1' = 'rgba(33, 150, 83, 1)', 25 | 'blue3' = 'rgba(86, 204, 242, 1)', 26 | 'blue2' = 'rgba(45, 156, 219, 1)', 27 | 'blue1' = 'rgba(47, 128, 237, 1)', 28 | 'yellow' = 'rgba(242, 201, 76, 1)', 29 | 'orange' = 'rgba(242, 153, 74, 1)', 30 | 'red' = 'rgba(235, 87, 87, 1)', 31 | 'neon' = 'rgba(228, 255, 193, 1)', 32 | 'gray5' = 'rgba(242, 242, 242, 1)', 33 | 'gray4' = 'rgba(224, 224, 224, 1)', 34 | 'gray3' = 'rgba(189, 189, 189, 1)', 35 | 'gray2' = 'rgba(130, 130, 130, 1)', 36 | 'gray1' = 'rgba(79, 79, 79, 1)', 37 | 'white' = 'rgba(255, 255, 255, 1)', 38 | 'black' = 'rgba(51, 51, 51, 1)',`; 39 | -------------------------------------------------------------------------------- /testdata/exampleData.json: -------------------------------------------------------------------------------- 1 | { 2 | "something": "value" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/figma-minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": { 3 | "id": "0:0", 4 | "name": "Document", 5 | "type": "DOCUMENT", 6 | "children": [ 7 | { 8 | "id": "2605:12", 9 | "name": "Design Tokens" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testdata/figmagicrc: -------------------------------------------------------------------------------- 1 | { 2 | "debugMode": false, 3 | "fontUnit": "rem", 4 | "figmaData": "figma.json", 5 | "figmagicFolder": ".figmagic", 6 | "outputFolderTokens": "tokens", 7 | "outputFormatTokens": "ts", 8 | "outputFolderGraphics": "graphics", 9 | "outputFormatGraphics": "svg", 10 | "outputScaleGraphics": 1, 11 | "spacingUnit": "rem", 12 | "usePostscriptFontNames": false, 13 | "useLiteralFontFamilies": false, 14 | "token": "***", 15 | "url": "***" 16 | } 17 | -------------------------------------------------------------------------------- /testdata/fileList.ts: -------------------------------------------------------------------------------- 1 | export const fileList = [ 2 | { 3 | url: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png', 4 | file: 'example.png' 5 | }, 6 | { 7 | url: 'https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg', 8 | file: 'example.svg' 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /testdata/graphics/getGraphics.ts: -------------------------------------------------------------------------------- 1 | export const processGraphicsTestData = { 2 | err: null, 3 | images: { 4 | '2710:7': 5 | 'https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/1c12/7bf2/5382af0fbf2908d72167b084836854f3', 6 | '2710:5': 7 | 'https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/0882/9cc7/c4731c8d0df07592cd6f7dc0519bb3bb' 8 | } 9 | }; 10 | 11 | export const processGraphicsTestDataWithError = { 12 | err: 400, 13 | images: { 14 | '2710:7': 15 | 'https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/1c12/7bf2/5382af0fbf2908d72167b084836854f3', 16 | '2710:5': 17 | 'https://s3-us-west-2.amazonaws.com/figma-alpha-api/img/0882/9cc7/c4731c8d0df07592cd6f7dc0519bb3bb' 18 | } 19 | }; 20 | 21 | export const graphicsIds = [ 22 | { id: '2710:7', name: 'More' }, 23 | { id: '2710:5', name: 'Close' } 24 | ]; 25 | -------------------------------------------------------------------------------- /testdata/svg/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /testdata/svg/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /testdata/svg/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /testdata/svg/svg.ts: -------------------------------------------------------------------------------- 1 | export const svgData = ` 2 | 3 | `; 4 | -------------------------------------------------------------------------------- /testdata/tokens/borderWidths.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const borderWidths = { 4 | "chunky": "8px", 5 | "fat": "4px", 6 | "regular": "2px", 7 | "hairline": "1px" 8 | } 9 | 10 | module.exports = borderWidths; -------------------------------------------------------------------------------- /testdata/tokens/borderWidths.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const borderWidths = { 4 | "chunky": "8px", 5 | "fat": "4px", 6 | "regular": "2px", 7 | "hairline": "1px" 8 | } 9 | 10 | export default borderWidths; -------------------------------------------------------------------------------- /testdata/tokens/borderWidths.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const borderWidths = { 4 | "chunky": "8px", 5 | "fat": "4px", 6 | "regular": "2px", 7 | "hairline": "1px" 8 | } 9 | 10 | export default borderWidths; -------------------------------------------------------------------------------- /testdata/tokens/colors.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const colors = { 4 | "green3": "rgba(111, 207, 151, 1)", 5 | "green2": "rgba(39, 174, 96, 1)", 6 | "green1": "rgba(33, 150, 83, 1)", 7 | "blue3": "rgba(86, 204, 242, 1)", 8 | "blue2": "rgba(45, 156, 219, 1)", 9 | "blue1": "rgba(47, 128, 237, 1)", 10 | "yellow": "rgba(242, 201, 76, 1)", 11 | "orange": "rgba(242, 153, 74, 1)", 12 | "red": "rgba(235, 87, 87, 1)", 13 | "neon": "rgba(228, 255, 193, 1)", 14 | "gray5": "rgba(242, 242, 242, 1)", 15 | "gray4": "rgba(224, 224, 224, 1)", 16 | "gray3": "rgba(189, 189, 189, 1)", 17 | "gray2": "rgba(130, 130, 130, 1)", 18 | "gray1": "rgba(79, 79, 79, 1)", 19 | "white": "rgba(255, 255, 255, 1)", 20 | "black": "rgba(51, 51, 51, 1)" 21 | } 22 | 23 | module.exports = colors; -------------------------------------------------------------------------------- /testdata/tokens/colors.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const colors = { 4 | "green3": "rgba(111, 207, 151, 1)", 5 | "green2": "rgba(39, 174, 96, 1)", 6 | "green1": "rgba(33, 150, 83, 1)", 7 | "blue3": "rgba(86, 204, 242, 1)", 8 | "blue2": "rgba(45, 156, 219, 1)", 9 | "blue1": "rgba(47, 128, 237, 1)", 10 | "yellow": "rgba(242, 201, 76, 1)", 11 | "orange": "rgba(242, 153, 74, 1)", 12 | "red": "rgba(235, 87, 87, 1)", 13 | "neon": "rgba(228, 255, 193, 1)", 14 | "gray5": "rgba(242, 242, 242, 1)", 15 | "gray4": "rgba(224, 224, 224, 1)", 16 | "gray3": "rgba(189, 189, 189, 1)", 17 | "gray2": "rgba(130, 130, 130, 1)", 18 | "gray1": "rgba(79, 79, 79, 1)", 19 | "white": "rgba(255, 255, 255, 1)", 20 | "black": "rgba(51, 51, 51, 1)" 21 | } 22 | 23 | export default colors; -------------------------------------------------------------------------------- /testdata/tokens/colors.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const colors = { 4 | "green3": "rgba(111, 207, 151, 1)", 5 | "green2": "rgba(39, 174, 96, 1)", 6 | "green1": "rgba(33, 150, 83, 1)", 7 | "blue3": "rgba(86, 204, 242, 1)", 8 | "blue2": "rgba(45, 156, 219, 1)", 9 | "blue1": "rgba(47, 128, 237, 1)", 10 | "yellow": "rgba(242, 201, 76, 1)", 11 | "orange": "rgba(242, 153, 74, 1)", 12 | "red": "rgba(235, 87, 87, 1)", 13 | "neon": "rgba(228, 255, 193, 1)", 14 | "gray5": "rgba(242, 242, 242, 1)", 15 | "gray4": "rgba(224, 224, 224, 1)", 16 | "gray3": "rgba(189, 189, 189, 1)", 17 | "gray2": "rgba(130, 130, 130, 1)", 18 | "gray1": "rgba(79, 79, 79, 1)", 19 | "white": "rgba(255, 255, 255, 1)", 20 | "black": "rgba(51, 51, 51, 1)" 21 | } 22 | 23 | export default colors; -------------------------------------------------------------------------------- /testdata/tokens/durations.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const durations = { 4 | "veryLong": 1, 5 | "long": 0.6, 6 | "medium": 0.25, 7 | "short": 0.15 8 | } 9 | 10 | module.exports = durations; -------------------------------------------------------------------------------- /testdata/tokens/durations.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const durations = { 4 | "veryLong": 1, 5 | "long": 0.6, 6 | "medium": 0.25, 7 | "short": 0.15 8 | } 9 | 10 | export default durations; -------------------------------------------------------------------------------- /testdata/tokens/durations.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const durations = { 4 | "veryLong": 1, 5 | "long": 0.6, 6 | "medium": 0.25, 7 | "short": 0.15 8 | } 9 | 10 | export default durations; -------------------------------------------------------------------------------- /testdata/tokens/fontFamilies.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontFamilies = { 4 | "regular": "Helvetica Neue" 5 | } 6 | 7 | module.exports = fontFamilies; -------------------------------------------------------------------------------- /testdata/tokens/fontFamilies.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontFamilies = { 4 | "regular": "Helvetica Neue" 5 | } 6 | 7 | export default fontFamilies; -------------------------------------------------------------------------------- /testdata/tokens/fontFamilies.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontFamilies = { 4 | "regular": "Helvetica Neue" 5 | } 6 | 7 | export default fontFamilies; -------------------------------------------------------------------------------- /testdata/tokens/fontFamiliesPostscript.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontFamilies = { 4 | "bold": "HelveticaNeue-Bold", 5 | "medium": "HelveticaNeue-Medium", 6 | "regular": "HelveticaNeue", 7 | "light": "HelveticaNeue-Light" 8 | } 9 | 10 | module.exports = fontFamilies; -------------------------------------------------------------------------------- /testdata/tokens/fontFamiliesPostscript.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontFamilies = { 4 | "bold": "HelveticaNeue-Bold", 5 | "medium": "HelveticaNeue-Medium", 6 | "regular": "HelveticaNeue", 7 | "light": "HelveticaNeue-Light" 8 | } 9 | 10 | export default fontFamilies; -------------------------------------------------------------------------------- /testdata/tokens/fontFamiliesPostscript.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontFamilies = { 4 | "bold": "HelveticaNeue-Bold", 5 | "medium": "HelveticaNeue-Medium", 6 | "regular": "HelveticaNeue", 7 | "light": "HelveticaNeue-Light" 8 | } 9 | 10 | export default fontFamilies; -------------------------------------------------------------------------------- /testdata/tokens/fontSizes.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontSizes = { 4 | "sub": "0.75rem", 5 | "paragraph": "1rem", 6 | "h6": "1.125rem", 7 | "h5": "1.25rem", 8 | "h4": "1.625rem", 9 | "h3": "2rem", 10 | "h2": "2.5rem", 11 | "h1": "3rem" 12 | } 13 | 14 | module.exports = fontSizes; -------------------------------------------------------------------------------- /testdata/tokens/fontSizes.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontSizes = { 4 | "sub": "0.75rem", 5 | "paragraph": "1rem", 6 | "h6": "1.125rem", 7 | "h5": "1.25rem", 8 | "h4": "1.625rem", 9 | "h3": "2rem", 10 | "h2": "2.5rem", 11 | "h1": "3rem" 12 | } 13 | 14 | export default fontSizes; -------------------------------------------------------------------------------- /testdata/tokens/fontSizes.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontSizes = { 4 | "sub": "0.75rem", 5 | "paragraph": "1rem", 6 | "h6": "1.125rem", 7 | "h5": "1.25rem", 8 | "h4": "1.625rem", 9 | "h3": "2rem", 10 | "h2": "2.5rem", 11 | "h1": "3rem" 12 | } 13 | 14 | export default fontSizes; -------------------------------------------------------------------------------- /testdata/tokens/fontWeights.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontWeights = { 4 | "bold": 700, 5 | "medium": 500, 6 | "regular": 400, 7 | "light": 300 8 | } 9 | 10 | module.exports = fontWeights; -------------------------------------------------------------------------------- /testdata/tokens/fontWeights.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontWeights = { 4 | "bold": 700, 5 | "medium": 500, 6 | "regular": 400, 7 | "light": 300 8 | } 9 | 10 | export default fontWeights; -------------------------------------------------------------------------------- /testdata/tokens/fontWeights.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const fontWeights = { 4 | "bold": 700, 5 | "medium": 500, 6 | "regular": 400, 7 | "light": 300 8 | } 9 | 10 | export default fontWeights; -------------------------------------------------------------------------------- /testdata/tokens/letterSpacings.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const letterSpacings = { 4 | "tight": "-0.045em", 5 | "wide": "0.05em", 6 | "regular": "0em" 7 | } 8 | 9 | module.exports = letterSpacings; -------------------------------------------------------------------------------- /testdata/tokens/letterSpacings.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const letterSpacings = { 4 | "tight": "-0.045em", 5 | "wide": "0.05em", 6 | "regular": "0em" 7 | } 8 | 9 | export default letterSpacings; -------------------------------------------------------------------------------- /testdata/tokens/letterSpacings.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const letterSpacings = { 4 | "tight": "-0.045em", 5 | "wide": "0.05em", 6 | "regular": "0em" 7 | } 8 | 9 | export default letterSpacings; -------------------------------------------------------------------------------- /testdata/tokens/lineHeights.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const lineHeights = { 4 | "xs": "1.00", 5 | "s": "1.35", 6 | "m": "1.45", 7 | "l": "1.65" 8 | } 9 | 10 | module.exports = lineHeights; -------------------------------------------------------------------------------- /testdata/tokens/lineHeights.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const lineHeights = { 4 | "xs": "1.00", 5 | "s": "1.35", 6 | "m": "1.45", 7 | "l": "1.65" 8 | } 9 | 10 | export default lineHeights; -------------------------------------------------------------------------------- /testdata/tokens/lineHeights.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const lineHeights = { 4 | "xs": "1.00", 5 | "s": "1.35", 6 | "m": "1.45", 7 | "l": "1.65" 8 | } 9 | 10 | export default lineHeights; -------------------------------------------------------------------------------- /testdata/tokens/mediaQueries.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const mediaQueries = { 4 | "mobileSm": "320px", 5 | "mobileMd": "480px", 6 | "mobileLg": "580px", 7 | "mobileMax": "767px", 8 | "tabletMin": "768px", 9 | "tabletMax": "1024px", 10 | "desktopMd": "1180px", 11 | "desktopLg": "1440px", 12 | "wide": "1920px" 13 | } 14 | 15 | module.exports = mediaQueries; -------------------------------------------------------------------------------- /testdata/tokens/mediaQueries.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const mediaQueries = { 4 | "mobileSm": "320px", 5 | "mobileMd": "480px", 6 | "mobileLg": "580px", 7 | "mobileMax": "767px", 8 | "tabletMin": "768px", 9 | "tabletMax": "1024px", 10 | "desktopMd": "1180px", 11 | "desktopLg": "1440px", 12 | "wide": "1920px" 13 | } 14 | 15 | export default mediaQueries; -------------------------------------------------------------------------------- /testdata/tokens/mediaQueries.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const mediaQueries = { 4 | "mobileSm": "320px", 5 | "mobileMd": "480px", 6 | "mobileLg": "580px", 7 | "mobileMax": "767px", 8 | "tabletMin": "768px", 9 | "tabletMax": "1024px", 10 | "desktopMd": "1180px", 11 | "desktopLg": "1440px", 12 | "wide": "1920px" 13 | } 14 | 15 | export default mediaQueries; -------------------------------------------------------------------------------- /testdata/tokens/opacities.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const opacities = { 4 | "opaque": 1, 5 | "disabled": 0.65, 6 | "semiOpaque": 0.5, 7 | "transparent": 0 8 | } 9 | 10 | module.exports = opacities; -------------------------------------------------------------------------------- /testdata/tokens/opacities.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const opacities = { 4 | "opaque": 1, 5 | "disabled": 0.65, 6 | "semiOpaque": 0.5, 7 | "transparent": 0 8 | } 9 | 10 | export default opacities; -------------------------------------------------------------------------------- /testdata/tokens/opacities.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const opacities = { 4 | "opaque": 1, 5 | "disabled": 0.65, 6 | "semiOpaque": 0.5, 7 | "transparent": 0 8 | } 9 | 10 | export default opacities; -------------------------------------------------------------------------------- /testdata/tokens/radii.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const radii = { 4 | "circle": "100px", 5 | "soft": "8px", 6 | "rounded": "4px", 7 | "hard": "0px" 8 | } 9 | 10 | module.exports = radii; -------------------------------------------------------------------------------- /testdata/tokens/radii.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const radii = { 4 | "circle": "100px", 5 | "soft": "8px", 6 | "rounded": "4px", 7 | "hard": "0px" 8 | } 9 | 10 | export default radii; -------------------------------------------------------------------------------- /testdata/tokens/radii.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const radii = { 4 | "circle": "100px", 5 | "soft": "8px", 6 | "rounded": "4px", 7 | "hard": "0px" 8 | } 9 | 10 | export default radii; -------------------------------------------------------------------------------- /testdata/tokens/shadows.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const shadows = { 4 | "deepMulti": "0px 4px 20px rgba(0, 0, 0, 0.1), 0px 4px 4px rgba(0, 0, 0, 0.17)", 5 | "deep": "3px 3px 3px rgba(196, 196, 196, 0.75)", 6 | "medium": "0px 0px 5px rgba(0, 0, 0, 0.5)", 7 | "soft": "0px 0px 5px rgba(196, 196, 196, 1)" 8 | } 9 | 10 | module.exports = shadows; -------------------------------------------------------------------------------- /testdata/tokens/shadows.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const shadows = { 4 | "deepMulti": "0px 4px 20px rgba(0, 0, 0, 0.1), 0px 4px 4px rgba(0, 0, 0, 0.17)", 5 | "deep": "3px 3px 3px rgba(196, 196, 196, 0.75)", 6 | "medium": "0px 0px 5px rgba(0, 0, 0, 0.5)", 7 | "soft": "0px 0px 5px rgba(196, 196, 196, 1)" 8 | } 9 | 10 | export default shadows; -------------------------------------------------------------------------------- /testdata/tokens/shadows.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const shadows = { 4 | "deepMulti": "0px 4px 20px rgba(0, 0, 0, 0.1), 0px 4px 4px rgba(0, 0, 0, 0.17)", 5 | "deep": "3px 3px 3px rgba(196, 196, 196, 0.75)", 6 | "medium": "0px 0px 5px rgba(0, 0, 0, 0.5)", 7 | "soft": "0px 0px 5px rgba(196, 196, 196, 1)" 8 | } 9 | 10 | export default shadows; -------------------------------------------------------------------------------- /testdata/tokens/spacing.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const spacing = { 4 | "huge": "6rem", 5 | "large": "4rem", 6 | "big": "3rem", 7 | "medium": "2rem", 8 | "small": "1rem", 9 | "tiny": "0.5rem" 10 | } 11 | 12 | module.exports = spacing; -------------------------------------------------------------------------------- /testdata/tokens/spacing.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const spacing = { 4 | "huge": "6rem", 5 | "large": "4rem", 6 | "big": "3rem", 7 | "medium": "2rem", 8 | "small": "1rem", 9 | "tiny": "0.5rem" 10 | } 11 | 12 | export default spacing; -------------------------------------------------------------------------------- /testdata/tokens/spacing.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const spacing = { 4 | "huge": "6rem", 5 | "large": "4rem", 6 | "big": "3rem", 7 | "medium": "2rem", 8 | "small": "1rem", 9 | "tiny": "0.5rem" 10 | } 11 | 12 | export default spacing; -------------------------------------------------------------------------------- /testdata/tokens/zIndices.js: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const zIndices = { 4 | "top": 100, 5 | "focus": 10, 6 | "higher": 2, 7 | "high": 1, 8 | "regular": 0 9 | } 10 | 11 | module.exports = zIndices; -------------------------------------------------------------------------------- /testdata/tokens/zIndices.mjs: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const zIndices = { 4 | "top": 100, 5 | "focus": 10, 6 | "higher": 2, 7 | "high": 1, 8 | "regular": 0 9 | } 10 | 11 | export default zIndices; -------------------------------------------------------------------------------- /testdata/tokens/zIndices.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTO-GENERATED BY FIGMAGIC. DO NOT MAKE EDITS IN THIS FILE! CHANGES WILL GET OVER-WRITTEN BY ANY FURTHER PROCESSING. 2 | 3 | const zIndices = { 4 | "top": 100, 5 | "focus": 10, 6 | "higher": 2, 7 | "high": 1, 8 | "regular": 0 9 | } 10 | 11 | export default zIndices; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "allowSyntheticDefaultImports": true, 5 | "moduleResolution": "node", 6 | "outDir": "build", 7 | "target": "ES2022", 8 | "lib": ["ES2022"], 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "importHelpers": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmitOnError": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "removeComments": true, 18 | "resolveJsonModule": true, 19 | "sourceMap": true, 20 | "strict": true, 21 | "strictFunctionTypes": true, 22 | "skipLibCheck": true 23 | }, 24 | "include": ["./bin", "./index.ts"], 25 | "exclude": ["./templates/*", "./build"] 26 | } 27 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./bin"], 3 | "out": "typedoc-docs", 4 | "sidebarLinks": { 5 | "Figmagic on GitHub": "https://github.com/mikaelvesavuori/figmagic/" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /typedoc.theme.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 3 | } 4 | 5 | :root { 6 | --color-background: #06070B; 7 | --code-background: #09213E; 8 | } 9 | 10 | code, code > * { 11 | font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; 12 | font-weight: normal; 13 | } 14 | 15 | @media (min-width: 770px) { 16 | .col-content { 17 | padding: 0 4rem; 18 | } 19 | } 20 | 21 | .site-menu, .page-menu { 22 | font-size: 0.75rem; 23 | } 24 | 25 | .tsd-panel > h2 { 26 | margin-top: 4rem; 27 | } 28 | 29 | pre > code > span.hl-0 { 30 | color: #FCEE8F; 31 | } 32 | 33 | pre > code > span.hl-2 { 34 | color: #FFFFFF; 35 | } 36 | 37 | pre > code > span.hl-3 { 38 | color: #B4FDFE; 39 | } 40 | 41 | pre > code > span.hl-4 { 42 | color: #EF87DC; 43 | } 44 | --------------------------------------------------------------------------------