@sit-onyx/figma-utils 0.0.0-20250804145452
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/LICENSE.txt +190 -0
- package/README.md +22 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -0
- package/dist/commands/import-flags.d.ts +13 -0
- package/dist/commands/import-flags.js +42 -0
- package/dist/commands/import-icons.d.ts +14 -0
- package/dist/commands/import-icons.js +44 -0
- package/dist/commands/import-variables.d.ts +16 -0
- package/dist/commands/import-variables.js +77 -0
- package/dist/flags/generate.d.ts +8 -0
- package/dist/flags/generate.js +18 -0
- package/dist/flags/parse.d.ts +12 -0
- package/dist/flags/parse.js +34 -0
- package/dist/icons/generate.d.ts +8 -0
- package/dist/icons/generate.js +18 -0
- package/dist/icons/parse.d.ts +18 -0
- package/dist/icons/parse.js +16 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/types/figma.d.ts +113 -0
- package/dist/types/figma.js +1 -0
- package/dist/utils/fetch.d.ts +26 -0
- package/dist/utils/fetch.js +50 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.js +15 -0
- package/dist/utils/optimize.d.ts +8 -0
- package/dist/utils/optimize.js +25 -0
- package/dist/variables/generate.d.ts +82 -0
- package/dist/variables/generate.js +123 -0
- package/dist/variables/parse.d.ts +42 -0
- package/dist/variables/parse.js +127 -0
- package/package.json +47 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma API response when fetching from https://api.figma.com/v1/files/${fileKey}/variables/local
|
|
3
|
+
*/
|
|
4
|
+
export type FigmaVariablesApiResponse = {
|
|
5
|
+
meta: {
|
|
6
|
+
variableCollections: Record<string, VariablesCollection>;
|
|
7
|
+
variables: Record<string, Variable>;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type VariablesCollection = {
|
|
11
|
+
defaultModeId: string;
|
|
12
|
+
hiddenFromPublishing: boolean;
|
|
13
|
+
modes: Mode[];
|
|
14
|
+
};
|
|
15
|
+
export type Mode = {
|
|
16
|
+
modeId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
export type Variable = {
|
|
20
|
+
name: string;
|
|
21
|
+
variableCollectionId: string;
|
|
22
|
+
hiddenFromPublishing: boolean;
|
|
23
|
+
deletedButReferenced?: boolean;
|
|
24
|
+
valuesByMode: Record<string, VariableValue>;
|
|
25
|
+
};
|
|
26
|
+
export type VariableValue = RGBAValue | ColorsAlias | number | string;
|
|
27
|
+
export type RGBAValue = {
|
|
28
|
+
r: number;
|
|
29
|
+
g: number;
|
|
30
|
+
b: number;
|
|
31
|
+
a: number;
|
|
32
|
+
};
|
|
33
|
+
export type ColorsAlias = {
|
|
34
|
+
type: "VARIABLE_ALIAS";
|
|
35
|
+
id: string;
|
|
36
|
+
};
|
|
37
|
+
export type ParsedVariable = {
|
|
38
|
+
/** Figma mode name or undefined if its the default mode. */
|
|
39
|
+
modeName?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Mapping from variable name to its value.
|
|
42
|
+
* @example
|
|
43
|
+
* ```json
|
|
44
|
+
* {
|
|
45
|
+
* "primary-100": "#ffffff",
|
|
46
|
+
* "border-radius-s": "1rem",
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
variables: Record<string, string>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Figma API response when fetching from https://api.figma.com/v1/files/${fileKey}/components
|
|
54
|
+
*/
|
|
55
|
+
export type FigmaComponentsApiResponse = {
|
|
56
|
+
meta: {
|
|
57
|
+
components: Component[];
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* An arrangement of published UI elements that can be instantiated across Figma files
|
|
62
|
+
*/
|
|
63
|
+
export type Component = {
|
|
64
|
+
/**
|
|
65
|
+
* ID of the component node within the Figma file
|
|
66
|
+
*/
|
|
67
|
+
node_id: string;
|
|
68
|
+
/**
|
|
69
|
+
* Name of the component
|
|
70
|
+
*/
|
|
71
|
+
name: string;
|
|
72
|
+
/**
|
|
73
|
+
* Data on component's containing frame, if component resides within a frame
|
|
74
|
+
*/
|
|
75
|
+
containing_frame: FrameInfo;
|
|
76
|
+
/**
|
|
77
|
+
* The description of the component as entered by the publisher
|
|
78
|
+
*/
|
|
79
|
+
description: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Data on the frame a component resides in
|
|
83
|
+
*/
|
|
84
|
+
export type FrameInfo = {
|
|
85
|
+
/**
|
|
86
|
+
* ID of the frame node within the file
|
|
87
|
+
*/
|
|
88
|
+
nodeId: string;
|
|
89
|
+
/**
|
|
90
|
+
* Name of the frame
|
|
91
|
+
*/
|
|
92
|
+
name: string;
|
|
93
|
+
/**
|
|
94
|
+
* ID of the frame's residing page
|
|
95
|
+
*/
|
|
96
|
+
pageId: string;
|
|
97
|
+
/**
|
|
98
|
+
* Name of the frame's residing page
|
|
99
|
+
*/
|
|
100
|
+
pageName: string;
|
|
101
|
+
};
|
|
102
|
+
export type ParsedIcon = {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
aliases: string[];
|
|
106
|
+
category: string;
|
|
107
|
+
};
|
|
108
|
+
export type ParsedFlag = {
|
|
109
|
+
id: string;
|
|
110
|
+
code: string;
|
|
111
|
+
internationalName: string;
|
|
112
|
+
continent: string;
|
|
113
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FigmaComponentsApiResponse, FigmaVariablesApiResponse } from "../types/figma.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches the Figma Variables for the given file from the Figma API v1.
|
|
4
|
+
*
|
|
5
|
+
* @param fileKey File key. Example: https://www.figma.com/file/your-file-key-here
|
|
6
|
+
* @param accessToken Personal access token with scope/permission `file_variables:read`
|
|
7
|
+
* @see https://www.figma.com/developers/api#get-local-variables-endpoint
|
|
8
|
+
*/
|
|
9
|
+
export declare const fetchFigmaVariables: (fileKey: string, accessToken: string) => Promise<FigmaVariablesApiResponse>;
|
|
10
|
+
/**
|
|
11
|
+
* Fetches the Figma components for the given file from the Figma API v1.
|
|
12
|
+
*
|
|
13
|
+
* @param fileKey File key. Example: https://www.figma.com/file/your-file-key-here
|
|
14
|
+
* @param accessToken Personal access token with scope/permission `file_read` or `files:read`
|
|
15
|
+
* @see https://www.figma.com/developers/api#get-file-components-endpoint
|
|
16
|
+
*/
|
|
17
|
+
export declare const fetchFigmaComponents: (fileKey: string, accessToken: string) => Promise<FigmaComponentsApiResponse>;
|
|
18
|
+
export declare const fetchFigmaSVGs: (fileKey: string, componentIds: string[], accessToken: string) => Promise<Record<string, string>>;
|
|
19
|
+
/**
|
|
20
|
+
* Generic utility to fetch Figma API routes.
|
|
21
|
+
*
|
|
22
|
+
* @param url API route, e.g. "https://api.figma.com/v1/files/${filekey}"
|
|
23
|
+
* @param accessToken Access token for authentication
|
|
24
|
+
* @throws Error if request was not successful
|
|
25
|
+
*/
|
|
26
|
+
export declare const fetchFigma: <T = unknown>(url: string, accessToken: string) => Promise<T>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches the Figma Variables for the given file from the Figma API v1.
|
|
3
|
+
*
|
|
4
|
+
* @param fileKey File key. Example: https://www.figma.com/file/your-file-key-here
|
|
5
|
+
* @param accessToken Personal access token with scope/permission `file_variables:read`
|
|
6
|
+
* @see https://www.figma.com/developers/api#get-local-variables-endpoint
|
|
7
|
+
*/
|
|
8
|
+
export const fetchFigmaVariables = async (fileKey, accessToken) => {
|
|
9
|
+
return fetchFigma(`https://api.figma.com/v1/files/${fileKey}/variables/local`, accessToken);
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Fetches the Figma components for the given file from the Figma API v1.
|
|
13
|
+
*
|
|
14
|
+
* @param fileKey File key. Example: https://www.figma.com/file/your-file-key-here
|
|
15
|
+
* @param accessToken Personal access token with scope/permission `file_read` or `files:read`
|
|
16
|
+
* @see https://www.figma.com/developers/api#get-file-components-endpoint
|
|
17
|
+
*/
|
|
18
|
+
export const fetchFigmaComponents = async (fileKey, accessToken) => {
|
|
19
|
+
return fetchFigma(`https://api.figma.com/v1/files/${fileKey}/components`, accessToken);
|
|
20
|
+
};
|
|
21
|
+
export const fetchFigmaSVGs = async (fileKey, componentIds, accessToken) => {
|
|
22
|
+
const result = await fetchFigma(`https://api.figma.com/v1/images/${fileKey}?ids=${componentIds.join()}&format=svg`, accessToken);
|
|
23
|
+
await Promise.all(Object.entries(result.images).map(async ([id, imageUrl]) => {
|
|
24
|
+
const response = await fetch(imageUrl);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch SVG content for component ${id}: ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
result.images[id] = await response.text();
|
|
29
|
+
}));
|
|
30
|
+
return result.images;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Generic utility to fetch Figma API routes.
|
|
34
|
+
*
|
|
35
|
+
* @param url API route, e.g. "https://api.figma.com/v1/files/${filekey}"
|
|
36
|
+
* @param accessToken Access token for authentication
|
|
37
|
+
* @throws Error if request was not successful
|
|
38
|
+
*/
|
|
39
|
+
export const fetchFigma = async (url, accessToken) => {
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
headers: {
|
|
42
|
+
"X-FIGMA-TOKEN": accessToken,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const body = await response.json();
|
|
46
|
+
if (response.status !== 200) {
|
|
47
|
+
throw new Error(`Figma API request failed. Response body: ${JSON.stringify(body)}`);
|
|
48
|
+
}
|
|
49
|
+
return body;
|
|
50
|
+
};
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
/**
|
|
3
|
+
* Checks whether the given path is a directory.
|
|
4
|
+
*
|
|
5
|
+
* @returns `true` if path exists and is a directory, `false` otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export const isDirectory = async (path) => {
|
|
8
|
+
try {
|
|
9
|
+
const stats = await stat(path);
|
|
10
|
+
return stats.isDirectory();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimizes the given SVG content for usage inside an icon library using [svgo](https://svgo.dev).
|
|
3
|
+
* Will apply the following optimizations:
|
|
4
|
+
* - remove dimensions (height/width) so it can be set via CSS
|
|
5
|
+
* - "preset-default" to reduce file size and redundant information
|
|
6
|
+
* - (only if type "icon"): remove all fills so the color can be set via CSS
|
|
7
|
+
*/
|
|
8
|
+
export declare const optimizeSvg: (svgContent: string, type?: "icon" | "image") => string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { optimize } from "svgo";
|
|
2
|
+
/**
|
|
3
|
+
* Optimizes the given SVG content for usage inside an icon library using [svgo](https://svgo.dev).
|
|
4
|
+
* Will apply the following optimizations:
|
|
5
|
+
* - remove dimensions (height/width) so it can be set via CSS
|
|
6
|
+
* - "preset-default" to reduce file size and redundant information
|
|
7
|
+
* - (only if type "icon"): remove all fills so the color can be set via CSS
|
|
8
|
+
*/
|
|
9
|
+
export const optimizeSvg = (svgContent, type = "icon") => {
|
|
10
|
+
const plugins = [{ name: "preset-default" }, { name: "removeDimensions" }];
|
|
11
|
+
if (type === "icon") {
|
|
12
|
+
plugins.push({
|
|
13
|
+
name: "removeAttrs",
|
|
14
|
+
params: {
|
|
15
|
+
// remove all fills so we can set the color via CSS
|
|
16
|
+
attrs: ["fill"],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const { data } = optimize(svgContent, {
|
|
21
|
+
multipass: true,
|
|
22
|
+
plugins,
|
|
23
|
+
});
|
|
24
|
+
return data;
|
|
25
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* Parsed Figma variables for an additionally dark theme.
|
|
12
|
+
*/
|
|
13
|
+
dataDarkTheme?: ParsedVariable;
|
|
14
|
+
};
|
|
15
|
+
export type GenerateAsCSSOptions = BaseGenerateOptions & {
|
|
16
|
+
/**
|
|
17
|
+
* Selector to use for the CSS format. You can use {mode} as placeholder for the mode name.
|
|
18
|
+
*
|
|
19
|
+
* @default ":root"
|
|
20
|
+
* @example
|
|
21
|
+
* for the mode named "dark", passing the selector "html.{mode}" will result in "html.dark"
|
|
22
|
+
*/
|
|
23
|
+
selector?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Generates the given parsed Figma variables into CSS variables.
|
|
27
|
+
*
|
|
28
|
+
* @param data Parsed Figma variables
|
|
29
|
+
* @param options Optional options to fine-tune the generated output
|
|
30
|
+
* @returns File content of the .css file
|
|
31
|
+
*/
|
|
32
|
+
export declare const generateAsCSS: (data: ParsedVariable, options?: GenerateAsCSSOptions) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Generates the given parsed Figma variables into SCSS variables.
|
|
35
|
+
*
|
|
36
|
+
* @returns File content of the .scss file
|
|
37
|
+
*/
|
|
38
|
+
export declare const generateAsSCSS: (data: ParsedVariable, options?: BaseGenerateOptions) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Generates the given parsed Figma variables as JSON.
|
|
41
|
+
* Alias variables will be resolved to their actual value.
|
|
42
|
+
*
|
|
43
|
+
* @returns File content of the .json file
|
|
44
|
+
*/
|
|
45
|
+
export declare const generateAsJSON: (data: ParsedVariable) => string;
|
|
46
|
+
/**
|
|
47
|
+
* Recursively resolves the value for the given variable name.
|
|
48
|
+
* So if the value is an alias, the output will be the actual alias value instead of a reference by name.
|
|
49
|
+
* If the value is not an alias, its value will be directly returned.
|
|
50
|
+
*
|
|
51
|
+
* @param name Variable name to resolve
|
|
52
|
+
* @param allVariables All available variables
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const allVariables = {
|
|
56
|
+
* "variable-a": 42,
|
|
57
|
+
* "variable-b": "{variable-a}"
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* const resolvedValue = resolveValue("variable-b", allVariables);
|
|
61
|
+
* // const resolvedValue = 42;
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare const resolveValue: (name: string, allVariables: ParsedVariable["variables"]) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Generates the timestamp comment that is added to the start of every generated file.
|
|
67
|
+
*/
|
|
68
|
+
export declare const generateTimestampComment: (modeName?: string) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Checks whether the given variable value is an alias / variable reference to another variable.
|
|
71
|
+
* Alias values are enclosed by curly braces.
|
|
72
|
+
*
|
|
73
|
+
* @example "{your-variable-name}"
|
|
74
|
+
* @returns `isAlias` whether the variable is an alias and `aliasName` the raw variable name without curly braces.
|
|
75
|
+
*/
|
|
76
|
+
export declare const isAliasVariable: (variableValue?: string) => {
|
|
77
|
+
isAlias: boolean;
|
|
78
|
+
aliasName: string;
|
|
79
|
+
} | {
|
|
80
|
+
isAlias: RegExpExecArray | null;
|
|
81
|
+
aliasName: string;
|
|
82
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the given parsed Figma variables into CSS variables.
|
|
3
|
+
*
|
|
4
|
+
* @param data Parsed Figma variables
|
|
5
|
+
* @param options Optional options to fine-tune the generated output
|
|
6
|
+
* @returns File content of the .css file
|
|
7
|
+
*/
|
|
8
|
+
export const generateAsCSS = (data, options) => {
|
|
9
|
+
const variableContent = getCssOrScssVariableContent(data.variables, (name) => ` --${name}`, (name) => `var(--${name})`, options);
|
|
10
|
+
const fullSelector = options?.selector?.trim().replaceAll("{mode}", data.modeName ?? "") || ":root";
|
|
11
|
+
return `${generateTimestampComment(data.modeName)}
|
|
12
|
+
${fullSelector} {\n${variableContent.join("\n")}\n}\n`;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Generates the given parsed Figma variables into SCSS variables.
|
|
16
|
+
*
|
|
17
|
+
* @returns File content of the .scss file
|
|
18
|
+
*/
|
|
19
|
+
export const generateAsSCSS = (data, options) => {
|
|
20
|
+
const variableContent = getCssOrScssVariableContent(data.variables, (name) => `$${name}`, (name) => `$${name}`, options);
|
|
21
|
+
return `${generateTimestampComment(data.modeName)}\n${variableContent.join("\n")}\n`;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Generates the given parsed Figma variables as JSON.
|
|
25
|
+
* Alias variables will be resolved to their actual value.
|
|
26
|
+
*
|
|
27
|
+
* @returns File content of the .json file
|
|
28
|
+
*/
|
|
29
|
+
export const generateAsJSON = (data) => {
|
|
30
|
+
const variables = structuredClone(data.variables);
|
|
31
|
+
// recursively resolve aliases to plain values since keys can not be referenced in a .json file
|
|
32
|
+
// like we could e.g. in a .css file
|
|
33
|
+
Object.keys(variables).forEach((name) => {
|
|
34
|
+
variables[name] = resolveValue(name, variables);
|
|
35
|
+
});
|
|
36
|
+
return `${JSON.stringify(variables, null, 2)}\n`;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Recursively resolves the value for the given variable name.
|
|
40
|
+
* So if the value is an alias, the output will be the actual alias value instead of a reference by name.
|
|
41
|
+
* If the value is not an alias, its value will be directly returned.
|
|
42
|
+
*
|
|
43
|
+
* @param name Variable name to resolve
|
|
44
|
+
* @param allVariables All available variables
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const allVariables = {
|
|
48
|
+
* "variable-a": 42,
|
|
49
|
+
* "variable-b": "{variable-a}"
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* const resolvedValue = resolveValue("variable-b", allVariables);
|
|
53
|
+
* // const resolvedValue = 42;
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const resolveValue = (name, allVariables) => {
|
|
57
|
+
const { isAlias, aliasName } = isAliasVariable(allVariables[name]);
|
|
58
|
+
if (!isAlias)
|
|
59
|
+
return allVariables[name];
|
|
60
|
+
return resolveValue(aliasName, allVariables);
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Generates the timestamp comment that is added to the start of every generated file.
|
|
64
|
+
*/
|
|
65
|
+
export const generateTimestampComment = (modeName) => {
|
|
66
|
+
return `/**
|
|
67
|
+
* Do not edit directly.${modeName ? `\n * This file contains the specific variables for the "${modeName}" theme.` : ""}
|
|
68
|
+
* Imported from Figma API on ${new Date().toUTCString()}
|
|
69
|
+
*/`;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Checks whether the given variable value is an alias / variable reference to another variable.
|
|
73
|
+
* Alias values are enclosed by curly braces.
|
|
74
|
+
*
|
|
75
|
+
* @example "{your-variable-name}"
|
|
76
|
+
* @returns `isAlias` whether the variable is an alias and `aliasName` the raw variable name without curly braces.
|
|
77
|
+
*/
|
|
78
|
+
export const isAliasVariable = (variableValue) => {
|
|
79
|
+
if (!variableValue)
|
|
80
|
+
return { isAlias: false, aliasName: "" };
|
|
81
|
+
const isAlias = /{.*}/.exec(variableValue);
|
|
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 variablesDarkTheme Variable data (name +value) for additionally dark theme
|
|
91
|
+
* @param nameFormatter Function to format the variable name
|
|
92
|
+
* @param aliasFormatter Function to format a reference to another variable (e.g. `var(--name)` for CSS)
|
|
93
|
+
* @param options Generator options
|
|
94
|
+
*/
|
|
95
|
+
const getCssOrScssVariableContent = (variables, nameFormatter, aliasFormatter, options) => {
|
|
96
|
+
const variablesDarkTheme = options?.dataDarkTheme?.variables;
|
|
97
|
+
return Object.entries(variables).map(([name, value]) => {
|
|
98
|
+
const lightRawValue = value;
|
|
99
|
+
const darkRawValue = variablesDarkTheme?.[name];
|
|
100
|
+
const { isAlias: isLightAlias, aliasName: lightAliasName } = isAliasVariable(lightRawValue);
|
|
101
|
+
const { isAlias: isDarkAlias, aliasName: darkAliasName } = isAliasVariable(darkRawValue ?? lightRawValue);
|
|
102
|
+
let lightValue = isLightAlias ? aliasFormatter(lightAliasName) : lightRawValue;
|
|
103
|
+
let darkValue = isDarkAlias ? aliasFormatter(darkAliasName) : (darkRawValue ?? lightRawValue);
|
|
104
|
+
if (options?.resolveAlias) {
|
|
105
|
+
if (isLightAlias) {
|
|
106
|
+
lightValue = resolveValue(name, variables);
|
|
107
|
+
}
|
|
108
|
+
if (isDarkAlias) {
|
|
109
|
+
darkValue = resolveValue(name, variablesDarkTheme ?? {});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const formattedName = nameFormatter(name);
|
|
113
|
+
if (variablesDarkTheme && darkRawValue) {
|
|
114
|
+
if (lightValue === darkValue) {
|
|
115
|
+
return `${formattedName}: ${lightValue};`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
return `${formattedName}: light-dark(${lightValue}, ${darkValue});`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return `${formattedName}: ${lightValue};`;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FigmaVariablesApiResponse, ParsedVariable, RGBAValue, Variable, VariableValue } from "../types/figma.js";
|
|
2
|
+
export type ParseFigmaVariablesOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Base for converting pixel in rem. Set to `false` for disabling rem conversion and use pixel values.
|
|
5
|
+
* @default 16
|
|
6
|
+
*/
|
|
7
|
+
remBase?: number | false;
|
|
8
|
+
};
|
|
9
|
+
/** Default Figma mode name if only one mode exists and no other name is specified by the designer. */
|
|
10
|
+
export declare const DEFAULT_MODE_NAME: "Mode 1";
|
|
11
|
+
/**
|
|
12
|
+
* Parses Figma variables received from the Figma API to a minimal JSON.
|
|
13
|
+
* Numeric / pixel values will be transformed to rem.
|
|
14
|
+
* Variables / collections that are hidden from publishing will not be parsed.
|
|
15
|
+
*
|
|
16
|
+
* @param apiResponse Variables response body received from the Figma API.
|
|
17
|
+
*/
|
|
18
|
+
export declare const parseFigmaVariables: (apiResponse: FigmaVariablesApiResponse, options?: ParseFigmaVariablesOptions) => ParsedVariable[];
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the given Figma variable value to a string value. Value types:
|
|
21
|
+
* - number: converted to rem, e.g. 16 => "1rem"
|
|
22
|
+
* - color: converted to HEX color, e.g. {r:1, g: 1, b: 1, a: 1} => "#ffffff"
|
|
23
|
+
* - alias: referenced with variable name, e.g. "--primary-100" => "{--primary-100}"
|
|
24
|
+
* (curly brackets will indicate that the value is an alias / reference)
|
|
25
|
+
*
|
|
26
|
+
* @param value Figma variable value
|
|
27
|
+
* @param allVariables Object of all variables. Needed for variables that use aliases.
|
|
28
|
+
*/
|
|
29
|
+
export declare const resolveFigmaVariableValue: (value: VariableValue, allVariables: Record<string, Variable>, remBase?: ParseFigmaVariablesOptions["remBase"], name?: string) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Converts a RGBA value to a hex color.
|
|
32
|
+
* Transparency will only be added if its not 1, e.g. "#000000" instead of "#000000ff"
|
|
33
|
+
*/
|
|
34
|
+
export declare const rgbaToHex: (value: RGBAValue) => string;
|
|
35
|
+
/**
|
|
36
|
+
* Normalizes the given variable name by apply these transformations:
|
|
37
|
+
* - replace slashes with "-"
|
|
38
|
+
* - replace whitespace with "-"
|
|
39
|
+
* - replace "+" with "-"
|
|
40
|
+
* - replace "&" with "-"
|
|
41
|
+
*/
|
|
42
|
+
export declare const normalizeVariableName: (name: string) => string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Default Figma mode name if only one mode exists and no other name is specified by the designer. */
|
|
2
|
+
export const DEFAULT_MODE_NAME = "Mode 1";
|
|
3
|
+
/**
|
|
4
|
+
* Parses Figma variables received from the Figma API to a minimal JSON.
|
|
5
|
+
* Numeric / pixel values will be transformed to rem.
|
|
6
|
+
* Variables / collections that are hidden from publishing will not be parsed.
|
|
7
|
+
*
|
|
8
|
+
* @param apiResponse Variables response body received from the Figma API.
|
|
9
|
+
*/
|
|
10
|
+
export const parseFigmaVariables = (apiResponse, options) => {
|
|
11
|
+
const parsedData = [];
|
|
12
|
+
/**
|
|
13
|
+
* Loop through each variable and mode and create a new object.
|
|
14
|
+
*/
|
|
15
|
+
Object.values(apiResponse.meta.variables).forEach((variable) => {
|
|
16
|
+
const collection = apiResponse.meta.variableCollections[variable.variableCollectionId];
|
|
17
|
+
if (variable.hiddenFromPublishing ||
|
|
18
|
+
variable.deletedButReferenced ||
|
|
19
|
+
collection.hiddenFromPublishing)
|
|
20
|
+
return;
|
|
21
|
+
// parse variable value for every mode
|
|
22
|
+
Object.values(collection.modes).forEach((mode) => {
|
|
23
|
+
const variableName = normalizeVariableName(variable.name);
|
|
24
|
+
const variableValue = resolveFigmaVariableValue(variable.valuesByMode?.[mode.modeId], apiResponse.meta.variables, options?.remBase, variableName);
|
|
25
|
+
// add/update parsed variable value
|
|
26
|
+
const existingIndex = parsedData.findIndex((i) => i.modeName === mode.name);
|
|
27
|
+
if (existingIndex !== -1) {
|
|
28
|
+
parsedData[existingIndex].variables[variableName] = variableValue;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
parsedData.push({
|
|
32
|
+
modeName: mode.name,
|
|
33
|
+
variables: { [variableName]: variableValue },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
parsedData.forEach((data) => {
|
|
39
|
+
if (data.modeName === DEFAULT_MODE_NAME)
|
|
40
|
+
delete data.modeName;
|
|
41
|
+
const numberRegex = /\d+/;
|
|
42
|
+
// sort variables by name
|
|
43
|
+
// for variables with the same name that just end with a different number (e.g. my-var-100 and my-var-200)
|
|
44
|
+
// sort them by number instead of alphabetically so e.g. 100 is sorted before 1000
|
|
45
|
+
data.variables = Object.keys(data.variables)
|
|
46
|
+
.map((key) => {
|
|
47
|
+
const asNumber = numberRegex.exec(key)?.[0] ?? "";
|
|
48
|
+
return {
|
|
49
|
+
key,
|
|
50
|
+
asNumber: +asNumber || undefined, // prevent NaN
|
|
51
|
+
base: key.replace(asNumber, ""),
|
|
52
|
+
};
|
|
53
|
+
})
|
|
54
|
+
.sort((a, b) => {
|
|
55
|
+
if (a.asNumber && b.asNumber && a.base === b.base)
|
|
56
|
+
return a.asNumber - b.asNumber;
|
|
57
|
+
return a.key.localeCompare(b.key);
|
|
58
|
+
})
|
|
59
|
+
.reduce((variables, { key }) => {
|
|
60
|
+
variables[key] = data.variables[key];
|
|
61
|
+
return variables;
|
|
62
|
+
}, {});
|
|
63
|
+
});
|
|
64
|
+
return parsedData;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the given Figma variable value to a string value. Value types:
|
|
68
|
+
* - number: converted to rem, e.g. 16 => "1rem"
|
|
69
|
+
* - color: converted to HEX color, e.g. {r:1, g: 1, b: 1, a: 1} => "#ffffff"
|
|
70
|
+
* - alias: referenced with variable name, e.g. "--primary-100" => "{--primary-100}"
|
|
71
|
+
* (curly brackets will indicate that the value is an alias / reference)
|
|
72
|
+
*
|
|
73
|
+
* @param value Figma variable value
|
|
74
|
+
* @param allVariables Object of all variables. Needed for variables that use aliases.
|
|
75
|
+
*/
|
|
76
|
+
export const resolveFigmaVariableValue = (value, allVariables, remBase = 16, name) => {
|
|
77
|
+
if (typeof value === "number") {
|
|
78
|
+
if (name?.includes("font-weight"))
|
|
79
|
+
return `${value}`;
|
|
80
|
+
// numeric value, parse as rem or pixel value
|
|
81
|
+
// note: value 0 should also be parsed as "0rem" instead of just "0" because otherwise
|
|
82
|
+
// the CSS variable could not be used together with "calc()"
|
|
83
|
+
if (remBase === false || remBase <= 0)
|
|
84
|
+
return `${value}px`;
|
|
85
|
+
return `${value / remBase}rem`;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
return `"${value}"`;
|
|
89
|
+
}
|
|
90
|
+
if ("type" in value) {
|
|
91
|
+
// parse value as alias
|
|
92
|
+
if (value.type !== "VARIABLE_ALIAS") {
|
|
93
|
+
throw new Error(`Unknown variable value type: ${value.type}`);
|
|
94
|
+
}
|
|
95
|
+
const reference = allVariables[value.id];
|
|
96
|
+
if (!reference) {
|
|
97
|
+
throw new Error(`Could not find variables alias with ID "${value.id}"`);
|
|
98
|
+
}
|
|
99
|
+
return `{${normalizeVariableName(reference.name)}}`;
|
|
100
|
+
}
|
|
101
|
+
return rgbaToHex(value);
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Converts a RGBA value to a hex color.
|
|
105
|
+
* Transparency will only be added if its not 1, e.g. "#000000" instead of "#000000ff"
|
|
106
|
+
*/
|
|
107
|
+
export const rgbaToHex = (value) => {
|
|
108
|
+
const hex = Object.values(value)
|
|
109
|
+
.map((color) => Math.floor(color * 255))
|
|
110
|
+
.map((color) => color.toString(16))
|
|
111
|
+
.map((color) => color.padStart(2, "0"))
|
|
112
|
+
.join("")
|
|
113
|
+
.replace(/^/, "#");
|
|
114
|
+
if (value.a === 1)
|
|
115
|
+
return hex.substring(0, hex.length - 2);
|
|
116
|
+
return hex;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Normalizes the given variable name by apply these transformations:
|
|
120
|
+
* - replace slashes with "-"
|
|
121
|
+
* - replace whitespace with "-"
|
|
122
|
+
* - replace "+" with "-"
|
|
123
|
+
* - replace "&" with "-"
|
|
124
|
+
*/
|
|
125
|
+
export const normalizeVariableName = (name) => {
|
|
126
|
+
return name.replaceAll("/", "-").replaceAll(" ", "-").replaceAll("+", "-").replaceAll("&", "-");
|
|
127
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sit-onyx/figma-utils",
|
|
3
|
+
"description": "Utility functions and CLI for importing data from the Figma API into different formats (e.g. CSS, SCSS etc.)",
|
|
4
|
+
"version": "0.0.0-20250804145452",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Schwarz IT KG",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"@sit-onyx/figma-utils": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://onyx.schwarz/development/packages/figma-utils.html",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/SchwarzIT/onyx",
|
|
28
|
+
"directory": "packages/figma-utils"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/SchwarzIT/onyx/issues"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^14.0.0"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"svgo": "^4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "pnpm run '/type-check|build-only/'",
|
|
41
|
+
"build-only": "rimraf dist && tsc -p tsconfig.node.json --composite false",
|
|
42
|
+
"type-check": "tsc --noEmit -p tsconfig.vitest.json --composite false",
|
|
43
|
+
"@sit-onyx/figma-utils": "node ./dist/cli.js",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"test:coverage": "vitest run --coverage"
|
|
46
|
+
}
|
|
47
|
+
}
|