react-icons-sprite 1.0.0-rc.1 → 1.0.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,147 @@
1
+ import { r as resolveIconImport, t as computeIconId } from "./compute-icon-id-B590yK7l.mjs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createElement } from "react";
6
+ import { renderToStaticMarkup } from "react-dom/server";
7
+ //#region src/sprite/render-icon.ts
8
+ const SVG_INNER_RE = /<svg\b[^>]*>([\s\S]*?)<\/svg>/i;
9
+ const VIEWBOX_RE = /viewBox=["']([^"']+)["']/i;
10
+ const SVG_OPEN_RE = /<svg\b([^>]*)>/i;
11
+ const SVG_ATTR_RE = /([:\w-]+)=("[^"]*"|'[^']*')/g;
12
+ const OMITTED_SVG_ATTRIBUTES = new Set([
13
+ "xmlns",
14
+ "viewBox",
15
+ "width",
16
+ "height"
17
+ ]);
18
+ const parsePackageSpecifier = (specifier) => {
19
+ if (specifier.startsWith(".") || path.isAbsolute(specifier)) return null;
20
+ const parts = specifier.split("/");
21
+ const packageName = specifier.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
22
+ const rest = parts.slice(specifier.startsWith("@") ? 2 : 1).join("/");
23
+ return {
24
+ packageName,
25
+ subpath: rest ? `./${rest}` : "."
26
+ };
27
+ };
28
+ const findPackageRoot = (packageName, baseDir) => {
29
+ let current = path.resolve(baseDir);
30
+ while (true) {
31
+ const candidate = path.join(current, "node_modules", packageName);
32
+ if (existsSync(path.join(candidate, "package.json"))) return candidate;
33
+ const parent = path.dirname(current);
34
+ if (parent === current) return null;
35
+ current = parent;
36
+ }
37
+ };
38
+ const pickExportTarget = (value) => {
39
+ if (typeof value === "string") return value;
40
+ if (!value || typeof value !== "object") return null;
41
+ const record = value;
42
+ return pickExportTarget(record.import) ?? pickExportTarget(record.default) ?? pickExportTarget(record.module) ?? pickExportTarget(record.require);
43
+ };
44
+ const resolveExportTarget = (exportsField, subpath) => {
45
+ if (typeof exportsField === "string" || Array.isArray(exportsField)) return subpath === "." ? pickExportTarget(exportsField) : null;
46
+ if (!exportsField || typeof exportsField !== "object") return null;
47
+ const exportsRecord = exportsField;
48
+ const exact = pickExportTarget(exportsRecord[subpath]);
49
+ if (exact) return exact;
50
+ for (const [key, value] of Object.entries(exportsRecord)) {
51
+ if (!key.includes("*")) continue;
52
+ const [prefix, suffix] = key.split("*");
53
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
54
+ const matched = subpath.slice(prefix.length, subpath.length - suffix.length);
55
+ const target = pickExportTarget(value);
56
+ if (target) return target.replaceAll("*", matched);
57
+ }
58
+ return null;
59
+ };
60
+ const fileExists = (filePath) => existsSync(filePath);
61
+ const resolveFileCandidate = (filePath) => {
62
+ return [
63
+ filePath,
64
+ `${filePath}.mjs`,
65
+ `${filePath}.js`,
66
+ path.join(filePath, "index.mjs"),
67
+ path.join(filePath, "index.js")
68
+ ].find(fileExists) ?? null;
69
+ };
70
+ const resolveFromBaseDir = (specifier, baseDir) => {
71
+ const parsed = parsePackageSpecifier(specifier);
72
+ if (!parsed) return null;
73
+ const packageRoot = findPackageRoot(parsed.packageName, baseDir);
74
+ if (!packageRoot) return null;
75
+ const packageJson = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
76
+ const exportTarget = resolveExportTarget(packageJson.exports, parsed.subpath);
77
+ if (exportTarget) return resolveFileCandidate(path.join(packageRoot, exportTarget)) ?? null;
78
+ if (parsed.subpath === ".") {
79
+ const entry = packageJson.module ?? packageJson.main;
80
+ if (entry) return resolveFileCandidate(path.join(packageRoot, entry));
81
+ }
82
+ return resolveFileCandidate(path.join(packageRoot, parsed.subpath));
83
+ };
84
+ const resolveImportSpecifier = (specifier, options) => {
85
+ if (!options.baseDir) return specifier;
86
+ const resolved = resolveFromBaseDir(specifier, options.baseDir);
87
+ return resolved ? pathToFileURL(resolved).href : specifier;
88
+ };
89
+ const extractSymbolAttributes = (svgMarkup) => {
90
+ const openingAttributes = SVG_OPEN_RE.exec(svgMarkup)?.[1];
91
+ if (!openingAttributes) return "";
92
+ const attributes = [];
93
+ for (const [, name, value] of openingAttributes.matchAll(SVG_ATTR_RE)) {
94
+ if (OMITTED_SVG_ATTRIBUTES.has(name)) continue;
95
+ attributes.push(`${name}=${value}`);
96
+ }
97
+ return attributes.join(" ");
98
+ };
99
+ const pickExport = (moduleExports, exportName) => {
100
+ if (exportName === "default") return moduleExports.default;
101
+ return moduleExports[exportName] ?? moduleExports.default;
102
+ };
103
+ const isRenderableComponent = (value) => {
104
+ if (typeof value === "function") return true;
105
+ if (!value || typeof value !== "object") return false;
106
+ return "$$typeof" in value;
107
+ };
108
+ const isFontAwesomeIconDefinition = (value) => {
109
+ if (!value || typeof value !== "object") return false;
110
+ const icon = value.icon;
111
+ if (!Array.isArray(icon) || icon.length < 5) return false;
112
+ return typeof icon[0] === "number" && typeof icon[1] === "number";
113
+ };
114
+ const renderFontAwesomeIconDefinition = (iconDefinition) => {
115
+ const [width, height, , , svgPathData] = iconDefinition.icon;
116
+ return {
117
+ symbolBody: (Array.isArray(svgPathData) ? svgPathData : [svgPathData]).map((d) => `<path d="${d}"/>`).join(""),
118
+ viewBox: `0 0 ${width} ${height}`,
119
+ symbolAttributes: ""
120
+ };
121
+ };
122
+ const renderIcon = async (pack, exportName, options = {}) => {
123
+ const iconComponent = pickExport(await import(resolveImportSpecifier(resolveIconImport(pack, exportName), options)), exportName);
124
+ if (isFontAwesomeIconDefinition(iconComponent)) return renderFontAwesomeIconDefinition(iconComponent);
125
+ if (!isRenderableComponent(iconComponent)) throw new Error(`[react-icons-sprite] Unable to render icon "${exportName}" from "${pack}". Expected a React component export.`);
126
+ const svgMarkup = renderToStaticMarkup(createElement(iconComponent));
127
+ const svgInner = SVG_INNER_RE.exec(svgMarkup)?.[1];
128
+ if (!svgInner) throw new Error(`[react-icons-sprite] Unable to extract SVG content for "${exportName}" from "${pack}".`);
129
+ return {
130
+ symbolBody: svgInner,
131
+ viewBox: VIEWBOX_RE.exec(svgMarkup)?.[1] ?? "0 0 24 24",
132
+ symbolAttributes: extractSymbolAttributes(svgMarkup)
133
+ };
134
+ };
135
+ //#endregion
136
+ //#region src/sprite/build-sprite.ts
137
+ const buildSprite = async (icons, options = {}) => {
138
+ if (!icons.length) return "<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display:none\"></svg>";
139
+ return `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${(await Promise.all(icons.map(async ({ pack, exportName }) => {
140
+ const rendered = await renderIcon(pack, exportName, { baseDir: options.baseDir });
141
+ const id = computeIconId(pack, exportName);
142
+ const symbolAttributes = rendered.symbolAttributes ? ` ${rendered.symbolAttributes}` : "";
143
+ return `<symbol id="${id}" viewBox="${rendered.viewBox}"${symbolAttributes}>${rendered.symbolBody}</symbol>`;
144
+ }))).join("")}</svg>`;
145
+ };
146
+ //#endregion
147
+ export { buildSprite as t };
@@ -1,4 +1,4 @@
1
- import { i as createCollector } from "./compute-icon-id-DKiwIqyH.mjs";
1
+ import { i as createCollector } from "./compute-icon-id-B590yK7l.mjs";
2
2
  //#region src/collector.ts
