Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 90 additions & 96 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
import process from 'node:process';

import { sendWebhookMessage } from '../src/integrations/slack/api.js';
import { sendWebhookMessage } from '~/integrations/slack/api';

const {
SLACK_GDS_ALARM_WEBHOOK_URL,
Expand Down Expand Up @@ -72,6 +72,6 @@ try {
await sendWebhookMessage(SLACK_GDS_ALARM_WEBHOOK_URL, message);
console.log('✅ Slack 알림이 성공적으로 전송되었습니다.');
} catch (error) {
console.error('❌ Slack 알림 전송 중 오류 발생:', error.message);
console.error('❌ Slack 알림 전송 중 오류 발생:', error);
process.exit(1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
* Sync icons from Figma to local React components
*
* Usage:
* node --env-file=.env ./commands/sync-icons.mjs
* tsx --env-file=.env ./commands/sync-icons.ts
*
* Environment variables required:
* - FIGMA_TOKEN: Your Figma personal access token
* - TYPE: Icon type to sync ('basic' or 'symbol')
*
* Note: Node 20.6+ required for --env-file flag
*/
import { camelCase, startCase } from 'lodash-es';
import { createHash } from 'node:crypto';
Expand All @@ -26,20 +25,20 @@ import {
FIGMA_ICONS_SYMBOL_COLOR_COUNTRY_NODE_ID,
FIGMA_ICONS_SYMBOL_COLOR_NODE_ID,
FIGMA_NODE_TYPES,
} from '../src/icons/constants.js';
import { ICON_TYPES } from '../src/icons/icon-types.js';
import getIconComponentIndex from '../src/icons/templates/icon/icon-component-index.js';
import getIconComponent from '../src/icons/templates/icon/icon-component.js';
import getIconsIndex from '../src/icons/templates/icon/icons-index.js';
} from '~/icons/constants';
import { ICON_TYPES } from '~/icons/icon-types';
import getIconComponentIndex from '~/icons/templates/icon/icon-component-index';
import getIconsIndex from '~/icons/templates/icon/icons-index';
import type { IconNode } from '~/integrations/figma/lib';
import {
filterDocumentByNodeType,
getIconJsx,
getIconComponent,
getNodesWithUrl,
} from '../src/integrations/figma/lib.js';
} from '~/integrations/figma/lib';

const TYPE = process.env.TYPE;

function findRoot(dir) {
function findRoot(dir: string): string {
if (existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir;
const parent = path.dirname(dir);
if (parent === dir)
Expand All @@ -48,15 +47,14 @@ function findRoot(dir) {
}
const CURRENT_DIRECTORY = findRoot(path.dirname(fileURLToPath(import.meta.url)));
const FIGMA_EMOJI_PREFIX_PATTERN = /❤️\s*/g;
// Resolve the repo's prettier config so generated files match what `pnpm format` produces,
// plugins (import sorting) included.
const PRETTIER_OPTIONS = {
...(await prettier.resolveConfig(path.join(CURRENT_DIRECTORY, 'packages/icons/src/index.ts'))),
parser: 'typescript',
tabWidth: 4,
semi: true,
singleQuote: true,
printWidth: 100,
};

function normalizeIconName(name) {
function normalizeIconName(name: string) {
return startCase(camelCase(name.replace(FIGMA_EMOJI_PREFIX_PATTERN, ''))).replace(/ /g, '');
}

Expand All @@ -67,31 +65,30 @@ if (!process.env.FIGMA_TOKEN) {
process.exit(1);
}

if (!TYPE || !(TYPE in ICON_TYPES)) {
console.error(
pc.red(
` GDS FIGMA EXPORT ERROR: TYPE must be one of ${Object.keys(ICON_TYPES).join(', ')}.`,
),
);
process.exit(1);
}

try {
const { nodeIds, targetPath } = ICON_TYPES[TYPE];
let FILE_KEY = FIGMA_ICONS_FILE_KEY;
const FILE_KEY = FIGMA_ICONS_FILE_KEY;

// Get nodes (icons) set as COMPONENT in the file.
let components = [];
if (TYPE === 'basic' || TYPE === 'symbol') {
FILE_KEY = FIGMA_ICONS_FILE_KEY;
// Basic icons are composed of 2 frames, so nodeIds are in array form
for (const nodeId of nodeIds) {
const nodeComponents = await filterDocumentByNodeType({
nodeType: FIGMA_NODE_TYPES.Component,
fileKey: FILE_KEY,
nodeIds: nodeId,
depth: 1,
});
components = components.concat(nodeComponents);
}
} else {
components = await filterDocumentByNodeType({
// Each icon type spans several frames, so nodeIds are fetched one frame at a time.
let components: IconNode[] = [];
for (const nodeId of nodeIds) {
const nodeComponents = await filterDocumentByNodeType({
nodeType: FIGMA_NODE_TYPES.Component,
fileKey: FILE_KEY,
nodeIds,
nodeIds: nodeId,
depth: 1,
});
components = components.concat(nodeComponents);
}

const componentsInfo = {
Expand Down Expand Up @@ -121,10 +118,10 @@ try {
// Convert svg code to React components through image URLs and save locally.
console.log(pc.yellow(` GDS FIGMA EXPORT: `) + `Converting to React components...`);
const parentIconPath = path.join(CURRENT_DIRECTORY, targetPath);
const newIconNameArr = [];
const updatedIconNameArr = [];
const newIconNameArr: string[] = [];
const updatedIconNameArr: string[] = [];
const limit = pLimit(10);
const md5 = (str) => createHash('md5').update(str).digest('hex');
const md5 = (str: string) => createHash('md5').update(str).digest('hex');

const promiseCreateIcons = componentsWithUrl.map(({ name, url, parentId }) =>
limit(async () => {
Expand All @@ -143,8 +140,7 @@ try {
parentId === decodeURIComponent(FIGMA_ICONS_SYMBOL_COLOR_NODE_ID) ||
parentId === decodeURIComponent(FIGMA_ICONS_SYMBOL_COLOR_COUNTRY_NODE_ID);

const iconJsx = await getIconJsx({ url, isColorIcon });
const IconComponent = getIconComponent(iconName, iconJsx);
const IconComponent = await getIconComponent({ url, iconName, isColorIcon });
const formattedComponent = await prettier.format(IconComponent, PRETTIER_OPTIONS);

let shouldWrite = isNewIcon;
Expand All @@ -153,7 +149,7 @@ try {
await fs.mkdir(saveTargetPath, { recursive: true });
newIconNameArr.push(iconName);
} else {
let existingContent = null;
let existingContent: string | null = null;
try {
existingContent = await fs.readFile(iconFilePath, 'utf8');
} catch {
Expand Down Expand Up @@ -184,7 +180,7 @@ try {

// Detect and remove deleted icons
console.log(pc.yellow(` GDS FIGMA EXPORT: `) + `Checking for deleted icons...`);
const deletedIconNameArr = [];
const deletedIconNameArr: string[] = [];
const figmaIconNames = new Set(componentsInfo.nameArr);

// Get existing icon directories
Expand Down
24 changes: 18 additions & 6 deletions scripts/sync-figma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,27 @@
"description": "Internal CLI scripts for syncing assets from Figma",
"type": "module",
"scripts": {
"format": "prettier --write \"./src/**/*.{js,mjs,md}\"",
"format:check": "prettier --check \"./src/**/*.{js,mjs,md}\"",
"notify:slack": "node ./commands/notify-slack.mjs",
"sync-icons:basic": "TYPE=basic node --env-file-if-exists=.env ./commands/sync-icons.mjs",
"sync-icons:symbol": "TYPE=symbol node --env-file-if-exists=.env ./commands/sync-icons.mjs"
"format": "prettier --write \"./{commands,src}/**/*.{ts,md}\"",
"format:check": "prettier --check \"./{commands,src}/**/*.{ts,md}\"",
"notify:slack": "tsx ./commands/notify-slack.ts",
"sync-icons:basic": "TYPE=basic tsx --env-file-if-exists=.env ./commands/sync-icons.ts",
"sync-icons:symbol": "TYPE=symbol tsx --env-file-if-exists=.env ./commands/sync-icons.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"lodash-es": "^4.18.1",
"p-limit": "^6.2.0",
"picocolors": "^1.1.1"
"picocolors": "^1.1.1",
"prettier": "^3.9.6"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.20.1",
"tsx": "^4.23.12",
"typescript": "catalog:"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ const FIGMA_NODE_TYPES = {
Frame: 'FRAME',
Component: 'COMPONENT',
ComponentSet: 'COMPONENT_SET',
};
} as const;

type FigmaNodeType = (typeof FIGMA_NODE_TYPES)[keyof typeof FIGMA_NODE_TYPES];

export type { FigmaNodeType };

export {
FIGMA_ICONS_FILE_KEY,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { FIGMA_ICONS_BASIC_NODE_IDS, FIGMA_ICONS_SYMBOL_NODE_IDS } from './constants.js';
import { FIGMA_ICONS_BASIC_NODE_IDS, FIGMA_ICONS_SYMBOL_NODE_IDS } from './constants';

/**
* Script information by npm script
*/
const ICON_TYPES = {
type IconType = {
id: string;
nodeIds: string[];
targetPath: string;
};

const ICON_TYPES: Record<string, IconType> = {
basic: {
id: 'basic',
nodeIds: FIGMA_ICONS_BASIC_NODE_IDS,
Expand All @@ -16,4 +22,6 @@ const ICON_TYPES = {
},
};

export type { IconType };

export { ICON_TYPES };
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default (name) => `
export default (name: string) => `
import ${name} from './${name}';

export default ${name};
Expand Down
11 changes: 0 additions & 11 deletions scripts/sync-figma/src/icons/templates/icon/icon-component.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default (iconNames) => `
export default (iconNames: string[]) => `
${iconNames.map((iconName) => `export { default as ${iconName} } from './${iconName}';`).join(' ')}
`;
41 changes: 0 additions & 41 deletions scripts/sync-figma/src/integrations/figma/api.js

This file was deleted.

77 changes: 77 additions & 0 deletions scripts/sync-figma/src/integrations/figma/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import process from 'node:process';

import type { FigmaNodeType } from '~/icons/constants';

const headers = {
'X-FIGMA-TOKEN': process.env.FIGMA_TOKEN ?? '',
};

/** Only the slice of the Figma node shape this script actually reads. */
type FigmaNode = {
id: string;
name: string;
type: FigmaNodeType;
children?: FigmaNode[];
};

type GetFileNodesResponse = {
nodes: Record<string, { document: FigmaNode }>;
};

type GetImageResponse = {
images: Record<string, string>;
};

/**
* GET file nodes
*
* @link https://www.figma.com/developers/api#get-file-nodes-endpoint
*/
const getFileNodes = async ({
fileKey,
nodeIds,
depth = 1,
}: {
fileKey: string;
nodeIds: string;
depth?: number;
}): Promise<GetFileNodesResponse> => {
const result = await fetch(
`https://api.figma.com/v1/files/${fileKey}/nodes?ids=${nodeIds}&depth=${depth}`,
{ headers },
);
if (!result.ok) {
throw new Error(`Figma API error: ${result.status} ${result.statusText}`);
}
return result.json() as Promise<GetFileNodesResponse>;
};

/**
* GET image
*
* @link https://www.figma.com/developers/api#get-images-endpoint
*/
const getImage = async ({
fileKey,
nodeIds,
format = 'svg',
}: {
fileKey: string;
nodeIds: string;
format?: string;
}): Promise<GetImageResponse> => {
const result = await fetch(
`https://api.figma.com/v1/images/${fileKey}?ids=${nodeIds}&format=${format}&svg_include_id=false`,
{
headers,
},
);
if (!result.ok) {
throw new Error(`Figma API error: ${result.status} ${result.statusText}`);
}
return result.json() as Promise<GetImageResponse>;
};

export type { FigmaNode };

export { getFileNodes, getImage };
Loading
Loading