@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.
@@ -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
+ };
@@ -0,0 +1,7 @@
1
+ import { PathLike } from "node:fs";
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 declare const isDirectory: (path: PathLike) => Promise<boolean>;
@@ -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
+ }