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

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sit-onyx/figma-utils
2
2
 
3
- Utility functions and CLI for importing data from the Figma API into CSS/SCSS variables.
3
+ Utility functions and CLI for importing data from the Figma API into different formats (e.g. CSS, SCSS etc.).
4
4
 
5
5
  ## Use as CLI
6
6
 
@@ -1,2 +1,15 @@
1
1
  import { Command } from "commander";
2
+ export type ImportCommandOptions = {
3
+ fileKey: string;
4
+ token: string;
5
+ filename: string;
6
+ format: string[];
7
+ dir?: string;
8
+ modes?: string[];
9
+ selector: string;
10
+ };
2
11
  export declare const importCommand: Command;
12
+ /**
13
+ * Action to run when executing the import action. Only intended to be called manually for testing.
14
+ */
15
+ export declare function importCommandAction(options: ImportCommandOptions): Promise<void>;
@@ -1,53 +1,63 @@
1
1
  import { Command } from "commander";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { DEFAULT_MODE_NAME, fetchFigmaVariables, generateAsCSS, generateAsSCSS, parseFigmaVariables, } from "../index.js";
4
+ import { DEFAULT_MODE_NAME, fetchFigmaVariables, generateAsCSS, generateAsJSON, generateAsSCSS, parseFigmaVariables, } from "../index.js";
5
5
  export const importCommand = new Command("import-variables")
6
6
  .description("CLI tool to import Figma variables into CSS, SCSS etc. variables.")
7
7
  .requiredOption("-k, --file-key <string>", "Figma file key (required)")
8
8
  .requiredOption("-t, --token <string>", "Figma access token with scope `file_variables:read` (required)")
