From f976052a9db9bf3a0e6ee8b19d32bff22223d460 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 3 Dec 2024 00:29:03 +0300 Subject: [PATCH 01/48] feat: add base fragments-cli --- .gitignore | 4 + .pnp.cjs | 146 +++++++++++++++++++++++---- fragments/fragments-cli/package.json | 42 ++++++++ fragments/fragments-cli/src/index.ts | 45 +++++++++ fragments/fragments-cli/src/run.ts | 25 +++++ package.json | 3 +- tsconfig.json | 2 +- yarn.lock | 92 +++++++++++++++-- 8 files changed, 324 insertions(+), 35 deletions(-) create mode 100644 fragments/fragments-cli/package.json create mode 100644 fragments/fragments-cli/src/index.ts create mode 100644 fragments/fragments-cli/src/run.ts diff --git a/.gitignore b/.gitignore index e8ddd1a..7089ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,7 @@ package.tgz # Generated files generated/ + +# VS Code +.yarn/sdks +.vscode diff --git a/.pnp.cjs b/.pnp.cjs index 08c628c..1a3a144 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -21,6 +21,14 @@ const RAW_RUNTIME_STATE = "name": "@atls/figma-assets-cli",\ "reference": "workspace:assets/assets-cli"\ },\ + {\ + "name": "@atls/figma-fragments-cli",\ + "reference": "workspace:fragments/fragments-cli"\ + },\ + {\ + "name": "@atls/figma-fragments-generator",\ + "reference": "workspace:fragments/fragments-generator"\ + },\ {\ "name": "@atls/figma-file-loader",\ "reference": "workspace:loaders/file-loader"\ @@ -80,6 +88,8 @@ const RAW_RUNTIME_STATE = ["@atls/figma-assets", ["virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#workspace:assets/assets", "workspace:assets/assets"]],\ ["@atls/figma-assets-cli", ["workspace:assets/assets-cli"]],\ ["@atls/figma-file-loader", ["virtual:75728d86037c75604505b9c0fbfc0ce3edc9d369e1826ac0d2d661dfb48b9446ca5a5e54a2ca8ec969b4beb532afca4cf558bf306737b461fca84524ac2142e6#workspace:loaders/file-loader", "workspace:loaders/file-loader"]],\ + ["@atls/figma-fragments-cli", ["workspace:fragments/fragments-cli"]],\ + ["@atls/figma-fragments-generator", ["virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator", "workspace:fragments/fragments-generator"]],\ ["@atls/figma-theme", ["virtual:a9526061832803f8bc7fed186e9699b3bcb0fb7fb989d17328e56b61e9f17fd49895330e461073d51dd5dcbab09bf72a55678f1e398bacacdb001a56e84fd54f#workspace:theme/theme", "workspace:theme/theme"]],\ ["@atls/figma-theme-borders-generator", ["virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-borders-generator", "workspace:theme/theme-borders-generator"]],\ ["@atls/figma-theme-cli", ["workspace:theme/theme-cli"]],\ @@ -91,7 +101,7 @@ const RAW_RUNTIME_STATE = ["@atls/figma-theme-line-heights-generator", ["virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-line-heights-generator", "workspace:theme/theme-line-heights-generator"]],\ ["@atls/figma-theme-radii-generator", ["virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-radii-generator", "workspace:theme/theme-radii-generator"]],\ ["@atls/figma-theme-shadows-generator", ["virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-shadows-generator", "workspace:theme/theme-shadows-generator"]],\ - ["@atls/figma-utils", ["virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils", "workspace:utils/utils"]],\ + ["@atls/figma-utils", ["virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils", "workspace:utils/utils"]],\ ["figma", ["workspace:."]]\ ],\ "fallbackPool": [\ @@ -412,6 +422,61 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@atls/figma-fragments-cli", [\ + ["workspace:fragments/fragments-cli", {\ + "packageLocation": "./fragments/fragments-cli/",\ + "packageDependencies": [\ + ["@atls/figma-fragments-cli", "workspace:fragments/fragments-cli"],\ + ["@atls/figma-file-loader", "virtual:75728d86037c75604505b9c0fbfc0ce3edc9d369e1826ac0d2d661dfb48b9446ca5a5e54a2ca8ec969b4beb532afca4cf558bf306737b461fca84524ac2142e6#workspace:loaders/file-loader"],\ + ["@atls/figma-fragments-generator", "virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator"],\ + ["@swc-node/register", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.9.0"],\ + ["@swc/core", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.6.1"],\ + ["@types/node", "npm:18.19.34"],\ + ["@types/npmlog", "npm:7.0.0"],\ + ["@yarnpkg/builder", "npm:4.1.1"],\ + ["commander", "npm:12.1.0"],\ + ["figma-js", "npm:1.16.1-0"],\ + ["npmlog", "npm:7.0.1"],\ + ["prettier", "npm:2.8.8"],\ + ["ts-node", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:10.9.2"],\ + ["typescript", "patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ + ["@atls/figma-fragments-generator", [\ + ["virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator", {\ + "packageLocation": "./.yarn/__virtual__/@atls-figma-fragments-generator-virtual-49afb7a318/1/fragments/fragments-generator/",\ + "packageDependencies": [\ + ["@atls/figma-fragments-generator", "virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ + ["@types/figma-js", null],\ + ["@types/node", "npm:18.19.34"],\ + ["@types/react", "npm:18.3.12"],\ + ["figma-js", "npm:1.16.1-0"],\ + ["pretty-format", "npm:29.7.0"],\ + ["react", "npm:18.3.1"]\ + ],\ + "packagePeers": [\ + "@types/figma-js",\ + "figma-js"\ + ],\ + "linkType": "SOFT"\ + }],\ + ["workspace:fragments/fragments-generator", {\ + "packageLocation": "./fragments/fragments-generator/",\ + "packageDependencies": [\ + ["@atls/figma-fragments-generator", "workspace:fragments/fragments-generator"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ + ["@types/node", "npm:18.19.34"],\ + ["@types/react", "npm:18.3.12"],\ + ["figma-js", "npm:1.16.1-0"],\ + ["pretty-format", "npm:29.7.0"],\ + ["react", "npm:18.3.1"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@atls/figma-theme", [\ ["virtual:a9526061832803f8bc7fed186e9699b3bcb0fb7fb989d17328e56b61e9f17fd49895330e461073d51dd5dcbab09bf72a55678f1e398bacacdb001a56e84fd54f#workspace:theme/theme", {\ "packageLocation": "./.yarn/__virtual__/@atls-figma-theme-virtual-11c101ca5a/1/theme/theme/",\ @@ -425,7 +490,7 @@ const RAW_RUNTIME_STATE = ["@atls/figma-theme-line-heights-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-line-heights-generator"],\ ["@atls/figma-theme-radii-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-radii-generator"],\ ["@atls/figma-theme-shadows-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-shadows-generator"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"],\ @@ -449,7 +514,7 @@ const RAW_RUNTIME_STATE = ["@atls/figma-theme-line-heights-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-line-heights-generator"],\ ["@atls/figma-theme-radii-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-radii-generator"],\ ["@atls/figma-theme-shadows-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-shadows-generator"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"],\ ["prettier", "npm:2.8.8"]\ @@ -463,7 +528,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-borders-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-borders-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -481,7 +546,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-borders-generator", "workspace:theme/theme-borders-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -515,7 +580,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-colors-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-colors-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/color-namer", "npm:1.3.3"],\ ["@types/figma-js", null],\ @@ -536,7 +601,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-colors-generator", "workspace:theme/theme-colors-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/color-namer", "npm:1.3.3"],\ ["@types/node", "npm:18.19.34"],\ ["camelcase", "npm:8.0.0"],\ @@ -552,7 +617,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-font-sizes-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-font-sizes-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -570,7 +635,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-font-sizes-generator", "workspace:theme/theme-font-sizes-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -583,7 +648,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-font-weights-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-font-weights-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -601,7 +666,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-font-weights-generator", "workspace:theme/theme-font-weights-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -614,7 +679,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-fonts-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-fonts-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -632,7 +697,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-fonts-generator", "workspace:theme/theme-fonts-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -670,7 +735,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-line-heights-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-line-heights-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -688,7 +753,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-line-heights-generator", "workspace:theme/theme-line-heights-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -701,7 +766,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-radii-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-radii-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -719,7 +784,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-radii-generator", "workspace:theme/theme-radii-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -732,7 +797,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-shadows-generator", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:theme/theme-shadows-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/atls__figma-theme-generator-common", null],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -750,7 +815,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@atls/figma-theme-shadows-generator", "workspace:theme/theme-shadows-generator"],\ ["@atls/figma-theme-generator-common", "virtual:db0b9474357124b458aead7660f3a49f6e68621d02df2ccdad2b83d64a332b0b44c8f97688bcec423e1cb6b72ab57bc7f9b2a6b875566d3fdc89601e350ffd56#workspace:theme/theme-generator-common"],\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/node", "npm:18.19.34"],\ ["figma-js", "npm:1.16.1-0"]\ ],\ @@ -758,10 +823,10 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@atls/figma-utils", [\ - ["virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils", {\ - "packageLocation": "./.yarn/__virtual__/@atls-figma-utils-virtual-3ffffdc578/1/utils/utils/",\ + ["virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils", {\ + "packageLocation": "./.yarn/__virtual__/@atls-figma-utils-virtual-d68f039556/1/utils/utils/",\ "packageDependencies": [\ - ["@atls/figma-utils", "virtual:110ecadd7cd0118d6c1cb51237279043a3b537909cb38e8afea9755c0b417513a97d716eff6d67a1e509fa203eaf7fce9b9165e9bc1d1314b1a7eb794e684bf7#workspace:utils/utils"],\ + ["@atls/figma-utils", "virtual:49afb7a3182fe506e5c9913de9cbbf24653279ee03f4c7d3ebc7e46dd08314f013bccd17b37e21d7a9fd1c8d0e758f0cc00a7882d8bcbeb96169e63d8ecae001#workspace:utils/utils"],\ ["@types/color-namer", "npm:1.3.3"],\ ["@types/figma-js", null],\ ["@types/node", "npm:18.19.34"],\ @@ -3225,6 +3290,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/prop-types", [\ + ["npm:15.7.13", {\ + "packageLocation": "../.yarn/berry/cache/@types-prop-types-npm-15.7.13-ac81cbe352-10.zip/node_modules/@types/prop-types/",\ + "packageDependencies": [\ + ["@types/prop-types", "npm:15.7.13"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@types/react", [\ + ["npm:18.3.12", {\ + "packageLocation": "../.yarn/berry/cache/@types-react-npm-18.3.12-69c5fbaab9-10.zip/node_modules/@types/react/",\ + "packageDependencies": [\ + ["@types/react", "npm:18.3.12"],\ + ["@types/prop-types", "npm:15.7.13"],\ + ["csstype", "npm:3.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/responselike", [\ ["npm:1.0.2", {\ "packageLocation": "../.yarn/berry/cache/@types-responselike-npm-1.0.2-85e41dffe9-10.zip/node_modules/@types/responselike/",\ @@ -6589,6 +6674,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["csstype", [\ + ["npm:3.1.3", {\ + "packageLocation": "../.yarn/berry/cache/csstype-npm-3.1.3-e9a1c85013-10.zip/node_modules/csstype/",\ + "packageDependencies": [\ + ["csstype", "npm:3.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["d", [\ ["npm:1.0.1", {\ "packageLocation": "../.yarn/berry/cache/d-npm-1.0.1-64afbbc689-10.zip/node_modules/d/",\ @@ -11039,6 +11133,14 @@ const RAW_RUNTIME_STATE = ["prop-types", "npm:15.8.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:18.3.1", {\ + "packageLocation": "../.yarn/berry/cache/react-npm-18.3.1-af38f3c1ae-10.zip/node_modules/react/",\ + "packageDependencies": [\ + ["react", "npm:18.3.1"],\ + ["loose-envify", "npm:1.4.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["react-devtools-core", [\ diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json new file mode 100644 index 0000000..33c5675 --- /dev/null +++ b/fragments/fragments-cli/package.json @@ -0,0 +1,42 @@ +{ + "name": "@atls/figma-fragments-cli", + "version": "0.0.1", + "license": "BSD-3-Clause", + "type": "module", + "main": "src/index.ts", + "bin": { + "generate-fragments": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "yarn library build", + "generate-fragments": "ts-node-esm src/index.ts", + "generate-fragments-new": "node --import @swc-node/register/esm-register src/index.ts", + "prepack": "yarn run build", + "postpack": "rm -rf dist" + }, + "dependencies": { + "@atls/figma-file-loader": "workspace:*", + "@atls/figma-fragments-generator": "workspace:*", + "commander": "12.1.0", + "figma-js": "1.16.1-0", + "npmlog": "7.0.1", + "prettier": "2.8.8" + }, + "devDependencies": { + "@swc-node/register": "1.9.0", + "@swc/core": "1.6.1", + "@types/node": "18.19.34", + "@types/npmlog": "7.0.0", + "@yarnpkg/builder": "4.1.1", + "ts-node": "10.9.2", + "typescript": "5.2.2" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } +} diff --git a/fragments/fragments-cli/src/index.ts b/fragments/fragments-cli/src/index.ts new file mode 100644 index 0000000..f95d0c0 --- /dev/null +++ b/fragments/fragments-cli/src/index.ts @@ -0,0 +1,45 @@ +import { createInterface } from 'node:readline' + +import logger from 'npmlog' +import { program } from 'commander' + +import { run } from './run.js' + +logger.heading = 'figma-fragments' + +program + .option('-o, --output [output]', 'Output dir') + .option('-v, --verbose', 'Verbose output') + .option('-n, --node-id ', 'Node id for generating') + .arguments('') + .parse(process.argv) + +const fileId = program.args.at(0) +const options = program.opts() + +if (options.verbose) { + logger.level = 'verbose' +} + +if (!fileId) { + logger.error('fileId', 'Figma file id required.') +} else { + // const readline = createInterface({ + // input: process.stdin, + // output: process.stdout, + // }) + + // readline.question(`Enter your Figma access token:\n`, (id) => { + // if (!id || id === '') throw Error('ID must not be empty') + // // eslint-disable-next-line dot-notation + + // readline.close() + + // }) + + process.env['FIGMA_TOKEN'] = 'secret' + + run(fileId, options.nodeId?.replace('-', ':'), options.output) + .then(() => logger.info('info', 'Fragments successful generated')) + .catch((error) => logger.error('error', error.message)) +} diff --git a/fragments/fragments-cli/src/run.ts b/fragments/fragments-cli/src/run.ts new file mode 100644 index 0000000..1376335 --- /dev/null +++ b/fragments/fragments-cli/src/run.ts @@ -0,0 +1,25 @@ +import path from 'path' +import prettier from 'prettier' +import { promises as fs } from 'fs' + +import { FigmaFileLoader } from '@atls/figma-file-loader' +import { FigmaThemeFragmentsGenerator } from '@atls/figma-fragments-generator' + +export const run = async (fileId, nodeId, output) => { + const loader = new FigmaFileLoader() + const generator = new FigmaThemeFragmentsGenerator() + + const response = await loader.loadNode(fileId, nodeId) + + console.log(response.nodes[nodeId]?.document) + + const component = generator.generate(response) + + const target = path.join(output, 'fragments.tsx') + + const options = await prettier.resolveConfig(target) + + const data = await prettier.format(component, { ...options }) + + await fs.writeFile(target, data) +} diff --git a/package.json b/package.json index 776f5c1..dd8b733 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "private": true, "license": "BSD-3-Clause", "workspaces": [ - "loaders/*", "assets/*", + "fragments/*", + "loaders/*", "theme/*", "utils/*" ], diff --git a/tsconfig.json b/tsconfig.json index 6a491ad..4ce8e9f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,7 +47,7 @@ "integration/**/*.test.ts", "theme/theme-cli/theme" ], - "include": ["theme/**/*", "loaders/**/*", "assets/**/*", "utils/**/*"], + "include": ["assets/**/*", "fragments/**/*", "loaders/**/*", "theme/**/*", "utils/**/*"], "ts-node": { "esm": true } diff --git a/yarn.lock b/yarn.lock index 79b6d6f..8c6abc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -246,6 +246,43 @@ __metadata: languageName: unknown linkType: soft +"@atls/figma-fragments-cli@workspace:fragments/fragments-cli": + version: 0.0.0-use.local + resolution: "@atls/figma-fragments-cli@workspace:fragments/fragments-cli" + dependencies: + "@atls/figma-file-loader": "workspace:*" + "@atls/figma-fragments-generator": "workspace:*" + "@swc-node/register": "npm:1.9.0" + "@swc/core": "npm:1.6.1" + "@types/node": "npm:18.19.34" + "@types/npmlog": "npm:7.0.0" + "@yarnpkg/builder": "npm:4.1.1" + commander: "npm:12.1.0" + figma-js: "npm:1.16.1-0" + npmlog: "npm:7.0.1" + prettier: "npm:2.8.8" + ts-node: "npm:10.9.2" + typescript: "npm:5.2.2" + bin: + generate-fragments: dist/index.js + languageName: unknown + linkType: soft + +"@atls/figma-fragments-generator@workspace:*, @atls/figma-fragments-generator@workspace:fragments/fragments-generator": + version: 0.0.0-use.local + resolution: "@atls/figma-fragments-generator@workspace:fragments/fragments-generator" + dependencies: + "@atls/figma-utils": "workspace:*" + "@types/node": "npm:18.19.34" + "@types/react": "npm:18.3.12" + figma-js: "npm:1.16.1-0" + pretty-format: "npm:29.7.0" + react: "npm:18.3.1" + peerDependencies: + figma-js: "*" + languageName: unknown + linkType: soft + "@atls/figma-theme-borders-generator@workspace:*, @atls/figma-theme-borders-generator@workspace:theme/theme-borders-generator": version: 0.0.0-use.local resolution: "@atls/figma-theme-borders-generator@workspace:theme/theme-borders-generator" @@ -2318,6 +2355,23 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.13 + resolution: "@types/prop-types@npm:15.7.13" + checksum: 10/8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c + languageName: node + linkType: hard + +"@types/react@npm:18.3.12": + version: 18.3.12 + resolution: "@types/react@npm:18.3.12" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.0.2" + checksum: 10/c9bbdfeacd5347d2240e0d2cb5336bc57dbc1b9ff557b6c4024b49df83419e4955553518169d3736039f1b62608e15b35762a6c03d49bd86e33add4b43b19033 + languageName: node + linkType: hard + "@types/responselike@npm:^1.0.0": version: 1.0.2 resolution: "@types/responselike@npm:1.0.2" @@ -4408,6 +4462,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.0.2": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 10/f593cce41ff5ade23f44e77521e3a1bcc2c64107041e1bf6c3c32adc5187d0d60983292fda326154d20b01079e24931aa5b08e4467cc488b60bb1e7f6d478ade + languageName: node + linkType: hard + "d@npm:1, d@npm:^1.0.1": version: 1.0.1 resolution: "d@npm:1.0.1" @@ -8124,26 +8185,26 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^28.1.3": - version: 28.1.3 - resolution: "pretty-format@npm:28.1.3" +"pretty-format@npm:29.7.0, pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": + version: 29.7.0 + resolution: "pretty-format@npm:29.7.0" dependencies: - "@jest/schemas": "npm:^28.1.3" - ansi-regex: "npm:^5.0.1" + "@jest/schemas": "npm:^29.6.3" ansi-styles: "npm:^5.0.0" react-is: "npm:^18.0.0" - checksum: 10/26626d33e201388174a1ce352be46b8087f28184bf3684a88b2f7cf633e28419ffc664628eec261ba13b0f03748c3a6f85db063a2022f75a354c7b9e4e06526b + checksum: 10/dea96bc83c83cd91b2bfc55757b6b2747edcaac45b568e46de29deee80742f17bc76fe8898135a70d904f4928eafd8bb693cd1da4896e8bdd3c5e82cadf1d2bb languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": - version: 29.7.0 - resolution: "pretty-format@npm:29.7.0" +"pretty-format@npm:^28.1.3": + version: 28.1.3 + resolution: "pretty-format@npm:28.1.3" dependencies: - "@jest/schemas": "npm:^29.6.3" + "@jest/schemas": "npm:^28.1.3" + ansi-regex: "npm:^5.0.1" ansi-styles: "npm:^5.0.0" react-is: "npm:^18.0.0" - checksum: 10/dea96bc83c83cd91b2bfc55757b6b2747edcaac45b568e46de29deee80742f17bc76fe8898135a70d904f4928eafd8bb693cd1da4896e8bdd3c5e82cadf1d2bb + checksum: 10/26626d33e201388174a1ce352be46b8087f28184bf3684a88b2f7cf633e28419ffc664628eec261ba13b0f03748c3a6f85db063a2022f75a354c7b9e4e06526b languageName: node linkType: hard @@ -8333,6 +8394,15 @@ __metadata: languageName: node linkType: hard +"react@npm:18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf + languageName: node + linkType: hard + "react@npm:^16.13.1": version: 16.14.0 resolution: "react@npm:16.14.0" From 87a57b758f905992e706ce74349de00d06fa37b2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 3 Dec 2024 00:29:11 +0300 Subject: [PATCH 02/48] feat: add base fragments-generator --- fragments/fragments-generator/package.json | 33 +++++++++++++++++++ .../src/figma-fragments.generator.ts | 33 +++++++++++++++++++ fragments/fragments-generator/src/index.ts | 1 + .../fragments-generator/src/strategy/index.ts | 1 + .../src/strategy/simple-mapping.strategy.ts | 24 ++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 fragments/fragments-generator/package.json create mode 100644 fragments/fragments-generator/src/figma-fragments.generator.ts create mode 100644 fragments/fragments-generator/src/index.ts create mode 100644 fragments/fragments-generator/src/strategy/index.ts create mode 100644 fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts diff --git a/fragments/fragments-generator/package.json b/fragments/fragments-generator/package.json new file mode 100644 index 0000000..8e0a6ad --- /dev/null +++ b/fragments/fragments-generator/package.json @@ -0,0 +1,33 @@ +{ + "name": "@atls/figma-fragments-generator", + "version": "0.0.1", + "license": "BSD-3-Clause", + "type": "module", + "main": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn library build", + "prepack": "yarn run build", + "postpack": "rm -rf dist" + }, + "dependencies": { + "@atls/figma-utils": "workspace:*", + "pretty-format": "29.7.0", + "react": "18.3.1" + }, + "devDependencies": { + "@types/node": "18.19.34", + "@types/react": "18.3.12", + "figma-js": "1.16.1-0" + }, + "peerDependencies": { + "figma-js": "*" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } +} diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts new file mode 100644 index 0000000..45d2c0c --- /dev/null +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -0,0 +1,33 @@ +import { FileNodesResponse } from 'figma-js' +import { Text } from 'figma-js' + +import { isText } from '@atls/figma-utils' +import { walk } from '@atls/figma-utils' + +import { SimpleMappingStrategy } from './strategy/index.js' + +export class FigmaThemeFragmentsGenerator { + readonly name = 'fragments' + + getTextNodes(nodes): Text[] { + const textNodes: Text[] = [] + + walk(nodes, (node) => { + if (isText(node)) { + textNodes.push(node) + } + }) + + return textNodes + } + + generate(response: FileNodesResponse): string { + const strategy = new SimpleMappingStrategy() + + const textNodes = this.getTextNodes(response.nodes) + + const component = strategy.execute(textNodes) + + return component + } +} diff --git a/fragments/fragments-generator/src/index.ts b/fragments/fragments-generator/src/index.ts new file mode 100644 index 0000000..766894a --- /dev/null +++ b/fragments/fragments-generator/src/index.ts @@ -0,0 +1 @@ +export * from './figma-fragments.generator.js' diff --git a/fragments/fragments-generator/src/strategy/index.ts b/fragments/fragments-generator/src/strategy/index.ts new file mode 100644 index 0000000..809d935 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/index.ts @@ -0,0 +1 @@ +export * from './simple-mapping.strategy.js' diff --git a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts new file mode 100644 index 0000000..95c7ebb --- /dev/null +++ b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts @@ -0,0 +1,24 @@ +import { Text } from 'figma-js' +import { plugins } from 'pretty-format' +import { format } from 'pretty-format' +import { createElement } from 'react' + +export class SimpleMappingStrategy { + execute(textNodes: Text[] = []) { + const { name, style } = textNodes[0] + + const element = createElement( + 'Text', + { + fontSize: style.fontSize, + fontWeight: style.fontWeight, + }, + name + ) + + return format(element, { + plugins: [plugins.ReactElement], + printFunctionName: false, + }) + } +} From a7124f6fcaab397d3e429210609953a8ead9fbe8 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 3 Dec 2024 00:29:26 +0300 Subject: [PATCH 03/48] feat: add load-node method to figma-file-loader --- loaders/file-loader/src/FigmaFileLoader.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/loaders/file-loader/src/FigmaFileLoader.ts b/loaders/file-loader/src/FigmaFileLoader.ts index 46f969a..707b718 100644 --- a/loaders/file-loader/src/FigmaFileLoader.ts +++ b/loaders/file-loader/src/FigmaFileLoader.ts @@ -1,7 +1,8 @@ -import { Client } from 'figma-js' -import { ClientInterface } from 'figma-js' -import { FileResponse } from 'figma-js' -import { Node } from 'figma-js' +import { Client } from 'figma-js' +import { ClientInterface } from 'figma-js' +import { FileResponse } from 'figma-js' +import { FileNodesResponse } from 'figma-js' +import { Node } from 'figma-js' export class FigmaFileLoader { figma: ClientInterface @@ -17,6 +18,11 @@ export class FigmaFileLoader { return data } + async loadNode(fileId: string, nodeId: string): Promise { + const { data } = await this.figma.fileNodes(fileId, { ids: [nodeId] }) + return data + } + async loadDocument(fileId: string, documentId: string): Promise { const file = await this.load(fileId) From df80c7de3004b23b088dad2b11943b1dda4bbe67 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 4 Dec 2024 11:04:54 +0300 Subject: [PATCH 04/48] feat: add theme option to generate fragments command --- fragments/fragments-cli/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fragments/fragments-cli/src/index.ts b/fragments/fragments-cli/src/index.ts index f95d0c0..564d89a 100644 --- a/fragments/fragments-cli/src/index.ts +++ b/fragments/fragments-cli/src/index.ts @@ -9,6 +9,7 @@ logger.heading = 'figma-fragments' program .option('-o, --output [output]', 'Output dir') + .option('-t, --theme ', 'Path to theme file') .option('-v, --verbose', 'Verbose output') .option('-n, --node-id ', 'Node id for generating') .arguments('') @@ -39,7 +40,7 @@ if (!fileId) { process.env['FIGMA_TOKEN'] = 'secret' - run(fileId, options.nodeId?.replace('-', ':'), options.output) + run(fileId, options.nodeId?.replace('-', ':'), options.output, options.theme) .then(() => logger.info('info', 'Fragments successful generated')) .catch((error) => logger.error('error', error.message)) } From fb2d43da26713a09446d71b36fe9e35d9b0cd27c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 4 Dec 2024 11:08:15 +0300 Subject: [PATCH 05/48] feat: add process-file theme path --- fragments/fragments-cli/package.json | 3 +++ fragments/fragments-cli/src/run.ts | 38 +++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json index 33c5675..d181b47 100644 --- a/fragments/fragments-cli/package.json +++ b/fragments/fragments-cli/package.json @@ -20,6 +20,7 @@ "dependencies": { "@atls/figma-file-loader": "workspace:*", "@atls/figma-fragments-generator": "workspace:*", + "@babel/standalone": "7.22.20", "commander": "12.1.0", "figma-js": "1.16.1-0", "npmlog": "7.0.1", @@ -28,6 +29,8 @@ "devDependencies": { "@swc-node/register": "1.9.0", "@swc/core": "1.6.1", + "@types/babel__core": "7.20.5", + "@types/babel__standalone": "7.1.7", "@types/node": "18.19.34", "@types/npmlog": "7.0.0", "@yarnpkg/builder": "4.1.1", diff --git a/fragments/fragments-cli/src/run.ts b/fragments/fragments-cli/src/run.ts index 1376335..6039a48 100644 --- a/fragments/fragments-cli/src/run.ts +++ b/fragments/fragments-cli/src/run.ts @@ -1,19 +1,49 @@ +import { transform } from '@babel/standalone' + import path from 'path' import prettier from 'prettier' import { promises as fs } from 'fs' +import { readFileSync } from 'fs' +import { join } from 'path' import { FigmaFileLoader } from '@atls/figma-file-loader' import { FigmaThemeFragmentsGenerator } from '@atls/figma-fragments-generator' -export const run = async (fileId, nodeId, output) => { +const processFile = (filePath: string): any => { + const replacementsFile = readFileSync(filePath.replace('.js', '.ts')).toString('utf-8') + const { code } = transform(replacementsFile, { + presets: ['env'], + plugins: ['transform-modules-commonjs'], + }) + + if (!code) throw Error('Could not read the file') + + // eslint-disable-next-line no-eval, security/detect-eval-with-expression + const module = { exports: {} } + const exports = module.exports + const require = (modulePath) => { + const absolutePath = path.resolve(path.dirname(filePath), modulePath) + return processFile(absolutePath) + } + eval(` + (function(exports, module, require) { + ${code} + })(exports, module, require); + `) + return module.exports +} + +export const run = async (fileId, nodeId, output, themeFilePath) => { + const theme: Record> = processFile( + join(process.cwd(), themeFilePath) + ).lightThemeTokens + const loader = new FigmaFileLoader() const generator = new FigmaThemeFragmentsGenerator() const response = await loader.loadNode(fileId, nodeId) - console.log(response.nodes[nodeId]?.document) - - const component = generator.generate(response) + const component = generator.generate(response, theme) const target = path.join(output, 'fragments.tsx') From 82f386c4feb9c52b1cd1d85746c410c3e3f36e9f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 4 Dec 2024 11:11:01 +0300 Subject: [PATCH 06/48] feat: add get-value-key-from-theme to fragments-generator strategy --- .pnp.cjs | 34 ++++++++++++++ .../src/figma-fragments.generator.ts | 4 +- .../src/strategy/simple-mapping.strategy.ts | 45 ++++++++++++++++--- yarn.lock | 32 +++++++++++++ 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index 1a3a144..125ce82 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -429,8 +429,11 @@ const RAW_RUNTIME_STATE = ["@atls/figma-fragments-cli", "workspace:fragments/fragments-cli"],\ ["@atls/figma-file-loader", "virtual:75728d86037c75604505b9c0fbfc0ce3edc9d369e1826ac0d2d661dfb48b9446ca5a5e54a2ca8ec969b4beb532afca4cf558bf306737b461fca84524ac2142e6#workspace:loaders/file-loader"],\ ["@atls/figma-fragments-generator", "virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator"],\ + ["@babel/standalone", "npm:7.22.20"],\ ["@swc-node/register", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.9.0"],\ ["@swc/core", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.6.1"],\ + ["@types/babel__core", "npm:7.20.5"],\ + ["@types/babel__standalone", "npm:7.1.7"],\ ["@types/node", "npm:18.19.34"],\ ["@types/npmlog", "npm:7.0.0"],\ ["@yarnpkg/builder", "npm:4.1.1"],\ @@ -1613,6 +1616,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@babel/standalone", [\ + ["npm:7.22.20", {\ + "packageLocation": "../.yarn/berry/cache/@babel-standalone-npm-7.22.20-674a6ef7e3-10.zip/node_modules/@babel/standalone/",\ + "packageDependencies": [\ + ["@babel/standalone", "npm:7.22.20"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@babel/template", [\ ["npm:7.24.7", {\ "packageLocation": "../.yarn/berry/cache/@babel-template-npm-7.24.7-d08a527e2b-10.zip/node_modules/@babel/template/",\ @@ -3081,6 +3093,18 @@ const RAW_RUNTIME_STATE = ["@types/babel__traverse", "npm:7.20.3"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.20.5", {\ + "packageLocation": "../.yarn/berry/cache/@types-babel__core-npm-7.20.5-4d95f75eab-10.zip/node_modules/@types/babel__core/",\ + "packageDependencies": [\ + ["@types/babel__core", "npm:7.20.5"],\ + ["@babel/parser", "npm:7.24.7"],\ + ["@babel/types", "npm:7.24.7"],\ + ["@types/babel__generator", "npm:7.6.6"],\ + ["@types/babel__template", "npm:7.4.3"],\ + ["@types/babel__traverse", "npm:7.20.3"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@types/babel__generator", [\ @@ -3093,6 +3117,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/babel__standalone", [\ + ["npm:7.1.7", {\ + "packageLocation": "../.yarn/berry/cache/@types-babel__standalone-npm-7.1.7-b329c4f042-10.zip/node_modules/@types/babel__standalone/",\ + "packageDependencies": [\ + ["@types/babel__standalone", "npm:7.1.7"],\ + ["@types/babel__core", "npm:7.20.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/babel__template", [\ ["npm:7.4.3", {\ "packageLocation": "../.yarn/berry/cache/@types-babel__template-npm-7.4.3-ce042d883b-10.zip/node_modules/@types/babel__template/",\ diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index 45d2c0c..c0f455e 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -21,8 +21,8 @@ export class FigmaThemeFragmentsGenerator { return textNodes } - generate(response: FileNodesResponse): string { - const strategy = new SimpleMappingStrategy() + generate(response: FileNodesResponse, theme: Record>): string { + const strategy = new SimpleMappingStrategy(theme) const textNodes = this.getTextNodes(response.nodes) diff --git a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts index 95c7ebb..41e6343 100644 --- a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts @@ -1,17 +1,48 @@ -import { Text } from 'figma-js' -import { plugins } from 'pretty-format' -import { format } from 'pretty-format' -import { createElement } from 'react' +import { Text } from 'figma-js' +import { plugins } from 'pretty-format' +import { format } from 'pretty-format' +import { createElement } from 'react' +import { toColorOpacityString } from '@atls/figma-utils' +import { toColorString } from '@atls/figma-utils' export class SimpleMappingStrategy { + theme: Record> = {} + + constructor(theme: Record>) { + this.theme = theme + } + + getValueKeyFromTheme(themeKey: string, value: string) { + if (!this.theme[themeKey]) { + return + } + + const valueKey = Object.entries(this.theme[themeKey]).find((item) => item[1] === value)?.[0] + + return valueKey ? `$${valueKey}` : undefined + } + execute(textNodes: Text[] = []) { - const { name, style } = textNodes[0] + const { name, style, fills } = textNodes[0] + + const { color = { a: 1, b: 0, g: 0, r: 0 }, opacity } = fills[0] const element = createElement( 'Text', { - fontSize: style.fontSize, - fontWeight: style.fontWeight, + color: + this.getValueKeyFromTheme( + 'colors', + opacity ? toColorOpacityString(color, opacity) : toColorString(color) + ) || toColorString(color), + fontSize: this.getValueKeyFromTheme('fontSizes', `${style.fontSize}px`) || style.fontSize, + fontWeight: + this.getValueKeyFromTheme('fontWeights', `${style.fontWeight}`) || style.fontWeight, + lineHeight: + this.getValueKeyFromTheme( + 'lineHeights', + `${((style.lineHeightPercentFontSize || 100) / 100)?.toFixed(1)}` + ) || `${style.lineHeightPx}px`, }, name ) diff --git a/yarn.lock b/yarn.lock index 8c6abc7..0adde51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -252,8 +252,11 @@ __metadata: dependencies: "@atls/figma-file-loader": "workspace:*" "@atls/figma-fragments-generator": "workspace:*" + "@babel/standalone": "npm:7.22.20" "@swc-node/register": "npm:1.9.0" "@swc/core": "npm:1.6.1" + "@types/babel__core": "npm:7.20.5" + "@types/babel__standalone": "npm:7.1.7" "@types/node": "npm:18.19.34" "@types/npmlog": "npm:7.0.0" "@yarnpkg/builder": "npm:4.1.1" @@ -849,6 +852,13 @@ __metadata: languageName: node linkType: hard +"@babel/standalone@npm:7.22.20": + version: 7.22.20 + resolution: "@babel/standalone@npm:7.22.20" + checksum: 10/accf19752fe94d24dddc9b7c3c7b800d83d49713c5116323c22a8f13bb136408274ae6c93a0304a29a5c42eef15d244da96f11a8cfbfb06d02055556efbcac45 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.7, @babel/template@npm:^7.3.3": version: 7.24.7 resolution: "@babel/template@npm:7.24.7" @@ -2162,6 +2172,19 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:7.20.5, @types/babel__core@npm:^7.1.0": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": "npm:^7.20.7" + "@babel/types": "npm:^7.20.7" + "@types/babel__generator": "npm:*" + "@types/babel__template": "npm:*" + "@types/babel__traverse": "npm:*" + checksum: 10/c32838d280b5ab59d62557f9e331d3831f8e547ee10b4f85cb78753d97d521270cebfc73ce501e9fb27fe71884d1ba75e18658692c2f4117543f0fc4e3e118b3 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.1.14": version: 7.20.3 resolution: "@types/babel__core@npm:7.20.3" @@ -2184,6 +2207,15 @@ __metadata: languageName: node linkType: hard +"@types/babel__standalone@npm:7.1.7": + version: 7.1.7 + resolution: "@types/babel__standalone@npm:7.1.7" + dependencies: + "@types/babel__core": "npm:^7.1.0" + checksum: 10/a19ab145c6079b8f85aa7724d0c0f72db2235d11d3ed98cc188b8c31515c5e9e2297de35c590b7c44efe9d46bc04111ce9fcb7d41765bea096b2cbf51a0df236 + languageName: node + linkType: hard + "@types/babel__template@npm:*": version: 7.4.3 resolution: "@types/babel__template@npm:7.4.3" From 990b3772fef45f7ddcb6275dc68e4554937124e6 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 5 Dec 2024 11:01:04 +0300 Subject: [PATCH 07/48] feat: add component wrapper fro fragments --- .../src/figma-fragments.generator.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index c0f455e..b8c9120 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -21,13 +21,21 @@ export class FigmaThemeFragmentsGenerator { return textNodes } + createComponent(fragment: string): string { + return ` + import React from 'react' + import { memo } from 'react' + + export const GeneratedFragment = memo(() => (${fragment}))` + } + generate(response: FileNodesResponse, theme: Record>): string { const strategy = new SimpleMappingStrategy(theme) const textNodes = this.getTextNodes(response.nodes) - const component = strategy.execute(textNodes) + const fragment = strategy.execute(textNodes) - return component + return this.createComponent(fragment) } } From 1f6b36bf8de65c9c619fcd10a2427da2801abd3b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 5 Dec 2024 11:02:18 +0300 Subject: [PATCH 08/48] feat: add create wrapper element for array text elements --- .../src/strategy/simple-mapping.strategy.ts | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts index 41e6343..a626d8e 100644 --- a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts @@ -1,10 +1,14 @@ +import { Paint } from 'figma-js' import { Text } from 'figma-js' +import { TypeStyle } from 'figma-js' +import { ReactElement } from 'react' import { plugins } from 'pretty-format' import { format } from 'pretty-format' import { createElement } from 'react' import { toColorOpacityString } from '@atls/figma-utils' import { toColorString } from '@atls/figma-utils' + export class SimpleMappingStrategy { theme: Record> = {} @@ -17,37 +21,43 @@ export class SimpleMappingStrategy { return } - const valueKey = Object.entries(this.theme[themeKey]).find((item) => item[1] === value)?.[0] + const valueKey = Object.entries(this.theme[themeKey]).find( + (item) => item[1] === value && !['button', 'input'].includes(item[0]) + )?.[0] return valueKey ? `$${valueKey}` : undefined } + createTextAttributes(style: TypeStyle, fills: readonly Paint[]) { + const { color = { a: 1, b: 0, g: 0, r: 0 }, opacity } = fills[0] + + const stringColor = toColorString(color) + const themeColor = opacity ? toColorOpacityString(color, opacity) : stringColor + + const themeLineHeight = ((style.lineHeightPercentFontSize || 100) / 100)?.toFixed(1) + + return { + color: this.getValueKeyFromTheme('colors', themeColor) || stringColor, + fontSize: this.getValueKeyFromTheme('fontSizes', `${style.fontSize}px`) || style.fontSize, + fontWeight: + this.getValueKeyFromTheme('fontWeights', `${style.fontWeight}`) || style.fontWeight, + lineHeight: + this.getValueKeyFromTheme('lineHeights', themeLineHeight) || `${style.lineHeightPx}px`, + } + } + execute(textNodes: Text[] = []) { - const { name, style, fills } = textNodes[0] + const elements: ReactElement[] = [] - const { color = { a: 1, b: 0, g: 0, r: 0 }, opacity } = fills[0] + textNodes.forEach((node) => { + const { name, style, fills } = node + + elements.push(createElement('Text', this.createTextAttributes(style, fills), name)) + }) + + const fragment = elements.length === 1 ? elements[0] : createElement('Box', {}, elements) - const element = createElement( - 'Text', - { - color: - this.getValueKeyFromTheme( - 'colors', - opacity ? toColorOpacityString(color, opacity) : toColorString(color) - ) || toColorString(color), - fontSize: this.getValueKeyFromTheme('fontSizes', `${style.fontSize}px`) || style.fontSize, - fontWeight: - this.getValueKeyFromTheme('fontWeights', `${style.fontWeight}`) || style.fontWeight, - lineHeight: - this.getValueKeyFromTheme( - 'lineHeights', - `${((style.lineHeightPercentFontSize || 100) / 100)?.toFixed(1)}` - ) || `${style.lineHeightPx}px`, - }, - name - ) - - return format(element, { + return format(fragment, { plugins: [plugins.ReactElement], printFunctionName: false, }) From 9d1519ca12511fc1268d5cce4e447bd766ad4cef Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 6 Dec 2024 12:09:19 +0300 Subject: [PATCH 09/48] feat: add figma file-utils with process and write file utils destructure fragments-cli run util --- .pnp.cjs | 24 ++++++++++-- fragments/fragments-cli/package.json | 7 +--- fragments/fragments-cli/src/run.ts | 57 +++++++++------------------- utils/file/package.json | 29 ++++++++++++++ utils/file/src/index.ts | 2 + utils/file/src/process-file.util.ts | 33 ++++++++++++++++ utils/file/src/write-file.util.ts | 13 +++++++ utils/utils/src/walk.ts | 7 +++- yarn.lock | 17 +++++++-- 9 files changed, 135 insertions(+), 54 deletions(-) create mode 100644 utils/file/package.json create mode 100644 utils/file/src/index.ts create mode 100644 utils/file/src/process-file.util.ts create mode 100644 utils/file/src/write-file.util.ts diff --git a/.pnp.cjs b/.pnp.cjs index 125ce82..81561cd 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -77,6 +77,10 @@ const RAW_RUNTIME_STATE = "name": "@atls/figma-theme-shadows-generator",\ "reference": "workspace:theme/theme-shadows-generator"\ },\ + {\ + "name": "@atls/figma-file-utils",\ + "reference": "workspace:utils/file"\ + },\ {\ "name": "@atls/figma-utils",\ "reference": "workspace:utils/utils"\ @@ -88,6 +92,7 @@ const RAW_RUNTIME_STATE = ["@atls/figma-assets", ["virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#workspace:assets/assets", "workspace:assets/assets"]],\ ["@atls/figma-assets-cli", ["workspace:assets/assets-cli"]],\ ["@atls/figma-file-loader", ["virtual:75728d86037c75604505b9c0fbfc0ce3edc9d369e1826ac0d2d661dfb48b9446ca5a5e54a2ca8ec969b4beb532afca4cf558bf306737b461fca84524ac2142e6#workspace:loaders/file-loader", "workspace:loaders/file-loader"]],\ + ["@atls/figma-file-utils", ["workspace:utils/file"]],\ ["@atls/figma-fragments-cli", ["workspace:fragments/fragments-cli"]],\ ["@atls/figma-fragments-generator", ["virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator", "workspace:fragments/fragments-generator"]],\ ["@atls/figma-theme", ["virtual:a9526061832803f8bc7fed186e9699b3bcb0fb7fb989d17328e56b61e9f17fd49895330e461073d51dd5dcbab09bf72a55678f1e398bacacdb001a56e84fd54f#workspace:theme/theme", "workspace:theme/theme"]],\ @@ -422,25 +427,36 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@atls/figma-file-utils", [\ + ["workspace:utils/file", {\ + "packageLocation": "./utils/file/",\ + "packageDependencies": [\ + ["@atls/figma-file-utils", "workspace:utils/file"],\ + ["@babel/standalone", "npm:7.22.20"],\ + ["@types/babel__core", "npm:7.20.5"],\ + ["@types/babel__standalone", "npm:7.1.7"],\ + ["@types/node", "npm:18.19.34"],\ + ["prettier", "npm:2.8.8"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@atls/figma-fragments-cli", [\ ["workspace:fragments/fragments-cli", {\ "packageLocation": "./fragments/fragments-cli/",\ "packageDependencies": [\ ["@atls/figma-fragments-cli", "workspace:fragments/fragments-cli"],\ ["@atls/figma-file-loader", "virtual:75728d86037c75604505b9c0fbfc0ce3edc9d369e1826ac0d2d661dfb48b9446ca5a5e54a2ca8ec969b4beb532afca4cf558bf306737b461fca84524ac2142e6#workspace:loaders/file-loader"],\ + ["@atls/figma-file-utils", "workspace:utils/file"],\ ["@atls/figma-fragments-generator", "virtual:8d41429ff8893e59f14f513f07558a38e34b88c9ebd315931533c77deb02f350ab58c2460ca0593e0c001f806cd97c2b081622cc990fdbc17fa6dbca612d2f7c#workspace:fragments/fragments-generator"],\ - ["@babel/standalone", "npm:7.22.20"],\ ["@swc-node/register", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.9.0"],\ ["@swc/core", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.6.1"],\ - ["@types/babel__core", "npm:7.20.5"],\ - ["@types/babel__standalone", "npm:7.1.7"],\ ["@types/node", "npm:18.19.34"],\ ["@types/npmlog", "npm:7.0.0"],\ ["@yarnpkg/builder", "npm:4.1.1"],\ ["commander", "npm:12.1.0"],\ ["figma-js", "npm:1.16.1-0"],\ ["npmlog", "npm:7.0.1"],\ - ["prettier", "npm:2.8.8"],\ ["ts-node", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:10.9.2"],\ ["typescript", "patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441"]\ ],\ diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json index d181b47..9cc21b9 100644 --- a/fragments/fragments-cli/package.json +++ b/fragments/fragments-cli/package.json @@ -19,18 +19,15 @@ }, "dependencies": { "@atls/figma-file-loader": "workspace:*", + "@atls/figma-file-utils": "workspace:*", "@atls/figma-fragments-generator": "workspace:*", - "@babel/standalone": "7.22.20", "commander": "12.1.0", "figma-js": "1.16.1-0", - "npmlog": "7.0.1", - "prettier": "2.8.8" + "npmlog": "7.0.1" }, "devDependencies": { "@swc-node/register": "1.9.0", "@swc/core": "1.6.1", - "@types/babel__core": "7.20.5", - "@types/babel__standalone": "7.1.7", "@types/node": "18.19.34", "@types/npmlog": "7.0.0", "@yarnpkg/builder": "4.1.1", diff --git a/fragments/fragments-cli/src/run.ts b/fragments/fragments-cli/src/run.ts index 6039a48..46557fa 100644 --- a/fragments/fragments-cli/src/run.ts +++ b/fragments/fragments-cli/src/run.ts @@ -1,42 +1,27 @@ -import { transform } from '@babel/standalone' +import assert from 'node:assert' -import path from 'path' -import prettier from 'prettier' -import { promises as fs } from 'fs' -import { readFileSync } from 'fs' import { join } from 'path' import { FigmaFileLoader } from '@atls/figma-file-loader' import { FigmaThemeFragmentsGenerator } from '@atls/figma-fragments-generator' +import { processFile } from '@atls/figma-file-utils' +import { writeFile } from '@atls/figma-file-utils' -const processFile = (filePath: string): any => { - const replacementsFile = readFileSync(filePath.replace('.js', '.ts')).toString('utf-8') - const { code } = transform(replacementsFile, { - presets: ['env'], - plugins: ['transform-modules-commonjs'], - }) - - if (!code) throw Error('Could not read the file') - - // eslint-disable-next-line no-eval, security/detect-eval-with-expression - const module = { exports: {} } - const exports = module.exports - const require = (modulePath) => { - const absolutePath = path.resolve(path.dirname(filePath), modulePath) - return processFile(absolutePath) - } - eval(` - (function(exports, module, require) { - ${code} - })(exports, module, require); - `) - return module.exports -} +export const run = async ( + fileId: string, + nodeId: string, + output: string, + themeFilePath: string +) => { + const absoluteThemeFilePath = join(process.cwd(), themeFilePath) + const exports = processFile(absoluteThemeFilePath) + + const theme = Object.values(exports)?.[0] as Record> -export const run = async (fileId, nodeId, output, themeFilePath) => { - const theme: Record> = processFile( - join(process.cwd(), themeFilePath) - ).lightThemeTokens + assert.ok( + theme, + `Could not process the theme with path ${absoluteThemeFilePath}. Please try again` + ) const loader = new FigmaFileLoader() const generator = new FigmaThemeFragmentsGenerator() @@ -45,11 +30,5 @@ export const run = async (fileId, nodeId, output, themeFilePath) => { const component = generator.generate(response, theme) - const target = path.join(output, 'fragments.tsx') - - const options = await prettier.resolveConfig(target) - - const data = await prettier.format(component, { ...options }) - - await fs.writeFile(target, data) + await writeFile(output, 'fragments.tsx', component) } diff --git a/utils/file/package.json b/utils/file/package.json new file mode 100644 index 0000000..c3c180b --- /dev/null +++ b/utils/file/package.json @@ -0,0 +1,29 @@ +{ + "name": "@atls/figma-file-utils", + "version": "0.0.4", + "license": "BSD-3-Clause", + "type": "module", + "main": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn library build", + "prepack": "yarn run build", + "postpack": "rm -rf dist" + }, + "dependencies": { + "@babel/standalone": "7.22.20", + "prettier": "2.8.8" + }, + "devDependencies": { + "@types/babel__core": "7.20.5", + "@types/babel__standalone": "7.1.7", + "@types/node": "18.19.34" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } +} diff --git a/utils/file/src/index.ts b/utils/file/src/index.ts new file mode 100644 index 0000000..8aa0eed --- /dev/null +++ b/utils/file/src/index.ts @@ -0,0 +1,2 @@ +export * from './process-file.util.js' +export * from './write-file.util.js' diff --git a/utils/file/src/process-file.util.ts b/utils/file/src/process-file.util.ts new file mode 100644 index 0000000..e46319e --- /dev/null +++ b/utils/file/src/process-file.util.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert' + +import { transform } from '@babel/standalone' + +import path from 'path' +import { readFileSync } from 'fs' + +export const processFile = (filePath: string): any => { + const file = readFileSync(filePath.replace('.js', '.ts')).toString('utf-8') + + const { code } = transform(file, { + presets: ['env'], + plugins: ['transform-modules-commonjs'], + }) + + assert.ok(code, `Could not process the code with path ${filePath}. Please try again`) + + const module = { exports: {} } + const exports = module.exports + const require = (modulePath: string): any => { + const absolutePath = path.resolve(path.dirname(filePath), modulePath) + return processFile(absolutePath) + } + + // eslint-disable-next-line no-eval, security/detect-eval-with-expression + eval(` + (function(exports, module, require) { + ${code} + })(exports, module, require); + `) + + return module.exports +} diff --git a/utils/file/src/write-file.util.ts b/utils/file/src/write-file.util.ts new file mode 100644 index 0000000..8eb4fd6 --- /dev/null +++ b/utils/file/src/write-file.util.ts @@ -0,0 +1,13 @@ +import path from 'path' +import prettier from 'prettier' +import { promises } from 'fs' + +export const writeFile = async (filePath: string, name: string, content: string): Promise => { + const target = path.join(filePath, name) + + const options = await prettier.resolveConfig(target) + + const data = await prettier.format(content, { ...options }) + + await promises.writeFile(target, data) +} diff --git a/utils/utils/src/walk.ts b/utils/utils/src/walk.ts index 5014d0b..f402526 100644 --- a/utils/utils/src/walk.ts +++ b/utils/utils/src/walk.ts @@ -1,10 +1,13 @@ -import { Node } from 'figma-js' -import { Text } from 'figma-js' +import { Node } from 'figma-js' +import { Text } from 'figma-js' +import { Frame } from 'figma-js' const isEmpty = (node: any) => !node || Object.keys(node).length === 0 export const isText = (node: Node): node is Text => node.type === 'TEXT' +export const isFrame = (node: Node): node is Frame => node.type === 'FRAME' + export const walk = (targetNode: any, cb: (node: any) => any) => { if (isEmpty(targetNode) || typeof targetNode === 'string' || typeof targetNode === 'number') { return diff --git a/yarn.lock b/yarn.lock index 0adde51..2eea81a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -246,24 +246,33 @@ __metadata: languageName: unknown linkType: soft +"@atls/figma-file-utils@workspace:*, @atls/figma-file-utils@workspace:utils/file": + version: 0.0.0-use.local + resolution: "@atls/figma-file-utils@workspace:utils/file" + dependencies: + "@babel/standalone": "npm:7.22.20" + "@types/babel__core": "npm:7.20.5" + "@types/babel__standalone": "npm:7.1.7" + "@types/node": "npm:18.19.34" + prettier: "npm:2.8.8" + languageName: unknown + linkType: soft + "@atls/figma-fragments-cli@workspace:fragments/fragments-cli": version: 0.0.0-use.local resolution: "@atls/figma-fragments-cli@workspace:fragments/fragments-cli" dependencies: "@atls/figma-file-loader": "workspace:*" + "@atls/figma-file-utils": "workspace:*" "@atls/figma-fragments-generator": "workspace:*" - "@babel/standalone": "npm:7.22.20" "@swc-node/register": "npm:1.9.0" "@swc/core": "npm:1.6.1" - "@types/babel__core": "npm:7.20.5" - "@types/babel__standalone": "npm:7.1.7" "@types/node": "npm:18.19.34" "@types/npmlog": "npm:7.0.0" "@yarnpkg/builder": "npm:4.1.1" commander: "npm:12.1.0" figma-js: "npm:1.16.1-0" npmlog: "npm:7.0.1" - prettier: "npm:2.8.8" ts-node: "npm:10.9.2" typescript: "npm:5.2.2" bin: From 3850bf349dbfc4cc97a64656f71abe82bf51cd61 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 6 Dec 2024 12:10:26 +0300 Subject: [PATCH 10/48] refactor: parse node-id in load-node method --- fragments/fragments-cli/src/index.ts | 2 +- loaders/file-loader/src/FigmaFileLoader.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fragments/fragments-cli/src/index.ts b/fragments/fragments-cli/src/index.ts index 564d89a..95a0460 100644 --- a/fragments/fragments-cli/src/index.ts +++ b/fragments/fragments-cli/src/index.ts @@ -40,7 +40,7 @@ if (!fileId) { process.env['FIGMA_TOKEN'] = 'secret' - run(fileId, options.nodeId?.replace('-', ':'), options.output, options.theme) + run(fileId, options.nodeId, options.output, options.theme) .then(() => logger.info('info', 'Fragments successful generated')) .catch((error) => logger.error('error', error.message)) } diff --git a/loaders/file-loader/src/FigmaFileLoader.ts b/loaders/file-loader/src/FigmaFileLoader.ts index 707b718..7009c76 100644 --- a/loaders/file-loader/src/FigmaFileLoader.ts +++ b/loaders/file-loader/src/FigmaFileLoader.ts @@ -19,7 +19,7 @@ export class FigmaFileLoader { } async loadNode(fileId: string, nodeId: string): Promise { - const { data } = await this.figma.fileNodes(fileId, { ids: [nodeId] }) + const { data } = await this.figma.fileNodes(fileId, { ids: [nodeId.replace('-', ':')] }) return data } From 6e4cc3206841fbab47b45e62ed61e03933999d4e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 6 Dec 2024 12:13:52 +0300 Subject: [PATCH 11/48] feat: add create dynamic fragments tree to fragments generator --- .../src/figma-fragments.generator.ts | 20 +--- .../src/strategy/simple-mapping.strategy.ts | 97 ++++++++++++++++--- .../src/strategy/strategy.constants.ts | 7 ++ .../src/strategy/strategy.interfaces.ts | 7 ++ 4 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 fragments/fragments-generator/src/strategy/strategy.constants.ts create mode 100644 fragments/fragments-generator/src/strategy/strategy.interfaces.ts diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index b8c9120..50c0578 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -1,26 +1,10 @@ import { FileNodesResponse } from 'figma-js' -import { Text } from 'figma-js' - -import { isText } from '@atls/figma-utils' -import { walk } from '@atls/figma-utils' import { SimpleMappingStrategy } from './strategy/index.js' export class FigmaThemeFragmentsGenerator { readonly name = 'fragments' - getTextNodes(nodes): Text[] { - const textNodes: Text[] = [] - - walk(nodes, (node) => { - if (isText(node)) { - textNodes.push(node) - } - }) - - return textNodes - } - createComponent(fragment: string): string { return ` import React from 'react' @@ -32,9 +16,7 @@ export class FigmaThemeFragmentsGenerator { generate(response: FileNodesResponse, theme: Record>): string { const strategy = new SimpleMappingStrategy(theme) - const textNodes = this.getTextNodes(response.nodes) - - const fragment = strategy.execute(textNodes) + const fragment = strategy.execute(response.nodes) return this.createComponent(fragment) } diff --git a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts index a626d8e..50ce7d4 100644 --- a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts @@ -1,35 +1,54 @@ +import { FileNodesResponse } from 'figma-js' +import { Frame } from 'figma-js' import { Paint } from 'figma-js' import { Text } from 'figma-js' import { TypeStyle } from 'figma-js' +import { Fragment } from 'react' import { ReactElement } from 'react' import { plugins } from 'pretty-format' import { format } from 'pretty-format' +import { cloneElement } from 'react' import { createElement } from 'react' +import { isFrame } from '@atls/figma-utils' +import { isText } from '@atls/figma-utils' import { toColorOpacityString } from '@atls/figma-utils' +import { walk } from '@atls/figma-utils' import { toColorString } from '@atls/figma-utils' +import { THEME_KEY_PREFIX } from './strategy.constants.js' +import { TreeElement } from './strategy.interfaces.js' +import { defaultFigmaColor } from './strategy.constants.js' +import { colorsIgnorePatterns } from './strategy.constants.js' + export class SimpleMappingStrategy { theme: Record> = {} + elements: Record = {} constructor(theme: Record>) { this.theme = theme } - getValueKeyFromTheme(themeKey: string, value: string) { - if (!this.theme[themeKey]) { + getValueKeyFromTheme(themeKey: string, search: string) { + const vars = this.theme[themeKey] + + if (!vars) { return } - const valueKey = Object.entries(this.theme[themeKey]).find( - (item) => item[1] === value && !['button', 'input'].includes(item[0]) - )?.[0] + const valueKey = Object.entries(vars).find(([key, value]) => { + if (themeKey === 'colors' && colorsIgnorePatterns.some((pattern) => key.includes(pattern))) { + return false + } + + return value === search + })?.[0] - return valueKey ? `$${valueKey}` : undefined + return valueKey ? `${THEME_KEY_PREFIX}${valueKey}` : undefined } createTextAttributes(style: TypeStyle, fills: readonly Paint[]) { - const { color = { a: 1, b: 0, g: 0, r: 0 }, opacity } = fills[0] + const { color = defaultFigmaColor, opacity } = fills[0] const stringColor = toColorString(color) const themeColor = opacity ? toColorOpacityString(color, opacity) : stringColor @@ -46,16 +65,68 @@ export class SimpleMappingStrategy { } } - execute(textNodes: Text[] = []) { - const elements: ReactElement[] = [] + createTextElement(node: Text) { + const { characters, style, fills } = node + + return createElement('Text', this.createTextAttributes(style, fills), characters) + } + + createFrameElement(node: Frame) { + return createElement('Box', {}) + } + + createFragmentElement(elements: TreeElement[]) { + if (elements.length <= 1) { + return this.createElementsTree(elements[0]) + } + + return createElement( + Fragment, + null, + elements.map((node) => this.createElementsTree(node)) + ) + } + + createElementsTree(rootElement: TreeElement): ReactElement { + if (!rootElement.childrenIds.length) { + return rootElement.element + } - textNodes.forEach((node) => { - const { name, style, fills } = node + const element = cloneElement( + rootElement.element, + rootElement.element.props, + rootElement.childrenIds.map((id) => this.createElementsTree(this.elements[id])) + ) - elements.push(createElement('Text', this.createTextAttributes(style, fills), name)) + return element + } + + execute(nodes: FileNodesResponse['nodes']) { + const findParentId = (childrenId: string) => + Object.entries(this.elements).find((element) => + element[1].childrenIds.includes(childrenId))?.[0] || null + + walk(nodes, (node) => { + if (isText(node)) { + this.elements[node.id] = { + element: this.createTextElement(node), + childrenIds: [], + parentId: findParentId(node.id), + } + } + + if (isFrame(node)) { + this.elements[node.id] = { + element: this.createFrameElement(node), + childrenIds: node?.children.map((node) => node.id) || [], + parentId: findParentId(node.id), + } + } }) - const fragment = elements.length === 1 ? elements[0] : createElement('Box', {}, elements) + const rootNodes = Object.values(this.elements).filter((element) => !element.parentId) + + const fragment = this.createFragmentElement(rootNodes) return format(fragment, { plugins: [plugins.ReactElement], diff --git a/fragments/fragments-generator/src/strategy/strategy.constants.ts b/fragments/fragments-generator/src/strategy/strategy.constants.ts new file mode 100644 index 0000000..60afbe5 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/strategy.constants.ts @@ -0,0 +1,7 @@ +import { Color } from 'figma-js' + +export const THEME_KEY_PREFIX = '$' + +export const colorsIgnorePatterns = ['button', 'input'] + +export const defaultFigmaColor: Color = { r: 0, g: 0, b: 0, a: 1 } diff --git a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts b/fragments/fragments-generator/src/strategy/strategy.interfaces.ts new file mode 100644 index 0000000..6a4b182 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/strategy.interfaces.ts @@ -0,0 +1,7 @@ +import { ReactElement } from 'react' + +export interface TreeElement { + childrenIds: string[] + parentId: string | null + element: ReactElement +} From 388370af75be20192290cd4e09df4afe8c0b64d1 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 10 Dec 2024 11:46:56 +0300 Subject: [PATCH 12/48] refactor: destructure create-fragment strategy --- .../src/figma-fragments.generator.ts | 4 +- .../src/strategy/create-fragment.strategy.ts | 101 +++++++++++++ .../fragments-generator/src/strategy/index.ts | 2 +- .../src/strategy/simple-mapping.strategy.ts | 136 ------------------ 4 files changed, 104 insertions(+), 139 deletions(-) create mode 100644 fragments/fragments-generator/src/strategy/create-fragment.strategy.ts delete mode 100644 fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index 50c0578..bb212fb 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -1,6 +1,6 @@ import { FileNodesResponse } from 'figma-js' -import { SimpleMappingStrategy } from './strategy/index.js' +import { CreateFragmentStrategy } from './strategy/index.js' export class FigmaThemeFragmentsGenerator { readonly name = 'fragments' @@ -14,7 +14,7 @@ export class FigmaThemeFragmentsGenerator { } generate(response: FileNodesResponse, theme: Record>): string { - const strategy = new SimpleMappingStrategy(theme) + const strategy = new CreateFragmentStrategy(theme) const fragment = strategy.execute(response.nodes) diff --git a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts b/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts new file mode 100644 index 0000000..2b0b227 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts @@ -0,0 +1,101 @@ +import type { CreteFragmentResult } from './strategy.interfaces.js' +import type { TreeElement } from './strategy.interfaces.js' +import type { FileNodesResponse } from 'figma-js' +import type { ReactElement } from 'react' + +import { Fragment } from 'react' +import { plugins } from 'pretty-format' +import { format } from 'pretty-format' +import { cloneElement } from 'react' +import { createElement } from 'react' + +import { isFrame } from '@atls/figma-utils' +import { isText } from '@atls/figma-utils' +import { walk } from '@atls/figma-utils' + +import { CreateBoxStrategy } from './create-box.strategy.js' +import { CreateTextStrategy } from './create-text.strategy.js' + +export class CreateFragmentStrategy { + private elements: Record = {} + + private text: CreateTextStrategy + private box: CreateBoxStrategy + + constructor(theme: Record>) { + this.text = new CreateTextStrategy(theme) + this.box = new CreateBoxStrategy(theme) + } + + private createFragmentElement(elements: TreeElement[]) { + if (elements.length <= 1) { + return this.createElementsTree(elements[0]) + } + + return createElement( + Fragment, + null, + elements.map((node) => this.createElementsTree(node)) + ) + } + + private createElementsTree(rootElement: TreeElement): ReactElement { + if (!rootElement.childrenIds.length) { + return rootElement.element + } + + const element = cloneElement( + rootElement.element, + rootElement.element.props, + rootElement.childrenIds.map((id) => + this.elements[id] ? this.createElementsTree(this.elements[id]) : null) + ) + + return element + } + + private findParentId(childrenId: string) { + const parentId = Object.entries(this.elements).find((element) => + element[1].childrenIds.includes(childrenId))?.[0] + + return parentId || null + } + + execute(nodes: FileNodesResponse['nodes']): CreteFragmentResult { + const imports = new Set() + + walk(nodes, (node) => { + if (isText(node)) { + this.text.getImports().forEach((value) => imports.add(value)) + + this.elements[node.id] = { + element: this.text.createElement(node), + childrenIds: [], + parentId: this.findParentId(node.id), + } + } + + if (isFrame(node)) { + this.box.getImports().forEach((value) => imports.add(value)) + + this.elements[node.id] = { + element: this.box.createElement(node), + childrenIds: node?.children.map((node) => node.id) || [], + parentId: this.findParentId(node.id), + } + } + }) + + const rootNodes = Object.values(this.elements).filter((element) => !element.parentId) + + const fragment = format(this.createFragmentElement(rootNodes), { + plugins: [plugins.ReactElement], + printFunctionName: false, + }) + + return { + fragment, + imports: Array.from(imports), + } + } +} diff --git a/fragments/fragments-generator/src/strategy/index.ts b/fragments/fragments-generator/src/strategy/index.ts index 809d935..783a1bf 100644 --- a/fragments/fragments-generator/src/strategy/index.ts +++ b/fragments/fragments-generator/src/strategy/index.ts @@ -1 +1 @@ -export * from './simple-mapping.strategy.js' +export * from './create-fragment.strategy.js' diff --git a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts deleted file mode 100644 index 50ce7d4..0000000 --- a/fragments/fragments-generator/src/strategy/simple-mapping.strategy.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { FileNodesResponse } from 'figma-js' -import { Frame } from 'figma-js' -import { Paint } from 'figma-js' -import { Text } from 'figma-js' -import { TypeStyle } from 'figma-js' -import { Fragment } from 'react' -import { ReactElement } from 'react' -import { plugins } from 'pretty-format' -import { format } from 'pretty-format' -import { cloneElement } from 'react' -import { createElement } from 'react' - -import { isFrame } from '@atls/figma-utils' -import { isText } from '@atls/figma-utils' -import { toColorOpacityString } from '@atls/figma-utils' -import { walk } from '@atls/figma-utils' -import { toColorString } from '@atls/figma-utils' - -import { THEME_KEY_PREFIX } from './strategy.constants.js' -import { TreeElement } from './strategy.interfaces.js' -import { defaultFigmaColor } from './strategy.constants.js' -import { colorsIgnorePatterns } from './strategy.constants.js' - -export class SimpleMappingStrategy { - theme: Record> = {} - elements: Record = {} - - constructor(theme: Record>) { - this.theme = theme - } - - getValueKeyFromTheme(themeKey: string, search: string) { - const vars = this.theme[themeKey] - - if (!vars) { - return - } - - const valueKey = Object.entries(vars).find(([key, value]) => { - if (themeKey === 'colors' && colorsIgnorePatterns.some((pattern) => key.includes(pattern))) { - return false - } - - return value === search - })?.[0] - - return valueKey ? `${THEME_KEY_PREFIX}${valueKey}` : undefined - } - - createTextAttributes(style: TypeStyle, fills: readonly Paint[]) { - const { color = defaultFigmaColor, opacity } = fills[0] - - const stringColor = toColorString(color) - const themeColor = opacity ? toColorOpacityString(color, opacity) : stringColor - - const themeLineHeight = ((style.lineHeightPercentFontSize || 100) / 100)?.toFixed(1) - - return { - color: this.getValueKeyFromTheme('colors', themeColor) || stringColor, - fontSize: this.getValueKeyFromTheme('fontSizes', `${style.fontSize}px`) || style.fontSize, - fontWeight: - this.getValueKeyFromTheme('fontWeights', `${style.fontWeight}`) || style.fontWeight, - lineHeight: - this.getValueKeyFromTheme('lineHeights', themeLineHeight) || `${style.lineHeightPx}px`, - } - } - - createTextElement(node: Text) { - const { characters, style, fills } = node - - return createElement('Text', this.createTextAttributes(style, fills), characters) - } - - createFrameElement(node: Frame) { - return createElement('Box', {}) - } - - createFragmentElement(elements: TreeElement[]) { - if (elements.length <= 1) { - return this.createElementsTree(elements[0]) - } - - return createElement( - Fragment, - null, - elements.map((node) => this.createElementsTree(node)) - ) - } - - createElementsTree(rootElement: TreeElement): ReactElement { - if (!rootElement.childrenIds.length) { - return rootElement.element - } - - const element = cloneElement( - rootElement.element, - rootElement.element.props, - rootElement.childrenIds.map((id) => this.createElementsTree(this.elements[id])) - ) - - return element - } - - execute(nodes: FileNodesResponse['nodes']) { - const findParentId = (childrenId: string) => - Object.entries(this.elements).find((element) => - element[1].childrenIds.includes(childrenId))?.[0] || null - - walk(nodes, (node) => { - if (isText(node)) { - this.elements[node.id] = { - element: this.createTextElement(node), - childrenIds: [], - parentId: findParentId(node.id), - } - } - - if (isFrame(node)) { - this.elements[node.id] = { - element: this.createFrameElement(node), - childrenIds: node?.children.map((node) => node.id) || [], - parentId: findParentId(node.id), - } - } - }) - - const rootNodes = Object.values(this.elements).filter((element) => !element.parentId) - - const fragment = this.createFragmentElement(rootNodes) - - return format(fragment, { - plugins: [plugins.ReactElement], - printFunctionName: false, - }) - } -} From a976f4b0c18fcd92e02ddf42d98c8a747cf57223 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 10 Dec 2024 11:47:57 +0300 Subject: [PATCH 13/48] feat: add dynamic imports when creating component --- .../fragments-generator/src/figma-fragments.generator.ts | 9 +++++---- .../src/strategy/strategy.interfaces.ts | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index bb212fb..b90e39d 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -1,14 +1,15 @@ -import { FileNodesResponse } from 'figma-js' +import { FileNodesResponse } from 'figma-js' import { CreateFragmentStrategy } from './strategy/index.js' export class FigmaThemeFragmentsGenerator { readonly name = 'fragments' - createComponent(fragment: string): string { + createComponent(fragment: string, imports?: Array): string { return ` import React from 'react' import { memo } from 'react' + ${imports?.join('\n')} export const GeneratedFragment = memo(() => (${fragment}))` } @@ -16,8 +17,8 @@ export class FigmaThemeFragmentsGenerator { generate(response: FileNodesResponse, theme: Record>): string { const strategy = new CreateFragmentStrategy(theme) - const fragment = strategy.execute(response.nodes) + const { fragment, imports } = strategy.execute(response.nodes) - return this.createComponent(fragment) + return this.createComponent(fragment, imports) } } diff --git a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts b/fragments/fragments-generator/src/strategy/strategy.interfaces.ts index 6a4b182..17d45de 100644 --- a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts +++ b/fragments/fragments-generator/src/strategy/strategy.interfaces.ts @@ -1,7 +1,12 @@ import { ReactElement } from 'react' export interface TreeElement { - childrenIds: string[] + childrenIds: Array parentId: string | null element: ReactElement } + +export interface CreteFragmentResult { + fragment: string + imports: Array +} From 96c828200aceb90c70c3175a295f632eec69c27d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 10 Dec 2024 11:48:52 +0300 Subject: [PATCH 14/48] feat: add theme-mapping strategy --- .../src/strategy/theme-mapping.strategy.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts new file mode 100644 index 0000000..e87ea24 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -0,0 +1,28 @@ +import { THEME_KEY_PREFIX } from './strategy.constants.js' +import { colorsIgnorePatterns } from './strategy.constants.js' + +export class ThemeMappingStrategy { + private theme: Record> = {} + + constructor(theme: Record>) { + this.theme = theme + } + + getValueKeyFromTheme(themeKey: string, search: string) { + const vars = this.theme[themeKey] + + if (!vars) { + return + } + + const valueKey = Object.entries(vars).find(([key, value]) => { + if (themeKey === 'colors' && colorsIgnorePatterns.some((pattern) => key.includes(pattern))) { + return false + } + + return value === search + })?.[0] + + return valueKey ? `${THEME_KEY_PREFIX}${valueKey}` : undefined + } +} From fac044abdbeba5036d1f574d6fd1d764fdd6f529 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 10 Dec 2024 11:49:04 +0300 Subject: [PATCH 15/48] feat: add create-text strategy --- .../src/strategy/create-text.strategy.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/create-text.strategy.ts diff --git a/fragments/fragments-generator/src/strategy/create-text.strategy.ts b/fragments/fragments-generator/src/strategy/create-text.strategy.ts new file mode 100644 index 0000000..0d0e7a1 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/create-text.strategy.ts @@ -0,0 +1,46 @@ +import { Paint } from 'figma-js' +import { Text } from 'figma-js' +import { TypeStyle } from 'figma-js' +import { createElement } from 'react' + +import { toColorOpacityString } from '@atls/figma-utils' +import { toColorString } from '@atls/figma-utils' + +import { ThemeMappingStrategy } from './theme-mapping.strategy.js' +import { defaultFigmaColor } from './strategy.constants.js' + +export class CreateTextStrategy extends ThemeMappingStrategy { + constructor(theme: Record>) { + super(theme) + } + + private createAttributes(style: TypeStyle, fills: readonly Paint[]) { + const { color = defaultFigmaColor, opacity } = fills[0] + + const stringColor = toColorString(color) + const themeColor = opacity ? toColorOpacityString(color, opacity) : stringColor + + const themeLineHeight = ((style.lineHeightPercentFontSize || 100) / 100)?.toFixed(1) + + return { + color: this.getValueKeyFromTheme('colors', themeColor) || stringColor, + fontSize: this.getValueKeyFromTheme('fontSizes', `${style.fontSize}px`) || style.fontSize, + fontWeight: + this.getValueKeyFromTheme('fontWeights', `${style.fontWeight}`) || style.fontWeight, + lineHeight: + this.getValueKeyFromTheme('lineHeights', themeLineHeight) || `${style.lineHeightPx}px`, + } + } + + getImports() { + return [`import { Text } from '@ui/text'`, `import { FormattedMessage } from 'react-intl'`] + } + + createElement(node: Text) { + const { characters, style, fills } = node + + const childrenElement = createElement('FormattedMessage', { defaultMessage: characters }) + + return createElement('Text', this.createAttributes(style, fills), childrenElement) + } +} From 99c446fde714dea3db4b39b930ac924857dc7cd7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 10 Dec 2024 11:49:51 +0300 Subject: [PATCH 16/48] feat: add create-box strategy --- .../src/strategy/create-box.strategy.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/create-box.strategy.ts diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts new file mode 100644 index 0000000..bbc5e3f --- /dev/null +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -0,0 +1,94 @@ +import { Frame } from 'figma-js' +import { createElement } from 'react' + +import { ThemeMappingStrategy } from './theme-mapping.strategy.js' + +export class CreateBoxStrategy extends ThemeMappingStrategy { + constructor(theme: Record>) { + super(theme) + } + + getPaddingFromTheme(padding: number | undefined) { + if (padding === undefined) { + return undefined + } + + return this.getValueKeyFromTheme('spaces', `${padding}px`) || `${padding}px` + } + + createPaddingAttributes({ + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, + }: Record) { + const top = this.getPaddingFromTheme(paddingTop) + const bottom = this.getPaddingFromTheme(paddingBottom) + const left = this.getPaddingFromTheme(paddingLeft) + const right = this.getPaddingFromTheme(paddingRight) + + if ([top, bottom, left, right].every((value) => value && value === top)) { + return { padding: top } + } + + const attributes: Record = {} + + if (top && bottom && top === bottom) { + attributes.paddingY = top + } + + if (left && right && left === right) { + attributes.paddingX = left + } + + if (top && !attributes.paddingY) { + attributes.paddingTop = top + } + + if (bottom && !attributes.paddingY) { + attributes.paddingBottom = bottom + } + + if (left && !attributes.paddingX) { + attributes.paddingLeft = left + } + + if (right && !attributes.paddingX) { + attributes.paddingRight = right + } + + return attributes + } + + getImports() { + return [`import { Box } from '@ui/layout'`] + } + + createElement(node: Frame) { + const { + layoutMode, + itemSpacing, + counterAxisAlignItems, + primaryAxisAlignItems, + paddingBottom, + paddingLeft, + paddingRight, + paddingTop, + } = node + + const figmaAlignItems = { + CENTER: 'center', + SPACE_BETWEEN: 'space-between', + } + + return createElement('Box', { + flexDirection: layoutMode === 'VERTICAL' ? 'column' : undefined, + justifyContent: figmaAlignItems[primaryAxisAlignItems!] || undefined, + alignItems: figmaAlignItems[counterAxisAlignItems!] || undefined, + gap: itemSpacing + ? this.getValueKeyFromTheme('spaces', `${itemSpacing}px`) || `${itemSpacing}px` + : undefined, + ...this.createPaddingAttributes({ paddingBottom, paddingLeft, paddingRight, paddingTop }), + }) + } +} From 9734654ebd707a54b0dc6d00bff4ac46a109a98a Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:43:08 +0300 Subject: [PATCH 17/48] refactor: move getting attributes to theme-mapping strategy --- .../src/strategy/create-text.strategy.ts | 28 +++------- .../src/strategy/strategy.constants.ts | 4 -- .../src/strategy/theme-mapping.strategy.ts | 56 ++++++++++++++++++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/fragments/fragments-generator/src/strategy/create-text.strategy.ts b/fragments/fragments-generator/src/strategy/create-text.strategy.ts index 0d0e7a1..5921b1b 100644 --- a/fragments/fragments-generator/src/strategy/create-text.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-text.strategy.ts @@ -1,13 +1,10 @@ -import { Paint } from 'figma-js' -import { Text } from 'figma-js' -import { TypeStyle } from 'figma-js' -import { createElement } from 'react' +import type { Paint } from 'figma-js' +import type { Text } from 'figma-js' +import type { TypeStyle } from 'figma-js' -import { toColorOpacityString } from '@atls/figma-utils' -import { toColorString } from '@atls/figma-utils' +import { createElement } from 'react' import { ThemeMappingStrategy } from './theme-mapping.strategy.js' -import { defaultFigmaColor } from './strategy.constants.js' export class CreateTextStrategy extends ThemeMappingStrategy { constructor(theme: Record>) { @@ -15,20 +12,13 @@ export class CreateTextStrategy extends ThemeMappingStrategy { } private createAttributes(style: TypeStyle, fills: readonly Paint[]) { - const { color = defaultFigmaColor, opacity } = fills[0] - - const stringColor = toColorString(color) - const themeColor = opacity ? toColorOpacityString(color, opacity) : stringColor - - const themeLineHeight = ((style.lineHeightPercentFontSize || 100) / 100)?.toFixed(1) + const { fontSize, fontWeight, lineHeightPercentFontSize, lineHeightPx } = style return { - color: this.getValueKeyFromTheme('colors', themeColor) || stringColor, - fontSize: this.getValueKeyFromTheme('fontSizes', `${style.fontSize}px`) || style.fontSize, - fontWeight: - this.getValueKeyFromTheme('fontWeights', `${style.fontWeight}`) || style.fontWeight, - lineHeight: - this.getValueKeyFromTheme('lineHeights', themeLineHeight) || `${style.lineHeightPx}px`, + color: this.getColor(fills), + fontSize: this.getFontSize(fontSize), + fontWeight: this.getFontWeight(fontWeight), + lineHeight: this.getLineHeight(lineHeightPercentFontSize, lineHeightPx), } } diff --git a/fragments/fragments-generator/src/strategy/strategy.constants.ts b/fragments/fragments-generator/src/strategy/strategy.constants.ts index 60afbe5..185c3d6 100644 --- a/fragments/fragments-generator/src/strategy/strategy.constants.ts +++ b/fragments/fragments-generator/src/strategy/strategy.constants.ts @@ -1,7 +1,3 @@ -import { Color } from 'figma-js' - export const THEME_KEY_PREFIX = '$' export const colorsIgnorePatterns = ['button', 'input'] - -export const defaultFigmaColor: Color = { r: 0, g: 0, b: 0, a: 1 } diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index e87ea24..689eb8a 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -1,3 +1,12 @@ +import type { Effect } from 'figma-js' +import type { FrameBase } from 'figma-js' +import type { Paint } from 'figma-js' +import type { TypeStyle } from 'figma-js' + +import { toColorOpacityString } from '@atls/figma-utils' +import { toColorString } from '@atls/figma-utils' +import { toPxString } from '@atls/figma-utils' + import { THEME_KEY_PREFIX } from './strategy.constants.js' import { colorsIgnorePatterns } from './strategy.constants.js' @@ -8,7 +17,7 @@ export class ThemeMappingStrategy { this.theme = theme } - getValueKeyFromTheme(themeKey: string, search: string) { + getValueKeyFromTheme(themeKey: string, search: string): string | undefined { const vars = this.theme[themeKey] if (!vars) { @@ -25,4 +34,49 @@ export class ThemeMappingStrategy { return valueKey ? `${THEME_KEY_PREFIX}${valueKey}` : undefined } + + getColor(fills: readonly Paint[]): string | undefined { + if (!fills[0]) { + return undefined + } + + const { color, opacity } = fills[0] + + if (!color) { + return undefined + } + + if (opacity) { + const colorOpacityString = toColorOpacityString(color, opacity) + + return this.getValueKeyFromTheme('colors', colorOpacityString) || colorOpacityString + } + + const colorString = toColorString(color) + + return this.getValueKeyFromTheme('colors', colorString) || colorString + } + + getFontSize(fontSize: TypeStyle['fontSize']): string { + const fontSizePx = toPxString(fontSize) + + return this.getValueKeyFromTheme('fontSizes', fontSizePx) || fontSizePx + } + + getFontWeight(fontWeight: TypeStyle['fontWeight']): string | TypeStyle['fontWeight'] { + return this.getValueKeyFromTheme('fontWeights', `${fontWeight}`) || fontWeight + } + + getLineHeight( + lineHeightPercentFontSize: TypeStyle['lineHeightPercentFontSize'], + lineHeightPx: TypeStyle['lineHeightPx'] + ): string { + const lineHeight = ((lineHeightPercentFontSize || 100) / 100)?.toFixed(1) + + return this.getValueKeyFromTheme('lineHeights', lineHeight) || toPxString(lineHeightPx) + } + + getFlexDirection(layoutMode: FrameBase['layoutMode']): string | undefined { + return layoutMode === 'VERTICAL' ? 'column' : undefined + } } From 935e76338eaf6c404f6720f163b824b1b943f0e9 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:43:42 +0300 Subject: [PATCH 18/48] feat: add to-px-string util --- utils/utils/src/index.ts | 1 + utils/utils/src/px.ts | 1 + 2 files changed, 2 insertions(+) create mode 100644 utils/utils/src/px.ts diff --git a/utils/utils/src/index.ts b/utils/utils/src/index.ts index 7baf3ef..511d2af 100644 --- a/utils/utils/src/index.ts +++ b/utils/utils/src/index.ts @@ -1,3 +1,4 @@ export * from './colors.js' +export * from './px.js' export * from './radii.js' export * from './walk.js' diff --git a/utils/utils/src/px.ts b/utils/utils/src/px.ts new file mode 100644 index 0000000..132e5ca --- /dev/null +++ b/utils/utils/src/px.ts @@ -0,0 +1 @@ +export const toPxString = (string: string | number) => `${string}px` From b621cb39820c483692a691ffb9c241da3d4743d2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:45:31 +0300 Subject: [PATCH 19/48] refactor: move getting box attributes to theme-mapping strategy --- .../src/strategy/create-box.strategy.ts | 69 ++--------------- .../src/strategy/theme-mapping.strategy.ts | 75 +++++++++++++++++++ 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index bbc5e3f..9fddb93 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -8,58 +8,6 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { super(theme) } - getPaddingFromTheme(padding: number | undefined) { - if (padding === undefined) { - return undefined - } - - return this.getValueKeyFromTheme('spaces', `${padding}px`) || `${padding}px` - } - - createPaddingAttributes({ - paddingTop, - paddingBottom, - paddingLeft, - paddingRight, - }: Record) { - const top = this.getPaddingFromTheme(paddingTop) - const bottom = this.getPaddingFromTheme(paddingBottom) - const left = this.getPaddingFromTheme(paddingLeft) - const right = this.getPaddingFromTheme(paddingRight) - - if ([top, bottom, left, right].every((value) => value && value === top)) { - return { padding: top } - } - - const attributes: Record = {} - - if (top && bottom && top === bottom) { - attributes.paddingY = top - } - - if (left && right && left === right) { - attributes.paddingX = left - } - - if (top && !attributes.paddingY) { - attributes.paddingTop = top - } - - if (bottom && !attributes.paddingY) { - attributes.paddingBottom = bottom - } - - if (left && !attributes.paddingX) { - attributes.paddingLeft = left - } - - if (right && !attributes.paddingX) { - attributes.paddingRight = right - } - - return attributes - } - getImports() { return [`import { Box } from '@ui/layout'`] } @@ -76,19 +24,12 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { paddingTop, } = node - const figmaAlignItems = { - CENTER: 'center', - SPACE_BETWEEN: 'space-between', - } - return createElement('Box', { - flexDirection: layoutMode === 'VERTICAL' ? 'column' : undefined, - justifyContent: figmaAlignItems[primaryAxisAlignItems!] || undefined, - alignItems: figmaAlignItems[counterAxisAlignItems!] || undefined, - gap: itemSpacing - ? this.getValueKeyFromTheme('spaces', `${itemSpacing}px`) || `${itemSpacing}px` - : undefined, - ...this.createPaddingAttributes({ paddingBottom, paddingLeft, paddingRight, paddingTop }), + flexDirection: this.getFlexDirection(layoutMode), + justifyContent: this.getJustifyContent(primaryAxisAlignItems), + alignItems: this.getAlignItems(counterAxisAlignItems), + gap: this.getGap(itemSpacing), + ...this.getPaddings({ paddingBottom, paddingLeft, paddingRight, paddingTop }), }) } } diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index 689eb8a..6f25404 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -79,4 +79,79 @@ export class ThemeMappingStrategy { getFlexDirection(layoutMode: FrameBase['layoutMode']): string | undefined { return layoutMode === 'VERTICAL' ? 'column' : undefined } + + getJustifyContent(primaryAxisAlignItems: FrameBase['primaryAxisAlignItems']): string | undefined { + if (primaryAxisAlignItems === 'SPACE_BETWEEN') { + return 'space-between' + } + + return primaryAxisAlignItems === 'CENTER' ? 'center' : undefined + } + + getAlignItems(counterAxisAlignItems: FrameBase['counterAxisAlignItems']): string | undefined { + return counterAxisAlignItems === 'CENTER' ? 'center' : undefined + } + + getGap(itemSpacing: FrameBase['itemSpacing']): string | undefined { + if (!itemSpacing) { + return undefined + } + + const gapPx = toPxString(itemSpacing) + + return this.getValueKeyFromTheme('spaces', gapPx) || gapPx + } + + getPadding(padding: number | undefined): string | undefined { + if (!padding) { + return undefined + } + + const paddingPx = toPxString(padding) + + return this.getValueKeyFromTheme('spaces', paddingPx) || paddingPx + } + + getPaddings(paddings: Record): Record { + const { paddingTop, paddingBottom, paddingLeft, paddingRight } = paddings + + const top = this.getPadding(paddingTop) + const bottom = this.getPadding(paddingBottom) + const left = this.getPadding(paddingLeft) + const right = this.getPadding(paddingRight) + + const isSameValues = [top, bottom, left, right].every((value) => value && value === top) + + if (isSameValues && top) { + return { padding: top } + } + + const attributes: Record = {} + + if (top && bottom && top === bottom) { + attributes.paddingY = top + } + + if (left && right && left === right) { + attributes.paddingX = left + } + + if (top && !attributes.paddingY) { + attributes.paddingTop = top + } + + if (bottom && !attributes.paddingY) { + attributes.paddingBottom = bottom + } + + if (left && !attributes.paddingX) { + attributes.paddingLeft = left + } + + if (right && !attributes.paddingX) { + attributes.paddingRight = right + } + + return attributes + } } From 0572a6f4f7ca11e8e8bc879d975d780f1ddb2052 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:47:03 +0300 Subject: [PATCH 20/48] feat: add get-border-radius to theme-mapping strategy --- .../src/strategy/create-box.strategy.ts | 2 ++ .../src/strategy/theme-mapping.strategy.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index 9fddb93..011681f 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -22,6 +22,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { paddingLeft, paddingRight, paddingTop, + cornerRadius, } = node return createElement('Box', { @@ -29,6 +30,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { justifyContent: this.getJustifyContent(primaryAxisAlignItems), alignItems: this.getAlignItems(counterAxisAlignItems), gap: this.getGap(itemSpacing), + borderRadius: this.getBorderRadius(cornerRadius), ...this.getPaddings({ paddingBottom, paddingLeft, paddingRight, paddingTop }), }) } diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index 6f25404..cb80dfb 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -102,6 +102,16 @@ export class ThemeMappingStrategy { return this.getValueKeyFromTheme('spaces', gapPx) || gapPx } + getBorderRadius(cornerRadius: FrameBase['cornerRadius']): string | undefined { + if (!cornerRadius) { + return undefined + } + + const borderRadiusPx = toPxString(cornerRadius) + + return this.getValueKeyFromTheme('radii', borderRadiusPx) || borderRadiusPx + } + getPadding(padding: number | undefined): string | undefined { if (!padding) { return undefined From b5aee131f443a286b99edda799d2f51a6ef126b3 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:48:37 +0300 Subject: [PATCH 21/48] feat: add get-border to theme-mapping strategy --- .../src/strategy/create-box.strategy.ts | 3 +++ .../src/strategy/theme-mapping.strategy.ts | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index 011681f..f178918 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -23,6 +23,8 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { paddingRight, paddingTop, cornerRadius, + strokes, + strokeWeight, } = node return createElement('Box', { @@ -30,6 +32,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { justifyContent: this.getJustifyContent(primaryAxisAlignItems), alignItems: this.getAlignItems(counterAxisAlignItems), gap: this.getGap(itemSpacing), + border: this.getBorder(strokes, strokeWeight), borderRadius: this.getBorderRadius(cornerRadius), ...this.getPaddings({ paddingBottom, paddingLeft, paddingRight, paddingTop }), }) diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index cb80dfb..02e4a47 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -112,6 +112,28 @@ export class ThemeMappingStrategy { return this.getValueKeyFromTheme('radii', borderRadiusPx) || borderRadiusPx } + getBorder( + strokes: readonly Paint[], + strokeWeight: FrameBase['strokeWeight'] + ): string | undefined { + if (!strokes[0]) { + return undefined + } + + const { type, color, opacity } = strokes[0] + + if (!color || !strokeWeight) { + return undefined + } + + const strokeType = String(type).toLowerCase() + const strokeColor = opacity ? toColorOpacityString(color, opacity) : toColorString(color) + + const border = `${toPxString(strokeWeight)} ${strokeType} ${strokeColor}` + + return this.getValueKeyFromTheme('borders', border) || border + } + getPadding(padding: number | undefined): string | undefined { if (!padding) { return undefined From d10f334ac524748e296831381207d2bf945568be Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:49:36 +0300 Subject: [PATCH 22/48] feat: add get-shadow to theme-mapping strategy --- .../src/strategy/create-box.strategy.ts | 2 ++ .../src/strategy/theme-mapping.strategy.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index f178918..208b228 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -25,6 +25,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { cornerRadius, strokes, strokeWeight, + effects, } = node return createElement('Box', { @@ -34,6 +35,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { gap: this.getGap(itemSpacing), border: this.getBorder(strokes, strokeWeight), borderRadius: this.getBorderRadius(cornerRadius), + boxShadow: this.getShadow(effects), ...this.getPaddings({ paddingBottom, paddingLeft, paddingRight, paddingTop }), }) } diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index 02e4a47..1cfd815 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -134,6 +134,25 @@ export class ThemeMappingStrategy { return this.getValueKeyFromTheme('borders', border) || border } + getShadow(effects: readonly Effect[]): string | undefined { + const shadows: string[] = [] + + effects.forEach(({ type, radius, offset, color }) => { + if (['DROP_SHADOW', 'INNER_SHADOW'].includes(type) && offset && color) { + const offsetX = toPxString(offset.x) + const offsetY = toPxString(offset.y) + const blurRadius = toPxString(radius) + const colorString = toColorString(color) + + const shadow = `${offsetX} ${offsetY} ${blurRadius} ${colorString}` + + shadows.push(this.getValueKeyFromTheme('shadows', shadow) || shadow) + } + }) + + return shadows.join(', ') + } + getPadding(padding: number | undefined): string | undefined { if (!padding) { return undefined From 6817d5e286a8a996f6d76532c86fc55294525077 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 11 Dec 2024 11:50:39 +0300 Subject: [PATCH 23/48] feat: add background attribute to create box element --- .../fragments-generator/src/strategy/create-box.strategy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index 208b228..d568855 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -25,6 +25,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { cornerRadius, strokes, strokeWeight, + background, effects, } = node @@ -32,6 +33,7 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { flexDirection: this.getFlexDirection(layoutMode), justifyContent: this.getJustifyContent(primaryAxisAlignItems), alignItems: this.getAlignItems(counterAxisAlignItems), + background: this.getColor(background), gap: this.getGap(itemSpacing), border: this.getBorder(strokes, strokeWeight), borderRadius: this.getBorderRadius(cornerRadius), From dcf960f1a0470a46ae634902ca7b746ff40e3650 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 13 Dec 2024 12:09:04 +0300 Subject: [PATCH 24/48] feat: add width and height attributes to create-box-element --- .../fragments-generator/src/strategy/create-box.strategy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index d568855..a31996e 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -27,9 +27,12 @@ export class CreateBoxStrategy extends ThemeMappingStrategy { strokeWeight, background, effects, + absoluteBoundingBox, } = node return createElement('Box', { + width: absoluteBoundingBox.width ? `${absoluteBoundingBox.width}px` : undefined, + height: absoluteBoundingBox.height ? `${absoluteBoundingBox.height}px` : undefined, flexDirection: this.getFlexDirection(layoutMode), justifyContent: this.getJustifyContent(primaryAxisAlignItems), alignItems: this.getAlignItems(counterAxisAlignItems), From daf6397fc59fa10dcc73c636028b8cfded79ed23 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 13 Dec 2024 12:10:46 +0300 Subject: [PATCH 25/48] feat: add id attribute to formatted-message element --- .../fragments-generator/src/strategy/create-text.strategy.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fragments/fragments-generator/src/strategy/create-text.strategy.ts b/fragments/fragments-generator/src/strategy/create-text.strategy.ts index 5921b1b..da170c3 100644 --- a/fragments/fragments-generator/src/strategy/create-text.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-text.strategy.ts @@ -29,7 +29,10 @@ export class CreateTextStrategy extends ThemeMappingStrategy { createElement(node: Text) { const { characters, style, fills } = node - const childrenElement = createElement('FormattedMessage', { defaultMessage: characters }) + const childrenElement = createElement('FormattedMessage', { + id: characters?.replaceAll(' ', '_').toLowerCase() || 'text', + defaultMessage: characters, + }) return createElement('Text', this.createAttributes(style, fills), childrenElement) } From 007370349af889a9648005a0ae82612d9f0334d6 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 13 Dec 2024 12:11:32 +0300 Subject: [PATCH 26/48] feat: add text-align attribute to create-text-element --- .../src/strategy/create-text.strategy.ts | 9 +++++++-- .../src/strategy/theme-mapping.strategy.ts | 14 ++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/fragments/fragments-generator/src/strategy/create-text.strategy.ts b/fragments/fragments-generator/src/strategy/create-text.strategy.ts index da170c3..1395a89 100644 --- a/fragments/fragments-generator/src/strategy/create-text.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-text.strategy.ts @@ -12,13 +12,18 @@ export class CreateTextStrategy extends ThemeMappingStrategy { } private createAttributes(style: TypeStyle, fills: readonly Paint[]) { - const { fontSize, fontWeight, lineHeightPercentFontSize, lineHeightPx } = style + const fontSize = style?.fontSize || undefined + const fontWeight = style?.fontWeight || undefined + const lineHeightPercentFontSize = style?.lineHeightPercentFontSize || undefined + const lineHeightPx = style?.lineHeightPx || undefined + const textAlignHorizontal = style?.textAlignHorizontal || undefined return { color: this.getColor(fills), - fontSize: this.getFontSize(fontSize), + fontSize: fontSize ? this.getFontSize(fontSize) : undefined, fontWeight: this.getFontWeight(fontWeight), lineHeight: this.getLineHeight(lineHeightPercentFontSize, lineHeightPx), + textAlign: this.getTextAlign(textAlignHorizontal), } } diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index 1cfd815..6e31fac 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -67,13 +67,19 @@ export class ThemeMappingStrategy { return this.getValueKeyFromTheme('fontWeights', `${fontWeight}`) || fontWeight } + getTextAlign(textAlignHorizontal: TypeStyle['textAlignHorizontal']): string | undefined { + return textAlignHorizontal === 'LEFT' ? undefined : textAlignHorizontal.toLocaleLowerCase() + } + getLineHeight( lineHeightPercentFontSize: TypeStyle['lineHeightPercentFontSize'], - lineHeightPx: TypeStyle['lineHeightPx'] - ): string { + lineHeightPx?: TypeStyle['lineHeightPx'] + ): string | undefined { const lineHeight = ((lineHeightPercentFontSize || 100) / 100)?.toFixed(1) - return this.getValueKeyFromTheme('lineHeights', lineHeight) || toPxString(lineHeightPx) + const lineHeightPxString = lineHeightPx ? toPxString(lineHeightPx) : undefined + + return this.getValueKeyFromTheme('lineHeights', lineHeight) || lineHeightPxString } getFlexDirection(layoutMode: FrameBase['layoutMode']): string | undefined { @@ -150,7 +156,7 @@ export class ThemeMappingStrategy { } }) - return shadows.join(', ') + return shadows.length ? shadows.join(', ') : undefined } getPadding(padding: number | undefined): string | undefined { From e72805d7914a94d4e9e6d7fae0911ba35bebc429 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 13 Dec 2024 12:12:55 +0300 Subject: [PATCH 27/48] feat: add create-button strategy --- .../src/strategy/create-button.strategy.ts | 21 +++++++++++++++++++ .../src/strategy/create-fragment.strategy.ts | 21 +++++++++++++++++++ .../src/strategy/strategy.interfaces.ts | 7 +++++++ 3 files changed, 49 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/create-button.strategy.ts diff --git a/fragments/fragments-generator/src/strategy/create-button.strategy.ts b/fragments/fragments-generator/src/strategy/create-button.strategy.ts new file mode 100644 index 0000000..de708e7 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/create-button.strategy.ts @@ -0,0 +1,21 @@ +import { Instance } from 'figma-js' +import { Fragment } from 'react' +import { createElement } from 'react' + +import { ComponentProperties } from './strategy.interfaces.js' + +export class CreateButtonStrategy { + getImports() { + return [`import { Button } from '@ui/button'`] + } + + createElement(node: Instance) { + if ('componentProperties' in node) { + const style = (node.componentProperties as ComponentProperties).Style + + return createElement('Button', { variant: style?.value.toString().toLocaleLowerCase() }) + } + + return createElement(Fragment) + } +} diff --git a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts b/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts index 2b0b227..8ca5f05 100644 --- a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts @@ -10,10 +10,12 @@ import { cloneElement } from 'react' import { createElement } from 'react' import { isFrame } from '@atls/figma-utils' +import { isInstance } from '@atls/figma-utils' import { isText } from '@atls/figma-utils' import { walk } from '@atls/figma-utils' import { CreateBoxStrategy } from './create-box.strategy.js' +import { CreateButtonStrategy } from './create-button.strategy.js' import { CreateTextStrategy } from './create-text.strategy.js' export class CreateFragmentStrategy { @@ -21,10 +23,12 @@ export class CreateFragmentStrategy { private text: CreateTextStrategy private box: CreateBoxStrategy + private button: CreateButtonStrategy constructor(theme: Record>) { this.text = new CreateTextStrategy(theme) this.box = new CreateBoxStrategy(theme) + this.button = new CreateButtonStrategy() } private createFragmentElement(elements: TreeElement[]) { @@ -65,6 +69,23 @@ export class CreateFragmentStrategy { const imports = new Set() walk(nodes, (node) => { + if (isInstance(node)) { + if (node.name.includes('Button')) { + this.button.getImports().forEach((value) => imports.add(value)) + + const buttonChildren = node?.children.map((node) => node.id) || [] + const buttonDeepChildren = isInstance(node.children[0]) + ? node.children[0].children.map((node) => node.id) || [] + : [] + + this.elements[node.id] = { + element: this.button.createElement(node), + childrenIds: [...buttonChildren, ...buttonDeepChildren], + parentId: this.findParentId(node.id), + } + } + } + if (isText(node)) { this.text.getImports().forEach((value) => imports.add(value)) diff --git a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts b/fragments/fragments-generator/src/strategy/strategy.interfaces.ts index 17d45de..f70cfa6 100644 --- a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts +++ b/fragments/fragments-generator/src/strategy/strategy.interfaces.ts @@ -10,3 +10,10 @@ export interface CreteFragmentResult { fragment: string imports: Array } + +export interface ComponentProperty { + type: 'COMPONENT' | 'COMPONENT_SET' + value: boolean | string +} + +export type ComponentProperties = Record From 1491ea8cc7e45438776202904b43064d91d3b509 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 13 Dec 2024 12:14:25 +0300 Subject: [PATCH 28/48] feat: add base create-input strategy --- .../src/strategy/create-input.strategy.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/create-input.strategy.ts diff --git a/fragments/fragments-generator/src/strategy/create-input.strategy.ts b/fragments/fragments-generator/src/strategy/create-input.strategy.ts new file mode 100644 index 0000000..905d257 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/create-input.strategy.ts @@ -0,0 +1,21 @@ +import { Instance } from 'figma-js' +import { Fragment } from 'react' +import { createElement } from 'react' + +import { ComponentProperties } from './strategy.interfaces.js' + +export class CreateInputStrategy { + getImports() { + return [`import { Input } from '@ui/input'`] + } + + createElement(node: Instance) { + if ('componentProperties' in node) { + const style = (node.componentProperties as ComponentProperties).Style + + return createElement('Input', { variant: style?.value.toString().toLocaleLowerCase() }) + } + + return createElement(Fragment) + } +} From 2d86b022b854211e853c403b8ffbd18082f92064 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 09:59:43 +0300 Subject: [PATCH 29/48] feat: add create-input strategy to create-fragment --- .../src/strategy/create-fragment.strategy.ts | 35 +++++++++++++++---- .../src/strategy/create-input.strategy.ts | 20 +++++++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts b/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts index 8ca5f05..5cec2a0 100644 --- a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts @@ -16,19 +16,25 @@ import { walk } from '@atls/figma-utils' import { CreateBoxStrategy } from './create-box.strategy.js' import { CreateButtonStrategy } from './create-button.strategy.js' +import { CreateInputStrategy } from './create-input.strategy.js' import { CreateTextStrategy } from './create-text.strategy.js' export class CreateFragmentStrategy { private elements: Record = {} private text: CreateTextStrategy + private box: CreateBoxStrategy + private button: CreateButtonStrategy + private input: CreateInputStrategy + constructor(theme: Record>) { this.text = new CreateTextStrategy(theme) this.box = new CreateBoxStrategy(theme) this.button = new CreateButtonStrategy() + this.input = new CreateInputStrategy() } private createFragmentElement(elements: TreeElement[]) { @@ -67,20 +73,35 @@ export class CreateFragmentStrategy { execute(nodes: FileNodesResponse['nodes']): CreteFragmentResult { const imports = new Set() + const ignoreNodes = new Set() walk(nodes, (node) => { + if (ignoreNodes.has(node.id)) { + return + } + if (isInstance(node)) { - if (node.name.includes('Button')) { + if (node.name.toLowerCase().includes('button')) { this.button.getImports().forEach((value) => imports.add(value)) - const buttonChildren = node?.children.map((node) => node.id) || [] - const buttonDeepChildren = isInstance(node.children[0]) - ? node.children[0].children.map((node) => node.id) || [] - : [] + const buttonChildren = new Set() + walk(node?.children, (child) => buttonChildren.add(child.id)) this.elements[node.id] = { element: this.button.createElement(node), - childrenIds: [...buttonChildren, ...buttonDeepChildren], + childrenIds: Array.from(buttonChildren), + parentId: this.findParentId(node.id), + } + } + + if (node.name.toLowerCase().includes('input')) { + this.input.getImports().forEach((value) => imports.add(value)) + + walk(node?.children, (child) => ignoreNodes.add(child.id)) + + this.elements[node.id] = { + element: this.input.createElement(node), + childrenIds: [], parentId: this.findParentId(node.id), } } @@ -101,7 +122,7 @@ export class CreateFragmentStrategy { this.elements[node.id] = { element: this.box.createElement(node), - childrenIds: node?.children.map((node) => node.id) || [], + childrenIds: node?.children.map((child) => child.id) || [], parentId: this.findParentId(node.id), } } diff --git a/fragments/fragments-generator/src/strategy/create-input.strategy.ts b/fragments/fragments-generator/src/strategy/create-input.strategy.ts index 905d257..fee2a44 100644 --- a/fragments/fragments-generator/src/strategy/create-input.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-input.strategy.ts @@ -1,7 +1,11 @@ import { Instance } from 'figma-js' +import { Text } from 'figma-js' import { Fragment } from 'react' import { createElement } from 'react' +import { isFrame } from '@atls/figma-utils' +import { isText } from '@atls/figma-utils' + import { ComponentProperties } from './strategy.interfaces.js' export class CreateInputStrategy { @@ -11,9 +15,21 @@ export class CreateInputStrategy { createElement(node: Instance) { if ('componentProperties' in node) { - const style = (node.componentProperties as ComponentProperties).Style + const type = (node.componentProperties as ComponentProperties).Type + const field = node.children.find((child) => child.name?.toLocaleLowerCase() === 'field') + + let placeholder: string | undefined + + if (field && isFrame(field)) { + const textNode = field.children.find((child) => isText(child)) as Text | undefined + + placeholder = textNode ? textNode.characters : undefined + } - return createElement('Input', { variant: style?.value.toString().toLocaleLowerCase() }) + return createElement('Input', { + variant: type?.value.toString().toLocaleLowerCase(), + placeholder, + }) } return createElement(Fragment) From 93af260de850253d6081e7fac596f24130ed8826 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:01:30 +0300 Subject: [PATCH 30/48] feat: add theme-mapping strategy test --- .../src/strategy/tests/tests.constants.ts | 44 ++++ .../src/strategy/tests/theme-mapping.test.ts | 208 ++++++++++++++++++ .../src/strategy/theme-mapping.strategy.ts | 24 +- 3 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 fragments/fragments-generator/src/strategy/tests/tests.constants.ts create mode 100644 fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts diff --git a/fragments/fragments-generator/src/strategy/tests/tests.constants.ts b/fragments/fragments-generator/src/strategy/tests/tests.constants.ts new file mode 100644 index 0000000..78c8656 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/tests/tests.constants.ts @@ -0,0 +1,44 @@ +export const theme = { + colors: { + white: 'rgba(255, 255, 255, 1)', + black: 'rgba(39, 39, 42, 1)', + }, + borders: { + black: '1px solid rgba(0, 0, 0, 0.10)', + bunker: '1px solid rgba(24, 24, 27, 0.05)', + white: '1px solid rgba(255, 255, 255, 0.10)', + }, + fontSizes: { + small: '12px', + normal: '19px', + medium: '36px', + large: '64px', + }, + fontWeights: { + light: '300', + regular: '400', + medium: '500', + bold: '600', + large: '700', + }, + lineHeights: { + normal: '1.2', + medium: '1.6', + large: '2.0', + }, + radii: { + small: '2px', + normal: '7px', + medium: '16px', + large: '99px', + }, + shadows: { + black: '0px 0px 1px rgba(0, 0, 0, 0.12)', + bunker: '0px 1px 1px rgba(0, 0, 0, 0.16)', + }, + spaces: { + small: '2px', + normal: '7px', + medium: '16px', + }, +} diff --git a/fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts b/fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts new file mode 100644 index 0000000..8d1577b --- /dev/null +++ b/fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts @@ -0,0 +1,208 @@ +import { Effect } from 'figma-js' +import { Paint } from 'figma-js' + +import { THEME_KEY_PREFIX } from '../strategy.constants.js' +import { ThemeMappingStrategy } from '../theme-mapping.strategy.js' +import { theme } from './tests.constants.js' + +describe('ThemeMappingStrategy', () => { + let strategy: ThemeMappingStrategy + + beforeEach(() => { + strategy = new ThemeMappingStrategy(theme) + }) + + describe('getValueKeyFromTheme', () => { + it('returns the key from the theme when a matching value is found', () => { + expect(strategy.getValueKeyFromTheme('colors', theme.colors.white)).toBe( + `${THEME_KEY_PREFIX}white` + ) + }) + + it('returns undefined when no matching value is found', () => { + expect(strategy.getValueKeyFromTheme('colors', 'rgba(0, 0, 0, 1)')).toBeUndefined() + + expect(strategy.getValueKeyFromTheme('texts', 'test')).toBeUndefined() + }) + }) + + describe('getColor', () => { + it('returns the color key from the theme when opacity is provided', () => { + const fills: Paint[] = [ + { color: { r: 1, g: 1, b: 1, a: 1 }, blendMode: 'COLOR', type: 'SOLID' }, + ] + + expect(strategy.getColor(fills)).toBe(`${THEME_KEY_PREFIX}white`) + }) + + it('returns undefined when no fills are provided', () => { + expect(strategy.getColor([])).toBeUndefined() + }) + }) + + describe('getFontSize', () => { + it('returns the font size key from the theme', () => { + expect(strategy.getFontSize(parseInt(theme.fontSizes.small, 10))).toBe( + `${THEME_KEY_PREFIX}small` + ) + + expect(strategy.getFontSize(14)).toBe('14px') + }) + }) + + describe('getFontWeight', () => { + it('returns the font weight key from the theme', () => { + expect(strategy.getFontWeight(600)).toBe(`${THEME_KEY_PREFIX}bold`) + + expect(strategy.getFontWeight(900)).toBe(900) + }) + }) + + describe('getTextAlign', () => { + it('returns undefined for LEFT alignment', () => { + expect(strategy.getTextAlign('LEFT')).toBeUndefined() + }) + + it('returns lowercase text alignment for other values', () => { + expect(strategy.getTextAlign('CENTER')).toBe('center') + expect(strategy.getTextAlign('RIGHT')).toBe('right') + expect(strategy.getTextAlign('JUSTIFIED')).toBe('justify') + }) + }) + + describe('getLineHeight', () => { + it('returns the line height key from the theme', () => { + expect(strategy.getLineHeight(150, 20)).toBe('20px') + }) + }) + + describe('getFlexDirection', () => { + it('returns column for VERTICAL layout mode', () => { + expect(strategy.getFlexDirection('VERTICAL')).toBe('column') + }) + + it('returns undefined for other layout modes', () => { + expect(strategy.getFlexDirection('HORIZONTAL')).toBeUndefined() + expect(strategy.getFlexDirection('NONE')).toBeUndefined() + }) + }) + + describe('getJustifyContent', () => { + it('returns justify content for other SPACE_BETWEEN and CENTER values', () => { + expect(strategy.getJustifyContent('SPACE_BETWEEN')).toBe('space-between') + expect(strategy.getJustifyContent('CENTER')).toBe('center') + }) + + it('returns undefined for other alignments', () => { + expect(strategy.getJustifyContent('MAX')).toBeUndefined() + expect(strategy.getJustifyContent('MIN')).toBeUndefined() + }) + }) + + describe('getAlignItems', () => { + it('returns center for CENTER alignment', () => { + expect(strategy.getAlignItems('CENTER')).toBe('center') + }) + + it('returns undefined for other alignments', () => { + expect(strategy.getAlignItems('MAX')).toBeUndefined() + expect(strategy.getAlignItems('MIN')).toBeUndefined() + }) + }) + + describe('getGap', () => { + it('returns the gap value from the theme', () => { + expect(strategy.getGap(parseInt(theme.spaces.small, 10))).toBe(`${THEME_KEY_PREFIX}small`) + expect(strategy.getGap(8)).toBe('8px') + }) + + it('returns undefined when no gap is provided', () => { + expect(strategy.getGap(undefined)).toBeUndefined() + }) + }) + + describe('getBorderRadius', () => { + it('returns the border radius value from the theme', () => { + expect(strategy.getBorderRadius(parseInt(theme.radii.small, 10))).toBe( + `${THEME_KEY_PREFIX}small` + ) + expect(strategy.getBorderRadius(5)).toBe('5px') + }) + + it('returns undefined when no border radius is provided', () => { + expect(strategy.getBorderRadius(undefined)).toBeUndefined() + }) + }) + + describe('getBorder', () => { + it('returns the border value with weight, type, and color', () => { + const strokes: Paint[] = [ + { color: { r: 1, g: 1, b: 1, a: 1 }, blendMode: 'COLOR', type: 'SOLID', opacity: 0.1 }, + ] + + expect(strategy.getBorder(strokes, 1)).toBe(`${THEME_KEY_PREFIX}white`) + }) + + it('returns undefined when strokes are not provided', () => { + expect(strategy.getBorder([], 1)).toBeUndefined() + }) + }) + + describe('getShadow', () => { + it('returns the shadow value for DROP_SHADOW effects', () => { + const effects: Effect[] = [ + { + type: 'DROP_SHADOW', + radius: 1, + offset: { x: 0, y: 0 }, + color: { r: 0, g: 0, b: 0, a: 0.12 }, + visible: true, + }, + ] + + expect(strategy.getShadow(effects)).toBe(`${THEME_KEY_PREFIX}black`) + }) + + it('returns undefined when no valid effects are provided', () => { + expect(strategy.getShadow([])).toBeUndefined() + }) + }) + + describe('getPadding', () => { + it('returns the padding value from the theme', () => { + expect(strategy.getPadding(parseInt(theme.spaces.small, 10))).toBe(`${THEME_KEY_PREFIX}small`) + expect(strategy.getPadding(10)).toBe('10px') + }) + + it('returns undefined when no padding is provided', () => { + expect(strategy.getPadding(undefined)).toBeUndefined() + }) + }) + + describe('getPaddings', () => { + it('returns unified padding when all sides are the same', () => { + const paddings = { paddingTop: 10, paddingBottom: 10, paddingLeft: 10, paddingRight: 10 } + expect(strategy.getPaddings(paddings)).toEqual({ padding: '10px' }) + }) + + it('returns individual paddings when values differ', () => { + const paddings = { + paddingTop: 10, + paddingBottom: 20, + paddingLeft: 30, + paddingRight: 40, + } + expect(strategy.getPaddings(paddings)).toEqual({ + paddingTop: '10px', + paddingBottom: '20px', + paddingLeft: '30px', + paddingRight: '40px', + }) + }) + + it('returns combined paddings when applicable', () => { + const paddings = { paddingTop: 10, paddingBottom: 10, paddingLeft: 20, paddingRight: 20 } + expect(strategy.getPaddings(paddings)).toEqual({ paddingY: '10px', paddingX: '20px' }) + }) + }) +}) diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts index 6e31fac..5980415 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts @@ -21,7 +21,7 @@ export class ThemeMappingStrategy { const vars = this.theme[themeKey] if (!vars) { - return + return undefined } const valueKey = Object.entries(vars).find(([key, value]) => { @@ -68,7 +68,16 @@ export class ThemeMappingStrategy { } getTextAlign(textAlignHorizontal: TypeStyle['textAlignHorizontal']): string | undefined { - return textAlignHorizontal === 'LEFT' ? undefined : textAlignHorizontal.toLocaleLowerCase() + switch (textAlignHorizontal) { + case 'RIGHT': + return 'right' + case 'CENTER': + return 'center' + case 'JUSTIFIED': + return 'justify' + default: + return undefined + } } getLineHeight( @@ -87,11 +96,14 @@ export class ThemeMappingStrategy { } getJustifyContent(primaryAxisAlignItems: FrameBase['primaryAxisAlignItems']): string | undefined { - if (primaryAxisAlignItems === 'SPACE_BETWEEN') { - return 'space-between' + switch (primaryAxisAlignItems) { + case 'SPACE_BETWEEN': + return 'space-between' + case 'CENTER': + return 'center' + default: + return undefined } - - return primaryAxisAlignItems === 'CENTER' ? 'center' : undefined } getAlignItems(counterAxisAlignItems: FrameBase['counterAxisAlignItems']): string | undefined { From 065e7c69ab8a0948d50a8c4ecae9e17c6b5e9318 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:01:57 +0300 Subject: [PATCH 31/48] feat: add create-box strategy test --- .../src/strategy/create-box.strategy.ts | 4 - .../src/strategy/tests/create-box.test.ts | 104 ++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 fragments/fragments-generator/src/strategy/tests/create-box.test.ts diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategy/create-box.strategy.ts index a31996e..6d47b45 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-box.strategy.ts @@ -4,10 +4,6 @@ import { createElement } from 'react' import { ThemeMappingStrategy } from './theme-mapping.strategy.js' export class CreateBoxStrategy extends ThemeMappingStrategy { - constructor(theme: Record>) { - super(theme) - } - getImports() { return [`import { Box } from '@ui/layout'`] } diff --git a/fragments/fragments-generator/src/strategy/tests/create-box.test.ts b/fragments/fragments-generator/src/strategy/tests/create-box.test.ts new file mode 100644 index 0000000..a632de0 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/tests/create-box.test.ts @@ -0,0 +1,104 @@ +import { Frame } from 'figma-js' +import { createElement } from 'react' + +import { CreateBoxStrategy } from '../create-box.strategy.js' +import { theme } from './tests.constants.js' + +jest.mock('react', () => ({ + createElement: jest.fn(), +})) + +describe('CreateBoxStrategy', () => { + let strategy: CreateBoxStrategy + + beforeEach(() => { + strategy = new CreateBoxStrategy(theme) + }) + + describe('getImports', () => { + it('returns the Box import statement', () => { + expect(strategy.getImports()).toEqual([`import { Box } from '@ui/layout'`]) + }) + }) + + describe('createElement', () => { + it('creates a Box element with appropriate props', () => { + const mockNode: Partial = { + layoutMode: 'HORIZONTAL', + itemSpacing: 16, + counterAxisAlignItems: 'CENTER', + primaryAxisAlignItems: 'CENTER', + paddingBottom: 10, + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + cornerRadius: 4, + strokes: [ + { type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 0.8, blendMode: 'COLOR' }, + ], + strokeWeight: 1, + background: [{ color: { r: 1, g: 1, b: 1, a: 1 }, type: 'SOLID', blendMode: 'COLOR' }], + effects: [ + { + type: 'DROP_SHADOW', + radius: 4, + offset: { x: 2, y: 2 }, + color: { r: 1, g: 1, b: 1, a: 1 }, + visible: true, + }, + ], + absoluteBoundingBox: { x: 0, y: 0, width: 300, height: 100 }, + } + + strategy.createElement(mockNode as Frame) + + expect(createElement).toHaveBeenCalledWith('Box', { + width: '300px', + height: '100px', + flexDirection: undefined, + justifyContent: 'center', + alignItems: 'center', + background: '$white', + gap: '$medium', + border: '1px solid rgba(255, 255, 255, 0.80)', + borderRadius: '4px', + boxShadow: '2px 2px 4px rgba(255, 255, 255, 1)', + padding: '10px', + }) + }) + + it('handles missing node properties gracefully', () => { + const mockNode: Partial = { + layoutMode: undefined, + itemSpacing: undefined, + counterAxisAlignItems: undefined, + primaryAxisAlignItems: undefined, + paddingBottom: undefined, + paddingLeft: undefined, + paddingRight: undefined, + paddingTop: undefined, + cornerRadius: undefined, + strokes: [], + strokeWeight: 2, + background: [], + effects: [], + absoluteBoundingBox: { x: 0, y: 0, width: 300, height: 100 }, + } + + strategy.createElement(mockNode as Frame) + + expect(createElement).toHaveBeenCalledWith('Box', { + width: '300px', + height: '100px', + flexDirection: undefined, + justifyContent: undefined, + alignItems: undefined, + background: undefined, + gap: undefined, + border: undefined, + borderRadius: undefined, + boxShadow: undefined, + }) + }) + }) +}) From dcaf889ab3dc0a5e5740a6b3e126ab4aaeaff0b4 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:02:16 +0300 Subject: [PATCH 32/48] feat: add create-text strategy test --- .../src/strategy/create-text.strategy.ts | 4 - .../src/strategy/tests/create-text.test.ts | 95 +++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 fragments/fragments-generator/src/strategy/tests/create-text.test.ts diff --git a/fragments/fragments-generator/src/strategy/create-text.strategy.ts b/fragments/fragments-generator/src/strategy/create-text.strategy.ts index 1395a89..908f2c4 100644 --- a/fragments/fragments-generator/src/strategy/create-text.strategy.ts +++ b/fragments/fragments-generator/src/strategy/create-text.strategy.ts @@ -7,10 +7,6 @@ import { createElement } from 'react' import { ThemeMappingStrategy } from './theme-mapping.strategy.js' export class CreateTextStrategy extends ThemeMappingStrategy { - constructor(theme: Record>) { - super(theme) - } - private createAttributes(style: TypeStyle, fills: readonly Paint[]) { const fontSize = style?.fontSize || undefined const fontWeight = style?.fontWeight || undefined diff --git a/fragments/fragments-generator/src/strategy/tests/create-text.test.ts b/fragments/fragments-generator/src/strategy/tests/create-text.test.ts new file mode 100644 index 0000000..8238dab --- /dev/null +++ b/fragments/fragments-generator/src/strategy/tests/create-text.test.ts @@ -0,0 +1,95 @@ +import { Text } from 'figma-js' +import { createElement } from 'react' + +import { CreateTextStrategy } from '../create-text.strategy.js' +import { theme } from './tests.constants.js' + +jest.mock('react', () => ({ + createElement: jest.fn(), +})) + +describe('CreateTextStrategy', () => { + let strategy: CreateTextStrategy + + beforeEach(() => { + strategy = new CreateTextStrategy(theme) + }) + + describe('getImports', () => { + it('returns the Text and FormattedMessage imports', () => { + expect(strategy.getImports()).toEqual([ + `import { Text } from '@ui/text'`, + `import { FormattedMessage } from 'react-intl'`, + ]) + }) + }) + + describe('createElement', () => { + it('creates a Text element with the correct attributes and children', () => { + const node = { + characters: 'Test text', + style: { + fontSize: 12, + fontWeight: 400, + lineHeightPercentFontSize: 120, + lineHeightPx: 16, + textAlignHorizontal: 'RIGHT', + }, + fills: [ + { type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, blendMode: 'COLOR' }, + ], + } + jest.spyOn(strategy, 'getColor').mockReturnValue('$white') + jest.spyOn(strategy, 'getFontSize').mockReturnValue('$small') + jest.spyOn(strategy, 'getFontWeight').mockReturnValue('$regular') + jest.spyOn(strategy, 'getLineHeight').mockReturnValue('16px') + jest.spyOn(strategy, 'getTextAlign').mockReturnValue('right') + + strategy.createElement(node as never as Text) + + expect(createElement).toHaveBeenCalledWith('FormattedMessage', { + id: 'test_text', + defaultMessage: 'Test text', + }) + + expect(createElement).toHaveBeenLastCalledWith( + 'Text', + { + color: '$white', + fontSize: '$small', + fontWeight: '$regular', + lineHeight: '16px', + textAlign: 'right', + }, + undefined + ) + }) + + it('handles missing characters gracefully', () => { + const node = { + characters: undefined, + style: {}, + fills: [], + } + + strategy.createElement(node as never as Text) + + expect(createElement).toHaveBeenCalledWith('FormattedMessage', { + id: 'text', + defaultMessage: undefined, + }) + + expect(createElement).toHaveBeenCalledWith( + 'Text', + { + color: undefined, + fontSize: undefined, + fontWeight: undefined, + lineHeight: undefined, + textAlign: undefined, + }, + undefined + ) + }) + }) +}) From a34be588293b5f55bec97dd65087de84e798053d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:02:50 +0300 Subject: [PATCH 33/48] feat: add create-button strategy test --- .../src/strategy/tests/create-button.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/tests/create-button.test.ts diff --git a/fragments/fragments-generator/src/strategy/tests/create-button.test.ts b/fragments/fragments-generator/src/strategy/tests/create-button.test.ts new file mode 100644 index 0000000..c79c5d6 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/tests/create-button.test.ts @@ -0,0 +1,56 @@ +import { Fragment } from 'react' +import { createElement } from 'react' + +import { CreateButtonStrategy } from '../create-button.strategy.js' + +jest.mock('react', () => ({ + createElement: jest.fn(), +})) + +describe('CreateButtonStrategy', () => { + let strategy: CreateButtonStrategy + + beforeEach(() => { + strategy = new CreateButtonStrategy() + }) + + describe('getImports', () => { + it('returns the correct import statement', () => { + expect(strategy.getImports()).toEqual([`import { Button } from '@ui/button'`]) + }) + }) + + describe('createElement', () => { + it('creates a Button element with the correct variant', () => { + const node = { + componentProperties: { + Style: { value: 'Primary' }, + }, + } as any + + strategy.createElement(node) + + expect(createElement).toHaveBeenCalledWith('Button', { + variant: 'primary', + }) + }) + + it('creates a Fragment element when componentProperties are missing', () => { + const node = {} as any + + strategy.createElement(node) + + expect(createElement).toHaveBeenCalledWith(Fragment) + }) + + it('creates a Fragment element when Style is not defined in componentProperties', () => { + const node = { + componentProperties: {}, + } as any + + strategy.createElement(node) + + expect(createElement).toHaveBeenCalledWith(Fragment) + }) + }) +}) From 5f2ba769d7d940d6d4a87e82532e19e753c430c7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:03:01 +0300 Subject: [PATCH 34/48] feat: add create-input strategy test --- .../src/strategy/tests/create-input.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 fragments/fragments-generator/src/strategy/tests/create-input.test.ts diff --git a/fragments/fragments-generator/src/strategy/tests/create-input.test.ts b/fragments/fragments-generator/src/strategy/tests/create-input.test.ts new file mode 100644 index 0000000..b5570c2 --- /dev/null +++ b/fragments/fragments-generator/src/strategy/tests/create-input.test.ts @@ -0,0 +1,71 @@ +import { Fragment } from 'react' +import { createElement } from 'react' + +import { CreateInputStrategy } from '../create-input.strategy.js' + +jest.mock('react', () => ({ + createElement: jest.fn(), +})) + +describe('CreateInputStrategy', () => { + let strategy: CreateInputStrategy + + beforeEach(() => { + strategy = new CreateInputStrategy() + jest.clearAllMocks() + }) + + describe('getImports', () => { + it('returns the correct import statement', () => { + expect(strategy.getImports()).toEqual([`import { Input } from '@ui/input'`]) + }) + }) + + describe('createElement', () => { + it('creates an Input element with a variant and placeholder', () => { + const node = { + componentProperties: { + Type: { value: 'Primary' }, + }, + children: [ + { + name: 'Field', + type: 'FRAME', + children: [{ type: 'TEXT', characters: 'Test text' }], + }, + ], + } as any + + strategy.createElement(node) + + expect(createElement).toHaveBeenCalledWith('Input', { + variant: 'primary', + placeholder: 'Test text', + }) + }) + + it('creates an Input element without a placeholder when no valid field is found', () => { + const node = { + componentProperties: { + Type: { value: 'Primary' }, + }, + children: [], + } as any + + strategy.createElement(node) + + expect(createElement).toHaveBeenCalledWith('Input', { + variant: 'primary', + placeholder: undefined, + }) + }) + + it('creates a Fragment element when componentProperties are missing', () => { + const node = {} as any + + strategy.createElement(node) + + expect(createElement).toHaveBeenCalledWith(Fragment) + }) + }) +}) From 41d73714b40ab71d257a3ac015c31dafdc49872f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:04:04 +0300 Subject: [PATCH 35/48] feat: add is-instance to figma utils --- utils/utils/src/walk.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/utils/utils/src/walk.ts b/utils/utils/src/walk.ts index f402526..f5230b4 100644 --- a/utils/utils/src/walk.ts +++ b/utils/utils/src/walk.ts @@ -1,6 +1,7 @@ -import { Node } from 'figma-js' -import { Text } from 'figma-js' -import { Frame } from 'figma-js' +import { Node } from 'figma-js' +import { Text } from 'figma-js' +import { Frame } from 'figma-js' +import { Instance } from 'figma-js' const isEmpty = (node: any) => !node || Object.keys(node).length === 0 @@ -8,6 +9,8 @@ export const isText = (node: Node): node is Text => node.type === 'TEXT' export const isFrame = (node: Node): node is Frame => node.type === 'FRAME' +export const isInstance = (node: Node): node is Instance => node.type === 'INSTANCE' + export const walk = (targetNode: any, cb: (node: any) => any) => { if (isEmpty(targetNode) || typeof targetNode === 'string' || typeof targetNode === 'number') { return From 50a92274357d0026c90b2a3e5941101f9cb83c8c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:07:19 +0300 Subject: [PATCH 36/48] feat: add question to get figma access token to figma-cli --- fragments/fragments-cli/src/index.ts | 34 +++++++++++++--------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/fragments/fragments-cli/src/index.ts b/fragments/fragments-cli/src/index.ts index 95a0460..0361fc6 100644 --- a/fragments/fragments-cli/src/index.ts +++ b/fragments/fragments-cli/src/index.ts @@ -25,22 +25,20 @@ if (options.verbose) { if (!fileId) { logger.error('fileId', 'Figma file id required.') } else { - // const readline = createInterface({ - // input: process.stdin, - // output: process.stdout, - // }) - - // readline.question(`Enter your Figma access token:\n`, (id) => { - // if (!id || id === '') throw Error('ID must not be empty') - // // eslint-disable-next-line dot-notation - - // readline.close() - - // }) - - process.env['FIGMA_TOKEN'] = 'secret' - - run(fileId, options.nodeId, options.output, options.theme) - .then(() => logger.info('info', 'Fragments successful generated')) - .catch((error) => logger.error('error', error.message)) + const readline = createInterface({ + input: process.stdin, + output: process.stdout, + }) + + readline.question(`Enter your Figma access token:\n`, (id) => { + if (!id || id === '') throw Error('ID must not be empty') + // eslint-disable-next-line dot-notation + process.env['FIGMA_TOKEN'] = id + + readline.close() + + run(fileId, options.nodeId, options.output, options.theme) + .then(() => logger.info('info', 'Fragments successful generated')) + .catch((error) => logger.error('error', error)) + }) } From db09f90574819ac0ea8aed39cd8ab842d627d381 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 10:08:55 +0300 Subject: [PATCH 37/48] fix: eslint validation --- utils/file/src/process-file.util.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/utils/file/src/process-file.util.ts b/utils/file/src/process-file.util.ts index e46319e..c923903 100644 --- a/utils/file/src/process-file.util.ts +++ b/utils/file/src/process-file.util.ts @@ -16,13 +16,15 @@ export const processFile = (filePath: string): any => { assert.ok(code, `Could not process the code with path ${filePath}. Please try again`) const module = { exports: {} } - const exports = module.exports + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { exports } = module + // eslint-disable-next-line @typescript-eslint/no-unused-vars const require = (modulePath: string): any => { const absolutePath = path.resolve(path.dirname(filePath), modulePath) return processFile(absolutePath) } - // eslint-disable-next-line no-eval, security/detect-eval-with-expression + // eslint-disable-next-line no-eval eval(` (function(exports, module, require) { ${code} From ec97298d0abc4dfb974ca6d2413dee706fa82392 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 16:29:47 +0300 Subject: [PATCH 38/48] chore: add yarn sdks --- .gitignore | 1 - .yarn/sdks/eslint/bin/eslint.js | 32 +++ .yarn/sdks/eslint/lib/api.js | 32 +++ .yarn/sdks/eslint/lib/unsupported-api.js | 32 +++ .yarn/sdks/eslint/package.json | 14 ++ .yarn/sdks/integrations.yml | 5 + .yarn/sdks/typescript/bin/tsc | 32 +++ .yarn/sdks/typescript/bin/tsserver | 32 +++ .yarn/sdks/typescript/lib/tsc.js | 32 +++ .yarn/sdks/typescript/lib/tsserver.js | 248 +++++++++++++++++++ .yarn/sdks/typescript/lib/tsserverlibrary.js | 248 +++++++++++++++++++ .yarn/sdks/typescript/lib/typescript.js | 32 +++ .yarn/sdks/typescript/package.json | 10 + 13 files changed, 749 insertions(+), 1 deletion(-) create mode 100755 .yarn/sdks/eslint/bin/eslint.js create mode 100644 .yarn/sdks/eslint/lib/api.js create mode 100644 .yarn/sdks/eslint/lib/unsupported-api.js create mode 100644 .yarn/sdks/eslint/package.json create mode 100644 .yarn/sdks/integrations.yml create mode 100755 .yarn/sdks/typescript/bin/tsc create mode 100755 .yarn/sdks/typescript/bin/tsserver create mode 100644 .yarn/sdks/typescript/lib/tsc.js create mode 100644 .yarn/sdks/typescript/lib/tsserver.js create mode 100644 .yarn/sdks/typescript/lib/tsserverlibrary.js create mode 100644 .yarn/sdks/typescript/lib/typescript.js create mode 100644 .yarn/sdks/typescript/package.json diff --git a/.gitignore b/.gitignore index 7089ecc..c8ed97d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,5 +50,4 @@ package.tgz generated/ # VS Code -.yarn/sdks .vscode diff --git a/.yarn/sdks/eslint/bin/eslint.js b/.yarn/sdks/eslint/bin/eslint.js new file mode 100755 index 0000000..e6604ff --- /dev/null +++ b/.yarn/sdks/eslint/bin/eslint.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/bin/eslint.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/bin/eslint.js your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`)); diff --git a/.yarn/sdks/eslint/lib/api.js b/.yarn/sdks/eslint/lib/api.js new file mode 100644 index 0000000..8addf97 --- /dev/null +++ b/.yarn/sdks/eslint/lib/api.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint`)); diff --git a/.yarn/sdks/eslint/lib/unsupported-api.js b/.yarn/sdks/eslint/lib/unsupported-api.js new file mode 100644 index 0000000..c2b464c --- /dev/null +++ b/.yarn/sdks/eslint/lib/unsupported-api.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/use-at-your-own-risk + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/use-at-your-own-risk your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`)); diff --git a/.yarn/sdks/eslint/package.json b/.yarn/sdks/eslint/package.json new file mode 100644 index 0000000..263cd3d --- /dev/null +++ b/.yarn/sdks/eslint/package.json @@ -0,0 +1,14 @@ +{ + "name": "eslint", + "version": "8.57.0-sdk", + "main": "./lib/api.js", + "type": "commonjs", + "bin": { + "eslint": "./bin/eslint.js" + }, + "exports": { + "./package.json": "./package.json", + ".": "./lib/api.js", + "./use-at-your-own-risk": "./lib/unsupported-api.js" + } +} diff --git a/.yarn/sdks/integrations.yml b/.yarn/sdks/integrations.yml new file mode 100644 index 0000000..aa9d0d0 --- /dev/null +++ b/.yarn/sdks/integrations.yml @@ -0,0 +1,5 @@ +# This file is automatically generated by @yarnpkg/sdks. +# Manual changes might be lost! + +integrations: + - vscode diff --git a/.yarn/sdks/typescript/bin/tsc b/.yarn/sdks/typescript/bin/tsc new file mode 100755 index 0000000..867a7bd --- /dev/null +++ b/.yarn/sdks/typescript/bin/tsc @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/bin/tsc + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/bin/tsc your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`)); diff --git a/.yarn/sdks/typescript/bin/tsserver b/.yarn/sdks/typescript/bin/tsserver new file mode 100755 index 0000000..3fc5aa3 --- /dev/null +++ b/.yarn/sdks/typescript/bin/tsserver @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/bin/tsserver + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/bin/tsserver your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`)); diff --git a/.yarn/sdks/typescript/lib/tsc.js b/.yarn/sdks/typescript/lib/tsc.js new file mode 100644 index 0000000..da411bd --- /dev/null +++ b/.yarn/sdks/typescript/lib/tsc.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsc.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/lib/tsc.js your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`)); diff --git a/.yarn/sdks/typescript/lib/tsserver.js b/.yarn/sdks/typescript/lib/tsserver.js new file mode 100644 index 0000000..6249c46 --- /dev/null +++ b/.yarn/sdks/typescript/lib/tsserver.js @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserver.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +const moduleWrapper = exports => { + return wrapWithUserWrapper(moduleWrapperFn(exports)); +}; + +const moduleWrapperFn = tsserver => { + if (!process.versions.pnp) { + return tsserver; + } + + const {isAbsolute} = require(`path`); + const pnpApi = require(`pnpapi`); + + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); + + // VSCode sends the zip paths to TS using the "zip://" prefix, that TS + // doesn't understand. This layer makes sure to remove the protocol + // before forwarding it to TS, and to add it back on all returned paths. + + function toEditorPath(str) { + // We add the `zip:` prefix to both `.zip/` paths and virtual paths + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { + // We also take the opportunity to turn virtual paths into physical ones; + // this makes it much easier to work with workspaces that list peer + // dependencies, since otherwise Ctrl+Click would bring us to the virtual + // file instances instead of the real ones. + // + // We only do this to modules owned by the the dependency tree roots. + // This avoids breaking the resolution when jumping inside a vendor + // with peer dep (otherwise jumping into react-dom would show resolution + // errors on react). + // + const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; + if (resolved) { + const locator = pnpApi.findPackageLocator(resolved); + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { + str = resolved; + } + } + + str = normalize(str); + + if (str.match(/\.zip\//)) { + switch (hostInfo) { + // Absolute VSCode `Uri.fsPath`s need to start with a slash. + // VSCode only adds it automatically for supported schemes, + // so we have to do it manually for the `zip` scheme. + // The path needs to start with a caret otherwise VSCode doesn't handle the protocol + // + // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 + // + // 2021-10-08: VSCode changed the format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + // 2022-04-06: VSCode changed the format in 1.66. + // Before | ^/zip//c:/foo/bar.zip/package.json + // After | ^/zip/c:/foo/bar.zip/package.json + // + // 2022-05-06: VSCode changed the format in 1.68 + // Before | ^/zip/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; + + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; + + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; + + case `vscode`: { + str = `^/zip/${str}`; + } break; + + // To make "go to definition" work, + // We have to resolve the actual file system path from virtual path + // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; + + // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) + // We have to resolve the actual file system path from virtual path, + // everything else is up to neovim + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; + } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); + } + } + + return str; + } + + function fromEditorPath(str) { + switch (hostInfo) { + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; + + case `vscode`: + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; + } + } + + // Force enable 'allowLocalPluginLoads' + // TypeScript tries to resolve plugins using a path relative to itself + // which doesn't work when using the global cache + // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 + // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but + // TypeScript already does local loads and if this code is running the user trusts the workspace + // https://github.com/microsoft/vscode/issues/45856 + const ConfiguredProject = tsserver.server.ConfiguredProject; + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { + this.projectService.allowLocalPluginLoads = true; + return originalEnablePluginsWithOptions.apply(this, arguments); + }; + + // And here is the point where we hijack the VSCode <-> TS communications + // by adding ourselves in the middle. We locate everything that looks + // like an absolute path of ours and normalize it. + + const Session = tsserver.server.Session; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; + let hostInfo = `unknown`; + + Object.assign(Session.prototype, { + onMessage(/** @type {string | object} */ message) { + const isStringMessage = typeof message === 'string'; + const parsedMessage = isStringMessage ? JSON.parse(message) : message; + + if ( + parsedMessage != null && + typeof parsedMessage === `object` && + parsedMessage.arguments && + typeof parsedMessage.arguments.hostInfo === `string` + ) { + hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) + + if (major === 1) { + if (minor < 61) { + hostInfo += ` <1.61`; + } else if (minor < 66) { + hostInfo += ` <1.66`; + } else if (minor < 68) { + hostInfo += ` <1.68`; + } + } + } + } + + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); + + return originalOnMessage.call( + this, + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) + ); + }, + + send(/** @type {any} */ msg) { + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } + }); + + return tsserver; +}; + +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); +} + +// Defer to the real typescript/lib/tsserver.js your application uses +module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); diff --git a/.yarn/sdks/typescript/lib/tsserverlibrary.js b/.yarn/sdks/typescript/lib/tsserverlibrary.js new file mode 100644 index 0000000..0e50e0a --- /dev/null +++ b/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserverlibrary.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +const moduleWrapper = exports => { + return wrapWithUserWrapper(moduleWrapperFn(exports)); +}; + +const moduleWrapperFn = tsserver => { + if (!process.versions.pnp) { + return tsserver; + } + + const {isAbsolute} = require(`path`); + const pnpApi = require(`pnpapi`); + + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); + + // VSCode sends the zip paths to TS using the "zip://" prefix, that TS + // doesn't understand. This layer makes sure to remove the protocol + // before forwarding it to TS, and to add it back on all returned paths. + + function toEditorPath(str) { + // We add the `zip:` prefix to both `.zip/` paths and virtual paths + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { + // We also take the opportunity to turn virtual paths into physical ones; + // this makes it much easier to work with workspaces that list peer + // dependencies, since otherwise Ctrl+Click would bring us to the virtual + // file instances instead of the real ones. + // + // We only do this to modules owned by the the dependency tree roots. + // This avoids breaking the resolution when jumping inside a vendor + // with peer dep (otherwise jumping into react-dom would show resolution + // errors on react). + // + const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; + if (resolved) { + const locator = pnpApi.findPackageLocator(resolved); + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { + str = resolved; + } + } + + str = normalize(str); + + if (str.match(/\.zip\//)) { + switch (hostInfo) { + // Absolute VSCode `Uri.fsPath`s need to start with a slash. + // VSCode only adds it automatically for supported schemes, + // so we have to do it manually for the `zip` scheme. + // The path needs to start with a caret otherwise VSCode doesn't handle the protocol + // + // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 + // + // 2021-10-08: VSCode changed the format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + // 2022-04-06: VSCode changed the format in 1.66. + // Before | ^/zip//c:/foo/bar.zip/package.json + // After | ^/zip/c:/foo/bar.zip/package.json + // + // 2022-05-06: VSCode changed the format in 1.68 + // Before | ^/zip/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; + + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; + + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; + + case `vscode`: { + str = `^/zip/${str}`; + } break; + + // To make "go to definition" work, + // We have to resolve the actual file system path from virtual path + // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; + + // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) + // We have to resolve the actual file system path from virtual path, + // everything else is up to neovim + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; + } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); + } + } + + return str; + } + + function fromEditorPath(str) { + switch (hostInfo) { + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; + + case `vscode`: + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; + } + } + + // Force enable 'allowLocalPluginLoads' + // TypeScript tries to resolve plugins using a path relative to itself + // which doesn't work when using the global cache + // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 + // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but + // TypeScript already does local loads and if this code is running the user trusts the workspace + // https://github.com/microsoft/vscode/issues/45856 + const ConfiguredProject = tsserver.server.ConfiguredProject; + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { + this.projectService.allowLocalPluginLoads = true; + return originalEnablePluginsWithOptions.apply(this, arguments); + }; + + // And here is the point where we hijack the VSCode <-> TS communications + // by adding ourselves in the middle. We locate everything that looks + // like an absolute path of ours and normalize it. + + const Session = tsserver.server.Session; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; + let hostInfo = `unknown`; + + Object.assign(Session.prototype, { + onMessage(/** @type {string | object} */ message) { + const isStringMessage = typeof message === 'string'; + const parsedMessage = isStringMessage ? JSON.parse(message) : message; + + if ( + parsedMessage != null && + typeof parsedMessage === `object` && + parsedMessage.arguments && + typeof parsedMessage.arguments.hostInfo === `string` + ) { + hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) + + if (major === 1) { + if (minor < 61) { + hostInfo += ` <1.61`; + } else if (minor < 66) { + hostInfo += ` <1.66`; + } else if (minor < 68) { + hostInfo += ` <1.68`; + } + } + } + } + + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); + + return originalOnMessage.call( + this, + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) + ); + }, + + send(/** @type {any} */ msg) { + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } + }); + + return tsserver; +}; + +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); +} + +// Defer to the real typescript/lib/tsserverlibrary.js your application uses +module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); diff --git a/.yarn/sdks/typescript/lib/typescript.js b/.yarn/sdks/typescript/lib/typescript.js new file mode 100644 index 0000000..7b6cc22 --- /dev/null +++ b/.yarn/sdks/typescript/lib/typescript.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript`)); diff --git a/.yarn/sdks/typescript/package.json b/.yarn/sdks/typescript/package.json new file mode 100644 index 0000000..d32f391 --- /dev/null +++ b/.yarn/sdks/typescript/package.json @@ -0,0 +1,10 @@ +{ + "name": "typescript", + "version": "5.2.2-sdk", + "main": "./lib/typescript.js", + "type": "commonjs", + "bin": { + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" + } +} From 86f7e049b63771c6d4055de73b8dbe13998b1127 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 16:32:06 +0300 Subject: [PATCH 39/48] chore: add exports to package json files --- fragments/fragments-cli/package.json | 3 +++ fragments/fragments-generator/package.json | 3 +++ utils/file/package.json | 3 +++ 3 files changed, 9 insertions(+) diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json index 9cc21b9..1f37a67 100644 --- a/fragments/fragments-cli/package.json +++ b/fragments/fragments-cli/package.json @@ -3,6 +3,9 @@ "version": "0.0.1", "license": "BSD-3-Clause", "type": "module", + "exports": { + ".": "./src/index.ts" + }, "main": "src/index.ts", "bin": { "generate-fragments": "dist/index.js" diff --git a/fragments/fragments-generator/package.json b/fragments/fragments-generator/package.json index 8e0a6ad..993c513 100644 --- a/fragments/fragments-generator/package.json +++ b/fragments/fragments-generator/package.json @@ -3,6 +3,9 @@ "version": "0.0.1", "license": "BSD-3-Clause", "type": "module", + "exports": { + ".": "./src/index.ts" + }, "main": "src/index.ts", "files": [ "dist" diff --git a/utils/file/package.json b/utils/file/package.json index c3c180b..f4877bf 100644 --- a/utils/file/package.json +++ b/utils/file/package.json @@ -3,6 +3,9 @@ "version": "0.0.4", "license": "BSD-3-Clause", "type": "module", + "exports": { + ".": "./src/index.ts" + }, "main": "src/index.ts", "files": [ "dist" From a427d7e56f9894eccd4ed06dbe05fa9d4848809e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 16:33:17 +0300 Subject: [PATCH 40/48] chore: remove generate-fragments script --- fragments/fragments-cli/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json index 1f37a67..c5eeaf3 100644 --- a/fragments/fragments-cli/package.json +++ b/fragments/fragments-cli/package.json @@ -15,8 +15,7 @@ ], "scripts": { "build": "yarn library build", - "generate-fragments": "ts-node-esm src/index.ts", - "generate-fragments-new": "node --import @swc-node/register/esm-register src/index.ts", + "generate-fragments": "node --import @swc-node/register/esm-register src/index.ts", "prepack": "yarn run build", "postpack": "rm -rf dist" }, From 3f575efb4a1e6973eba298ac6127391e726caf20 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 16:38:48 +0300 Subject: [PATCH 41/48] refactor: use import from node --- fragments/fragments-cli/src/run.ts | 5 ++--- utils/file/src/process-file.util.ts | 7 +++---- utils/file/src/write-file.util.ts | 5 +++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/fragments/fragments-cli/src/run.ts b/fragments/fragments-cli/src/run.ts index 46557fa..050477a 100644 --- a/fragments/fragments-cli/src/run.ts +++ b/fragments/fragments-cli/src/run.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert' - -import { join } from 'path' +import assert from 'node:assert/strict' +import { join } from 'node:path' import { FigmaFileLoader } from '@atls/figma-file-loader' import { FigmaThemeFragmentsGenerator } from '@atls/figma-fragments-generator' diff --git a/utils/file/src/process-file.util.ts b/utils/file/src/process-file.util.ts index c923903..87423e7 100644 --- a/utils/file/src/process-file.util.ts +++ b/utils/file/src/process-file.util.ts @@ -1,10 +1,9 @@ -import assert from 'node:assert' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import path from 'node:path' import { transform } from '@babel/standalone' -import path from 'path' -import { readFileSync } from 'fs' - export const processFile = (filePath: string): any => { const file = readFileSync(filePath.replace('.js', '.ts')).toString('utf-8') diff --git a/utils/file/src/write-file.util.ts b/utils/file/src/write-file.util.ts index 8eb4fd6..ac48fac 100644 --- a/utils/file/src/write-file.util.ts +++ b/utils/file/src/write-file.util.ts @@ -1,6 +1,7 @@ -import path from 'path' +import { promises } from 'node:fs' +import path from 'node:path' + import prettier from 'prettier' -import { promises } from 'fs' export const writeFile = async (filePath: string, name: string, content: string): Promise => { const target = path.join(filePath, name) From c46e1a2d8597deed1a256da5564cc8f2e6eca78e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 16:58:47 +0300 Subject: [PATCH 42/48] refactor: use pino logger instead of npmlog --- .pnp.cjs | 211 ++++++++++++++++++++++++++- fragments/fragments-cli/package.json | 4 +- fragments/fragments-cli/src/index.ts | 22 ++- yarn.lock | 182 ++++++++++++++++++++++- 4 files changed, 406 insertions(+), 13 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index 81561cd..8f25836 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -452,11 +452,11 @@ const RAW_RUNTIME_STATE = ["@swc-node/register", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.9.0"],\ ["@swc/core", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:1.6.1"],\ ["@types/node", "npm:18.19.34"],\ - ["@types/npmlog", "npm:7.0.0"],\ ["@yarnpkg/builder", "npm:4.1.1"],\ ["commander", "npm:12.1.0"],\ ["figma-js", "npm:1.16.1-0"],\ - ["npmlog", "npm:7.0.1"],\ + ["pino", "npm:9.5.0"],\ + ["pino-pretty", "npm:13.0.0"],\ ["ts-node", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:10.9.2"],\ ["typescript", "patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441"]\ ],\ @@ -5840,6 +5840,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["atomic-sleep", [\ + ["npm:1.0.0", {\ + "packageLocation": "../.yarn/berry/cache/atomic-sleep-npm-1.0.0-17d8a762a3-10.zip/node_modules/atomic-sleep/",\ + "packageDependencies": [\ + ["atomic-sleep", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["auto-bind", [\ ["npm:4.0.0", {\ "packageLocation": "../.yarn/berry/cache/auto-bind-npm-4.0.0-1cda90694b-10.zip/node_modules/auto-bind/",\ @@ -6753,6 +6762,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dateformat", [\ + ["npm:4.6.3", {\ + "packageLocation": "../.yarn/berry/cache/dateformat-npm-4.6.3-aa1a4cb7f9-10.zip/node_modules/dateformat/",\ + "packageDependencies": [\ + ["dateformat", "npm:4.6.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["debug", [\ ["npm:4.3.4", {\ "packageLocation": "../.yarn/berry/cache/debug-npm-4.3.4-4513954577-10.zip/node_modules/debug/",\ @@ -7547,6 +7565,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fast-copy", [\ + ["npm:3.0.2", {\ + "packageLocation": "../.yarn/berry/cache/fast-copy-npm-3.0.2-d747bd131f-10.zip/node_modules/fast-copy/",\ + "packageDependencies": [\ + ["fast-copy", "npm:3.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fast-deep-equal", [\ ["npm:3.1.3", {\ "packageLocation": "../.yarn/berry/cache/fast-deep-equal-npm-3.1.3-790edcfcf5-10.zip/node_modules/fast-deep-equal/",\ @@ -7600,6 +7627,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fast-redact", [\ + ["npm:3.5.0", {\ + "packageLocation": "../.yarn/berry/cache/fast-redact-npm-3.5.0-80acfe2b04-10.zip/node_modules/fast-redact/",\ + "packageDependencies": [\ + ["fast-redact", "npm:3.5.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["fast-safe-stringify", [\ + ["npm:2.1.1", {\ + "packageLocation": "../.yarn/berry/cache/fast-safe-stringify-npm-2.1.1-7ce89033ca-10.zip/node_modules/fast-safe-stringify/",\ + "packageDependencies": [\ + ["fast-safe-stringify", "npm:2.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fastq", [\ ["npm:1.15.0", {\ "packageLocation": "../.yarn/berry/cache/fastq-npm-1.15.0-1013f6514e-10.zip/node_modules/fastq/",\ @@ -8210,6 +8255,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["help-me", [\ + ["npm:5.0.0", {\ + "packageLocation": "../.yarn/berry/cache/help-me-npm-5.0.0-6239bd310f-10.zip/node_modules/help-me/",\ + "packageDependencies": [\ + ["help-me", "npm:5.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["html-escaper", [\ ["npm:2.0.2", {\ "packageLocation": "../.yarn/berry/cache/html-escaper-npm-2.0.2-38e51ef294-10.zip/node_modules/html-escaper/",\ @@ -9691,6 +9745,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["joycon", [\ + ["npm:3.1.1", {\ + "packageLocation": "../.yarn/berry/cache/joycon-npm-3.1.1-3033e0e5f4-10.zip/node_modules/joycon/",\ + "packageDependencies": [\ + ["joycon", "npm:3.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["js-tokens", [\ ["npm:4.0.0", {\ "packageLocation": "../.yarn/berry/cache/js-tokens-npm-4.0.0-0ac852e9e2-10.zip/node_modules/js-tokens/",\ @@ -10493,6 +10556,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["on-exit-leak-free", [\ + ["npm:2.1.2", {\ + "packageLocation": "../.yarn/berry/cache/on-exit-leak-free-npm-2.1.2-0d0c5ad67d-10.zip/node_modules/on-exit-leak-free/",\ + "packageDependencies": [\ + ["on-exit-leak-free", "npm:2.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["once", [\ ["npm:1.4.0", {\ "packageLocation": "../.yarn/berry/cache/once-npm-1.4.0-ccf03ef07a-10.zip/node_modules/once/",\ @@ -10752,6 +10824,67 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["pino", [\ + ["npm:9.5.0", {\ + "packageLocation": "../.yarn/berry/cache/pino-npm-9.5.0-c25201092d-10.zip/node_modules/pino/",\ + "packageDependencies": [\ + ["pino", "npm:9.5.0"],\ + ["atomic-sleep", "npm:1.0.0"],\ + ["fast-redact", "npm:3.5.0"],\ + ["on-exit-leak-free", "npm:2.1.2"],\ + ["pino-abstract-transport", "npm:2.0.0"],\ + ["pino-std-serializers", "npm:7.0.0"],\ + ["process-warning", "npm:4.0.0"],\ + ["quick-format-unescaped", "npm:4.0.4"],\ + ["real-require", "npm:0.2.0"],\ + ["safe-stable-stringify", "npm:2.5.0"],\ + ["sonic-boom", "npm:4.2.0"],\ + ["thread-stream", "npm:3.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["pino-abstract-transport", [\ + ["npm:2.0.0", {\ + "packageLocation": "../.yarn/berry/cache/pino-abstract-transport-npm-2.0.0-696dba31d0-10.zip/node_modules/pino-abstract-transport/",\ + "packageDependencies": [\ + ["pino-abstract-transport", "npm:2.0.0"],\ + ["split2", "npm:4.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["pino-pretty", [\ + ["npm:13.0.0", {\ + "packageLocation": "../.yarn/berry/cache/pino-pretty-npm-13.0.0-70bb86dd46-10.zip/node_modules/pino-pretty/",\ + "packageDependencies": [\ + ["pino-pretty", "npm:13.0.0"],\ + ["colorette", "npm:2.0.20"],\ + ["dateformat", "npm:4.6.3"],\ + ["fast-copy", "npm:3.0.2"],\ + ["fast-safe-stringify", "npm:2.1.1"],\ + ["help-me", "npm:5.0.0"],\ + ["joycon", "npm:3.1.1"],\ + ["minimist", "npm:1.2.8"],\ + ["on-exit-leak-free", "npm:2.1.2"],\ + ["pino-abstract-transport", "npm:2.0.0"],\ + ["pump", "npm:3.0.0"],\ + ["secure-json-parse", "npm:2.7.0"],\ + ["sonic-boom", "npm:4.2.0"],\ + ["strip-json-comments", "npm:3.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["pino-std-serializers", [\ + ["npm:7.0.0", {\ + "packageLocation": "../.yarn/berry/cache/pino-std-serializers-npm-7.0.0-94d470ae0c-10.zip/node_modules/pino-std-serializers/",\ + "packageDependencies": [\ + ["pino-std-serializers", "npm:7.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["pirates", [\ ["npm:4.0.6", {\ "packageLocation": "../.yarn/berry/cache/pirates-npm-4.0.6-a8ec571a43-10.zip/node_modules/pirates/",\ @@ -11021,6 +11154,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["process-warning", [\ + ["npm:4.0.0", {\ + "packageLocation": "../.yarn/berry/cache/process-warning-npm-4.0.0-d61ed74d22-10.zip/node_modules/process-warning/",\ + "packageDependencies": [\ + ["process-warning", "npm:4.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["promise-retry", [\ ["npm:2.0.1", {\ "packageLocation": "../.yarn/berry/cache/promise-retry-npm-2.0.1-871f0b01b7-10.zip/node_modules/promise-retry/",\ @@ -11120,6 +11262,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["quick-format-unescaped", [\ + ["npm:4.0.4", {\ + "packageLocation": "../.yarn/berry/cache/quick-format-unescaped-npm-4.0.4-7e22c9b7dc-10.zip/node_modules/quick-format-unescaped/",\ + "packageDependencies": [\ + ["quick-format-unescaped", "npm:4.0.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["quick-lru", [\ ["npm:5.1.1", {\ "packageLocation": "../.yarn/berry/cache/quick-lru-npm-5.1.1-e38e0edce3-10.zip/node_modules/quick-lru/",\ @@ -11278,6 +11429,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["real-require", [\ + ["npm:0.2.0", {\ + "packageLocation": "../.yarn/berry/cache/real-require-npm-0.2.0-7f69dbc7b6-10.zip/node_modules/real-require/",\ + "packageDependencies": [\ + ["real-require", "npm:0.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["regenerator-runtime", [\ ["npm:0.14.0", {\ "packageLocation": "../.yarn/berry/cache/regenerator-runtime-npm-0.14.0-e060897cf7-10.zip/node_modules/regenerator-runtime/",\ @@ -11456,6 +11616,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["safe-stable-stringify", [\ + ["npm:2.5.0", {\ + "packageLocation": "../.yarn/berry/cache/safe-stable-stringify-npm-2.5.0-42ba8d9d22-10.zip/node_modules/safe-stable-stringify/",\ + "packageDependencies": [\ + ["safe-stable-stringify", "npm:2.5.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["safer-buffer", [\ ["npm:2.1.2", {\ "packageLocation": "../.yarn/berry/cache/safer-buffer-npm-2.1.2-8d5c0b705e-10.zip/node_modules/safer-buffer/",\ @@ -11498,6 +11667,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["secure-json-parse", [\ + ["npm:2.7.0", {\ + "packageLocation": "../.yarn/berry/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-10.zip/node_modules/secure-json-parse/",\ + "packageDependencies": [\ + ["secure-json-parse", "npm:2.7.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["semver", [\ ["npm:6.3.1", {\ "packageLocation": "../.yarn/berry/cache/semver-npm-6.3.1-bcba31fdbe-10.zip/node_modules/semver/",\ @@ -11647,6 +11825,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["sonic-boom", [\ + ["npm:4.2.0", {\ + "packageLocation": "../.yarn/berry/cache/sonic-boom-npm-4.2.0-b2baf3f5bd-10.zip/node_modules/sonic-boom/",\ + "packageDependencies": [\ + ["sonic-boom", "npm:4.2.0"],\ + ["atomic-sleep", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["source-map", [\ ["npm:0.6.1", {\ "packageLocation": "../.yarn/berry/cache/source-map-npm-0.6.1-1a3621db16-10.zip/node_modules/source-map/",\ @@ -11701,6 +11889,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["split2", [\ + ["npm:4.2.0", {\ + "packageLocation": "../.yarn/berry/cache/split2-npm-4.2.0-16aa3883ba-10.zip/node_modules/split2/",\ + "packageDependencies": [\ + ["split2", "npm:4.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["sprintf-js", [\ ["npm:1.0.3", {\ "packageLocation": "../.yarn/berry/cache/sprintf-js-npm-1.0.3-73f0a322fa-10.zip/node_modules/sprintf-js/",\ @@ -12181,6 +12378,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["thread-stream", [\ + ["npm:3.1.0", {\ + "packageLocation": "../.yarn/berry/cache/thread-stream-npm-3.1.0-ac5663dfb7-10.zip/node_modules/thread-stream/",\ + "packageDependencies": [\ + ["thread-stream", "npm:3.1.0"],\ + ["real-require", "npm:0.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["tiny-glob", [\ ["npm:0.2.9", {\ "packageLocation": "../.yarn/berry/cache/tiny-glob-npm-0.2.9-068f4ab3f8-10.zip/node_modules/tiny-glob/",\ diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json index c5eeaf3..09a3146 100644 --- a/fragments/fragments-cli/package.json +++ b/fragments/fragments-cli/package.json @@ -25,13 +25,13 @@ "@atls/figma-fragments-generator": "workspace:*", "commander": "12.1.0", "figma-js": "1.16.1-0", - "npmlog": "7.0.1" + "pino": "9.5.0", + "pino-pretty": "13.0.0" }, "devDependencies": { "@swc-node/register": "1.9.0", "@swc/core": "1.6.1", "@types/node": "18.19.34", - "@types/npmlog": "7.0.0", "@yarnpkg/builder": "4.1.1", "ts-node": "10.9.2", "typescript": "5.2.2" diff --git a/fragments/fragments-cli/src/index.ts b/fragments/fragments-cli/src/index.ts index 0361fc6..2a3d5ff 100644 --- a/fragments/fragments-cli/src/index.ts +++ b/fragments/fragments-cli/src/index.ts @@ -1,11 +1,17 @@ import { createInterface } from 'node:readline' -import logger from 'npmlog' import { program } from 'commander' +import { pino } from 'pino' import { run } from './run.js' -logger.heading = 'figma-fragments' +const logger = pino({ + name: 'figma-fragments', + level: 'info', + transport: { + target: 'pino-pretty', + }, +}) program .option('-o, --output [output]', 'Output dir') @@ -19,7 +25,7 @@ const fileId = program.args.at(0) const options = program.opts() if (options.verbose) { - logger.level = 'verbose' + logger.level = 'debug' } if (!fileId) { @@ -31,14 +37,18 @@ if (!fileId) { }) readline.question(`Enter your Figma access token:\n`, (id) => { - if (!id || id === '') throw Error('ID must not be empty') + if (!id) { + logger.error('ID must not be empty') + readline.close() + return + } // eslint-disable-next-line dot-notation process.env['FIGMA_TOKEN'] = id readline.close() run(fileId, options.nodeId, options.output, options.theme) - .then(() => logger.info('info', 'Fragments successful generated')) - .catch((error) => logger.error('error', error)) + .then(() => logger.info('Fragments successful generated')) + .catch((error) => logger.error(error, 'Error generating fragments')) }) } diff --git a/yarn.lock b/yarn.lock index 2eea81a..54604be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -268,11 +268,11 @@ __metadata: "@swc-node/register": "npm:1.9.0" "@swc/core": "npm:1.6.1" "@types/node": "npm:18.19.34" - "@types/npmlog": "npm:7.0.0" "@yarnpkg/builder": "npm:4.1.1" commander: "npm:12.1.0" figma-js: "npm:1.16.1-0" - npmlog: "npm:7.0.1" + pino: "npm:9.5.0" + pino-pretty: "npm:13.0.0" ts-node: "npm:10.9.2" typescript: "npm:5.2.2" bin: @@ -3790,6 +3790,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10/3ab6d2cf46b31394b4607e935ec5c1c3c4f60f3e30f0913d35ea74b51b3585e84f590d09e58067f11762eec71c87d25314ce859030983dc0e4397eed21daa12e + languageName: node + linkType: hard + "auto-bind@npm:4.0.0": version: 4.0.0 resolution: "auto-bind@npm:4.0.0" @@ -4372,7 +4379,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.20": +"colorette@npm:^2.0.20, colorette@npm:^2.0.7": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f @@ -4527,6 +4534,13 @@ __metadata: languageName: node linkType: hard +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10/5c149c91bf9ce2142c89f84eee4c585f0cb1f6faf2536b1af89873f862666a28529d1ccafc44750aa01384da2197c4f76f4e149a3cc0c1cb2c46f5cc45f2bcb5 + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" @@ -5230,6 +5244,13 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-copy@npm:3.0.2" + checksum: 10/97e1022e2aaa27acf4a986d679310bfd66bfb87fe8da9dd33b698e3e50189484001cf1eeb9670e19b59d9d299828ed86c8da354c954f125995ab2a6331c5f290 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -5277,6 +5298,20 @@ __metadata: languageName: node linkType: hard +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: 10/24b27e2023bd5a62f908d97a753b1adb8d89206b260f97727728e00b693197dea2fc2aa3711147a385d0ec6e713569fd533df37a4ef947e08cb65af3019c7ad5 + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -5821,6 +5856,13 @@ __metadata: languageName: node linkType: hard +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10/5f99bd91dae93d02867175c3856c561d7e3a24f16999b08f5fc79689044b938d7ed58457f4d8c8744c01403e6e0470b7896baa344d112b2355842fd935a75d69 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -7114,6 +7156,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10/4b36e3479144ec196425f46b3618f8a96ce7e1b658f091a309cd4906215f5b7a402d7df331a3e0a09681381a658d0c5f039cb3cf6907e0a1e17ed847f5d37775 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -7842,6 +7891,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10/f7b4b7200026a08f6e4a17ba6d72e6c5cbb41789ed9cf7deaf9d9e322872c7dc5a7898549a894651ee0ee9ae635d34a678115bf8acdfba8ebd2ba2af688b563c + languageName: node + linkType: hard + "once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -8069,6 +8125,66 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10/e5699ecb06c7121055978e988e5cecea5b6892fc2589c64f1f86df5e7386bbbfd2ada268839e911b021c6b3123428aed7c6be3ac7940eee139556c75324c7e83 + languageName: node + linkType: hard + +"pino-pretty@npm:13.0.0": + version: 13.0.0 + resolution: "pino-pretty@npm:13.0.0" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.2" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pump: "npm:^3.0.0" + secure-json-parse: "npm:^2.4.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^3.1.1" + bin: + pino-pretty: bin.js + checksum: 10/9861fdbe88db000e3b0fe959f0fb7b5913e8d16af70373155d48854c5d509629e7e1ba09ed3fac24a9bd2729451567a698938b9741d84de63eb549843450e71c + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10/884e08f65aa5463d820521ead3779d4472c78fc434d8582afb66f9dcb8d8c7119c69524b68106cb8caf92c0487be7794cf50e5b9c0383ae65b24bf2a03480951 + languageName: node + linkType: hard + +"pino@npm:9.5.0": + version: 9.5.0 + resolution: "pino@npm:9.5.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.1.1" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^4.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10/e2dba79524be133e2a0800ad0424bbf2c4c94c46f028a81e514d40951666f3dcec76c582fd5ce6f6155df0f5a9d6d0257924fdf043fe285236d36c76509a59dd + languageName: node + linkType: hard + "pirates@npm:^4.0.4, pirates@npm:^4.0.6": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -8258,6 +8374,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^4.0.0": + version: 4.0.0 + resolution: "process-warning@npm:4.0.0" + checksum: 10/0d6ec069f3a6fe1d3379c0247329a297f1f3b9ea7e1d828db0a8f61e0e8337a98b7eb201547350924bc4a101ddcf2fa5cf5563ffe2c54c27651f7996d328483e + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -8348,6 +8471,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10/591eca457509a99368b623db05248c1193aa3cedafc9a077d7acab09495db1231017ba3ad1b5386e5633271edd0a03b312d8640a59ee585b8516a42e15438aa7 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -8486,6 +8616,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10/ddf44ee76301c774e9c9f2826da8a3c5c9f8fc87310f4a364e803ef003aa1a43c378b4323051ced212097fff1af459070f4499338b36a7469df1d4f7e8c0ba4c + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.14.0": version: 0.14.0 resolution: "regenerator-runtime@npm:0.14.0" @@ -8657,6 +8794,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -8696,6 +8840,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.4.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10/974386587060b6fc5b1ac06481b2f9dbbb0d63c860cc73dc7533f27835fdb67b0ef08762dbfef25625c15bc0a0c366899e00076cb0d556af06b71e22f1dede4c + languageName: node + linkType: hard + "semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -8831,6 +8982,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.0 + resolution: "sonic-boom@npm:4.2.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/385ef7fb5ea5976c1d2a1fef0b6df8df6b7caba8696d2d67f689d60c05e3ea2d536752ce7e1c69b9fad844635f1036d07c446f8e8149f5c6a80e0040a455b310 + languageName: node + linkType: hard + "source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" @@ -8881,6 +9041,13 @@ __metadata: languageName: node linkType: hard +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab + languageName: node + linkType: hard + "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -9268,6 +9435,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10/ea2d816c4f6077a7062fac5414a88e82977f807c82ee330938fb9691fe11883bb03f078551c0518bb649c239e47ba113d44014fcbb5db42c5abd5996f35e4213 + languageName: node + linkType: hard + "tiny-glob@npm:0.2.9": version: 0.2.9 resolution: "tiny-glob@npm:0.2.9" From e47ec73fdc50a45f95a43b855196ffd315804c85 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 17:58:19 +0300 Subject: [PATCH 43/48] feat: add name option to generate fragment --- .pnp.cjs | 10 ++++++++++ fragments/fragments-cli/package.json | 1 + fragments/fragments-cli/src/index.ts | 3 ++- fragments/fragments-cli/src/run.ts | 11 ++++++++--- .../src/figma-fragments.generator.ts | 12 ++++++++---- yarn.lock | 8 ++++++++ 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index 8f25836..0ce5119 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -455,6 +455,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/builder", "npm:4.1.1"],\ ["commander", "npm:12.1.0"],\ ["figma-js", "npm:1.16.1-0"],\ + ["kebab-case", "npm:2.0.1"],\ ["pino", "npm:9.5.0"],\ ["pino-pretty", "npm:13.0.0"],\ ["ts-node", "virtual:63996f181b49e19025c5dcecfbb3d817b3bcc13460e0f9e0c2bce2e464d78fa9e18ed9b6d33bd212a0aec2ce50e61aadce49fae9bb9fe09e311ab0ae3bd8b92b#npm:10.9.2"],\ @@ -9856,6 +9857,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["kebab-case", [\ + ["npm:2.0.1", {\ + "packageLocation": "../.yarn/berry/cache/kebab-case-npm-2.0.1-f8a6727ebb-10.zip/node_modules/kebab-case/",\ + "packageDependencies": [\ + ["kebab-case", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["keyv", [\ ["npm:4.5.4", {\ "packageLocation": "../.yarn/berry/cache/keyv-npm-4.5.4-4c8e2cf7f7-10.zip/node_modules/keyv/",\ diff --git a/fragments/fragments-cli/package.json b/fragments/fragments-cli/package.json index 09a3146..fb9b2c0 100644 --- a/fragments/fragments-cli/package.json +++ b/fragments/fragments-cli/package.json @@ -25,6 +25,7 @@ "@atls/figma-fragments-generator": "workspace:*", "commander": "12.1.0", "figma-js": "1.16.1-0", + "kebab-case": "2.0.1", "pino": "9.5.0", "pino-pretty": "13.0.0" }, diff --git a/fragments/fragments-cli/src/index.ts b/fragments/fragments-cli/src/index.ts index 2a3d5ff..90f1003 100644 --- a/fragments/fragments-cli/src/index.ts +++ b/fragments/fragments-cli/src/index.ts @@ -18,6 +18,7 @@ program .option('-t, --theme ', 'Path to theme file') .option('-v, --verbose', 'Verbose output') .option('-n, --node-id ', 'Node id for generating') + .option('--name ', 'The name of the generated fragment') .arguments('') .parse(process.argv) @@ -47,7 +48,7 @@ if (!fileId) { readline.close() - run(fileId, options.nodeId, options.output, options.theme) + run(fileId, options.nodeId, options.output, options.theme, options.name) .then(() => logger.info('Fragments successful generated')) .catch((error) => logger.error(error, 'Error generating fragments')) }) diff --git a/fragments/fragments-cli/src/run.ts b/fragments/fragments-cli/src/run.ts index 050477a..c62dcef 100644 --- a/fragments/fragments-cli/src/run.ts +++ b/fragments/fragments-cli/src/run.ts @@ -1,6 +1,8 @@ import assert from 'node:assert/strict' import { join } from 'node:path' +import kebabCase from 'kebab-case' + import { FigmaFileLoader } from '@atls/figma-file-loader' import { FigmaThemeFragmentsGenerator } from '@atls/figma-fragments-generator' import { processFile } from '@atls/figma-file-utils' @@ -10,7 +12,8 @@ export const run = async ( fileId: string, nodeId: string, output: string, - themeFilePath: string + themeFilePath: string, + name: string = 'GeneratedFragment' ) => { const absoluteThemeFilePath = join(process.cwd(), themeFilePath) const exports = processFile(absoluteThemeFilePath) @@ -27,7 +30,9 @@ export const run = async ( const response = await loader.loadNode(fileId, nodeId) - const component = generator.generate(response, theme) + const component = generator.generate(response, theme, name) + + const fileName = `${kebabCase(name, false)}.component.tsx` - await writeFile(output, 'fragments.tsx', component) + await writeFile(output, fileName, component) } diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index b90e39d..334f2cf 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -5,20 +5,24 @@ import { CreateFragmentStrategy } from './strategy/index.js' export class FigmaThemeFragmentsGenerator { readonly name = 'fragments' - createComponent(fragment: string, imports?: Array): string { + createComponent(fragment: string, name: string, imports?: Array): string { return ` import React from 'react' import { memo } from 'react' ${imports?.join('\n')} - export const GeneratedFragment = memo(() => (${fragment}))` + export const ${name} = memo(() => (${fragment}))` } - generate(response: FileNodesResponse, theme: Record>): string { + generate( + response: FileNodesResponse, + theme: Record>, + name: string + ): string { const strategy = new CreateFragmentStrategy(theme) const { fragment, imports } = strategy.execute(response.nodes) - return this.createComponent(fragment, imports) + return this.createComponent(fragment, name, imports) } } diff --git a/yarn.lock b/yarn.lock index 54604be..4f936e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -271,6 +271,7 @@ __metadata: "@yarnpkg/builder": "npm:4.1.1" commander: "npm:12.1.0" figma-js: "npm:1.16.1-0" + kebab-case: "npm:2.0.1" pino: "npm:9.5.0" pino-pretty: "npm:13.0.0" ts-node: "npm:10.9.2" @@ -7259,6 +7260,13 @@ __metadata: languageName: node linkType: hard +"kebab-case@npm:2.0.1": + version: 2.0.1 + resolution: "kebab-case@npm:2.0.1" + checksum: 10/dcfae8fad06e481a98a542c96cafef8f6e0c3a896f127ed6e05310f202588bea73971d1d620ebd48be6e93cee04230c9ccb97f0f1ed8ad01b3ac8ba1f2c3509d + languageName: node + linkType: hard + "keyv@npm:^4.0.0, keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" From dbe6d4847624695d3da70db57a7b1f567584cb5e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 17:58:54 +0300 Subject: [PATCH 44/48] refactor: change fragment generator strategies folder structure and file names --- .../src/figma-fragments.generator.ts | 2 +- .../create-box}/create-box.strategy.ts | 5 +++-- .../create-box}/create-box.test.ts | 7 ++++--- .../src/strategies/create-box/index.ts | 1 + .../create-button}/create-button.strategy.ts | 5 +++-- .../create-button}/create-button.test.ts | 2 +- .../src/strategies/create-button/index.ts | 1 + .../create-fragment}/create-fragment.strategy.ts | 14 +++++++------- .../create-fragment}/index.ts | 0 .../create-input}/create-input.strategy.ts | 7 ++++--- .../create-input}/create-input.test.ts | 2 +- .../src/strategies/create-input/index.ts | 1 + .../create-text}/create-text.strategy.ts | 2 +- .../create-text}/create-text.test.ts | 7 ++++--- .../src/strategies/create-text/index.ts | 1 + .../fragments-generator/src/strategies/index.ts | 6 ++++++ .../strategies.constants.ts} | 4 ++++ .../strategies.interfaces.ts} | 2 +- .../src/strategies/theme-mapping/index.ts | 1 + .../theme-mapping}/theme-mapping.strategy.ts | 6 +++--- .../theme-mapping}/theme-mapping.test.ts | 10 +++++----- .../src/strategy/strategy.constants.ts | 3 --- 22 files changed, 53 insertions(+), 36 deletions(-) rename fragments/fragments-generator/src/{strategy => strategies/create-box}/create-box.strategy.ts (91%) rename fragments/fragments-generator/src/{strategy/tests => strategies/create-box}/create-box.test.ts (94%) create mode 100644 fragments/fragments-generator/src/strategies/create-box/index.ts rename fragments/fragments-generator/src/{strategy => strategies/create-button}/create-button.strategy.ts (81%) rename fragments/fragments-generator/src/{strategy/tests => strategies/create-button}/create-button.test.ts (95%) create mode 100644 fragments/fragments-generator/src/strategies/create-button/index.ts rename fragments/fragments-generator/src/{strategy => strategies/create-fragment}/create-fragment.strategy.ts (89%) rename fragments/fragments-generator/src/{strategy => strategies/create-fragment}/index.ts (100%) rename fragments/fragments-generator/src/{strategy => strategies/create-input}/create-input.strategy.ts (85%) rename fragments/fragments-generator/src/{strategy/tests => strategies/create-input}/create-input.test.ts (96%) create mode 100644 fragments/fragments-generator/src/strategies/create-input/index.ts rename fragments/fragments-generator/src/{strategy => strategies/create-text}/create-text.strategy.ts (95%) rename fragments/fragments-generator/src/{strategy/tests => strategies/create-text}/create-text.test.ts (93%) create mode 100644 fragments/fragments-generator/src/strategies/create-text/index.ts create mode 100644 fragments/fragments-generator/src/strategies/index.ts rename fragments/fragments-generator/src/{strategy/tests/tests.constants.ts => strategies/strategies.constants.ts} (89%) rename fragments/fragments-generator/src/{strategy/strategy.interfaces.ts => strategies/strategies.interfaces.ts} (89%) create mode 100644 fragments/fragments-generator/src/strategies/theme-mapping/index.ts rename fragments/fragments-generator/src/{strategy => strategies/theme-mapping}/theme-mapping.strategy.ts (97%) rename fragments/fragments-generator/src/{strategy/tests => strategies/theme-mapping}/theme-mapping.test.ts (95%) delete mode 100644 fragments/fragments-generator/src/strategy/strategy.constants.ts diff --git a/fragments/fragments-generator/src/figma-fragments.generator.ts b/fragments/fragments-generator/src/figma-fragments.generator.ts index 334f2cf..852f4e5 100644 --- a/fragments/fragments-generator/src/figma-fragments.generator.ts +++ b/fragments/fragments-generator/src/figma-fragments.generator.ts @@ -1,6 +1,6 @@ import { FileNodesResponse } from 'figma-js' -import { CreateFragmentStrategy } from './strategy/index.js' +import { CreateFragmentStrategy } from './strategies/index.js' export class FigmaThemeFragmentsGenerator { readonly name = 'fragments' diff --git a/fragments/fragments-generator/src/strategy/create-box.strategy.ts b/fragments/fragments-generator/src/strategies/create-box/create-box.strategy.ts similarity index 91% rename from fragments/fragments-generator/src/strategy/create-box.strategy.ts rename to fragments/fragments-generator/src/strategies/create-box/create-box.strategy.ts index 6d47b45..6b845ad 100644 --- a/fragments/fragments-generator/src/strategy/create-box.strategy.ts +++ b/fragments/fragments-generator/src/strategies/create-box/create-box.strategy.ts @@ -1,7 +1,8 @@ -import { Frame } from 'figma-js' +import type { Frame } from 'figma-js' + import { createElement } from 'react' -import { ThemeMappingStrategy } from './theme-mapping.strategy.js' +import { ThemeMappingStrategy } from '../theme-mapping/index.js' export class CreateBoxStrategy extends ThemeMappingStrategy { getImports() { diff --git a/fragments/fragments-generator/src/strategy/tests/create-box.test.ts b/fragments/fragments-generator/src/strategies/create-box/create-box.test.ts similarity index 94% rename from fragments/fragments-generator/src/strategy/tests/create-box.test.ts rename to fragments/fragments-generator/src/strategies/create-box/create-box.test.ts index a632de0..b136e81 100644 --- a/fragments/fragments-generator/src/strategy/tests/create-box.test.ts +++ b/fragments/fragments-generator/src/strategies/create-box/create-box.test.ts @@ -1,8 +1,9 @@ -import { Frame } from 'figma-js' +import type { Frame } from 'figma-js' + import { createElement } from 'react' -import { CreateBoxStrategy } from '../create-box.strategy.js' -import { theme } from './tests.constants.js' +import { CreateBoxStrategy } from './create-box.strategy.js' +import { theme } from '../strategies.constants.js' jest.mock('react', () => ({ createElement: jest.fn(), diff --git a/fragments/fragments-generator/src/strategies/create-box/index.ts b/fragments/fragments-generator/src/strategies/create-box/index.ts new file mode 100644 index 0000000..0d8194c --- /dev/null +++ b/fragments/fragments-generator/src/strategies/create-box/index.ts @@ -0,0 +1 @@ +export * from './create-box.strategy.js' diff --git a/fragments/fragments-generator/src/strategy/create-button.strategy.ts b/fragments/fragments-generator/src/strategies/create-button/create-button.strategy.ts similarity index 81% rename from fragments/fragments-generator/src/strategy/create-button.strategy.ts rename to fragments/fragments-generator/src/strategies/create-button/create-button.strategy.ts index de708e7..3a08d69 100644 --- a/fragments/fragments-generator/src/strategy/create-button.strategy.ts +++ b/fragments/fragments-generator/src/strategies/create-button/create-button.strategy.ts @@ -1,8 +1,9 @@ -import { Instance } from 'figma-js' +import type { Instance } from 'figma-js' + import { Fragment } from 'react' import { createElement } from 'react' -import { ComponentProperties } from './strategy.interfaces.js' +import { ComponentProperties } from '../strategies.interfaces.js' export class CreateButtonStrategy { getImports() { diff --git a/fragments/fragments-generator/src/strategy/tests/create-button.test.ts b/fragments/fragments-generator/src/strategies/create-button/create-button.test.ts similarity index 95% rename from fragments/fragments-generator/src/strategy/tests/create-button.test.ts rename to fragments/fragments-generator/src/strategies/create-button/create-button.test.ts index c79c5d6..35f6fbd 100644 --- a/fragments/fragments-generator/src/strategy/tests/create-button.test.ts +++ b/fragments/fragments-generator/src/strategies/create-button/create-button.test.ts @@ -1,7 +1,7 @@ import { Fragment } from 'react' import { createElement } from 'react' -import { CreateButtonStrategy } from '../create-button.strategy.js' +import { CreateButtonStrategy } from './create-button.strategy.js' jest.mock('react', () => ({ createElement: jest.fn(), diff --git a/fragments/fragments-generator/src/strategies/create-button/index.ts b/fragments/fragments-generator/src/strategies/create-button/index.ts new file mode 100644 index 0000000..64289fe --- /dev/null +++ b/fragments/fragments-generator/src/strategies/create-button/index.ts @@ -0,0 +1 @@ +export * from './create-button.strategy.js' diff --git a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts b/fragments/fragments-generator/src/strategies/create-fragment/create-fragment.strategy.ts similarity index 89% rename from fragments/fragments-generator/src/strategy/create-fragment.strategy.ts rename to fragments/fragments-generator/src/strategies/create-fragment/create-fragment.strategy.ts index 5cec2a0..2ef2d66 100644 --- a/fragments/fragments-generator/src/strategy/create-fragment.strategy.ts +++ b/fragments/fragments-generator/src/strategies/create-fragment/create-fragment.strategy.ts @@ -1,5 +1,5 @@ -import type { CreteFragmentResult } from './strategy.interfaces.js' -import type { TreeElement } from './strategy.interfaces.js' +import type { CreteFragmentResult } from '../strategies.interfaces.js' +import type { TreeElement } from '../strategies.interfaces.js' import type { FileNodesResponse } from 'figma-js' import type { ReactElement } from 'react' @@ -14,10 +14,10 @@ import { isInstance } from '@atls/figma-utils' import { isText } from '@atls/figma-utils' import { walk } from '@atls/figma-utils' -import { CreateBoxStrategy } from './create-box.strategy.js' -import { CreateButtonStrategy } from './create-button.strategy.js' -import { CreateInputStrategy } from './create-input.strategy.js' -import { CreateTextStrategy } from './create-text.strategy.js' +import { CreateBoxStrategy } from '../create-box/index.js' +import { CreateButtonStrategy } from '../create-button/index.js' +import { CreateInputStrategy } from '../create-input/index.js' +import { CreateTextStrategy } from '../create-text/index.js' export class CreateFragmentStrategy { private elements: Record = {} @@ -37,7 +37,7 @@ export class CreateFragmentStrategy { this.input = new CreateInputStrategy() } - private createFragmentElement(elements: TreeElement[]) { + private createFragmentElement(elements: Array) { if (elements.length <= 1) { return this.createElementsTree(elements[0]) } diff --git a/fragments/fragments-generator/src/strategy/index.ts b/fragments/fragments-generator/src/strategies/create-fragment/index.ts similarity index 100% rename from fragments/fragments-generator/src/strategy/index.ts rename to fragments/fragments-generator/src/strategies/create-fragment/index.ts diff --git a/fragments/fragments-generator/src/strategy/create-input.strategy.ts b/fragments/fragments-generator/src/strategies/create-input/create-input.strategy.ts similarity index 85% rename from fragments/fragments-generator/src/strategy/create-input.strategy.ts rename to fragments/fragments-generator/src/strategies/create-input/create-input.strategy.ts index fee2a44..34caf35 100644 --- a/fragments/fragments-generator/src/strategy/create-input.strategy.ts +++ b/fragments/fragments-generator/src/strategies/create-input/create-input.strategy.ts @@ -1,12 +1,13 @@ -import { Instance } from 'figma-js' -import { Text } from 'figma-js' +import type { Instance } from 'figma-js' +import type { Text } from 'figma-js' + import { Fragment } from 'react' import { createElement } from 'react' import { isFrame } from '@atls/figma-utils' import { isText } from '@atls/figma-utils' -import { ComponentProperties } from './strategy.interfaces.js' +import { ComponentProperties } from '../strategies.interfaces.js' export class CreateInputStrategy { getImports() { diff --git a/fragments/fragments-generator/src/strategy/tests/create-input.test.ts b/fragments/fragments-generator/src/strategies/create-input/create-input.test.ts similarity index 96% rename from fragments/fragments-generator/src/strategy/tests/create-input.test.ts rename to fragments/fragments-generator/src/strategies/create-input/create-input.test.ts index b5570c2..d31b904 100644 --- a/fragments/fragments-generator/src/strategy/tests/create-input.test.ts +++ b/fragments/fragments-generator/src/strategies/create-input/create-input.test.ts @@ -1,7 +1,7 @@ import { Fragment } from 'react' import { createElement } from 'react' -import { CreateInputStrategy } from '../create-input.strategy.js' +import { CreateInputStrategy } from './create-input.strategy.js' jest.mock('react', () => ({ createElement: jest.fn(), diff --git a/fragments/fragments-generator/src/strategies/create-input/index.ts b/fragments/fragments-generator/src/strategies/create-input/index.ts new file mode 100644 index 0000000..6f5a20b --- /dev/null +++ b/fragments/fragments-generator/src/strategies/create-input/index.ts @@ -0,0 +1 @@ +export * from './create-input.strategy.js' diff --git a/fragments/fragments-generator/src/strategy/create-text.strategy.ts b/fragments/fragments-generator/src/strategies/create-text/create-text.strategy.ts similarity index 95% rename from fragments/fragments-generator/src/strategy/create-text.strategy.ts rename to fragments/fragments-generator/src/strategies/create-text/create-text.strategy.ts index 908f2c4..db3f993 100644 --- a/fragments/fragments-generator/src/strategy/create-text.strategy.ts +++ b/fragments/fragments-generator/src/strategies/create-text/create-text.strategy.ts @@ -4,7 +4,7 @@ import type { TypeStyle } from 'figma-js' import { createElement } from 'react' -import { ThemeMappingStrategy } from './theme-mapping.strategy.js' +import { ThemeMappingStrategy } from '../theme-mapping/index.js' export class CreateTextStrategy extends ThemeMappingStrategy { private createAttributes(style: TypeStyle, fills: readonly Paint[]) { diff --git a/fragments/fragments-generator/src/strategy/tests/create-text.test.ts b/fragments/fragments-generator/src/strategies/create-text/create-text.test.ts similarity index 93% rename from fragments/fragments-generator/src/strategy/tests/create-text.test.ts rename to fragments/fragments-generator/src/strategies/create-text/create-text.test.ts index 8238dab..9f9e266 100644 --- a/fragments/fragments-generator/src/strategy/tests/create-text.test.ts +++ b/fragments/fragments-generator/src/strategies/create-text/create-text.test.ts @@ -1,8 +1,9 @@ -import { Text } from 'figma-js' +import type { Text } from 'figma-js' + import { createElement } from 'react' -import { CreateTextStrategy } from '../create-text.strategy.js' -import { theme } from './tests.constants.js' +import { CreateTextStrategy } from './create-text.strategy.js' +import { theme } from '../strategies.constants.js' jest.mock('react', () => ({ createElement: jest.fn(), diff --git a/fragments/fragments-generator/src/strategies/create-text/index.ts b/fragments/fragments-generator/src/strategies/create-text/index.ts new file mode 100644 index 0000000..6645ba9 --- /dev/null +++ b/fragments/fragments-generator/src/strategies/create-text/index.ts @@ -0,0 +1 @@ +export * from './create-text.strategy.js' diff --git a/fragments/fragments-generator/src/strategies/index.ts b/fragments/fragments-generator/src/strategies/index.ts new file mode 100644 index 0000000..553a975 --- /dev/null +++ b/fragments/fragments-generator/src/strategies/index.ts @@ -0,0 +1,6 @@ +export * from './create-box/index.js' +export * from './create-button/index.js' +export * from './create-fragment/index.js' +export * from './create-input/index.js' +export * from './create-text/index.js' +export * from './theme-mapping/index.js' diff --git a/fragments/fragments-generator/src/strategy/tests/tests.constants.ts b/fragments/fragments-generator/src/strategies/strategies.constants.ts similarity index 89% rename from fragments/fragments-generator/src/strategy/tests/tests.constants.ts rename to fragments/fragments-generator/src/strategies/strategies.constants.ts index 78c8656..663d267 100644 --- a/fragments/fragments-generator/src/strategy/tests/tests.constants.ts +++ b/fragments/fragments-generator/src/strategies/strategies.constants.ts @@ -1,3 +1,7 @@ +export const THEME_KEY_PREFIX = '$' + +export const colorsIgnorePatterns = ['button', 'input'] + export const theme = { colors: { white: 'rgba(255, 255, 255, 1)', diff --git a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts b/fragments/fragments-generator/src/strategies/strategies.interfaces.ts similarity index 89% rename from fragments/fragments-generator/src/strategy/strategy.interfaces.ts rename to fragments/fragments-generator/src/strategies/strategies.interfaces.ts index f70cfa6..65d5696 100644 --- a/fragments/fragments-generator/src/strategy/strategy.interfaces.ts +++ b/fragments/fragments-generator/src/strategies/strategies.interfaces.ts @@ -1,4 +1,4 @@ -import { ReactElement } from 'react' +import type { ReactElement } from 'react' export interface TreeElement { childrenIds: Array diff --git a/fragments/fragments-generator/src/strategies/theme-mapping/index.ts b/fragments/fragments-generator/src/strategies/theme-mapping/index.ts new file mode 100644 index 0000000..49b999b --- /dev/null +++ b/fragments/fragments-generator/src/strategies/theme-mapping/index.ts @@ -0,0 +1 @@ +export * from './theme-mapping.strategy.js' diff --git a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts b/fragments/fragments-generator/src/strategies/theme-mapping/theme-mapping.strategy.ts similarity index 97% rename from fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts rename to fragments/fragments-generator/src/strategies/theme-mapping/theme-mapping.strategy.ts index 5980415..27f5462 100644 --- a/fragments/fragments-generator/src/strategy/theme-mapping.strategy.ts +++ b/fragments/fragments-generator/src/strategies/theme-mapping/theme-mapping.strategy.ts @@ -7,8 +7,8 @@ import { toColorOpacityString } from '@atls/figma-utils' import { toColorString } from '@atls/figma-utils' import { toPxString } from '@atls/figma-utils' -import { THEME_KEY_PREFIX } from './strategy.constants.js' -import { colorsIgnorePatterns } from './strategy.constants.js' +import { THEME_KEY_PREFIX } from '../strategies.constants.js' +import { colorsIgnorePatterns } from '../strategies.constants.js' export class ThemeMappingStrategy { private theme: Record> = {} @@ -153,7 +153,7 @@ export class ThemeMappingStrategy { } getShadow(effects: readonly Effect[]): string | undefined { - const shadows: string[] = [] + const shadows: Array = [] effects.forEach(({ type, radius, offset, color }) => { if (['DROP_SHADOW', 'INNER_SHADOW'].includes(type) && offset && color) { diff --git a/fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts b/fragments/fragments-generator/src/strategies/theme-mapping/theme-mapping.test.ts similarity index 95% rename from fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts rename to fragments/fragments-generator/src/strategies/theme-mapping/theme-mapping.test.ts index 8d1577b..957a580 100644 --- a/fragments/fragments-generator/src/strategy/tests/theme-mapping.test.ts +++ b/fragments/fragments-generator/src/strategies/theme-mapping/theme-mapping.test.ts @@ -1,9 +1,9 @@ -import { Effect } from 'figma-js' -import { Paint } from 'figma-js' +import type { Effect } from 'figma-js' +import type { Paint } from 'figma-js' -import { THEME_KEY_PREFIX } from '../strategy.constants.js' -import { ThemeMappingStrategy } from '../theme-mapping.strategy.js' -import { theme } from './tests.constants.js' +import { THEME_KEY_PREFIX } from '../strategies.constants.js' +import { ThemeMappingStrategy } from './theme-mapping.strategy.js' +import { theme } from '../strategies.constants.js' describe('ThemeMappingStrategy', () => { let strategy: ThemeMappingStrategy diff --git a/fragments/fragments-generator/src/strategy/strategy.constants.ts b/fragments/fragments-generator/src/strategy/strategy.constants.ts deleted file mode 100644 index 185c3d6..0000000 --- a/fragments/fragments-generator/src/strategy/strategy.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const THEME_KEY_PREFIX = '$' - -export const colorsIgnorePatterns = ['button', 'input'] From 5c37c9aa0ed231b071711a30ed53cf6d5e0d3fe8 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 16 Dec 2024 17:59:01 +0300 Subject: [PATCH 45/48] docs: add fragments-generator to readme --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 47730d5..24be07a 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,25 @@ ID = SHoss54mn2SZLnI0e3OiJj Первым промптом с вас спросят Access Token. +## Генератор фрагментов + +Пакет `@atls/figma-fragments-cli` является энтрипоинтом по созданию фрагментов. Для создание `` фрагмента из дизайна берутся ноды c типом `FRAME`, для `` с типом `TEXT`, `