Skip to content

Commit

Permalink
[Feature] svg 아이콘 컴포넌트 방식 변환 자동화 스크립트 작성 (#31)
Browse files Browse the repository at this point in the history
* rename: scripts wow-ui 외부로 이동

* chore: eslintcache 파일 삭제

* refactor: 상수값 위치 변경

* chore: tsx 의존성 루트로 이동해서 설치

* feat: svg 파일 리액트 컴포넌트 방식으로 변환하는 스크립트 작성

* feat: svg 파일 삭제 시 변환되어 있는 컴포넌트 파일도 삭제되는 로직 추가

* refactor: 스크립트 리팩토링

* chore: wow-icons 빌드 input 설정 변경

* chore: 빌드 설정 변경

* chore: wow-icons 빌드 설정 변경

* chore: 스크립트 수정

* chore: wow-ui 빌드 시 babel runtime external로 설정하도록 변경

* chore: root에 wowds-tokens dev dependency 설치

* rename: theme 패키지 내부 폴더구조 정리

* chore: theme 패키지 exports 속성 변경

* chore: theme 패키지 tsconfig 수정

* chore: 스크립트 수정 및 적용

* chore: 스크립트 및 아이콘 코드 수정

* chore: root tsconfig에 styled-system exclude하도록 설정

* chore: wow-ui 빌드 설정 변경

* chore: wow-ui preserveModules 설정 추가

* chore: 스크립트 및 아이콘 수정

* chore: wow-icons에 절대 경로 설정 적용

* chore: 함수 이름 변경

* chore: 컴포넌트 디렉토리 존재하지 않는 경우 예외 처리

* chore: wow-icons 롤업 절대 경로 설정 추가

* chore: 의존성 정리

* chore: 아이콘 타입 추론 관련 설정 변경

* chore: stroke/fill 포함 여부에 따라 props 구성하도록 스크립트 수정

* chore: 안 쓰는 import문 삭제

* chore: format 스크립트 변경

* chore: 이미 생성된 컴포넌트는 다시 쓰지 않도록 수정
  • Loading branch information
ghdtjgus76 authored May 31, 2024
1 parent acb62fd commit 379fa1b
Show file tree
Hide file tree
Showing 30 changed files with 396 additions and 342 deletions.
1 change: 0 additions & 1 deletion .eslintcache

This file was deleted.

1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = {
globals: {
React: true,
JSX: true,
console: true,
},
env: {
browser: true,
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
"@changesets/changelog-git": "^0.2.0",
"@changesets/cli": "^2.27.1",
"@pandacss/dev": "^0.39.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-url": "^8.0.2",
"@svgr/rollup": "^8.1.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.2",
Expand Down Expand Up @@ -69,11 +69,13 @@
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-preserve-directives": "^0.4.0",
"shared-config": "workspace:^",
"theme": "workspace:*",
"theme": "workspace:^",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsx": "^4.11.0",
"turbo": "latest",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"wowds-tokens": "workspace:^"
},
"packageManager": "[email protected]",
"engines": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { promises as fs } from "fs";
import packageJSON from "package.json";
import path from "path";

import packageJSON from "../wow-ui/package.json";

type ExportItem =
| {
types: string;
Expand All @@ -13,8 +14,6 @@ type ExportObject = { [key: string]: ExportItem };
type EntryFileObject = { [key: string]: string };

const COMPONENT_PATH = "./src/components";
const ROLLUP_CONFIG_PATH = "rollup.config.js";
const PACKAGEJSON_PATH = "package.json";

const generateExports = (files: string[]) => {
const exportsObj: ExportObject = {
Expand All @@ -36,6 +35,8 @@ const generateExports = (files: string[]) => {
};

const applyExportsToPackageJSON = async (exportsObj: ExportObject) => {
const PACKAGEJSON_PATH = "package.json";

packageJSON.exports = packageJSON.exports || {};
Object.assign(packageJSON.exports, exportsObj);

Expand All @@ -53,6 +54,8 @@ const generateRollupEntryFiles = (files: string[]) => {
};

const applyEntryFilesToRollupConfig = async (entryFileObj: EntryFileObject) => {
const ROLLUP_CONFIG_PATH = "rollup.config.js";

const dirname = path.resolve();
const rollupConfigPath = path.join(dirname, ROLLUP_CONFIG_PATH);
let rollupConfigContent = await fs.readFile(rollupConfigPath, "utf-8");
Expand Down
140 changes: 140 additions & 0 deletions packages/scripts/generateReactComponentFromSvg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { existsSync, promises as fs } from "fs";
import path from "path";

const SVG_DIR = "../wow-icons/src/svg";
const COMPONENT_DIR = "../wow-icons/src/react";

type SvgComponentMap = { [key: string]: string };

const generateSvgComponentMap = async () => {
const svgFiles = (await fs.readdir(SVG_DIR)).reduce<SvgComponentMap>(
(map, svgFile) => {
const componentName = path
.basename(svgFile, ".svg")
.replace(/(^\w|-\w)/g, (match) => match.replace("-", "").toUpperCase());
map[componentName] = svgFile;

return map;
},
{}
);

return svgFiles;
};

const deleteUnusedComponentFiles = async (svgComponentMap: SvgComponentMap) => {
if (!existsSync(COMPONENT_DIR)) {
fs.mkdir(COMPONENT_DIR);
return;
}

const componentFiles = await fs.readdir(COMPONENT_DIR);
const componentFilesToDelete = componentFiles.filter((componentFile) => {
const componentName = path.basename(componentFile, ".tsx");
return !(componentName in svgComponentMap);
});

await Promise.all(
componentFilesToDelete.map((file) => {
const componentFilePath = path.resolve(COMPONENT_DIR, file);
return fs.unlink(componentFilePath);
})
);
};

const createComponentContent = (
componentName: string,
svgContent: string,
svgFile: string
): string => {
const iconName = path.basename(svgFile, ".svg");
const hasStroke = svgContent.includes("stroke=");
const fillAttributes = (svgContent.match(/fill="([^"]*)"/g) || []).filter(
(attr) => attr !== 'fill="none"'
);
const hasFill = fillAttributes.length;
const propsString = `{ className, width = 24, height = 24, viewBox = "0 0 24 24"${hasStroke || hasFill ? ` ${hasStroke ? ', stroke = "white"' : ""}${hasFill ? ', fill = "white"' : ""}` : ""}, ...rest }`;
const modifiedSvgContent = svgContent
.replace(/-(\w)/g, (_, letter) => letter.toUpperCase())
.replace(/width="(\d+)"/g, `width={width}`)
.replace(/height="(\d+)"/g, `height={height}`)
.replace(/viewBox="(.*?)"/g, `viewBox={viewBox}`)
.replace(/<svg([^>]*)fill="[^"]*"([^>]*)>/, "<svg$1$2>")
.replace(/fill="([^"]+)"/g, `fill={color[fill]}`)
.replace(/stroke="([^"]+)"/g, `stroke={color[stroke]}`)
.replace(
/<svg([^>]*)>/,
`<svg$1 aria-label="${iconName} icon" fill="none" ref={ref} className={className} {...rest}>`
);

return `
import { forwardRef } from 'react';
import { color } from "wowds-tokens";
import type { IconProps } from "../types/Icon.ts";
const ${componentName} = forwardRef<SVGSVGElement, IconProps>(
(${propsString}, ref) => {
return (
${modifiedSvgContent}
);
}
);
${componentName}.displayName = '${componentName}';
export default ${componentName};
`;
};

const generateComponentFiles = async (svgComponentMap: SvgComponentMap) => {
const components: string[] = [];

for (const [componentName, svgFile] of Object.entries(svgComponentMap)) {
const componentFilePath = path.resolve(
COMPONENT_DIR,
`${componentName}.tsx`
);

if (existsSync(componentFilePath)) {
components.push(componentName);
continue;
}

const svgFilePath = path.resolve(SVG_DIR, svgFile);
const svgContent = (await fs.readFile(svgFilePath)).toString();

const componentContent = createComponentContent(
componentName,
svgContent,
svgFile
);

await fs.writeFile(componentFilePath, componentContent);
components.push(componentName);
}

return components;
};

const generateExportFile = async (components: string[]) => {
const EXPORT_FILE_PATH = "../wow-icons/src/react/index.ts";
const exportFileContent = components
.map(
(component) =>
`export { default as ${component} } from "./${component}.tsx";`
)
.join("\n");

await fs.writeFile(EXPORT_FILE_PATH, exportFileContent);
};

(async () => {
try {
const svgComponentMap = await generateSvgComponentMap();
await deleteUnusedComponentFiles(svgComponentMap);
const components = await generateComponentFiles(svgComponentMap);
await generateExportFile(components);
} catch (error) {
console.log("Error generating components:", error);
}
})();
2 changes: 1 addition & 1 deletion packages/shared-config/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const baseConfig = {
format: "cjs",
},
],
external: ["react/jsx-runtime"],
external: ["react", "react-dom", "react/jsx-runtime"],
plugins: [
peerDepsExternal(),
resolve({ extensions }),
Expand Down
5 changes: 3 additions & 2 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./utils": "./utils/index.ts"
".": "./src/tokens/index.ts",
"./utils": "./src/utils/index.ts",
"./types": "./src/types/index.ts"
},
"devDependencies": {
"@pandacss/dev": "^0.39.0"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
99 changes: 99 additions & 0 deletions packages/theme/src/types/Color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export type ColorToken =
| "red50"
| "red100"
| "red150"
| "red200"
| "red300"
| "red400"
| "red500"
| "red600"
| "red700"
| "red800"
| "red850"
| "red900"
| "red950"
| "blue50"
| "blue100"
| "blue150"
| "blue200"
| "blue300"
| "blue400"
| "blue500"
| "blue600"
| "blue700"
| "blue800"
| "blue850"
| "blue900"
| "blue950"
| "yellow50"
| "yellow100"
| "yellow150"
| "yellow200"
| "yellow300"
| "yellow400"
| "yellow500"
| "yellow600"
| "yellow700"
| "yellow800"
| "yellow850"
| "yellow900"
| "yellow950"
| "green50"
| "green100"
| "green150"
| "green200"
| "green300"
| "green400"
| "green500"
| "green600"
| "green700"
| "green800"
| "green850"
| "green900"
| "green950"
| "mono50"
| "mono100"
| "mono150"
| "mono200"
| "mono300"
| "mono400"
| "mono500"
| "mono600"
| "mono700"
| "mono800"
| "mono850"
| "mono900"
| "mono950"
| "white"
| "black"
| "whiteOpacity20"
| "whiteOpacity40"
| "whiteOpacity60"
| "whiteOpacity80"
| "blackOpacity20"
| "blackOpacity40"
| "blackOpacity60"
| "blackOpacity80"
| "primary"
| "success"
| "error"
| "backgroundNormal"
| "backgroundAlternative"
| "backgroundDimmer"
| "sub"
| "outline"
| "textBlack"
| "textWhite"
| "darkDisabled"
| "lightDisabled"
| "blueHover"
| "monoHover"
| "elevatedHover"
| "bluePressed"
| "blueBackgroundPressed"
| "monoBackgroundPressed"
| "shadowSmall"
| "shadowMedium"
| "blueShadow"
| "discord"
| "github";
1 change: 1 addition & 0 deletions packages/theme/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Color.ts";
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/theme/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
"allowImportingTsExtensions": true,
"jsx": "react-jsx"
},
"include": ["src", "utils"],
"include": ["src"],
"exclude": ["node_modules"]
}
7 changes: 5 additions & 2 deletions packages/wow-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"types": "./dist/react/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "rm -rf dist && rollup -c --bundleConfigAsCjs && tsc --emitDeclarationOnly"
"build": "pnpm generate:icons && rm -rf dist && rollup -c --bundleConfigAsCjs && tsc --emitDeclarationOnly",
"generate:icons": "tsx ../scripts/generateReactComponentFromSvg.ts && pnpm format && pnpm lint",
"lint": "eslint --fix ./src/react/**/*.tsx",
"format": "prettier --write ./src/react/**/*"
},
"keywords": [],
"author": "gdsc-hongik",
Expand Down
5 changes: 3 additions & 2 deletions packages/wow-icons/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import baseConfig from "../shared-config/rollup.config.js";
import babel from "@rollup/plugin-babel";
import svgr from "@svgr/rollup";
import url from "@rollup/plugin-url";
import typescript from "@rollup/plugin-typescript";

const extensions = [".ts", ".tsx", ".js", ".jsx"];

export default {
...baseConfig,
input: "./src/react/index.ts",
plugins: [
...baseConfig.plugins,
babel({
Expand All @@ -15,6 +16,6 @@ export default {
babelHelpers: "runtime",
}),
url(),
svgr(),
typescript(),
],
};
Loading

0 comments on commit 379fa1b

Please sign in to comment.