react-icons-sprite 0.9.1 → 0.9.2-rc.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.
@@ -0,0 +1,73 @@
1
+ import { r as resolveIconImport, t as computeIconId } from "./compute-icon-id-C96eIC66.mjs";
2
+ import { createElement } from "react";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+ //#region src/sprite/render-icon.ts
5
+ const SVG_INNER_RE = /<svg\b[^>]*>([\s\S]*?)<\/svg>/i;
6
+ const VIEWBOX_RE = /viewBox=["']([^"']+)["']/i;
7
+ const SVG_OPEN_RE = /<svg\b([^>]*)>/i;
8
+ const SVG_ATTR_RE = /([:\w-]+)=("[^"]*"|'[^']*')/g;
9
+ const OMITTED_SVG_ATTRIBUTES = new Set([
10
+ "xmlns",
11
+ "viewBox",
12
+ "width",
13
+ "height"
14
+ ]);
15
+ const extractSymbolAttributes = (svgMarkup) => {
16
+ const openingAttributes = SVG_OPEN_RE.exec(svgMarkup)?.[1];
17
+ if (!openingAttributes) return "";
18
+ const attributes = [];
19
+ for (const [, name, value] of openingAttributes.matchAll(SVG_ATTR_RE)) {
20
+ if (OMITTED_SVG_ATTRIBUTES.has(name)) continue;
21
+ attributes.push(`${name}=${value}`);
22
+ }
23
+ return attributes.join(" ");
24
+ };
25
+ const pickExport = (moduleExports, exportName) => {
26
+ if (exportName === "default") return moduleExports.default;
27
+ return moduleExports[exportName] ?? moduleExports.default;
28
+ };
29
+ const isRenderableComponent = (value) => {
30
+ if (typeof value === "function") return true;
31
+ if (!value || typeof value !== "object") return false;
32
+ return "$$typeof" in value;
33
+ };
34
+ const isFontAwesomeIconDefinition = (value) => {
35
+ if (!value || typeof value !== "object") return false;
36
+ const icon = value.icon;
37
+ if (!Array.isArray(icon) || icon.length < 5) return false;
38
+ return typeof icon[0] === "number" && typeof icon[1] === "number";
39
+ };
40
+ const renderFontAwesomeIconDefinition = (iconDefinition) => {
41
+ const [width, height, , , svgPathData] = iconDefinition.icon;
42
+ return {
43
+ symbolBody: (Array.isArray(svgPathData) ? svgPathData : [svgPathData]).map((d) => `<path d="${d}"/>`).join(""),
44
+ viewBox: `0 0 ${width} ${height}`,
45
+ symbolAttributes: ""
46
+ };
47
+ };
48
+ const renderIcon = async (pack, exportName) => {
49
+ const iconComponent = pickExport(await import(resolveIconImport(pack, exportName)), exportName);
50
+ if (isFontAwesomeIconDefinition(iconComponent)) return renderFontAwesomeIconDefinition(iconComponent);
51
+ if (!isRenderableComponent(iconComponent)) throw new Error(`[react-icons-sprite] Unable to render icon "${exportName}" from "${pack}". Expected a React component export.`);
52
+ const svgMarkup = renderToStaticMarkup(createElement(iconComponent));
53
+ const svgInner = SVG_INNER_RE.exec(svgMarkup)?.[1];
54
+ if (!svgInner) throw new Error(`[react-icons-sprite] Unable to extract SVG content for "${exportName}" from "${pack}".`);
55
+ return {
56
+ symbolBody: svgInner,
57
+ viewBox: VIEWBOX_RE.exec(svgMarkup)?.[1] ?? "0 0 24 24",
58
+ symbolAttributes: extractSymbolAttributes(svgMarkup)
59
+ };
60
+ };
61
+ //#endregion
62
+ //#region src/sprite/build-sprite.ts
63
+ const buildSprite = async (icons) => {
64
+ if (!icons.length) return "<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display:none\"></svg>";
65
+ return `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${(await Promise.all(icons.map(async ({ pack, exportName }) => {
66
+ const rendered = await renderIcon(pack, exportName);
67
+ const id = computeIconId(pack, exportName);
68
+ const symbolAttributes = rendered.symbolAttributes ? ` ${rendered.symbolAttributes}` : "";
69
+ return `<symbol id="${id}" viewBox="${rendered.viewBox}"${symbolAttributes}>${rendered.symbolBody}</symbol>`;
70
+ }))).join("")}</svg>`;
71
+ };
72
+ //#endregion
73
+ export { buildSprite as t };
@@ -1,4 +1,4 @@
1
- import { r as createCollector } from "./core-B0kAb7AT.mjs";
1
+ import { i as createCollector } from "./compute-icon-id-C96eIC66.mjs";
2
2
  //#region src/collector.ts
3
3
  const collector = createCollector();
4
4
  //#endregion
