react-icons-sprite 0.8.0-rc.1 → 0.9.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.
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import MagicString from "magic-string";
|
|
4
|
+
import { Visitor, parseSync } from "oxc-parser";
|
|
5
|
+
//#region src/core.ts
|
|
6
|
+
const ICON_SOURCE = "react-icons-sprite";
|
|
7
|
+
const ICON_COMPONENT_NAME = "ReactIconsSpriteIcon";
|
|
8
|
+
const DEFAULT_ICON_SOURCES = [
|
|
9
|
+
/^react-icons\/[\w-]+$/,
|
|
10
|
+
/^lucide-react$/,
|
|
11
|
+
/^@radix-ui\/react-icons$/,
|
|
12
|
+
/^@heroicons\/react(?:\/.*)?$/,
|
|
13
|
+
/^@tabler\/icons-react$/,
|
|
14
|
+
/^phosphor-react$/,
|
|
15
|
+
/^@phosphor-icons\/react$/,
|
|
16
|
+
/^react-feather$/,
|
|
17
|
+
/^react-bootstrap-icons$/,
|
|
18
|
+
/^grommet-icons$/,
|
|
19
|
+
/^@remixicon\/react$/,
|
|
20
|
+
/^devicons-react$/,
|
|
21
|
+
/^@fortawesome\/react-fontawesome$/,
|
|
22
|
+
/^@fortawesome\/[\w-]+-svg-icons$/,
|
|
23
|
+
/^@mui\/icons-material(?:\/.*)?$/,
|
|
24
|
+
/^@carbon\/icons-react$/
|
|
25
|
+
];
|
|
26
|
+
const sourceMatchesSupported = (source, sources = DEFAULT_ICON_SOURCES) => sources.some((re) => re.test(source));
|
|
27
|
+
const normalizeAlias = (pack) => {
|
|
28
|
+
return pack.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
29
|
+
};
|
|
30
|
+
const computeIconId = (pack, exportName) => {
|
|
31
|
+
return `ri-${normalizeAlias(pack)}-${exportName}`;
|
|
32
|
+
};
|
|
33
|
+
const getRange = (node) => {
|
|
34
|
+
if (Array.isArray(node.range) && node.range.length === 2) return [node.range[0], node.range[1]];
|
|
35
|
+
if (typeof node.start === "number" && typeof node.end === "number") return [node.start, node.end];
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
const parseAst = (code, filename) => {
|
|
39
|
+
return parseSync(filename, code, {
|
|
40
|
+
lang: "tsx",
|
|
41
|
+
sourceType: "module",
|
|
42
|
+
range: true
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const collectIconImports = (program, sources = DEFAULT_ICON_SOURCES) => {
|
|
46
|
+
const map = /* @__PURE__ */ new Map();
|
|
47
|
+
const body = program.body ?? [];
|
|
48
|
+
for (const node of body) {
|
|
49
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
50
|
+
const pack = node.source?.value;
|
|
51
|
+
if (typeof pack !== "string" || !sourceMatchesSupported(pack, sources) || node.importKind === "type") continue;
|
|
52
|
+
const specifiers = node.specifiers ?? [];
|
|
53
|
+
for (const spec of specifiers) if (spec.type === "ImportSpecifier") {
|
|
54
|
+
if (spec.importKind === "type") continue;
|
|
55
|
+
const imported = spec.imported;
|
|
56
|
+
const local = spec.local;
|
|
57
|
+
if (imported?.type === "Identifier" && local?.type === "Identifier" && imported.name && local.name) map.set(local.name, {
|
|
58
|
+
pack,
|
|
59
|
+
exportName: imported.name,
|
|
60
|
+
decl: node,
|
|
61
|
+
spec
|
|
62
|
+
});
|
|
63
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
64
|
+
const local = spec.local;
|
|
65
|
+
if (local?.type === "Identifier" && local.name) map.set(local.name, {
|
|
66
|
+
pack,
|
|
67
|
+
exportName: "default",
|
|
68
|
+
decl: node,
|
|
69
|
+
spec
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
};
|
|
75
|
+
const findExistingIconImport = (program) => {
|
|
76
|
+
let iconLocalName = ICON_COMPONENT_NAME;
|
|
77
|
+
let hasIconImport = false;
|
|
78
|
+
const body = program.body ?? [];
|
|
79
|
+
for (const node of body) {
|
|
80
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
81
|
+
if (node.source?.value !== "react-icons-sprite") continue;
|
|
82
|
+
const specifiers = node.specifiers ?? [];
|
|
83
|
+
for (const spec of specifiers) {
|
|
84
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
85
|
+
const imported = spec.imported;
|
|
86
|
+
if (imported?.type === "Identifier" && imported.name === "ReactIconsSpriteIcon") {
|
|
87
|
+
hasIconImport = true;
|
|
88
|
+
const local = spec.local;
|
|
89
|
+
iconLocalName = local?.type === "Identifier" && local.name ? local.name : ICON_COMPONENT_NAME;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (hasIconImport) break;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
hasIconImport,
|
|
97
|
+
iconLocalName
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const removeImportSpecifier = (ms, code, spec) => {
|
|
101
|
+
const range = getRange(spec);
|
|
102
|
+
if (!range) return;
|
|
103
|
+
const [start, end] = range;
|
|
104
|
+
let from = start;
|
|
105
|
+
let to = end;
|
|
106
|
+
let i = start - 1;
|
|
107
|
+
while (i >= 0 && /\s/.test(code[i])) i -= 1;
|
|
108
|
+
if (i >= 0 && code[i] === ",") from = i;
|
|
109
|
+
else {
|
|
110
|
+
let j = end;
|
|
111
|
+
while (j < code.length && /\s/.test(code[j])) j += 1;
|
|
112
|
+
if (j < code.length && code[j] === ",") to = j + 1;
|
|
113
|
+
}
|
|
114
|
+
ms.remove(from, to);
|
|
115
|
+
};
|
|
116
|
+
const removeEntireImport = (ms, code, decl) => {
|
|
117
|
+
const range = getRange(decl);
|
|
118
|
+
if (!range) return;
|
|
119
|
+
let [from, to] = range;
|
|
120
|
+
while (to < code.length && /[ \t]/.test(code[to])) to += 1;
|
|
121
|
+
if (code[to] === "\r" && code[to + 1] === "\n") to += 2;
|
|
122
|
+
else if (code[to] === "\n") to += 1;
|
|
123
|
+
ms.remove(from, to);
|
|
124
|
+
};
|
|
125
|
+
const fixIconSelfClosingSpacing = (outputCode, iconLocalName) => {
|
|
126
|
+
const re = new RegExp(`<${iconLocalName}([^>]*?)/>`, "g");
|
|
127
|
+
return outputCode.replace(re, (_match, attrs) => {
|
|
128
|
+
return `<${iconLocalName}${attrs.replace(/\s+$/g, "")} />`;
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES, options = {}) => {
|
|
132
|
+
const { sourceMap = false } = options;
|
|
133
|
+
const parsed = parseAst(code, id);
|
|
134
|
+
if (parsed.errors.length > 0) throw new Error(parsed.errors[0]?.message ?? `Failed to parse: ${id}`);
|
|
135
|
+
const { program } = parsed;
|
|
136
|
+
const localNameToImport = collectIconImports(program, sources);
|
|
137
|
+
if (localNameToImport.size === 0) return {
|
|
138
|
+
code,
|
|
139
|
+
map: null,
|
|
140
|
+
anyReplacements: false
|
|
141
|
+
};
|
|
142
|
+
const { hasIconImport, iconLocalName } = findExistingIconImport(program);
|
|
143
|
+
const ms = new MagicString(code);
|
|
144
|
+
const usedLocalNames = /* @__PURE__ */ new Set();
|
|
145
|
+
let anyReplacements = false;
|
|
146
|
+
new Visitor({
|
|
147
|
+
JSXOpeningElement(node) {
|
|
148
|
+
const name = node.name;
|
|
149
|
+
if (name?.type !== "JSXIdentifier") return;
|
|
150
|
+
const local = name.name;
|
|
151
|
+
if (!local || local === iconLocalName) return;
|
|
152
|
+
const meta = localNameToImport.get(local);
|
|
153
|
+
if (!meta) return;
|
|
154
|
+
let iconPack = meta.pack;
|
|
155
|
+
let iconExport = meta.exportName;
|
|
156
|
+
let usedLocal = local;
|
|
157
|
+
const attrs = node.attributes ?? [];
|
|
158
|
+
let hasIconId = false;
|
|
159
|
+
let iconAttr;
|
|
160
|
+
for (const a of attrs) {
|
|
161
|
+
if (a.type !== "JSXAttribute") continue;
|
|
162
|
+
const attrName = a.name;
|
|
163
|
+
if (attrName?.type === "JSXIdentifier" && attrName.name === "iconId") hasIconId = true;
|
|
164
|
+
if (attrName?.type === "JSXIdentifier" && attrName.name === "icon") iconAttr = a;
|
|
165
|
+
}
|
|
166
|
+
if (meta.pack === "@fortawesome/react-fontawesome" && meta.exportName === "FontAwesomeIcon" && iconAttr) {
|
|
167
|
+
const value = iconAttr.value;
|
|
168
|
+
if (value?.type === "JSXExpressionContainer") {
|
|
169
|
+
const expr = value.expression;
|
|
170
|
+
if (expr?.type === "Identifier") {
|
|
171
|
+
const iconLocal = expr.name;
|
|
172
|
+
if (iconLocal) {
|
|
173
|
+
const iconMeta = localNameToImport.get(iconLocal);
|
|
174
|
+
if (iconMeta) {
|
|
175
|
+
iconPack = iconMeta.pack;
|
|
176
|
+
iconExport = iconMeta.exportName;
|
|
177
|
+
usedLocal = iconLocal;
|
|
178
|
+
const iconAttrRange = getRange(iconAttr);
|
|
179
|
+
if (iconAttrRange) {
|
|
180
|
+
let [from, to] = iconAttrRange;
|
|
181
|
+
while (to < code.length && /\s/.test(code[to])) to += 1;
|
|
182
|
+
ms.remove(from, to);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const nameRange = getRange(name);
|
|
190
|
+
if (nameRange) ms.overwrite(nameRange[0], nameRange[1], iconLocalName);
|
|
191
|
+
if (!hasIconId) {
|
|
192
|
+
const idValue = computeIconId(iconPack, iconExport);
|
|
193
|
+
const insertPos = nameRange?.[1];
|
|
194
|
+
if (typeof insertPos === "number") ms.appendLeft(insertPos, ` iconId="${idValue}"`);
|
|
195
|
+
}
|
|
196
|
+
usedLocalNames.add(local);
|
|
197
|
+
if (usedLocal !== local) usedLocalNames.add(usedLocal);
|
|
198
|
+
anyReplacements = true;
|
|
199
|
+
register(iconPack, iconExport);
|
|
200
|
+
},
|
|
201
|
+
JSXClosingElement(node) {
|
|
202
|
+
const name = node.name;
|
|
203
|
+
if (name?.type !== "JSXIdentifier") return;
|
|
204
|
+
const local = name.name;
|
|
205
|
+
if (!local || local === iconLocalName) return;
|
|
206
|
+
if (!localNameToImport.get(local)) return;
|
|
207
|
+
const nameRange = getRange(name);
|
|
208
|
+
if (nameRange) ms.overwrite(nameRange[0], nameRange[1], iconLocalName);
|
|
209
|
+
}
|
|
210
|
+
}).visit(program);
|
|
211
|
+
if (!anyReplacements) return {
|
|
212
|
+
code,
|
|
213
|
+
map: null,
|
|
214
|
+
anyReplacements: false
|
|
215
|
+
};
|
|
216
|
+
const declSpecifierCount = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const { decl, spec } of localNameToImport.values()) {
|
|
218
|
+
const localName = spec.local;
|
|
219
|
+
if (!localName?.name || !usedLocalNames.has(localName.name)) continue;
|
|
220
|
+
const declNode = decl;
|
|
221
|
+
let count = declSpecifierCount.get(declNode);
|
|
222
|
+
if (typeof count !== "number") {
|
|
223
|
+
count = 0;
|
|
224
|
+
const specifiers = declNode.specifiers ?? [];
|
|
225
|
+
for (const oneSpec of specifiers) if (oneSpec.type === "ImportSpecifier" || oneSpec.type === "ImportDefaultSpecifier") count += 1;
|
|
226
|
+
declSpecifierCount.set(declNode, count);
|
|
227
|
+
}
|
|
228
|
+
if (count <= 1) removeEntireImport(ms, code, declNode);
|
|
229
|
+
else removeImportSpecifier(ms, code, spec);
|
|
230
|
+
}
|
|
231
|
+
if (!hasIconImport) ms.prepend(`import { ${iconLocalName} } from "${ICON_SOURCE}";\n`);
|
|
232
|
+
const transformedCode = ms.toString();
|
|
233
|
+
return {
|
|
234
|
+
code: transformedCode.includes(`<${iconLocalName}`) ? fixIconSelfClosingSpacing(transformedCode, iconLocalName) : transformedCode,
|
|
235
|
+
map: sourceMap ? (() => {
|
|
236
|
+
const rawMap = ms.generateMap({
|
|
237
|
+
source: id,
|
|
238
|
+
includeContent: true,
|
|
239
|
+
hires: true
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
...rawMap,
|
|
243
|
+
sourcesContent: rawMap.sourcesContent?.map((sourceContent) => sourceContent ?? "")
|
|
244
|
+
};
|
|
245
|
+
})() : null,
|
|
246
|
+
anyReplacements
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const PRESENTATION_ATTRS = new Set([
|
|
250
|
+
"fill",
|
|
251
|
+
"stroke",
|
|
252
|
+
"stroke-width",
|
|
253
|
+
"stroke-linecap",
|
|
254
|
+
"stroke-linejoin",
|
|
255
|
+
"stroke-miterlimit",
|
|
256
|
+
"stroke-dasharray",
|
|
257
|
+
"stroke-dashoffset",
|
|
258
|
+
"stroke-opacity",
|
|
259
|
+
"fill-rule",
|
|
260
|
+
"fill-opacity",
|
|
261
|
+
"color",
|
|
262
|
+
"opacity",
|
|
263
|
+
"shape-rendering",
|
|
264
|
+
"vector-effect"
|
|
265
|
+
]);
|
|
266
|
+
const ATTR_RE = /([a-zA-Z_:.-]+)\s*=\s*"([^"]*)"/g;
|
|
267
|
+
const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
268
|
+
const resolveSpecificImportPath = (pack, exportName) => {
|
|
269
|
+
if (/^@mui\/icons-material(?:\/.*)?$/.test(pack)) {
|
|
270
|
+
if (pack.split("/").length > 2) return pack;
|
|
271
|
+
return `${pack}/${exportName}`;
|
|
272
|
+
}
|
|
273
|
+
if (/^@radix-ui\/react-icons$/.test(pack)) return `${pack}/${exportName}`;
|
|
274
|
+
if (/^@heroicons\/react\/(?:\d{2})\/(?:outline|solid)$/.test(pack)) return `${pack}/${exportName}`;
|
|
275
|
+
if (/^@fortawesome\/[\w-]+-svg-icons$/.test(pack)) return `${pack}/${exportName}`;
|
|
276
|
+
if (/^lucide-react$/.test(pack)) return `${pack}/icons/${toKebab(exportName)}`;
|
|
277
|
+
if (/^@phosphor-icons\/react$/.test(pack)) return `${pack}/dist/ssr/${exportName}.es.js`;
|
|
278
|
+
if (/^phosphor-react$/.test(pack)) return `${pack}/dist/icons/${exportName}.esm.js`;
|
|
279
|
+
if (/^@tabler\/icons-react$/.test(pack)) return `${pack}/dist/esm/icons/${exportName}.mjs`;
|
|
280
|
+
if (/^react-feather$/.test(pack)) return `${pack}/dist/icons/${toKebab(exportName)}`;
|
|
281
|
+
if (/^react-bootstrap-icons$/.test(pack)) return `${pack}/dist/icons/${toKebab(exportName)}`;
|
|
282
|
+
if (/^@carbon\/icons-react$/.test(pack)) return `${pack}/lib/${exportName}.js`;
|
|
283
|
+
return null;
|
|
284
|
+
};
|
|
285
|
+
const renderOneIcon = async (pack, exportName) => {
|
|
286
|
+
let mod;
|
|
287
|
+
const specificPath = resolveSpecificImportPath(pack, exportName);
|
|
288
|
+
if (specificPath) try {
|
|
289
|
+
mod = await import(
|
|
290
|
+
/* @vite-ignore */
|
|
291
|
+
specificPath
|
|
292
|
+
);
|
|
293
|
+
if (mod && "default" in mod && Object.keys(mod).length === 1) mod[exportName] = mod.default;
|
|
294
|
+
} catch {
|
|
295
|
+
mod = await import(
|
|
296
|
+
/* @vite-ignore */
|
|
297
|
+
pack
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
else mod = await import(
|
|
301
|
+
/* @vite-ignore */
|
|
302
|
+
pack
|
|
303
|
+
);
|
|
304
|
+
const modRecord = mod;
|
|
305
|
+
const Comp = modRecord[exportName] ?? modRecord.default;
|
|
306
|
+
if (!Comp) throw new Error(`Icon export not found: ${pack} -> ${exportName}`);
|
|
307
|
+
const id = computeIconId(pack, exportName);
|
|
308
|
+
if (pack.includes("fortawesome")) {
|
|
309
|
+
const [width, height, , , pathData] = Comp.icon;
|
|
310
|
+
return {
|
|
311
|
+
id,
|
|
312
|
+
symbol: `<symbol id="${id}" viewBox="${`0 0 ${width} ${height}`}">${(Array.isArray(pathData) ? pathData : [pathData]).map((d) => `<path d="${d}" />`).join("")}</symbol>`
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const html = renderToStaticMarkup(createElement(Comp, {}));
|
|
316
|
+
const viewBox = html.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24";
|
|
317
|
+
const svgAttrsRaw = html.match(/^<svg\b([^>]*)>/i)?.[1] ?? "";
|
|
318
|
+
const attrs = [];
|
|
319
|
+
for (const [, k, v] of svgAttrsRaw.matchAll(ATTR_RE)) {
|
|
320
|
+
const key = k.toLowerCase();
|
|
321
|
+
if (PRESENTATION_ATTRS.has(key)) attrs.push(`${key}="${v}"`);
|
|
322
|
+
}
|
|
323
|
+
const inner = html.replace(/^<svg[^>]*>/i, "").replace(/<\/svg>\s*$/i, "").replace(/<svg[^>]*>/gi, "").replace(/<\/svg>/gi, "");
|
|
324
|
+
return {
|
|
325
|
+
id,
|
|
326
|
+
symbol: `<symbol id="${id}" viewBox="${viewBox}"${attrs.length ? ` ${attrs.join(" ")}` : ""}>${inner}</symbol>`
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
const buildSprite = async (icons) => {
|
|
330
|
+
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>`;
|
|
331
|
+
};
|
|
332
|
+
const createCollector = () => {
|
|
333
|
+
const set = /* @__PURE__ */ new Map();
|
|
334
|
+
return {
|
|
335
|
+
add(pack, exportName) {
|
|
336
|
+
set.set(`${pack}:${exportName}`, {
|
|
337
|
+
pack,
|
|
338
|
+
exportName
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
toList() {
|
|
342
|
+
return Array.from(set.values());
|
|
343
|
+
},
|
|
344
|
+
clear() {
|
|
345
|
+
set.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
//#endregion
|
|
350
|
+
export { transformModule as i, buildSprite as n, createCollector as r, DEFAULT_ICON_SOURCES as t };
|
package/dist/vite/plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REACT_ICONS_SPRITE_URL_PLACEHOLDER } from "../index.mjs";
|
|
2
|
-
import { i as transformModule, n as buildSprite, r as createCollector, t as DEFAULT_ICON_SOURCES } from "../core-
|
|
2
|
+
import { i as transformModule, n as buildSprite, r as createCollector, t as DEFAULT_ICON_SOURCES } from "../core-vRZuyfCR.mjs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
//#region src/vite/plugin.ts
|
|
5
5
|
const reactIconsSprite = (options = {}) => {
|
|
@@ -18,7 +18,7 @@ const reactIconsSprite = (options = {}) => {
|
|
|
18
18
|
try {
|
|
19
19
|
const { code: next, map, anyReplacements } = transformModule(code, id, (pack, exportName) => {
|
|
20
20
|
collector.add(pack, exportName);
|
|
21
|
-
}, DEFAULT_ICON_SOURCES);
|
|
21
|
+
}, DEFAULT_ICON_SOURCES, { sourceMap: true });
|
|
22
22
|
if (!anyReplacements) return null;
|
|
23
23
|
return {
|
|
24
24
|
code: next,
|
package/dist/webpack/loader.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as transformModule } from "../core-
|
|
2
|
-
import { t as collector } from "../collector-
|
|
1
|
+
import { i as transformModule } from "../core-vRZuyfCR.mjs";
|
|
2
|
+
import { t as collector } from "../collector-CZ15UM_G.mjs";
|
|
3
3
|
//#region src/webpack/loader.ts
|
|
4
4
|
const reactIconsSpriteLoader = async function(source) {
|
|
5
5
|
if (this.mode === "development") return source;
|
package/dist/webpack/plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REACT_ICONS_SPRITE_URL_PLACEHOLDER } from "../index.mjs";
|
|
2
|
-
import { n as buildSprite } from "../core-
|
|
3
|
-
import { t as collector } from "../collector-
|
|
2
|
+
import { n as buildSprite } from "../core-vRZuyfCR.mjs";
|
|
3
|
+
import { t as collector } from "../collector-CZ15UM_G.mjs";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
//#region src/webpack/plugin.ts
|
|
6
6
|
var ReactIconsSpriteWebpackPlugin = class {
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "react-icons-sprite",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "A lightweight Vite and Webpack plugin for react-icons that builds a single SVG sprite and rewrites icons to <use>, reducing bundle size and runtime overhead.",
|
|
6
|
+
"description": "A lightweight Vite, Rsbuild and Webpack plugin for react-icons that builds a single SVG sprite and rewrites icons to <use>, reducing bundle size and runtime overhead.",
|
|
7
7
|
"author": "Jure Rotar <hello@jurerotar.com>",
|
|
8
8
|
"homepage": "https://github.com/jurerotar/react-icons-sprite#readme",
|
|
9
9
|
"license": "MIT",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
],
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsdown",
|
|
53
|
+
"bench:transform": "vitest bench tests/transform.bench.ts",
|
|
53
54
|
"dev": "tsdown --watch",
|
|
54
55
|
"format": "biome format --write --no-errors-on-unmatched",
|
|
55
56
|
"format:check": "biome format --no-errors-on-unmatched",
|
|
@@ -65,25 +66,20 @@
|
|
|
65
66
|
"react-dom": ">= 16"
|
|
66
67
|
},
|
|
67
68
|
"dependencies": {
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"@babel/traverse": "7.29.0",
|
|
71
|
-
"@babel/types": "7.29.0"
|
|
69
|
+
"magic-string": "0.30.21",
|
|
70
|
+
"oxc-parser": "0.119.0"
|
|
72
71
|
},
|
|
73
72
|
"devDependencies": {
|
|
74
73
|
"@carbon/icons-react": "11.76.0",
|
|
75
|
-
"@types/
|
|
76
|
-
"@types/babel__traverse": "7.28.0",
|
|
77
|
-
"@types/node": "25.4.0",
|
|
74
|
+
"@types/node": "25.5.0",
|
|
78
75
|
"@types/react-dom": "19.2.3",
|
|
79
76
|
"@typescript/native-preview": "7.0.0-dev.20260311.1",
|
|
80
77
|
"react": "19.2.4",
|
|
81
78
|
"react-dom": "19.2.4",
|
|
82
79
|
"tsdown": "0.21.2",
|
|
83
80
|
"typescript": "5.9.3",
|
|
84
|
-
"vite": "
|
|
85
|
-
"
|
|
86
|
-
"vitest": "4.0.18",
|
|
81
|
+
"vite": "8.0.0",
|
|
82
|
+
"vitest": "4.1.0",
|
|
87
83
|
"webpack": "5.105.4"
|
|
88
84
|
},
|
|
89
85
|
"keywords": [
|
package/dist/core-C637uIv9.mjs
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
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
|
-
//#region src/core.ts
|
|
8
|
-
const traverse = _traverse.default ?? _traverse;
|
|
9
|
-
const generate = _generate.default ?? _generate;
|
|
10
|
-
const ICON_SOURCE = "react-icons-sprite";
|
|
11
|
-
const ICON_COMPONENT_NAME = "ReactIconsSpriteIcon";
|
|
12
|
-
const DEFAULT_ICON_SOURCES = [
|
|
13
|
-
/^react-icons\/[\w-]+$/,
|
|
14
|
-
/^lucide-react$/,
|
|
15
|
-
/^@radix-ui\/react-icons$/,
|
|
16
|
-
/^@heroicons\/react(?:\/.*)?$/,
|
|
17
|
-
/^@tabler\/icons-react$/,
|
|
18
|
-
/^phosphor-react$/,
|
|
19
|
-
/^@phosphor-icons\/react$/,
|
|
20
|
-
/^react-feather$/,
|
|
21
|
-
/^react-bootstrap-icons$/,
|
|
22
|
-
/^grommet-icons$/,
|
|
23
|
-
/^@remixicon\/react$/,
|
|
24
|
-
/^devicons-react$/,
|
|
25
|
-
/^@fortawesome\/react-fontawesome$/,
|
|
26
|
-
/^@fortawesome\/[\w-]+-svg-icons$/,
|
|
27
|
-
/^@mui\/icons-material(?:\/.*)?$/,
|
|
28
|
-
/^@carbon\/icons-react$/
|
|
29
|
-
];
|
|
30
|
-
const sourceMatchesSupported = (source, sources = DEFAULT_ICON_SOURCES) => sources.some((re) => re.test(source));
|
|
31
|
-
const normalizeAlias = (pack) => {
|
|
32
|
-
return pack.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
33
|
-
};
|
|
34
|
-
const computeIconId = (pack, exportName) => {
|
|
35
|
-
return `ri-${normalizeAlias(pack)}-${exportName}`;
|
|
36
|
-
};
|
|
37
|
-
const parseAst = (code, filename = "module.tsx") => {
|
|
38
|
-
return parse(code, {
|
|
39
|
-
sourceType: "module",
|
|
40
|
-
plugins: ["jsx", "typescript"],
|
|
41
|
-
sourceFilename: filename
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
const collectIconImports = (ast, sources = DEFAULT_ICON_SOURCES) => {
|
|
45
|
-
const map = /* @__PURE__ */ new Map();
|
|
46
|
-
for (const node of ast.program.body) if (t.isImportDeclaration(node) && sourceMatchesSupported(node.source.value, sources) && node.importKind !== "type") {
|
|
47
|
-
const pack = node.source.value;
|
|
48
|
-
for (const spec of node.specifiers) if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported) && t.isIdentifier(spec.local) && spec.importKind !== "type") {
|
|
49
|
-
const exportName = spec.imported.name;
|
|
50
|
-
const localName = spec.local.name;
|
|
51
|
-
map.set(localName, {
|
|
52
|
-
pack,
|
|
53
|
-
exportName,
|
|
54
|
-
decl: node,
|
|
55
|
-
spec
|
|
56
|
-
});
|
|
57
|
-
} else if (t.isImportDefaultSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
58
|
-
const exportName = "default";
|
|
59
|
-
const localName = spec.local.name;
|
|
60
|
-
map.set(localName, {
|
|
61
|
-
pack,
|
|
62
|
-
exportName,
|
|
63
|
-
decl: node,
|
|
64
|
-
spec
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return map;
|
|
69
|
-
};
|
|
70
|
-
const findExistingIconImport = (ast) => {
|
|
71
|
-
let iconLocalName = ICON_COMPONENT_NAME;
|
|
72
|
-
let hasIconImport = false;
|
|
73
|
-
for (const n of ast.program.body) if (t.isImportDeclaration(n) && n.source.value === "react-icons-sprite") {
|
|
74
|
-
for (const s of n.specifiers) if (t.isImportSpecifier(s) && t.isIdentifier(s.imported, { name: "ReactIconsSpriteIcon" })) {
|
|
75
|
-
hasIconImport = true;
|
|
76
|
-
iconLocalName = t.isIdentifier(s.local) ? s.local.name : ICON_COMPONENT_NAME;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
if (hasIconImport) break;
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
hasIconImport,
|
|
83
|
-
iconLocalName
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
const replaceJsxWithSprite = (ast, localNameToImport, iconLocalName, register) => {
|
|
87
|
-
const usedLocalNames = /* @__PURE__ */ new Set();
|
|
88
|
-
let anyReplacements = false;
|
|
89
|
-
const isAlreadyIcon = (name) => t.isJSXIdentifier(name) && name.name === iconLocalName;
|
|
90
|
-
traverse(ast, {
|
|
91
|
-
JSXOpeningElement(path) {
|
|
92
|
-
const name = path.node.name;
|
|
93
|
-
if (!t.isJSXIdentifier(name)) return;
|
|
94
|
-
const local = name.name;
|
|
95
|
-
const meta = localNameToImport.get(local);
|
|
96
|
-
if (!meta) return;
|
|
97
|
-
if (isAlreadyIcon(name)) return;
|
|
98
|
-
let iconPack = meta.pack;
|
|
99
|
-
let iconExport = meta.exportName;
|
|
100
|
-
let usedLocal = local;
|
|
101
|
-
if (meta.pack === "@fortawesome/react-fontawesome" && meta.exportName === "FontAwesomeIcon") {
|
|
102
|
-
const iconAttr = path.node.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: "icon" }));
|
|
103
|
-
if (iconAttr && t.isJSXExpressionContainer(iconAttr.value) && t.isIdentifier(iconAttr.value.expression)) {
|
|
104
|
-
const iconLocalName = iconAttr.value.expression.name;
|
|
105
|
-
const iconMeta = localNameToImport.get(iconLocalName);
|
|
106
|
-
if (iconMeta) {
|
|
107
|
-
iconPack = iconMeta.pack;
|
|
108
|
-
iconExport = iconMeta.exportName;
|
|
109
|
-
usedLocal = iconLocalName;
|
|
110
|
-
path.node.attributes = path.node.attributes.filter((a) => a !== iconAttr);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
path.node.name = t.jSXIdentifier(iconLocalName);
|
|
115
|
-
if (!path.node.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: "iconId" }))) {
|
|
116
|
-
const idValue = computeIconId(iconPack, iconExport);
|
|
117
|
-
path.node.attributes.unshift(t.jSXAttribute(t.jSXIdentifier("iconId"), t.stringLiteral(idValue)));
|
|
118
|
-
}
|
|
119
|
-
usedLocalNames.add(local);
|
|
120
|
-
if (usedLocal !== local) usedLocalNames.add(usedLocal);
|
|
121
|
-
anyReplacements = true;
|
|
122
|
-
register(iconPack, iconExport);
|
|
123
|
-
},
|
|
124
|
-
JSXClosingElement(path) {
|
|
125
|
-
const name = path.node.name;
|
|
126
|
-
if (!t.isJSXIdentifier(name)) return;
|
|
127
|
-
const local = name.name;
|
|
128
|
-
if (!localNameToImport.has(local)) return;
|
|
129
|
-
if (!isAlreadyIcon(name)) path.node.name = t.jSXIdentifier(iconLocalName);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
return {
|
|
133
|
-
usedLocalNames,
|
|
134
|
-
anyReplacements
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
const insertIconImport = (ast, iconLocalName = ICON_COMPONENT_NAME) => {
|
|
138
|
-
const firstImportIndex = ast.program.body.findIndex((n) => t.isImportDeclaration(n));
|
|
139
|
-
const iconImportDecl = t.importDeclaration([t.importSpecifier(t.identifier(iconLocalName), t.identifier(ICON_COMPONENT_NAME))], t.stringLiteral(ICON_SOURCE));
|
|
140
|
-
if (firstImportIndex >= 0) ast.program.body.splice(firstImportIndex + 1, 0, iconImportDecl);
|
|
141
|
-
else ast.program.body.unshift(iconImportDecl);
|
|
142
|
-
};
|
|
143
|
-
const pruneUsedSpecifiers = (ast, localNameToImport, usedLocalNames) => {
|
|
144
|
-
for (const { decl } of new Set([...localNameToImport.values()])) decl.specifiers = decl.specifiers.filter((s) => {
|
|
145
|
-
if ((t.isImportSpecifier(s) || t.isImportDefaultSpecifier(s)) && t.isIdentifier(s.local)) return !usedLocalNames.has(s.local.name);
|
|
146
|
-
return true;
|
|
147
|
-
});
|
|
148
|
-
ast.program.body = ast.program.body.filter((n) => !t.isImportDeclaration(n) || n.specifiers.length > 0);
|
|
149
|
-
};
|
|
150
|
-
const generateCode = (ast, origCode, id) => {
|
|
151
|
-
const { code, map } = generate(ast, {
|
|
152
|
-
sourceMaps: true,
|
|
153
|
-
sourceFileName: id
|
|
154
|
-
}, origCode);
|
|
155
|
-
return {
|
|
156
|
-
code,
|
|
157
|
-
map
|
|
158
|
-
};
|
|
159
|
-
};
|
|
160
|
-
const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES) => {
|
|
161
|
-
const ast = parseAst(code, id);
|
|
162
|
-
const localNameToImport = collectIconImports(ast, sources);
|
|
163
|
-
if (localNameToImport.size === 0) return {
|
|
164
|
-
code,
|
|
165
|
-
map: null,
|
|
166
|
-
anyReplacements: false
|
|
167
|
-
};
|
|
168
|
-
const { hasIconImport, iconLocalName } = findExistingIconImport(ast);
|
|
169
|
-
const { usedLocalNames, anyReplacements } = replaceJsxWithSprite(ast, localNameToImport, iconLocalName, register);
|
|
170
|
-
if (!anyReplacements) return {
|
|
171
|
-
code,
|
|
172
|
-
map: null,
|
|
173
|
-
anyReplacements: false
|
|
174
|
-
};
|
|
175
|
-
if (!hasIconImport) insertIconImport(ast, iconLocalName);
|
|
176
|
-
pruneUsedSpecifiers(ast, localNameToImport, usedLocalNames);
|
|
177
|
-
return {
|
|
178
|
-
...generateCode(ast, code, id),
|
|
179
|
-
anyReplacements
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
const PRESENTATION_ATTRS = new Set([
|
|
183
|
-
"fill",
|
|
184
|
-
"stroke",
|
|
185
|
-
"stroke-width",
|
|
186
|
-
"stroke-linecap",
|
|
187
|
-
"stroke-linejoin",
|
|
188
|
-
"stroke-miterlimit",
|
|
189
|
-
"stroke-dasharray",
|
|
190
|
-
"stroke-dashoffset",
|
|
191
|
-
"stroke-opacity",
|
|
192
|
-
"fill-rule",
|
|
193
|
-
"fill-opacity",
|
|
194
|
-
"color",
|
|
195
|
-
"opacity",
|
|
196
|
-
"shape-rendering",
|
|
197
|
-
"vector-effect"
|
|
198
|
-
]);
|
|
199
|
-
const ATTR_RE = /([a-zA-Z_:.-]+)\s*=\s*"([^"]*)"/g;
|
|
200
|
-
const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
201
|
-
const resolveSpecificImportPath = (pack, exportName) => {
|
|
202
|
-
if (/^@mui\/icons-material(?:\/.*)?$/.test(pack)) {
|
|
203
|
-
if (pack.split("/").length > 2) return pack;
|
|
204
|
-
return `${pack}/${exportName}`;
|
|
205
|
-
}
|
|
206
|
-
if (/^@radix-ui\/react-icons$/.test(pack)) return `${pack}/${exportName}`;
|
|
207
|
-
if (/^@heroicons\/react\/(?:\d{2})\/(?:outline|solid)$/.test(pack)) return `${pack}/${exportName}`;
|
|
208
|
-
if (/^@fortawesome\/[\w-]+-svg-icons$/.test(pack)) return `${pack}/${exportName}`;
|
|
209
|
-
if (/^lucide-react$/.test(pack)) return `${pack}/icons/${toKebab(exportName)}`;
|
|
210
|
-
if (/^@phosphor-icons\/react$/.test(pack)) return `${pack}/dist/ssr/${exportName}.es.js`;
|
|
211
|
-
if (/^phosphor-react$/.test(pack)) return `${pack}/dist/icons/${exportName}.esm.js`;
|
|
212
|
-
if (/^@tabler\/icons-react$/.test(pack)) return `${pack}/dist/esm/icons/${exportName}.mjs`;
|
|
213
|
-
if (/^react-feather$/.test(pack)) return `${pack}/dist/icons/${toKebab(exportName)}`;
|
|
214
|
-
if (/^react-bootstrap-icons$/.test(pack)) return `${pack}/dist/icons/${toKebab(exportName)}`;
|
|
215
|
-
if (/^@carbon\/icons-react$/.test(pack)) return `${pack}/lib/${exportName}.js`;
|
|
216
|
-
return null;
|
|
217
|
-
};
|
|
218
|
-
const renderOneIcon = async (pack, exportName) => {
|
|
219
|
-
let mod;
|
|
220
|
-
const specificPath = resolveSpecificImportPath(pack, exportName);
|
|
221
|
-
if (specificPath) try {
|
|
222
|
-
mod = await import(
|
|
223
|
-
/* @vite-ignore */
|
|
224
|
-
specificPath
|
|
225
|
-
);
|
|
226
|
-
if (mod && "default" in mod && Object.keys(mod).length === 1) mod[exportName] = mod.default;
|
|
227
|
-
} catch {
|
|
228
|
-
mod = await import(
|
|
229
|
-
/* @vite-ignore */
|
|
230
|
-
pack
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
else mod = await import(
|
|
234
|
-
/* @vite-ignore */
|
|
235
|
-
pack
|
|
236
|
-
);
|
|
237
|
-
let Comp = mod[exportName] ?? mod.default;
|
|
238
|
-
if (pack.includes("fortawesome") && Comp && typeof Comp === "object" && "icon" in Comp && Array.isArray(Comp.icon)) {
|
|
239
|
-
const [width, height, , , pathData] = Comp.icon;
|
|
240
|
-
const viewBox = `0 0 ${width} ${height}`;
|
|
241
|
-
const id = computeIconId(pack, exportName);
|
|
242
|
-
return {
|
|
243
|
-
id,
|
|
244
|
-
symbol: `<symbol id="${id}" viewBox="${viewBox}">${(Array.isArray(pathData) ? pathData : [pathData]).map((d) => `<path d="${d}" />`).join("")}</symbol>`
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
if (Comp && typeof Comp === "object" && "default" in Comp && !("$$typeof" in Comp)) Comp = Comp.default;
|
|
248
|
-
if (!Comp) throw new Error(`Icon export not found: ${pack} -> ${exportName}`);
|
|
249
|
-
const id = computeIconId(pack, exportName);
|
|
250
|
-
const html = renderToStaticMarkup(createElement(Comp, {}));
|
|
251
|
-
const viewBox = html.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24";
|
|
252
|
-
const svgAttrsRaw = html.match(/^<svg\b([^>]*)>/i)?.[1] ?? "";
|
|
253
|
-
const attrs = [];
|
|
254
|
-
for (const [, k, v] of svgAttrsRaw.matchAll(ATTR_RE)) {
|
|
255
|
-
const key = k.toLowerCase();
|
|
256
|
-
if (PRESENTATION_ATTRS.has(key)) attrs.push(`${key}="${v}"`);
|
|
257
|
-
}
|
|
258
|
-
const inner = html.replace(/^<svg[^>]*>/i, "").replace(/<\/svg>\s*$/i, "").replace(/<svg[^>]*>/gi, "").replace(/<\/svg>/gi, "");
|
|
259
|
-
return {
|
|
260
|
-
id,
|
|
261
|
-
symbol: `<symbol id="${id}" viewBox="${viewBox}"${attrs.length ? ` ${attrs.join(" ")}` : ""}>${inner}</symbol>`
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
const buildSprite = async (icons) => {
|
|
265
|
-
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>`;
|
|
266
|
-
};
|
|
267
|
-
const createCollector = () => {
|
|
268
|
-
const set = /* @__PURE__ */ new Map();
|
|
269
|
-
return {
|
|
270
|
-
add(pack, exportName) {
|
|
271
|
-
set.set(`${pack}:${exportName}`, {
|
|
272
|
-
pack,
|
|
273
|
-
exportName
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
toList() {
|
|
277
|
-
return Array.from(set.values());
|
|
278
|
-
},
|
|
279
|
-
clear() {
|
|
280
|
-
set.clear();
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
};
|
|
284
|
-
//#endregion
|
|
285
|
-
export { transformModule as i, buildSprite as n, createCollector as r, DEFAULT_ICON_SOURCES as t };
|