rdicons 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/cli.js +366 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ npx rdicons add tabler:bell ph:heart --format tsx --out src/icons --color "#4f46
|
|
|
18
18
|
rdicons add <set:name> [more...] download icons into a folder
|
|
19
19
|
rdicons search <query> [--set s] find icons by name
|
|
20
20
|
rdicons info <set:name> show the code for one icon (every install option)
|
|
21
|
+
rdicons migrate <path...> --to <set> switch a project's icon imports to another library
|
|
22
|
+
rdicons audit <path...> which icon libraries a project uses, duplicates and licenses
|
|
21
23
|
rdicons sets list the 19 libraries with their licenses
|
|
22
24
|
```
|
|
23
25
|
|
|
@@ -30,6 +32,29 @@ rdicons sets list the 19 libraries with their licenses
|
|
|
30
32
|
| `--stroke` | the icon's own | stroke width (outline icons) |
|
|
31
33
|
| `--padding` | `0` | extra space around the icon |
|
|
32
34
|
| `--background` | none | solid background |
|
|
35
|
+
| `--animated [mode]` | off | rd icons only: `hover` (default), `parent`, `play`, `loop` |
|
|
36
|
+
| `--speed` | `1` | animation speed (with `--animated`) |
|
|
37
|
+
|
|
38
|
+
## Switch icon library: `migrate`
|
|
39
|
+
|
|
40
|
+
Moving a project from one icon library to another usually means editing every import by hand. `migrate` does it for you:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx rdicons migrate src --to tabler # preview: nothing is changed
|
|
44
|
+
npx rdicons migrate src --to rd --write # apply it (commit your work first)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It reads the imports of `lucide-react`, `@tabler/icons-react`, `@phosphor-icons/react`, `@heroicons/react` and `react-icons` (Feather, Lucide, Tabler, Phosphor, Remix, Bootstrap, Octicons, Radix, Heroicons 2, Material) and points them at `@rdicons/react/<library>` (or `@rdicons/vue/<library>` for Vue). Icons are matched by name and by common alternative names (`home` and `house`); the names you use in your JSX stay the same. Icons with no match stay on the old import and are listed, so the project keeps building. Tabler's `stroke` prop becomes `strokeWidth`. `--ext` chooses the file types, `node_modules` and build folders are skipped.
|
|
48
|
+
|
|
49
|
+
## What does the project use? `audit`
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx rdicons audit src # read-only report
|
|
53
|
+
npx rdicons audit src --strict # exit 1 for mixed libraries, duplicates or a library that needs credit (for CI)
|
|
54
|
+
npx rdicons audit src --json # the same as JSON
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It lists the libraries your code imports icons from (rdicons packages, `lucide-react`, Tabler, Phosphor, Heroicons, `react-icons`), how many distinct icons each one supplies, icons used from two libraries at once (`house` and `home`), the license of each library (and who you must credit for CC BY), and which of the icons from other libraries also exist in rd. Nothing is changed.
|
|
33
58
|
| `--animated [mode]` | off | add the icon's animation (rd icons): `hover` (default), `parent`, `play` or `loop`. The CSS is inside the file. Other libraries have no animation and get the still icon |
|
|
34
59
|
| `--speed <n>` | `1` | animation speed, `2` is twice as fast (needs `--animated`) |
|
|
35
60
|
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
-
import { join, resolve } from "path";
|
|
4
|
+
import { mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
5
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
6
6
|
|
|
7
7
|
// ../core/dist/index.js
|
|
8
8
|
var STROKE_WIDTH_RE = /stroke-width="[^"]*"/g;
|
|
@@ -124,8 +124,328 @@ export function Example() {
|
|
|
124
124
|
];
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// src/audit.ts
|
|
128
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
129
|
+
import { resolve as resolve2 } from "path";
|
|
130
|
+
|
|
131
|
+
// src/migrate.ts
|
|
132
|
+
import { readdir, readFile, stat, writeFile } from "fs/promises";
|
|
133
|
+
import { join, relative, resolve } from "path";
|
|
134
|
+
var kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-zA-Z])(\d)/g, "$1-$2").toLowerCase();
|
|
135
|
+
var stripSuffix = (s, suffix) => s.length > suffix.length && s.endsWith(suffix) ? s.slice(0, -suffix.length) : s;
|
|
136
|
+
var stripPrefix = (s, prefix) => s.length > prefix.length && s.startsWith(prefix) ? s.slice(prefix.length) : s;
|
|
137
|
+
var lucide = (framework) => ({ set: "lucide", framework, slug: (n) => kebab(stripSuffix(stripPrefix(n, "Lucide"), "Icon")) });
|
|
138
|
+
var REACT_ICONS = {
|
|
139
|
+
fi: { set: "feather" },
|
|
140
|
+
lu: { set: "lucide" },
|
|
141
|
+
tb: { set: "tabler" },
|
|
142
|
+
pi: { set: "ph" },
|
|
143
|
+
ri: { set: "ri" },
|
|
144
|
+
bs: { set: "bi" },
|
|
145
|
+
go: { set: "octicon" },
|
|
146
|
+
rx: { set: "radix-icons" },
|
|
147
|
+
hi2: { set: "heroicons" },
|
|
148
|
+
md: { set: "material-symbols" }
|
|
149
|
+
};
|
|
150
|
+
function sourceFor(spec) {
|
|
151
|
+
if (spec === "lucide-react") return lucide("react");
|
|
152
|
+
if (spec === "lucide-vue-next") return lucide("vue");
|
|
153
|
+
if (spec === "@tabler/icons-react") return { set: "tabler", framework: "react", slug: (n) => kebab(stripPrefix(n, "Icon")), warn: 'Tabler uses the prop "stroke"; rdicons uses "strokeWidth" (fixed for you where it is a plain prop)' };
|
|
154
|
+
if (spec === "@tabler/icons-vue") return { set: "tabler", framework: "vue", slug: (n) => kebab(stripPrefix(n, "Icon")) };
|
|
155
|
+
if (spec === "@phosphor-icons/react") return { set: "ph", framework: "react", slug: (n) => kebab(stripSuffix(stripPrefix(n, "Ph"), "Icon")), warn: 'Phosphor uses the prop "weight" (regular, bold, fill...); rdicons has no such prop, so pick the style by name (house-bold) instead' };
|
|
156
|
+
const hero = spec.match(/^@heroicons\/(react|vue)\/(?:\d\d\/)?(outline|solid)$/);
|
|
157
|
+
if (hero) return { set: "heroicons", framework: hero[1], slug: (n) => kebab(stripSuffix(n, "Icon")), style: hero[2] === "solid" ? "filled" : "outline" };
|
|
158
|
+
const ri = spec.match(/^react-icons\/([a-z0-9]+)$/);
|
|
159
|
+
if (ri && REACT_ICONS[ri[1]]) {
|
|
160
|
+
const pack = ri[1];
|
|
161
|
+
const prefix = pack === "hi2" ? "Hi" : pack[0].toUpperCase() + pack.slice(1);
|
|
162
|
+
return {
|
|
163
|
+
set: REACT_ICONS[pack].set,
|
|
164
|
+
framework: "react",
|
|
165
|
+
slug: (n) => {
|
|
166
|
+
if (!n.startsWith(prefix)) return null;
|
|
167
|
+
let s = kebab(n.slice(prefix.length));
|
|
168
|
+
if (pack === "hi2") s = s.replace(/^(outline|solid|mini)-/, "");
|
|
169
|
+
if (pack === "md") s = s.replace(/-outline$/, "");
|
|
170
|
+
return s;
|
|
171
|
+
},
|
|
172
|
+
style: void 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
var ALIASES = [
|
|
178
|
+
["home", "house"],
|
|
179
|
+
["trash", "trash-2", "bin", "delete"],
|
|
180
|
+
["settings", "cog", "gear", "setting"],
|
|
181
|
+
["search", "magnifier", "magnifying-glass"],
|
|
182
|
+
["mail", "envelope", "email"],
|
|
183
|
+
["user", "person", "account"],
|
|
184
|
+
["edit", "pencil", "pen"],
|
|
185
|
+
["x", "close", "times", "cross"],
|
|
186
|
+
["check", "tick"],
|
|
187
|
+
["plus", "add"],
|
|
188
|
+
["minus", "remove"],
|
|
189
|
+
["bell", "notification"],
|
|
190
|
+
["star", "favorite"],
|
|
191
|
+
["share", "share-2"],
|
|
192
|
+
["lock", "padlock"],
|
|
193
|
+
["calendar", "calendar-days"],
|
|
194
|
+
["camera", "photo-camera"],
|
|
195
|
+
["image", "picture", "photo"],
|
|
196
|
+
["eye", "visibility"],
|
|
197
|
+
["link", "chain"],
|
|
198
|
+
["folder", "directory"],
|
|
199
|
+
["file", "document"],
|
|
200
|
+
["message", "chat", "comment"],
|
|
201
|
+
["phone", "telephone", "call"],
|
|
202
|
+
["heart", "favourite"],
|
|
203
|
+
["arrow-left", "arrow-small-left", "left-arrow"],
|
|
204
|
+
["arrow-right", "arrow-small-right", "right-arrow"],
|
|
205
|
+
["chevron-down", "angle-down", "caret-down"],
|
|
206
|
+
["chevron-up", "angle-up", "caret-up"],
|
|
207
|
+
["chevron-left", "angle-left", "caret-left"],
|
|
208
|
+
["chevron-right", "angle-right", "caret-right"]
|
|
209
|
+
];
|
|
210
|
+
var STYLE_SUFFIX = /-(fill|filled|solid|line|bold|thin|light|duotone|regular|outline|sharp)$/;
|
|
211
|
+
var styleOf = (slug) => /-(fill|filled|solid)$/.test(slug) ? "filled" : "outline";
|
|
212
|
+
function findIcon(slug, target, forceStyle) {
|
|
213
|
+
const style = forceStyle ?? styleOf(slug);
|
|
214
|
+
const base = slug.replace(STYLE_SUFFIX, "");
|
|
215
|
+
const group = ALIASES.find((g) => g.includes(base));
|
|
216
|
+
const bases = [base, ...group ? group.filter((x) => x !== base) : []];
|
|
217
|
+
const variants = style === "filled" ? ["-fill", "-filled", "-solid", "-fill-24"] : ["", "-line", "-regular", "-outline", "-24-regular"];
|
|
218
|
+
const candidates = [slug, ...bases.flatMap((b) => variants.map((v) => b + v)), ...bases];
|
|
219
|
+
for (const c of candidates) {
|
|
220
|
+
const comp = target.get(c);
|
|
221
|
+
if (comp !== void 0) return { name: c, comp };
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".svelte-kit", "coverage", "out", ".turbo"]);
|
|
226
|
+
async function* walk(path, exts) {
|
|
227
|
+
const s = await stat(path);
|
|
228
|
+
if (s.isFile()) {
|
|
229
|
+
yield path;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
for (const e of await readdir(path, { withFileTypes: true })) {
|
|
233
|
+
if (e.isDirectory()) {
|
|
234
|
+
if (!SKIP_DIRS.has(e.name)) yield* walk(join(path, e.name), exts);
|
|
235
|
+
} else if (exts.has(e.name.slice(e.name.lastIndexOf(".") + 1).toLowerCase())) yield join(path, e.name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
var IMPORT = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\3([ \t]*;?)([ \t]*\r?\n)?/g;
|
|
239
|
+
async function migrate(paths, opts, deps) {
|
|
240
|
+
const targetNames = new Map(await deps.names(opts.to));
|
|
241
|
+
const exts = new Set(opts.exts);
|
|
242
|
+
const files = [];
|
|
243
|
+
const frameworks = /* @__PURE__ */ new Set();
|
|
244
|
+
let changed = 0;
|
|
245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
246
|
+
for (const p of paths) {
|
|
247
|
+
for await (const file of walk(resolve(p), exts)) {
|
|
248
|
+
if (seen.has(file)) continue;
|
|
249
|
+
seen.add(file);
|
|
250
|
+
const text = await readFile(file, "utf8");
|
|
251
|
+
if (!/import\s+(type\s+)?\{/.test(text)) continue;
|
|
252
|
+
const report = { file, migrated: [], unmatched: [], warnings: [] };
|
|
253
|
+
const jsxFixes = [];
|
|
254
|
+
const merged = /* @__PURE__ */ new Map();
|
|
255
|
+
const out = text.replace(IMPORT, (whole, typeKw, list, quote, spec, semi, nl) => {
|
|
256
|
+
const eol = nl ?? "";
|
|
257
|
+
const source = sourceFor(spec);
|
|
258
|
+
if (!source) return whole;
|
|
259
|
+
const kept = [];
|
|
260
|
+
const moved = [];
|
|
261
|
+
const hit = [];
|
|
262
|
+
for (const raw of list.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
263
|
+
const isType = /^type\s+/.test(raw);
|
|
264
|
+
const clean = raw.replace(/^type\s+/, "");
|
|
265
|
+
const [exported, local] = clean.split(/\s+as\s+/).map((x) => x.trim());
|
|
266
|
+
const slug = source.slug(exported);
|
|
267
|
+
const found = slug ? findIcon(slug, targetNames, source.style) : null;
|
|
268
|
+
if (!found || isType) {
|
|
269
|
+
kept.push(raw);
|
|
270
|
+
if (!isType) report.unmatched.push(`${exported} (${spec})`);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const localName = local ?? exported;
|
|
274
|
+
moved.push(found.comp === localName ? found.comp : `${found.comp} as ${localName}`);
|
|
275
|
+
hit.push(`${exported} -> ${found.name}`);
|
|
276
|
+
if (spec === "@tabler/icons-react") jsxFixes.push(localName);
|
|
277
|
+
}
|
|
278
|
+
if (!moved.length) return whole;
|
|
279
|
+
frameworks.add(source.framework);
|
|
280
|
+
report.migrated.push(...hit.map((h) => {
|
|
281
|
+
const [from, to] = h.split(" -> ");
|
|
282
|
+
return { from: `${source.set}:${from}`, to: `${opts.to}:${to}` };
|
|
283
|
+
}));
|
|
284
|
+
if (source.warn) report.warnings.push(source.warn);
|
|
285
|
+
const pkg = `@rdicons/${source.framework}/${opts.to}`;
|
|
286
|
+
const q = quote;
|
|
287
|
+
const key = `${typeKw ?? ""}|${pkg}`;
|
|
288
|
+
const keptLine = kept.length ? `import ${typeKw ?? ""}{ ${kept.join(", ")} } from ${q}${spec}${q}${semi}${eol || "\n"}` : "";
|
|
289
|
+
const existing = merged.get(key);
|
|
290
|
+
if (existing) {
|
|
291
|
+
existing.specs.push(...moved);
|
|
292
|
+
return keptLine;
|
|
293
|
+
}
|
|
294
|
+
merged.set(key, { specs: [...moved], line: (specs) => `import ${typeKw ?? ""}{ ${specs.join(", ")} } from ${q}${pkg}${q}${semi}${eol}` });
|
|
295
|
+
return `${keptLine}\0RD:${key}\0`;
|
|
296
|
+
}).replace(/\u0000RD:([^\u0000]*)\u0000/g, (_, key) => {
|
|
297
|
+
const m = merged.get(key);
|
|
298
|
+
return m.line([...new Set(m.specs)]);
|
|
299
|
+
});
|
|
300
|
+
if (out === text) {
|
|
301
|
+
if (report.unmatched.length) files.push(report);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
let final = out;
|
|
305
|
+
for (const name of new Set(jsxFixes)) final = final.replace(new RegExp(`(<${name}\\b[^>]*?)\\sstroke=`, "g"), "$1 strokeWidth=");
|
|
306
|
+
changed++;
|
|
307
|
+
files.push(report);
|
|
308
|
+
if (opts.write) await writeFile(file, final, "utf8");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
files,
|
|
313
|
+
changed,
|
|
314
|
+
written: opts.write,
|
|
315
|
+
installHint: [...frameworks].map((f) => `npm install @rdicons/${f}`)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function formatReport(r, root) {
|
|
319
|
+
const lines = [];
|
|
320
|
+
let moved = 0;
|
|
321
|
+
const left = [];
|
|
322
|
+
for (const f of r.files) {
|
|
323
|
+
moved += f.migrated.length;
|
|
324
|
+
if (f.migrated.length) lines.push(`${r.written ? "\u2713" : "\xB7"} ${relative(root, f.file) || f.file}: ${f.migrated.length} icon${f.migrated.length === 1 ? "" : "s"}`);
|
|
325
|
+
for (const u of f.unmatched) left.push(` ${relative(root, f.file) || f.file}: ${u}`);
|
|
326
|
+
}
|
|
327
|
+
lines.push("");
|
|
328
|
+
lines.push(`${moved} icon${moved === 1 ? "" : "s"} in ${r.changed} file${r.changed === 1 ? "" : "s"} ${r.written ? "rewritten" : "can be rewritten"}.`);
|
|
329
|
+
if (left.length) lines.push("", `No match in the target library (left on the old import, pick a replacement by hand):`, ...left);
|
|
330
|
+
const warnings = [...new Set(r.files.flatMap((f) => f.warnings))];
|
|
331
|
+
if (warnings.length) lines.push("", "Check:", ...warnings.map((w) => " - " + w));
|
|
332
|
+
if (r.installHint.length && r.changed) lines.push("", `Then install the new package: ${r.installHint.join(" and ")}`);
|
|
333
|
+
if (!r.written && r.changed) lines.push("", "This was a preview. Nothing was changed: run the same command with --write to apply it (commit your work first).");
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/audit.ts
|
|
338
|
+
function rdiconsSet(spec) {
|
|
339
|
+
let m = spec.match(/^@rdicons\/(?:react|vue|svelte)\/([a-z0-9-]+)$/);
|
|
340
|
+
if (m) return m[1];
|
|
341
|
+
m = spec.match(/^@rdicons\/(?:react|vue|svelte)-([a-z0-9-]+)$/);
|
|
342
|
+
if (m) return m[1];
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
var STYLE_SUFFIX2 = /-(fill|filled|solid|line|bold|thin|light|duotone|regular|outline|sharp)$/;
|
|
346
|
+
function conceptOf(slug) {
|
|
347
|
+
const base = slug.replace(STYLE_SUFFIX2, "");
|
|
348
|
+
const group = ALIASES.find((g) => g.includes(base));
|
|
349
|
+
return group ? group[0] : base;
|
|
350
|
+
}
|
|
351
|
+
async function audit(paths, opts, deps) {
|
|
352
|
+
const exts = new Set(opts.exts);
|
|
353
|
+
const setInfo = new Map((await deps.sets()).map((s) => [s.prefix, s]));
|
|
354
|
+
const use = /* @__PURE__ */ new Map();
|
|
355
|
+
const seen = /* @__PURE__ */ new Set();
|
|
356
|
+
let files = 0, filesWithIcons = 0;
|
|
357
|
+
const note = (set, icon, file, via) => {
|
|
358
|
+
const u = use.get(set) ?? { icons: /* @__PURE__ */ new Set(), files: /* @__PURE__ */ new Set(), via };
|
|
359
|
+
u.icons.add(icon);
|
|
360
|
+
u.files.add(file);
|
|
361
|
+
u.via ||= via;
|
|
362
|
+
use.set(set, u);
|
|
363
|
+
};
|
|
364
|
+
for (const p of paths) {
|
|
365
|
+
for await (const file of walk(resolve2(p), exts)) {
|
|
366
|
+
if (seen.has(file)) continue;
|
|
367
|
+
seen.add(file);
|
|
368
|
+
files++;
|
|
369
|
+
const text = await readFile2(file, "utf8");
|
|
370
|
+
let any = false;
|
|
371
|
+
for (const m of text.matchAll(new RegExp(IMPORT.source, "g"))) {
|
|
372
|
+
const isType = !!m[1];
|
|
373
|
+
const spec = m[4];
|
|
374
|
+
if (isType) continue;
|
|
375
|
+
const names = m[2].split(",").map((x) => x.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
376
|
+
const rd = rdiconsSet(spec);
|
|
377
|
+
if (rd) {
|
|
378
|
+
for (const n of names) {
|
|
379
|
+
if (n === "Icon" || /^[a-z]/.test(n)) continue;
|
|
380
|
+
note(rd, kebab(n), file, true);
|
|
381
|
+
any = true;
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const source = sourceFor(spec);
|
|
386
|
+
if (!source) continue;
|
|
387
|
+
for (const n of names) {
|
|
388
|
+
const slug = source.slug(n);
|
|
389
|
+
if (!slug) continue;
|
|
390
|
+
note(source.set, slug, file, false);
|
|
391
|
+
any = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (any) filesWithIcons++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const libraries = [...use.entries()].map(([set, u]) => ({ set, label: setInfo.get(set)?.label ?? set, license: setInfo.get(set)?.license ?? "see the library", viaRdicons: u.via, icons: [...u.icons].sort(), files: u.files.size })).sort((a, b) => b.icons.length - a.icons.length);
|
|
398
|
+
const totalIcons = libraries.reduce((n, l) => n + l.icons.length, 0);
|
|
399
|
+
const byConcept = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const l of libraries) for (const i of l.icons) byConcept.set(conceptOf(i), [...byConcept.get(conceptOf(i)) ?? [], `${l.set}:${i}`]);
|
|
401
|
+
const duplicates = [...byConcept.entries()].filter(([, list]) => new Set(list.map((x) => x.split(":")[0])).size > 1).sort((a, b) => a[0].localeCompare(b[0]));
|
|
402
|
+
const inRd = { total: 0, animated: 0, examples: [] };
|
|
403
|
+
const others = libraries.filter((l) => l.set !== "rd");
|
|
404
|
+
if (others.length) {
|
|
405
|
+
const rdNames = new Map(await deps.names("rd"));
|
|
406
|
+
let animated = null;
|
|
407
|
+
for (const l of others) {
|
|
408
|
+
for (const i of l.icons) {
|
|
409
|
+
const hit = findIcon(i, rdNames);
|
|
410
|
+
if (!hit) continue;
|
|
411
|
+
inRd.total++;
|
|
412
|
+
animated ??= await deps.animated().catch(() => /* @__PURE__ */ new Set());
|
|
413
|
+
if (animated.has(hit.name)) inRd.animated++;
|
|
414
|
+
if (inRd.examples.length < 8) inRd.examples.push(`${l.set}:${i} -> rd:${hit.name}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const needsCredit = libraries.filter((l) => /CC[ -]BY/i.test(l.license));
|
|
419
|
+
const findings = [];
|
|
420
|
+
if (libraries.length > 1) findings.push(`Icons come from ${libraries.length} libraries (${libraries.map((l) => l.set).join(", ")}). Mixed styles are the usual reason icons look uneven; the pack builder and consistency checker on the site can line them up.`);
|
|
421
|
+
if (duplicates.length) findings.push(`${duplicates.length} icon${duplicates.length === 1 ? " is" : "s are"} used from more than one library (${duplicates.slice(0, 3).map(([c]) => c).join(", ")}${duplicates.length > 3 ? ", ..." : ""}). Pick one library for each.`);
|
|
422
|
+
for (const l of needsCredit) findings.push(`${l.label} is ${l.license}: you must credit the author wherever you ship these icons.`);
|
|
423
|
+
const fromOtherPackages = libraries.filter((l) => !l.viaRdicons);
|
|
424
|
+
if (fromOtherPackages.length) findings.push(`${fromOtherPackages.reduce((n, l) => n + l.icons.length, 0)} icons still come from other icon packages (${fromOtherPackages.map((l) => l.set).join(", ")}). \`rdicons migrate\` can move them to one rdicons library.`);
|
|
425
|
+
return { files, filesWithIcons, libraries, totalIcons, duplicates, inRd, needsCredit, findings };
|
|
426
|
+
}
|
|
427
|
+
function formatAudit(r) {
|
|
428
|
+
const out = [];
|
|
429
|
+
if (!r.totalIcons) return `Looked at ${r.files} file${r.files === 1 ? "" : "s"} and found no icon imports from rdicons, lucide-react, Tabler, Phosphor, Heroicons or react-icons.`;
|
|
430
|
+
out.push(`${r.totalIcons} distinct icon${r.totalIcons === 1 ? "" : "s"} in ${r.filesWithIcons} of ${r.files} files`, "");
|
|
431
|
+
const pad = Math.max(...r.libraries.map((l) => l.set.length)) + 1;
|
|
432
|
+
for (const l of r.libraries) out.push(` ${l.set.padEnd(pad)} ${String(l.icons.length).padStart(4)} icon${l.icons.length === 1 ? " " : "s"} ${l.license}${l.viaRdicons ? "" : " (not through rdicons)"}`);
|
|
433
|
+
if (r.duplicates.length) {
|
|
434
|
+
out.push("", "Same icon from more than one library:");
|
|
435
|
+
for (const [c, list] of r.duplicates.slice(0, 12)) out.push(` ${c}: ${list.join(", ")}`);
|
|
436
|
+
if (r.duplicates.length > 12) out.push(` ... and ${r.duplicates.length - 12} more`);
|
|
437
|
+
}
|
|
438
|
+
if (r.inRd.total) {
|
|
439
|
+
out.push("", `${r.inRd.total} of the icons from other libraries also exist in rd (${r.inRd.animated} animated):`);
|
|
440
|
+
for (const e of r.inRd.examples) out.push(" " + e);
|
|
441
|
+
}
|
|
442
|
+
if (r.findings.length) out.push("", "Worth a look:", ...r.findings.map((f) => " - " + f));
|
|
443
|
+
else out.push("", "Nothing to fix: one library, no duplicates, no attribution needed.");
|
|
444
|
+
return out.join("\n");
|
|
445
|
+
}
|
|
446
|
+
|
|
127
447
|
// src/cli.ts
|
|
128
|
-
var VERSION = "0.
|
|
448
|
+
var VERSION = "0.5.0";
|
|
129
449
|
var BASE = (process.env.RDICONS_URL || "https://rdicons.vimalbhatt.com").replace(/\/$/, "");
|
|
130
450
|
var HELP = `rdicons ${VERSION}: add open-source icons to your project
|
|
131
451
|
|
|
@@ -133,6 +453,8 @@ Usage
|
|
|
133
453
|
rdicons add <set:name> [more...] [options] download icons into a folder
|
|
134
454
|
rdicons search <query> [--set lucide] find icons by name
|
|
135
455
|
rdicons info <set:name> show the code to use one icon (every install option)
|
|
456
|
+
rdicons migrate <path...> --to <library> switch a project's icon imports to another library (preview first)
|
|
457
|
+
rdicons audit <path...> report which icon libraries a project uses, duplicates and licenses
|
|
136
458
|
rdicons sets list the 19 icon libraries
|
|
137
459
|
|
|
138
460
|
Options for "add"
|
|
@@ -146,7 +468,21 @@ Options for "add"
|
|
|
146
468
|
--animated [mode] add the icon's animation (rd icons): hover (default), parent, play or loop
|
|
147
469
|
--speed <number> animation speed, 2 is twice as fast (default 1; needs --animated)
|
|
148
470
|
|
|
471
|
+
Options for "migrate"
|
|
472
|
+
--to <library> the library to move to, e.g. lucide, tabler, ph, rd (see "rdicons sets")
|
|
473
|
+
--write apply the changes (without it you only get a preview)
|
|
474
|
+
--ext js,ts,tsx,... which files to look at (default js,jsx,ts,tsx,mjs,cjs,vue)
|
|
475
|
+
|
|
476
|
+
Options for "audit"
|
|
477
|
+
--json print the result as JSON
|
|
478
|
+
--strict exit with an error when icons come from more than one library, an icon is used from two
|
|
479
|
+
libraries, or a library needs credit (for CI)
|
|
480
|
+
--ext js,ts,tsx,... which files to look at (default js,jsx,ts,tsx,mjs,cjs,vue,svelte)
|
|
481
|
+
|
|
149
482
|
Examples
|
|
483
|
+
npx rdicons audit src which libraries does this project use?
|
|
484
|
+
npx rdicons migrate src --to tabler preview: lucide-react imports -> @rdicons/react/tabler
|
|
485
|
+
npx rdicons migrate src --to rd --write apply it
|
|
150
486
|
npx rdicons add lucide:house
|
|
151
487
|
npx rdicons add tabler:bell ph:heart --format tsx --out src/icons --color "#4f46e5"
|
|
152
488
|
npx rdicons add rd:bell --animated loop --speed 1.5 --format tsx
|
|
@@ -201,6 +537,7 @@ function parseArgs(argv) {
|
|
|
201
537
|
if (a.startsWith("--")) {
|
|
202
538
|
const [k, inline] = a.slice(2).split("=");
|
|
203
539
|
if (inline !== void 0) flags[k] = inline;
|
|
540
|
+
else if (k === "write" || k === "strict" || k === "json") flags[k] = "true";
|
|
204
541
|
else if (k === "animated") flags[k] = MODES.includes(argv[i + 1]) ? argv[++i] : "true";
|
|
205
542
|
else if (argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) flags[k] = argv[++i];
|
|
206
543
|
else flags[k] = "true";
|
|
@@ -262,7 +599,7 @@ async function cmdAdd(positional, flags) {
|
|
|
262
599
|
if (!positional.length) fail("Which icon? For example: rdicons add lucide:house");
|
|
263
600
|
const format = flags.format ?? "svg";
|
|
264
601
|
if (!["svg", "jsx", "tsx"].includes(format)) fail("--format must be svg, jsx or tsx");
|
|
265
|
-
const out =
|
|
602
|
+
const out = resolve3(flags.out ?? "icons");
|
|
266
603
|
const mode = flags.animated === void 0 ? null : flags.animated === "true" ? "hover" : flags.animated;
|
|
267
604
|
if (mode && !MODES.includes(mode)) fail(`--animated must be one of ${MODES.join(", ")} (or just --animated for hover)`);
|
|
268
605
|
const speed = flags.speed === void 0 ? 1 : Number(flags.speed);
|
|
@@ -289,11 +626,32 @@ async function cmdAdd(positional, flags) {
|
|
|
289
626
|
const file = format === "svg" ? `${it.s}-${it.n}.svg` : `${comp}.${format}`;
|
|
290
627
|
const jsx = format === "svg" ? "" : svgToJsx(svg, comp);
|
|
291
628
|
const text = format === "svg" ? svg + "\n" : (anim ? jsxifyAnimated(jsx, format === "tsx") : jsx) + "\n";
|
|
292
|
-
await
|
|
293
|
-
console.log(`\u2713 ${it.s}:${it.n} -> ${
|
|
629
|
+
await writeFile2(join2(out, file), text, "utf8");
|
|
630
|
+
console.log(`\u2713 ${it.s}:${it.n} -> ${join2(flags.out ?? "icons", file)}`);
|
|
294
631
|
}
|
|
295
632
|
console.log('\nTip: run "rdicons sets" to see each library license (some need attribution).');
|
|
296
633
|
}
|
|
634
|
+
async function cmdMigrate(positional, flags) {
|
|
635
|
+
if (!positional.length) fail("Which folder or file? For example: rdicons migrate src --to tabler");
|
|
636
|
+
const to = flags.to;
|
|
637
|
+
if (!to || to === "true") fail('Which library do you want to move to? For example: --to tabler (run "rdicons sets" for the list)');
|
|
638
|
+
const sets = await listSets();
|
|
639
|
+
if (!sets.some((s) => s.prefix === to)) fail(`Unknown library "${to}". Choose one of: ${sets.map((s) => s.prefix).join(", ")}`);
|
|
640
|
+
const exts = (flags.ext && flags.ext !== "true" ? flags.ext : "js,jsx,ts,tsx,mjs,cjs,vue").split(",").map((x) => x.trim().replace(/^\./, "").toLowerCase());
|
|
641
|
+
const result = await migrate(positional, { to, write: flags.write === "true", exts }, { names: (set) => getJson(`/api/names?set=${encodeURIComponent(set)}`) });
|
|
642
|
+
console.log(formatReport(result, process.cwd()));
|
|
643
|
+
}
|
|
644
|
+
async function cmdAudit(positional, flags) {
|
|
645
|
+
if (!positional.length) fail("Which folder or file? For example: rdicons audit src");
|
|
646
|
+
const exts = (flags.ext && flags.ext !== "true" ? flags.ext : "js,jsx,ts,tsx,mjs,cjs,vue,svelte").split(",").map((x) => x.trim().replace(/^\./, "").toLowerCase());
|
|
647
|
+
const result = await audit(positional, { exts }, {
|
|
648
|
+
sets: async () => (await listSets()).map((s) => ({ prefix: s.prefix, label: s.label, license: s.license })),
|
|
649
|
+
names: (set) => getJson(`/api/names?set=${encodeURIComponent(set)}`),
|
|
650
|
+
animated: async () => new Set(Object.keys(await getJson("/api/animations?set=rd")))
|
|
651
|
+
});
|
|
652
|
+
console.log(flags.json === "true" ? JSON.stringify(result, null, 2) : formatAudit(result));
|
|
653
|
+
if (flags.strict === "true" && (result.libraries.length > 1 || result.duplicates.length > 0 || result.needsCredit.length > 0)) process.exitCode = 1;
|
|
654
|
+
}
|
|
297
655
|
async function main() {
|
|
298
656
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
299
657
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -303,6 +661,8 @@ async function main() {
|
|
|
303
661
|
if (cmd === "search") return cmdSearch(positional, flags);
|
|
304
662
|
if (cmd === "info") return cmdInfo(positional);
|
|
305
663
|
if (cmd === "add") return cmdAdd(positional, flags);
|
|
664
|
+
if (cmd === "migrate") return cmdMigrate(positional, flags);
|
|
665
|
+
if (cmd === "audit") return cmdAudit(positional, flags);
|
|
306
666
|
fail(`Unknown command "${cmd}". Run rdicons --help`);
|
|
307
667
|
}
|
|
308
668
|
main().catch((e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rdicons",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "rdicons command line: search 78,000+ open-source icons and add them to your project as SVG, JSX or TSX. Try: npx rdicons add lucide:house",
|
|
5
5
|
"keywords": ["icons", "cli", "svg", "react", "tsx", "icon-generator", "lucide", "tabler", "rdicons"],
|
|
6
6
|
"license": "MIT",
|