Skip to content

Commit

Permalink
@seed-design/icon (#162)
Browse files Browse the repository at this point in the history
* chore: test

* chore: clean

* chore: yarn

* feat: seed-design/icon

* chore: remove gitattributes

* chore: seed-design versioning

* chore: bump

* chore: unlink example folder

* Create cyan-bugs-attend.md
  • Loading branch information
junghyeonsu authored Feb 8, 2023
1 parent abd9a62 commit 8dbbbc8
Show file tree
Hide file tree
Showing 52 changed files with 932 additions and 356 deletions.
3 changes: 1 addition & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["web"]
"updateInternalDependencies": "patch"
}
5 changes: 5 additions & 0 deletions .changeset/cyan-bugs-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@seed-design/icon": patch
---

init project
3 changes: 2 additions & 1 deletion docs/.vscode/settings.json → .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"editor.codeActionsOnSave": {
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true,
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8 changes: 4 additions & 4 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
],
"author": "junghyeonsu",
"scripts": {
"dev": "gatsby develop",
"dev:storybook": "storybook dev -p 6006",
"develop": "gatsby develop",
"build": "GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --verbose --log-pages",
"build:storybook": "storybook build",
"storybook": "storybook dev -p 6006",
"clean": "gatsby clean",
"dev": "gatsby develop",
"dev:storybook": "storybook dev -p 6006",
"develop": "gatsby develop",
"format": "eslint --fix . --ext .ts,.tsx,.json",
"lint": "eslint . --ext .ts,.tsx,.json",
"serve": "gatsby serve",
"storybook": "storybook dev -p 6006",
"typecheck": "tsc --noEmit",
"validate:meta-json": "node scripts/validate-meta-json.js"
},
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"workspaces": [
"packages/*",
"packages/machines/*",
"examples/*",
"docs"
],
"scripts": {
Expand Down
15 changes: 15 additions & 0 deletions packages/icon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.DS_Store
*.log
*.tsbuildinfo
*.zip
*.env

node_modules/
/lib/
/bin/
/dist/

/bin.mjs
/bin.mjs.LEGAL.txt
/bin.min.mjs
/bin.min.mjs.LEGAL.txt
1 change: 1 addition & 0 deletions packages/icon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @seed-design/icon
18 changes: 18 additions & 0 deletions packages/icon/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import esbuild from 'esbuild';
import pkg from './package.json' assert { type: 'json' };

esbuild.build({
entryPoints: ['./src/index.ts'],
outfile: './bin/index.mjs',
bundle: true,
write: true,
treeShaking: true,
sourcemap: false,
minify: true,
format: 'esm',
platform: 'node',
target: ['node16'],
external: [
...Object.keys(pkg.dependencies),
],
});
40 changes: 40 additions & 0 deletions packages/icon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@seed-design/icon",
"version": "0.0.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/daangn/seed-design.git",
"directory": "packages/icon"
},
"bin": {
"seed-icon": "./bin/index.mjs"
},
"scripts": {
"prepack": "yarn build",
"build": "yarn build:seed-icon-cli",
"build:seed-icon-cli": "node build.mjs",
"test": "uvu -r tsm tests"
},
"publishConfig": {
"access": "public"
},
"files": [
"src"
],
"devDependencies": {
"@types/js-yaml": "^4.0.5",
"esbuild": "^0.17.6",
"tsm": "^2.3.0",
"typescript": "^4.5.2",
"uvu": "^0.5.6"
},
"dependencies": {
"@karrotmarket/karrot-ui-icon": "0.0.0-20230208.3",
"app-root-path": "^3.1.0",
"chalk": "^5.2.0",
"commander": "^10.0.0",
"js-yaml": "^4.1.0",
"string-dedent": "^3.0.1"
}
}
84 changes: 84 additions & 0 deletions packages/icon/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env node
import appRoot from 'app-root-path';
import chalk from 'chalk';
import { Command } from 'commander';
import fs from 'fs';
import yaml from 'js-yaml';
import path from 'path';
import pkg from '../package.json' assert { type: 'json' };

import generateComponent from './templates/component.js';
import generateConfig from './templates/config';
import generateSprite from './templates/sprite.js';
import { IconConfig } from './types';
import { validateIcons } from './validates/icons';

const program = new Command();
const configPath = path.resolve(appRoot.path, 'icon.config.yml');
const version = pkg.version;

const initCommand = new Command('init')
.alias('-i')
.description('Initialize icon.config.yml')
.action(() => {
try {
const config = generateConfig();
fs.writeFileSync(configPath, config);
console.log(chalk.green('icon.config.yml generated!'));
} catch (e) {
console.error(e);
}
});

const generateCommand = new Command('generate')
.alias('gen')
.description('Generate SVG sprite and SeedIcon component')
.action(() => {
try {
const fileContents = yaml.load(fs.readFileSync(configPath, 'utf8')) as IconConfig;

const icons = fileContents.icons;

const spriteFileName = fileContents.spriteFileName || 'sprite';
const spriteOutputPath = fileContents.spriteOutputPath || 'assets';

const componentFileName = fileContents.componentFileName || 'SeedIcon';
const componentOutputPath = fileContents.componentOutputPath || 'src';

validateIcons(icons);

const seedIconComponent = generateComponent({ componentOutputPath, spriteOutputPath, spriteFileName, version, icons });
const spriteSvg = generateSprite({ icons });

const spriteOutputDir = path.resolve(spriteOutputPath);
const iconComponentOutputDir = path.resolve(componentOutputPath);

if (!fs.existsSync(spriteOutputDir)) {
fs.mkdirSync(spriteOutputDir, { recursive: true });
}

if (!fs.existsSync(iconComponentOutputDir)) {
fs.mkdirSync(iconComponentOutputDir, { recursive: true });
}

fs.writeFileSync(path.resolve(spriteOutputPath, `${spriteFileName}.svg`), spriteSvg);
fs.writeFileSync(path.resolve(componentOutputPath, `${componentFileName}.tsx`), seedIconComponent);

console.log(chalk.green(`SVG sprite generate complete at ${spriteOutputPath}/${spriteFileName}.svg!`));
console.log(chalk.green(`SeedIcon component generate complete at ${componentOutputPath}/${componentFileName}.tsx!`));
} catch (error) {
if (error instanceof Error) {
if (error.message) {
console.error(error.message);
}
}
process.exit(1);
}
});

