@sit-onyx/figma-utils 1.0.0-beta.1 → 1.0.0-beta.3

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/dist/cli.js CHANGED
@@ -2,10 +2,11 @@
2
2
  import { Command } from "commander";
3
3
  import fs from "node:fs";
4
4
  import { fileURLToPath, URL } from "node:url";
5
- import { importCommand } from "./commands/import-variables.js";
5
+ import { importIconsCommand } from "./commands/import-icons.js";
6
+ import { importVariablesCommand } from "./commands/import-variables.js";
6
7
  const packageJson = JSON.parse(fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
7
8
  const cli = new Command();
8
9
  cli.version(packageJson.version, "-v, --version").description(packageJson.description);
9
- const availableCommands = [importCommand];
10
+ const availableCommands = [importVariablesCommand, importIconsCommand];
10
11
  availableCommands.forEach((command) => cli.addCommand(command));
11
12
  cli.parse();
@@ -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,47 @@
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 { optimizeSvg } from "../icons/optimize.js";
6
+ import { parseComponentsToIcons } from "../icons/parse.js";
7
+ import { fetchFigmaComponents, fetchFigmaSVGs } from "../index.js";
8
+ import { isDirectory } from "../utils/fs.js";
9
+ export const importIconsCommand = new Command("import-icons")
10
+ .description("CLI tool to import SVG icons from Figma.")
11
+ .requiredOption("-k, --file-key <string>", "Figma file key (required)")
12
+ .requiredOption("-t, --token <string>", "Figma access token with scope `file_read` or `files:read` (required)")
13
+ .requiredOption("-p, --page-id <string>", "Figma page ID that contains the icons (required)")
14
+ .option("-d, --dir <string>", "Directory to save the icons to. Defaults to current working directory of the script.")
15
+ .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.')
16
+ .option("-s, --alias-separator <string>", "Separator for icon alias names (which can be set to the component description in Figma).", "|")
17
+ .action(importIconsCommandAction);
18
+ /**
19
+ * Action to run when executing the import action. Only intended to be called manually for testing.
20
+ */
21
+ export async function importIconsCommandAction(options) {
22
+ console.log("Fetching components from Figma API...");
23
+ const data = await fetchFigmaComponents(options.fileKey, options.token);
24
+ console.log("Parsing Figma icons...");
25
+ const parsedIcons = parseComponentsToIcons({
26
+ components: data.meta.components,
27
+ pageId: options.pageId,
28
+ aliasSeparator: options.aliasSeparator,
29
+ });
30
+ const outputDirectory = options.dir ?? process.cwd();
31
+ console.log(`Fetching SVG content for ${parsedIcons.length} icons...`);
32
+ const svgContents = await fetchFigmaSVGs(options.fileKey, parsedIcons.map(({ id }) => id), options.token);
33
+ console.log("Optimizing and writing icon files...");
34
+ if (!(await isDirectory(outputDirectory))) {
35
+ await mkdir(outputDirectory, { recursive: true });
36
+ }
37
+ await Promise.all(parsedIcons.map((icon) => {
38
+ const content = optimizeSvg(svgContents[icon.id]);
39
+ const fullPath = path.join(outputDirectory, `${icon.name}.svg`);
40
+ return writeFile(fullPath, content, "utf-8");
41
+ }));
42
+ if (options.metaFile) {
43
+ console.log("Writing icon metadata...");
44
+ await writeIconMetadata(options.metaFile, parsedIcons);
45
+ }
46
+ console.log("Done.");
47
+ }
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- export type ImportCommandOptions = {
2
+ export type ImportVariablesCommandOptions = {
3
3
  fileKey: string;
4
4
  token: string;
5
5
  format: string[];
@@ -8,8 +8,8 @@ export type ImportCommandOptions = {
8
8
  modes?: string[];
9
9
  selector: string;
10
10
  };
11
- export declare const importCommand: Command;
11
+ export declare const importVariablesCommand: Command;
12
12
  /**
13
13
  * Action to run when executing the import action. Only intended to be called manually for testing.
14
14
  */
15
- export declare function importCommandAction(options: ImportCommandOptions): Promise<void>;
15
+ 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 importCommand = new Command("import-variables")
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)")
@@ -11,11 +11,11 @@ export const importCommand = new Command("import-variables")
11
11
  .option("-d, --dir <string>", "Working directory to use. Defaults to current working directory of the script.")
12
12
  .option("-m, --modes <strings...>", "Can be used to only export specific Figma modes. If unset, all modes will be exported as a separate file.")
13
13
  .option("-s, --selector <string>", 'CSS selector to use for the CSS format. 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(importCommandAction);
14
+ .action(importVariablesCommandAction);
15
15
  /**
16
16
  * Action to run when executing the import action. Only intended to be called manually for testing.
17
17
  */
18
- export async function importCommandAction(options) {
18
+ export async function importVariablesCommandAction(options) {
19
19
  const generators = {
20
20
  CSS: (data) => generateAsCSS(data, { selector: options.selector }),
21
21
  SCSS: generateAsSCSS,
@@ -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,23 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { isDirectory } from "../utils/fs.js";
4
+ /**
5
+ * Writes a JSON file with metadata of the given icons (category, aliases etc.).
6
+ *
7
+ * @param path File path of the .json file, e.g. "./metadata.json"
8
+ * @param icons Icons to write metadata for
9
+ */
10
+ export const writeIconMetadata = async (path, icons) => {
11
+ const metaDirname = dirname(path);
12
+ if (!(await isDirectory(metaDirname))) {
13
+ await mkdir(metaDirname, { recursive: true });
14
+ }
15
+ const iconMetadata = icons.reduce((meta, icon) => {
16
+ meta[icon.name] = {
17
+ category: icon.category,
18
+ aliases: icon.aliases,
19
+ };
20
+ return meta;
21
+ }, {});
22
+ await writeFile(path, JSON.stringify(iconMetadata, null, 2), "utf-8");
23
+ };
@@ -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 all fills so the color can be set via CSS
5
+ * - remove dimensions (height/width) so it can be set via CSS
6
+ * - "preset-default" to reduce file size and redundant information
7
+ */
8
+ export declare const optimizeSvg: (svgContent: string) => 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 all fills so the color can be set via CSS
6
+ * - remove dimensions (height/width) so it can be set via CSS
7
+ * - "preset-default" to reduce file size and redundant information
8
+ */
9
+ export const optimizeSvg = (svgContent) => {
10
+ const { data } = optimize(svgContent, {
11
+ multipass: true,
12
+ plugins: [
13
+ { name: "preset-default" },
14
+ { name: "removeDimensions" },
15
+ {
16
+ name: "removeAttrs",
17
+ params: {
18
+ // remove all fills so we can set the color via CSS
19
+ attrs: ["fill"],
20
+ },
21
+ },
22
+ ],
23
+ });
24
+ return data;
25
+ };
@@ -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
+ aliases: component.description
9
+ .split(options.aliasSeparator ?? "|")
10
+ .map((alias) => alias.trim())
11
+ .filter((i) => i !== ""),
12
+ category: component.containing_frame.name.trim(),
13
+ };
14
+ })
15
+ .sort((a, b) => a.name.localeCompare(b.name));
16
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
+ export * from "./icons/generate.js";
2
+ export * from "./icons/optimize.js";
3
+ export * from "./icons/parse.js";
1
4
  export * from "./types/figma.js";
2
5
  export * from "./utils/fetch.js";
3
- export * from "./utils/generate.js";
4
- export * from "./utils/parse.js";
6
+ export * from "./variables/generate.js";
7
+ export * from "./variables/parse.js";
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
+ export * from "./icons/generate.js";
2
+ export * from "./icons/optimize.js";
3
+ export * from "./icons/parse.js";
1
4
  export * from "./types/figma.js";
2
5
  export * from "./utils/fetch.js";
3
- export * from "./utils/generate.js";
4
- export * from "./utils/parse.js";
6
+ export * from "./variables/generate.js";
7
+ export * from "./variables/parse.js";
@@ -48,3 +48,59 @@ export type ParsedVariable = {
48
48
  */
49
49
  variables: Record<string, string>;
50
50
  };
51
+ /**
52
+ * Figma API response when fetching from https://api.figma.com/v1/files/${fileKey}/components
53
+ */
54
+ export type FigmaComponentsApiResponse = {
55
+ meta: {
56
+ components: Component[];
57
+ };
58
+ };
59
+ /**
60
+ * An arrangement of published UI elements that can be instantiated across Figma files
61
+ */
62
+ export type Component = {
63
+ /**
64
+ * ID of the component node within the Figma file
65
+ */
66
+ node_id: string;
67
+ /**
68
+ * Name of the component
69
+ */
70
+ name: string;
71
+ /**
72
+ * Data on component's containing frame, if component resides within a frame
73
+ */
74
+ containing_frame: FrameInfo;
75
+ /**
76
+ * The description of the component as entered by the publisher
77
+ */
78
+ description: string;
79
+ };
80
+ /**
81
+ * Data on the frame a component resides in
82
+ */
83
+ export type FrameInfo = {
84
+ /**
85
+ * ID of the frame node within the file
86
+ */
87
+ nodeId: string;
88
+ /**
89
+ * Name of the frame
90
+ */
91
+ name: string;
92
+ /**
93
+ * ID of the frame's residing page
94
+ */
95
+ pageId: string;
96
+ /**
97
+ * Name of the frame's residing page
98
+ */
99
+ pageName: string;
100
+ };
101
+ export type ParsedIcon = {
102
+ id: string;
103
+ name: string;
104
+ aliases: string[];
105
+ category: string;
106
+ };
@@ -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>;
@@ -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
- const response = await fetch(`https://api.figma.com/v1/files/${fileKey}/variables/local`, {
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
  },
@@ -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 (e) {
13
+ return false;
14
+ }
15
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
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.1",
4
+ "version": "1.0.0-beta.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "@sit-onyx/figma-utils": "./dist/cli.js"
@@ -31,6 +31,9 @@
31
31
  "dependencies": {
32
32
  "commander": "^12.1.0"
33
33
  },
34
+ "optionalDependencies": {
35
+ "svgo": "^3.3.2"
36
+ },
34
37
  "scripts": {
35
38
  "build": "pnpm run '/type-check|build-only/'",
36
39
  "build-only": "rimraf dist && tsc -p tsconfig.node.json --composite false",
File without changes
File without changes
File without changes
File without changes