react-icons-sprite 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # react-icons-sprite
2
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!
3
+ `react-icons-sprite` is a lightweight plugin for Vite and Webpack, 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.
7
4
 
8
5
  ## Motivation
9
6
 
@@ -104,19 +101,54 @@ Install the plugin via npm or yarn:
104
101
  npm install --save-dev react-icons-sprite
105
102
  ```
106
103
 
107
- Only Vite is currently supported. You only need to add the plugin to the `plugins` array.
104
+ ### Vite
105
+ Add the plugin to the `plugins` array in your Vite config.
108
106
 
109
107
  ```typescript
110
108
  // vite.config.ts
111
109
  import { defineConfig } from 'vite';
112
110
  import { reactIconsSprite } from 'react-icons-sprite/vite';
113
111
 
114
- const viteConfig = defineConfig({
115
- // ... rest of config
112
+ export default defineConfig({
113
+ plugins: [reactIconsSprite()],
114
+ });
115
+ ```
116
+
117
+ ### Webpack
118
+ Add the loader to transform modules that import from `react-icons/*` and install the plugin to emit the sprite and rewrite the placeholder URL.
119
+
120
+ ```js
121
+ // webpack.config.js (v5)
122
+ const path = require('path');
123
+ const { reactIconsSprite } = require('react-icons-sprite/webpack');
124
+
125
+ module.exports = {
126
+ mode: 'production',
127
+ // ... your existing config
128
+ module: {
129
+ rules: [
130
+ {
131
+ test: /\.(mjs|cjs|js|jsx|ts|tsx)$/,
132
+ exclude: /node_modules/,
133
+ use: [
134
+ {
135
+ loader: require.resolve('react-icons-sprite/webpack/loader'),
136
+ },
137
+ // put your ts/tsx loader after ours (e.g. babel-loader or ts-loader)
138
+ ],
139
+ },
140
+ ],
141
+ },
116
142
  plugins: [
117
- reactIconsSprite(),
143
+ reactIconsSprite({
144
+ // optional: fileName: 'icons.svg'
145
+ }),
118
146
  ],
119
- });
147
+ output: {
148
+ // ensure your publicPath is set correctly if you deploy under a sub-path
149
+ // publicPath: '/',
150
+ },
151
+ };
120
152
  ```
121
153
 
122
154
  ## How it works
@@ -134,4 +166,3 @@ Contributions are welcome! Feel free to open an issue or submit a pull request.
134
166
  ## License
135
167
 
136
168
  This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
137
-
@@ -0,0 +1,7 @@
1
+ import { r as createCollector } from "./core-CcX_8jJU.mjs";
2
+
3
+ //#region src/collector.ts
4
+ const collector = createCollector();
5
+
6
+ //#endregion
7
+ export { collector as t };
@@ -64,8 +64,7 @@ const replaceJsxWithSprite = (ast, localNameToImport, iconLocalName, register) =
64
64
  if (!meta) return;
65
65
  if (isAlreadyIcon(name)) return;
66
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}`)));
67
+ if (!path.node.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: "iconId" }))) path.node.attributes.unshift(t.jSXAttribute(t.jSXIdentifier("iconId"), t.stringLiteral(`ri-${meta.exportName}`)));
69
68
  usedLocalNames.add(local);
70
69
  anyReplacements = true;
71
70
  register(meta.pack, meta.exportName);
@@ -147,11 +146,10 @@ const PRESENTATION_ATTRS = new Set([
147
146
  ]);
148
147
  const ATTR_RE = /([a-zA-Z_:.-]+)\s*=\s*"([^"]*)"/g;
149
148
  const renderOneIcon = async (pack, exportName) => {
150
- const mod = await import(
149
+ const Comp = (await import(
151
150
  /* @vite-ignore */
152
151
  pack
153
- );
154
- const Comp = mod[exportName];
152
+ ))[exportName];
155
153
  if (!Comp) throw new Error(`Icon export not found: ${pack} -> ${exportName}`);
156
154
  const id = `ri-${exportName}`;
157
155
  const html = renderToStaticMarkup(createElement(Comp));
@@ -163,17 +161,13 @@ const renderOneIcon = async (pack, exportName) => {
163
161
  if (PRESENTATION_ATTRS.has(key)) attrs.push(`${key}="${v}"`);
164
162
  }
165
163
  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
164
  return {
169
165
  id,
170
- symbol
166
+ symbol: `<symbol id="${id}" viewBox="${viewBox}"${attrs.length ? ` ${attrs.join(" ")}` : ""}>${inner}</symbol>`
171
167
  };
172
168
  };