@@ -0,0 +1,62 @@
1
+ //#region src/collector/create-collector.ts
2
+ const createCollector = () => {
3
+ const collected = /* @__PURE__ */ new Set();
4
+ return {
5
+ add(pack, exportName) {
6
+ collected.add(`${pack}:${exportName}`);
7
+ },
8
+ toList() {
9
+ return [...collected].map((key) => {
10
+ const [pack, exportName] = key.split(":");
11
+ return {
12
+ pack,
13
+ exportName
14
+ };
15
+ });
16
+ },
17
+ clear() {
18
+ collected.clear();
19
+ }
20
+ };
21
+ };
22
+ //#endregion
23
+ //#region src/utils/kebab-case.ts
24
+ const kebabCase = (value) => {
25
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-z0-9]+/gi, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").toLowerCase();
26
+ };
27
+ //#endregion
28
+ //#region src/packs/icon-resolvers.ts
29
+ const DEFAULT_ICON_SOURCES = [
30
+ /^react-icons\/[\w-]+$/,
31
+ /^@fortawesome\/[\w-]+-svg-icons$/,
32
+ /^lucide-react$/,
33
+ /^@heroicons\/react(?:\/.*)?$/,
34
+ /^@tabler\/icons-react$/,
35
+ /^@radix-ui\/react-icons$/,
36
+ /^phosphor-react$/,
37
+ /^@phosphor-icons\/react$/,
38
+ /^react-feather$/,
39
+ /^react-bootstrap-icons$/,
40
+ /^grommet-icons$/,
41
+ /^@remixicon\/react$/,
42
+ /^devicons-react$/,
43
+ /^@mui\/icons-material(?:\/.*)?$/,
44
+ /^@carbon\/icons-react$/
45
+ ];
46
+ const resolvers = {
47
+ "lucide-react": (_pack, name) => `lucide-react/dist/esm/icons/${kebabCase(name)}.js`,
48
+ "@tabler/icons-react": (_pack, name) => `@tabler/icons-react/dist/esm/icons/${name}.mjs`
49
+ };
50
+ const resolveIconImport = (pack, exportName) => {
51
+ return resolvers[pack]?.(pack, exportName) ?? pack;
52
+ };
53
+ //#endregion
54
+ //#region src/utils/compute-icon-id.ts
55
+ const normalizePackAlias = (pack) => {
56
+ return kebabCase(pack.replace(/^@/, ""));
57
+ };
58
+ const computeIconId = (pack, iconName) => {
59
+ return `ri-${normalizePackAlias(pack)}-${iconName}`;
60
+ };
61
+ //#endregion
62
+ export { createCollector as i, DEFAULT_ICON_SOURCES as n, resolveIconImport as r, computeIconId as t };
@@ -0,0 +1,290 @@
1
+ import { n as DEFAULT_ICON_SOURCES, t as computeIconId } from "./compute-icon-id-C96eIC66.mjs";
2
+ import { parseSync } from "oxc-parser";
3
+ import MagicString from "magic-string";
4
+ //#region src/transform/edit-applier.ts
5
+ const applyEdits = (code, edits) => {
6
+ const magicString = new MagicString(code);
7
+ for (const edit of edits) {
8
+ if (edit.type === "replace") magicString.overwrite(edit.from, edit.to, edit.value);
9
+ if (edit.type === "insert") magicString.appendLeft(edit.pos, edit.value);
10
+ if (edit.type === "remove") magicString.remove(edit.from, edit.to);
11
+ }
12
+ return magicString;
13
+ };
14
+ //#endregion
15
+ //#region src/transform/edit-builder.ts
16
+ const buildEdits = (usages, componentName, usedSymbols, register) => {
17
+ const edits = [];
18
+ for (const usage of usages) {
19
+ edits.push({
20
+ type: "replace",
21
+ from: usage.range[0],
22
+ to: usage.range[1],
23
+ value: componentName
24
+ });
25
+ if (usage.kind === "opening") {
26
+ edits.push({
27
+ type: "insert",
28
+ pos: usage.range[1],
29
+ value: ` iconId="${computeIconId(usage.pack, usage.exportName)}"`
30
+ });
31
+ usedSymbols.add(usage.local);
32
+ register(usage.pack, usage.exportName);
33
+ }
34
+ }
35
+ return edits;
36
+ };
37
+ //#endregion
38
+ //#region src/transform/fast-filter.ts
39
+ const fastFilter = (code) => {
40
+ if (!code.includes("<")) return false;
41
+ if (!code.includes("import")) return false;
42
+ return true;
43
+ };
44
+ //#endregion
45
+ //#region src/transform/import-scanner.ts
46
+ const IMPORT_RE = /import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"]/g;
47
+ const scanIconImports = (code, sources) => {
48
+ const imports = [];
49
+ for (const match of code.matchAll(IMPORT_RE)) {
50
+ const [, specifier, pack] = match;
51
+ if (!sources.some((source) => source.test(pack))) continue;
52
+ const names = specifier.replace(/[{}]/g, "").split(",").map((segment) => segment.trim()).filter(Boolean).map((segment) => segment.split(" as ")[1] ?? segment);
53
+ imports.push({
54
+ pack,
55
+ names
56
+ });
57
+ }
58
+ return imports;
59
+ };
60
+ //#endregion
61
+ //#region src/transform/usage-scanner.ts
62
+ const detectUsage = (code, names) => {
63
+ if (!names.length) return false;
64
+ const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
65
+ return new RegExp(`<(${escaped.join("|")})\\b`).test(code);
66
+ };
67
+ //#endregion
68
+ //#region src/transform/transform-module.ts
69
+ const ICON_SOURCE = "react-icons-sprite";
70
+ const ICON_COMPONENT_NAME = "ReactIconsSpriteIcon";
71
+ const FONTAWESOME_REACT_PACK = "@fortawesome/react-fontawesome";
72
+ const isFontAwesomeIconPack = (pack) => {
73
+ return /^@fortawesome\/[\w-]+-svg-icons$/.test(pack);
74
+ };
75
+ const isObject = (value) => {
76
+ return Boolean(value) && typeof value === "object";
77
+ };
78
+ const walkAst = (node, visit) => {
79
+ if (!isObject(node)) return;
80
+ visit(node);
81
+ for (const value of Object.values(node)) {
82
+ if (Array.isArray(value)) {
83
+ for (const child of value) walkAst(child, visit);
84
+ continue;
85
+ }
86
+ walkAst(value, visit);
87
+ }
88
+ };
89
+ const buildSymbolTable = (program, sources) => {
90
+ const table = /* @__PURE__ */ new Map();
91
+ const body = program.body ?? [];
92
+ for (const node of body) {
93
+ if (!isObject(node) || node.type !== "ImportDeclaration") continue;
94
+ const source = isObject(node.source) ? node.source : void 0;
95
+ const pack = typeof source?.value === "string" ? source.value : void 0;
96
+ if (!pack || !sources.some((sourceMatcher) => sourceMatcher.test(pack))) continue;
97
+ const specifiers = node.specifiers ?? [];
98
+ for (const specifier of specifiers) {
99
+ if (!isObject(specifier) || !isObject(specifier.local)) continue;
100
+ const localName = specifier.local.name;
101
+ if (typeof localName !== "string") continue;
102
+ if (specifier.type === "ImportSpecifier") {
103
+ const imported = isObject(specifier.imported) ? specifier.imported : void 0;
104
+ const importedName = typeof imported?.name === "string" ? imported.name : typeof imported?.value === "string" ? imported.value : void 0;
105
+ if (importedName) table.set(localName, {
106
+ pack,
107
+ exportName: importedName
108
+ });
109
+ continue;
110
+ }
111
+ if (specifier.type === "ImportDefaultSpecifier") table.set(localName, {
112
+ pack,
113
+ exportName: "default"
114
+ });
115
+ }
116
+ }
117
+ return table;
118
+ };
119
+ const detectIconUsage = (program, symbols) => {
120
+ const usages = [];
121
+ walkAst(program, (node) => {
122
+ if (node.type !== "JSXOpeningElement" && node.type !== "JSXClosingElement") return;
123
+ const name = isObject(node.name) ? node.name : void 0;
124
+ if (!name || name.type !== "JSXIdentifier" || typeof name.name !== "string") return;
125
+ const symbol = symbols.get(name.name);
126
+ if (!symbol) return;
127
+ const range = name.range;
128
+ if (!range || range.length !== 2) return;
129
+ usages.push({
130
+ local: name.name,
131
+ range,
132
+ pack: symbol.pack,
133
+ exportName: symbol.exportName,
134
+ kind: node.type === "JSXOpeningElement" ? "opening" : "closing"
135
+ });
136
+ });
137
+ return usages;
138
+ };
139
+ const detectFontAwesomeComponents = (program) => {
140
+ const componentLocals = /* @__PURE__ */ new Set();
141
+ const body = program.body ?? [];
142
+ for (const node of body) {
143
+ if (!isObject(node) || node.type !== "ImportDeclaration") continue;
144
+ if ((isObject(node.source) ? node.source : void 0)?.value !== FONTAWESOME_REACT_PACK) continue;
145
+ const specifiers = node.specifiers ?? [];
146
+ for (const specifier of specifiers) {
147
+ if (!isObject(specifier) || specifier.type !== "ImportSpecifier" || !isObject(specifier.local) || !isObject(specifier.imported)) continue;
148
+ const importedName = typeof specifier.imported.name === "string" ? specifier.imported.name : void 0;
149
+ const localName = typeof specifier.local.name === "string" ? specifier.local.name : void 0;
150
+ if (importedName === "FontAwesomeIcon" && localName) componentLocals.add(localName);
151
+ }
152
+ }
153
+ return componentLocals;
154
+ };
155
+ const detectFontAwesomeIconUsages = (program, symbols, fontAwesomeComponentLocals) => {
156
+ if (!fontAwesomeComponentLocals.size) return [];
157
+ const usages = [];
158
+ walkAst(program, (node) => {
159
+ if (node.type !== "JSXOpeningElement") return;
160
+ const name = isObject(node.name) ? node.name : void 0;
161
+ if (!name || name.type !== "JSXIdentifier" || typeof name.name !== "string" || !fontAwesomeComponentLocals.has(name.name)) return;
162
+ const componentRange = name.range;
163
+ if (!componentRange || componentRange.length !== 2) return;
164
+ const attributes = node.attributes ?? [];
165
+ for (const attribute of attributes) {
166
+ if (!isObject(attribute) || attribute.type !== "JSXAttribute") continue;
167
+ const attributeName = isObject(attribute.name) ? attribute.name : void 0;
168
+ if (!attributeName || attributeName.type !== "JSXIdentifier" || attributeName.name !== "icon") continue;
169
+ const value = isObject(attribute.value) ? attribute.value : void 0;
170
+ if (!value || value.type !== "JSXExpressionContainer") continue;
171
+ const expression = isObject(value.expression) ? value.expression : void 0;
172
+ if (!expression || expression.type !== "Identifier" || typeof expression.name !== "string") continue;
173
+ const symbol = symbols.get(expression.name);
174
+ if (!symbol || !isFontAwesomeIconPack(symbol.pack)) continue;
175
+ const iconAttributeRange = attribute.range;
176
+ if (!iconAttributeRange || iconAttributeRange.length !== 2) continue;
177
+ usages.push({
178
+ componentRange,
179
+ iconAttributeRange,
180
+ iconLocal: expression.name,
181
+ pack: symbol.pack,
182
+ exportName: symbol.exportName
183
+ });
184
+ break;
185
+ }
186
+ });
187
+ return usages;
188
+ };
189
+ const cleanupImports = (program, symbols, usedLocals) => {
190
+ const edits = [];
191
+ const body = program.body ?? [];
192
+ for (const node of body) {
193
+ if (!isObject(node) || node.type !== "ImportDeclaration") continue;
194
+ const specifiers = node.specifiers ?? [];
195
+ const declarationRange = node.range;
196
+ let hasUsedIconSpecifier = false;
197
+ for (const specifier of specifiers) {
198
+ if (!isObject(specifier) || !isObject(specifier.local)) continue;
199
+ const localName = specifier.local.name;
200
+ if (typeof localName === "string" && symbols.has(localName) && usedLocals.has(localName)) {
201
+ hasUsedIconSpecifier = true;
202
+ break;
203
+ }
204
+ }
205
+ if (hasUsedIconSpecifier && declarationRange) edits.push({
206
+ type: "remove",
207
+ from: declarationRange[0],
208
+ to: declarationRange[1]
209
+ });
210
+ }
211
+ return edits;
212
+ };
213
+ const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES) => {
214
+ if (!fastFilter(code)) return {
215
+ code,
216
+ map: null,
217
+ anyReplacements: false
218
+ };
219
+ const scanned = scanIconImports(code, sources);
220
+ if (!scanned.length) return {
221
+ code,
222
+ map: null,
223
+ anyReplacements: false
224
+ };
225
+ const hasJsxComponentUsage = detectUsage(code, scanned.flatMap((item) => item.names));
226
+ const hasPotentialFontAwesomeUsage = code.includes(FONTAWESOME_REACT_PACK) && scanned.some((item) => isFontAwesomeIconPack(item.pack));
227
+ if (!hasJsxComponentUsage && !hasPotentialFontAwesomeUsage) return {
228
+ code,
229
+ map: null,
230
+ anyReplacements: false
231
+ };
232
+ const program = parseSync(id, code, {
233
+ lang: "tsx",
234
+ sourceType: "module",
235
+ range: true
236
+ }).program;
237
+ const table = buildSymbolTable(program, sources);
238
+ if (!table.size) return {
239
+ code,
240
+ map: null,
241
+ anyReplacements: false
242
+ };
243
+ const usages = detectIconUsage(program, table);
244
+ const fontAwesomeUsages = detectFontAwesomeIconUsages(program, table, detectFontAwesomeComponents(program));
245
+ if (!usages.length && !fontAwesomeUsages.length) return {
246
+ code,
247
+ map: null,
248
+ anyReplacements: false
249
+ };
250
+ const used = /* @__PURE__ */ new Set();
251
+ const edits = buildEdits(usages, ICON_COMPONENT_NAME, used, register);
252
+ const registeredFontAwesomeIcons = /* @__PURE__ */ new Set();
253
+ for (const usage of fontAwesomeUsages) {
254
+ edits.push({
255
+ type: "replace",
256
+ from: usage.componentRange[0],
257
+ to: usage.componentRange[1],
258
+ value: ICON_COMPONENT_NAME
259
+ });
260
+ edits.push({
261
+ type: "insert",
262
+ pos: usage.componentRange[1],
263
+ value: ` iconId="${computeIconId(usage.pack, usage.exportName)}"`
264
+ });
265
+ edits.push({
266
+ type: "remove",
267
+ from: usage.iconAttributeRange[0],
268
+ to: usage.iconAttributeRange[1]
269
+ });
270
+ used.add(usage.iconLocal);
271
+ const key = `${usage.pack}:${usage.exportName}`;
272
+ if (!registeredFontAwesomeIcons.has(key)) {
273
+ registeredFontAwesomeIcons.add(key);
274
+ register(usage.pack, usage.exportName);
275
+ }
276
+ }
277
+ const cleanupEdits = cleanupImports(program, table, used);
278
+ const magicString = applyEdits(code, [...edits, ...cleanupEdits]);
279
+ if (!code.includes("react-icons-sprite")) magicString.prepend(`import { ${ICON_COMPONENT_NAME} } from "${ICON_SOURCE}";\n`);
280
+ return {
281
+ code: magicString.toString(),
282
+ map: magicString.generateMap({
283
+ source: id,
284
+ hires: true
285
+ }),
286
+ anyReplacements: true
287
+ };
288
+ };
289
+ //#endregion
290
+ export { transformModule as t };
@@ -1,5 +1,7 @@
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-B0kAb7AT.mjs";
2
+ import { i as createCollector, n as DEFAULT_ICON_SOURCES } from "../compute-icon-id-C96eIC66.mjs";
3
+ import { t as buildSprite } from "../build-sprite-BFhBc3Ev.mjs";
4
+ import { t as transformModule } from "../transform-module-DzmqobWK.mjs";
3
5
  import { createHash } from "node:crypto";
