Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d37a588

Browse files
feat: generator for react, vue, svelte and angular
1 parent 1e996d8 commit d37a588

File tree

8 files changed

+390
-50
lines changed

8 files changed

+390
-50
lines changed

β€Žmain.tsβ€Ž

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,29 @@
1-
// generate-icons.ts
2-
import *asfs from "fs"
3-
import *aspath from "path"
4-
import { toPascalCase,extractSvgContent,camelizeSvgAttributes,generateComponent} from "./src/shared"
1+
import{generateAngularIcons}from"./src/generators/angular"
2+
import {generateReactIcons} from "./src/generators/react"
3+
import {generateSvelteIcons} from "./src/generators/svelte"
4+
import { generateVueIcons} from "./src/generators/vue"
55

66
const SVG_DIR = "src/icons-svg"
7-
const OUTPUT_DIR = "dist/components/icons"
8-
9-
10-
function main() {
11-
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true })
12-
13-
const withSvgSource = path.resolve("src/shared/hoc/withSvg.tsx")
14-
const withSvgTarget = path.resolve("dist/components/icons/withSvg.tsx")
15-
16-
if (!fs.existsSync(withSvgTarget)) {
17-
fs.copyFileSync(withSvgSource, withSvgTarget)
18-
}
19-
const files = fs.readdirSync(SVG_DIR).filter((f) => f.endsWith(".svg"))
20-
const exports: string[] = []
21-
22-
for (const file of files) {
23-
const baseName = path.basename(file)
24-
const componentName = toPascalCase(baseName)
25-
const svgRaw = fs.readFileSync(path.join(SVG_DIR, file), "utf-8")
26-
const svgInner = camelizeSvgAttributes(extractSvgContent(svgRaw))
27-
28-
const componentCode = generateComponent(componentName, svgInner)
29-
const outPath = path.join(OUTPUT_DIR, `${componentName}.tsx`)
30-
fs.writeFileSync(outPath, componentCode)
31-
exports.push(`export { ${componentName} } from "./${componentName}"`)
32-
}
33-
34-
fs.writeFileSync(path.join(OUTPUT_DIR, "index.ts"), exports.join("\n") + "\n")
35-
console.log("βœ… Icons generated!")
7+
const REACT_ICONS_DIR = "dist/icons/react"
8+
const VUE_ICONS_DIR = "dist/icons/vue"
9+
const SVELTE_ICONS_DIR = "dist/icons/svelte"
10+
const ANGULAR_ICONS_DIR = "dist/icons/angular"
11+
12+
const framework = process.argv[2]
13+
14+
switch (framework) {
15+
case "react":
16+
generateReactIcons(SVG_DIR, REACT_ICONS_DIR)
17+
break
18+
case "vue":
19+
generateVueIcons(SVG_DIR, VUE_ICONS_DIR)
20+
break
21+
case "svelte":
22+
generateSvelteIcons(SVG_DIR, SVELTE_ICONS_DIR)
23+
break
24+
case "angular":
25+
generateAngularIcons(SVG_DIR, ANGULAR_ICONS_DIR)
26+
break
27+
default:
28+
console.log("❌ Please specify a framework: react | vue | svelte | angular")
3629
}
37-
38-
main()

