@mrzr/react-svg-icons 0.0.0-stage → 0.1.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/build.d.ts +17 -0
- package/dist/build.js +137 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +76 -0
- package/dist/color.d.ts +22 -0
- package/dist/color.js +132 -0
- package/dist/component.d.ts +5 -0
- package/dist/component.js +162 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +82 -0
- package/dist/format.d.ts +7 -0
- package/dist/format.js +42 -0
- package/dist/header.d.ts +4 -0
- package/dist/header.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/jsx.d.ts +13 -0
- package/dist/jsx.js +132 -0
- package/dist/names.d.ts +4 -0
- package/dist/names.js +52 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.js +119 -0
- package/package.json +75 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- **`Icon` keeps the stroke width of the SVG.** It no longer passes `strokeWidth`, so an icon loaded by name is drawn with the width it has when imported directly (`logout` at 1.5, not 2). `defaultStrokeWidth` now only applies to an SVG that has no `stroke-width`. If you relied on `Icon` forcing the width, pass `strokeWidth` to it.
|
|
6
|
+
- **The full package name** is in the output: the banner of the generated files and the start of every message say `@mrzr/react-svg-icons`, and the docs write `npx @mrzr/react-svg-icons`. A bare `npx react-svg-icons` would run a different package of that name from npm. Files with the old banner are still recognised as generated.
|
|
7
|
+
- The README and the wiki say what the tool does, and no longer mention Figma or other frameworks.
|
|
8
|
+
- A test no longer depends on whether `tsc` prints colours, which failed `npm publish` in a terminal that forces them.
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
First release.
|
|
13
|
+
|
|
14
|
+
- Turns a folder of SVGs into React components: SVGO-optimised, typed, with `size`, `color`, `stroke`, `fill`, `strokeWidth`, `opacity`, `title`, a forwarded `ref`, and `colors`, `opacities` and `strokeWidths` lists with one entry per colour of the drawing.
|
|
15
|
+
- Writes a typed registry (`variant.name`, one lazy import per icon) and a lazy `Icon` component. A nested folder is part of the name, and two icons with one name stop the run.
|
|
16
|
+
- The config is data, never a script: YAML (`react-svg-icons.config.yaml`, `.yml`), JSON (`react-svg-icons.config.json`) or an rc file (`.react-svg-iconsrc` and its `.yaml`, `.yml` and `.json` variants). Options: `input`, `outDir`, `removeWords`, `defaultSize`, `defaultStrokeWidth`, `format`. An unknown option is an error.
|
|
17
|
+
- Formats the output with the project's own prettier and config (`format: prettier`, or `false` for raw text).
|
|
18
|
+
- `react-svg-icons --check` reports `missing`, `changed` and `leftover` files and exits with 1; line endings are ignored.
|
|
19
|
+
- Never wipes the output folder: only a generated file that a run no longer produces is deleted, with the folder it leaves empty.
|
|
20
|
+
- Folders with no SVG are ignored, and an SVG that would overwrite a generated file (`icon`, `registry`, `index`) is an error.
|
|
21
|
+
- `generate`, `write` and `compare` for use from a script.
|
|
22
|
+
- Requires Node.js 20 or newer.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mrzr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mrzr/react-svg-icons
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Turn a folder of SVG files into typed React components (TSX). Every icon gets props for its size and colours, so you can restyle it without editing the SVG.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install --save-dev @mrzr/react-svg-icons
|
|
7
|
+
npx @mrzr/react-svg-icons
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
Put your SVG files in a folder (subfolders are variants) and add `react-svg-icons.config.yaml` next to your `package.json`:
|
|
15
|
+
|
|
16
|
+
<!-- verbatim: examples/basic/react-svg-icons.config.yaml -->
|
|
17
|
+
```yaml
|
|
18
|
+
input: icons
|
|
19
|
+
outDir: generated
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @mrzr/react-svg-icons # writes the components into generated/
|
|
24
|
+
npx @mrzr/react-svg-icons --check # writes nothing; exit code 1 when they are out of date
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use an icon by name, loaded on demand, or import it directly:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Icon } from "./generated";
|
|
31
|
+
import { Logout } from "./generated/bold";
|
|
32
|
+
|
|
33
|
+
<Icon name="bold.logout" size={20} />
|
|
34
|
+
<Logout size={20} color="crimson" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The config can also be `.yml`, `.json` or an rc file (`.react-svg-iconsrc`).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## What you get
|
|
42
|
+
|
|
43
|
+
- **A typed component per SVG**, optimised with SVGO, with a forwarded `ref`
|
|
44
|
+
- **Props to customise it**: `size`, `color`, `stroke`, `fill`, `strokeWidth`, `opacity`, `title`, and `colors`, `opacities`, `strokeWidths` lists that set the colours of the drawing one by one
|
|
45
|
+
- **A typed `name`** for `Icon`: `name="bold.logut"` is a type error, and each icon loads as its own chunk
|
|
46
|
+
- **Output formatted by your own prettier**, so it matches what your formatting would leave
|
|
47
|
+
- **`--check`** for CI, and cleanup of generated files whose SVG you removed. A file the tool did not write is never touched
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
Everything is in the **[Wiki](https://github.com/mohammadreza-zr/react-svg-icons/wiki)**:
|
|
54
|
+
|
|
55
|
+
- **Start:** [Installation](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Installation) · [Quick start](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Quick-Start)
|
|
56
|
+
- **Guides:** [Input format](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Input-Format) · [Naming](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Naming) · [Components](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Components) · [The Icon component](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Icon-Component)
|
|
57
|
+
- **Reference:** [Configuration](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Configuration) · [CLI](https://github.com/mohammadreza-zr/react-svg-icons/wiki/CLI) · [API](https://github.com/mohammadreza-zr/react-svg-icons/wiki/API-Reference)
|
|
58
|
+
- **Help:** [Troubleshooting](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Troubleshooting) · [FAQ](https://github.com/mohammadreza-zr/react-svg-icons/wiki/FAQ) · [Contributing](https://github.com/mohammadreza-zr/react-svg-icons/wiki/Contributing)
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
Node.js 20 or newer. The generated code needs React (tested with 19) and, in TypeScript, `"moduleResolution": "bundler"`, because its imports have no file extension.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactSvgIconsConfig } from "./config.js";
|
|
2
|
+
export interface OutputFile {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Output {
|
|
7
|
+
outDir: string;
|
|
8
|
+
files: OutputFile[];
|
|
9
|
+
}
|
|
10
|
+
export interface Difference {
|
|
11
|
+
path: string;
|
|
12
|
+
reason: "missing" | "changed" | "leftover";
|
|
13
|
+
}
|
|
14
|
+
export declare function generate(input: ReactSvgIconsConfig, cwd?: string): Promise<Output>;
|
|
15
|
+
export declare function compare(output: Output): Difference[];
|
|
16
|
+
/** Writes the files, and removes only generated files that are no longer produced. */
|
|
17
|
+
export declare function write(output: Output): void;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { formatFiles } from "./format.js";
|
|
4
|
+
import { generateComponentCode } from "./component.js";
|
|
5
|
+
import { resolveConfig } from "./config.js";
|
|
6
|
+
import { HEADER, MARKER_PATTERN } from "./header.js";
|
|
7
|
+
import { cleanIconName, toCamelCase, toKebabCase } from "./names.js";
|
|
8
|
+
import { iconComponentCode, registryCode } from "./registry.js";
|
|
9
|
+
function buildTree(dir, basePath = []) {
|
|
10
|
+
const node = { name: path.basename(dir), path: basePath, files: [], children: [] };
|
|
11
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const fullPath = path.join(dir, entry.name);
|
|
14
|
+
if (entry.isDirectory())
|
|
15
|
+
node.children.push(buildTree(fullPath, [...basePath, entry.name]));
|
|
16
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".svg"))
|
|
17
|
+
node.files.push({ name: entry.name, fullPath });
|
|
18
|
+
}
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
const hasIcons = (node) => node.files.length > 0 || node.children.some(hasIcons);
|
|
22
|
+
function walkTree(node, isRoot, outDir, config, out) {
|
|
23
|
+
const currentDir = isRoot ? outDir : path.join(outDir, ...node.path);
|
|
24
|
+
const generated = [];
|
|
25
|
+
const used = new Set();
|
|
26
|
+
const allFiles = [];
|
|
27
|
+
for (const file of node.files) {
|
|
28
|
+
let componentName = cleanIconName(file.name, node.name, config.removeWords);
|
|
29
|
+
const wanted = componentName;
|
|
30
|
+
let counter = 2;
|
|
31
|
+
while (used.has(componentName))
|
|
32
|
+
componentName = `${wanted}${counter++}`;
|
|
33
|
+
used.add(componentName);
|
|
34
|
+
const fileName = `${toKebabCase(componentName)}.tsx`;
|
|
35
|
+
const stem = fileName.replace(".tsx", "");
|
|
36
|
+
if (stem === "index" || (isRoot && (stem === "icon" || stem === "registry"))) {
|
|
37
|
+
throw new Error(`@mrzr/react-svg-icons: ${file.fullPath} would be written to ${fileName}, a name the generated ${stem} file already uses; rename the SVG`);
|
|
38
|
+
}
|
|
39
|
+
const code = generateComponentCode(readFileSync(file.fullPath, "utf8"), componentName, file.fullPath, config);
|
|
40
|
+
out.push({ path: path.join(currentDir, fileName), content: code });
|
|
41
|
+
generated.push({ componentName, fileName });
|
|
42
|
+
allFiles.push({ componentName, fileName, folderName: node.name, path: node.path });
|
|
43
|
+
}
|
|
44
|
+
const children = [];
|
|
45
|
+
for (const child of node.children.filter(hasIcons)) {
|
|
46
|
+
const result = walkTree(child, false, outDir, config, out);
|
|
47
|
+
children.push(result);
|
|
48
|
+
allFiles.push(...result.allFiles);
|
|
49
|
+
}
|
|
50
|
+
let index = HEADER;
|
|
51
|
+
if (isRoot) {
|
|
52
|
+
// No root-level namespace export on purpose: a static `export * from` of a
|
|
53
|
+
// variant would pull every icon of it into one chunk and defeat the lazy
|
|
54
|
+
// imports of the registry.
|
|
55
|
+
out.push({ path: path.join(currentDir, "registry.ts"), content: registryCode(allFiles) });
|
|
56
|
+
out.push({ path: path.join(currentDir, "icon.tsx"), content: iconComponentCode(config.defaultSize) });
|
|
57
|
+
index += `export * from "./registry";\nexport * from "./icon";\n`;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
for (const file of generated) {
|
|
61
|
+
const base = file.fileName.replace(".tsx", "");
|
|
62
|
+
index += `export { ${file.componentName} } from "./${base}";\nexport type { ${file.componentName}Props } from "./${base}";\n`;
|
|
63
|
+
}
|
|
64
|
+
for (const child of children)
|
|
65
|
+
index += `export * as ${child.camelName} from "./${child.name}";\n`;
|
|
66
|
+
}
|
|
67
|
+
out.push({ path: path.join(currentDir, "index.ts"), content: index });
|
|
68
|
+
return { name: node.name, camelName: toCamelCase(node.name), allFiles };
|
|
69
|
+
}
|
|
70
|
+
export async function generate(input, cwd = process.cwd()) {
|
|
71
|
+
const config = resolveConfig(input);
|
|
72
|
+
const inputDir = path.resolve(cwd, config.input);
|
|
73
|
+
const outDir = path.resolve(cwd, config.outDir);
|
|
74
|
+
if (!existsSync(inputDir))
|
|
75
|
+
throw new Error(`@mrzr/react-svg-icons: input folder not found: ${inputDir}`);
|
|
76
|
+
if (outDir === inputDir || inputDir.startsWith(outDir + path.sep)) {
|
|
77
|
+
throw new Error(`@mrzr/react-svg-icons: outDir (${outDir}) must not hold the input folder (${inputDir})`);
|
|
78
|
+
}
|
|
79
|
+
const tree = buildTree(inputDir);
|
|
80
|
+
const files = [];
|
|
81
|
+
walkTree(tree, true, outDir, config, files);
|
|
82
|
+
const iconCount = files.filter((f) => f.path.endsWith(".tsx") && !f.path.endsWith(`${path.sep}icon.tsx`)).length;
|
|
83
|
+
if (iconCount === 0)
|
|
84
|
+
throw new Error(`@mrzr/react-svg-icons: no .svg files in ${inputDir}`);
|
|
85
|
+
return { outDir, files: await formatFiles(files, cwd, config.format) };
|
|
86
|
+
}
|
|
87
|
+
const lf = (text) => text.replace(/\r\n/g, "\n");
|
|
88
|
+
function existingGenerated(dir) {
|
|
89
|
+
if (!existsSync(dir))
|
|
90
|
+
return [];
|
|
91
|
+
const found = [];
|
|
92
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
const full = path.join(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory())
|
|
95
|
+
found.push(...existingGenerated(full));
|
|
96
|
+
else if (/\.tsx?$/.test(entry.name) && MARKER_PATTERN.test(readFileSync(full, "utf8").slice(0, 200)))
|
|
97
|
+
found.push(full);
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
export function compare(output) {
|
|
102
|
+
const wanted = new Set(output.files.map((f) => f.path));
|
|
103
|
+
const differences = [];
|
|
104
|
+
for (const file of output.files) {
|
|
105
|
+
if (!existsSync(file.path))
|
|
106
|
+
differences.push({ path: file.path, reason: "missing" });
|
|
107
|
+
else if (lf(readFileSync(file.path, "utf8")) !== lf(file.content))
|
|
108
|
+
differences.push({ path: file.path, reason: "changed" });
|
|
109
|
+
}
|
|
110
|
+
for (const file of existingGenerated(output.outDir)) {
|
|
111
|
+
if (!wanted.has(file))
|
|
112
|
+
differences.push({ path: file, reason: "leftover" });
|
|
113
|
+
}
|
|
114
|
+
return differences;
|
|
115
|
+
}
|
|
116
|
+
function removeEmptyDirs(dir, keep) {
|
|
117
|
+
if (!existsSync(dir))
|
|
118
|
+
return;
|
|
119
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
120
|
+
if (entry.isDirectory())
|
|
121
|
+
removeEmptyDirs(path.join(dir, entry.name), keep);
|
|
122
|
+
}
|
|
123
|
+
if (dir !== keep && readdirSync(dir).length === 0)
|
|
124
|
+
rmdirSync(dir);
|
|
125
|
+
}
|
|
126
|
+
/** Writes the files, and removes only generated files that are no longer produced. */
|
|
127
|
+
export function write(output) {
|
|
128
|
+
for (const file of output.files) {
|
|
129
|
+
mkdirSync(path.dirname(file.path), { recursive: true });
|
|
130
|
+
writeFileSync(file.path, file.content);
|
|
131
|
+
}
|
|
132
|
+
for (const difference of compare(output)) {
|
|
133
|
+
if (difference.reason === "leftover")
|
|
134
|
+
unlinkSync(difference.path);
|
|
135
|
+
}
|
|
136
|
+
removeEmptyDirs(output.outDir, output.outDir);
|
|
137
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { compare, generate, write } from "./build.js";
|
|
5
|
+
import { findConfig, loadConfig } from "./config.js";
|
|
6
|
+
const HELP = `@mrzr/react-svg-icons — SVG icons to typed React components
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
npx @mrzr/react-svg-icons [build] [--config <file>] [--check]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--config <file> Config file (default: react-svg-icons.config.yaml|yml|json or .react-svg-iconsrc in the cwd)
|
|
13
|
+
--check Write nothing; exit 1 when the output on disk is out of date
|
|
14
|
+
-v, --version Print the version
|
|
15
|
+
-h, --help Show this help
|
|
16
|
+
`;
|
|
17
|
+
function parse(argv) {
|
|
18
|
+
const args = { check: false, help: false, version: false };
|
|
19
|
+
const rest = [...argv];
|
|
20
|
+
if (rest[0] === "build")
|
|
21
|
+
rest.shift();
|
|
22
|
+
for (let i = 0; i < rest.length; i++) {
|
|
23
|
+
const arg = rest[i];
|
|
24
|
+
if (arg === "--check")
|
|
25
|
+
args.check = true;
|
|
26
|
+
else if (arg === "-h" || arg === "--help")
|
|
27
|
+
args.help = true;
|
|
28
|
+
else if (arg === "-v" || arg === "--version")
|
|
29
|
+
args.version = true;
|
|
30
|
+
else if (arg === "--config") {
|
|
31
|
+
const value = rest[++i];
|
|
32
|
+
if (!value)
|
|
33
|
+
throw new Error("@mrzr/react-svg-icons: --config needs a file");
|
|
34
|
+
args.config = value;
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
throw new Error(`@mrzr/react-svg-icons: unknown argument "${arg}" (see --help)`);
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
async function main(argv) {
|
|
42
|
+
const args = parse(argv);
|
|
43
|
+
if (args.help) {
|
|
44
|
+
process.stdout.write(HELP);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if (args.version) {
|
|
48
|
+
const pkg = new URL("../package.json", import.meta.url);
|
|
49
|
+
console.log(JSON.parse(readFileSync(pkg, "utf8")).version);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
const file = findConfig(cwd, args.config);
|
|
54
|
+
const config = await loadConfig(file);
|
|
55
|
+
const output = await generate(config, path.dirname(file));
|
|
56
|
+
if (args.check) {
|
|
57
|
+
const differences = compare(output);
|
|
58
|
+
if (differences.length === 0) {
|
|
59
|
+
console.log(`✔ icons are up to date (${output.files.length} files)`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
for (const d of differences)
|
|
63
|
+
console.error(`✖ ${d.reason}: ${path.relative(cwd, d.path)}`);
|
|
64
|
+
console.error("Run `npx @mrzr/react-svg-icons` and commit the result.");
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
write(output);
|
|
68
|
+
console.log(`✔ ${output.files.length} files in ${path.relative(cwd, output.outDir) || "."}`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
main(process.argv.slice(2)).then((code) => {
|
|
72
|
+
process.exitCode = code;
|
|
73
|
+
}, (error) => {
|
|
74
|
+
console.error(error instanceof Error ? error.message : error);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
});
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { INode } from "svgson";
|
|
2
|
+
export interface ColorInfo {
|
|
3
|
+
key: string;
|
|
4
|
+
normalizedColor: string;
|
|
5
|
+
value: string;
|
|
6
|
+
count: number;
|
|
7
|
+
firstOrder: number;
|
|
8
|
+
isTranslucent: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function normalizeColor(value: unknown): string;
|
|
11
|
+
export declare function isColorLike(value: unknown): boolean;
|
|
12
|
+
export declare function isTranslucentColorValue(value: unknown): boolean;
|
|
13
|
+
export declare function collectColorInfo(svgNode: INode): ColorInfo[];
|
|
14
|
+
export interface IconContext {
|
|
15
|
+
colorSlots: string[];
|
|
16
|
+
getColorIndex(originalColor: unknown, isTranslucent: boolean): number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Slot 0 is the most used opaque colour, then the other opaque ones, then the
|
|
20
|
+
* translucent ones; equal counts keep the order they first appear in.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createIconContext(colorInfos: ColorInfo[]): IconContext;
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function normalizeColor(value) {
|
|
2
|
+
return String(value).trim().toLowerCase().replace(/\s+/g, " ");
|
|
3
|
+
}
|
|
4
|
+
export function isColorLike(value) {
|
|
5
|
+
if (value == null)
|
|
6
|
+
return false;
|
|
7
|
+
const v = normalizeColor(value);
|
|
8
|
+
if (!v)
|
|
9
|
+
return false;
|
|
10
|
+
if (["none", "transparent", "currentcolor", "inherit"].includes(v))
|
|
11
|
+
return false;
|
|
12
|
+
if (v.startsWith("url("))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
export function isTranslucentColorValue(value) {
|
|
17
|
+
const v = normalizeColor(value);
|
|
18
|
+
if (!v)
|
|
19
|
+
return false;
|
|
20
|
+
if (v.includes("rgba")) {
|
|
21
|
+
const match = v.match(/rgba\(([^)]+)\)/);
|
|
22
|
+
if (match) {
|
|
23
|
+
const parts = match[1].split(",").map((p) => p.trim());
|
|
24
|
+
if (parts.length === 4) {
|
|
25
|
+
const a = Number.parseFloat(parts[3]);
|
|
26
|
+
if (Number.isFinite(a) && a < 1)
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const slashAlpha = v.match(/\/\s*([0-9]*\.?[0-9]+)\s*\)/);
|
|
32
|
+
if (slashAlpha) {
|
|
33
|
+
const a = Number.parseFloat(slashAlpha[1]);
|
|
34
|
+
if (Number.isFinite(a) && a < 1)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export function collectColorInfo(svgNode) {
|
|
40
|
+
const infos = new Map();
|
|
41
|
+
let firstOrder = 0;
|
|
42
|
+
function register(value, opacityHint) {
|
|
43
|
+
if (value == null)
|
|
44
|
+
return;
|
|
45
|
+
const normalized = normalizeColor(value);
|
|
46
|
+
if (normalized === "currentcolor" || !isColorLike(value))
|
|
47
|
+
return;
|
|
48
|
+
const isTranslucent = opacityHint < 1 || isTranslucentColorValue(value);
|
|
49
|
+
const key = `${normalized}__${isTranslucent ? "alpha" : "solid"}`;
|
|
50
|
+
if (!infos.has(key)) {
|
|
51
|
+
infos.set(key, {
|
|
52
|
+
key,
|
|
53
|
+
normalizedColor: normalized,
|
|
54
|
+
value: String(value),
|
|
55
|
+
count: 0,
|
|
56
|
+
firstOrder: firstOrder++,
|
|
57
|
+
isTranslucent,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
infos.get(key).count += 1;
|
|
61
|
+
}
|
|
62
|
+
function visit(node, inheritedOpacity) {
|
|
63
|
+
if (node.type !== "element")
|
|
64
|
+
return;
|
|
65
|
+
const attrs = node.attributes || {};
|
|
66
|
+
let currentOpacity = inheritedOpacity;
|
|
67
|
+
if (attrs.opacity != null) {
|
|
68
|
+
const p = Number.parseFloat(attrs.opacity);
|
|
69
|
+
if (Number.isFinite(p))
|
|
70
|
+
currentOpacity *= p;
|
|
71
|
+
}
|
|
72
|
+
const fo = attrs["fill-opacity"] ?? attrs.fillOpacity;
|
|
73
|
+
const so = attrs["stroke-opacity"] ?? attrs.strokeOpacity;
|
|
74
|
+
if (attrs.fill != null) {
|
|
75
|
+
let fo2 = currentOpacity;
|
|
76
|
+
if (fo != null) {
|
|
77
|
+
const p = Number.parseFloat(fo);
|
|
78
|
+
if (Number.isFinite(p))
|
|
79
|
+
fo2 *= p;
|
|
80
|
+
}
|
|
81
|
+
register(attrs.fill, fo2);
|
|
82
|
+
}
|
|
83
|
+
if (attrs.stroke != null) {
|
|
84
|
+
let so2 = currentOpacity;
|
|
85
|
+
if (so != null) {
|
|
86
|
+
const p = Number.parseFloat(so);
|
|
87
|
+
if (Number.isFinite(p))
|
|
88
|
+
so2 *= p;
|
|
89
|
+
}
|
|
90
|
+
register(attrs.stroke, so2);
|
|
91
|
+
}
|
|
92
|
+
for (const child of node.children || [])
|
|
93
|
+
visit(child, currentOpacity);
|
|
94
|
+
}
|
|
95
|
+
visit(svgNode, 1);
|
|
96
|
+
return [...infos.values()];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Slot 0 is the most used opaque colour, then the other opaque ones, then the
|
|
100
|
+
* translucent ones; equal counts keep the order they first appear in.
|
|
101
|
+
*/
|
|
102
|
+
export function createIconContext(colorInfos) {
|
|
103
|
+
const sorted = [...colorInfos].sort((a, b) => {
|
|
104
|
+
if (a.isTranslucent !== b.isTranslucent)
|
|
105
|
+
return a.isTranslucent ? 1 : -1;
|
|
106
|
+
if (b.count !== a.count)
|
|
107
|
+
return b.count - a.count;
|
|
108
|
+
return a.firstOrder - b.firstOrder;
|
|
109
|
+
});
|
|
110
|
+
const colorIndex = new Map();
|
|
111
|
+
const colorSlots = [];
|
|
112
|
+
sorted.forEach((info, index) => {
|
|
113
|
+
colorIndex.set(info.key, index);
|
|
114
|
+
colorSlots.push(info.value);
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
colorSlots,
|
|
118
|
+
getColorIndex(originalColor, isTranslucent) {
|
|
119
|
+
const normalized = normalizeColor(originalColor);
|
|
120
|
+
const key = `${normalized}__${isTranslucent ? "alpha" : "solid"}`;
|
|
121
|
+
if (colorIndex.has(key))
|
|
122
|
+
return colorIndex.get(key);
|
|
123
|
+
const other = `${normalized}__${isTranslucent ? "solid" : "alpha"}`;
|
|
124
|
+
if (colorIndex.has(other))
|
|
125
|
+
return colorIndex.get(other);
|
|
126
|
+
const index = colorSlots.length;
|
|
127
|
+
colorIndex.set(key, index);
|
|
128
|
+
colorSlots.push(String(originalColor));
|
|
129
|
+
return index;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { optimize } from "svgo";
|
|
2
|
+
import * as svgson from "svgson";
|
|
3
|
+
import { collectColorInfo, createIconContext, isTranslucentColorValue, normalizeColor } from "./color.js";
|
|
4
|
+
import { HEADER } from "./header.js";
|
|
5
|
+
import { attributeToJsx, escapeJsString, nodeToJsx, transformNode } from "./jsx.js";
|
|
6
|
+
const parseSync = svgson.parseSync ??
|
|
7
|
+
svgson.default.parseSync;
|
|
8
|
+
function optimizeSvg(svg) {
|
|
9
|
+
return optimize(svg, {
|
|
10
|
+
multipass: true,
|
|
11
|
+
plugins: [
|
|
12
|
+
{
|
|
13
|
+
name: "preset-default",
|
|
14
|
+
params: { overrides: { cleanupIds: false } },
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
}).data;
|
|
18
|
+
}
|
|
19
|
+
function walk(node, fn) {
|
|
20
|
+
if (!node)
|
|
21
|
+
return;
|
|
22
|
+
fn(node);
|
|
23
|
+
for (const child of node.children || [])
|
|
24
|
+
walk(child, fn);
|
|
25
|
+
}
|
|
26
|
+
function mostCommon(values) {
|
|
27
|
+
if (!values.length)
|
|
28
|
+
return undefined;
|
|
29
|
+
const counts = new Map();
|
|
30
|
+
let best = values[0];
|
|
31
|
+
for (const value of values) {
|
|
32
|
+
counts.set(value, (counts.get(value) || 0) + 1);
|
|
33
|
+
if (counts.get(value) > (counts.get(best) || 0))
|
|
34
|
+
best = value;
|
|
35
|
+
}
|
|
36
|
+
return best;
|
|
37
|
+
}
|
|
38
|
+
export function generateComponentCode(rawSvg, componentName, file, options) {
|
|
39
|
+
let ast;
|
|
40
|
+
try {
|
|
41
|
+
ast = parseSync(optimizeSvg(rawSvg));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`@mrzr/react-svg-icons: ${file} is not a valid SVG (${error.message})`);
|
|
45
|
+
}
|
|
46
|
+
const svgNode = (ast.name === "svg" ? ast : (ast.children || []).find((n) => n.name === "svg"));
|
|
47
|
+
if (!svgNode)
|
|
48
|
+
throw new Error(`@mrzr/react-svg-icons: no <svg> root in ${file}`);
|
|
49
|
+
svgNode.attributes = svgNode.attributes || {};
|
|
50
|
+
const fallbackWidth = Number.parseFloat(svgNode.attributes.width || "24");
|
|
51
|
+
const fallbackHeight = Number.parseFloat(svgNode.attributes.height || "24");
|
|
52
|
+
const viewBox = svgNode.attributes.viewBox ||
|
|
53
|
+
svgNode.attributes.viewbox ||
|
|
54
|
+
`0 0 ${Number.isFinite(fallbackWidth) ? fallbackWidth : 24} ${Number.isFinite(fallbackHeight) ? fallbackHeight : 24}`;
|
|
55
|
+
const strokeWidthValues = [];
|
|
56
|
+
walk(svgNode, (node) => {
|
|
57
|
+
if (node.type !== "element")
|
|
58
|
+
return;
|
|
59
|
+
const v = node.attributes?.["stroke-width"] ?? node.attributes?.strokeWidth;
|
|
60
|
+
if (v != null)
|
|
61
|
+
strokeWidthValues.push(String(v));
|
|
62
|
+
});
|
|
63
|
+
const commonStrokeWidth = mostCommon(strokeWidthValues);
|
|
64
|
+
const parsedStrokeWidth = commonStrokeWidth == null ? options.defaultStrokeWidth : Number.parseFloat(commonStrokeWidth);
|
|
65
|
+
const strokeWidthJsDefault = Number.isFinite(parsedStrokeWidth)
|
|
66
|
+
? parsedStrokeWidth
|
|
67
|
+
: escapeJsString(commonStrokeWidth);
|
|
68
|
+
const ctx = createIconContext(collectColorInfo(svgNode));
|
|
69
|
+
const rootFill = svgNode.attributes.fill;
|
|
70
|
+
const rootStroke = svgNode.attributes.stroke;
|
|
71
|
+
let rootFillAttribute;
|
|
72
|
+
if (rootFill == null || normalizeColor(rootFill) === "none" || normalizeColor(rootFill) === "transparent") {
|
|
73
|
+
rootFillAttribute = `fill="none"`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const index = ctx.getColorIndex(rootFill, isTranslucentColorValue(rootFill));
|
|
77
|
+
rootFillAttribute = `fill={colors?.[${index}] ?? fill ?? color ?? "currentColor"}`;
|
|
78
|
+
}
|
|
79
|
+
let rootStrokeAttribute = "";
|
|
80
|
+
if (rootStroke != null && normalizeColor(rootStroke) !== "none") {
|
|
81
|
+
const index = ctx.getColorIndex(rootStroke, isTranslucentColorValue(rootStroke));
|
|
82
|
+
rootStrokeAttribute = `stroke={colors?.[${index}] ?? stroke ?? color ?? "currentColor"}`;
|
|
83
|
+
}
|
|
84
|
+
for (const name of [
|
|
85
|
+
"width", "height", "viewBox", "viewbox", "fill", "stroke",
|
|
86
|
+
"stroke-width", "strokeWidth", "xmlns", "xmlns:xlink", "version", "style",
|
|
87
|
+
]) {
|
|
88
|
+
delete svgNode.attributes[name];
|
|
89
|
+
}
|
|
90
|
+
transformNode(svgNode, ctx);
|
|
91
|
+
const rootExtraAttrs = Object.entries(svgNode.attributes || {})
|
|
92
|
+
.map(([name, value]) => attributeToJsx(name, value))
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(" ");
|
|
95
|
+
const childrenJsx = (svgNode.children || []).map((child) => nodeToJsx(child, 3)).join("");
|
|
96
|
+
const colorComment = ctx.colorSlots.length
|
|
97
|
+
? `/**\n * Original color slots:\n${ctx.colorSlots.map((slot, i) => ` * - colors[${i}] = ${slot}`).join("\n")}\n */\n`
|
|
98
|
+
: "";
|
|
99
|
+
const rootPropLines = [
|
|
100
|
+
"ref={ref}",
|
|
101
|
+
"width={size}",
|
|
102
|
+
"height={size}",
|
|
103
|
+
`viewBox={${escapeJsString(viewBox)}}`,
|
|
104
|
+
rootFillAttribute,
|
|
105
|
+
rootStrokeAttribute,
|
|
106
|
+
rootExtraAttrs,
|
|
107
|
+
`role={title ? "img" : undefined}`,
|
|
108
|
+
`aria-hidden={title ? undefined : true}`,
|
|
109
|
+
"{...props}",
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.map((line) => ` ${line}`)
|
|
113
|
+
.join("\n");
|
|
114
|
+
return `${HEADER}${colorComment}import * as React from "react";
|
|
115
|
+
|
|
116
|
+
export type ${componentName}Props = Omit<
|
|
117
|
+
React.SVGProps<SVGSVGElement>,
|
|
118
|
+
"color" | "stroke" | "fill" | "strokeWidth" | "opacity"
|
|
119
|
+
> & {
|
|
120
|
+
size?: number | string;
|
|
121
|
+
color?: string;
|
|
122
|
+
stroke?: string;
|
|
123
|
+
fill?: string;
|
|
124
|
+
strokeWidth?: number | string;
|
|
125
|
+
opacity?: number | string;
|
|
126
|
+
colors?: (string | undefined)[];
|
|
127
|
+
opacities?: (number | string | undefined)[];
|
|
128
|
+
strokeWidths?: (number | string | undefined)[];
|
|
129
|
+
title?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const ${componentName} = React.forwardRef<
|
|
133
|
+
SVGSVGElement,
|
|
134
|
+
${componentName}Props
|
|
135
|
+
>(
|
|
136
|
+
function ${componentName}(
|
|
137
|
+
{
|
|
138
|
+
size = ${options.defaultSize},
|
|
139
|
+
color = "currentColor",
|
|
140
|
+
stroke,
|
|
141
|
+
fill,
|
|
142
|
+
strokeWidth = ${strokeWidthJsDefault},
|
|
143
|
+
opacity,
|
|
144
|
+
colors,
|
|
145
|
+
opacities,
|
|
146
|
+
strokeWidths,
|
|
147
|
+
title,
|
|
148
|
+
...props
|
|
149
|
+
},
|
|
150
|
+
ref
|
|
151
|
+
) {
|
|
152
|
+
return (
|
|
153
|
+
<svg
|
|
154
|
+
${rootPropLines}
|
|
155
|
+
>
|
|
156
|
+
{title ? <title>{title}</title> : null}
|
|
157
|
+
${childrenJsx} </svg>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
`;
|
|
162
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ReactSvgIconsConfig {
|
|
2
|
+
/** Folder with the exported `*.svg` files. Subfolders become variants. */
|
|
3
|
+
input: string;
|
|
4
|
+
/** Folder the components are written to. */
|
|
5
|
+
outDir: string;
|
|
6
|
+
/** Words dropped from every icon name, in any case: `[ic, icon]`. */
|
|
7
|
+
removeWords?: string[];
|
|
8
|
+
/** The `size` an icon has when none is given. Default 24. */
|
|
9
|
+
defaultSize?: number;
|
|
10
|
+
/** The `strokeWidth` of an icon whose SVG has none. Default 2. */
|
|
11
|
+
defaultStrokeWidth?: number;
|
|
12
|
+
/** `"prettier"` formats with the project's prettier and its config; `false` writes raw. Default `"prettier"`. */
|
|
13
|
+
format?: "prettier" | false;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedConfig {
|
|
16
|
+
input: string;
|
|
17
|
+
outDir: string;
|
|
18
|
+
removeWords: string[];
|
|
19
|
+
defaultSize: number;
|
|
20
|
+
defaultStrokeWidth: number;
|
|
21
|
+
format: "prettier" | false;
|
|
22
|
+
}
|
|
23
|
+
export declare function defineConfig(config: ReactSvgIconsConfig): ReactSvgIconsConfig;
|
|
24
|
+
export declare function resolveConfig(config: ReactSvgIconsConfig): ResolvedConfig;
|
|
25
|
+
export declare function findConfig(cwd: string, explicit?: string): string;
|
|
26
|
+
export declare function loadConfig(file: string): Promise<ReactSvgIconsConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
const OPTIONS = ["input", "outDir", "removeWords", "defaultSize", "defaultStrokeWidth", "format"];
|
|
5
|
+
export function defineConfig(config) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
export function resolveConfig(config) {
|
|
9
|
+
for (const key of Object.keys(config)) {
|
|
10
|
+
if (!OPTIONS.includes(key)) {
|
|
11
|
+
throw new Error(`@mrzr/react-svg-icons: unknown option "${key}" (options: ${OPTIONS.join(", ")})`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!config.input)
|
|
15
|
+
throw new Error("@mrzr/react-svg-icons: `input` is required");
|
|
16
|
+
if (!config.outDir)
|
|
17
|
+
throw new Error("@mrzr/react-svg-icons: `outDir` is required");
|
|
18
|
+
const number = (name, value, fallback) => {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return fallback;
|
|
21
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
22
|
+
throw new Error(`@mrzr/react-svg-icons: \`${name}\` must be a positive number`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
if (config.removeWords !== undefined && !Array.isArray(config.removeWords)) {
|
|
27
|
+
throw new Error("@mrzr/react-svg-icons: `removeWords` must be a list of words");
|
|
28
|
+
}
|
|
29
|
+
if (config.format !== undefined && config.format !== "prettier" && config.format !== false) {
|
|
30
|
+
throw new Error('@mrzr/react-svg-icons: `format` must be "prettier" or false');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
input: config.input,
|
|
34
|
+
outDir: config.outDir,
|
|
35
|
+
removeWords: (config.removeWords ?? []).map((w) => String(w).toLowerCase()),
|
|
36
|
+
defaultSize: number("defaultSize", config.defaultSize, 24),
|
|
37
|
+
defaultStrokeWidth: number("defaultStrokeWidth", config.defaultStrokeWidth, 2),
|
|
38
|
+
format: config.format ?? "prettier",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const CONFIG_NAMES = [
|
|
42
|
+
"react-svg-icons.config.yaml",
|
|
43
|
+
"react-svg-icons.config.yml",
|
|
44
|
+
"react-svg-icons.config.json",
|
|
45
|
+
".react-svg-iconsrc",
|
|
46
|
+
".react-svg-iconsrc.yaml",
|
|
47
|
+
".react-svg-iconsrc.yml",
|
|
48
|
+
".react-svg-iconsrc.json",
|
|
49
|
+
];
|
|
50
|
+
const SCRIPT = /\.(c|m)?(j|t)s$/;
|
|
51
|
+
export function findConfig(cwd, explicit) {
|
|
52
|
+
if (explicit) {
|
|
53
|
+
const file = path.resolve(cwd, explicit);
|
|
54
|
+
if (!existsSync(file))
|
|
55
|
+
throw new Error(`@mrzr/react-svg-icons: no config at ${file}`);
|
|
56
|
+
return file;
|
|
57
|
+
}
|
|
58
|
+
for (const name of CONFIG_NAMES) {
|
|
59
|
+
const file = path.resolve(cwd, name);
|
|
60
|
+
if (existsSync(file))
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`@mrzr/react-svg-icons: no config found; create ${CONFIG_NAMES[0]} (see the README)`);
|
|
64
|
+
}
|
|
65
|
+
export async function loadConfig(file) {
|
|
66
|
+
if (SCRIPT.test(file)) {
|
|
67
|
+
throw new Error(`@mrzr/react-svg-icons: ${file} is a script; the config must be YAML or JSON`);
|
|
68
|
+
}
|
|
69
|
+
const text = readFileSync(file, "utf8").replace(/^/, "");
|
|
70
|
+
const isJson = file.endsWith(".json");
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = isJson ? JSON.parse(text) : parseYaml(text);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`@mrzr/react-svg-icons: ${file} is not valid ${isJson ? "JSON" : "YAML"} (${error.message})`);
|
|
77
|
+
}
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
79
|
+
throw new Error(`@mrzr/react-svg-icons: ${file} must hold a mapping of options`);
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
/** The project's own prettier first, then the one installed next to this package. */
|
|
5
|
+
async function loadPrettier(cwd) {
|
|
6
|
+
try {
|
|
7
|
+
const found = createRequire(path.join(cwd, "noop.js")).resolve("prettier");
|
|
8
|
+
return (await import(pathToFileURL(found).href)).default;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
try {
|
|
12
|
+
return (await import("prettier")).default;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function formatFiles(files, cwd, format) {
|
|
20
|
+
if (format === false)
|
|
21
|
+
return files;
|
|
22
|
+
const prettier = await loadPrettier(cwd);
|
|
23
|
+
if (!prettier) {
|
|
24
|
+
console.warn("@mrzr/react-svg-icons: prettier is not installed, so the output is not formatted. " +
|
|
25
|
+
"Install it in the project, or set `format: false` to silence this.");
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
const config = (await prettier.resolveConfig(path.join(cwd, "package.json"))) ?? {};
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
try {
|
|
32
|
+
out.push({
|
|
33
|
+
path: file.path,
|
|
34
|
+
content: await prettier.format(file.content, { ...config, parser: "typescript", filepath: file.path }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`@mrzr/react-svg-icons: the generated ${file.path} does not parse (${error.message})`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
package/dist/header.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const MARKER = "auto-generated by @mrzr/react-svg-icons";
|
|
2
|
+
/** Also the banner of 0.1.0, which did not carry the scope. */
|
|
3
|
+
export declare const MARKER_PATTERN: RegExp;
|
|
4
|
+
export declare const HEADER = "/* eslint-disable */\n/* This file is auto-generated by @mrzr/react-svg-icons. Do not edit manually. */\n\n";
|
package/dist/header.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const MARKER = "auto-generated by @mrzr/react-svg-icons";
|
|
2
|
+
/** Also the banner of 0.1.0, which did not carry the scope. */
|
|
3
|
+
export const MARKER_PATTERN = /auto-generated by (@mrzr\/)?react-svg-icons/;
|
|
4
|
+
export const HEADER = `/* eslint-disable */\n/* This file is ${MARKER}. Do not edit manually. */\n\n`;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/jsx.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { INode } from "svgson";
|
|
2
|
+
import { type IconContext } from "./color.js";
|
|
3
|
+
export interface IconNode extends INode {
|
|
4
|
+
mappedFill?: number;
|
|
5
|
+
mappedStroke?: number;
|
|
6
|
+
nativeOpacity?: number;
|
|
7
|
+
nativeFillOpacity?: number;
|
|
8
|
+
nativeStrokeOpacity?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const escapeJsString: (value: unknown) => string;
|
|
11
|
+
export declare function attributeToJsx(name: string, value: unknown): string;
|
|
12
|
+
export declare function transformNode(node: IconNode, ctx: IconContext, inheritedOpacity?: number): void;
|
|
13
|
+
export declare function nodeToJsx(node: IconNode, indent?: number): string;
|
package/dist/jsx.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isColorLike, isTranslucentColorValue } from "./color.js";
|
|
2
|
+
const IGNORE_ATTRS = new Set(["xmlns", "xmlns:xlink", "version", "style"]);
|
|
3
|
+
const ATTR_ALIAS = {
|
|
4
|
+
class: "className",
|
|
5
|
+
"xlink:href": "href",
|
|
6
|
+
"xml:space": "xmlSpace",
|
|
7
|
+
};
|
|
8
|
+
export const escapeJsString = (value) => JSON.stringify(String(value));
|
|
9
|
+
function camelize(name) {
|
|
10
|
+
return name.replace(/[-:](\w)/g, (_, c) => c.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
function toJsxAttributeName(name) {
|
|
13
|
+
if (name.startsWith("data-") || name.startsWith("aria-"))
|
|
14
|
+
return name;
|
|
15
|
+
if (ATTR_ALIAS[name])
|
|
16
|
+
return ATTR_ALIAS[name];
|
|
17
|
+
return camelize(name);
|
|
18
|
+
}
|
|
19
|
+
export function attributeToJsx(name, value) {
|
|
20
|
+
if (IGNORE_ATTRS.has(name))
|
|
21
|
+
return "";
|
|
22
|
+
const key = toJsxAttributeName(name);
|
|
23
|
+
if (value === true)
|
|
24
|
+
return `${key}=""`;
|
|
25
|
+
if (value == null || value === false)
|
|
26
|
+
return "";
|
|
27
|
+
return `${key}={${escapeJsString(value)}}`;
|
|
28
|
+
}
|
|
29
|
+
export function transformNode(node, ctx, inheritedOpacity = 1) {
|
|
30
|
+
if (node.type !== "element")
|
|
31
|
+
return;
|
|
32
|
+
const attrs = node.attributes || {};
|
|
33
|
+
let currentOpacity = inheritedOpacity;
|
|
34
|
+
if (attrs.opacity != null) {
|
|
35
|
+
const parsed = Number.parseFloat(attrs.opacity);
|
|
36
|
+
if (Number.isFinite(parsed)) {
|
|
37
|
+
node.nativeOpacity = parsed;
|
|
38
|
+
currentOpacity *= parsed;
|
|
39
|
+
delete attrs.opacity;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (node.name !== "svg") {
|
|
43
|
+
const fillOpacityAttr = attrs["fill-opacity"] ?? attrs.fillOpacity;
|
|
44
|
+
const strokeOpacityAttr = attrs["stroke-opacity"] ?? attrs.strokeOpacity;
|
|
45
|
+
if (attrs.fill != null && isColorLike(attrs.fill)) {
|
|
46
|
+
let fillOpacity = currentOpacity;
|
|
47
|
+
let nativeFillOpacity = 1;
|
|
48
|
+
if (fillOpacityAttr != null) {
|
|
49
|
+
const p = Number.parseFloat(fillOpacityAttr);
|
|
50
|
+
if (Number.isFinite(p)) {
|
|
51
|
+
fillOpacity *= p;
|
|
52
|
+
nativeFillOpacity = p;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const isTranslucent = fillOpacity < 1 || isTranslucentColorValue(attrs.fill);
|
|
56
|
+
node.mappedFill = ctx.getColorIndex(attrs.fill, isTranslucent);
|
|
57
|
+
node.nativeFillOpacity = nativeFillOpacity < 1 ? nativeFillOpacity : undefined;
|
|
58
|
+
delete attrs.fill;
|
|
59
|
+
delete attrs["fill-opacity"];
|
|
60
|
+
delete attrs.fillOpacity;
|
|
61
|
+
}
|
|
62
|
+
if (attrs.stroke != null && isColorLike(attrs.stroke)) {
|
|
63
|
+
let strokeOpacity = currentOpacity;
|
|
64
|
+
let nativeStrokeOpacity = 1;
|
|
65
|
+
if (strokeOpacityAttr != null) {
|
|
66
|
+
const p = Number.parseFloat(strokeOpacityAttr);
|
|
67
|
+
if (Number.isFinite(p)) {
|
|
68
|
+
strokeOpacity *= p;
|
|
69
|
+
nativeStrokeOpacity = p;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const isTranslucent = strokeOpacity < 1 || isTranslucentColorValue(attrs.stroke);
|
|
73
|
+
node.mappedStroke = ctx.getColorIndex(attrs.stroke, isTranslucent);
|
|
74
|
+
node.nativeStrokeOpacity = nativeStrokeOpacity < 1 ? nativeStrokeOpacity : undefined;
|
|
75
|
+
delete attrs.stroke;
|
|
76
|
+
delete attrs["stroke-opacity"];
|
|
77
|
+
delete attrs.strokeOpacity;
|
|
78
|
+
}
|
|
79
|
+
delete attrs["stroke-width"];
|
|
80
|
+
delete attrs.strokeWidth;
|
|
81
|
+
}
|
|
82
|
+
for (const child of node.children || [])
|
|
83
|
+
transformNode(child, ctx, currentOpacity);
|
|
84
|
+
}
|
|
85
|
+
export function nodeToJsx(node, indent = 3) {
|
|
86
|
+
const pad = " ".repeat(indent);
|
|
87
|
+
if (node.type === "text") {
|
|
88
|
+
const text = String(node.value || "").trim();
|
|
89
|
+
if (!text)
|
|
90
|
+
return "";
|
|
91
|
+
return `${pad}{${escapeJsString(text)}}\n`;
|
|
92
|
+
}
|
|
93
|
+
if (node.type !== "element")
|
|
94
|
+
return "";
|
|
95
|
+
const attrParts = Object.entries(node.attributes || {})
|
|
96
|
+
.map(([name, value]) => attributeToJsx(name, value))
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
const styleProps = [];
|
|
99
|
+
const hasMappedFill = node.mappedFill !== undefined;
|
|
100
|
+
const hasMappedStroke = node.mappedStroke !== undefined;
|
|
101
|
+
const mappedIndex = hasMappedStroke ? node.mappedStroke : hasMappedFill ? node.mappedFill : undefined;
|
|
102
|
+
if (hasMappedFill) {
|
|
103
|
+
styleProps.push(`fill: colors?.[${node.mappedFill}] ?? fill ?? color ?? "currentColor"`);
|
|
104
|
+
}
|
|
105
|
+
if (hasMappedStroke) {
|
|
106
|
+
styleProps.push(`stroke: colors?.[${node.mappedStroke}] ?? stroke ?? color ?? "currentColor"`);
|
|
107
|
+
}
|
|
108
|
+
if (mappedIndex !== undefined) {
|
|
109
|
+
let baseOpacity = node.nativeOpacity ?? 1;
|
|
110
|
+
if (hasMappedFill && node.nativeFillOpacity !== undefined)
|
|
111
|
+
baseOpacity *= node.nativeFillOpacity;
|
|
112
|
+
else if (hasMappedStroke && node.nativeStrokeOpacity !== undefined)
|
|
113
|
+
baseOpacity *= node.nativeStrokeOpacity;
|
|
114
|
+
if (baseOpacity !== 1) {
|
|
115
|
+
styleProps.push(`opacity: opacities?.[${mappedIndex}] ?? opacity ?? ${baseOpacity}`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
styleProps.push(`opacity: opacities?.[${mappedIndex}] ?? opacity`);
|
|
119
|
+
}
|
|
120
|
+
attrParts.push(`strokeWidth={strokeWidths?.[${mappedIndex}] ?? strokeWidth}`);
|
|
121
|
+
}
|
|
122
|
+
else if (node.nativeOpacity !== undefined) {
|
|
123
|
+
styleProps.push(`opacity: opacity ?? ${node.nativeOpacity}`);
|
|
124
|
+
}
|
|
125
|
+
if (styleProps.length > 0)
|
|
126
|
+
attrParts.push(`style={{ ${styleProps.join(", ")} }}`);
|
|
127
|
+
const openingAttrs = attrParts.length ? ` ${attrParts.join(" ")}` : "";
|
|
128
|
+
const children = (node.children || []).map((child) => nodeToJsx(child, indent + 1)).join("");
|
|
129
|
+
if (!children.trim())
|
|
130
|
+
return `${pad}<${node.name}${openingAttrs} />\n`;
|
|
131
|
+
return `${pad}<${node.name}${openingAttrs}>\n${children}${pad}</${node.name}>\n`;
|
|
132
|
+
}
|
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function splitWords(value: string): string[];
|
|
2
|
+
export declare function cleanIconName(fileName: string, folderName: string, removeWords: readonly string[]): string;
|
|
3
|
+
export declare function toKebabCase(str: string): string;
|
|
4
|
+
export declare function toCamelCase(str: string): string;
|
package/dist/names.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function splitWords(value) {
|
|
2
|
+
const normalized = String(value)
|
|
3
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
4
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
5
|
+
.replace(/(\d+)/g, " $1 ")
|
|
6
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
7
|
+
.trim();
|
|
8
|
+
if (!normalized)
|
|
9
|
+
return [];
|
|
10
|
+
return normalized.split(/\s+/).filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
function capitalizeWord(word) {
|
|
13
|
+
if (/^\d+$/.test(word))
|
|
14
|
+
return word;
|
|
15
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
export function cleanIconName(fileName, folderName, removeWords) {
|
|
18
|
+
let words = splitWords(fileName.replace(/\.svg$/i, ""));
|
|
19
|
+
if (words[0]?.toLowerCase() === "icon") {
|
|
20
|
+
words.shift();
|
|
21
|
+
while (words.length && /^\d+$/.test(words[0]))
|
|
22
|
+
words.shift();
|
|
23
|
+
}
|
|
24
|
+
words = words.filter((w) => !removeWords.includes(w.toLowerCase()));
|
|
25
|
+
const folderWords = new Set(splitWords(folderName).map((w) => w.toLowerCase()));
|
|
26
|
+
words = words.filter((w) => !folderWords.has(w.toLowerCase()));
|
|
27
|
+
if (!words.length)
|
|
28
|
+
return "Icon";
|
|
29
|
+
let pascal = words.map(capitalizeWord).join("");
|
|
30
|
+
if (/^[0-9]/.test(pascal))
|
|
31
|
+
pascal = `Icon${pascal}`;
|
|
32
|
+
pascal = pascal.replace(/[^a-zA-Z0-9]/g, "");
|
|
33
|
+
return pascal || "Icon";
|
|
34
|
+
}
|
|
35
|
+
export function toKebabCase(str) {
|
|
36
|
+
return String(str)
|
|
37
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
38
|
+
.replace(/[\s_]+/g, "-")
|
|
39
|
+
.replace(/[^a-zA-Z0-9-]/g, "")
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
export function toCamelCase(str) {
|
|
43
|
+
const pascal = String(str)
|
|
44
|
+
.replace(/[-_]+/g, " ")
|
|
45
|
+
.replace(/[^\w\s]/g, "")
|
|
46
|
+
.replace(/\s+(.)(\w*)/g, (_, $1, $2) => $1.toUpperCase() + $2.toLowerCase())
|
|
47
|
+
.replace(/^./, (s) => s.toUpperCase());
|
|
48
|
+
let camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
49
|
+
if (/^[0-9]/.test(camel) || !camel)
|
|
50
|
+
camel = `folder${camel || "Unknown"}`;
|
|
51
|
+
return camel;
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface IconFile {
|
|
2
|
+
componentName: string;
|
|
3
|
+
fileName: string;
|
|
4
|
+
folderName: string;
|
|
5
|
+
path: string[];
|
|
6
|
+
}
|
|
7
|
+
/** `bold.logout`, `a.b.x` for a nested folder; an icon at the top takes the input folder's name. */
|
|
8
|
+
export declare function registryKey(file: IconFile): string;
|
|
9
|
+
export declare function registryCode(files: IconFile[]): string;
|
|
10
|
+
export declare function iconComponentCode(defaultSize: number): string;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { HEADER } from "./header.js";
|
|
2
|
+
/** `bold.logout`, `a.b.x` for a nested folder; an icon at the top takes the input folder's name. */
|
|
3
|
+
export function registryKey(file) {
|
|
4
|
+
const base = file.fileName.replace(".tsx", "");
|
|
5
|
+
return `${file.path.length ? file.path.join(".") : file.folderName}.${base}`;
|
|
6
|
+
}
|
|
7
|
+
export function registryCode(files) {
|
|
8
|
+
const seen = new Map();
|
|
9
|
+
const entries = files.map((file) => {
|
|
10
|
+
const base = file.fileName.replace(".tsx", "");
|
|
11
|
+
const importPath = "./" + [...file.path, base].join("/");
|
|
12
|
+
const key = registryKey(file);
|
|
13
|
+
if (seen.has(key)) {
|
|
14
|
+
throw new Error(`@mrzr/react-svg-icons: two icons have the name "${key}": ${seen.get(key)} and ${importPath}`);
|
|
15
|
+
}
|
|
16
|
+
seen.set(key, importPath);
|
|
17
|
+
return ` "${key}": () => import("${importPath}").then((m) => m.${file.componentName}),`;
|
|
18
|
+
});
|
|
19
|
+
return `${HEADER}
|
|
20
|
+
|
|
21
|
+
export const iconRegistry = {
|
|
22
|
+
${entries.join("\n")}
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export type IconName = keyof typeof iconRegistry;
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
export function iconComponentCode(defaultSize) {
|
|
29
|
+
return `${HEADER}
|
|
30
|
+
"use client";
|
|
31
|
+
|
|
32
|
+
import * as React from "react";
|
|
33
|
+
import { iconRegistry, type IconName } from "./registry";
|
|
34
|
+
|
|
35
|
+
export type IconProps = Omit<
|
|
36
|
+
React.SVGProps<SVGSVGElement>,
|
|
37
|
+
"color" | "stroke" | "fill" | "strokeWidth" | "opacity"
|
|
38
|
+
> & {
|
|
39
|
+
name: IconName;
|
|
40
|
+
size?: number | string;
|
|
41
|
+
color?: string;
|
|
42
|
+
stroke?: string;
|
|
43
|
+
fill?: string;
|
|
44
|
+
strokeWidth?: number | string;
|
|
45
|
+
opacity?: number | string;
|
|
46
|
+
colors?: (string | undefined)[];
|
|
47
|
+
opacities?: (number | string | undefined)[];
|
|
48
|
+
strokeWidths?: (number | string | undefined)[];
|
|
49
|
+
title?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DEFAULT_ICON_PROPS = {
|
|
53
|
+
size: ${defaultSize},
|
|
54
|
+
color: "currentColor",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type RegistryLoader = (typeof iconRegistry)[IconName];
|
|
58
|
+
type ResolvedIconComponent = Awaited<ReturnType<RegistryLoader>>;
|
|
59
|
+
type LazyIconComponent = React.LazyExoticComponent<ResolvedIconComponent>;
|
|
60
|
+
|
|
61
|
+
// CRITICAL: React.lazy() must NEVER be called inside render — a fresh lazy
|
|
62
|
+
// wrapper type on every render makes React unmount the icon, re-suspend and
|
|
63
|
+
// remount it on every single parent render. Create one wrapper per icon
|
|
64
|
+
// name and reuse it for the lifetime of the app.
|
|
65
|
+
const lazyComponents = new Map<IconName, LazyIconComponent>();
|
|
66
|
+
|
|
67
|
+
const getLazyIcon = (name: IconName): LazyIconComponent | null => {
|
|
68
|
+
const cached = lazyComponents.get(name);
|
|
69
|
+
if (cached) return cached;
|
|
70
|
+
|
|
71
|
+
const loader = iconRegistry[name];
|
|
72
|
+
if (!loader) return null;
|
|
73
|
+
|
|
74
|
+
const created = React.lazy(() =>
|
|
75
|
+
loader().then((Component) => ({ default: Component })),
|
|
76
|
+
) as LazyIconComponent;
|
|
77
|
+
lazyComponents.set(name, created);
|
|
78
|
+
return created;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const IconBase = React.forwardRef<SVGSVGElement, IconProps>(
|
|
82
|
+
({ name, ...props }, ref) => {
|
|
83
|
+
const LazyIcon = getLazyIcon(name);
|
|
84
|
+
|
|
85
|
+
if (!LazyIcon) {
|
|
86
|
+
if (process.env.NODE_ENV !== "production") {
|
|
87
|
+
console.warn(\`[Icon] Unknown icon name: "\${name}".\`);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mergedProps = { ...DEFAULT_ICON_PROPS, ...props };
|
|
93
|
+
const size = mergedProps.size ?? ${defaultSize};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<React.Suspense
|
|
97
|
+
fallback={
|
|
98
|
+
<svg
|
|
99
|
+
width={size}
|
|
100
|
+
height={size}
|
|
101
|
+
viewBox="0 0 24 24"
|
|
102
|
+
className={mergedProps.className}
|
|
103
|
+
aria-hidden="true"
|
|
104
|
+
/>
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
<LazyIcon ref={ref} {...mergedProps} />
|
|
108
|
+
</React.Suspense>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
IconBase.displayName = "Icon";
|
|
114
|
+
|
|
115
|
+
// Only export the lazy component here
|
|
116
|
+
export const Icon = IconBase;
|
|
117
|
+
export type { IconName };
|
|
118
|
+
`;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrzr/react-svg-icons",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Turn a folder of SVG icons into typed React components, with a prop per colour and a lazy-loaded Icon.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"icons",
|
|
7
|
+
"svg",
|
|
8
|
+
"react",
|
|
9
|
+
"svgo",
|
|
10
|
+
"codegen",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/mohammadreza-zr/react-svg-icons.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/mohammadreza-zr/react-svg-icons#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/mohammadreza-zr/react-svg-icons/issues"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"react-svg-icons": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p .",
|
|
46
|
+
"typecheck": "tsc -p . --noEmit",
|
|
47
|
+
"test": "npm run build && node scripts/test.mjs",
|
|
48
|
+
"docs:sync": "bash scripts/sync-wiki.sh",
|
|
49
|
+
"prepublishOnly": "npm test"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"svgo": "^4.0.2",
|
|
53
|
+
"svgson": "^5.3.1",
|
|
54
|
+
"yaml": "^2.9.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"prettier": ">=3"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"prettier": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "^26.6.5",
|
|
69
|
+
"@types/react": "^19.2.14",
|
|
70
|
+
"@types/react-dom": "^19.2.3",
|
|
71
|
+
"esbuild": "^0.28.2",
|
|
72
|
+
"prettier": "3.9.10",
|
|
73
|
+
"react": "^19.2.5",
|
|
74
|
+
"react-dom": "^19.2.5",
|
|
75
|
+
"typescript": "~6.0.2"
|
|
76
|
+
}
|
|
77
|
+
}
|