react-icons-sprite 0.9.2-rc.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-sprite-j9zt_17z.mjs +147 -0
- package/dist/{collector-C6kn7NXC.mjs → collector-YV25X1xI.mjs} +1 -1
- package/dist/{compute-icon-id-C96eIC66.mjs → compute-icon-id-B590yK7l.mjs} +18 -4
- package/dist/transform-module-zJo6_2rz.mjs +477 -0
- package/dist/vite/plugin.mjs +8 -4
- package/dist/webpack/loader.mjs +2 -2
- package/dist/webpack/plugin.mjs +3 -3
- package/package.json +12 -13
- package/dist/build-sprite-BFhBc3Ev.mjs +0 -73
- package/dist/transform-module-DzmqobWK.mjs +0 -290
|
@@ -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 };
|
|
@@ -43,12 +43,26 @@ const DEFAULT_ICON_SOURCES = [
|
|
|
43
43
|
/^@mui\/icons-material(?:\/.*)?$/,
|
|
44
44
|
/^@carbon\/icons-react$/
|
|
45
45
|
];
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
"
|
|
46
|
+
const phosphorIconPathName = (name) => name.endsWith("Icon") ? name.slice(0, -4) : name;
|
|
47
|
+
const exactResolvers = {
|
|
48
|
+
"lucide-react": (pack, name) => `${pack}/dist/esm/icons/${kebabCase(name)}.mjs`,
|
|
49
|
+
"@radix-ui/react-icons": (pack) => `${pack}/dist/react-icons.esm.js`,
|
|
50
|
+
"@tabler/icons-react": (pack, name) => `${pack}/dist/esm/icons/${name}.mjs`,
|
|
51
|
+
"@phosphor-icons/react": (pack, name) => `${pack}/dist/ssr/${phosphorIconPathName(name)}`,
|
|
52
|
+
"phosphor-react": (pack, name) => `${pack}/dist/icons/${name}.esm.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`
|
|
49
58
|
};
|
|
50
59
|
const resolveIconImport = (pack, exportName) => {
|
|
51
|
-
|
|
60
|
+
const exactResolver = exactResolvers[pack];
|
|
61
|
+
if (exactResolver) return exactResolver(pack, exportName);
|
|
62
|
+
if (/^@mui\/icons-material(?:\/.*)?$/.test(pack)) return pack.split("/").length > 2 ? pack : `${pack}/${exportName}`;
|
|
63
|
+
if (/^@heroicons\/react\/(?:\d{2})\/(?:outline|solid)$/.test(pack)) return `${pack}/${exportName}`;
|
|
64
|
+
if (/^@fortawesome\/[\w-]+-svg-icons$/.test(pack)) return `${pack}/${exportName}`;
|
|
65
|
+
return pack;
|
|
52
66
|
};
|
|
53
67
|
//#endregion
|
|
54
68
|
//#region src/utils/compute-icon-id.ts
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { n as DEFAULT_ICON_SOURCES, t as computeIconId } from "./compute-icon-id-B590yK7l.mjs";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
//#region src/transform/edit-applier.ts
|
|
4
|
+
const applyEdits = (code, edits) => {
|
|
5
|
+
const magicString = new MagicString(code);
|
|
6
|
+
for (const edit of edits) {
|
|
7
|
+
if (edit.type === "replace") magicString.overwrite(edit.from, edit.to, edit.value);
|
|
8
|
+
if (edit.type === "insert") magicString.appendLeft(edit.pos, edit.value);
|
|
9
|
+
if (edit.type === "remove") magicString.remove(edit.from, edit.to);
|
|
10
|
+
}
|
|
11
|
+
return magicString;
|
|
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
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/transform/edit-builder.ts
|
|
45
|
+
const buildEdits = (usages, componentName, usedSymbols, register) => {
|
|
46
|
+
const edits = [];
|
|
47
|
+
const iconIdCache = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const usage of usages) {
|
|
49
|
+
edits.push({
|
|
50
|
+
type: "replace",
|
|
51
|
+
from: usage.range[0],
|
|
52
|
+
to: usage.range[1],
|
|
53
|
+
value: componentName
|
|
54
|
+
});
|
|
55
|
+
if (usage.kind === "opening") {
|
|
56
|
+
if (!usage.hasIconId) {
|
|
57
|
+
const key = `${usage.pack}:${usage.exportName}`;
|
|
58
|
+
let iconId = iconIdCache.get(key);
|
|
59
|
+
if (!iconId) {
|
|
60
|
+
iconId = computeIconId(usage.pack, usage.exportName);
|
|
61
|
+
iconIdCache.set(key, iconId);
|
|
62
|
+
}
|
|
63
|
+
edits.push({
|
|
64
|
+
type: "insert",
|
|
65
|
+
pos: usage.range[1],
|
|
66
|
+
value: ` iconId="${iconId}"`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
usedSymbols.add(usage.local);
|
|
70
|
+
register(usage.pack, usage.exportName);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return edits;
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/transform/fast-filter.ts
|
|
77
|
+
const fastFilter = (code) => {
|
|
78
|
+
if (!code.includes("<")) return false;
|
|
79
|
+
if (!code.includes("import")) return false;
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/transform/transform-module.ts
|
|
84
|
+
const ICON_SOURCE = "react-icons-sprite";
|
|
85
|
+
const ICON_COMPONENT_NAME = "ReactIconsSpriteIcon";
|
|
86
|
+
const FONTAWESOME_REACT_PACK = "@fortawesome/react-fontawesome";
|
|
87
|
+
const isFontAwesomeIconPack = (pack) => {
|
|
88
|
+
return /^@fortawesome\/[\w-]+-svg-icons$/.test(pack);
|
|
89
|
+
};
|
|
90
|
+
const sourceMatches = (source, pack) => {
|
|
91
|
+
source.lastIndex = 0;
|
|
92
|
+
return source.test(pack);
|
|
93
|
+
};
|
|
94
|
+
const IMPORT_RE = /import\s+([^;]+?)\s+from\s+(['"])([^'"]+)\2\s*;?/g;
|
|
95
|
+
const trimRange = (value, offset) => {
|
|
96
|
+
let start = 0;
|
|
97
|
+
let end = value.length;
|
|
98
|
+
while (start < end && /\s/.test(value[start])) start += 1;
|
|
99
|
+
while (end > start && /\s/.test(value[end - 1])) end -= 1;
|
|
100
|
+
return start === end ? null : [offset + start, offset + end];
|
|
101
|
+
};
|
|
102
|
+
const parseNamedSpecifiers = (specifier, specifierOffset, imports) => {
|
|
103
|
+
const start = specifier.indexOf("{");
|
|
104
|
+
const end = specifier.lastIndexOf("}");
|
|
105
|
+
if (start === -1 || end === -1 || end <= start) return;
|
|
106
|
+
let segmentStart = start + 1;
|
|
107
|
+
for (let index = start + 1; index <= end; index += 1) {
|
|
108
|
+
if (index !== end && specifier[index] !== ",") continue;
|
|
109
|
+
const range = trimRange(specifier.slice(segmentStart, index), specifierOffset + segmentStart);
|
|
110
|
+
segmentStart = index + 1;
|
|
111
|
+
if (!range) continue;
|
|
112
|
+
const text = specifier.slice(range[0] - specifierOffset, range[1] - specifierOffset);
|
|
113
|
+
if (text.startsWith("type ")) continue;
|
|
114
|
+
const aliasMatch = /^(.*?)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(text);
|
|
115
|
+
const exportName = aliasMatch ? aliasMatch[1].trim() : text.trim();
|
|
116
|
+
const local = aliasMatch ? aliasMatch[2] : exportName;
|
|
117
|
+
if (exportName && local) imports.push({
|
|
118
|
+
local,
|
|
119
|
+
exportName,
|
|
120
|
+
range
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const countImportSpecifiers = (specifier) => {
|
|
125
|
+
let count = 0;
|
|
126
|
+
const braceStart = specifier.indexOf("{");
|
|
127
|
+
if ((braceStart === -1 ? specifier : specifier.slice(0, braceStart)).replace(/,$/, "").trim()) count += 1;
|
|
128
|
+
const braceEnd = specifier.lastIndexOf("}");
|
|
129
|
+
if (braceStart !== -1 && braceEnd > braceStart) {
|
|
130
|
+
const named = specifier.slice(braceStart + 1, braceEnd);
|
|
131
|
+
for (const segment of named.split(",")) if (segment.trim()) count += 1;
|
|
132
|
+
}
|
|
133
|
+
return count;
|
|
134
|
+
};
|
|
135
|
+
const scanImportsDetailed = (code, sources) => {
|
|
136
|
+
const imports = [];
|
|
137
|
+
IMPORT_RE.lastIndex = 0;
|
|
138
|
+
for (const match of code.matchAll(IMPORT_RE)) {
|
|
139
|
+
const [statement, specifier, , pack] = match;
|
|
140
|
+
if (!sources.some((source) => sourceMatches(source, pack))) continue;
|
|
141
|
+
if (specifier.trim().startsWith("type ")) continue;
|
|
142
|
+
const matchStart = match.index;
|
|
143
|
+
const specifierOffset = matchStart + statement.indexOf(specifier);
|
|
144
|
+
const specifiers = [];
|
|
145
|
+
const braceStart = specifier.indexOf("{");
|
|
146
|
+
const defaultRange = trimRange((braceStart === -1 ? specifier : specifier.slice(0, braceStart)).replace(/,$/, ""), specifierOffset);
|
|
147
|
+
if (defaultRange && !specifier.slice(defaultRange[0] - specifierOffset, defaultRange[1] - specifierOffset).startsWith("type ")) {
|
|
148
|
+
const local = specifier.slice(defaultRange[0] - specifierOffset, defaultRange[1] - specifierOffset);
|
|
149
|
+
if (local && /^[A-Za-z_$][\w$]*$/.test(local)) specifiers.push({
|
|
150
|
+
local,
|
|
151
|
+
exportName: "default",
|
|
152
|
+
range: defaultRange
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
parseNamedSpecifiers(specifier, specifierOffset, specifiers);
|
|
156
|
+
if (specifiers.length) imports.push({
|
|
157
|
+
pack,
|
|
158
|
+
declarationRange: [matchStart, matchStart + statement.length],
|
|
159
|
+
specifiers,
|
|
160
|
+
specifierCount: countImportSpecifiers(specifier)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return imports;
|
|
164
|
+
};
|
|
165
|
+
const buildScannedSymbolTable = (imports) => {
|
|
166
|
+
const table = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const item of imports) for (const specifier of item.specifiers) table.set(specifier.local, {
|
|
168
|
+
pack: item.pack,
|
|
169
|
+
exportName: specifier.exportName
|
|
170
|
+
});
|
|
171
|
+
return table;
|
|
172
|
+
};
|
|
173
|
+
const scanSpriteIconImport = (code) => {
|
|
174
|
+
IMPORT_RE.lastIndex = 0;
|
|
175
|
+
for (const match of code.matchAll(IMPORT_RE)) {
|
|
176
|
+
const [, specifier, , source] = match;
|
|
177
|
+
if (source !== "react-icons-sprite") continue;
|
|
178
|
+
const specifiers = [];
|
|
179
|
+
parseNamedSpecifiers(specifier, match.index + match[0].indexOf(specifier), specifiers);
|
|
180
|
+
for (const item of specifiers) if (item.exportName === "ReactIconsSpriteIcon") return {
|
|
181
|
+
hasImport: true,
|
|
182
|
+
localName: item.local
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
hasImport: false,
|
|
187
|
+
localName: ICON_COMPONENT_NAME
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const escapeRegExp = (value) => {
|
|
191
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
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
|
+
};
|
|
202
|
+
const scanJsxIconUsages = (code, symbols) => {
|
|
203
|
+
if (!symbols.size) return [];
|
|
204
|
+
const usages = [];
|
|
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);
|
|
219
|
+
const symbol = symbols.get(local);
|
|
220
|
+
if (!symbol) continue;
|
|
221
|
+
const kind = closing ? "closing" : "opening";
|
|
222
|
+
let hasIconId = false;
|
|
223
|
+
if (!closing) {
|
|
224
|
+
const tagEnd = findJsxOpeningTagEnd(code, cursor);
|
|
225
|
+
hasIconId = tagEnd !== -1 && hasIconIdAttribute(code, cursor, tagEnd);
|
|
226
|
+
}
|
|
227
|
+
usages.push({
|
|
228
|
+
local,
|
|
229
|
+
range: [localStart, cursor],
|
|
230
|
+
pack: symbol.pack,
|
|
231
|
+
exportName: symbol.exportName,
|
|
232
|
+
kind,
|
|
233
|
+
hasIconId
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return usages;
|
|
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
|
+
};
|
|
248
|
+
const findJsxOpeningTagEnd = (code, start) => {
|
|
249
|
+
let quote = null;
|
|
250
|
+
let braceDepth = 0;
|
|
251
|
+
for (let index = start; index < code.length; index += 1) {
|
|
252
|
+
const char = code[index];
|
|
253
|
+
if (quote) {
|
|
254
|
+
if (char === quote && code[index - 1] !== "\\") quote = null;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
258
|
+
quote = char;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (char === "{") {
|
|
262
|
+
braceDepth += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (char === "}") {
|
|
266
|
+
braceDepth -= 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (char === ">" && braceDepth === 0) return index;
|
|
270
|
+
}
|
|
271
|
+
return -1;
|
|
272
|
+
};
|
|
273
|
+
const scanFontAwesomeComponents = (code) => {
|
|
274
|
+
const locals = /* @__PURE__ */ new Set();
|
|
275
|
+
IMPORT_RE.lastIndex = 0;
|
|
276
|
+
for (const match of code.matchAll(IMPORT_RE)) {
|
|
277
|
+
const [, specifier, , source] = match;
|
|
278
|
+
if (source !== FONTAWESOME_REACT_PACK) continue;
|
|
279
|
+
const specifiers = [];
|
|
280
|
+
parseNamedSpecifiers(specifier, match.index + match[0].indexOf(specifier), specifiers);
|
|
281
|
+
for (const item of specifiers) if (item.exportName === "FontAwesomeIcon") locals.add(item.local);
|
|
282
|
+
}
|
|
283
|
+
return locals;
|
|
284
|
+
};
|
|
285
|
+
const scanFontAwesomeUsages = (code, symbols, componentLocals) => {
|
|
286
|
+
if (!componentLocals.size) return [];
|
|
287
|
+
const names = [...componentLocals].map(escapeRegExp).join("|");
|
|
288
|
+
const tagRe = new RegExp(`<\\s*(${names})\\b`, "g");
|
|
289
|
+
const usages = [];
|
|
290
|
+
for (const match of code.matchAll(tagRe)) {
|
|
291
|
+
const componentLocal = match[1];
|
|
292
|
+
const componentStart = match.index + match[0].lastIndexOf(componentLocal);
|
|
293
|
+
const tagEnd = findJsxOpeningTagEnd(code, componentStart + componentLocal.length);
|
|
294
|
+
if (tagEnd === -1) continue;
|
|
295
|
+
const attributes = code.slice(componentStart + componentLocal.length, tagEnd);
|
|
296
|
+
const iconMatch = /\sicon\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}/.exec(attributes);
|
|
297
|
+
if (!iconMatch || iconMatch.index === void 0) continue;
|
|
298
|
+
const symbol = symbols.get(iconMatch[1]);
|
|
299
|
+
if (!symbol || !isFontAwesomeIconPack(symbol.pack)) continue;
|
|
300
|
+
const attributeStart = componentStart + componentLocal.length + iconMatch.index;
|
|
301
|
+
usages.push({
|
|
302
|
+
componentLocal,
|
|
303
|
+
componentRange: [componentStart, componentStart + componentLocal.length],
|
|
304
|
+
iconAttributeRange: [attributeStart, attributeStart + iconMatch[0].length],
|
|
305
|
+
hasIconId: /\biconId\s*=/.test(attributes),
|
|
306
|
+
iconLocal: iconMatch[1],
|
|
307
|
+
pack: symbol.pack,
|
|
308
|
+
exportName: symbol.exportName
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return usages;
|
|
312
|
+
};
|
|
313
|
+
const cleanupScannedImports = (code, imports, usedLocals) => {
|
|
314
|
+
const edits = [];
|
|
315
|
+
for (const item of imports) {
|
|
316
|
+
const usedSpecifiers = item.specifiers.filter((specifier) => usedLocals.has(specifier.local));
|
|
317
|
+
if (!usedSpecifiers.length) continue;
|
|
318
|
+
if (usedSpecifiers.length === item.specifiers.length && item.specifierCount === item.specifiers.length) {
|
|
319
|
+
edits.push({
|
|
320
|
+
type: "remove",
|
|
321
|
+
from: item.declarationRange[0],
|
|
322
|
+
to: extendToLineEnd(code, item.declarationRange[1])
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
for (const specifier of usedSpecifiers) {
|
|
327
|
+
const [from, to] = specifierRemovalRange(code, specifier.range);
|
|
328
|
+
edits.push({
|
|
329
|
+
type: "remove",
|
|
330
|
+
from,
|
|
331
|
+
to
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return edits;
|
|
336
|
+
};
|
|
337
|
+
const cleanupScannedFontAwesomeComponentImports = (code, usedLocals) => {
|
|
338
|
+
const edits = [];
|
|
339
|
+
IMPORT_RE.lastIndex = 0;
|
|
340
|
+
for (const match of code.matchAll(IMPORT_RE)) {
|
|
341
|
+
const [statement, specifier, , source] = match;
|
|
342
|
+
if (source !== FONTAWESOME_REACT_PACK) continue;
|
|
343
|
+
const specifiers = [];
|
|
344
|
+
parseNamedSpecifiers(specifier, match.index + statement.indexOf(specifier), specifiers);
|
|
345
|
+
const removableSpecifiers = specifiers.filter((item) => item.exportName === "FontAwesomeIcon" && usedLocals.has(item.local));
|
|
346
|
+
if (!removableSpecifiers.length) continue;
|
|
347
|
+
if (removableSpecifiers.length === specifiers.length) {
|
|
348
|
+
edits.push({
|
|
349
|
+
type: "remove",
|
|
350
|
+
from: match.index,
|
|
351
|
+
to: extendToLineEnd(code, match.index + statement.length)
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
for (const specifier of removableSpecifiers) {
|
|
356
|
+
const [from, to] = specifierRemovalRange(code, specifier.range);
|
|
357
|
+
edits.push({
|
|
358
|
+
type: "remove",
|
|
359
|
+
from,
|
|
360
|
+
to
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return edits;
|
|
365
|
+
};
|
|
366
|
+
const extendToLineEnd = (code, end) => {
|
|
367
|
+
let to = end;
|
|
368
|
+
while (to < code.length && /[ \t]/.test(code[to])) to += 1;
|
|
369
|
+
if (code[to] === "\r" && code[to + 1] === "\n") return to + 2;
|
|
370
|
+
if (code[to] === "\n") return to + 1;
|
|
371
|
+
return to;
|
|
372
|
+
};
|
|
373
|
+
const specifierRemovalRange = (code, [start, end]) => {
|
|
374
|
+
let from = start;
|
|
375
|
+
let to = end;
|
|
376
|
+
let before = start - 1;
|
|
377
|
+
while (before >= 0 && /\s/.test(code[before])) before -= 1;
|
|
378
|
+
if (before >= 0 && code[before] === ",") {
|
|
379
|
+
from = before;
|
|
380
|
+
return [from, to];
|
|
381
|
+
}
|
|
382
|
+
let after = end;
|
|
383
|
+
while (after < code.length && /\s/.test(code[after])) after += 1;
|
|
384
|
+
if (after < code.length && code[after] === ",") to = consumeTrailingWhitespace(code, after + 1);
|
|
385
|
+
return [from, to];
|
|
386
|
+
};
|
|
387
|
+
const consumeTrailingWhitespace = (code, start) => {
|
|
388
|
+
let to = start;
|
|
389
|
+
while (to < code.length && /\s/.test(code[to])) to += 1;
|
|
390
|
+
return to;
|
|
391
|
+
};
|
|
392
|
+
const transformModule = (code, id, register, sources = DEFAULT_ICON_SOURCES, options = {}) => {
|
|
393
|
+
const { sourceMap = false } = options;
|
|
394
|
+
if (!fastFilter(code)) return {
|
|
395
|
+
code,
|
|
396
|
+
map: null,
|
|
397
|
+
anyReplacements: false
|
|
398
|
+
};
|
|
399
|
+
const scannedImports = scanImportsDetailed(code, sources);
|
|
400
|
+
if (!scannedImports.length) return {
|
|
401
|
+
code,
|
|
402
|
+
map: null,
|
|
403
|
+
anyReplacements: false
|
|
404
|
+
};
|
|
405
|
+
const hasPotentialFontAwesomeUsage = code.includes(FONTAWESOME_REACT_PACK) && scannedImports.some((item) => isFontAwesomeIconPack(item.pack));
|
|
406
|
+
const table = buildScannedSymbolTable(scannedImports);
|
|
407
|
+
if (!table.size) return {
|
|
408
|
+
code,
|
|
409
|
+
map: null,
|
|
410
|
+
anyReplacements: false
|
|
411
|
+
};
|
|
412
|
+
const spriteIconImport = code.includes("react-icons-sprite") ? scanSpriteIconImport(code) : {
|
|
413
|
+
hasImport: false,
|
|
414
|
+
localName: ICON_COMPONENT_NAME
|
|
415
|
+
};
|
|
416
|
+
const usages = scanJsxIconUsages(code, table);
|
|
417
|
+
const fontAwesomeUsages = hasPotentialFontAwesomeUsage ? scanFontAwesomeUsages(code, table, scanFontAwesomeComponents(code)) : [];
|
|
418
|
+
if (!usages.length && !fontAwesomeUsages.length) return {
|
|
419
|
+
code,
|
|
420
|
+
map: null,
|
|
421
|
+
anyReplacements: false
|
|
422
|
+
};
|
|
423
|
+
const used = /* @__PURE__ */ new Set();
|
|
424
|
+
const usedFontAwesomeComponents = /* @__PURE__ */ new Set();
|
|
425
|
+
const edits = buildEdits(usages, spriteIconImport.localName, used, register);
|
|
426
|
+
const registeredFontAwesomeIcons = /* @__PURE__ */ new Set();
|
|
427
|
+
for (const usage of fontAwesomeUsages) {
|
|
428
|
+
edits.push({
|
|
429
|
+
type: "replace",
|
|
430
|
+
from: usage.componentRange[0],
|
|
431
|
+
to: usage.componentRange[1],
|
|
432
|
+
value: spriteIconImport.localName
|
|
433
|
+
});
|
|
434
|
+
if (!usage.hasIconId) edits.push({
|
|
435
|
+
type: "insert",
|
|
436
|
+
pos: usage.componentRange[1],
|
|
437
|
+
value: ` iconId="${computeIconId(usage.pack, usage.exportName)}"`
|
|
438
|
+
});
|
|
439
|
+
edits.push({
|
|
440
|
+
type: "remove",
|
|
441
|
+
from: usage.iconAttributeRange[0],
|
|
442
|
+
to: consumeTrailingWhitespace(code, usage.iconAttributeRange[1])
|
|
443
|
+
});
|
|
444
|
+
used.add(usage.iconLocal);
|
|
445
|
+
usedFontAwesomeComponents.add(usage.componentLocal);
|
|
446
|
+
const key = `${usage.pack}:${usage.exportName}`;
|
|
447
|
+
if (!registeredFontAwesomeIcons.has(key)) {
|
|
448
|
+
registeredFontAwesomeIcons.add(key);
|
|
449
|
+
register(usage.pack, usage.exportName);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const cleanupEdits = cleanupScannedImports(code, scannedImports, used);
|
|
453
|
+
const cleanupFontAwesomeEdits = usedFontAwesomeComponents.size ? cleanupScannedFontAwesomeComponentImports(code, usedFontAwesomeComponents) : [];
|
|
454
|
+
const allEdits = [
|
|
455
|
+
...edits,
|
|
456
|
+
...cleanupEdits,
|
|
457
|
+
...cleanupFontAwesomeEdits
|
|
458
|
+
];
|
|
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);
|
|
466
|
+
if (!spriteIconImport.hasImport) magicString.prepend(importPrefix);
|
|
467
|
+
return {
|
|
468
|
+
code: magicString.toString(),
|
|
469
|
+
map: sourceMap ? magicString.generateMap({
|
|
470
|
+
source: id,
|
|
471
|
+
hires: true
|
|
472
|
+
}) : null,
|
|
473
|
+
anyReplacements: true
|
|
474
|
+
};
|
|
475
|
+
};
|
|
476
|
+
//#endregion
|
|
477
|
+
export { transformModule as t };
|
package/dist/vite/plugin.mjs
CHANGED
|
@@ -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-
|
|
3
|
-
import { t as buildSprite } from "../build-sprite-
|
|
4
|
-
import { t as transformModule } from "../transform-module-
|
|
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",
|
package/dist/webpack/loader.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as transformModule } from "../transform-module-
|
|
2
|
-
import { t as collector } from "../collector-
|
|
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;
|
package/dist/webpack/plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REACT_ICONS_SPRITE_URL_PLACEHOLDER } from "../index.mjs";
|
|
2
|
-
import { t as buildSprite } from "../build-sprite-
|
|
3
|
-
import { t as collector } from "../collector-
|
|
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": "0.
|
|
4
|
+
"version": "1.0.0-rc.2",
|
|
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>",
|
|
@@ -65,21 +65,20 @@
|
|
|
65
65
|
"react-dom": ">= 16"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"magic-string": "0.30.21"
|
|
69
|
-
"oxc-parser": "0.119.0"
|
|
68
|
+
"magic-string": "0.30.21"
|
|
70
69
|
},
|
|
71
70
|
"devDependencies": {
|
|
72
|
-
"@carbon/icons-react": "11.
|
|
73
|
-
"@types/node": "25.
|
|
71
|
+
"@carbon/icons-react": "11.81.0",
|
|
72
|
+
"@types/node": "25.9.1",
|
|
74
73
|
"@types/react-dom": "19.2.3",
|
|
75
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
76
|
-
"react": "19.2.
|
|
77
|
-
"react-dom": "19.2.
|
|
78
|
-
"tsdown": "0.
|
|
79
|
-
"typescript": "
|
|
80
|
-
"vite": "8.0.
|
|
81
|
-
"vitest": "4.1.
|
|
82
|
-
"webpack": "5.
|
|
74
|
+
"@typescript/native-preview": "7.0.0-dev.20260521.1",
|
|
75
|
+
"react": "19.2.6",
|
|
76
|
+
"react-dom": "19.2.6",
|
|
77
|
+
"tsdown": "0.22.0",
|
|
78
|
+
"typescript": "6.0.3",
|
|
79
|
+
"vite": "8.0.14",
|
|
80
|
+
"vitest": "4.1.7",
|
|
81
|
+
"webpack": "5.107.1"
|
|
83
82
|
},
|
|
84
83
|
"keywords": [
|
|
85
84
|
"vite",
|
|
@@ -1,73 +0,0 @@
|
|
|
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,290 +0,0 @@
|
|
|
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 };
|