9
- .option("-f, --format <string>", "Output format. Supported are: CSS, SCSS", "CSS")
9
+ .option("-f, --format <strings...>", "Output formats. Supported are: CSS, SCSS, JSON", ["CSS"])
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
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")
14
- .action(async (options) => {
14
+ .action(importCommandAction);
15
+ /**
16
+ * Action to run when executing the import action. Only intended to be called manually for testing.
17
+ */
18
+ export async function importCommandAction(options) {
15
19
  const generators = {
16
- CSS: (data) => generateAsCSS(data, options.selector),
20
+ CSS: (data) => generateAsCSS(data, { selector: options.selector }),
17
21
  SCSS: generateAsSCSS,
22
+ JSON: generateAsJSON,
18
23
  };
19
- if (!(options.format in generators)) {
20
- throw new Error(`Unknown format: ${options.format}. Supported: ${Object.keys(generators).join(", ")}`);
21
- }
24
+ options.format.forEach((format) => {
25
+ if (!(format in generators)) {
26
+ throw new Error(`Unknown format "${format}". Supported: ${Object.keys(generators).join(", ")}`);
27
+ }
28
+ });
22
29
  console.log("Fetching variables from Figma API...");
23
30
  const data = await fetchFigmaVariables(options.fileKey, options.token);
24
31
  console.log("Parsing Figma variables...");
25
32
  const parsedVariables = parseFigmaVariables(data);
26
33
  if (options.modes?.length) {
27
34
  // verify that all modes are found
28
- for (const mode of options.modes) {
35
+ options.modes.forEach((mode) => {
29
36
  if (parsedVariables.find((i) => i.modeName === mode))
30
- continue;
37
+ return;
31
38
  const availableModes = parsedVariables
32
39
  .map((i) => i.modeName ?? DEFAULT_MODE_NAME)
33
40
  .map((mode) => `"${mode}"`);
34
41
  throw new Error(`Mode "${mode}" not found. Available modes: ${Object.values(availableModes).join(", ")}`);
35
- }
42
+ });
36
43
  }
37
44
  const outputDirectory = options.dir ?? process.cwd();
38
45
  const filename = options.filename ?? "variables";
39
46
  console.log(`Generating ${options.format} variables...`);
40
- parsedVariables.forEach((data) => {
41
- // if the user passed specific modes to be exported, we will only generate those
42
- // otherwise all modes will be exported.
43
- // the default mode (undefined data.modeName) is always generated because its mode name can
44
- // not be specified by the designer in Figma
45
- const isModeIncluded = !options.modes?.length || !data.modeName || options.modes.includes(data.modeName);
46
- if (!isModeIncluded)
47
- return;
48
- const baseName = data.modeName ? `${filename}-${data.modeName}` : filename;
49
- const fullPath = path.join(outputDirectory, `${baseName}.${options.format.toLowerCase()}`);
50
- fs.writeFileSync(fullPath, generators[options.format](data));
47
+ options.format.forEach((format) => {
48
+ console.log(`Generating ${format} variables...`);
49
+ parsedVariables.forEach((data) => {
50
+ // if the user passed specific modes to be exported, we will only generate those
51
+ // otherwise all modes will be exported.
52
+ // the default mode (undefined data.modeName) is always generated because its mode name can
53
+ // not be specified by the designer in Figma
54
+ const isModeIncluded = !options.modes?.length || !data.modeName || options.modes.includes(data.modeName);
55
+ if (!isModeIncluded)
56
+ return;
57
+ const baseName = data.modeName ? `${filename}-${data.modeName}` : filename;
58
+ const fullPath = path.join(outputDirectory, `${baseName}.${format.toLowerCase()}`);
59
+ fs.writeFileSync(fullPath, generators[format](data));
60
+ });
51
61
  });
52
62
  console.log("Done.");
53
- });
63
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
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,19 +1,64 @@
1
1
  import { ParsedVariable } from "../types/figma.js";
2
+ export type BaseGenerateOptions = {
3
+ /**
4
+ * If `true`, alias variable values will be resolved to their actual value instead
5
+ * of using a reference by their name.
6
+ *
7
+ * @default false
8
+ */
9
+ resolveAlias?: boolean;
10
+ };
11
+ export type GenerateAsCSSOptions = BaseGenerateOptions & {
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"
15
+ *
16
+ * @default ":root"
17
+ * @example
18
+ * for the mode named "dark", passing the selector "html" will result in "html.dark"
19
+ */
20
+ selector?: string;
21
+ };
2
22
  /**
3
23
  * Generates the given parsed Figma variables into CSS variables.
4
24
  *
5
25
  * @param data Parsed Figma variables
6
- * @param selector CSS selector to use for the CSS format. The mode name will be added to the selector
7
- * 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"
26
+ * @param options Optional options to fine-tune the generated output
8
27
  * @returns File content of the .css file
9
28
  */
10
- export declare const generateAsCSS: (data: ParsedVariable, selector?: string) => string;
29
+ export declare const generateAsCSS: (data: ParsedVariable, options?: GenerateAsCSSOptions) => string;
11
30
  /**
12
31
  * Generates the given parsed Figma variables into SCSS variables.
13
32
  *
14
33
  * @returns File content of the .scss file
15
34
  */
16
- export declare const generateAsSCSS: (data: ParsedVariable) => string;
35
+ export declare const generateAsSCSS: (data: ParsedVariable, options?: BaseGenerateOptions) => string;
36
+ /**
37
+ * Generates the given parsed Figma variables as JSON.
38
+ * Alias variables will be resolved to their actual value.
39
+ *
40
+ * @returns File content of the .json file
41
+ */
42
+ export declare const generateAsJSON: (data: ParsedVariable) => string;
43
+ /**
44
+ * Recursively resolves the value for the given variable name.
45
+ * So if the value is an alias, the output will be the actual alias value instead of a reference by name.
46
+ * If the value is not an alias, its value will be directly returned.
47
+ *
48
+ * @param name Variable name to resolve
49
+ * @param allVariables All available variables
50
+ * @example
51
+ * ```ts
52
+ * const allVariables = {
53
+ * "variable-a": 42,
54
+ * "variable-b": "{variable-a}"
55
+ * }
56
+ *
57
+ * const resolvedValue = resolveValue("variable-b", allVariables);
58
+ * // const resolvedValue = 42;
59
+ * ```
60
+ */
61
+ export declare const resolveValue: (name: string, allVariables: ParsedVariable["variables"]) => string;
17
62
  /**
18
63
  * Generates the timestamp comment that is added to the start of every generated file.
19
64
  */
@@ -23,9 +68,9 @@ export declare const generateTimestampComment: (modeName?: string) => string;
23
68
  * Alias values are enclosed by curly braces.
24
69
  *
25
70
  * @example "{your-variable-name}"
26
- * @returns `isAlias` whether the variable is an alias and `variableName` the raw variable name without curly braces.
71
+ * @returns `isAlias` whether the variable is an alias and `aliasName` the raw variable name without curly braces.
27
72
  */
28
73
  export declare const isAliasVariable: (variableValue: string) => {
29
74
  isAlias: RegExpExecArray | null;
30
- variableName: string;
75
+ aliasName: string;
31
76
  };
@@ -2,17 +2,12 @@
2
2
  * Generates the given parsed Figma variables into CSS variables.
3
3
  *
4
4
  * @param data Parsed Figma variables
5
- * @param selector CSS selector to use for the CSS format. The mode name will be added to the selector
6
- * 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"
5
+ * @param options Optional options to fine-tune the generated output
7
6
  * @returns File content of the .css file
8
7
  */
9
- export const generateAsCSS = (data, selector = ":root") => {
10
- const variableContent = Object.entries(data.variables).map(([name, value]) => {
11
- const { isAlias, variableName } = isAliasVariable(value);
12
- const variableValue = isAlias ? `var(--${variableName})` : value;
13
- return ` --${name}: ${variableValue};`;
14
- });
15
- let fullSelector = selector.trim();
8
+ export const generateAsCSS = (data, options) => {
9
+ const variableContent = getCssOrScssVariableContent(data.variables, (name) => ` --${name}`, (name) => `var(--${name})`, options);
10
+ let fullSelector = options?.selector?.trim() || ":root";
16
11
  if (fullSelector !== ":root")
17
12
  fullSelector += `.${data.modeName}`;
18
13
  return `${generateTimestampComment(data.modeName)}
@@ -23,14 +18,49 @@ ${fullSelector} {\n${variableContent.join("\n")}\n}\n`;
23
18
  *
24
19
  * @returns File content of the .scss file
25
20
  */
26
- export const generateAsSCSS = (data) => {
27
- const variableContent = Object.entries(data.variables).map(([name, value]) => {
28
- const { isAlias, variableName } = isAliasVariable(value);
29
- const variableValue = isAlias ? `$${variableName}` : value;
30
- return `$${name}: ${variableValue};`;
31
- });
21
+ export const generateAsSCSS = (data, options) => {
22
+ const variableContent = getCssOrScssVariableContent(data.variables, (name) => `$${name}`, (name) => `$${name}`, options);
32
23
  return `${generateTimestampComment(data.modeName)}\n${variableContent.join("\n")}\n`;
33
24
  };
25
+ /**
26
+ * Generates the given parsed Figma variables as JSON.
27
+ * Alias variables will be resolved to their actual value.
28
+ *
29
+ * @returns File content of the .json file
30
+ */
31
+ export const generateAsJSON = (data) => {
32
+ const variables = structuredClone(data.variables);
33
+ // recursively resolve aliases to plain values since keys can not be referenced in a .json file
34
+ // like we could e.g. in a .css file
35
+ Object.keys(variables).forEach((name) => {
36
+ variables[name] = resolveValue(name, variables);
37
+ });
38
+ return `${JSON.stringify(variables, null, 2)}\n`;
39
+ };
40
+ /**
41
+ * Recursively resolves the value for the given variable name.
42
+ * So if the value is an alias, the output will be the actual alias value instead of a reference by name.
43
+ * If the value is not an alias, its value will be directly returned.
44
+ *
45
+ * @param name Variable name to resolve
46
+ * @param allVariables All available variables
47
+ * @example
48
+ * ```ts
49
+ * const allVariables = {
50
+ * "variable-a": 42,
51
+ * "variable-b": "{variable-a}"
52
+ * }
53
+ *
54
+ * const resolvedValue = resolveValue("variable-b", allVariables);
55
+ * // const resolvedValue = 42;
56
+ * ```
57
+ */
58
+ export const resolveValue = (name, allVariables) => {
59
+ const { isAlias, aliasName } = isAliasVariable(allVariables[name]);
60
+ if (!isAlias)
61
+ return allVariables[name];
62
+ return resolveValue(aliasName, allVariables);
63
+ };
34
64
  /**
35
65
  * Generates the timestamp comment that is added to the start of every generated file.
36
66
  */
@@ -45,10 +75,29 @@ export const generateTimestampComment = (modeName) => {
45
75
  * Alias values are enclosed by curly braces.
46
76
  *
47
77
  * @example "{your-variable-name}"
48
- * @returns `isAlias` whether the variable is an alias and `variableName` the raw variable name without curly braces.
78
+ * @returns `isAlias` whether the variable is an alias and `aliasName` the raw variable name without curly braces.
49
79
  */
50
80
  export const isAliasVariable = (variableValue) => {
51
81
  const isAlias = /{.*}/.exec(variableValue);
52
- const variableName = variableValue.replace("{", "").replace("}", "");
53
- return { isAlias, variableName };
82
+ const aliasName = variableValue.replace("{", "").replace("}", "");
83
+ return { isAlias, aliasName };
84
+ };
85
+ /**
86
+ * Gets the variable file content of the CSS or SCSS file as array where each element
87
+ * represents a single line of the file.
88
+ *
89
+ * @param variables Variable data (name + value)
90
+ * @param nameFormatter Function to format the variable name
91
+ * @param aliasFormatter Function to format a reference to another variable (e.g. `var(--name)` for CSS)
92
+ * @param options Generator options
93
+ */
94
+ const getCssOrScssVariableContent = (variables, nameFormatter, aliasFormatter, options) => {
95
+ return Object.entries(variables).map(([name, value]) => {
96
+ const { isAlias, aliasName } = isAliasVariable(value);
97
+ let variableValue = isAlias ? aliasFormatter(aliasName) : value;
98
+ if (isAlias && options?.resolveAlias) {
99
+ variableValue = resolveValue(name, variables);
100
+ }
101
+ return `${nameFormatter(name)}: ${variableValue};`;
102
+ });
54
103
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
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
+ });
@@ -34,8 +34,30 @@ export const parseFigmaVariables = (apiResponse, options) => {
34
34
  });
35
35
  });
36
36
  parsedData.forEach((data) => {
37
- if (data.modeName === "DEFAULT_MODE_NAME")
37
+ if (data.modeName === DEFAULT_MODE_NAME)
38
38
  delete data.modeName;
39
+ const numberRegex = /\d+/;
40
+ // sort variables by name
41
+ // for variables with the same name that just end with a different number (e.g. my-var-100 and my-var-200)
42
+ // sort them by number instead of alphabetically so e.g. 100 is sorted before 1000
43
+ data.variables = Object.keys(data.variables)
44
+ .map((key) => {
45
+ const asNumber = numberRegex.exec(key)?.[0] ?? "";
46
+ return {
47
+ key,
48
+ asNumber: +asNumber || undefined, // prevent NaN
49
+ base: key.replace(asNumber, ""),
50
+ };
51
+ })
52
+ .sort((a, b) => {
53
+ if (a.asNumber && b.asNumber && a.base === b.base)
54
+ return a.asNumber - b.asNumber;
55
+ return a.key.localeCompare(b.key);
56
+ })
57
+ .reduce((variables, { key }) => {
58
+ variables[key] = data.variables[key];
59
+ return variables;
60
+ }, {});
39
61
  });
40
62
  return parsedData;
41
63
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,187 @@
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
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sit-onyx/figma-utils",
3
- "description": "Utility functions and CLI for importing data from the Figma API into CSS/SCSS variables",
4
- "version": "1.0.0-alpha.0",
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",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "@sit-onyx/figma-utils": "./dist/cli.js"
@@ -22,7 +22,11 @@
22
22
  "commander": "^11.1.0"
23
23
  },
24
24
  "scripts": {
25
- "build": "rimraf dist && tsc",
26
- "@sit-onyx/figma-utils": "node ./dist/cli.js"
25
+ "build": "pnpm run '/type-check|build-only/'",
26
+ "build-only": "rimraf dist && tsc -p tsconfig.node.json --composite false",
27
+ "type-check": "tsc --noEmit -p tsconfig.vitest.json --composite false",
28
+ "@sit-onyx/figma-utils": "node ./dist/cli.js",
29
+ "test": "vitest",
30
+ "test:coverage": "vitest run --coverage"
27
31
  }
28
32
  }