program
.version(version, '-v, --version')
.description('Generate SVG sprite and SeedIcon component')
.addCommand(initCommand)
.addCommand(generateCommand)
.parse(process.argv);
52 changes: 52 additions & 0 deletions packages/icon/src/templates/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { join } from 'path';
import dedent from 'string-dedent';
import { generateRelativePath } from '../utils/path';

import type { IconName } from '../types';

interface ComponentInterface {
componentOutputPath: string;
spriteOutputPath: string;
spriteFileName: string;
version: string;
icons: IconName[];
}

export default function generate({ componentOutputPath, spriteOutputPath, spriteFileName, version, icons }: ComponentInterface) {
const relativeSpritePath = generateRelativePath(componentOutputPath, spriteOutputPath);
const relativeSpriteUrl = join(relativeSpritePath, `${spriteFileName}.svg`);

return dedent`
import { forwardRef, type ForwardRefRenderFunction } from "react";
import spriteUrl from "${relativeSpriteUrl}";
export interface SeedIconProps {
name: IconName;
className?: string;
};
const SeedIcon: ForwardRefRenderFunction<HTMLSpanElement, SeedIconProps> = (
{ name, className },
ref,
) => {
return (
<span
ref={ref}
className={className}
data-seed-icon={name}
data-seed-icon-version="${version}"
>
<svg viewBox="0 0 24 24">
<use href={\`\${spriteUrl}#\${name}\`} />
</svg>
</span>
);
};
export default forwardRef(SeedIcon);
type IconName = (
| ${icons.map((icon) => `"${icon}"`).join('\n | ')}
);\n
`;
}
23 changes: 23 additions & 0 deletions packages/icon/src/templates/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import dedent from "string-dedent";

export default function generate() {
return dedent`
# 컴포넌트가 저장될 경로입니다. 프로젝트 루트 기준입니다.
componentOutputPath: src
# 컴포넌트의 이름입니다.
componentFileName: SeedIcon
# svg 파일이 저장될 경로입니다. 프로젝트 루트 기준입니다.
spriteOutputPath: assets
# svg 파일의 이름입니다.
spriteFileName: sprite
# https://www.figma.com/file/58VvezaS8z1FsIOr9KFHKW/icon?node-id=0%3A1
# 위 피그마 파일에서 사용되는 아이콘 이름을 추가해주세요.
icons:
- icon_add_circle_fill
- icon_add_circle_regular
- icon_add_circle_thin
`;
};
16 changes: 16 additions & 0 deletions packages/icon/src/templates/sprite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import dedent from "string-dedent";
import IconData from "@karrotmarket/karrot-ui-icon/lib/IconData.js";

import type { IconName } from "../types";

export default function generate({ icons }: { icons: IconName[] }) {
return dedent`
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
${icons?.map((id) => {
const icon = IconData[id];
return icon
.replace("<svg", ` <symbol id="${id}"`)
.replace(/<path/g, " <path")
.replace("</svg>", " </symbol>")}).join("")}</svg>\n
`;
};
12 changes: 12 additions & 0 deletions packages/icon/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type IconData from "@karrotmarket/karrot-ui-icon/lib/IconData";

export type IconName = keyof typeof IconData;
export interface IconConfig {
componentOutputPath: string;
componentFileName: string;

spriteOutputPath: string;
spriteFileName: string;

icons: IconName[];
}
6 changes: 6 additions & 0 deletions packages/icon/src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { relative } from "path";

export const generateRelativePath = (from: string, to: string) => {
const relatived = relative(from, to);
return relatived.startsWith("..") ? relatived : `./${relatived}`;
}
16 changes: 16 additions & 0 deletions packages/icon/src/validates/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import IconData from "@karrotmarket/karrot-ui-icon/lib/IconData.js";

import type { IconName } from "../types";

export const validateIcons = (icons: IconName[]) => {
if (!icons) {
throw new Error("icons is not defined in icon.config.yml");
}

for (const icon of icons) {
const iconName = icon as IconName;
if (!IconData[iconName]) {
throw new Error(`icon ${icon} is not exist`);
}
}
}
46 changes: 46 additions & 0 deletions packages/icon/tests/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test } from 'uvu';
import * as assert from 'uvu/assert';

import { generateRelativePath } from '../src/utils/path';

const tests = [
{
componentPath: "src",
spritePath: "assets",
expect: "../assets"
},
{
componentPath: "src",
spritePath: "src/assets",
expect: "./assets"
},
{
componentPath: "src/components",
spritePath: "src/assets",
expect: "../assets"
},
{
componentPath: "src/components",
spritePath: "src/components/assets",
expect: "./assets"
},
{
componentPath: "src/components",
spritePath: "",
expect: "../.."
},
{
componentPath: "",
spritePath: "",
expect: "./"
}
]

tests.forEach(({ componentPath, spritePath, expect }) => {
test(`generateRelativePath ${componentPath}, ${spritePath}`, () => {
const relativePath = generateRelativePath(componentPath, spritePath);
assert.is(relativePath, expect);
});
});

test.run();
Loading

0 comments on commit 8dbbbc8

Please sign in to comment.