@sit-onyx/figma-utils 1.0.0-alpha.2 → 1.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ export const importCommand = new Command("import-variables")
10
10
  .option("-n, --filename <string>", "Base name of the generated variables file", "variables")
11
11
  .option("-d, --dir <string>", "Working directory to use. Defaults to current working directory of the script.")
12
12
  .option("-m, --modes <strings...>", "Can be used to only export specific Figma modes. If unset, all modes will be exported as a separate file.")
13
- .option("-s, --selector <string>", 'CSS selector to use for the CSS format. The mode name will be added to the selector if it is set to something other than ":root", e.g. for the mode named "dark", passing the selector "html" will result in "html.dark"', ":root")
13
+ .option("-s, --selector <string>", 'CSS selector to use for the CSS format. You can use {mode} as placeholder for the mode name, so e.g. for the mode named "dark", passing the selector "html.{mode}" will result in "html.dark"', ":root")
14
14
  .action(importCommandAction);
15
15
  /**
16
16
  * Action to run when executing the import action. Only intended to be called manually for testing.
@@ -10,12 +10,11 @@ export type BaseGenerateOptions = {
10
10
  };
11
11
  export type GenerateAsCSSOptions = BaseGenerateOptions & {
12
12
  /**
13
- * Selector to use for the CSS format. The mode name will be added to the selector
14
- * if it is set to something other than ":root"
13
+ * Selector to use for the CSS format. You can use {mode} as placeholder for the mode name.
15
14
  *
16
15
  * @default ":root"
17
16
  * @example
18
- * for the mode named "dark", passing the selector "html" will result in "html.dark"
17
+ * for the mode named "dark", passing the selector "html.{mode}" will result in "html.dark"
19
18
  */
20
19
  selector?: string;
21
20
  };
@@ -7,9 +7,7 @@
7
7
  */
