Skip to content

Commit

Permalink
feat: implement Figma icon import CLI (#1754)
Browse files Browse the repository at this point in the history
Relates to #939

Implement Figma CLI command to import and optimize SVG icons from Figma.
Also added a GitHub action which is run daily to create a PR with
updated icons.
  • Loading branch information
larsrickert authored Aug 23, 2024
1 parent 80c36cb commit b525ca5
Show file tree
Hide file tree
Showing 664 changed files with 3,911 additions and 2,609 deletions.
8 changes: 8 additions & 0 deletions .changeset/slow-dryers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@sit-onyx/figma-utils": minor
---

feat: add `import-icons` CLI command

Add new command to import SVG icons from Figma.
For further information, see [our docs](https://onyx.schwarz/development/packages/figma-utils.html#import-icons)
45 changes: 45 additions & 0 deletions .changeset/tame-months-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
"@sit-onyx/icons": major
---

feat: update latest icons

All icons are updated to use the latest icons from

#### New icons

- anchor
- arrow-small-up-right-top
- broom
- can
- cheese
- chocolate
- company-plus
- computer-eye
- container-large
- container-small
- containers
- controller
- crane
- dolphin
- git
- globe-network
- globe-shield
- icecream
- parking-search
- prawn
- ship-container
- store-test
- tie
- truck-attention
- truck-empty

#### Deleted icons

- computer-argus (renamed to computer-eye)

#### Other breaking changes

- removed `ICON_CATEGORIES`, use `groupIconsByCategory(ICON_METADATA)` instead
- removed `optimizeSvg`, `isDirectory` and `readAllIconPaths`
- moved exported types from `/utils` to `/types`
49 changes: 49 additions & 0 deletions .github/workflows/import-figma-icons.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Import Figma icons
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * *" # run daily at 6 am

jobs:
import:
name: Import Figma icons
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm

- name: 📦 Install dependencies
run: pnpm install

- name: 🛠️ Build Figma utils
run: pnpm run build
working-directory: packages/figma-utils

# delete all icons before importing them so no longer existing or renamed icons are removed
- name: Clear icons
run: rm -rfv src/assets/*
working-directory: packages/icons

- name: Import icons
run: |
pnpm run @sit-onyx/figma-utils import-icons -k "${{ vars.FIGMA_FILE_KEY_ICONS }}" -t "${{ secrets.FIGMA_TOKEN }}" -p "${{ vars.FIGMA_ICON_PAGE_ID }}" -d "../icons/src/assets" -m "../icons/src/metadata.json"
working-directory: packages/figma-utils

- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
commit-message: "feat: update Figma icons"
title: "feat: update Figma icons"
body: This is an auto-generated pull request. All Figma icons have been updated.
branch-suffix: short-commit-hash # needed to not override other pull requests created via workflows
team-reviewers: "@SchwarzIT/onyx-ux"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_NAME: ${{ github.ref_name }}
6 changes: 5 additions & 1 deletion apps/docs/src/.vitepress/components/OnyxIconLibrary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ const filteredCategories = computed(() => {
icons: category.icons.filter(
(icon) =>
icon.iconName.toLowerCase().includes(lowerCaseSearch) ||
icon.metadata.aliases?.some((alias) => alias.includes(lowerCaseSearch)),
icon.metadata.aliases?.some(
(alias) =>
alias.includes(lowerCaseSearch) ||
alias.replace(/-/g, " ").includes(lowerCaseSearch),
),
),
};
})
Expand Down
9 changes: 6 additions & 3 deletions apps/docs/src/.vitepress/utils-icons.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ICON_CATEGORIES, getIconImportName } from "@sit-onyx/icons";
import { getIconImportName, groupIconsByCategory, ICON_METADATA } from "@sit-onyx/icons";
import { capitalize } from "vue";

export type EnrichedIcon = {
Expand Down Expand Up @@ -33,8 +33,10 @@ const getIconContextData = (iconName: string, allIconContents: Record<string, st
// Collects all needed icon context data and provides them as a list.
export const getEnrichedIconCategoryList = (
allIconContents: Record<string, string>,
): EnrichedCategory[] =>
Object.entries(ICON_CATEGORIES).map(([category, icons]) => ({
): EnrichedCategory[] => {
const categories = groupIconsByCategory(ICON_METADATA);

return Object.entries(categories).map(([category, icons]) => ({
name: category,
icons: icons.map((icon) => ({
...icon,
Expand All @@ -46,3 +48,4 @@ export const getEnrichedIconCategoryList = (
},
})),
}));
};
66 changes: 64 additions & 2 deletions apps/docs/src/development/packages/figma-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,18 @@ yarn install @sit-onyx/figma-utils@beta

:::

### Examples
### Import variables as CSS

#### Import Figma variables to CSS variables
::: info CLI command
Importing variables is also supported via CLI. For more information, run:

```sh
npx @sit-onyx/figma-utils@beta import-variables --help
```

:::

Alternatively, you can implement it manually for full control and customization:

```ts
import fs from "node:fs";
Expand Down Expand Up @@ -93,3 +102,56 @@ parsedVariables.forEach((mode) => {
fs.writeFileSync(fullPath, fileContent);
});
```

### Import icons

::: info CLI command
Importing icons is also supported via CLI. For more information, run:

```sh
npx @sit-onyx/figma-utils@beta import-icons --help
```

:::

Alternatively, you can implement it manually for full control and customization:

```ts
import fs from "node:fs";
import path from "node:path";
import { fetchFigmaComponents, optimizeSvg } from "@sit-onyx/figma-utils";

const FILE_KEY = "your-figma-file-key";
const FIGMA_TOKEN = "your-figma-access-token";
const ICON_PAGE_ID = "your-page-id-that-contains-the-icons"; // e.g. "1:345"

// fetch icon components from Figma API
const data = await fetchFigmaComponents(FILE_KEY, FIGMA_TOKEN);

// parse components into a normalized format
const parsedIcons = parseComponentsToIcons({
components: data.meta.components,
pageId: ICON_PAGE_ID,
});

// fetch actual SVG content of the icons
const svgContents = await fetchFigmaSVGs(
FILE_KEY,
parsedIcons.map(({ id }) => id),
FIGMA_TOKEN,
);

const outputDirectory = process.cwd();

// write .svg files for all icons
await Promise.all(
parsedIcons.map((icon) => {
const content = optimizeSvg(svgContents[icon.id]);
const fullPath = path.join(outputDirectory, `${icon.name}.svg`);
return writeFile(fullPath, content, "utf-8");
}),
);

// optionally write file with metadata (categories, alias names etc.)
await writeIconMetadata(path.join(outputDirectory, "metadata.json"), parsedIcons);
```
3 changes: 3 additions & 0 deletions packages/figma-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@
},
"dependencies": {
"commander": "^12.1.0"
},
"optionalDependencies": {
"svgo": "^3.3.2"
}
}
5 changes: 3 additions & 2 deletions packages/figma-utils/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import { Command } from "commander";
import fs from "node:fs";
import { fileURLToPath, URL } from "node:url";
import { importCommand } from "./commands/import-variables.js";
import { importIconsCommand } from "./commands/import-icons.js";
import { importVariablesCommand } from "./commands/import-variables.js";

const packageJson = JSON.parse(
fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"),
Expand All @@ -11,7 +12,7 @@ const packageJson = JSON.parse(
const cli = new Command();
cli.version(packageJson.version, "-v, --version").description(packageJson.description);

const availableCommands = [importCommand];
const availableCommands = [importVariablesCommand, importIconsCommand];
availableCommands.forEach((command) => cli.addCommand(command));

cli.parse();
85 changes: 85 additions & 0 deletions packages/figma-utils/src/commands/import-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Command } from "commander";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { writeIconMetadata } from "../icons/generate.js";
import { optimizeSvg } from "../icons/optimize.js";
import { parseComponentsToIcons } from "../icons/parse.js";
import { fetchFigmaComponents, fetchFigmaSVGs } from "../index.js";
import { isDirectory } from "../utils/fs.js";

export type ImportIconsCommandOptions = {
fileKey: string;
token: string;
pageId: string;
aliasSeparator: string;
dir?: string;
metaFile?: string;
};

export const importIconsCommand = new Command("import-icons")
.description("CLI tool to import SVG icons from Figma.")
.requiredOption("-k, --file-key <string>", "Figma file key (required)")
.requiredOption(
"-t, --token <string>",
"Figma access token with scope `file_read` or `files:read` (required)",
)
.requiredOption("-p, --page-id <string>", "Figma page ID that contains the icons (required)")
.option(
"-d, --dir <string>",
"Directory to save the icons to. Defaults to current working directory of the script.",
)
.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.',
)
.option(
"-s, --alias-separator <string>",
"Separator for icon alias names (which can be set to the component description in Figma).",
"|",
)
.action(importIconsCommandAction);

/**
* Action to run when executing the import action. Only intended to be called manually for testing.
*/
export async function importIconsCommandAction(options: ImportIconsCommandOptions) {
console.log("Fetching components from Figma API...");
const data = await fetchFigmaComponents(options.fileKey, options.token);

console.log("Parsing Figma icons...");
const parsedIcons = parseComponentsToIcons({
components: data.meta.components,
pageId: options.pageId,
aliasSeparator: options.aliasSeparator,
});
const outputDirectory = options.dir ?? process.cwd();

console.log(`Fetching SVG content for ${parsedIcons.length} icons...`);

const svgContents = await fetchFigmaSVGs(
options.fileKey,
parsedIcons.map(({ id }) => id),
options.token,
);

console.log("Optimizing and writing icon files...");

if (!(await isDirectory(outputDirectory))) {
await mkdir(outputDirectory, { recursive: true });
}

await Promise.all(
parsedIcons.map((icon) => {
const content = optimizeSvg(svgContents[icon.id]);
const fullPath = path.join(outputDirectory, `${icon.name}.svg`);
return writeFile(fullPath, content, "utf-8");
}),
);

if (options.metaFile) {
console.log("Writing icon metadata...");
await writeIconMetadata(options.metaFile, parsedIcons);
}

console.log("Done.");
}
11 changes: 6 additions & 5 deletions packages/figma-utils/src/commands/import-variables.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "node:fs";
import { beforeEach, describe, expect, test, vi } from "vitest";
import * as functions from "../index.js";
import { ImportCommandOptions, importCommandAction } from "./import-variables.js";
import { ImportVariablesCommandOptions, importVariablesCommandAction } from "./import-variables.js";

vi.mock("node:fs");

Expand All @@ -14,7 +14,7 @@ describe("import-variables.ts", () => {
format: ["CSS"],
token: "test-token",
selector: ":root",
} satisfies ImportCommandOptions;
} satisfies ImportVariablesCommandOptions;

beforeEach(() => {
vi.clearAllMocks();
Expand All @@ -23,7 +23,8 @@ describe("import-variables.ts", () => {
});

test("should throw error for unknown formats", () => {
const promise = () => importCommandAction({ ...mockOptions, format: ["does-not-exist"] });
const promise = () =>
importVariablesCommandAction({ ...mockOptions, format: ["does-not-exist"] });
expect(promise).rejects.toThrowError(
'Unknown format "does-not-exist". Supported: CSS, SCSS, JSON',
);
Expand All @@ -35,7 +36,7 @@ describe("import-variables.ts", () => {
]);

const promise = () =>
importCommandAction({
importVariablesCommandAction({
...mockOptions,
modes: ["test-mode-1", "does-not-exist"],
});
Expand All @@ -53,7 +54,7 @@ describe("import-variables.ts", () => {

vi.spyOn(functions, "generateAsCSS").mockReturnValue("mock-css-file-content");

await importCommandAction({ ...mockOptions, modes: ["test-mode-1", "test-mode-2"] });
await importVariablesCommandAction({ ...mockOptions, modes: ["test-mode-1", "test-mode-2"] });

expect(functions.fetchFigmaVariables).toHaveBeenCalledOnce();
expect(functions.parseFigmaVariables).toHaveBeenCalledOnce();
Expand Down
Loading

0 comments on commit b525ca5

Please sign in to comment.