@sit-onyx/figma-utils 1.0.0-beta.0 → 1.0.0-beta.10
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 -5
- package/dist/cli.js +4 -2
- 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 +4 -3
- package/dist/commands/import-variables.js +18 -8
- 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 +7 -2
- package/dist/index.js +7 -2
- package/dist/types/figma.d.ts +64 -1
- package/dist/utils/fetch.d.ts +19 -1
- package/dist/utils/fetch.js +33 -1
- 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/{utils → variables}/generate.d.ts +8 -1
- package/dist/{utils → variables}/generate.js +27 -5
- package/dist/{utils → variables}/parse.d.ts +1 -1
- package/dist/{utils → variables}/parse.js +12 -5
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
<div align="center" style="text-align: center">
|
|
2
|
-
<
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-light.svg">
|
|
4
|
-
<source media="(prefers-color-scheme: light)" type="image/svg+xml" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg">
|
|
5
|
-
<img alt="onyx logo" src="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg" width="160px">
|
|
6
|
-
</picture>
|
|
2
|
+
<img alt="onyx logo" src="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo.svg" height="96px">
|
|
7
3
|
</div>
|
|
8
4
|
|
|
9
5
|
<br>
|
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { fileURLToPath, URL } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { importFlagsCommand } from "./commands/import-flags.js";
|
|
6
|
+
import { importIconsCommand } from "./commands/import-icons.js";
|
|
7
|
+
import { importVariablesCommand } from "./commands/import-variables.js";
|
|
6
8
|
const packageJson = JSON.parse(fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
7
9
|
const cli = new Command();
|
|
8
10
|
cli.version(packageJson.version, "-v, --version").description(packageJson.description);
|
|
9
|
-
const availableCommands = [
|
|
11
|
+
const availableCommands = [importVariablesCommand, importIconsCommand, importFlagsCommand];
|
|
10
12
|
availableCommands.forEach((command) => cli.addCommand(command));
|
|
11
13
|
cli.parse();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export type ImportFlagsCommandOptions = {
|
|
3
|
+
fileKey: string;
|
|
4
|
+
token: string;
|
|
5
|
+
pageId: string;
|
|
6
|
+
dir?: string;
|
|
7
|
+
metaFile?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const importFlagsCommand: Command;
|
|
10
|
+
/**
|
|
11
|
+
* Action to run when executing the import action. Only intended to be called manually for testing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function importFlagsCommandAction(options: ImportFlagsCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeFlagMetadata } from "../flags/generate.js";
|
|
5
|
+
import { parseComponentsToFlags } from "../flags/parse.js";
|
|
6
|
+
import { fetchFigmaComponents, fetchFigmaSVGs } from "../index.js";
|
|
7
|
+
import { optimizeSvg } from "../utils/optimize.js";
|
|
8
|
+
export const importFlagsCommand = new Command("import-flags")
|
|
9
|
+
.description("CLI tool to import SVG flags from Figma.")
|
|
10
|
+
.requiredOption("-k, --file-key <string>", "Figma file key (required)")
|
|
11
|
+
.requiredOption("-t, --token <string>", "Figma access token with scope `file_read` or `files:read` (required)")
|
|
12
|
+
.requiredOption("-p, --page-id <string>", "Figma page ID that contains the flags (required)")
|
|
13
|
+
.option("-d, --dir <string>", "Directory to save the flags to. Defaults to current working directory of the script.")
|
|
14
|
+
.option("-m, --meta-file <string>", 'JSON filename/path to write flag metadata to (country name etc.). Must end with ".json". If unset, no metadata will be generated.')
|
|
15
|
+
.action(importFlagsCommandAction);
|
|
16
|
+
/**
|
|
17
|
+
* Action to run when executing the import action. Only intended to be called manually for testing.
|
|
18
|
+
*/
|
|
19
|
+
export async function importFlagsCommandAction(options) {
|
|
20
|
+
console.log("Fetching components from Figma API...");
|
|
21
|
+
const data = await fetchFigmaComponents(options.fileKey, options.token);
|
|
22
|
+
console.log("Parsing Figma flags...");
|
|
23
|
+
const parsedFlags = parseComponentsToFlags({
|
|
24
|
+
components: data.meta.components,
|
|
25
|
+
pageId: options.pageId,
|
|
26
|
+
});
|
|
27
|
+
const outputDirectory = options.dir ?? process.cwd();
|
|
28
|
+
console.log(`Fetching SVG content for ${parsedFlags.length} flags...`);
|
|
29
|
+
const svgContents = await fetchFigmaSVGs(options.fileKey, parsedFlags.map(({ id }) => id), options.token);
|
|
30
|
+
console.log("Optimizing and writing flag files...");
|
|
31
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
32
|
+
await Promise.all(parsedFlags.map((flag) => {
|
|
33
|
+
const content = optimizeSvg(svgContents[flag.id], "image");
|
|
34
|
+
const fullPath = path.join(outputDirectory, `${flag.code}.svg`);
|
|
35
|
+
return writeFile(fullPath, content, "utf-8");
|
|
36
|
+
}));
|
|
37
|
+
if (options.metaFile) {
|
|
38
|
+
console.log("Writing flag metadata...");
|
|
39
|
+
await writeFlagMetadata(options.metaFile, parsedFlags);
|
|
40
|
+
}
|
|
41
|
+
console.log("Done.");
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export type ImportIconsCommandOptions = {
|
|
3
|
+
fileKey: string;
|
|
4
|
+
token: string;
|
|
5
|
+
pageId: string;
|
|
6
|
+
aliasSeparator: string;
|
|
7
|
+
dir?: string;
|
|
8
|
+
metaFile?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const importIconsCommand: Command;
|
|
11
|
+
/**
|
|
12
|
+
* Action to run when executing the import action. Only intended to be called manually for testing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function importIconsCommandAction(options: ImportIconsCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeIconMetadata } from "../icons/generate.js";
|
|
5
|
+
import { parseComponentsToIcons } from "../icons/parse.js";
|
|
6
|
+
import { fetchFigmaComponents, fetchFigmaSVGs } from "../index.js";
|
|
7
|
+
import { optimizeSvg } from "../utils/optimize.js";
|
|
8
|
+
export const importIconsCommand = new Command("import-icons")
|
|
9
|
+
.description("CLI tool to import SVG icons from Figma.")
|
|
10
|
+
.requiredOption("-k, --file-key <string>", "Figma file key (required)")
|
|
11
|
+
.requiredOption("-t, --token <string>", "Figma access token with scope `file_read` or `files:read` (required)")
|
|
12
|
+
.requiredOption("-p, --page-id <string>", "Figma page ID that contains the icons (required)")
|
|
13
|
+
.option("-d, --dir <string>", "Directory to save the icons to. Defaults to current working directory of the script.")
|
|
14
|
+
.option("-m, --meta-file <string>", 'JSON filename/path to write icon metadata to (categories, alias names etc.). Must end with ".json". If unset, no metadata will be generated.')
|
|
15
|
+
.option("-s, --alias-separator <string>", "Separator for icon alias names (which can be set to the component description in Figma).", ",")
|
|
16
|
+
.action(importIconsCommandAction);
|
|
17
|
+
/**
|
|
18
|
+
* Action to run when executing the import action. Only intended to be called manually for testing.
|
|
19
|
+
*/
|
|
20
|
+
export async function importIconsCommandAction(options) {
|
|
21
|
+
console.log("Fetching components from Figma API...");
|
|
22
|
+
const data = await fetchFigmaComponents(options.fileKey, options.token);
|
|
23
|
+
console.log("Parsing Figma icons...");
|
|
24
|
+
const parsedIcons = parseComponentsToIcons({
|
|
25
|
+
components: data.meta.components,
|
|
26
|
+
pageId: options.pageId,
|
|
27
|
+
aliasSeparator: options.aliasSeparator,
|
|
28
|
+
});
|
|
29
|
+
const outputDirectory = options.dir ?? process.cwd();
|
|
30
|
+
console.log(`Fetching SVG content for ${parsedIcons.length} icons...`);
|
|
31
|
+
const svgContents = await fetchFigmaSVGs(options.fileKey, parsedIcons.map(({ id }) => id), options.token);
|
|
32
|
+
console.log("Optimizing and writing icon files...");
|
|
33
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
34
|
+
await Promise.all(parsedIcons.map((icon) => {
|
|
35
|
+
const content = optimizeSvg(svgContents[icon.id]);
|
|
36
|
+
const fullPath = path.join(outputDirectory, `${icon.name}.svg`);
|
|
37
|
+
return writeFile(fullPath, content, "utf-8");
|
|
38
|
+
}));
|
|
39
|
+
if (options.metaFile) {
|
|
40
|
+
console.log("Writing icon metadata...");
|
|
41
|
+
await writeIconMetadata(options.metaFile, parsedIcons);
|
|
42
|
+
}
|
|
43
|
+
console.log("Done.");
|
|
44
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
export type
|
|
2
|
+
export type ImportVariablesCommandOptions = {
|
|
3
3
|
fileKey: string;
|
|
4
4
|
token: string;
|
|
5
5
|
format: string[];
|
|
6
6
|
filename?: string;
|
|
7
7
|
dir?: string;
|
|
8
8
|
modes?: string[];
|
|
9
|
+
combinesDarkLight?: boolean;
|
|
9
10
|
selector: string;
|
|
10
11
|
};
|
|
11
|
-
export declare const
|
|
12
|
+
export declare const importVariablesCommand: Command;
|
|
12
13
|
/**
|
|
13
14
|
* Action to run when executing the import action. Only intended to be called manually for testing.
|
|
14
15
|
*/
|
|
15
|
-
export declare function
|
|
16
|
+
export declare function importVariablesCommandAction(options: ImportVariablesCommandOptions): Promise<void>;
|
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DEFAULT_MODE_NAME, fetchFigmaVariables, generateAsCSS, generateAsJSON, generateAsSCSS, parseFigmaVariables, } from "../index.js";
|
|
5
|
-
export const
|
|
5
|
+
export const importVariablesCommand = 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)")
|
|
@@ -10,16 +10,17 @@ export const importCommand = new Command("import-variables")
|
|
|
10
10
|
.option("-n, --filename <string>", "Base name / prefix of the generated variables file. Will append the mode name")
|
|
11
11
|
.option("-d, --dir <string>", "Working directory to use. Defaults to current working directory of the script.")
|
|
12
12
|
.option("-m, --modes <strings...>", "Can be used to only export specific Figma modes. If unset, all modes will be exported as a separate file.")
|
|
13
|
+
.option("-c, --combines-dark-light", "Combines the dark theme data with the light theme data by using the light-dark() CSS function. The Figma file must include two modes with -light and -dark prefix, e.g. example-light and example-dark.")
|
|
13
14
|
.option("-s, --selector <string>", 'CSS selector to use for the CSS format. You can use {mode} as placeholder for the mode name, so e.g. for the mode named "dark", passing the selector "html.{mode}" will result in "html.dark"', ":root")
|
|
14
|
-
.action(
|
|
15
|
+
.action(importVariablesCommandAction);
|
|
15
16
|
/**
|
|
16
17
|
* Action to run when executing the import action. Only intended to be called manually for testing.
|
|
17
18
|
*/
|
|
18
|
-
export async function
|
|
19
|
+
export async function importVariablesCommandAction(options) {
|
|
19
20
|
const generators = {
|
|
20
|
-
CSS: (data) => generateAsCSS(data, { selector: options.selector }),
|
|
21
|
-
SCSS: generateAsSCSS,
|
|
22
|
-
JSON: generateAsJSON,
|
|
21
|
+
CSS: (data, dataDark) => generateAsCSS(data, { selector: options.selector, dataDarkTheme: dataDark }),
|
|
22
|
+
SCSS: (data, dataDark) => generateAsSCSS(data, { dataDarkTheme: dataDark }),
|
|
23
|
+
JSON: (data) => generateAsJSON(data),
|
|
23
24
|
};
|
|
24
25
|
options.format.forEach((format) => {
|
|
25
26
|
if (!(format in generators)) {
|
|
@@ -59,8 +60,17 @@ export async function importCommandAction(options) {
|
|
|
59
60
|
if (!isModeIncluded)
|
|
60
61
|
return;
|
|
61
62
|
const baseName = getBaseFileName(data.modeName);
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
if (options.combinesDarkLight) {
|
|
64
|
+
const themeName = baseName.replace("-light", "").replace("-dark", "");
|
|
65
|
+
const fullPath = path.join(outputDirectory, `${themeName}.${format.toLowerCase()}`);
|
|
66
|
+
// find the matching theme
|
|
67
|
+
const dataDark = parsedVariables.find((themeData) => themeData.modeName === themeName + "-dark");
|
|
68
|
+
fs.writeFileSync(fullPath, generators[format]({ ...data, modeName: themeName }, dataDark));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const fullPath = path.join(outputDirectory, `${baseName}.${format.toLowerCase()}`);
|
|
72
|
+
fs.writeFileSync(fullPath, generators[format](data));
|
|
73
|
+
}
|
|
64
74
|
});
|
|
65
75
|
});
|
|
66
76
|
console.log("Done.");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ParsedFlag } from "../types/figma.js";
|
|
2
|
+
/**
|
|
3
|
+
* Writes a JSON file with metadata of the given flags (code, name etc.).
|
|
4
|
+
*
|
|
5
|
+
* @param path File path of the .json file, e.g. "./metadata.json"
|
|
6
|
+
* @param flags Flags to write metadata for
|
|
7
|
+
*/
|
|
8
|
+
export declare const writeFlagMetadata: (path: string, flags: ParsedFlag[]) => Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Writes a JSON file with metadata of the given flags (code, name etc.).
|
|
5
|
+
*
|
|
6
|
+
* @param path File path of the .json file, e.g. "./metadata.json"
|
|
7
|
+
* @param flags Flags to write metadata for
|
|
8
|
+
*/
|
|
9
|
+
export const writeFlagMetadata = async (path, flags) => {
|
|
10
|
+
const metaDirname = dirname(path);
|
|
11
|
+
await mkdir(metaDirname, { recursive: true });
|
|
12
|
+
const flagMetadata = flags.reduce((meta, flag) => {
|
|
13
|
+
const { id: _id, code, ...rest } = flag;
|
|
14
|
+
meta[code] = rest;
|
|
15
|
+
return meta;
|
|
16
|
+
}, {});
|
|
17
|
+
await writeFile(path, JSON.stringify(flagMetadata, null, 2), "utf-8");
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Component, ParsedFlag } from "../types/figma.js";
|
|
2
|
+
export type ParseFlagComponentsOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Available Figma components.
|
|
5
|
+
*/
|
|
6
|
+
components: Component[];
|
|
7
|
+
/**
|
|
8
|
+
* Page ID that contains all flags. Components will be filtered accordingly.
|
|
9
|
+
*/
|
|
10
|
+
pageId: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const parseComponentsToFlags: (options: ParseFlagComponentsOptions) => ParsedFlag[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map of country names for country codes that are not (yet) supported by `Intl.DisplayNames`.
|
|
3
|
+
*/
|
|
4
|
+
const UNKNOWN_COUNTRY_NAMES = {
|
|
5
|
+
"CA-BC": "British Columbia",
|
|
6
|
+
"GB-ENG": "England",
|
|
7
|
+
"GB-SCT": "Scotland",
|
|
8
|
+
"GB-WLS": "Wales",
|
|
9
|
+
"US-HI": "Hawaii",
|
|
10
|
+
};
|
|
11
|
+
export const parseComponentsToFlags = (options) => {
|
|
12
|
+
const pageComponents = options.components.filter(({ containing_frame }) => containing_frame.pageId === options.pageId);
|
|
13
|
+
const countryCodeFormatter = new Intl.DisplayNames("en", { type: "region" });
|
|
14
|
+
return (pageComponents
|
|
15
|
+
.map((component) => {
|
|
16
|
+
const code = component.description.trim();
|
|
17
|
+
let internationalName = UNKNOWN_COUNTRY_NAMES[code] ?? "";
|
|
18
|
+
try {
|
|
19
|
+
internationalName = countryCodeFormatter.of(code) || internationalName;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// noop
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
id: component.node_id,
|
|
26
|
+
code,
|
|
27
|
+
continent: component.containing_frame.name.trim(),
|
|
28
|
+
internationalName,
|
|
29
|
+
};
|
|
30
|
+
})
|
|
31
|
+
// remove invalid flags without a country code
|
|
32
|
+
.filter(({ code }) => code)
|
|
33
|
+
.sort((a, b) => a.code.localeCompare(b.code)));
|
|
34
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ParsedIcon } from "../types/figma.js";
|
|
2
|
+
/**
|
|
3
|
+
* Writes a JSON file with metadata of the given icons (category, aliases etc.).
|
|
4
|
+
*
|
|
5
|
+
* @param path File path of the .json file, e.g. "./metadata.json"
|
|
6
|
+
* @param icons Icons to write metadata for
|
|
7
|
+
*/
|
|
8
|
+
export declare const writeIconMetadata: (path: string, icons: ParsedIcon[]) => Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Writes a JSON file with metadata of the given icons (category, aliases etc.).
|
|
5
|
+
*
|
|
6
|
+
* @param path File path of the .json file, e.g. "./metadata.json"
|
|
7
|
+
* @param icons Icons to write metadata for
|
|
8
|
+
*/
|
|
9
|
+
export const writeIconMetadata = async (path, icons) => {
|
|
10
|
+
const metaDirname = dirname(path);
|
|
11
|
+
await mkdir(metaDirname, { recursive: true });
|
|
12
|
+
const iconMetadata = icons.reduce((meta, icon) => {
|
|
13
|
+
const { id: _id, name, ...rest } = icon;
|
|
14
|
+
meta[name] = rest;
|
|
15
|
+
return meta;
|
|
16
|
+
}, {});
|
|
17
|
+
await writeFile(path, JSON.stringify(iconMetadata, null, 2), "utf-8");
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Component, ParsedIcon } from "../types/figma.js";
|
|
2
|
+
export type ParseIconComponentsOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Available Figma components.
|
|
5
|
+
*/
|
|
6
|
+
components: Component[];
|
|
7
|
+
/**
|
|
8
|
+
* Page ID that contains all icons. Components will be filtered accordingly.
|
|
9
|
+
*/
|
|
10
|
+
pageId: string;
|
|
11
|
+
/**
|
|
12
|
+
* Separator for icon alias names (which can be set to the component description in Figma).
|
|
13
|
+
*
|
|
14
|
+
* @default ","
|
|
15
|
+
*/
|
|
16
|
+
aliasSeparator?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const parseComponentsToIcons: (options: ParseIconComponentsOptions) => ParsedIcon[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const parseComponentsToIcons = (options) => {
|
|
2
|
+
const pageComponents = options.components.filter(({ containing_frame }) => containing_frame.pageId === options.pageId);
|
|
3
|
+
return pageComponents
|
|
4
|
+
.map((component) => {
|
|
5
|
+
return {
|
|
6
|
+
id: component.node_id,
|
|
7
|
+
name: component.name,
|
|
8
|
+
category: component.containing_frame.name.trim(),
|
|
9
|
+
aliases: component.description
|
|
10
|
+
.split(options.aliasSeparator ?? ",")
|
|
11
|
+
.map((alias) => alias.trim())
|
|
12
|
+
.filter((i) => i !== ""),
|
|
13
|
+
};
|
|
14
|
+
})
|
|
15
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
16
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
export * from "./flags/generate.js";
|
|
2
|
+
export * from "./flags/parse.js";
|
|
3
|
+
export * from "./icons/generate.js";
|
|
4
|
+
export * from "./icons/parse.js";
|
|
1
5
|
export * from "./types/figma.js";
|
|
2
6
|
export * from "./utils/fetch.js";
|
|
3
|
-
export * from "./utils/
|
|
4
|
-
export * from "./
|
|
7
|
+
export * from "./utils/optimize.js";
|
|
8
|
+
export * from "./variables/generate.js";
|
|
9
|
+
export * from "./variables/parse.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
export * from "./flags/generate.js";
|
|
2
|
+
export * from "./flags/parse.js";
|
|
3
|
+
export * from "./icons/generate.js";
|
|
4
|
+
export * from "./icons/parse.js";
|
|
1
5
|
export * from "./types/figma.js";
|
|
2
6
|
export * from "./utils/fetch.js";
|
|
3
|
-
export * from "./utils/
|
|
4
|
-
export * from "./
|
|
7
|
+
export * from "./utils/optimize.js";
|
|
8
|
+
export * from "./variables/generate.js";
|
|
9
|
+
export * from "./variables/parse.js";
|
package/dist/types/figma.d.ts
CHANGED
|
@@ -20,9 +20,10 @@ export type Variable = {
|
|
|
20
20
|
name: string;
|
|
21
21
|
variableCollectionId: string;
|
|
22
22
|
hiddenFromPublishing: boolean;
|
|
23
|
+
deletedButReferenced?: boolean;
|
|
23
24
|
valuesByMode: Record<string, VariableValue>;
|
|
24
25
|
};
|
|
25
|
-
export type VariableValue = RGBAValue | ColorsAlias | number;
|
|
26
|
+
export type VariableValue = RGBAValue | ColorsAlias | number | string;
|
|
26
27
|
export type RGBAValue = {
|
|
27
28
|
r: number;
|
|
28
29
|
g: number;
|
|
@@ -48,3 +49,65 @@ export type ParsedVariable = {
|
|
|
48
49
|
*/
|
|
49
50
|
variables: Record<string, string>;
|
|
50
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
|
+
};
|
package/dist/utils/fetch.d.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import { FigmaVariablesApiResponse } from "../types/figma.js";
|
|
1
|
+
import { FigmaComponentsApiResponse, FigmaVariablesApiResponse } from "../types/figma.js";
|
|
2
2
|
/**
|
|
3
3
|
* Fetches the Figma Variables for the given file from the Figma API v1.
|
|
4
4
|
*
|
|
5
5
|
* @param fileKey File key. Example: https://www.figma.com/file/your-file-key-here
|
|
6
6
|
* @param accessToken Personal access token with scope/permission `file_variables:read`
|
|
7
|
+
* @see https://www.figma.com/developers/api#get-local-variables-endpoint
|
|
7
8
|
*/
|
|
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>;
|
package/dist/utils/fetch.js
CHANGED
|
@@ -3,9 +3,41 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @param fileKey File key. Example: https://www.figma.com/file/your-file-key-here
|
|
5
5
|
* @param accessToken Personal access token with scope/permission `file_variables:read`
|
|
6
|
+
* @see https://www.figma.com/developers/api#get-local-variables-endpoint
|
|
6
7
|
*/
|
|
7
8
|
export const fetchFigmaVariables = async (fileKey, accessToken) => {
|
|
8
|
-
|
|
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, {
|
|
9
41
|
headers: {
|
|
10
42
|
"X-FIGMA-TOKEN": accessToken,
|
|
11
43
|
},
|
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
|
+
};
|
|
@@ -7,6 +7,10 @@ export type BaseGenerateOptions = {
|
|
|
7
7
|
* @default false
|
|
8
8
|
*/
|
|
9
9
|
resolveAlias?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Parsed Figma variables for an additionally dark theme.
|
|
12
|
+
*/
|
|
13
|
+
dataDarkTheme?: ParsedVariable;
|
|
10
14
|
};
|
|
11
15
|
export type GenerateAsCSSOptions = BaseGenerateOptions & {
|
|
12
16
|
/**
|
|
@@ -69,7 +73,10 @@ export declare const generateTimestampComment: (modeName?: string) => string;
|
|
|
69
73
|
* @example "{your-variable-name}"
|
|
70
74
|
* @returns `isAlias` whether the variable is an alias and `aliasName` the raw variable name without curly braces.
|
|
71
75
|
*/
|
|
72
|
-
export declare const isAliasVariable: (variableValue
|
|
76
|
+
export declare const isAliasVariable: (variableValue?: string) => {
|
|
77
|
+
isAlias: boolean;
|
|
78
|
+
aliasName: string;
|
|
79
|
+
} | {
|
|
73
80
|
isAlias: RegExpExecArray | null;
|
|
74
81
|
aliasName: string;
|
|
75
82
|
};
|
|
@@ -76,6 +76,8 @@ export const generateTimestampComment = (modeName) => {
|
|
|
76
76
|
* @returns `isAlias` whether the variable is an alias and `aliasName` the raw variable name without curly braces.
|
|
77
77
|
*/
|
|
78
78
|
export const isAliasVariable = (variableValue) => {
|
|
79
|
+
if (!variableValue)
|
|
80
|
+
return { isAlias: false, aliasName: "" };
|
|
79
81
|
const isAlias = /{.*}/.exec(variableValue);
|
|
80
82
|
const aliasName = variableValue.replace("{", "").replace("}", "");
|
|
81
83
|
return { isAlias, aliasName };
|
|
@@ -85,17 +87,37 @@ export const isAliasVariable = (variableValue) => {
|
|
|
85
87
|
* represents a single line of the file.
|
|
86
88
|
*
|
|
87
89
|
* @param variables Variable data (name + value)
|
|
90
|
+
* @param variablesDarkTheme Variable data (name +value) for additionally dark theme
|
|
88
91
|
* @param nameFormatter Function to format the variable name
|
|
89
92
|
* @param aliasFormatter Function to format a reference to another variable (e.g. `var(--name)` for CSS)
|
|
90
93
|
* @param options Generator options
|
|
91
94
|
*/
|
|
92
95
|
const getCssOrScssVariableContent = (variables, nameFormatter, aliasFormatter, options) => {
|
|
96
|
+
const variablesDarkTheme = options?.dataDarkTheme?.variables;
|
|
93
97
|
return Object.entries(variables).map(([name, value]) => {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
111
|
}
|
|
99
|
-
|
|
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};`;
|
|
100
122
|
});
|
|
101
123
|
};
|
|
@@ -26,7 +26,7 @@ export declare const parseFigmaVariables: (apiResponse: FigmaVariablesApiRespons
|
|
|
26
26
|
* @param value Figma variable value
|
|
27
27
|
* @param allVariables Object of all variables. Needed for variables that use aliases.
|
|
28
28
|
*/
|
|
29
|
-
export declare const resolveFigmaVariableValue: (value: VariableValue, allVariables: Record<string, Variable>, remBase?: ParseFigmaVariablesOptions["remBase"]) => string;
|
|
29
|
+
export declare const resolveFigmaVariableValue: (value: VariableValue, allVariables: Record<string, Variable>, remBase?: ParseFigmaVariablesOptions["remBase"], name?: string) => string;
|
|
30
30
|
/**
|
|
31
31
|
* Converts a RGBA value to a hex color.
|
|
32
32
|
* Transparency will only be added if its not 1, e.g. "#000000" instead of "#000000ff"
|
|
@@ -14,12 +14,14 @@ export const parseFigmaVariables = (apiResponse, options) => {
|
|
|
14
14
|
*/
|
|
15
15
|
Object.values(apiResponse.meta.variables).forEach((variable) => {
|
|
16
16
|
const collection = apiResponse.meta.variableCollections[variable.variableCollectionId];
|
|
17
|
-
if (variable.hiddenFromPublishing ||
|
|
17
|
+
if (variable.hiddenFromPublishing ||
|
|
18
|
+
variable.deletedButReferenced ||
|
|
19
|
+
collection.hiddenFromPublishing)
|
|
18
20
|
return;
|
|
19
21
|
// parse variable value for every mode
|
|
20
22
|
Object.values(collection.modes).forEach((mode) => {
|
|
21
23
|
const variableName = normalizeVariableName(variable.name);
|
|
22
|
-
const variableValue = resolveFigmaVariableValue(variable.valuesByMode?.[mode.modeId], apiResponse.meta.variables, options?.remBase);
|
|
24
|
+
const variableValue = resolveFigmaVariableValue(variable.valuesByMode?.[mode.modeId], apiResponse.meta.variables, options?.remBase, variableName);
|
|
23
25
|
// add/update parsed variable value
|
|
24
26
|
const existingIndex = parsedData.findIndex((i) => i.modeName === mode.name);
|
|
25
27
|
if (existingIndex !== -1) {
|
|
@@ -71,15 +73,20 @@ export const parseFigmaVariables = (apiResponse, options) => {
|
|
|
71
73
|
* @param value Figma variable value
|
|
72
74
|
* @param allVariables Object of all variables. Needed for variables that use aliases.
|
|
73
75
|
*/
|
|
74
|
-
export const resolveFigmaVariableValue = (value, allVariables, remBase = 16) => {
|
|
76
|
+
export const resolveFigmaVariableValue = (value, allVariables, remBase = 16, name) => {
|
|
75
77
|
if (typeof value === "number") {
|
|
78
|
+
if (name?.includes("font-weight"))
|
|
79
|
+
return `${value}`;
|
|
76
80
|
// numeric value, parse as rem or pixel value
|
|
77
|
-
|
|
78
|
-
|
|
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()"
|
|
79
83
|
if (remBase === false || remBase <= 0)
|
|
80
84
|
return `${value}px`;
|
|
81
85
|
return `${value / remBase}rem`;
|
|
82
86
|
}
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
return `"${value}"`;
|
|
89
|
+
}
|
|
83
90
|
if ("type" in value) {
|
|
84
91
|
// parse value as alias
|
|
85
92
|
if (value.type !== "VARIABLE_ALIAS") {
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sit-onyx/figma-utils",
|
|
3
3
|
"description": "Utility functions and CLI for importing data from the Figma API into different formats (e.g. CSS, SCSS etc.)",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.10",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"author": "Schwarz IT KG",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
6
11
|
"bin": {
|
|
7
12
|
"@sit-onyx/figma-utils": "./dist/cli.js"
|
|
8
13
|
},
|
|
9
14
|
"files": [
|
|
10
15
|
"dist"
|
|
11
16
|
],
|
|
12
|
-
"engines": {
|
|
13
|
-
"node": ">=18"
|
|
14
|
-
},
|
|
15
17
|
"types": "./dist/index.d.ts",
|
|
16
18
|
"exports": {
|
|
17
19
|
".": {
|
|
@@ -29,7 +31,10 @@
|
|
|
29
31
|
"url": "https://github.com/SchwarzIT/onyx/issues"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
32
|
-
"commander": "^
|
|
34
|
+
"commander": "^14.0.0"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"svgo": "^4.0.0"
|
|
33
38
|
},
|
|
34
39
|
"scripts": {
|
|
35
40
|
"build": "pnpm run '/type-check|build-only/'",
|