4
6
  //#region src/vite/plugin.ts
5
7
  const reactIconsSprite = (options = {}) => {
@@ -18,7 +20,7 @@ const reactIconsSprite = (options = {}) => {
18
20
  try {
19
21
  const { code: next, map, anyReplacements } = transformModule(code, id, (pack, exportName) => {
20
22
  collector.add(pack, exportName);
21
- }, DEFAULT_ICON_SOURCES, { sourceMap: true });
23
+ }, DEFAULT_ICON_SOURCES);
22
24
  if (!anyReplacements) return null;
23
25
  return {
24
26
  code: next,
@@ -1,5 +1,5 @@
1
- import { i as transformModule } from "../core-B0kAb7AT.mjs";
2
- import { t as collector } from "../collector-BvJbH8Da.mjs";
1
+ import { t as transformModule } from "../transform-module-DzmqobWK.mjs";
2
+ import { t as collector } from "../collector-C6kn7NXC.mjs";
3
3
  //#region src/webpack/loader.ts
4
4
  const reactIconsSpriteLoader = async function(source) {
5
5
  if (this.mode === "development") return source;
@@ -1,6 +1,6 @@
1
1
  import { REACT_ICONS_SPRITE_URL_PLACEHOLDER } from "../index.mjs";
2
- import { n as buildSprite } from "../core-B0kAb7AT.mjs";
3
- import { t as collector } from "../collector-BvJbH8Da.mjs";
2
+ import { t as buildSprite } from "../build-sprite-BFhBc3Ev.mjs";
3
+ import { t as collector } from "../collector-C6kn7NXC.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,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "react-icons-sprite",
4
- "version": "0.9.1",
4
+ "version": "0.9.2-rc.1",
5
5
  "type": "module",
6
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>",
@@ -1,404 +0,0 @@
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 getParseLangCandidates = (filename) => {
39
- const lower = (filename.split("?", 1)[0] ?? filename).toLowerCase();
40
- if (lower.endsWith(".tsx")) return [
41
- "tsx",
42
- "ts",
43
- "jsx",
44
- "js"
45
- ];
46
- if (lower.endsWith(".ts")) return [
47
- "ts",
48
- "tsx",
49
- "js",
50
- "jsx"
51
- ];
52
- if (lower.endsWith(".jsx")) return [
53
- "jsx",
54
- "js",
55
- "tsx",
56
- "ts"
57
- ];
58
- return [
59
- "js",
60
- "jsx",
61
- "ts",
62
- "tsx"
63
- ];
64
- };
65
- const parseAst = (code, filename) => {
66
- const languages = getParseLangCandidates(filename);
67
- let firstResult;
68
- for (const lang of languages) {
69
- const result = parseSync(filename, code, {
70
- lang,
71
- sourceType: "module",
72
- range: true
73
- });
74
- firstResult ??= result;
75
- if (result.errors.length === 0) return result;
76
- }
77
- return firstResult ?? parseSync(filename, code, {
78
- lang: "tsx",
79
- sourceType: "module",
80
- range: true
81
- });
82
- };
83
- const collectIconImports = (program, sources = DEFAULT_ICON_SOURCES) => {
84
- const map = /* @__PURE__ */ new Map();
85
- const body = program.body ?? [];
86
- for (const node of body) {
87
- if (node.type !== "ImportDeclaration") continue;
88
- const pack = node.source?.value;
89
- if (typeof pack !== "string" || !sourceMatchesSupported(pack, sources) || node.importKind === "type") continue;
90
- const specifiers = node.specifiers ?? [];
91
- for (const spec of specifiers) if (spec.type === "ImportSpecifier") {
92
- if (spec.importKind === "type") continue;
93
- const imported = spec.imported;
94
- const local = spec.local;
95
- if (imported?.type === "Identifier" && local?.type === "Identifier" && imported.name && local.name) map.set(local.name, {
96
- pack,
97
- exportName: imported.name,
98
- decl: node,
99
- spec
100
- });
101
- } else if (spec.type === "ImportDefaultSpecifier") {
102
- const local = spec.local;
103
- if (local?.type === "Identifier" && local.name) map.set(local.name, {
104
- pack,
105
- exportName: "default",
106
- decl: node,
107
- spec
108
- });
109
- }
110
- }
111
- return map;
112
- };
113
- const findExistingIconImport = (program) => {
114
- let iconLocalName = ICON_COMPONENT_NAME;
115
- let hasIconImport = false;
116
- const body = program.body ?? [];
117
- for (const node of body) {
118
- if (node.type !== "ImportDeclaration") continue;
119
- if (node.source?.value !== "react-icons-sprite") continue;
120
- const specifiers = node.specifiers ?? [];
121
- for (const spec of specifiers) {
122
- if (spec.type !== "ImportSpecifier") continue;
123
- const imported = spec.imported;
124
- if (imported?.type === "Identifier" && imported.name === "ReactIconsSpriteIcon") {
125
- hasIconImport = true;
126
- const local = spec.local;
127
- iconLocalName = local?.type === "Identifier" && local.name ? local.name : ICON_COMPONENT_NAME;
128
- break;
129
- }
130
- }
131
- if (hasIconImport) break;
132
- }
133
- return {
134
- hasIconImport,
135
- iconLocalName
136
- };
137
- };
138
- const removeImportSpecifier = (ms, code, spec) => {
139
- const range = getRange(spec);
140
- if (!range) return;
141
- const [start, end] = range;
142
- let from = start;
143
- let to = end;
144
- let i = start - 1;
145
- while (i >= 0 && /\s/.test(code[i])) i -= 1;
146
- if (i >= 0 && code[i] === ",") from = i;
147
- else {
148
- let j = end;
149
- while (j < code.length && /\s/.test(code[j])) j += 1;
150
- if (j < code.length && code[j] === ",") to = j + 1;
151
- }
152
- ms.remove(from, to);
153
- };
154
- const removeEntireImport = (ms, code, decl) => {
155
- const range = getRange(decl);
156
- if (!range) return;
157
- let [from, to] = range;
158
- while (to < code.length && /[ \t]/.test(code[to])) to += 1;
159
- if (code[to] === "\r" && code[to + 1] === "\n") to += 2;
160
- else if (code[to] === "\n") to += 1;
161
- ms.remove(from, to);
162
- };
163
- const fixIconSelfClosingSpacing = (outputCode, iconLocalName) => {
164
- const re = new RegExp(`<${iconLocalName}([^>]*?)/>`, "g");
165
- return outputCode.replace(re, (_match, attrs) => {
166
- return `<${iconLocalName}${attrs.replace(/\s+$/g, "")} />`;
167
- });
168
- };
169
- const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES, options = {}) => {
170
- const { sourceMap = false } = options;
171
- const parsed = parseAst(code, id);
172
- if (parsed.errors.length > 0) throw new Error(parsed.errors[0]?.message ?? `Failed to parse: ${id}`);
173
- const { program } = parsed;
174
- const localNameToImport = collectIconImports(program, sources);
175
- if (localNameToImport.size === 0) return {
176
- code,
177
- map: null,
178
- anyReplacements: false
179
- };
180
- const { hasIconImport, iconLocalName } = findExistingIconImport(program);
181
- const ms = new MagicString(code);
182
- const usedLocalNames = /* @__PURE__ */ new Set();
183
- let anyReplacements = false;
184
- new Visitor({
185
- JSXOpeningElement(node) {
186
- const name = node.name;
187
- if (name?.type !== "JSXIdentifier") return;
188
- const local = name.name;
189
- if (!local || local === iconLocalName) return;
190
- const meta = localNameToImport.get(local);
191
- if (!meta) return;
192
- let iconPack = meta.pack;
193
- let iconExport = meta.exportName;
194
- let usedLocal = local;
195
- const attrs = node.attributes ?? [];
196
- let hasIconId = false;
197
- let iconAttr;
198
- for (const a of attrs) {
199
- if (a.type !== "JSXAttribute") continue;
200
- const attrName = a.name;
201
- if (attrName?.type === "JSXIdentifier" && attrName.name === "iconId") hasIconId = true;
202
- if (attrName?.type === "JSXIdentifier" && attrName.name === "icon") iconAttr = a;
203
- }
204
- if (meta.pack === "@fortawesome/react-fontawesome" && meta.exportName === "FontAwesomeIcon" && iconAttr) {
205
- const value = iconAttr.value;
206
- if (value?.type === "JSXExpressionContainer") {
207
- const expr = value.expression;
208
- if (expr?.type === "Identifier") {
209
- const iconLocal = expr.name;
210
- if (iconLocal) {
211
- const iconMeta = localNameToImport.get(iconLocal);
212
- if (iconMeta) {
213
- iconPack = iconMeta.pack;
214
- iconExport = iconMeta.exportName;
215
- usedLocal = iconLocal;
216
- const iconAttrRange = getRange(iconAttr);
217
- if (iconAttrRange) {
218
- let [from, to] = iconAttrRange;
219
- while (to < code.length && /\s/.test(code[to])) to += 1;
220
- ms.remove(from, to);
221
- }
222
- }
223
- }
224
- }
225
- }
226
- }
227
- const nameRange = getRange(name);
228
- if (nameRange) ms.overwrite(nameRange[0], nameRange[1], iconLocalName);
229
- if (!hasIconId) {
230
- const idValue = computeIconId(iconPack, iconExport);
231
- const insertPos = nameRange?.[1];
232
- if (typeof insertPos === "number") ms.appendLeft(insertPos, ` iconId="${idValue}"`);
233
- }
234
- usedLocalNames.add(local);
235
- if (usedLocal !== local) usedLocalNames.add(usedLocal);
236
- anyReplacements = true;
237
- register(iconPack, iconExport);
238
- },
239
- JSXClosingElement(node) {
240
- const name = node.name;
241
- if (name?.type !== "JSXIdentifier") return;
242
- const local = name.name;
243
- if (!local || local === iconLocalName) return;
244
- if (!localNameToImport.get(local)) return;
245
- const nameRange = getRange(name);
246
- if (nameRange) ms.overwrite(nameRange[0], nameRange[1], iconLocalName);
247
- }
248
- }).visit(program);
249
- if (!anyReplacements) return {
250
- code,
251
- map: null,
252
- anyReplacements: false
253
- };
254
- const declToAllSpecs = /* @__PURE__ */ new Map();
255
- const declToUsedSpecs = /* @__PURE__ */ new Map();
256
- for (const { decl, spec } of localNameToImport.values()) {
257
- const declNode = decl;
258
- const specNode = spec;
259
- const all = declToAllSpecs.get(declNode) ?? [];
260
- if (all.length === 0) {
261
- const specifiers = declNode.specifiers ?? [];
262
- for (const oneSpec of specifiers) if (oneSpec.type === "ImportSpecifier" || oneSpec.type === "ImportDefaultSpecifier") all.push(oneSpec);
263
- declToAllSpecs.set(declNode, all);
264
- }
265
- const localName = specNode.local;
266
- if (!localName?.name || !usedLocalNames.has(localName.name)) continue;
267
- const used = declToUsedSpecs.get(declNode) ?? [];
268
- used.push(specNode);
269
- declToUsedSpecs.set(declNode, used);
270
- }
271
- for (const [declNode, usedSpecs] of declToUsedSpecs.entries()) {
272
- const allSpecs = declToAllSpecs.get(declNode) ?? [];
273
- if (allSpecs.length > 0 && usedSpecs.length >= allSpecs.length) {
274
- removeEntireImport(ms, code, declNode);
275
- continue;
276
- }
277
- const byStartDesc = [...usedSpecs].sort((a, b) => {
278
- const aRange = getRange(a);
279
- const bRange = getRange(b);
280
- if (!aRange || !bRange) return 0;
281
- return bRange[0] - aRange[0];
282
- });
283
- for (const usedSpec of byStartDesc) removeImportSpecifier(ms, code, usedSpec);
284
- }
285
- if (!hasIconImport) ms.prepend(`import { ${iconLocalName} } from "${ICON_SOURCE}";\n`);
286
- const transformedCode = ms.toString();
287
- return {
288
- code: transformedCode.includes(`<${iconLocalName}`) ? fixIconSelfClosingSpacing(transformedCode, iconLocalName) : transformedCode,
289
- map: sourceMap ? (() => {
290
- const rawMap = ms.generateMap({
291
- source: id,
292
- includeContent: true,
293
- hires: true
294
- });
295
- return {
296
- ...rawMap,
297
- sourcesContent: rawMap.sourcesContent?.map((sourceContent) => sourceContent ?? "")
298
- };
299
- })() : null,
300
- anyReplacements
301
- };
302
- };
303
- const PRESENTATION_ATTRS = new Set([
304
- "fill",
305
- "stroke",
306
- "stroke-width",
307
- "stroke-linecap",
308
- "stroke-linejoin",
309
- "stroke-miterlimit",
310
- "stroke-dasharray",
311
- "stroke-dashoffset",
312
- "stroke-opacity",
313
- "fill-rule",
314
- "fill-opacity",
315
- "color",
316
- "opacity",
317
- "shape-rendering",
318
- "vector-effect"
319
- ]);
320
- const ATTR_RE = /([a-zA-Z_:.-]+)\s*=\s*"([^"]*)"/g;
321
- const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
322
- const resolveSpecificImportPath = (pack, exportName) => {
323
- if (/^@mui\/icons-material(?:\/.*)?$/.test(pack)) {
324
- if (pack.split("/").length > 2) return pack;
325
- return `${pack}/${exportName}`;
326
- }
327
- if (/^@radix-ui\/react-icons$/.test(pack)) return `${pack}/${exportName}`;
328
- if (/^@heroicons\/react\/(?:\d{2})\/(?:outline|solid)$/.test(pack)) return `${pack}/${exportName}`;
329
- if (/^@fortawesome\/[\w-]+-svg-icons$/.test(pack)) return `${pack}/${exportName}`;
330
- if (/^lucide-react$/.test(pack)) return `${pack}/icons/${toKebab(exportName)}`;
331
- if (/^@phosphor-icons\/react$/.test(pack)) return `${pack}/dist/ssr/${exportName}.es.js`;
332
- if (/^phosphor-react$/.test(pack)) return `${pack}/dist/icons/${exportName}.esm.js`;
333
- if (/^@tabler\/icons-react$/.test(pack)) return `${pack}/dist/esm/icons/${exportName}.mjs`;
334
- if (/^react-feather$/.test(pack)) return `${pack}/dist/icons/${toKebab(exportName)}`;
335
- if (/^react-bootstrap-icons$/.test(pack)) return `${pack}/dist/icons/${toKebab(exportName)}`;
336
- if (/^@carbon\/icons-react$/.test(pack)) return `${pack}/lib/${exportName}.js`;
337
- return null;
338
- };
339
- const renderOneIcon = async (pack, exportName) => {
340
- let mod;
341
- const specificPath = resolveSpecificImportPath(pack, exportName);
342
- if (specificPath) try {
343
- mod = await import(
344
- /* @vite-ignore */
345
- specificPath
346
- );
347
- if (mod && "default" in mod && Object.keys(mod).length === 1) mod[exportName] = mod.default;
348
- } catch {
349
- mod = await import(
350
- /* @vite-ignore */
351
- pack
352
- );
353
- }
354
- else mod = await import(
355
- /* @vite-ignore */
356
- pack
357
- );
358
- const modRecord = mod;
359
- const Comp = modRecord[exportName] ?? modRecord.default;
360
- if (!Comp) throw new Error(`Icon export not found: ${pack} -> ${exportName}`);
361
- const id = computeIconId(pack, exportName);
362
- if (pack.includes("fortawesome")) {
363
- const [width, height, , , pathData] = Comp.icon;
364
- return {
365
- id,
366
- symbol: `<symbol id="${id}" viewBox="${`0 0 ${width} ${height}`}">${(Array.isArray(pathData) ? pathData : [pathData]).map((d) => `<path d="${d}" />`).join("")}</symbol>`
367
- };
368
- }
369
- const html = renderToStaticMarkup(createElement(Comp, {}));
370
- const viewBox = html.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24";
371
- const svgAttrsRaw = html.match(/^<svg\b([^>]*)>/i)?.[1] ?? "";
372
- const attrs = [];
373
- for (const [, k, v] of svgAttrsRaw.matchAll(ATTR_RE)) {
374
- const key = k.toLowerCase();
375
- if (PRESENTATION_ATTRS.has(key)) attrs.push(`${key}="${v}"`);
376
- }
377
- const inner = html.replace(/^<svg[^>]*>/i, "").replace(/<\/svg>\s*$/i, "").replace(/<svg[^>]*>/gi, "").replace(/<\/svg>/gi, "");
378
- return {
379
- id,
380
- symbol: `<symbol id="${id}" viewBox="${viewBox}"${attrs.length ? ` ${attrs.join(" ")}` : ""}>${inner}</symbol>`
381
- };
382
- };
383
- const buildSprite = async (icons) => {
384
- 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>`;
385
- };
386
- const createCollector = () => {
387
- const set = /* @__PURE__ */ new Map();
388
- return {
389
- add(pack, exportName) {
390
- set.set(`${pack}:${exportName}`, {
391
- pack,
392
- exportName
393
- });
394
- },
395
- toList() {
396
- return Array.from(set.values());
397
- },
398
- clear() {
399
- set.clear();
400
- }
401
- };
402
- };
403
- //#endregion
404
- export { transformModule as i, buildSprite as n, createCollector as r, DEFAULT_ICON_SOURCES as t };