react-icons-sprite 0.3.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 };
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import { createElement } from "react";
3
2
  import { renderToStaticMarkup } from "react-dom/server";
4
3
  import * as t from "@babel/types";
@@ -189,52 +188,4 @@ const createCollector = () => {
189
188
  };
190
189
 
191
190
  //#endregion
192
- //#region src/vite/plugin.ts
193
- const reactIconsSprite = (options = {}) => {
194
- const { fileName } = options;
195
- const collector = createCollector();
196
- return {
197
- name: "vite-plugin-react-icons-sprite",
198
- enforce: "pre",
199
- apply: "build",
200
- buildStart() {
201
- collector.clear();
202
- },
203
- transform(code, id) {
204
- const cleanId = id.split("?", 1)[0];
205
- if (!/\.(mjs|cjs|js|jsx|ts|tsx)$/.test(cleanId)) return null;
206
- if (!/from\s+['"]react-icons\//.test(code)) return null;
207
- try {
208
- const { code: next, map, anyReplacements } = transformModule(code, id, (pack, exportName) => {
209
- collector.add(pack, exportName);
210
- });
211
- if (!anyReplacements) return null;
212
- return {
213
- code: next,
214
- map
215
- };
216
- } catch (error) {
217
- console.error(error);
218
- return null;
219
- }
220
- },
221
- async generateBundle(_options, bundle) {
222
- const spriteXml = await buildSprite(collector.toList());
223
- const generatedHash = createHash("sha256").update(spriteXml).digest("hex").slice(0, 8);
224
- const emitFileOptions = {
225
- type: "asset",
226
- source: spriteXml
227
- };
228
- if (fileName) emitFileOptions.fileName = fileName;
229
- else emitFileOptions.name = "react-icons-sprite.svg";
230
- const assetId = this.emitFile(emitFileOptions);
231
- const finalUrl = `/${this.getFileName(assetId)}?v=${encodeURIComponent(generatedHash)}`;
232
- for (const [, item] of Object.entries(bundle)) if (item.type === "chunk" && typeof item.code === "string") {
233
- if (item.code.includes(PLACEHOLDER)) item.code = item.code.replaceAll(PLACEHOLDER, finalUrl);
234
- }
235
- }
236
- };
237
- };
238
-
239
- //#endregion
240
- 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 };
@@ -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.3.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": {
@@ -53,21 +68,24 @@
53
68
  "@babel/parser": "7.28.5",
54
69
  "@babel/traverse": "7.28.5",
55
70
  "@babel/types": "7.28.5",
56
- "@biomejs/biome": "2.3.0",
71
+ "@biomejs/biome": "2.3.5",
57
72
  "@types/babel__generator": "7.27.0",
58
73
  "@types/babel__traverse": "7.28.0",
59
- "@types/node": "^24.9.1",
60
- "@types/react-dom": "19.2.2",
74
+ "@types/node": "24.9.1",
75
+ "@types/react-dom": "19.2.3",
61
76
  "react": "19.2.0",
62
77
  "react-dom": "19.2.0",
63
78
  "react-icons": "5.5.0",
64
- "tsdown": "0.15.10",
79
+ "tsdown": "0.16.3",
65
80
  "typescript": "5.9.3",
66
- "vite": "7.1.12"
81
+ "vite": "7.2.2",
82
+ "webpack": "5.102.1"
67
83
  },
68
84
  "keywords": [
69
85
  "vite",
70
86
  "vite-plugin",
87
+ "webpack",
88
+ "webpack-plugin",
71
89
  "rollup",
72
90
  "rolldown",
73
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 };
File without changes
File without changes