@sit-onyx/figma-utils 1.0.0-alpha.0 → 1.0.0-alpha.1
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 +1 -1
- package/dist/commands/import-variables.d.ts +13 -0
- package/dist/commands/import-variables.js +32 -22
- package/dist/commands/import-variables.spec.d.ts +1 -0
- package/dist/commands/import-variables.spec.js +49 -0
- package/dist/utils/generate.d.ts +51 -6
- package/dist/utils/generate.js +67 -18
- package/dist/utils/generate.spec.d.ts +1 -0
- package/dist/utils/generate.spec.js +77 -0
- package/dist/utils/parse.js +1 -1
- package/dist/utils/parse.spec.d.ts +1 -0
- package/dist/utils/parse.spec.js +187 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -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 <
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
35
|
+
options.modes.forEach((mode) => {
|
|
29
36
|
if (parsedVariables.find((i) => i.modeName === mode))
|
|
30
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
});
|
package/dist/utils/generate.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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 `
|
|
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
|
-
|
|
75
|
+
aliasName: string;
|
|
31
76
|
};
|
package/dist/utils/generate.js
CHANGED
|
@@ -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
|
|
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,
|
|
10
|
-
const variableContent =
|
|
11
|
-
|
|
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 =
|
|
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 `
|
|
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
|
|
53
|
-
return { isAlias,
|
|
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
|
+
});
|
package/dist/utils/parse.js
CHANGED
|
@@ -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
|
|
4
|
-
"version": "1.0.0-alpha.
|
|
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.1",
|
|
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": "
|
|
26
|
-
"
|
|
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
|
}
|