β€Žpackage.jsonβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
{
2-
"name": "svg2tsx",
3-
"version": "1.0.0",
2+
"name": "svg-to-react-vue-svelte-angular",
3+
"version": "0.0.1",
44
"main": "main.ts",
55
"scripts": {
6-
"convert": "ts-node main.ts"
6+
"generate:react": "ts-node main.ts react",
7+
"generate:vue": "ts-node main.ts vue",
8+
"generate:svelte": "ts-node main.ts svelte",
9+
"generate:angular": "ts-node main.ts angular",
10+
"generate:all": "npm run generate:react && npm run generate:vue && npm run generate:svelte && npm run generate:angular"
711
},
8-
"keywords": [],
9-
"author": "",
12+
"keywords": [
13+
"svg",
14+
"react",
15+
"vue",
16+
"svelte",
17+
"angular",
18+
"icons",
19+
"generator",
20+
"convert",
21+
"ts",
22+
"typescript"
23+
],
24+
"author": "Iqboljon Hasan",
1025
"license": "ISC",
11-
"description": "",
26+
"description": "A tool for converting SVG icons to React, Vue, Svelte, and Angular components",
1227
"devDependencies": {
1328
"ts-node": "^10.9.2",
1429
"typescript": "^5.8.3"

β€Žsrc/generators/angular.tsβ€Ž

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
import { extractSvgContent, camelizeSvgAttributes, toPascalCase } from "../shared"
4+
5+
export function generateAngularIcons(inputDir: string, destDir: string) {
6+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
7+
8+
const files = fs.readdirSync(inputDir).filter(f => f.endsWith(".svg"))
9+
10+
for (const file of files) {
11+
const baseName = file.replace(".svg", "")
12+
const className = toPascalCase(baseName)
13+
const selector = `icon-${baseName.toLowerCase()}`
14+
const svgInner = camelizeSvgAttributes(extractSvgContent(
15+
fs.readFileSync(path.join(inputDir, file), "utf-8")
16+
))
17+
18+
const componentDir = path.join(destDir, baseName)
19+
if (!fs.existsSync(componentDir)) fs.mkdirSync(componentDir)
20+
21+
// component.ts
22+
const tsCode = `import { Component, Input } from '@angular/core';
23+
24+
@Component({
25+
selector: '${selector}',
26+
templateUrl: './${baseName}.component.html',
27+
standalone: true
28+
})
29+
export class ${className}Component {
30+
@Input() size: number = 24
31+
@Input() color: string = 'currentColor'
32+
@Input() strokeWidth: number = 1.5
33+
@Input() fill: string = 'none'
34+
@Input() title?: string
35+
@Input() ariaHidden: boolean = false
36+
@Input() role: string = 'img'
37+
}
38+
`
39+
40+
// component.html
41+
const html = `<svg
42+
xmlns="http://www.w3.org/2000/svg"
43+
[attr.width]="size"
44+
[attr.height]="size"
45+
[attr.stroke]="color"
46+
[attr.stroke-width]="strokeWidth"
47+
[attr.fill]="fill"
48+
[attr.role]="ariaHidden ? 'presentation' : role"
49+
[attr.aria-hidden]="ariaHidden"
50+
viewBox="0 0 24 24"
51+
>
52+
<title *ngIf="title">{{ title }}</title>
53+
${svgInner}
54+
</svg>
55+
`
56+
57+
fs.writeFileSync(path.join(componentDir, `${baseName}.component.ts`), tsCode)
58+
fs.writeFileSync(path.join(componentDir, `${baseName}.component.html`), html)
59+
}
60+
61+
console.log("βœ… Angular components generated!")
62+
}

β€Žsrc/generators/react.tsβ€Ž

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
import { toPascalCase, extractSvgContent, camelizeSvgAttributes, generateComponent } from "../../src/shared"
4+
5+
export function generateReactIcons(inputDir: string, destDir: string) {
6+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
7+
8+
const withSvgSource = path.resolve("src/shared/hoc/withSvg.tsx")
9+
const withSvgTarget = path.resolve(destDir, "withSvg.tsx")
10+
11+
if (!fs.existsSync(withSvgTarget)) {
12+
fs.copyFileSync(withSvgSource, withSvgTarget)
13+
}
14+
const files = fs.readdirSync(inputDir).filter((f) => f.endsWith(".svg"))
15+
const exports: string[] = []
16+
17+
for (const file of files) {
18+
const baseName = path.basename(file)
19+
const componentName = toPascalCase(baseName)
20+
const svgRaw = fs.readFileSync(path.join(inputDir, file), "utf-8")
21+
const svgInner = camelizeSvgAttributes(extractSvgContent(svgRaw))
22+
23+
const componentCode = generateComponent(componentName, svgInner)
24+
const outPath = path.join(destDir, `${componentName}.tsx`)
25+
fs.writeFileSync(outPath, componentCode)
26+
exports.push(`export { ${componentName} } from "./${componentName}"`)
27+
}
28+
29+
fs.writeFileSync(path.join(destDir, "index.ts"), exports.join("\n") + "\n")
30+
console.log("βœ… React Icons generated!")
31+
}

β€Žsrc/generators/svelte.tsβ€Ž

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
import { toPascalCase, extractSvgContent, camelizeSvgAttributes, generateComponent } from "../shared"
4+
5+
export function generateSvelteIcons(inputDir: string, destDir: string) {
6+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
7+
8+
const files = fs.readdirSync(inputDir).filter(f => f.endsWith(".svg"))
9+
10+
for (const file of files) {
11+
const name = toPascalCase(file.replace(".svg", ""))
12+
const raw = fs.readFileSync(path.join(inputDir, file), "utf-8")
13+
const svgInner = camelizeSvgAttributes(extractSvgContent(raw))
14+
15+
const component = `<script lang="ts">
16+
export let size: number = 24
17+
export let color: string = "currentColor"
18+
export let strokeWidth: number = 1.5
19+
export let fill: string = "none"
20+
export let title: string | undefined
21+
export let ariaHidden: boolean = false
22+
export let role: string = ariaHidden ? "presentation" : "img"
23+
</script>
24+
25+
<svg
26+
xmlns="http://www.w3.org/2000/svg"
27+
width={size}
28+
height={size}
29+
fill={fill}
30+
stroke={color}
31+
stroke-width={strokeWidth}
32+
role={role}
33+
aria-hidden={ariaHidden}
34+
viewBox="0 0 24 24"
35+
>
36+
{#if title}<title>{title}</title>{/if}
37+
${svgInner}
38+
</svg>
39+
`
40+
41+
const outPath = path.join(destDir, `${name}.svelte`)
42+
fs.writeFileSync(outPath, component)
43+
}
44+
45+
console.log("βœ… Svelte icons generated!")
46+
}

β€Žsrc/generators/vue.tsβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
import { toPascalCase, extractSvgContent, camelizeSvgAttributes, generateComponent } from "../shared"
4+
5+
export function generateVueIcons(inputDir: string, destDir: string) {
6+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
7+
8+
const files = fs.readdirSync(inputDir).filter(f => f.endsWith(".svg"))
9+
10+
for (const file of files) {
11+
const name = toPascalCase(file.replace(".svg", ""))
12+
const raw = fs.readFileSync(path.join(inputDir, file), "utf-8")
13+
const svgInner = camelizeSvgAttributes(extractSvgContent(raw))
14+
15+
const component = `<script setup lang="ts">
16+
defineProps<{
17+
size?: number
18+
color?: string
19+
strokeWidth?: number
20+
fill?: string
21+
title?: string
22+
ariaHidden?: boolean
23+
role?: string
24+
}>()
25+
</script>
26+
27+
<template>
28+
<svg
29+
xmlns="http://www.w3.org/2000/svg"
30+
:width="size || 24"
31+
:height="size || 24"
32+
:stroke="color || 'currentColor'"
33+
:stroke-width="strokeWidth || 1.5"
34+
:fill="fill || 'none'"
35+
:role="role || (ariaHidden ? 'presentation' : 'img')"
36+
:aria-hidden="ariaHidden ?? false"
37+
viewBox="0 0 24 24"
38+
>
39+
<title v-if="title">{{ title }}</title>
40+
${svgInner}
41+
</svg>
42+
</template>
43+
`
44+
45+
const outPath = path.join(destDir, `${name}.vue`)
46+
fs.writeFileSync(outPath, component)
47+
}
48+
49+
console.log("βœ… Vue components generated!")
50+
}
51+

β€Žsrc/shared/hoc/withSvg.tsxβ€Ž

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,54 @@
1-
// src/components/icons/withSvg.tsx
1+
import type { SVGProps } from "react"
2+
import React from "react"
23

3-
import type { SVGProps, FC } from "react"
4-
5-
export interface IconProps extends SVGProps<SVGSVGElement> {
4+
type BaseProps = SVGProps<SVGSVGElement> & {
65
size?: number
76
color?: string
7+
fill?: string
8+
}
9+
10+
/**
11+
* Icon is visible to screen readers (accessibility required)
12+
* πŸ‘‰ Must provide a `title`
13+
*/
14+
type AccessibleProps = {
15+
"aria-hidden"?: false
16+
role?: "img"
17+
title: string
818
}
919

10-
export function withSvg(Path: JSX.Element): FC<IconProps> {
11-
return ({ size = 20, color, viewBox = "0 0 24 24", ...props }) => (
20+
/**
21+
* Icon is hidden from screen readers
22+
* πŸ‘‰ Should not have `title`
23+
*/
24+
type DecorativeProps = {
25+
"aria-hidden"?: true
26+
role?: "presentation"
27+
title?: undefined
28+
}
29+
30+
/**
31+
* IconProps: Accept either accessible or decorative
32+
*/
33+
export type IconProps = BaseProps & (AccessibleProps | DecorativeProps)
34+
35+
export function withSvg(Path: JSX.Element) {
36+
return ({
37+
size = 20,
38+
color,
39+
fill = "none",
40+
title,
41+
...props
42+
}: IconProps) => (
1243
<svg
1344
xmlns="http://www.w3.org/2000/svg"
14-
viewBox={viewBox}
1545
width={size}
1646
height={size}
17-
fill="none"
18-
color={color}
47+
fill={fill}
48+
stroke={color}
1949
{...props}
2050
>
51+
{title && <title>{title}</title>}
2152
{Path}
2253
</svg>
2354
)

0 commit comments

Comments
(0)

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /