diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ced896694..517035a4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -939,6 +939,31 @@ importers: picocolors: specifier: ^1.1.1 version: 1.1.1 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/node': + specifier: ^22.20.1 + version: 22.20.1 + eslint: + specifier: 'catalog:' + version: 9.39.5(jiti@2.7.0) + tsx: + specifier: ^4.23.12 + version: 4.23.13 + typescript: + specifier: 'catalog:' + version: 5.9.3 scripts/ts-api-extractor: dependencies: @@ -14803,9 +14828,9 @@ snapshots: dependencies: acorn: 8.16.0 - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk@8.3.5: dependencies: @@ -14818,8 +14843,7 @@ snapshots: acorn@8.17.0: {} - acorn@8.18.0: - optional: true + acorn@8.18.0: {} agent-base@7.1.4: {} @@ -15755,7 +15779,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.17.0 + acorn: 8.18.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -15879,7 +15903,7 @@ snapshots: '@typescript-eslint/parser': 8.59.4(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.5(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.5(jiti@2.7.0)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)) @@ -15912,7 +15936,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.5(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.5(jiti@2.7.0)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -15935,7 +15959,7 @@ snapshots: '@typescript-eslint/parser': 8.59.4(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.5(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.5(jiti@2.7.0)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -16100,8 +16124,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} diff --git a/scripts/sync-figma/commands/notify-slack.mjs b/scripts/sync-figma/commands/notify-slack.ts similarity index 87% rename from scripts/sync-figma/commands/notify-slack.mjs rename to scripts/sync-figma/commands/notify-slack.ts index 43d523766..4c2478e5f 100644 --- a/scripts/sync-figma/commands/notify-slack.mjs +++ b/scripts/sync-figma/commands/notify-slack.ts @@ -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, @@ -68,10 +68,5 @@ const message = { ], }; -try { - await sendWebhookMessage(SLACK_GDS_ALARM_WEBHOOK_URL, message); - console.log('✅ Slack 알림이 성공적으로 전송되었습니다.'); -} catch (error) { - console.error('❌ Slack 알림 전송 중 오류 발생:', error.message); - process.exit(1); -} +await sendWebhookMessage(SLACK_GDS_ALARM_WEBHOOK_URL, message); +console.log('✅ Slack 알림이 성공적으로 전송되었습니다.'); diff --git a/scripts/sync-figma/commands/sync-icons.mjs b/scripts/sync-figma/commands/sync-icons.ts similarity index 84% rename from scripts/sync-figma/commands/sync-icons.mjs rename to scripts/sync-figma/commands/sync-icons.ts index 70be7eb5d..b698d9155 100644 --- a/scripts/sync-figma/commands/sync-icons.mjs +++ b/scripts/sync-figma/commands/sync-icons.ts @@ -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'; @@ -26,20 +25,17 @@ 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'; -import { - filterDocumentByNodeType, - getIconJsx, - getNodesWithUrl, -} from '../src/integrations/figma/lib.js'; +} from '~/icons/constants'; +import { ICON_TYPES } from '~/icons/icon-types'; +import getIconComponent from '~/icons/templates/icon/icon-component'; +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, getNodesWithUrl } 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) @@ -56,7 +52,7 @@ const PRETTIER_OPTIONS = { printWidth: 100, }; -function normalizeIconName(name) { +function normalizeIconName(name: string) { return startCase(camelCase(name.replace(FIGMA_EMOJI_PREFIX_PATTERN, ''))).replace(/ /g, ''); } @@ -67,31 +63,30 @@ if (!process.env.FIGMA_TOKEN) { process.exit(1); } +if (!TYPE || !Object.hasOwn(ICON_TYPES, TYPE)) { + 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 = { @@ -121,10 +116,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 () => { @@ -153,7 +148,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 { @@ -184,7 +179,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 diff --git a/scripts/sync-figma/eslint.config.mjs b/scripts/sync-figma/eslint.config.mjs new file mode 100644 index 000000000..68b4b5c13 --- /dev/null +++ b/scripts/sync-figma/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '@repo/eslint-config/base'; + +export default [...configs]; diff --git a/scripts/sync-figma/package.json b/scripts/sync-figma/package.json index 9e1047801..56bd76392 100644 --- a/scripts/sync-figma/package.json +++ b/scripts/sync-figma/package.json @@ -5,15 +5,25 @@ "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" + "lint": "eslint ./commands ./src", + "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": { "lodash-es": "^4.18.1", "p-limit": "^6.2.0", - "picocolors": "^1.1.1" + "picocolors": "^1.1.1", + "prettier": "^3.9.6" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.20.1", + "eslint": "catalog:", + "tsx": "^4.23.12", + "typescript": "catalog:" } } diff --git a/scripts/sync-figma/src/icons/constants.js b/scripts/sync-figma/src/icons/constants.ts similarity index 90% rename from scripts/sync-figma/src/icons/constants.js rename to scripts/sync-figma/src/icons/constants.ts index 125422fad..3245c8552 100644 --- a/scripts/sync-figma/src/icons/constants.js +++ b/scripts/sync-figma/src/icons/constants.ts @@ -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, diff --git a/scripts/sync-figma/src/icons/icon-types.js b/scripts/sync-figma/src/icons/icon-types.ts similarity index 70% rename from scripts/sync-figma/src/icons/icon-types.js rename to scripts/sync-figma/src/icons/icon-types.ts index 4e23d0548..03a2c1709 100644 --- a/scripts/sync-figma/src/icons/icon-types.js +++ b/scripts/sync-figma/src/icons/icon-types.ts @@ -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 = { basic: { id: 'basic', nodeIds: FIGMA_ICONS_BASIC_NODE_IDS, @@ -16,4 +22,6 @@ const ICON_TYPES = { }, }; +export type { IconType }; + export { ICON_TYPES }; diff --git a/scripts/sync-figma/src/icons/templates/icon/icon-component-index.js b/scripts/sync-figma/src/icons/templates/icon/icon-component-index.ts similarity index 63% rename from scripts/sync-figma/src/icons/templates/icon/icon-component-index.js rename to scripts/sync-figma/src/icons/templates/icon/icon-component-index.ts index ff523af71..433696b42 100644 --- a/scripts/sync-figma/src/icons/templates/icon/icon-component-index.js +++ b/scripts/sync-figma/src/icons/templates/icon/icon-component-index.ts @@ -1,4 +1,4 @@ -export default (name) => ` +export default (name: string) => ` import ${name} from './${name}'; export default ${name}; diff --git a/scripts/sync-figma/src/icons/templates/icon/icon-component.js b/scripts/sync-figma/src/icons/templates/icon/icon-component.ts similarity index 78% rename from scripts/sync-figma/src/icons/templates/icon/icon-component.js rename to scripts/sync-figma/src/icons/templates/icon/icon-component.ts index fb923445e..778a4e6d9 100644 --- a/scripts/sync-figma/src/icons/templates/icon/icon-component.js +++ b/scripts/sync-figma/src/icons/templates/icon/icon-component.ts @@ -1,4 +1,4 @@ -export default (IconName, Icon) => ` +export default (IconName: string, Icon: string) => ` import IconBase from '~/components/icon-base'; import type { IconProps } from '~/components/icon-base'; diff --git a/scripts/sync-figma/src/icons/templates/icon/icons-index.js b/scripts/sync-figma/src/icons/templates/icon/icons-index.ts similarity index 71% rename from scripts/sync-figma/src/icons/templates/icon/icons-index.js rename to scripts/sync-figma/src/icons/templates/icon/icons-index.ts index ec3d76fc4..04af1157b 100644 --- a/scripts/sync-figma/src/icons/templates/icon/icons-index.js +++ b/scripts/sync-figma/src/icons/templates/icon/icons-index.ts @@ -1,3 +1,3 @@ -export default (iconNames) => ` +export default (iconNames: string[]) => ` ${iconNames.map((iconName) => `export { default as ${iconName} } from './${iconName}';`).join(' ')} `; diff --git a/scripts/sync-figma/src/integrations/figma/api.js b/scripts/sync-figma/src/integrations/figma/api.js deleted file mode 100644 index 72e1ff3f4..000000000 --- a/scripts/sync-figma/src/integrations/figma/api.js +++ /dev/null @@ -1,41 +0,0 @@ -import process from 'node:process'; - -const headers = { - 'X-FIGMA-TOKEN': process.env.FIGMA_TOKEN, -}; - -/** - * GET file nodes - * - * @link https://www.figma.com/developers/api#get-file-nodes-endpoint - */ -const getFileNodes = async ({ fileKey, nodeIds, depth = 1 }) => { - 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(); -}; - -/** - * GET image - * - * @link https://www.figma.com/developers/api#get-images-endpoint - */ -const getImage = async ({ fileKey, nodeIds, format = 'svg' }) => { - 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(); -}; - -export { getFileNodes, getImage }; diff --git a/scripts/sync-figma/src/integrations/figma/api.ts b/scripts/sync-figma/src/integrations/figma/api.ts new file mode 100644 index 000000000..52a5e0195 --- /dev/null +++ b/scripts/sync-figma/src/integrations/figma/api.ts @@ -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; +}; + +type GetImageResponse = { + images: Record; +}; + +/** + * 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 => { + 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; +}; + +/** + * 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 => { + 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; +}; + +export type { FigmaNode }; + +export { getFileNodes, getImage }; diff --git a/scripts/sync-figma/src/integrations/figma/lib.js b/scripts/sync-figma/src/integrations/figma/lib.ts similarity index 50% rename from scripts/sync-figma/src/integrations/figma/lib.js rename to scripts/sync-figma/src/integrations/figma/lib.ts index a4d1f0fd1..f7df33e81 100644 --- a/scripts/sync-figma/src/integrations/figma/lib.js +++ b/scripts/sync-figma/src/integrations/figma/lib.ts @@ -1,23 +1,44 @@ -import { getFileNodes, getImage } from './api.js'; -import { makeFlexibleColorIcon, remakeMaskStyle, svgToIconComponent } from './transforms.js'; +import type { FigmaNodeType } from '~/icons/constants'; + +import type { FigmaNode } from './api'; +import { getFileNodes, getImage } from './api'; +import { makeFlexibleColorIcon, remakeMaskStyle, svgToIconComponent } from './transforms'; + +type IconNode = FigmaNode & { parentId: string }; +type IconNodeWithUrl = IconNode & { url: string }; /** * Filter documents from Figma by specific Node Type. */ -const filterDocumentByNodeType = async ({ nodeType, fileKey, nodeIds, depth }) => { +const filterDocumentByNodeType = async ({ + nodeType, + fileKey, + nodeIds, + depth, +}: { + nodeType: FigmaNodeType; + fileKey: string; + nodeIds: string; + depth?: number; +}): Promise => { const { nodes } = await getFileNodes({ fileKey, nodeIds, depth, }); - const childrenNodes = []; + const childrenNodes: IconNode[] = []; for (const key in nodes) { - if (nodes.hasOwnProperty(key)) { - const parent = nodes[key].document; + if (Object.hasOwn(nodes, key)) { + const node = nodes[key]; + if (!node) { + throw new Error(`Figma node not found: ${key}`); + } + + const parent = node.document; const parentId = parent.id; - parent.children.forEach((child) => { + parent.children?.forEach((child) => { childrenNodes.push({ ...child, parentId: parentId, @@ -33,16 +54,26 @@ const filterDocumentByNodeType = async ({ nodeType, fileKey, nodeIds, depth }) = /** * Get image URLs through the IDs of nodes received from Figma. */ -const getNodesWithUrl = async ({ nodes, fileKey }) => { +const getNodesWithUrl = async ({ + nodes, + fileKey, +}: { + nodes: IconNode[]; + fileKey: string; +}): Promise => { const nodeIds = nodes.map((node) => node.id).join(','); const { images } = await getImage({ fileKey, nodeIds, }); - const nodesWithUrl = nodes.map((item) => ({ - ...item, - url: images[item.id], - })); + const nodesWithUrl = nodes.map((item) => { + const url = images[item.id]; + if (!url) { + throw new Error(`Figma image URL missing for node: ${item.id} (${item.name})`); + } + + return { ...item, url }; + }); return nodesWithUrl; }; @@ -50,7 +81,13 @@ const getNodesWithUrl = async ({ nodes, fileKey }) => { /** * Convert svg files from Figma to React components. */ -const getIconJsx = async ({ url, isColorIcon }) => { +const getIconJsx = async ({ + url, + isColorIcon, +}: { + url: string; + isColorIcon: boolean; +}): Promise => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch SVG: ${response.status} ${response.statusText}`); @@ -66,4 +103,6 @@ const getIconJsx = async ({ url, isColorIcon }) => { } }; +export type { IconNode, IconNodeWithUrl }; + export { filterDocumentByNodeType, getNodesWithUrl, getIconJsx }; diff --git a/scripts/sync-figma/src/integrations/figma/transforms.js b/scripts/sync-figma/src/integrations/figma/transforms.ts similarity index 82% rename from scripts/sync-figma/src/integrations/figma/transforms.js rename to scripts/sync-figma/src/integrations/figma/transforms.ts index b6273a29c..14dc06483 100644 --- a/scripts/sync-figma/src/integrations/figma/transforms.js +++ b/scripts/sync-figma/src/integrations/figma/transforms.ts @@ -1,16 +1,16 @@ import { camelCase } from 'lodash-es'; -const svgToIconComponent = (svgDom) => +const svgToIconComponent = (svgDom: string) => svgDom .toString() .replace(/(?<=]*)\s+(width|height)="[^"]*"/g, '') - .replace(/^\/gm, '') + .replace(/^/gm, '') .replace(/\s\w*:\w*="/gm, (w) => ' ' + camelCase(w) + '="') .replace(/\s\w*-\w*="/gm, (w) => ' ' + camelCase(w) + '="') .replace(/>/, ' {...props} >'); -const remakeMaskStyle = (IconComponent) => { +const remakeMaskStyle = (IconComponent: string) => { const matches = IconComponent.matchAll(/(style)="(.*?)"/gi); for (const match of matches) { const splitColon = match[2].split(':'); @@ -29,7 +29,7 @@ const remakeMaskStyle = (IconComponent) => { return IconComponent; }; -const makeFlexibleColorIcon = (IconComponent) => +const makeFlexibleColorIcon = (IconComponent: string) => IconComponent.replace(/fill="black"/g, '').replace(/fill="none"/g, ''); export { svgToIconComponent, remakeMaskStyle, makeFlexibleColorIcon }; diff --git a/scripts/sync-figma/src/integrations/slack/api.js b/scripts/sync-figma/src/integrations/slack/api.ts similarity index 81% rename from scripts/sync-figma/src/integrations/slack/api.js rename to scripts/sync-figma/src/integrations/slack/api.ts index 49392ffd0..ccacf13e1 100644 --- a/scripts/sync-figma/src/integrations/slack/api.js +++ b/scripts/sync-figma/src/integrations/slack/api.ts @@ -1,4 +1,4 @@ -const sendWebhookMessage = async (webhookUrl, payload) => { +const sendWebhookMessage = async (webhookUrl: string, payload: unknown) => { const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/scripts/sync-figma/tsconfig.json b/scripts/sync-figma/tsconfig.json new file mode 100644 index 000000000..2f87b4400 --- /dev/null +++ b/scripts/sync-figma/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@repo/typescript-config/base", + "compilerOptions": { + "noEmit": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "target": "ES2023", + "lib": ["ES2023"], + "types": ["node"] + }, + "include": ["commands", "src"] +}