react-icons-sprite 0.1.0

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2025 Jure Rotar
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 ADDED
@@ -0,0 +1,137 @@
1
+ # react-icons-sprite
2
+
3
+ `react-icons-sprite` is a lightweight plugin built on top of [react-icons](https://github.com/react-icons/react-icons). It automatically detects the `react-icons` components you use, generates a single SVG spritesheet containing them, and rewrites your code to reference those symbols via `<use>`. This approach both shrinks your bundle (no more inlined React components for every icon) and reduces runtime overhead, since React no longer has to reconcile large, nested SVG trees.
4
+
5
+ > [!NOTE]
6
+ > Only Vite is currently supported. If you'd like to see a Webpack or Turbopack implementation, please open an issue!
7
+
8
+ ## Motivation
9
+
10
+ By default, when you use `react-icons`, each icon is a React component. For example:
11
+
12
+ ```tsx
13
+ import { LuWheat } from "react-icons/lu";
14
+
15
+ export function Example() {
16
+ return <LuWheat />;
17
+ }
18
+ ```
19
+
20
+ looks harmless, but at build time this compiles to something like:
21
+
22
+ ```javascript
23
+ import { b as e } from './iconBase-BU3rGdXB.js'
24
+
25
+ function t(t) {
26
+ return e({
27
+ tag: `svg`, attr: { viewBox: `0 0 640 512` }, child: [{
28
+ tag: `path`, attr: {
29
+ d: `M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z`
30
+ }, child: []
31
+ }]
32
+ })(t)
33
+ }
34
+
35
+ function n(t) {
36
+ return e({
37
+ tag: `svg`, attr: { viewBox: `0 0 496 512` }, child: [{
38
+ tag: `path`, attr: {
39
+ d: `M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z`
40
+ }, child: []
41
+ }]
42
+ })(t)
43
+ }
44
+
45
+ // ... potentially dozens more child components.
46
+
47
+ export { t, n };
48
+ ```
49
+
50
+ That means:
51
+
52
+ - Every icon adds hundreds of characters of JSX to your bundle.
53
+ - At runtime, React must create and diff dozens of DOM nodes for every icon render.
54
+ - If you render 100 icons, React is reconciling potentially thousands of <path> elements.
55
+
56
+ Now compare that to output when using a spritesheet with `<use>`:
57
+
58
+ ```tsx
59
+ import { LuWheat } from "react-icons/lu";
60
+
61
+ export function Example() {
62
+ return <LuWheat />;
63
+ }
64
+ ```
65
+
66
+ ```javascript
67
+ import { b as n } from './icon-FONPSuqX.js';
68
+
69
+ var r = e(t());
70
+ const i = () => (0, r.jsx)(n, { iconId: `ri-LuWheat` });
71
+ export { i as IconWheat };
72
+ ```
73
+
74
+ `icon-FONPSuqX.js` in this case is our simple icon wrapper, which looks like this:
75
+
76
+ ```javascript
77
+ var n = e(t());
78
+ const r = ({ iconId: e, ...t }) => {
79
+ let r = `assets/react-icons-sprite-C-JClopV.svg#${e}`;
80
+ return (0, n.jsxs)(`svg`, {
81
+ height: `1em`,
82
+ width: `1em`,
83
+ preserveAspectRatio: `xMidYMid meet`,
84
+ viewBox: `0 0 24 24`, ...t,
85
+ children: [(0, n.jsx)(`title`, { children: e }), (0, n.jsx)(`use`, { href: r })]
86
+ })
87
+ };
88
+ export { r as b };
89
+ ```
90
+
91
+ Runtime difference:
92
+
93
+ - React only reconciles two DOM nodes (`<svg>` + `<use>`).
94
+ - All heavy path data lives in the static spritesheet once, not duplicated across components.
95
+ - Updating 100 icons is as cheap as updating 100 `<use>` tags.
96
+
97
+ This is a big win when you’re rendering icons in lists, tables, or maps where dozens or hundreds of them appear at once.
98
+
99
+ ## Installation
100
+
101
+ Install the plugin via npm or yarn:
102
+
103
+ ```bash
104
+ npm install --save-dev react-icons-sprite
105
+ ```
106
+
107
+ Only Vite is currently supported. You only need to add the plugin to the `plugins` array.
108
+
109
+ ```typescript
110
+ // vite.config.ts
111
+ import { defineConfig } from 'vite';
112
+ import { reactIconsSprite } from 'react-icons-sprite/vite';
113
+
114
+ const viteConfig = defineConfig({
115
+ // ... rest of config
116
+ plugins: [
117
+ reactIconsSprite(),
118
+ ],
119
+ });
120
+ ```
121
+
122
+ ## How it works
123
+
124
+ In **development mode**, the plugin does nothing special. Icons are rendered as they normally would from `react-icons`. This keeps hot module replacement (HMR) snappy — there’s no extra parsing of the codebase or regenerating of the sprite on every save. If the plugin were to build the sprite during dev, it would need to constantly scan for `react-icons` imports and rebuild the sheet, which is expensive and slows down iteration. So, in dev, you get the normal `react-icons` behavior.
125
+
126
+ In **build mode**, the plugin transforms your code. It parses each module, looks for imports from `react-icons/*`, and rewrites the JSX. Instead of rendering full inline `<svg>` trees, it replaces them with `<ReactIconsSpriteIcon iconId="..." />`. While doing this, it collects every unique icon used across the project. After the bundling step, the plugin renders all those icons once to static markup and generates a single SVG file containing `<symbol>` definitions for each one. Finally, it rewrites your bundle to point every `<ReactIconsSpriteIcon>` at that spritesheet using a `<use>` tag.
127
+
128
+ The result: during development you keep fast feedback loops, and in production you ship a single optimized sprite file with lightweight `<use>` references.
129
+
130
+ ## Contributing
131
+
132
+ Contributions are welcome! Feel free to open an issue or submit a pull request.
133
+
134
+ ## License
135
+
136
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
137
+
package/dist/icon.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import React from "react";
3
+
4
+ //#region src/icon.d.ts
5
+ type IconProps = React.SVGProps<SVGSVGElement> & {
6
+ iconId: string;
7
+ };
8
+ declare const ReactIconsSpriteIcon: ({
9
+ iconId,
10
+ ...rest
11
+ }: IconProps) => react_jsx_runtime0.JSX.Element;
12
+ //#endregion
13
+ export { IconProps, ReactIconsSpriteIcon };
package/dist/icon.js ADDED
@@ -0,0 +1,18 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+
3
+ //#region src/icon.tsx
4
+ const ReactIconsSpriteIcon = ({ iconId,...rest }) => {
5
+ const spriteHref = "__SPRITE_URL_PLACEHOLDER__";
6
+ const iconHref = `${spriteHref}#${iconId}`;
7
+ return /* @__PURE__ */ jsx("svg", {
8
+ height: "1em",
9
+ width: "1em",
10
+ preserveAspectRatio: "xMidYMid meet",
11
+ viewBox: "0 0 24 24",
12
+ ...rest,
13
+ children: /* @__PURE__ */ jsx("use", { href: iconHref })
14
+ });
15
+ };
16
+
17
+ //#endregion
18
+ export { ReactIconsSpriteIcon };
@@ -0,0 +1,13 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/vite/plugin.d.ts
4
+ type ReactIconsSpriteVitePluginOptions = {
5
+ /**
6
+ * Append a cache-busting query parameter to the emitted sprite URL.
7
+ * Example: { spriteUrlVersion: "1.2.3" } -> "/assets/react-icons-sprite.svg?v=1.2.3"
8
+ */
9
+ spriteUrlVersion?: string;
10
+ };
11
+ declare const reactIconsSprite: (options?: ReactIconsSpriteVitePluginOptions) => Plugin;
12
+ //#endregion
13
+ export { ReactIconsSpriteVitePluginOptions, reactIconsSprite };
@@ -0,0 +1,243 @@
1
+ import { createElement } from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import * as t from "@babel/types";
4
+ import { parse } from "@babel/parser";
5
+ import _traverse from "@babel/traverse";
6
+ import _generate from "@babel/generator";
7
+
8
+ //#region src/core.ts
9
+ const traverse = _traverse.default ?? _traverse;
10
+ const generate = _generate.default ?? _generate;
11
+ const PLACEHOLDER = "__SPRITE_URL_PLACEHOLDER__";
12
+ const ICON_SOURCE = "react-icons-sprite";
13
+ const ICON_COMPONENT_NAME = "ReactIconsSpriteIcon";
14
+ const parseAst = (code, filename = "module.tsx") => {
15
+ return parse(code, {
16
+ sourceType: "module",
17
+ plugins: ["jsx", "typescript"],
18
+ sourceFilename: filename
19
+ });
20
+ };
21
+ const collectReactIconImports = (ast) => {
22
+ const map = /* @__PURE__ */ new Map();
23
+ for (const node of ast.program.body) if (t.isImportDeclaration(node) && /^react-icons\/[\w-]+$/.test(node.source.value) && node.importKind !== "type") {
24
+ const pack = node.source.value;
25
+ for (const spec of node.specifiers) if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported) && t.isIdentifier(spec.local) && spec.importKind !== "type") {
26
+ const exportName = spec.imported.name;
27
+ const localName = spec.local.name;
28
+ map.set(localName, {
29
+ pack,
30
+ exportName,
31
+ decl: node,
32
+ spec
33
+ });
34
+ }
35
+ }
36
+ return map;
37
+ };
38
+ const findExistingIconImport = (ast) => {
39
+ let iconLocalName = ICON_COMPONENT_NAME;
40
+ let hasIconImport = false;
41
+ for (const n of ast.program.body) if (t.isImportDeclaration(n) && n.source.value === ICON_SOURCE) {
42
+ for (const s of n.specifiers) if (t.isImportSpecifier(s) && t.isIdentifier(s.imported, { name: ICON_COMPONENT_NAME })) {
43
+ hasIconImport = true;
44
+ iconLocalName = t.isIdentifier(s.local) ? s.local.name : ICON_COMPONENT_NAME;
45
+ break;
46
+ }
47
+ if (hasIconImport) break;
48
+ }
49
+ return {
50
+ hasIconImport,
51
+ iconLocalName
52
+ };
53
+ };
54
+ const replaceJsxWithSprite = (ast, localNameToImport, iconLocalName, register) => {
55
+ const usedLocalNames = /* @__PURE__ */ new Set();
56
+ let anyReplacements = false;
57
+ const isAlreadyIcon = (name) => t.isJSXIdentifier(name) && name.name === iconLocalName;
58
+ traverse(ast, {
59
+ JSXOpeningElement(path) {
60
+ const name = path.node.name;
61
+ if (!t.isJSXIdentifier(name)) return;
62
+ const local = name.name;
63
+ const meta = localNameToImport.get(local);
64
+ if (!meta) return;
65
+ if (isAlreadyIcon(name)) return;
66
+ path.node.name = t.jSXIdentifier(iconLocalName);
67
+ const hasIconId = path.node.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: "iconId" }));
68
+ if (!hasIconId) path.node.attributes.unshift(t.jSXAttribute(t.jSXIdentifier("iconId"), t.stringLiteral(`ri-${meta.exportName}`)));
69
+ usedLocalNames.add(local);
70
+ anyReplacements = true;
71
+ register(meta.pack, meta.exportName);
72
+ },
73
+ JSXClosingElement(path) {
74
+ const name = path.node.name;
75
+ if (!t.isJSXIdentifier(name)) return;
76
+ const local = name.name;
77
+ if (!localNameToImport.has(local)) return;
78
+ if (!isAlreadyIcon(name)) path.node.name = t.jSXIdentifier(iconLocalName);
79
+ }
80
+ });
81
+ return {
82
+ usedLocalNames,
83
+ anyReplacements
84
+ };
85
+ };
86
+ const insertIconImport = (ast, iconLocalName = ICON_COMPONENT_NAME) => {
87
+ const firstImportIndex = ast.program.body.findIndex((n) => t.isImportDeclaration(n));
88
+ const iconImportDecl = t.importDeclaration([t.importSpecifier(t.identifier(iconLocalName), t.identifier(ICON_COMPONENT_NAME))], t.stringLiteral(ICON_SOURCE));
89
+ if (firstImportIndex >= 0) ast.program.body.splice(firstImportIndex + 1, 0, iconImportDecl);
90
+ else ast.program.body.unshift(iconImportDecl);
91
+ };
92
+ const pruneUsedSpecifiers = (ast, localNameToImport, usedLocalNames) => {
93
+ for (const { decl } of new Set([...localNameToImport.values()])) decl.specifiers = decl.specifiers.filter((s) => {
94
+ if (!t.isImportSpecifier(s) || !t.isIdentifier(s.local)) return true;
95
+ return !usedLocalNames.has(s.local.name);
96
+ });
97
+ ast.program.body = ast.program.body.filter((n) => !t.isImportDeclaration(n) || n.specifiers.length > 0);
98
+ };
99
+ const generateCode = (ast, origCode, id) => {
100
+ const { code, map } = generate(ast, {
101
+ sourceMaps: true,
102
+ sourceFileName: id
103
+ }, origCode);
104
+ return {
105
+ code,
106
+ map
107
+ };
108
+ };
109
+ const transformModule = (code, id, register) => {
110
+ const ast = parseAst(code, id);
111
+ const localNameToImport = collectReactIconImports(ast);
112
+ if (localNameToImport.size === 0) return {
113
+ code,
114
+ map: null,
115
+ anyReplacements: false
116
+ };
117
+ const { hasIconImport, iconLocalName } = findExistingIconImport(ast);
118
+ const { usedLocalNames, anyReplacements } = replaceJsxWithSprite(ast, localNameToImport, iconLocalName, register);
119
+ if (!anyReplacements) return {
120
+ code,
121
+ map: null,
122
+ anyReplacements: false
123
+ };
124
+ if (!hasIconImport) insertIconImport(ast, iconLocalName);
125
+ pruneUsedSpecifiers(ast, localNameToImport, usedLocalNames);
126
+ return {
127
+ ...generateCode(ast, code, id),
128
+ anyReplacements
129
+ };
130
+ };
131
+ const PRESENTATION_ATTRS = new Set([
132
+ "fill",
133
+ "stroke",
134
+ "stroke-width",
135
+ "stroke-linecap",
136
+ "stroke-linejoin",
137
+ "stroke-miterlimit",
138
+ "stroke-dasharray",
139
+ "stroke-dashoffset",
140
+ "stroke-opacity",
141
+ "fill-rule",
142
+ "fill-opacity",
143
+ "color",
144
+ "opacity",
145
+ "shape-rendering",
146
+ "vector-effect"
147
+ ]);
148
+ const ATTR_RE = /([a-zA-Z_:.-]+)\s*=\s*"([^"]*)"/g;
149
+ const renderOneIcon = async (pack, exportName) => {
150
+ const mod = await import(
151
+ /* @vite-ignore */
152
+ pack
153
+ );
154
+ const Comp = mod[exportName];
155
+ if (!Comp) throw new Error(`Icon export not found: ${pack} -> ${exportName}`);
156
+ const id = `ri-${exportName}`;
157
+ const html = renderToStaticMarkup(createElement(Comp));
158
+ const viewBox = html.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24";
159
+ const svgAttrsRaw = html.match(/^<svg\b([^>]*)>/i)?.[1] ?? "";
160
+ const attrs = [];
161
+ for (const [, k, v] of svgAttrsRaw.matchAll(ATTR_RE)) {
162
+ const key = k.toLowerCase();
163
+ if (PRESENTATION_ATTRS.has(key)) attrs.push(`${key}="${v}"`);
164
+ }
165
+ const inner = html.replace(/^<svg[^>]*>/i, "").replace(/<\/svg>\s*$/i, "");
166
+ const stylePart = attrs.length ? ` ${attrs.join(" ")}` : "";
167
+ const symbol = `<symbol id="${id}" viewBox="${viewBox}"${stylePart}>${inner}</symbol>`;
168
+ return {
169
+ id,
170
+ symbol
171
+ };
172
+ };
173
+ const buildSprite = async (icons) => {
174
+ const rendered = await Promise.all(Array.from(icons).map(({ pack, exportName }) => renderOneIcon(pack, exportName)));
175
+ const symbols = rendered.map((r) => r.symbol).join("");
176
+ return `<svg xmlns="http://www.w3.org/2000/svg"><defs>${symbols}</defs></svg>`;
177
+ };
178
+ const createCollector = () => {
179
+ const set = /* @__PURE__ */ new Map();
180
+ return {
181
+ add(pack, exportName) {
182
+ set.set(`${pack}:${exportName}`, {
183
+ pack,
184
+ exportName
185
+ });
186
+ },
187
+ toList() {
188
+ return Array.from(set.values());
189
+ },
190
+ clear() {
191
+ set.clear();
192
+ }
193
+ };
194
+ };
195
+
196
+ //#endregion
197
+ //#region src/vite/plugin.ts
198
+ const reactIconsSprite = (options = {}) => {
199
+ const { spriteUrlVersion } = options;
200
+ const collector = createCollector();
201
+ return {
202
+ name: "vite-plugin-react-icons-sprite",
203
+ enforce: "pre",
204
+ apply: "build",
205
+ buildStart() {
206
+ collector.clear();
207
+ },
208
+ transform(code, id) {
209
+ const cleanId = id.split("?", 1)[0];
210
+ if (!/\.(mjs|cjs|js|jsx|ts|tsx)$/.test(cleanId)) return null;
211
+ if (!/from\s+['"]react-icons\//.test(code)) return null;
212
+ try {
213
+ const { code: next, map, anyReplacements } = transformModule(code, id, (pack, exportName) => {
214
+ collector.add(pack, exportName);
215
+ });
216
+ if (!anyReplacements) return null;
217
+ return {
218
+ code: next,
219
+ map
220
+ };
221
+ } catch (error) {
222
+ console.error(error);
223
+ return null;
224
+ }
225
+ },
226
+ async generateBundle(_options, bundle) {
227
+ const spriteXml = await buildSprite(collector.toList());
228
+ const assetId = this.emitFile({
229
+ type: "asset",
230
+ name: "react-icons-sprite.svg",
231
+ source: spriteXml
232
+ });
233
+ const fileName = this.getFileName(assetId);
234
+ const finalUrl = spriteUrlVersion && spriteUrlVersion.length > 0 ? `/${fileName}?v=${encodeURIComponent(spriteUrlVersion)}` : `/${fileName}`;
235
+ for (const [, item] of Object.entries(bundle)) if (item.type === "chunk" && typeof item.code === "string") {
236
+ if (item.code.includes(PLACEHOLDER)) item.code = item.code.replaceAll(PLACEHOLDER, finalUrl);
237
+ }
238
+ }
239
+ };
240
+ };
241
+
242
+ //#endregion
243
+ export { reactIconsSprite };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "react-icons-sprite",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "",
7
+ "author": "Jure Rotar <hello@jurerotar.com>",
8
+ "homepage": "https://github.com/jurerotar/react-icons-sprite#README",
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/icon.d.ts",
15
+ "import": "./dist/icon.js",
16
+ "default": "./dist/icon.js"
17
+ },
18
+ "./vite": {
19
+ "types": "./dist/vite/plugin.d.ts",
20
+ "import": "./dist/vite/plugin.js",
21
+ "default": "./dist/vite/plugin.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/jurerotar/react-icons-sprite.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/jurerotar/react-icons-sprite/issues"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "dev": "tsdown --watch",
40
+ "format": "biome format . --write",
41
+ "lint": "biome lint .",
42
+ "lint:fix": "biome lint . --write",
43
+ "prepublishOnly": "npm run build",
44
+ "release": "npm publish --access public"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">= 16",
48
+ "react-dom": ">= 16",
49
+ "react-icons": ">= 5"
50
+ },
51
+ "devDependencies": {
52
+ "@babel/generator": "7.28.3",
53
+ "@babel/parser": "7.28.3",
54
+ "@babel/traverse": "7.28.3",
55
+ "@babel/types": "7.28.2",
56
+ "@biomejs/biome": "2.2.2",
57
+ "@types/babel__generator": "7.27.0",
58
+ "@types/babel__traverse": "7.28.0",
59
+ "@types/react-dom": "19.1.9",
60
+ "react": "19.1.1",
61
+ "react-dom": "19.1.1",
62
+ "react-icons": "5.5.0",
63
+ "tsdown": "0.14.2",
64
+ "typescript": "5.9.2",
65
+ "vite": "7.1.3"
66
+ },
67
+ "keywords": [
68
+ "vite",
69
+ "vite-plugin",
70
+ "rollup",
71
+ "rolldown",
72
+ "react-icons",
73
+ "react",
74
+ "icons",
75
+ "svg",
76
+ "svg-sprite",
77
+ "spritesheet",
78
+ "icon-sprite"
79
+ ]
80
+ }