3
3
  const collector = createCollector();
4
4
  //#endregion
@@ -43,15 +43,18 @@ const DEFAULT_ICON_SOURCES = [
43
43
  /^@mui\/icons-material(?:\/.*)?$/,
44
44
  /^@carbon\/icons-react$/
45
45
  ];
46
+ const phosphorIconPathName = (name) => name.endsWith("Icon") ? name.slice(0, -4) : name;
46
47
  const exactResolvers = {
47
- "lucide-react": (pack, name) => `${pack}/dist/esm/icons/${kebabCase(name)}.js`,
48
- "@radix-ui/react-icons": (pack, name) => `${pack}/${name}`,
48
+ "lucide-react": (pack, name) => `${pack}/dist/esm/icons/${kebabCase(name)}.mjs`,
49
+ "@radix-ui/react-icons": (pack) => `${pack}/dist/react-icons.esm.js`,
49
50
  "@tabler/icons-react": (pack, name) => `${pack}/dist/esm/icons/${name}.mjs`,
50
- "@phosphor-icons/react": (pack, name) => `${pack}/dist/ssr/${name}.es.js`,
51
+ "@phosphor-icons/react": (pack, name) => `${pack}/dist/ssr/${phosphorIconPathName(name)}`,
51
52
  "phosphor-react": (pack, name) => `${pack}/dist/icons/${name}.esm.js`,
52
- "react-feather": (pack, name) => `${pack}/dist/icons/${kebabCase(name)}`,
53
- "react-bootstrap-icons": (pack, name) => `${pack}/dist/icons/${kebabCase(name)}`,
54
- "@carbon/icons-react": (pack, name) => `${pack}/lib/${name}.js`
53
+ "react-bootstrap-icons": (pack, name) => `${pack}/dist/icons/${kebabCase(name)}.js`,
54
+ "react-feather": (pack, name) => `${pack}/dist/icons/${kebabCase(name)}.js`,
55
+ "grommet-icons": (pack, name) => `${pack}/icons/${name}.js`,
56
+ "devicons-react": (pack, name) => `${pack}/icons/${name}`,
57
+ "@carbon/icons-react": (pack, name) => `${pack}/es/${name}.js`
55
58
  };
