@juanmsl/svg-to-react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@juanmsl/svg-to-react",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "private": false,
6
+ "bin": {
7
+ "svg-to-react": "./dist/cjs/index.js"
8
+ },
9
+ "main": "./dist/cjs/index.js",
10
+ "module": "./dist/esm/index.js",
11
+ "types": "./dist/esm/index.d.ts",
12
+ "scripts": {
13
+ "develop": "ts-node ./src/index.ts",
14
+ "build": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
15
+ "publish-package": "yarn build && yarn publish --no-git-tag-version",
16
+ "clean": "rm -rf ./dist"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "xml2js": "^0.6.2"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.11.6",
26
+ "@types/xml2js": "^0.4.14",
27
+ "polpo-tsconfig": "*",
28
+ "ts-node": "^10.9.2",
29
+ "typescript": "^5.5.4"
30
+ }
31
+ }
@@ -0,0 +1,51 @@
1
+ import { Icon } from './icon';
2
+ import { titleCase } from '../helpers';
3
+ import fs from 'fs';
4
+
5
+ interface CategoryEntity {
6
+ addIcon(icon: Icon): void;
7
+ createFile(outputFolder: string): void;
8
+ }
9
+
10
+ export class Category implements CategoryEntity {
11
+ private icons: Array<Icon>;
12
+ readonly name: string;
13
+
14
+ constructor(name: string) {
15
+ this.icons = [];
16
+ this.name = name;
17
+ }
18
+
19
+ public addIcon(icon: Icon) {
20
+ if (icon.isValid) {
21
+ this.icons.push(icon);
22
+ }
23
+ }
24
+
25
+ public async createFile(outputFolder: string) {
26
+ const fileName = `${this.name}.tsx`;
27
+ console.log('');
28
+ console.log(`Category: ${this.name}`);
29
+ console.log(`Icons: ${this.icons.length}`);
30
+ console.log(`File: ${outputFolder}/${fileName}`);
31
+
32
+ await Promise.all(this.icons.map(icon => icon.init()));
33
+
34
+ const types = this.icons.map(icon => `'${icon.name}'`).join(' | ');
35
+ const categoryTitleCase = titleCase(this.name);
36
+ const categoryType = `export type ${categoryTitleCase}IconsT = Record<${types}, IconT>;`;
37
+ const categoryObject = `export const ${categoryTitleCase}Icons: ${categoryTitleCase}IconsT`;
38
+ const iconsRendered = this.icons.map(icon => icon.jsx).join(',\n');
39
+
40
+ const data = `import { IconT } from '.';
41
+
42
+ ${categoryType}
43
+
44
+ ${categoryObject} = {
45
+ ${iconsRendered}
46
+ };
47
+ `;
48
+
49
+ fs.writeFileSync(`${outputFolder}/${fileName}`, data);
50
+ }
51
+ }
@@ -0,0 +1,93 @@
1
+ import fs from 'fs';
2
+ import xml2js from 'xml2js';
3
+ import { camelCase } from '../helpers';
4
+
5
+ interface IconEntity {
6
+ name: string | null;
7
+ category: string | null;
8
+ jsx: string;
9
+ isValid: boolean;
10
+
11
+ init(): void;
12
+ }
13
+
14
+ export class Icon implements IconEntity {
15
+ private readonly _category: string | null;
16
+ private readonly _name: string | null;
17
+ private readonly path: string;
18
+ private viewBox: string = "";
19
+ private svg: string = "";
20
+ constructor(root: string, file: string) {
21
+ const items = file.replace(".svg", "").split(", ");
22
+
23
+ const mappedValues = items.reduce(
24
+ (prev, item) => {
25
+ const [label, value] = item.split("=");
26
+
27
+ return {
28
+ ...prev,
29
+ [label.toLowerCase()]: value,
30
+ };
31
+ },
32
+ {
33
+ name: null,
34
+ category: null,
35
+ },
36
+ );
37
+ this.path = `${root}/${file}`;
38
+ this._name = mappedValues.name;
39
+ this._category = mappedValues.category;
40
+ }
41
+
42
+ public async init() {
43
+ if (this.isValid) {
44
+ return await this.readFile();
45
+ }
46
+
47
+ Promise.reject(new Error("Icon without name or category or both"));
48
+ }
49
+
50
+ private async readFile() {
51
+ console.info(`Transform in progress: [${this._category}] -> ${this._name}`);
52
+ const file = await fs.promises.readFile(this.path, "utf8");
53
+
54
+ const data = await xml2js.parseStringPromise(file, {
55
+ explicitArray: false,
56
+ attrNameProcessors: [name => camelCase(name, "-")],
57
+ attrValueProcessors: [
58
+ (value, name) => (name === "fill" ? "{fill}" : value),
59
+ ],
60
+ });
61
+
62
+ this.viewBox = data.svg.$.viewBox;
63
+
64
+ const builder = new xml2js.Builder({
65
+ headless: true,
66
+ });
67
+
68
+ const icon = builder.buildObject({
69
+ g: {
70
+ ...data.svg,
71
+ $: {},
72
+ },
73
+ });
74
+
75
+ this.svg = icon.split('fill="{fill}"').join("fill={fill}");
76
+ }
77
+
78
+ public get isValid(): boolean {
79
+ return Boolean(this._name && this._category);
80
+ }
81
+
82
+ public get name(): string | null {
83
+ return this._name;
84
+ }
85
+
86
+ public get category(): string | null {
87
+ return this._category;
88
+ }
89
+
90
+ public get jsx(): string {
91
+ return ` '${this.name}': {\n viewBox: '${this.viewBox}',\n svg: (fill) => (\n${this.svg}\n )\n }`;
92
+ }
93
+ }
@@ -0,0 +1,2 @@
1
+ export * from './category';
2
+ export * from './icon';
@@ -0,0 +1 @@
1
+ export * from './text';
@@ -0,0 +1,13 @@
1
+ export const titleCase = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
2
+
3
+ export const camelCase = (text: string, separator = ' ') =>
4
+ text
5
+ .split(separator)
6
+ .map((word, key) => {
7
+ if (key === 0) {
8
+ return word;
9
+ }
10
+
11
+ return titleCase(word);
12
+ })
13
+ .join('');
package/src/index.ts ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+
5
+ import { Category, Icon } from "./core";
6
+ import { titleCase } from "./helpers";
7
+
8
+
9
+ type ConfigFile = {
10
+ rootSVGDirectory: string;
11
+ outputDirectory: string;
12
+ };
13
+
14
+ const configFile = fs.readFileSync('svgconfig.json', 'utf8');
15
+ const config: ConfigFile = JSON.parse(configFile);
16
+
17
+ const icons: Array<Icon> = fs
18
+ .readdirSync(config.rootSVGDirectory)
19
+ .filter(file => file.endsWith('.svg'))
20
+ .map<Icon>(file => new Icon(config.rootSVGDirectory, file));
21
+
22
+ type IconCategories = {
23
+ [index: string]: Category;
24
+ };
25
+
26
+ const categoryFiles = icons.reduce<IconCategories>((categories, icon) => {
27
+ if(icon.category) {
28
+ categories[icon.category] = categories[icon.category] ?? new Category(icon.category);
29
+ categories[icon.category].addIcon(icon);
30
+ }
31
+
32
+ return categories;
33
+ }, {});
34
+
35
+ Object.values(categoryFiles).forEach(async category => {
36
+ await category.createFile(config.outputDirectory);
37
+ });
38
+
39
+ console.log(`Creating file: ${config.outputDirectory}/index.ts`);
40
+
41
+ const categories = Object.keys(categoryFiles).sort();
42
+
43
+ const importCategpries = categories.map(
44
+ category => `import { ${titleCase(category)}Icons, ${titleCase(category)}IconsT } from './${category}';`
45
+ ).join('\n');
46
+
47
+ const iconCollectionT = categories.map(
48
+ category => `${titleCase(category)}IconsT`
49
+ ).join(' & ')
50
+
51
+ const exportIcons = categories.map(
52
+ category => ` ...${titleCase(category)}Icons,`
53
+ ).join('\n')
54
+
55
+ const indexFile = `import React from 'react';
56
+
57
+ ${importCategpries}
58
+
59
+ export type IconT = {
60
+ viewBox: string;
61
+ svg: (fill: string) => React.ReactNode;
62
+ };
63
+
64
+ export type IconCollectionT = ${iconCollectionT};
65
+
66
+ export const Icons: IconCollectionT = {
67
+ ${exportIcons}
68
+ };
69
+ `;
70
+
71
+ fs.writeFileSync(`${config.outputDirectory}/index.ts`, indexFile);
72
+
73
+ console.info("SVG's transformed! :D");
package/svgconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "rootSVGDirectory": "./svg",
3
+ "outputDirectory": "./icons"
4
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "CommonJS",
5
+ "outDir": "./dist/cjs"
6
+ },
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "polpo-tsconfig/nodejs.json",
3
+ "include": ["src"],
4
+ "compilerOptions": {
5
+ "outDir": "./dist/esm",
6
+ },
7
+ "ts-node": {
8
+ "esm": true
9
+ }
10
+ }