173
169
  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>`;
170
+ return `<svg xmlns="http://www.w3.org/2000/svg"><defs>${(await Promise.all(Array.from(icons).map(({ pack, exportName }) => renderOneIcon(pack, exportName)))).map((r) => r.symbol).join("")}</defs></svg>`;
177
171
  };
178
172
  const createCollector = () => {
179
173
  const set = /* @__PURE__ */ new Map();
@@ -194,52 +188,4 @@ const createCollector = () => {
194
188
  };
195
189
 
196
190
  //#endregion
197
- //#region src/vite/plugin.ts
198
- const reactIconsSprite = (options = {}) => {
199
- const { spriteUrlVersion, fileName } = 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 emitFileOptions = {
229
- type: "asset",
230
- source: spriteXml
231
- };
232
- if (fileName) emitFileOptions.fileName = fileName;
233
- else emitFileOptions.name = "react-icons-sprite.svg";
234
- const assetId = this.emitFile(emitFileOptions);
235
- const name = this.getFileName(assetId);
236
- const finalUrl = spriteUrlVersion && spriteUrlVersion.length > 0 ? `/${name}?v=${encodeURIComponent(spriteUrlVersion)}` : `/${name}`;
237
- for (const [, item] of Object.entries(bundle)) if (item.type === "chunk" && typeof item.code === "string") {
238
- if (item.code.includes(PLACEHOLDER)) item.code = item.code.replaceAll(PLACEHOLDER, finalUrl);
239
- }
240
- }
241
- };
242
- };
243
-
244
- //#endregion
245
- export { reactIconsSprite };
191
+ export { transformModule as i, buildSprite as n, createCollector as r, PLACEHOLDER as t };
@@ -0,0 +1,12 @@
1
+ import { JSX, SVGProps } from "react";
2
+
3
+ //#region src/icon.d.ts
4
+ type IconProps = SVGProps<SVGSVGElement> & {
5
+ iconId: string;
6
+ };
7
+ declare const ReactIconsSpriteIcon: ({
8
+ iconId,
9
+ ...rest
10
+ }: IconProps) => JSX.Element;
11
+ //#endregion
12
+ export { IconProps, ReactIconsSpriteIcon };
@@ -2,8 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
 
3
3
  //#region src/icon.tsx
4
4
  const ReactIconsSpriteIcon = ({ iconId,...rest }) => {
5
- const spriteHref = "__SPRITE_URL_PLACEHOLDER__";
6
- const iconHref = `${spriteHref}#${iconId}`;
5
+ const iconHref = `__SPRITE_URL_PLACEHOLDER__#${iconId}`;
7
6
  return /* @__PURE__ */ jsx("svg", {
8
7
  height: "1em",
9
8
  width: "1em",
@@ -2,11 +2,6 @@ import { Plugin } from "vite";
2
2
 
3
3
  //#region src/vite/plugin.d.ts
4
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
5
  /**
11
6
  * If passed, this exact string will be used for the emitted file name.
12
7
  * If fileName is omitted, name will be generated as `react-icons-sprite-[hash].svg.
@@ -0,0 +1,52 @@
1
+ import { i as transformModule, n as buildSprite, r as createCollector, t as PLACEHOLDER } from "../core-CcX_8jJU.mjs";
2
+ import { createHash } from "node:crypto";
3
+
4
+ //#region src/vite/plugin.ts
5
+ const reactIconsSprite = (options = {}) => {
6
+ const { fileName } = options;
7
+ const collector = createCollector();
8
+ return {
9
+ name: "vite-plugin-react-icons-sprite",
10
+ enforce: "pre",
11
+ apply: "build",
12
+ buildStart() {
13
+ collector.clear();
14
+ },
15
+ transform(code, id) {
16
+ const cleanId = id.split("?", 1)[0];
17
+ if (!/\.(mjs|cjs|js|jsx|ts|tsx)$/.test(cleanId)) return null;
18
+ if (!/from\s+['"]react-icons\//.test(code)) return null;
19
+ try {
20
+ const { code: next, map, anyReplacements } = transformModule(code, id, (pack, exportName) => {
21
+ collector.add(pack, exportName);
22
+ });
23
+ if (!anyReplacements) return null;
24
+ return {
25
+ code: next,
26
+ map
27
+ };
28
+ } catch (error) {
29
+ console.error(error);
30
+ return null;
31
+ }
32
+ },
33
+ async generateBundle(_options, bundle) {
34
+ const spriteXml = await buildSprite(collector.toList());
35
+ const generatedHash = createHash("sha256").update(spriteXml).digest("hex").slice(0, 8);
36
+ const emitFileOptions = {
37
+ type: "asset",
38
+ source: spriteXml
39
+ };
40
+ if (fileName) emitFileOptions.fileName = fileName;
41
+ else emitFileOptions.name = "react-icons-sprite.svg";
42
+ const assetId = this.emitFile(emitFileOptions);
43
+ const finalUrl = `/${this.getFileName(assetId)}?v=${encodeURIComponent(generatedHash)}`;
44
+ for (const [, item] of Object.entries(bundle)) if (item.type === "chunk" && typeof item.code === "string") {
45
+ if (item.code.includes(PLACEHOLDER)) item.code = item.code.replaceAll(PLACEHOLDER, finalUrl);
46
+ }
47
+ }
48
+ };
49
+ };
50
+
51
+ //#endregion
52
+ export { reactIconsSprite };
@@ -0,0 +1,6 @@
1
+ import { LoaderDefinitionFunction } from "webpack";
2
+
3
+ //#region src/webpack/loader.d.ts
4
+ declare const reactIconsSpriteLoader: LoaderDefinitionFunction;
5
+ //#endregion
6
+ export { reactIconsSpriteLoader as default };
@@ -0,0 +1,22 @@
1
+ import { i as transformModule } from "../core-CcX_8jJU.mjs";
2
+ import { t as collector } from "../collector-CbA7qCnf.mjs";
3
+
4
+ //#region src/webpack/loader.ts
5
+ const reactIconsSpriteLoader = async function(source) {
6
+ const id = this.resourcePath;
7
+ try {
8
+ if (!/\.(mjs|cjs|js|jsx|ts|tsx)$/i.test(id) || !/from\s+['"]react-icons\//.test(String(source))) return source;
9
+ const { code, anyReplacements } = transformModule(String(source), id, (pack, exportName) => {
10
+ collector.add(pack, exportName);
11
+ });
12
+ if (!anyReplacements) return source;
13
+ return code;
14
+ } catch (err) {
15
+ this.emitWarning(/* @__PURE__ */ new Error(`[react-icons-sprite] Failed to transform ${id}: ${String(err)}`));
16
+ return source;
17
+ }
18
+ };
19
+ var loader_default = reactIconsSpriteLoader;
20
+
21
+ //#endregion
22
+ export { loader_default as default };
@@ -0,0 +1,18 @@
1
+ import { Compiler, WebpackPluginInstance } from "webpack";
2
+
3
+ //#region src/webpack/plugin.d.ts
4
+ type ReactIconsSpriteWebpackPluginOptions = {
5
+ /**
6
+ * If passed, this exact string will be used for the emitted file name.
7
+ * If fileName is omitted, name will be generated as `react-icons-sprite.svg`.
8
+ * This is useful when, for example, multiple sprite sheets are generated during client and server builds.
9
+ */
10
+ fileName?: string;
11
+ };
12
+ declare class ReactIconsSpriteWebpackPlugin implements WebpackPluginInstance {
13
+ private readonly fileName?;
14
+ constructor(options?: ReactIconsSpriteWebpackPluginOptions);
15
+ apply(compiler: Compiler): void;
16
+ }
17
+ //#endregion
18
+ export { ReactIconsSpriteWebpackPlugin, ReactIconsSpriteWebpackPluginOptions };
@@ -0,0 +1,46 @@
1
+ import { n as buildSprite, t as PLACEHOLDER } from "../core-CcX_8jJU.mjs";
2
+ import { t as collector } from "../collector-CbA7qCnf.mjs";
3
+ import { createHash } from "node:crypto";
4
+
5
+ //#region src/webpack/plugin.ts
6
+ var ReactIconsSpriteWebpackPlugin = class {
7
+ fileName;
8
+ constructor(options = {}) {
9
+ this.fileName = options.fileName;
10
+ }
11
+ apply(compiler) {
12
+ const pluginName = "react-icons-sprite-webpack-plugin";
13
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
14
+ collector.clear();
15
+ const stage = compiler.webpack?.Compilation ? compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE : 4e3;
16
+ compilation.hooks.processAssets.tapPromise({
17
+ name: pluginName,
18
+ stage
19
+ }, async () => {
20
+ const spriteXml = await buildSprite(collector.toList());
21
+ const generatedHash = createHash("sha256").update(spriteXml).digest("hex").slice(0, 8);
22
+ const name = this.fileName ?? "react-icons-sprite.svg";
23
+ const RawSource = compiler.webpack?.sources?.RawSource;
24
+ if (!RawSource) throw new Error("[react-icons-sprite] Unable to access webpack RawSource");
25
+ compilation.emitAsset(name, new RawSource(spriteXml));
26
+ const outputPublicPath = compilation.outputOptions?.publicPath;
27
+ let base = "";
28
+ if (typeof outputPublicPath === "string" && outputPublicPath !== "auto") base = outputPublicPath.endsWith("/") ? outputPublicPath : `${outputPublicPath}/`;
29
+ else base = "/";
30
+ const finalUrl = `${base}${name}?v=${encodeURIComponent(generatedHash)}`;
31
+ for (const asset of compilation.getAssets()) {
32
+ const filename = asset.name;
33
+ if (!/\.(js|mjs|cjs)$/i.test(filename)) continue;
34
+ const src = asset.source.source();
35
+ if (typeof src !== "string") continue;
36
+ if (!src.includes(PLACEHOLDER)) continue;
37
+ const next = src.replaceAll(PLACEHOLDER, finalUrl);
38
+ compilation.updateAsset(filename, new RawSource(next));
39
+ }
40
+ });
41
+ });
42
+ }
43
+ };
44
+
45
+ //#endregion
46
+ export { ReactIconsSpriteWebpackPlugin };
package/package.json CHANGED
@@ -1,24 +1,39 @@
1
1
  {
2
2
  "name": "react-icons-sprite",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "",
7
7
  "author": "Jure Rotar <hello@jurerotar.com>",
8
8
  "homepage": "https://github.com/jurerotar/react-icons-sprite#README",
9
- "main": "dist/index.js",
10
- "types": "dist/index.d.ts",
9
+ "main": "dist/icon.mjs",
10
+ "types": "dist/icon.d.mts",
11
11
  "sideEffects": false,
12
12
  "exports": {
13
13
  ".": {
14
- "types": "./dist/icon.d.ts",
15
- "import": "./dist/icon.js",
16
- "default": "./dist/icon.js"
14
+ "types": "./dist/icon.d.mts",
15
+ "import": "./dist/icon.mjs",
16
+ "default": "./dist/icon.mjs"
17
17
  },
18
18
  "./vite": {
19
- "types": "./dist/vite/plugin.d.ts",
20
- "import": "./dist/vite/plugin.js",
21
- "default": "./dist/vite/plugin.js"
19
+ "types": "./dist/vite/plugin.d.mts",
20
+ "import": "./dist/vite/plugin.mjs",
21
+ "default": "./dist/vite/plugin.mjs"
22
+ },
23
+ "./webpack": {
24
+ "types": "./dist/webpack/plugin.d.mts",
25
+ "import": "./dist/webpack/plugin.mjs",
26
+ "default": "./dist/webpack/plugin.mjs"
27
+ },
28
+ "./webpack/loader": {
29
+ "types": "./dist/webpack/loader.d.mts",
30
+ "import": "./dist/webpack/loader.mjs",
31
+ "default": "./dist/webpack/loader.mjs"
32
+ },
33
+ "./turbopack": {
34
+ "types": "./dist/turbopack/plugin.d.mts",
35
+ "import": "./dist/turbopack/plugin.mjs",
36
+ "default": "./dist/turbopack/plugin.mjs"
22
37
  }
23
38
  },
24
39
  "publishConfig": {
@@ -49,24 +64,28 @@
49
64
  "react-icons": ">= 5"
50
65
  },
51
66
  "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",
67
+ "@babel/generator": "7.28.5",
68
+ "@babel/parser": "7.28.5",
69
+ "@babel/traverse": "7.28.5",
70
+ "@babel/types": "7.28.5",
71
+ "@biomejs/biome": "2.3.5",
57
72
  "@types/babel__generator": "7.27.0",
58
73
  "@types/babel__traverse": "7.28.0",
59
- "@types/react-dom": "19.1.9",
60
- "react": "19.1.1",
61
- "react-dom": "19.1.1",
74
+ "@types/node": "24.9.1",
75
+ "@types/react-dom": "19.2.3",
76
+ "react": "19.2.0",
77
+ "react-dom": "19.2.0",
62
78
  "react-icons": "5.5.0",
63
- "tsdown": "0.14.2",
64
- "typescript": "5.9.2",
65
- "vite": "7.1.3"
79
+ "tsdown": "0.16.3",
80
+ "typescript": "5.9.3",
81
+ "vite": "7.2.2",
82
+ "webpack": "5.102.1"
66
83
  },
67
84
  "keywords": [
68
85
  "vite",
69
86
  "vite-plugin",
87
+ "webpack",
88
+ "webpack-plugin",
70
89
  "rollup",
71
90
  "rolldown",
72
91
  "react-icons",
package/dist/icon.d.ts DELETED
@@ -1,13 +0,0 @@
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 };