rdicons 0.4.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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +138 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,6 +19,7 @@ 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
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
22
23
  rdicons sets list the 19 libraries with their licenses
23
24
  ```
24
25
 
@@ -44,6 +45,16 @@ npx rdicons migrate src --to rd --write # apply it (commit your work first)
44
45
  ```
45
46
 
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.
47
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 |
48
59
  | `--speed <n>` | `1` | animation speed, `2` is twice as fast (needs `--animated`) |
49
60
 
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { mkdir, writeFile as writeFile2 } from "fs/promises";
5
- import { join as join2, resolve as resolve2 } from "path";
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,6 +124,10 @@ 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
+
127
131
  // src/migrate.ts
128
132
  import { readdir, readFile, stat, writeFile } from "fs/promises";
129
133
  import { join, relative, resolve } from "path";
@@ -330,8 +334,118 @@ function formatReport(r, root) {
330
334
  return lines.join("\n");
331
335
  }
332
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
+
333
447
  // src/cli.ts
334
- var VERSION = "0.4.0";
448
+ var VERSION = "0.5.0";
335
449
  var BASE = (process.env.RDICONS_URL || "https://rdicons.vimalbhatt.com").replace(/\/$/, "");
336
450
  var HELP = `rdicons ${VERSION}: add open-source icons to your project
337
451
 
@@ -340,6 +454,7 @@ Usage
340
454
  rdicons search <query> [--set lucide] find icons by name
341
455
  rdicons info <set:name> show the code to use one icon (every install option)
342
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
343
458
  rdicons sets list the 19 icon libraries
344
459
 
345
460
  Options for "add"
@@ -358,7 +473,14 @@ Options for "migrate"
358
473
  --write apply the changes (without it you only get a preview)
359
474
  --ext js,ts,tsx,... which files to look at (default js,jsx,ts,tsx,mjs,cjs,vue)
360
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
+
361
482
  Examples
483
+ npx rdicons audit src which libraries does this project use?
362
484
  npx rdicons migrate src --to tabler preview: lucide-react imports -> @rdicons/react/tabler
363
485
  npx rdicons migrate src --to rd --write apply it
364
486
  npx rdicons add lucide:house
@@ -415,7 +537,7 @@ function parseArgs(argv) {
415
537
  if (a.startsWith("--")) {
416
538
  const [k, inline] = a.slice(2).split("=");
417
539
  if (inline !== void 0) flags[k] = inline;
418
- else if (k === "write") flags[k] = "true";
540
+ else if (k === "write" || k === "strict" || k === "json") flags[k] = "true";
419
541
  else if (k === "animated") flags[k] = MODES.includes(argv[i + 1]) ? argv[++i] : "true";
420
542
  else if (argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) flags[k] = argv[++i];
421
543
  else flags[k] = "true";
@@ -477,7 +599,7 @@ async function cmdAdd(positional, flags) {
477
599
  if (!positional.length) fail("Which icon? For example: rdicons add lucide:house");
478
600
  const format = flags.format ?? "svg";
479
601
  if (!["svg", "jsx", "tsx"].includes(format)) fail("--format must be svg, jsx or tsx");
480
- const out = resolve2(flags.out ?? "icons");
602
+ const out = resolve3(flags.out ?? "icons");
481
603
  const mode = flags.animated === void 0 ? null : flags.animated === "true" ? "hover" : flags.animated;
482
604
  if (mode && !MODES.includes(mode)) fail(`--animated must be one of ${MODES.join(", ")} (or just --animated for hover)`);
483
605
  const speed = flags.speed === void 0 ? 1 : Number(flags.speed);
@@ -519,6 +641,17 @@ async function cmdMigrate(positional, flags) {
519
641
  const result = await migrate(positional, { to, write: flags.write === "true", exts }, { names: (set) => getJson(`/api/names?set=${encodeURIComponent(set)}`) });
520
642
  console.log(formatReport(result, process.cwd()));
521
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
+ }
522
655
  async function main() {
523
656
  const [cmd, ...rest] = process.argv.slice(2);
524
657
  const { flags, positional } = parseArgs(rest);
@@ -529,6 +662,7 @@ async function main() {
529
662
  if (cmd === "info") return cmdInfo(positional);
530
663
  if (cmd === "add") return cmdAdd(positional, flags);
531
664
  if (cmd === "migrate") return cmdMigrate(positional, flags);
665
+ if (cmd === "audit") return cmdAudit(positional, flags);
532
666
  fail(`Unknown command "${cmd}". Run rdicons --help`);
533
667
  }
534
668
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdicons",
3
- "version": "0.4.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",