8
8
  export const generateAsCSS = (data, options) => {
9
9
  const variableContent = getCssOrScssVariableContent(data.variables, (name) => ` --${name}`, (name) => `var(--${name})`, options);
10
- let fullSelector = options?.selector?.trim() || ":root";
11
- if (fullSelector !== ":root")
12
- fullSelector += `.${data.modeName}`;
10
+ const fullSelector = options?.selector?.trim().replaceAll("{mode}", data.modeName ?? "") || ":root";
13
11
  return `${generateTimestampComment(data.modeName)}
14
12
  ${fullSelector} {\n${variableContent.join("\n")}\n}\n`;
15
13
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sit-onyx/figma-utils",
3
3
  "description": "Utility functions and CLI for importing data from the Figma API into different formats (e.g. CSS, SCSS etc.)",
4
- "version": "1.0.0-alpha.2",
4
+ "version": "1.0.0-alpha.4",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "@sit-onyx/figma-utils": "./dist/cli.js"
@@ -12,6 +12,7 @@
12
12
  "engines": {
13
13
  "node": ">=18"
14
14
  },
15
+ "types": "./dist/index.d.ts",
15
16
  "exports": {
16
17
  ".": {
17
18
  "types": "./dist/index.d.ts",
@@ -1 +0,0 @@
1
- export {};
@@ -1,49 +0,0 @@
1
- import fs from "node:fs";
2
- import { beforeEach, describe, expect, test, vi } from "vitest";
3
- import * as functions from "../index.js";
4
- import { importCommandAction } from "./import-variables.js";
5
- vi.mock("node:fs");
6
- vi.mock("../index.js");
7
- describe("import-variables.ts", () => {
8
- const mockOptions = {
9
- fileKey: "test-file-key",
10
- filename: "test-file-name",
11
- format: ["CSS"],
12
- token: "test-token",
13
- selector: ":root",
14
- };
15
- beforeEach(() => {
16
- vi.clearAllMocks();
17
- vi.spyOn(console, "log").mockImplementation(() => ({}));
18
- vi.spyOn(process, "cwd").mockReturnValue("test-cwd");
19
- });
20
- test("should throw error for unknown formats", () => {
21
- const promise = () => importCommandAction({ ...mockOptions, format: ["does-not-exist"] });
22
- expect(promise).rejects.toThrowError('Unknown format "does-not-exist". Supported: CSS, SCSS, JSON');
23
- });
24
- test("should throw error for unknown modes", () => {
25
- vi.spyOn(functions, "parseFigmaVariables").mockReturnValue([
26
- { modeName: "test-mode-1", variables: {} },
27
- ]);
28
- const promise = () => importCommandAction({
29
- ...mockOptions,
30
- modes: ["test-mode-1", "does-not-exist"],
31
- });
32
- expect(promise).rejects.toThrowError('Mode "does-not-exist" not found. Available modes: "test-mode-1"');
33
- });
34
- test("should generate variables", async () => {
35
- vi.spyOn(functions, "parseFigmaVariables").mockReturnValue([
36
- { modeName: "test-mode-1", variables: {} },
37
- { modeName: "test-mode-2", variables: {} },
38
- { modeName: "test-mode-3", variables: {} },
39
- ]);
40
- vi.spyOn(functions, "generateAsCSS").mockReturnValue("mock-css-file-content");
41
- await importCommandAction({ ...mockOptions, modes: ["test-mode-1", "test-mode-2"] });
42
- expect(functions.fetchFigmaVariables).toHaveBeenCalledOnce();
43
- expect(functions.parseFigmaVariables).toHaveBeenCalledOnce();
44
- expect(functions.generateAsCSS).toHaveBeenCalledTimes(2);
45
- expect(fs.writeFileSync).toHaveBeenCalledTimes(2);
46
- expect(fs.writeFileSync).toHaveBeenCalledWith("test-cwd/test-file-name-test-mode-1.css", "mock-css-file-content");
47
- expect(fs.writeFileSync).toHaveBeenCalledWith("test-cwd/test-file-name-test-mode-2.css", "mock-css-file-content");
48
- });
49
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,77 +0,0 @@
1
- import { beforeEach, describe, expect, test, vi } from "vitest";
2
- import { generateAsCSS, generateAsJSON, generateAsSCSS } from "./generate.js";
3
- describe("generate.ts", () => {
4
- const mockData = {
5
- modeName: "test-mode-1",
6
- variables: {
7
- "test-1": "#ffffff",
8
- "test-2": "1rem",
9
- "test-3": "{test-2}",
10
- },
11
- };
12
- beforeEach(() => {
13
- vi.setSystemTime(new Date(2024, 0, 7, 13, 42));
14
- });
15
- test("should generate as CSS", () => {
16
- const fileContent = generateAsCSS(mockData);
17
- expect(fileContent).toBe(`/**
18
- * Do not edit directly.
19
- * This file contains the specific variables for the "test-mode-1" theme.
20
- * Imported from Figma API on Sun, 07 Jan 2024 13:42:00 GMT
21
- */
22
- :root {
23
- --test-1: #ffffff;
24
- --test-2: 1rem;
25
- --test-3: var(--test-2);
26
- }
27
- `);
28
- });
29
- test("should generate as CSS with custom selector", () => {
30
- const fileContent = generateAsCSS(mockData, { selector: "html" });
31
- expect(fileContent).toBe(`/**
32
- * Do not edit directly.
33
- * This file contains the specific variables for the "test-mode-1" theme.
34
- * Imported from Figma API on Sun, 07 Jan 2024 13:42:00 GMT
35
- */
36
- html.test-mode-1 {
37
- --test-1: #ffffff;
38
- --test-2: 1rem;
39
- --test-3: var(--test-2);
40
- }
41
- `);
42
- });
43
- test("should generate as CSS with resolved aliases", () => {
44
- const fileContent = generateAsCSS(mockData, { resolveAlias: true });
45
- expect(fileContent).toBe(`/**
46
- * Do not edit directly.
47
- * This file contains the specific variables for the "test-mode-1" theme.
48
- * Imported from Figma API on Sun, 07 Jan 2024 13:42:00 GMT
49
- */
50
- :root {
51
- --test-1: #ffffff;
52
- --test-2: 1rem;
53
- --test-3: 1rem;
54
- }
55
- `);
56
- });
57
- test("should generate as SCSS", () => {
58
- const fileContent = generateAsSCSS(mockData);
59
- expect(fileContent).toBe(`/**
60
- * Do not edit directly.
61
- * This file contains the specific variables for the "test-mode-1" theme.
62
- * Imported from Figma API on Sun, 07 Jan 2024 13:42:00 GMT
63
- */
64
- $test-1: #ffffff;
65
- $test-2: 1rem;
66
- $test-3: $test-2;
67
- `);
68
- });
69
- test("should generate as JSON", () => {
70
- const fileContent = generateAsJSON(mockData);
71
- expect(JSON.parse(fileContent)).toStrictEqual({
72
- "test-1": "#ffffff",
73
- "test-2": "1rem",
74
- "test-3": "1rem",
75
- });
76
- });
77
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,187 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { DEFAULT_MODE_NAME, normalizeVariableName, parseFigmaVariables, resolveFigmaVariableValue, rgbaToHex, } from "./parse.js";
3
- describe("parse.ts", () => {
4
- test("should convert RGBA to hex color", () => {
5
- let hex = rgbaToHex({ r: 1, g: 1, b: 1, a: 1 });
6
- expect(hex).toBe("#ffffff");
7
- hex = rgbaToHex({ r: 1, g: 0, b: 0, a: 1 });
8
- expect(hex).toBe("#ff0000");
9
- hex = rgbaToHex({ r: 0, g: 1, b: 0, a: 1 });
10
- expect(hex).toBe("#00ff00");
11
- hex = rgbaToHex({ r: 0, g: 0, b: 1, a: 1 });
12
- expect(hex).toBe("#0000ff");
13
- hex = rgbaToHex({ r: 0, g: 0, b: 0, a: 0 });
14
- expect(hex).toBe("#00000000");
15
- });
16
- test("should normalize variable name", () => {
17
- const name = normalizeVariableName("a/b c+d&e");
18
- expect(name).toBe("a-b-c-d-e");
19
- });
20
- test("should resolve color variable value", () => {
21
- const value = { r: 1, g: 1, b: 1, a: 1 };
22
- const resolvedValue = resolveFigmaVariableValue(value, {});
23
- expect(resolvedValue).toBe("#ffffff");
24
- });
25
- test("should resolve numeric value and convert to rem", () => {
26
- // with default rem base
27
- let resolvedValue = resolveFigmaVariableValue(16, {});
28
- expect(resolvedValue).toBe("1rem");
29
- // with individual rem base
30
- resolvedValue = resolveFigmaVariableValue(16, {}, 8);
31
- expect(resolvedValue).toBe("2rem");
32
- // without rem base (pixel value)
33
- resolvedValue = resolveFigmaVariableValue(16, {}, false);
34
- expect(resolvedValue).toBe("16px");
35
- });
36
- test("should resolve alias value", () => {
37
- const value = { type: "VARIABLE_ALIAS", id: "test-1" };
38
- const allVariables = {
39
- "test-1": {
40
- hiddenFromPublishing: false,
41
- name: "test-variable-1",
42
- valuesByMode: {},
43
- variableCollectionId: "collection-1",
44
- },
45
- };
46
- const resolvedValue = resolveFigmaVariableValue(value, allVariables);
47
- expect(resolvedValue).toBe("{test-variable-1}");
48
- // should throw error if alias can not be found
49
- expect(() => resolveFigmaVariableValue({ type: "VARIABLE_ALIAS", id: "does-not-exist" }, allVariables)).toThrowError();
50
- });
51
- test("should parse all Figma variables", () => {
52
- const apiResponse = {
53
- meta: {
54
- variableCollections: {
55
- "collection-1": {
56
- hiddenFromPublishing: false,
57
- defaultModeId: "test-1",
58
- modes: [
59
- { modeId: "test-1", name: "Test 1" },
60
- { modeId: "test-2", name: "Test 2" },
61
- { modeId: "test-3", name: "Test 3" },
62
- ],
63
- },
64
- "collection-2": {
65
- hiddenFromPublishing: false,
66
- defaultModeId: "test-2",
67
- modes: [
68
- { modeId: "test-1", name: "Test 1" },
69
- { modeId: "test-2", name: "Test 2" },
70
- { modeId: "test-3", name: "Test 3" },
71
- ],
72
- },
73
- },
74
- variables: {
75
- "variable-1": {
76
- hiddenFromPublishing: false,
77
- name: "variable-1",
78
- variableCollectionId: "collection-1",
79
- valuesByMode: {
80
- "test-1": { r: 1, g: 1, b: 1, a: 1 },
81
- "test-2": { type: "VARIABLE_ALIAS", id: "variable-2" },
82
- "test-3": 42,
83
- },
84
- },
85
- "variable-2": {
86
- hiddenFromPublishing: false,
87
- name: "variable-2",
88
- variableCollectionId: "collection-2",
89
- valuesByMode: {
90
- "test-1": { type: "VARIABLE_ALIAS", id: "variable-1" },
91
- "test-2": 42,
92
- "test-3": { r: 1, g: 1, b: 1, a: 1 },
93
- },
94
- },
95
- },
96
- },
97
- };
98
- const parsedVariables = parseFigmaVariables(apiResponse);
99
- expect(parsedVariables).toStrictEqual([
100
- {
101
- modeName: "Test 1",
102
- variables: {
103
- "variable-1": "#ffffff",
104
- "variable-2": "{variable-1}",
105
- },
106
- },
107
- {
108
- modeName: "Test 2",
109
- variables: {
110
- "variable-1": "{variable-2}",
111
- "variable-2": "2.625rem",
112
- },
113
- },
114
- {
115
- modeName: "Test 3",
116
- variables: {
117
- "variable-1": "2.625rem",
118
- "variable-2": "#ffffff",
119
- },
120
- },
121
- ]);
122
- });
123
- test("should ignore variables/collections that are hidden from publishing", () => {
124
- const apiResponse = {
125
- meta: {
126
- variableCollections: {
127
- "collection-1": {
128
- hiddenFromPublishing: true,
129
- defaultModeId: "test-1",
130
- modes: [{ modeId: "test-1", name: "Test 1" }],
131
- },
132
- },
133
- variables: {
134
- "variable-1": {
135
- hiddenFromPublishing: true,
136
- name: "variable-1",
137
- variableCollectionId: "collection-1",
138
- valuesByMode: {
139
- "test-1": 42,
140
- },
141
- },
142
- "variable-2": {
143
- hiddenFromPublishing: false,
144
- name: "variable-2",
145
- variableCollectionId: "collection-1",
146
- valuesByMode: {
147
- "test-1": 42,
148
- },
149
- },
150
- },
151
- },
152
- };
153
- const parsedVariables = parseFigmaVariables(apiResponse);
154
- expect(parsedVariables).toStrictEqual([]);
155
- });
156
- test("should clear mode name if its the default Figma mode name", () => {
157
- const apiResponse = {
158
- meta: {
159
- variableCollections: {
160
- "collection-1": {
161
- hiddenFromPublishing: false,
162
- defaultModeId: "test-1",
163
- modes: [{ modeId: "test-1", name: DEFAULT_MODE_NAME }],
164
- },
165
- },
166
- variables: {
167
- "variable-1": {
168
- hiddenFromPublishing: false,
169
- name: "variable-1",
170
- variableCollectionId: "collection-1",
171
- valuesByMode: {
172
- "test-1": 16,
173
- },
174
- },
175
- },
176
- },
177
- };
178
- const parsedVariables = parseFigmaVariables(apiResponse);
179
- expect(parsedVariables).toStrictEqual([
180
- {
181
- variables: {
182
- "variable-1": "1rem",
183
- },
184
- },
185
- ]);
186
- });
187
- });