56
59
  const resolveIconImport = (pack, exportName) => {
57
60
  const exactResolver = exactResolvers[pack];
@@ -1,4 +1,4 @@
1
- import { n as DEFAULT_ICON_SOURCES, t as computeIconId } from "./compute-icon-id-DKiwIqyH.mjs";
1
+ import { n as DEFAULT_ICON_SOURCES, t as computeIconId } from "./compute-icon-id-B590yK7l.mjs";
2
2
  import MagicString from "magic-string";
3
3
  //#region src/transform/edit-applier.ts
4
4
  const applyEdits = (code, edits) => {
@@ -10,6 +10,36 @@ const applyEdits = (code, edits) => {
10
10
  }
11
11
  return magicString;
12
12
  };
13
+ const editStart = (edit) => {
14
+ return edit.type === "insert" ? edit.pos : edit.from;
15
+ };
16
+ const applyEditsToString = (code, edits) => {
17
+ if (!edits.length) return code;
18
+ const orderedEdits = [...edits].sort((left, right) => {
19
+ const startDelta = editStart(left) - editStart(right);
20
+ if (startDelta !== 0) return startDelta;
21
+ if (left.type === "insert" && right.type !== "insert") return 1;
22
+ if (left.type !== "insert" && right.type === "insert") return -1;
23
+ return 0;
24
+ });
25
+ let result = "";
26
+ let cursor = 0;
27
+ for (const edit of orderedEdits) {
28
+ if (edit.type === "insert") {
29
+ if (edit.pos <= cursor) {
30
+ result += edit.value;
31
+ continue;
32
+ }
33
+ result += code.slice(cursor, edit.pos) + edit.value;
34
+ cursor = edit.pos;
35
+ continue;
36
+ }
37
+ result += code.slice(cursor, edit.from);
38
+ if (edit.type === "replace") result += edit.value;
39
+ cursor = edit.to;
40
+ }
41
+ return result + code.slice(cursor);
42
+ };
13
43
  //#endregion
14
44
  //#region src/transform/edit-builder.ts
15
45
  const buildEdits = (usages, componentName, usedSymbols, register) => {
@@ -160,25 +190,43 @@ const scanSpriteIconImport = (code) => {
160
190
  const escapeRegExp = (value) => {
161
191
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
192
  };
193
+ const isIdentifierStart = (char) => {
194
+ return char !== void 0 && (char >= "A" && char <= "Z" || char >= "a" && char <= "z" || char === "_" || char === "$");
195
+ };
196
+ const isIdentifierPart = (char) => {
197
+ return char !== void 0 && (isIdentifierStart(char) || char >= "0" && char <= "9");
198
+ };
199
+ const isWhitespace = (char) => {
200
+ return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
201
+ };
163
202
  const scanJsxIconUsages = (code, symbols) => {
164
203
  if (!symbols.size) return [];
165
- const names = [...symbols.keys()].map(escapeRegExp).join("|");
166
- const tagRe = new RegExp(`<\\s*(/?)\\s*(${names})\\b`, "g");
167
204
  const usages = [];
168
- for (const match of code.matchAll(tagRe)) {
169
- const [, closing, local] = match;
205
+ for (let index = 0; index < code.length; index += 1) {
206
+ if (code.charCodeAt(index) !== 60) continue;
207
+ let cursor = index + 1;
208
+ while (isWhitespace(code[cursor])) cursor += 1;
209
+ const closing = code[cursor] === "/";
210
+ if (closing) {
211
+ cursor += 1;
212
+ while (isWhitespace(code[cursor])) cursor += 1;
213
+ }
214
+ if (!isIdentifierStart(code[cursor])) continue;
215
+ const localStart = cursor;
216
+ cursor += 1;
217
+ while (isIdentifierPart(code[cursor])) cursor += 1;
218
+ const local = code.slice(localStart, cursor);
170
219
  const symbol = symbols.get(local);
171
220
  if (!symbol) continue;
172
- const localStart = match.index + match[0].lastIndexOf(local);
173
221
  const kind = closing ? "closing" : "opening";
174
222
  let hasIconId = false;
175
223
  if (!closing) {
176
- const tagEnd = findJsxOpeningTagEnd(code, localStart + local.length);
177
- hasIconId = tagEnd !== -1 && /\biconId\s*=/.test(code.slice(localStart + local.length, tagEnd));
224
+ const tagEnd = findJsxOpeningTagEnd(code, cursor);
225
+ hasIconId = tagEnd !== -1 && hasIconIdAttribute(code, cursor, tagEnd);
178
226
  }
179
227
  usages.push({
180
228
  local,
181
- range: [localStart, localStart + local.length],
229
+ range: [localStart, cursor],
182
230
  pack: symbol.pack,
183
231
  exportName: symbol.exportName,
184
232
  kind,
@@ -187,6 +235,16 @@ const scanJsxIconUsages = (code, symbols) => {
187
235
  }
188
236
  return usages;
189
237
  };
238
+ const hasIconIdAttribute = (code, start, end) => {
239
+ for (let index = start; index < end; index += 1) {
240
+ if (code.charCodeAt(index) !== 105 || code.charCodeAt(index + 1) !== 99 || code.charCodeAt(index + 2) !== 111 || code.charCodeAt(index + 3) !== 110 || code.charCodeAt(index + 4) !== 73 || code.charCodeAt(index + 5) !== 100) continue;
241
+ if (isIdentifierPart(code[index - 1]) || isIdentifierPart(code[index + 6])) continue;
242
+ let cursor = index + 6;
243
+ while (isWhitespace(code[cursor])) cursor += 1;
244
+ if (code[cursor] === "=") return true;
245
+ }
246
+ return false;
247
+ };
190
248
  const findJsxOpeningTagEnd = (code, start) => {
191
249
  let quote = null;
192
250
  let braceDepth = 0;
@@ -351,7 +409,10 @@ const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES, opt
351
409
  map: null,
352
410
  anyReplacements: false
353
411
  };
354
- const spriteIconImport = scanSpriteIconImport(code);
412
+ const spriteIconImport = code.includes("react-icons-sprite") ? scanSpriteIconImport(code) : {
413
+ hasImport: false,
414
+ localName: ICON_COMPONENT_NAME
415
+ };
355
416
  const usages = scanJsxIconUsages(code, table);
356
417
  const fontAwesomeUsages = hasPotentialFontAwesomeUsage ? scanFontAwesomeUsages(code, table, scanFontAwesomeComponents(code)) : [];
357
418
  if (!usages.length && !fontAwesomeUsages.length) return {
@@ -389,13 +450,19 @@ const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES, opt
389
450
  }
390
451
  }
391
452
  const cleanupEdits = cleanupScannedImports(code, scannedImports, used);
392
- const cleanupFontAwesomeEdits = cleanupScannedFontAwesomeComponentImports(code, usedFontAwesomeComponents);
393
- const magicString = applyEdits(code, [
453
+ const cleanupFontAwesomeEdits = usedFontAwesomeComponents.size ? cleanupScannedFontAwesomeComponentImports(code, usedFontAwesomeComponents) : [];
454
+ const allEdits = [
394
455
  ...edits,
395
456
  ...cleanupEdits,
396
457
  ...cleanupFontAwesomeEdits
397
- ]);
458
+ ];
398
459
  const importPrefix = `import { ${ICON_COMPONENT_NAME} } from "${ICON_SOURCE}";\n`;
460
+ if (!sourceMap) return {
461
+ code: `${spriteIconImport.hasImport ? "" : importPrefix}${applyEditsToString(code, allEdits)}`,
462
+ map: null,
463
+ anyReplacements: true
464
+ };
465
+ const magicString = applyEdits(code, allEdits);
399
466
  if (!spriteIconImport.hasImport) magicString.prepend(importPrefix);
400
467
  return {
401
468
  code: magicString.toString(),
@@ -1,12 +1,13 @@
1
1
  import { REACT_ICONS_SPRITE_URL_PLACEHOLDER } from "../index.mjs";
2
- import { i as createCollector, n as DEFAULT_ICON_SOURCES } from "../compute-icon-id-DKiwIqyH.mjs";
3
- import { t as buildSprite } from "../build-sprite-BZCizCDt.mjs";
4
- import { t as transformModule } from "../transform-module-Dn8lfegG.mjs";
2
+ import { i as createCollector, n as DEFAULT_ICON_SOURCES } from "../compute-icon-id-B590yK7l.mjs";
3
+ import { t as buildSprite } from "../build-sprite-j9zt_17z.mjs";
4
+ import { t as transformModule } from "../transform-module-zJo6_2rz.mjs";
5
5
  import { createHash } from "node:crypto";
6
6
  //#region src/vite/plugin.ts
7
7
  const reactIconsSprite = (options = {}) => {
8
8
  const { fileName } = options;
9
9
  const collector = createCollector();
10
+ let root = process.cwd();
10
11
  return {
11
12
  name: "vite-plugin-react-icons-sprite",
12
13
  enforce: "pre",
@@ -14,6 +15,9 @@ const reactIconsSprite = (options = {}) => {
14
15
  buildStart() {
15
16
  collector.clear();
16
17
  },
18
+ configResolved(config) {
19
+ root = config.root;
20
+ },
17
21
  transform(code, id) {
18
22
  const cleanId = id.split("?", 1)[0];
19
23
  if (!/\.(mjs|cjs|js|jsx|ts|tsx)$/.test(cleanId)) return null;
@@ -32,7 +36,7 @@ const reactIconsSprite = (options = {}) => {
32
36
  }
33
37
  },
34
38
  async generateBundle(_options, bundle) {
35
- const spriteXml = await buildSprite(collector.toList());
39
+ const spriteXml = await buildSprite(collector.toList(), { baseDir: root });
36
40
  const generatedHash = createHash("sha256").update(spriteXml).digest("hex").slice(0, 8);
37
41
  const emitFileOptions = {
38
42
  type: "asset",
@@ -1,5 +1,5 @@
1
- import { t as transformModule } from "../transform-module-Dn8lfegG.mjs";
2
- import { t as collector } from "../collector-wdoob7qt.mjs";
1
+ import { t as transformModule } from "../transform-module-zJo6_2rz.mjs";
2
+ import { t as collector } from "../collector-YV25X1xI.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 { t as buildSprite } from "../build-sprite-BZCizCDt.mjs";
3
- import { t as collector } from "../collector-wdoob7qt.mjs";
2
+ import { t as buildSprite } from "../build-sprite-j9zt_17z.mjs";
3
+ import { t as collector } from "../collector-YV25X1xI.mjs";
4
4
  import { createHash } from "node:crypto";
5
5
  //#region src/webpack/plugin.ts
6
6
  var ReactIconsSpriteWebpackPlugin = class {
@@ -18,7 +18,7 @@ var ReactIconsSpriteWebpackPlugin = class {
18
18
  name: pluginName,
19
19
  stage
20
20
  }, async () => {
21
- const spriteXml = await buildSprite(collector.toList());
21
+ const spriteXml = await buildSprite(collector.toList(), { baseDir: compiler.context });
22
22
  const generatedHash = createHash("sha256").update(spriteXml).digest("hex").slice(0, 8);
23
23
  const name = this.fileName ?? `react-icons-sprite-${generatedHash}.svg`;
24
24
  const RawSource = compiler.webpack?.sources?.RawSource;
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": "1.0.0-rc.1",
4
+ "version": "1.0.0",
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>",
@@ -71,7 +71,7 @@
71
71
  "@carbon/icons-react": "11.81.0",
72
72
  "@types/node": "25.9.1",
73
73
  "@types/react-dom": "19.2.3",
74
- "@typescript/native-preview": "7.0.0-dev.20260521.1",
74
+ "@typescript/native-preview": "7.0.0-dev.20260522.1",
75
75
  "react": "19.2.6",
76
76
  "react-dom": "19.2.6",
77
77
  "tsdown": "0.22.0",
@@ -1,73 +0,0 @@
1
- import { r as resolveIconImport, t as computeIconId } from "./compute-icon-id-DKiwIqyH.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 };